vendor/contao/core-bundle/src/Resources/contao/dca/tl_article.php line 204

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of Contao.
  4.  *
  5.  * (c) Leo Feyer
  6.  *
  7.  * @license LGPL-3.0-or-later
  8.  */
  9. use Contao\ArticleModel;
  10. use Contao\Backend;
  11. use Contao\BackendUser;
  12. use Contao\Config;
  13. use Contao\Controller;
  14. use Contao\CoreBundle\EventListener\DataContainer\ContentCompositionListener;
  15. use Contao\CoreBundle\Exception\AccessDeniedException;
  16. use Contao\DataContainer;
  17. use Contao\Image;
  18. use Contao\Input;
  19. use Contao\LayoutModel;
  20. use Contao\PageModel;
  21. use Contao\StringUtil;
  22. use Contao\System;
  23. use Contao\Versions;
  24. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  25. $this->loadDataContainer('tl_page');
  26. $GLOBALS['TL_DCA']['tl_article'] = array
  27. (
  28.     // Config
  29.     'config' => array
  30.     (
  31.         'dataContainer'               => 'Table',
  32.         'ptable'                      => 'tl_page',
  33.         'ctable'                      => array('tl_content'),
  34.         'switchToEdit'                => true,
  35.         'enableVersioning'            => true,
  36.         'markAsCopy'                  => 'title',
  37.         'onload_callback' => array
  38.         (
  39.             array('tl_article''checkPermission'),
  40.             array('tl_article''addCustomLayoutSectionReferences'),
  41.             array('tl_page''addBreadcrumb')
  42.         ),
  43.         'sql' => array
  44.         (
  45.             'keys' => array
  46.             (
  47.                 'id' => 'primary',
  48.                 'alias' => 'index',
  49.                 'pid,published,inColumn,start,stop' => 'index'
  50.             )
  51.         )
  52.     ),
  53.     // List
  54.     'list' => array
  55.     (
  56.         'sorting' => array
  57.         (
  58.             'mode'                    => 6,
  59.             'panelLayout'             => 'filter;search'
  60.         ),
  61.         'label' => array
  62.         (
  63.             'fields'                  => array('title''inColumn'),
  64.             'format'                  => '%s <span style="color:#999;padding-left:3px">[%s]</span>',
  65.             'label_callback'          => array('tl_article''addIcon')
  66.         ),
  67.         'global_operations' => array
  68.         (
  69.             'toggleNodes' => array
  70.             (
  71.                 'href'                => '&amp;ptg=all',
  72.                 'class'               => 'header_toggle',
  73.                 'showOnSelect'        => true
  74.             ),
  75.             'all' => array
  76.             (
  77.                 'href'                => 'act=select',
  78.                 'class'               => 'header_edit_all',
  79.                 'attributes'          => 'onclick="Backend.getScrollOffset()" accesskey="e"'
  80.             )
  81.         ),
  82.         'operations' => array
  83.         (
  84.             'edit' => array
  85.             (
  86.                 'href'                => 'table=tl_content',
  87.                 'icon'                => 'edit.svg',
  88.                 'button_callback'     => array('tl_article''editArticle')
  89.             ),
  90.             'editheader' => array
  91.             (
  92.                 'href'                => 'act=edit',
  93.                 'icon'                => 'header.svg',
  94.                 'button_callback'     => array('tl_article''editHeader')
  95.             ),
  96.             'copy' => array
  97.             (
  98.                 'href'                => 'act=paste&amp;mode=copy',
  99.                 'icon'                => 'copy.svg',
  100.                 'attributes'          => 'onclick="Backend.getScrollOffset()"',
  101.                 'button_callback'     => array('tl_article''copyArticle')
  102.             ),
  103.             'cut' => array
  104.             (
  105.                 'href'                => 'act=paste&amp;mode=cut',
  106.                 'icon'                => 'cut.svg',
  107.                 'attributes'          => 'onclick="Backend.getScrollOffset()"',
  108.                 'button_callback'     => array('tl_article''cutArticle')
  109.             ),
  110.             'delete' => array
  111.             (
  112.                 'href'                => 'act=delete',
  113.                 'icon'                => 'delete.svg',
  114.                 'attributes'          => 'onclick="if(!confirm(\'' . ($GLOBALS['TL_LANG']['MSC']['deleteConfirm'] ?? null) . '\'))return false;Backend.getScrollOffset()"',
  115.                 'button_callback'     => array('tl_article''deleteArticle')
  116.             ),
  117.             'toggle' => array
  118.             (
  119.                 'icon'                => 'visible.svg',
  120.                 'attributes'          => 'onclick="Backend.getScrollOffset();return AjaxRequest.toggleVisibility(this,%s)"',
  121.                 'button_callback'     => array('tl_article''toggleIcon'),
  122.                 'showInHeader'        => true
  123.             ),
  124.             'show' => array
  125.             (
  126.                 'href'                => 'act=show',
  127.                 'icon'                => 'show.svg'
  128.             )
  129.         )
  130.     ),
  131.     // Select
  132.     'select' => array
  133.     (
  134.         'buttons_callback' => array
  135.         (
  136.             array('tl_article''addAliasButton')
  137.         )
  138.     ),
  139.     // Palettes
  140.     'palettes' => array
  141.     (
  142.         '__selector__'                => array('protected'),
  143.         'default'                     => '{title_legend},title,alias,author;{layout_legend},inColumn,keywords;{teaser_legend:hide},teaserCssID,showTeaser,teaser;{syndication_legend},printable;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID;{publish_legend},published,start,stop'
  144.     ),
  145.     // Subpalettes
  146.     'subpalettes' => array
  147.     (
  148.         'protected'                   => 'groups'
  149.     ),
  150.     // Fields
  151.     'fields' => array
  152.     (
  153.         'id' => array
  154.         (
  155.             'label'                   => array('ID'),
  156.             'search'                  => true,
  157.             'sql'                     => "int(10) unsigned NOT NULL auto_increment"
  158.         ),
  159.         'pid' => array
  160.         (
  161.             'foreignKey'              => 'tl_page.title',
  162.             'sql'                     => "int(10) unsigned NOT NULL default 0",
  163.             'relation'                => array('type'=>'belongsTo''load'=>'lazy')
  164.         ),
  165.         'sorting' => array
  166.         (
  167.             'sql'                     => "int(10) unsigned NOT NULL default 0"
  168.         ),
  169.         'tstamp' => array
  170.         (
  171.             'sql'                     => "int(10) unsigned NOT NULL default 0"
  172.         ),
  173.         'title' => array
  174.         (
  175.             'exclude'                 => true,
  176.             'inputType'               => 'text',
  177.             'search'                  => true,
  178.             'eval'                    => array('mandatory'=>true'decodeEntities'=>true'maxlength'=>255'tl_class'=>'w50'),
  179.             'sql'                     => "varchar(255) NOT NULL default ''"
  180.         ),
  181.         'alias' => array
  182.         (
  183.             'exclude'                 => true,
  184.             'inputType'               => 'text',
  185.             'search'                  => true,
  186.             'eval'                    => array('rgxp'=>'alias''doNotCopy'=>true'maxlength'=>255'tl_class'=>'w50 clr'),
  187.             'save_callback' => array
  188.             (
  189.                 array('tl_article''generateAlias')
  190.             ),
  191.             'sql'                     => "varchar(255) BINARY NOT NULL default ''"
  192.         ),
  193.         'author' => array
  194.         (
  195.             'default'                 => BackendUser::getInstance()->id,
  196.             'exclude'                 => true,
  197.             'search'                  => true,
  198.             'filter'                  => true,
  199.             'inputType'               => 'select',
  200.             'foreignKey'              => 'tl_user.name',
  201.             'eval'                    => array('doNotCopy'=>true'mandatory'=>true'chosen'=>true'includeBlankOption'=>true'tl_class'=>'w50'),
  202.             'sql'                     => "int(10) unsigned NOT NULL default 0",
  203.             'relation'                => array('type'=>'hasOne''load'=>'lazy')
  204.         ),
  205.         'inColumn' => array
  206.         (
  207.             'exclude'                 => true,
  208.             'filter'                  => true,
  209.             'inputType'               => 'select',
  210.             'options_callback'        => array('tl_article''getActiveLayoutSections'),
  211.             'eval'                    => array('mandatory'=>true'tl_class'=>'w50'),
  212.             'reference'               => &$GLOBALS['TL_LANG']['COLS'],
  213.             'sql'                     => "varchar(32) NOT NULL default 'main'"
  214.         ),
  215.         'keywords' => array
  216.         (
  217.             'exclude'                 => true,
  218.             'inputType'               => 'textarea',
  219.             'search'                  => true,
  220.             'eval'                    => array('style'=>'height:60px''decodeEntities'=>true'tl_class'=>'clr'),
  221.             'sql'                     => "text NULL"
  222.         ),
  223.         'showTeaser' => array
  224.         (
  225.             'exclude'                 => true,
  226.             'inputType'               => 'checkbox',
  227.             'eval'                    => array('tl_class'=>'w50 m12'),
  228.             'sql'                     => "char(1) NOT NULL default ''"
  229.         ),
  230.         'teaserCssID' => array
  231.         (
  232.             'exclude'                 => true,
  233.             'inputType'               => 'text',
  234.             'eval'                    => array('multiple'=>true'size'=>2'tl_class'=>'w50'),
  235.             'sql'                     => "varchar(255) NOT NULL default ''"
  236.         ),
  237.         'teaser' => array
  238.         (
  239.             'exclude'                 => true,
  240.             'inputType'               => 'textarea',
  241.             'search'                  => true,
  242.             'eval'                    => array('rte'=>'tinyMCE''tl_class'=>'clr'),
  243.             'sql'                     => "text NULL"
  244.         ),
  245.         'printable' => array
  246.         (
  247.             'exclude'                 => true,
  248.             'inputType'               => 'checkbox',
  249.             'options'                 => array('print''facebook''twitter'),
  250.             'eval'                    => array('multiple'=>true),
  251.             'reference'               => &$GLOBALS['TL_LANG']['tl_article'],
  252.             'sql'                     => "varchar(255) NOT NULL default ''"
  253.         ),
  254.         'customTpl' => array
  255.         (
  256.             'exclude'                 => true,
  257.             'inputType'               => 'select',
  258.             'options_callback' => static function ()
  259.             {
  260.                 return Controller::getTemplateGroup('mod_article_', array(), 'mod_article');
  261.             },
  262.             'eval'                    => array('chosen'=>true'tl_class'=>'w50'),
  263.             'sql'                     => "varchar(64) NOT NULL default ''"
  264.         ),
  265.         'protected' => array
  266.         (
  267.             'exclude'                 => true,
  268.             'filter'                  => true,
  269.             'inputType'               => 'checkbox',
  270.             'eval'                    => array('submitOnChange'=>true),
  271.             'sql'                     => "char(1) NOT NULL default ''"
  272.         ),
  273.         'groups' => array
  274.         (
  275.             'exclude'                 => true,
  276.             'filter'                  => true,
  277.             'inputType'               => 'checkbox',
  278.             'foreignKey'              => 'tl_member_group.name',
  279.             'eval'                    => array('mandatory'=>true'multiple'=>true),
  280.             'sql'                     => "blob NULL",
  281.             'relation'                => array('type'=>'hasMany''load'=>'lazy')
  282.         ),
  283.         'guests' => array
  284.         (
  285.             'exclude'                 => true,
  286.             'filter'                  => true,
  287.             'inputType'               => 'checkbox',
  288.             'eval'                    => array('tl_class'=>'w50'),
  289.             'sql'                     => "char(1) NOT NULL default ''"
  290.         ),
  291.         'cssID' => array
  292.         (
  293.             'exclude'                 => true,
  294.             'inputType'               => 'text',
  295.             'eval'                    => array('multiple'=>true'size'=>2'tl_class'=>'w50 clr'),
  296.             'sql'                     => "varchar(255) NOT NULL default ''"
  297.         ),
  298.         'published' => array
  299.         (
  300.             'exclude'                 => true,
  301.             'filter'                  => true,
  302.             'inputType'               => 'checkbox',
  303.             'eval'                    => array('doNotCopy'=>true),
  304.             'sql'                     => "char(1) NOT NULL default ''"
  305.         ),
  306.         'start' => array
  307.         (
  308.             'exclude'                 => true,
  309.             'inputType'               => 'text',
  310.             'eval'                    => array('rgxp'=>'datim''datepicker'=>true'tl_class'=>'w50 wizard'),
  311.             'sql'                     => "varchar(10) NOT NULL default ''"
  312.         ),
  313.         'stop' => array
  314.         (
  315.             'exclude'                 => true,
  316.             'inputType'               => 'text',
  317.             'eval'                    => array('rgxp'=>'datim''datepicker'=>true'tl_class'=>'w50 wizard'),
  318.             'sql'                     => "varchar(10) NOT NULL default ''"
  319.         )
  320.     )
  321. );
  322. /**
  323.  * Provide miscellaneous methods that are used by the data configuration array.
  324.  *
  325.  * @author Leo Feyer <https://github.com/leofeyer>
  326.  */
  327. class tl_article extends Backend
  328. {
  329.     /**
  330.      * Import the back end user object
  331.      */
  332.     public function __construct()
  333.     {
  334.         parent::__construct();
  335.         $this->import(BackendUser::class, 'User');
  336.     }
  337.     /**
  338.      * Check permissions to edit table tl_page
  339.      *
  340.      * @throws AccessDeniedException
  341.      */
  342.     public function checkPermission()
  343.     {
  344.         if ($this->User->isAdmin)
  345.         {
  346.             return;
  347.         }
  348.         /** @var SessionInterface $objSession */
  349.         $objSession System::getContainer()->get('session');
  350.         $session $objSession->all();
  351.         // Set the default page user and group
  352.         $GLOBALS['TL_DCA']['tl_page']['fields']['cuser']['default'] = (int) Config::get('defaultUser') ?: $this->User->id;
  353.         $GLOBALS['TL_DCA']['tl_page']['fields']['cgroup']['default'] = (int) Config::get('defaultGroup') ?: (int) $this->User->groups[0];
  354.         // Restrict the page tree
  355.         if (empty($this->User->pagemounts) || !is_array($this->User->pagemounts))
  356.         {
  357.             $root = array(0);
  358.         }
  359.         else
  360.         {
  361.             $root $this->User->pagemounts;
  362.         }
  363.         $GLOBALS['TL_DCA']['tl_page']['list']['sorting']['root'] = $root;
  364.         // Set allowed page IDs (edit multiple)
  365.         if (is_array($session['CURRENT']['IDS']))
  366.         {
  367.             $edit_all = array();
  368.             $delete_all = array();
  369.             foreach ($session['CURRENT']['IDS'] as $id)
  370.             {
  371.                 $objArticle $this->Database->prepare("SELECT p.pid, p.includeChmod, p.chmod, p.cuser, p.cgroup FROM tl_article a, tl_page p WHERE a.id=? AND a.pid=p.id")
  372.                                              ->limit(1)
  373.                                              ->execute($id);
  374.                 if ($objArticle->numRows 1)
  375.                 {
  376.                     continue;
  377.                 }
  378.                 $row $objArticle->row();
  379.                 if ($this->User->isAllowed(BackendUser::CAN_EDIT_ARTICLES$row))
  380.                 {
  381.                     $edit_all[] = $id;
  382.                 }
  383.                 if ($this->User->isAllowed(BackendUser::CAN_DELETE_ARTICLES$row))
  384.                 {
  385.                     $delete_all[] = $id;
  386.                 }
  387.             }
  388.             $session['CURRENT']['IDS'] = (Input::get('act') == 'deleteAll') ? $delete_all $edit_all;
  389.         }
  390.         // Set allowed clipboard IDs
  391.         if (isset($session['CLIPBOARD']['tl_article']) && is_array($session['CLIPBOARD']['tl_article']['id']))
  392.         {
  393.             $clipboard = array();
  394.             foreach ($session['CLIPBOARD']['tl_article']['id'] as $id)
  395.             {
  396.                 $objArticle $this->Database->prepare("SELECT p.pid, p.includeChmod, p.chmod, p.cuser, p.cgroup FROM tl_article a, tl_page p WHERE a.id=? AND a.pid=p.id")
  397.                                              ->limit(1)
  398.                                              ->execute($id);
  399.                 if ($objArticle->numRows 1)
  400.                 {
  401.                     continue;
  402.                 }
  403.                 if ($this->User->isAllowed(BackendUser::CAN_EDIT_ARTICLE_HIERARCHY$objArticle->row()))
  404.                 {
  405.                     $clipboard[] = $id;
  406.                 }
  407.             }
  408.             $session['CLIPBOARD']['tl_article']['id'] = $clipboard;
  409.         }
  410.         $permission 0;
  411.         // Overwrite the session
  412.         $objSession->replace($session);
  413.         // Check current action
  414.         if (Input::get('act') && Input::get('act') != 'paste')
  415.         {
  416.             // Set ID of the article's page
  417.             $objPage $this->Database->prepare("SELECT pid FROM tl_article WHERE id=?")
  418.                                       ->limit(1)
  419.                                       ->execute(Input::get('id'));
  420.             $ids $objPage->numRows ? array($objPage->pid) : array();
  421.             // Set permission
  422.             switch (Input::get('act'))
  423.             {
  424.                 case 'edit':
  425.                 case 'toggle':
  426.                     $permission BackendUser::CAN_EDIT_ARTICLES;
  427.                     break;
  428.                 case 'move':
  429.                     $permission BackendUser::CAN_EDIT_ARTICLE_HIERARCHY;
  430.                     $ids[] = Input::get('sid');
  431.                     break;
  432.                 // Do not insert articles into a website root page
  433.                 case 'create':
  434.                 case 'copy':
  435.                 case 'copyAll':
  436.                 case 'cut':
  437.                 case 'cutAll':
  438.                     $permission BackendUser::CAN_EDIT_ARTICLE_HIERARCHY;
  439.                     // Insert into a page
  440.                     if (Input::get('mode') == 2)
  441.                     {
  442.                         $objParent $this->Database->prepare("SELECT id, type FROM tl_page WHERE id=?")
  443.                                                     ->limit(1)
  444.                                                     ->execute(Input::get('pid'));
  445.                         $ids[] = Input::get('pid');
  446.                     }
  447.                     // Insert after an article
  448.                     else
  449.                     {
  450.                         $objParent $this->Database->prepare("SELECT id, type FROM tl_page WHERE id=(SELECT pid FROM tl_article WHERE id=?)")
  451.                                                     ->limit(1)
  452.                                                     ->execute(Input::get('pid'));
  453.                         $ids[] = $objParent->id;
  454.                     }
  455.                     if ($objParent->numRows && $objParent->type == 'root')
  456.                     {
  457.                         throw new AccessDeniedException('Attempt to insert an article into website root page ID ' Input::get('pid') . '.');
  458.                     }
  459.                     break;
  460.                 case 'delete':
  461.                     $permission BackendUser::CAN_DELETE_ARTICLES;
  462.                     break;
  463.             }
  464.             // Check user permissions
  465.             $pagemounts = array();
  466.             // Get all allowed pages for the current user
  467.             foreach ($this->User->pagemounts as $root)
  468.             {
  469.                 $pagemounts[] = array($root);
  470.                 $pagemounts[] = $this->Database->getChildRecords($root'tl_page');
  471.             }
  472.             if (!empty($pagemounts))
  473.             {
  474.                 $pagemounts array_merge(...$pagemounts);
  475.             }
  476.             $pagemounts array_unique($pagemounts);
  477.             // Check each page
  478.             foreach ($ids as $id)
  479.             {
  480.                 if (!in_array($id$pagemounts))
  481.                 {
  482.                     throw new AccessDeniedException('Page ID ' $id ' is not mounted.');
  483.                 }
  484.                 if (Input::get('act') == 'show')
  485.                 {
  486.                     continue;
  487.                 }
  488.                 $objPage PageModel::findById($id);
  489.                 // Check whether the current user has permission for the current page
  490.                 if ($objPage !== null && !$this->User->isAllowed($permission$objPage->row()))
  491.                 {
  492.                     throw new AccessDeniedException('Not enough permissions to ' Input::get('act') . ' ' . (Input::get('id') ? 'article ID ' Input::get('id') : ' articles') . ' on page ID ' $id ' or to paste it/them into page ID ' $id '.');
  493.                 }
  494.             }
  495.         }
  496.     }
  497.     /**
  498.      * Add an image to each page in the tree
  499.      *
  500.      * @param array  $row
  501.      * @param string $label
  502.      *
  503.      * @return string
  504.      */
  505.     public function addIcon($row$label)
  506.     {
  507.         $image 'articles';
  508.         $unpublished = ($row['start'] && $row['start'] > time()) || ($row['stop'] && $row['stop'] <= time());
  509.         if ($unpublished || !$row['published'])
  510.         {
  511.             $image .= '_';
  512.         }
  513.         return '<a href="contao/preview.php?page=' $row['pid'] . '&amp;article=' . ($row['alias'] ?: $row['id']) . '" title="' StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['view']) . '" target="_blank">' Image::getHtml($image '.svg''''data-icon="' . ($unpublished $image rtrim($image'_')) . '.svg" data-icon-disabled="' rtrim($image'_') . '_.svg"') . '</a> ' $label;
  514.     }
  515.     /**
  516.      * Auto-generate an article alias if it has not been set yet
  517.      *
  518.      * @param mixed         $varValue
  519.      * @param DataContainer $dc
  520.      *
  521.      * @return string
  522.      *
  523.      * @throws Exception
  524.      */
  525.     public function generateAlias($varValueDataContainer $dc)
  526.     {
  527.         $aliasExists = function (string $alias) use ($dc): bool
  528.         {
  529.             if (in_array($alias, array('top''wrapper''header''container''main''left''right''footer'), true))
  530.             {
  531.                 return true;
  532.             }
  533.             return $this->Database->prepare("SELECT id FROM tl_article WHERE alias=? AND id!=?")->execute($alias$dc->id)->numRows 0;
  534.         };
  535.         // Generate an alias if there is none
  536.         if (!$varValue)
  537.         {
  538.             $varValue System::getContainer()->get('contao.slug')->generate($dc->activeRecord->title$dc->activeRecord->pid$aliasExists);
  539.         }
  540.         elseif (preg_match('/^[1-9]\d*$/'$varValue))
  541.         {
  542.             throw new Exception(sprintf($GLOBALS['TL_LANG']['ERR']['aliasNumeric'], $varValue));
  543.         }
  544.         elseif ($aliasExists($varValue))
  545.         {
  546.             throw new Exception(sprintf($GLOBALS['TL_LANG']['ERR']['aliasExists'], $varValue));
  547.         }
  548.         return $varValue;
  549.     }
  550.     /**
  551.      * Return all active layout sections as array
  552.      *
  553.      * @param DataContainer $dc
  554.      *
  555.      * @return array
  556.      */
  557.     public function getActiveLayoutSections(DataContainer $dc)
  558.     {
  559.         // Show only active sections
  560.         if ($dc->activeRecord->pid ?? null)
  561.         {
  562.             $arrSections = array();
  563.             $objPage PageModel::findWithDetails($dc->activeRecord->pid);
  564.             // Get the layout sections
  565.             if ($objPage->layout)
  566.             {
  567.                 $objLayout LayoutModel::findByPk($objPage->layout);
  568.                 if ($objLayout === null)
  569.                 {
  570.                     return array();
  571.                 }
  572.                 $arrModules StringUtil::deserialize($objLayout->modules);
  573.                 if (empty($arrModules) || !is_array($arrModules))
  574.                 {
  575.                     return array();
  576.                 }
  577.                 // Find all sections with an article module (see #6094)
  578.                 foreach ($arrModules as $arrModule)
  579.                 {
  580.                     if ($arrModule['mod'] == && $arrModule['enable'])
  581.                     {
  582.                         $arrSections[] = $arrModule['col'];
  583.                     }
  584.                 }
  585.             }
  586.         }
  587.         // Show all sections (e.g. "override all" mode)
  588.         else
  589.         {
  590.             $arrSections = array('header''left''right''main''footer');
  591.             $objLayout $this->Database->query("SELECT sections FROM tl_layout WHERE sections!=''");
  592.             while ($objLayout->next())
  593.             {
  594.                 $arrCustom StringUtil::deserialize($objLayout->sections);
  595.                 // Add the custom layout sections
  596.                 if (!empty($arrCustom) && is_array($arrCustom))
  597.                 {
  598.                     foreach ($arrCustom as $v)
  599.                     {
  600.                         if (!empty($v['id']))
  601.                         {
  602.                             $arrSections[] = $v['id'];
  603.                         }
  604.                     }
  605.                 }
  606.             }
  607.         }
  608.         return Backend::convertLayoutSectionIdsToAssociativeArray($arrSections);
  609.     }
  610.     /**
  611.      * Return the edit article button
  612.      *
  613.      * @param array  $row
  614.      * @param string $href
  615.      * @param string $label
  616.      * @param string $title
  617.      * @param string $icon
  618.      * @param string $attributes
  619.      *
  620.      * @return string
  621.      */
  622.     public function editArticle($row$href$label$title$icon$attributes)
  623.     {
  624.         $objPage PageModel::findById($row['pid']);
  625.         return $this->User->isAllowed(BackendUser::CAN_EDIT_ARTICLES$objPage->row()) ? '<a href="' $this->addToUrl($href '&amp;id=' $row['id']) . '" title="' StringUtil::specialchars($title) . '"' $attributes '>' Image::getHtml($icon$label) . '</a> ' Image::getHtml(preg_replace('/\.svg$/i''_.svg'$icon)) . ' ';
  626.     }
  627.     /**
  628.      * Return the edit header button
  629.      *
  630.      * @param array  $row
  631.      * @param string $href
  632.      * @param string $label
  633.      * @param string $title
  634.      * @param string $icon
  635.      * @param string $attributes
  636.      *
  637.      * @return string
  638.      */
  639.     public function editHeader($row$href$label$title$icon$attributes)
  640.     {
  641.         if (!$this->User->canEditFieldsOf('tl_article'))
  642.         {
  643.             return Image::getHtml(preg_replace('/\.svg$/i''_.svg'$icon)) . ' ';
  644.         }
  645.         $objPage PageModel::findById($row['pid']);
  646.         return $this->User->isAllowed(BackendUser::CAN_EDIT_ARTICLES$objPage->row()) ? '<a href="' $this->addToUrl($href '&amp;id=' $row['id']) . '" title="' StringUtil::specialchars($title) . '"' $attributes '>' Image::getHtml($icon$label) . '</a> ' Image::getHtml(preg_replace('/\.svg$/i''_.svg'$icon)) . ' ';
  647.     }
  648.     /**
  649.      * Return the copy article button
  650.      *
  651.      * @param array  $row
  652.      * @param string $href
  653.      * @param string $label
  654.      * @param string $title
  655.      * @param string $icon
  656.      * @param string $attributes
  657.      * @param string $table
  658.      *
  659.      * @return string
  660.      */
  661.     public function copyArticle($row$href$label$title$icon$attributes$table)
  662.     {
  663.         if ($GLOBALS['TL_DCA'][$table]['config']['closed'] ?? null)
  664.         {
  665.             return '';
  666.         }
  667.         $objPage PageModel::findById($row['pid']);
  668.         return $this->User->isAllowed(BackendUser::CAN_EDIT_ARTICLE_HIERARCHY$objPage->row()) ? '<a href="' $this->addToUrl($href '&amp;id=' $row['id']) . '" title="' StringUtil::specialchars($title) . '"' $attributes '>' Image::getHtml($icon$label) . '</a> ' Image::getHtml(preg_replace('/\.svg$/i''_.svg'$icon)) . ' ';
  669.     }
  670.     /**
  671.      * Return the cut article button
  672.      *
  673.      * @param array  $row
  674.      * @param string $href
  675.      * @param string $label
  676.      * @param string $title
  677.      * @param string $icon
  678.      * @param string $attributes
  679.      *
  680.      * @return string
  681.      */
  682.     public function cutArticle($row$href$label$title$icon$attributes)
  683.     {
  684.         $objPage PageModel::findById($row['pid']);
  685.         return $this->User->isAllowed(BackendUser::CAN_EDIT_ARTICLE_HIERARCHY$objPage->row()) ? '<a href="' $this->addToUrl($href '&amp;id=' $row['id']) . '" title="' StringUtil::specialchars($title) . '"' $attributes '>' Image::getHtml($icon$label) . '</a> ' Image::getHtml(preg_replace('/\.svg$/i''_.svg'$icon)) . ' ';
  686.     }
  687.     /**
  688.      * Return the paste article button
  689.      *
  690.      * @param DataContainer $dc
  691.      * @param array         $row
  692.      * @param string        $table
  693.      * @param boolean       $cr
  694.      * @param array         $arrClipboard
  695.      *
  696.      * @return string
  697.      *
  698.      * @deprecated
  699.      */
  700.     public function pasteArticle(DataContainer $dc$row$table$cr$arrClipboard=null)
  701.     {
  702.         trigger_deprecation('contao/core-bundle''4.10''Using "tl_article::pasteArticle()" has been deprecated and will no longer work in Contao 5.0.');
  703.         return System::getContainer()
  704.             ->get(ContentCompositionListener::class)
  705.             ->renderArticlePasteButton($dc$row$table$cr$arrClipboard)
  706.         ;
  707.     }
  708.     /**
  709.      * Return the delete article button
  710.      *
  711.      * @param array  $row
  712.      * @param string $href
  713.      * @param string $label
  714.      * @param string $title
  715.      * @param string $icon
  716.      * @param string $attributes
  717.      *
  718.      * @return string
  719.      */
  720.     public function deleteArticle($row$href$label$title$icon$attributes)
  721.     {
  722.         $objPage PageModel::findById($row['pid']);
  723.         return $this->User->isAllowed(BackendUser::CAN_DELETE_ARTICLES$objPage->row()) ? '<a href="' $this->addToUrl($href '&amp;id=' $row['id']) . '" title="' StringUtil::specialchars($title) . '"' $attributes '>' Image::getHtml($icon$label) . '</a> ' Image::getHtml(preg_replace('/\.svg$/i''_.svg'$icon)) . ' ';
  724.     }
  725.     /**
  726.      * Automatically generate the folder URL aliases
  727.      *
  728.      * @param array $arrButtons
  729.      *
  730.      * @return array
  731.      */
  732.     public function addAliasButton($arrButtons)
  733.     {
  734.         if (!$this->User->hasAccess('tl_article::alias''alexf'))
  735.         {
  736.             return $arrButtons;
  737.         }
  738.         // Generate the aliases
  739.         if (isset($_POST['alias']) && Input::post('FORM_SUBMIT') == 'tl_select')
  740.         {
  741.             /** @var SessionInterface $objSession */
  742.             $objSession System::getContainer()->get('session');
  743.             $session $objSession->all();
  744.             $ids $session['CURRENT']['IDS'] ?? array();
  745.             foreach ($ids as $id)
  746.             {
  747.                 $objArticle ArticleModel::findByPk($id);
  748.                 if ($objArticle === null)
  749.                 {
  750.                     continue;
  751.                 }
  752.                 $strAlias System::getContainer()->get('contao.slug')->generate($objArticle->title$objArticle->pid);
  753.                 // The alias has not changed
  754.                 if ($strAlias == $objArticle->alias)
  755.                 {
  756.                     continue;
  757.                 }
  758.                 // Initialize the version manager
  759.                 $objVersions = new Versions('tl_article'$id);
  760.                 $objVersions->initialize();
  761.                 // Store the new alias
  762.                 $this->Database->prepare("UPDATE tl_article SET alias=? WHERE id=?")
  763.                                ->execute($strAlias$id);
  764.                 // Create a new version
  765.                 $objVersions->create();
  766.             }
  767.             $this->redirect($this->getReferer());
  768.         }
  769.         // Add the button
  770.         $arrButtons['alias'] = '<button type="submit" name="alias" id="alias" class="tl_submit" accesskey="a">' $GLOBALS['TL_LANG']['MSC']['aliasSelected'] . '</button> ';
  771.         return $arrButtons;
  772.     }
  773.     /**
  774.      * Return the "toggle visibility" button
  775.      *
  776.      * @param array  $row
  777.      * @param string $href
  778.      * @param string $label
  779.      * @param string $title
  780.      * @param string $icon
  781.      * @param string $attributes
  782.      *
  783.      * @return string
  784.      */
  785.     public function toggleIcon($row$href$label$title$icon$attributes)
  786.     {
  787.         if (Input::get('tid'))
  788.         {
  789.             $this->toggleVisibility(Input::get('tid'), (Input::get('state') == 1), (func_num_args() <= 12 null func_get_arg(12)));
  790.             $this->redirect($this->getReferer());
  791.         }
  792.         // Check permissions AFTER checking the tid, so hacking attempts are logged
  793.         if (!$this->User->hasAccess('tl_article::published''alexf'))
  794.         {
  795.             return '';
  796.         }
  797.         $href .= '&amp;tid=' $row['id'] . '&amp;state=' . ($row['published'] ? '' 1);
  798.         if (!$row['published'])
  799.         {
  800.             $icon 'invisible.svg';
  801.         }
  802.         $objPage PageModel::findById($row['pid']);
  803.         if (!$this->User->isAllowed(BackendUser::CAN_EDIT_ARTICLES$objPage->row()))
  804.         {
  805.             if ($row['published'])
  806.             {
  807.                 $icon preg_replace('/\.svg$/i''_.svg'$icon); // see #8126
  808.             }
  809.             return Image::getHtml($icon) . ' ';
  810.         }
  811.         return '<a href="' $this->addToUrl($href) . '" title="' StringUtil::specialchars($title) . '"' $attributes '>' Image::getHtml($icon$label'data-state="' . ($row['published'] ? 0) . '"') . '</a> ';
  812.     }
  813.     /**
  814.      * Disable/enable a user group
  815.      *
  816.      * @param integer       $intId
  817.      * @param boolean       $blnVisible
  818.      * @param DataContainer $dc
  819.      *
  820.      * @throws AccessDeniedException
  821.      */
  822.     public function toggleVisibility($intId$blnVisibleDataContainer $dc=null)
  823.     {
  824.         // Set the ID and action
  825.         Input::setGet('id'$intId);
  826.         Input::setGet('act''toggle');
  827.         if ($dc)
  828.         {
  829.             $dc->id $intId// see #8043
  830.         }
  831.         // Trigger the onload_callback
  832.         if (is_array($GLOBALS['TL_DCA']['tl_article']['config']['onload_callback'] ?? null))
  833.         {
  834.             foreach ($GLOBALS['TL_DCA']['tl_article']['config']['onload_callback'] as $callback)
  835.             {
  836.                 if (is_array($callback))
  837.                 {
  838.                     $this->import($callback[0]);
  839.                     $this->{$callback[0]}->{$callback[1]}($dc);
  840.                 }
  841.                 elseif (is_callable($callback))
  842.                 {
  843.                     $callback($dc);
  844.                 }
  845.             }
  846.         }
  847.         // Check the field access
  848.         if (!$this->User->hasAccess('tl_article::published''alexf'))
  849.         {
  850.             throw new AccessDeniedException('Not enough permissions to publish/unpublish article ID "' $intId '".');
  851.         }
  852.         $objRow $this->Database->prepare("SELECT * FROM tl_article WHERE id=?")
  853.                                  ->limit(1)
  854.                                  ->execute($intId);
  855.         if ($objRow->numRows 1)
  856.         {
  857.             throw new AccessDeniedException('Invalid article ID "' $intId '".');
  858.         }
  859.         // Set the current record
  860.         if ($dc)
  861.         {
  862.             $dc->activeRecord $objRow;
  863.         }
  864.         $objVersions = new Versions('tl_article'$intId);
  865.         $objVersions->initialize();
  866.         // Trigger the save_callback
  867.         if (is_array($GLOBALS['TL_DCA']['tl_article']['fields']['published']['save_callback'] ?? null))
  868.         {
  869.             foreach ($GLOBALS['TL_DCA']['tl_article']['fields']['published']['save_callback'] as $callback)
  870.             {
  871.                 if (is_array($callback))
  872.                 {
  873.                     $this->import($callback[0]);
  874.                     $blnVisible $this->{$callback[0]}->{$callback[1]}($blnVisible$dc);
  875.                 }
  876.                 elseif (is_callable($callback))
  877.                 {
  878.                     $blnVisible $callback($blnVisible$dc);
  879.                 }
  880.             }
  881.         }
  882.         $time time();
  883.         // Update the database
  884.         $this->Database->prepare("UPDATE tl_article SET tstamp=$time, published='" . ($blnVisible '1' '') . "' WHERE id=?")
  885.                        ->execute($intId);
  886.         if ($dc)
  887.         {
  888.             $dc->activeRecord->tstamp $time;
  889.             $dc->activeRecord->published = ($blnVisible '1' '');
  890.         }
  891.         // Trigger the onsubmit_callback
  892.         if (is_array($GLOBALS['TL_DCA']['tl_article']['config']['onsubmit_callback'] ?? null))
  893.         {
  894.             foreach ($GLOBALS['TL_DCA']['tl_article']['config']['onsubmit_callback'] as $callback)
  895.             {
  896.                 if (is_array($callback))
  897.                 {
  898.                     $this->import($callback[0]);
  899.                     $this->{$callback[0]}->{$callback[1]}($dc);
  900.                 }
  901.                 elseif (is_callable($callback))
  902.                 {
  903.                     $callback($dc);
  904.                 }
  905.             }
  906.         }
  907.         $objVersions->create();
  908.         if ($dc)
  909.         {
  910.             $dc->invalidateCacheTags();
  911.         }
  912.     }
  913. }