Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / workflows / src / Plugin / WorkflowTypeBase.php
1 <?php
2
3 namespace Drupal\workflows\Plugin;
4
5 use Drupal\Component\Plugin\PluginBase;
6 use Drupal\Core\Plugin\PluginWithFormsTrait;
7 use Drupal\workflows\State;
8 use Drupal\workflows\StateInterface;
9 use Drupal\workflows\Transition;
10 use Drupal\workflows\WorkflowInterface;
11 use Drupal\workflows\WorkflowTypeInterface;
12
13 /**
14  * A base class for Workflow type plugins.
15  *
16  * @see \Drupal\workflows\Annotation\WorkflowType
17  */
18 abstract class WorkflowTypeBase extends PluginBase implements WorkflowTypeInterface {
19
20   use PluginWithFormsTrait;
21
22   /**
23    * A regex for matching a valid state/transition machine name.
24    */
25   const VALID_ID_REGEX = '/[^a-z0-9_]+/';
26
27   /**
28    * {@inheritdoc}
29    */
30   public function __construct(array $configuration, $plugin_id, $plugin_definition) {
31     parent::__construct($configuration, $plugin_id, $plugin_definition);
32     $this->setConfiguration($configuration);
33   }
34
35   /**
36    * {@inheritdoc}
37    */
38   public function label() {
39     $definition = $this->getPluginDefinition();
40     // The label can be an object.
41     // @see \Drupal\Core\StringTranslation\TranslatableMarkup
42     return $definition['label'];
43   }
44
45   /**
46    * {@inheritdoc}
47    */
48   public function workflowHasData(WorkflowInterface $workflow) {
49     return FALSE;
50   }
51
52   /**
53    * {@inheritdoc}
54    */
55   public function workflowStateHasData(WorkflowInterface $workflow, StateInterface $state) {
56     return FALSE;
57   }
58
59   /**
60    * {@inheritdoc}
61    */
62   public function getConfiguration() {
63     return $this->configuration;
64   }
65
66   /**
67    * {@inheritdoc}
68    */
69   public function setConfiguration(array $configuration) {
70     $this->configuration = $configuration + $this->defaultConfiguration();
71   }
72
73   /**
74    * {@inheritdoc}
75    */
76   public function getRequiredStates() {
77     return $this->getPluginDefinition()['required_states'];
78   }
79
80   /**
81    * {@inheritdoc}
82    */
83   public function defaultConfiguration() {
84     return [
85       'states' => [],
86       'transitions' => [],
87     ];
88   }
89
90   /**
91    * {@inheritdoc}
92    */
93   public function calculateDependencies() {
94     return [];
95   }
96
97   /**
98    * {@inheritdoc}
99    */
100   public function onDependencyRemoval(array $dependencies) {
101     return FALSE;
102   }
103
104   /**
105    * {@inheritdoc}
106    */
107   public function getInitialState() {
108     $ordered_states = $this->getStates();
109     return reset($ordered_states);
110   }
111
112   /**
113    * {@inheritdoc}
114    */
115   public function addState($state_id, $label) {
116     if ($this->hasState($state_id)) {
117       throw new \InvalidArgumentException("The state '$state_id' already exists in workflow.");
118     }
119     if (preg_match(static::VALID_ID_REGEX, $state_id)) {
120       throw new \InvalidArgumentException("The state ID '$state_id' must contain only lowercase letters, numbers, and underscores");
121     }
122     $this->configuration['states'][$state_id] = [
123       'label' => $label,
124       'weight' => $this->getNextWeight($this->configuration['states']),
125     ];
126     ksort($this->configuration['states']);
127     return $this;
128   }
129
130   /**
131    * {@inheritdoc}
132    */
133   public function hasState($state_id) {
134     return isset($this->configuration['states'][$state_id]);
135   }
136
137   /**
138    * {@inheritdoc}
139    */
140   public function getStates($state_ids = NULL) {
141     if ($state_ids === NULL) {
142       $state_ids = array_keys($this->configuration['states']);
143     }
144     /** @var \Drupal\workflows\StateInterface[] $states */
145     $states = array_combine($state_ids, array_map([$this, 'getState'], $state_ids));
146     return static::labelWeightMultisort($states);
147   }
148
149   /**
150    * {@inheritdoc}
151    */
152   public function getState($state_id) {
153     if (!isset($this->configuration['states'][$state_id])) {
154       throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.'");
155     }
156     return new State(
157       $this,
158       $state_id,
159       $this->configuration['states'][$state_id]['label'],
160       $this->configuration['states'][$state_id]['weight']
161     );
162   }
163
164   /**
165    * {@inheritdoc}
166    */
167   public function setStateLabel($state_id, $label) {
168     if (!$this->hasState($state_id)) {
169       throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.'");
170     }
171     $this->configuration['states'][$state_id]['label'] = $label;
172     return $this;
173   }
174
175   /**
176    * {@inheritdoc}
177    */
178   public function setStateWeight($state_id, $weight) {
179     if (!$this->hasState($state_id)) {
180       throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.'");
181     }
182     $this->configuration['states'][$state_id]['weight'] = $weight;
183     return $this;
184   }
185
186   /**
187    * {@inheritdoc}
188    */
189   public function deleteState($state_id) {
190     if (!$this->hasState($state_id)) {
191       throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.'");
192     }
193     if (count($this->configuration['states']) === 1) {
194       throw new \InvalidArgumentException("The state '$state_id' can not be deleted from workflow as it is the only state.");
195     }
196
197     foreach ($this->configuration['transitions'] as $transition_id => $transition) {
198       if ($transition['to'] === $state_id) {
199         $this->deleteTransition($transition_id);
200         continue;
201       }
202       $from_key = array_search($state_id, $transition['from'], TRUE);
203       if ($from_key !== FALSE) {
204         // Remove state from the from array.
205         unset($transition['from'][$from_key]);
206         if (empty($transition['from'])) {
207           // There are no more 'from' entries, remove the transition.
208           $this->deleteTransition($transition_id);
209           continue;
210         }
211         // We changed the from state, update the transition.
212         $this->setTransitionFromStates($transition_id, $transition['from']);
213       }
214     }
215     unset($this->configuration['states'][$state_id]);
216     return $this;
217   }
218
219   /**
220    * {@inheritdoc}
221    */
222   public function addTransition($transition_id, $label, array $from_state_ids, $to_state_id) {
223     if ($this->hasTransition($transition_id)) {
224       throw new \InvalidArgumentException("The transition '$transition_id' already exists in workflow.'");
225     }
226     if (preg_match(static::VALID_ID_REGEX, $transition_id)) {
227       throw new \InvalidArgumentException("The transition ID '$transition_id' must contain only lowercase letters, numbers, and underscores.");
228     }
229
230     if (!$this->hasState($to_state_id)) {
231       throw new \InvalidArgumentException("The state '$to_state_id' does not exist in workflow.'");
232     }
233     $this->configuration['transitions'][$transition_id] = [
234       'label' => $label,
235       'from' => [],
236       'to' => $to_state_id,
237       // Always add to the end.
238       'weight' => $this->getNextWeight($this->configuration['transitions']),
239     ];
240
241     try {
242       $this->setTransitionFromStates($transition_id, $from_state_ids);
243     }
244     catch (\InvalidArgumentException $e) {
245       unset($this->configuration['transitions'][$transition_id]);
246       throw $e;
247     }
248
249     ksort($this->configuration['transitions']);
250     return $this;
251   }
252
253   /**
254    * {@inheritdoc}
255    */
256   public function getTransitions(array $transition_ids = NULL) {
257     if ($transition_ids === NULL) {
258       $transition_ids = array_keys($this->configuration['transitions']);
259     }
260     /** @var \Drupal\workflows\TransitionInterface[] $transitions */
261     $transitions = array_combine($transition_ids, array_map([$this, 'getTransition'], $transition_ids));
262     return static::labelWeightMultisort($transitions);
263   }
264
265   /**
266    * Sort states or transitions by weight, label, and key.
267    *
268    * @param \Drupal\workflows\StateInterface[]|\Drupal\workflows\TransitionInterface[] $objects
269    *   An array of state or transition objects to multi-sort, keyed by the
270    *   state or transition ID.
271    *
272    * @return \Drupal\workflows\StateInterface[]|\Drupal\workflows\TransitionInterface[]
273    *   An array of sorted transitions or states, keyed by the state or
274    *   transition ID.
275    */
276   protected static function labelWeightMultisort($objects) {
277     if (count($objects) > 1) {
278       // Separate weights, labels, and keys into arrays.
279       $weights = $labels = [];
280       $keys = array_keys($objects);
281       foreach ($objects as $id => $object) {
282         $weights[$id] = $object->weight();
283         $labels[$id] = $object->label();
284       }
285       // Sort weights, labels, and keys in the same order as each other.
286       array_multisort(
287       // Use the numerical weight as the primary sort.
288         $weights, SORT_NUMERIC, SORT_ASC,
289         // When objects have the same weight, sort them alphabetically by label.
290         $labels, SORT_NATURAL, SORT_ASC,
291         // Ensure that the keys (the object IDs) are sorted in the same order as
292         // the weights.
293         $keys
294       );
295       // Combine keys and weights to make sure the weights are keyed with the
296       // correct keys.
297       $weights = array_combine($keys, $weights);
298       // Return the objects sorted by weight.
299       return array_replace($weights, $objects);
300     }
301     return $objects;
302   }
303
304   /**
305    * {@inheritdoc}
306    */
307   public function getTransition($transition_id) {
308     if (!$this->hasTransition($transition_id)) {
309       throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.'");
310     }
311     return new Transition(
312       $this,
313       $transition_id,
314       $this->configuration['transitions'][$transition_id]['label'],
315       $this->configuration['transitions'][$transition_id]['from'],
316       $this->configuration['transitions'][$transition_id]['to'],
317       $this->configuration['transitions'][$transition_id]['weight']
318     );
319   }
320
321   /**
322    * {@inheritdoc}
323    */
324   public function hasTransition($transition_id) {
325     return isset($this->configuration['transitions'][$transition_id]);
326   }
327
328   /**
329    * {@inheritdoc}
330    */
331   public function getTransitionsForState($state_id, $direction = 'from') {
332     $transition_ids = array_keys(array_filter($this->configuration['transitions'], function ($transition) use ($state_id, $direction) {
333       return in_array($state_id, (array) $transition[$direction], TRUE);
334     }));
335     return $this->getTransitions($transition_ids);
336   }
337
338   /**
339    * {@inheritdoc}
340    */
341   public function getTransitionFromStateToState($from_state_id, $to_state_id) {
342     $transition_id = $this->getTransitionIdFromStateToState($from_state_id, $to_state_id);
343     if (empty($transition_id)) {
344       throw new \InvalidArgumentException("The transition from '$from_state_id' to '$to_state_id' does not exist in workflow.'");
345     }
346     return $this->getTransition($transition_id);
347   }
348
349   /**
350    * {@inheritdoc}
351    */
352   public function hasTransitionFromStateToState($from_state_id, $to_state_id) {
353     return $this->getTransitionIdFromStateToState($from_state_id, $to_state_id) !== NULL;
354   }
355
356   /**
357    * Gets the transition ID from state to state.
358    *
359    * @param string $from_state_id
360    *   The state ID to transition from.
361    * @param string $to_state_id
362    *   The state ID to transition to.
363    *
364    * @return string|null
365    *   The transition ID, or NULL if no transition exists.
366    */
367   protected function getTransitionIdFromStateToState($from_state_id, $to_state_id) {
368     foreach ($this->configuration['transitions'] as $transition_id => $transition) {
369       if (in_array($from_state_id, $transition['from'], TRUE) && $transition['to'] === $to_state_id) {
370         return $transition_id;
371       }
372     }
373     return NULL;
374   }
375
376   /**
377    * {@inheritdoc}
378    */
379   public function setTransitionLabel($transition_id, $label) {
380     if (!$this->hasTransition($transition_id)) {
381       throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
382     }
383     $this->configuration['transitions'][$transition_id]['label'] = $label;
384     return $this;
385   }
386
387   /**
388    * {@inheritdoc}
389    */
390   public function setTransitionWeight($transition_id, $weight) {
391     if (!$this->hasTransition($transition_id)) {
392       throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.'");
393     }
394     $this->configuration['transitions'][$transition_id]['weight'] = $weight;
395     return $this;
396   }
397
398   /**
399    * {@inheritdoc}
400    */
401   public function setTransitionFromStates($transition_id, array $from_state_ids) {
402     if (!$this->hasTransition($transition_id)) {
403       throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
404     }
405
406     // Ensure that the states exist.
407     foreach ($from_state_ids as $from_state_id) {
408       if (!$this->hasState($from_state_id)) {
409         throw new \InvalidArgumentException("The state '$from_state_id' does not exist in workflow.");
410       }
411       if ($this->hasTransitionFromStateToState($from_state_id, $this->configuration['transitions'][$transition_id]['to'])) {
412         $existing_transition_id = $this->getTransitionIdFromStateToState($from_state_id, $this->configuration['transitions'][$transition_id]['to']);
413         if ($transition_id !== $existing_transition_id) {
414           throw new \InvalidArgumentException("The '$existing_transition_id' transition already allows '$from_state_id' to '{$this->configuration['transitions'][$transition_id]['to']}' transitions in workflow.");
415         }
416       }
417     }
418
419     // Preserve the order of the state IDs in the from value and don't save any
420     // keys.
421     $from_state_ids = array_values($from_state_ids);
422     sort($from_state_ids);
423     $this->configuration['transitions'][$transition_id]['from'] = $from_state_ids;
424
425     return $this;
426   }
427
428   /**
429    * {@inheritdoc}
430    */
431   public function deleteTransition($transition_id) {
432     if (!$this->hasTransition($transition_id)) {
433       throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
434     }
435     unset($this->configuration['transitions'][$transition_id]);
436     return $this;
437   }
438
439   /**
440    * Gets the weight for a new state or transition.
441    *
442    * @param array $items
443    *   An array of states or transitions information where each item has a
444    *   'weight' key with a numeric value.
445    *
446    * @return int
447    *   The weight for a new item in the array so that it has the highest weight.
448    */
449   protected function getNextWeight(array $items) {
450     return array_reduce($items, function ($carry, $item) {
451       return max($carry, $item['weight'] + 1);
452     }, 0);
453   }
454
455 }