diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
index 2d69334c47..a88dff0f22 100644
--- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
+++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
@@ -1,263 +1,272 @@
newIssue('config.unknown.'.$key)
->setShortName($short)
->setName($name)
->setSummary($summary);
$stack = PhabricatorEnv::getConfigSourceStack();
$stack = $stack->getStack();
$found = array();
$found_local = false;
$found_database = false;
foreach ($stack as $source_key => $source) {
$value = $source->getKeys(array($key));
if ($value) {
$found[] = $source->getName();
if ($source instanceof PhabricatorConfigDatabaseSource) {
$found_database = true;
}
if ($source instanceof PhabricatorConfigLocalSource) {
$found_local = true;
}
}
}
$message = $message."\n\n".pht(
'This configuration value is defined in these %d '.
'configuration source(s): %s.',
count($found),
implode(', ', $found));
$issue->setMessage($message);
if ($found_local) {
$command = csprintf('phabricator/ $ ./bin/config delete %s', $key);
$issue->addCommand($command);
}
if ($found_database) {
$issue->addPhabricatorConfig($key);
}
}
}
/**
* Return a map of deleted config options. Keys are option keys; values are
* explanations of what happened to the option.
*/
public static function getAncientConfig() {
$reason_auth = pht(
'This option has been migrated to the "Auth" application. Your old '.
'configuration is still in effect, but now stored in "Auth" instead of '.
'configuration. Going forward, you can manage authentication from '.
'the web UI.');
$auth_config = array(
'controller.oauth-registration',
'auth.password-auth-enabled',
'facebook.auth-enabled',
'facebook.registration-enabled',
'facebook.auth-permanent',
'facebook.application-id',
'facebook.application-secret',
'facebook.require-https-auth',
'github.auth-enabled',
'github.registration-enabled',
'github.auth-permanent',
'github.application-id',
'github.application-secret',
'google.auth-enabled',
'google.registration-enabled',
'google.auth-permanent',
'google.application-id',
'google.application-secret',
'ldap.auth-enabled',
'ldap.hostname',
'ldap.port',
'ldap.base_dn',
'ldap.search_attribute',
'ldap.search-first',
'ldap.username-attribute',
'ldap.real_name_attributes',
'ldap.activedirectory_domain',
'ldap.version',
'ldap.referrals',
'ldap.anonymous-user-name',
'ldap.anonymous-user-password',
'ldap.start-tls',
'disqus.auth-enabled',
'disqus.registration-enabled',
'disqus.auth-permanent',
'disqus.application-id',
'disqus.application-secret',
'phabricator.oauth-uri',
'phabricator.auth-enabled',
'phabricator.registration-enabled',
'phabricator.auth-permanent',
'phabricator.application-id',
'phabricator.application-secret',
);
$ancient_config = array_fill_keys($auth_config, $reason_auth);
$markup_reason = pht(
'Custom remarkup rules are now added by subclassing '.
'PhabricatorRemarkupCustomInlineRule or '.
'PhabricatorRemarkupCustomBlockRule.');
$session_reason = pht(
'Sessions now expire and are garbage collected rather than having an '.
'arbitrary concurrency limit.');
$differential_field_reason = pht(
'All Differential fields are now managed through the configuration '.
'option "%s". Use that option to configure which fields are shown.',
'differential.fields');
$reply_domain_reason = pht(
'Individual application reply handler domains have been removed. '.
'Configure a reply domain with "%s".',
'metamta.reply-handler-domain');
$reply_handler_reason = pht(
'Reply handlers can no longer be overridden with configuration.');
$monospace_reason = pht(
'Phabricator no longer supports global customization of monospaced '.
'fonts.');
+ $public_mail_reason = pht(
+ 'Inbound mail addresses are now configured for each application '.
+ 'in the Applications tool.');
+
$ancient_config += array(
'phid.external-loaders' =>
pht(
'External loaders have been replaced. Extend `PhabricatorPHIDType` '.
'to implement new PHID and handle types.'),
'maniphest.custom-task-extensions-class' =>
pht(
'Maniphest fields are now loaded automatically. You can configure '.
'them with `maniphest.fields`.'),
'maniphest.custom-fields' =>
pht(
'Maniphest fields are now defined in '.
'`maniphest.custom-field-definitions`. Existing definitions have '.
'been migrated.'),
'differential.custom-remarkup-rules' => $markup_reason,
'differential.custom-remarkup-block-rules' => $markup_reason,
'auth.sshkeys.enabled' => pht(
'SSH keys are now actually useful, so they are always enabled.'),
'differential.anonymous-access' => pht(
'Phabricator now has meaningful global access controls. See '.
'`policy.allow-public`.'),
'celerity.resource-path' => pht(
'An alternate resource map is no longer supported. Instead, use '.
'multiple maps. See T4222.'),
'metamta.send-immediately' => pht(
'Mail is now always delivered by the daemons.'),
'auth.sessions.conduit' => $session_reason,
'auth.sessions.web' => $session_reason,
'tokenizer.ondemand' => pht(
'Phabricator now manages typeahead strategies automatically.'),
'differential.revision-custom-detail-renderer' => pht(
'Obsolete; use standard rendering events instead.'),
'differential.show-host-field' => $differential_field_reason,
'differential.show-test-plan-field' => $differential_field_reason,
'differential.field-selector' => $differential_field_reason,
'phabricator.show-beta-applications' => pht(
'This option has been renamed to `phabricator.show-prototypes` '.
'to emphasize the unfinished nature of many prototype applications. '.
'Your existing setting has been migrated.'),
'notification.user' => pht(
'The notification server no longer requires root permissions. Start '.
'the server as the user you want it to run under.'),
'notification.debug' => pht(
'Notifications no longer have a dedicated debugging mode.'),
'translation.provider' => pht(
'The translation implementation has changed and providers are no '.
'longer used or supported.'),
'config.mask' => pht(
'Use `config.hide` instead of this option.'),
'phd.start-taskmasters' => pht(
'Taskmasters now use an autoscaling pool. You can configure the '.
'pool size with `phd.taskmasters`.'),
'storage.engine-selector' => pht(
'Phabricator now automatically discovers available storage engines '.
'at runtime.'),
'storage.upload-size-limit' => pht(
'Phabricator now supports arbitrarily large files. Consult the '.
'documentation for configuration details.'),
'security.allow-outbound-http' => pht(
'This option has been replaced with the more granular option '.
'`security.outbound-blacklist`.'),
'metamta.reply.show-hints' => pht(
'Phabricator no longer shows reply hints in mail.'),
'metamta.differential.reply-handler-domain' => $reply_domain_reason,
'metamta.diffusion.reply-handler-domain' => $reply_domain_reason,
'metamta.macro.reply-handler-domain' => $reply_domain_reason,
'metamta.maniphest.reply-handler-domain' => $reply_domain_reason,
'metamta.pholio.reply-handler-domain' => $reply_domain_reason,
'metamta.diffusion.reply-handler' => $reply_handler_reason,
'metamta.differential.reply-handler' => $reply_handler_reason,
'metamta.maniphest.reply-handler' => $reply_handler_reason,
'metamta.package.reply-handler' => $reply_handler_reason,
'metamta.precedence-bulk' => pht(
'Phabricator now always sends transaction mail with '.
'"Precedence: bulk" to improve deliverability.'),
'style.monospace' => $monospace_reason,
'style.monospace.windows' => $monospace_reason,
'search.engine-selector' => pht(
'Phabricator now automatically discovers available search engines '.
'at runtime.'),
+
+ 'metamta.files.public-create-email' => $public_mail_reason,
+ 'metamta.maniphest.public-create-email' => $public_mail_reason,
+ 'metamta.maniphest.default-public-author' => $public_mail_reason,
+ 'metamta.paste.public-create-email' => $public_mail_reason,
);
return $ancient_config;
}
}
diff --git a/src/applications/files/config/PhabricatorFilesConfigOptions.php b/src/applications/files/config/PhabricatorFilesConfigOptions.php
index 7f18da3e99..ac62d410fe 100644
--- a/src/applications/files/config/PhabricatorFilesConfigOptions.php
+++ b/src/applications/files/config/PhabricatorFilesConfigOptions.php
@@ -1,194 +1,180 @@
'image/jpeg',
'image/jpg' => 'image/jpg',
'image/png' => 'image/png',
'image/gif' => 'image/gif',
'text/plain' => 'text/plain; charset=utf-8',
'text/x-diff' => 'text/plain; charset=utf-8',
// ".ico" favicon files, which have mime type diversity. See:
// http://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type
'image/x-ico' => 'image/x-icon',
'image/x-icon' => 'image/x-icon',
'image/vnd.microsoft.icon' => 'image/x-icon',
'audio/x-wav' => 'audio/x-wav',
'application/ogg' => 'application/ogg',
'audio/mpeg' => 'audio/mpeg',
);
$image_default = 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,
);
$audio_default = array(
'audio/x-wav' => true,
'application/ogg' => true,
'audio/mpeg' => true,
);
// largely lifted from http://en.wikipedia.org/wiki/Internet_media_type
$icon_default = array(
// audio file icon
'audio/basic' => 'fa-file-audio-o',
'audio/L24' => 'fa-file-audio-o',
'audio/mp4' => 'fa-file-audio-o',
'audio/mpeg' => 'fa-file-audio-o',
'audio/ogg' => 'fa-file-audio-o',
'audio/vorbis' => 'fa-file-audio-o',
'audio/vnd.rn-realaudio' => 'fa-file-audio-o',
'audio/vnd.wave' => 'fa-file-audio-o',
'audio/webm' => 'fa-file-audio-o',
// movie file icon
'video/mpeg' => 'fa-file-movie-o',
'video/mp4' => 'fa-file-movie-o',
'video/ogg' => 'fa-file-movie-o',
'video/quicktime' => 'fa-file-movie-o',
'video/webm' => 'fa-file-movie-o',
'video/x-matroska' => 'fa-file-movie-o',
'video/x-ms-wmv' => 'fa-file-movie-o',
'video/x-flv' => 'fa-file-movie-o',
// pdf file icon
'application/pdf' => 'fa-file-pdf-o',
// zip file icon
'application/zip' => 'fa-file-zip-o',
// msword icon
'application/msword' => 'fa-file-word-o',
// msexcel
'application/vnd.ms-excel' => 'fa-file-excel-o',
// mspowerpoint
'application/vnd.ms-powerpoint' => 'fa-file-powerpoint-o',
) + array_fill_keys(array_keys($image_default), 'fa-file-image-o');
// NOTE: These options are locked primarily because adding "text/plain"
// as an image MIME type increases SSRF vulnerability by allowing users
// to load text files from remote servers as "images" (see T6755 for
// discussion).
return array(
$this->newOption('files.viewable-mime-types', 'wild', $viewable_default)
->setLocked(true)
->setSummary(
pht('Configure which MIME types are viewable in the browser.'))
->setDescription(
pht(
'Configure which uploaded file types may be viewed directly '.
'in the browser. Other file types will be downloaded instead '.
'of displayed. This is mainly a usability consideration, since '.
'browsers tend to freak out when viewing enormous binary files.'.
"\n\n".
'The keys in this map are vieweable MIME types; the values are '.
'the MIME types they are delivered as when they are viewed in '.
'the browser.')),
$this->newOption('files.image-mime-types', 'set', $image_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types are images.'))
->setDescription(
pht(
'List of MIME types which can be used as the `src` for an '.
'`` tag.')),
$this->newOption('files.audio-mime-types', 'set', $audio_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types are audio.'))
->setDescription(
pht(
'List of MIME types which can be used to render an '.
'`` tag.')),
$this->newOption('files.icon-mime-types', 'wild', $icon_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types map to which icons.'))
->setDescription(
pht(
'Map of MIME type to icon name. MIME types which can not be '.
'found default to icon `doc_files`.')),
$this->newOption('storage.mysql-engine.max-size', 'int', 1000000)
->setSummary(
pht(
'Configure the largest file which will be put into the MySQL '.
'storage engine.')),
$this->newOption('storage.local-disk.path', 'string', null)
->setLocked(true)
->setSummary(pht('Local storage disk path.'))
->setDescription(
pht(
"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.")),
$this->newOption('storage.s3.bucket', 'string', null)
->setSummary(pht('Amazon S3 bucket.'))
->setDescription(
pht(
"Set this to a valid Amazon S3 bucket to store files there. You ".
"must also configure S3 access keys in the 'Amazon Web Services' ".
"group.")),
- $this->newOption(
- 'metamta.files.public-create-email',
- 'string',
- null)
- ->setLocked(true)
- ->setLockedMessage(pht(
- 'This configuration is deprecated. See description for details.'))
- ->setSummary(pht('DEPRECATED - Allow uploaded files via email.'))
- ->setDescription(
- pht(
- 'This config has been deprecated in favor of [[ '.
- '/applications/view/PhabricatorFilesApplication/ | '.
- 'application settings ]], which allow for multiple email '.
- 'addresses and other functionality.')),
$this->newOption(
'metamta.files.subject-prefix',
'string',
'[File]')
->setDescription(pht('Subject prefix for Files email.')),
$this->newOption('files.enable-imagemagick', 'bool', false)
->setBoolOptions(
array(
pht('Enable'),
pht('Disable'),
))
->setDescription(
pht(
'This option will use Imagemagick to rescale images, so animated '.
'GIFs can be thumbnailed and set as profile pictures. Imagemagick '.
'must be installed and the "convert" binary must be available to '.
'the webserver for this to work.')),
);
}
}
diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
index c2b87629f0..04d59c9966 100644
--- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
+++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
@@ -1,374 +1,323 @@
array(
'name' => pht('Unbreak Now!'),
'short' => pht('Unbreak!'),
'color' => 'indigo',
'keywords' => array('unbreak'),
),
90 => array(
'name' => pht('Needs Triage'),
'short' => pht('Triage'),
'color' => 'violet',
'keywords' => array('triage'),
),
80 => array(
'name' => pht('High'),
'short' => pht('High'),
'color' => 'red',
'keywords' => array('high'),
),
50 => array(
'name' => pht('Normal'),
'short' => pht('Normal'),
'color' => 'orange',
'keywords' => array('normal'),
),
25 => array(
'name' => pht('Low'),
'short' => pht('Low'),
'color' => 'yellow',
'keywords' => array('low'),
),
0 => array(
'name' => pht('Wishlist'),
'short' => pht('Wish'),
'color' => 'sky',
'keywords' => array('wish', 'wishlist'),
),
);
$status_type = 'custom:ManiphestStatusConfigOptionType';
$status_defaults = array(
'open' => array(
'name' => pht('Open'),
'special' => ManiphestTaskStatus::SPECIAL_DEFAULT,
'prefixes' => array(
'open',
'opens',
'reopen',
'reopens',
),
),
'resolved' => array(
'name' => pht('Resolved'),
'name.full' => pht('Closed, Resolved'),
'closed' => true,
'special' => ManiphestTaskStatus::SPECIAL_CLOSED,
'prefixes' => array(
'closed',
'closes',
'close',
'fix',
'fixes',
'fixed',
'resolve',
'resolves',
'resolved',
),
'suffixes' => array(
'as resolved',
'as fixed',
),
'keywords' => array('closed', 'fixed', 'resolved'),
),
'wontfix' => array(
'name' => pht('Wontfix'),
'name.full' => pht('Closed, Wontfix'),
'closed' => true,
'prefixes' => array(
'wontfix',
'wontfixes',
'wontfixed',
),
'suffixes' => array(
'as wontfix',
),
),
'invalid' => array(
'name' => pht('Invalid'),
'name.full' => pht('Closed, Invalid'),
'closed' => true,
'prefixes' => array(
'invalidate',
'invalidates',
'invalidated',
),
'suffixes' => array(
'as invalid',
),
),
'duplicate' => array(
'name' => pht('Duplicate'),
'name.full' => pht('Closed, Duplicate'),
'transaction.icon' => 'fa-files-o',
'special' => ManiphestTaskStatus::SPECIAL_DUPLICATE,
'closed' => true,
),
'spite' => array(
'name' => pht('Spite'),
'name.full' => pht('Closed, Spite'),
'name.action' => pht('Spited'),
'transaction.icon' => 'fa-thumbs-o-down',
'silly' => true,
'closed' => true,
'prefixes' => array(
'spite',
'spites',
'spited',
),
'suffixes' => array(
'out of spite',
'as spite',
),
),
);
$status_description = $this->deformat(pht(<<.// Allows you to specify a list of
text prefixes which will trigger a task transition into this status
when mentioned in a commit message. For example, providing "closes" here
will allow users to move tasks to this status by writing `Closes T123` in
commit messages.
- `suffixes` //Optional list.// Allows you to specify a list of
text suffixes which will trigger a task transition into this status
when mentioned in a commit message, after a valid prefix. For example,
providing "as invalid" here will allow users to move tasks
to this status by writing `Closes T123 as invalid`, even if another status
is selected by the "Closes" prefix.
- `keywords` //Optional list.// Allows you to specify a list
of keywords which can be used with `!status` commands in email to select
this status.
Statuses will appear in the UI in the order specified. Note the status marked
`special` as `duplicate` is not settable directly and will not appear in UI
elements, and that any status marked `silly` does not appear if Phabricator
is configured with `phabricator.serious-business` set to true.
Examining the default configuration and examples below will probably be helpful
in understanding these options.
EOTEXT
));
$status_example = array(
'open' => array(
'name' => 'Open',
'special' => 'default',
),
'closed' => array(
'name' => 'Closed',
'special' => 'closed',
'closed' => true,
),
'duplicate' => array(
'name' => 'Duplicate',
'special' => 'duplicate',
'closed' => true,
),
);
$json = new PhutilJSON();
$status_example = $json->encodeFormatted($status_example);
// This is intentionally blank for now, until we can move more Maniphest
// logic to custom fields.
$default_fields = array();
foreach ($default_fields as $key => $enabled) {
$default_fields[$key] = array(
'disabled' => !$enabled,
);
}
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
return array(
$this->newOption('maniphest.custom-field-definitions', 'wild', array())
->setSummary(pht('Custom Maniphest fields.'))
->setDescription(
pht(
'Array of custom fields for Maniphest tasks. For details on '.
'adding custom fields to Maniphest, see "Configuring Custom '.
'Fields" in the documentation.'))
->addExample(
'{"mycompany:estimated-hours": {"name": "Estimated Hours", '.
'"type": "int", "caption": "Estimated number of hours this will '.
'take."}}',
pht('Valid Setting')),
$this->newOption('maniphest.fields', $custom_field_type, $default_fields)
->setCustomData(id(new ManiphestTask())->getCustomFieldBaseClass())
->setDescription(pht('Select and reorder task fields.')),
$this->newOption('maniphest.priorities', 'wild', $priority_defaults)
->setSummary(pht('Configure Maniphest priority names.'))
->setDescription(
pht(
'Allows you to edit or override the default priorities available '.
'in Maniphest, like "High", "Normal" and "Low". The configuration '.
'should contain a map of priority constants to priority '.
'specifications (see defaults below for examples).'.
"\n\n".
'The keys you can define for a priority are:'.
"\n\n".
' - `name` Name of the priority.'."\n".
' - `short` Alternate shorter name, used in UIs where there is '.
' not much space available.'."\n".
' - `color` A color for this priority, like "red" or "blue".'.
' - `keywords` An optional list of keywords which can '.
' be used to select this priority when using `!priority` '.
' commands in email.'.
"\n\n".
'You can choose which priority is the default for newly created '.
'tasks with `maniphest.default-priority`.')),
$this->newOption('maniphest.statuses', $status_type, $status_defaults)
->setSummary(pht('Configure Maniphest task statuses.'))
->setDescription($status_description)
->addExample($status_example, pht('Minimal Valid Config')),
$this->newOption('maniphest.default-priority', 'int', 90)
->setSummary(pht('Default task priority for create flows.'))
->setDescription(
pht(
'Choose a default priority for newly created tasks. You can '.
'review and adjust available priorities by using the '.
'{{maniphest.priorities}} configuration option. The default value '.
'(`90`) corresponds to the default "Needs Triage" priority.')),
$this->newOption(
'metamta.maniphest.subject-prefix',
'string',
'[Maniphest]')
->setDescription(pht('Subject prefix for Maniphest mail.')),
- $this->newOption(
- 'metamta.maniphest.public-create-email',
- 'string',
- null)
- ->setLocked(true)
- ->setLockedMessage(pht(
- 'This configuration is deprecated. See description for details.'))
- ->setSummary(pht('DEPRECATED - Allow filing bugs via email.'))
- ->setDescription(
- pht(
- 'This config has been deprecated in favor of [[ '.
- '/applications/view/PhabricatorManiphestApplication/ | '.
- 'application settings ]], which allow for multiple email '.
- 'addresses and other functionality.'."\n\n".
- '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.')),
- $this->newOption(
- 'metamta.maniphest.default-public-author',
- 'string',
- null)
- ->setLocked(true)
- ->setLockedMessage(pht(
- 'This configuration is deprecated. See description for details.'))
- ->setSummary(pht(
- 'DEPRECATED - Username anonymous bugs are filed under.'))
- ->setDescription(
- pht(
- 'This config has been deprecated in favor of [[ '.
- '/applications/view/PhabricatorManiphestApplication/ | '.
- 'application settings ]], which allow for multiple email '.
- 'addresses each with its own default author, and other '.
- 'functionality.'."\n\n".
- '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.')),
$this->newOption(
'maniphest.priorities.unbreak-now',
'int',
100)
->setSummary(pht('Priority used to populate "Unbreak Now" on home.'))
->setDescription(
pht(
'Temporary setting. If set, this priority is used to populate the '.
'"Unbreak Now" panel on the home page. You should adjust this if '.
'you adjust priorities using `maniphest.priorities`.')),
$this->newOption(
'maniphest.priorities.needs-triage',
'int',
90)
->setSummary(pht('Priority used to populate "Needs Triage" on home.'))
->setDescription(
pht(
'Temporary setting. If set, this priority is used to populate the '.
'"Needs Triage" panel on the home page. You should adjust this if '.
'you adjust priorities using `maniphest.priorities`.')),
);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php
index 915ea6c33a..d9a4e44381 100644
--- a/src/applications/maniphest/controller/ManiphestTaskEditController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php
@@ -1,767 +1,754 @@
id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$response_type = $request->getStr('responseType', 'task');
$order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER);
$can_edit_assign = $this->hasApplicationCapability(
ManiphestEditAssignCapability::CAPABILITY);
$can_edit_policies = $this->hasApplicationCapability(
ManiphestEditPoliciesCapability::CAPABILITY);
$can_edit_priority = $this->hasApplicationCapability(
ManiphestEditPriorityCapability::CAPABILITY);
$can_edit_projects = $this->hasApplicationCapability(
ManiphestEditProjectsCapability::CAPABILITY);
$can_edit_status = $this->hasApplicationCapability(
ManiphestEditStatusCapability::CAPABILITY);
$can_create_projects = PhabricatorPolicyFilter::hasCapability(
$user,
PhabricatorApplication::getByClass('PhabricatorProjectApplication'),
ProjectCreateProjectsCapability::CAPABILITY);
$parent_task = null;
$template_id = null;
if ($this->id) {
$task = id(new ManiphestTaskQuery())
->setViewer($user)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($this->id))
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->executeOne();
if (!$task) {
return new Aphront404Response();
}
} else {
$task = ManiphestTask::initializeNewTask($user);
// We currently do not allow you to set the task status when creating
// a new task, although now that statuses are custom it might make
// sense.
$can_edit_status = false;
// These allow task creation with defaults.
if (!$request->isFormPost()) {
$task->setTitle($request->getStr('title'));
if ($can_edit_projects) {
$projects = $request->getStr('projects');
if ($projects) {
$tokens = $request->getStrList('projects');
$type_project = PhabricatorProjectProjectPHIDType::TYPECONST;
foreach ($tokens as $key => $token) {
if (phid_get_type($token) == $type_project) {
// If this is formatted like a PHID, leave it as-is.
continue;
}
if (preg_match('/^#/', $token)) {
// If this already has a "#", leave it as-is.
continue;
}
// Add a "#" prefix.
$tokens[$key] = '#'.$token;
}
$default_projects = id(new PhabricatorObjectQuery())
->setViewer($user)
->withNames($tokens)
->execute();
$default_projects = mpull($default_projects, 'getPHID');
if ($default_projects) {
$task->attachProjectPHIDs($default_projects);
}
}
}
if ($can_edit_priority) {
$priority = $request->getInt('priority');
if ($priority !== null) {
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
if (isset($priority_map[$priority])) {
$task->setPriority($priority);
}
}
}
$task->setDescription($request->getStr('description'));
if ($can_edit_assign) {
$assign = $request->getStr('assign');
if (strlen($assign)) {
$assign_user = id(new PhabricatorPeopleQuery())
->setViewer($user)
->withUsernames(array($assign))
->executeOne();
if (!$assign_user) {
$assign_user = id(new PhabricatorPeopleQuery())
->setViewer($user)
->withPHIDs(array($assign))
->executeOne();
}
if ($assign_user) {
$task->setOwnerPHID($assign_user->getPHID());
}
}
}
}
$template_id = $request->getInt('template');
// You can only have a parent task if you're creating a new task.
$parent_id = $request->getInt('parent');
if (strlen($parent_id)) {
$parent_task = id(new ManiphestTaskQuery())
->setViewer($user)
->withIDs(array($parent_id))
->executeOne();
if (!$parent_task) {
return new Aphront404Response();
}
if (!$template_id) {
$template_id = $parent_id;
}
}
}
$errors = array();
$e_title = true;
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_EDIT);
$field_list->setViewer($user);
$field_list->readFieldsFromStorage($task);
$aux_fields = $field_list->getFields();
if ($request->isFormPost()) {
$changes = array();
$new_title = $request->getStr('title');
$new_desc = $request->getStr('description');
$new_status = $request->getStr('status');
if (!$task->getID()) {
$workflow = 'create';
} else {
$workflow = '';
}
$changes[ManiphestTransaction::TYPE_TITLE] = $new_title;
$changes[ManiphestTransaction::TYPE_DESCRIPTION] = $new_desc;
if ($can_edit_status) {
$changes[ManiphestTransaction::TYPE_STATUS] = $new_status;
} else if (!$task->getID()) {
// Create an initial status transaction for the burndown chart.
// TODO: We can probably remove this once Facts comes online.
$changes[ManiphestTransaction::TYPE_STATUS] = $task->getStatus();
}
$owner_tokenizer = $request->getArr('assigned_to');
$owner_phid = reset($owner_tokenizer);
if (!strlen($new_title)) {
$e_title = pht('Required');
$errors[] = pht('Title is required.');
}
$old_values = array();
foreach ($aux_fields as $aux_arr_key => $aux_field) {
// TODO: This should be buildFieldTransactionsFromRequest() once we
// switch to ApplicationTransactions properly.
$aux_old_value = $aux_field->getOldValueForApplicationTransactions();
$aux_field->readValueFromRequest($request);
$aux_new_value = $aux_field->getNewValueForApplicationTransactions();
// TODO: We're faking a call to the ApplicaitonTransaction validation
// logic here. We need valid objects to pass, but they aren't used
// in a meaningful way. For now, build User objects. Once the Maniphest
// objects exist, this will switch over automatically. This is a big
// hack but shouldn't be long for this world.
$placeholder_editor = new PhabricatorUserProfileEditor();
$field_errors = $aux_field->validateApplicationTransactions(
$placeholder_editor,
PhabricatorTransactions::TYPE_CUSTOMFIELD,
array(
id(new ManiphestTransaction())
->setOldValue($aux_old_value)
->setNewValue($aux_new_value),
));
foreach ($field_errors as $error) {
$errors[] = $error->getMessage();
}
$old_values[$aux_field->getFieldKey()] = $aux_old_value;
}
if ($errors) {
$task->setTitle($new_title);
$task->setDescription($new_desc);
$task->setPriority($request->getInt('priority'));
$task->setOwnerPHID($owner_phid);
$task->attachSubscriberPHIDs($request->getArr('cc'));
$task->attachProjectPHIDs($request->getArr('projects'));
} else {
if ($can_edit_priority) {
$changes[ManiphestTransaction::TYPE_PRIORITY] =
$request->getInt('priority');
}
if ($can_edit_assign) {
$changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid;
}
$changes[PhabricatorTransactions::TYPE_SUBSCRIBERS] =
array('=' => $request->getArr('cc'));
if ($can_edit_projects) {
$projects = $request->getArr('projects');
$changes[PhabricatorTransactions::TYPE_EDGE] =
$projects;
$column_phid = $request->getStr('columnPHID');
// allow for putting a task in a project column at creation -only-
if (!$task->getID() && $column_phid && $projects) {
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($user)
->withProjectPHIDs($projects)
->withPHIDs(array($column_phid))
->executeOne();
if ($column) {
$changes[ManiphestTransaction::TYPE_PROJECT_COLUMN] =
array(
'new' => array(
'projectPHID' => $column->getProjectPHID(),
'columnPHIDs' => array($column_phid),
),
'old' => array(
'projectPHID' => $column->getProjectPHID(),
'columnPHIDs' => array(),
),
);
}
}
}
if ($can_edit_policies) {
$changes[PhabricatorTransactions::TYPE_VIEW_POLICY] =
$request->getStr('viewPolicy');
$changes[PhabricatorTransactions::TYPE_EDIT_POLICY] =
$request->getStr('editPolicy');
}
$template = new ManiphestTransaction();
$transactions = array();
foreach ($changes as $type => $value) {
$transaction = clone $template;
$transaction->setTransactionType($type);
if ($type == ManiphestTransaction::TYPE_PROJECT_COLUMN) {
$transaction->setNewValue($value['new']);
$transaction->setOldValue($value['old']);
} else if ($type == PhabricatorTransactions::TYPE_EDGE) {
$project_type =
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$transaction
->setMetadataValue('edge:type', $project_type)
->setNewValue(
array(
'=' => array_fuse($value),
));
} else {
$transaction->setNewValue($value);
}
$transactions[] = $transaction;
}
if ($aux_fields) {
foreach ($aux_fields as $aux_field) {
$transaction = clone $template;
$transaction->setTransactionType(
PhabricatorTransactions::TYPE_CUSTOMFIELD);
$aux_key = $aux_field->getFieldKey();
$transaction->setMetadataValue('customfield:key', $aux_key);
$old = idx($old_values, $aux_key);
$new = $aux_field->getNewValueForApplicationTransactions();
$transaction->setOldValue($old);
$transaction->setNewValue($new);
$transactions[] = $transaction;
}
}
if ($transactions) {
$is_new = !$task->getID();
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$transactions = $event->getValue('transactions');
$editor = id(new ManiphestTransactionEditor())
->setActor($user)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->applyTransactions($task, $transactions);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
}
if ($parent_task) {
// TODO: This should be transactional now.
id(new PhabricatorEdgeEditor())
->addEdge(
$parent_task->getPHID(),
ManiphestTaskDependsOnTaskEdgeType::EDGECONST,
$task->getPHID())
->save();
$workflow = $parent_task->getID();
}
if ($request->isAjax()) {
switch ($response_type) {
case 'card':
$owner = null;
if ($task->getOwnerPHID()) {
$owner = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($task->getOwnerPHID()))
->executeOne();
}
$tasks = id(new ProjectBoardTaskCard())
->setViewer($user)
->setTask($task)
->setOwner($owner)
->setCanEdit(true)
->getItem();
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($user)
->withPHIDs(array($request->getStr('columnPHID')))
->executeOne();
if (!$column) {
return new Aphront404Response();
}
// re-load projects for accuracy as they are not re-loaded via
// the editor
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$task->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
$task->attachProjectPHIDs($project_phids);
$remove_from_board = false;
if (!in_array($column->getProjectPHID(), $project_phids)) {
$remove_from_board = true;
}
$positions = id(new PhabricatorProjectColumnPositionQuery())
->setViewer($user)
->withColumns(array($column))
->execute();
$task_phids = mpull($positions, 'getObjectPHID');
$column_tasks = id(new ManiphestTaskQuery())
->setViewer($user)
->withPHIDs($task_phids)
->execute();
if ($order == PhabricatorProjectColumn::ORDER_NATURAL) {
// TODO: This is a little bit awkward, because PHP and JS use
// slightly different sort order parameters to achieve the same
// effect. It would be good to unify this a bit at some point.
$sort_map = array();
foreach ($positions as $position) {
$sort_map[$position->getObjectPHID()] = array(
-$position->getSequence(),
$position->getID(),
);
}
} else {
$sort_map = mpull(
$column_tasks,
'getPrioritySortVector',
'getPHID');
}
$data = array(
'sortMap' => $sort_map,
'removeFromBoard' => $remove_from_board,
);
break;
case 'task':
default:
$tasks = $this->renderSingleTask($task);
$data = array();
break;
}
return id(new AphrontAjaxResponse())->setContent(
array(
'tasks' => $tasks,
'data' => $data,
));
}
$redirect_uri = '/T'.$task->getID();
if ($workflow) {
$redirect_uri .= '?workflow='.$workflow;
}
return id(new AphrontRedirectResponse())
->setURI($redirect_uri);
}
} else {
if (!$task->getID()) {
$task->attachSubscriberPHIDs(array(
$user->getPHID(),
));
if ($template_id) {
$template_task = id(new ManiphestTaskQuery())
->setViewer($user)
->withIDs(array($template_id))
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->executeOne();
if ($template_task) {
$cc_phids = array_unique(array_merge(
$template_task->getSubscriberPHIDs(),
array($user->getPHID())));
$task->attachSubscriberPHIDs($cc_phids);
$task->attachProjectPHIDs($template_task->getProjectPHIDs());
$task->setOwnerPHID($template_task->getOwnerPHID());
$task->setPriority($template_task->getPriority());
$task->setViewPolicy($template_task->getViewPolicy());
$task->setEditPolicy($template_task->getEditPolicy());
$template_fields = PhabricatorCustomField::getObjectFields(
$template_task,
PhabricatorCustomField::ROLE_EDIT);
$fields = $template_fields->getFields();
foreach ($fields as $key => $field) {
if (!$field->shouldCopyWhenCreatingSimilarTask()) {
unset($fields[$key]);
}
if (empty($aux_fields[$key])) {
unset($fields[$key]);
}
}
if ($fields) {
id(new PhabricatorCustomFieldList($fields))
->setViewer($user)
->readFieldsFromStorage($template_task);
foreach ($fields as $key => $field) {
$aux_fields[$key]->setValueFromStorage(
$field->getValueForStorage());
}
}
}
}
}
}
$error_view = null;
if ($errors) {
$error_view = new PHUIInfoView();
$error_view->setErrors($errors);
}
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
if ($task->getOwnerPHID()) {
$assigned_value = array($task->getOwnerPHID());
} else {
$assigned_value = array();
}
if ($task->getSubscriberPHIDs()) {
$cc_value = $task->getSubscriberPHIDs();
} else {
$cc_value = array();
}
if ($task->getProjectPHIDs()) {
$projects_value = $task->getProjectPHIDs();
} else {
$projects_value = array();
}
$cancel_id = nonempty($task->getID(), $template_id);
if ($cancel_id) {
$cancel_uri = '/T'.$cancel_id;
} else {
$cancel_uri = '/maniphest/';
}
if ($task->getID()) {
$button_name = pht('Save Task');
$header_name = pht('Edit Task');
} else if ($parent_task) {
$cancel_uri = '/T'.$parent_task->getID();
$button_name = pht('Create Task');
$header_name = pht('Create New Subtask');
} else {
$button_name = pht('Create Task');
$header_name = pht('Create New Task');
}
require_celerity_resource('maniphest-task-edit-css');
$project_tokenizer_id = celerity_generate_unique_node_id();
$form = new AphrontFormView();
$form
->setUser($user)
->addHiddenInput('template', $template_id)
->addHiddenInput('responseType', $response_type)
->addHiddenInput('order', $order)
->addHiddenInput('ungrippable', $request->getStr('ungrippable'))
->addHiddenInput('columnPHID', $request->getStr('columnPHID'));
if ($parent_task) {
$form
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Parent Task'))
->setValue($user->renderHandle($parent_task->getPHID())))
->addHiddenInput('parent', $parent_task->getID());
}
$form
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Title'))
->setName('title')
->setError($e_title)
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
->setValue($task->getTitle()));
if ($can_edit_status) {
// See T4819.
$status_map = ManiphestTaskStatus::getTaskStatusMap();
$dup_status = ManiphestTaskStatus::getDuplicateStatus();
if ($task->getStatus() != $dup_status) {
unset($status_map[$dup_status]);
}
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Status'))
->setName('status')
->setValue($task->getStatus())
->setOptions($status_map));
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($user)
->setObject($task)
->execute();
if ($can_edit_assign) {
$form->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Assigned To'))
->setName('assigned_to')
->setValue($assigned_value)
->setUser($user)
->setDatasource(new PhabricatorPeopleDatasource())
->setLimit(1));
}
$form
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('CC'))
->setName('cc')
->setValue($cc_value)
->setUser($user)
->setDatasource(new PhabricatorMetaMTAMailableDatasource()));
if ($can_edit_priority) {
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Priority'))
->setName('priority')
->setOptions($priority_map)
->setValue($task->getPriority()));
}
if ($can_edit_policies) {
$form
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicyObject($task)
->setPolicies($policies)
->setName('viewPolicy'))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicyObject($task)
->setPolicies($policies)
->setName('editPolicy'));
}
if ($can_edit_projects) {
$caption = null;
if ($can_create_projects) {
$caption = javelin_tag(
'a',
array(
'href' => '/project/create/',
'mustcapture' => true,
'sigil' => 'project-create',
),
pht('Create New Project'));
}
$form
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects'))
->setName('projects')
->setValue($projects_value)
->setID($project_tokenizer_id)
->setCaption($caption)
->setDatasource(new PhabricatorProjectDatasource()));
}
$field_list->appendFieldsToForm($form);
require_celerity_resource('phui-info-view-css');
Javelin::initBehavior('project-create', array(
'tokenizerID' => $project_tokenizer_id,
));
- $description_control = new PhabricatorRemarkupControl();
- // "Upsell" creating tasks via email in create flows if the instance is
- // configured for this awesomeness.
- $email_create = PhabricatorEnv::getEnvConfig(
- 'metamta.maniphest.public-create-email');
- if (!$task->getID() && $email_create) {
- $email_hint = pht(
- 'You can also create tasks by sending an email to: %s',
- phutil_tag('tt', array(), $email_create));
- $description_control->setCaption($email_hint);
- }
-
- $description_control
+ $description_control = id(new PhabricatorRemarkupControl())
->setLabel(pht('Description'))
->setName('description')
->setID('description-textarea')
->setValue($task->getDescription())
->setUser($user);
$form
->appendChild($description_control);
-
if ($request->isAjax()) {
$dialog = id(new AphrontDialogView())
->setUser($user)
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle($header_name)
->appendChild(
array(
$error_view,
$form->buildLayoutView(),
))
->addCancelButton($cancel_uri)
->addSubmitButton($button_name);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($button_name));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($header_name)
->setFormErrors($errors)
->setForm($form);
$preview = id(new PHUIRemarkupPreviewPanel())
->setHeader(pht('Description Preview'))
->setControlID('description-textarea')
->setPreviewURI($this->getApplicationURI('task/descriptionpreview/'));
if ($task->getID()) {
$page_objects = array($task->getPHID());
} else {
$page_objects = array();
}
$crumbs = $this->buildApplicationCrumbs();
if ($task->getID()) {
$crumbs->addTextCrumb('T'.$task->getID(), '/T'.$task->getID());
}
$crumbs->addTextCrumb($header_name);
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
$preview,
),
array(
'title' => $header_name,
'pageObjects' => $page_objects,
));
}
}
diff --git a/src/applications/paste/config/PhabricatorPasteConfigOptions.php b/src/applications/paste/config/PhabricatorPasteConfigOptions.php
index e12fdc954d..bb970905c4 100644
--- a/src/applications/paste/config/PhabricatorPasteConfigOptions.php
+++ b/src/applications/paste/config/PhabricatorPasteConfigOptions.php
@@ -1,46 +1,32 @@
newOption(
- 'metamta.paste.public-create-email',
- 'string',
- null)
- ->setLocked(true)
- ->setLockedMessage(pht(
- 'This configuration is deprecated. See description for details.'))
- ->setSummary(pht('DEPRECATED - Allow creating pastes via email.'))
- ->setDescription(
- pht(
- 'This config has been deprecated in favor of [[ '.
- '/applications/view/PhabricatorPasteApplication/ | '.
- 'application settings ]], which allow for multiple email '.
- 'addresses and other functionality.')),
$this->newOption(
'metamta.paste.subject-prefix',
'string',
'[Paste]')
->setDescription(pht('Subject prefix for Paste email.')),
);
}
}