@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
at upstream/main 463 lines 14 kB view raw
1<?php 2 3final class PhabricatorProjectTransactionEditor 4 extends PhabricatorApplicationTransactionEditor { 5 6 private $isMilestone; 7 8 private function setIsMilestone($is_milestone) { 9 $this->isMilestone = $is_milestone; 10 return $this; 11 } 12 13 public function getIsMilestone() { 14 return $this->isMilestone; 15 } 16 17 public function getEditorApplicationClass() { 18 return PhabricatorProjectApplication::class; 19 } 20 21 public function getEditorObjectsDescription() { 22 return pht('Projects'); 23 } 24 25 public function getCreateObjectTitle($author, $object) { 26 return pht('%s created this project.', $author); 27 } 28 29 public function getCreateObjectTitleForFeed($author, $object) { 30 return pht('%s created %s.', $author, $object); 31 } 32 33 public function getTransactionTypes() { 34 $types = parent::getTransactionTypes(); 35 36 $types[] = PhabricatorTransactions::TYPE_EDGE; 37 $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; 38 $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; 39 $types[] = PhabricatorTransactions::TYPE_JOIN_POLICY; 40 41 return $types; 42 } 43 44 protected function validateAllTransactions( 45 PhabricatorLiskDAO $object, 46 array $xactions) { 47 48 $errors = array(); 49 50 // Prevent creating projects which are both subprojects and milestones, 51 // since this does not make sense, won't work, and will break everything. 52 $parent_xaction = null; 53 foreach ($xactions as $xaction) { 54 switch ($xaction->getTransactionType()) { 55 case PhabricatorProjectParentTransaction::TRANSACTIONTYPE: 56 case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE: 57 if ($xaction->getNewValue() === null) { 58 continue 2; 59 } 60 61 if (!$parent_xaction) { 62 $parent_xaction = $xaction; 63 continue 2; 64 } 65 66 $errors[] = new PhabricatorApplicationTransactionValidationError( 67 $xaction->getTransactionType(), 68 pht('Invalid'), 69 pht( 70 'When creating a project, specify a maximum of one parent '. 71 'project or milestone project. A project can not be both a '. 72 'subproject and a milestone.'), 73 $xaction); 74 break 2; 75 } 76 } 77 78 $is_milestone = $this->getIsMilestone(); 79 80 $is_parent = $object->getHasSubprojects(); 81 82 foreach ($xactions as $xaction) { 83 switch ($xaction->getTransactionType()) { 84 case PhabricatorTransactions::TYPE_EDGE: 85 $type = $xaction->getMetadataValue('edge:type'); 86 if ($type != PhabricatorProjectProjectHasMemberEdgeType::EDGECONST) { 87 break; 88 } 89 90 if ($is_parent) { 91 $errors[] = new PhabricatorApplicationTransactionValidationError( 92 $xaction->getTransactionType(), 93 pht('Invalid'), 94 pht( 95 'You can not change members of a project with subprojects '. 96 'directly. Members of any subproject are automatically '. 97 'members of the parent project.'), 98 $xaction); 99 } 100 101 if ($is_milestone) { 102 $errors[] = new PhabricatorApplicationTransactionValidationError( 103 $xaction->getTransactionType(), 104 pht('Invalid'), 105 pht( 106 'You can not change members of a milestone. Members of the '. 107 'parent project are automatically members of the milestone.'), 108 $xaction); 109 } 110 break; 111 } 112 } 113 114 return $errors; 115 } 116 117 protected function willPublish(PhabricatorLiskDAO $object, array $xactions) { 118 // NOTE: We're using the omnipotent user here because the original actor 119 // may no longer have permission to view the object. 120 return id(new PhabricatorProjectQuery()) 121 ->setViewer(PhabricatorUser::getOmnipotentUser()) 122 ->withPHIDs(array($object->getPHID())) 123 ->needAncestorMembers(true) 124 ->executeOne(); 125 } 126 127 protected function shouldSendMail( 128 PhabricatorLiskDAO $object, 129 array $xactions) { 130 return true; 131 } 132 133 protected function getMailSubjectPrefix() { 134 return pht('[Project]'); 135 } 136 137 protected function getMailTo(PhabricatorLiskDAO $object) { 138 return array( 139 $this->getActingAsPHID(), 140 ); 141 } 142 143 protected function getMailCc(PhabricatorLiskDAO $object) { 144 return array(); 145 } 146 147 public function getMailTagsMap() { 148 return array( 149 PhabricatorProjectTransaction::MAILTAG_METADATA => 150 pht('Project name, hashtags, icon, image, or color changes.'), 151 PhabricatorProjectTransaction::MAILTAG_MEMBERS => 152 pht('Project membership changes.'), 153 PhabricatorProjectTransaction::MAILTAG_WATCHERS => 154 pht('Project watcher list changes.'), 155 PhabricatorProjectTransaction::MAILTAG_OTHER => 156 pht('Other project activity not listed above occurs.'), 157 ); 158 } 159 160 protected function buildReplyHandler(PhabricatorLiskDAO $object) { 161 return id(new ProjectReplyHandler()) 162 ->setMailReceiver($object); 163 } 164 165 protected function buildMailTemplate(PhabricatorLiskDAO $object) { 166 $name = $object->getName(); 167 168 return id(new PhabricatorMetaMTAMail()) 169 ->setSubject("{$name}"); 170 } 171 172 protected function buildMailBody( 173 PhabricatorLiskDAO $object, 174 array $xactions) { 175 176 $body = parent::buildMailBody($object, $xactions); 177 178 $uri = '/project/profile/'.$object->getID().'/'; 179 $body->addLinkSection( 180 pht('PROJECT DETAIL'), 181 PhabricatorEnv::getProductionURI($uri)); 182 183 return $body; 184 } 185 186 protected function shouldPublishFeedStory( 187 PhabricatorLiskDAO $object, 188 array $xactions) { 189 return true; 190 } 191 192 protected function supportsSearch() { 193 return true; 194 } 195 196 protected function applyFinalEffects( 197 PhabricatorLiskDAO $object, 198 array $xactions) { 199 200 $materialize = false; 201 $new_parent = null; 202 foreach ($xactions as $xaction) { 203 switch ($xaction->getTransactionType()) { 204 case PhabricatorTransactions::TYPE_EDGE: 205 switch ($xaction->getMetadataValue('edge:type')) { 206 case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST: 207 $materialize = true; 208 break; 209 } 210 break; 211 case PhabricatorProjectParentTransaction::TRANSACTIONTYPE: 212 case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE: 213 $materialize = true; 214 $new_parent = $object->getParentProject(); 215 break; 216 } 217 } 218 219 if ($new_parent) { 220 // If we just created the first subproject of this parent, we want to 221 // copy all of the real members to the subproject. 222 if (!$new_parent->getHasSubprojects()) { 223 $member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; 224 225 $project_members = PhabricatorEdgeQuery::loadDestinationPHIDs( 226 $new_parent->getPHID(), 227 $member_type); 228 229 if ($project_members) { 230 $editor = new PhabricatorEdgeEditor(); 231 foreach ($project_members as $phid) { 232 $editor->addEdge($object->getPHID(), $member_type, $phid); 233 } 234 $editor->save(); 235 } 236 } 237 } 238 239 // TODO: We should dump an informational transaction onto the parent 240 // project to show that we created the sub-thing. 241 242 if ($materialize) { 243 id(new PhabricatorProjectsMembershipIndexEngineExtension()) 244 ->rematerialize($object); 245 } 246 247 if ($new_parent) { 248 id(new PhabricatorProjectsMembershipIndexEngineExtension()) 249 ->rematerialize($new_parent); 250 } 251 252 // See PHI1046. Milestones are always in the Space of their parent project. 253 // Synchronize the database values to match the application values. 254 $conn = $object->establishConnection('w'); 255 queryfx( 256 $conn, 257 'UPDATE %R SET spacePHID = %ns 258 WHERE parentProjectPHID = %s AND milestoneNumber IS NOT NULL', 259 $object, 260 $object->getSpacePHID(), 261 $object->getPHID()); 262 263 return parent::applyFinalEffects($object, $xactions); 264 } 265 266 public function addSlug(PhabricatorProject $project, $slug, $force) { 267 $slug = PhabricatorSlug::normalizeProjectSlug($slug); 268 $table = new PhabricatorProjectSlug(); 269 $project_phid = $project->getPHID(); 270 271 if ($force) { 272 // If we have the `$force` flag set, we only want to ignore an existing 273 // slug if it's for the same project. We'll error on collisions with 274 // other projects. 275 $current = $table->loadOneWhere( 276 'slug = %s AND projectPHID = %s', 277 $slug, 278 $project_phid); 279 } else { 280 // Without the `$force` flag, we'll just return without doing anything 281 // if any other project already has the slug. 282 $current = $table->loadOneWhere( 283 'slug = %s', 284 $slug); 285 } 286 287 if ($current) { 288 return; 289 } 290 291 return id(new PhabricatorProjectSlug()) 292 ->setSlug($slug) 293 ->setProjectPHID($project_phid) 294 ->save(); 295 } 296 297 public function removeSlugs(PhabricatorProject $project, array $slugs) { 298 // Do not allow removing the project's primary slug which the edit form 299 // may allow through a series of renames/moves. See T15636 300 if (($key = array_search($project->getPrimarySlug(), $slugs)) !== false) { 301 unset($slugs[$key]); 302 } 303 304 if (!$slugs) { 305 return; 306 } 307 308 // We're going to try to delete both the literal and normalized versions 309 // of all slugs. This allows us to destroy old slugs that are no longer 310 // valid. 311 foreach ($this->normalizeSlugs($slugs) as $slug) { 312 $slugs[] = $slug; 313 } 314 315 $objects = id(new PhabricatorProjectSlug())->loadAllWhere( 316 'projectPHID = %s AND slug IN (%Ls)', 317 $project->getPHID(), 318 $slugs); 319 320 foreach ($objects as $object) { 321 $object->delete(); 322 } 323 } 324 325 public function normalizeSlugs(array $slugs) { 326 foreach ($slugs as $key => $slug) { 327 $slugs[$key] = PhabricatorSlug::normalizeProjectSlug($slug); 328 } 329 330 $slugs = array_unique($slugs); 331 $slugs = array_values($slugs); 332 333 return $slugs; 334 } 335 336 protected function adjustObjectForPolicyChecks( 337 PhabricatorLiskDAO $object, 338 array $xactions) { 339 340 $copy = parent::adjustObjectForPolicyChecks($object, $xactions); 341 342 $type_edge = PhabricatorTransactions::TYPE_EDGE; 343 $edgetype_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; 344 345 // See T13462. If we're creating a milestone, set a dummy milestone 346 // number so the project behaves like a milestone and uses milestone 347 // policy rules. Otherwise, we'll end up checking the default policies 348 // (which are not relevant to milestones) instead of the parent project 349 // policies (which are the correct policies). 350 if ($this->getIsMilestone() && !$copy->isMilestone()) { 351 $copy->setMilestoneNumber(1); 352 } 353 354 $hint = null; 355 if ($this->getIsMilestone()) { 356 // See T13462. If we're creating a milestone, predict that the members 357 // of the newly created milestone will be the same as the members of the 358 // parent project, since this is the governing rule. 359 360 $parent = $copy->getParentProject(); 361 362 $parent = id(new PhabricatorProjectQuery()) 363 ->setViewer($this->getActor()) 364 ->withPHIDs(array($parent->getPHID())) 365 ->needMembers(true) 366 ->executeOne(); 367 $members = $parent->getMemberPHIDs(); 368 369 $hint = array_fuse($members); 370 } else { 371 $member_xaction = null; 372 foreach ($xactions as $xaction) { 373 if ($xaction->getTransactionType() !== $type_edge) { 374 continue; 375 } 376 377 $edgetype = $xaction->getMetadataValue('edge:type'); 378 if ($edgetype !== $edgetype_member) { 379 continue; 380 } 381 382 $member_xaction = $xaction; 383 } 384 385 if ($member_xaction) { 386 $object_phid = $object->getPHID(); 387 388 if ($object_phid) { 389 $project = id(new PhabricatorProjectQuery()) 390 ->setViewer($this->getActor()) 391 ->withPHIDs(array($object_phid)) 392 ->needMembers(true) 393 ->executeOne(); 394 $members = $project->getMemberPHIDs(); 395 } else { 396 $members = array(); 397 } 398 399 $clone_xaction = clone $member_xaction; 400 $hint = $this->getPHIDTransactionNewValue($clone_xaction, $members); 401 $hint = array_fuse($hint); 402 } 403 } 404 405 if ($hint !== null) { 406 $rule = new PhabricatorProjectMembersPolicyRule(); 407 PhabricatorPolicyRule::passTransactionHintToRule( 408 $copy, 409 $rule, 410 $hint); 411 } 412 413 return $copy; 414 } 415 416 protected function expandTransactions( 417 PhabricatorLiskDAO $object, 418 array $xactions) { 419 420 $actor = $this->getActor(); 421 $actor_phid = $actor->getPHID(); 422 423 $results = parent::expandTransactions($object, $xactions); 424 425 $is_milestone = $object->isMilestone(); 426 foreach ($xactions as $xaction) { 427 switch ($xaction->getTransactionType()) { 428 case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE: 429 if ($xaction->getNewValue() !== null) { 430 $is_milestone = true; 431 } 432 break; 433 } 434 } 435 436 $this->setIsMilestone($is_milestone); 437 438 return $results; 439 } 440 441 protected function shouldApplyHeraldRules( 442 PhabricatorLiskDAO $object, 443 array $xactions) { 444 return true; 445 } 446 447 protected function buildHeraldAdapter( 448 PhabricatorLiskDAO $object, 449 array $xactions) { 450 451 // Herald rules may run on behalf of other users and need to execute 452 // membership checks against ancestors. 453 $project = id(new PhabricatorProjectQuery()) 454 ->setViewer(PhabricatorUser::getOmnipotentUser()) 455 ->withPHIDs(array($object->getPHID())) 456 ->needAncestorMembers(true) 457 ->executeOne(); 458 459 return id(new PhabricatorProjectHeraldAdapter()) 460 ->setProject($project); 461 } 462 463}