Backup of db before drupal security update
[yaffs-website] / web / core / modules / taxonomy / src / Form / OverviewTerms.php
1 <?php
2
3 namespace Drupal\taxonomy\Form;
4
5 use Drupal\Core\Entity\EntityManagerInterface;
6 use Drupal\Core\Form\FormBase;
7 use Drupal\Core\Extension\ModuleHandlerInterface;
8 use Drupal\Core\Form\FormStateInterface;
9 use Drupal\taxonomy\VocabularyInterface;
10 use Symfony\Component\DependencyInjection\ContainerInterface;
11
12 /**
13  * Provides terms overview form for a taxonomy vocabulary.
14  */
15 class OverviewTerms extends FormBase {
16
17   /**
18    * The module handler service.
19    *
20    * @var \Drupal\Core\Extension\ModuleHandlerInterface
21    */
22   protected $moduleHandler;
23
24   /**
25    * The entity manager.
26    *
27    * @var \Drupal\Core\Entity\EntityManagerInterface
28    */
29   protected $entityManager;
30
31   /**
32    * The term storage handler.
33    *
34    * @var \Drupal\taxonomy\TermStorageInterface
35    */
36   protected $storageController;
37
38   /**
39    * Constructs an OverviewTerms object.
40    *
41    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
42    *   The module handler service.
43    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
44    *   The entity manager service.
45    */
46   public function __construct(ModuleHandlerInterface $module_handler, EntityManagerInterface $entity_manager) {
47     $this->moduleHandler = $module_handler;
48     $this->entityManager = $entity_manager;
49     $this->storageController = $entity_manager->getStorage('taxonomy_term');
50   }
51
52   /**
53    * {@inheritdoc}
54    */
55   public static function create(ContainerInterface $container) {
56     return new static(
57       $container->get('module_handler'),
58       $container->get('entity.manager')
59     );
60   }
61
62   /**
63    * {@inheritdoc}
64    */
65   public function getFormId() {
66     return 'taxonomy_overview_terms';
67   }
68
69   /**
70    * Form constructor.
71    *
72    * Display a tree of all the terms in a vocabulary, with options to edit
73    * each one. The form is made drag and drop by the theme function.
74    *
75    * @param array $form
76    *   An associative array containing the structure of the form.
77    * @param \Drupal\Core\Form\FormStateInterface $form_state
78    *   The current state of the form.
79    * @param \Drupal\taxonomy\VocabularyInterface $taxonomy_vocabulary
80    *   The vocabulary to display the overview form for.
81    *
82    * @return array
83    *   The form structure.
84    */
85   public function buildForm(array $form, FormStateInterface $form_state, VocabularyInterface $taxonomy_vocabulary = NULL) {
86     // @todo Remove global variables when https://www.drupal.org/node/2044435 is
87     //   in.
88     global $pager_page_array, $pager_total, $pager_total_items;
89
90     $form_state->set(['taxonomy', 'vocabulary'], $taxonomy_vocabulary);
91     $parent_fields = FALSE;
92
93     $page = $this->getRequest()->query->get('page') ?: 0;
94     // Number of terms per page.
95     $page_increment = $this->config('taxonomy.settings')->get('terms_per_page_admin');
96     // Elements shown on this page.
97     $page_entries = 0;
98     // Elements at the root level before this page.
99     $before_entries = 0;
100     // Elements at the root level after this page.
101     $after_entries = 0;
102     // Elements at the root level on this page.
103     $root_entries = 0;
104
105     // Terms from previous and next pages are shown if the term tree would have
106     // been cut in the middle. Keep track of how many extra terms we show on
107     // each page of terms.
108     $back_step = NULL;
109     $forward_step = 0;
110
111     // An array of the terms to be displayed on this page.
112     $current_page = [];
113
114     $delta = 0;
115     $term_deltas = [];
116     $tree = $this->storageController->loadTree($taxonomy_vocabulary->id(), 0, NULL, TRUE);
117     $tree_index = 0;
118     do {
119       // In case this tree is completely empty.
120       if (empty($tree[$tree_index])) {
121         break;
122       }
123       $delta++;
124       // Count entries before the current page.
125       if ($page && ($page * $page_increment) > $before_entries && !isset($back_step)) {
126         $before_entries++;
127         continue;
128       }
129       // Count entries after the current page.
130       elseif ($page_entries > $page_increment && isset($complete_tree)) {
131         $after_entries++;
132         continue;
133       }
134
135       // Do not let a term start the page that is not at the root.
136       $term = $tree[$tree_index];
137       if (isset($term->depth) && ($term->depth > 0) && !isset($back_step)) {
138         $back_step = 0;
139         while ($pterm = $tree[--$tree_index]) {
140           $before_entries--;
141           $back_step++;
142           if ($pterm->depth == 0) {
143             $tree_index--;
144             // Jump back to the start of the root level parent.
145             continue 2;
146           }
147         }
148       }
149       $back_step = isset($back_step) ? $back_step : 0;
150
151       // Continue rendering the tree until we reach the a new root item.
152       if ($page_entries >= $page_increment + $back_step + 1 && $term->depth == 0 && $root_entries > 1) {
153         $complete_tree = TRUE;
154         // This new item at the root level is the first item on the next page.
155         $after_entries++;
156         continue;
157       }
158       if ($page_entries >= $page_increment + $back_step) {
159         $forward_step++;
160       }
161
162       // Finally, if we've gotten down this far, we're rendering a term on this
163       // page.
164       $page_entries++;
165       $term_deltas[$term->id()] = isset($term_deltas[$term->id()]) ? $term_deltas[$term->id()] + 1 : 0;
166       $key = 'tid:' . $term->id() . ':' . $term_deltas[$term->id()];
167
168       // Keep track of the first term displayed on this page.
169       if ($page_entries == 1) {
170         $form['#first_tid'] = $term->id();
171       }
172       // Keep a variable to make sure at least 2 root elements are displayed.
173       if ($term->parents[0] == 0) {
174         $root_entries++;
175       }
176       $current_page[$key] = $term;
177     } while (isset($tree[++$tree_index]));
178
179     // Because we didn't use a pager query, set the necessary pager variables.
180     $total_entries = $before_entries + $page_entries + $after_entries;
181     $pager_total_items[0] = $total_entries;
182     $pager_page_array[0] = $page;
183     $pager_total[0] = ceil($total_entries / $page_increment);
184
185     // If this form was already submitted once, it's probably hit a validation
186     // error. Ensure the form is rebuilt in the same order as the user
187     // submitted.
188     $user_input = $form_state->getUserInput();
189     if (!empty($user_input)) {
190       // Get the POST order.
191       $order = array_flip(array_keys($user_input['terms']));
192       // Update our form with the new order.
193       $current_page = array_merge($order, $current_page);
194       foreach ($current_page as $key => $term) {
195         // Verify this is a term for the current page and set at the current
196         // depth.
197         if (is_array($user_input['terms'][$key]) && is_numeric($user_input['terms'][$key]['term']['tid'])) {
198           $current_page[$key]->depth = $user_input['terms'][$key]['term']['depth'];
199         }
200         else {
201           unset($current_page[$key]);
202         }
203       }
204     }
205
206     $errors = $form_state->getErrors();
207     $destination = $this->getDestinationArray();
208     $row_position = 0;
209     // Build the actual form.
210     $form['terms'] = [
211       '#type' => 'table',
212       '#header' => [$this->t('Name'), $this->t('Weight'), $this->t('Operations')],
213       '#empty' => $this->t('No terms available. <a href=":link">Add term</a>.', [':link' => $this->url('entity.taxonomy_term.add_form', ['taxonomy_vocabulary' => $taxonomy_vocabulary->id()])]),
214       '#attributes' => [
215         'id' => 'taxonomy',
216       ],
217     ];
218     foreach ($current_page as $key => $term) {
219       /** @var $term \Drupal\Core\Entity\EntityInterface */
220       $term = $this->entityManager->getTranslationFromContext($term);
221       $form['terms'][$key]['#term'] = $term;
222       $indentation = [];
223       if (isset($term->depth) && $term->depth > 0) {
224         $indentation = [
225           '#theme' => 'indentation',
226           '#size' => $term->depth,
227         ];
228       }
229       $form['terms'][$key]['term'] = [
230         '#prefix' => !empty($indentation) ? drupal_render($indentation) : '',
231         '#type' => 'link',
232         '#title' => $term->getName(),
233         '#url' => $term->urlInfo(),
234       ];
235       if ($taxonomy_vocabulary->getHierarchy() != VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) {
236         $parent_fields = TRUE;
237         $form['terms'][$key]['term']['tid'] = [
238           '#type' => 'hidden',
239           '#value' => $term->id(),
240           '#attributes' => [
241             'class' => ['term-id'],
242           ],
243         ];
244         $form['terms'][$key]['term']['parent'] = [
245           '#type' => 'hidden',
246           // Yes, default_value on a hidden. It needs to be changeable by the
247           // javascript.
248           '#default_value' => $term->parents[0],
249           '#attributes' => [
250             'class' => ['term-parent'],
251           ],
252         ];
253         $form['terms'][$key]['term']['depth'] = [
254           '#type' => 'hidden',
255           // Same as above, the depth is modified by javascript, so it's a
256           // default_value.
257           '#default_value' => $term->depth,
258           '#attributes' => [
259             'class' => ['term-depth'],
260           ],
261         ];
262       }
263       $form['terms'][$key]['weight'] = [
264         '#type' => 'weight',
265         '#delta' => $delta,
266         '#title' => $this->t('Weight for added term'),
267         '#title_display' => 'invisible',
268         '#default_value' => $term->getWeight(),
269         '#attributes' => [
270           'class' => ['term-weight'],
271         ],
272       ];
273       $operations = [
274         'edit' => [
275           'title' => $this->t('Edit'),
276           'query' => $destination,
277           'url' => $term->urlInfo('edit-form'),
278         ],
279         'delete' => [
280           'title' => $this->t('Delete'),
281           'query' => $destination,
282           'url' => $term->urlInfo('delete-form'),
283         ],
284       ];
285       if ($this->moduleHandler->moduleExists('content_translation') && content_translation_translate_access($term)->isAllowed()) {
286         $operations['translate'] = [
287           'title' => $this->t('Translate'),
288           'query' => $destination,
289           'url' => $term->urlInfo('drupal:content-translation-overview'),
290         ];
291       }
292       $form['terms'][$key]['operations'] = [
293         '#type' => 'operations',
294         '#links' => $operations,
295       ];
296
297       $form['terms'][$key]['#attributes']['class'] = [];
298       if ($parent_fields) {
299         $form['terms'][$key]['#attributes']['class'][] = 'draggable';
300       }
301
302       // Add classes that mark which terms belong to previous and next pages.
303       if ($row_position < $back_step || $row_position >= $page_entries - $forward_step) {
304         $form['terms'][$key]['#attributes']['class'][] = 'taxonomy-term-preview';
305       }
306
307       if ($row_position !== 0 && $row_position !== count($tree) - 1) {
308         if ($row_position == $back_step - 1 || $row_position == $page_entries - $forward_step - 1) {
309           $form['terms'][$key]['#attributes']['class'][] = 'taxonomy-term-divider-top';
310         }
311         elseif ($row_position == $back_step || $row_position == $page_entries - $forward_step) {
312           $form['terms'][$key]['#attributes']['class'][] = 'taxonomy-term-divider-bottom';
313         }
314       }
315
316       // Add an error class if this row contains a form error.
317       foreach ($errors as $error_key => $error) {
318         if (strpos($error_key, $key) === 0) {
319           $form['terms'][$key]['#attributes']['class'][] = 'error';
320         }
321       }
322       $row_position++;
323     }
324
325     if ($parent_fields) {
326       $form['terms']['#tabledrag'][] = [
327         'action' => 'match',
328         'relationship' => 'parent',
329         'group' => 'term-parent',
330         'subgroup' => 'term-parent',
331         'source' => 'term-id',
332         'hidden' => FALSE,
333       ];
334       $form['terms']['#tabledrag'][] = [
335         'action' => 'depth',
336         'relationship' => 'group',
337         'group' => 'term-depth',
338         'hidden' => FALSE,
339       ];
340       $form['terms']['#attached']['library'][] = 'taxonomy/drupal.taxonomy';
341       $form['terms']['#attached']['drupalSettings']['taxonomy'] = [
342         'backStep' => $back_step,
343         'forwardStep' => $forward_step,
344       ];
345     }
346     $form['terms']['#tabledrag'][] = [
347       'action' => 'order',
348       'relationship' => 'sibling',
349       'group' => 'term-weight',
350     ];
351
352     if ($taxonomy_vocabulary->getHierarchy() != VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) {
353       $form['actions'] = ['#type' => 'actions', '#tree' => FALSE];
354       $form['actions']['submit'] = [
355         '#type' => 'submit',
356         '#value' => $this->t('Save'),
357         '#button_type' => 'primary',
358       ];
359       $form['actions']['reset_alphabetical'] = [
360         '#type' => 'submit',
361         '#submit' => ['::submitReset'],
362         '#value' => $this->t('Reset to alphabetical'),
363       ];
364     }
365
366     $form['pager_pager'] = ['#type' => 'pager'];
367     return $form;
368   }
369
370   /**
371    * Form submission handler.
372    *
373    * Rather than using a textfield or weight field, this form depends entirely
374    * upon the order of form elements on the page to determine new weights.
375    *
376    * Because there might be hundreds or thousands of taxonomy terms that need to
377    * be ordered, terms are weighted from 0 to the number of terms in the
378    * vocabulary, rather than the standard -10 to 10 scale. Numbers are sorted
379    * lowest to highest, but are not necessarily sequential. Numbers may be
380    * skipped when a term has children so that reordering is minimal when a child
381    * is added or removed from a term.
382    *
383    * @param array $form
384    *   An associative array containing the structure of the form.
385    * @param \Drupal\Core\Form\FormStateInterface $form_state
386    *   The current state of the form.
387    */
388   public function submitForm(array &$form, FormStateInterface $form_state) {
389     // Sort term order based on weight.
390     uasort($form_state->getValue('terms'), ['Drupal\Component\Utility\SortArray', 'sortByWeightElement']);
391
392     $vocabulary = $form_state->get(['taxonomy', 'vocabulary']);
393     // Update the current hierarchy type as we go.
394     $hierarchy = VocabularyInterface::HIERARCHY_DISABLED;
395
396     $changed_terms = [];
397     $tree = $this->storageController->loadTree($vocabulary->id(), 0, NULL, TRUE);
398
399     if (empty($tree)) {
400       return;
401     }
402
403     // Build a list of all terms that need to be updated on previous pages.
404     $weight = 0;
405     $term = $tree[0];
406     while ($term->id() != $form['#first_tid']) {
407       if ($term->parents[0] == 0 && $term->getWeight() != $weight) {
408         $term->setWeight($weight);
409         $changed_terms[$term->id()] = $term;
410       }
411       $weight++;
412       $hierarchy = $term->parents[0] != 0 ? VocabularyInterface::HIERARCHY_SINGLE : $hierarchy;
413       $term = $tree[$weight];
414     }
415
416     // Renumber the current page weights and assign any new parents.
417     $level_weights = [];
418     foreach ($form_state->getValue('terms') as $tid => $values) {
419       if (isset($form['terms'][$tid]['#term'])) {
420         $term = $form['terms'][$tid]['#term'];
421         // Give terms at the root level a weight in sequence with terms on previous pages.
422         if ($values['term']['parent'] == 0 && $term->getWeight() != $weight) {
423           $term->setWeight($weight);
424           $changed_terms[$term->id()] = $term;
425         }
426         // Terms not at the root level can safely start from 0 because they're all on this page.
427         elseif ($values['term']['parent'] > 0) {
428           $level_weights[$values['term']['parent']] = isset($level_weights[$values['term']['parent']]) ? $level_weights[$values['term']['parent']] + 1 : 0;
429           if ($level_weights[$values['term']['parent']] != $term->getWeight()) {
430             $term->setWeight($level_weights[$values['term']['parent']]);
431             $changed_terms[$term->id()] = $term;
432           }
433         }
434         // Update any changed parents.
435         if ($values['term']['parent'] != $term->parents[0]) {
436           $term->parent->target_id = $values['term']['parent'];
437           $changed_terms[$term->id()] = $term;
438         }
439         $hierarchy = $term->parents[0] != 0 ? VocabularyInterface::HIERARCHY_SINGLE : $hierarchy;
440         $weight++;
441       }
442     }
443
444     // Build a list of all terms that need to be updated on following pages.
445     for ($weight; $weight < count($tree); $weight++) {
446       $term = $tree[$weight];
447       if ($term->parents[0] == 0 && $term->getWeight() != $weight) {
448         $term->parent->target_id = $term->parents[0];
449         $term->setWeight($weight);
450         $changed_terms[$term->id()] = $term;
451       }
452       $hierarchy = $term->parents[0] != 0 ? VocabularyInterface::HIERARCHY_SINGLE : $hierarchy;
453     }
454
455     // Save all updated terms.
456     foreach ($changed_terms as $term) {
457       $term->save();
458     }
459
460     // Update the vocabulary hierarchy to flat or single hierarchy.
461     if ($vocabulary->getHierarchy() != $hierarchy) {
462       $vocabulary->setHierarchy($hierarchy);
463       $vocabulary->save();
464     }
465     drupal_set_message($this->t('The configuration options have been saved.'));
466   }
467
468   /**
469    * Redirects to confirmation form for the reset action.
470    */
471   public function submitReset(array &$form, FormStateInterface $form_state) {
472     /** @var $vocabulary \Drupal\taxonomy\VocabularyInterface */
473     $vocabulary = $form_state->get(['taxonomy', 'vocabulary']);
474     $form_state->setRedirectUrl($vocabulary->urlInfo('reset-form'));
475   }
476
477 }