@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<?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}