1 <?php
  2 
  3 /**
  4  * @package redaxo\structure
  5  */
  6 class rex_article_service
  7 {
  8     /**
  9      * Erstellt einen neuen Artikel.
 10      *
 11      * @param array $data Array mit den Daten des Artikels
 12      *
 13      * @throws rex_api_exception
 14      *
 15      * @return string Eine Statusmeldung
 16      */
 17     public static function addArticle($data)
 18     {
 19         if (!is_array($data)) {
 20             throw new rex_api_exception('Expecting $data to be an array!');
 21         }
 22 
 23         self::reqKey($data, 'category_id');
 24         self::reqKey($data, 'priority');
 25         self::reqKey($data, 'name');
 26 
 27         if ($data['priority'] <= 0) {
 28             $data['priority'] = 1;
 29         }
 30 
 31         // parent may be null, when adding in the root cat
 32         $parent = rex_category::get($data['category_id']);
 33         if ($parent) {
 34             $path = $parent->getPath();
 35             $path .= $parent->getId() . '|';
 36         } else {
 37             $path = '|';
 38         }
 39 
 40         if (rex_plugin::get('structure', 'content')->isAvailable()) {
 41             $templates = rex_template::getTemplatesForCategory($data['category_id']);
 42 
 43             // Wenn Template nicht vorhanden, dann entweder erlaubtes nehmen
 44             // oder leer setzen.
 45             if (!isset($templates[$data['template_id']])) {
 46                 $data['template_id'] = 0;
 47                 if (count($templates) > 0) {
 48                     $data['template_id'] = key($templates);
 49                 }
 50             }
 51         }
 52 
 53         $message = rex_i18n::msg('article_added');
 54 
 55         $AART = rex_sql::factory();
 56         unset($id);
 57         $user = self::getUser();
 58         foreach (rex_clang::getAllIds() as $key) {
 59             // ------- Kategorienamen holen
 60             $category = rex_category::get($data['category_id'], $key);
 61 
 62             $categoryName = '';
 63             if ($category) {
 64                 $categoryName = $category->getName();
 65             }
 66 
 67             $AART->setTable(rex::getTablePrefix() . 'article');
 68             if (!isset($id) || !$id) {
 69                 $id = $AART->setNewId('id');
 70             } else {
 71                 $AART->setValue('id', $id);
 72             }
 73             $AART->setValue('name', $data['name']);
 74             $AART->setValue('catname', $categoryName);
 75             $AART->setValue('clang_id', $key);
 76             $AART->setValue('parent_id', $data['category_id']);
 77             $AART->setValue('priority', $data['priority']);
 78             $AART->setValue('path', $path);
 79             $AART->setValue('startarticle', 0);
 80             $AART->setValue('status', 0);
 81             $AART->setValue('template_id', $data['template_id']);
 82             $AART->addGlobalCreateFields($user);
 83             $AART->addGlobalUpdateFields($user);
 84 
 85             try {
 86                 $AART->insert();
 87                 // ----- PRIOR
 88                 self::newArtPrio($data['category_id'], $key, 0, $data['priority']);
 89             } catch (rex_sql_exception $e) {
 90                 throw new rex_api_exception($e);
 91             }
 92 
 93             rex_article_cache::delete($id, $key);
 94 
 95             // ----- EXTENSION POINT
 96             $message = rex_extension::registerPoint(new rex_extension_point('ART_ADDED', $message, [
 97                 'id' => $id,
 98                 'clang' => $key,
 99                 'status' => 0,
100                 'name' => $data['name'],
101                 'parent_id' => $data['category_id'],
102                 'priority' => $data['priority'],
103                 'path' => $path,
104                 'template_id' => $data['template_id'],
105                 'data' => $data,
106             ]));
107         }
108 
109         return $message;
110     }
111 
112     /**
113      * Bearbeitet einen Artikel.
114      *
115      * @param int   $article_id Id des Artikels der verändert werden soll
116      * @param int   $clang      Id der Sprache
117      * @param array $data       Array mit den Daten des Artikels
118      *
119      * @throws rex_api_exception
120      *
121      * @return string Eine Statusmeldung
122      */
123     public static function editArticle($article_id, $clang, $data)
124     {
125         if (!is_array($data)) {
126             throw  new rex_api_exception('Expecting $data to be an array!');
127         }
128 
129         self::reqKey($data, 'name');
130 
131         // Artikel mit alten Daten selektieren
132         $thisArt = rex_sql::factory();
133         $thisArt->setQuery('select * from ' . rex::getTablePrefix() . 'article where id=? and clang_id=?', [$article_id, $clang]);
134 
135         if ($thisArt->getRows() != 1) {
136             throw new rex_api_exception('Unable to find article with id "' . $article_id . '" and clang "' . $clang . '"!');
137         }
138 
139         $ooArt = rex_article::get($article_id, $clang);
140         $data['category_id'] = $ooArt->getCategoryId();
141 
142         if (rex_plugin::get('structure', 'content')->isAvailable()) {
143             $templates = rex_template::getTemplatesForCategory($data['category_id']);
144 
145             // Wenn Template nicht vorhanden, dann entweder erlaubtes nehmen
146             // oder leer setzen.
147             if (!isset($templates[$data['template_id']])) {
148                 $data['template_id'] = 0;
149                 if (count($templates) > 0) {
150                     $data['template_id'] = key($templates);
151                 }
152             }
153         }
154 
155         if (isset($data['priority'])) {
156             if ($data['priority'] <= 0) {
157                 $data['priority'] = 1;
158             }
159         }
160 
161         // complete remaining optional aprams
162         foreach (['path', 'priority'] as $optionalData) {
163             if (!isset($data[$optionalData])) {
164                 $data[$optionalData] = $thisArt->getValue($optionalData);
165             }
166         }
167 
168         $EA = rex_sql::factory();
169         $EA->setTable(rex::getTablePrefix() . 'article');
170         $EA->setWhere(['id' => $article_id, 'clang_id' => $clang]);
171         $EA->setValue('name', $data['name']);
172         $EA->setValue('template_id', $data['template_id']);
173         $EA->setValue('priority', $data['priority']);
174         $EA->addGlobalUpdateFields(self::getUser());
175 
176         try {
177             $EA->update();
178             $message = rex_i18n::msg('article_updated');
179 
180             // ----- PRIOR
181             rex_sql::factory()
182                 ->setTable(rex::getTable('article'))
183                 ->setWhere('id = :id AND clang_id != :clang', ['id' => $article_id, 'clang' => $clang])
184                 ->setValue('priority', $data['priority'])
185                 ->addGlobalUpdateFields(self::getUser())
186                 ->update();
187 
188             foreach (rex_clang::getAllIds() as $clangId) {
189                 self::newArtPrio($data['category_id'], $clangId, $data['priority'], $thisArt->getValue('priority'));
190             }
191             rex_article_cache::delete($article_id);
192 
193             // ----- EXTENSION POINT
194             $message = rex_extension::registerPoint(new rex_extension_point('ART_UPDATED', $message, [
195                 'id' => $article_id,
196                 'article' => clone $EA,
197                 'article_old' => clone $thisArt,
198                 'status' => $thisArt->getValue('status'),
199                 'name' => $data['name'],
200                 'clang' => $clang,
201                 'parent_id' => $data['category_id'],
202                 'priority' => $data['priority'],
203                 'path' => $data['path'],
204                 'template_id' => $data['template_id'],
205                 'data' => $data,
206             ]));
207         } catch (rex_sql_exception $e) {
208             throw new rex_api_exception($e);
209         }
210 
211         return $message;
212     }
213 
214     /**
215      * Löscht einen Artikel und reorganisiert die Prioritäten verbleibender Geschwister-Artikel.
216      *
217      * @param int $article_id Id des Artikels die gelöscht werden soll
218      *
219      * @throws rex_api_exception
220      *
221      * @return string Eine Statusmeldung
222      */
223     public static function deleteArticle($article_id)
224     {
225         $Art = rex_sql::factory();
226         $Art->setQuery('select * from ' . rex::getTablePrefix() . 'article where id=? and startarticle=0', [$article_id]);
227 
228         if ($Art->getRows() > 0) {
229             $message = self::_deleteArticle($article_id);
230             $parent_id = $Art->getValue('parent_id');
231 
232             foreach (rex_clang::getAllIds() as $clang) {
233                 // ----- PRIOR
234                 self::newArtPrio($Art->getValue('parent_id'), $clang, 0, 1);
235 
236                 // ----- EXTENSION POINT
237                 $message = rex_extension::registerPoint(new rex_extension_point('ART_DELETED', $message, [
238                     'id' => $article_id,
239                     'clang' => $clang,
240                     'parent_id' => $parent_id,
241                     'name' => $Art->getValue('name'),
242                     'status' => $Art->getValue('status'),
243                     'priority' => $Art->getValue('priority'),
244                     'path' => $Art->getValue('path'),
245                     'template_id' => $Art->getValue('template_id'),
246                 ]));
247 
248                 $Art->next();
249             }
250         } else {
251             throw new rex_api_exception(rex_i18n::msg('article_doesnt_exist'));
252         }
253 
254         return $message;
255     }
256 
257     /**
258      * Löscht einen Artikel.
259      *
260      * @param int $id ArtikelId des Artikels, der gelöscht werden soll
261      *
262      * @throws rex_api_exception
263      *
264      * @return string Eine Statusmeldung
265      */
266     public static function _deleteArticle($id)
267     {
268         // artikel loeschen
269 
270         // kontrolle ob erlaubnis nicht hier.. muss vorher geschehen
271 
272         // -> startarticle = 0
273         // --> artikelfiles löschen
274         // ---> article
275         // ---> content
276         // ---> clist
277         // ---> alist
278         // -> startarticle = 1
279         // --> rekursiv aufrufen
280 
281         if ($id == rex_article::getSiteStartArticleId()) {
282             throw new rex_api_exception(rex_i18n::msg('cant_delete_sitestartarticle'));
283         }
284         if ($id == rex_article::getNotfoundArticleId()) {
285             throw new rex_api_exception(rex_i18n::msg('cant_delete_notfoundarticle'));
286         }
287 
288         $ART = rex_sql::factory();
289         $ART->setQuery('select * from ' . rex::getTablePrefix() . 'article where id=? and clang_id=?', [$id, rex_clang::getStartId()]);
290 
291         $message = '';
292         if ($ART->getRows() > 0) {
293             $parent_id = $ART->getValue('parent_id');
294             $message = rex_extension::registerPoint(new rex_extension_point('ART_PRE_DELETED', $message, [
295                 'id' => $id,
296                 'parent_id' => $parent_id,
297                 'name' => $ART->getValue('name'),
298                 'status' => $ART->getValue('status'),
299                 'priority' => $ART->getValue('priority'),
300                 'path' => $ART->getValue('path'),
301                 'template_id' => $ART->getValue('template_id'),
302             ]));
303 
304             if ($ART->getValue('startarticle') == 1) {
305                 $message = rex_i18n::msg('category_deleted');
306                 $SART = rex_sql::factory();
307                 $SART->setQuery('select * from ' . rex::getTablePrefix() . 'article where parent_id=? and clang_id=?', [$id, rex_clang::getStartId()]);
308                 for ($i = 0; $i < $SART->getRows(); ++$i) {
309                     self::_deleteArticle($id);
310                     $SART->next();
311                 }
312             } else {
313                 $message = rex_i18n::msg('article_deleted');
314             }
315 
316             rex_article_cache::delete($id);
317             $ART->setQuery('delete from ' . rex::getTablePrefix() . 'article where id=?', [$id]);
318             $ART->setQuery('delete from ' . rex::getTablePrefix() . 'article_slice where article_id=?', [$id]);
319 
320             // --------------------------------------------------- Listen generieren
321             rex_article_cache::deleteLists($parent_id);
322 
323             return $message;
324         }
325         throw new rex_api_exception(rex_i18n::msg('category_doesnt_exist'));
326     }
327 
328     /**
329      * Ändert den Status des Artikels.
330      *
331      * @param int      $article_id Id des Artikels die gelöscht werden soll
332      * @param int      $clang      Id der Sprache
333      * @param int|null $status     Status auf den der Artikel gesetzt werden soll, oder NULL wenn zum nächsten Status weitergeschaltet werden soll
334      *
335      * @throws rex_api_exception
336      *
337      * @return int Der neue Status des Artikels
338      */
339     public static function articleStatus($article_id, $clang, $status = null)
340     {
341         $GA = rex_sql::factory();
342         $GA->setQuery('select * from ' . rex::getTablePrefix() . 'article where id=? and clang_id=?', [$article_id, $clang]);
343         if ($GA->getRows() == 1) {
344             // Status wurde nicht von außen vorgegeben,
345             // => zyklisch auf den nächsten Weiterschalten
346             if (!$status) {
347                 $newstatus = self::nextStatus($GA->getValue('status'));
348             } else {
349                 $newstatus = $status;
350             }
351 
352             $EA = rex_sql::factory();
353             $EA->setTable(rex::getTablePrefix() . 'article');
354             $EA->setWhere(['id' => $article_id, 'clang_id' => $clang]);
355             $EA->setValue('status', $newstatus);
356             $EA->addGlobalUpdateFields(self::getUser());
357 
358             try {
359                 $EA->update();
360 
361                 rex_article_cache::delete($article_id, $clang);
362 
363                 // ----- EXTENSION POINT
364                 rex_extension::registerPoint(new rex_extension_point('ART_STATUS', null, [
365                     'id' => $article_id,
366                     'clang' => $clang,
367                     'status' => $newstatus,
368                 ]));
369             } catch (rex_sql_exception $e) {
370                 throw new rex_api_exception($e);
371             }
372         } else {
373             throw new rex_api_exception(rex_i18n::msg('no_such_category'));
374         }
375 
376         return $newstatus;
377     }
378 
379     /**
380      * Gibt alle Stati zurück, die für einen Artikel gültig sind.
381      *
382      * @return array Array von Stati
383      */
384     public static function statusTypes()
385     {
386         static $artStatusTypes;
387 
388         if (!$artStatusTypes) {
389             $artStatusTypes = [
390                 // Name, CSS-Class
391                 [rex_i18n::msg('status_offline'), 'rex-offline', 'rex-icon-offline'],
392                 [rex_i18n::msg('status_online'), 'rex-online', 'rex-icon-online'],
393             ];
394 
395             // ----- EXTENSION POINT
396             $artStatusTypes = rex_extension::registerPoint(new rex_extension_point('ART_STATUS_TYPES', $artStatusTypes));
397         }
398 
399         return $artStatusTypes;
400     }
401 
402     public static function nextStatus($currentStatus)
403     {
404         $artStatusTypes = self::statusTypes();
405         return ($currentStatus + 1) % count($artStatusTypes);
406     }
407 
408     public static function prevStatus($currentStatus)
409     {
410         $artStatusTypes = self::statusTypes();
411         if (($currentStatus - 1) < 0) {
412             return count($artStatusTypes) - 1;
413         }
414 
415         return ($currentStatus - 1) % count($artStatusTypes);
416     }
417 
418     /**
419      * Berechnet die Prios der Artikel in einer Kategorie neu.
420      *
421      * @param int $parent_id KategorieId der Kategorie, die erneuert werden soll
422      * @param int $clang     ClangId der Kategorie, die erneuert werden soll
423      * @param int $new_prio  Neue PrioNr der Kategorie
424      * @param int $old_prio  Alte PrioNr der Kategorie
425      */
426     public static function newArtPrio($parent_id, $clang, $new_prio, $old_prio)
427     {
428         if ($new_prio != $old_prio) {
429             if ($new_prio < $old_prio) {
430                 $addsql = 'desc';
431             } else {
432                 $addsql = 'asc';
433             }
434 
435             rex_sql_util::organizePriorities(
436                 rex::getTable('article'),
437                 'priority',
438                 'clang_id=' . (int) $clang . ' AND ((startarticle<>1 AND parent_id=' . (int) $parent_id . ') OR (startarticle=1 AND id=' . (int) $parent_id . '))',
439                 'priority,updatedate ' . $addsql
440             );
441 
442             rex_article_cache::deleteLists($parent_id);
443         }
444     }
445 
446     /**
447      * Konvertiert einen Artikel in eine Kategorie.
448      *
449      * @param int $art_id Artikel ID des Artikels, der in eine Kategorie umgewandelt werden soll
450      *
451      * @return bool TRUE bei Erfolg, sonst FALSE
452      */
453     public static function article2category($art_id)
454     {
455         $sql = rex_sql::factory();
456 
457         // LANG SCHLEIFE
458         foreach (rex_clang::getAllIds() as $clang) {
459             // artikel
460             $sql->setQuery('select parent_id, name from ' . rex::getTablePrefix() . 'article where id=? and startarticle=0 and clang_id=?', [$art_id, $clang]);
461 
462             if (!isset($parent_id)) {
463                 $parent_id = $sql->getValue('parent_id');
464             }
465 
466             // artikel updaten
467             $sql->setTable(rex::getTablePrefix() . 'article');
468             $sql->setWhere(['id' => $art_id, 'clang_id' => $clang]);
469             $sql->setValue('startarticle', 1);
470             $sql->setValue('catname', $sql->getValue('name'));
471             $sql->setValue('catpriority', 100);
472             $sql->update();
473 
474             rex_category_service::newCatPrio($parent_id, $clang, 0, 100);
475         }
476 
477         rex_article_cache::deleteLists($parent_id);
478         rex_article_cache::delete($art_id);
479 
480         foreach (rex_clang::getAllIds() as $clang) {
481             rex_extension::registerPoint(new rex_extension_point('ART_TO_CAT', '', [
482                 'id' => $art_id,
483                 'clang' => $clang,
484             ]));
485         }
486 
487         return true;
488     }
489 
490     /**
491      * Konvertiert eine Kategorie in einen Artikel.
492      *
493      * @param int $art_id Artikel ID der Kategorie, die in einen Artikel umgewandelt werden soll
494      *
495      * @return bool TRUE bei Erfolg, sonst FALSE
496      */
497     public static function category2article($art_id)
498     {
499         $sql = rex_sql::factory();
500 
501         // Kategorie muss leer sein
502         $sql->setQuery('SELECT pid FROM ' . rex::getTablePrefix() . 'article WHERE parent_id=? LIMIT 1', [$art_id]);
503         if ($sql->getRows() != 0) {
504             return false;
505         }
506 
507         // LANG SCHLEIFE
508         foreach (rex_clang::getAllIds() as $clang) {
509             // artikel
510             $sql->setQuery('select parent_id, name from ' . rex::getTablePrefix() . 'article where id=? and startarticle=1 and clang_id=?', [$art_id, $clang]);
511 
512             if (!isset($parent_id)) {
513                 $parent_id = $sql->getValue('parent_id');
514             }
515 
516             // artikel updaten
517             $sql->setTable(rex::getTablePrefix() . 'article');
518             $sql->setWhere(['id' => $art_id, 'clang_id' => $clang]);
519             $sql->setValue('startarticle', 0);
520             $sql->setValue('priority', 100);
521             $sql->update();
522 
523             self::newArtPrio($parent_id, $clang, 0, 100);
524         }
525 
526         rex_article_cache::deleteLists($parent_id);
527         rex_article_cache::delete($art_id);
528 
529         foreach (rex_clang::getAllIds() as $clang) {
530             rex_extension::registerPoint(new rex_extension_point('CAT_TO_ART', '', [
531                 'id' => $art_id,
532                 'clang' => $clang,
533             ]));
534         }
535 
536         return true;
537     }
538 
539     /**
540      * Konvertiert einen Artikel zum Startartikel der eigenen Kategorie.
541      *
542      * @param int $neu_id Artikel ID des Artikels, der Startartikel werden soll
543      *
544      * @return bool TRUE bei Erfolg, sonst FALSE
545      */
546     public static function article2startarticle($neu_id)
547     {
548         $GAID = [];
549 
550         // neuen startartikel holen und schauen ob da
551         $neu = rex_sql::factory();
552         $neu->setQuery('select * from ' . rex::getTablePrefix() . 'article where id=? and startarticle=0 and clang_id=?', [$neu_id, rex_clang::getStartId()]);
553         if ($neu->getRows() != 1) {
554             return false;
555         }
556         $neu_cat_id = $neu->getValue('parent_id');
557 
558         // in oberster kategorie dann return
559         if ($neu_cat_id == 0) {
560             return false;
561         }
562 
563         // alten startartikel
564         $alt = rex_sql::factory();
565         $alt->setQuery('select * from ' . rex::getTablePrefix() . 'article where id=? and startarticle=1 and clang_id=?', [$neu_cat_id, rex_clang::getStartId()]);
566         if ($alt->getRows() != 1) {
567             return false;
568         }
569         $alt_id = $alt->getValue('id');
570         $parent_id = $alt->getValue('parent_id');
571 
572         // cat felder sammeln. +
573         $params = ['path', 'priority', 'catname', 'startarticle', 'catpriority', 'status'];
574         $db_fields = rex_structure_element::getClassVars();
575         foreach ($db_fields as $field) {
576             if (substr($field, 0, 4) == 'cat_') {
577                 $params[] = $field;
578             }
579         }
580 
581         // LANG SCHLEIFE
582         foreach (rex_clang::getAllIds() as $clang) {
583             // alter startartikel
584             $alt->setQuery('select * from ' . rex::getTablePrefix() . 'article where id=? and startarticle=1 and clang_id=?', [$neu_cat_id, $clang]);
585 
586             // neuer startartikel
587             $neu->setQuery('select * from ' . rex::getTablePrefix() . 'article where id=? and startarticle=0 and clang_id=?', [$neu_id, $clang]);
588 
589             // alter startartikel updaten
590             $alt2 = rex_sql::factory();
591             $alt2->setTable(rex::getTablePrefix() . 'article');
592             $alt2->setWhere(['id' => $alt_id, 'clang_id' => $clang]);
593             $alt2->setValue('parent_id', $neu_id);
594 
595             // neuer startartikel updaten
596             $neu2 = rex_sql::factory();
597             $neu2->setTable(rex::getTablePrefix() . 'article');
598             $neu2->setWhere(['id' => $neu_id, 'clang_id' => $clang]);
599             $neu2->setValue('parent_id', $alt->getValue('parent_id'));
600 
601             // austauschen der definierten paramater
602             foreach ($params as $param) {
603                 $alt2->setValue($param, $neu->getValue($param));
604                 $neu2->setValue($param, $alt->getValue($param));
605             }
606             $alt2->update();
607             $neu2->update();
608         }
609 
610         // alle artikel suchen nach |art_id| und pfade ersetzen
611         // alles artikel mit parent_id alt_id suchen und ersetzen
612 
613         $articles = rex_sql::factory();
614         $ia = rex_sql::factory();
615         $articles->setQuery('select * from ' . rex::getTablePrefix() . "article where path like '%|$alt_id|%'");
616         for ($i = 0; $i < $articles->getRows(); ++$i) {
617             $iid = $articles->getValue('id');
618             $ipath = str_replace("|$alt_id|", "|$neu_id|", $articles->getValue('path'));
619 
620             $ia->setTable(rex::getTablePrefix() . 'article');
621             $ia->setWhere(['id' => $iid]);
622             $ia->setValue('path', $ipath);
623             if ($articles->getValue('parent_id') == $alt_id) {
624                 $ia->setValue('parent_id', $neu_id);
625             }
626             $ia->update();
627             $GAID[$iid] = $iid;
628             $articles->next();
629         }
630 
631         $GAID[$neu_id] = $neu_id;
632         $GAID[$alt_id] = $alt_id;
633         $GAID[$parent_id] = $parent_id;
634 
635         foreach ($GAID as $gid) {
636             rex_article_cache::delete($gid);
637         }
638 
639         rex_complex_perm::replaceItem('structure', $alt_id, $neu_id);
640 
641         foreach (rex_clang::getAllIds() as $clang) {
642             rex_extension::registerPoint(new rex_extension_point('ART_TO_STARTARTICLE', '', [
643                 'id' => $neu_id,
644                 'id_old' => $alt_id,
645                 'clang' => $clang,
646             ]));
647         }
648 
649         return true;
650     }
651 
652     /**
653      * Kopiert die Metadaten eines Artikels in einen anderen Artikel.
654      *
655      * @param int   $from_id    ArtikelId des Artikels, aus dem kopiert werden (Quell ArtikelId)
656      * @param int   $to_id      ArtikelId des Artikel, in den kopiert werden sollen (Ziel ArtikelId)
657      * @param int   $from_clang ClangId des Artikels, aus dem kopiert werden soll (Quell ClangId)
658      * @param int   $to_clang   ClangId des Artikels, in den kopiert werden soll (Ziel ClangId)
659      * @param array $params     Array von Spaltennamen, welche kopiert werden sollen
660      *
661      * @return bool TRUE bei Erfolg, sonst FALSE
662      */
663     public static function copyMeta($from_id, $to_id, $from_clang = 1, $to_clang = 1, $params = [])
664     {
665         $from_clang = (int) $from_clang;
666         $to_clang = (int) $to_clang;
667         $from_id = (int) $from_id;
668         $to_id = (int) $to_id;
669         if (!is_array($params)) {
670             $params = [];
671         }
672 
673         if ($from_id == $to_id && $from_clang == $to_clang) {
674             return false;
675         }
676 
677         $gc = rex_sql::factory();
678         $gc->setQuery('select * from ' . rex::getTablePrefix() . 'article where clang_id=? and id=?', [$from_clang, $from_id]);
679 
680         if ($gc->getRows() == 1) {
681             $uc = rex_sql::factory();
682             // $uc->setDebug();
683             $uc->setTable(rex::getTablePrefix() . 'article');
684             $uc->setWhere(['clang_id' => $to_clang, 'id' => $to_id]);
685             $uc->addGlobalUpdateFields(self::getUser());
686 
687             foreach ($params as $key => $value) {
688                 $uc->setValue($value, $gc->getValue($value));
689             }
690 
691             $uc->update();
692 
693             rex_article_cache::deleteMeta($to_id, $to_clang);
694             return true;
695         }
696         return false;
697     }
698 
699     /**
700      * Kopieren eines Artikels von einer Kategorie in eine andere.
701      *
702      * @param int $id        ArtikelId des zu kopierenden Artikels
703      * @param int $to_cat_id KategorieId in die der Artikel kopiert werden soll
704      *
705      * @return bool FALSE bei Fehler, sonst die Artikel Id des neue kopierten Artikels
706      */
707     public static function copyArticle($id, $to_cat_id)
708     {
709         $id = (int) $id;
710         $to_cat_id = (int) $to_cat_id;
711         $new_id = '';
712 
713         $user = self::getUser();
714 
715         // Artikel in jeder Sprache kopieren
716         foreach (rex_clang::getAllIds() as $clang) {
717             // validierung der id & from_cat_id
718             $from_sql = rex_sql::factory();
719             $from_sql->setQuery('select * from ' . rex::getTablePrefix() . 'article where clang_id=? and id=?', [$clang, $id]);
720 
721             if ($from_sql->getRows() == 1) {
722                 // validierung der to_cat_id
723                 $to_sql = rex_sql::factory();
724                 $to_sql->setQuery('select * from ' . rex::getTablePrefix() . 'article where clang_id=? and startarticle=1 and id=?', [$clang, $to_cat_id]);
725 
726                 if ($to_sql->getRows() == 1 || $to_cat_id == 0) {
727                     if ($to_sql->getRows() == 1) {
728                         $path = $to_sql->getValue('path') . $to_sql->getValue('id') . '|';
729                         $catname = $to_sql->getValue('catname');
730                     } else {
731                         // In RootEbene
732                         $path = '|';
733                         $catname = $from_sql->getValue('name');
734                     }
735 
736                     $art_sql = rex_sql::factory();
737                     $art_sql->setTable(rex::getTablePrefix() . 'article');
738                     if ($new_id == '') {
739                         $new_id = $art_sql->setNewId('id');
740                     }
741                     $art_sql->setValue('id', $new_id); // neuen auto_incrment erzwingen
742                     $art_sql->setValue('parent_id', $to_cat_id);
743                     $art_sql->setValue('catname', $catname);
744                     $art_sql->setValue('catpriority', 0);
745                     $art_sql->setValue('path', $path);
746                     $art_sql->setValue('name', $from_sql->getValue('name') . ' ' . rex_i18n::msg('structure_copy'));
747                     $art_sql->setValue('priority', 99999); // Artikel als letzten Artikel in die neue Kat einfügen
748                     $art_sql->setValue('status', 0); // Kopierter Artikel offline setzen
749                     $art_sql->setValue('startarticle', 0);
750                     $art_sql->addGlobalUpdateFields($user);
751                     $art_sql->addGlobalCreateFields($user);
752 
753                     // schon gesetzte Felder nicht wieder überschreiben
754                     $dont_copy = ['id', 'pid', 'parent_id', 'catname', 'name', 'catpriority', 'path', 'priority', 'status', 'updatedate', 'updateuser', 'createdate', 'createuser', 'startarticle'];
755 
756                     foreach (array_diff($from_sql->getFieldnames(), $dont_copy) as $fld_name) {
757                         $art_sql->setValue($fld_name, $from_sql->getValue($fld_name));
758                     }
759 
760                     $art_sql->setValue('clang_id', $clang);
761                     $art_sql->insert();
762 
763                     $revisions = rex_sql::factory();
764                     $revisions->setQuery('select revision from ' . rex::getTablePrefix() . 'article_slice where priority=1 AND article_id=? AND clang_id=? GROUP BY revision', [$id, $clang]);
765                     foreach ($revisions as $rev) {
766                         // FIXME this dependency is very ugly!
767                         // ArticleSlices kopieren
768                         rex_content_service::copyContent($id, $new_id, $clang, $clang, $rev->getValue('revision'));
769                     }
770 
771                     // Prios neu berechnen
772                     self::newArtPrio($to_cat_id, $clang, 1, 0);
773 
774                     rex_extension::registerPoint(new rex_extension_point('ART_COPIED', null, [
775                         'id_source' => $id,
776                         'id' => $new_id,
777                         'clang' => $clang,
778                         'category_id' => $to_cat_id,
779                     ]));
780                 } else {
781                     return false;
782                 }
783             } else {
784                 return false;
785             }
786         }
787 
788         // Caches des Artikels löschen, in allen Sprachen
789         rex_article_cache::delete($id);
790 
791         // Caches der Kategorien löschen, da sich derin befindliche Artikel geändert haben
792         rex_article_cache::delete($to_cat_id);
793 
794         return $new_id;
795     }
796 
797     /**
798      * Verschieben eines Artikels von einer Kategorie in eine Andere.
799      *
800      * @param int $id          ArtikelId des zu verschiebenden Artikels
801      * @param int $from_cat_id KategorieId des Artikels, der Verschoben wird
802      * @param int $to_cat_id   KategorieId in die der Artikel verschoben werden soll
803      *
804      * @return bool TRUE bei Erfolg, sonst FALSE
805      */
806     public static function moveArticle($id, $from_cat_id, $to_cat_id)
807     {
808         $id = (int) $id;
809         $to_cat_id = (int) $to_cat_id;
810         $from_cat_id = (int) $from_cat_id;
811 
812         if ($from_cat_id == $to_cat_id) {
813             return false;
814         }
815 
816         // Artikel in jeder Sprache verschieben
817         foreach (rex_clang::getAllIds() as $clang) {
818             // validierung der id & from_cat_id
819             $from_sql = rex_sql::factory();
820             $from_sql->setQuery('select * from ' . rex::getTablePrefix() . 'article where clang_id=? and startarticle<>1 and id=? and parent_id=?', [$clang, $id, $from_cat_id]);
821 
822             if ($from_sql->getRows() == 1) {
823                 // validierung der to_cat_id
824                 $to_sql = rex_sql::factory();
825                 $to_sql->setQuery('select * from ' . rex::getTablePrefix() . 'article where clang_id=? and startarticle=1 and id=?', [$clang, $to_cat_id]);
826 
827                 if ($to_sql->getRows() == 1 || $to_cat_id == 0) {
828                     if ($to_sql->getRows() == 1) {
829                         $parent_id = $to_sql->getValue('id');
830                         $path = $to_sql->getValue('path') . $to_sql->getValue('id') . '|';
831                         $catname = $to_sql->getValue('catname');
832                     } else {
833                         // In RootEbene
834                         $parent_id = 0;
835                         $path = '|';
836                         $catname = $from_sql->getValue('name');
837                     }
838 
839                     $art_sql = rex_sql::factory();
840                     //$art_sql->setDebug();
841 
842                     $art_sql->setTable(rex::getTablePrefix() . 'article');
843                     $art_sql->setValue('parent_id', $parent_id);
844                     $art_sql->setValue('path', $path);
845                     $art_sql->setValue('catname', $catname);
846                     // Artikel als letzten Artikel in die neue Kat einfügen
847                     $art_sql->setValue('priority', '99999');
848                     // Kopierter Artikel offline setzen
849                     $art_sql->setValue('status', $from_sql->getValue('status'));
850                     $art_sql->addGlobalUpdateFields(self::getUser());
851 
852                     $art_sql->setWhere('clang_id="' . $clang . '" and startarticle<>1 and id="' . $id . '" and parent_id="' . $from_cat_id . '"');
853                     $art_sql->update();
854 
855                     // Prios neu berechnen
856                     self::newArtPrio($to_cat_id, $clang, 1, 0);
857                     self::newArtPrio($from_cat_id, $clang, 1, 0);
858 
859                     rex_extension::registerPoint(new rex_extension_point('ART_MOVED', null, [
860                         'id' => $id,
861                         'clang' => $clang,
862                         'category_id' => $parent_id,
863                     ]));
864                 } else {
865                     return false;
866                 }
867             } else {
868                 return false;
869             }
870         }
871 
872         // Caches des Artikels löschen, in allen Sprachen
873         rex_article_cache::delete($id);
874 
875         // Caches der Kategorien löschen, da sich derin befindliche Artikel geändert haben
876         rex_article_cache::delete($from_cat_id);
877         rex_article_cache::delete($to_cat_id);
878 
879         return true;
880     }
881 
882     /**
883      * Checks whether the required array key $keyName isset.
884      *
885      * @param array  $array   The array
886      * @param string $keyName The key
887      *
888      * @throws rex_api_exception
889      */
890     protected static function reqKey($array, $keyName)
891     {
892         if (!isset($array[$keyName])) {
893             throw new rex_api_exception('Missing required parameter "' . $keyName . '"!');
894         }
895     }
896 
897     private static function getUser()
898     {
899         if (rex::getUser()) {
900             return rex::getUser()->getLogin();
901         }
902 
903         if (method_exists(rex::class, 'getEnvironment')) {
904             return rex::getEnvironment();
905         }
906 
907         return 'frontend';
908     }
909 }
910