1 <?php
  2 
  3 /**
  4  * Funktionensammlung für die Strukturverwaltung.
  5  *
  6  * @package redaxo\structure
  7  */
  8 class rex_category_service
  9 {
 10     /**
 11      * Erstellt eine neue Kategorie.
 12      *
 13      * @param int   $category_id KategorieId in der die neue Kategorie erstellt werden soll
 14      * @param array $data        Array mit den Daten der Kategorie
 15      *
 16      * @throws rex_api_exception
 17      *
 18      * @return string Eine Statusmeldung
 19      */
 20     public static function addCategory($category_id, array $data)
 21     {
 22         $message = '';
 23 
 24         if (!is_array($data)) {
 25             throw  new rex_api_exception('Expecting $data to be an array!');
 26         }
 27 
 28         self::reqKey($data, 'catpriority');
 29         self::reqKey($data, 'catname');
 30 
 31         // parent may be null, when adding in the root cat
 32         $parent = rex_category::get($category_id);
 33         if ($parent) {
 34             $path = $parent->getPath();
 35             $path .= $parent->getId() . '|';
 36         } else {
 37             $path = '|';
 38         }
 39 
 40         if ($data['catpriority'] <= 0) {
 41             $data['catpriority'] = 1;
 42         }
 43 
 44         if (!isset($data['name'])) {
 45             $data['name'] = $data['catname'];
 46         }
 47 
 48         if (!isset($data['status'])) {
 49             $data['status'] = 0;
 50         }
 51 
 52         $contentAvailable = rex_plugin::get('structure', 'content')->isAvailable();
 53         if ($contentAvailable) {
 54             $startpageTemplates = [];
 55             if ($category_id != '') {
 56                 // TemplateId vom Startartikel der jeweiligen Sprache vererben
 57                 $sql = rex_sql::factory();
 58                 // $sql->setDebug();
 59                 $sql->setQuery('select clang_id,template_id from ' . rex::getTablePrefix() . 'article where id=? and startarticle=1', [$category_id]);
 60                 for ($i = 0; $i < $sql->getRows(); $i++, $sql->next()) {
 61                     $startpageTemplates[$sql->getValue('clang_id')] = $sql->getValue('template_id');
 62                 }
 63             }
 64 
 65             // Alle Templates der Kategorie
 66             $templates = rex_template::getTemplatesForCategory($category_id);
 67         }
 68 
 69         $user = self::getUser();
 70 
 71         // Kategorie in allen Sprachen anlegen
 72         $AART = rex_sql::factory();
 73         foreach (rex_clang::getAllIds() as $key) {
 74             if ($contentAvailable) {
 75                 $template_id = rex_template::getDefaultId();
 76                 if (isset($startpageTemplates[$key]) && $startpageTemplates[$key] != '') {
 77                     $template_id = $startpageTemplates[$key];
 78                 }
 79 
 80                 // Wenn Template nicht vorhanden, dann entweder erlaubtes nehmen
 81                 // oder leer setzen.
 82                 if (!isset($templates[$template_id])) {
 83                     $template_id = 0;
 84                     if (count($templates) > 0) {
 85                         $template_id = key($templates);
 86                     }
 87                 }
 88             }
 89 
 90             $AART->setTable(rex::getTablePrefix() . 'article');
 91             if (!isset($id)) {
 92                 $id = $AART->setNewId('id');
 93             } else {
 94                 $AART->setValue('id', $id);
 95             }
 96 
 97             $AART->setValue('clang_id', $key);
 98             $AART->setValue('template_id', $template_id);
 99             $AART->setValue('name', $data['name']);
100             $AART->setValue('catname', $data['catname']);
101             $AART->setValue('catpriority', $data['catpriority']);
102             $AART->setValue('parent_id', $category_id);
103             $AART->setValue('priority', 1);
104             $AART->setValue('path', $path);
105             $AART->setValue('startarticle', 1);
106             $AART->setValue('status', $data['status']);
107             $AART->addGlobalUpdateFields($user);
108             $AART->addGlobalCreateFields($user);
109 
110             try {
111                 $AART->insert();
112 
113                 // ----- PRIOR
114                 if (isset($data['catpriority'])) {
115                     self::newCatPrio($category_id, $key, 0, $data['catpriority']);
116                 }
117 
118                 $message = rex_i18n::msg('category_added_and_startarticle_created');
119 
120                 rex_article_cache::delete($id, $key);
121 
122                 // ----- EXTENSION POINT
123                 // Objekte clonen, damit diese nicht von der extension veraendert werden koennen
124                 $message = rex_extension::registerPoint(new rex_extension_point('CAT_ADDED', $message, [
125                     'category' => clone $AART,
126                     'id' => $id,
127                     'parent_id' => $category_id,
128                     'clang' => $key,
129                     'name' => $data['catname'],
130                     'priority' => $data['catpriority'],
131                     'path' => $path,
132                     'status' => $data['status'],
133                     'article' => clone $AART,
134                     'data' => $data,
135                 ]));
136             } catch (rex_sql_exception $e) {
137                 throw new rex_api_exception($e);
138             }
139         }
140 
141         return $message;
142     }
143 
144     /**
145      * Bearbeitet einer Kategorie.
146      *
147      * @param int   $category_id Id der Kategorie die verändert werden soll
148      * @param int   $clang       Id der Sprache
149      * @param array $data        Array mit den Daten der Kategorie
150      *
151      * @throws rex_api_exception
152      *
153      * @return string Eine Statusmeldung
154      */
155     public static function editCategory($category_id, $clang, array $data)
156     {
157         if (!is_array($data)) {
158             throw  new rex_api_exception('Expecting $data to be an array!');
159         }
160 
161         // --- Kategorie mit alten Daten selektieren
162         $thisCat = rex_sql::factory();
163         $thisCat->setQuery('SELECT * FROM ' . rex::getTablePrefix() . 'article WHERE startarticle=1 and id=? and clang_id=?', [$category_id, $clang]);
164 
165         // --- Kategorie selbst updaten
166         $EKAT = rex_sql::factory();
167         $EKAT->setTable(rex::getTablePrefix() . 'article');
168         $EKAT->setWhere(['id' => $category_id, 'startarticle' => 1, 'clang_id' => $clang]);
169 
170         if (isset($data['catname'])) {
171             $EKAT->setValue('catname', $data['catname']);
172         }
173         if (isset($data['catpriority'])) {
174             $EKAT->setValue('catpriority', $data['catpriority']);
175         }
176 
177         $user = self::getUser();
178 
179         $EKAT->addGlobalUpdateFields($user);
180 
181         try {
182             $EKAT->update();
183 
184             // --- Kategorie Kindelemente updaten
185             if (isset($data['catname'])) {
186                 $ArtSql = rex_sql::factory();
187                 $ArtSql->setQuery('SELECT id FROM ' . rex::getTablePrefix() . 'article WHERE parent_id=? AND startarticle=0 AND clang_id=?', [$category_id, $clang]);
188 
189                 $EART = rex_sql::factory();
190                 for ($i = 0; $i < $ArtSql->getRows(); ++$i) {
191                     $EART->setTable(rex::getTablePrefix() . 'article');
192                     $EART->setWhere(['id' => $ArtSql->getValue('id'), 'startarticle' => '0', 'clang_id' => $clang]);
193                     $EART->setValue('catname', $data['catname']);
194                     $EART->addGlobalUpdateFields($user);
195 
196                     $EART->update();
197                     rex_article_cache::delete($ArtSql->getValue('id'), $clang);
198 
199                     $ArtSql->next();
200                 }
201             }
202 
203             // ----- PRIOR
204             if (isset($data['catpriority'])) {
205                 $parent_id = $thisCat->getValue('parent_id');
206                 $old_prio = $thisCat->getValue('catpriority');
207 
208                 if ($data['catpriority'] <= 0) {
209                     $data['catpriority'] = 1;
210                 }
211 
212                 rex_sql::factory()
213                     ->setTable(rex::getTable('article'))
214                     ->setWhere('id = :id AND clang_id != :clang', ['id' => $category_id, 'clang' => $clang])
215                     ->setValue('catpriority', $data['catpriority'])
216                     ->addGlobalUpdateFields($user)
217                     ->update();
218 
219                 foreach (rex_clang::getAllIds() as $clangId) {
220                     self::newCatPrio($parent_id, $clangId, $data['catpriority'], $old_prio);
221                 }
222             }
223 
224             $message = rex_i18n::msg('category_updated');
225 
226             rex_article_cache::delete($category_id);
227 
228             // ----- EXTENSION POINT
229             // Objekte clonen, damit diese nicht von der extension veraendert werden koennen
230             $message = rex_extension::registerPoint(new rex_extension_point('CAT_UPDATED', $message, [
231                 'id' => $category_id,
232 
233                 'category' => clone $EKAT,
234                 'category_old' => clone $thisCat,
235                 'article' => clone $EKAT,
236 
237                 'parent_id' => $thisCat->getValue('parent_id'),
238                 'clang' => $clang,
239                 'name' => isset($data['catname']) ? $data['catname'] : $thisCat->getValue('catname'),
240                 'priority' => isset($data['catpriority']) ? $data['catpriority'] : $thisCat->getValue('catpriority'),
241                 'path' => $thisCat->getValue('path'),
242                 'status' => $thisCat->getValue('status'),
243 
244                 'data' => $data,
245             ]));
246         } catch (rex_sql_exception $e) {
247             throw new rex_api_exception($e);
248         }
249 
250         return $message;
251     }
252 
253     /**
254      * Löscht eine Kategorie und reorganisiert die Prioritäten verbleibender Geschwister-Kategorien.
255      *
256      * @param int $category_id Id der Kategorie die gelöscht werden soll
257      *
258      * @throws rex_api_exception
259      *
260      * @return string Eine Statusmeldung
261      */
262     public static function deleteCategory($category_id)
263     {
264         $clang = 1;
265 
266         $thisCat = rex_sql::factory();
267         $thisCat->setQuery('SELECT * FROM ' . rex::getTablePrefix() . 'article WHERE id=? and clang_id=?', [$category_id, $clang]);
268 
269         // Prüfen ob die Kategorie existiert
270         if ($thisCat->getRows() == 1) {
271             $KAT = rex_sql::factory();
272             $KAT->setQuery('select * from ' . rex::getTablePrefix() . 'article where parent_id=? and clang_id=? and startarticle=1', [$category_id, $clang]);
273             // Prüfen ob die Kategorie noch Unterkategorien besitzt
274             if ($KAT->getRows() == 0) {
275                 $KAT->setQuery('select * from ' . rex::getTablePrefix() . 'article where parent_id=? and clang_id=? and startarticle=0', [$category_id, $clang]);
276                 // Prüfen ob die Kategorie noch Artikel besitzt (ausser dem Startartikel)
277                 if ($KAT->getRows() == 0) {
278                     $thisCat = rex_sql::factory();
279                     $thisCat->setQuery('SELECT * FROM ' . rex::getTablePrefix() . 'article WHERE id=?', [$category_id]);
280 
281                     $parent_id = $thisCat->getValue('parent_id');
282                     $message = rex_article_service::_deleteArticle($category_id);
283 
284                     foreach ($thisCat as $row) {
285                         $_clang = $row->getValue('clang_id');
286 
287                         // ----- PRIOR
288                         self::newCatPrio($parent_id, $_clang, 0, 1);
289 
290                         // ----- EXTENSION POINT
291                         $message = rex_extension::registerPoint(new rex_extension_point('CAT_DELETED', $message, [
292                             'id' => $category_id,
293                             'parent_id' => $parent_id,
294                             'clang' => $_clang,
295                             'name' => $row->getValue('catname'),
296                             'priority' => $row->getValue('catpriority'),
297                             'path' => $row->getValue('path'),
298                             'status' => $row->getValue('status'),
299                         ]));
300                     }
301 
302                     rex_complex_perm::removeItem('structure', $category_id);
303                 } else {
304                     throw new rex_api_exception(rex_i18n::msg('category_could_not_be_deleted') . ' ' . rex_i18n::msg('category_still_contains_articles'));
305                 }
306             } else {
307                 throw new rex_api_exception(rex_i18n::msg('category_could_not_be_deleted') . ' ' . rex_i18n::msg('category_still_contains_subcategories'));
308             }
309         } else {
310             throw new rex_api_exception(rex_i18n::msg('category_could_not_be_deleted'));
311         }
312 
313         return $message;
314     }
315 
316     /**
317      * Ändert den Status der Kategorie.
318      *
319      * @param int      $category_id Id der Kategorie die gelöscht werden soll
320      * @param int      $clang       Id der Sprache
321      * @param int|null $status      Status auf den die Kategorie gesetzt werden soll, oder NULL wenn zum nächsten Status weitergeschaltet werden soll
322      *
323      * @throws rex_api_exception
324      *
325      * @return int Der neue Status der Kategorie
326      */
327     public static function categoryStatus($category_id, $clang, $status = null)
328     {
329         $KAT = rex_sql::factory();
330         $KAT->setQuery('select * from ' . rex::getTablePrefix() . 'article where id=? and clang_id=? and startarticle=1', [$category_id, $clang]);
331         if ($KAT->getRows() == 1) {
332             // Status wurde nicht von außen vorgegeben,
333             // => zyklisch auf den nächsten Weiterschalten
334             if (!$status) {
335                 $newstatus = self::nextStatus($KAT->getValue('status'));
336             } else {
337                 $newstatus = $status;
338             }
339 
340             $EKAT = rex_sql::factory();
341             $EKAT->setTable(rex::getTablePrefix() . 'article');
342             $EKAT->setWhere(['id' => $category_id,  'clang_id' => $clang, 'startarticle' => 1]);
343             $EKAT->setValue('status', $newstatus);
344             $EKAT->addGlobalCreateFields(self::getUser());
345 
346             try {
347                 $EKAT->update();
348 
349                 rex_article_cache::delete($category_id, $clang);
350 
351                 // ----- EXTENSION POINT
352                 rex_extension::registerPoint(new rex_extension_point('CAT_STATUS', null, [
353                     'id' => $category_id,
354                     'clang' => $clang,
355                     'status' => $newstatus,
356                 ]));
357             } catch (rex_sql_exception $e) {
358                 throw new rex_api_exception($e);
359             }
360         } else {
361             throw new rex_api_exception(rex_i18n::msg('no_such_category'));
362         }
363 
364         return $newstatus;
365     }
366 
367     /**
368      * Gibt alle Stati zurück, die für eine Kategorie gültig sind.
369      *
370      * @return array Array von Stati
371      */
372     public static function statusTypes()
373     {
374         static $catStatusTypes;
375 
376         if (!$catStatusTypes) {
377             $catStatusTypes = [
378                 // Name, CSS-Class, Icon
379                 [rex_i18n::msg('status_offline'), 'rex-offline', 'rex-icon-offline'],
380                 [rex_i18n::msg('status_online'), 'rex-online', 'rex-icon-online'],
381             ];
382 
383             // ----- EXTENSION POINT
384             $catStatusTypes = rex_extension::registerPoint(new rex_extension_point('CAT_STATUS_TYPES', $catStatusTypes));
385         }
386 
387         return $catStatusTypes;
388     }
389 
390     public static function nextStatus($currentStatus)
391     {
392         $catStatusTypes = self::statusTypes();
393         return ($currentStatus + 1) % count($catStatusTypes);
394     }
395 
396     public static function prevStatus($currentStatus)
397     {
398         $catStatusTypes = self::statusTypes();
399         if (($currentStatus - 1) < 0) {
400             return count($catStatusTypes) - 1;
401         }
402 
403         return ($currentStatus - 1) % count($catStatusTypes);
404     }
405 
406     /**
407      * Kopiert eine Kategorie in eine andere.
408      *
409      * @param int $from_cat KategorieId der Kategorie, die kopiert werden soll (Quelle)
410      * @param int $to_cat   KategorieId der Kategorie, IN die kopiert werden soll (Ziel)
411      */
412     public static function copyCategory($from_cat, $to_cat)
413     {
414         // TODO rex_copyCategory implementieren
415     }
416 
417     /**
418      * Berechnet die Prios der Kategorien in einer Kategorie neu.
419      *
420      * @param int $parent_id KategorieId der Kategorie, die erneuert werden soll
421      * @param int $clang     ClangId der Kategorie, die erneuert werden soll
422      * @param int $new_prio  Neue PrioNr der Kategorie
423      * @param int $old_prio  Alte PrioNr der Kategorie
424      */
425     public static function newCatPrio($parent_id, $clang, $new_prio, $old_prio)
426     {
427         if ($new_prio != $old_prio) {
428             if ($new_prio < $old_prio) {
429                 $addsql = 'desc';
430             } else {
431                 $addsql = 'asc';
432             }
433 
434             rex_sql_util::organizePriorities(
435                 rex::getTable('article'),
436                 'catpriority',
437                 'clang_id=' . (int) $clang . ' AND parent_id=' . (int) $parent_id . ' AND startarticle=1',
438                 'catpriority,updatedate ' . $addsql
439             );
440 
441             rex_article_cache::deleteLists($parent_id);
442         }
443     }
444 
445     /**
446      * Verschieben einer Kategorie in eine andere.
447      *
448      * @param int $from_cat KategorieId der Kategorie, die verschoben werden soll (Quelle)
449      * @param int $to_cat   KategorieId der Kategorie, IN die verschoben werden soll (Ziel)
450      *
451      * @return bool TRUE bei Erfolg, sonst FALSE
452      */
453     public static function moveCategory($from_cat, $to_cat)
454     {
455         $from_cat = (int) $from_cat;
456         $to_cat = (int) $to_cat;
457 
458         if ($from_cat == $to_cat) {
459             // kann nicht in gleiche kategroie kopiert werden
460             return false;
461         }
462 
463         // kategorien vorhanden ?
464         // ist die zielkategorie im pfad der quellkategeorie ?
465         $fcat = rex_sql::factory();
466         $fcat->setQuery('select * from ' . rex::getTablePrefix() . 'article where startarticle=1 and id=? and clang_id=?', [$from_cat, rex_clang::getStartId()]);
467 
468         $tcat = rex_sql::factory();
469         $tcat->setQuery('select * from ' . rex::getTablePrefix() . 'article where startarticle=1 and id=? and clang_id=?', [$to_cat, rex_clang::getStartId()]);
470 
471         if ($fcat->getRows() != 1 || ($tcat->getRows() != 1 && $to_cat != 0)) {
472             // eine der kategorien existiert nicht
473             return false;
474         }
475         if ($to_cat > 0) {
476             $tcats = explode('|', $tcat->getValue('path'));
477             if (in_array($from_cat, $tcats)) {
478                 // zielkategorie ist in quellkategorie -> nicht verschiebbar
479                 return false;
480             }
481         }
482 
483         // ----- folgende cats regenerate
484         $RC = [];
485         $RC[$fcat->getValue('parent_id')] = 1;
486         $RC[$from_cat] = 1;
487         $RC[$to_cat] = 1;
488 
489         if ($to_cat > 0) {
490             $to_path = $tcat->getValue('path') . $to_cat . '|';
491         } else {
492             $to_path = '|';
493         }
494 
495         $from_path = $fcat->getValue('path') . $from_cat . '|';
496 
497         $gcats = rex_sql::factory();
498         // $gcats->setDebug();
499         $gcats->setQuery('select * from ' . rex::getTablePrefix() . 'article where path like ? and clang_id=?', [$from_path . '%', rex_clang::getStartId()]);
500 
501         $up = rex_sql::factory();
502         // $up->setDebug();
503         for ($i = 0; $i < $gcats->getRows(); ++$i) {
504             // make update
505             $new_path = $to_path . $from_cat . '|' . str_replace($from_path, '', $gcats->getValue('path'));
506             $icid = $gcats->getValue('id');
507 
508             // path aendern und speichern
509             $up->setTable(rex::getTablePrefix() . 'article');
510             $up->setWhere(['id' => $icid]);
511             $up->setValue('path', $new_path);
512             $up->update();
513 
514             // cat in gen eintragen
515             $RC[$icid] = 1;
516 
517             $gcats->next();
518         }
519 
520         // ----- clang holen, max catprio holen und entsprechen updaten
521         $gmax = rex_sql::factory();
522         $up = rex_sql::factory();
523         // $up->setDebug();
524         foreach (rex_clang::getAllIds() as $clang) {
525             $gmax->setQuery('select max(catpriority) from ' . rex::getTablePrefix() . 'article where parent_id=? and clang_id=?', [$to_cat, $clang]);
526             $catpriority = (int) $gmax->getValue('max(catpriority)');
527             $up->setTable(rex::getTablePrefix() . 'article');
528             $up->setWhere(['id' => $from_cat, 'clang_id' => $clang]);
529             $up->setValue('path', $to_path);
530             $up->setValue('parent_id', $to_cat);
531             $up->setValue('catpriority', ($catpriority + 1));
532             $up->update();
533         }
534 
535         // ----- generiere artikel neu - ohne neue inhaltsgenerierung
536         foreach ($RC as $id => $key) {
537             rex_article_cache::delete($id);
538         }
539 
540         foreach (rex_clang::getAllIds() as $clang) {
541             self::newCatPrio($fcat->getValue('parent_id'), $clang, 0, 1);
542 
543             rex_extension::registerPoint(new rex_extension_point('CAT_MOVED', null, [
544                 'id' => $from_cat,
545                 'clang_id' => $clang,
546                 'category_id' => $to_cat,
547             ]));
548         }
549 
550         return true;
551     }
552 
553     /**
554      * Checks whether the required array key $keyName isset.
555      *
556      * @param array  $array   The array
557      * @param string $keyName The key
558      *
559      * @throws rex_api_exception
560      */
561     protected static function reqKey(array $array, $keyName)
562     {
563         if (!isset($array[$keyName])) {
564             throw new rex_api_exception('Missing required parameter "' . $keyName . '"!');
565         }
566     }
567 
568     private static function getUser()
569     {
570         if (rex::getUser()) {
571             return rex::getUser()->getLogin();
572         }
573 
574         if (method_exists(rex::class, 'getEnvironment')) {
575             return rex::getEnvironment();
576         }
577 
578         return 'frontend';
579     }
580 }
581