Page MenuHomestyx hydra

No OneTemporary

diff --git a/resources/sql/autopatches/20140805.boardcol.2.php b/resources/sql/autopatches/20140805.boardcol.2.php
index 317de4e370..40d6c46ec2 100644
--- a/resources/sql/autopatches/20140805.boardcol.2.php
+++ b/resources/sql/autopatches/20140805.boardcol.2.php
@@ -1,53 +1,53 @@
<?php
// Was PhabricatorEdgeConfig::TYPE_COLUMN_HAS_OBJECT
$type_has_object = 44;
$column = new PhabricatorProjectColumn();
$conn_w = $column->establishConnection('w');
$rows = queryfx_all(
$conn_w,
'SELECT src, dst FROM %T WHERE type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
$type_has_object);
$cols = array();
foreach ($rows as $row) {
$cols[$row['src']][] = $row['dst'];
}
$sql = array();
foreach ($cols as $col_phid => $obj_phids) {
echo pht("Migrating column '%s'...", $col_phid)."\n";
$column = id(new PhabricatorProjectColumn())->loadOneWhere(
'phid = %s',
$col_phid);
if (!$column) {
echo pht("Column '%s' does not exist.", $col_phid)."\n";
continue;
}
$sequence = 0;
foreach ($obj_phids as $obj_phid) {
$sql[] = qsprintf(
$conn_w,
'(%s, %s, %s, %d)',
$column->getProjectPHID(),
$column->getPHID(),
$obj_phid,
$sequence++);
}
}
echo pht('Inserting rows...')."\n";
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (boardPHID, columnPHID, objectPHID, sequence)
- VALUES %Q',
+ VALUES %LQ',
id(new PhabricatorProjectColumnPosition())->getTableName(),
$chunk);
}
echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130820.file-mailkey-populate.php b/resources/sql/patches/20130820.file-mailkey-populate.php
index ba4d6d1606..0db10bef58 100644
--- a/resources/sql/patches/20130820.file-mailkey-populate.php
+++ b/resources/sql/patches/20130820.file-mailkey-populate.php
@@ -1,38 +1,38 @@
<?php
echo pht('Populating Phabricator files with mail keys xactions...')."\n";
$table = new PhabricatorFile();
$table_name = $table->getTableName();
$conn_w = $table->establishConnection('w');
$conn_w->openTransaction();
$sql = array();
foreach (new LiskRawMigrationIterator($conn_w, 'file') as $row) {
// NOTE: MySQL requires that the INSERT specify all columns which don't
// have default values when configured in strict mode. This query will
// never actually insert rows, but we need to hand it values anyway.
$sql[] = qsprintf(
$conn_w,
'(%d, %s, 0, 0, 0, 0, 0, 0, 0, 0)',
$row['id'],
Filesystem::readRandomCharacters(20));
}
if ($sql) {
- foreach (PhabricatorLiskDAO::chunkSQL($sql, ', ') as $chunk) {
+ foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T
(id, mailKey, phid, byteSize, storageEngine, storageFormat,
- storageHandle, dateCreated, dateModified, metadata) VALUES %Q '.
+ storageHandle, dateCreated, dateModified, metadata) VALUES %LQ '.
'ON DUPLICATE KEY UPDATE mailKey = VALUES(mailKey)',
$table_name,
$chunk);
}
}
$table->saveTransaction();
echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20131106.diffphid.2.mig.php b/resources/sql/patches/20131106.diffphid.2.mig.php
index 67fd14aad0..7976c910eb 100644
--- a/resources/sql/patches/20131106.diffphid.2.mig.php
+++ b/resources/sql/patches/20131106.diffphid.2.mig.php
@@ -1,47 +1,47 @@
<?php
$diff_table = new DifferentialDiff();
$conn_w = $diff_table->establishConnection('w');
$size = 1000;
$row_iter = id(new LiskMigrationIterator($diff_table))->setPageSize($size);
$chunk_iter = new PhutilChunkedIterator($row_iter, $size);
foreach ($chunk_iter as $chunk) {
$sql = array();
foreach ($chunk as $diff) {
$id = $diff->getID();
echo pht('Migrating diff ID %d...', $id)."\n";
$phid = $diff->getPHID();
if (strlen($phid)) {
continue;
}
$type_diff = DifferentialDiffPHIDType::TYPECONST;
$new_phid = PhabricatorPHID::generateNewPHID($type_diff);
$sql[] = qsprintf(
$conn_w,
'(%d, %s)',
$id,
$new_phid);
}
if (!$sql) {
continue;
}
- foreach (PhabricatorLiskDAO::chunkSQL($sql, ', ') as $sql_chunk) {
+ foreach (PhabricatorLiskDAO::chunkSQL($sql) as $sql_chunk) {
queryfx(
$conn_w,
- 'INSERT IGNORE INTO %T (id, phid) VALUES %Q
+ 'INSERT IGNORE INTO %T (id, phid) VALUES %LQ
ON DUPLICATE KEY UPDATE phid = VALUES(phid)',
$diff_table->getTableName(),
$sql_chunk);
}
}
echo pht('Done.')."\n";
diff --git a/src/applications/cache/PhabricatorKeyValueDatabaseCache.php b/src/applications/cache/PhabricatorKeyValueDatabaseCache.php
index c6a52024fe..0b4609074a 100644
--- a/src/applications/cache/PhabricatorKeyValueDatabaseCache.php
+++ b/src/applications/cache/PhabricatorKeyValueDatabaseCache.php
@@ -1,174 +1,174 @@
<?php
final class PhabricatorKeyValueDatabaseCache
extends PhutilKeyValueCache {
const CACHE_FORMAT_RAW = 'raw';
const CACHE_FORMAT_DEFLATE = 'deflate';
public function setKeys(array $keys, $ttl = null) {
if (PhabricatorEnv::isReadOnly()) {
return;
}
if ($keys) {
$map = $this->digestKeys(array_keys($keys));
$conn_w = $this->establishConnection('w');
$sql = array();
foreach ($map as $key => $hash) {
$value = $keys[$key];
list($format, $storage_value) = $this->willWriteValue($key, $value);
$sql[] = qsprintf(
$conn_w,
'(%s, %s, %s, %B, %d, %nd)',
$hash,
$key,
$format,
$storage_value,
time(),
$ttl ? (time() + $ttl) : null);
}
$guard = AphrontWriteGuard::beginScopedUnguardedWrites();
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T
(cacheKeyHash, cacheKey, cacheFormat, cacheData,
- cacheCreated, cacheExpires) VALUES %Q
+ cacheCreated, cacheExpires) VALUES %LQ
ON DUPLICATE KEY UPDATE
cacheKey = VALUES(cacheKey),
cacheFormat = VALUES(cacheFormat),
cacheData = VALUES(cacheData),
cacheCreated = VALUES(cacheCreated),
cacheExpires = VALUES(cacheExpires)',
$this->getTableName(),
$chunk);
}
unset($guard);
}
return $this;
}
public function getKeys(array $keys) {
$results = array();
if ($keys) {
$map = $this->digestKeys($keys);
$rows = queryfx_all(
$this->establishConnection('r'),
'SELECT * FROM %T WHERE cacheKeyHash IN (%Ls)',
$this->getTableName(),
$map);
$rows = ipull($rows, null, 'cacheKey');
foreach ($keys as $key) {
if (empty($rows[$key])) {
continue;
}
$row = $rows[$key];
if ($row['cacheExpires'] && ($row['cacheExpires'] < time())) {
continue;
}
try {
$results[$key] = $this->didReadValue(
$row['cacheFormat'],
$row['cacheData']);
} catch (Exception $ex) {
// Treat this as a cache miss.
phlog($ex);
}
}
}
return $results;
}
public function deleteKeys(array $keys) {
if ($keys) {
$map = $this->digestKeys($keys);
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE cacheKeyHash IN (%Ls)',
$this->getTableName(),
$map);
}
return $this;
}
public function destroyCache() {
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T',
$this->getTableName());
return $this;
}
/* -( Raw Cache Access )--------------------------------------------------- */
public function establishConnection($mode) {
// TODO: This is the only concrete table we have on the database right
// now.
return id(new PhabricatorMarkupCache())->establishConnection($mode);
}
public function getTableName() {
return 'cache_general';
}
/* -( Implementation )----------------------------------------------------- */
private function digestKeys(array $keys) {
$map = array();
foreach ($keys as $key) {
$map[$key] = PhabricatorHash::digestForIndex($key);
}
return $map;
}
private function willWriteValue($key, $value) {
if (!is_string($value)) {
throw new Exception(pht('Only strings may be written to the DB cache!'));
}
static $can_deflate;
if ($can_deflate === null) {
$can_deflate = function_exists('gzdeflate') &&
PhabricatorEnv::getEnvConfig('cache.enable-deflate');
}
if ($can_deflate) {
$deflated = PhabricatorCaches::maybeDeflateData($value);
if ($deflated !== null) {
return array(self::CACHE_FORMAT_DEFLATE, $deflated);
}
}
return array(self::CACHE_FORMAT_RAW, $value);
}
private function didReadValue($format, $value) {
switch ($format) {
case self::CACHE_FORMAT_RAW:
return $value;
case self::CACHE_FORMAT_DEFLATE:
return PhabricatorCaches::inflateData($value);
default:
throw new Exception(pht('Unknown cache format.'));
}
}
}
diff --git a/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php b/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php
index 59d8476c87..cf7fc30554 100644
--- a/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php
+++ b/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php
@@ -1,305 +1,305 @@
<?php
final class PhabricatorCalendarNotificationEngine
extends Phobject {
private $cursor;
private $notifyWindow;
public function getCursor() {
if (!$this->cursor) {
$now = PhabricatorTime::getNow();
$this->cursor = $now - phutil_units('10 minutes in seconds');
}
return $this->cursor;
}
public function setCursor($cursor) {
$this->cursor = $cursor;
return $this;
}
public function setNotifyWindow($notify_window) {
$this->notifyWindow = $notify_window;
return $this;
}
public function getNotifyWindow() {
if (!$this->notifyWindow) {
return phutil_units('15 minutes in seconds');
}
return $this->notifyWindow;
}
public function publishNotifications() {
$cursor = $this->getCursor();
$now = PhabricatorTime::getNow();
if ($cursor > $now) {
return;
}
$calendar_class = 'PhabricatorCalendarApplication';
if (!PhabricatorApplication::isClassInstalled($calendar_class)) {
return;
}
try {
$lock = PhabricatorGlobalLock::newLock('calendar.notify')
->lock(5);
} catch (PhutilLockException $ex) {
return;
}
$caught = null;
try {
$this->sendNotifications();
} catch (Exception $ex) {
$caught = $ex;
}
$lock->unlock();
// Wait a little while before checking for new notifications to send.
$this->setCursor($cursor + phutil_units('1 minute in seconds'));
if ($caught) {
throw $caught;
}
}
private function sendNotifications() {
$cursor = $this->getCursor();
$window_min = $cursor - phutil_units('16 hours in seconds');
$window_max = $cursor + phutil_units('16 hours in seconds');
$viewer = PhabricatorUser::getOmnipotentUser();
$events = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withDateRange($window_min, $window_max)
->withIsCancelled(false)
->withIsImported(false)
->setGenerateGhosts(true)
->execute();
if (!$events) {
// No events are starting soon in any timezone, so there is nothing
// left to be done.
return;
}
$attendee_map = array();
foreach ($events as $key => $event) {
$notifiable_phids = array();
foreach ($event->getInvitees() as $invitee) {
if (!$invitee->isAttending()) {
continue;
}
$notifiable_phids[] = $invitee->getInviteePHID();
}
if ($notifiable_phids) {
$attendee_map[$key] = array_fuse($notifiable_phids);
} else {
unset($events[$key]);
}
}
if (!$attendee_map) {
// None of the events have any notifiable attendees, so there is no
// one to notify of anything.
return;
}
$all_attendees = array();
foreach ($attendee_map as $key => $attendee_phids) {
foreach ($attendee_phids as $attendee_phid) {
$all_attendees[$attendee_phid] = $attendee_phid;
}
}
$user_map = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs($all_attendees)
->withIsDisabled(false)
->needUserSettings(true)
->execute();
$user_map = mpull($user_map, null, 'getPHID');
if (!$user_map) {
// None of the attendees are valid users: they're all imported users
// or projects or invalid or some other kind of unnotifiable entity.
return;
}
$all_event_phids = array();
foreach ($events as $key => $event) {
foreach ($event->getNotificationPHIDs() as $phid) {
$all_event_phids[$phid] = $phid;
}
}
$table = new PhabricatorCalendarNotification();
$conn = $table->establishConnection('w');
$rows = queryfx_all(
$conn,
'SELECT * FROM %T WHERE eventPHID IN (%Ls) AND targetPHID IN (%Ls)',
$table->getTableName(),
$all_event_phids,
$all_attendees);
$sent_map = array();
foreach ($rows as $row) {
$event_phid = $row['eventPHID'];
$target_phid = $row['targetPHID'];
$initial_epoch = $row['utcInitialEpoch'];
$sent_map[$event_phid][$target_phid][$initial_epoch] = $row;
}
$now = PhabricatorTime::getNow();
$notify_min = $now;
$notify_max = $now + $this->getNotifyWindow();
$notify_map = array();
foreach ($events as $key => $event) {
$initial_epoch = $event->getUTCInitialEpoch();
$event_phids = $event->getNotificationPHIDs();
// Select attendees who actually exist, and who we have not sent any
// notifications to yet.
$attendee_phids = $attendee_map[$key];
$users = array_select_keys($user_map, $attendee_phids);
foreach ($users as $user_phid => $user) {
foreach ($event_phids as $event_phid) {
if (isset($sent_map[$event_phid][$user_phid][$initial_epoch])) {
unset($users[$user_phid]);
continue 2;
}
}
}
if (!$users) {
continue;
}
// Discard attendees for whom the event start time isn't soon. Events
// may start at different times for different users, so we need to
// check every user's start time.
foreach ($users as $user_phid => $user) {
$user_datetime = $event->newStartDateTime()
->setViewerTimezone($user->getTimezoneIdentifier());
$user_epoch = $user_datetime->getEpoch();
if ($user_epoch < $notify_min || $user_epoch > $notify_max) {
unset($users[$user_phid]);
continue;
}
$view = id(new PhabricatorCalendarEventNotificationView())
->setViewer($user)
->setEvent($event)
->setDateTime($user_datetime)
->setEpoch($user_epoch);
$notify_map[$user_phid][] = $view;
}
}
$mail_list = array();
$mark_list = array();
$now = PhabricatorTime::getNow();
foreach ($notify_map as $user_phid => $events) {
$user = $user_map[$user_phid];
$locale = PhabricatorEnv::beginScopedLocale($user->getTranslation());
$caught = null;
try {
$mail_list[] = $this->newMailMessage($user, $events);
} catch (Exception $ex) {
$caught = $ex;
}
unset($locale);
if ($caught) {
throw $ex;
}
foreach ($events as $view) {
$event = $view->getEvent();
foreach ($event->getNotificationPHIDs() as $phid) {
$mark_list[] = qsprintf(
$conn,
'(%s, %s, %d, %d)',
$phid,
$user_phid,
$event->getUTCInitialEpoch(),
$now);
}
}
}
// Mark all the notifications we're about to send as delivered so we
// do not double-notify.
foreach (PhabricatorLiskDAO::chunkSQL($mark_list) as $chunk) {
queryfx(
$conn,
'INSERT IGNORE INTO %T
(eventPHID, targetPHID, utcInitialEpoch, didNotifyEpoch)
- VALUES %Q',
+ VALUES %LQ',
$table->getTableName(),
$chunk);
}
foreach ($mail_list as $mail) {
$mail->saveAndSend();
}
}
private function newMailMessage(PhabricatorUser $viewer, array $events) {
$events = msort($events, 'getEpoch');
$next_event = head($events);
$body = new PhabricatorMetaMTAMailBody();
foreach ($events as $event) {
$body->addTextSection(
null,
pht(
'%s is starting in %s minute(s), at %s.',
$event->getEvent()->getName(),
$event->getDisplayMinutes(),
$event->getDisplayTimeWithTimezone()));
$body->addLinkSection(
pht('EVENT DETAIL'),
PhabricatorEnv::getProductionURI($event->getEvent()->getURI()));
}
$next_event = head($events)->getEvent();
$subject = $next_event->getName();
if (count($events) > 1) {
$more = pht(
'(+%s more...)',
new PhutilNumber(count($events) - 1));
$subject = "{$subject} {$more}";
}
$calendar_phid = id(new PhabricatorCalendarApplication())
->getPHID();
return id(new PhabricatorMetaMTAMail())
->setSubject($subject)
->addTos(array($viewer->getPHID()))
->setSensitiveContent(false)
->setFrom($calendar_phid)
->setIsBulk(true)
->setSubjectPrefix(pht('[Calendar]'))
->setVarySubjectPrefix(pht('[Reminder]'))
->setThreadID($next_event->getPHID(), false)
->setRelatedPHID($next_event->getPHID())
->setBody($body->render())
->setHTMLBody($body->renderHTML());
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionUpdateCoverageConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionUpdateCoverageConduitAPIMethod.php
index be0d2c4faa..256d023345 100644
--- a/src/applications/diffusion/conduit/DiffusionUpdateCoverageConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionUpdateCoverageConduitAPIMethod.php
@@ -1,117 +1,117 @@
<?php
final class DiffusionUpdateCoverageConduitAPIMethod
extends DiffusionConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.updatecoverage';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return pht('Publish coverage information for a repository.');
}
protected function defineReturnType() {
return 'void';
}
protected function defineParamTypes() {
$modes = array(
'overwrite',
'update',
);
return array(
'repositoryPHID' => 'required phid',
'branch' => 'required string',
'commit' => 'required string',
'coverage' => 'required map<string, string>',
'mode' => 'optional '.$this->formatStringConstants($modes),
);
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$repository_phid = $request->getValue('repositoryPHID');
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withPHIDs(array($repository_phid))
->executeOne();
if (!$repository) {
throw new Exception(
pht('No repository exists with PHID "%s".', $repository_phid));
}
$commit_name = $request->getValue('commit');
$commit = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepository($repository)
->withIdentifiers(array($commit_name))
->executeOne();
if (!$commit) {
throw new Exception(
pht('No commit exists with identifier "%s".', $commit_name));
}
$branch = PhabricatorRepositoryBranch::loadOrCreateBranch(
$repository->getID(),
$request->getValue('branch'));
$coverage = $request->getValue('coverage');
$path_map = id(new DiffusionPathIDQuery(array_keys($coverage)))
->loadPathIDs();
$conn = $repository->establishConnection('w');
$sql = array();
foreach ($coverage as $path => $coverage_info) {
$sql[] = qsprintf(
$conn,
'(%d, %d, %d, %s)',
$branch->getID(),
$path_map[$path],
$commit->getID(),
$coverage_info);
}
$table_name = 'repository_coverage';
$conn->openTransaction();
$mode = $request->getValue('mode');
switch ($mode) {
case '':
case 'overwrite':
// sets the coverage for the whole branch, deleting all previous
// coverage information
queryfx(
$conn,
'DELETE FROM %T WHERE branchID = %d',
$table_name,
$branch->getID());
break;
case 'update':
// sets the coverage for the provided files on the specified commit
break;
default:
$conn->killTransaction();
throw new Exception(pht('Invalid mode "%s".', $mode));
}
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn,
- 'INSERT INTO %T (branchID, pathID, commitID, coverage) VALUES %Q'.
- ' ON DUPLICATE KEY UPDATE coverage=VALUES(coverage)',
+ 'INSERT INTO %T (branchID, pathID, commitID, coverage) VALUES %LQ'.
+ ' ON DUPLICATE KEY UPDATE coverage = VALUES(coverage)',
$table_name,
$chunk);
}
$conn->saveTransaction();
}
}
diff --git a/src/applications/diviner/publisher/DivinerLivePublisher.php b/src/applications/diviner/publisher/DivinerLivePublisher.php
index 80f3bd7a1e..e9e1848d6e 100644
--- a/src/applications/diviner/publisher/DivinerLivePublisher.php
+++ b/src/applications/diviner/publisher/DivinerLivePublisher.php
@@ -1,175 +1,175 @@
<?php
final class DivinerLivePublisher extends DivinerPublisher {
private $book;
protected function getBook() {
if (!$this->book) {
$book_name = $this->getConfig('name');
$book = id(new DivinerLiveBook())->loadOneWhere(
'name = %s',
$book_name);
if (!$book) {
$book = id(new DivinerLiveBook())
->setName($book_name)
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN)
->save();
}
$conn_w = $book->establishConnection('w');
$conn_w->openTransaction();
$book
->setRepositoryPHID($this->getRepositoryPHID())
->setConfigurationData($this->getConfigurationData())
->save();
// TODO: This is gross. Without this, the repository won't be updated for
// atoms which have already been published.
queryfx(
$conn_w,
'UPDATE %T SET repositoryPHID = %s WHERE bookPHID = %s',
id(new DivinerLiveSymbol())->getTableName(),
$this->getRepositoryPHID(),
$book->getPHID());
$conn_w->saveTransaction();
$this->book = $book;
PhabricatorSearchWorker::queueDocumentForIndexing($book->getPHID());
}
return $this->book;
}
private function loadSymbolForAtom(DivinerAtom $atom) {
$symbol = id(new DivinerAtomQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBookPHIDs(array($this->getBook()->getPHID()))
->withTypes(array($atom->getType()))
->withNames(array($atom->getName()))
->withContexts(array($atom->getContext()))
->withIndexes(array($this->getAtomSimilarIndex($atom)))
->executeOne();
if ($symbol) {
return $symbol;
}
return id(new DivinerLiveSymbol())
->setBookPHID($this->getBook()->getPHID())
->setType($atom->getType())
->setName($atom->getName())
->setContext($atom->getContext())
->setAtomIndex($this->getAtomSimilarIndex($atom));
}
private function loadAtomStorageForSymbol(DivinerLiveSymbol $symbol) {
$storage = id(new DivinerLiveAtom())->loadOneWhere(
'symbolPHID = %s',
$symbol->getPHID());
if ($storage) {
return $storage;
}
return id(new DivinerLiveAtom())
->setSymbolPHID($symbol->getPHID());
}
protected function loadAllPublishedHashes() {
$symbols = id(new DivinerAtomQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBookPHIDs(array($this->getBook()->getPHID()))
->withGhosts(false)
->execute();
return mpull($symbols, 'getGraphHash');
}
protected function deleteDocumentsByHash(array $hashes) {
$atom_table = new DivinerLiveAtom();
$symbol_table = new DivinerLiveSymbol();
$conn_w = $symbol_table->establishConnection('w');
$strings = array();
foreach ($hashes as $hash) {
$strings[] = qsprintf($conn_w, '%s', $hash);
}
- foreach (PhabricatorLiskDAO::chunkSQL($strings, ', ') as $chunk) {
+ foreach (PhabricatorLiskDAO::chunkSQL($strings) as $chunk) {
queryfx(
$conn_w,
'UPDATE %T SET graphHash = NULL, nodeHash = NULL
- WHERE graphHash IN (%Q)',
+ WHERE graphHash IN (%LQ)',
$symbol_table->getTableName(),
$chunk);
}
queryfx(
$conn_w,
'DELETE a FROM %T a LEFT JOIN %T s
ON a.symbolPHID = s.phid
WHERE s.graphHash IS NULL',
$atom_table->getTableName(),
$symbol_table->getTableName());
}
protected function createDocumentsByHash(array $hashes) {
foreach ($hashes as $hash) {
$atom = $this->getAtomFromGraphHash($hash);
$ref = $atom->getRef();
$symbol = $this->loadSymbolForAtom($atom);
$is_documentable = $this->shouldGenerateDocumentForAtom($atom);
$symbol
->setRepositoryPHID($this->getRepositoryPHID())
->setGraphHash($hash)
->setIsDocumentable((int)$is_documentable)
->setTitle($ref->getTitle())
->setGroupName($ref->getGroup())
->setNodeHash($atom->getHash());
if ($atom->getType() !== DivinerAtom::TYPE_FILE) {
$renderer = $this->getRenderer();
$summary = $renderer->getAtomSummary($atom);
$symbol->setSummary($summary);
} else {
$symbol->setSummary('');
}
$symbol->save();
PhabricatorSearchWorker::queueDocumentForIndexing($symbol->getPHID());
// TODO: We probably need a finer-grained sense of what "documentable"
// atoms are. Neither files nor methods are currently considered
// documentable, but for different reasons: files appear nowhere, while
// methods just don't appear at the top level. These are probably
// separate concepts. Since we need atoms in order to build method
// documentation, we insert them here. This also means we insert files,
// which are unnecessary and unused. Make sure this makes sense, but then
// probably introduce separate "isTopLevel" and "isDocumentable" flags?
// TODO: Yeah do that soon ^^^
if ($atom->getType() !== DivinerAtom::TYPE_FILE) {
$storage = $this->loadAtomStorageForSymbol($symbol)
->setAtomData($atom->toDictionary())
->setContent(null)
->save();
}
}
}
public function findAtomByRef(DivinerAtomRef $ref) {
// TODO: Actually implement this.
return null;
}
}
diff --git a/src/applications/fact/daemon/PhabricatorFactDaemon.php b/src/applications/fact/daemon/PhabricatorFactDaemon.php
index 3fa6c6f0ff..57813b3021 100644
--- a/src/applications/fact/daemon/PhabricatorFactDaemon.php
+++ b/src/applications/fact/daemon/PhabricatorFactDaemon.php
@@ -1,201 +1,201 @@
<?php
final class PhabricatorFactDaemon extends PhabricatorDaemon {
private $engines;
protected function run() {
$this->setEngines(PhabricatorFactEngine::loadAllEngines());
while (!$this->shouldExit()) {
PhabricatorCaches::destroyRequestCache();
$iterators = $this->getAllApplicationIterators();
foreach ($iterators as $iterator_name => $iterator) {
$this->processIteratorWithCursor($iterator_name, $iterator);
}
$this->log(pht('Zzz...'));
$this->sleep(60 * 5);
}
}
public static function getAllApplicationIterators() {
$apps = PhabricatorApplication::getAllInstalledApplications();
$iterators = array();
foreach ($apps as $app) {
foreach ($app->getFactObjectsForAnalysis() as $object) {
$iterator = new PhabricatorFactUpdateIterator($object);
$iterators[get_class($object)] = $iterator;
}
}
return $iterators;
}
public function processIteratorWithCursor($iterator_name, $iterator) {
$this->log(pht("Processing cursor '%s'.", $iterator_name));
$cursor = id(new PhabricatorFactCursor())->loadOneWhere(
'name = %s',
$iterator_name);
if (!$cursor) {
$cursor = new PhabricatorFactCursor();
$cursor->setName($iterator_name);
$position = null;
} else {
$position = $cursor->getPosition();
}
if ($position) {
$iterator->setPosition($position);
}
$new_cursor_position = $this->processIterator($iterator);
if ($new_cursor_position) {
$cursor->setPosition($new_cursor_position);
$cursor->save();
}
}
public function setEngines(array $engines) {
assert_instances_of($engines, 'PhabricatorFactEngine');
$viewer = PhabricatorUser::getOmnipotentUser();
foreach ($engines as $engine) {
$engine->setViewer($viewer);
}
$this->engines = $engines;
return $this;
}
public function processIterator($iterator) {
$result = null;
$datapoints = array();
$count = 0;
foreach ($iterator as $key => $object) {
$phid = $object->getPHID();
$this->log(pht('Processing %s...', $phid));
$object_datapoints = $this->newDatapoints($object);
$count += count($object_datapoints);
$datapoints[$phid] = $object_datapoints;
if ($count > 1024) {
$this->updateDatapoints($datapoints);
$datapoints = array();
$count = 0;
}
$result = $key;
}
if ($count) {
$this->updateDatapoints($datapoints);
$datapoints = array();
$count = 0;
}
return $result;
}
private function newDatapoints(PhabricatorLiskDAO $object) {
$facts = array();
foreach ($this->engines as $engine) {
if (!$engine->supportsDatapointsForObject($object)) {
continue;
}
$facts[] = $engine->newDatapointsForObject($object);
}
return array_mergev($facts);
}
private function updateDatapoints(array $map) {
foreach ($map as $phid => $facts) {
assert_instances_of($facts, 'PhabricatorFactIntDatapoint');
}
$phids = array_keys($map);
if (!$phids) {
return;
}
$fact_keys = array();
$objects = array();
foreach ($map as $phid => $facts) {
foreach ($facts as $fact) {
$fact_keys[$fact->getKey()] = true;
$object_phid = $fact->getObjectPHID();
$objects[$object_phid] = $object_phid;
$dimension_phid = $fact->getDimensionPHID();
if ($dimension_phid !== null) {
$objects[$dimension_phid] = $dimension_phid;
}
}
}
$key_map = id(new PhabricatorFactKeyDimension())
->newDimensionMap(array_keys($fact_keys), true);
$object_map = id(new PhabricatorFactObjectDimension())
->newDimensionMap(array_keys($objects), true);
$table = new PhabricatorFactIntDatapoint();
$conn = $table->establishConnection('w');
$table_name = $table->getTableName();
$sql = array();
foreach ($map as $phid => $facts) {
foreach ($facts as $fact) {
$key_id = $key_map[$fact->getKey()];
$object_id = $object_map[$fact->getObjectPHID()];
$dimension_phid = $fact->getDimensionPHID();
if ($dimension_phid !== null) {
$dimension_id = $object_map[$dimension_phid];
} else {
$dimension_id = null;
}
$sql[] = qsprintf(
$conn,
'(%d, %d, %nd, %d, %d)',
$key_id,
$object_id,
$dimension_id,
$fact->getValue(),
$fact->getEpoch());
}
}
$rebuilt_ids = array_select_keys($object_map, $phids);
$table->openTransaction();
queryfx(
$conn,
'DELETE FROM %T WHERE objectID IN (%Ld)',
$table_name,
$rebuilt_ids);
if ($sql) {
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn,
'INSERT INTO %T
(keyID, objectID, dimensionID, value, epoch)
- VALUES %Q',
+ VALUES %LQ',
$table_name,
$chunk);
}
}
$table->saveTransaction();
}
}
diff --git a/src/applications/fact/storage/PhabricatorFactDimension.php b/src/applications/fact/storage/PhabricatorFactDimension.php
index 9c05121c9c..5644da331e 100644
--- a/src/applications/fact/storage/PhabricatorFactDimension.php
+++ b/src/applications/fact/storage/PhabricatorFactDimension.php
@@ -1,110 +1,110 @@
<?php
abstract class PhabricatorFactDimension extends PhabricatorFactDAO {
abstract protected function getDimensionColumnName();
final public function newDimensionID($key, $create = false) {
$map = $this->newDimensionMap(array($key), $create);
return idx($map, $key);
}
final public function newDimensionUnmap(array $ids) {
if (!$ids) {
return array();
}
$conn = $this->establishConnection('r');
$column = $this->getDimensionColumnName();
$rows = queryfx_all(
$conn,
'SELECT id, %C FROM %T WHERE id IN (%Ld)',
$column,
$this->getTableName(),
$ids);
$rows = ipull($rows, $column, 'id');
return $rows;
}
final public function newDimensionMap(array $keys, $create = false) {
if (!$keys) {
return array();
}
$conn = $this->establishConnection('r');
$column = $this->getDimensionColumnName();
$rows = queryfx_all(
$conn,
'SELECT id, %C FROM %T WHERE %C IN (%Ls)',
$column,
$this->getTableName(),
$column,
$keys);
$rows = ipull($rows, 'id', $column);
$map = array();
$need = array();
foreach ($keys as $key) {
if (isset($rows[$key])) {
$map[$key] = (int)$rows[$key];
} else {
$need[] = $key;
}
}
if (!$need) {
return $map;
}
if (!$create) {
return $map;
}
$sql = array();
foreach ($need as $key) {
$sql[] = qsprintf(
$conn,
'(%s)',
$key);
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn,
- 'INSERT IGNORE INTO %T (%C) VALUES %Q',
+ 'INSERT IGNORE INTO %T (%C) VALUES %LQ',
$this->getTableName(),
$column,
$chunk);
}
unset($unguarded);
$rows = queryfx_all(
$conn,
'SELECT id, %C FROM %T WHERE %C IN (%Ls)',
$column,
$this->getTableName(),
$column,
$need);
$rows = ipull($rows, 'id', $column);
foreach ($need as $key) {
if (isset($rows[$key])) {
$map[$key] = (int)$rows[$key];
} else {
throw new Exception(
pht(
'Failed to load or generate dimension ID ("%s") for dimension '.
'key "%s".',
get_class($this),
$key));
}
}
return $map;
}
}
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index 66c523573f..19fd57b0d2 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,1014 +1,1014 @@
<?php
final class ManiphestTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $moreValidationErrors = array();
public function getEditorApplicationClass() {
return 'PhabricatorManiphestApplication';
}
public function getEditorObjectsDescription() {
return pht('Maniphest Tasks');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_COLUMNS;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this task.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return null;
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return $xaction->getNewValue();
}
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return (bool)$new;
}
return parent::transactionHasEffect($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
foreach ($xaction->getNewValue() as $move) {
$this->applyBoardMove($object, $move);
}
break;
}
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// When we change the status of a task, update tasks this tasks blocks
// with a message to the effect of "alincoln resolved blocking task Txxx."
$unblock_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
$unblock_xaction = $xaction;
break;
}
}
if ($unblock_xaction !== null) {
$blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);
if ($blocked_phids) {
// In theory we could apply these through policies, but that seems a
// little bit surprising. For now, use the actor's vision.
$blocked_tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withPHIDs($blocked_phids)
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->execute();
$old = $unblock_xaction->getOldValue();
$new = $unblock_xaction->getNewValue();
foreach ($blocked_tasks as $blocked_task) {
$parent_xaction = id(new ManiphestTransaction())
->setTransactionType(
ManiphestTaskUnblockTransaction::TRANSACTIONTYPE)
->setOldValue(array($object->getPHID() => $old))
->setNewValue(array($object->getPHID() => $new));
if ($this->getIsNewObject()) {
$parent_xaction->setMetadataValue('blocker.new', true);
}
id(new ManiphestTransactionEditor())
->setActor($this->getActor())
->setActingAsPHID($this->getActingAsPHID())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($blocked_task, array($parent_xaction));
}
}
}
return $xactions;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return 'maniphest-task-'.$object->getPHID();
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
if ($object->getOwnerPHID()) {
$phids[] = $object->getOwnerPHID();
}
$phids[] = $this->getActingAsPHID();
return $phids;
}
public function getMailTagsMap() {
return array(
ManiphestTransaction::MAILTAG_STATUS =>
pht("A task's status changes."),
ManiphestTransaction::MAILTAG_OWNER =>
pht("A task's owner changes."),
ManiphestTransaction::MAILTAG_PRIORITY =>
pht("A task's priority changes."),
ManiphestTransaction::MAILTAG_CC =>
pht("A task's subscribers change."),
ManiphestTransaction::MAILTAG_PROJECTS =>
pht("A task's associated projects change."),
ManiphestTransaction::MAILTAG_UNBLOCK =>
pht("One of a task's subtasks changes status."),
ManiphestTransaction::MAILTAG_COLUMN =>
pht('A task is moved between columns on a workboard.'),
ManiphestTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a task.'),
ManiphestTransaction::MAILTAG_OTHER =>
pht('Other task activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ManiphestReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject("T{$id}: {$title}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
if ($this->getIsNewObject()) {
$body->addRemarkupSection(
pht('TASK DESCRIPTION'),
$object->getDescription());
}
$body->addLinkSection(
pht('TASK DETAIL'),
PhabricatorEnv::getProductionURI('/T'.$object->getID()));
$board_phids = array();
$type_columns = PhabricatorTransactions::TYPE_COLUMNS;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_columns) {
$moves = $xaction->getNewValue();
foreach ($moves as $move) {
$board_phids[] = $move['boardPHID'];
}
}
}
if ($board_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->requireActor())
->withPHIDs($board_phids)
->execute();
foreach ($projects as $project) {
$body->addLinkSection(
pht('WORKBOARD'),
PhabricatorEnv::getProductionURI(
'/project/board/'.$project->getID().'/'));
}
}
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldManiphestTaskAdapter())
->setTask($object);
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
$copy->setOwnerPHID($xaction->getNewValue());
break;
default:
break;
}
}
return $copy;
}
/**
* Get priorities for moving a task to a new priority.
*/
public static function getEdgeSubpriority(
$priority,
$is_end) {
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPriorities(array($priority))
->setLimit(1);
if ($is_end) {
$query->setOrderVector(array('-priority', '-subpriority', '-id'));
} else {
$query->setOrderVector(array('priority', 'subpriority', 'id'));
}
$result = $query->executeOne();
$step = (double)(2 << 32);
if ($result) {
$base = $result->getSubpriority();
if ($is_end) {
$sub = ($base - $step);
} else {
$sub = ($base + $step);
}
} else {
$sub = 0;
}
return array($priority, $sub);
}
/**
* Get priorities for moving a task before or after another task.
*/
public static function getAdjacentSubpriority(
ManiphestTask $dst,
$is_after) {
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->setOrder(ManiphestTaskQuery::ORDER_PRIORITY)
->withPriorities(array($dst->getPriority()))
->setLimit(1);
if ($is_after) {
$query->setAfterID($dst->getID());
} else {
$query->setBeforeID($dst->getID());
}
$adjacent = $query->executeOne();
$base = $dst->getSubpriority();
$step = (double)(2 << 32);
// If we find an adjacent task, we average the two subpriorities and
// return the result.
if ($adjacent) {
$epsilon = 1.0;
// If the adjacent task has a subpriority that is identical or very
// close to the task we're looking at, we're going to spread out all
// the nearby tasks.
$adjacent_sub = $adjacent->getSubpriority();
if ((abs($adjacent_sub - $base) < $epsilon)) {
$base = self::disperseBlock(
$dst,
$epsilon * 2);
if ($is_after) {
$sub = $base - $epsilon;
} else {
$sub = $base + $epsilon;
}
} else {
$sub = ($adjacent_sub + $base) / 2;
}
} else {
// Otherwise, we take a step away from the target's subpriority and
// use that.
if ($is_after) {
$sub = ($base - $step);
} else {
$sub = ($base + $step);
}
}
return array($dst->getPriority(), $sub);
}
/**
* Distribute a cluster of tasks with similar subpriorities.
*/
private static function disperseBlock(
ManiphestTask $task,
$spacing) {
$conn = $task->establishConnection('w');
// Find a block of subpriority space which is, on average, sparse enough
// to hold all the tasks that are inside it with a reasonable level of
// separation between them.
// We'll start by looking near the target task for a range of numbers
// which has more space available than tasks. For example, if the target
// task has subpriority 33 and we want to separate each task by at least 1,
// we might start by looking in the range [23, 43].
// If we find fewer than 20 tasks there, we have room to reassign them
// with the desired level of separation. We space them out, then we're
// done.
// However: if we find more than 20 tasks, we don't have enough room to
// distribute them. We'll widen our search and look in a bigger range,
// maybe [13, 53]. This range has more space, so if we find fewer than
// 40 tasks in this range we can spread them out. If we still find too
// many tasks, we keep widening the search.
$base = $task->getSubpriority();
$scale = 4.0;
while (true) {
$range = ($spacing * $scale) / 2.0;
$min = ($base - $range);
$max = ($base + $range);
$result = queryfx_one(
$conn,
'SELECT COUNT(*) N FROM %T WHERE priority = %d AND
subpriority BETWEEN %f AND %f',
$task->getTableName(),
$task->getPriority(),
$min,
$max);
$count = $result['N'];
if ($count < $scale) {
// We have found a block which we can make sparse enough, so bail and
// continue below with our selection.
break;
}
// This block had too many tasks for its size, so try again with a
// bigger block.
$scale *= 2.0;
}
$rows = queryfx_all(
$conn,
'SELECT id FROM %T WHERE priority = %d AND
subpriority BETWEEN %f AND %f
ORDER BY priority, subpriority, id',
$task->getTableName(),
$task->getPriority(),
$min,
$max);
$task_id = $task->getID();
$result = null;
// NOTE: In strict mode (which we encourage enabling) we can't structure
// this bulk update as an "INSERT ... ON DUPLICATE KEY UPDATE" unless we
// provide default values for ALL of the columns that don't have defaults.
// This is gross, but we may be moving enough rows that individual
// queries are unreasonably slow. An alternate construction which might
// be worth evaluating is to use "CASE". Another approach is to disable
// strict mode for this query.
$extra_columns = array(
'phid' => '""',
'authorPHID' => '""',
'status' => '""',
'priority' => 0,
'title' => '""',
'description' => '""',
'dateCreated' => 0,
'dateModified' => 0,
'mailKey' => '""',
'viewPolicy' => '""',
'editPolicy' => '""',
'ownerOrdering' => '""',
'spacePHID' => '""',
'bridgedObjectPHID' => '""',
'properties' => '""',
'points' => 0,
'subtype' => '""',
);
$defaults = implode(', ', $extra_columns);
$sql = array();
$offset = 0;
// Often, we'll have more room than we need in the range. Distribute the
// tasks evenly over the whole range so that we're less likely to end up
// with tasks spaced exactly the minimum distance apart, which may
// get shifted again later. We have one fewer space to distribute than we
// have tasks.
$divisor = (double)(count($rows) - 1.0);
if ($divisor > 0) {
$available_distance = (($max - $min) / $divisor);
} else {
$available_distance = 0.0;
}
foreach ($rows as $row) {
$subpriority = $min + ($offset * $available_distance);
// If this is the task that we're spreading out relative to, keep track
// of where it is ending up so we can return the new subpriority.
$id = $row['id'];
if ($id == $task_id) {
$result = $subpriority;
}
$sql[] = qsprintf(
$conn,
'(%d, %Q, %f)',
$id,
$defaults,
$subpriority);
$offset++;
}
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn,
- 'INSERT INTO %T (id, %Q, subpriority) VALUES %Q
+ 'INSERT INTO %T (id, %Q, subpriority) VALUES %LQ
ON DUPLICATE KEY UPDATE subpriority = VALUES(subpriority)',
$task->getTableName(),
implode(', ', array_keys($extra_columns)),
$chunk);
}
return $result;
}
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$errors = parent::validateAllTransactions($object, $xactions);
if ($this->moreValidationErrors) {
$errors = array_merge($errors, $this->moreValidationErrors);
}
return $errors;
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$actor = $this->getActor();
$actor_phid = $actor->getPHID();
$results = parent::expandTransactions($object, $xactions);
$is_unassigned = ($object->getOwnerPHID() === null);
$any_assign = false;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() ==
ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) {
$any_assign = true;
break;
}
}
$is_open = !$object->isClosed();
$new_status = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
$new_status = $xaction->getNewValue();
break;
}
}
if ($new_status === null) {
$is_closing = false;
} else {
$is_closing = ManiphestTaskStatus::isClosedStatus($new_status);
}
// If the task is not assigned, not being assigned, currently open, and
// being closed, try to assign the actor as the owner.
if ($is_unassigned && !$any_assign && $is_open && $is_closing) {
$is_claim = ManiphestTaskStatus::isClaimStatus($new_status);
// Don't assign the actor if they aren't a real user.
// Don't claim the task if the status is configured to not claim.
if ($actor_phid && $is_claim) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE)
->setNewValue($actor_phid);
}
}
// Automatically subscribe the author when they create a task.
if ($this->getIsNewObject()) {
if ($actor_phid) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(
array(
'+' => array($actor_phid => $actor_phid),
));
}
}
return $results;
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$results = parent::expandTransaction($object, $xaction);
$type = $xaction->getTransactionType();
switch ($type) {
case PhabricatorTransactions::TYPE_COLUMNS:
try {
$more_xactions = $this->buildMoveTransaction($object, $xaction);
foreach ($more_xactions as $more_xaction) {
$results[] = $more_xaction;
}
} catch (Exception $ex) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$ex->getMessage(),
$xaction);
$this->moreValidationErrors[] = $error;
}
break;
case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
// If this is a no-op update, don't expand it.
$old_value = $object->getOwnerPHID();
$new_value = $xaction->getNewValue();
if ($old_value === $new_value) {
continue;
}
// When a task is reassigned, move the old owner to the subscriber
// list so they're still in the loop.
if ($old_value) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setIgnoreOnNoEffect(true)
->setNewValue(
array(
'+' => array($old_value => $old_value),
));
}
break;
}
return $results;
}
private function buildMoveTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$new = $xaction->getNewValue();
if (!is_array($new)) {
$this->validateColumnPHID($new);
$new = array($new);
}
$nearby_phids = array();
foreach ($new as $key => $value) {
if (!is_array($value)) {
$this->validateColumnPHID($value);
$value = array(
'columnPHID' => $value,
);
}
PhutilTypeSpec::checkMap(
$value,
array(
'columnPHID' => 'string',
'beforePHID' => 'optional string',
'afterPHID' => 'optional string',
));
$new[$key] = $value;
if (!empty($value['beforePHID'])) {
$nearby_phids[] = $value['beforePHID'];
}
if (!empty($value['afterPHID'])) {
$nearby_phids[] = $value['afterPHID'];
}
}
if ($nearby_phids) {
$nearby_objects = id(new PhabricatorObjectQuery())
->setViewer($this->getActor())
->withPHIDs($nearby_phids)
->execute();
$nearby_objects = mpull($nearby_objects, null, 'getPHID');
} else {
$nearby_objects = array();
}
$column_phids = ipull($new, 'columnPHID');
if ($column_phids) {
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($this->getActor())
->withPHIDs($column_phids)
->execute();
$columns = mpull($columns, null, 'getPHID');
} else {
$columns = array();
}
$board_phids = mpull($columns, 'getProjectPHID');
$object_phid = $object->getPHID();
$object_phids = $nearby_phids;
// Note that we may not have an object PHID if we're creating a new
// object.
if ($object_phid) {
$object_phids[] = $object_phid;
}
if ($object_phids) {
$layout_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($this->getActor())
->setBoardPHIDs($board_phids)
->setObjectPHIDs($object_phids)
->setFetchAllBoards(true)
->executeLayout();
}
foreach ($new as $key => $spec) {
$column_phid = $spec['columnPHID'];
$column = idx($columns, $column_phid);
if (!$column) {
throw new Exception(
pht(
'Column move transaction specifies column PHID "%s", but there '.
'is no corresponding column with this PHID.',
$column_phid));
}
$board_phid = $column->getProjectPHID();
$nearby = array();
if (!empty($spec['beforePHID'])) {
$nearby['beforePHID'] = $spec['beforePHID'];
}
if (!empty($spec['afterPHID'])) {
$nearby['afterPHID'] = $spec['afterPHID'];
}
if (count($nearby) > 1) {
throw new Exception(
pht(
'Column move transaction moves object to multiple positions. '.
'Specify only "beforePHID" or "afterPHID", not both.'));
}
foreach ($nearby as $where => $nearby_phid) {
if (empty($nearby_objects[$nearby_phid])) {
throw new Exception(
pht(
'Column move transaction specifies object "%s" as "%s", but '.
'there is no corresponding object with this PHID.',
$object_phid,
$where));
}
$nearby_columns = $layout_engine->getObjectColumns(
$board_phid,
$nearby_phid);
$nearby_columns = mpull($nearby_columns, null, 'getPHID');
if (empty($nearby_columns[$column_phid])) {
throw new Exception(
pht(
'Column move transaction specifies object "%s" as "%s" in '.
'column "%s", but this object is not in that column!',
$nearby_phid,
$where,
$column_phid));
}
}
if ($object_phid) {
$old_columns = $layout_engine->getObjectColumns(
$board_phid,
$object_phid);
$old_column_phids = mpull($old_columns, 'getPHID');
} else {
$old_column_phids = array();
}
$spec += array(
'boardPHID' => $board_phid,
'fromColumnPHIDs' => $old_column_phids,
);
// Check if the object is already in this column, and isn't being moved.
// We can just drop this column change if it has no effect.
$from_map = array_fuse($spec['fromColumnPHIDs']);
$already_here = isset($from_map[$column_phid]);
$is_reordering = (bool)$nearby;
if ($already_here && !$is_reordering) {
unset($new[$key]);
} else {
$new[$key] = $spec;
}
}
$new = array_values($new);
$xaction->setNewValue($new);
$more = array();
// If we're moving the object into a column and it does not already belong
// in the column, add the appropriate board. For normal columns, this
// is the board PHID. For proxy columns, it is the proxy PHID, unless the
// object is already a member of some descendant of the proxy PHID.
// The major case where this can happen is moves via the API, but it also
// happens when a user drags a task from the "Backlog" to a milestone
// column.
if ($object_phid) {
$current_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object_phid,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
$current_phids = array_fuse($current_phids);
} else {
$current_phids = array();
}
$add_boards = array();
foreach ($new as $move) {
$column_phid = $move['columnPHID'];
$board_phid = $move['boardPHID'];
$column = $columns[$column_phid];
$proxy_phid = $column->getProxyPHID();
// If this is a normal column, add the board if the object isn't already
// associated.
if (!$proxy_phid) {
if (!isset($current_phids[$board_phid])) {
$add_boards[] = $board_phid;
}
continue;
}
// If this is a proxy column but the object is already associated with
// the proxy board, we don't need to do anything.
if (isset($current_phids[$proxy_phid])) {
continue;
}
// If this a proxy column and the object is already associated with some
// descendant of the proxy board, we also don't need to do anything.
$descendants = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAncestorProjectPHIDs(array($proxy_phid))
->execute();
$found_descendant = false;
foreach ($descendants as $descendant) {
if (isset($current_phids[$descendant->getPHID()])) {
$found_descendant = true;
break;
}
}
if ($found_descendant) {
continue;
}
// Otherwise, we're moving the object to a proxy column which it is not
// a member of yet, so add an association to the column's proxy board.
$add_boards[] = $proxy_phid;
}
if ($add_boards) {
$more[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setIgnoreOnNoEffect(true)
->setNewValue(
array(
'+' => array_fuse($add_boards),
));
}
return $more;
}
private function applyBoardMove($object, array $move) {
$board_phid = $move['boardPHID'];
$column_phid = $move['columnPHID'];
$before_phid = idx($move, 'beforePHID');
$after_phid = idx($move, 'afterPHID');
$object_phid = $object->getPHID();
// We're doing layout with the omnipotent viewer to make sure we don't
// remove positions in columns that exist, but which the actual actor
// can't see.
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
$select_phids = array($board_phid);
$descendants = id(new PhabricatorProjectQuery())
->setViewer($omnipotent_viewer)
->withAncestorProjectPHIDs($select_phids)
->execute();
foreach ($descendants as $descendant) {
$select_phids[] = $descendant->getPHID();
}
$board_tasks = id(new ManiphestTaskQuery())
->setViewer($omnipotent_viewer)
->withEdgeLogicPHIDs(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
array($select_phids))
->execute();
$board_tasks = mpull($board_tasks, null, 'getPHID');
$board_tasks[$object_phid] = $object;
// Make sure tasks are sorted by ID, so we lay out new positions in
// a consistent way.
$board_tasks = msort($board_tasks, 'getID');
$object_phids = array_keys($board_tasks);
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($omnipotent_viewer)
->setBoardPHIDs(array($board_phid))
->setObjectPHIDs($object_phids)
->executeLayout();
// TODO: This logic needs to be revised when we legitimately support
// multiple column positions.
$columns = $engine->getObjectColumns($board_phid, $object_phid);
foreach ($columns as $column) {
$engine->queueRemovePosition(
$board_phid,
$column->getPHID(),
$object_phid);
}
if ($before_phid) {
$engine->queueAddPositionBefore(
$board_phid,
$column_phid,
$object_phid,
$before_phid);
} else if ($after_phid) {
$engine->queueAddPositionAfter(
$board_phid,
$column_phid,
$object_phid,
$after_phid);
} else {
$engine->queueAddPosition(
$board_phid,
$column_phid,
$object_phid);
}
$engine->applyPositionUpdates();
}
private function validateColumnPHID($value) {
if (phid_get_type($value) == PhabricatorProjectColumnPHIDType::TYPECONST) {
return;
}
throw new Exception(
pht(
'When moving objects between columns on a board, columns must '.
'be identified by PHIDs. This transaction uses "%s" to identify '.
'a column, but that is not a valid column PHID.',
$value));
}
}
diff --git a/src/applications/people/storage/PhabricatorUserCache.php b/src/applications/people/storage/PhabricatorUserCache.php
index 2afb06a437..71b3e4816b 100644
--- a/src/applications/people/storage/PhabricatorUserCache.php
+++ b/src/applications/people/storage/PhabricatorUserCache.php
@@ -1,166 +1,166 @@
<?php
final class PhabricatorUserCache extends PhabricatorUserDAO {
protected $userPHID;
protected $cacheIndex;
protected $cacheKey;
protected $cacheData;
protected $cacheType;
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
'cacheIndex' => 'bytes12',
'cacheKey' => 'text255',
'cacheData' => 'text',
'cacheType' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_usercache' => array(
'columns' => array('userPHID', 'cacheIndex'),
'unique' => true,
),
'key_cachekey' => array(
'columns' => array('cacheIndex'),
),
'key_cachetype' => array(
'columns' => array('cacheType'),
),
),
) + parent::getConfiguration();
}
public function save() {
$this->cacheIndex = Filesystem::digestForIndex($this->getCacheKey());
return parent::save();
}
public static function writeCache(
PhabricatorUserCacheType $type,
$key,
$user_phid,
$raw_value) {
self::writeCaches(
array(
array(
'type' => $type,
'key' => $key,
'userPHID' => $user_phid,
'value' => $raw_value,
),
));
}
public static function writeCaches(array $values) {
if (PhabricatorEnv::isReadOnly()) {
return;
}
if (!$values) {
return;
}
$table = new self();
$conn_w = $table->establishConnection('w');
$sql = array();
foreach ($values as $value) {
$key = $value['key'];
$sql[] = qsprintf(
$conn_w,
'(%s, %s, %s, %s, %s)',
$value['userPHID'],
PhabricatorHash::digestForIndex($key),
$key,
$value['value'],
$value['type']->getUserCacheType());
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (userPHID, cacheIndex, cacheKey, cacheData, cacheType)
- VALUES %Q
+ VALUES %LQ
ON DUPLICATE KEY UPDATE
cacheData = VALUES(cacheData),
cacheType = VALUES(cacheType)',
$table->getTableName(),
$chunk);
}
unset($unguarded);
}
public static function readCaches(
PhabricatorUserCacheType $type,
$key,
array $user_phids) {
$table = new self();
$conn = $table->establishConnection('r');
$rows = queryfx_all(
$conn,
'SELECT userPHID, cacheData FROM %T WHERE userPHID IN (%Ls)
AND cacheType = %s AND cacheIndex = %s',
$table->getTableName(),
$user_phids,
$type->getUserCacheType(),
PhabricatorHash::digestForIndex($key));
return ipull($rows, 'cacheData', 'userPHID');
}
public static function clearCache($key, $user_phid) {
return self::clearCaches($key, array($user_phid));
}
public static function clearCaches($key, array $user_phids) {
if (PhabricatorEnv::isReadOnly()) {
return;
}
if (!$user_phids) {
return;
}
$table = new self();
$conn_w = $table->establishConnection('w');
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$conn_w,
'DELETE FROM %T WHERE cacheIndex = %s AND userPHID IN (%Ls)',
$table->getTableName(),
PhabricatorHash::digestForIndex($key),
$user_phids);
unset($unguarded);
}
public static function clearCacheForAllUsers($key) {
if (PhabricatorEnv::isReadOnly()) {
return;
}
$table = new self();
$conn_w = $table->establishConnection('w');
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$conn_w,
'DELETE FROM %T WHERE cacheIndex = %s',
$table->getTableName(),
PhabricatorHash::digestForIndex($key));
unset($unguarded);
}
}
diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php
index a049354ad7..f134b2c633 100644
--- a/src/applications/project/storage/PhabricatorProject.php
+++ b/src/applications/project/storage/PhabricatorProject.php
@@ -1,887 +1,887 @@
<?php
final class PhabricatorProject extends PhabricatorProjectDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorConduitResultInterface,
PhabricatorColumnProxyInterface,
PhabricatorSpacesInterface {
protected $name;
protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
protected $authorPHID;
protected $primarySlug;
protected $profileImagePHID;
protected $icon;
protected $color;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
protected $joinPolicy;
protected $isMembershipLocked;
protected $parentProjectPHID;
protected $hasWorkboard;
protected $hasMilestones;
protected $hasSubprojects;
protected $milestoneNumber;
protected $projectPath;
protected $projectDepth;
protected $projectPathKey;
protected $properties = array();
protected $spacePHID;
private $memberPHIDs = self::ATTACHABLE;
private $watcherPHIDs = self::ATTACHABLE;
private $sparseWatchers = self::ATTACHABLE;
private $sparseMembers = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $profileImageFile = self::ATTACHABLE;
private $slugs = self::ATTACHABLE;
private $parentProject = self::ATTACHABLE;
const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken';
const ITEM_PICTURE = 'project.picture';
const ITEM_PROFILE = 'project.profile';
const ITEM_POINTS = 'project.points';
const ITEM_WORKBOARD = 'project.workboard';
const ITEM_MEMBERS = 'project.members';
const ITEM_MANAGE = 'project.manage';
const ITEM_MILESTONES = 'project.milestones';
const ITEM_SUBPROJECTS = 'project.subprojects';
public static function initializeNewProject(
PhabricatorUser $actor,
PhabricatorProject $parent = null) {
$app = id(new PhabricatorApplicationQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withClasses(array('PhabricatorProjectApplication'))
->executeOne();
$view_policy = $app->getPolicy(
ProjectDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
ProjectDefaultEditCapability::CAPABILITY);
$join_policy = $app->getPolicy(
ProjectDefaultJoinCapability::CAPABILITY);
// If this is the child of some other project, default the Space to the
// Space of the parent.
if ($parent) {
$space_phid = $parent->getSpacePHID();
} else {
$space_phid = $actor->getDefaultSpacePHID();
}
$default_icon = PhabricatorProjectIconSet::getDefaultIconKey();
$default_color = PhabricatorProjectIconSet::getDefaultColorKey();
return id(new PhabricatorProject())
->setAuthorPHID($actor->getPHID())
->setIcon($default_icon)
->setColor($default_color)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setJoinPolicy($join_policy)
->setSpacePHID($space_phid)
->setIsMembershipLocked(0)
->attachMemberPHIDs(array())
->attachSlugs(array())
->setHasWorkboard(0)
->setHasMilestones(0)
->setHasSubprojects(0)
->attachParentProject(null);
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
PhabricatorPolicyCapability::CAN_JOIN,
);
}
public function getPolicy($capability) {
if ($this->isMilestone()) {
return $this->getParentProject()->getPolicy($capability);
}
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
case PhabricatorPolicyCapability::CAN_JOIN:
return $this->getJoinPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->isMilestone()) {
return $this->getParentProject()->hasAutomaticCapability(
$capability,
$viewer);
}
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->isUserMember($viewer->getPHID())) {
// Project members can always view a project.
return true;
}
break;
case PhabricatorPolicyCapability::CAN_EDIT:
$parent = $this->getParentProject();
if ($parent) {
$can_edit_parent = PhabricatorPolicyFilter::hasCapability(
$viewer,
$parent,
$can_edit);
if ($can_edit_parent) {
return true;
}
}
break;
case PhabricatorPolicyCapability::CAN_JOIN:
if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) {
// Project editors can always join a project.
return true;
}
break;
}
return false;
}
public function describeAutomaticCapability($capability) {
// TODO: Clarify the additional rules that parent and subprojects imply.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return pht('Members of a project can always view it.');
case PhabricatorPolicyCapability::CAN_JOIN:
return pht('Users who can edit a project can always join it.');
}
return null;
}
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$parent = $this->getParentProject();
if ($parent) {
$extended[] = array(
$parent,
PhabricatorPolicyCapability::CAN_VIEW,
);
}
break;
}
return $extended;
}
public function isUserMember($user_phid) {
if ($this->memberPHIDs !== self::ATTACHABLE) {
return in_array($user_phid, $this->memberPHIDs);
}
return $this->assertAttachedKey($this->sparseMembers, $user_phid);
}
public function setIsUserMember($user_phid, $is_member) {
if ($this->sparseMembers === self::ATTACHABLE) {
$this->sparseMembers = array();
}
$this->sparseMembers[$user_phid] = $is_member;
return $this;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort128',
'status' => 'text32',
'primarySlug' => 'text128?',
'isMembershipLocked' => 'bool',
'profileImagePHID' => 'phid?',
'icon' => 'text32',
'color' => 'text32',
'mailKey' => 'bytes20',
'joinPolicy' => 'policy',
'parentProjectPHID' => 'phid?',
'hasWorkboard' => 'bool',
'hasMilestones' => 'bool',
'hasSubprojects' => 'bool',
'milestoneNumber' => 'uint32?',
'projectPath' => 'hashpath64',
'projectDepth' => 'uint32',
'projectPathKey' => 'bytes4',
),
self::CONFIG_KEY_SCHEMA => array(
'key_icon' => array(
'columns' => array('icon'),
),
'key_color' => array(
'columns' => array('color'),
),
'key_milestone' => array(
'columns' => array('parentProjectPHID', 'milestoneNumber'),
'unique' => true,
),
'key_primaryslug' => array(
'columns' => array('primarySlug'),
'unique' => true,
),
'key_path' => array(
'columns' => array('projectPath', 'projectDepth'),
),
'key_pathkey' => array(
'columns' => array('projectPathKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorProjectProjectPHIDType::TYPECONST);
}
public function attachMemberPHIDs(array $phids) {
$this->memberPHIDs = $phids;
return $this;
}
public function getMemberPHIDs() {
return $this->assertAttached($this->memberPHIDs);
}
public function isArchived() {
return ($this->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED);
}
public function getProfileImageURI() {
return $this->getProfileImageFile()->getBestURI();
}
public function attachProfileImageFile(PhabricatorFile $file) {
$this->profileImageFile = $file;
return $this;
}
public function getProfileImageFile() {
return $this->assertAttached($this->profileImageFile);
}
public function isUserWatcher($user_phid) {
if ($this->watcherPHIDs !== self::ATTACHABLE) {
return in_array($user_phid, $this->watcherPHIDs);
}
return $this->assertAttachedKey($this->sparseWatchers, $user_phid);
}
public function isUserAncestorWatcher($user_phid) {
$is_watcher = $this->isUserWatcher($user_phid);
if (!$is_watcher) {
$parent = $this->getParentProject();
if ($parent) {
return $parent->isUserWatcher($user_phid);
}
}
return $is_watcher;
}
public function getWatchedAncestorPHID($user_phid) {
if ($this->isUserWatcher($user_phid)) {
return $this->getPHID();
}
$parent = $this->getParentProject();
if ($parent) {
return $parent->getWatchedAncestorPHID($user_phid);
}
return null;
}
public function setIsUserWatcher($user_phid, $is_watcher) {
if ($this->sparseWatchers === self::ATTACHABLE) {
$this->sparseWatchers = array();
}
$this->sparseWatchers[$user_phid] = $is_watcher;
return $this;
}
public function attachWatcherPHIDs(array $phids) {
$this->watcherPHIDs = $phids;
return $this;
}
public function getWatcherPHIDs() {
return $this->assertAttached($this->watcherPHIDs);
}
public function getAllAncestorWatcherPHIDs() {
$parent = $this->getParentProject();
if ($parent) {
$watchers = $parent->getAllAncestorWatcherPHIDs();
} else {
$watchers = array();
}
foreach ($this->getWatcherPHIDs() as $phid) {
$watchers[$phid] = $phid;
}
return $watchers;
}
public function attachSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function getSlugs() {
return $this->assertAttached($this->slugs);
}
public function getColor() {
if ($this->isArchived()) {
return PHUITagView::COLOR_DISABLED;
}
return $this->color;
}
public function getURI() {
$id = $this->getID();
return "/project/view/{$id}/";
}
public function getProfileURI() {
$id = $this->getID();
return "/project/profile/{$id}/";
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
if (!strlen($this->getPHID())) {
$this->setPHID($this->generatePHID());
}
if (!strlen($this->getProjectPathKey())) {
$hash = PhabricatorHash::digestForIndex($this->getPHID());
$hash = substr($hash, 0, 4);
$this->setProjectPathKey($hash);
}
$path = array();
$depth = 0;
if ($this->parentProjectPHID) {
$parent = $this->getParentProject();
$path[] = $parent->getProjectPath();
$depth = $parent->getProjectDepth() + 1;
}
$path[] = $this->getProjectPathKey();
$path = implode('', $path);
$limit = self::getProjectDepthLimit();
if ($depth >= $limit) {
throw new Exception(pht('Project depth is too great.'));
}
$this->setProjectPath($path);
$this->setProjectDepth($depth);
$this->openTransaction();
$result = parent::save();
$this->updateDatasourceTokens();
$this->saveTransaction();
return $result;
}
public static function getProjectDepthLimit() {
// This is limited by how many path hashes we can fit in the path
// column.
return 16;
}
public function updateDatasourceTokens() {
$table = self::TABLE_DATASOURCE_TOKEN;
$conn_w = $this->establishConnection('w');
$id = $this->getID();
$slugs = queryfx_all(
$conn_w,
'SELECT * FROM %T WHERE projectPHID = %s',
id(new PhabricatorProjectSlug())->getTableName(),
$this->getPHID());
$all_strings = ipull($slugs, 'slug');
$all_strings[] = $this->getDisplayName();
$all_strings = implode(' ', $all_strings);
$tokens = PhabricatorTypeaheadDatasource::tokenizeString($all_strings);
$sql = array();
foreach ($tokens as $token) {
$sql[] = qsprintf($conn_w, '(%d, %s)', $id, $token);
}
$this->openTransaction();
queryfx(
$conn_w,
'DELETE FROM %T WHERE projectID = %d',
$table,
$id);
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn_w,
- 'INSERT INTO %T (projectID, token) VALUES %Q',
+ 'INSERT INTO %T (projectID, token) VALUES %LQ',
$table,
$chunk);
}
$this->saveTransaction();
}
public function isMilestone() {
return ($this->getMilestoneNumber() !== null);
}
public function getParentProject() {
return $this->assertAttached($this->parentProject);
}
public function attachParentProject(PhabricatorProject $project = null) {
$this->parentProject = $project;
return $this;
}
public function getAncestorProjectPaths() {
$parts = array();
$path = $this->getProjectPath();
$parent_length = (strlen($path) - 4);
for ($ii = $parent_length; $ii > 0; $ii -= 4) {
$parts[] = substr($path, 0, $ii);
}
return $parts;
}
public function getAncestorProjects() {
$ancestors = array();
$cursor = $this->getParentProject();
while ($cursor) {
$ancestors[] = $cursor;
$cursor = $cursor->getParentProject();
}
return $ancestors;
}
public function supportsEditMembers() {
if ($this->isMilestone()) {
return false;
}
if ($this->getHasSubprojects()) {
return false;
}
return true;
}
public function supportsMilestones() {
if ($this->isMilestone()) {
return false;
}
return true;
}
public function supportsSubprojects() {
if ($this->isMilestone()) {
return false;
}
return true;
}
public function loadNextMilestoneNumber() {
$current = queryfx_one(
$this->establishConnection('w'),
'SELECT MAX(milestoneNumber) n
FROM %T
WHERE parentProjectPHID = %s',
$this->getTableName(),
$this->getPHID());
if (!$current) {
$number = 1;
} else {
$number = (int)$current['n'] + 1;
}
return $number;
}
public function getDisplayName() {
$name = $this->getName();
// If this is a milestone, show it as "Parent > Sprint 99".
if ($this->isMilestone()) {
$name = pht(
'%s (%s)',
$this->getParentProject()->getName(),
$name);
}
return $name;
}
public function getDisplayIconKey() {
if ($this->isMilestone()) {
$key = PhabricatorProjectIconSet::getMilestoneIconKey();
} else {
$key = $this->getIcon();
}
return $key;
}
public function getDisplayIconIcon() {
$key = $this->getDisplayIconKey();
return PhabricatorProjectIconSet::getIconIcon($key);
}
public function getDisplayIconName() {
$key = $this->getDisplayIconKey();
return PhabricatorProjectIconSet::getIconName($key);
}
public function getDisplayColor() {
if ($this->isMilestone()) {
return $this->getParentProject()->getColor();
}
return $this->getColor();
}
public function getDisplayIconComposeIcon() {
$icon = $this->getDisplayIconIcon();
return $icon;
}
public function getDisplayIconComposeColor() {
$color = $this->getDisplayColor();
$map = array(
'grey' => 'charcoal',
'checkered' => 'backdrop',
);
return idx($map, $color, $color);
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getDefaultWorkboardSort() {
return $this->getProperty('workboard.sort.default');
}
public function setDefaultWorkboardSort($sort) {
return $this->setProperty('workboard.sort.default', $sort);
}
public function getDefaultWorkboardFilter() {
return $this->getProperty('workboard.filter.default');
}
public function setDefaultWorkboardFilter($filter) {
return $this->setProperty('workboard.filter.default', $filter);
}
public function getWorkboardBackgroundColor() {
return $this->getProperty('workboard.background');
}
public function setWorkboardBackgroundColor($color) {
return $this->setProperty('workboard.background', $color);
}
public function getDisplayWorkboardBackgroundColor() {
$color = $this->getWorkboardBackgroundColor();
if ($color === null) {
$parent = $this->getParentProject();
if ($parent) {
return $parent->getDisplayWorkboardBackgroundColor();
}
}
if ($color === 'none') {
$color = null;
}
return $color;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('projects.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorProjectCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorProjectTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorProjectTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
if ($this->isMilestone()) {
return $this->getParentProject()->getSpacePHID();
}
return $this->spacePHID;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$columns = id(new PhabricatorProjectColumn())
->loadAllWhere('projectPHID = %s', $this->getPHID());
foreach ($columns as $column) {
$engine->destroyObject($column);
}
$slugs = id(new PhabricatorProjectSlug())
->loadAllWhere('projectPHID = %s', $this->getPHID());
foreach ($slugs as $slug) {
$slug->delete();
}
$this->saveTransaction();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorProjectFulltextEngine();
}
/* -( PhabricatorFerretInterface )--------------------------------------- */
public function newFerretEngine() {
return new PhabricatorProjectFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the project.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('slug')
->setType('string')
->setDescription(pht('Primary slug/hashtag.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('milestone')
->setType('int?')
->setDescription(pht('For milestones, milestone sequence number.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('parent')
->setType('map<string, wild>?')
->setDescription(
pht(
'For subprojects and milestones, a brief description of the '.
'parent project.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('depth')
->setType('int')
->setDescription(
pht(
'For subprojects and milestones, depth of this project in the '.
'tree. Root projects have depth 0.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('icon')
->setType('map<string, wild>')
->setDescription(pht('Information about the project icon.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('color')
->setType('map<string, wild>')
->setDescription(pht('Information about the project color.')),
);
}
public function getFieldValuesForConduit() {
$color_key = $this->getColor();
$color_name = PhabricatorProjectIconSet::getColorName($color_key);
if ($this->isMilestone()) {
$milestone = (int)$this->getMilestoneNumber();
} else {
$milestone = null;
}
$parent = $this->getParentProject();
if ($parent) {
$parent_ref = $parent->getRefForConduit();
} else {
$parent_ref = null;
}
return array(
'name' => $this->getName(),
'slug' => $this->getPrimarySlug(),
'milestone' => $milestone,
'depth' => (int)$this->getProjectDepth(),
'parent' => $parent_ref,
'icon' => array(
'key' => $this->getDisplayIconKey(),
'name' => $this->getDisplayIconName(),
'icon' => $this->getDisplayIconIcon(),
),
'color' => array(
'key' => $color_key,
'name' => $color_name,
),
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhabricatorProjectsMembersSearchEngineAttachment())
->setAttachmentKey('members'),
id(new PhabricatorProjectsWatchersSearchEngineAttachment())
->setAttachmentKey('watchers'),
id(new PhabricatorProjectsAncestorsSearchEngineAttachment())
->setAttachmentKey('ancestors'),
);
}
/**
* Get an abbreviated representation of this project for use in providing
* "parent" and "ancestor" information.
*/
public function getRefForConduit() {
return array(
'id' => (int)$this->getID(),
'phid' => $this->getPHID(),
'name' => $this->getName(),
);
}
/* -( PhabricatorColumnProxyInterface )------------------------------------ */
public function getProxyColumnName() {
return $this->getName();
}
public function getProxyColumnIcon() {
return $this->getDisplayIconIcon();
}
public function getProxyColumnClass() {
if ($this->isMilestone()) {
return 'phui-workboard-column-milestone';
}
return null;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php
index 671287c3d6..e201ef40fc 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php
@@ -1,181 +1,181 @@
<?php
final class PhabricatorRepositoryManagementParentsWorkflow
extends PhabricatorRepositoryManagementWorkflow {
protected function didConstruct() {
$this
->setName('parents')
->setExamples('**parents** [options] [__repository__] ...')
->setSynopsis(
pht(
'Build parent caches in repositories that are missing the data, '.
'or rebuild them in a specific __repository__.'))
->setArguments(
array(
array(
'name' => 'repos',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$repos = $this->loadRepositories($args, 'repos');
if (!$repos) {
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->execute();
}
$console = PhutilConsole::getConsole();
foreach ($repos as $repo) {
$monogram = $repo->getMonogram();
if ($repo->isSVN()) {
$console->writeOut(
"%s\n",
pht(
'Skipping "%s": Subversion repositories do not require this '.
'cache to be built.',
$monogram));
continue;
}
$this->rebuildRepository($repo);
}
return 0;
}
private function rebuildRepository(PhabricatorRepository $repo) {
$console = PhutilConsole::getConsole();
$console->writeOut("%s\n", pht('Rebuilding "%s"...', $repo->getMonogram()));
$refs = id(new PhabricatorRepositoryRefCursorQuery())
->setViewer($this->getViewer())
->withRefTypes(array(PhabricatorRepositoryRefCursor::TYPE_BRANCH))
->withRepositoryPHIDs(array($repo->getPHID()))
->needPositions(true)
->execute();
$graph = array();
foreach ($refs as $ref) {
if (!$repo->shouldTrackBranch($ref->getRefName())) {
continue;
}
$console->writeOut(
"%s\n",
pht('Rebuilding branch "%s"...', $ref->getRefName()));
foreach ($ref->getPositionIdentifiers() as $commit) {
if ($repo->isGit()) {
$stream = new PhabricatorGitGraphStream($repo, $commit);
} else {
$stream = new PhabricatorMercurialGraphStream($repo, $commit);
}
$discover = array($commit);
while ($discover) {
$target = array_pop($discover);
if (isset($graph[$target])) {
continue;
}
$graph[$target] = $stream->getParents($target);
foreach ($graph[$target] as $parent) {
$discover[] = $parent;
}
}
}
}
$console->writeOut(
"%s\n",
pht(
'Found %s total commit(s); updating...',
phutil_count($graph)));
$commit_table = id(new PhabricatorRepositoryCommit());
$commit_table_name = $commit_table->getTableName();
$conn_w = $commit_table->establishConnection('w');
$bar = id(new PhutilConsoleProgressBar())
->setTotal(count($graph));
$need = array();
foreach ($graph as $child => $parents) {
foreach ($parents as $parent) {
$need[$parent] = $parent;
}
$need[$child] = $child;
}
$map = array();
foreach (array_chunk($need, 2048) as $chunk) {
$rows = queryfx_all(
$conn_w,
'SELECT id, commitIdentifier FROM %T
WHERE commitIdentifier IN (%Ls) AND repositoryID = %d',
$commit_table_name,
$chunk,
$repo->getID());
foreach ($rows as $row) {
$map[$row['commitIdentifier']] = $row['id'];
}
}
$insert_sql = array();
$delete_sql = array();
foreach ($graph as $child => $parents) {
$names = $parents;
$names[] = $child;
foreach ($names as $name) {
if (empty($map[$name])) {
throw new Exception(pht('Unknown commit "%s"!', $name));
}
}
if (!$parents) {
// Write an explicit 0 to indicate "no parents" instead of "no data".
$insert_sql[] = qsprintf(
$conn_w,
'(%d, 0)',
$map[$child]);
} else {
foreach ($parents as $parent) {
$insert_sql[] = qsprintf(
$conn_w,
'(%d, %d)',
$map[$child],
$map[$parent]);
}
}
$delete_sql[] = $map[$child];
$bar->update(1);
}
$commit_table->openTransaction();
foreach (PhabricatorLiskDAO::chunkSQL($delete_sql) as $chunk) {
queryfx(
$conn_w,
- 'DELETE FROM %T WHERE childCommitID IN (%Q)',
+ 'DELETE FROM %T WHERE childCommitID IN (%LQ)',
PhabricatorRepository::TABLE_PARENTS,
$chunk);
}
foreach (PhabricatorLiskDAO::chunkSQL($insert_sql) as $chunk) {
queryfx(
$conn_w,
- 'INSERT INTO %T (childCommitID, parentCommitID) VALUES %Q',
+ 'INSERT INTO %T (childCommitID, parentCommitID) VALUES %LQ',
PhabricatorRepository::TABLE_PARENTS,
$chunk);
}
$commit_table->saveTransaction();
$bar->done();
}
}
diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php
index 6d79742a32..afbf42af97 100644
--- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php
+++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php
@@ -1,160 +1,160 @@
<?php
abstract class PhabricatorRepositoryCommitChangeParserWorker
extends PhabricatorRepositoryCommitParserWorker {
protected function getImportStepFlag() {
return PhabricatorRepositoryCommit::IMPORTED_CHANGE;
}
public function getRequiredLeaseTime() {
// It can take a very long time to parse commits; some commits in the
// Facebook repository affect many millions of paths. Acquire 24h leases.
return phutil_units('24 hours in seconds');
}
abstract protected function parseCommitChanges(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit);
protected function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$this->log("%s\n", pht('Parsing "%s"...', $commit->getMonogram()));
$hint = $this->loadCommitHint($commit);
if ($hint && $hint->isUnreadable()) {
$this->log(
pht(
'This commit is marked as unreadable, so changes will not be '.
'parsed.'));
return;
}
if (!$this->shouldSkipImportStep()) {
$results = $this->parseCommitChanges($repository, $commit);
if ($results) {
$this->writeCommitChanges($repository, $commit, $results);
}
$commit->writeImportStatusFlag($this->getImportStepFlag());
PhabricatorSearchWorker::queueDocumentForIndexing($commit->getPHID());
}
$this->finishParse();
}
public function parseChangesForUnitTest(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
return $this->parseCommitChanges($repository, $commit);
}
public static function lookupOrCreatePaths(array $paths) {
$repository = new PhabricatorRepository();
$conn_w = $repository->establishConnection('w');
$result_map = self::lookupPaths($paths);
$missing_paths = array_fill_keys($paths, true);
$missing_paths = array_diff_key($missing_paths, $result_map);
$missing_paths = array_keys($missing_paths);
if ($missing_paths) {
foreach (array_chunk($missing_paths, 128) as $path_chunk) {
$sql = array();
foreach ($path_chunk as $path) {
$sql[] = qsprintf($conn_w, '(%s, %s)', $path, md5($path));
}
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (path, pathHash) VALUES %Q',
PhabricatorRepository::TABLE_PATH,
implode(', ', $sql));
}
$result_map += self::lookupPaths($missing_paths);
}
return $result_map;
}
private static function lookupPaths(array $paths) {
$repository = new PhabricatorRepository();
$conn_w = $repository->establishConnection('w');
$result_map = array();
foreach (array_chunk($paths, 128) as $path_chunk) {
$chunk_map = queryfx_all(
$conn_w,
'SELECT path, id FROM %T WHERE pathHash IN (%Ls)',
PhabricatorRepository::TABLE_PATH,
array_map('md5', $path_chunk));
foreach ($chunk_map as $row) {
$result_map[$row['path']] = $row['id'];
}
}
return $result_map;
}
protected function finishParse() {
$commit = $this->commit;
if ($this->shouldQueueFollowupTasks()) {
$this->queueTask(
'PhabricatorRepositoryCommitOwnersWorker',
array(
'commitID' => $commit->getID(),
));
}
}
private function writeCommitChanges(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
array $changes) {
$repository_id = (int)$repository->getID();
$commit_id = (int)$commit->getID();
// NOTE: This SQL is being built manually instead of with qsprintf()
// because some SVN changes affect an enormous number of paths (millions)
// and this showed up as significantly slow on a profile at some point.
$changes_sql = array();
foreach ($changes as $change) {
$values = array(
$repository_id,
(int)$change->getPathID(),
$commit_id,
nonempty((int)$change->getTargetPathID(), 'null'),
nonempty((int)$change->getTargetCommitID(), 'null'),
(int)$change->getChangeType(),
(int)$change->getFileType(),
(int)$change->getIsDirect(),
(int)$change->getCommitSequence(),
);
$changes_sql[] = '('.implode(', ', $values).')';
}
$conn_w = $repository->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE commitID = %d',
PhabricatorRepository::TABLE_PATHCHANGE,
$commit_id);
foreach (PhabricatorLiskDAO::chunkSQL($changes_sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T
(repositoryID, pathID, commitID, targetPathID, targetCommitID,
changeType, fileType, isDirect, commitSequence)
- VALUES %Q',
+ VALUES %LQ',
PhabricatorRepository::TABLE_PATHCHANGE,
$chunk);
}
}
}
diff --git a/src/applications/search/engineextension/PhabricatorFerretFulltextEngineExtension.php b/src/applications/search/engineextension/PhabricatorFerretFulltextEngineExtension.php
index fe50518f3f..8647bc83eb 100644
--- a/src/applications/search/engineextension/PhabricatorFerretFulltextEngineExtension.php
+++ b/src/applications/search/engineextension/PhabricatorFerretFulltextEngineExtension.php
@@ -1,255 +1,255 @@
<?php
final class PhabricatorFerretFulltextEngineExtension
extends PhabricatorFulltextEngineExtension {
const EXTENSIONKEY = 'ferret';
public function getExtensionName() {
return pht('Ferret Fulltext Engine');
}
public function shouldIndexFulltextObject($object) {
return ($object instanceof PhabricatorFerretInterface);
}
public function indexFulltextObject(
$object,
PhabricatorSearchAbstractDocument $document) {
$phid = $document->getPHID();
$engine = $object->newFerretEngine();
$is_closed = 0;
$author_phid = null;
$owner_phid = null;
foreach ($document->getRelationshipData() as $relationship) {
list($related_type, $related_phid) = $relationship;
switch ($related_type) {
case PhabricatorSearchRelationship::RELATIONSHIP_OPEN:
$is_closed = 0;
break;
case PhabricatorSearchRelationship::RELATIONSHIP_CLOSED:
$is_closed = 1;
break;
case PhabricatorSearchRelationship::RELATIONSHIP_OWNER:
$owner_phid = $related_phid;
break;
case PhabricatorSearchRelationship::RELATIONSHIP_UNOWNED:
$owner_phid = null;
break;
case PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR:
$author_phid = $related_phid;
break;
}
}
$stemmer = $engine->newStemmer();
// Copy all of the "title" and "body" fields to create new "core" fields.
// This allows users to search "in title or body" with the "core:" prefix.
$document_fields = $document->getFieldData();
$virtual_fields = array();
foreach ($document_fields as $field) {
$virtual_fields[] = $field;
list($key, $raw_corpus) = $field;
switch ($key) {
case PhabricatorSearchDocumentFieldType::FIELD_TITLE:
case PhabricatorSearchDocumentFieldType::FIELD_BODY:
$virtual_fields[] = array(
PhabricatorSearchDocumentFieldType::FIELD_CORE,
$raw_corpus,
);
break;
}
$virtual_fields[] = array(
PhabricatorSearchDocumentFieldType::FIELD_ALL,
$raw_corpus,
);
}
$empty_template = array(
'raw' => array(),
'term' => array(),
'normal' => array(),
);
$ferret_corpus_map = array();
foreach ($virtual_fields as $field) {
list($key, $raw_corpus) = $field;
if (!strlen($raw_corpus)) {
continue;
}
$term_corpus = $engine->newTermsCorpus($raw_corpus);
$normal_corpus = $stemmer->stemCorpus($raw_corpus);
$normal_corpus = $engine->newTermsCorpus($normal_corpus);
if (!isset($ferret_corpus_map[$key])) {
$ferret_corpus_map[$key] = $empty_template;
}
$ferret_corpus_map[$key]['raw'][] = $raw_corpus;
$ferret_corpus_map[$key]['term'][] = $term_corpus;
$ferret_corpus_map[$key]['normal'][] = $normal_corpus;
}
$ferret_fields = array();
$ngrams_source = array();
foreach ($ferret_corpus_map as $key => $fields) {
$raw_corpus = $fields['raw'];
$raw_corpus = implode("\n", $raw_corpus);
if (strlen($raw_corpus)) {
$ngrams_source[] = $raw_corpus;
}
$normal_corpus = $fields['normal'];
$normal_corpus = implode("\n", $normal_corpus);
if (strlen($normal_corpus)) {
$ngrams_source[] = $normal_corpus;
}
$term_corpus = $fields['term'];
$term_corpus = implode("\n", $term_corpus);
if (strlen($term_corpus)) {
$ngrams_source[] = $term_corpus;
}
$ferret_fields[] = array(
'fieldKey' => $key,
'rawCorpus' => $raw_corpus,
'termCorpus' => $term_corpus,
'normalCorpus' => $normal_corpus,
);
}
$ngrams_source = implode("\n", $ngrams_source);
$ngrams = $engine->getTermNgramsFromString($ngrams_source);
$object->openTransaction();
try {
$conn = $object->establishConnection('w');
$this->deleteOldDocument($engine, $object, $document);
queryfx(
$conn,
'INSERT INTO %T (objectPHID, isClosed, epochCreated, epochModified,
authorPHID, ownerPHID) VALUES (%s, %d, %d, %d, %ns, %ns)',
$engine->getDocumentTableName(),
$object->getPHID(),
$is_closed,
$document->getDocumentCreated(),
$document->getDocumentModified(),
$author_phid,
$owner_phid);
$document_id = $conn->getInsertID();
foreach ($ferret_fields as $ferret_field) {
queryfx(
$conn,
'INSERT INTO %T (documentID, fieldKey, rawCorpus, termCorpus,
normalCorpus) VALUES (%d, %s, %s, %s, %s)',
$engine->getFieldTableName(),
$document_id,
$ferret_field['fieldKey'],
$ferret_field['rawCorpus'],
$ferret_field['termCorpus'],
$ferret_field['normalCorpus']);
}
if ($ngrams) {
$common = queryfx_all(
$conn,
'SELECT ngram FROM %T WHERE ngram IN (%Ls)',
$engine->getCommonNgramsTableName(),
$ngrams);
$common = ipull($common, 'ngram', 'ngram');
foreach ($ngrams as $key => $ngram) {
if (isset($common[$ngram])) {
unset($ngrams[$key]);
continue;
}
// NOTE: MySQL discards trailing whitespace in CHAR(X) columns.
$trim_ngram = rtrim($ngram, ' ');
if (isset($common[$ngram])) {
unset($ngrams[$key]);
continue;
}
}
}
if ($ngrams) {
$sql = array();
foreach ($ngrams as $ngram) {
$sql[] = qsprintf(
$conn,
'(%d, %s)',
$document_id,
$ngram);
}
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn,
- 'INSERT INTO %T (documentID, ngram) VALUES %Q',
+ 'INSERT INTO %T (documentID, ngram) VALUES %LQ',
$engine->getNgramsTableName(),
$chunk);
}
}
} catch (Exception $ex) {
$object->killTransaction();
throw $ex;
}
$object->saveTransaction();
}
private function deleteOldDocument(
PhabricatorFerretEngine $engine,
$object,
PhabricatorSearchAbstractDocument $document) {
$conn = $object->establishConnection('w');
$old_document = queryfx_one(
$conn,
'SELECT * FROM %T WHERE objectPHID = %s',
$engine->getDocumentTableName(),
$object->getPHID());
if (!$old_document) {
return;
}
$old_id = $old_document['id'];
queryfx(
$conn,
'DELETE FROM %T WHERE id = %d',
$engine->getDocumentTableName(),
$old_id);
queryfx(
$conn,
'DELETE FROM %T WHERE documentID = %d',
$engine->getFieldTableName(),
$old_id);
queryfx(
$conn,
'DELETE FROM %T WHERE documentID = %d',
$engine->getNgramsTableName(),
$old_id);
}
}
diff --git a/src/applications/search/management/PhabricatorSearchManagementNgramsWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementNgramsWorkflow.php
index 1a1124b4d8..3d87378849 100644
--- a/src/applications/search/management/PhabricatorSearchManagementNgramsWorkflow.php
+++ b/src/applications/search/management/PhabricatorSearchManagementNgramsWorkflow.php
@@ -1,139 +1,139 @@
<?php
final class PhabricatorSearchManagementNgramsWorkflow
extends PhabricatorSearchManagementWorkflow {
protected function didConstruct() {
$this
->setName('ngrams')
->setSynopsis(
pht(
'Recompute common ngrams. This is an advanced workflow that '.
'can harm search quality if used improperly.'))
->setArguments(
array(
array(
'name' => 'reset',
'help' => pht('Reset all common ngram records.'),
),
array(
'name' => 'threshold',
'param' => 'threshold',
'help' => pht(
'Prune ngrams present in more than this fraction of '.
'documents. Provide a value between 0.0 and 1.0.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$min_documents = 4096;
$is_reset = $args->getArg('reset');
$threshold = $args->getArg('threshold');
if ($is_reset && $threshold !== null) {
throw new PhutilArgumentUsageException(
pht('Specify either --reset or --threshold, not both.'));
}
if (!$is_reset && $threshold === null) {
throw new PhutilArgumentUsageException(
pht('Specify either --reset or --threshold.'));
}
if (!$is_reset) {
if (!is_numeric($threshold)) {
throw new PhutilArgumentUsageException(
pht('Specify a numeric threshold between 0 and 1.'));
}
$threshold = (double)$threshold;
if ($threshold <= 0 || $threshold >= 1) {
throw new PhutilArgumentUsageException(
pht('Threshold must be greater than 0.0 and less than 1.0.'));
}
}
$all_objects = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorFerretInterface')
->execute();
foreach ($all_objects as $object) {
$engine = $object->newFerretEngine();
$conn = $object->establishConnection('w');
$display_name = get_class($object);
if ($is_reset) {
echo tsprintf(
"%s\n",
pht(
'Resetting common ngrams for "%s".',
$display_name));
queryfx(
$conn,
'DELETE FROM %T',
$engine->getCommonNgramsTableName());
continue;
}
$document_count = queryfx_one(
$conn,
'SELECT COUNT(*) N FROM %T',
$engine->getDocumentTableName());
$document_count = $document_count['N'];
if ($document_count < $min_documents) {
echo tsprintf(
"%s\n",
pht(
'Too few documents of type "%s" for any ngrams to be common.',
$display_name));
continue;
}
$min_frequency = (int)ceil($document_count * $threshold);
$common_ngrams = queryfx_all(
$conn,
'SELECT ngram, COUNT(*) N FROM %T
GROUP BY ngram
HAVING N >= %d',
$engine->getNgramsTableName(),
$min_frequency);
if (!$common_ngrams) {
echo tsprintf(
"%s\n",
pht(
'No new common ngrams exist for "%s".',
$display_name));
continue;
}
$sql = array();
foreach ($common_ngrams as $ngram) {
$sql[] = qsprintf(
$conn,
'(%s, 1)',
$ngram['ngram']);
}
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn,
'INSERT IGNORE INTO %T (ngram, needsCollection)
- VALUES %Q',
+ VALUES %LQ',
$engine->getCommonNgramsTableName(),
$chunk);
}
echo tsprintf(
"%s\n",
pht(
'Updated common ngrams for "%s".',
$display_name));
}
}
}
diff --git a/src/applications/system/engine/PhabricatorSystemActionEngine.php b/src/applications/system/engine/PhabricatorSystemActionEngine.php
index b6a8e26711..6b8352a29e 100644
--- a/src/applications/system/engine/PhabricatorSystemActionEngine.php
+++ b/src/applications/system/engine/PhabricatorSystemActionEngine.php
@@ -1,201 +1,201 @@
<?php
final class PhabricatorSystemActionEngine extends Phobject {
/**
* Prepare to take an action, throwing an exception if the user has exceeded
* the rate limit.
*
* The `$actors` are a list of strings. Normally this will be a list of
* user PHIDs, but some systems use other identifiers (like email
* addresses). Each actor's score threshold is tracked independently. If
* any actor exceeds the rate limit for the action, this method throws.
*
* The `$action` defines the actual thing being rate limited, and sets the
* limit.
*
* You can pass either a positive, zero, or negative `$score` to this method:
*
* - If the score is positive, the user is given that many points toward
* the rate limit after the limit is checked. Over time, this will cause
* them to hit the rate limit and be prevented from taking further
* actions.
* - If the score is zero, the rate limit is checked but no score changes
* are made. This allows you to check for a rate limit before beginning
* a workflow, so the user doesn't fill in a form only to get rate limited
* at the end.
* - If the score is negative, the user is credited points, allowing them
* to take more actions than the limit normally permits. By awarding
* points for failed actions and credits for successful actions, a
* system can be sensitive to failure without overly restricting
* legitimate uses.
*
* If any actor is exceeding their rate limit, this method throws a
* @{class:PhabricatorSystemActionRateLimitException}.
*
* @param list<string> List of actors.
* @param PhabricatorSystemAction Action being taken.
* @param float Score or credit, see above.
* @return void
*/
public static function willTakeAction(
array $actors,
PhabricatorSystemAction $action,
$score) {
// If the score for this action is negative, we're giving the user a credit,
// so don't bother checking if they're blocked or not.
if ($score >= 0) {
$blocked = self::loadBlockedActors($actors, $action, $score);
if ($blocked) {
foreach ($blocked as $actor => $actor_score) {
throw new PhabricatorSystemActionRateLimitException(
$action,
$actor_score);
}
}
}
if ($score != 0) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
self::recordAction($actors, $action, $score);
unset($unguarded);
}
}
public static function loadBlockedActors(
array $actors,
PhabricatorSystemAction $action,
$score) {
$scores = self::loadScores($actors, $action);
$window = self::getWindow();
$blocked = array();
foreach ($scores as $actor => $actor_score) {
// For the purposes of checking for a block, we just use the raw
// persistent score and do not include the score for this action. This
// allows callers to test for a block without adding any points and get
// the same result they would if they were adding points: we only
// trigger a rate limit when the persistent score exceeds the threshold.
if ($action->shouldBlockActor($actor, $actor_score)) {
// When reporting the results, we do include the points for this
// action. This makes the error messages more clear, since they
// more accurately report the number of actions the user has really
// tried to take.
$blocked[$actor] = $actor_score + ($score / $window);
}
}
return $blocked;
}
public static function loadScores(
array $actors,
PhabricatorSystemAction $action) {
if (!$actors) {
return array();
}
$actor_hashes = array();
foreach ($actors as $actor) {
$actor_hashes[] = PhabricatorHash::digestForIndex($actor);
}
$log = new PhabricatorSystemActionLog();
$window = self::getWindow();
$conn_r = $log->establishConnection('r');
$scores = queryfx_all(
$conn_r,
'SELECT actorIdentity, SUM(score) totalScore FROM %T
WHERE action = %s AND actorHash IN (%Ls)
AND epoch >= %d GROUP BY actorHash',
$log->getTableName(),
$action->getActionConstant(),
$actor_hashes,
(time() - $window));
$scores = ipull($scores, 'totalScore', 'actorIdentity');
foreach ($scores as $key => $score) {
$scores[$key] = $score / $window;
}
$scores = $scores + array_fill_keys($actors, 0);
return $scores;
}
private static function recordAction(
array $actors,
PhabricatorSystemAction $action,
$score) {
$log = new PhabricatorSystemActionLog();
$conn_w = $log->establishConnection('w');
$sql = array();
foreach ($actors as $actor) {
$sql[] = qsprintf(
$conn_w,
'(%s, %s, %s, %f, %d)',
PhabricatorHash::digestForIndex($actor),
$actor,
$action->getActionConstant(),
$score,
time());
}
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (actorHash, actorIdentity, action, score, epoch)
- VALUES %Q',
+ VALUES %LQ',
$log->getTableName(),
$chunk);
}
}
private static function getWindow() {
// Limit queries to the last hour of data so we don't need to look at as
// many rows. We can use an arbitrarily larger window instead (we normalize
// scores to actions per second) but all the actions we care about limiting
// have a limit much higher than one action per hour.
return phutil_units('1 hour in seconds');
}
/**
* Reset all action counts for actions taken by some set of actors in the
* previous action window.
*
* @param list<string> Actors to reset counts for.
* @return int Number of actions cleared.
*/
public static function resetActions(array $actors) {
$log = new PhabricatorSystemActionLog();
$conn_w = $log->establishConnection('w');
$now = PhabricatorTime::getNow();
$hashes = array();
foreach ($actors as $actor) {
$hashes[] = PhabricatorHash::digestForIndex($actor);
}
queryfx(
$conn_w,
'DELETE FROM %T
WHERE actorHash IN (%Ls) AND epoch BETWEEN %d AND %d',
$log->getTableName(),
$hashes,
$now - self::getWindow(),
$now);
return $conn_w->getAffectedRows();
}
}
diff --git a/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php b/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php
index efe12e68b8..8ae5529f95 100644
--- a/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php
+++ b/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php
@@ -1,328 +1,328 @@
<?php
/**
* Convenience class to perform operations on an entire field list, like reading
* all values from storage.
*
* $field_list = new PhabricatorCustomFieldList($fields);
*
*/
final class PhabricatorCustomFieldList extends Phobject {
private $fields;
private $viewer;
public function __construct(array $fields) {
assert_instances_of($fields, 'PhabricatorCustomField');
$this->fields = $fields;
}
public function getFields() {
return $this->fields;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
foreach ($this->getFields() as $field) {
$field->setViewer($viewer);
}
return $this;
}
public function readFieldsFromObject(
PhabricatorCustomFieldInterface $object) {
$fields = $this->getFields();
foreach ($fields as $field) {
$field
->setObject($object)
->readValueFromObject($object);
}
return $this;
}
/**
* Read stored values for all fields which support storage.
*
* @param PhabricatorCustomFieldInterface Object to read field values for.
* @return void
*/
public function readFieldsFromStorage(
PhabricatorCustomFieldInterface $object) {
$this->readFieldsFromObject($object);
$fields = $this->getFields();
id(new PhabricatorCustomFieldStorageQuery())
->addFields($fields)
->execute();
return $this;
}
public function appendFieldsToForm(AphrontFormView $form) {
$enabled = array();
foreach ($this->fields as $field) {
if ($field->shouldEnableForRole(PhabricatorCustomField::ROLE_EDIT)) {
$enabled[] = $field;
}
}
$phids = array();
foreach ($enabled as $field_key => $field) {
$phids[$field_key] = $field->getRequiredHandlePHIDsForEdit();
}
$all_phids = array_mergev($phids);
if ($all_phids) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->viewer)
->withPHIDs($all_phids)
->execute();
} else {
$handles = array();
}
foreach ($enabled as $field_key => $field) {
$field_handles = array_select_keys($handles, $phids[$field_key]);
$instructions = $field->getInstructionsForEdit();
if (strlen($instructions)) {
$form->appendRemarkupInstructions($instructions);
}
$form->appendChild($field->renderEditControl($field_handles));
}
}
public function appendFieldsToPropertyList(
PhabricatorCustomFieldInterface $object,
PhabricatorUser $viewer,
PHUIPropertyListView $view) {
$this->readFieldsFromStorage($object);
$fields = $this->fields;
foreach ($fields as $field) {
$field->setViewer($viewer);
}
// Move all the blocks to the end, regardless of their configuration order,
// because it always looks silly to render a block in the middle of a list
// of properties.
$head = array();
$tail = array();
foreach ($fields as $key => $field) {
$style = $field->getStyleForPropertyView();
switch ($style) {
case 'property':
case 'header':
$head[$key] = $field;
break;
case 'block':
$tail[$key] = $field;
break;
default:
throw new Exception(
pht(
"Unknown field property view style '%s'; valid styles are ".
"'%s' and '%s'.",
$style,
'block',
'property'));
}
}
$fields = $head + $tail;
$add_header = null;
$phids = array();
foreach ($fields as $key => $field) {
$phids[$key] = $field->getRequiredHandlePHIDsForPropertyView();
}
$all_phids = array_mergev($phids);
if ($all_phids) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($all_phids)
->execute();
} else {
$handles = array();
}
foreach ($fields as $key => $field) {
$field_handles = array_select_keys($handles, $phids[$key]);
$label = $field->renderPropertyViewLabel();
$value = $field->renderPropertyViewValue($field_handles);
if ($value !== null) {
switch ($field->getStyleForPropertyView()) {
case 'header':
// We want to hide headers if the fields they're associated with
// don't actually produce any visible properties. For example, in a
// list like this:
//
// Header A
// Prop A: Value A
// Header B
// Prop B: Value B
//
// ...if the "Prop A" field returns `null` when rendering its
// property value and we rendered naively, we'd get this:
//
// Header A
// Header B
// Prop B: Value B
//
// This is silly. Instead, we hide "Header A".
$add_header = $value;
break;
case 'property':
if ($add_header !== null) {
// Add the most recently seen header.
$view->addSectionHeader($add_header);
$add_header = null;
}
$view->addProperty($label, $value);
break;
case 'block':
$icon = $field->getIconForPropertyView();
$view->invokeWillRenderEvent();
if ($label !== null) {
$view->addSectionHeader($label, $icon);
}
$view->addTextContent($value);
break;
}
}
}
}
public function buildFieldTransactionsFromRequest(
PhabricatorApplicationTransaction $template,
AphrontRequest $request) {
$xactions = array();
$role = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS;
foreach ($this->fields as $field) {
if (!$field->shouldEnableForRole($role)) {
continue;
}
$transaction_type = $field->getApplicationTransactionType();
$xaction = id(clone $template)
->setTransactionType($transaction_type);
if ($transaction_type == PhabricatorTransactions::TYPE_CUSTOMFIELD) {
// For TYPE_CUSTOMFIELD transactions only, we provide the old value
// as an input.
$old_value = $field->getOldValueForApplicationTransactions();
$xaction->setOldValue($old_value);
}
$field->readValueFromRequest($request);
$xaction
->setNewValue($field->getNewValueForApplicationTransactions());
if ($transaction_type == PhabricatorTransactions::TYPE_CUSTOMFIELD) {
// For TYPE_CUSTOMFIELD transactions, add the field key in metadata.
$xaction->setMetadataValue('customfield:key', $field->getFieldKey());
}
$metadata = $field->getApplicationTransactionMetadata();
foreach ($metadata as $key => $value) {
$xaction->setMetadataValue($key, $value);
}
$xactions[] = $xaction;
}
return $xactions;
}
/**
* Publish field indexes into index tables, so ApplicationSearch can search
* them.
*
* @return void
*/
public function rebuildIndexes(PhabricatorCustomFieldInterface $object) {
$indexes = array();
$index_keys = array();
$phid = $object->getPHID();
$role = PhabricatorCustomField::ROLE_APPLICATIONSEARCH;
foreach ($this->fields as $field) {
if (!$field->shouldEnableForRole($role)) {
continue;
}
$index_keys[$field->getFieldIndex()] = true;
foreach ($field->buildFieldIndexes() as $index) {
$index->setObjectPHID($phid);
$indexes[$index->getTableName()][] = $index;
}
}
if (!$indexes) {
return;
}
$any_index = head(head($indexes));
$conn_w = $any_index->establishConnection('w');
foreach ($indexes as $table => $index_list) {
$sql = array();
foreach ($index_list as $index) {
$sql[] = $index->formatForInsert($conn_w);
}
$indexes[$table] = $sql;
}
$any_index->openTransaction();
foreach ($indexes as $table => $sql_list) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE objectPHID = %s AND indexKey IN (%Ls)',
$table,
$phid,
array_keys($index_keys));
if (!$sql_list) {
continue;
}
foreach (PhabricatorLiskDAO::chunkSQL($sql_list) as $chunk) {
queryfx(
$conn_w,
- 'INSERT INTO %T (objectPHID, indexKey, indexValue) VALUES %Q',
+ 'INSERT INTO %T (objectPHID, indexKey, indexValue) VALUES %LQ',
$table,
$chunk);
}
}
$any_index->saveTransaction();
}
public function updateAbstractDocument(
PhabricatorSearchAbstractDocument $document) {
$role = PhabricatorCustomField::ROLE_GLOBALSEARCH;
foreach ($this->getFields() as $field) {
if (!$field->shouldEnableForRole($role)) {
continue;
}
$field->updateAbstractDocument($document);
}
}
}
diff --git a/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php b/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php
index 53d16a7948..cff7390949 100644
--- a/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php
+++ b/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php
@@ -1,69 +1,69 @@
<?php
final class QueryFormattingTestCase extends PhabricatorTestCase {
public function testQueryFormatting() {
$conn = id(new PhabricatorUser())->establishConnection('r');
$this->assertEqual(
'NULL',
- qsprintf($conn, '%nd', null));
+ (string)qsprintf($conn, '%nd', null));
$this->assertEqual(
'0',
- qsprintf($conn, '%nd', 0));
+ (string)qsprintf($conn, '%nd', 0));
$this->assertEqual(
'0',
- qsprintf($conn, '%d', 0));
+ (string)qsprintf($conn, '%d', 0));
$raised = null;
try {
qsprintf($conn, '%d', 'derp');
} catch (Exception $ex) {
$raised = $ex;
}
$this->assertTrue(
(bool)$raised,
pht('%s should raise exception for invalid %%d conversion.', 'qsprintf'));
$this->assertEqual(
"'<S>'",
- qsprintf($conn, '%s', null));
+ (string)qsprintf($conn, '%s', null));
$this->assertEqual(
'NULL',
- qsprintf($conn, '%ns', null));
+ (string)qsprintf($conn, '%ns', null));
$this->assertEqual(
"'<S>', '<S>'",
- qsprintf($conn, '%Ls', array('x', 'y')));
+ (string)qsprintf($conn, '%Ls', array('x', 'y')));
$this->assertEqual(
"'<B>'",
- qsprintf($conn, '%B', null));
+ (string)qsprintf($conn, '%B', null));
$this->assertEqual(
'NULL',
- qsprintf($conn, '%nB', null));
+ (string)qsprintf($conn, '%nB', null));
$this->assertEqual(
"'<B>', '<B>'",
- qsprintf($conn, '%LB', array('x', 'y')));
+ (string)qsprintf($conn, '%LB', array('x', 'y')));
$this->assertEqual(
'<C>',
- qsprintf($conn, '%T', 'x'));
+ (string)qsprintf($conn, '%T', 'x'));
$this->assertEqual(
'<C>',
- qsprintf($conn, '%C', 'y'));
+ (string)qsprintf($conn, '%C', 'y'));
$this->assertEqual(
'<C>.<C>',
- qsprintf($conn, '%R', new AphrontDatabaseTableRef('x', 'y')));
+ (string)qsprintf($conn, '%R', new AphrontDatabaseTableRef('x', 'y')));
}
}
diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
index b300efbf4b..e47d701df9 100644
--- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
+++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
@@ -1,331 +1,328 @@
<?php
/**
* @task config Configuring Storage
*/
abstract class PhabricatorLiskDAO extends LiskDAO {
private static $namespaceStack = array();
const ATTACHABLE = '<attachable>';
const CONFIG_APPLICATION_SERIALIZERS = 'phabricator/serializers';
/* -( Configuring Storage )------------------------------------------------ */
/**
* @task config
*/
public static function pushStorageNamespace($namespace) {
self::$namespaceStack[] = $namespace;
}
/**
* @task config
*/
public static function popStorageNamespace() {
array_pop(self::$namespaceStack);
}
/**
* @task config
*/
public static function getDefaultStorageNamespace() {
return PhabricatorEnv::getEnvConfig('storage.default-namespace');
}
/**
* @task config
*/
public static function getStorageNamespace() {
$namespace = end(self::$namespaceStack);
if (!strlen($namespace)) {
$namespace = self::getDefaultStorageNamespace();
}
if (!strlen($namespace)) {
throw new Exception(pht('No storage namespace configured!'));
}
return $namespace;
}
/**
* @task config
*/
protected function establishLiveConnection($mode) {
$namespace = self::getStorageNamespace();
$database = $namespace.'_'.$this->getApplicationName();
$is_readonly = PhabricatorEnv::isReadOnly();
if ($is_readonly && ($mode != 'r')) {
$this->raiseImproperWrite($database);
}
$connection = $this->newClusterConnection(
$this->getApplicationName(),
$database,
$mode);
// TODO: This should be testing if the mode is "r", but that would probably
// break a lot of things. Perform a more narrow test for readonly mode
// until we have greater certainty that this works correctly most of the
// time.
if ($is_readonly) {
$connection->setReadOnly(true);
}
return $connection;
}
private function newClusterConnection($application, $database, $mode) {
$master = PhabricatorDatabaseRef::getMasterDatabaseRefForApplication(
$application);
$master_exception = null;
if ($master && !$master->isSevered()) {
$connection = $master->newApplicationConnection($database);
if ($master->isReachable($connection)) {
return $connection;
} else {
if ($mode == 'w') {
$this->raiseImpossibleWrite($database);
}
PhabricatorEnv::setReadOnly(
true,
PhabricatorEnv::READONLY_UNREACHABLE);
$master_exception = $master->getConnectionException();
}
}
$replica = PhabricatorDatabaseRef::getReplicaDatabaseRefForApplication(
$application);
if ($replica) {
$connection = $replica->newApplicationConnection($database);
$connection->setReadOnly(true);
if ($replica->isReachable($connection)) {
return $connection;
}
}
if (!$master && !$replica) {
$this->raiseUnconfigured($database);
}
$this->raiseUnreachable($database, $master_exception);
}
private function raiseImproperWrite($database) {
throw new PhabricatorClusterImproperWriteException(
pht(
'Unable to establish a write-mode connection (to application '.
'database "%s") because Phabricator is in read-only mode. Whatever '.
'you are trying to do does not function correctly in read-only mode.',
$database));
}
private function raiseImpossibleWrite($database) {
throw new PhabricatorClusterImpossibleWriteException(
pht(
'Unable to connect to master database ("%s"). This is a severe '.
'failure; your request did not complete.',
$database));
}
private function raiseUnconfigured($database) {
throw new Exception(
pht(
'Unable to establish a connection to any database host '.
'(while trying "%s"). No masters or replicas are configured.',
$database));
}
private function raiseUnreachable($database, Exception $proxy = null) {
$message = pht(
'Unable to establish a connection to any database host '.
'(while trying "%s"). All masters and replicas are completely '.
'unreachable.',
$database);
if ($proxy) {
$proxy_message = pht(
'%s: %s',
get_class($proxy),
$proxy->getMessage());
$message = $message."\n\n".$proxy_message;
}
throw new PhabricatorClusterStrandedException($message);
}
/**
* @task config
*/
public function getTableName() {
$str = 'phabricator';
$len = strlen($str);
$class = strtolower(get_class($this));
if (!strncmp($class, $str, $len)) {
$class = substr($class, $len);
}
$app = $this->getApplicationName();
if (!strncmp($class, $app, strlen($app))) {
$class = substr($class, strlen($app));
}
if (strlen($class)) {
return $app.'_'.$class;
} else {
return $app;
}
}
/**
* @task config
*/
abstract public function getApplicationName();
protected function getDatabaseName() {
return self::getStorageNamespace().'_'.$this->getApplicationName();
}
/**
* Break a list of escaped SQL statement fragments (e.g., VALUES lists for
* INSERT, previously built with @{function:qsprintf}) into chunks which will
* fit under the MySQL 'max_allowed_packet' limit.
*
- * Chunks are glued together with `$glue`, by default ", ".
- *
* If a statement is too large to fit within the limit, it is broken into
* its own chunk (but might fail when the query executes).
*/
public static function chunkSQL(
array $fragments,
- $glue = ', ',
$limit = null) {
if ($limit === null) {
// NOTE: Hard-code this at 1MB for now, minus a 10% safety buffer.
// Eventually we could query MySQL or let the user configure it.
$limit = (int)((1024 * 1024) * 0.90);
}
$result = array();
$chunk = array();
$len = 0;
- $glue_len = strlen($glue);
+ $glue_len = strlen(', ');
foreach ($fragments as $fragment) {
- $this_len = strlen($fragment);
+ if ($fragment instanceof PhutilQueryString) {
+ $this_len = strlen($fragment->getUnmaskedString());
+ } else {
+ $this_len = strlen($fragment);
+ }
if ($chunk) {
// Chunks after the first also imply glue.
$this_len += $glue_len;
}
if ($len + $this_len <= $limit) {
$len += $this_len;
$chunk[] = $fragment;
} else {
if ($chunk) {
$result[] = $chunk;
}
- $len = strlen($fragment);
+ $len = ($this_len - $glue_len);
$chunk = array($fragment);
}
}
if ($chunk) {
$result[] = $chunk;
}
- foreach ($result as $key => $fragment_list) {
- $result[$key] = implode($glue, $fragment_list);
- }
-
return $result;
}
protected function assertAttached($property) {
if ($property === self::ATTACHABLE) {
throw new PhabricatorDataNotAttachedException($this);
}
return $property;
}
protected function assertAttachedKey($value, $key) {
$this->assertAttached($value);
if (!array_key_exists($key, $value)) {
throw new PhabricatorDataNotAttachedException($this);
}
return $value[$key];
}
protected function detectEncodingForStorage($string) {
return phutil_is_utf8($string) ? 'utf8' : null;
}
protected function getUTF8StringFromStorage($string, $encoding) {
if ($encoding == 'utf8') {
return $string;
}
if (function_exists('mb_detect_encoding')) {
if (strlen($encoding)) {
$try_encodings = array(
$encoding,
);
} else {
// TODO: This is pretty much a guess, and probably needs to be
// configurable in the long run.
$try_encodings = array(
'JIS',
'EUC-JP',
'SJIS',
'ISO-8859-1',
);
}
$guess = mb_detect_encoding($string, $try_encodings);
if ($guess) {
return mb_convert_encoding($string, 'UTF-8', $guess);
}
}
return phutil_utf8ize($string);
}
protected function willReadData(array &$data) {
parent::willReadData($data);
static $custom;
if ($custom === null) {
$custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS);
}
if ($custom) {
foreach ($custom as $key => $serializer) {
$data[$key] = $serializer->willReadValue($data[$key]);
}
}
}
protected function willWriteData(array &$data) {
static $custom;
if ($custom === null) {
$custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS);
}
if ($custom) {
foreach ($custom as $key => $serializer) {
$data[$key] = $serializer->willWriteValue($data[$key]);
}
}
parent::willWriteData($data);
}
}
diff --git a/src/infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php b/src/infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php
index 8ce7608a8b..66ffdb17bc 100644
--- a/src/infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php
+++ b/src/infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php
@@ -1,66 +1,68 @@
<?php
final class LiskChunkTestCase extends PhabricatorTestCase {
public function testSQLChunking() {
$fragments = array(
'a',
'a',
'b',
'b',
'ccc',
'dd',
'e',
);
$this->assertEqual(
array(
- 'aa',
- 'bb',
- 'ccc',
- 'dd',
- 'e',
+ array('a'),
+ array('a'),
+ array('b'),
+ array('b'),
+ array('ccc'),
+ array('dd'),
+ array('e'),
),
- PhabricatorLiskDAO::chunkSQL($fragments, '', 2));
+ PhabricatorLiskDAO::chunkSQL($fragments, 2));
$fragments = array(
'a',
'a',
'a',
'XX',
'a',
'a',
'a',
'a',
);
$this->assertEqual(
array(
- 'a, a, a',
- 'XX, a, a',
- 'a, a',
+ array('a', 'a', 'a'),
+ array('XX', 'a', 'a'),
+ array('a', 'a'),
),
- PhabricatorLiskDAO::chunkSQL($fragments, ', ', 8));
+ PhabricatorLiskDAO::chunkSQL($fragments, 8));
$fragments = array(
'xxxxxxxxxx',
'yyyyyyyyyy',
'a',
'b',
'c',
'zzzzzzzzzz',
);
$this->assertEqual(
array(
- 'xxxxxxxxxx',
- 'yyyyyyyyyy',
- 'a, b, c',
- 'zzzzzzzzzz',
+ array('xxxxxxxxxx'),
+ array('yyyyyyyyyy'),
+ array('a', 'b', 'c'),
+ array('zzzzzzzzzz'),
),
- PhabricatorLiskDAO::chunkSQL($fragments, ', ', 8));
+ PhabricatorLiskDAO::chunkSQL($fragments, 8));
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jun 10, 5:06 PM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
140576
Default Alt Text
(147 KB)

Event Timeline