@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement child/descendant query rules in Projects

Summary:
Ref T10010. This adds infrastructure for querying projects by type, depth, parent and ancestor.

I needed to revise the "extended policy check" cycle detection rules. When, e.g., querying a grandchild, they incorrectly detected a cycle because both the child and grandchild needed to check the policy of the grandparent.

Instead, simplify it to just do a basic runaway calldepth check. There are many other safety mechanisms to make it so this can't ever occur.

(Cycle detection does have existing test coverage, and those tests still pass, it just takes a little longer to detect the cycle internally.)

There is still no way to create subprojects in the UI.

Test Plan: Added and executed unit tests.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10010

Differential Revision: https://secure.phabricator.com/D14862

+255 -55
+2
resources/sql/autopatches/20151223.proj.04.keycol.sql
··· 1 + ALTER TABLE {$NAMESPACE}_project.project 2 + ADD projectPathKey BINARY(4) NOT NULL;
+24
resources/sql/autopatches/20151223.proj.05.updatekeys.php
··· 1 + <?php 2 + 3 + $table = new PhabricatorProject(); 4 + $conn_w = $table->establishConnection('w'); 5 + 6 + foreach (new LiskMigrationIterator($table) as $project) { 7 + $path = $project->getProjectPath(); 8 + $key = $project->getProjectPathKey(); 9 + 10 + if (strlen($path) && ($key !== "\0\0\0\0")) { 11 + continue; 12 + } 13 + 14 + $path_key = PhabricatorHash::digestForIndex($project->getPHID()); 15 + $path_key = substr($path_key, 0, 4); 16 + 17 + queryfx( 18 + $conn_w, 19 + 'UPDATE %T SET projectPath = %s, projectPathKey = %s WHERE id = %d', 20 + $project->getTableName(), 21 + $path_key, 22 + $path_key, 23 + $project->getID()); 24 + }
+2
resources/sql/autopatches/20151223.proj.06.uniq.sql
··· 1 + ALTER TABLE {$NAMESPACE}_project.project 2 + ADD UNIQUE KEY `key_pathkey` (projectPathKey);
+52 -48
src/applications/policy/filter/PhabricatorPolicyFilter.php
··· 243 243 } 244 244 245 245 private function applyExtendedPolicyChecks(array $extended_objects) { 246 - // First, we're going to detect cycles and reject any objects which are 247 - // part of a cycle. We don't want to loop forever if an object has a 248 - // self-referential or nonsense policy. 249 - 250 - static $in_flight = array(); 251 - 252 - $all_phids = array(); 253 - foreach ($extended_objects as $key => $object) { 254 - $phid = $object->getPHID(); 255 - if (isset($in_flight[$phid])) { 256 - // TODO: This could be more user-friendly. 257 - $this->rejectObject($extended_objects[$key], false, '<cycle>'); 258 - unset($extended_objects[$key]); 259 - continue; 260 - } 261 - 262 - // We might throw from rejectObject(), so we don't want to actually mark 263 - // anything as in-flight until we survive this entire step. 264 - $all_phids[$phid] = $phid; 265 - } 266 - 267 - foreach ($all_phids as $phid) { 268 - $in_flight[$phid] = true; 269 - } 270 - 271 - $caught = null; 272 - try { 273 - $extended_objects = $this->executeExtendedPolicyChecks($extended_objects); 274 - } catch (Exception $ex) { 275 - $caught = $ex; 276 - } 277 - 278 - foreach ($all_phids as $phid) { 279 - unset($in_flight[$phid]); 280 - } 281 - 282 - if ($caught) { 283 - throw $caught; 284 - } 285 - 286 - return $extended_objects; 287 - } 288 - 289 - private function executeExtendedPolicyChecks(array $extended_objects) { 290 246 $viewer = $this->viewer; 291 247 $filter_capabilities = $this->capabilities; 292 248 ··· 396 352 } 397 353 398 354 if ($objects_in) { 399 - $objects_out = id(new PhabricatorPolicyFilter()) 400 - ->setViewer($viewer) 401 - ->requireCapabilities($capabilities) 402 - ->apply($objects_in); 355 + $objects_out = $this->executeExtendedPolicyChecks( 356 + $viewer, 357 + $capabilities, 358 + $objects_in, 359 + $key_map); 403 360 $objects_out = mpull($objects_out, null, 'getPHID'); 404 361 } else { 405 362 $objects_out = array(); ··· 433 390 } 434 391 435 392 return $extended_objects; 393 + } 394 + 395 + private function executeExtendedPolicyChecks( 396 + PhabricatorUser $viewer, 397 + array $capabilities, 398 + array $objects, 399 + array $key_map) { 400 + 401 + // Do crude cycle detection by seeing if we have a huge stack depth. 402 + // Although more sophisticated cycle detection is possible in theory, 403 + // it is difficult with hierarchical objects like subprojects. Many other 404 + // checks make it difficult to create cycles normally, so just do a 405 + // simple check here to limit damage. 406 + 407 + static $depth; 408 + 409 + $depth++; 410 + 411 + if ($depth > 32) { 412 + foreach ($objects as $key => $object) { 413 + $this->rejectObject($objects[$key], false, '<cycle>'); 414 + unset($objects[$key]); 415 + continue; 416 + } 417 + } 418 + 419 + if (!$objects) { 420 + return array(); 421 + } 422 + 423 + $caught = null; 424 + try { 425 + $result = id(new PhabricatorPolicyFilter()) 426 + ->setViewer($viewer) 427 + ->requireCapabilities($capabilities) 428 + ->apply($objects); 429 + } catch (Exception $ex) { 430 + $caught = $ex; 431 + } 432 + 433 + $depth--; 434 + 435 + if ($caught) { 436 + throw $caught; 437 + } 438 + 439 + return $result; 436 440 } 437 441 438 442 private function checkCapability(
+74
src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
··· 113 113 $this->assertTrue($caught instanceof Exception); 114 114 } 115 115 116 + public function testAncestryQueries() { 117 + $user = $this->createUser(); 118 + $user->save(); 119 + 120 + $ancestor = $this->createProject($user); 121 + $parent = $this->createProject($user, $ancestor); 122 + $child = $this->createProject($user, $parent); 123 + 124 + $projects = id(new PhabricatorProjectQuery()) 125 + ->setViewer($user) 126 + ->withAncestorProjectPHIDs(array($ancestor->getPHID())) 127 + ->execute(); 128 + $this->assertEqual(2, count($projects)); 129 + 130 + $projects = id(new PhabricatorProjectQuery()) 131 + ->setViewer($user) 132 + ->withParentProjectPHIDs(array($ancestor->getPHID())) 133 + ->execute(); 134 + $this->assertEqual(1, count($projects)); 135 + $this->assertEqual( 136 + $parent->getPHID(), 137 + head($projects)->getPHID()); 138 + 139 + $projects = id(new PhabricatorProjectQuery()) 140 + ->setViewer($user) 141 + ->withAncestorProjectPHIDs(array($ancestor->getPHID())) 142 + ->withDepthBetween(2, null) 143 + ->execute(); 144 + $this->assertEqual(1, count($projects)); 145 + $this->assertEqual( 146 + $child->getPHID(), 147 + head($projects)->getPHID()); 148 + 149 + $parent2 = $this->createProject($user, $ancestor); 150 + $child2 = $this->createProject($user, $parent2); 151 + $grandchild2 = $this->createProject($user, $child2); 152 + 153 + $projects = id(new PhabricatorProjectQuery()) 154 + ->setViewer($user) 155 + ->withAncestorProjectPHIDs(array($ancestor->getPHID())) 156 + ->execute(); 157 + $this->assertEqual(5, count($projects)); 158 + 159 + $projects = id(new PhabricatorProjectQuery()) 160 + ->setViewer($user) 161 + ->withParentProjectPHIDs(array($ancestor->getPHID())) 162 + ->execute(); 163 + $this->assertEqual(2, count($projects)); 164 + 165 + $projects = id(new PhabricatorProjectQuery()) 166 + ->setViewer($user) 167 + ->withAncestorProjectPHIDs(array($ancestor->getPHID())) 168 + ->withDepthBetween(2, null) 169 + ->execute(); 170 + $this->assertEqual(3, count($projects)); 171 + 172 + $projects = id(new PhabricatorProjectQuery()) 173 + ->setViewer($user) 174 + ->withAncestorProjectPHIDs(array($ancestor->getPHID())) 175 + ->withDepthBetween(3, null) 176 + ->execute(); 177 + $this->assertEqual(1, count($projects)); 178 + 179 + $projects = id(new PhabricatorProjectQuery()) 180 + ->setViewer($user) 181 + ->withPHIDs( 182 + array( 183 + $child->getPHID(), 184 + $grandchild2->getPHID(), 185 + )) 186 + ->execute(); 187 + $this->assertEqual(2, count($projects)); 188 + } 189 + 116 190 public function testParentProject() { 117 191 $user = $this->createUser(); 118 192 $user->save();
+85
src/applications/project/query/PhabricatorProjectQuery.php
··· 11 11 private $nameTokens; 12 12 private $icons; 13 13 private $colors; 14 + private $ancestorPHIDs; 15 + private $parentPHIDs; 16 + private $isMilestone; 17 + private $minDepth; 18 + private $maxDepth; 14 19 15 20 private $status = 'status-any'; 16 21 const STATUS_ANY = 'status-any'; ··· 66 71 67 72 public function withColors(array $colors) { 68 73 $this->colors = $colors; 74 + return $this; 75 + } 76 + 77 + public function withParentProjectPHIDs($parent_phids) { 78 + $this->parentPHIDs = $parent_phids; 79 + return $this; 80 + } 81 + 82 + public function withAncestorProjectPHIDs($ancestor_phids) { 83 + $this->ancestorPHIDs = $ancestor_phids; 84 + return $this; 85 + } 86 + 87 + public function withIsMilestone($is_milestone) { 88 + $this->isMilestone = $is_milestone; 89 + return $this; 90 + } 91 + 92 + public function withDepthBetween($min, $max) { 93 + $this->minDepth = $min; 94 + $this->maxDepth = $max; 69 95 return $this; 70 96 } 71 97 ··· 335 361 $conn, 336 362 'color IN (%Ls)', 337 363 $this->colors); 364 + } 365 + 366 + if ($this->parentPHIDs !== null) { 367 + $where[] = qsprintf( 368 + $conn, 369 + 'parentProjectPHID IN (%Ls)', 370 + $this->parentPHIDs); 371 + } 372 + 373 + if ($this->ancestorPHIDs !== null) { 374 + $ancestor_paths = queryfx_all( 375 + $conn, 376 + 'SELECT projectPath, projectDepth FROM %T WHERE phid IN (%Ls)', 377 + id(new PhabricatorProject())->getTableName(), 378 + $this->ancestorPHIDs); 379 + if (!$ancestor_paths) { 380 + throw new PhabricatorEmptyQueryException(); 381 + } 382 + 383 + $sql = array(); 384 + foreach ($ancestor_paths as $ancestor_path) { 385 + $sql[] = qsprintf( 386 + $conn, 387 + '(projectPath LIKE %> AND projectDepth > %d)', 388 + $ancestor_path['projectPath'], 389 + $ancestor_path['projectDepth']); 390 + } 391 + 392 + $where[] = '('.implode(' OR ', $sql).')'; 393 + 394 + $where[] = qsprintf( 395 + $conn, 396 + 'parentProjectPHID IS NOT NULL'); 397 + } 398 + 399 + if ($this->isMilestone !== null) { 400 + if ($this->isMilestone) { 401 + $where[] = qsprintf( 402 + $conn, 403 + 'milestoneNumber IS NOT NULL'); 404 + } else { 405 + $where[] = qsprintf( 406 + $conn, 407 + 'milestoneNumber IS NULL'); 408 + } 409 + } 410 + 411 + if ($this->minDepth !== null) { 412 + $where[] = qsprintf( 413 + $conn, 414 + 'projectDepth >= %d', 415 + $this->minDepth); 416 + } 417 + 418 + if ($this->maxDepth !== null) { 419 + $where[] = qsprintf( 420 + $conn, 421 + 'projectDepth <= %d', 422 + $this->maxDepth); 338 423 } 339 424 340 425 return $where;
+16 -7
src/applications/project/storage/PhabricatorProject.php
··· 33 33 34 34 protected $projectPath; 35 35 protected $projectDepth; 36 + protected $projectPathKey; 36 37 37 38 private $memberPHIDs = self::ATTACHABLE; 38 39 private $watcherPHIDs = self::ATTACHABLE; ··· 196 197 'milestoneNumber' => 'uint32?', 197 198 'projectPath' => 'hashpath64', 198 199 'projectDepth' => 'uint32', 200 + 'projectPathKey' => 'bytes4', 199 201 ), 200 202 self::CONFIG_KEY_SCHEMA => array( 201 203 'key_phid' => null, ··· 223 225 ), 224 226 'key_path' => array( 225 227 'columns' => array('projectPath', 'projectDepth'), 228 + ), 229 + 'key_pathkey' => array( 230 + 'columns' => array('projectPathKey'), 231 + 'unique' => true, 226 232 ), 227 233 ), 228 234 ) + parent::getConfiguration(); ··· 308 314 309 315 if (!strlen($this->getPHID())) { 310 316 $this->setPHID($this->generatePHID()); 317 + } 318 + 319 + if (!strlen($this->getProjectPathKey())) { 320 + $hash = PhabricatorHash::digestForIndex($this->getPHID()); 321 + $hash = substr($hash, 0, 4); 322 + $this->setProjectPathKey($hash); 311 323 } 312 324 313 325 $path = array(); ··· 317 329 $path[] = $parent->getProjectPath(); 318 330 $depth = $parent->getProjectDepth() + 1; 319 331 } 320 - $hash = PhabricatorHash::digestForIndex($this->getPHID()); 321 - $path[] = substr($hash, 0, 4); 322 - 332 + $path[] = $this->getProjectPathKey(); 323 333 $path = implode('', $path); 324 334 325 335 $limit = self::getProjectDepthLimit(); 326 - if (strlen($path) > ($limit * 4)) { 327 - throw new Exception( 328 - pht('Unable to save project: path length is too long.')); 336 + if ($depth >= $limit) { 337 + throw new Exception(pht('Project depth is too great.')); 329 338 } 330 339 331 340 $this->setProjectPath($path); ··· 403 412 $path = $this->getProjectPath(); 404 413 $parent_length = (strlen($path) - 4); 405 414 406 - for ($ii = $parent_length; $ii >= 0; $ii -= 4) { 415 + for ($ii = $parent_length; $ii > 0; $ii -= 4) { 407 416 $parts[] = substr($path, 0, $ii); 408 417 } 409 418