diff --git a/conf/production.conf.php b/conf/production.conf.php index e9a474ee11..c0a7bc2448 100644 --- a/conf/production.conf.php +++ b/conf/production.conf.php @@ -1,6 +1,5 @@ setTagline('permanently destroy a Differential Revision'); $args->setSynopsis(<<parseStandardArguments(); $args->parse( array( array( 'name' => 'revision', 'wildcard' => true, ), )); $revisions = $args->getArg('revision'); if (count($revisions) != 1) { $args->printHelpAndExit(); } $id = trim(strtolower(head($revisions)), 'd '); $revision = id(new DifferentialRevision())->load($id); if (!$revision) { throw new Exception("No revision '{$id}' exists!"); } $title = $revision->getTitle(); $ok = phutil_console_confirm("Really destroy 'D{$id}: {$title}' forever?"); if (!$ok) { throw new Exception("User aborted workflow."); } $revision->delete(); echo "OK, destroyed revision.\n"; - diff --git a/scripts/mail/mail_handler.php b/scripts/mail/mail_handler.php index 11d8f1c29f..63d6688a5c 100755 --- a/scripts/mail/mail_handler.php +++ b/scripts/mail/mail_handler.php @@ -1,96 +1,94 @@ #!/usr/bin/env php 1) { foreach (array_slice($argv, 1) as $arg) { if (!preg_match('/^-/', $arg)) { $_SERVER['PHABRICATOR_ENV'] = $arg; break; } } } $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; require_once $root.'/externals/mimemailparser/MimeMailParser.class.php'; $args = new PhutilArgumentParser($argv); $args->parseStandardArguments(); $args->parse( array( array( 'name' => 'process-duplicates', 'help' => pht( "Process this message, even if it's a duplicate of another message. ". "This is mostly useful when debugging issues with mail routing."), ), array( 'name' => 'env', 'wildcard' => true, ), )); $parser = new MimeMailParser(); $parser->setText(file_get_contents('php://stdin')); $text_body = $parser->getMessageBody('text'); $text_body_headers = $parser->getMessageBodyHeaders('text'); $content_type = idx($text_body_headers, 'content-type'); if ( !phutil_is_utf8($text_body) && (preg_match('/charset="(.*?)"/', $content_type, $matches) || preg_match('/charset=(\S+)/', $content_type, $matches)) ) { $text_body = phutil_utf8_convert($text_body, "UTF-8", $matches[1]); } $headers = $parser->getHeaders(); $headers['subject'] = iconv_mime_decode($headers['subject'], 0, "UTF-8"); $headers['from'] = iconv_mime_decode($headers['from'], 0, "UTF-8"); if ($args->getArg('process-duplicates')) { $headers['message-id'] = Filesystem::readRandomCharacters(64); } $received = new PhabricatorMetaMTAReceivedMail(); $received->setHeaders($headers); $received->setBodies(array( 'text' => $text_body, 'html' => $parser->getMessageBody('html'), )); $attachments = array(); foreach ($parser->getAttachments() as $attachment) { if (preg_match('@text/(plain|html)@', $attachment->getContentType()) && $attachment->getContentDisposition() == 'inline') { // If this is an "inline" attachment with some sort of text content-type, // do not treat it as a file for attachment. MimeMailParser already picked // it up in the getMessageBody() call above. We still want to treat 'inline' // attachments with other content types (e.g., images) as attachments. continue; } $file = PhabricatorFile::newFromFileData( $attachment->getContent(), array( 'name' => $attachment->getFilename(), )); $attachments[] = $file->getPHID(); } try { $received->setAttachments($attachments); $received->save(); $received->processReceivedMail(); } catch (Exception $e) { $received ->setMessage('EXCEPTION: '.$e->getMessage()) ->save(); throw $e; } - - diff --git a/scripts/mail/manage_mail.php b/scripts/mail/manage_mail.php index fefb1e0da3..9822565175 100755 --- a/scripts/mail/manage_mail.php +++ b/scripts/mail/manage_mail.php @@ -1,22 +1,21 @@ #!/usr/bin/env php setTagline('manage mail'); $args->setSynopsis(<<parseStandardArguments(); $workflows = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorMailManagementWorkflow') ->loadObjects(); $workflows[] = new PhutilHelpArgumentWorkflow(); $args->parseWorkflows($workflows); - diff --git a/scripts/repository/test_connection.php b/scripts/repository/test_connection.php index 6047f3429d..226a967c78 100755 --- a/scripts/repository/test_connection.php +++ b/scripts/repository/test_connection.php @@ -1,6 +1,5 @@ #!/usr/bin/env php loadAll(); echo "Updating relationships for ".count($tasks)." tasks"; foreach ($tasks as $task) { ManiphestTaskProject::updateTaskProjects($task); ManiphestTaskSubscriber::updateTaskSubscribers($task); echo '.'; } echo "\nDone.\n"; - diff --git a/src/aphront/console/DarkConsoleCore.php b/src/aphront/console/DarkConsoleCore.php index 3501f514ea..3389109ebf 100644 --- a/src/aphront/console/DarkConsoleCore.php +++ b/src/aphront/console/DarkConsoleCore.php @@ -1,138 +1,137 @@ setType('class') ->setAncestorClass('DarkConsolePlugin') ->selectAndLoadSymbols(); foreach ($symbols as $symbol) { $plugin = newv($symbol['name'], array()); if (!$plugin->shouldStartup()) { continue; } $plugin->setConsoleCore($this); $plugin->didStartup(); $this->plugins[$symbol['name']] = $plugin; } } public function getPlugins() { return $this->plugins; } public function getKey(AphrontRequest $request) { $plugins = $this->getPlugins(); foreach ($plugins as $plugin) { $plugin->setRequest($request); $plugin->willShutdown(); } foreach ($plugins as $plugin) { $plugin->didShutdown(); } foreach ($plugins as $plugin) { $plugin->setData($plugin->generateData()); } $plugins = msort($plugins, 'getOrderKey'); $key = Filesystem::readRandomCharacters(24); $tabs = array(); $data = array(); foreach ($plugins as $plugin) { $class = get_class($plugin); $tabs[] = array( 'class' => $class, 'name' => $plugin->getName(), 'color' => $plugin->getColor(), ); $data[$class] = $this->sanitizeForJSON($plugin->getData()); } $storage = array( 'vers' => self::STORAGE_VERSION, 'tabs' => $tabs, 'data' => $data, 'user' => $request->getUser() ? $request->getUser()->getPHID() : null, ); $cache = new PhabricatorKeyValueDatabaseCache(); $cache = new PhutilKeyValueCacheProfiler($cache); $cache->setProfiler(PhutilServiceProfiler::getInstance()); // This encoding may fail if there are, e.g., database queries which // include binary data. It would be a little cleaner to try to strip these, // but just do something non-broken here if we end up with unrepresentable // data. $json = @json_encode($storage); if (!$json) { $json = '{}'; } $cache->setKeys( array( 'darkconsole:'.$key => $json, ), $ttl = (60 * 60 * 6)); return $key; } public function getColor() { foreach ($this->getPlugins() as $plugin) { if ($plugin->getColor()) { return $plugin->getColor(); } } } public function render(AphrontRequest $request) { $user = $request->getUser(); $visible = $user ? $user->getConsoleVisible() : true; return javelin_tag( 'div', array( 'id' => 'darkconsole', 'class' => 'dark-console', 'style' => $visible ? '' : 'display: none;', 'data-console-key' => $this->getKey($request), 'data-console-color' => $this->getColor(), ), ''); } /** * Sometimes, tab data includes binary information (like INSERT queries which * write file data into the database). To successfully JSON encode it, we * need to convert it to UTF-8. */ private function sanitizeForJSON($data) { if (is_object($data)) { return ''; } else if (is_array($data)) { foreach ($data as $key => $value) { $data[$key] = $this->sanitizeForJSON($value); } return $data; } else { return phutil_utf8ize($data); } } } - diff --git a/src/aphront/console/plugin/DarkConsoleServicesPlugin.php b/src/aphront/console/plugin/DarkConsoleServicesPlugin.php index 38538f0564..e0f760d271 100644 --- a/src/aphront/console/plugin/DarkConsoleServicesPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleServicesPlugin.php @@ -1,296 +1,295 @@ getServiceCallLog(); foreach ($log as $key => $entry) { $config = idx($entry, 'config', array()); unset($log[$key]['config']); if (!$should_analyze) { $log[$key]['explain'] = array( 'sev' => 7, 'size' => null, 'reason' => 'Disabled', ); // Query analysis is disabled for this request, so don't do any of it. continue; } if ($entry['type'] != 'query') { continue; } // For each SELECT query, go issue an EXPLAIN on it so we can flag stuff // causing table scans, etc. if (preg_match('/^\s*SELECT\b/i', $entry['query'])) { $conn = PhabricatorEnv::newObjectFromConfig( 'mysql.implementation', array($entry['config'])); try { $explain = queryfx_all( $conn, 'EXPLAIN %Q', $entry['query']); $badness = 0; $size = 1; $reason = null; foreach ($explain as $table) { $size *= (int)$table['rows']; switch ($table['type']) { case 'index': $cur_badness = 1; $cur_reason = 'Index'; break; case 'const': $cur_badness = 1; $cur_reason = 'Const'; break; case 'eq_ref'; $cur_badness = 2; $cur_reason = 'EqRef'; break; case 'range': $cur_badness = 3; $cur_reason = 'Range'; break; case 'ref': $cur_badness = 3; $cur_reason = 'Ref'; break; case 'fulltext': $cur_badness = 3; $cur_reason = 'Fulltext'; break; case 'ALL': if (preg_match('/Using where/', $table['Extra'])) { if ($table['rows'] < 256 && !empty($table['possible_keys'])) { $cur_badness = 2; $cur_reason = 'Small Table Scan'; } else { $cur_badness = 6; $cur_reason = 'TABLE SCAN!'; } } else { $cur_badness = 3; $cur_reason = 'Whole Table'; } break; default: if (preg_match('/No tables used/i', $table['Extra'])) { $cur_badness = 1; $cur_reason = 'No Tables'; } else if (preg_match('/Impossible/i', $table['Extra'])) { $cur_badness = 1; $cur_reason = 'Empty'; } else { $cur_badness = 4; $cur_reason = "Can't Analyze"; } break; } if ($cur_badness > $badness) { $badness = $cur_badness; $reason = $cur_reason; } } $log[$key]['explain'] = array( 'sev' => $badness, 'size' => $size, 'reason' => $reason, ); } catch (Exception $ex) { $log[$key]['explain'] = array( 'sev' => 5, 'size' => null, 'reason' => $ex->getMessage(), ); } } } return array( 'start' => PhabricatorStartup::getStartTime(), 'end' => microtime(true), 'log' => $log, 'analyzeURI' => (string)$this ->getRequestURI() ->alter('__analyze__', true), 'didAnalyze' => $should_analyze, ); } public function renderPanel() { $data = $this->getData(); $log = $data['log']; $results = array(); $results[] = phutil_tag( 'div', array('class' => 'dark-console-panel-header'), array( phutil_tag( 'a', array( 'href' => $data['analyzeURI'], 'class' => $data['didAnalyze'] ? 'disabled button' : 'green button', ), pht('Analyze Query Plans')), phutil_tag('h1', array(), pht('Calls to External Services')), phutil_tag('div', array('style' => 'clear: both;')), )); $page_total = $data['end'] - $data['start']; $totals = array(); $counts = array(); foreach ($log as $row) { $totals[$row['type']] = idx($totals, $row['type'], 0) + $row['duration']; $counts[$row['type']] = idx($counts, $row['type'], 0) + 1; } $totals['All Services'] = array_sum($totals); $counts['All Services'] = array_sum($counts); $totals['Entire Page'] = $page_total; $counts['Entire Page'] = 0; $summary = array(); foreach ($totals as $type => $total) { $summary[] = array( $type, number_format($counts[$type]), number_format((int)(1000000 * $totals[$type])).' us', sprintf('%.1f%%', 100 * $totals[$type] / $page_total), ); } $summary_table = new AphrontTableView($summary); $summary_table->setColumnClasses( array( '', 'n', 'n', 'wide', )); $summary_table->setHeaders( array( 'Type', 'Count', 'Total Cost', 'Page Weight', )); $results[] = $summary_table->render(); $rows = array(); foreach ($log as $row) { $analysis = null; switch ($row['type']) { case 'query': $info = $row['query']; $info = wordwrap($info, 128, "\n", true); if (!empty($row['explain'])) { $analysis = phutil_tag( 'span', array( 'class' => 'explain-sev-'.$row['explain']['sev'], ), $row['explain']['reason']); } break; case 'connect': $info = $row['host'].':'.$row['database']; break; case 'exec': $info = $row['command']; break; case 'conduit': $info = $row['method']; break; case 'http': $info = $row['uri']; break; default: $info = '-'; break; } $rows[] = array( $row['type'], '+'.number_format(1000 * ($row['begin'] - $data['start'])).' ms', number_format(1000000 * $row['duration']).' us', $info, $analysis, ); } $table = new AphrontTableView($rows); $table->setColumnClasses( array( null, 'n', 'n', 'wide', '', )); $table->setHeaders( array( 'Event', 'Start', 'Duration', 'Details', 'Analysis', )); $results[] = $table->render(); return phutil_implode_html("\n", $results); } } - diff --git a/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php b/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php index 3855dd4a89..c65a4a27b8 100644 --- a/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php +++ b/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php @@ -1,79 +1,78 @@ $value->getMessage(), 'event' => $event, 'file' => $value->getFile(), 'line' => $value->getLine(), 'str' => $value->getMessage(), 'trace' => $metadata['trace'], ); break; case PhutilErrorHandler::ERROR: // $value is a simple string self::$errors[] = array( 'details' => $value, 'event' => $event, 'file' => $metadata['file'], 'line' => $metadata['line'], 'str' => $value, 'trace' => $metadata['trace'], ); break; case PhutilErrorHandler::PHLOG: // $value can be anything self::$errors[] = array( 'details' => PhutilReadableSerializer::printShallow($value, 3), 'event' => $event, 'file' => $metadata['file'], 'line' => $metadata['line'], 'str' => PhutilReadableSerializer::printShort($value), 'trace' => $metadata['trace'], ); break; default: error_log('Unknown event : '.$event); break; } } } - diff --git a/src/aphront/console/plugin/event/DarkConsoleEventPluginAPI.php b/src/aphront/console/plugin/event/DarkConsoleEventPluginAPI.php index 3298fb46c0..afac58ceb4 100644 --- a/src/aphront/console/plugin/event/DarkConsoleEventPluginAPI.php +++ b/src/aphront/console/plugin/event/DarkConsoleEventPluginAPI.php @@ -1,31 +1,30 @@ listen(PhabricatorEventType::TYPE_ALL); } public function handleEvent(PhutilEvent $event) { if (self::$discardMode) { return; } self::$events[] = $event; } } - diff --git a/src/applications/audit/application/PhabricatorApplicationAudit.php b/src/applications/audit/application/PhabricatorApplicationAudit.php index ce0a120b16..d568a7be3d 100644 --- a/src/applications/audit/application/PhabricatorApplicationAudit.php +++ b/src/applications/audit/application/PhabricatorApplicationAudit.php @@ -1,81 +1,80 @@ array( '' => 'PhabricatorAuditListController', 'view/(?P[^/]+)/(?:(?P[^/]+)/)?' => 'PhabricatorAuditListController', 'addcomment/' => 'PhabricatorAuditAddCommentController', 'preview/(?P[1-9]\d*)/' => 'PhabricatorAuditPreviewController', ), ); } public function getApplicationGroup() { return self::GROUP_CORE; } public function getApplicationOrder() { return 0.130; } public function loadStatus(PhabricatorUser $user) { $status = array(); $phids = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user); $commits = id(new PhabricatorAuditCommitQuery()) ->withAuthorPHIDs($phids) ->withStatus(PhabricatorAuditCommitQuery::STATUS_CONCERN) ->execute(); $count = count($commits); $type = PhabricatorApplicationStatusView::TYPE_NEEDS_ATTENTION; $status[] = id(new PhabricatorApplicationStatusView()) ->setType($type) ->setText(pht('%d Problem Commit(s)', $count)) ->setCount($count); $audits = id(new PhabricatorAuditQuery()) ->withAuditorPHIDs($phids) ->withStatus(PhabricatorAuditQuery::STATUS_OPEN) ->withAwaitingUser($user) ->execute(); $count = count($audits); $type = PhabricatorApplicationStatusView::TYPE_WARNING; $status[] = id(new PhabricatorApplicationStatusView()) ->setType($type) ->setText(pht('%d Commit(s) Awaiting Audit', $count)) ->setCount($count); return $status; } } - diff --git a/src/applications/audit/events/AuditActionMenuEventListener.php b/src/applications/audit/events/AuditActionMenuEventListener.php index 73a5e1a01b..97dfe7af67 100644 --- a/src/applications/audit/events/AuditActionMenuEventListener.php +++ b/src/applications/audit/events/AuditActionMenuEventListener.php @@ -1,46 +1,45 @@ listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS); } public function handleEvent(PhutilEvent $event) { switch ($event->getType()) { case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS: $this->handleActionsEvent($event); break; } } private function handleActionsEvent(PhutilEvent $event) { $object = $event->getValue('object'); $actions = null; if ($object instanceof PhabricatorUser) { $actions = $this->renderUserItems($event); } $this->addActionMenuItems($event, $actions); } private function renderUserItems(PhutilEvent $event) { if (!$this->canUseApplication($event->getUser())) { return null; } $user = $event->getValue('object'); $username = phutil_escape_uri($user->getUsername()); $view_uri = '/audit/view/author/'.$username.'/'; return id(new PhabricatorActionView()) ->setIcon('audit-dark') ->setIconSheet(PHUIIconView::SPRITE_APPS) ->setName(pht('View Commits')) ->setHref($view_uri); } } - diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php b/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php index ff6564c95e..bb5b35b986 100644 --- a/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php +++ b/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php @@ -1,143 +1,142 @@ provider = $provider; return $this; } public function getProvider() { return $this->provider; } public function getApplicationName() { return 'auth'; } public function getApplicationTransactionType() { return PhabricatorPHIDConstants::PHID_TYPE_AUTH; } public function getApplicationTransactionCommentObject() { return null; } public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_ENABLE: if ($new) { return 'new'; } else { return 'delete'; } } return parent::getIcon(); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_ENABLE: if ($new) { return 'green'; } else { return 'red'; } } return parent::getColor(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_ENABLE: if ($old === null) { return pht( '%s created this provider.', $this->renderHandleLink($author_phid)); } else if ($new) { return pht( '%s enabled this provider.', $this->renderHandleLink($author_phid)); } else { return pht( '%s disabled this provider.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_REGISTRATION: if ($new) { return pht( '%s enabled registration.', $this->renderHandleLink($author_phid)); } else { return pht( '%s disabled registration.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_LINK: if ($new) { return pht( '%s enabled accont linking.', $this->renderHandleLink($author_phid)); } else { return pht( '%s disabled account linking.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_UNLINK: if ($new) { return pht( '%s enabled account unlinking.', $this->renderHandleLink($author_phid)); } else { return pht( '%s disabled account unlinking.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_PROPERTY: $provider = $this->getProvider(); if ($provider) { $title = $provider->renderConfigPropertyTransactionTitle($this); if (strlen($title)) { return $title; } } return pht( '%s edited a property of this provider.', $this->renderHandleLink($author_phid)); break; } return parent::getTitle(); } } - diff --git a/src/applications/base/controller/__tests__/PhabricatorApplicationTest.php b/src/applications/base/controller/__tests__/PhabricatorApplicationTest.php index b20faaf553..d5e993bc4f 100644 --- a/src/applications/base/controller/__tests__/PhabricatorApplicationTest.php +++ b/src/applications/base/controller/__tests__/PhabricatorApplicationTest.php @@ -1,38 +1,37 @@ policies = array(); } public function setPolicy($capability, $value) { $this->policies[$capability] = $value; return $this; } public function getPolicy($capability) { return idx($this->policies, $capability, parent::getPolicy($capability)); } public function shouldAppearInLaunchView() { return false; } public function canUninstall() { return false; } public function getRoutes() { return array( ); } } - diff --git a/src/applications/chatlog/applications/PhabricatorApplicationChatLog.php b/src/applications/chatlog/applications/PhabricatorApplicationChatLog.php index 5c873ac414..707cf46344 100644 --- a/src/applications/chatlog/applications/PhabricatorApplicationChatLog.php +++ b/src/applications/chatlog/applications/PhabricatorApplicationChatLog.php @@ -1,41 +1,40 @@ array( '' => 'PhabricatorChatLogChannelListController', 'channel/(?P[^/]+)/' => 'PhabricatorChatLogChannelLogController', ), ); } } - diff --git a/src/applications/chatlog/storage/PhabricatorChatLogChannel.php b/src/applications/chatlog/storage/PhabricatorChatLogChannel.php index e9ea837379..5e3f355a7b 100644 --- a/src/applications/chatlog/storage/PhabricatorChatLogChannel.php +++ b/src/applications/chatlog/storage/PhabricatorChatLogChannel.php @@ -1,40 +1,39 @@ viewPolicy; break; case PhabricatorPolicyCapability::CAN_EDIT: return $this->editPolicy; break; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } - diff --git a/src/applications/config/check/PhabricatorSetupCheckImagemagick.php b/src/applications/config/check/PhabricatorSetupCheckImagemagick.php index be041518d2..6d201be53d 100644 --- a/src/applications/config/check/PhabricatorSetupCheckImagemagick.php +++ b/src/applications/config/check/PhabricatorSetupCheckImagemagick.php @@ -1,24 +1,23 @@ newIssue('files.enable-imagemagick') ->setName(pht( "'convert' binary not found or Imagemagick is not installed.")) ->setMessage($message) ->addRelatedPhabricatorConfig('files.enable-imagemagick') ->addPhabricatorConfig('environment.append-paths'); } } } } - diff --git a/src/applications/config/storage/PhabricatorConfigTransaction.php b/src/applications/config/storage/PhabricatorConfigTransaction.php index 75c2cc34a3..579b8b3b65 100644 --- a/src/applications/config/storage/PhabricatorConfigTransaction.php +++ b/src/applications/config/storage/PhabricatorConfigTransaction.php @@ -1,119 +1,118 @@ getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_EDIT: // TODO: After T2213 show the actual values too; for now, we don't // have the tools to do it without making a bit of a mess of it. $old_del = idx($old, 'deleted'); $new_del = idx($new, 'deleted'); if ($old_del && !$new_del) { return pht( '%s created this configuration entry.', $this->renderHandleLink($author_phid)); } else if (!$old_del && $new_del) { return pht( '%s deleted this configuration entry.', $this->renderHandleLink($author_phid)); } else if ($old_del && $new_del) { // This is a bug. return pht( '%s deleted this configuration entry (again?).', $this->renderHandleLink($author_phid)); } else { return pht( '%s edited this configuration entry.', $this->renderHandleLink($author_phid)); } break; } return parent::getTitle(); } public function getIcon() { switch ($this->getTransactionType()) { case self::TYPE_EDIT: return 'edit'; } return parent::getIcon(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_EDIT: return true; } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { $old = $this->getOldValue(); $new = $this->getNewValue(); if ($old['deleted']) { $old_text = ''; } else { $old_text = PhabricatorConfigJSON::prettyPrintJSON($old['value']); } if ($new['deleted']) { $new_text = ''; } else { $new_text = PhabricatorConfigJSON::prettyPrintJSON($new['value']); } return $this->renderTextCorpusChangeDetails( $viewer, $old_text, $new_text); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_EDIT: $old_del = idx($old, 'deleted'); $new_del = idx($new, 'deleted'); if ($old_del && !$new_del) { return PhabricatorTransactions::COLOR_GREEN; } else if (!$old_del && $new_del) { return PhabricatorTransactions::COLOR_RED; } else { return PhabricatorTransactions::COLOR_BLUE; } break; } } } - diff --git a/src/applications/conpherence/events/ConpherenceActionMenuEventListener.php b/src/applications/conpherence/events/ConpherenceActionMenuEventListener.php index 1fa71004eb..ca5145bd5b 100644 --- a/src/applications/conpherence/events/ConpherenceActionMenuEventListener.php +++ b/src/applications/conpherence/events/ConpherenceActionMenuEventListener.php @@ -1,45 +1,44 @@ listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS); } public function handleEvent(PhutilEvent $event) { switch ($event->getType()) { case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS: $this->handleActionsEvent($event); break; } } private function handleActionsEvent(PhutilEvent $event) { $object = $event->getValue('object'); $actions = null; if ($object instanceof PhabricatorUser) { $actions = $this->renderUserItems($event); } $this->addActionMenuItems($event, $actions); } private function renderUserItems(PhutilEvent $event) { if (!$this->canUseApplication($event->getUser())) { return null; } $user = $event->getValue('object'); $href = '/conpherence/new/?participant='.$user->getPHID(); return id(new PhabricatorActionView()) ->setIcon('message') ->setName(pht('Send Message')) ->setWorkflow(true) ->setHref($href); } } - diff --git a/src/applications/conpherence/events/ConpherenceHovercardEventListener.php b/src/applications/conpherence/events/ConpherenceHovercardEventListener.php index 6020d6f7e0..616af030ea 100644 --- a/src/applications/conpherence/events/ConpherenceHovercardEventListener.php +++ b/src/applications/conpherence/events/ConpherenceHovercardEventListener.php @@ -1,42 +1,41 @@ listen(PhabricatorEventType::TYPE_UI_DIDRENDERHOVERCARD); } public function handleEvent(PhutilEvent $event) { switch ($event->getType()) { case PhabricatorEventType::TYPE_UI_DIDRENDERHOVERCARD: $this->handleHovercardEvent($event); break; } } private function handleHovercardEvent($event) { $hovercard = $event->getValue('hovercard'); $user = $event->getValue('object'); if (!($user instanceof PhabricatorUser)) { return; } $conpherence_uri = new PhutilURI( '/conpherence/new/?participant='.$user->getPHID()); $name = pht('Send a Message'); $hovercard->addAction($name, $conpherence_uri, true); $event->setValue('hovercard', $hovercard); } } - diff --git a/src/applications/differential/application/PhabricatorApplicationDifferential.php b/src/applications/differential/application/PhabricatorApplicationDifferential.php index 50b0dd3c04..705b2932bb 100644 --- a/src/applications/differential/application/PhabricatorApplicationDifferential.php +++ b/src/applications/differential/application/PhabricatorApplicationDifferential.php @@ -1,135 +1,134 @@ [1-9]\d*)' => 'DifferentialRevisionViewController', '/differential/' => array( '(?:query/(?P[^/]+)/)?' => 'DifferentialRevisionListController', 'diff/' => array( '(?P[1-9]\d*)/' => 'DifferentialDiffViewController', 'create/' => 'DifferentialDiffCreateController', ), 'changeset/' => 'DifferentialChangesetViewController', 'revision/edit/(?:(?P[1-9]\d*)/)?' => 'DifferentialRevisionEditController', 'revision/editpro/(?:(?P[1-9]\d*)/)?' => 'DifferentialRevisionEditControllerPro', 'revision/land/(?:(?P[1-9]\d*))/(?P[^/]+)/' => 'DifferentialRevisionLandController', 'comment/' => array( 'preview/(?P[1-9]\d*)/' => 'DifferentialCommentPreviewController', 'save/' => 'DifferentialCommentSaveController', 'savepro/(?P[1-9]\d*)/' => 'DifferentialCommentSaveControllerPro', 'inline/' => array( 'preview/(?P[1-9]\d*)/' => 'DifferentialInlineCommentPreviewController', 'edit/(?P[1-9]\d*)/' => 'DifferentialInlineCommentEditController', ), ), 'preview/' => 'PhabricatorMarkupPreviewController', ), ); } public function getApplicationGroup() { return self::GROUP_CORE; } public function getApplicationOrder() { return 0.100; } public function getRemarkupRules() { return array( new DifferentialRemarkupRule(), ); } public function loadStatus(PhabricatorUser $user) { $revisions = id(new DifferentialRevisionQuery()) ->setViewer($user) ->withResponsibleUsers(array($user->getPHID())) ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) ->needRelationships(true) ->execute(); list($blocking, $active, $waiting) = DifferentialRevisionQuery::splitResponsible( $revisions, array($user->getPHID())); $status = array(); $blocking = count($blocking); $type = PhabricatorApplicationStatusView::TYPE_NEEDS_ATTENTION; $status[] = id(new PhabricatorApplicationStatusView()) ->setType($type) ->setText(pht('%d Review(s) Blocking Others', $blocking)) ->setCount($blocking); $active = count($active); $type = PhabricatorApplicationStatusView::TYPE_WARNING; $status[] = id(new PhabricatorApplicationStatusView()) ->setType($type) ->setText(pht('%d Review(s) Need Attention', $active)) ->setCount($active); $waiting = count($waiting); $type = PhabricatorApplicationStatusView::TYPE_INFO; $status[] = id(new PhabricatorApplicationStatusView()) ->setType($type) ->setText(pht('%d Review(s) Waiting on Others', $waiting)) ->setCount($waiting); return $status; } protected function getCustomCapabilities() { return array( DifferentialCapabilityDefaultView::CAPABILITY => array( 'caption' => pht( 'Default view policy for newly created revisions.') ), ); } } - diff --git a/src/applications/differential/controller/DifferentialRevisionLandController.php b/src/applications/differential/controller/DifferentialRevisionLandController.php index aecc5644b7..774906671c 100644 --- a/src/applications/differential/controller/DifferentialRevisionLandController.php +++ b/src/applications/differential/controller/DifferentialRevisionLandController.php @@ -1,154 +1,153 @@ revisionID = $data['id']; $this->strategyClass = $data['strategy']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $revision_id = $this->revisionID; $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer($viewer) ->executeOne(); if (!$revision) { return new Aphront404Response(); } if (is_subclass_of($this->strategyClass, 'DifferentialLandingStrategy')) { $this->pushStrategy = newv($this->strategyClass, array()); } else { throw new Exception( "Strategy type must be a valid class name and must subclass ". "DifferentialLandingStrategy. ". "'{$this->strategyClass}' is not a subclass of ". "DifferentialLandingStrategy."); } if ($request->isDialogFormPost()) { $response = null; $text = ''; try { $response = $this->attemptLand($revision, $request); $title = pht("Success!"); $text = pht("Revision was successfully landed."); } catch (Exception $ex) { $title = pht("Failed to land revision"); if ($ex instanceof PhutilProxyException) { $text = hsprintf( '%s:
%s
', $ex->getMessage(), $ex->getPreviousException()->getMessage()); } else { $text = phutil_tag('pre', array(), $ex->getMessage()); } $text = id(new AphrontErrorView()) ->appendChild($text); } if ($response instanceof AphrontDialogView) { $dialog = $response; } else { $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle($title) ->appendChild(phutil_tag('p', array(), $text)) ->addCancelButton('/D'.$revision_id, pht('Done')); } return id(new AphrontDialogResponse())->setDialog($dialog); } $is_disabled = $this->pushStrategy->isActionDisabled( $viewer, $revision, $revision->getRepository()); if ($is_disabled) { if (is_string($is_disabled)) { $explain = $is_disabled; } else { $explain = pht("This action is not currently enabled."); } $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht("Can't land revision")) ->appendChild($explain) ->addCancelButton('/D'.$revision_id); return id(new AphrontDialogResponse())->setDialog($dialog); } $prompt = hsprintf('%s

%s', pht( 'This will squash and rebase revision %s, and push it to '. 'the default / master branch.', $revision_id), pht('It is an experimental feature and may not work.')); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht("Land Revision %s?", $revision_id)) ->appendChild($prompt) ->setSubmitURI($request->getRequestURI()) ->addSubmitButton(pht('Land it!')) ->addCancelButton('/D'.$revision_id); return id(new AphrontDialogResponse())->setDialog($dialog); } private function attemptLand($revision, $request) { $status = $revision->getStatus(); if ($status != ArcanistDifferentialRevisionStatus::ACCEPTED) { throw new Exception("Only Accepted revisions can be landed."); } $repository = $revision->getRepository(); if ($repository === null) { throw new Exception("revision is not attached to a repository."); } $can_push = PhabricatorPolicyFilter::hasCapability( $request->getUser(), $repository, DiffusionCapabilityPush::CAPABILITY); if (!$can_push) { throw new Exception( pht('You do not have permission to push to this repository.')); } $lock = $this->lockRepository($repository); try { $response = $this->pushStrategy->processLandRequest( $request, $revision, $repository); } catch (Exception $e) { $lock->unlock(); throw $e; } $lock->unlock(); return $response; } private function lockRepository($repository) { $lock_name = __CLASS__.':'.($repository->getCallsign()); $lock = PhabricatorGlobalLock::newLock($lock_name); $lock->lock(); return $lock; } } - diff --git a/src/applications/differential/editor/DifferentialRevisionEditor.php b/src/applications/differential/editor/DifferentialRevisionEditor.php index 0ddbbb1720..10411f2346 100644 --- a/src/applications/differential/editor/DifferentialRevisionEditor.php +++ b/src/applications/differential/editor/DifferentialRevisionEditor.php @@ -1,997 +1,996 @@ aphrontRequestForEventDispatch = $request; return $this; } public function getAphrontRequestForEventDispatch() { return $this->aphrontRequestForEventDispatch; } public function __construct(DifferentialRevision $revision) { $this->revision = $revision; $this->isCreate = !($revision->getID()); } public static function newRevisionFromConduitWithDiff( array $fields, DifferentialDiff $diff, PhabricatorUser $actor) { $revision = DifferentialRevision::initializeNewRevision($actor); $revision->setPHID($revision->generatePHID()); $editor = new DifferentialRevisionEditor($revision); $editor->setActor($actor); $editor->addDiff($diff, null); $editor->copyFieldsFromConduit($fields); $editor->save(); return $revision; } public function copyFieldsFromConduit(array $fields) { $actor = $this->getActor(); $revision = $this->revision; $revision->loadRelationships(); $all_fields = DifferentialFieldSelector::newSelector() ->getFieldSpecifications(); $aux_fields = array(); foreach ($all_fields as $aux_field) { $aux_field->setRevision($revision); $aux_field->setDiff($this->diff); $aux_field->setUser($actor); if ($aux_field->shouldAppearOnCommitMessage()) { $aux_fields[$aux_field->getCommitMessageKey()] = $aux_field; } } foreach ($fields as $field => $value) { if (empty($aux_fields[$field])) { throw new Exception( "Parsed commit message contains unrecognized field '{$field}'."); } $aux_fields[$field]->setValueFromParsedCommitMessage($value); } foreach ($aux_fields as $aux_field) { $aux_field->validateField(); } $this->setAuxiliaryFields($all_fields); } public function setAuxiliaryFields(array $auxiliary_fields) { assert_instances_of($auxiliary_fields, 'DifferentialFieldSpecification'); $this->auxiliaryFields = $auxiliary_fields; return $this; } public function getRevision() { return $this->revision; } public function setReviewers(array $reviewers) { $this->reviewers = $reviewers; return $this; } public function setCCPHIDs(array $cc) { $this->cc = $cc; return $this; } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; } public function addDiff(DifferentialDiff $diff, $comments) { if ($diff->getRevisionID() && $diff->getRevisionID() != $this->getRevision()->getID()) { $diff_id = (int)$diff->getID(); $targ_id = (int)$this->getRevision()->getID(); $real_id = (int)$diff->getRevisionID(); throw new Exception( "Can not attach diff #{$diff_id} to Revision D{$targ_id}, it is ". "already attached to D{$real_id}."); } $this->diff = $diff; $this->comments = $comments; $repository = id(new DifferentialRepositoryLookup()) ->setViewer($this->getActor()) ->setDiff($diff) ->lookupRepository(); if ($repository) { $this->getRevision()->setRepositoryPHID($repository->getPHID()); } return $this; } protected function getDiff() { return $this->diff; } protected function getComments() { return $this->comments; } protected function getActorPHID() { return $this->getActor()->getPHID(); } public function isNewRevision() { return !$this->getRevision()->getID(); } /** * A silent update does not trigger Herald rules or send emails. This is used * for auto-amends at commit time. */ public function setSilentUpdate($silent) { $this->silentUpdate = $silent; return $this; } public function save() { $revision = $this->getRevision(); $is_new = $this->isNewRevision(); $revision->loadRelationships(); $this->willWriteRevision(); if ($this->reviewers === null) { $this->reviewers = $revision->getReviewers(); } if ($this->cc === null) { $this->cc = $revision->getCCPHIDs(); } if ($is_new) { $content_blocks = array(); foreach ($this->auxiliaryFields as $field) { if ($field->shouldExtractMentions()) { $content_blocks[] = $field->renderValueForCommitMessage(false); } } $phids = PhabricatorMarkupEngine::extractPHIDsFromMentions( $content_blocks); $this->cc = array_unique(array_merge($this->cc, $phids)); } $diff = $this->getDiff(); if ($diff) { $revision->setLineCount($diff->getLineCount()); } // Save the revision, to generate its ID and PHID if it is new. We need // the ID/PHID in order to record them in Herald transcripts, but don't // want to hold a transaction open while running Herald because it is // potentially somewhat slow. The downside is that we may end up with a // saved revision/diff pair without appropriate CCs. We could be better // about this -- for example: // // - Herald can't affect reviewers, so we could compute them before // opening the transaction and then save them in the transaction. // - Herald doesn't *really* need PHIDs to compute its effects, we could // run it before saving these objects and then hand over the PHIDs later. // // But this should address the problem of orphaned revisions, which is // currently the only problem we experience in practice. $revision->openTransaction(); if ($diff) { $revision->setBranchName($diff->getBranch()); $revision->setArcanistProjectPHID($diff->getArcanistProjectPHID()); } $revision->save(); if ($diff) { $diff->setRevisionID($revision->getID()); $diff->save(); } $revision->saveTransaction(); // We're going to build up three dictionaries: $add, $rem, and $stable. The // $add dictionary has added reviewers/CCs. The $rem dictionary has // reviewers/CCs who have been removed, and the $stable array is // reviewers/CCs who haven't changed. We're going to send new reviewers/CCs // a different ("welcome") email than we send stable reviewers/CCs. $old = array( 'rev' => array_fill_keys($revision->getReviewers(), true), 'ccs' => array_fill_keys($revision->getCCPHIDs(), true), ); $xscript_header = null; $xscript_uri = null; $new = array( 'rev' => array_fill_keys($this->reviewers, true), 'ccs' => array_fill_keys($this->cc, true), ); $rem_ccs = array(); $xscript_phid = null; if ($diff) { $unsubscribed_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $revision->getPHID(), PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER); $adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter( $revision, $diff); $adapter->setExplicitCCs($new['ccs']); $adapter->setExplicitReviewers($new['rev']); $adapter->setForbiddenCCs($unsubscribed_phids); $adapter->setIsNewObject($is_new); $xscript = HeraldEngine::loadAndApplyRules($adapter); $xscript_uri = '/herald/transcript/'.$xscript->getID().'/'; $xscript_phid = $xscript->getPHID(); $xscript_header = $xscript->getXHeraldRulesHeader(); $xscript_header = HeraldTranscript::saveXHeraldRulesHeader( $revision->getPHID(), $xscript_header); $sub = array( 'rev' => $adapter->getReviewersAddedByHerald(), 'ccs' => $adapter->getCCsAddedByHerald(), ); $rem_ccs = $adapter->getCCsRemovedByHerald(); $blocking_reviewers = array_keys( $adapter->getBlockingReviewersAddedByHerald()); HarbormasterBuildable::applyBuildPlans( $diff->getPHID(), $revision->getPHID(), $adapter->getBuildPlans()); } else { $sub = array( 'rev' => array(), 'ccs' => array(), ); $blocking_reviewers = array(); } // Remove any CCs which are prevented by Herald rules. $sub['ccs'] = array_diff_key($sub['ccs'], $rem_ccs); $new['ccs'] = array_diff_key($new['ccs'], $rem_ccs); $add = array(); $rem = array(); $stable = array(); foreach (array('rev', 'ccs') as $key) { $add[$key] = array(); if ($new[$key] !== null) { $add[$key] += array_diff_key($new[$key], $old[$key]); } $add[$key] += array_diff_key($sub[$key], $old[$key]); $combined = $sub[$key]; if ($new[$key] !== null) { $combined += $new[$key]; } $rem[$key] = array_diff_key($old[$key], $combined); $stable[$key] = array_diff_key($old[$key], $add[$key] + $rem[$key]); } // Prevent Herald rules from adding a revision's owner as a reviewer. unset($add['rev'][$revision->getAuthorPHID()]); self::updateReviewers( $revision, $this->getActor(), array_keys($add['rev']), array_keys($rem['rev']), $blocking_reviewers); // We want to attribute new CCs to a "reasonPHID", representing the reason // they were added. This is either a user (if some user explicitly CCs // them, or uses "Add CCs...") or a Herald transcript PHID, indicating that // they were added by a Herald rule. if ($add['ccs'] || $rem['ccs']) { $reasons = array(); foreach ($add['ccs'] as $phid => $ignored) { if (empty($new['ccs'][$phid])) { $reasons[$phid] = $xscript_phid; } else { $reasons[$phid] = $this->getActorPHID(); } } foreach ($rem['ccs'] as $phid => $ignored) { if (empty($new['ccs'][$phid])) { $reasons[$phid] = $this->getActorPHID(); } else { $reasons[$phid] = $xscript_phid; } } } else { $reasons = $this->getActorPHID(); } self::alterCCs( $revision, $this->cc, array_keys($rem['ccs']), array_keys($add['ccs']), $reasons); $this->updateAuxiliaryFields(); // Add the author and users included from Herald rules to the relevant set // of users so they get a copy of the email. if (!$this->silentUpdate) { if ($is_new) { $add['rev'][$this->getActorPHID()] = true; if ($diff) { $add['rev'] += $adapter->getEmailPHIDsAddedByHerald(); } } else { $stable['rev'][$this->getActorPHID()] = true; if ($diff) { $stable['rev'] += $adapter->getEmailPHIDsAddedByHerald(); } } } $mail = array(); $phids = array($this->getActorPHID()); $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getActor()) ->withPHIDs($phids) ->execute(); $actor_handle = $handles[$this->getActorPHID()]; $changesets = null; $old_status = $revision->getStatus(); if ($diff) { $changesets = $diff->loadChangesets(); // TODO: This should probably be in DifferentialFeedbackEditor? if (!$is_new) { $this->createComment(); $mail[] = id(new DifferentialNewDiffMail( $revision, $actor_handle, $changesets)) ->setActor($this->getActor()) ->setIsFirstMailAboutRevision(false) ->setIsFirstMailToRecipients(false) ->setCommentText($this->getComments()) ->setToPHIDs(array_keys($stable['rev'])) ->setCCPHIDs(array_keys($stable['ccs'])); } // Save the changes we made above. $diff->setDescription(preg_replace('/\n.*/s', '', $this->getComments())); $diff->save(); $this->updateAffectedPathTable($revision, $diff, $changesets); $this->updateRevisionHashTable($revision, $diff); // An updated diff should require review, as long as it's not closed // or accepted. The "accepted" status is "sticky" to encourage courtesy // re-diffs after someone accepts with minor changes/suggestions. $status = $revision->getStatus(); if ($status != ArcanistDifferentialRevisionStatus::CLOSED && $status != ArcanistDifferentialRevisionStatus::ACCEPTED) { $revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); } } else { $diff = $revision->loadActiveDiff(); if ($diff) { $changesets = $diff->loadChangesets(); } else { $changesets = array(); } } $revision->save(); // If the actor just deleted all the blocking/rejected reviewers, we may // be able to put the revision into "accepted". switch ($revision->getStatus()) { case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: $revision = self::updateAcceptedStatus( $this->getActor(), $revision); break; } $this->didWriteRevision(); $event_data = array( 'revision_id' => $revision->getID(), 'revision_phid' => $revision->getPHID(), 'revision_name' => $revision->getTitle(), 'revision_author_phid' => $revision->getAuthorPHID(), 'action' => $is_new ? DifferentialAction::ACTION_CREATE : DifferentialAction::ACTION_UPDATE, 'feedback_content' => $is_new ? phutil_utf8_shorten($revision->getSummary(), 140) : $this->getComments(), 'actor_phid' => $revision->getAuthorPHID(), ); $mailed_phids = array(); if (!$this->silentUpdate) { $revision->loadRelationships(); if ($add['rev']) { $message = id(new DifferentialNewDiffMail( $revision, $actor_handle, $changesets)) ->setActor($this->getActor()) ->setIsFirstMailAboutRevision($is_new) ->setIsFirstMailToRecipients(true) ->setToPHIDs(array_keys($add['rev'])); if ($is_new) { // The first time we send an email about a revision, put the CCs in // the "CC:" field of the same "Review Requested" email that reviewers // get, so you don't get two initial emails if you're on a list that // is CC'd. $message->setCCPHIDs(array_keys($add['ccs'])); } $mail[] = $message; } // If we added CCs, we want to send them an email, but only if they were // not already a reviewer and were not added as one (in these cases, they // got a "NewDiff" mail, either in the past or just a moment ago). You can // still get two emails, but only if a revision is updated and you are // added as a reviewer at the same time a list you are on is added as a // CC, which is rare and reasonable. $implied_ccs = self::getImpliedCCs($revision); $implied_ccs = array_fill_keys($implied_ccs, true); $add['ccs'] = array_diff_key($add['ccs'], $implied_ccs); if (!$is_new && $add['ccs']) { $mail[] = id(new DifferentialCCWelcomeMail( $revision, $actor_handle, $changesets)) ->setActor($this->getActor()) ->setIsFirstMailToRecipients(true) ->setToPHIDs(array_keys($add['ccs'])); } foreach ($mail as $message) { $message->setHeraldTranscriptURI($xscript_uri); $message->setXHeraldRulesHeader($xscript_header); $message->send(); $mailed_phids[] = $message->getRawMail()->buildRecipientList(); } $mailed_phids = array_mergev($mailed_phids); } id(new PhabricatorFeedStoryPublisher()) ->setStoryType('PhabricatorFeedStoryDifferential') ->setStoryData($event_data) ->setStoryTime(time()) ->setStoryAuthorPHID($revision->getAuthorPHID()) ->setRelatedPHIDs( array( $revision->getPHID(), $revision->getAuthorPHID(), )) ->setPrimaryObjectPHID($revision->getPHID()) ->setSubscribedPHIDs( array_merge( array($revision->getAuthorPHID()), $revision->getReviewers(), $revision->getCCPHIDs())) ->setMailRecipientPHIDs($mailed_phids) ->publish(); id(new PhabricatorSearchIndexer()) ->queueDocumentForIndexing($revision->getPHID()); } public static function addCC( DifferentialRevision $revision, $phid, $reason) { return self::alterCCs( $revision, $revision->getCCPHIDs(), $rem = array(), $add = array($phid), $reason); } protected static function alterCCs( DifferentialRevision $revision, array $stable_phids, array $rem_phids, array $add_phids, $reason_phid) { $dont_add = self::getImpliedCCs($revision); $add_phids = array_diff($add_phids, $dont_add); id(new PhabricatorSubscriptionsEditor()) ->setActor(PhabricatorUser::getOmnipotentUser()) ->setObject($revision) ->subscribeExplicit($add_phids) ->unsubscribe($rem_phids) ->save(); } private static function getImpliedCCs(DifferentialRevision $revision) { return array_merge( $revision->getReviewers(), array($revision->getAuthorPHID())); } public static function updateReviewers( DifferentialRevision $revision, PhabricatorUser $actor, array $add_phids, array $remove_phids, array $blocking_phids = array()) { $reviewers = $revision->getReviewers(); $editor = id(new PhabricatorEdgeEditor()) ->setActor($actor); $reviewer_phids_map = array_fill_keys($reviewers, true); $blocking_phids = array_fuse($blocking_phids); foreach ($add_phids as $phid) { // Adding an already existing edge again would have cause memory loss // That is, the previous state for that reviewer would be lost if (isset($reviewer_phids_map[$phid])) { // TODO: If we're writing a blocking edge, we should overwrite an // existing weaker edge (like "added" or "commented"), just not a // stronger existing edge. continue; } if (isset($blocking_phids[$phid])) { $status = DifferentialReviewerStatus::STATUS_BLOCKING; } else { $status = DifferentialReviewerStatus::STATUS_ADDED; } $options = array( 'data' => array( 'status' => $status, ) ); $editor->addEdge( $revision->getPHID(), PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER, $phid, $options); } foreach ($remove_phids as $phid) { $editor->removeEdge( $revision->getPHID(), PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER, $phid); } $editor->save(); } public static function updateReviewerStatus( DifferentialRevision $revision, PhabricatorUser $actor, $reviewer_phid, $status) { $options = array( 'data' => array( 'status' => $status ) ); $active_diff = $revision->loadActiveDiff(); if ($active_diff) { $options['data']['diff'] = $active_diff->getID(); } id(new PhabricatorEdgeEditor()) ->setActor($actor) ->addEdge( $revision->getPHID(), PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER, $reviewer_phid, $options) ->save(); } private function createComment() { $template = id(new DifferentialComment()) ->setAuthorPHID($this->getActorPHID()) ->setRevision($this->revision); if ($this->contentSource) { $content_source = $this->contentSource; } else { $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_LEGACY, array()); } $template->setContentSource($content_source); // Write the "update active diff" transaction. id(clone $template) ->setAction(DifferentialAction::ACTION_UPDATE) ->setMetadata( array( DifferentialComment::METADATA_DIFF_ID => $this->getDiff()->getID(), )) ->save(); // If we have a comment, write the "add a comment" transaction. if (strlen($this->getComments())) { id(clone $template) ->setAction(DifferentialAction::ACTION_COMMENT) ->setContent($this->getComments()) ->save(); } } private function updateAuxiliaryFields() { $aux_map = array(); foreach ($this->auxiliaryFields as $aux_field) { $key = $aux_field->getStorageKey(); if ($key !== null) { $val = $aux_field->getValueForStorage(); $aux_map[$key] = $val; } } if (!$aux_map) { return; } $revision = $this->revision; $fields = id(new DifferentialAuxiliaryField())->loadAllWhere( 'revisionPHID = %s AND name IN (%Ls)', $revision->getPHID(), array_keys($aux_map)); $fields = mpull($fields, null, 'getName'); foreach ($aux_map as $key => $val) { $obj = idx($fields, $key); if (!strlen($val)) { // If the new value is empty, just delete the old row if one exists and // don't add a new row if it doesn't. if ($obj) { $obj->delete(); } } else { if (!$obj) { $obj = new DifferentialAuxiliaryField(); $obj->setRevisionPHID($revision->getPHID()); $obj->setName($key); } if ($obj->getValue() !== $val) { $obj->setValue($val); $obj->save(); } } } } private function willWriteRevision() { foreach ($this->auxiliaryFields as $aux_field) { $aux_field->willWriteRevision($this); } $this->dispatchEvent( PhabricatorEventType::TYPE_DIFFERENTIAL_WILLEDITREVISION); } private function didWriteRevision() { foreach ($this->auxiliaryFields as $aux_field) { $aux_field->didWriteRevision($this); } $this->dispatchEvent( PhabricatorEventType::TYPE_DIFFERENTIAL_DIDEDITREVISION); } private function dispatchEvent($type) { $event = new PhabricatorEvent( $type, array( 'revision' => $this->revision, 'new' => $this->isCreate, )); $event->setUser($this->getActor()); $request = $this->getAphrontRequestForEventDispatch(); if ($request) { $event->setAphrontRequest($request); } PhutilEventEngine::dispatchEvent($event); } /** * Update the table which links Differential revisions to paths they affect, * so Diffusion can efficiently find pending revisions for a given file. */ private function updateAffectedPathTable( DifferentialRevision $revision, DifferentialDiff $diff, array $changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); $project = $diff->loadArcanistProject(); if (!$project) { // Probably an old revision from before projects. return; } $repository = $project->loadRepository(); if (!$repository) { // Probably no project <-> repository link, or the repository where the // project lives is untracked. return; } $path_prefix = null; $local_root = $diff->getSourceControlPath(); if ($local_root) { // We're in a working copy which supports subdirectory checkouts (e.g., // SVN) so we need to figure out what prefix we should add to each path // (e.g., trunk/projects/example/) to get the absolute path from the // root of the repository. DVCS systems like Git and Mercurial are not // affected. // Normalize both paths and check if the repository root is a prefix of // the local root. If so, throw it away. Note that this correctly handles // the case where the remote path is "/". $local_root = id(new PhutilURI($local_root))->getPath(); $local_root = rtrim($local_root, '/'); $repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath(); $repo_root = rtrim($repo_root, '/'); if (!strncmp($repo_root, $local_root, strlen($repo_root))) { $path_prefix = substr($local_root, strlen($repo_root)); } } $paths = array(); foreach ($changesets as $changeset) { $paths[] = $path_prefix.'/'.$changeset->getFilename(); } // Mark this as also touching all parent paths, so you can see all pending // changes to any file within a directory. $all_paths = array(); foreach ($paths as $local) { foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) { $all_paths[$path] = true; } } $all_paths = array_keys($all_paths); $path_ids = PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths( $all_paths); $table = new DifferentialAffectedPath(); $conn_w = $table->establishConnection('w'); $sql = array(); foreach ($path_ids as $path_id) { $sql[] = qsprintf( $conn_w, '(%d, %d, %d, %d)', $repository->getID(), $path_id, time(), $revision->getID()); } queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', $table->getTableName(), $revision->getID()); foreach (array_chunk($sql, 256) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q', $table->getTableName(), implode(', ', $chunk)); } } /** * Update the table connecting revisions to DVCS local hashes, so we can * identify revisions by commit/tree hashes. */ private function updateRevisionHashTable( DifferentialRevision $revision, DifferentialDiff $diff) { $vcs = $diff->getSourceControlSystem(); if ($vcs == DifferentialRevisionControlSystem::SVN) { // Subversion has no local commit or tree hash information, so we don't // have to do anything. return; } $property = id(new DifferentialDiffProperty())->loadOneWhere( 'diffID = %d AND name = %s', $diff->getID(), 'local:commits'); if (!$property) { return; } $hashes = array(); $data = $property->getData(); switch ($vcs) { case DifferentialRevisionControlSystem::GIT: foreach ($data as $commit) { $hashes[] = array( ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT, $commit['commit'], ); $hashes[] = array( ArcanistDifferentialRevisionHash::HASH_GIT_TREE, $commit['tree'], ); } break; case DifferentialRevisionControlSystem::MERCURIAL: foreach ($data as $commit) { $hashes[] = array( ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT, $commit['rev'], ); } break; } $conn_w = $revision->establishConnection('w'); $sql = array(); foreach ($hashes as $info) { list($type, $hash) = $info; $sql[] = qsprintf( $conn_w, '(%d, %s, %s)', $revision->getID(), $type, $hash); } queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', ArcanistDifferentialRevisionHash::TABLE_NAME, $revision->getID()); if ($sql) { queryfx( $conn_w, 'INSERT INTO %T (revisionID, type, hash) VALUES %Q', ArcanistDifferentialRevisionHash::TABLE_NAME, implode(', ', $sql)); } } /** * Try to move a revision to "accepted". We look for: * * - at least one accepting reviewer who is a user; and * - no rejects; and * - no blocking reviewers. */ public static function updateAcceptedStatus( PhabricatorUser $viewer, DifferentialRevision $revision) { $revision = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs(array($revision->getID())) ->needRelationships(true) ->needReviewerStatus(true) ->needReviewerAuthority(true) ->executeOne(); $has_user_accept = false; foreach ($revision->getReviewerStatus() as $reviewer) { $status = $reviewer->getStatus(); if ($status == DifferentialReviewerStatus::STATUS_BLOCKING) { // We have a blocking reviewer, so just leave the revision in its // existing state. return $revision; } if ($status == DifferentialReviewerStatus::STATUS_REJECTED) { // We have a rejecting reviewer, so leave the revisoin as is. return $revision; } if ($reviewer->isUser()) { if ($status == DifferentialReviewerStatus::STATUS_ACCEPTED) { $has_user_accept = true; } } } if ($has_user_accept) { $revision ->setStatus(ArcanistDifferentialRevisionStatus::ACCEPTED) ->save(); } return $revision; } } - diff --git a/src/applications/differential/event/DifferentialActionMenuEventListener.php b/src/applications/differential/event/DifferentialActionMenuEventListener.php index a7f42bde4f..6f46c44c04 100644 --- a/src/applications/differential/event/DifferentialActionMenuEventListener.php +++ b/src/applications/differential/event/DifferentialActionMenuEventListener.php @@ -1,70 +1,69 @@ listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS); } public function handleEvent(PhutilEvent $event) { switch ($event->getType()) { case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS: $this->handleActionsEvent($event); break; } } private function handleActionsEvent(PhutilEvent $event) { $object = $event->getValue('object'); $actions = null; if ($object instanceof PhabricatorUser) { $actions = $this->renderUserItems($event); } else if ($object instanceof ManiphestTask) { $actions = $this->renderTaskItems($event); } $this->addActionMenuItems($event, $actions); } private function renderUserItems(PhutilEvent $event) { if (!$this->canUseApplication($event->getUser())) { return null; } $person = $event->getValue('object'); $href = '/differential/?authorPHIDs[]='.$person->getPHID(); return id(new PhabricatorActionView()) ->setRenderAsForm(true) ->setIcon('differential-dark') ->setIconSheet(PHUIIconView::SPRITE_APPS) ->setName(pht('View Revisions')) ->setHref($href); } private function renderTaskItems(PhutilEvent $event) { if (!$this->canUseApplication($event->getUser())) { return null; } $task = $event->getValue('object'); $phid = $task->getPHID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $event->getUser(), $task, PhabricatorPolicyCapability::CAN_EDIT); return id(new PhabricatorActionView()) ->setName(pht('Edit Differential Revisions')) ->setHref("/search/attach/{$phid}/DREV/") ->setWorkflow(true) ->setIcon('attach') ->setDisabled(!$can_edit) ->setWorkflow(true); } } - diff --git a/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php b/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php index e95dc3acbd..9d2a6021aa 100644 --- a/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php +++ b/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php @@ -1,96 +1,95 @@ revision = $revision; $this->exception = $exception; $this->originalBody = $original_body; } protected function renderBody() { // Never called since buildBody() is overridden. } protected function renderSubject() { return "Exception: unable to process your mail request"; } protected function renderVaryPrefix() { return ''; } protected function buildBody() { $exception = $this->exception; $original_body = $this->originalBody; $message = $exception->getMessage(); return <<setOldOffset($oldOffset); $hunk->setOldLen($oldLen); $hunk->setNewOffset($newOffset); $hunk->setNewLen($newLen); $hunk->setChanges($changes); return $hunk; } // Returns a change that consists of a single hunk, starting at line 1. private function createSingleChange($old_lines, $new_lines, $changes) { return array( 0 => $this->createHunk(1, $old_lines, 1, $new_lines, $changes), ); } private function createHunksFromFile($name) { $data = Filesystem::readFile(dirname(__FILE__).'/data/'.$name); $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($data); if (count($changes) !== 1) { throw new Exception("Expected 1 changeset for '{$name}'!"); } $diff = DifferentialDiff::newFromRawChanges($changes); return head($diff->getChangesets())->getHunks(); } public function testOneLineOldComment() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(1, 0, "-a"); $context = $parser->makeContextDiff( $hunks, 0, 1, 0, 0); $this->assertEqual("@@ -1,1 @@\n-a", $context); } public function testOneLineNewComment() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(0, 1, "+a"); $context = $parser->makeContextDiff( $hunks, 1, 1, 0, 0); $this->assertEqual("@@ +1,1 @@\n+a", $context); } public function testCannotFindContext() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(0, 1, "+a"); $context = $parser->makeContextDiff( $hunks, 1, 2, 0, 0); $this->assertEqual("", $context); } public function testOverlapFromStartOfHunk() { $parser = new DifferentialHunkParser(); $hunks = array( 0 => $this->createHunk(23, 2, 42, 2, " 1\n 2"), ); $context = $parser->makeContextDiff( $hunks, 1, 41, 1, 0); $this->assertEqual("@@ -23,1 +42,1 @@\n 1", $context); } public function testOverlapAfterEndOfHunk() { $parser = new DifferentialHunkParser(); $hunks = array( 0 => $this->createHunk(23, 2, 42, 2, " 1\n 2"), ); $context = $parser->makeContextDiff( $hunks, 1, 43, 1, 0); $this->assertEqual("@@ -24,1 +43,1 @@\n 2", $context); } public function testInclusionOfNewFileInOldCommentFromStart() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(2, 3, "+n1\n". " e1/2\n". "-o2\n". "+n3\n"); $context = $parser->makeContextDiff( $hunks, 0, 1, 1, 0); $this->assertEqual( "@@ -1,2 +2,1 @@\n". " e1/2\n". "-o2", $context); } public function testInclusionOfOldFileInNewCommentFromStart() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(2, 2, "-o1\n". " e2/1\n". "-o3\n". "+n2\n"); $context = $parser->makeContextDiff( $hunks, 1, 1, 1, 0); $this->assertEqual( "@@ -2,1 +1,2 @@\n". " e2/1\n". "+n2", $context); } public function testNoNewlineAtEndOfFile() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(0, 1, "+a\n". "\\No newline at end of file"); // Note that this only works with additional context. $context = $parser->makeContextDiff( $hunks, 1, 2, 0, 1); $this->assertEqual( "@@ +1,1 @@\n". "+a\n". "\\No newline at end of file", $context); } public function testMultiLineNewComment() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(7, 7, " e1\n". " e2\n". "-o3\n". "-o4\n". "+n3\n". " e5/4\n". " e6/5\n". "+n6\n". " e7\n"); $context = $parser->makeContextDiff( $hunks, 1, 2, 4, 0); $this->assertEqual( "@@ -2,5 +2,5 @@\n". " e2\n". "-o3\n". "-o4\n". "+n3\n". " e5/4\n". " e6/5\n". "+n6", $context); } public function testMultiLineOldComment() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(7, 7, " e1\n". " e2\n". "-o3\n". "-o4\n". "+n3\n". " e5/4\n". " e6/5\n". "+n6\n". " e7\n"); $context = $parser->makeContextDiff( $hunks, 0, 2, 4, 0); $this->assertEqual( "@@ -2,5 +2,4 @@\n". " e2\n". "-o3\n". "-o4\n". "+n3\n". " e5/4\n". " e6/5", $context); } public function testInclusionOfNewFileInOldCommentFromStartWithContext() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(2, 3, "+n1\n". " e1/2\n". "-o2\n". "+n3\n"); $context = $parser->makeContextDiff( $hunks, 0, 1, 1, 1); $this->assertEqual( "@@ -1,2 +1,2 @@\n". "+n1\n". " e1/2\n". "-o2", $context); } public function testInclusionOfOldFileInNewCommentFromStartWithContext() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(2, 2, "-o1\n". " e2/1\n". "-o3\n". "+n2\n"); $context = $parser->makeContextDiff( $hunks, 1, 1, 1, 1); $this->assertEqual( "@@ -1,3 +1,2 @@\n". "-o1\n". " e2/1\n". "-o3\n". "+n2", $context); } public function testMissingContext() { $tests = array( 'missing_context.diff' => array( 4 => true, ), 'missing_context_2.diff' => array( 5 => true, ), 'missing_context_3.diff' => array( 4 => true, 13 => true, ), ); foreach ($tests as $name => $expect) { $hunks = $this->createHunksFromFile($name); $parser = new DifferentialHunkParser(); $actual = $parser->getHunkStartLines($hunks); $this->assertEqual($expect, $actual, $name); } } } - diff --git a/src/applications/differential/storage/DifferentialCustomFieldNumericIndex.php b/src/applications/differential/storage/DifferentialCustomFieldNumericIndex.php index a9155467de..3939d4e205 100644 --- a/src/applications/differential/storage/DifferentialCustomFieldNumericIndex.php +++ b/src/applications/differential/storage/DifferentialCustomFieldNumericIndex.php @@ -1,11 +1,10 @@ anchorName = $anchor_name; return $this; } public function getAnchorName() { return $this->anchorName; } public function setBaseURI(PhutilURI $base_uri) { $this->baseURI = $base_uri; return $this; } public function getBaseURI() { return $this->baseURI; } public function setTitle($title) { $this->title = $title; return $this; } public function getTitle() { return $this->title; } public function setCollapsed($collapsed) { $this->collapsed = $collapsed; return $this; } public function build(array $changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI($this->getBaseURI()); $nav->setFlexible(true); $nav->setCollapsed($this->collapsed); $anchor = $this->getAnchorName(); $tree = new PhutilFileTree(); foreach ($changesets as $changeset) { try { $tree->addPath($changeset->getFilename(), $changeset); } catch (Exception $ex) { // TODO: See T1702. When viewing the versus diff of diffs, we may // have files with the same filename. For example, if you have a setup // like this in SVN: // // a/ // README // b/ // README // // ...and you run "arc diff" once from a/, and again from b/, you'll // get two diffs with path README. However, in the versus diff view we // will compute their absolute repository paths and detect that they // aren't really the same file. This is correct, but causes us to // throw when inserting them. // // We should probably compute the smallest unique path for each file // and show these as "a/README" and "b/README" when diffed against // one another. However, we get this wrong in a lot of places (the // other TOC shows two "README" files, and we generate the same anchor // hash for both) so I'm just stopping the bleeding until we can get // a proper fix in place. } } require_celerity_resource('phabricator-filetree-view-css'); $filetree = array(); $path = $tree; while (($path = $path->getNextNode())) { $data = $path->getData(); $name = $path->getName(); $style = 'padding-left: '.(2 + (3 * $path->getDepth())).'px'; $href = null; if ($data) { $href = '#'.$data->getAnchorName(); $title = $name; $icon = 'phabricator-filetree-icon-file'; } else { $name .= '/'; $title = $path->getFullPath().'/'; $icon = 'phabricator-filetree-icon-dir'; } $icon = phutil_tag( 'span', array( 'class' => 'phabricator-filetree-icon '.$icon, ), ''); $name_element = phutil_tag( 'span', array( 'class' => 'phabricator-filetree-name', ), $name); $filetree[] = javelin_tag( $href ? 'a' : 'span', array( 'href' => $href, 'style' => $style, 'title' => $title, 'class' => 'phabricator-filetree-item', ), array($icon, $name_element)); } $tree->destroy(); $filetree = phutil_tag( 'div', array( 'class' => 'phabricator-filetree', ), $filetree); Javelin::initBehavior('phabricator-file-tree', array()); $nav->addLabel(pht('Changed Files')); $nav->addCustomBlock($filetree); $nav->setActive(true); $nav->selectFilter(null); return $nav; } } - diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index 403522a056..e8b238053d 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -1,604 +1,603 @@ getHTTPHeader('Content-Type'); $user_agent = idx($_SERVER, 'HTTP_USER_AGENT'); $vcs = null; if ($request->getExists('service')) { $service = $request->getStr('service'); // We get this initially for `info/refs`. // Git also gives us a User-Agent like "git/1.8.2.3". $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; } else if (strncmp($user_agent, "git/", 4) === 0) { $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; } else if ($content_type == 'application/x-git-upload-pack-request') { // We get this for `git-upload-pack`. $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; } else if ($content_type == 'application/x-git-receive-pack-request') { // We get this for `git-receive-pack`. $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; } else if ($request->getExists('cmd')) { // Mercurial also sends an Accept header like // "application/mercurial-0.1", and a User-Agent like // "mercurial/proto-1.0". $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL; } else { // Subversion also sends an initial OPTIONS request (vs GET/POST), and // has a User-Agent like "SVN/1.8.3 (x86_64-apple-darwin11.4.2) // serf/1.3.2". $dav = $request->getHTTPHeader('DAV'); $dav = new PhutilURI($dav); if ($dav->getDomain() === 'subversion.tigris.org') { $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN; } } return $vcs; } private static function getCallsign(AphrontRequest $request) { $uri = $request->getRequestURI(); $regex = '@^/diffusion/(?P[A-Z]+)(/|$)@'; $matches = null; if (!preg_match($regex, (string)$uri, $matches)) { return null; } return $matches['callsign']; } public function processRequest() { $request = $this->getRequest(); $callsign = self::getCallsign($request); // If authentication credentials have been provided, try to find a user // that actually matches those credentials. if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { $username = $_SERVER['PHP_AUTH_USER']; $password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']); $viewer = $this->authenticateHTTPRepositoryUser($username, $password); if (!$viewer) { return new PhabricatorVCSResponse( 403, pht('Invalid credentials.')); } } else { // User hasn't provided credentials, which means we count them as // being "not logged in". $viewer = new PhabricatorUser(); } $allow_public = PhabricatorEnv::getEnvConfig('policy.allow-public'); $allow_auth = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth'); if (!$allow_public) { if (!$viewer->isLoggedIn()) { if ($allow_auth) { return new PhabricatorVCSResponse( 401, pht('You must log in to access repositories.')); } else { return new PhabricatorVCSResponse( 403, pht('Public and authenticated HTTP access are both forbidden.')); } } } try { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withCallsigns(array($callsign)) ->executeOne(); if (!$repository) { return new PhabricatorVCSResponse( 404, pht('No such repository exists.')); } } catch (PhabricatorPolicyException $ex) { if ($viewer->isLoggedIn()) { return new PhabricatorVCSResponse( 403, pht('You do not have permission to access this repository.')); } else { if ($allow_auth) { return new PhabricatorVCSResponse( 401, pht('You must log in to access this repository.')); } else { return new PhabricatorVCSResponse( 403, pht( 'This repository requires authentication, which is forbidden '. 'over HTTP.')); } } } if (!$repository->isTracked()) { return new PhabricatorVCSResponse( 403, pht('This repository is inactive.')); } $is_push = !$this->isReadOnlyRequest($repository); switch ($repository->getServeOverHTTP()) { case PhabricatorRepository::SERVE_READONLY: if ($is_push) { return new PhabricatorVCSResponse( 403, pht('This repository is read-only over HTTP.')); } break; case PhabricatorRepository::SERVE_READWRITE: if ($is_push) { $can_push = PhabricatorPolicyFilter::hasCapability( $viewer, $repository, DiffusionCapabilityPush::CAPABILITY); if (!$can_push) { if ($viewer->isLoggedIn()) { return new PhabricatorVCSResponse( 403, pht('You do not have permission to push to this repository.')); } else { if ($allow_auth) { return new PhabricatorVCSResponse( 401, pht('You must log in to push to this repository.')); } else { return new PhabricatorVCSResponse( 403, pht( 'Pushing to this repository requires authentication, '. 'which is forbidden over HTTP.')); } } } } break; case PhabricatorRepository::SERVE_OFF: default: return new PhabricatorVCSResponse( 403, pht('This repository is not available over HTTP.')); } $vcs_type = $repository->getVersionControlSystem(); $req_type = $this->isVCSRequest($request); if ($vcs_type != $req_type) { switch ($req_type) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $result = new PhabricatorVCSResponse( 500, pht('This is not a Git repository.')); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $result = new PhabricatorVCSResponse( 500, pht('This is not a Mercurial repository.')); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $result = new PhabricatorVCSResponse( 500, pht('This is not a Subversion repository.')); break; default: $result = new PhabricatorVCSResponse( 500, pht('Unknown request type.')); break; } } else { switch ($vcs_type) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $result = $this->serveGitRequest($repository, $viewer); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $result = $this->serveMercurialRequest($repository, $viewer); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $result = new PhabricatorVCSResponse( 500, pht( 'Phabricator does not support HTTP access to Subversion '. 'repositories.')); break; default: $result = new PhabricatorVCSResponse( 500, pht('Unknown version control system.')); break; } } $code = $result->getHTTPResponseCode(); if ($is_push && ($code == 200)) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $repository->writeStatusMessage( PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, PhabricatorRepositoryStatusMessage::CODE_OKAY); unset($unguarded); } return $result; } private function isReadOnlyRequest( PhabricatorRepository $repository) { $request = $this->getRequest(); $method = $_SERVER['REQUEST_METHOD']; // TODO: This implementation is safe by default, but very incomplete. switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $service = $request->getStr('service'); $path = $this->getRequestDirectoryPath($repository); // NOTE: Service names are the reverse of what you might expect, as they // are from the point of view of the server. The main read service is // "git-upload-pack", and the main write service is "git-receive-pack". if ($method == 'GET' && $path == '/info/refs' && $service == 'git-upload-pack') { return true; } if ($path == '/git-upload-pack') { return true; } break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $cmd = $request->getStr('cmd'); if ($cmd == 'batch') { $cmds = idx($this->getMercurialArguments(), 'cmds'); return DiffusionMercurialWireProtocol::isReadOnlyBatchCommand($cmds); } return DiffusionMercurialWireProtocol::isReadOnlyCommand($cmd); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: break; } return false; } /** * @phutil-external-symbol class PhabricatorStartup */ private function serveGitRequest( PhabricatorRepository $repository, PhabricatorUser $viewer) { $request = $this->getRequest(); $request_path = $this->getRequestDirectoryPath($repository); $repository_root = $repository->getLocalPath(); // Rebuild the query string to strip `__magic__` parameters and prevent // issues where we might interpret inputs like "service=read&service=write" // differently than the server does and pass it an unsafe command. // NOTE: This does not use getPassthroughRequestParameters() because // that code is HTTP-method agnostic and will encode POST data. $query_data = $_GET; foreach ($query_data as $key => $value) { if (!strncmp($key, '__', 2)) { unset($query_data[$key]); } } $query_string = http_build_query($query_data, '', '&'); // We're about to wipe out PATH with the rest of the environment, so // resolve the binary first. $bin = Filesystem::resolveBinary('git-http-backend'); if (!$bin) { throw new Exception("Unable to find `git-http-backend` in PATH!"); } $env = array( 'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'], 'QUERY_STRING' => $query_string, 'CONTENT_TYPE' => $request->getHTTPHeader('Content-Type'), 'HTTP_CONTENT_ENCODING' => $request->getHTTPHeader('Content-Encoding'), 'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'], 'GIT_PROJECT_ROOT' => $repository_root, 'GIT_HTTP_EXPORT_ALL' => '1', 'PATH_INFO' => $request_path, 'REMOTE_USER' => $viewer->getUsername(), // TODO: Set these correctly. // GIT_COMMITTER_NAME // GIT_COMMITTER_EMAIL ) + $this->getCommonEnvironment($viewer); $input = PhabricatorStartup::getRawInput(); $command = csprintf('%s', $bin); $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command)) ->setEnv($env, true) ->write($input) ->resolve(); if ($err) { if ($this->isValidGitShallowCloneResponse($stdout, $stderr)) { // Ignore the error if the response passes this special check for // validity. $err = 0; } } if ($err) { return new PhabricatorVCSResponse( 500, pht('Error %d: %s', $err, $stderr)); } return id(new DiffusionGitResponse())->setGitData($stdout); } private function getRequestDirectoryPath(PhabricatorRepository $repository) { $request = $this->getRequest(); $request_path = $request->getRequestURI()->getPath(); $base_path = preg_replace('@^/diffusion/[A-Z]+@', '', $request_path); // For Git repositories, strip an optional directory component if it // isn't the name of a known Git resource. This allows users to clone // repositories as "/diffusion/X/anything.git", for example. if ($repository->isGit()) { $known = array( 'info', 'git-upload-pack', 'git-receive-pack', ); foreach ($known as $key => $path) { $known[$key] = preg_quote($path, '@'); } $known = implode('|', $known); if (preg_match('@^/([^/]+)/('.$known.')(/|$)@', $base_path)) { $base_path = preg_replace('@^/([^/]+)@', '', $base_path); } } return $base_path; } private function authenticateHTTPRepositoryUser( $username, PhutilOpaqueEnvelope $password) { if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) { // No HTTP auth permitted. return null; } if (!strlen($username)) { // No username. return null; } if (!strlen($password->openEnvelope())) { // No password. return null; } $user = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withUsernames(array($username)) ->executeOne(); if (!$user) { // Username doesn't match anything. return null; } if (!$user->isUserActivated()) { // User is not activated. return null; } $password_entry = id(new PhabricatorRepositoryVCSPassword()) ->loadOneWhere('userPHID = %s', $user->getPHID()); if (!$password_entry) { // User doesn't have a password set. return null; } if (!$password_entry->comparePassword($password, $user)) { // Password doesn't match. return null; } // If the user's password is stored using a less-than-optimal hash, upgrade // them to the strongest available hash. $hash_envelope = new PhutilOpaqueEnvelope( $password_entry->getPasswordHash()); if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) { $password_entry->setPassword($password, $user); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $password_entry->save(); unset($unguarded); } return $user; } private function serveMercurialRequest( PhabricatorRepository $repository, PhabricatorUser $viewer) { $request = $this->getRequest(); $bin = Filesystem::resolveBinary('hg'); if (!$bin) { throw new Exception("Unable to find `hg` in PATH!"); } $env = $this->getCommonEnvironment($viewer); $input = PhabricatorStartup::getRawInput(); $cmd = $request->getStr('cmd'); $args = $this->getMercurialArguments(); $args = $this->formatMercurialArguments($cmd, $args); if (strlen($input)) { $input = strlen($input)."\n".$input."0\n"; } $command = csprintf('%s serve --stdio', $bin); $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command)) ->setEnv($env, true) ->setCWD($repository->getLocalPath()) ->write("{$cmd}\n{$args}{$input}") ->resolve(); if ($err) { return new PhabricatorVCSResponse( 500, pht('Error %d: %s', $err, $stderr)); } if ($cmd == 'getbundle' || $cmd == 'changegroup' || $cmd == 'changegroupsubset') { // We're not completely sure that "changegroup" and "changegroupsubset" // actually work, they're for very old Mercurial. $body = gzcompress($stdout); } else if ($cmd == 'unbundle') { // This includes diagnostic information and anything echoed by commit // hooks. We ignore `stdout` since it just has protocol garbage, and // substitute `stderr`. $body = strlen($stderr)."\n".$stderr; } else { list($length, $body) = explode("\n", $stdout, 2); } return id(new DiffusionMercurialResponse())->setContent($body); } private function getMercurialArguments() { // Mercurial sends arguments in HTTP headers. "Why?", you might wonder, // "Why would you do this?". $args_raw = array(); for ($ii = 1; ; $ii++) { $header = 'HTTP_X_HGARG_'.$ii; if (!array_key_exists($header, $_SERVER)) { break; } $args_raw[] = $_SERVER[$header]; } $args_raw = implode('', $args_raw); return id(new PhutilQueryStringParser()) ->parseQueryString($args_raw); } private function formatMercurialArguments($command, array $arguments) { $spec = DiffusionMercurialWireProtocol::getCommandArgs($command); $out = array(); // Mercurial takes normal arguments like this: // // name // value $has_star = false; foreach ($spec as $arg_key) { if ($arg_key == '*') { $has_star = true; continue; } if (isset($arguments[$arg_key])) { $value = $arguments[$arg_key]; $size = strlen($value); $out[] = "{$arg_key} {$size}\n{$value}"; unset($arguments[$arg_key]); } } if ($has_star) { // Mercurial takes arguments for variable argument lists roughly like // this: // // * // argname1 // argvalue1 // argname2 // argvalue2 $count = count($arguments); $out[] = "* {$count}\n"; foreach ($arguments as $key => $value) { if (in_array($key, $spec)) { // We already added this argument above, so skip it. continue; } $size = strlen($value); $out[] = "{$key} {$size}\n{$value}"; } } return implode('', $out); } private function isValidGitShallowCloneResponse($stdout, $stderr) { // If you execute `git clone --depth N ...`, git sends a request which // `git-http-backend` responds to by emitting valid output and then exiting // with a failure code and an error message. If we ignore this error, // everything works. // This is a pretty funky fix: it would be nice to more precisely detect // that a request is a `--depth N` clone request, but we don't have any code // to decode protocol frames yet. Instead, look for reasonable evidence // in the error and output that we're looking at a `--depth` clone. // For evidence this isn't completely crazy, see: // https://github.com/schacon/grack/pull/7 $stdout_regexp = '(^Content-Type: application/x-git-upload-pack-result)m'; $stderr_regexp = '(The remote end hung up unexpectedly)'; $has_pack = preg_match($stdout_regexp, $stdout); $is_hangup = preg_match($stderr_regexp, $stderr); return $has_pack && $is_hangup; } private function getCommonEnvironment(PhabricatorUser $viewer) { $remote_addr = $this->getRequest()->getRemoteAddr(); return array( DiffusionCommitHookEngine::ENV_USER => $viewer->getUsername(), DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS => $remote_addr, DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'http', ); } } - diff --git a/src/applications/diffusion/events/DiffusionHovercardEventListener.php b/src/applications/diffusion/events/DiffusionHovercardEventListener.php index ccba5c8260..ebd91d994c 100644 --- a/src/applications/diffusion/events/DiffusionHovercardEventListener.php +++ b/src/applications/diffusion/events/DiffusionHovercardEventListener.php @@ -1,76 +1,75 @@ listen(PhabricatorEventType::TYPE_UI_DIDRENDERHOVERCARD); } public function handleEvent(PhutilEvent $event) { switch ($event->getType()) { case PhabricatorEventType::TYPE_UI_DIDRENDERHOVERCARD: $this->handleHovercardEvent($event); break; } } private function handleHovercardEvent($event) { $viewer = $event->getUser(); $hovercard = $event->getValue('hovercard'); $object_handle = $event->getValue('handle'); $commit = $event->getValue('object'); if (!($commit instanceof PhabricatorRepositoryCommit)) { return; } $commit_data = $commit->loadCommitData(); $revision = PhabricatorEdgeQuery::loadDestinationPHIDs( $commit->getPHID(), PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV); $revision = reset($revision); $author = $commit->getAuthorPHID(); $phids = array_filter(array( $revision, $author, )); $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs($phids) ->execute(); if ($author) { $author = $handles[$author]->renderLink(); } else { $author = phutil_tag('em', array(), $commit_data->getAuthorName()); } $hovercard->setTitle($object_handle->getName()); $hovercard->setDetail($commit->getSummary()); $hovercard->addField(pht('Author'), $author); $hovercard->addField(pht('Date'), phabricator_date($commit->getEpoch(), $viewer)); if ($commit->getAuditStatus() != PhabricatorAuditCommitStatusConstants::NONE) { $hovercard->addField(pht('Audit Status'), PhabricatorAuditCommitStatusConstants::getStatusName( $commit->getAuditStatus())); } if ($revision) { $rev_handle = $handles[$revision]; $hovercard->addField(pht('Revision'), $rev_handle->renderLink()); } $event->setValue('hovercard', $hovercard); } } - diff --git a/src/applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php b/src/applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php index 00602372a0..b0c6183761 100644 --- a/src/applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php +++ b/src/applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php @@ -1,43 +1,42 @@ assertEqual( '/', DiffusionPathIDQuery::getParentPath('/'), 'Parent of /'); $this->assertEqual( '/', DiffusionPathIDQuery::getParentPath('x.txt'), 'Parent of x.txt'); $this->assertEqual( '/a', DiffusionPathIDQuery::getParentPath('/a/b'), 'Parent of /a/b'); $this->assertEqual( '/a', DiffusionPathIDQuery::getParentPath('/a///b'), 'Parent of /a///b'); } public function testExpandEdgeCases() { $this->assertEqual( array('/'), DiffusionPathIDQuery::expandPathToRoot('/')); $this->assertEqual( array('/'), DiffusionPathIDQuery::expandPathToRoot('//')); $this->assertEqual( array('/a/b', '/a', '/'), DiffusionPathIDQuery::expandPathToRoot('/a/b')); $this->assertEqual( array('/a/b', '/a', '/'), DiffusionPathIDQuery::expandPathToRoot('/a//b')); $this->assertEqual( array('/a/b', '/a', '/'), DiffusionPathIDQuery::expandPathToRoot('a/b')); } } - diff --git a/src/applications/diviner/application/PhabricatorApplicationDiviner.php b/src/applications/diviner/application/PhabricatorApplicationDiviner.php index a0b140ae77..3ff59349f5 100644 --- a/src/applications/diviner/application/PhabricatorApplicationDiviner.php +++ b/src/applications/diviner/application/PhabricatorApplicationDiviner.php @@ -1,75 +1,74 @@ array( '' => 'DivinerLegacyController', 'query/((?[^/]+)/)?' => 'DivinerAtomListController', 'find/' => 'DivinerFindController', ), '/docs/(?P[^/]+)/' => 'DivinerJumpController', '/book/(?P[^/]+)/' => 'DivinerBookController', '/book/'. '(?P[^/]+)/'. '(?P[^/]+)/'. '(?:(?P[^/]+)/)?'. '(?P[^/]+)/'. '(?:(?P\d+)/)?' => 'DivinerAtomController', ); } public function getApplicationGroup() { return self::GROUP_COMMUNICATION; } public function getRemarkupRules() { return array( new DivinerRemarkupRuleSymbol(), ); } public function buildMainMenuItems( PhabricatorUser $user, PhabricatorController $controller = null) { $items = array(); $application = null; if ($controller) { $application = $controller->getCurrentApplication(); } if ($application && $application->getHelpURI()) { $item = id(new PHUIListItemView()) ->setName(pht('%s Help', $application->getName())) ->addClass('core-menu-item') ->setIcon('info-sm') ->setOrder(200) ->setHref($application->getHelpURI()); $items[] = $item; } return $items; } } - diff --git a/src/applications/diviner/atomizer/DivinerPHPAtomizer.php b/src/applications/diviner/atomizer/DivinerPHPAtomizer.php index 0c344aee13..18405b9116 100644 --- a/src/applications/diviner/atomizer/DivinerPHPAtomizer.php +++ b/src/applications/diviner/atomizer/DivinerPHPAtomizer.php @@ -1,319 +1,318 @@ setLanguage('php'); } protected function executeAtomize($file_name, $file_data) { $future = xhpast_get_parser_future($file_data); $tree = XHPASTTree::newFromDataAndResolvedExecFuture( $file_data, $future->resolve()); $atoms = array(); $root = $tree->getRootNode(); $func_decl = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); foreach ($func_decl as $func) { $name = $func->getChildByIndex(2); $atom = $this->newAtom(DivinerAtom::TYPE_FUNCTION) ->setName($name->getConcreteString()) ->setLine($func->getLineNumber()) ->setFile($file_name); $this->findAtomDocblock($atom, $func); $this->parseParams($atom, $func); $this->parseReturnType($atom, $func); $atoms[] = $atom; } $class_types = array( DivinerAtom::TYPE_CLASS => 'n_CLASS_DECLARATION', DivinerAtom::TYPE_INTERFACE => 'n_INTERFACE_DECLARATION', ); foreach ($class_types as $atom_type => $node_type) { $class_decls = $root->selectDescendantsOfType($node_type); foreach ($class_decls as $class) { $name = $class->getChildByIndex(1, 'n_CLASS_NAME'); $atom = $this->newAtom($atom_type) ->setName($name->getConcreteString()) ->setFile($file_name) ->setLine($class->getLineNumber()); // This parses "final" and "abstract". $attributes = $class->getChildByIndex(0, 'n_CLASS_ATTRIBUTES'); foreach ($attributes->selectDescendantsOfType('n_STRING') as $attr) { $atom->setProperty($attr->getConcreteString(), true); } // If this exists, it is n_EXTENDS_LIST. $extends = $class->getChildByIndex(2); $extends_class = $extends->selectDescendantsOfType('n_CLASS_NAME'); foreach ($extends_class as $parent_class) { $atom->addExtends( $this->newRef( DivinerAtom::TYPE_CLASS, $parent_class->getConcreteString())); } // If this exists, it is n_IMPLEMENTS_LIST. $implements = $class->getChildByIndex(3); $iface_names = $implements->selectDescendantsOfType('n_CLASS_NAME'); foreach ($iface_names as $iface_name) { $atom->addExtends( $this->newRef( DivinerAtom::TYPE_INTERFACE, $iface_name->getConcreteString())); } $this->findAtomDocblock($atom, $class); $methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION'); foreach ($methods as $method) { $matom = $this->newAtom(DivinerAtom::TYPE_METHOD); $this->findAtomDocblock($matom, $method); $attribute_list = $method->getChildByIndex(0); $attributes = $attribute_list->selectDescendantsOfType('n_STRING'); if ($attributes) { foreach ($attributes as $attribute) { $attr = strtolower($attribute->getConcreteString()); switch ($attr) { case 'final': case 'abstract': case 'static': $matom->setProperty($attr, true); break; case 'public': case 'protected': case 'private': $matom->setProperty('access', $attr); break; } } } else { $matom->setProperty('access', 'public'); } $this->parseParams($matom, $method); $matom->setName($method->getChildByIndex(2)->getConcreteString()); $matom->setLine($method->getLineNumber()); $matom->setFile($file_name); $this->parseReturnType($matom, $method); $atom->addChild($matom); $atoms[] = $matom; } $atoms[] = $atom; } } return $atoms; } private function parseParams(DivinerAtom $atom, AASTNode $func) { $params = $func ->getChildByIndex(3, 'n_DECLARATAION_PARAMETER_LIST') ->selectDescendantsOfType('n_DECLARATION_PARAMETER'); $param_spec = array(); if ($atom->getDocblockRaw()) { $metadata = $atom->getDocblockMeta(); } else { $metadata = array(); } $docs = idx($metadata, 'param'); if ($docs) { $docs = explode("\n", $docs); $docs = array_filter($docs); } else { $docs = array(); } if (count($docs)) { if (count($docs) < count($params)) { $atom->addWarning( pht( 'This call takes %d parameters, but only %d are documented.', count($params), count($docs))); } } foreach ($params as $param) { $name = $param->getChildByIndex(1)->getConcreteString(); $dict = array( 'type' => $param->getChildByIndex(0)->getConcreteString(), 'default' => $param->getChildByIndex(2)->getConcreteString(), ); if ($docs) { $doc = array_shift($docs); if ($doc) { $dict += $this->parseParamDoc($atom, $doc, $name); } } $param_spec[] = array( 'name' => $name, ) + $dict; } if ($docs) { foreach ($docs as $doc) { if ($doc) { $param_spec[] = $this->parseParamDoc($atom, $doc, null); } } } // TODO: Find `assert_instances_of()` calls in the function body and // add their type information here. See T1089. $atom->setProperty('parameters', $param_spec); } private function findAtomDocblock(DivinerAtom $atom, XHPASTNode $node) { $token = $node->getDocblockToken(); if ($token) { $atom->setDocblockRaw($token->getValue()); return true; } else { $tokens = $node->getTokens(); if ($tokens) { $prev = head($tokens); while ($prev = $prev->getPrevToken()) { if ($prev->isAnyWhitespace()) { continue; } break; } if ($prev && $prev->isComment()) { $value = $prev->getValue(); $matches = null; if (preg_match('/@(return|param|task|author)/', $value, $matches)) { $atom->addWarning( pht( 'Atom "%s" is preceded by a comment containing "@%s", but the '. 'comment is not a documentation comment. Documentation '. 'comments must begin with "/**", followed by a newline. Did '. 'you mean to use a documentation comment? (As the comment is '. 'not a documentation comment, it will be ignored.)', $atom->getName(), $matches[1])); } } } $atom->setDocblockRaw(''); return false; } } protected function parseParamDoc(DivinerAtom $atom, $doc, $name) { $dict = array(); $split = preg_split('/\s+/', trim($doc), $limit = 2); if (!empty($split[0])) { $dict['doctype'] = $split[0]; } if (!empty($split[1])) { $docs = $split[1]; // If the parameter is documented like "@param int $num Blah blah ..", // get rid of the `$num` part (which Diviner considers optional). If it // is present and different from the declared name, raise a warning. $matches = null; if (preg_match('/^(\\$\S+)\s+/', $docs, $matches)) { if ($name !== null) { if ($matches[1] !== $name) { $atom->addWarning( pht( 'Parameter "%s" is named "%s" in the documentation. The '. 'documentation may be out of date.', $name, $matches[1])); } } $docs = substr($docs, strlen($matches[0])); } $dict['docs'] = $docs; } return $dict; } private function parseReturnType(DivinerAtom $atom, XHPASTNode $decl) { $return_spec = array(); $metadata = $atom->getDocblockMeta(); $return = idx($metadata, 'return'); if (!$return) { $return = idx($metadata, 'returns'); if ($return) { $atom->addWarning( pht('Documentation uses `@returns`, but should use `@return`.')); } } if ($atom->getName() == '__construct' && $atom->getType() == 'method') { $return_spec = array( 'doctype' => 'this', 'docs' => '//Implicit.//', ); if ($return) { $atom->addWarning( 'Method __construct() has explicitly documented @return. The '. '__construct() method always returns $this. Diviner documents '. 'this implicitly.'); } } else if ($return) { $split = preg_split('/\s+/', trim($return), $limit = 2); if (!empty($split[0])) { $type = $split[0]; } if ($decl->getChildByIndex(1)->getTypeName() == 'n_REFERENCE') { $type = $type.' &'; } $docs = null; if (!empty($split[1])) { $docs = $split[1]; } $return_spec = array( 'doctype' => $type, 'docs' => $docs, ); } else { $return_spec = array( 'type' => 'wild', ); } $atom->setProperty('return', $return_spec); } } - diff --git a/src/applications/drydock/worker/DrydockAllocatorWorker.php b/src/applications/drydock/worker/DrydockAllocatorWorker.php index 6c2064c587..0e9d99aa97 100644 --- a/src/applications/drydock/worker/DrydockAllocatorWorker.php +++ b/src/applications/drydock/worker/DrydockAllocatorWorker.php @@ -1,183 +1,181 @@ lease)) { $lease = id(new DrydockLeaseQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs(array($this->getTaskData())) ->executeOne(); if (!$lease) { throw new PhabricatorWorkerPermanentFailureException( pht("No such lease %d!", $this->getTaskData())); } $this->lease = $lease; } return $this->lease; } private function logToDrydock($message) { DrydockBlueprintImplementation::writeLog( null, $this->loadLease(), $message); } protected function doWork() { $lease = $this->loadLease(); $this->logToDrydock('Allocating Lease'); try { $this->allocateLease($lease); } catch (Exception $ex) { // TODO: We should really do this when archiving the task, if we've // suffered a permanent failure. But we don't have hooks for that yet // and always fail after the first retry right now, so this is // functionally equivalent. $lease->reload(); if ($lease->getStatus() == DrydockLeaseStatus::STATUS_PENDING) { $lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN); $lease->save(); } throw $ex; } } private function loadAllBlueprints() { $viewer = PhabricatorUser::getOmnipotentUser(); $instances = id(new DrydockBlueprintQuery()) ->setViewer($viewer) ->execute(); $blueprints = array(); foreach ($instances as $instance) { $blueprints[$instance->getPHID()] = $instance; } return $blueprints; } private function allocateLease(DrydockLease $lease) { $type = $lease->getResourceType(); $blueprints = $this->loadAllBlueprints(); // TODO: Policy stuff. $pool = id(new DrydockResource())->loadAllWhere( 'type = %s AND status = %s', $lease->getResourceType(), DrydockResourceStatus::STATUS_OPEN); $this->logToDrydock( pht('Found %d Open Resource(s)', count($pool))); $candidates = array(); foreach ($pool as $key => $candidate) { if (!isset($blueprints[$candidate->getBlueprintPHID()])) { unset($pool[$key]); continue; } $blueprint = $blueprints[$candidate->getBlueprintPHID()]; $implementation = $blueprint->getImplementation(); if ($implementation->filterResource($candidate, $lease)) { $candidates[] = $candidate; } } $this->logToDrydock(pht('%d Open Resource(s) Remain', count($candidates))); $resource = null; if ($candidates) { shuffle($candidates); foreach ($candidates as $candidate_resource) { $blueprint = $blueprints[$candidate_resource->getBlueprintPHID()] ->getImplementation(); if ($blueprint->allocateLease($candidate_resource, $lease)) { $resource = $candidate_resource; break; } } } if (!$resource) { $blueprints = DrydockBlueprintImplementation ::getAllBlueprintImplementationsForResource($type); $this->logToDrydock( pht('Found %d Blueprints', count($blueprints))); foreach ($blueprints as $key => $candidate_blueprint) { if (!$candidate_blueprint->isEnabled()) { unset($blueprints[$key]); continue; } } $this->logToDrydock( pht('%d Blueprints Enabled', count($blueprints))); foreach ($blueprints as $key => $candidate_blueprint) { if (!$candidate_blueprint->canAllocateMoreResources($pool)) { unset($blueprints[$key]); continue; } } $this->logToDrydock( pht('%d Blueprints Can Allocate', count($blueprints))); if (!$blueprints) { $lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN); $lease->save(); $this->logToDrydock( "There are no resources of type '{$type}' available, and no ". "blueprints which can allocate new ones."); return; } // TODO: Rank intelligently. shuffle($blueprints); $blueprint = head($blueprints); $resource = $blueprint->allocateResource($lease); if (!$blueprint->allocateLease($resource, $lease)) { // TODO: This "should" happen only if we lost a race with another lease, // which happened to acquire this resource immediately after we // allocated it. In this case, the right behavior is to retry // immediately. However, other things like a blueprint allocating a // resource it can't actually allocate the lease on might be happening // too, in which case we'd just allocate infinite resources. Probably // what we should do is test for an active or allocated lease and retry // if we find one (although it might have already been released by now) // and fail really hard ("your configuration is a huge broken mess") // otherwise. But just throw for now since this stuff is all edge-casey. // Alternatively we could bring resources up in a "BESPOKE" status // and then switch them to "OPEN" only after the allocating lease gets // its grubby mitts on the resource. This might make more sense but // is a bit messy. throw new Exception("Lost an allocation race?"); } } $blueprint = $resource->getBlueprint(); $blueprint->acquireLease($resource, $lease); } } - - diff --git a/src/applications/feed/application/PhabricatorApplicationFeed.php b/src/applications/feed/application/PhabricatorApplicationFeed.php index 4f2cafc8d4..21c8092e3d 100644 --- a/src/applications/feed/application/PhabricatorApplicationFeed.php +++ b/src/applications/feed/application/PhabricatorApplicationFeed.php @@ -1,36 +1,35 @@ array( 'public/' => 'PhabricatorFeedPublicStreamController', '(?P\d+)/' => 'PhabricatorFeedDetailController', '(?:query/(?P[^/]+)/)?' => 'PhabricatorFeedListController', ), ); } public function getApplicationGroup() { return self::GROUP_COMMUNICATION; } } - diff --git a/src/applications/flag/application/PhabricatorApplicationFlags.php b/src/applications/flag/application/PhabricatorApplicationFlags.php index 51737fed67..d7d16997f2 100644 --- a/src/applications/flag/application/PhabricatorApplicationFlags.php +++ b/src/applications/flag/application/PhabricatorApplicationFlags.php @@ -1,61 +1,60 @@ setViewer($user) ->withOwnerPHIDs(array($user->getPHID())) ->execute(); $count = count($flags); $type = PhabricatorApplicationStatusView::TYPE_NEEDS_ATTENTION; $status[] = id(new PhabricatorApplicationStatusView()) ->setType($type) ->setText(pht('%d Flagged Object(s)', $count)) ->setCount($count); return $status; } public function getRoutes() { return array( '/flag/' => array( '(?:query/(?P[^/]+)/)?' => 'PhabricatorFlagListController', 'view/(?P[^/]+)/' => 'PhabricatorFlagListController', 'edit/(?P[^/]+)/' => 'PhabricatorFlagEditController', 'delete/(?P[1-9]\d*)/' => 'PhabricatorFlagDeleteController', ), ); } } - diff --git a/src/applications/flag/events/PhabricatorFlagsUIEventListener.php b/src/applications/flag/events/PhabricatorFlagsUIEventListener.php index 04314d311c..df7b1d03d7 100644 --- a/src/applications/flag/events/PhabricatorFlagsUIEventListener.php +++ b/src/applications/flag/events/PhabricatorFlagsUIEventListener.php @@ -1,62 +1,61 @@ listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS); } public function handleEvent(PhutilEvent $event) { switch ($event->getType()) { case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS: $this->handleActionEvent($event); break; } } private function handleActionEvent($event) { $user = $event->getUser(); $object = $event->getValue('object'); if (!$object || !$object->getPHID()) { // If we have no object, or the object doesn't have a PHID yet, we can't // flag it. return; } if (!($object instanceof PhabricatorFlaggableInterface)) { return; } if (!$this->canUseApplication($event->getUser())) { return null; } $flag = PhabricatorFlagQuery::loadUserFlag($user, $object->getPHID()); if ($flag) { $color = PhabricatorFlagColor::getColorName($flag->getColor()); $flag_action = id(new PhabricatorActionView()) ->setWorkflow(true) ->setHref('/flag/delete/'.$flag->getID().'/') ->setName(pht('Remove %s Flag', $color)) ->setIcon('flag-'.$flag->getColor()); } else { $flag_action = id(new PhabricatorActionView()) ->setWorkflow(true) ->setHref('/flag/edit/'.$object->getPHID().'/') ->setName(pht('Flag For Later')) ->setIcon('flag'); if (!$user->isLoggedIn()) { $flag_action->setDisabled(true); } } $actions = $event->getValue('actions'); $actions[] = $flag_action; $event->setValue('actions', $actions); } } - diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index 136a0b1cb0..4f5343d445 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -1,1080 +1,1079 @@ contentSource = $content_source; return $this; } public function getContentSource() { return $this->contentSource; } public function getIsNewObject() { if (is_bool($this->isNewObject)) { return $this->isNewObject; } throw new Exception(pht('You must setIsNewObject to a boolean first!')); } public function setIsNewObject($new) { $this->isNewObject = (bool) $new; return $this; } abstract public function getPHID(); abstract public function getHeraldName(); public function getHeraldField($field_name) { switch ($field_name) { case self::FIELD_RULE: return null; case self::FIELD_CONTENT_SOURCE: return $this->getContentSource()->getSource(); case self::FIELD_ALWAYS: return true; case self::FIELD_IS_NEW_OBJECT: return $this->getIsNewObject(); default: throw new Exception( "Unknown field '{$field_name}'!"); } } abstract public function applyHeraldEffects(array $effects); public function isAvailableToUser(PhabricatorUser $viewer) { $applications = id(new PhabricatorApplicationQuery()) ->setViewer($viewer) ->withInstalled(true) ->withClasses(array($this->getAdapterApplicationClass())) ->execute(); return !empty($applications); } /** * NOTE: You generally should not override this; it exists to support legacy * adapters which had hard-coded content types. */ public function getAdapterContentType() { return get_class($this); } abstract public function getAdapterContentName(); abstract public function getAdapterContentDescription(); abstract public function getAdapterApplicationClass(); abstract public function getObject(); public function supportsRuleType($rule_type) { return false; } public function canTriggerOnObject($object) { return false; } public function explainValidTriggerObjects() { return pht('This adapter can not trigger on objects.'); } public function getTriggerObjectPHIDs() { return array($this->getPHID()); } public function getAdapterSortKey() { return sprintf( '%08d%s', $this->getAdapterSortOrder(), $this->getAdapterContentName()); } public function getAdapterSortOrder() { return 1000; } /* -( Fields )------------------------------------------------------------- */ public function getFields() { return array( self::FIELD_ALWAYS, self::FIELD_RULE, ); } public function getFieldNameMap() { return array( self::FIELD_TITLE => pht('Title'), self::FIELD_BODY => pht('Body'), self::FIELD_AUTHOR => pht('Author'), self::FIELD_ASSIGNEE => pht('Assignee'), self::FIELD_COMMITTER => pht('Committer'), self::FIELD_REVIEWER => pht('Reviewer'), self::FIELD_REVIEWERS => pht('Reviewers'), self::FIELD_CC => pht('CCs'), self::FIELD_TAGS => pht('Tags'), self::FIELD_DIFF_FILE => pht('Any changed filename'), self::FIELD_DIFF_CONTENT => pht('Any changed file content'), self::FIELD_DIFF_ADDED_CONTENT => pht('Any added file content'), self::FIELD_DIFF_REMOVED_CONTENT => pht('Any removed file content'), self::FIELD_DIFF_ENORMOUS => pht('Change is enormous'), self::FIELD_REPOSITORY => pht('Repository'), self::FIELD_REPOSITORY_PROJECTS => pht('Repository\'s projects'), self::FIELD_RULE => pht('Another Herald rule'), self::FIELD_AFFECTED_PACKAGE => pht('Any affected package'), self::FIELD_AFFECTED_PACKAGE_OWNER => pht("Any affected package's owner"), self::FIELD_CONTENT_SOURCE => pht('Content Source'), self::FIELD_ALWAYS => pht('Always'), self::FIELD_AUTHOR_PROJECTS => pht("Author's projects"), self::FIELD_PROJECTS => pht("Projects"), self::FIELD_PUSHER => pht('Pusher'), self::FIELD_PUSHER_PROJECTS => pht("Pusher's projects"), self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'), self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'), self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'), self::FIELD_DIFFERENTIAL_ACCEPTED => pht('Accepted Differential revision'), self::FIELD_IS_MERGE_COMMIT => pht('Commit is a merge'), self::FIELD_BRANCHES => pht('Commit\'s branches'), self::FIELD_AUTHOR_RAW => pht('Raw author name'), self::FIELD_COMMITTER_RAW => pht('Raw committer name'), self::FIELD_IS_NEW_OBJECT => pht('Is newly created?'), self::FIELD_TASK_PRIORITY => pht('Task priority'), ); } /* -( Conditions )--------------------------------------------------------- */ public function getConditionNameMap() { return array( self::CONDITION_CONTAINS => pht('contains'), self::CONDITION_NOT_CONTAINS => pht('does not contain'), self::CONDITION_IS => pht('is'), self::CONDITION_IS_NOT => pht('is not'), self::CONDITION_IS_ANY => pht('is any of'), self::CONDITION_IS_TRUE => pht('is true'), self::CONDITION_IS_FALSE => pht('is false'), self::CONDITION_IS_NOT_ANY => pht('is not any of'), self::CONDITION_INCLUDE_ALL => pht('include all of'), self::CONDITION_INCLUDE_ANY => pht('include any of'), self::CONDITION_INCLUDE_NONE => pht('do not include'), self::CONDITION_IS_ME => pht('is myself'), self::CONDITION_IS_NOT_ME => pht('is not myself'), self::CONDITION_REGEXP => pht('matches regexp'), self::CONDITION_RULE => pht('matches:'), self::CONDITION_NOT_RULE => pht('does not match:'), self::CONDITION_EXISTS => pht('exists'), self::CONDITION_NOT_EXISTS => pht('does not exist'), self::CONDITION_UNCONDITIONALLY => '', // don't show anything! self::CONDITION_REGEXP_PAIR => pht('matches regexp pair'), self::CONDITION_HAS_BIT => pht('has bit'), self::CONDITION_NOT_BIT => pht('lacks bit'), ); } public function getConditionsForField($field) { switch ($field) { case self::FIELD_TITLE: case self::FIELD_BODY: case self::FIELD_COMMITTER_RAW: case self::FIELD_AUTHOR_RAW: return array( self::CONDITION_CONTAINS, self::CONDITION_NOT_CONTAINS, self::CONDITION_IS, self::CONDITION_IS_NOT, self::CONDITION_REGEXP, ); case self::FIELD_AUTHOR: case self::FIELD_COMMITTER: case self::FIELD_REVIEWER: case self::FIELD_PUSHER: case self::FIELD_TASK_PRIORITY: return array( self::CONDITION_IS_ANY, self::CONDITION_IS_NOT_ANY, ); case self::FIELD_REPOSITORY: case self::FIELD_ASSIGNEE: return array( self::CONDITION_IS_ANY, self::CONDITION_IS_NOT_ANY, self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, ); case self::FIELD_TAGS: case self::FIELD_REVIEWERS: case self::FIELD_CC: case self::FIELD_AUTHOR_PROJECTS: case self::FIELD_PROJECTS: case self::FIELD_AFFECTED_PACKAGE: case self::FIELD_AFFECTED_PACKAGE_OWNER: case self::FIELD_PUSHER_PROJECTS: case self::FIELD_REPOSITORY_PROJECTS: return array( self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, ); case self::FIELD_DIFF_FILE: case self::FIELD_BRANCHES: return array( self::CONDITION_CONTAINS, self::CONDITION_REGEXP, ); case self::FIELD_DIFF_CONTENT: case self::FIELD_DIFF_ADDED_CONTENT: case self::FIELD_DIFF_REMOVED_CONTENT: return array( self::CONDITION_CONTAINS, self::CONDITION_REGEXP, self::CONDITION_REGEXP_PAIR, ); case self::FIELD_RULE: return array( self::CONDITION_RULE, self::CONDITION_NOT_RULE, ); case self::FIELD_CONTENT_SOURCE: return array( self::CONDITION_IS, self::CONDITION_IS_NOT, ); case self::FIELD_ALWAYS: return array( self::CONDITION_UNCONDITIONALLY, ); case self::FIELD_DIFFERENTIAL_REVIEWERS: return array( self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, ); case self::FIELD_DIFFERENTIAL_CCS: return array( self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, ); case self::FIELD_DIFFERENTIAL_REVISION: case self::FIELD_DIFFERENTIAL_ACCEPTED: return array( self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, ); case self::FIELD_IS_MERGE_COMMIT: case self::FIELD_DIFF_ENORMOUS: case self::FIELD_IS_NEW_OBJECT: return array( self::CONDITION_IS_TRUE, self::CONDITION_IS_FALSE, ); default: throw new Exception( "This adapter does not define conditions for field '{$field}'!"); } } public function doesConditionMatch( HeraldEngine $engine, HeraldRule $rule, HeraldCondition $condition, $field_value) { $condition_type = $condition->getFieldCondition(); $condition_value = $condition->getValue(); switch ($condition_type) { case self::CONDITION_CONTAINS: // "Contains" can take an array of strings, as in "Any changed // filename" for diffs. foreach ((array)$field_value as $value) { if (stripos($value, $condition_value) !== false) { return true; } } return false; case self::CONDITION_NOT_CONTAINS: return (stripos($field_value, $condition_value) === false); case self::CONDITION_IS: return ($field_value == $condition_value); case self::CONDITION_IS_NOT: return ($field_value != $condition_value); case self::CONDITION_IS_ME: return ($field_value == $rule->getAuthorPHID()); case self::CONDITION_IS_NOT_ME: return ($field_value != $rule->getAuthorPHID()); case self::CONDITION_IS_ANY: if (!is_array($condition_value)) { throw new HeraldInvalidConditionException( "Expected condition value to be an array."); } $condition_value = array_fuse($condition_value); return isset($condition_value[$field_value]); case self::CONDITION_IS_NOT_ANY: if (!is_array($condition_value)) { throw new HeraldInvalidConditionException( "Expected condition value to be an array."); } $condition_value = array_fuse($condition_value); return !isset($condition_value[$field_value]); case self::CONDITION_INCLUDE_ALL: if (!is_array($field_value)) { throw new HeraldInvalidConditionException( "Object produced non-array value!"); } if (!is_array($condition_value)) { throw new HeraldInvalidConditionException( "Expected condition value to be an array."); } $have = array_select_keys(array_fuse($field_value), $condition_value); return (count($have) == count($condition_value)); case self::CONDITION_INCLUDE_ANY: return (bool)array_select_keys( array_fuse($field_value), $condition_value); case self::CONDITION_INCLUDE_NONE: return !array_select_keys( array_fuse($field_value), $condition_value); case self::CONDITION_EXISTS: case self::CONDITION_IS_TRUE: return (bool)$field_value; case self::CONDITION_NOT_EXISTS: case self::CONDITION_IS_FALSE: return !$field_value; case self::CONDITION_UNCONDITIONALLY: return (bool)$field_value; case self::CONDITION_REGEXP: foreach ((array)$field_value as $value) { // We add the 'S' flag because we use the regexp multiple times. // It shouldn't cause any troubles if the flag is already there // - /.*/S is evaluated same as /.*/SS. $result = @preg_match($condition_value . 'S', $value); if ($result === false) { throw new HeraldInvalidConditionException( "Regular expression is not valid!"); } if ($result) { return true; } } return false; case self::CONDITION_REGEXP_PAIR: // Match a JSON-encoded pair of regular expressions against a // dictionary. The first regexp must match the dictionary key, and the // second regexp must match the dictionary value. If any key/value pair // in the dictionary matches both regexps, the condition is satisfied. $regexp_pair = json_decode($condition_value, true); if (!is_array($regexp_pair)) { throw new HeraldInvalidConditionException( "Regular expression pair is not valid JSON!"); } if (count($regexp_pair) != 2) { throw new HeraldInvalidConditionException( "Regular expression pair is not a pair!"); } $key_regexp = array_shift($regexp_pair); $value_regexp = array_shift($regexp_pair); foreach ((array)$field_value as $key => $value) { $key_matches = @preg_match($key_regexp, $key); if ($key_matches === false) { throw new HeraldInvalidConditionException( "First regular expression is invalid!"); } if ($key_matches) { $value_matches = @preg_match($value_regexp, $value); if ($value_matches === false) { throw new HeraldInvalidConditionException( "Second regular expression is invalid!"); } if ($value_matches) { return true; } } } return false; case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: $rule = $engine->getRule($condition_value); if (!$rule) { throw new HeraldInvalidConditionException( "Condition references a rule which does not exist!"); } $is_not = ($condition_type == self::CONDITION_NOT_RULE); $result = $engine->doesRuleMatch($rule, $this); if ($is_not) { $result = !$result; } return $result; case self::CONDITION_HAS_BIT: return (($condition_value & $field_value) === $condition_value); case self::CONDITION_NOT_BIT: return (($condition_value & $field_value) !== $condition_value); default: throw new HeraldInvalidConditionException( "Unknown condition '{$condition_type}'."); } } public function willSaveCondition(HeraldCondition $condition) { $condition_type = $condition->getFieldCondition(); $condition_value = $condition->getValue(); switch ($condition_type) { case self::CONDITION_REGEXP: $ok = @preg_match($condition_value, ''); if ($ok === false) { throw new HeraldInvalidConditionException( pht( 'The regular expression "%s" is not valid. Regular expressions '. 'must have enclosing characters (e.g. "@/path/to/file@", not '. '"/path/to/file") and be syntactically correct.', $condition_value)); } break; case self::CONDITION_REGEXP_PAIR: $json = json_decode($condition_value, true); if (!is_array($json)) { throw new HeraldInvalidConditionException( pht( 'The regular expression pair "%s" is not valid JSON. Enter a '. 'valid JSON array with two elements.', $condition_value)); } if (count($json) != 2) { throw new HeraldInvalidConditionException( pht( 'The regular expression pair "%s" must have exactly two '. 'elements.', $condition_value)); } $key_regexp = array_shift($json); $val_regexp = array_shift($json); $key_ok = @preg_match($key_regexp, ''); if ($key_ok === false) { throw new HeraldInvalidConditionException( pht( 'The first regexp in the regexp pair, "%s", is not a valid '. 'regexp.', $key_regexp)); } $val_ok = @preg_match($val_regexp, ''); if ($val_ok === false) { throw new HeraldInvalidConditionException( pht( 'The second regexp in the regexp pair, "%s", is not a valid '. 'regexp.', $val_regexp)); } break; case self::CONDITION_CONTAINS: case self::CONDITION_NOT_CONTAINS: case self::CONDITION_IS: case self::CONDITION_IS_NOT: case self::CONDITION_IS_ANY: case self::CONDITION_IS_NOT_ANY: case self::CONDITION_INCLUDE_ALL: case self::CONDITION_INCLUDE_ANY: case self::CONDITION_INCLUDE_NONE: case self::CONDITION_IS_ME: case self::CONDITION_IS_NOT_ME: case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: case self::CONDITION_EXISTS: case self::CONDITION_NOT_EXISTS: case self::CONDITION_UNCONDITIONALLY: case self::CONDITION_HAS_BIT: case self::CONDITION_NOT_BIT: case self::CONDITION_IS_TRUE: case self::CONDITION_IS_FALSE: // No explicit validation for these types, although there probably // should be in some cases. break; default: throw new HeraldInvalidConditionException( pht( 'Unknown condition "%s"!', $condition_type)); } } /* -( Actions )------------------------------------------------------------ */ abstract public function getActions($rule_type); public function getActionNameMap($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: return array( self::ACTION_NOTHING => pht('Do nothing'), self::ACTION_ADD_CC => pht('Add emails to CC'), self::ACTION_REMOVE_CC => pht('Remove emails from CC'), self::ACTION_EMAIL => pht('Send an email to'), self::ACTION_AUDIT => pht('Trigger an Audit by'), self::ACTION_FLAG => pht('Mark with flag'), self::ACTION_ASSIGN_TASK => pht('Assign task to'), self::ACTION_ADD_PROJECTS => pht('Add projects'), self::ACTION_ADD_REVIEWERS => pht('Add reviewers'), self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'), self::ACTION_APPLY_BUILD_PLANS => pht('Run build plans'), self::ACTION_BLOCK => pht('Block change with message'), ); case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return array( self::ACTION_NOTHING => pht('Do nothing'), self::ACTION_ADD_CC => pht('Add me to CC'), self::ACTION_REMOVE_CC => pht('Remove me from CC'), self::ACTION_EMAIL => pht('Send me an email'), self::ACTION_AUDIT => pht('Trigger an Audit by me'), self::ACTION_FLAG => pht('Mark with flag'), self::ACTION_ASSIGN_TASK => pht('Assign task to me'), self::ACTION_ADD_PROJECTS => pht('Add projects'), self::ACTION_ADD_REVIEWERS => pht('Add me as a reviewer'), self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add me as a blocking reviewer'), ); default: throw new Exception("Unknown rule type '{$rule_type}'!"); } } public function willSaveAction( HeraldRule $rule, HeraldAction $action) { $target = $action->getTarget(); if (is_array($target)) { $target = array_keys($target); } $author_phid = $rule->getAuthorPHID(); $rule_type = $rule->getRuleType(); if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) { switch ($action->getAction()) { case self::ACTION_EMAIL: case self::ACTION_ADD_CC: case self::ACTION_REMOVE_CC: case self::ACTION_AUDIT: case self::ACTION_ASSIGN_TASK: case self::ACTION_ADD_REVIEWERS: case self::ACTION_ADD_BLOCKING_REVIEWERS: // For personal rules, force these actions to target the rule owner. $target = array($author_phid); break; case self::ACTION_FLAG: // Make sure flag color is valid; set to blue if not. $color_map = PhabricatorFlagColor::getColorNameMap(); if (empty($color_map[$target])) { $target = PhabricatorFlagColor::COLOR_BLUE; } break; case self::ACTION_BLOCK: case self::ACTION_NOTHING: break; default: throw new HeraldInvalidActionException( pht( 'Unrecognized action type "%s"!', $action->getAction())); } } $action->setTarget($target); } /* -( Values )------------------------------------------------------------- */ public function getValueTypeForFieldAndCondition($field, $condition) { switch ($condition) { case self::CONDITION_CONTAINS: case self::CONDITION_NOT_CONTAINS: case self::CONDITION_REGEXP: case self::CONDITION_REGEXP_PAIR: return self::VALUE_TEXT; case self::CONDITION_IS: case self::CONDITION_IS_NOT: switch ($field) { case self::FIELD_CONTENT_SOURCE: return self::VALUE_CONTENT_SOURCE; default: return self::VALUE_TEXT; } break; case self::CONDITION_IS_ANY: case self::CONDITION_IS_NOT_ANY: switch ($field) { case self::FIELD_REPOSITORY: return self::VALUE_REPOSITORY; case self::FIELD_TASK_PRIORITY: return self::VALUE_TASK_PRIORITY; default: return self::VALUE_USER; } break; case self::CONDITION_INCLUDE_ALL: case self::CONDITION_INCLUDE_ANY: case self::CONDITION_INCLUDE_NONE: switch ($field) { case self::FIELD_REPOSITORY: return self::VALUE_REPOSITORY; case self::FIELD_CC: return self::VALUE_EMAIL; case self::FIELD_TAGS: return self::VALUE_TAG; case self::FIELD_AFFECTED_PACKAGE: return self::VALUE_OWNERS_PACKAGE; case self::FIELD_AUTHOR_PROJECTS: case self::FIELD_PUSHER_PROJECTS: case self::FIELD_PROJECTS: case self::FIELD_REPOSITORY_PROJECTS: return self::VALUE_PROJECT; case self::FIELD_REVIEWERS: return self::VALUE_USER_OR_PROJECT; default: return self::VALUE_USER; } break; case self::CONDITION_IS_ME: case self::CONDITION_IS_NOT_ME: case self::CONDITION_EXISTS: case self::CONDITION_NOT_EXISTS: case self::CONDITION_UNCONDITIONALLY: case self::CONDITION_IS_TRUE: case self::CONDITION_IS_FALSE: return self::VALUE_NONE; case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: return self::VALUE_RULE; default: throw new Exception("Unknown condition '{$condition}'."); } } public static function getValueTypeForAction($action, $rule_type) { $is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); if ($is_personal) { switch ($action) { case self::ACTION_ADD_CC: case self::ACTION_REMOVE_CC: case self::ACTION_EMAIL: case self::ACTION_NOTHING: case self::ACTION_AUDIT: case self::ACTION_ASSIGN_TASK: case self::ACTION_ADD_REVIEWERS: case self::ACTION_ADD_BLOCKING_REVIEWERS: return self::VALUE_NONE; case self::ACTION_FLAG: return self::VALUE_FLAG_COLOR; case self::ACTION_ADD_PROJECTS: return self::VALUE_PROJECT; default: throw new Exception("Unknown or invalid action '{$action}'."); } } else { switch ($action) { case self::ACTION_ADD_CC: case self::ACTION_REMOVE_CC: case self::ACTION_EMAIL: return self::VALUE_EMAIL; case self::ACTION_NOTHING: return self::VALUE_NONE; case self::ACTION_ADD_PROJECTS: return self::VALUE_PROJECT; case self::ACTION_FLAG: return self::VALUE_FLAG_COLOR; case self::ACTION_ASSIGN_TASK: return self::VALUE_USER; case self::ACTION_AUDIT: case self::ACTION_ADD_REVIEWERS: case self::ACTION_ADD_BLOCKING_REVIEWERS: return self::VALUE_USER_OR_PROJECT; case self::ACTION_APPLY_BUILD_PLANS: return self::VALUE_BUILD_PLAN; case self::ACTION_BLOCK: return self::VALUE_TEXT; default: throw new Exception("Unknown or invalid action '{$action}'."); } } } /* -( Repetition )--------------------------------------------------------- */ public function getRepetitionOptions() { return array( HeraldRepetitionPolicyConfig::EVERY, ); } public static function applyFlagEffect(HeraldEffect $effect, $phid) { $color = $effect->getTarget(); // TODO: Silly that we need to load this again here. $rule = id(new HeraldRule())->load($effect->getRuleID()); $user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $rule->getAuthorPHID()); $flag = PhabricatorFlagQuery::loadUserFlag($user, $phid); if ($flag) { return new HeraldApplyTranscript( $effect, false, pht('Object already flagged.')); } $handle = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs(array($phid)) ->executeOne(); $flag = new PhabricatorFlag(); $flag->setOwnerPHID($user->getPHID()); $flag->setType($handle->getType()); $flag->setObjectPHID($handle->getPHID()); // TOOD: Should really be transcript PHID, but it doesn't exist yet. $flag->setReasonPHID($user->getPHID()); $flag->setColor($color); $flag->setNote( pht('Flagged by Herald Rule "%s".', $rule->getName())); $flag->save(); return new HeraldApplyTranscript( $effect, true, pht('Added flag.')); } public static function getAllAdapters() { static $adapters; if (!$adapters) { $adapters = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); $adapters = msort($adapters, 'getAdapterSortKey'); } return $adapters; } public static function getAdapterForContentType($content_type) { $adapters = self::getAllAdapters(); foreach ($adapters as $adapter) { if ($adapter->getAdapterContentType() == $content_type) { return $adapter; } } throw new Exception( pht( 'No adapter exists for Herald content type "%s".', $content_type)); } public static function getEnabledAdapterMap(PhabricatorUser $viewer) { $map = array(); $adapters = HeraldAdapter::getAllAdapters(); foreach ($adapters as $adapter) { if (!$adapter->isAvailableToUser($viewer)) { continue; } $type = $adapter->getAdapterContentType(); $name = $adapter->getAdapterContentName(); $map[$type] = $name; } return $map; } public function renderRuleAsText(HeraldRule $rule, array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $out = array(); if ($rule->getMustMatchAll()) { $out[] = pht('When all of these conditions are met:'); } else { $out[] = pht('When any of these conditions are met:'); } $out[] = null; foreach ($rule->getConditions() as $condition) { $out[] = $this->renderConditionAsText($condition, $handles); } $out[] = null; $integer_code_for_every = HeraldRepetitionPolicyConfig::toInt( HeraldRepetitionPolicyConfig::EVERY); if ($rule->getRepetitionPolicy() == $integer_code_for_every) { $out[] = pht('Take these actions every time this rule matches:'); } else { $out[] = pht('Take these actions the first time this rule matches:'); } $out[] = null; foreach ($rule->getActions() as $action) { $out[] = $this->renderActionAsText($action, $handles); } return phutil_implode_html("\n", $out); } private function renderConditionAsText( HeraldCondition $condition, array $handles) { $field_type = $condition->getFieldName(); $field_name = idx($this->getFieldNameMap(), $field_type); $condition_type = $condition->getFieldCondition(); $condition_name = idx($this->getConditionNameMap(), $condition_type); $value = $this->renderConditionValueAsText($condition, $handles); return hsprintf(' %s %s %s', $field_name, $condition_name, $value); } private function renderActionAsText( HeraldAction $action, array $handles) { $rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL; $action_type = $action->getAction(); $action_name = idx($this->getActionNameMap($rule_global), $action_type); $target = $this->renderActionTargetAsText($action, $handles); return hsprintf(' %s %s', $action_name, $target); } private function renderConditionValueAsText( HeraldCondition $condition, array $handles) { $value = $condition->getValue(); if (!is_array($value)) { $value = array($value); } switch ($condition->getFieldName()) { case self::FIELD_TASK_PRIORITY: $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); foreach ($value as $index => $val) { $name = idx($priority_map, $val); if ($name) { $value[$index] = $name; } } break; default: foreach ($value as $index => $val) { $handle = idx($handles, $val); if ($handle) { $value[$index] = $handle->renderLink(); } } break; } $value = phutil_implode_html(', ', $value); return $value; } private function renderActionTargetAsText( HeraldAction $action, array $handles) { $target = $action->getTarget(); if (!is_array($target)) { $target = array($target); } foreach ($target as $index => $val) { switch ($action->getAction()) { case self::ACTION_FLAG: $target[$index] = PhabricatorFlagColor::getColorName($val); break; default: $handle = idx($handles, $val); if ($handle) { $target[$index] = $handle->renderLink(); } break; } } $target = phutil_implode_html(', ', $target); return $target; } /** * Given a @{class:HeraldRule}, this function extracts all the phids that * we'll want to load as handles later. * * This function performs a somewhat hacky approach to figuring out what * is and is not a phid - try to get the phid type and if the type is * *not* unknown assume its a valid phid. * * Don't try this at home. Use more strongly typed data at home. * * Think of the children. */ public static function getHandlePHIDs(HeraldRule $rule) { $phids = array($rule->getAuthorPHID()); foreach ($rule->getConditions() as $condition) { $value = $condition->getValue(); if (!is_array($value)) { $value = array($value); } foreach ($value as $val) { if (phid_get_type($val) != PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { $phids[] = $val; } } } foreach ($rule->getActions() as $action) { $target = $action->getTarget(); if (!is_array($target)) { $target = array($target); } foreach ($target as $val) { if (phid_get_type($val) != PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { $phids[] = $val; } } } if ($rule->isObjectRule()) { $phids[] = $rule->getTriggerObjectPHID(); } return $phids; } } - diff --git a/src/applications/herald/engine/HeraldEffect.php b/src/applications/herald/engine/HeraldEffect.php index 1d4955c7ed..9863ae3f39 100644 --- a/src/applications/herald/engine/HeraldEffect.php +++ b/src/applications/herald/engine/HeraldEffect.php @@ -1,79 +1,78 @@ objectPHID = $object_phid; return $this; } public function getObjectPHID() { return $this->objectPHID; } public function setAction($action) { $this->action = $action; return $this; } public function getAction() { return $this->action; } public function setTarget($target) { $this->target = $target; return $this; } public function getTarget() { return $this->target; } public function setRuleID($rule_id) { $this->ruleID = $rule_id; return $this; } public function getRuleID() { return $this->ruleID; } public function setRulePHID($rule_phid) { $this->rulePHID = $rule_phid; return $this; } public function getRulePHID() { return $this->rulePHID; } public function setEffector($effector) { $this->effector = $effector; return $this; } public function getEffector() { return $this->effector; } public function setReason($reason) { $this->reason = $reason; return $this; } public function getReason() { return $this->reason; } } - diff --git a/src/applications/herald/storage/HeraldRuleTransactionComment.php b/src/applications/herald/storage/HeraldRuleTransactionComment.php index 5db8952403..56022ef863 100644 --- a/src/applications/herald/storage/HeraldRuleTransactionComment.php +++ b/src/applications/herald/storage/HeraldRuleTransactionComment.php @@ -1,11 +1,10 @@ "; $long_array = array( 'a' => $long_string, 'b' => $long_string, ); $mixed_array = array( 'a' => 'abc', 'b' => 'def', 'c' => $long_string, ); $fields = array( 'ls' => $long_string, 'la' => $long_array, 'ma' => $mixed_array, ); $truncated_fields = id(new HeraldObjectTranscript()) ->setFields($fields) ->getFields(); $this->assertEqual($short_string, $truncated_fields['ls']); $this->assertEqual( array('a', '<...>'), array_keys($truncated_fields['la'])); $this->assertEqual( $short_string.'!<...>', implode('!', $truncated_fields['la'])); $this->assertEqual( array('a', 'b', 'c'), array_keys($truncated_fields['ma'])); $this->assertEqual( 'abc!def!'.substr($short_string, 6), implode('!', $truncated_fields['ma'])); } -} \ No newline at end of file +} diff --git a/src/applications/macro/storage/PhabricatorFileImageMacro.php b/src/applications/macro/storage/PhabricatorFileImageMacro.php index b8cdbf6a8f..578e0c3513 100644 --- a/src/applications/macro/storage/PhabricatorFileImageMacro.php +++ b/src/applications/macro/storage/PhabricatorFileImageMacro.php @@ -1,113 +1,112 @@ file = $file; return $this; } public function getFile() { return $this->assertAttached($this->file); } public function attachAudio(PhabricatorFile $audio = null) { $this->audio = $audio; return $this; } public function getAudio() { return $this->assertAttached($this->audio); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorMacroPHIDTypeMacro::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorMacroEditor(); } public function getApplicationTransactionObject() { return new PhabricatorMacroTransaction(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } - diff --git a/src/applications/macro/storage/PhabricatorMacroTransaction.php b/src/applications/macro/storage/PhabricatorMacroTransaction.php index bcac44ae19..c29cb92265 100644 --- a/src/applications/macro/storage/PhabricatorMacroTransaction.php +++ b/src/applications/macro/storage/PhabricatorMacroTransaction.php @@ -1,304 +1,303 @@ getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorMacroTransactionType::TYPE_FILE: case PhabricatorMacroTransactionType::TYPE_AUDIO: if ($old !== null) { $phids[] = $old; } $phids[] = $new; break; } return $phids; } public function shouldHide() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorMacroTransactionType::TYPE_NAME: return ($old === null); } return parent::shouldHide(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorMacroTransactionType::TYPE_NAME: return pht( '%s renamed this macro from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); break; case PhabricatorMacroTransactionType::TYPE_DISABLED: if ($new) { return pht( '%s disabled this macro.', $this->renderHandleLink($author_phid)); } else { return pht( '%s restored this macro.', $this->renderHandleLink($author_phid)); } break; case PhabricatorMacroTransactionType::TYPE_AUDIO: if (!$old) { return pht( '%s attached audio: %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($new)); } else { return pht( '%s changed the audio for this macro from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($old), $this->renderHandleLink($new)); } case PhabricatorMacroTransactionType::TYPE_AUDIO_BEHAVIOR: switch ($new) { case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_ONCE: return pht( '%s set the audio to play once.', $this->renderHandleLink($author_phid)); case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_LOOP: return pht( '%s set the audio to loop.', $this->renderHandleLink($author_phid)); default: return pht( '%s disabled the audio for this macro.', $this->renderHandleLink($author_phid)); } case PhabricatorMacroTransactionType::TYPE_FILE: if ($old === null) { return pht( '%s created this macro.', $this->renderHandleLink($author_phid)); } else { return pht( '%s changed the image for this macro from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($old), $this->renderHandleLink($new)); } break; } return parent::getTitle(); } public function getTitleForFeed(PhabricatorFeedStory $story) { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorMacroTransactionType::TYPE_NAME: return pht( '%s renamed %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old, $new); case PhabricatorMacroTransactionType::TYPE_DISABLED: if ($new) { return pht( '%s disabled %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s restored %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } case PhabricatorMacroTransactionType::TYPE_FILE: if ($old === null) { return pht( '%s created %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s updated the image for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } case PhabricatorMacroTransactionType::TYPE_AUDIO: if (!$old) { return pht( '%s attached audio to %s: %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $this->renderHandleLink($new)); } else { return pht( '%s changed the audio for %s from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $this->renderHandleLink($old), $this->renderHandleLink($new)); } case PhabricatorMacroTransactionType::TYPE_AUDIO_BEHAVIOR: switch ($new) { case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_ONCE: return pht( '%s set the audio for %s to play once.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_LOOP: return pht( '%s set the audio for %s to loop.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); default: return pht( '%s disabled the audio for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } } return parent::getTitleForFeed($story); } public function getActionName() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorMacroTransactionType::TYPE_NAME: if ($old === null) { return pht('Created'); } else { return pht('Renamed'); } case PhabricatorMacroTransactionType::TYPE_DISABLED: if ($new) { return pht('Disabled'); } else { return pht('Restored'); } case PhabricatorMacroTransactionType::TYPE_FILE: if ($old === null) { return pht('Created'); } else { return pht('Edited Image'); } case PhabricatorMacroTransactionType::TYPE_AUDIO: return pht('Audio'); case PhabricatorMacroTransactionType::TYPE_AUDIO_BEHAVIOR: return pht('Audio Behavior'); } return parent::getActionName(); } public function getActionStrength() { switch ($this->getTransactionType()) { case PhabricatorMacroTransactionType::TYPE_DISABLED: return 2.0; case PhabricatorMacroTransactionType::TYPE_FILE: return 1.5; } return parent::getActionStrength(); } public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorMacroTransactionType::TYPE_NAME: return 'edit'; case PhabricatorMacroTransactionType::TYPE_FILE: if ($old === null) { return 'create'; } else { return 'edit'; } case PhabricatorMacroTransactionType::TYPE_DISABLED: if ($new) { return 'delete'; } else { return 'undo'; } case PhabricatorMacroTransactionType::TYPE_AUDIO: return 'herald'; } return parent::getIcon(); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorMacroTransactionType::TYPE_NAME: return PhabricatorTransactions::COLOR_BLUE; case PhabricatorMacroTransactionType::TYPE_FILE: if ($old === null) { return PhabricatorTransactions::COLOR_GREEN; } else { return PhabricatorTransactions::COLOR_BLUE; } case PhabricatorMacroTransactionType::TYPE_DISABLED: if ($new) { return PhabricatorTransactions::COLOR_BLACK; } else { return PhabricatorTransactions::COLOR_SKY; } } return parent::getColor(); } } - diff --git a/src/applications/macro/storage/PhabricatorMacroTransactionComment.php b/src/applications/macro/storage/PhabricatorMacroTransactionComment.php index 565f5040ee..1963a554a0 100644 --- a/src/applications/macro/storage/PhabricatorMacroTransactionComment.php +++ b/src/applications/macro/storage/PhabricatorMacroTransactionComment.php @@ -1,11 +1,10 @@ [1-9]\d*)' => 'ManiphestTaskDetailController', '/maniphest/' => array( '(?:query/(?P[^/]+)/)?' => 'ManiphestTaskListController', 'report/(?:(?P\w+)/)?' => 'ManiphestReportController', 'batch/' => 'ManiphestBatchEditController', 'task/' => array( 'create/' => 'ManiphestTaskEditController', 'edit/(?P[1-9]\d*)/' => 'ManiphestTaskEditController', 'descriptionpreview/' => 'PhabricatorMarkupPreviewController', ), 'transaction/' => array( 'save/' => 'ManiphestTransactionSaveController', 'preview/(?P[1-9]\d*)/' => 'ManiphestTransactionPreviewController', ), 'export/(?P[^/]+)/' => 'ManiphestExportController', 'subpriority/' => 'ManiphestSubpriorityController', 'subscribe/(?Padd|rem)/(?P[1-9]\d*)/' => 'ManiphestSubscribeController', ), ); } public function loadStatus(PhabricatorUser $user) { $status = array(); $query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->withOwners(array($user->getPHID())); $count = count($query->execute()); $type = PhabricatorApplicationStatusView::TYPE_WARNING; $status[] = id(new PhabricatorApplicationStatusView()) ->setType($type) ->setText(pht('%s Assigned Task(s)', new PhutilNumber($count))) ->setCount($count); return $status; } public function getQuickCreateItems(PhabricatorUser $viewer) { $items = array(); $item = id(new PHUIListItemView()) ->setName(pht('Maniphest Task')) ->setAppIcon('maniphest-dark') ->setHref($this->getBaseURI().'task/create/'); $items[] = $item; return $items; } protected function getCustomCapabilities() { return array( ManiphestCapabilityDefaultView::CAPABILITY => array( 'caption' => pht( 'Default view policy for newly created tasks.'), ), ManiphestCapabilityDefaultEdit::CAPABILITY => array( 'caption' => pht( 'Default edit policy for newly created tasks.'), ), ManiphestCapabilityEditStatus::CAPABILITY => array( ), ManiphestCapabilityEditAssign::CAPABILITY => array( ), ManiphestCapabilityEditPolicies::CAPABILITY => array( ), ManiphestCapabilityEditPriority::CAPABILITY => array( ), ManiphestCapabilityEditProjects::CAPABILITY => array( ), ManiphestCapabilityBulkEdit::CAPABILITY => array( ), ); } } - diff --git a/src/applications/maniphest/storage/ManiphestCustomFieldNumericIndex.php b/src/applications/maniphest/storage/ManiphestCustomFieldNumericIndex.php index 640552c63c..79d7a3a8e6 100644 --- a/src/applications/maniphest/storage/ManiphestCustomFieldNumericIndex.php +++ b/src/applications/maniphest/storage/ManiphestCustomFieldNumericIndex.php @@ -1,11 +1,10 @@ getNewValue(); $old = $this->getOldValue(); switch ($this->getTransactionType()) { case self::TYPE_OWNER: if ($new) { $phids[] = $new; } if ($old) { $phids[] = $old; } break; case self::TYPE_CCS: case self::TYPE_PROJECTS: $phids = array_mergev( array( $phids, nonempty($old, array()), nonempty($new, array()), )); break; case self::TYPE_EDGE: $phids = array_mergev( array( $phids, array_keys(nonempty($old, array())), array_keys(nonempty($new, array())), )); break; case self::TYPE_ATTACH: $old = nonempty($old, array()); $new = nonempty($new, array()); $phids = array_mergev( array( $phids, array_keys(idx($new, 'FILE', array())), array_keys(idx($old, 'FILE', array())), )); break; } return $phids; } public function shouldHide() { switch ($this->getTransactionType()) { case self::TYPE_TITLE: case self::TYPE_DESCRIPTION: case self::TYPE_PRIORITY: if ($this->getOldValue() === null) { return true; } else { return false; } break; } return false; } public function getActionStrength() { switch ($this->getTransactionType()) { case self::TYPE_STATUS: return 1.3; case self::TYPE_OWNER: return 1.2; case self::TYPE_PRIORITY: return 1.1; } return parent::getActionStrength(); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_OWNER: if ($this->getAuthorPHID() == $new) { return 'green'; } else if (!$new) { return 'black'; } else if (!$old) { return 'green'; } else { return 'green'; } case self::TYPE_STATUS: if ($new == ManiphestTaskStatus::STATUS_OPEN) { return 'green'; } else { return 'black'; } case self::TYPE_PRIORITY: if ($old == ManiphestTaskPriority::getDefaultPriority()) { return 'green'; } else if ($old > $new) { return 'grey'; } else { return 'yellow'; } } return parent::getColor(); } public function getActionName() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: return pht('Retitled'); case self::TYPE_STATUS: switch ($new) { case ManiphestTaskStatus::STATUS_OPEN: if ($old === null) { return pht('Created'); } else { return pht('Reopened'); } case ManiphestTaskStatus::STATUS_CLOSED_SPITE: return pht('Spited'); case ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE: return pht('Merged'); default: return pht('Closed'); } case self::TYPE_DESCRIPTION: return pht('Edited'); case self::TYPE_OWNER: if ($this->getAuthorPHID() == $new) { return pht('Claimed'); } else if (!$new) { return pht('Up For Grabs'); } else if (!$old) { return pht('Assigned'); } else { return pht('Reassigned'); } case self::TYPE_CCS: return pht('Changed CC'); case self::TYPE_PROJECTS: return pht('Changed Projects'); case self::TYPE_PRIORITY: if ($old == ManiphestTaskPriority::getDefaultPriority()) { return pht('Triaged'); } else if ($old > $new) { return pht('Lowered Priority'); } else { return pht('Raised Priority'); } case self::TYPE_EDGE: case self::TYPE_ATTACH: return pht('Attached'); } return parent::getActionName(); } public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_OWNER: return 'user'; case self::TYPE_CCS: return 'meta-mta'; case self::TYPE_TITLE: return 'edit'; case self::TYPE_STATUS: switch ($new) { case ManiphestTaskStatus::STATUS_OPEN: return 'create'; case ManiphestTaskStatus::STATUS_CLOSED_SPITE: return 'dislike'; case ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE: return 'delete'; default: return 'check'; } case self::TYPE_DESCRIPTION: return 'edit'; case self::TYPE_PROJECTS: return 'project'; case self::TYPE_PRIORITY: if ($old == ManiphestTaskPriority::getDefaultPriority()) { return 'normal-priority'; return pht('Triaged'); } else if ($old > $new) { return 'lower-priority'; } else { return 'raise-priority'; } case self::TYPE_EDGE: case self::TYPE_ATTACH: return 'attach'; } return parent::getIcon(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: return pht( '%s changed the title from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); case self::TYPE_DESCRIPTION: return pht( '%s edited the task description.', $this->renderHandleLink($author_phid)); case self::TYPE_STATUS: switch ($new) { case ManiphestTaskStatus::STATUS_OPEN: if ($old === null) { return pht( '%s created this task.', $this->renderHandleLink($author_phid)); } else { return pht( '%s reopened this task.', $this->renderHandleLink($author_phid)); } case ManiphestTaskStatus::STATUS_CLOSED_SPITE: return pht( '%s closed this task out of spite.', $this->renderHandleLink($author_phid)); case ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE: return pht( '%s closed this task as a duplicate.', $this->renderHandleLink($author_phid)); default: $status_name = idx( ManiphestTaskStatus::getTaskStatusMap(), $new, '???'); return pht( '%s closed this task as "%s".', $this->renderHandleLink($author_phid), $status_name); } case self::TYPE_OWNER: if ($author_phid == $new) { return pht( '%s claimed this task.', $this->renderHandleLink($author_phid)); } else if (!$new) { return pht( '%s placed this task up for grabs.', $this->renderHandleLink($author_phid)); } else if (!$old) { return pht( '%s assigned this task to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($new)); } else { return pht( '%s reassigned this task from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($old), $this->renderHandleLink($new)); } case self::TYPE_PROJECTS: $added = array_diff($new, $old); $removed = array_diff($old, $new); if ($added && !$removed) { return pht( '%s added %d project(s): %s', $this->renderHandleLink($author_phid), count($added), $this->renderHandleList($added)); } else if ($removed && !$added) { return pht( '%s removed %d project(s): %s', $this->renderHandleLink($author_phid), count($removed), $this->renderHandleList($removed)); } else if ($removed && $added) { return pht( '%s changed project(s), added %d: %s; removed %d: %s', $this->renderHandleLink($author_phid), count($added), $this->renderHandleList($added), count($removed), $this->renderHandleList($removed)); } else { // This is hit when rendering previews. return pht( '%s changed projects...', $this->renderHandleLink($author_phid)); } case self::TYPE_PRIORITY: $old_name = ManiphestTaskPriority::getTaskPriorityName($old); $new_name = ManiphestTaskPriority::getTaskPriorityName($new); if ($old == ManiphestTaskPriority::getDefaultPriority()) { return pht( '%s triaged this task as "%s" priority.', $this->renderHandleLink($author_phid), $new_name); } else if ($old > $new) { return pht( '%s lowered the priority of this task from "%s" to "%s".', $this->renderHandleLink($author_phid), $old_name, $new_name); } else { return pht( '%s raised the priority of this task from "%s" to "%s".', $this->renderHandleLink($author_phid), $old_name, $new_name); } case self::TYPE_CCS: // TODO: Remove this when we switch to subscribers. Just reuse the // code in the parent. $clone = clone $this; $clone->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS); return $clone->getTitle(); case self::TYPE_EDGE: // TODO: Remove this when we switch to real edges. Just reuse the // code in the parent; $clone = clone $this; $clone->setTransactionType(PhabricatorTransactions::TYPE_EDGE); return $clone->getTitle(); case self::TYPE_ATTACH: $old = nonempty($old, array()); $new = nonempty($new, array()); $new = array_keys(idx($new, 'FILE', array())); $old = array_keys(idx($old, 'FILE', array())); $added = array_diff($new, $old); $removed = array_diff($old, $new); if ($added && !$removed) { return pht( '%s attached %d file(s): %s', $this->renderHandleLink($author_phid), count($added), $this->renderHandleList($added)); } else if ($removed && !$added) { return pht( '%s detached %d file(s): %s', $this->renderHandleLink($author_phid), count($removed), $this->renderHandleList($removed)); } else { return pht( '%s changed file(s), attached %d: %s; detached %d: %s', $this->renderHandleLink($author_phid), count($added), $this->renderHandleList($added), count($removed), $this->renderHandleList($removed)); } } return parent::getTitle(); } public function getTitleForFeed(PhabricatorFeedStory $story) { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: return pht( '%s renamed %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old, $new); case self::TYPE_DESCRIPTION: return pht( '%s edited the description of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case self::TYPE_STATUS: switch ($new) { case ManiphestTaskStatus::STATUS_OPEN: if ($old === null) { return pht( '%s created %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s reopened %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } case ManiphestTaskStatus::STATUS_CLOSED_SPITE: return pht( '%s closed %s out of spite.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE: return pht( '%s closed %s as a duplicate.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); default: $status_name = idx( ManiphestTaskStatus::getTaskStatusMap(), $new, '???'); return pht( '%s closed %s as "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $status_name); } case self::TYPE_OWNER: if ($author_phid == $new) { return pht( '%s claimed %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else if (!$new) { return pht( '%s placed %s up for grabs.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else if (!$old) { return pht( '%s assigned %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $this->renderHandleLink($new)); } else { return pht( '%s reassigned %s from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $this->renderHandleLink($old), $this->renderHandleLink($new)); } case self::TYPE_PROJECTS: $added = array_diff($new, $old); $removed = array_diff($old, $new); if ($added && !$removed) { return pht( '%s added %d project(s) to %s: %s', $this->renderHandleLink($author_phid), count($added), $this->renderHandleLink($object_phid), $this->renderHandleList($added)); } else if ($removed && !$added) { return pht( '%s removed %d project(s) from %s: %s', $this->renderHandleLink($author_phid), count($removed), $this->renderHandleLink($object_phid), $this->renderHandleList($removed)); } else if ($removed && $added) { return pht( '%s changed project(s) of %s, added %d: %s; removed %d: %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), count($added), $this->renderHandleList($added), count($removed), $this->renderHandleList($removed)); } case self::TYPE_PRIORITY: $old_name = ManiphestTaskPriority::getTaskPriorityName($old); $new_name = ManiphestTaskPriority::getTaskPriorityName($new); if ($old == ManiphestTaskPriority::getDefaultPriority()) { return pht( '%s triaged %s as "%s" priority.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $new_name); } else if ($old > $new) { return pht( '%s lowered the priority of %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old_name, $new_name); } else { return pht( '%s raised the priority of %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old_name, $new_name); } case self::TYPE_CCS: // TODO: Remove this when we switch to subscribers. Just reuse the // code in the parent. $clone = clone $this; $clone->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS); return $clone->getTitleForFeed($story); case self::TYPE_EDGE: // TODO: Remove this when we switch to real edges. Just reuse the // code in the parent; $clone = clone $this; $clone->setTransactionType(PhabricatorTransactions::TYPE_EDGE); return $clone->getTitleForFeed($story); case self::TYPE_ATTACH: $old = nonempty($old, array()); $new = nonempty($new, array()); $new = array_keys(idx($new, 'FILE', array())); $old = array_keys(idx($old, 'FILE', array())); $added = array_diff($new, $old); $removed = array_diff($old, $new); if ($added && !$removed) { return pht( '%s attached %d file(s) of %s: %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), count($added), $this->renderHandleList($added)); } else if ($removed && !$added) { return pht( '%s detached %d file(s) of %s: %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), count($removed), $this->renderHandleList($removed)); } else { return pht( '%s changed file(s) for %s, attached %d: %s; detached %d: %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), count($added), $this->renderHandleList($added), count($removed), $this->renderHandleList($removed)); } } return parent::getTitleForFeed($story); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: return true; } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { case self::TYPE_STATUS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_STATUS; break; case self::TYPE_OWNER: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OWNER; break; case self::TYPE_CCS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_CC; break; case self::TYPE_PROJECTS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PROJECTS; break; case self::TYPE_PRIORITY: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PRIORITY; break; case PhabricatorTransactions::TYPE_COMMENT: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_COMMENT; break; default: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OTHER; break; } return $tags; } } - diff --git a/src/applications/meta/application/PhabricatorApplicationApplications.php b/src/applications/meta/application/PhabricatorApplicationApplications.php index 765e5e0b0d..66c611406c 100644 --- a/src/applications/meta/application/PhabricatorApplicationApplications.php +++ b/src/applications/meta/application/PhabricatorApplicationApplications.php @@ -1,46 +1,45 @@ array( '(?:query/(?P[^/]+)/)?' => 'PhabricatorApplicationsListController', 'view/(?P\w+)/' => 'PhabricatorApplicationDetailViewController', 'edit/(?P\w+)/' => 'PhabricatorApplicationEditController', '(?P\w+)/(?Pinstall|uninstall)/' => 'PhabricatorApplicationUninstallController', ), ); } } - diff --git a/src/applications/meta/controller/PhabricatorApplicationUninstallController.php b/src/applications/meta/controller/PhabricatorApplicationUninstallController.php index 7c480ad81e..8445526a04 100644 --- a/src/applications/meta/controller/PhabricatorApplicationUninstallController.php +++ b/src/applications/meta/controller/PhabricatorApplicationUninstallController.php @@ -1,93 +1,92 @@ application = $data['application']; $this->action = $data['action']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $selected = PhabricatorApplication::getByClass($this->application); if (!$selected) { return new Aphront404Response(); } $view_uri = $this->getApplicationURI('view/'.$this->application); $beta_enabled = PhabricatorEnv::getEnvConfig( 'phabricator.show-beta-applications'); $dialog = id(new AphrontDialogView()) ->setUser($user) ->addCancelButton($view_uri); if ($selected->isBeta() && !$beta_enabled) { $dialog ->setTitle(pht('Beta Applications Not Enabled')) ->appendChild( pht( 'To manage beta applications, enable them by setting %s in your '. 'Phabricator configuration.', phutil_tag('tt', array(), 'phabricator.show-beta-applications'))); return id(new AphrontDialogResponse())->setDialog($dialog); } if ($request->isDialogFormPost()) { $this->manageApplication(); return id(new AphrontRedirectResponse())->setURI($view_uri); } if ($this->action == 'install') { if ($selected->canUninstall()) { $dialog->setTitle('Confirmation') ->appendChild( 'Install '. $selected->getName(). ' application?') ->addSubmitButton('Install'); } else { $dialog->setTitle('Information') ->appendChild('You cannot install an installed application.'); } } else { if ($selected->canUninstall()) { $dialog->setTitle('Confirmation') ->appendChild( 'Really Uninstall '. $selected->getName(). ' application?') ->addSubmitButton('Uninstall'); } else { $dialog->setTitle('Information') ->appendChild( 'This application cannot be uninstalled, because it is required for Phabricator to work.'); } } return id(new AphrontDialogResponse())->setDialog($dialog); } public function manageApplication() { $key = 'phabricator.uninstalled-applications'; $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); $list = $config_entry->getValue(); $uninstalled = PhabricatorEnv::getEnvConfig($key); if (isset($uninstalled[$this->application])) { unset($list[$this->application]); } else { $list[$this->application] = true; } PhabricatorConfigEditor::storeNewValue( $config_entry, $list, $this->getRequest()); } } - diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php index 7e41491ace..44c8acd2f5 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php @@ -1,158 +1,157 @@ params['from'] = $email; $this->params['from-name'] = $name; return $this; } public function addReplyTo($email, $name = '') { if (empty($this->params['reply-to'])) { $this->params['reply-to'] = array(); } $this->params['reply-to'][] = array( 'email' => $email, 'name' => $name, ); return $this; } public function addTos(array $emails) { foreach ($emails as $email) { $this->params['tos'][] = $email; } return $this; } public function addCCs(array $emails) { foreach ($emails as $email) { $this->params['ccs'][] = $email; } return $this; } public function addAttachment($data, $filename, $mimetype) { if (empty($this->params['files'])) { $this->params['files'] = array(); } $this->params['files'][$filename] = $data; } public function addHeader($header_name, $header_value) { $this->params['headers'][] = array($header_name, $header_value); return $this; } public function setBody($body) { $this->params['body'] = $body; return $this; } public function setSubject($subject) { $this->params['subject'] = $subject; return $this; } public function setIsHTML($is_html) { $this->params['is-html'] = $is_html; return $this; } public function supportsMessageIDHeader() { return false; } public function send() { $user = PhabricatorEnv::getEnvConfig('sendgrid.api-user'); $key = PhabricatorEnv::getEnvConfig('sendgrid.api-key'); if (!$user || !$key) { throw new Exception( "Configure 'sendgrid.api-user' and 'sendgrid.api-key' to use ". "SendGrid for mail delivery."); } $params = array(); $ii = 0; foreach (idx($this->params, 'tos', array()) as $to) { $params['to['.($ii++).']'] = $to; } $params['subject'] = idx($this->params, 'subject'); if (idx($this->params, 'is-html')) { $params['html'] = idx($this->params, 'body'); } else { $params['text'] = idx($this->params, 'body'); } $params['from'] = idx($this->params, 'from'); if (idx($this->params, 'from-name')) { $params['fromname'] = $this->params['from-name']; } if (idx($this->params, 'reply-to')) { $replyto = $this->params['reply-to']; // Pick off the email part, no support for the name part in this API. $params['replyto'] = $replyto[0]['email']; } foreach (idx($this->params, 'files', array()) as $name => $data) { $params['files['.$name.']'] = $data; } $headers = idx($this->params, 'headers', array()); // See SendGrid Support Ticket #29390; there's no explicit REST API support // for CC right now but it works if you add a generic "Cc" header. // // SendGrid said this is supported: // "You can use CC as you are trying to do there [by adding a generic // header]. It is supported despite our limited documentation to this // effect, I am glad you were able to figure it out regardless. ..." if (idx($this->params, 'ccs')) { $headers[] = array('Cc', implode(', ', $this->params['ccs'])); } if ($headers) { // Convert to dictionary. $headers = ipull($headers, 1, 0); $headers = json_encode($headers); $params['headers'] = $headers; } $params['api_user'] = $user; $params['api_key'] = $key; $future = new HTTPSFuture( 'https://sendgrid.com/api/mail.send.json', $params); $future->setMethod('POST'); list($body) = $future->resolvex(); $response = json_decode($body, true); if (!is_array($response)) { throw new Exception("Failed to JSON decode response: {$body}"); } if ($response['message'] !== 'success') { $errors = implode(";", $response['errors']); throw new Exception("Request failed with errors: {$errors}."); } return true; } } - diff --git a/src/applications/nuance/application/PhabricatorApplicationNuance.php b/src/applications/nuance/application/PhabricatorApplicationNuance.php index b2d123ee2e..ac962b1982 100644 --- a/src/applications/nuance/application/PhabricatorApplicationNuance.php +++ b/src/applications/nuance/application/PhabricatorApplicationNuance.php @@ -1,73 +1,72 @@ array( 'item/' => array( 'view/(?P[1-9]\d*)/' => 'NuanceItemViewController', 'edit/(?P[1-9]\d*)/' => 'NuanceItemEditController', 'new/' => 'NuanceItemEditController', ), 'source/' => array( 'view/(?P[1-9]\d*)/' => 'NuanceSourceViewController', 'edit/(?P[1-9]\d*)/' => 'NuanceSourceEditController', 'new/' => 'NuanceSourceEditController', ), 'queue/' => array( 'view/(?P[1-9]\d*)/' => 'NuanceQueueViewController', 'edit/(?P[1-9]\d*)/' => 'NuanceQueueEditController', 'new/' => 'NuanceQueueEditController', ), 'requestor/' => array( 'view/(?P[1-9]\d*)/' => 'NuanceRequestorViewController', 'edit/(?P[1-9]\d*)/' => 'NuanceRequestorEditController', 'new/' => 'NuanceRequestorEditController', ), ), ); } protected function getCustomCapabilities() { return array( NuanceCapabilitySourceDefaultView::CAPABILITY => array( 'caption' => pht( 'Default view policy for newly created sources.'), ), NuanceCapabilitySourceDefaultEdit::CAPABILITY => array( 'caption' => pht( 'Default edit policy for newly created sources.'), ), NuanceCapabilitySourceManage::CAPABILITY => array( ), ); } } - diff --git a/src/applications/nuance/query/NuanceItemQuery.php b/src/applications/nuance/query/NuanceItemQuery.php index 18a96b9f79..d37c2176f8 100644 --- a/src/applications/nuance/query/NuanceItemQuery.php +++ b/src/applications/nuance/query/NuanceItemQuery.php @@ -1,71 +1,70 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withSourceIDs($source_ids) { $this->sourceIDs = $source_ids; return $this; } public function loadPage() { $table = new NuanceItem(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function buildWhereClause($conn_r) { $where = array(); $where[] = $this->buildPagingClause($conn_r); if ($this->sourceID) { $where[] = qsprintf( $conn_r, 'sourceID IN (%Ld)', $this->sourceIDs); } if ($this->ids) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } return $this->formatWhereClause($where); } } - diff --git a/src/applications/nuance/query/NuanceQuery.php b/src/applications/nuance/query/NuanceQuery.php index c286cdbd3b..617cada36d 100644 --- a/src/applications/nuance/query/NuanceQuery.php +++ b/src/applications/nuance/query/NuanceQuery.php @@ -1,11 +1,10 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function loadPage() { $table = new NuanceQueue(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function buildWhereClause($conn_r) { $where = array(); $where[] = $this->buildPagingClause($conn_r); if ($this->ids) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } return $this->formatWhereClause($where); } } - diff --git a/src/applications/nuance/query/NuanceRequestorQuery.php b/src/applications/nuance/query/NuanceRequestorQuery.php index c9e8fc5ea4..a1df9b98f3 100644 --- a/src/applications/nuance/query/NuanceRequestorQuery.php +++ b/src/applications/nuance/query/NuanceRequestorQuery.php @@ -1,57 +1,56 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function loadPage() { $table = new NuanceRequestor(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function buildWhereClause($conn_r) { $where = array(); $where[] = $this->buildPagingClause($conn_r); if ($this->ids) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } return $this->formatWhereClause($where); } } - diff --git a/src/applications/nuance/source/NuanceSourceDefinition.php b/src/applications/nuance/source/NuanceSourceDefinition.php index 86eea6ddeb..e643daa0b1 100644 --- a/src/applications/nuance/source/NuanceSourceDefinition.php +++ b/src/applications/nuance/source/NuanceSourceDefinition.php @@ -1,263 +1,262 @@ actor = $actor; return $this; } public function getActor() { return $this->actor; } public function requireActor() { $actor = $this->getActor(); if (!$actor) { throw new Exception('You must "setActor()" first!'); } return $actor; } public function setSourceObject(NuanceSource $source) { $source->setType($this->getSourceTypeConstant()); $this->sourceObject = $source; return $this; } public function getSourceObject() { return $this->sourceObject; } public function requireSourceObject() { $source = $this->getSourceObject(); if (!$source) { throw new Exception('You must "setSourceObject()" first!'); } return $source; } public static function getSelectOptions() { $definitions = self::getAllDefinitions(); $options = array(); foreach ($definitions as $definition) { $key = $definition->getSourceTypeConstant(); $name = $definition->getName(); $options[$key] = $name; } return $options; } /** * Gives a @{class:NuanceSourceDefinition} object for a given * @{class:NuanceSource}. Note you still need to @{method:setActor} * before the @{class:NuanceSourceDefinition} object will be useful. */ public static function getDefinitionForSource(NuanceSource $source) { $definitions = self::getAllDefinitions(); $map = mpull($definitions, null, 'getSourceTypeConstant'); $definition = $map[$source->getType()]; $definition->setSourceObject($source); return $definition; } public static function getAllDefinitions() { static $definitions; if ($definitions === null) { $objects = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); foreach ($objects as $definition) { $key = $definition->getSourceTypeConstant(); $name = $definition->getName(); if (isset($definitions[$key])) { $conflict = $definitions[$key]; throw new Exception(sprintf( 'Defintion %s conflicts with definition %s. This is a programming '. 'error.', $conflict, $name)); } } $definitions = $objects; } return $definitions; } /** * A human readable string like "Twitter" or "Phabricator Form". */ abstract public function getName(); /** * This should be a any VARCHAR(32). * * @{method:getAllDefinitions} will throw if you choose a string that * collides with another @{class:NuanceSourceDefinition} class. */ abstract public function getSourceTypeConstant(); /** * Code to create and update @{class:NuanceItem}s and * @{class:NuanceRequestor}s via daemons goes here. * * If that does not make sense for the @{class:NuanceSource} you are * defining, simply return null. For example, * @{class:NuancePhabricatorFormSourceDefinition} since these are one-way * contact forms. */ abstract public function updateItems(); private function loadSourceObjectPolicies( PhabricatorUser $user, NuanceSource $source) { $user = $this->requireActor(); $source = $this->requireSourceObject(); return id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($source) ->execute(); } final public function getEditTitle() { $source = $this->requireSourceObject(); if ($source->getPHID()) { $title = pht('Edit "%s" source.', $source->getName()); } else { $title = pht('Create a new "%s" source.', $this->getName()); } return $title; } final public function buildEditLayout(AphrontRequest $request) { $actor = $this->requireActor(); $source = $this->requireSourceObject(); $form_errors = array(); $error_messages = array(); $transactions = array(); $validation_exception = null; if ($request->isFormPost()) { $transactions = $this->buildTransactions($request); try { $editor = id(new NuanceSourceEditor()) ->setActor($actor) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->applyTransactions($source, $transactions); return id(new AphrontRedirectResponse()) ->setURI($source->getURI()); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; } } $form = $this->renderEditForm($validation_exception); $layout = id(new PHUIObjectBoxView()) ->setHeaderText($this->getEditTitle()) ->setValidationException($validation_exception) ->setFormErrors($error_messages) ->setForm($form); return $layout; } /** * Code to create a form to edit the @{class:NuanceItem} you are defining. * * return @{class:AphrontFormView} */ private function renderEditForm( PhabricatorApplicationTransactionValidationException $ex = null) { $user = $this->requireActor(); $source = $this->requireSourceObject(); $policies = $this->loadSourceObjectPolicies($user, $source); $e_name = null; if ($ex) { $e_name = $ex->getShortMessage(NuanceSourceTransaction::TYPE_NAME); } $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setError($e_name) ->setValue($source->getName())) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Type')) ->setName('type') ->setOptions(self::getSelectOptions()) ->setValue($source->getType())); $form = $this->augmentEditForm($form, $ex); $form ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($source) ->setPolicies($policies) ->setName('viewPolicy')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($source) ->setPolicies($policies) ->setName('editPolicy')) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($source->getURI()) ->setValue(pht('Save'))); return $form; } /** * return @{class:AphrontFormView} */ protected function augmentEditForm( AphrontFormView $form, PhabricatorApplicationTransactionValidationException $ex = null) { return $form; } /** * Hook to build up @{class:PhabricatorTransactions}. * * return array $transactions */ protected function buildTransactions(AphrontRequest $request) { $transactions = array(); $transactions[] = id(new NuanceSourceTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($request->getStr('editPolicy')); $transactions[] = id(new NuanceSourceTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($request->getStr('viewPolicy')); $transactions[] = id(new NuanceSourceTransaction()) ->setTransactionType(NuanceSourceTransaction::TYPE_NAME) ->setNewvalue($request->getStr('name')); return $transactions; } abstract public function renderView(); abstract public function renderListView(); } - diff --git a/src/applications/owners/mail/OwnersPackageReplyHandler.php b/src/applications/owners/mail/OwnersPackageReplyHandler.php index 31bdf4cb00..e530ee41ed 100644 --- a/src/applications/owners/mail/OwnersPackageReplyHandler.php +++ b/src/applications/owners/mail/OwnersPackageReplyHandler.php @@ -1,32 +1,30 @@ listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS); } public function handleEvent(PhutilEvent $event) { switch ($event->getType()) { case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS: $this->handleActionsEvent($event); break; } } private function handleActionsEvent(PhutilEvent $event) { $object = $event->getValue('object'); $actions = null; if ($object instanceof ManiphestTask) { $actions = $this->renderTaskItems($event); } $this->addActionMenuItems($event, $actions); } private function renderTaskItems(PhutilEvent $event) { if (!$this->canUseApplication($event->getUser())) { return; } $task = $event->getValue('object'); $phid = $task->getPHID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $event->getUser(), $task, PhabricatorPolicyCapability::CAN_EDIT); return id(new PhabricatorActionView()) ->setName(pht('Edit Pholio Mocks')) ->setHref("/search/attach/{$phid}/MOCK/edge/") ->setWorkflow(true) ->setIcon('attach') ->setDisabled(!$can_edit) ->setWorkflow(true); } } - diff --git a/src/applications/phragment/application/PhabricatorApplicationPhragment.php b/src/applications/phragment/application/PhabricatorApplicationPhragment.php index aaa8f3c4bc..d82e2dba5b 100644 --- a/src/applications/phragment/application/PhabricatorApplicationPhragment.php +++ b/src/applications/phragment/application/PhabricatorApplicationPhragment.php @@ -1,68 +1,67 @@ array( '' => 'PhragmentBrowseController', 'browse/(?P.*)' => 'PhragmentBrowseController', 'create/(?P.*)' => 'PhragmentCreateController', 'update/(?P.*)' => 'PhragmentUpdateController', 'policy/(?P.*)' => 'PhragmentPolicyController', 'history/(?P.*)' => 'PhragmentHistoryController', 'zip/(?P.*)' => 'PhragmentZIPController', 'zip@(?P[^/]+)/(?P.*)' => 'PhragmentZIPController', 'version/(?P[0-9]*)/' => 'PhragmentVersionController', 'patch/(?P[0-9x]*)/(?P[0-9]*)/' => 'PhragmentPatchController', 'revert/(?P[0-9]*)/(?P.*)' => 'PhragmentRevertController', 'snapshot/' => array( 'create/(?P.*)' => 'PhragmentSnapshotCreateController', 'view/(?P[0-9]*)/' => 'PhragmentSnapshotViewController', 'delete/(?P[0-9]*)/' => 'PhragmentSnapshotDeleteController', 'promote/' => array( 'latest/(?P.*)' => 'PhragmentSnapshotPromoteController', '(?P[0-9]*)/' => 'PhragmentSnapshotPromoteController', ), ), ), ); } protected function getCustomCapabilities() { return array( PhragmentCapabilityCanCreate::CAPABILITY => array( ), ); } } - diff --git a/src/applications/phrequent/application/PhabricatorApplicationPhrequent.php b/src/applications/phrequent/application/PhabricatorApplicationPhrequent.php index bf406cd703..35f2dd9fd9 100644 --- a/src/applications/phrequent/application/PhabricatorApplicationPhrequent.php +++ b/src/applications/phrequent/application/PhabricatorApplicationPhrequent.php @@ -1,62 +1,61 @@ array( '(?:query/(?P[^/]+)/)?' => 'PhrequentListController', 'track/(?P[a-z]+)/(?P[^/]+)/' => 'PhrequentTrackController' ), ); } public function loadStatus(PhabricatorUser $user) { $status = array(); // Show number of objects that are currently // being tracked for a user. $count = PhrequentUserTimeQuery::getUserTotalObjectsTracked($user); $type = PhabricatorApplicationStatusView::TYPE_NEEDS_ATTENTION; $status[] = id(new PhabricatorApplicationStatusView()) ->setType($type) ->setText(pht('%d Object(s) Tracked', $count)) ->setCount($count); return $status; } } - diff --git a/src/applications/phrequent/query/PhrequentSearchEngine.php b/src/applications/phrequent/query/PhrequentSearchEngine.php index 5e3d152f3a..48fd907b51 100644 --- a/src/applications/phrequent/query/PhrequentSearchEngine.php +++ b/src/applications/phrequent/query/PhrequentSearchEngine.php @@ -1,114 +1,113 @@ getParameter('limit', 1000); } public function buildSavedQueryFromRequest(AphrontRequest $request) { $saved = new PhabricatorSavedQuery(); $saved->setParameter( 'userPHIDs', $this->readUsersFromRequest($request, 'users')); $saved->setParameter('ended', $request->getStr('ended')); $saved->setParameter('order', $request->getStr('order')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhrequentUserTimeQuery()); $user_phids = $saved->getParameter('userPHIDs'); if ($user_phids) { $query->withUserPHIDs($user_phids); } $ended = $saved->getParameter('ended'); if ($ended != null) { $query->withEnded($ended); } $order = $saved->getParameter('order'); if ($order != null) { $query->setOrder($order); } return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved_query) { $user_phids = $saved_query->getParameter('userPHIDs', array()); $ended = $saved_query->getParameter( 'ended', PhrequentUserTimeQuery::ENDED_ALL); $order = $saved_query->getParameter( 'order', PhrequentUserTimeQuery::ORDER_ENDED_DESC); $phids = array_merge($user_phids); $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireViewer()) ->withPHIDs($phids) ->execute(); $form ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/users/') ->setName('users') ->setLabel(pht('Users')) ->setValue($handles)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Ended')) ->setName('ended') ->setValue($ended) ->setOptions(PhrequentUserTimeQuery::getEndedSearchOptions())) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Order')) ->setName('order') ->setValue($order) ->setOptions(PhrequentUserTimeQuery::getOrderSearchOptions())); } protected function getURI($path) { return '/phrequent/'.$path; } public function getBuiltinQueryNames() { $names = array( 'tracking' => pht('Currently Tracking'), 'all' => pht('All Tracked'), ); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query ->setParameter('order', PhrequentUserTimeQuery::ORDER_ENDED_DESC); case 'tracking': return $query ->setParameter('ended', PhrequentUserTimeQuery::ENDED_NO) ->setParameter('order', PhrequentUserTimeQuery::ORDER_ENDED_DESC); } return parent::buildSavedQueryFromBuiltin($query_key); } } - diff --git a/src/applications/phriction/application/PhabricatorApplicationPhriction.php b/src/applications/phriction/application/PhabricatorApplicationPhriction.php index d0573e872b..5ede3d5229 100644 --- a/src/applications/phriction/application/PhabricatorApplicationPhriction.php +++ b/src/applications/phriction/application/PhabricatorApplicationPhriction.php @@ -1,70 +1,69 @@ /)' => 'PhrictionDocumentController', // Match "/w/x/y/z/" with slug "x/y/z/". '/w/(?P.+/)' => 'PhrictionDocumentController', '/phriction/' => array( '(?:query/(?P[^/]+)/)?' => 'PhrictionListController', 'history(?P/)' => 'PhrictionHistoryController', 'history/(?P.+/)' => 'PhrictionHistoryController', 'edit/(?:(?P[1-9]\d*)/)?' => 'PhrictionEditController', 'delete/(?P[1-9]\d*)/' => 'PhrictionDeleteController', 'new/' => 'PhrictionNewController', 'move/(?:(?P[1-9]\d*)/)?' => 'PhrictionMoveController', 'preview/' => 'PhabricatorMarkupPreviewController', 'diff/(?P[1-9]\d*)/' => 'PhrictionDiffController', ), ); } public function getApplicationGroup() { return self::GROUP_COMMUNICATION; } public function getApplicationOrder() { return 0.140; } } - diff --git a/src/applications/policy/application/PhabricatorApplicationPolicy.php b/src/applications/policy/application/PhabricatorApplicationPolicy.php index 6ecf69c9c9..7d0ff9df2a 100644 --- a/src/applications/policy/application/PhabricatorApplicationPolicy.php +++ b/src/applications/policy/application/PhabricatorApplicationPolicy.php @@ -1,24 +1,23 @@ array( 'explain/(?P[^/]+)/(?P[^/]+)/' => 'PhabricatorPolicyExplainController', 'edit/(?:(?P[^/]+)/)?' => 'PhabricatorPolicyEditController', ), ); } } - diff --git a/src/applications/policy/capability/PhabricatorPolicyCapability.php b/src/applications/policy/capability/PhabricatorPolicyCapability.php index a34e0f76bd..1cff309a64 100644 --- a/src/applications/policy/capability/PhabricatorPolicyCapability.php +++ b/src/applications/policy/capability/PhabricatorPolicyCapability.php @@ -1,72 +1,70 @@ setAncestorClass(__CLASS__) ->loadObjects(); $map = mpull($capabilities, null, 'getCapabilityKey'); } return $map; } } - - diff --git a/src/applications/policy/query/PhabricatorPolicyQuery.php b/src/applications/policy/query/PhabricatorPolicyQuery.php index 37c38a5d6b..ac2c600305 100644 --- a/src/applications/policy/query/PhabricatorPolicyQuery.php +++ b/src/applications/policy/query/PhabricatorPolicyQuery.php @@ -1,237 +1,236 @@ object = $object; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public static function loadPolicies( PhabricatorUser $viewer, PhabricatorPolicyInterface $object) { $results = array(); $map = array(); foreach ($object->getCapabilities() as $capability) { $map[$capability] = $object->getPolicy($capability); } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->withPHIDs($map) ->execute(); foreach ($map as $capability => $phid) { $results[$capability] = $policies[$phid]; } return $results; } public static function renderPolicyDescriptions( PhabricatorUser $viewer, PhabricatorPolicyInterface $object, $icon = false) { $policies = self::loadPolicies($viewer, $object); foreach ($policies as $capability => $policy) { $policies[$capability] = $policy->renderDescription($icon); } return $policies; } public function loadPage() { if ($this->object && $this->phids) { throw new Exception( "You can not issue a policy query with both setObject() and ". "setPHIDs()."); } else if ($this->object) { $phids = $this->loadObjectPolicyPHIDs(); } else { $phids = $this->phids; } $phids = array_fuse($phids); $results = array(); // First, load global policies. foreach ($this->getGlobalPolicies() as $phid => $policy) { if (isset($phids[$phid])) { $results[$phid] = $policy; unset($phids[$phid]); } } // If we still need policies, we're going to have to fetch data. Bucket // the remaining policies into rule-based policies and handle-based // policies. if ($phids) { $rule_policies = array(); $handle_policies = array(); foreach ($phids as $phid) { $phid_type = phid_get_type($phid); if ($phid_type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) { $rule_policies[$phid] = $phid; } else { $handle_policies[$phid] = $phid; } } if ($handle_policies) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getViewer()) ->withPHIDs($handle_policies) ->execute(); foreach ($handle_policies as $phid) { $results[$phid] = PhabricatorPolicy::newFromPolicyAndHandle( $phid, $handles[$phid]); } } if ($rule_policies) { $rules = id(new PhabricatorPolicy())->loadAllWhere( 'phid IN (%Ls)', $rule_policies); $results += mpull($rules, null, 'getPHID'); } } $results = msort($results, 'getSortKey'); return $results; } public static function isGlobalPolicy($policy) { $globalPolicies = self::getGlobalPolicies(); if (isset($globalPolicies[$policy])) { return true; } return false; } public static function getGlobalPolicy($policy) { if (!self::isGlobalPolicy($policy)) { throw new Exception("Policy '{$policy}' is not a global policy!"); } return idx(self::getGlobalPolicies(), $policy); } private static function getGlobalPolicies() { static $constants = array( PhabricatorPolicies::POLICY_PUBLIC, PhabricatorPolicies::POLICY_USER, PhabricatorPolicies::POLICY_ADMIN, PhabricatorPolicies::POLICY_NOONE, ); $results = array(); foreach ($constants as $constant) { $results[$constant] = id(new PhabricatorPolicy()) ->setType(PhabricatorPolicyType::TYPE_GLOBAL) ->setPHID($constant) ->setName(self::getGlobalPolicyName($constant)) ->setShortName(self::getGlobalPolicyShortName($constant)) ->makeEphemeral(); } return $results; } private static function getGlobalPolicyName($policy) { switch ($policy) { case PhabricatorPolicies::POLICY_PUBLIC: return pht('Public (No Login Required)'); case PhabricatorPolicies::POLICY_USER: return pht('All Users'); case PhabricatorPolicies::POLICY_ADMIN: return pht('Administrators'); case PhabricatorPolicies::POLICY_NOONE: return pht('No One'); default: return pht('Unknown Policy'); } } private static function getGlobalPolicyShortName($policy) { switch ($policy) { case PhabricatorPolicies::POLICY_PUBLIC: return pht('Public'); default: return null; } } private function loadObjectPolicyPHIDs() { $phids = array(); $viewer = $this->getViewer(); if ($viewer->getPHID()) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withMemberPHIDs(array($viewer->getPHID())) ->execute(); foreach ($projects as $project) { $phids[] = $project->getPHID(); } // Include the "current viewer" policy. This improves consistency, but // is also useful for creating private instances of normally-shared object // types, like repositories. $phids[] = $viewer->getPHID(); } $capabilities = $this->object->getCapabilities(); foreach ($capabilities as $capability) { $policy = $this->object->getPolicy($capability); if (!$policy) { continue; } $phids[] = $policy; } // If this install doesn't have "Public" enabled, don't include it as an // option unless the object already has a "Public" policy. In this case we // retain the policy but enforce it as though it was "All Users". $show_public = PhabricatorEnv::getEnvConfig('policy.allow-public'); foreach ($this->getGlobalPolicies() as $phid => $policy) { if ($phid == PhabricatorPolicies::POLICY_PUBLIC) { if (!$show_public) { continue; } } $phids[] = $phid; } return $phids; } protected function shouldDisablePolicyFiltering() { // Policy filtering of policies is currently perilous and not required by // the application. return true; } public function getQueryApplicationClass() { return 'PhabricatorApplicationPolicy'; } } - diff --git a/src/applications/ponder/remarkup/PonderRemarkupRule.php b/src/applications/ponder/remarkup/PonderRemarkupRule.php index 9102600cfc..840c98fc0a 100644 --- a/src/applications/ponder/remarkup/PonderRemarkupRule.php +++ b/src/applications/ponder/remarkup/PonderRemarkupRule.php @@ -1,31 +1,30 @@ getEngine()->getConfig('viewer'); return id(new PonderQuestionQuery()) ->setViewer($viewer) ->withIDs($ids) ->execute(); } protected function shouldMarkupObject(array $params) { // NOTE: Q1, Q2, Q3 and Q4 are often used to refer to quarters of the year; // mark them up only in the {Q1} format. if ($params['type'] == 'ref') { if ($params['id'] <= 4) { return false; } } return true; } } - diff --git a/src/applications/ponder/storage/PonderAnswerTransaction.php b/src/applications/ponder/storage/PonderAnswerTransaction.php index f12303b3a7..24a8a86ba1 100644 --- a/src/applications/ponder/storage/PonderAnswerTransaction.php +++ b/src/applications/ponder/storage/PonderAnswerTransaction.php @@ -1,105 +1,104 @@ getTransactionType()) { case self::TYPE_CONTENT: $phids[] = $this->getObjectPHID(); break; } return $phids; } public function getTitle() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); switch ($this->getTransactionType()) { case self::TYPE_CONTENT: return pht( '%s edited %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } return parent::getTitle(); } public function getTitleForFeed(PhabricatorFeedStory $story) { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); switch ($this->getTransactionType()) { case self::TYPE_CONTENT: $answer = $story->getObject($object_phid); $question = $answer->getQuestion(); $answer_handle = $this->getHandle($object_phid); $link = $answer_handle->renderLink( $question->getFullTitle()); return pht( '%s updated their answer to %s', $this->renderHandleLink($author_phid), $link); } return parent::getTitleForFeed($story); } public function getBodyForFeed(PhabricatorFeedStory $story) { $new = $this->getNewValue(); $body = null; switch ($this->getTransactionType()) { case self::TYPE_CONTENT: return phutil_escape_html_newlines( phutil_utf8_shorten($new, 128)); break; } return parent::getBodyForFeed($story); } public function hasChangeDetails() { $old = $this->getOldValue(); switch ($this->getTransactionType()) { case self::TYPE_CONTENT: return $old !== null; } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } } - diff --git a/src/applications/ponder/storage/PonderAnswerTransactionComment.php b/src/applications/ponder/storage/PonderAnswerTransactionComment.php index 034798a20b..2dc601a3d6 100644 --- a/src/applications/ponder/storage/PonderAnswerTransactionComment.php +++ b/src/applications/ponder/storage/PonderAnswerTransactionComment.php @@ -1,11 +1,10 @@ getMarkupText($field)); $id = $this->getID(); return "ponder:c{$id}:{$field}:{$hash}"; } public function getMarkupText($field) { return $this->getContent(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::getEngine(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } public function getMarkupField() { return self::MARKUP_FIELD_CONTENT; } } - diff --git a/src/applications/ponder/storage/PonderQuestionTransactionComment.php b/src/applications/ponder/storage/PonderQuestionTransactionComment.php index c5e0c0f0e3..46e20ff1ed 100644 --- a/src/applications/ponder/storage/PonderQuestionTransactionComment.php +++ b/src/applications/ponder/storage/PonderQuestionTransactionComment.php @@ -1,11 +1,10 @@ loadDocumentByPHID($phid); $commit_data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $commit->getID()); $date_created = $commit->getEpoch(); $commit_message = $commit_data->getCommitMessage(); $author_phid = $commit_data->getCommitDetail('authorPHID'); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withIDs(array($commit->getRepositoryID())) ->executeOne(); if (!$repository) { throw new Exception("No such repository!"); } $title = 'r'.$repository->getCallsign().$commit->getCommitIdentifier(). " ".$commit_data->getSummary(); $doc = new PhabricatorSearchAbstractDocument(); $doc->setPHID($commit->getPHID()); $doc->setDocumentType(PhabricatorRepositoryPHIDTypeCommit::TYPECONST); $doc->setDocumentCreated($date_created); $doc->setDocumentModified($date_created); $doc->setDocumentTitle($title); $doc->addField( PhabricatorSearchField::FIELD_BODY, $commit_message); if ($author_phid) { $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR, $author_phid, PhabricatorPeoplePHIDTypeUser::TYPECONST, $date_created); } $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $commit->getPHID(), PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT); if ($project_phids) { foreach ($project_phids as $project_phid) { $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_PROJECT, $project_phid, PhabricatorProjectPHIDTypeProject::TYPECONST, $date_created); } } $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_REPOSITORY, $repository->getPHID(), PhabricatorRepositoryPHIDTypeRepository::TYPECONST, $date_created); $comments = id(new PhabricatorAuditComment())->loadAllWhere( 'targetPHID = %s', $commit->getPHID()); foreach ($comments as $comment) { if (strlen($comment->getContent())) { $doc->addField( PhabricatorSearchField::FIELD_COMMENT, $comment->getContent()); } } return $doc; } } - diff --git a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php index 446dcb19e6..1cec405290 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php +++ b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php @@ -1,391 +1,390 @@ getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_PUSH_POLICY: $phids[] = $old; $phids[] = $new; break; } return $phids; } public function shouldHide() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_REMOTE_URI: case self::TYPE_SSH_LOGIN: case self::TYPE_SSH_KEY: case self::TYPE_SSH_KEYFILE: case self::TYPE_HTTP_LOGIN: case self::TYPE_HTTP_PASS: // Hide null vs empty string changes. return (!strlen($old) && !strlen($new)); case self::TYPE_LOCAL_PATH: case self::TYPE_NAME: // Hide these on create, they aren't interesting and we have an // explicit "create" transaction. if (!strlen($old)) { return true; } break; } return parent::shouldHide(); } public function getIcon() { switch ($this->getTransactionType()) { case self::TYPE_VCS: return 'create'; } return parent::getIcon(); } public function getColor() { switch ($this->getTransactionType()) { case self::TYPE_VCS: return 'green'; } return parent::getIcon(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_VCS: return pht( '%s created this repository.', $this->renderHandleLink($author_phid)); case self::TYPE_ACTIVATE: if ($new) { return pht( '%s activated this repository.', $this->renderHandleLink($author_phid)); } else { return pht( '%s deactivated this repository.', $this->renderHandleLink($author_phid)); } case self::TYPE_NAME: return pht( '%s renamed this repository from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); case self::TYPE_DESCRIPTION: return pht( '%s updated the description of this repository.', $this->renderHandleLink($author_phid)); case self::TYPE_ENCODING: if (strlen($old) && !strlen($new)) { return pht( '%s removed the "%s" encoding configured for this repository.', $this->renderHandleLink($author_phid), $old); } else if (strlen($new) && !strlen($old)) { return pht( '%s set the encoding for this repository to "%s".', $this->renderHandleLink($author_phid), $new); } else { return pht( '%s changed the repository encoding from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); } case self::TYPE_DEFAULT_BRANCH: if (!strlen($new)) { return pht( '%s removed "%s" as the default branch.', $this->renderHandleLink($author_phid), $old); } else if (!strlen($old)) { return pht( '%s set the default branch to "%s".', $this->renderHandleLink($author_phid), $new); } else { return pht( '%s changed the default branch from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); } break; case self::TYPE_TRACK_ONLY: if (!$new) { return pht( '%s set this repository to track all branches.', $this->renderHandleLink($author_phid)); } else if (!$old) { return pht( '%s set this repository to track branches: %s.', $this->renderHandleLink($author_phid), implode(', ', $new)); } else { return pht( '%s changed track branches from "%s" to "%s".', $this->renderHandleLink($author_phid), implode(', ', $old), implode(', ', $new)); } break; case self::TYPE_AUTOCLOSE_ONLY: if (!$new) { return pht( '%s set this repository to autoclose on all branches.', $this->renderHandleLink($author_phid)); } else if (!$old) { return pht( '%s set this repository to autoclose on branches: %s.', $this->renderHandleLink($author_phid), implode(', ', $new)); } else { return pht( '%s changed autoclose branches from "%s" to "%s".', $this->renderHandleLink($author_phid), implode(', ', $old), implode(', ', $new)); } break; case self::TYPE_UUID: if (!strlen($new)) { return pht( '%s removed "%s" as the repository UUID.', $this->renderHandleLink($author_phid), $old); } else if (!strlen($old)) { return pht( '%s set the repository UUID to "%s".', $this->renderHandleLink($author_phid), $new); } else { return pht( '%s changed the repository UUID from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); } break; case self::TYPE_SVN_SUBPATH: if (!strlen($new)) { return pht( '%s removed "%s" as the Import Only path.', $this->renderHandleLink($author_phid), $old); } else if (!strlen($old)) { return pht( '%s set the repository to import only "%s".', $this->renderHandleLink($author_phid), $new); } else { return pht( '%s changed the import path from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); } break; case self::TYPE_NOTIFY: if ($new) { return pht( '%s enabled notifications and publishing for this repository.', $this->renderHandleLink($author_phid)); } else { return pht( '%s disabled notifications and publishing for this repository.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_AUTOCLOSE: if ($new) { return pht( '%s enabled autoclose for this repository.', $this->renderHandleLink($author_phid)); } else { return pht( '%s disabled autoclose for this repository.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_REMOTE_URI: if (!strlen($old)) { return pht( '%s set the remote URI for this repository to "%s".', $this->renderHandleLink($author_phid), $new); } else if (!strlen($new)) { return pht( '%s removed the remote URI for this repository.', $this->renderHandleLink($author_phid)); } else { return pht( '%s changed the remote URI for this repository from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); } break; case self::TYPE_SSH_LOGIN: return pht( '%s updated the SSH login for this repository.', $this->renderHandleLink($author_phid)); case self::TYPE_SSH_KEY: return pht( '%s updated the SSH key for this repository.', $this->renderHandleLink($author_phid)); case self::TYPE_SSH_KEYFILE: return pht( '%s updated the SSH keyfile for this repository.', $this->renderHandleLink($author_phid)); case self::TYPE_HTTP_LOGIN: return pht( '%s updated the HTTP login for this repository.', $this->renderHandleLink($author_phid)); case self::TYPE_HTTP_PASS: return pht( '%s updated the HTTP password for this repository.', $this->renderHandleLink($author_phid)); case self::TYPE_LOCAL_PATH: return pht( '%s changed the local path from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); case self::TYPE_HOSTING: if ($new) { return pht( '%s changed this repository to be hosted on Phabricator.', $this->renderHandleLink($author_phid)); } else { return pht( '%s changed this repository to track a remote elsewhere.', $this->renderHandleLink($author_phid)); } case self::TYPE_PROTOCOL_HTTP: return pht( '%s changed the availability of this repository over HTTP from '. '"%s" to "%s".', $this->renderHandleLink($author_phid), PhabricatorRepository::getProtocolAvailabilityName($old), PhabricatorRepository::getProtocolAvailabilityName($new)); case self::TYPE_PROTOCOL_SSH: return pht( '%s changed the availability of this repository over SSH from '. '"%s" to "%s".', $this->renderHandleLink($author_phid), PhabricatorRepository::getProtocolAvailabilityName($old), PhabricatorRepository::getProtocolAvailabilityName($new)); case self::TYPE_PUSH_POLICY: return pht( '%s changed the push policy of this repository from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderPolicyName($old), $this->renderPolicyName($new)); case self::TYPE_DANGEROUS: if ($new) { return pht( '%s disabled protection against dangerous changes.', $this->renderHandleLink($author_phid)); } else { return pht( '%s enabled protection against dangerous changes.', $this->renderHandleLink($author_phid)); } case self::TYPE_CLONE_NAME: if (strlen($old) && !strlen($new)) { return pht( '%s removed the clone name of this repository.', $this->renderHandleLink($author_phid)); } else if (strlen($new) && !strlen($old)) { return pht( '%s set the clone name of this repository to "%s".', $this->renderHandleLink($author_phid), $new); } else { return pht( '%s changed the clone name of this repository from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); } } return parent::getTitle(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: return true; } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } } - diff --git a/src/applications/search/controller/PhabricatorSearchAttachController.php b/src/applications/search/controller/PhabricatorSearchAttachController.php index b0b7bae84c..a2ec3b2fb1 100644 --- a/src/applications/search/controller/PhabricatorSearchAttachController.php +++ b/src/applications/search/controller/PhabricatorSearchAttachController.php @@ -1,337 +1,337 @@ phid = $data['phid']; $this->type = $data['type']; $this->action = idx($data, 'action', self::ACTION_ATTACH); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); - $handle = id(New PhabricatorHandleQuery()) + $handle = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs(array($this->phid)) ->executeOne(); $object_type = $handle->getType(); $attach_type = $this->type; $object = id(new PhabricatorObjectQuery()) ->setViewer($user) ->withPHIDs(array($this->phid)) ->executeOne(); if (!$object) { return new Aphront404Response(); } $edge_type = null; switch ($this->action) { case self::ACTION_EDGE: case self::ACTION_DEPENDENCIES: case self::ACTION_ATTACH: $edge_type = $this->getEdgeType($object_type, $attach_type); break; } if ($request->isFormPost()) { $phids = explode(';', $request->getStr('phids')); $phids = array_filter($phids); $phids = array_values($phids); if ($edge_type) { $do_txn = $object instanceof PhabricatorApplicationTransactionInterface; $old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $this->phid, $edge_type); $add_phids = $phids; $rem_phids = array_diff($old_phids, $add_phids); if ($do_txn) { $txn_editor = $object->getApplicationTransactionEditor() ->setActor($user) ->setContentSourceFromRequest($request); $txn_template = $object->getApplicationTransactionObject() ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $edge_type) ->setNewValue(array( '+' => array_fuse($add_phids), '-' => array_fuse($rem_phids))); $txn_editor->applyTransactions($object, array($txn_template)); } else { $editor = id(new PhabricatorEdgeEditor()); $editor->setActor($user); foreach ($add_phids as $phid) { $editor->addEdge($this->phid, $edge_type, $phid); } foreach ($rem_phids as $phid) { $editor->removeEdge($this->phid, $edge_type, $phid); } try { $editor->save(); } catch (PhabricatorEdgeCycleException $ex) { $this->raiseGraphCycleException($ex); } } return id(new AphrontReloadResponse())->setURI($handle->getURI()); } else { return $this->performMerge($object, $handle, $phids); } } else { if ($edge_type) { $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $this->phid, $edge_type); } else { // This is a merge. $phids = array(); } } $strings = $this->getStrings(); $handles = $this->loadViewerHandles($phids); $obj_dialog = new PhabricatorObjectSelectorDialog(); $obj_dialog ->setUser($user) ->setHandles($handles) ->setFilters($this->getFilters($strings)) ->setSelectedFilter($strings['selected']) ->setExcluded($this->phid) ->setCancelURI($handle->getURI()) ->setSearchURI('/search/select/'.$attach_type.'/') ->setTitle($strings['title']) ->setHeader($strings['header']) ->setButtonText($strings['button']) ->setInstructions($strings['instructions']); $dialog = $obj_dialog->buildDialog(); return id(new AphrontDialogResponse())->setDialog($dialog); } private function performMerge( ManiphestTask $task, PhabricatorObjectHandle $handle, array $phids) { $user = $this->getRequest()->getUser(); $response = id(new AphrontReloadResponse())->setURI($handle->getURI()); $phids = array_fill_keys($phids, true); unset($phids[$task->getPHID()]); // Prevent merging a task into itself. if (!$phids) { return $response; } $targets = id(new ManiphestTaskQuery()) ->setViewer($user) ->withPHIDs(array_keys($phids)) ->execute(); if (empty($targets)) { return $response; } $editor = id(new ManiphestTransactionEditor()) ->setActor($user) ->setContentSourceFromRequest($this->getRequest()) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $task_names = array(); $merge_into_name = 'T'.$task->getID(); $cc_vector = array(); $cc_vector[] = $task->getCCPHIDs(); foreach ($targets as $target) { $cc_vector[] = $target->getCCPHIDs(); $cc_vector[] = array( $target->getAuthorPHID(), $target->getOwnerPHID()); $close_task = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_STATUS) ->setNewValue(ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE); $merge_comment = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new ManiphestTransactionComment()) ->setContent("\xE2\x9C\x98 Merged into {$merge_into_name}.")); $editor->applyTransactions( $target, array( $close_task, $merge_comment, )); $task_names[] = 'T'.$target->getID(); } $all_ccs = array_mergev($cc_vector); $all_ccs = array_filter($all_ccs); $all_ccs = array_unique($all_ccs); $task_names = implode(', ', $task_names); $add_ccs = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_CCS) ->setNewValue($all_ccs); $merged_comment = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new ManiphestTransactionComment()) ->setContent("\xE2\x97\x80 Merged tasks: {$task_names}.")); $editor->applyTransactions($task, array($add_ccs, $merged_comment)); return $response; } private function getStrings() { switch ($this->type) { case DifferentialPHIDTypeRevision::TYPECONST: $noun = 'Revisions'; $selected = 'created'; break; case ManiphestPHIDTypeTask::TYPECONST: $noun = 'Tasks'; $selected = 'assigned'; break; case PhabricatorRepositoryPHIDTypeCommit::TYPECONST: $noun = 'Commits'; $selected = 'created'; break; case PholioPHIDTypeMock::TYPECONST: $noun = 'Mocks'; $selected = 'created'; break; } switch ($this->action) { case self::ACTION_EDGE: case self::ACTION_ATTACH: $dialog_title = "Manage Attached {$noun}"; $header_text = "Currently Attached {$noun}"; $button_text = "Save {$noun}"; $instructions = null; break; case self::ACTION_MERGE: $dialog_title = "Merge Duplicate Tasks"; $header_text = "Tasks To Merge"; $button_text = "Merge {$noun}"; $instructions = "These tasks will be merged into the current task and then closed. ". "The current task will grow stronger."; break; case self::ACTION_DEPENDENCIES: $dialog_title = "Edit Dependencies"; $header_text = "Current Dependencies"; $button_text = "Save Dependencies"; $instructions = null; break; } return array( 'target_plural_noun' => $noun, 'selected' => $selected, 'title' => $dialog_title, 'header' => $header_text, 'button' => $button_text, 'instructions' => $instructions, ); } private function getFilters(array $strings) { if ($this->type == PholioPHIDTypeMock::TYPECONST) { $filters = array( 'created' => 'Created By Me', 'all' => 'All '.$strings['target_plural_noun'], ); } else { $filters = array( 'assigned' => 'Assigned to Me', 'created' => 'Created By Me', 'open' => 'All Open '.$strings['target_plural_noun'], 'all' => 'All '.$strings['target_plural_noun'], ); } return $filters; } private function getEdgeType($src_type, $dst_type) { $t_cmit = PhabricatorRepositoryPHIDTypeCommit::TYPECONST; $t_task = ManiphestPHIDTypeTask::TYPECONST; $t_drev = DifferentialPHIDTypeRevision::TYPECONST; $t_mock = PholioPHIDTypeMock::TYPECONST; $map = array( $t_cmit => array( $t_task => PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK, ), $t_task => array( $t_cmit => PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT, $t_task => PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK, $t_drev => PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV, $t_mock => PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK, ), $t_drev => array( $t_drev => PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV, $t_task => PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK, ), $t_mock => array( $t_task => PhabricatorEdgeConfig::TYPE_MOCK_HAS_TASK, ), ); if (empty($map[$src_type][$dst_type])) { return null; } return $map[$src_type][$dst_type]; } private function raiseGraphCycleException(PhabricatorEdgeCycleException $ex) { $cycle = $ex->getCycle(); $handles = $this->loadViewerHandles($cycle); $names = array(); foreach ($cycle as $cycle_phid) { $names[] = $handles[$cycle_phid]->getFullName(); } $names = implode(" \xE2\x86\x92 ", $names); throw new Exception( "You can not create that dependency, because it would create a ". "circular dependency: {$names}."); } } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelConpherencePreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelConpherencePreferences.php index 95c19d9d5e..2085a3eb5c 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelConpherencePreferences.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelConpherencePreferences.php @@ -1,69 +1,68 @@ getUser(); $preferences = $user->loadPreferences(); $pref = PhabricatorUserPreferences::PREFERENCE_CONPH_NOTIFICATIONS; if ($request->isFormPost()) { $notifications = $request->getInt($pref); $preferences->setPreference($pref, $notifications); $preferences->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?saved=true')); } $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Conpherence Notifications')) ->setName($pref) ->setValue($preferences->getPreference($pref)) ->setOptions( array( ConpherenceSettings::EMAIL_ALWAYS => pht('Email Always'), ConpherenceSettings::NOTIFICATIONS_ONLY => pht('Notifications Only'), )) ->setCaption( pht('Should Conpherence send emails for updates or '. 'notifications only? This global setting can be overridden '. 'on a per-thread basis within Conpherence.'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Preferences'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Conpherence Preferences')) ->setForm($form) ->setFormSaved($request->getBool('saved')); return array( $form_box, ); } } - diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelDeveloperPreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelDeveloperPreferences.php index 3c23cc05a8..e5ba13fdbe 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelDeveloperPreferences.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelDeveloperPreferences.php @@ -1,98 +1,97 @@ getUser(); $preferences = $user->loadPreferences(); $pref_dark_console = PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE; $dark_console_value = $preferences->getPreference($pref_dark_console); if ($request->isFormPost()) { $new_dark_console = $request->getBool($pref_dark_console); $preferences->setPreference($pref_dark_console, $new_dark_console); // If the user turned Dark Console on, enable it (as though they had hit // "`"). if ($new_dark_console && !$dark_console_value) { $user->setConsoleVisible(true); $user->save(); } $preferences->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?saved=true')); } $is_console_enabled = PhabricatorEnv::getEnvConfig('darkconsole.enabled'); $preamble = pht( '**DarkConsole** is a developer console which can help build and '. 'debug Phabricator applications. It includes tools for understanding '. 'errors, performance, service calls, and other low-level aspects of '. 'Phabricator\'s inner workings.'); if ($is_console_enabled) { $instructions = pht( "%s\n\n". 'You can enable it for your account below. Enabling DarkConsole will '. 'slightly decrease performance, but give you access to debugging '. 'tools. You may want to disable it again later if you only need it '. 'temporarily.'. "\n\n". 'NOTE: After enabling DarkConsole, **press the ##`## key on your '. 'keyboard** to show or hide it.', $preamble); } else { $instructions = pht( "%s\n\n". 'Before you can turn on DarkConsole, it needs to be enabled in '. 'the configuration for this install (`darkconsole.enabled`).', $preamble); } $form = id(new AphrontFormView()) ->setUser($user) ->appendRemarkupInstructions($instructions) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Dark Console')) ->setName($pref_dark_console) ->setValue($dark_console_value) ->setOptions( array( 0 => pht('Disable DarkConsole'), 1 => pht('Enable DarkConsole'), )) ->setDisabled(!$is_console_enabled)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Preferences'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Developer Settings')) ->setFormSaved($request->getBool('saved')) ->setForm($form); return array( $form_box, ); } } - diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelDiffPreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelDiffPreferences.php index 26115e3e77..16ba31bd3d 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelDiffPreferences.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelDiffPreferences.php @@ -1,71 +1,70 @@ getUser(); $preferences = $user->loadPreferences(); $pref_filetree = PhabricatorUserPreferences::PREFERENCE_DIFF_FILETREE; if ($request->isFormPost()) { $filetree = $request->getInt($pref_filetree); if ($filetree && !$preferences->getPreference($pref_filetree)) { $preferences->setPreference( PhabricatorUserPreferences::PREFERENCE_NAV_COLLAPSED, false); } $preferences->setPreference($pref_filetree, $filetree); $preferences->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?saved=true')); } $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Show Filetree')) ->setName($pref_filetree) ->setValue($preferences->getPreference($pref_filetree)) ->setOptions( array( 0 => pht('Disable Filetree'), 1 => pht('Enable Filetree'), )) ->setCaption( pht("When looking at a revision or commit, enable a sidebar ". "showing affected files. You can press %s to show or hide ". "the sidebar.", phutil_tag('tt', array(), 'f')))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Preferences'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Diff Preferences')) ->setFormSaved($request->getBool('saved')) ->setForm($form); return array( $form_box, ); } } - diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelDisplayPreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelDisplayPreferences.php index c97f9d6046..d03a73b423 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelDisplayPreferences.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelDisplayPreferences.php @@ -1,150 +1,149 @@ getUser(); $preferences = $user->loadPreferences(); $pref_monospaced = PhabricatorUserPreferences::PREFERENCE_MONOSPACED; $pref_editor = PhabricatorUserPreferences::PREFERENCE_EDITOR; $pref_multiedit = PhabricatorUserPreferences::PREFERENCE_MULTIEDIT; $pref_titles = PhabricatorUserPreferences::PREFERENCE_TITLES; $pref_monospaced_textareas = PhabricatorUserPreferences::PREFERENCE_MONOSPACED_TEXTAREAS; if ($request->isFormPost()) { $monospaced = $request->getStr($pref_monospaced); // Prevent the user from doing stupid things. $monospaced = preg_replace('/[^a-z0-9 ,"]+/i', '', $monospaced); $preferences->setPreference($pref_titles, $request->getStr($pref_titles)); $preferences->setPreference($pref_editor, $request->getStr($pref_editor)); $preferences->setPreference( $pref_multiedit, $request->getStr($pref_multiedit)); $preferences->setPreference($pref_monospaced, $monospaced); $preferences->setPreference( $pref_monospaced_textareas, $request->getStr($pref_monospaced_textareas)); $preferences->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?saved=true')); } $example_string = << PhabricatorEnv::getDoclink( 'article/User_Guide_Configuring_an_External_Editor.html'), ), pht('User Guide: Configuring an External Editor')); $font_default = PhabricatorEnv::getEnvConfig('style.monospace'); $pref_monospaced_textareas_value = $preferences ->getPreference($pref_monospaced_textareas); if (!$pref_monospaced_textareas_value) { $pref_monospaced_textareas_value = 'disabled'; } $editor_instructions = pht('Link to edit files in external editor. '. '%%f is replaced by filename, %%l by line number, %%r by repository '. 'callsign, %%%% by literal %%. For documentation, see: %s', $editor_doc_link); $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Page Titles')) ->setName($pref_titles) ->setValue($preferences->getPreference($pref_titles)) ->setOptions( array( 'glyph' => pht("In page titles, show Tool names as unicode glyphs: " . "\xE2\x9A\x99"), 'text' => pht('In page titles, show Tool names as plain text: ' . '[Differential]'), ))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Editor Link')) ->setName($pref_editor) // How to pht() ->setCaption($editor_instructions) ->setValue($preferences->getPreference($pref_editor))) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Edit Multiple Files')) ->setName($pref_multiedit) ->setOptions(array( '' => pht('Supported (paths separated by spaces)'), 'disable' => pht('Not Supported'), )) ->setValue($preferences->getPreference($pref_multiedit))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Monospaced Font')) ->setName($pref_monospaced) // Check plz ->setCaption(hsprintf( '%s
(%s: %s)', pht('Overrides default fonts in tools like Differential.'), pht('Default'), $font_default)) ->setValue($preferences->getPreference($pref_monospaced))) ->appendChild( id(new AphrontFormMarkupControl()) ->setValue(phutil_tag( 'pre', array('class' => 'PhabricatorMonospaced'), $example_string))) ->appendChild( id(new AphrontFormRadioButtonControl()) ->setLabel(pht('Monospaced Textareas')) ->setName($pref_monospaced_textareas) ->setValue($pref_monospaced_textareas_value) ->addButton('enabled', pht('Enabled'), pht('Show all textareas using the monospaced font defined above.')) ->addButton('disabled', pht('Disabled'), null)); $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Preferences'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Display Preferences')) ->setFormSaved($request->getStr('saved') === 'true') ->setForm($form); return array( $form_box, ); } } - diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php index 82a7b13a55..276463861f 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php @@ -1,219 +1,218 @@ getUser(); $preferences = $user->loadPreferences(); require_celerity_resource('phabricator-settings-css'); $apps = id(new PhabricatorApplicationQuery()) ->setViewer($user) ->withUnlisted(false) ->execute(); $pref_tiles = PhabricatorUserPreferences::PREFERENCE_APP_TILES; $tiles = $preferences->getPreference($pref_tiles, array()); if ($request->isFormPost()) { $values = $request->getArr('tile'); foreach ($apps as $app) { $key = get_class($app); $value = idx($values, $key); switch ($value) { case PhabricatorApplication::TILE_FULL: case PhabricatorApplication::TILE_SHOW: case PhabricatorApplication::TILE_HIDE: $tiles[$key] = $value; break; default: unset($tiles[$key]); break; } } $preferences->setPreference($pref_tiles, $tiles); $preferences->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?saved=true')); } $form = id(new AphrontFormView()) ->setUser($user); $group_map = PhabricatorApplication::getApplicationGroups(); $output = array(); $applications = PhabricatorApplication::getAllInstalledApplications(); $applications = mgroup($applications, 'getApplicationGroup'); $applications = array_select_keys( $applications, array_keys($group_map)); foreach ($applications as $group => $apps) { $group_name = $group_map[$group]; $rows = array(); foreach ($apps as $app) { if (!$app->shouldAppearInLaunchView()) { continue; } $default = $app->getDefaultTileDisplay($user); if ($default == PhabricatorApplication::TILE_INVISIBLE) { continue; } $default_name = PhabricatorApplication::getTileDisplayName($default); $hide = PhabricatorApplication::TILE_HIDE; $show = PhabricatorApplication::TILE_SHOW; $full = PhabricatorApplication::TILE_FULL; $key = get_class($app); $default_radio_button_status = (idx($tiles, $key, 'default') == 'default') ? 'checked' : null; $hide_radio_button_status = (idx($tiles, $key, 'default') == $hide) ? 'checked' : null; $show_radio_button_status = (idx($tiles, $key, 'default') == $show) ? 'checked' : null; $full_radio_button_status = (idx($tiles, $key, 'default') == $full) ? 'checked' : null; $default_radio_button = phutil_tag( 'input', array( 'type' => 'radio', 'name' => 'tile['.$key.']', 'value' => 'default', 'checked' => $default_radio_button_status, )); $hide_radio_button = phutil_tag( 'input', array( 'type' => 'radio', 'name' => 'tile['.$key.']', 'value' => $hide, 'checked' => $hide_radio_button_status, )); $show_radio_button = phutil_tag( 'input', array( 'type' => 'radio', 'name' => 'tile['.$key.']', 'value' => $show, 'checked' => $show_radio_button_status, )); $full_radio_button = phutil_tag( 'input', array( 'type' => 'radio', 'name' => 'tile['.$key.']', 'value' => $full, 'checked' => $full_radio_button_status, )); $desc = $app->getShortDescription(); $app_column = hsprintf( "%s
%s, Default: %s", $app->getName(), $desc, $default_name); $rows[] = array( $app_column, $default_radio_button, $hide_radio_button, $show_radio_button, $full_radio_button, ); } if (empty($rows)) { continue; } $table = new AphrontTableView($rows); $table ->setClassName('phabricator-settings-homepagetable') ->setHeaders( array( pht('Applications'), pht('Default'), pht('Hidden'), pht('Small'), pht('Large'), )) ->setColumnClasses( array( '', 'fixed', 'fixed', 'fixed', 'fixed', )); $panel = id(new PHUIObjectBoxView()) ->setHeaderText($group_name) ->appendChild($table); $output[] = $panel; } $save_button = id(new AphrontFormSubmitControl()) ->setValue(pht('Save Preferences')); $output[] = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE) ->addClass('phabricator-settings-homepagetable-button') ->appendChild($save_button); $form->appendChild($output); $error_view = null; if ($request->getStr('saved') === 'true') { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Preferences Saved')) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setErrors(array(pht('Your preferences have been saved.'))); } $header = id(new PHUIHeaderView()) ->setHeader(pht('Home Page Preferences')); $form = id(new PHUIBoxView()) ->addClass('phabricator-settings-homepagetable-wrap') ->appendChild($form); return array($header, $error_view, $form); } } - diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php index 4330a6dc93..0c77448895 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php @@ -1,63 +1,62 @@ getUser(); $preferences = $user->loadPreferences(); $pref_jump = PhabricatorUserPreferences::PREFERENCE_SEARCHBAR_JUMP; $pref_shortcut = PhabricatorUserPreferences::PREFERENCE_SEARCH_SHORTCUT; if ($request->isFormPost()) { $preferences->setPreference($pref_jump, $request->getBool($pref_jump)); $preferences->setPreference($pref_shortcut, $request->getBool($pref_shortcut)); $preferences->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?saved=true')); } $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox($pref_jump, 1, pht('Enable jump nav functionality in all search boxes.'), $preferences->getPreference($pref_jump, 1)) ->addCheckbox($pref_shortcut, 1, pht("Press '/' to focus the search input."), $preferences->getPreference($pref_shortcut, 1))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Search Preferences')) ->setFormSaved($request->getStr('saved') === 'true') ->setForm($form); return array( $form_box, ); } } - diff --git a/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php b/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php index 9370ea361a..9769767bbb 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php @@ -1,127 +1,126 @@ getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: return ($old === null); } return parent::shouldHide(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorSlowvoteTransaction::TYPE_QUESTION: if ($old === null) { return pht( '%s created this poll.', $this->renderHandleLink($author_phid)); } else { return pht( '%s changed the poll question from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); } break; case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: return pht( '%s updated the description for this poll.', $this->renderHandleLink($author_phid)); case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: // TODO: This could be more detailed return pht( '%s changed who can see the responses.', $this->renderHandleLink($author_phid)); case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: if ($new) { return pht( '%s made poll responses appear in a random order.', $this->renderHandleLink($author_phid)); } else { return pht( '%s made poll responses appear in a fixed order.', $this->renderHandleLink($author_phid)); } break; } return parent::getTitle(); } public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorSlowvoteTransaction::TYPE_QUESTION: case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: return 'edit'; } return parent::getIcon(); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorSlowvoteTransaction::TYPE_QUESTION: case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: return PhabricatorTransactions::COLOR_BLUE; } return parent::getColor(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: return true; } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { return $this->renderTextCorpusChangeDetails($viewer); } } - diff --git a/src/applications/slowvote/storage/PhabricatorSlowvoteTransactionComment.php b/src/applications/slowvote/storage/PhabricatorSlowvoteTransactionComment.php index 876ee015de..0dc033e2fd 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvoteTransactionComment.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvoteTransactionComment.php @@ -1,11 +1,10 @@ array( '(?Padd|delete)/'. '(?P[^/]+)/' => 'PhabricatorSubscriptionsEditController', ), ); } } - diff --git a/src/applications/transactions/application/PhabricatorApplicationTransactions.php b/src/applications/transactions/application/PhabricatorApplicationTransactions.php index 96d76ca0e0..e91cb3dc80 100644 --- a/src/applications/transactions/application/PhabricatorApplicationTransactions.php +++ b/src/applications/transactions/application/PhabricatorApplicationTransactions.php @@ -1,27 +1,26 @@ array( 'edit/(?[^/]+)/' => 'PhabricatorApplicationTransactionCommentEditController', 'history/(?[^/]+)/' => 'PhabricatorApplicationTransactionCommentHistoryController', 'detail/(?[^/]+)/' => 'PhabricatorApplicationTransactionDetailController', ), ); } } - diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php index 3c35d017b3..6ff13c78a6 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php @@ -1,237 +1,236 @@ objectPHID = $object_phid; return $this; } public function getObjectPHID() { return $this->objectPHID; } public function setShowPreview($show_preview) { $this->showPreview = $show_preview; return $this; } public function getShowPreview() { return $this->showPreview; } public function setRequestURI(PhutilURI $request_uri) { $this->requestURI = $request_uri; return $this; } public function getRequestURI() { return $this->requestURI; } public function setDraft(PhabricatorDraft $draft) { $this->draft = $draft; return $this; } public function getDraft() { return $this->draft; } public function setSubmitButtonName($submit_button_name) { $this->submitButtonName = $submit_button_name; return $this; } public function getSubmitButtonName() { return $this->submitButtonName; } public function setAction($action) { $this->action = $action; return $this; } public function getAction() { return $this->action; } public function setHeaderText($text) { $this->headerText = $text; return $this; } public function render() { $user = $this->getUser(); if (!$user->isLoggedIn()) { $uri = id(new PhutilURI('/login/')) ->setQueryParam('next', (string) $this->getRequestURI()); return id(new PHUIObjectBoxView()) ->setFlush(true) ->setHeaderText(pht('Add Comment')) ->appendChild( javelin_tag( 'a', array( 'class' => 'login-to-comment button', 'sigil' => 'workflow', 'href' => $uri ), pht('Login to Comment'))); } $data = array(); $comment = $this->renderCommentPanel(); if ($this->getShowPreview()) { $preview = $this->renderPreviewPanel(); } else { $preview = null; } Javelin::initBehavior( 'phabricator-transaction-comment-form', array( 'formID' => $this->getFormID(), 'timelineID' => $this->getPreviewTimelineID(), 'panelID' => $this->getPreviewPanelID(), 'statusID' => $this->getStatusID(), 'commentID' => $this->getCommentID(), 'loadingString' => pht('Loading Preview...'), 'savingString' => pht('Saving Draft...'), 'draftString' => pht('Saved Draft'), 'showPreview' => $this->getShowPreview(), 'actionURI' => $this->getAction(), 'draftKey' => $this->getDraft() ? $this->getDraft()->getDraftKey() : null, )); $comment_box = id(new PHUIObjectBoxView()) ->setFlush(true) ->setHeaderText($this->headerText) ->appendChild($comment); return array($comment_box, $preview); } private function renderCommentPanel() { $status = phutil_tag( 'div', array( 'id' => $this->getStatusID(), ), ''); $draft_comment = ''; if ($this->getDraft()) { $draft_comment = $this->getDraft()->getDraft(); } if (!$this->getObjectPHID()) { throw new Exception("Call setObjectPHID() before render()!"); } return id(new AphrontFormView()) ->setUser($this->getUser()) ->addSigil('transaction-append') ->setWorkflow(true) ->setMetadata( array( 'objectPHID' => $this->getObjectPHID(), )) ->setAction($this->getAction()) ->setID($this->getFormID()) ->appendChild( id(new PhabricatorRemarkupControl()) ->setID($this->getCommentID()) ->setName('comment') ->setLabel(pht('Comment')) ->setUser($this->getUser()) ->setValue($draft_comment)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($this->getSubmitButtonName())) ->appendChild( id(new AphrontFormMarkupControl()) ->setValue($status)); } private function renderPreviewPanel() { $preview = id(new PHUITimelineView()) ->setID($this->getPreviewTimelineID()); return phutil_tag( 'div', array( 'id' => $this->getPreviewPanelID(), 'style' => 'display: none', ), $preview); } private function getPreviewPanelID() { if (!$this->previewPanelID) { $this->previewPanelID = celerity_generate_unique_node_id(); } return $this->previewPanelID; } private function getPreviewTimelineID() { if (!$this->previewTimelineID) { $this->previewTimelineID = celerity_generate_unique_node_id(); } return $this->previewTimelineID; } public function setFormID($id) { $this->formID = $id; return $this; } private function getFormID() { if (!$this->formID) { $this->formID = celerity_generate_unique_node_id(); } return $this->formID; } private function getStatusID() { if (!$this->statusID) { $this->statusID = celerity_generate_unique_node_id(); } return $this->statusID; } private function getCommentID() { if (!$this->commentID) { $this->commentID = celerity_generate_unique_node_id(); } return $this->commentID; } } - diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php index 83f016cb2b..55fe586bc6 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php @@ -1,57 +1,56 @@ newText = $new_text; return $this; } public function setOldText($old_text) { $this->oldText = $old_text; return $this; } public function render() { $old = $this->oldText; $new = $this->newText; // TODO: On mobile, or perhaps by default, we should switch to 1-up once // that is built. if (strlen($old)) { $old = phutil_utf8_hard_wrap($old, 80); $old = implode("\n", $old)."\n"; } if (strlen($new)) { $new = phutil_utf8_hard_wrap($new, 80); $new = implode("\n", $new)."\n"; } try { $engine = new PhabricatorDifferenceEngine(); $changeset = $engine->generateChangesetFromFileContent($old, $new); $whitespace_mode = DifferentialChangesetParser::WHITESPACE_SHOW_ALL; $markup_engine = new PhabricatorMarkupEngine(); $markup_engine->setViewer($this->getUser()); $parser = new DifferentialChangesetParser(); $parser->setChangeset($changeset); $parser->setMarkupEngine($markup_engine); $parser->setWhitespaceMode($whitespace_mode); return $parser->render(0, PHP_INT_MAX, array()); } catch (Exception $ex) { return $ex->getMessage(); } } } - diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php index 38592a2978..b02c6119ad 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php @@ -1,352 +1,351 @@ objectPHID = $object_phid; return $this; } public function getObjectPHID() { return $this->objectPHID; } public function setIsPreview($is_preview) { $this->isPreview = $is_preview; return $this; } public function setShowEditActions($show_edit_actions) { $this->showEditActions = $show_edit_actions; return $this; } public function getShowEditActions() { return $this->showEditActions; } public function setAnchorOffset($anchor_offset) { $this->anchorOffset = $anchor_offset; return $this; } public function setMarkupEngine(PhabricatorMarkupEngine $engine) { $this->engine = $engine; return $this; } public function setTransactions(array $transactions) { assert_instances_of($transactions, 'PhabricatorApplicationTransaction'); $this->transactions = $transactions; return $this; } public function buildEvents($with_hiding = false) { $user = $this->getUser(); $anchor = $this->anchorOffset; $xactions = $this->transactions; $xactions = $this->filterHiddenTransactions($xactions); $xactions = $this->groupRelatedTransactions($xactions); $groups = $this->groupDisplayTransactions($xactions); // If the viewer has interacted with this object, we hide things from // before their most recent interaction by default. This tends to make // very long threads much more manageable, because you don't have to // scroll through a lot of history and can focus on just new stuff. $show_group = null; if ($with_hiding) { // Find the most recent comment by the viewer. $group_keys = array_keys($groups); $group_keys = array_reverse($group_keys); // If we would only hide a small number of transactions, don't hide // anything. Just don't examine the last few keys. Also, we always // want to show the most recent pieces of activity, so don't examine // the first few keys either. $group_keys = array_slice($group_keys, 2, -2); $type_comment = PhabricatorTransactions::TYPE_COMMENT; foreach ($group_keys as $group_key) { $group = $groups[$group_key]; foreach ($group as $xaction) { if ($xaction->getAuthorPHID() == $user->getPHID() && $xaction->getTransactionType() == $type_comment) { // This is the most recent group where the user commented. $show_group = $group_key; break 2; } } } } $events = array(); $hide_by_default = ($show_group !== null); foreach ($groups as $group_key => $group) { if ($hide_by_default && ($show_group === $group_key)) { $hide_by_default = false; } $group_event = null; foreach ($group as $xaction) { $event = $this->renderEvent($xaction, $group, $anchor); $event->setHideByDefault($hide_by_default); $anchor++; if (!$group_event) { $group_event = $event; } else { $group_event->addEventToGroup($event); } } $events[] = $group_event; } return $events; } public function render() { if (!$this->getObjectPHID()) { throw new Exception("Call setObjectPHID() before render()!"); } $view = new PHUITimelineView(); $events = $this->buildEvents($with_hiding = true); foreach ($events as $event) { $view->addEvent($event); } if ($this->getShowEditActions()) { $list_id = celerity_generate_unique_node_id(); $view->setID($list_id); Javelin::initBehavior( 'phabricator-transaction-list', array( 'listID' => $list_id, 'objectPHID' => $this->getObjectPHID(), 'nextAnchor' => $this->anchorOffset + count($events), )); } return $view->render(); } protected function getOrBuildEngine() { if ($this->engine) { return $this->engine; } $field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT; $engine = id(new PhabricatorMarkupEngine()) ->setViewer($this->getUser()); foreach ($this->transactions as $xaction) { if (!$xaction->hasComment()) { continue; } $engine->addObject($xaction->getComment(), $field); } $engine->process(); return $engine; } private function buildChangeDetailsLink( PhabricatorApplicationTransaction $xaction) { return javelin_tag( 'a', array( 'href' => '/transactions/detail/'.$xaction->getPHID().'/', 'sigil' => 'workflow', ), pht('(Show Details)')); } protected function shouldGroupTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { return false; } protected function renderTransactionContent( PhabricatorApplicationTransaction $xaction) { $field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT; $engine = $this->getOrBuildEngine(); $comment = $xaction->getComment(); if ($xaction->hasComment()) { if ($comment->getIsDeleted()) { return phutil_tag( 'em', array(), pht('This comment has been deleted.')); } else { return $engine->getOutput($comment, $field); } } return null; } private function filterHiddenTransactions(array $xactions) { foreach ($xactions as $key => $xaction) { if ($xaction->shouldHide()) { unset($xactions[$key]); } } return $xactions; } private function groupRelatedTransactions(array $xactions) { $last = null; $last_key = null; $groups = array(); foreach ($xactions as $key => $xaction) { if ($last && $this->shouldGroupTransactions($last, $xaction)) { $groups[$last_key][] = $xaction; unset($xactions[$key]); } else { $last = $xaction; $last_key = $key; } } foreach ($xactions as $key => $xaction) { $xaction->attachTransactionGroup(idx($groups, $key, array())); } return $xactions; } private function groupDisplayTransactions(array $xactions) { $groups = array(); $group = array(); foreach ($xactions as $xaction) { if ($xaction->shouldDisplayGroupWith($group)) { $group[] = $xaction; } else { if ($group) { $groups[] = $group; } $group = array($xaction); } } if ($group) { $groups[] = $group; } foreach ($groups as $key => $group) { $group = msort($group, 'getActionStrength'); $group = array_reverse($group); $groups[$key] = $group; } return $groups; } private function renderEvent( PhabricatorApplicationTransaction $xaction, array $group, $anchor) { $viewer = $this->getUser(); $event = id(new PHUITimelineEventView()) ->setUser($viewer) ->setTransactionPHID($xaction->getPHID()) ->setUserHandle($xaction->getHandle($xaction->getAuthorPHID())) ->setIcon($xaction->getIcon()) ->setColor($xaction->getColor()); if (!$this->shouldSuppressTitle($xaction, $group)) { $title = $xaction->getTitle(); if ($xaction->hasChangeDetails()) { if (!$this->isPreview) { $details = $this->buildChangeDetailsLink($xaction); $title = array( $title, ' ', $details, ); } } $event->setTitle($title); } if ($this->isPreview) { $event->setIsPreview(true); } else { $event ->setDateCreated($xaction->getDateCreated()) ->setContentSource($xaction->getContentSource()) ->setAnchor($anchor); } $has_deleted_comment = $xaction->getComment() && $xaction->getComment()->getIsDeleted(); if ($this->getShowEditActions() && !$this->isPreview) { if ($xaction->getCommentVersion() > 1) { $event->setIsEdited(true); } $can_edit = PhabricatorPolicyCapability::CAN_EDIT; if ($xaction->hasComment() || $has_deleted_comment) { $has_edit_capability = PhabricatorPolicyFilter::hasCapability( $viewer, $xaction, $can_edit); if ($has_edit_capability) { $event->setIsEditable(true); } } } $content = $this->renderTransactionContent($xaction); if ($content) { $event->appendChild($content); } return $event; } private function shouldSuppressTitle( PhabricatorApplicationTransaction $xaction, array $group) { // This is a little hard-coded, but we don't have any other reasonable // cases for now. Suppress "commented on" if there are other actions in // the display group. if (count($group) > 1) { $type_comment = PhabricatorTransactions::TYPE_COMMENT; if ($xaction->getTransactionType() == $type_comment) { return true; } } return false; } } - diff --git a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php index 581d26d566..9ec4955be1 100644 --- a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php +++ b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php @@ -1,86 +1,85 @@ icon = $icon; return $this; } public function setName($name) { $this->name = $name; return $this; } public function setURI($uri) { $this->uri = $uri; return $this; } public function setPHID($phid) { $this->phid = $phid; return $this; } public function setPriorityString($priority_string) { $this->priorityString = $priority_string; return $this; } public function setDisplayName($display_name) { $this->displayName = $display_name; return $this; } public function setDisplayType($display_type) { $this->displayType = $display_type; return $this; } public function setImageURI($image_uri) { $this->imageURI = $image_uri; return $this; } public function setPriorityType($priority_type) { $this->priorityType = $priority_type; return $this; } public function setClosed($closed) { $this->closed = $closed; return $this; } public function getWireFormat() { $data = array( $this->name, $this->uri ? (string)$this->uri : null, $this->phid, $this->priorityString, $this->displayName, $this->displayType, $this->imageURI ? (string)$this->imageURI : null, $this->priorityType, $this->icon, $this->closed, ); while (end($data) === null) { array_pop($data); } return $data; } } - diff --git a/src/docs/user/configuration/troubleshooting_https.diviner b/src/docs/user/configuration/troubleshooting_https.diviner index 6950377219..19e335e615 100644 --- a/src/docs/user/configuration/troubleshooting_https.diviner +++ b/src/docs/user/configuration/troubleshooting_https.diviner @@ -1,74 +1,74 @@ @title Troubleshooting HTTPS @group config Detailed instructions for troubleshooting HTTPS connection problems. = Overview = If you're having trouble connecting to an HTTPS install of Phabricator, and particularly if you're receiving a "There was an error negotiating the SSL connection." error, this document may be able to help you diagnose and resolve the problem. -Connection negotation can fail for several reasons. The major ones are: +Connection negotiation can fail for several reasons. The major ones are: - You have not added the the Certificate Authority as a trusted authority (this is the most common problem, and usually the issue for self-signed certificates). - The SSL certificate is signed for the wrong domain. For example, a certificate signed for `www.example.com` will not work for `phabricator.example.com`. - The server rejects TLSv1 SNI connections for the domain (this is complicated, see below). = Certificate Authority Problems = SSL certificates need to be signed by a trusted authority (called a Certificate Authority or "CA") to be accepted. If the CA for a certificate is untrusted, the connection will fail (this defends the connection from an eavesdropping attack called "man in the middle"). Normally, you purchase a certificate from a known authority and clients have a list of trusted authorities. You can self-sign a certificate by creating your own CA, but clients will not trust it by default. They need to add the CA as a trusted authority. For instructions on adding CAs, see `libphutil/resources/ssl/README`. Although it is possible to accept certificates that aren't signed by trusted CAs, this is not currently supported because it compromises the ability of SSL to protect the connection against eavesdropping. = Domain Problems = Verify the domain the certificate was issued for. You can generally do this with: $ openssl x509 -text -in If the certificate was accidentally generated for, e.g. `www.example.com` but you installed Phabricator on `phabricator.example.com`, you need to generate a new certificate for the right domain. = SNI Problems = Server Name Identification ("SNI") is a feature of TLSv1 which works a bit like Apache VirtualHosts, and allows a server to present different certificates to clients who are connecting to it using different names. Servers that are not configured properly may reject TSLv1 SNI requests because they do not recognize the name the client is connecting with. This topic is complicated, but you can test for it by running: $ openssl s_client -connect example.com:443 -servername example.com Replace **both** instances of "example.com" with your domain. If you receive an error in `SSL23_GET_SERVER_HELLO` with `reason(1112)`, like this: CONNECTED(00000003) 87871:error:14077458:SSL routines:SSL23_GET_SERVER_HELLO:reason(1112): /SourceCache/OpenSSL098/OpenSSL098-44/src/ssl/s23_clnt.c:602: ...it indicates server is misconfigured. The most common cause of this problem is an Apache server that does not explicitly name the Phabricator domain as a valid VirtualHost. This error occurs only for some versions of the OpenSSL client library (from v0.9.8r or earlier until 1.0.0), so only some users may experience it. diff --git a/src/docs/user/contributing/php_coding_standards.diviner b/src/docs/user/contributing/php_coding_standards.diviner index 72a849fce5..cb3406e15b 100644 --- a/src/docs/user/contributing/php_coding_standards.diviner +++ b/src/docs/user/contributing/php_coding_standards.diviner @@ -1,170 +1,169 @@ @title PHP Coding Standards @group contrib This document describes PHP coding standards for Phabricator and related projects (like Arcanist and libphutil). = Overview = This document outlines technical and style guidelines which are followed in libphutil. Contributors should also follow these guidelines. Many of these guidelines are automatically enforced by lint. These guidelines are essentially identical to the Facebook guidelines, since I basically copy-pasted them. If you are already familiar with the Facebook guidelines, you probably don't need to read this super thoroughly. = Spaces, Linebreaks and Indentation = - Use two spaces for indentation. Don't use tab literal characters. - Use Unix linebreaks ("\n"), not MSDOS ("\r\n") or OS9 ("\r"). - Use K&R style braces and spacing. - Put a space after control keywords like ##if## and ##for##. - Put a space after commas in argument lists. - Put a space around operators like ##=##, ##<##, etc. - Don't put spaces after function names. - Parentheses should hug their contents. - Generally, prefer to wrap code at 80 columns. = Case and Capitalization = - Name variables and functions using ##lowercase_with_underscores##. - Name classes using ##UpperCamelCase##. - Name methods and properties using ##lowerCamelCase##. - Use uppercase for common acronyms like ID and HTML. - Name constants using ##UPPERCASE##. - Write ##true##, ##false## and ##null## in lowercase. = Comments = - Do not use "#" (shell-style) comments. - Prefer "//" comments inside function and method bodies. = PHP Language Style = - Use "" tag. - Prefer casts like ##(string)## to casting functions like ##strval()##. - Prefer type checks like ##$v === null## to type functions like ##is_null()##. - Avoid all crazy alternate forms of language constructs like "endwhile" and "<>". - Always put braces around conditional and loop blocks. = PHP Language Features = - Use PHP as a programming language, not a templating language. - Avoid globals. - Avoid extract(). - Avoid eval(). - Avoid variable variables. - Prefer classes over functions. - Prefer class constants over defines. - Avoid naked class properties; instead, define accessors. - Use exceptions for error conditions. - Use type hints, use `assert_instances_of()` for arrays holding objects. = Examples = **if/else:** if ($some_variable > 3) { // ... } else if ($some_variable === null) { // ... } else { // ... } You should always put braces around the body of an if clause, even if it is only one line long. Note spaces around operators and after control statements. Do not use the "endif" construct, and write "else if" as two words. **for:** for ($ii = 0; $ii < 10; $ii++) { // ... } Prefer $ii, $jj, $kk, etc., as iterators, since they're easier to pick out visually and react better to "Find Next..." in editors. **foreach:** foreach ($map as $key => $value) { // ... } **switch:** switch ($value) { case 1: // ... break; case 2: if ($flag) { // ... break; } break; default: // ... break; } ##break## statements should be indented to block level. **array literals:** $junk = array( 'nuts', 'bolts', 'refuse', ); Use a trailing comma and put the closing parenthesis on a separate line so that diffs which add elements to the array affect only one line. **operators:** $a + $b; // Put spaces around operators. $omg.$lol; // Exception: no spaces around string concatenation. $arr[] = $element; // Couple [] with the array when appending. $obj = new Thing(); // Always use parens. **function/method calls:** // One line eject($cargo); // Multiline AbstractFireFactoryFactoryEngine::promulgateConflagrationInstance( $fuel, $ignition_source); **function/method definitions:** function example_function($base_value, $additional_value) { return $base_value + $additional_value; } class C { public static function promulgateConflagrationInstance( IFuel $fuel, IgnitionSource $source) { // ... } } **class:** class Dog extends Animal { const CIRCLES_REQUIRED_TO_LIE_DOWN = 3; private $favoriteFood = 'dirt'; public function getFavoriteFood() { return $this->favoriteFood; } } - diff --git a/src/docs/user/developer/adding_new_css_and_js.diviner b/src/docs/user/developer/adding_new_css_and_js.diviner index 2443f9d5d6..83a80aca40 100644 --- a/src/docs/user/developer/adding_new_css_and_js.diviner +++ b/src/docs/user/developer/adding_new_css_and_js.diviner @@ -1,86 +1,85 @@ @title Adding New CSS and JS @group developer Explains how to add new CSS and JS files to Phabricator. = Overview = Phabricator uses a system called **Celerity** to manage static resources. If you are a current or former Facebook employee, Celerity is based on the Haste system used at Facebook and generally behaves similarly. = Adding a New File = To add a new CSS or JS file, create it in an appropriate location in ##webroot/rsrc/css/## or ##webroot/rsrc/js/## inside your ##phabricator/## directory. Each file must ##@provides## itself as a component, declared in a header comment: LANG=css /** * @provides duck-styles-css */ .duck-header { font-size: 9001px; } Note that this comment must be a Javadoc-style comment, not just any comment. If your component depends on other components (which is common in JS but rare and inadvisable in CSS), declare then with ##@requires##: LANG=js /** * @requires javelin-stratcom * @provides duck */ /** * Put class documentation here, NOT in the header block. */ JX.install('Duck', { ... }); Then rebuild the Celerity map (see the next section). = Changing an Existing File = When you add, move or remove a file, or change the contents of existing JS or CSS file, you should rebuild the Celerity map: phabricator/ $ ./scripts/celerity_mapper.php ./webroot If you've only changed file content things will generally work even if you don't, but they might start not working as well in the future if you skip this step. The generated file `resources/celerity/map.php` causes merge conflicts quite often. They can be resolved by running the Celerity mapper. You can automate this process by running: phabricator/ $ ./scripts/celerity/install_merge.sh This will install Git merge driver which will run when a conflict in this file occurs. = Including a File = To include a CSS or JS file in a page, use @{function:require_celerity_resource}: require_celerity_resource('duck-style-css'); require_celerity_resource('duck'); If your map is up to date, the resource should now be included correctly when the page is rendered. You should place this call as close to the code which actually uses the resource as possible, i.e. **not** at the top of your Controller. The idea is that you should @{function:require_celerity_resource} a resource only if you are actually using it on a specific rendering of the page, not just because some views of the page might require it. - diff --git a/src/docs/user/developer/unit_tests.diviner b/src/docs/user/developer/unit_tests.diviner index 55a67403f8..6aeaaab225 100644 --- a/src/docs/user/developer/unit_tests.diviner +++ b/src/docs/user/developer/unit_tests.diviner @@ -1,87 +1,86 @@ @title Writing Unit Tests @group developer Simple guide to libphutil, Arcanist and Phabricator unit tests. = Overview = libphutil, Arcanist and Phabricator provide and use a simple unit test framework. This document is aimed at project contributors and describes how to use it to add and run tests in these projects or other libphutil libraries. In the general case, you can integrate ##arc## with a custom unit test engine (like PHPUnit or any other unit testing library) to run tests in other projects. See @{article:Arcanist User Guide: Customizing Lint, Unit Tests and Workflows} for information on customizing engines. = Adding Tests = To add new tests to a libphutil, Arcanist or Phabricator module: - Create a ##__tests__/## directory in the module if it doesn't exist yet. - Add classes to the ##__tests__/## directory which extend from @{class:PhabricatorTestCase} (in Phabricator) or @{class@arcanist:ArcanistPhutilTestCase} (elsewhere). - Run ##arc liberate## on the library root so your classes are loadable. = Running Tests = Once you've added test classes, you can run them with: - ##arc unit path/to/module/##, to explicitly run module tests. - ##arc unit##, to run tests for all modules affected by changes in the working copy. - ##arc diff## will also run ##arc unit## for you. = Example Test Case = Here's a simple example test: lang=php class PhabricatorTrivialTestCase extends PhabricatorTestCase { private $two; public function willRunOneTest($test_name) { // You can execute setup steps which will run before each test in this // method. $this->two = 2; } public function testAllIsRightWithTheWorld() { $this->assertEqual(4, $this->two + $this->two, '2 + 2 = 4'); } } You can see this class at @{class:PhabricatorTrivialTestCase} and run it with: phabricator/ $ arc unit src/infrastructure/testing/testcase/ PASS <1ms* testAllIsRightWithTheWorld For more information on writing tests, see @{class@arcanist:ArcanistPhutilTestCase} and @{class:PhabricatorTestCase}. = Database Isolation = By default, Phabricator isolates unit tests from the database. It makes a crude effort to simulate some side effects (principally, ID assignment on insert), but any queries which read data will fail to select any rows and throw an exception about isolation. In general, isolation is good, but this can make certain types of tests difficult to write. When you encounter issues, you can deal with them in a number of ways. From best to worst: - Encounter no issues; your tests are fast and isolated. - Add more simulated side effects if you encounter minor issues and simulation is reasonable. - Build a real database simulation layer (fairly complex). - Disable isolation for a single test by using ##LiskDAO::endIsolateAllLiskEffectsToCurrentProcess();## before your test and ##LiskDAO::beginIsolateAllLiskEffectsToCurrentProcess();## after your test. This will disable isolation for one test. NOT RECOMMENDED. - Disable isolation for your entire test case by overriding ##getPhabricatorTestCaseConfiguration()## and providing ##self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK => false## in the configuration dictionary you return. This will disable isolation entirely. STRONGLY NOT RECOMMENDED. - diff --git a/src/docs/user/developer/using_oauthserver.diviner b/src/docs/user/developer/using_oauthserver.diviner index 9a1a989e66..9587d23410 100644 --- a/src/docs/user/developer/using_oauthserver.diviner +++ b/src/docs/user/developer/using_oauthserver.diviner @@ -1,120 +1,120 @@ @title Using the Phabricator OAuth Server @group developer How to use the Phabricator OAuth Server. = Overview = -Phabricator includes an OAuth Server which supports the -##Authorization Code Grant## flow as described in the OAuth 2.0 +Phabricator includes an OAuth Server which supports the +##Authorization Code Grant## flow as described in the OAuth 2.0 specification: http://tools.ietf.org/html/draft-ietf-oauth-v2-23 -This functionality can allow clients to integrate with a given -Phabricator instance in a secure way with granular data access. -For example, Phabricator can be used as a central identity store for any +This functionality can allow clients to integrate with a given +Phabricator instance in a secure way with granular data access. +For example, Phabricator can be used as a central identity store for any clients that implement OAuth 2.0. = Vocabulary = -- **Access token** - a token which allows a client to ask for data on -behalf of a resource owner. A given client will only be able to access +- **Access token** - a token which allows a client to ask for data on +behalf of a resource owner. A given client will only be able to access data included in the scope(s) the resource owner authorized that client for. -- **Authorization code** - a short-lived code which allows an authenticated +- **Authorization code** - a short-lived code which allows an authenticated client to ask for an access token on behalf of some resource owner. -- **Client** - this is the application or system asking for data from the +- **Client** - this is the application or system asking for data from the OAuth Server on behalf of the resource owner. -- **Resource owner** - this is the user the client and OAuth Server are +- **Resource owner** - this is the user the client and OAuth Server are concerned with on a given request. -- **Scope** - this defines a specific piece of granular data a client can -or can not access on behalf of a user. For example, if authorized for the -"whoami" scope on behalf of a given resource owner, the client can get the -results of Conduit.whoami for that resource owner when authenticated with +- **Scope** - this defines a specific piece of granular data a client can +or can not access on behalf of a user. For example, if authorized for the +"whoami" scope on behalf of a given resource owner, the client can get the +results of Conduit.whoami for that resource owner when authenticated with a valid access token. = Setup - Creating a Client = # Visit https://phabricator.example.com/oauthserver/client/create/ # Fill out the form # Profit = Obtaining an Authorization Code = -POST or GET https://phabricator.example.com/oauthserver/auth/ with the +POST or GET https://phabricator.example.com/oauthserver/auth/ with the following parameters: - Required - **client_id** - the id of the newly registered client. -- Required - **response_type** - the desired type of authorization code +- Required - **response_type** - the desired type of authorization code response. Only code is supported at this time. -- Optional - **redirect_uri** - override the redirect_uri the client -registered. This redirect_uri must have the same fully-qualified domain -and have at least the same query parameters as the redirect_uri the client +- Optional - **redirect_uri** - override the redirect_uri the client +registered. This redirect_uri must have the same fully-qualified domain +and have at least the same query parameters as the redirect_uri the client registered, as well as have no fragments. - Optional - **scope** - specify what scope(s) the client needs access to in a space-delimited list. -- Optional - **state** - an opaque value the client can send to the server -for programmatic excellence. Some clients use this value to implement XSRF +- Optional - **state** - an opaque value the client can send to the server +for programmatic excellence. Some clients use this value to implement XSRF protection or for debugging purposes. -If done correctly and the resource owner has not yet authorized the client -for the desired scope, then the resource owner will be presented with an -interface to authorize the client for the desired scope. The OAuth Server -will redirect to the pertinent redirect_uri with an authorization code or +If done correctly and the resource owner has not yet authorized the client +for the desired scope, then the resource owner will be presented with an +interface to authorize the client for the desired scope. The OAuth Server +will redirect to the pertinent redirect_uri with an authorization code or an error indicating the resource owner did not authorize the client, depending. -If done correctly and the resource owner has already authorized the client for -the desired scope, then the OAuth Server will redirect to the pertinent -redirect_uri with a valid authorization code. +If done correctly and the resource owner has already authorized the client for +the desired scope, then the OAuth Server will redirect to the pertinent +redirect_uri with a valid authorization code. -If there is an error, the OAuth Server will return a descriptive error -message. This error will be presented to the resource owner on the -Phabricator domain if there is reason to believe there is something fishy -with the client. For example, if there is an issue with the redirect_uri. -Otherwise, the OAuth Server will redirect to the pertinent redirect_uri +If there is an error, the OAuth Server will return a descriptive error +message. This error will be presented to the resource owner on the +Phabricator domain if there is reason to believe there is something fishy +with the client. For example, if there is an issue with the redirect_uri. +Otherwise, the OAuth Server will redirect to the pertinent redirect_uri and include the pertinent error information. = Obtaining an Access Token = -POST or GET https://phabricator.example.com/oauthserver/token/ +POST or GET https://phabricator.example.com/oauthserver/token/ with the following parameters: - Required - **client_id** - the id of the client -- Required - **client_secret** - the secret of the client. +- Required - **client_secret** - the secret of the client. This is used to authenticate the client. - Required - **code** - the authorization code obtained earlier. -- Required - **grant_type** - the desired type of access grant. +- Required - **grant_type** - the desired type of access grant. Only token is supported at this time. -- Optional - **redirect_uri** - should be the exact same redirect_uri as -the redirect_uri specified to obtain the authorization code. If no -redirect_uri was specified to obtain the authorization code then this +- Optional - **redirect_uri** - should be the exact same redirect_uri as +the redirect_uri specified to obtain the authorization code. If no +redirect_uri was specified to obtain the authorization code then this should not be specified. -If done correctly, the OAuth Server will redirect to the pertinent +If done correctly, the OAuth Server will redirect to the pertinent redirect_uri with an access token. -If there is an error, the OAuth Server will return a descriptive error +If there is an error, the OAuth Server will return a descriptive error message. = Using an Access Token = -Simply include a query param with the key of "access_token" and the value +Simply include a query param with the key of "access_token" and the value as the earlier obtained access token. For example: https://phabricator.example.com/api/user.whoami?access_token=ykc7ly7vtibj334oga4fnfbuvnwz4ocp If the token has expired or is otherwise invalid, the client will receive -an error indicating as such. In these cases, the client should re-initiate +an error indicating as such. In these cases, the client should re-initiate the entire ##Authorization Code Grant## flow. -NOTE: See "Scopes" section below for more information on what data is +NOTE: See "Scopes" section below for more information on what data is currently exposed through the OAuth Server. = Scopes = There are only two scopes supported at this time. -- **offline_access** - allows an access token to work indefinitely without +- **offline_access** - allows an access token to work indefinitely without expiring. -- **whoami** - allows the client to access the results of Conduit.whoami on +- **whoami** - allows the client to access the results of Conduit.whoami on behalf of the resource owner. diff --git a/src/docs/user/flavortext/javascript_object_array.diviner b/src/docs/user/flavortext/javascript_object_array.diviner index 19cca9973c..fde8b33ecf 100644 --- a/src/docs/user/flavortext/javascript_object_array.diviner +++ b/src/docs/user/flavortext/javascript_object_array.diviner @@ -1,153 +1,152 @@ @title Javascript Object and Array @group flavortext This document describes the behaviors of Object and Array in Javascript, and a specific approach to their use which produces basically reasonable language behavior. = Primitives = Javascript has two native datatype primitives, Object and Array. Both are classes, so you can use ##new## to instantiate new objects and arrays: COUNTEREXAMPLE var a = new Array(); // Not preferred. var o = new Object(); However, **you should prefer the shorthand notation** because it's more concise: lang=js var a = []; // Preferred. var o = {}; (A possible exception to this rule is if you want to use the allocation behavior of the Array constructor, but you almost certainly don't.) The language relationship between Object and Array is somewhat tricky. Object and Array are both classes, but "object" is also a primitive type. Object is //also// the base class of all classes. lang=js typeof Object; // "function" typeof Array; // "function" typeof {}; // "object" typeof []; // "object" var a = [], o = {}; o instanceof Object; // true o instanceof Array; // false a instanceof Object; // true a instanceof Array; // true = Objects are Maps, Arrays are Lists = PHP has a single ##array## datatype which behaves like as both map and a list, and a common mistake is to treat Javascript arrays (or objects) in the same way. **Don't do this.** It sort of works until it doesn't. Instead, learn how Javascript's native datatypes work and use them properly. In Javascript, you should think of Objects as maps ("dictionaries") and Arrays as lists ("vectors"). You store keys-value pairs in a map, and store ordered values in a list. So, store key-value pairs in Objects. var o = { // Good, an object is a map. name: 'Hubert', species: 'zebra' }; console.log(o.name); ...and store ordered values in Arrays. var a = [1, 2, 3]; // Good, an array is a list. a.push(4); Don't store key-value pairs in Arrays and don't expect Objects to be ordered. COUNTEREXAMPLE var a = []; a['name'] = 'Hubert'; // No! Don't do this! This technically works because Arrays are Objects and you think everything is fine and dandy, but it won't do what you want and will burn you. = Iterating over Maps and Lists = Iterate over a map like this: lang=js for (var k in object) { f(object[k]); } NOTE: There's some hasOwnProperty nonsense being omitted here, see below. Iterate over a list like this: lang=js for (var ii = 0; ii < list.length; ii++) { f(list[ii]); } NOTE: There's some sparse array nonsense being omitted here, see below. If you try to use ##for (var k in ...)## syntax to iterate over an Array, you'll pick up a whole pile of keys you didn't intend to and it won't work. If you try to use ##for (var ii = 0; ...)## syntax to iterate over an Object, it won't work at all. If you consistently treat Arrays as lists and Objects as maps and use the corresponding iterators, everything will pretty much always work in a reasonable way. = hasOwnProperty() = An issue with this model is that if you write stuff to Object.prototype, it will show up every time you use enumeration ##for##: COUNTEREXAMPLE var o = {}; Object.prototype.duck = "quack"; for (var k in o) { console.log(o[k]); // Logs "quack" } There are two ways to avoid this: - test that ##k## exists on ##o## by calling ##o.hasOwnProperty(k)## in every single loop everywhere in your program and only use libraries which also do this and never forget to do it ever; or - don't write to Object.prototype. Of these, the first option is terrible garbage. Go with the second option. = Sparse Arrays = Another wrench in this mess is that Arrays aren't precisely like lists, because they do have indexes and may be sparse: var a = []; a[2] = 1; console.log(a); // [undefined, undefined, 1] The correct way to deal with this is: for (var ii = 0; ii < list.length; ii++) { if (list[ii] == undefined) { continue; } f(list[ii]); } Avoid sparse arrays if possible. = Ordered Maps = If you need an ordered map, you need to have a map for key-value associations and a list for key order. Don't try to build an ordered map using one Object or one Array. This generally applies for other complicated datatypes, as well; you need to build them out of more than one primitive. - diff --git a/src/docs/user/flavortext/javascript_pitfalls.diviner b/src/docs/user/flavortext/javascript_pitfalls.diviner index bee2b94675..a4815e8f3a 100644 --- a/src/docs/user/flavortext/javascript_pitfalls.diviner +++ b/src/docs/user/flavortext/javascript_pitfalls.diviner @@ -1,88 +1,87 @@ @title Javascript Pitfalls @group flavortext This document discusses pitfalls and flaws in the Javascript language, and how to avoid, work around, or at least understand them. = Implicit Semicolons = Javascript tries to insert semicolons if you forgot them. This is a pretty horrible idea. Notably, it can mask syntax errors by transforming subexpressions on their own lines into statements with no effect: lang=js string = "Here is a fairly long string that does not fit on one " "line. Note that I forgot the string concatenation operators " "so this will compile and execute with the wrong behavior. "; Here's what ECMA262 says about this: When, as the program is parsed ..., a token ... is encountered that is not allowed by any production of the grammar, then a semicolon is automatically inserted before the offending token if one or more of the following conditions is true: ... To protect yourself against this "feature", don't use it. Always explicitly insert semicolons after each statement. You should also prefer to break lines in places where insertion of a semicolon would not make the unparseable parseable, usually after operators. = ##with## is Bad News = ##with## is a pretty bad feature, for this reason among others: with (object) { property = 3; // Might be on object, might be on window: who knows. } Avoid ##with##. = ##arguments## is not an Array = You can convert ##arguments## to an array using JX.$A() or similar. Note that you can pass ##arguments## to Function.prototype.apply() without converting it. = Object, Array, and iteration are needlessly hard = There is essentially only one reasonable, consistent way to use these primitives but it is not obvious. Navigate these troubled waters with @{article:Javascript Object and Array}. = typeof null == "object" = This statement is true in Javascript: typeof null == 'object' This is pretty much a bug in the language that can never be fixed now. = Number, String, and Boolean objects = Like Java, Javascript has primitive versions of number, string, and boolean, and object versions. In Java, there's some argument for this distinction. In Javascript, it's pretty much completely worthless and the behavior of these objects is wrong. String and Boolean in particular are essentially unusable: lang=js "pancake" == "pancake"; // true new String("pancake") == new String("pancake"); // false var b = new Boolean(false); b; // Shows 'false' in console. !b; // ALSO shows 'false' in console. !b == b; // So this is true! !!b == !b // Negate both sides and it's false! FUCK! if (b) { // Better fucking believe this will get executed. } There is no advantage to using the object forms (the primitive forms behave like objects and can have methods and properties, and inherit from Array.prototype, Number.prototype, etc.) and their logical behavior is at best absurd and at worst strictly wrong. **Never use** ##new Number()##, ##new String()## or ##new Boolean()## unless your Javascript is God Tier and you are absolutely sure you know what you are doing. - diff --git a/src/docs/user/flavortext/things_you_should_do_now.diviner b/src/docs/user/flavortext/things_you_should_do_now.diviner index 2ccc53b893..d446c741c2 100644 --- a/src/docs/user/flavortext/things_you_should_do_now.diviner +++ b/src/docs/user/flavortext/things_you_should_do_now.diviner @@ -1,140 +1,138 @@ @title Things You Should Do Now @group flavortext Describes things you should do now when building software, because the cost to do them increases over time and eventually becomes prohibitive or impossible. = Overview = If you're building a hot new web startup, there are a lot of decisions to make about what to focus on. Most things you'll build will take about the same amount of time to build regardless of what order you build them in, but there are a few technical things which become vastly more expensive to fix later. If you don't do these things early in development, they'll become very hard or impossible to do later. This is basically a list of things that would have saved Facebook huge amounts of time and effort down the road if someone had spent a tiny amount of time on them earlier in the development process. See also @{article:Things You Should Do Soon} for things that scale less drastically over time. = Start IDs At a Gigantic Number = If you're using integer IDs to identify data or objects, **don't** start your IDs at 1. Start them at a huge number (e.g., 2^33) so that no object ID will ever appear in any other role in your application (like a count, a natural index, a byte size, a timestamp, etc). This takes about 5 seconds if you do it before you launch and rules out a huge class of nasty bugs for all time. It becomes incredibly difficult as soon as you have production data. The kind of bug that this causes is accidental use of some other value as an ID: COUNTEREXAMPLE // Load the user's friends, returns a map of friend_id => true $friend_ids = user_get_friends($user_id); // Get the first 8 friends. $first_few_friends = array_slice($friend_ids, 0, 8); // Render those friends. render_user_friends($user_id, array_keys($first_few_friends)); Because array_slice() in PHP discards array indices and renumbers them, this doesn't render the user's first 8 friends but the users with IDs 0 through 7, e.g. Mark Zuckerberg (ID 4) and Dustin Moskovitz (ID 6). If you have IDs in this range, sooner or later something that isn't an ID will get treated like an ID and the operation will be valid and cause unexpected behavior. This is completely avoidable if you start your IDs at a gigantic number. = Only Store Valid UTF-8 = For the most part, you can ignore UTF-8 and unicode until later. However, there is one aspect of unicode you should address now: store only valid UTF-8 strings. Assuming you're storing data internally as UTF-8 (this is almost certainly the right choice and definitely the right choice if you have no idea how unicode works), you just need to sanitize all the data coming into your application and make sure it's valid UTF-8. If your application emits invalid UTF-8, other systems (like browsers) will break in unexpected and interesting ways. You will eventually be forced to ensure you emit only valid UTF-8 to avoid these problems. If you haven't sanitized your data, you'll basically have two options: - do a huge migration on literally all of your data to sanitize it; or - forever sanitize all data on its way out on the read pathways. As of 2011 Facebook is in the second group, and spends several milliseconds of CPU time sanitizing every display string on its way to the browser, which multiplies out to hundreds of servers worth of CPUs sitting in a datacenter paying the price for the invalid UTF-8 in the databases. You can likely learn enough about unicode to be confident in an implementation which addresses this problem within a few hours. You don't need to learn everything, just the basics. Your language probably already has a function which does the sanitizing for you. = Never Design a Blacklist-Based Security System = When you have an alternative, don't design security systems which are default permit, blacklist-based, or otherwise attempt to enumerate badness. When Facebook launched Platform, it launched with a blacklist-based CSS filter, which basically tried to enumerate all the "bad" parts of CSS and filter them out. This was a poor design choice and lead to basically infinite security holes for all time. It is very difficult to enumerate badness in a complex system and badness is often a moving target. Instead of trying to do this, design whitelist-based security systems where you list allowed things and reject anything you don't understand. Assume things are bad until you verify that they're OK. It's tempting to design blacklist-based systems because they're easier to write and accept more inputs. In the case of the CSS filter, the product goal was for users to just be able to use CSS normally and feel like this system was no different from systems they were familiar with. A whitelist-based system would reject some valid, safe inputs and create product friction. But this is a much better world than the alternative, where the blacklist-based system fails to reject some dangerous inputs and creates //security holes//. It //also// creates product friction because when you fix those holes you break existing uses, and that backward-compatibility friction makes it very difficult to move the system from a blacklist to a whitelist. So you're basically in trouble no matter what you do, and have a bunch of security holes you need to unbreak immediately, so you won't even have time to feel sorry for yourself. Designing blacklist-based security is one of the worst now-vs-future tradeoffs you can make. See also "The Six Dumbest Ideas in Computer Security": http://www.ranum.com/security/computer_security/ = Fail Very Loudly when SQL Syntax Errors Occur in Production = This doesn't apply if you aren't using SQL, but if you are: detect when a query fails because of a syntax error (in MySQL, it is error 1064). If the failure happened in production, fail in the loudest way possible. (I implemented this in 2008 at Facebook and had it just email me and a few other people directly. The system was eventually refined.) This basically creates a high-signal stream that tells you where you have SQL injection holes in your application. It will have some false positives and could theoretically have false negatives, but at Facebook it was pretty high signal considering how important the signal is. Of course, the real solution here is to not have SQL injection holes in your application, ever. As far as I'm aware, this system correctly detected the one SQL injection hole we had from mid-2008 until I left in 2011, which was in a hackathon project on an underisolated semi-production tier and didn't use the query escaping system the rest of the application does. Hopefully, whatever language you're writing in has good query libraries that can handle escaping for you. If so, use them. If you're using PHP and don't have a solution in place yet, the Phabricator implementation of qsprintf() is similar to Facebook's system and was successful there. - - diff --git a/src/docs/user/userguide/arcanist_lint.diviner b/src/docs/user/userguide/arcanist_lint.diviner index 67f5513e38..4be3c402df 100644 --- a/src/docs/user/userguide/arcanist_lint.diviner +++ b/src/docs/user/userguide/arcanist_lint.diviner @@ -1,198 +1,197 @@ @title Arcanist User Guide: Lint @group userguide Guide to lint, linters, and linter configuration. This is a configuration guide that helps you set up advanced features. If you're just getting started, you don't need to look at this yet. Instead, start with the @{article:Arcanist User Guide}. This guide explains how lint works when configured in an `arc` project. If you haven't set up a project yet, do that first. For instructions, see @{article:Arcanist User Guide: Configuring a New Project}. = Overview = "Lint" refers to a general class of programming tools which analyze source code and raise warnings and errors about it. For example, a linter might raise warnings about syntax errors, uses of undeclared variables, calls to deprecated functions, spacing and formatting conventions, misuse of scope, implicit fallthrough in switch statements, missing license headers, use of dangerous language features, or a variety of other issues. Integrating lint into your development pipeline has two major benefits: - you can detect and prevent a large class of programming errors; and - you can simplify code review by addressing many mechanical and formatting problems automatically. When arc is integrated with a lint toolkit, it enables the `arc lint` command and runs lint on changes during `arc diff`. The user is prompted to fix errors and warnings before sending their code for review, and lint issues which are not fixed are visible during review. There are many lint and static analysis tools available for a wide variety of languages. Arcanist ships with bindings for many popular tools, and you can write new bindings fairly easily if you have custom tools. = Available Linters = Arcanist ships with bindings for these linters: - [[http://www.jshint.com/ | JSHint]], a Javascript linter based on JSHint. See @{class@arcanist:ArcanistJSHintLinter}. - [[http://pypi.python.org/pypi/pep8 | PEP8]], a Python linter. See @{class@arcanist:ArcanistPEP8Linter}. - [[http://pypi.python.org/pypi/pyflakes | Pyflakes]], another Python linter. See @{class@arcanist:ArcanistPyFlakesLinter}. - [[http://pypi.python.org/pypi/pylint | Pylint]], yet another Python linter. See @{class@arcanist:ArcanistPyLintLinter}. - [[http://pear.php.net/package/PHP_CodeSniffer | PHP CodeSniffer]], a PHP linter. See @{class@arcanist:ArcanistPhpcsLinter}. Arcanist also ships with generic bindings which can be configured to parse the output of a broad range of lint programs: - @{class@arcanist:ArcanistScriptAndRegexLinter}, which runs a script and parses its output with a regular expression. - @{class@arcanist:ArcanistConduitLinter}, which invokes a linter over Conduit and can allow you to build client/server linters. Additionally, Arcanist ships with some general purpose linters: - @{class@arcanist:ArcanistTextLinter}, which enforces basic things like trailing whitespace, DOS newlines, file encoding, line widths, terminal newlines, and tab literals. - @{class@arcanist:ArcanistSpellingLinter}, which can detect common spelling mistakes. - @{class@arcanist:ArcanistFilenameLinter}, which can enforce generally sensible rules about not giving files nonsense names. - @{class@arcanist:ArcanistLicenseLinter}, which can make sure license headers are applied to all source files. - @{class@arcanist:ArcanistNoLintLinter}, which can disable lint for files marked unlintable. - @{class@arcanist:ArcanistGeneratedLinter}, which can disable lint for generated files. Finally, Arcanist has special-purpose linters: - @{class@arcanist:ArcanistXHPASTLinter}, the PHP linter used by Phabricator itself. This linter is powerful, but somewhat rigid (it enforces phutil rules and isn't very configurable for other rulesets). - @{class@arcanist:ArcanistPhutilLibraryLinter}, which enforces phutil library layout rules. You can add support for new linters in three ways: - write new bindings and contribute them to the upstream; - write new bindings and install them alongside Arcanist; or - use a generic binding like @{class@arcanist:ArcanistScriptAndRegexLinter} and drive the integration through configuration. = Configuring Lint = Arcanist's lint integration involves two major components: linters and lint engines. Linters themselves are programs which detect problems in a source file. Usually a linter is an external script, which Arcanist runs and passes a path to, like `jshint` or `pep8.py`. The script emits some messages, and Arcanist parses the output into structured errors. A piece of glue code (like @{class@arcanist:ArcanistJSHintLinter} or @{class@arcanist:ArcanistPEP8Linter}) handles calling the external script and interpreting its output. Lint engines coordinate linters, and decide which linters should run on which files. For instance, you might want to run `jshint` on all your `.js` files, and `pep8.py` on all your `.py` files. And you might not want to lint anything in `externals/` or `third-party/`, and maybe there are other files which you want to exclude or apply special rules for. To configure arc for lint, you specify the name of a lint engine, and possibly provide some additional configuration. To name a lint engine, set `lint.engine` in your `.arcconfig` to the name of a class which extends @{class@arcanist:ArcanistLintEngine}. For more information on `.arcconfig`, see @{article:Arcanist User Guide: Configuring a New Project}. You can also set a default lint engine by setting `lint.engine` in your global user config with `arc set-config lint.engine`, or specify one explicitly with `arc lint --engine `. The available engines are: - @{class@arcanist:ComprehensiveLintEngine}, which runs a wide array of linters on many types of files. This is probably of limited use in any real project because it is overbroad, but is a good starting point for getting lint doing things. - @{class@arcanist:ArcanistSingleLintEngine}, which runs a single linter on every file unconditionally. This can be used with a glue linter like @{class@arcanist:ArcanistScriptAndRegexLinter} to put engine logic in an external script. - A custom engine you write. For most projects, lint rules are sufficiently specialized that this is the best option. For instructions on writing a custom lint engine, see @{article:Arcanist User Guide: Customizing Lint, Unit Tests and Workflows} and @{class@arcanist:ExampleLintEngine}. = Using Lint to Improve Code Review = Code review is most valuable when it's about the big ideas in a change. It is substantially less valuable when it devolves into nitpicking over style, formatting, and naming conventions. The best response to receiving a review request full of style problems is probably to reject it immediately, point the author at your coding convention documentation, and ask them to fix it before sending it for review. But even this is a pretty negative experience for both parties, and less experienced reviewers sometimes go through the whole review and point out every problem individually. Lint can greatly reduce the negativity of this whole experience (and the amount of time wasted arguing about these things) by enforcing style and formatting rules automatically. Arcanist supports linters that not only raise warnings about these problems, but provide patches and fix the problems for the author -- before the code goes to review. Good linter integration means that code is pretty much mechanically correct by the time any reviewer sees it, provides clear rules about style which are especially helpful to new authors, and has the overall effect of pushing discussion away from stylistic nitpicks and toward useful examination of large ideas. It can also provide a straightforward solution to arguments about style: - If a rule is important enough that it should be enforced, the proponent must add it to lint so it is automatically detected or fixed in the future and no one has to argue about it ever again. - If it's not important enough for them to do the legwork to add it to lint, they have to stop complaining about it. This may or may not be an appropriate methodology to adopt at your organization, but it generally puts the incentives in the right places. = Philosophy of Lint = Some general thoughts on how to develop lint effectively, based on building lint tools at Facebook: - Don't write regex-based linters to enforce language rules. Use a real parser or AST-based tool. This is not a domain you can get right at any nontrivial complexity with raw regexes. That is not a challenge. Just don't do this. - False positives are pretty bad and should be avoided. You should aim to implement only rules that have very few false positives, and provide ways to mark false positives as OK. If running lint always raises 30 warnings about irrelevant nonsense, it greatly devalues the tool. - Move toward autocorrect rules. Most linters do not automatically correct the problems they detect, but Arcanist supports this and it's quite valuable to have the linter not only say "the convention is to put a space after comma in a function call" but to fix it for you. = Next Steps = Continue by: - integrating and customizing built-in linters and lint bindings with @{article:Arcanist User Guide: Customizing Existing Linters}; or - learning how to add new linters and lint engines with @{article:Arcanist User Guide: Customizing Lint, Unit Tests and Workflows}. - diff --git a/src/docs/user/userguide/differential_test_plans.diviner b/src/docs/user/userguide/differential_test_plans.diviner index 645578093d..81992097a2 100644 --- a/src/docs/user/userguide/differential_test_plans.diviner +++ b/src/docs/user/userguide/differential_test_plans.diviner @@ -1,67 +1,65 @@ @title Differential User Guide: Test Plans @group userguide This document describes things you should think about when developing a test plan. = Overview = When you send a revision for review in Differential you must include a test plan (this can be disabled or made optional in the config). A test plan is a repeatable list of steps which document what you have done to verify the behavior of a change. A good test plan convinces a reviewer that you have been thorough in making sure your change works as intended and has enough detail to allow someone unfamiliar with your change to verify its behavior. This document has some common things to think about when developing or reviewing a test plan. Some of these suggestions might not be applicable to the software you are writing; they are adapted from Facebook's internal documentation. = All Changes = - **Error Handling:** Are errors detected and handled properly? How does your change deal with error cases? Did you test them and make sure you got the right error messages and the right behavior? It's important that you test what happens when things go wrong, not just that the change works if everything else also works. - **Service Impact:** How does your change affect services like memcache, thrift, or databases? Are you adding new cachekeys or queries? Will this change add a lot of load to services? - **Performance:** How does your change affect performance? **NOTE**: If your change is a performance-motivated change, you should include measurements, profiles or other data in your test plan proving that you have improved performance. - **Unit Tests:** Is your change adequately covered by unit tests? Could you improve test coverage? If you're fixing a bug, did you add a test to prevent it from happening again? Are the unit tests testing just the code in question, or would a failure of a database or network service cause your test to fail? - **Concurrent Change Robustness:** If you're making a refactoring change, is it robust against people introducing new calls between the time you started the change and when you commit it? For example, if you change the parameter order of some function from ##f(a, b)## to ##f(b, a)## and a new callsite is introduced in the meantime, could it go unnoticed? How bad would that be? (Because of this risk, you should almost never make parameter order changes in weakly typed languages like PHP and Javascript.) - **Revert Plan:** If your change needs to be reverted and you aren't around, are any special steps or considerations that the reverter needs to know about? If there are, make sure they're adequately described in the "Revert Plan" field so someone without any knowledge of your patch (but with a general knowledge of the system) can successfully revert your change. - **Security:** Is your change robust against XSS, CSRF, and injection attacks? Are you verifying the user has the right capabilities or permissions? Are you consistently treating user data as untrustworthy? Are you escaping data properly, and using dangerous functions only when they are strictly necessary? - **Architecture:** Is this the right change? Could there be a better way to solve the problem? Have you talked to (or added as reviewers) domain experts if you aren't one yourself? What are the limitations of this solution? What tradeoffs did you make, and why? = Frontend / User-Facing Changes = - **Static Resources:** Will your change cause the application to serve more JS or CSS? Can you use less JS/CSS, or reuse more? - **Browsers:** Have you tested your change in multiple browsers? - - diff --git a/src/docs/user/userguide/herald.diviner b/src/docs/user/userguide/herald.diviner index 66272fb325..b2e11d65e4 100644 --- a/src/docs/user/userguide/herald.diviner +++ b/src/docs/user/userguide/herald.diviner @@ -1,99 +1,98 @@ @title Herald User Guide @group userguide Use Herald to get notified of changes you care about. = Overview = Herald allows you to write processing rules that take effect when objects (such as Differential revisions and commits) are created or updated. For instance, you might want to get notified every time someone sends out a revision that affects some file you're interested in, even if they didn't add you as a reviewer. Herald is less useful for small organizations (where everyone will generally know most of what's going on) but the usefulness of the application increases as an organization scales. Once there is too much activity to keep track of it all, Herald allows you to filter it down so you're only notified of things you are interested in. = Global and Personal Rules = You can create two kinds of Herald rules, //global// and //personal//: - **Personal Rules** are rules you own, but they can only affect you. Only you can edit or delete personal rules, but their actions are limited to adding you to CC, subscribing you, etc. - **Global Rules** are rules everyone owns, and they can affect anything. Anyone can edit or delete a global rule, and they can take any action, including affecting projects and mailing lists. The general idea is to prevent individuals from controlling rules that affect shared resources, so if a rule needs to be updated it's not a big deal if the person who created it is on vacation. = Rules, Conditions and Actions = The best way to think of Herald is as a system similar to the mail rules you can set up in most email clients, to organize mail based on "To", "Subject", etc. Herald works very similarly, but operates on Phabricator objects (like revisions and commits) instead of emails. Every time an object is created or updated, Herald rules are run on it and the actions for any matching rules are taken. To create a new Herald rule, choose which type of event you want to act on (e.g., changes to Differential Revisions, or Commits), and then set a list of conditions. For example, you might add the condition ##Author is alincoln (Abraham Lincoln)## to keep track of everything alincoln does. Finally, set a list of actions to take when the conditions match, like adding yourself to the CC list. Now you'll automatically be added to CC any time alincoln creates a revision, and can keep an eye on what he's up to. = Available Actions = Herald rules can take a number of actions. Note that some actions are only available from Global rules, and others only from Personal rules. Additionally, not every action is available for every object type (for instance, you can not trigger an audit based on a Differential revision). - **Add CC**: Add a user or mailing list to the CC list for the object. For personal rules, you can only add yourself. - **Remove CC**: Remove a user or mailing list from the CC list for the object. For personal rules, you can only remove yourself. - **Send an Email to**: Send one email, but don't subscribe to other updates. For personal rules, you can only email yourself. - **Trigger an Audit**: For commits, trigger an audit request for a project or user. For personal rules, you can only trigger an audit request to yourself. - **Mark with flag**: Flag the object for later review. This action is only available on personal rules. If an object already has a flag, this action will not add another flag. - **Do Nothing**: Don't do anything. This can be used to disable a rule temporarily, or to create a rule for an "Another Herald rule" condition. = Testing Rules = When you've created a rule, use the "Test Console" to test it out. Enter a revision or commit and Herald will do a dry run against that object, showing you which rules //would// match had it actually been updated. Dry runs executed via the test console don't take any actions. = Advanced Herald = A few features in Herald are particularly complicated: - **matches regexp pair**: for Differential revisions, you can set a condition like "Any changed file content matches regexp pair...". This allows you to specify two regexes in JSON format. The first will be used to match the filename of the changed file; the second will be used to match the content. For example, if you want to match revisions which add or remove calls to a "muffinize" function, //but only in JS files//, you can set the value to ##["/\\.js$/", "/muffinize/"]## or similar. - **Another Herald rule**: you can create Herald rules which depend on other rules. This can be useful if you need to express a more complicated predicate than "all" vs "any" allows, or have a common set of conditions which you want to share between several rules. If a rule is only being used as a group of conditions, you can set the action to "Do Nothing". - diff --git a/src/docs/user/userguide/mail_rules.diviner b/src/docs/user/userguide/mail_rules.diviner index b5c8253bb0..2000d6d160 100644 --- a/src/docs/user/userguide/mail_rules.diviner +++ b/src/docs/user/userguide/mail_rules.diviner @@ -1,82 +1,81 @@ @title User Guide: Managing Phabricator Email @group userguide How to effectively manage Phabricator email notifications. = Overview = Phabricator uses email as a major notification channel, but the amount of email it sends can seem overwhelming if you're working on an active team. This document discusses some strategies for managing email. By far the best approach to managing mail is to **write mail rules** to categorize mail. Essentially all modern mail clients allow you to quickly write sophisticated rules to route, categorize, or delete email. = Reducing Email = You can reduce the amount of email you receive by turning off some types of email in ##Settings -> Email Preferences##. For example, you can turn off email produced by your own actions (like when you comment on a revision), and some types of less-important notifications about events. = Mail Rules = The best approach to managing mail is to write mail rules. Simply writing rules to move mail from Differential, Maniphest and Herald to separate folders will vastly simplify mail management. Phabricator also sets a large number of headers (see below) which can allow you to write more sophisticated mail rules. = Mail Headers = Phabricator sends a variety of mail headers that can be useful in crafting rules to route and manage mail. Headers in plural contain lists. A list containing two items, ##1## and ##15## will generally be formatted like this: X-Header: <1>, <15> The intent is to allow you to write a rule which matches against "<1>". If you just match against "1", you'll incorrectly match "15", but matching "<1>" will correctly match only "<1>". Some other headers use a single value but can be presented multiple times. It is to support e-mail clients which are not able to create rules using regular expressions or wildcards (namely Outlook). The headers Phabricator adds to mail are: - ##X-Phabricator-Sent-This-Message##: this is attached to all mail Phabricator sends. You can use it to differentiate between email from Phabricator and replies/forwards of Phabricator mail from human beings. - ##X-Phabricator-To##: this is attached to all mail Phabricator sends. It shows the PHIDs of the original "To" line, before any mutation by the mailer configuration. - ##X-Phabricator-Cc##: this is attached to all mail Phabricator sends. It shows the PHIDs of the original "Cc" line, before any mutation by the mailer configuration. - ##X-Differential-Author##: this is attached to Differential mail and shows the revision's author. You can use it to filter mail about your revisions (or other users' revisions). - ##X-Differential-Reviewer##: this is attached to Differential mail and shows the reviewers. You can use it to filter mail about revisions you are reviewing, versus revisions you are explicitly CC'd on or CC'd as a result of Herald rules. - ##X-Differential-Reviewers##: list version of the previous. - ##X-Differential-CC##: this is attached to Differential mail and shows the CCs on the revision. - ##X-Differential-CCs##: list version of the previous. - ##X-Differential-Explicit-CC##: this is attached to Differential mail and shows the explicit CCs on the revision (those that were added by humans, not by Herald). - ##X-Differential-Explicit-CCs##: list version of the previous. - ##X-Phabricator-Mail-Tags##: this is attached to some mail and has a list of descriptors about the mail. (This is fairly new and subject to some change.) - ##X-Herald-Rules##: this is attached to some mail and shows Herald rule IDs which have triggered for the object. You can use this to sort or categorize mail that has triggered specific rules. - diff --git a/src/docs/user/userguide/maniphest_custom.diviner b/src/docs/user/userguide/maniphest_custom.diviner index 0193d52002..f285fe69d4 100644 --- a/src/docs/user/userguide/maniphest_custom.diviner +++ b/src/docs/user/userguide/maniphest_custom.diviner @@ -1,65 +1,64 @@ @title Maniphest User Guide: Adding Custom Fields @group userguide How to add custom fields to Maniphest. = Overview = Maniphest provides some support for adding new fields to tasks, like an "cost" field, a "milestone" field, etc. NOTE: Currently, these fields are somewhat limited. They primarily give you a structured way to record data on tasks, but there isn't much support for bringing them into other interfaces (e.g., querying by them, aggregating them, drawing graphs, etc.). If you have a use case, let us know what you want to do and maybe we can figure something out. This data is also exposed via the Conduit API, so you might be able to write your own interface if you want to do something very custom. = Simple Field Customization = If you don't need complicated display controls or sophisticated validation, you can add simple fields. These allow you to attach things like strings, numbers, and dropdown menus to the task template. Customize Maniphest fields by setting `maniphest.custom-field-definitions` in your configuration. For example, suppose you want to add "Estimated Hours" and "Actual Hours" fields. To do this, set your configuration like this: 'maniphest.custom-fields' => array( 'mycompany:estimated-hours' => array( 'name' => 'Estimated Hours', 'type' => 'int', 'caption' => 'Estimated number of hours this will take.', 'required' => true, ), 'mycompany:actual-hours' => array( 'name' => 'Actual Hours', 'type' => 'int', ), ) Each array key must be unique, and is used to organize the internal storage of the field. These options are available: - **name**: Display label for the field on the edit and detail interfaces. - **type**: Field type. The supported field types are: - **int**: An integer, rendered as a text field. - **text**: A string, rendered as a text field. - **bool**: A boolean value, rendered as a checkbox. - **select**: Allows the user to select from several options, rendered as a dropdown. - **remarkup**: A text area which allows the user to enter markup. - **users**: A typeahead which allows multiple users to be input. - **date**: A date/time picker. - **header**: Renders a visual divider which you can use to group fields. - **caption**: A caption to display underneath the field (optional). - **required**: True if the user should be required to provide a value. - **options**: If type is set to **select**, provide options for the dropdown as a dictionary. - **default**: Default field value. - **copy**: When a user creates a task, the UI gives them an option to "Create Another Similar Task". Some fields from the original task are copied into the new task, while others are not; by default, fields are not copied. If you want this field to be copied, specify `true` for the `copy` property. - diff --git a/src/docs/user/userguide/phame.diviner b/src/docs/user/userguide/phame.diviner index 1cf22b1b53..64e17d421f 100644 --- a/src/docs/user/userguide/phame.diviner +++ b/src/docs/user/userguide/phame.diviner @@ -1,56 +1,56 @@ @title Phame User Guide @group userguide Journal about your thoughts and feelings. Share with others. Profit. = Overview = Phame is a simple blogging platform. You can write drafts which only you can see. Later, you can publish these drafts as posts which anyone who can access -the Phabricator instance can see. You can also add posts to blogs to increase +the Phabricator instance can see. You can also add posts to blogs to increase your distribution. -Overall, Phame is intended to help an individual spread their message. As +Overall, Phame is intended to help an individual spread their message. As such, pertinent design decisions skew towards favoring the individual -rather than the collective. +rather than the collective. = Drafts = -Drafts are completely private so draft away. +Drafts are completely private so draft away. -= Posts = += Posts = Posts are accessible to anyone who has access to the Phabricator instance. = Blogs = Blogs are collections of posts. Each blog has associated metadata like a name, description, and set of bloggers who can add posts to the blog. Each blogger can also edit metadata about the blog and delete the blog outright. Soon, blogs will be useful for powering external websites, like blog.yourcompany.com -by making pertinent configuration changes with your DNS authority and +by making pertinent configuration changes with your DNS authority and Phabricator instance. -NOTE: removing a blogger from a given blog does not remove their posts that +NOTE: removing a blogger from a given blog does not remove their posts that are already associated with the blog. Rather, it removes their ability to edit metadata about and add posts to the blog. = Comment Widgets = Phame supports comment widgets from Facebook and Disqus. The adminstrator of the Phabricator instance must properly configure Phabricator to enable this functionality. A given comment widget is tied 1:1 with a given post. This means the same instance of a given comment widget will appear for a given post regardless of whether that post is being viewed in the context of a blog. -= Next Steps = += Next Steps = - Phame is extremely new and very basic for now. Give us feedback on what you'd like to see improve! See @{article:Give Feedback! Get Support!}. diff --git a/src/infrastructure/__tests__/PhabricatorInfrastructureTestCase.php b/src/infrastructure/__tests__/PhabricatorInfrastructureTestCase.php index 47cdbf14fe..68822568ae 100644 --- a/src/infrastructure/__tests__/PhabricatorInfrastructureTestCase.php +++ b/src/infrastructure/__tests__/PhabricatorInfrastructureTestCase.php @@ -1,104 +1,103 @@ true, ); } /** * This is more of an acceptance test case instead of a unittest. It verifies * that all symbols can be loaded correctly. It can catch problems like * missing methods in descendants of abstract base classes. */ public function testEverythingImplemented() { id(new PhutilSymbolLoader())->selectAndLoadSymbols(); } public function testApplicationsInstalled() { $all = PhabricatorApplication::getAllApplications(); $installed = PhabricatorApplication::getAllInstalledApplications(); $this->assertEqual( count($all), count($installed), 'In test cases, all applications should default to installed.'); } public function testMySQLAgreesWithUsAboutBMP() { // Build a string with every BMP character in it, then insert it into MySQL // and read it back. We expect to get the same string out that we put in, // demonstrating that strings which pass our BMP checks are also valid in // MySQL and no silent data truncation will occur. $buf = ''; for ($ii = 0x01; $ii <= 0x7F; $ii++) { $buf .= chr($ii); } for ($ii = 0xC2; $ii <= 0xDF; $ii++) { for ($jj = 0x80; $jj <= 0xBF; $jj++) { $buf .= chr($ii).chr($jj); } } // NOTE: This is \xE0\xA0\xZZ. for ($ii = 0xE0; $ii <= 0xE0; $ii++) { for ($jj = 0xA0; $jj <= 0xBF; $jj++) { for ($kk = 0x80; $kk <= 0xBF; $kk++) { $buf .= chr($ii).chr($jj).chr($kk); } } } // NOTE: This is \xE1\xZZ\xZZ through \xEF\xZZ\xZZ. for ($ii = 0xE1; $ii <= 0xEF; $ii++) { for ($jj = 0x80; $jj <= 0xBF; $jj++) { for ($kk = 0x80; $kk <= 0xBF; $kk++) { $buf .= chr($ii).chr($jj).chr($kk); } } } $this->assertEqual(194431, strlen($buf)); $this->assertEqual(true, phutil_is_utf8_with_only_bmp_characters($buf)); $write = id(new HarbormasterScratchTable()) ->setData('all.utf8.bmp') ->setBigData($buf) ->save(); $read = id(new HarbormasterScratchTable())->load($write->getID()); $this->assertEqual($buf, $read->getBigData()); } public function testRejectMySQLBMPQueries() { $table = new HarbormasterScratchTable(); $conn_r = $table->establishConnection('w'); $snowman = "\xE2\x98\x83"; $gclef = "\xF0\x9D\x84\x9E"; qsprintf($conn_r, 'SELECT %B', $snowman); qsprintf($conn_r, 'SELECT %s', $snowman); qsprintf($conn_r, 'SELECT %B', $gclef); $caught = null; try { qsprintf($conn_r, 'SELECT %s', $gclef); } catch (AphrontQueryCharacterSetException $ex) { $caught = $ex; } $this->assertEqual( true, ($caught instanceof AphrontQueryCharacterSetException)); } } - diff --git a/src/infrastructure/celerity/CeleritySpriteGenerator.php b/src/infrastructure/celerity/CeleritySpriteGenerator.php index 95102cf5ce..42e33c9ca6 100644 --- a/src/infrastructure/celerity/CeleritySpriteGenerator.php +++ b/src/infrastructure/celerity/CeleritySpriteGenerator.php @@ -1,867 +1,865 @@ getDirectoryList('icons_1x'); $colors = array( '', 'grey', 'white', ); $scales = array( '1x' => 1, '2x' => 2, ); $template = id(new PhutilSprite()) ->setSourceSize(14, 14); $sprites = array(); foreach ($colors as $color) { foreach ($icons as $icon) { $prefix = 'icons_'; if (strlen($color)) { $prefix .= $color.'_'; } $suffix = ''; if (strlen($color)) { $suffix = '-'.$color; } $sprite = id(clone $template) ->setName('icons-'.$icon.$suffix); $tcss = array(); $tcss[] = '.icons-'.$icon.$suffix; if ($color == 'white') { $tcss[] = '.device-desktop .phabricator-action-view:hover '. '.icons-'.$icon; $tcss[] = '.device-desktop .phui-list-sidenav '. '.phui-list-item-href:hover .icons-'.$icon; } $sprite->setTargetCSS(implode(', ', $tcss)); foreach ($scales as $scale_key => $scale) { $path = $this->getPath($prefix.$scale_key.'/'.$icon.'.png'); $sprite->setSourceFile($path, $scale); } $sprites[] = $sprite; } } $remarkup_icons = $this->getDirectoryList('remarkup_1x'); foreach ($remarkup_icons as $icon) { $prefix = 'remarkup_'; // Strip 'text_' from these file names. $class_name = substr($icon, 5); if ($class_name == 'fullscreen_off') { $tcss = '.remarkup-control-fullscreen-mode .remarkup-assist-fullscreen'; } else { $tcss = '.remarkup-assist-'.$class_name; } $sprite = id(clone $template) ->setName('remarkup-assist-'.$icon) ->setTargetCSS($tcss); foreach ($scales as $scale_key => $scale) { $path = $this->getPath($prefix.$scale_key.'/'.$icon.'.png'); $sprite->setSourceFile($path, $scale); } $sprites[] = $sprite; } $sheet = $this->buildSheet('icons', true); $sheet->setScales($scales); foreach ($sprites as $sprite) { $sheet->addSprite($sprite); } return $sheet; } public function buildActionsSheet() { $icons = $this->getDirectoryList('actions_white_1x'); $colors = array( 'dark', 'grey', 'white', ); $scales = array( '1x' => 1, '2x' => 2, ); $template = id(new PhutilSprite()) ->setSourceSize(24, 24); $sprites = array(); foreach ($colors as $color) { foreach ($icons as $icon) { $prefix = 'actions_'; if (strlen($color)) { $prefix .= $color.'_'; } $suffix = ''; if (strlen($color)) { $suffix = '-'.$color; } $sprite = id(clone $template) ->setName('actions-'.$icon.$suffix); $tcss = array(); $tcss[] = '.actions-'.$icon.$suffix; if ($color == 'dark') { $tcss[] = '.device-desktop '. '.actions-'.$icon.'-grey.phui-icon-view:hover'; } $sprite->setTargetCSS(implode(', ', $tcss)); foreach ($scales as $scale_key => $scale) { $path = $this->getPath($prefix.$scale_key.'/'.$icon.'.png'); $sprite->setSourceFile($path, $scale); } $sprites[] = $sprite; } } $sheet = $this->buildSheet('actions', true); $sheet->setScales($scales); foreach ($sprites as $sprite) { $sheet->addSprite($sprite); } return $sheet; } public function buildMiniconsSheet() { $icons = $this->getDirectoryList('minicons_white_1x'); $colors = array( 'white', 'dark', ); $scales = array( '1x' => 1, '2x' => 2, ); $template = id(new PhutilSprite()) ->setSourceSize(16, 16); $sprites = array(); foreach ($colors as $color) { foreach ($icons as $icon) { $prefix = 'minicons_'; if (strlen($color)) { $prefix .= $color.'_'; } $suffix = ''; if (strlen($color)) { $suffix = '-'.$color; } $sprite = id(clone $template) ->setName('minicons-'.$icon.$suffix); $sprite->setTargetCSS('.minicons-'.$icon.$suffix); foreach ($scales as $scale_key => $scale) { $path = $this->getPath($prefix.$scale_key.'/'.$icon.'.png'); $sprite->setSourceFile($path, $scale); } $sprites[] = $sprite; } } $sheet = $this->buildSheet('minicons', true); $sheet->setScales($scales); foreach ($sprites as $sprite) { $sheet->addSprite($sprite); } return $sheet; } public function buildMenuSheet() { $sprites = array(); $sources = array( 'seen_read_all' => array( 'x' => 18, 'y' => 18, 'css' => '.alert-notifications .phabricator-main-menu-alert-icon', ), 'seen_have_unread' => array( 'x' => 18, 'y' => 18, 'css' => '.alert-notifications:hover .phabricator-main-menu-alert-icon', ), 'unseen_any' => array( 'x' => 18, 'y' => 18, 'css' => '.alert-notifications.alert-unread .phabricator-main-menu-alert-icon', ), 'arrow-right' => array( 'x' => 9, 'y' => 31, 'css' => '.phabricator-crumb-divider', ), 'search' => array( 'x' => 24, 'y' => 24, 'css' => '.menu-icon-search', ), 'search_blue' => array( 'x' => 24, 'y' => 24, 'css' => '.menu-icon-search-blue', ), 'new' => array( 'x' => 24, 'y' => 24, 'css' => '.menu-icon-new', ), 'new_blue' => array( 'x' => 24, 'y' => 24, 'css' => '.menu-icon-new-blue', ), 'app' => array( 'x' => 24, 'y' => 24, 'css' => '.menu-icon-app', ), 'app_blue' => array( 'x' => 24, 'y' => 24, 'css' => '.menu-icon-app-blue', ), 'logo' => array( 'x' => 149, 'y' => 26, 'css' => '.phabricator-main-menu-logo-image', ), 'conf-off' => array( 'x' => 18, 'y' => 18, 'css' => '.alert-notifications .phabricator-main-menu-message-icon', ), 'conf-hover' => array( 'x' => 18, 'y' => 18, 'css' => '.alert-notifications:hover .phabricator-main-menu-message-icon', ), 'conf-unseen' => array( 'x' => 18, 'y' => 18, 'css' => '.alert-notifications.message-unread '. '.phabricator-main-menu-message-icon', ), ); $scales = array( '1x' => 1, '2x' => 2, ); $template = new PhutilSprite(); foreach ($sources as $name => $spec) { $sprite = id(clone $template) ->setName($name) ->setSourceSize($spec['x'], $spec['y']) ->setTargetCSS($spec['css']); foreach ($scales as $scale_name => $scale) { $path = 'menu_'.$scale_name.'/'.$name.'.png'; $path = $this->getPath($path); $sprite->setSourceFile($path, $scale); } $sprites[] = $sprite; } $sheet = $this->buildSheet('menu', true); $sheet->setScales($scales); foreach ($sprites as $sprite) { $sheet->addSprite($sprite); } return $sheet; } public function buildTokenSheet() { $icons = $this->getDirectoryList('tokens_1x'); $scales = array( '1x' => 1, '2x' => 2, ); $template = id(new PhutilSprite()) ->setSourceSize(16, 16); $sprites = array(); $prefix = 'tokens_'; foreach ($icons as $icon) { $sprite = id(clone $template) ->setName('tokens-'.$icon) ->setTargetCSS('.tokens-'.$icon); foreach ($scales as $scale_key => $scale) { $path = $this->getPath($prefix.$scale_key.'/'.$icon.'.png'); $sprite->setSourceFile($path, $scale); } $sprites[] = $sprite; } $sheet = $this->buildSheet('tokens', true); $sheet->setScales($scales); foreach ($sprites as $sprite) { $sheet->addSprite($sprite); } return $sheet; } public function buildButtonBarSheet() { $icons = $this->getDirectoryList('button_bar_1x'); $scales = array( '1x' => 1, '2x' => 2, ); $template = id(new PhutilSprite()) ->setSourceSize(14, 14); $sprites = array(); $prefix = 'button_bar_'; foreach ($icons as $icon) { $sprite = id(clone $template) ->setName('buttonbar-'.$icon) ->setTargetCSS('.buttonbar-'.$icon); foreach ($scales as $scale_key => $scale) { $path = $this->getPath($prefix.$scale_key.'/'.$icon.'.png'); $sprite->setSourceFile($path, $scale); } $sprites[] = $sprite; } $sheet = $this->buildSheet('buttonbar', true); $sheet->setScales($scales); foreach ($sprites as $sprite) { $sheet->addSprite($sprite); } return $sheet; } public function buildProjectsSheet() { $icons = $this->getDirectoryList('projects_1x'); $scales = array( '1x' => 1, '2x' => 2, ); $template = id(new PhutilSprite()) ->setSourceSize(50, 50); $sprites = array(); $prefix = 'projects-'; foreach ($icons as $icon) { $sprite = id(clone $template) ->setName($prefix.$icon) ->setTargetCSS('.'.$prefix.$icon); foreach ($scales as $scale_key => $scale) { $path = $this->getPath('projects_'.$scale_key.'/'.$icon.'.png'); $sprite->setSourceFile($path, $scale); } $sprites[] = $sprite; } $sheet = $this->buildSheet('projects', true); $sheet->setScales($scales); foreach ($sprites as $sprite) { $sheet->addSprite($sprite); } return $sheet; } public function buildPaymentsSheet() { $icons = $this->getDirectoryList('payments_2x'); $scales = array( '2x' => 1, ); $template = id(new PhutilSprite()) ->setSourceSize(60, 32); $sprites = array(); $prefix = 'payments_'; foreach ($icons as $icon) { $sprite = id(clone $template) ->setName('payments-'.$icon) ->setTargetCSS('.payments-'.$icon); foreach ($scales as $scale_key => $scale) { $path = $this->getPath($prefix.$scale_key.'/'.$icon.'.png'); $sprite->setSourceFile($path, $scale); } $sprites[] = $sprite; } $sheet = $this->buildSheet('payments', true); $sheet->setScales($scales); foreach ($sprites as $sprite) { $sheet->addSprite($sprite); } return $sheet; } public function buildConpherenceSheet() { $name = 'conpherence'; $icons = $this->getDirectoryList($name.'_1x'); $scales = array( '1x' => 1, '2x' => 2, ); $template = id(new PhutilSprite()) ->setSourceSize(32, 32); $sprites = array(); foreach ($icons as $icon) { $color = preg_match('/_on/', $icon) ? 'on' : 'off'; $prefix = $name.'_'; $sprite = id(clone $template) ->setName($prefix.$icon); $tcss = array(); $tcss[] = '.'.$prefix.$icon; if ($color == 'on') { $class = str_replace('_on', '_off', $prefix.$icon); $tcss[] = '.device-desktop .'.$class.':hover '; } $sprite->setTargetCSS(implode(', ', $tcss)); foreach ($scales as $scale_key => $scale) { $path = $this->getPath($prefix.$scale_key.'/'.$icon.'.png'); $sprite->setSourceFile($path, $scale); } $sprites[] = $sprite; } $sheet = $this->buildSheet($name, true); $sheet->setScales($scales); foreach ($sprites as $sprite) { $sheet->addSprite($sprite); } return $sheet; } public function buildDocsSheet() { $icons = $this->getDirectoryList('docs_1x'); $scales = array( '1x' => 1, '2x' => 2, ); $template = id(new PhutilSprite()) ->setSourceSize(32, 32); $sprites = array(); $prefix = 'docs_'; foreach ($icons as $icon) { $sprite = id(clone $template) ->setName($prefix.$icon) ->setTargetCSS('.'.$prefix.$icon); foreach ($scales as $scale_key => $scale) { $path = $this->getPath($prefix.$scale_key.'/'.$icon.'.png'); $sprite->setSourceFile($path, $scale); } $sprites[] = $sprite; } $sheet = $this->buildSheet('docs', true); $sheet->setScales($scales); foreach ($sprites as $sprite) { $sheet->addSprite($sprite); } return $sheet; } public function buildLoginSheet() { $icons = $this->getDirectoryList('login_1x'); $scales = array( '1x' => 1, '2x' => 2, ); $template = id(new PhutilSprite()) ->setSourceSize(34, 34); $sprites = array(); $prefix = 'login_'; foreach ($icons as $icon) { $sprite = id(clone $template) ->setName('login-'.$icon) ->setTargetCSS('.login-'.$icon); foreach ($scales as $scale_key => $scale) { $path = $this->getPath($prefix.$scale_key.'/'.$icon.'.png'); $sprite->setSourceFile($path, $scale); } $sprites[] = $sprite; } $sheet = $this->buildSheet('login', true); $sheet->setScales($scales); foreach ($sprites as $sprite) { $sheet->addSprite($sprite); } return $sheet; } public function buildStatusSheet() { $icons = $this->getDirectoryList('status_1x'); $scales = array( '1x' => 1, '2x' => 2, ); $template = id(new PhutilSprite()) ->setSourceSize(14, 14); $sprites = array(); $prefix = 'status_'; $extra_css = array( 'policy-custom-white' => ', .dropdown-menu-item:hover .status-policy-custom', 'policy-all-white' => ', .dropdown-menu-item:hover .status-policy-all', 'policy-unknown-white' => ', .dropdown-menu-item:hover .status-policy-unknown', 'policy-admin-white' => ', .dropdown-menu-item:hover .status-policy-admin', 'policy-public-white' => ', .dropdown-menu-item:hover .status-policy-public', 'policy-project-white' => ', .dropdown-menu-item:hover .status-policy-project', 'policy-noone-white' => ', .dropdown-menu-item:hover .status-policy-noone', ); foreach ($icons as $icon) { $sprite = id(clone $template) ->setName('status-'.$icon) ->setTargetCSS('.status-'.$icon.idx($extra_css, $icon)); foreach ($scales as $scale_key => $scale) { $path = $this->getPath($prefix.$scale_key.'/'.$icon.'.png'); $sprite->setSourceFile($path, $scale); } $sprites[] = $sprite; } $sheet = $this->buildSheet('status', true); $sheet->setScales($scales); foreach ($sprites as $sprite) { $sheet->addSprite($sprite); } return $sheet; } public function buildGradientSheet() { $gradients = $this->getDirectoryList('gradients'); $template = new PhutilSprite(); $unusual_heights = array( 'dark-menu-label' => 25, 'breadcrumbs' => 31, 'menu-label' => 24, 'red-header' => 70, 'blue-header' => 70, 'green-header' => 70, 'yellow-header' => 70, 'grey-header' => 70, 'dark-grey-header' => 70, 'lightblue-header' => 240, ); $extra_css = array( 'dark-menu-label' => ', .phabricator-dark-menu .phui-list-item-type-label', 'menu-label' => ', .phabricator-side-menu .phui-list-item-type-label', ); $sprites = array(); foreach ($gradients as $gradient) { $path = $this->getPath('gradients/'.$gradient.'.png'); $sprite = id(clone $template) ->setName('gradient-'.$gradient) ->setSourceFile($path) ->setTargetCSS('.gradient-'.$gradient.idx($extra_css, $gradient)); $sprite->setSourceSize(4, idx($unusual_heights, $gradient, 26)); $sprites[] = $sprite; } $sheet = $this->buildSheet( 'gradient', false, PhutilSpriteSheet::TYPE_REPEAT_X, ', .phabricator-dark-menu .phui-list-item-type-label, '. '.phabricator-side-menu .phui-list-item-type-label'); foreach ($sprites as $sprite) { $sheet->addSprite($sprite); } return $sheet; } public function buildMainHeaderSheet() { $gradients = $this->getDirectoryList('main_header'); $template = new PhutilSprite(); $sprites = array(); foreach ($gradients as $gradient) { $path = $this->getPath('main_header/'.$gradient.'.png'); $sprite = id(clone $template) ->setName('main-header-'.$gradient) ->setSourceFile($path) ->setTargetCSS('.main-header-'.$gradient); $sprite->setSourceSize(6, 44); $sprites[] = $sprite; } $sheet = $this->buildSheet('main-header', false, PhutilSpriteSheet::TYPE_REPEAT_X); foreach ($sprites as $sprite) { $sheet->addSprite($sprite); } return $sheet; } public function buildAppsSheet() { return $this->buildAppsSheetVariant(1); } public function buildAppsLargeSheet() { return $this->buildAppsSheetVariant(2); } public function buildAppsXLargeSheet() { return $this->buildAppsSheetVariant(3); } private function buildAppsSheetVariant($variant) { if ($variant == 1) { $scales = array( '1x' => 1, '2x' => 2, ); $variant_name = 'apps'; $variant_short = ''; $size_x = 14; $size_y = 14; $colors = array( 'dark' => 'dark', 'white' => 'white', ); } else if ($variant == 2) { $scales = array( '2x' => 1, '4x' => 2, ); $variant_name = 'apps-large'; $variant_short = '-large'; $size_x = 28; $size_y = 28; $colors = array( 'light' => 'lb', 'dark' => 'dark', 'blue' => 'blue', 'white' => 'white', ); } else { $scales = array( '4x' => 1, ); $variant_name = 'apps-xlarge'; $variant_short = '-xlarge'; $size_x = 56; $size_y = 56; $colors = array( 'dark' => 'dark', /* TODO: These are available but not currently used. 'blue' => 'blue', 'light' => 'lb', */ ); } $apps = $this->getDirectoryList('apps_dark_1x'); $template = id(new PhutilSprite()) ->setSourceSize($size_x, $size_y); $sprites = array(); foreach ($apps as $app) { foreach ($colors as $color => $color_path) { $css = '.apps-'.$app.'-'.$color.$variant_short; if ($color == 'blue' && $variant_name == 'apps-large') { $css .= ', .phabricator-crumb-view:hover .apps-'.$app.'-dark-large'; } if ($color == 'white' && $variant == 1) { $css .= ', .phui-list-item-href:hover .apps-'.$app.'-dark'; } $sprite = id(clone $template) ->setName('apps-'.$app.'-'.$color.$variant_short) ->setTargetCSS($css); foreach ($scales as $scale_name => $scale) { $path = $this->getPath( 'apps_'.$color_path.'_'.$scale_name.'/'.$app.'.png'); $sprite->setSourceFile($path, $scale); } $sprites[] = $sprite; } } $sheet = $this->buildSheet($variant_name, count($scales) > 1); $sheet->setScales($scales); foreach ($sprites as $sprite) { $sheet->addSprite($sprite); } return $sheet; } private function getPath($to_path = null) { $root = dirname(phutil_get_library_root('phabricator')); return $root.'/resources/sprite/'.$to_path; } private function getDirectoryList($dir) { $path = $this->getPath($dir); $result = array(); $images = Filesystem::listDirectory($path, $include_hidden = false); foreach ($images as $image) { if (!preg_match('/\.png$/', $image)) { throw new Exception( "Expected file '{$image}' in '{$path}' to be a sprite source ". "ending in '.png'."); } $result[] = substr($image, 0, -4); } return $result; } private function buildSheet( $name, $has_retina, $type = null, $extra_css = '') { $sheet = new PhutilSpriteSheet(); $at = '@'; switch ($type) { case PhutilSpriteSheet::TYPE_STANDARD: default: $type = PhutilSpriteSheet::TYPE_STANDARD; $repeat_rule = 'no-repeat'; break; case PhutilSpriteSheet::TYPE_REPEAT_X: $repeat_rule = 'repeat-x'; break; case PhutilSpriteSheet::TYPE_REPEAT_Y: $repeat_rule = 'repeat-y'; break; } $retina_rules = null; if ($has_retina) { $retina_rules = <<setSheetType($type); $sheet->setCSSHeader(<<server); return $uri->getDomain(); } public function connect() { $this->server = $this->getConfig('server'); $this->authtoken = $this->getConfig('authtoken'); $rooms = $this->getConfig('join'); // First, join the room if (!$rooms) { throw new Exception("Not configured to join any rooms!"); } $this->readBuffers = array(); // Set up our long poll in a curl multi request so we can // continue running while it executes in the background $this->multiHandle = curl_multi_init(); $this->readHandles = array(); foreach ($rooms as $room_id) { $this->joinRoom($room_id); // Set up the curl stream for reading $url = $this->buildStreamingUrl($room_id); $this->readHandle[$url] = curl_init(); curl_setopt($this->readHandle[$url], CURLOPT_URL, $url); curl_setopt($this->readHandle[$url], CURLOPT_RETURNTRANSFER, true); curl_setopt($this->readHandle[$url], CURLOPT_FOLLOWLOCATION, 1); curl_setopt( $this->readHandle[$url], CURLOPT_USERPWD, $this->authtoken.':x'); curl_setopt( $this->readHandle[$url], CURLOPT_HTTPHEADER, array("Content-type: application/json")); curl_setopt( $this->readHandle[$url], CURLOPT_WRITEFUNCTION, array($this, 'read')); curl_setopt($this->readHandle[$url], CURLOPT_BUFFERSIZE, 128); curl_setopt($this->readHandle[$url], CURLOPT_TIMEOUT, 0); curl_multi_add_handle($this->multiHandle, $this->readHandle[$url]); // Initialize read buffer $this->readBuffers[$url] = ''; } $this->active = null; $this->blockingMultiExec(); } protected function joinRoom($room_id) { // Optional hook, by default, do nothing } // This is our callback for the background curl multi-request. // Puts the data read in on the readBuffer for processing. private function read($ch, $data) { $info = curl_getinfo($ch); $length = strlen($data); $this->readBuffers[$info['url']] .= $data; return $length; } private function blockingMultiExec() { do { $status = curl_multi_exec($this->multiHandle, $this->active); } while ($status == CURLM_CALL_MULTI_PERFORM); // Check for errors if ($status != CURLM_OK) { throw new Exception( "Phabricator Bot had a problem reading from stream."); } } public function getNextMessages($poll_frequency) { $messages = array(); if (!$this->active) { throw new Exception("Phabricator Bot stopped reading from stream."); } // Prod our http request curl_multi_select($this->multiHandle, $poll_frequency); $this->blockingMultiExec(); // Process anything waiting on the read buffer while ($m = $this->processReadBuffer()) { $messages[] = $m; } return $messages; } private function processReadBuffer() { foreach ($this->readBuffers as $url => &$buffer) { $until = strpos($buffer, "}\r"); if ($until == false) { continue; } $message = substr($buffer, 0, $until + 1); $buffer = substr($buffer, $until + 2); $m_obj = json_decode($message, true); if ($message = $this->processMessage($m_obj)) { return $message; } } // If we're here, there's nothing to process return false; } protected function performPost($endpoint, $data = null) { $uri = new PhutilURI($this->server); $uri->setPath($endpoint); $payload = json_encode($data); list($output) = id(new HTTPSFuture($uri)) ->setMethod('POST') ->addHeader('Content-Type', 'application/json') ->addHeader('Authorization', $this->getAuthorizationHeader()) ->setData($payload) ->resolvex(); $output = trim($output); if (strlen($output)) { return json_decode($output, true); } return true; } protected function getAuthorizationHeader() { return 'Basic '.base64_encode($this->authtoken.':x'); } abstract protected function buildStreamingUrl($channel); abstract protected function processMessage($raw_object); } - diff --git a/src/infrastructure/events/PhabricatorEvent.php b/src/infrastructure/events/PhabricatorEvent.php index d92d7b6894..03116cdd9e 100644 --- a/src/infrastructure/events/PhabricatorEvent.php +++ b/src/infrastructure/events/PhabricatorEvent.php @@ -1,49 +1,44 @@ user = $user; return $this; } public function getUser() { return $this->user; } public function setAphrontRequest(AphrontRequest $aphront_request) { $this->aphrontRequest = $aphront_request; return $this; } public function getAphrontRequest() { return $this->aphrontRequest; } public function setConduitRequest(ConduitAPIRequest $conduit_request) { $this->conduitRequest = $conduit_request; return $this; } public function getConduitRequest() { return $this->conduitRequest; } } - - - - - diff --git a/src/infrastructure/events/PhabricatorEventListener.php b/src/infrastructure/events/PhabricatorEventListener.php index 5a0c8c1986..211cc1f544 100644 --- a/src/infrastructure/events/PhabricatorEventListener.php +++ b/src/infrastructure/events/PhabricatorEventListener.php @@ -1,56 +1,51 @@ application = $application; return $this; } public function getApplication() { return $this->application; } public function hasApplicationCapability( PhabricatorUser $viewer, $capability) { return PhabricatorPolicyFilter::hasCapability( $viewer, $this->getApplication(), $capability); } public function canUseApplication(PhabricatorUser $viewer) { return $this->hasApplicationCapability( $viewer, PhabricatorPolicyCapability::CAN_VIEW); } protected function addActionMenuItems(PhutilEvent $event, $items) { if ($event->getType() !== PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS) { throw new Exception("Not an action menu event!"); } if (!$items) { return; } if (!is_array($items)) { $items = array($items); } $event_actions = $event->getValue('actions'); foreach ($items as $item) { $event_actions[] = $item; } $event->setValue('actions', $event_actions); } } - - - - - diff --git a/src/infrastructure/events/PhabricatorExampleEventListener.php b/src/infrastructure/events/PhabricatorExampleEventListener.php index 03c83d7146..4cbbb6b0bb 100644 --- a/src/infrastructure/events/PhabricatorExampleEventListener.php +++ b/src/infrastructure/events/PhabricatorExampleEventListener.php @@ -1,36 +1,31 @@ listen(PhabricatorEventType::TYPE_TEST_DIDRUNTEST); } public function handleEvent(PhutilEvent $event) { // When an event you have called listen() for in your register() method // occurs, this method will be invoked. You should respond to the event. // In this case, we just echo a message out so the event test script will // do something visible. $console = PhutilConsole::getConsole(); $console->writeOut( "PhabricatorExampleEventListener got test event at %d\n", $event->getValue('time')); } } - - - - - diff --git a/src/view/control/AphrontTableView.php b/src/view/control/AphrontTableView.php index 17fca10d5a..e698421c88 100644 --- a/src/view/control/AphrontTableView.php +++ b/src/view/control/AphrontTableView.php @@ -1,323 +1,322 @@ data = $data; } public function setHeaders(array $headers) { $this->headers = $headers; return $this; } public function setColumnClasses(array $column_classes) { $this->columnClasses = $column_classes; return $this; } public function setRowClasses(array $row_classes) { $this->rowClasses = $row_classes; return $this; } public function setCellClasses(array $cell_classes) { $this->cellClasses = $cell_classes; return $this; } public function setNoDataString($no_data_string) { $this->noDataString = $no_data_string; return $this; } public function setClassName($class_name) { $this->className = $class_name; return $this; } public function setZebraStripes($zebra_stripes) { $this->zebraStripes = $zebra_stripes; return $this; } public function setColumnVisibility(array $visibility) { $this->columnVisibility = $visibility; return $this; } public function setDeviceVisibility(array $device_visibility) { $this->deviceVisibility = $device_visibility; return $this; } public function setDeviceReadyTable($ready) { $this->deviceReadyTable = $ready; return $this; } public function setShortHeaders(array $short_headers) { $this->shortHeaders = $short_headers; return $this; } /** * Parse a sorting parameter: * * list($sort, $reverse) = AphrontTableView::parseSortParam($sort_param); * * @param string Sort request parameter. * @return pair Sort value, sort direction. */ public static function parseSort($sort) { return array(ltrim($sort, '-'), preg_match('/^-/', $sort)); } public function makeSortable( PhutilURI $base_uri, $param, $selected, $reverse, array $sort_values) { $this->sortURI = $base_uri; $this->sortParam = $param; $this->sortSelected = $selected; $this->sortReverse = $reverse; $this->sortValues = array_values($sort_values); return $this; } public function render() { require_celerity_resource('aphront-table-view-css'); $table = array(); $col_classes = array(); foreach ($this->columnClasses as $key => $class) { if (strlen($class)) { $col_classes[] = $class; } else { $col_classes[] = null; } } $visibility = array_values($this->columnVisibility); $device_visibility = array_values($this->deviceVisibility); $headers = $this->headers; $short_headers = $this->shortHeaders; $sort_values = $this->sortValues; if ($headers) { while (count($headers) > count($visibility)) { $visibility[] = true; } while (count($headers) > count($device_visibility)) { $device_visibility[] = true; } while (count($headers) > count($short_headers)) { $short_headers[] = null; } while (count($headers) > count($sort_values)) { $sort_values[] = null; } $tr = array(); foreach ($headers as $col_num => $header) { if (!$visibility[$col_num]) { continue; } $classes = array(); if (!empty($col_classes[$col_num])) { $classes[] = $col_classes[$col_num]; } if (empty($device_visiblity[$col_num])) { $classes[] = 'aphront-table-nodevice'; } if ($sort_values[$col_num] !== null) { $classes[] = 'aphront-table-view-sortable'; $sort_value = $sort_values[$col_num]; $sort_glyph_class = 'aphront-table-down-sort'; if ($sort_value == $this->sortSelected) { if ($this->sortReverse) { $sort_glyph_class = 'aphront-table-up-sort'; } else if (!$this->sortReverse) { $sort_value = '-'.$sort_value; } $classes[] = 'aphront-table-view-sortable-selected'; } $sort_glyph = phutil_tag( 'span', array( 'class' => $sort_glyph_class, ), ''); $header = phutil_tag( 'a', array( 'href' => $this->sortURI->alter($this->sortParam, $sort_value), 'class' => 'aphront-table-view-sort-link', ), array( $header, ' ', $sort_glyph, )); } if ($classes) { $class = implode(' ', $classes); } else { $class = null; } if ($short_headers[$col_num] !== null) { $header_nodevice = phutil_tag( 'span', array( 'class' => 'aphront-table-view-nodevice', ), $header); $header_device = phutil_tag( 'span', array( 'class' => 'aphront-table-view-device', ), $short_headers[$col_num]); $header = hsprintf('%s %s', $header_nodevice, $header_device); } $tr[] = phutil_tag('th', array('class' => $class), $header); } $table[] = phutil_tag('tr', array(), $tr); } foreach ($col_classes as $key => $value) { if (($sort_values[$key] !== null) && ($sort_values[$key] == $this->sortSelected)) { $value = trim($value.' sorted-column'); } if ($value !== null) { $col_classes[$key] = $value; } } $data = $this->data; if ($data) { $row_num = 0; foreach ($data as $row) { while (count($row) > count($col_classes)) { $col_classes[] = null; } while (count($row) > count($visibility)) { $visibility[] = true; } $tr = array(); // NOTE: Use of a separate column counter is to allow this to work // correctly if the row data has string or non-sequential keys. $col_num = 0; foreach ($row as $value) { if (!$visibility[$col_num]) { ++$col_num; continue; } $class = $col_classes[$col_num]; if (!empty($this->cellClasses[$row_num][$col_num])) { $class = trim($class.' '.$this->cellClasses[$row_num][$col_num]); } $tr[] = phutil_tag('td', array('class' => $class), $value); ++$col_num; } $class = idx($this->rowClasses, $row_num); if ($this->zebraStripes && ($row_num % 2)) { if ($class !== null) { $class = 'alt alt-'.$class; } else { $class = 'alt'; } } $table[] = phutil_tag('tr', array('class' => $class), $tr); ++$row_num; } } else { $colspan = max(count(array_filter($visibility)), 1); $table[] = phutil_tag( 'tr', array('class' => 'no-data'), phutil_tag( 'td', array('colspan' => $colspan), coalesce($this->noDataString, pht('No data available.')))); } $table_class = 'aphront-table-view'; if ($this->className !== null) { $table_class .= ' '.$this->className; } if ($this->deviceReadyTable) { $table_class .= ' aphront-table-view-device-ready'; } $html = phutil_tag('table', array('class' => $table_class), $table); return phutil_tag_div('aphront-table-wrap', $html); } public static function renderSingleDisplayLine($line) { // TODO: Is there a cleaner way to do this? We use a relative div with // overflow hidden to provide the bounds, and an absolute span with // white-space: pre to prevent wrapping. We need to append a character // (  -- nonbreaking space) afterward to give the bounds div height // (alternatively, we could hard-code the line height). This is gross but // it's not clear that there's a better appraoch. return phutil_tag( 'div', array( 'class' => 'single-display-line-bounds', ), array( phutil_tag( 'span', array( 'class' => 'single-display-line-content', ), $line), "\xC2\xA0", )); } } - diff --git a/src/view/layout/PhabricatorFileLinkListView.php b/src/view/layout/PhabricatorFileLinkListView.php index 0eaf519d33..5824b6a8c2 100644 --- a/src/view/layout/PhabricatorFileLinkListView.php +++ b/src/view/layout/PhabricatorFileLinkListView.php @@ -1,37 +1,36 @@ files = $files; return $this; } private function getFiles() { return $this->files; } public function render() { $files = $this->getFiles(); if (!$files) { return ''; } require_celerity_resource('phabricator-remarkup-css'); $file_links = array(); foreach ($this->getFiles() as $file) { $view = id(new PhabricatorFileLinkView()) ->setFilePHID($file->getPHID()) ->setFileName($file->getName()) ->setFileDownloadURI($file->getDownloadURI()) ->setFileViewURI($file->getBestURI()) ->setFileViewable($file->isViewableImage()); $file_links[] = $view->render(); } return phutil_implode_html(phutil_tag('br'), $file_links); } } - diff --git a/src/view/viewutils.php b/src/view/viewutils.php index 44916e947b..b7fc259dfe 100644 --- a/src/view/viewutils.php +++ b/src/view/viewutils.php @@ -1,280 +1,279 @@ $now - $shift) { $format = pht('D, M j'); } else { $format = pht('M j Y'); } return $format; } function _phabricator_time_format($user) { $prefs = $user->loadPreferences(); $pref = $prefs->getPreference( PhabricatorUserPreferences::PREFERENCE_TIME_FORMAT); if (strlen($pref)) { return $pref; } return pht('g:i A'); } /** * This function does not usually need to be called directly. Instead, call * @{function:phabricator_date}, @{function:phabricator_time}, or * @{function:phabricator_datetime}. * * @param int Unix epoch timestamp. * @param PhabricatorUser User viewing the timestamp. * @param string Date format, as per DateTime class. * @return string Formatted, local date/time. */ function phabricator_format_local_time($epoch, $user, $format) { if (!$epoch) { // If we're missing date information for something, the DateTime class will // throw an exception when we try to construct an object. Since this is a // display function, just return an empty string. return ''; } $user_zone = $user->getTimezoneIdentifier(); static $zones = array(); if (empty($zones[$user_zone])) { $zones[$user_zone] = new DateTimeZone($user_zone); } $zone = $zones[$user_zone]; // NOTE: Although DateTime takes a second DateTimeZone parameter to its // constructor, it ignores it if the date string includes timezone // information. Further, it treats epoch timestamps ("@946684800") as having // a UTC timezone. Set the timezone explicitly after constructing the object. try { $date = new DateTime('@'.$epoch); } catch (Exception $ex) { // NOTE: DateTime throws an empty exception if the format is invalid, // just replace it with a useful one. throw new Exception( pht("Construction of a DateTime() with epoch '%s' ". "raised an exception.", $epoch)); } $date->setTimeZone($zone); return PhutilTranslator::getInstance()->translateDate($format, $date); } function phabricator_format_relative_time($duration) { return phabricator_format_units_generic( $duration, array(60, 60, 24, 7), array('s', 'm', 'h', 'd', 'w'), $precision = 0); } /** * Format a relative time (duration) into weeks, days, hours, minutes, * seconds, but unlike phabricator_format_relative_time, does so for more than * just the largest unit. * * @param int Duration in seconds. * @param int Levels to render - will render the three highest levels, ie: * 5 h, 37 m, 1 s * @return string Human-readable description. */ function phabricator_format_relative_time_detailed($duration, $levels = 2) { if ($duration == 0) { return 'now'; } $levels = max(1, min($levels, 5)); $remainder = 0; $is_negative = false; if ($duration < 0) { $is_negative = true; $duration = abs($duration); } $this_level = 1; $detailed_relative_time = phabricator_format_units_generic( $duration, array(60, 60, 24, 7), array('s', 'm', 'h', 'd', 'w'), $precision = 0, $remainder); $duration = $remainder; while ($remainder > 0 && $this_level < $levels) { $detailed_relative_time .= ', '.phabricator_format_units_generic( $duration, array(60, 60, 24, 7), array('s', 'm', 'h', 'd', 'w'), $precision = 0, $remainder); $duration = $remainder; $this_level++; }; if ($is_negative) { $detailed_relative_time .= ' ago'; } return $detailed_relative_time; } /** * Format a byte count for human consumption, e.g. "10MB" instead of * "10000000". * * @param int Number of bytes. * @return string Human-readable description. */ function phabricator_format_bytes($bytes) { return phabricator_format_units_generic( $bytes, // NOTE: Using the SI version of these units rather than the 1024 version. array(1000, 1000, 1000, 1000, 1000), array('B', 'KB', 'MB', 'GB', 'TB', 'PB'), $precision = 0); } /** * Parse a human-readable byte description (like "6MB") into an integer. * * @param string Human-readable description. * @return int Number of represented bytes. */ function phabricator_parse_bytes($input) { $bytes = trim($input); if (!strlen($bytes)) { return null; } // NOTE: Assumes US-centric numeral notation. $bytes = preg_replace('/[ ,]/', '', $bytes); $matches = null; if (!preg_match('/^(?:\d+(?:[.]\d+)?)([kmgtp]?)b?$/i', $bytes, $matches)) { throw new Exception("Unable to parse byte size '{$input}'!"); } $scale = array( 'k' => 1000, 'm' => 1000 * 1000, 'g' => 1000 * 1000 * 1000, 't' => 1000 * 1000 * 1000 * 1000, 'p' => 1000 * 1000 * 1000 * 1000 * 1000, ); $bytes = (float)$bytes; if ($matches[1]) { $bytes *= $scale[strtolower($matches[1])]; } return (int)$bytes; } function phabricator_format_units_generic( $n, array $scales, array $labels, $precision = 0, &$remainder = null) { $is_negative = false; if ($n < 0) { $is_negative = true; $n = abs($n); } $remainder = 0; $accum = 1; $scale = array_shift($scales); $label = array_shift($labels); while ($n >= $scale && count($labels)) { $remainder += ($n % $scale) * $accum; $n /= $scale; $accum *= $scale; $label = array_shift($labels); if (!count($scales)) { break; } $scale = array_shift($scales); } if ($is_negative) { $n = -$n; $remainder = -$remainder; } if ($precision) { $num_string = number_format($n, $precision); } else { $num_string = (int)floor($n); } if ($label) { $num_string .= ' '.$label; } return $num_string; } - diff --git a/support/aphlict/client/aphlict_test_client.php b/support/aphlict/client/aphlict_test_client.php index b6ca0480d6..52974a0731 100755 --- a/support/aphlict/client/aphlict_test_client.php +++ b/support/aphlict/client/aphlict_test_client.php @@ -1,56 +1,55 @@ #!/usr/bin/env php setTagline('test client for Aphlict server'); $args->setSynopsis(<<parseStandardArguments(); $args->parse( array( array( 'name' => 'server', 'param' => 'uri', 'default' => PhabricatorEnv::getEnvConfig('notification.client-uri'), 'help' => 'Connect to __uri__ instead of the default server.', ), )); $console = PhutilConsole::getConsole(); $errno = null; $errstr = null; $uri = $args->getArg('server'); $uri = new PhutilURI($uri); $uri->setProtocol('tcp'); $console->writeErr("Connecting...\n"); $socket = stream_socket_client( $uri, $errno, $errstr); if (!$socket) { $console->writeErr( "Unable to connect to Aphlict (at '$uri'). Error #{$errno}: {$errstr}"); exit(1); } else { $console->writeErr("Connected.\n"); } $io_channel = new PhutilSocketChannel($socket); $proto_channel = new PhutilJSONProtocolChannel($io_channel); $json = new PhutilJSON(); while (true) { $message = $proto_channel->waitForMessage(); $console->writeOut($json->encodeFormatted($message)); } - diff --git a/support/empty/README b/support/empty/README index 1d7f895991..019c55dcec 100644 --- a/support/empty/README +++ b/support/empty/README @@ -1,6 +1,5 @@ This is an empty, readable directory. If you need an empty, readable directory for some reason, you can use this one. Of course, it's not quite empty because it has this file in it. So it's a mostly-empty, readable directory. - diff --git a/webroot/index.php b/webroot/index.php index f880a77b90..7072727e7f 100644 --- a/webroot/index.php +++ b/webroot/index.php @@ -1,151 +1,150 @@ setData( array( 'R' => AphrontRequest::getHTTPHeader('Referer', '-'), 'r' => idx($_SERVER, 'REMOTE_ADDR', '-'), 'M' => idx($_SERVER, 'REQUEST_METHOD', '-'), )); DarkConsoleXHProfPluginAPI::hookProfiler(); DarkConsoleErrorLogPluginAPI::registerErrorHandler(); $sink = new AphrontPHPHTTPSink(); $response = PhabricatorSetupCheck::willProcessRequest(); if ($response) { PhabricatorStartup::endOutputCapture(); $sink->writeResponse($response); return; } $host = AphrontRequest::getHTTPHeader('Host'); $path = $_REQUEST['__path__']; switch ($host) { default: $config_key = 'aphront.default-application-configuration-class'; $application = PhabricatorEnv::newObjectFromConfig($config_key); break; } $application->setHost($host); $application->setPath($path); $application->willBuildRequest(); $request = $application->buildRequest(); // Until an administrator sets "phabricator.base-uri", assume it is the same // as the request URI. This will work fine in most cases, it just breaks down // when daemons need to do things. $request_protocol = ($request->isHTTPS() ? 'https' : 'http'); $request_base_uri = "{$request_protocol}://{$host}/"; PhabricatorEnv::setRequestBaseURI($request_base_uri); $write_guard = new AphrontWriteGuard(array($request, 'validateCSRF')); $application->setRequest($request); list($controller, $uri_data) = $application->buildController(); $access_log->setData( array( 'U' => (string)$request->getRequestURI()->getPath(), 'C' => get_class($controller), )); // If execution throws an exception and then trying to render that exception // throws another exception, we want to show the original exception, as it is // likely the root cause of the rendering exception. $original_exception = null; try { $response = $controller->willBeginExecution(); if ($request->getUser() && $request->getUser()->getPHID()) { $access_log->setData( array( 'u' => $request->getUser()->getUserName(), 'P' => $request->getUser()->getPHID(), )); } if (!$response) { $controller->willProcessRequest($uri_data); $response = $controller->processRequest(); } } catch (Exception $ex) { $original_exception = $ex; $response = $application->handleException($ex); } try { $response = $controller->didProcessRequest($response); $response = $application->willSendResponse($response, $controller); $response->setRequest($request); $unexpected_output = PhabricatorStartup::endOutputCapture(); if ($unexpected_output) { $unexpected_output = "Unexpected output:\n\n{$unexpected_output}"; phlog($unexpected_output); if ($response instanceof AphrontWebpageResponse) { echo phutil_tag( 'div', array('style' => 'background: #eeddff;'. 'white-space: pre-wrap;'. 'z-index: 200000;'. 'position: relative;'. 'padding: 8px;'. 'font-family: monospace'), $unexpected_output); } } $sink->writeResponse($response); } catch (Exception $ex) { $write_guard->dispose(); $access_log->write(); if ($original_exception) { $ex = new PhutilAggregateException( "Multiple exceptions during processing and rendering.", array( $original_exception, $ex, )); } PhabricatorStartup::didEncounterFatalException( 'Rendering Exception', $ex, $show_unexpected_traces); } $write_guard->dispose(); $access_log->setData( array( 'c' => $response->getHTTPResponseCode(), 'T' => PhabricatorStartup::getMicrosecondsSinceStart(), )); DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log); } catch (Exception $ex) { PhabricatorStartup::didEncounterFatalException( 'Core Exception', $ex, $show_unexpected_traces); } - diff --git a/webroot/rsrc/css/aphront/lightbox-attachment.css b/webroot/rsrc/css/aphront/lightbox-attachment.css index 6058b95fb9..a11512ce4e 100644 --- a/webroot/rsrc/css/aphront/lightbox-attachment.css +++ b/webroot/rsrc/css/aphront/lightbox-attachment.css @@ -1,107 +1,106 @@ /** * @provides lightbox-attachment-css */ .lightbox-attached { overflow: hidden; } .lightbox-attachment { position: fixed; top: 0; left: 0; bottom: 0; right: 0; overflow-y: auto; } .lightbox-attachment img { margin: 3% auto 0; max-height: 90%; max-width: 90%; } .lightbox-attachment .loading { position: absolute; top: -9999px; } .lightbox-attachment .attachment-name { width: 100%; color: #F2F2F2; line-height: 30px; text-align: center; } .lightbox-attachment .lightbox-status { background: #010101; color: #F2F2F2; line-height: 30px; position: fixed; bottom: 0px; width: 100%; } .lightbox-attachment .lightbox-status .lightbox-status-txt { padding: 0px 0px 0px 20px; } .lightbox-attachment .lightbox-status .lightbox-download { padding: 0px 20px 0px 0px; float: right; } .lightbox-attachment .lightbox-status .lightbox-download .lightbox-download-form { display: inline; } .lightbox-attachment .lightbox-status .lightbox-download .lightbox-download-form button { border: 0; background: #010101; } .lightbox-attachment .lightbox-status .lightbox-download .lightbox-download-form button:hover { background: #333; } .lightbox-attachment .lightbox-close { top: 22px; right: 20px; position: fixed; display: block; height: 26px; width: 26px; background: url('/rsrc/image/icon/lightbox/close-2.png'); } .lightbox-attachment .lightbox-close:hover { background: url('/rsrc/image/icon/lightbox/close-hover-2.png'); } .lightbox-attachment .lightbox-left { top: 46%; left: 20px; position: fixed; display: block; height: 38px; width: 21px; background: url('/rsrc/image/icon/lightbox/left-arrow-2.png'); } .lightbox-attachment .lightbox-left:hover { background: url('/rsrc/image/icon/lightbox/left-arrow-hover-2.png'); } .lightbox-attachment .lightbox-right { top: 46%; right: 20px; position: fixed; display: block; height: 38px; width: 21px; background: url('/rsrc/image/icon/lightbox/right-arrow-2.png'); } .lightbox-attachment .lightbox-right:hover { background: url('/rsrc/image/icon/lightbox/right-arrow-hover-2.png'); } - diff --git a/webroot/rsrc/css/aphront/phabricator-nav-view.css b/webroot/rsrc/css/aphront/phabricator-nav-view.css index efb8b91f61..b6682c6f44 100644 --- a/webroot/rsrc/css/aphront/phabricator-nav-view.css +++ b/webroot/rsrc/css/aphront/phabricator-nav-view.css @@ -1,103 +1,102 @@ /** * @provides phabricator-nav-view-css */ .jx-drag-col { cursor: col-resize; } .phabricator-nav { /* Force top margins in page content not to collapse with the top margin of the navigation container by giving it padding. Then put it in the right position by undoing the padding with a margin. */ padding-top: 1px; margin-top: -1px; } .phabricator-nav-column-background { position: fixed; top: 0; left: 0; /* On the iPhone, scrolling down causes the revealed area to fill with white, then draw with the texture after the action completes. Just make the element extend off the bottom of the screen to prevent this. */ bottom: -480px; width: 205px; background: #303539; box-shadow: inset -3px 0 3px rgba(0, 0, 0, 0.5); } .phabricator-nav-column-background, .phabricator-nav-local, .phabricator-nav-drag { display: none; } .device-desktop .has-local-nav .phabricator-nav-column-background, .device-desktop .has-local-nav .phabricator-nav-local, .device-desktop .has-local-nav .phabricator-nav-drag { display: block; } .device .phabricator-side-menu-home .phabricator-nav-column-background, .device .phabricator-side-menu-home .phabricator-nav-local { display: block; } .phabricator-nav-local { width: 205px; position: absolute; left: 0; white-space: nowrap; overflow-x: hidden; overflow-y: auto; } .phabricator-nav-drag { position: fixed; top: 0; bottom: 0; left: 205px; width: 7px; cursor: col-resize; background: #f5f5f5; border-style: solid; border-width: 0 1px 0 1px; border-color: #fff #999c9e #fff #999c9e; box-shadow: inset -1px 0px 1px rgba(0, 0, 0, 0.15); background-image: url(/rsrc/image/divot.png); background-position: center; background-repeat: no-repeat; } .device-desktop .phabricator-standard-page-body .has-drag-nav .phabricator-nav-content { margin-left: 212px; } .device-desktop .has-local-nav .phabricator-nav-content { margin-left: 205px; } .phabricator-side-menu-home .phabricator-nav-column-background, .phabricator-side-menu-home .phabricator-nav-local { width: 240px; } .device-desktop .phabricator-side-menu-home .phabricator-nav-content, .device-tablet .phabricator-side-menu-home .phabricator-nav-content { margin-left: 240px; } .device-phone .phabricator-side-menu-home .phabricator-nav-content { display: none; } .device-phone .phabricator-side-menu-home .phabricator-nav-column-background, .device-phone .phabricator-side-menu-home .phabricator-nav-local { width: 100%; } - diff --git a/webroot/rsrc/css/application/conpherence/notification.css b/webroot/rsrc/css/application/conpherence/notification.css index 565504cbc2..ec55ad0b3a 100644 --- a/webroot/rsrc/css/application/conpherence/notification.css +++ b/webroot/rsrc/css/application/conpherence/notification.css @@ -1,80 +1,79 @@ /** * @provides conpherence-notification-css */ /* kill styles on phabricator-notification */ .conpherence-notification { padding: 0; } .phabricator-notification .conpherence-menu-item-view { display: block; height: 55px; overflow: hidden; position: relative; text-decoration: none; border-bottom: none; border-right: 0; border-left: 0; } .phabricator-notification .conpherence-menu-item-view .conpherence-menu-item-image { top: 6px; left: 6px; display: block; position: absolute; width: 35px; height: 35px; background-size: 35px; border: 4px solid #e7e7e7; border-radius: 3px; } .phabricator-notification .conpherence-menu-item-view .conpherence-menu-item-title { display: block; margin-top: 12px; margin-left: 58px; text-align: left; font-weight: bold; font-size: 13px; color: #333; overflow: hidden; width: 220px; text-overflow: ellipsis; } .phabricator-notification .conpherence-menu-item-view .conpherence-menu-item-subtitle { display: block; color: #a1a5a9; font-size: 11px; margin-top: 2px; margin-left: 58px; font-style: italic; } .phabricator-notification .conpherence-menu-item-view .conpherence-menu-item-unread-count { position: absolute; left: 35px; top: 3px; background: {$red}; border-radius: 10px; color: #FFF; font-weight: bold; padding: 1px 5px 2px; border: 1px solid #333; font-size: 11px; } .phabricator-notification .conpherence-menu-item-view .conpherence-menu-item-date { position: absolute; top: 15px; right: 16px; color: #a1a5a9; font-size: 11px; } - diff --git a/webroot/rsrc/css/application/differential/core.css b/webroot/rsrc/css/application/differential/core.css index 2b4f6b4d9c..7ccd633f3f 100644 --- a/webroot/rsrc/css/application/differential/core.css +++ b/webroot/rsrc/css/application/differential/core.css @@ -1,26 +1,25 @@ /** * @provides differential-core-view-css */ .differential-primary-pane { margin-bottom: 32px; } .differential-panel { padding: 16px; } .differential-panel h1 { border-bottom: 1px solid #aaaa99; padding-bottom: 8px; margin-bottom: 8px; } .differential-unselectable tr td:nth-of-type(1) { -moz-user-select: -moz-none; -khtml-user-select: none; -webkit-user-select: none; -ms-user-select: none; user-select: none; } - diff --git a/webroot/rsrc/css/application/maniphest/batch-editor.css b/webroot/rsrc/css/application/maniphest/batch-editor.css index 10e9a61d62..f761cf1aa0 100644 --- a/webroot/rsrc/css/application/maniphest/batch-editor.css +++ b/webroot/rsrc/css/application/maniphest/batch-editor.css @@ -1,19 +1,18 @@ /** * @provides maniphest-batch-editor */ .maniphest-batch-actions-table { width: 100%; margin: 1em 0; } .maniphest-batch-actions-table td { padding: 4px 8px; vertical-align: middle; } .batch-editor-input { width: 100%; text-align: left; } - diff --git a/webroot/rsrc/css/application/people/people-profile.css b/webroot/rsrc/css/application/people/people-profile.css index 655bcb0c99..bc0e61fbf9 100644 --- a/webroot/rsrc/css/application/people/people-profile.css +++ b/webroot/rsrc/css/application/people/people-profile.css @@ -1,79 +1,78 @@ /** * @provides people-profile-css */ form.profile-image-form { display: inline-block; margin: 0 8px 8px 0; } button.profile-image-button { padding: 4px; margin: 0; } .compose-dialog button.profile-image-button-selected { background-image: none; background-color: {$lightblue}; border-color: {$blueborder}; } .compose-header { color: {$bluetext}; border-bottom: 1px solid {$lightblueborder}; padding: 4px 0; margin: 0 0 8px; } form.compose-dialog { width: 80%; } .compose-dialog .phui-icon-view { display: block; position: relative; width: 50px; height: 50px; background-color: {$darkgreytext}; } .compose-dialog .compose-background-red { background-color: {$red}; } .compose-dialog .compose-background-orange { background-color: {$orange}; } .compose-dialog .compose-background-yellow { background-color: {$yellow}; } .compose-dialog .compose-background-green { background-color: {$green}; } .compose-dialog .compose-background-blue { background-color: {$blue}; } .compose-dialog .compose-background-sky { background-color: {$sky}; } .compose-dialog .compose-background-indigo { background-color: {$indigo}; } .compose-dialog .compose-background-violet { background-color: {$violet}; } .compose-dialog .compose-background-charcoal { background-color: {$charcoal}; } .compose-dialog .compose-background-backdrop { background-color: {$backdrop}; } - diff --git a/webroot/rsrc/css/layout/phabricator-action-header-view.css b/webroot/rsrc/css/layout/phabricator-action-header-view.css index fbc573b919..11127e18b6 100644 --- a/webroot/rsrc/css/layout/phabricator-action-header-view.css +++ b/webroot/rsrc/css/layout/phabricator-action-header-view.css @@ -1,65 +1,64 @@ /** * @provides phabricator-action-header-view-css */ .phabricator-action-header { padding: 0 5px 0 8px; overflow: hidden; } .phabricator-action-header-title { color: {$darkgreytext}; float: left; font-size: 14px; font-weight: bold; line-height: 15px; padding: 8px 0; text-shadow: 0 1px 1px #fff; white-space: nowrap; } .phabricator-action-header-icon-list { float: right; padding-top: 4px; } .phabricator-action-header-icon-item { float: right; padding-left: 2px; } .phabricator-action-header-icon-item .phui-icon-view { display: inline-block; } .phabricator-action-header-icon-item .phui-tag-view { margin: 4px 2px 0; } .phabricator-action-header-link { color: {$darkgreytext}; } .gradient-green-header .phabricator-action-header-title, .gradient-red-header .phabricator-action-header-title, .gradient-blue-header .phabricator-action-header-title, .gradient-yellow-header .phabricator-action-header-title, .gradient-green-header .phabricator-action-header-link, .gradient-red-header .phabricator-action-header-link, .gradient-blue-header .phabricator-action-header-link, .gradient-yellow-header .phabricator-action-header-link { color: #fff; text-shadow: 0 -1px 1px rgba(0,0,0,.7); } .phabricator-action-header-icon-list .phui-tag-view { font-weight: normal; } .phabricator-action-header-title span { float: left; height: 16px; width: 16px; margin-right: 4px; } - diff --git a/webroot/rsrc/css/phui/phui-icon.css b/webroot/rsrc/css/phui/phui-icon.css index ccc6715696..9ab8036bc9 100644 --- a/webroot/rsrc/css/phui/phui-icon.css +++ b/webroot/rsrc/css/phui/phui-icon.css @@ -1,54 +1,52 @@ /** * @provides phui-icon-view-css */ .phui-icon-example .phui-icon-view { display: inline-block; vertical-align: top; } .phui-icon-view.sprite-minicons { height: 16px; width: 16px; } .phui-icon-view.sprite-actions { height: 24px; width: 24px; } .phui-icon-view.sprite-apps, .phui-icon-view.sprite-icons, .phui-icon-view.sprite-status, .phui-icon-view.sprite-buttonbar { height: 14px; width: 14px; } .phui-icon-view.sprite-tokens { height: 16px; width: 16px; } .phui-icon-view.sprite-payments { height: 32px; width: 60px; } .phui-icon-view.sprite-login { height: 34px; width: 34px; } .phui-icon-view.phuihead-medium { height: 50px; width: 50px; } .phui-icon-view.phuihead-small { height: 35px; width: 35px; background-size: 35px; } - - diff --git a/webroot/rsrc/externals/javelin/LICENSE b/webroot/rsrc/externals/javelin/LICENSE index 48fb9f83b0..7d06b3778a 100644 --- a/webroot/rsrc/externals/javelin/LICENSE +++ b/webroot/rsrc/externals/javelin/LICENSE @@ -1,25 +1,24 @@ Copyright (c) 2009, Evan Priestley and Facebook, inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Facebook, inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/webroot/rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js b/webroot/rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js index bc45d43fe0..09a2d3a764 100644 --- a/webroot/rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js +++ b/webroot/rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js @@ -1,39 +1,35 @@ /** * @requires javelin-event */ describe('Event Stop/Kill', function() { var target; beforeEach(function() { target = new JX.Event(); }); it('should stop an event', function() { expect(target.getStopped()).toBe(false); target.prevent(); expect(target.getStopped()).toBe(false); target.stop(); expect(target.getStopped()).toBe(true); }); it('should prevent the default action of an event', function() { expect(target.getPrevented()).toBe(false); target.stop(); expect(target.getPrevented()).toBe(false); target.prevent(); expect(target.getPrevented()).toBe(true); }); it('should kill (stop and prevent) an event', function() { expect(target.getPrevented()).toBe(false); expect(target.getStopped()).toBe(false); target.kill(); expect(target.getPrevented()).toBe(true); expect(target.getStopped()).toBe(true); }); }); - - - - diff --git a/webroot/rsrc/externals/javelin/core/__tests__/install.js b/webroot/rsrc/externals/javelin/core/__tests__/install.js index c6b78ce9e0..c13a281bd8 100644 --- a/webroot/rsrc/externals/javelin/core/__tests__/install.js +++ b/webroot/rsrc/externals/javelin/core/__tests__/install.js @@ -1,152 +1,151 @@ /** * @requires javelin-install */ describe('Javelin Install', function() { it('should extend from an object', function() { JX.install('Animal', { properties: { name: 'bob' } }); JX.install('Dog', { extend: 'Animal', members: { bark: function() { return 'bow wow'; } } }); var bob = new JX.Dog(); expect(bob.getName()).toEqual('bob'); expect(bob.bark()).toEqual('bow wow'); }); it('should create a class', function() { var Animal = JX.createClass({ name: 'Animal', properties: { name: 'bob' } }); var Dog = JX.createClass({ name: 'Dog', extend: Animal, members: { bark: function() { return 'bow wow'; } } }); var bob = new Dog(); expect(bob.getName()).toEqual('bob'); expect(bob.bark()).toEqual('bow wow'); }); it('should call base constructor when construct is not provided', function() { var Base = JX.createClass({ name: 'Base', construct: function() { this.baseCalled = true; } }); var Sub = JX.createClass({ name: 'Sub', extend: Base }); var obj = new Sub(); expect(obj.baseCalled).toBe(true); }); it('should call intialize after install', function() { var initialized = false; JX.install('TestClass', { properties: { foo: 'bar' }, initialize: function() { initialized = true; } }); expect(initialized).toBe(true); }); it('should call base ctor when construct is not provided in JX.install', function() { JX.install('Base', { construct: function() { this.baseCalled = true; } }); JX.install('Sub', { extend: 'Base' }); var obj = new JX.Sub(); expect(obj.baseCalled).toBe(true); }); it('[DEV] should throw when calling install with name', function() { ensure__DEV__(true, function() { expect(function() { JX.install('AngryAnimal', { name: 'Kitty' }); }).toThrow(); }); }); it('[DEV] should throw when calling createClass with initialize', function() { ensure__DEV__(true, function() { expect(function() { JX.createClass({ initialize: function() { } }); }).toThrow(); }); }); it('initialize() should be able to access the installed class', function() { JX.install('SomeClassWithInitialize', { initialize : function() { expect(!!JX.SomeClassWithInitialize).toBe(true); } }); }); it('should work with toString and its friends', function() { JX.install('NiceAnimal', { members: { toString: function() { return 'I am very nice.'; }, hasOwnProperty: function() { return true; } } }); expect(new JX.NiceAnimal().toString()).toEqual('I am very nice.'); expect(new JX.NiceAnimal().hasOwnProperty('dont-haz')).toEqual(true); }); }); - diff --git a/webroot/rsrc/externals/javelin/docs/Base.js b/webroot/rsrc/externals/javelin/docs/Base.js index dc207c7758..d2352e9c99 100644 --- a/webroot/rsrc/externals/javelin/docs/Base.js +++ b/webroot/rsrc/externals/javelin/docs/Base.js @@ -1,75 +1,70 @@ /** * @requires javelin-install * @javelin */ /** * This is not a real class, but @{function:JX.install} provides several methods * which exist on all Javelin classes. This class documents those methods. * * @task events Builtin Events * @group install */ JX.install('Base', { members : { /** * Invoke a class event, notifying all listeners. You must declare the * events your class invokes when you install it; see @{function:JX.install} * for documentation. Any arguments you provide will be passed to listener * callbacks. * * @param string Event type, must be declared when class is * installed. * @param ... Zero or more arguments. * * @return @{JX.Event} Event object which was dispatched. * @task events */ invoke : function(type, more) { // // }, /** * Listen for events emitted by this object instance. You can also use * the static flavor of this method to listen to events emitted by any * instance of this object. * * See also @{method:JX.Stratcom.listen}. * * @param string Type of event to listen for. * @param function Function to call when this event occurs. * @return object A reference to the installed listener. You can later * remove the listener by calling this object's remove() * method. * @task events */ listen : function(type, callback) { // // } }, statics : { /** * Static listen interface for listening to events produced by any instance * of this class. See @{method:listen} for documentation. * * @param string Type of event to listen for. * @param function Function to call when this event occurs. * @return object A reference to the installed listener. You can later * remove the listener by calling this object's remove() * method. * @task events */ listen : function(type, callback) { // // } } }); - - - - - diff --git a/webroot/rsrc/externals/javelin/docs/concepts/behaviors.diviner b/webroot/rsrc/externals/javelin/docs/concepts/behaviors.diviner index f41afe13cd..726762ecb4 100644 --- a/webroot/rsrc/externals/javelin/docs/concepts/behaviors.diviner +++ b/webroot/rsrc/externals/javelin/docs/concepts/behaviors.diviner @@ -1,182 +1,181 @@ @title Concepts: Behaviors @group concepts Javelin behaviors help you glue pieces of code together. = Overview = Javelin behaviors provide a place for you to put glue code. For instance, when a page loads, you often need to instantiate objects, or set up event listeners, or alert the user that they've won a hog, or create a dependency between two objects, or modify the DOM, etc. Sometimes there's enough code involved here or a particular setup step happens often enough that it makes sense to write a class, but sometimes it's just a few lines of one-off glue. Behaviors give you a structured place to put this glue so that it's consistently organized and can benefit from Javelin infrastructure. = Behavior Basics = Behaviors are defined with @{function:JX.behavior}: lang=js JX.behavior('win-a-hog', function(config, statics) { alert("YOU WON A HOG NAMED " + config.hogName + "!"); }); They are called with @{function:JX.initBehaviors}: lang=js JX.initBehaviors({ "win-a-hog" : [{hogName : "Ethel"}] }); Normally, you don't construct the @{function:JX.initBehaviors} call yourself, but instead use a server-side library which manages behavior initialization for you. For example, using the PHP library: lang=php $config = array('hogName' => 'Ethel'); JavelinHelper::initBehaviors('win-a-hog', $config); Regardless, this will alert the user that they've won a hog (named Ethel, which is a good name for a hog) when they load the page. The callback you pass to @{function:JX.behavior} should have this signature: lang=js function(config, statics) { // ... } The function will be invoked once for each configuration dictionary passed to @{function:JX.initBehaviors}, and the dictionary will be passed as the ##config## parameter. For example, to alert the user that they've won two hogs: lang=js JX.initBehaviors({ "win-a-hog" : [{hogName : "Ethel"}, {hogName: "Beatrice"}] }); This will invoke the function twice, once for each ##config## dictionary. Usually, you invoke a behavior multiple times if you have several similar controls on a page, like multiple @{class:JX.Tokenizer}s. An initially empty object will be passed in the ##statics## parameter, but changes to this object will persist across invocations of the behavior. For example: lang=js JX.initBehaviors('win-a-hog', function(config, statics) { statics.hogsWon = (statics.hogsWon || 0) + 1; if (statics.hogsWon == 1) { alert("YOU WON A HOG! YOUR HOG IS NAMED " + config.hogName + "!"); } else { alert("YOU WON ANOTHER HOG!!! THIS ONE IS NAMED " + config.hogName + "!"); } } One way to think about behaviors are that they take the anonymous function passed to @{function:JX.behavior} and put it in a private Javelin namespace, which you access with @{function:JX.initBehavior}. Another way to think about them is that you are defining methods which represent the entirety of the API exposed by the document. The recommended approach to glue code is that the server interact with Javascript on the client //only// by invoking behaviors, so the set of available behaviors represent the complete set of legal interactions available to the server. = History and Rationale = This section explains why behaviors exist and how they came about. You can understand and use them without knowing any of this, but it may be useful or interesting. In early 2007, Facebook often solved the "glue code" problem through the use of global functions and DOM Level 0 event handlers, by manually building HTML tags in PHP: lang=php echo ''. 'Click here to win!'. ''; (This example produces a link which the user can click to be alerted they have won a hog, which is slightly different from the automatic alert in the other examples in this document. Some subtle distinctions are ignored or glossed over here because they are not important to understanding behaviors.) This has a wide array of technical and architectural problems: - Correctly escaping parameters is cumbersome and difficult. - It resists static analysis, and is difficult to even grep for. You can't easily package, minify, or determine dependencies for the piece of JS in the result string. - DOM Level 0 events have a host of issues in a complex application environment. - The JS global namespace becomes polluted with application glue functions. - The server and client are tightly and relatively arbitrarily coupled, since many of these handlers called multiple functions or had logic in the strings. There is no structure to the coupling, so many callers relied on the full power of arbitrary JS execution. - It's utterly hideous. In 2007/2008, we introduced @{function@libphutil:jsprintf} and a function called onloadRegister() to solve some of the obvious problems: lang=php onloadRegister('win_a_hog(%s);', $hog_name); This registers the snippet for invocation after DOMContentReady fires. This API makes escaping manageable, and was combined with recommendations to structure code like this in order to address some of the other problems: lang=php $id = uniq_id(); echo 'Click here to win!'; onloadRegister('new WinAHogController(%s, %s);', $id, $hog_name); By 2010 (particularly with the introduction of XHP) the API had become more sophisticated, but this is basically how most of Facebook's glue code still works as of mid-2011. If you view the source of any page, you'll see a bunch of ##onloadRegister()## calls in the markup which are generated like this. This mitigates many of the problems but is still fairly awkward. Escaping is easier, but still possible to get wrong. Stuff is a bit easier to grep for, but not much. You can't get very far with static analysis unless you get very complex. Coupling between the languages has been reduced but not eliminated. And now you have a bunch of classes which only really have glue code in them. Javelin behaviors provide a more structured solution to some of these problems: - All your Javascript code is in Javascript files, not embedded in strings in in some host language on the server side. - You can use static analysis and minification tools normally. - Provided you use a reasonable server-side library, you can't get escaping wrong. - Coupling is reduced because server only passes data to the client, never code. - The server declares client dependencies explicitly, not implicitly inside a string literal. Behaviors are also relatively easy to grep for. - Behaviors exist in a private, structured namespace instead of the global namespace. - Separation between the document's layout and behavior is a consequence of the structure of behaviors. - The entire interface the server may invoke against can be readily inferred. Note that Javelin does provide @{function:JX.onload}, which behaves like ##onloadRegister()##. However, its use is discouraged. The two major downsides to the behavior design appear to be: - They have a higher setup cost than the ad-hoc methods, but Javelin philosophically places a very low value on this. - Because there's a further setup cost to migrate an existing behavior into a class, behaviors sometimes grow little by little until they are too big, have more than just glue code, and should have been refactored into a real class some time ago. This is a pretty high-level drawback and is manageable through awareness of the risk and code review. - diff --git a/webroot/rsrc/externals/javelin/docs/onload.js b/webroot/rsrc/externals/javelin/docs/onload.js index 8772745045..7c76c08598 100644 --- a/webroot/rsrc/externals/javelin/docs/onload.js +++ b/webroot/rsrc/externals/javelin/docs/onload.js @@ -1,22 +1,21 @@ /** * @javelin */ /** * Register a callback for invocation after DOMContentReady. * * NOTE: Although it isn't private, use of this function is heavily discouraged. * See @{article:Concepts: Behaviors} for information on using behaviors to * structure and invoke glue code. * * This function is defined as a side effect of init.js. * * @param function Callback function to invoke after DOMContentReady. * @return void * @group util */ JX.onload = function(callback) { // This isn't the real function definition, it's only defined here to let the // documentation generator find it. The actual definition is in init.js. }; - diff --git a/webroot/rsrc/externals/javelin/ext/reactor/core/DynVal.js b/webroot/rsrc/externals/javelin/ext/reactor/core/DynVal.js index b65950caa9..ace3fd8ebe 100644 --- a/webroot/rsrc/externals/javelin/ext/reactor/core/DynVal.js +++ b/webroot/rsrc/externals/javelin/ext/reactor/core/DynVal.js @@ -1,48 +1,47 @@ /** * @provides javelin-dynval * @requires javelin-install * javelin-reactornode * javelin-util * javelin-reactor * @javelin */ JX.install('DynVal', { members : { _lastPulseVal : null, _reactorNode : null, getValueNow : function() { return this._lastPulseVal; }, getChanges : function() { return this._reactorNode; }, forceValueNow : function(value) { this.getChanges().forceSendValue(value); }, transform : function(fn) { return new JX.DynVal( this.getChanges().transform(fn), fn(this.getValueNow()) ); }, calm : function(min_interval) { return new JX.DynVal( this.getChanges().calm(min_interval), this.getValueNow() ); } }, construct : function(stream, init) { this._lastPulseVal = init; this._reactorNode = new JX.ReactorNode([stream], JX.bind(this, function(pulse) { if (this._lastPulseVal == pulse) { return JX.Reactor.DoNotPropagate; } this._lastPulseVal = pulse; return pulse; })); } }); - diff --git a/webroot/rsrc/externals/javelin/ext/reactor/core/Reactor.js b/webroot/rsrc/externals/javelin/ext/reactor/core/Reactor.js index 0dbefbf78c..a40d864dc8 100644 --- a/webroot/rsrc/externals/javelin/ext/reactor/core/Reactor.js +++ b/webroot/rsrc/externals/javelin/ext/reactor/core/Reactor.js @@ -1,90 +1,89 @@ /** * @provides javelin-reactor * @requires javelin-install * javelin-util * @javelin */ JX.install('Reactor', { statics : { /** * Return this value from a ReactorNode transformer to indicate that * its listeners should not be activated. */ DoNotPropagate : {}, /** * For internal use by the Reactor system. */ propagatePulse : function(start_pulse, start_node) { var reverse_post_order = JX.Reactor._postOrder(start_node).reverse(); start_node.primeValue(start_pulse); for (var ix = 0; ix < reverse_post_order.length; ix++) { var node = reverse_post_order[ix]; var pulse = node.getNextPulse(); if (pulse === JX.Reactor.DoNotPropagate) { continue; } var next_pulse = node.getTransformer()(pulse); var sends_to = node.getListeners(); for (var jx = 0; jx < sends_to.length; jx++) { sends_to[jx].primeValue(next_pulse); } } }, /** * For internal use by the Reactor system. */ _postOrder : function(node, result, pending) { if (typeof result === "undefined") { result = []; pending = {}; } pending[node.getGraphID()] = true; var nexts = node.getListeners(); for (var ix = 0; ix < nexts.length; ix++) { var next = nexts[ix]; if (pending[next.getGraphID()]) { continue; } JX.Reactor._postOrder(next, result, pending); } result.push(node); return result; }, // Helper for lift. _valueNow : function(fn, dynvals) { var values = []; for (var ix = 0; ix < dynvals.length; ix++) { values.push(dynvals[ix].getValueNow()); } return fn.apply(null, values); }, /** * Lift a function over normal values to be a function over dynvals. * @param fn A function expecting normal values * @param dynvals Array of DynVals whose instaneous values will be passed * to fn. * @return A DynVal representing the changing value of fn applies to dynvals * over time. */ lift : function(fn, dynvals) { var valueNow = JX.bind(null, JX.Reactor._valueNow, fn, dynvals); var streams = []; for (var ix = 0; ix < dynvals.length; ix++) { streams.push(dynvals[ix].getChanges()); } var result = new JX['ReactorNode'](streams, valueNow); return new JX['DynVal'](result, valueNow()); } } }); - diff --git a/webroot/rsrc/externals/javelin/ext/reactor/core/ReactorNode.js b/webroot/rsrc/externals/javelin/ext/reactor/core/ReactorNode.js index 490b4c10f4..02c60f9c41 100644 --- a/webroot/rsrc/externals/javelin/ext/reactor/core/ReactorNode.js +++ b/webroot/rsrc/externals/javelin/ext/reactor/core/ReactorNode.js @@ -1,97 +1,96 @@ /** * @provides javelin-reactornode * @requires javelin-install * javelin-reactor * javelin-util * javelin-reactor-node-calmer * @javelin */ JX.install('ReactorNode', { members : { _transformer : null, _sendsTo : null, _nextPulse : null, _graphID : null, getGraphID : function() { return this._graphID || this.__id__; }, setGraphID : function(id) { this._graphID = id; return this; }, setTransformer : function(fn) { this._transformer = fn; return this; }, /** * Set up dest as a listener to this. */ listen : function(dest) { this._sendsTo[dest.__id__] = dest; return { remove : JX.bind(null, this._removeListener, dest) }; }, /** * Helper for listen. */ _removeListener : function(dest) { delete this._sendsTo[dest.__id__]; }, /** * For internal use by the Reactor system */ primeValue : function(value) { this._nextPulse = value; }, getListeners : function() { var result = []; for (var k in this._sendsTo) { result.push(this._sendsTo[k]); } return result; }, /** * For internal use by the Reactor system */ getNextPulse : function(pulse) { return this._nextPulse; }, getTransformer : function() { return this._transformer; }, forceSendValue : function(pulse) { JX.Reactor.propagatePulse(pulse, this); }, // fn should return JX.Reactor.DoNotPropagate to indicate a value that // should not be retransmitted. transform : function(fn) { return new JX.ReactorNode([this], fn); }, /** * Suppress events to happen at most once per min_interval. * The last event that fires within an interval will fire at the end * of the interval. Events that are sandwiched between other events * within an interval are dropped. */ calm : function(min_interval) { var result = new JX.ReactorNode([this], JX.id); var transformer = new JX.ReactorNodeCalmer(result, min_interval); result.setTransformer(JX.bind(transformer, transformer.onPulse)); return result; } }, construct : function(source_streams, transformer) { this._nextPulse = JX.Reactor.DoNotPropagate; this._transformer = transformer; this._sendsTo = {}; for (var ix = 0; ix < source_streams.length; ix++) { source_streams[ix].listen(this); } } }); - diff --git a/webroot/rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js b/webroot/rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js index f06fd702dd..d652a7cbc0 100644 --- a/webroot/rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js +++ b/webroot/rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js @@ -1,48 +1,47 @@ /** * @provides javelin-reactor-node-calmer * @requires javelin-install * javelin-reactor * javelin-util * @javelin */ JX.install('ReactorNodeCalmer', { properties : { lastTime : 0, timeout : null, minInterval : 0, reactorNode : null, isEnabled : true }, construct : function(node, min_interval) { this.setLastTime(-min_interval); this.setMinInterval(min_interval); this.setReactorNode(node); }, members: { onPulse : function(pulse) { if (!this.getIsEnabled()) { return pulse; } var current_time = JX.now(); if (current_time - this.getLastTime() > this.getMinInterval()) { this.setLastTime(current_time); return pulse; } else { clearTimeout(this.getTimeout()); this.setTimeout(setTimeout( JX.bind(this, this.send, pulse), this.getLastTime() + this.getMinInterval() - current_time )); return JX.Reactor.DoNotPropagate; } }, send : function(pulse) { this.setLastTime(JX.now()); this.setIsEnabled(false); this.getReactorNode().forceSendValue(pulse); this.setIsEnabled(true); } } }); - diff --git a/webroot/rsrc/externals/javelin/ext/reactor/dom/RDOM.js b/webroot/rsrc/externals/javelin/ext/reactor/dom/RDOM.js index 48b7b9687f..f34907f27f 100644 --- a/webroot/rsrc/externals/javelin/ext/reactor/dom/RDOM.js +++ b/webroot/rsrc/externals/javelin/ext/reactor/dom/RDOM.js @@ -1,407 +1,405 @@ /** * Javelin Reactive functions to work with the DOM. * @provides javelin-reactor-dom * @requires javelin-dom * javelin-dynval * javelin-reactor * javelin-reactornode * javelin-install * javelin-util * @javelin */ JX.install('RDOM', { statics : { _time : null, /** * DynVal of the current time in milliseconds. */ time : function() { if (JX.RDOM._time === null) { var time = new JX.ReactorNode([], JX.id); window.setInterval(function() { time.forceSendValue(JX.now()); }, 100); JX.RDOM._time = new JX.DynVal(time, JX.now()); } return JX.RDOM._time; }, /** * Given a DynVal[String], return a DOM text node whose value tracks it. */ $DT : function(dyn_string) { var node = document.createTextNode(dyn_string.getValueNow()); dyn_string.transform(function(s) { node.data = s; }); return node; }, _recvEventPulses : function(node, event) { var reactor_node = new JX.ReactorNode([], JX.id); var no_path = null; JX.DOM.listen( node, event, no_path, JX.bind(reactor_node, reactor_node.forceSendValue) ); reactor_node.setGraphID(JX.DOM.uniqID(node)); return reactor_node; }, _recvChangePulses : function(node) { return JX.RDOM._recvEventPulses(node, 'change').transform(function() { return node.value; }); }, /** * Sets up a bidirectional DynVal for a node. * @param node :: DOM Node * @param inPulsesFn :: DOM Node -> ReactorNode * @param inDynValFn :: DOM Node -> ReactorNode -> DynVal * @param outFn :: ReactorNode -> DOM Node */ _bidi : function(node, inPulsesFn, inDynValFn, outFn) { var inPulses = inPulsesFn(node); var inDynVal = inDynValFn(node, inPulses); outFn(inDynVal.getChanges(), node); inDynVal.getChanges().listen(inPulses); return inDynVal; }, /** * ReactorNode[String] of the incoming values of a radio group. * @param Array of DOM elements, all the radio buttons in a group. */ _recvRadioPulses : function(buttons) { var ins = []; for (var ii = 0; ii < buttons.length; ii++) { ins.push(JX.RDOM._recvChangePulses(buttons[ii])); } return new JX.ReactorNode(ins, JX.id); }, /** * DynVal[String] of the incoming values of a radio group. * pulses is a ReactorNode[String] of the incoming values of the group */ _recvRadio : function(buttons, pulses) { var init = ''; for (var ii = 0; ii < buttons.length; ii++) { if (buttons[ii].checked) { init = buttons[ii].value; break; } } return new JX.DynVal(pulses, init); }, /** * Send the pulses from the ReactorNode[String] to the radio group. * Sending an invalid value will result in a log message in __DEV__. */ _sendRadioPulses : function(rnode, buttons) { return rnode.transform(function(val) { var found; if (__DEV__) { found = false; } for (var ii = 0; ii < buttons.length; ii++) { if (buttons[ii].value == val) { buttons[ii].checked = true; if (__DEV__) { found = true; } } } if (__DEV__) { if (!found) { throw new Error("Mismatched radio button value"); } } }); }, /** * Bidirectional DynVal[String] for a radio group. * Sending an invalid value will result in a log message in __DEV__. */ radio : function(input) { return JX.RDOM._bidi( input, JX.RDOM._recvRadioPulses, JX.RDOM._recvRadio, JX.RDOM._sendRadioPulses ); }, /** * ReactorNode[Boolean] of the values of the checkbox when it changes. */ _recvCheckboxPulses : function(checkbox) { return JX.RDOM._recvChangePulses(checkbox).transform(function(val) { return Boolean(val); }); }, /** * DynVal[Boolean] of the value of a checkbox. */ _recvCheckbox : function(checkbox, pulses) { return new JX.DynVal(pulses, Boolean(checkbox.checked)); }, /** * Send the pulses from the ReactorNode[Boolean] to the checkbox */ _sendCheckboxPulses : function(rnode, checkbox) { return rnode.transform(function(val) { if (__DEV__) { if (!(val === true || val === false)) { throw new Error("Send boolean values to checkboxes."); } } checkbox.checked = val; }); }, /** * Bidirectional DynVal[Boolean] for a checkbox. */ checkbox : function(input) { return JX.RDOM._bidi( input, JX.RDOM._recvCheckboxPulses, JX.RDOM._recvCheckbox, JX.RDOM._sendCheckboxPulses ); }, /** * ReactorNode[String] of the changing values of a text input. */ _recvInputPulses : function(input) { // This misses advanced changes like paste events. var live_changes = [ JX.RDOM._recvChangePulses(input), JX.RDOM._recvEventPulses(input, 'keyup'), JX.RDOM._recvEventPulses(input, 'keypress'), JX.RDOM._recvEventPulses(input, 'keydown') ]; return new JX.ReactorNode(live_changes, function() { return input.value; }); }, /** * DynVal[String] of the value of a text input. */ _recvInput : function(input, pulses) { return new JX.DynVal(pulses, input.value); }, /** * Send the pulses from the ReactorNode[String] to the input */ _sendInputPulses : function(rnode, input) { var result = rnode.transform(function(val) { input.value = val; }); result.setGraphID(JX.DOM.uniqID(input)); return result; }, /** * Bidirectional DynVal[String] for a text input. */ input : function(input) { return JX.RDOM._bidi( input, JX.RDOM._recvInputPulses, JX.RDOM._recvInput, JX.RDOM._sendInputPulses ); }, /** * ReactorNode[String] of the incoming changes in value of a select element. */ _recvSelectPulses : function(select) { return JX.RDOM._recvChangePulses(select); }, /** * DynVal[String] of the value of a select element. */ _recvSelect : function(select, pulses) { return new JX.DynVal(pulses, select.value); }, /** * Send the pulses from the ReactorNode[String] to the select. * Sending an invalid value will result in a log message in __DEV__. */ _sendSelectPulses : function(rnode, select) { return rnode.transform(function(val) { select.value = val; if (__DEV__) { if (select.value !== val) { throw new Error("Mismatched select value"); } } }); }, /** * Bidirectional DynVal[String] for the value of a select. */ select : function(select) { return JX.RDOM._bidi( select, JX.RDOM._recvSelectPulses, JX.RDOM._recvSelect, JX.RDOM._sendSelectPulses ); }, /** * ReactorNode[undefined] that fires when a button is clicked. */ clickPulses : function(button) { return JX.RDOM._recvEventPulses(button, 'click').transform(function() { return null; }); }, /** * ReactorNode[Boolean] of whether the mouse is over a target. */ _recvIsMouseOverPulses : function(target) { var mouseovers = JX.RDOM._recvEventPulses(target, 'mouseover').transform( function() { return true; }); var mouseouts = JX.RDOM._recvEventPulses(target, 'mouseout').transform( function() { return false; }); return new JX.ReactorNode([mouseovers, mouseouts], JX.id); }, /** * DynVal[Boolean] of whether the mouse is over a target. */ isMouseOver : function(target) { // Not worth it to initialize this properly. return new JX.DynVal(JX.RDOM._recvIsMouseOverPulses(target), false); }, /** * ReactorNode[Boolean] of whether an element has the focus. */ _recvHasFocusPulses : function(target) { var focuses = JX.RDOM._recvEventPulses(target, 'focus').transform( function() { return true; }); var blurs = JX.RDOM._recvEventPulses(target, 'blur').transform( function() { return false; }); return new JX.ReactorNode([focuses, blurs], JX.id); }, /** * DynVal[Boolean] of whether an element has the focus. */ _recvHasFocus : function(target) { var is_focused_now = (target === document.activeElement); return new JX.DynVal(JX.RDOM._recvHasFocusPulses(target), is_focused_now); }, _sendHasFocusPulses : function(rnode, target) { rnode.transform(function(should_focus) { if (should_focus) { target.focus(); } else { target.blur(); } return should_focus; }); }, /** * Bidirectional DynVal[Boolean] of whether an element has the focus. */ hasFocus : function(target) { return JX.RDOM._bidi( target, JX.RDOM._recvHasFocusPulses, JX.RDOM._recvHasFocus, JX.RDOM._sendHasFocusPulses ); }, /** * Send a CSS class from a DynVal to a node */ sendClass : function(dynval, node, className) { return dynval.transform(function(add) { JX.DOM.alterClass(node, className, add); }); }, /** * Dynamically attach a set of DynVals to a DOM node's properties as * specified by props. * props: {left: someDynVal, style: {backgroundColor: someOtherDynVal}} */ sendProps : function(node, props) { var dynvals = []; var keys = []; var style_keys = []; for (var key in props) { keys.push(key); if (key === 'style') { for (var style_key in props[key]) { style_keys.push(style_key); dynvals.push(props[key][style_key]); node.style[style_key] = props[key][style_key].getValueNow(); } } else { dynvals.push(props[key]); node[key] = props[key].getValueNow(); } } return JX.Reactor.lift(JX.bind(null, function(keys, style_keys, node) { var args = JX.$A(arguments).slice(3); for (var ii = 0; ii < args.length; ii++) { if (keys[ii] === 'style') { for (var jj = 0; jj < style_keys.length; jj++) { node.style[style_keys[jj]] = args[ii]; ii++; } ii--; } else { node[keys[ii]] = args[ii]; } } }, keys, style_keys, node), dynvals); } } }); - - diff --git a/webroot/rsrc/externals/javelin/ext/view/HTMLView.js b/webroot/rsrc/externals/javelin/ext/view/HTMLView.js index aea6f7c7bd..244b252e05 100644 --- a/webroot/rsrc/externals/javelin/ext/view/HTMLView.js +++ b/webroot/rsrc/externals/javelin/ext/view/HTMLView.js @@ -1,138 +1,137 @@ /** * Dumb HTML views. Mostly to demonstrate how the visitor pattern over these * views works, as driven by validation. I'm not convinced it's actually a good * idea to do validation. * * @provides javelin-view-html * @requires javelin-install * javelin-dom * javelin-view-visitor * javelin-util */ JX.install('HTMLView', { extend: 'View', members : { render: function(rendered_children) { return JX.$N(this.getName(), this.getAllAttributes(), rendered_children); }, validate: function() { this.accept(JX.HTMLView.getValidatingVisitor()); } }, statics: { getValidatingVisitor: function() { return new JX.ViewVisitor(JX.HTMLView.validate); }, validate: function(view, children) { var spec = this._getHTMLSpec(); if (!(view.getName() in spec)) { throw new Error("invalid tag"); } var tag_spec = spec[view.getName()]; var attrs = view.getAllAttributes(); for (var attr in attrs) { if (!(attr in tag_spec)) { throw new Error("invalid attr"); } var validator = tag_spec[attr]; if (typeof validator === "function") { return validator(attrs[attr]); } } return true; }, _validateRel: function(target) { return target in { "_blank": 1, "_self": 1, "_parent": 1, "_top": 1 }; }, _getHTMLSpec: function() { var attrs_any_can_have = { className: 1, id: 1, sigil: 1 }; var form_elem_attrs = { name: 1, value: 1 }; var spec = { a: { href: 1, target: JX.HTMLView._validateRel }, b: {}, blockquote: {}, br: {}, button: JX.copy({}, form_elem_attrs), canvas: {}, code: {}, dd: {}, div: {}, dl: {}, dt: {}, em: {}, embed: {}, fieldset: {}, form: { type: 1 }, h1: {}, h2: {}, h3: {}, h4: {}, h5: {}, h6: {}, hr: {}, i: {}, iframe: { src: 1 }, img: { src: 1, alt: 1 }, input: JX.copy({}, form_elem_attrs), label: {'for': 1}, li: {}, ol: {}, optgroup: {}, option: JX.copy({}, form_elem_attrs), p: {}, pre: {}, q: {}, select: {}, span: {}, strong: {}, sub: {}, sup: {}, table: {}, tbody: {}, td: {}, textarea: {}, tfoot: {}, th: {}, thead: {}, tr: {}, ul: {} }; for (var k in spec) { JX.copy(spec[k], attrs_any_can_have); } return spec; }, registerToInterpreter: function(view_interpreter) { var spec = this._getHTMLSpec(); for (var tag in spec) { view_interpreter.register(tag, JX.HTMLView); } return view_interpreter; } } }); - diff --git a/webroot/rsrc/externals/javelin/ext/view/View.js b/webroot/rsrc/externals/javelin/ext/view/View.js index 39d608a715..3e67320bcf 100644 --- a/webroot/rsrc/externals/javelin/ext/view/View.js +++ b/webroot/rsrc/externals/javelin/ext/view/View.js @@ -1,192 +1,191 @@ /** * A View is a composable wrapper on JX.$N, allowing abstraction of higher-order * views and a consistent pattern of parameterization. It is intended * to be used either directly or as a building block for a syntactic sugar layer * for concise expression of markup patterns. * * @provides javelin-view * @requires javelin-install * javelin-util */ JX.install('View', { construct : function(attrs, children) { this._attributes = JX.copy({}, this.getDefaultAttributeValues()); JX.copy(this._attributes, attrs); this._rawChildren = {}; this._childKeys = []; if (children) { this.addChildren(JX.$AX(children)); } this.setName(this.__class__.__readable__); }, events: [ 'change' ], properties: { 'name': null }, members : { _attributes : null, _rawChildren : null, _childKeys: null, // keys of rawChildren, kept ordered. _nextChildKey: 0, // next key to use for a new child /* * Don't override. * TODO: Strongly typed attribute access (getIntAttr, getStringAttr...)? */ getAttr : function(attrName) { return this._attributes[attrName]; }, /* * Don't override. */ multisetAttr : function(attrs) { JX.copy(this._attributes, attrs); this.invoke('change'); return this; }, /* * Don't override. */ setAttr : function(attrName, value) { this._attributes[attrName] = value; this.invoke('change'); return this; }, /* * Child views can override to specify default values for attributes. */ getDefaultAttributeValues : function() { return {}; }, /** * Don't override. */ getAllAttributes: function() { return JX.copy({}, this._attributes); }, /** * Get the children. Don't override. */ getChildren : function() { var result = []; var should_repack = false; var ii; var key; for (ii = 0; ii < this._childKeys.length; ii++) { key = this._childKeys[ii]; if (this._rawChildren[key] === undefined) { should_repack = true; } else { result.push(this._rawChildren[key]); } } if (should_repack) { var new_child_keys = []; for (ii = 0; ii < this._childKeys.length; ii++) { key = this._childKeys[ii]; if (this._rawChildren[key] !== undefined) { new_child_keys.push(key); } } this._childKeys = new_child_keys; } return result; }, /** * Add children to the view. Returns array of removal handles. * Don't override. */ addChildren : function(children) { var result = []; for (var ii = 0; ii < children.length; ii++) { result.push(this._addChild(children[ii])); } this.invoke('change'); return result; }, /** * Add a single child view to the view. * Returns a removal handle, i.e. an object that has a method remove(), * that removes the added child from the view. * * Don't override. */ addChild: function(child) { var result = this._addChild(child); this.invoke('change'); return result; }, _addChild: function(child) { var key = this._nextChildKey++; this._rawChildren[key] = child; this._childKeys.push(key); return { remove: JX.bind(this, this._removeChild, key) }; }, _removeChild: function(child_key) { delete this._rawChildren[child_key]; this.invoke('change'); }, /** * Accept visitors. This allows adding new behaviors to Views without * having to change View classes themselves. * * This implements a post-order traversal over the tree of views. Children * are processed before parents, and for convenience the results of the * visitor on the children are passed to it when processing the parent. * * The visitor parameter is a callable which receives two parameters. * The first parameter is the view to visit. The second parameter is an * array of the results of visiting the view's children. * * Don't override. */ accept: function(visitor) { var results = []; var children = this.getChildren(); for(var ii = 0; ii < children.length; ii++) { var result; if (children[ii].accept) { result = children[ii].accept(visitor); } else { result = children[ii]; } results.push(result); } return visitor(this, results); }, /** * Given the already-rendered children, return the rendered result of * this view. * By default, just pass the children through. */ render: function(rendered_children) { return rendered_children; } } }); - diff --git a/webroot/rsrc/externals/javelin/ext/view/ViewRenderer.js b/webroot/rsrc/externals/javelin/ext/view/ViewRenderer.js index 3f11a3bec0..3fd3493683 100644 --- a/webroot/rsrc/externals/javelin/ext/view/ViewRenderer.js +++ b/webroot/rsrc/externals/javelin/ext/view/ViewRenderer.js @@ -1,20 +1,19 @@ /** * @provides javelin-view-renderer * @requires javelin-install * javelin-util */ JX.install('ViewRenderer', { members: { visit: function(view, children) { return view.render(children); } }, statics: { render: function(view) { var renderer = new JX.ViewRenderer(); return view.accept(JX.bind(renderer, renderer.visit)); } } }); - diff --git a/webroot/rsrc/externals/javelin/ext/view/ViewVisitor.js b/webroot/rsrc/externals/javelin/ext/view/ViewVisitor.js index 8c073cecb1..53d430c708 100644 --- a/webroot/rsrc/externals/javelin/ext/view/ViewVisitor.js +++ b/webroot/rsrc/externals/javelin/ext/view/ViewVisitor.js @@ -1,36 +1,35 @@ /** * @provides javelin-view-visitor * @requires javelin-install * javelin-util * * Add new behaviors to views without changing the view classes themselves. * * Allows you to register specific visitor functions for certain view classes. * If no visitor is registered for a view class, the default_visitor is used. * If no default_visitor is invoked, a no-op visitor is used. * * Registered visitors should be functions with signature * function(view, results_of_visiting_children) {} * Children are visited before their containing parents, and the return values * of the visitor on the children are passed to the parent. * */ JX.install('ViewVisitor', { construct: function(default_visitor) { this._visitors = {}; this._default = default_visitor || JX.bag; }, members: { _visitors: null, _default: null, register: function(cls, visitor) { this._visitors[cls] = visitor; }, visit: function(view, children) { var visitor = this._visitors[cls] || this._default; return visitor(view, children); } } }); - diff --git a/webroot/rsrc/externals/javelin/lib/DOM.js b/webroot/rsrc/externals/javelin/lib/DOM.js index 5c94d406bc..42608a8e92 100644 --- a/webroot/rsrc/externals/javelin/lib/DOM.js +++ b/webroot/rsrc/externals/javelin/lib/DOM.js @@ -1,965 +1,964 @@ /** * @requires javelin-magical-init * javelin-install * javelin-util * javelin-vector * javelin-stratcom * @provides javelin-dom * * @javelin-installs JX.$ * @javelin-installs JX.$N * @javelin-installs JX.$H * * @javelin */ /** * Select an element by its "id" attribute, like ##document.getElementById()##. * For example: * * var node = JX.$('some_id'); * * This will select the node with the specified "id" attribute: * * LANG=HTML *
...
* * If the specified node does not exist, @{JX.$()} will throw an exception. * * For other ways to select nodes from the document, see @{JX.DOM.scry()} and * @{JX.DOM.find()}. * * @param string "id" attribute to select from the document. * @return Node Node with the specified "id" attribute. * * @group dom */ JX.$ = function(id) { if (__DEV__) { if (!id) { JX.$E('Empty ID passed to JX.$()!'); } } var node = document.getElementById(id); if (!node || (node.id != id)) { if (__DEV__) { if (node && (node.id != id)) { JX.$E( 'JX.$("'+id+'"): '+ 'document.getElementById() returned an element without the '+ 'correct ID. This usually means that the element you are trying '+ 'to select is being masked by a form with the same value in its '+ '"name" attribute.'); } } JX.$E("JX.$('" + id + "') call matched no nodes."); } return node; }; /** * Upcast a string into an HTML object so it is treated as markup instead of * plain text. See @{JX.$N} for discussion of Javelin's security model. Every * time you call this function you potentially open up a security hole. Avoid * its use wherever possible. * * This class intentionally supports only a subset of HTML because many browsers * named "Internet Explorer" have awkward restrictions around what they'll * accept for conversion to document fragments. Alter your datasource to emit * valid HTML within this subset if you run into an unsupported edge case. All * the edge cases are crazy and you should always be reasonably able to emit * a cohesive tag instead of an unappendable fragment. * * You may use @{JX.$H} as a shortcut for creating new JX.HTML instances: * * JX.$N('div', {}, some_html_blob); // Treat as string (safe) * JX.$N('div', {}, JX.$H(some_html_blob)); // Treat as HTML (unsafe!) * * @task build String into HTML * @task nodes HTML into Nodes * * @group dom */ JX.install('HTML', { construct : function(str) { if (str instanceof JX.HTML) { this._content = str._content; return; } if (__DEV__) { if ((typeof str !== 'string') && (!str || !str.match)) { JX.$E( 'new JX.HTML(): ' + 'call initializes an HTML object with an empty value.'); } var tags = ['legend', 'thead', 'tbody', 'tfoot', 'column', 'colgroup', 'caption', 'tr', 'th', 'td', 'option']; var evil_stuff = new RegExp('^\\s*<(' + tags.join('|') + ')\\b', 'i'); var match = str.match(evil_stuff); if (match) { JX.$E( 'new JX.HTML("<' + match[1] + '>..."): ' + 'call initializes an HTML object with an invalid partial fragment ' + 'and can not be converted into DOM nodes. The enclosing tag of an ' + 'HTML content string must be appendable to a document fragment. ' + 'For example, is allowed but or are not.'); } var really_evil = /..."): ' + 'call initializes an HTML object with an embedded script tag! ' + 'Are you crazy?! Do NOT do this!!!'); } var wont_work = /..."): ' + 'call initializes an HTML object with an embedded tag. IE ' + 'will not do the right thing with this.'); } // TODO(epriestley): May need to deny