vendor/contao/news-bundle/src/Resources/contao/classes/News.php line 358

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. namespace Contao;
  10. use Symfony\Component\HttpFoundation\Request;
  11. use Symfony\Component\HttpFoundation\RequestStack;
  12. use Symfony\Component\HttpFoundation\Session\Session;
  13. use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
  14. /**
  15.  * Provide methods regarding news archives.
  16.  */
  17. class News extends Frontend
  18. {
  19.     /**
  20.      * URL cache array
  21.      * @var array
  22.      */
  23.     private static $arrUrlCache = array();
  24.     /**
  25.      * Page cache array
  26.      * @var array
  27.      */
  28.     private static $arrPageCache = array();
  29.     /**
  30.      * Update a particular RSS feed
  31.      *
  32.      * @param integer $intId
  33.      */
  34.     public function generateFeed($intId)
  35.     {
  36.         $objFeed NewsFeedModel::findByPk($intId);
  37.         if ($objFeed === null)
  38.         {
  39.             return;
  40.         }
  41.         $objFeed->feedName $objFeed->alias ?: 'news' $objFeed->id;
  42.         // Delete XML file
  43.         if (Input::get('act') == 'delete')
  44.         {
  45.             $this->import(Files::class, 'Files');
  46.             $this->Files->delete($objFeed->feedName '.xml');
  47.         }
  48.         // Update XML file
  49.         else
  50.         {
  51.             $this->generateFiles($objFeed->row());
  52.             System::getContainer()->get('monolog.logger.contao.cron')->info('Generated news feed "' $objFeed->feedName '.xml"');
  53.         }
  54.     }
  55.     /**
  56.      * Delete old files and generate all feeds
  57.      */
  58.     public function generateFeeds()
  59.     {
  60.         $this->import(Automator::class, 'Automator');
  61.         $this->Automator->purgeXmlFiles();
  62.         $objFeed NewsFeedModel::findAll();
  63.         if ($objFeed !== null)
  64.         {
  65.             while ($objFeed->next())
  66.             {
  67.                 $objFeed->feedName $objFeed->alias ?: 'news' $objFeed->id;
  68.                 $this->generateFiles($objFeed->row());
  69.                 System::getContainer()->get('monolog.logger.contao.cron')->info('Generated news feed "' $objFeed->feedName '.xml"');
  70.             }
  71.         }
  72.     }
  73.     /**
  74.      * Generate all feeds including a certain archive
  75.      * #
  76.      * @param integer $intId
  77.      */
  78.     public function generateFeedsByArchive($intId)
  79.     {
  80.         $objFeed NewsFeedModel::findByArchive($intId);
  81.         if ($objFeed !== null)
  82.         {
  83.             while ($objFeed->next())
  84.             {
  85.                 $objFeed->feedName $objFeed->alias ?: 'news' $objFeed->id;
  86.                 // Update the XML file
  87.                 $this->generateFiles($objFeed->row());
  88.                 System::getContainer()->get('monolog.logger.contao.cron')->info('Generated news feed "' $objFeed->feedName '.xml"');
  89.             }
  90.         }
  91.     }
  92.     /**
  93.      * Generate an XML files and save them to the root directory
  94.      *
  95.      * @param array $arrFeed
  96.      */
  97.     protected function generateFiles($arrFeed)
  98.     {
  99.         $arrArchives StringUtil::deserialize($arrFeed['archives']);
  100.         if (empty($arrArchives) || !\is_array($arrArchives))
  101.         {
  102.             return;
  103.         }
  104.         $strType = ($arrFeed['format'] == 'atom') ? 'generateAtom' 'generateRss';
  105.         $strLink $arrFeed['feedBase'] ?: Environment::get('base');
  106.         $strFile $arrFeed['feedName'];
  107.         $objFeed = new Feed($strFile);
  108.         $objFeed->link $strLink;
  109.         $objFeed->title $arrFeed['title'];
  110.         $objFeed->description $arrFeed['description'];
  111.         $objFeed->language $arrFeed['language'];
  112.         $objFeed->published $arrFeed['tstamp'];
  113.         // Get the items
  114.         if ($arrFeed['maxItems'] > 0)
  115.         {
  116.             $objArticle NewsModel::findPublishedByPids($arrArchivesnull$arrFeed['maxItems']);
  117.         }
  118.         else
  119.         {
  120.             $objArticle NewsModel::findPublishedByPids($arrArchives);
  121.         }
  122.         $container System::getContainer();
  123.         // Parse the items
  124.         if ($objArticle !== null)
  125.         {
  126.             $arrUrls = array();
  127.             /** @var RequestStack $requestStack */
  128.             $requestStack $container->get('request_stack');
  129.             $currentRequest $requestStack->getCurrentRequest();
  130.             $time time();
  131.             $origObjPage $GLOBALS['objPage'] ?? null;
  132.             while ($objArticle->next())
  133.             {
  134.                 // Never add unpublished elements to the RSS feeds
  135.                 if (!$objArticle->published || ($objArticle->start && $objArticle->start $time) || ($objArticle->stop && $objArticle->stop <= $time))
  136.                 {
  137.                     continue;
  138.                 }
  139.                 $jumpTo $objArticle->getRelated('pid')->jumpTo;
  140.                 // No jumpTo page set (see #4784)
  141.                 if (!$jumpTo)
  142.                 {
  143.                     continue;
  144.                 }
  145.                 $objParent $this->getPageWithDetails($jumpTo);
  146.                 // A jumpTo page is set but does no longer exist (see #5781)
  147.                 if ($objParent === null)
  148.                 {
  149.                     continue;
  150.                 }
  151.                 // Override the global page object (#2946)
  152.                 $GLOBALS['objPage'] = $objParent;
  153.                 // Get the jumpTo URL
  154.                 if (!isset($arrUrls[$jumpTo]))
  155.                 {
  156.                     $arrUrls[$jumpTo] = $objParent->getAbsoluteUrl(Config::get('useAutoItem') ? '/%s' '/items/%s');
  157.                 }
  158.                 $strUrl $arrUrls[$jumpTo];
  159.                 $objItem = new FeedItem();
  160.                 $objItem->title $objArticle->headline;
  161.                 $objItem->link $this->getLink($objArticle$strUrl);
  162.                 $objItem->published $objArticle->date;
  163.                 // Push a new request to the request stack (#3856)
  164.                 $request $this->createSubRequest($objItem->link$currentRequest);
  165.                 $request->attributes->set('_scope''frontend');
  166.                 $requestStack->push($request);
  167.                 /** @var UserModel $objAuthor */
  168.                 if (($objAuthor $objArticle->getRelated('author')) instanceof UserModel)
  169.                 {
  170.                     $objItem->author $objAuthor->name;
  171.                 }
  172.                 // Prepare the description
  173.                 if ($arrFeed['source'] == 'source_text')
  174.                 {
  175.                     $strDescription '';
  176.                     $objElement ContentModel::findPublishedByPidAndTable($objArticle->id'tl_news');
  177.                     if ($objElement !== null)
  178.                     {
  179.                         // Overwrite the request (see #7756)
  180.                         $strRequest Environment::get('request');
  181.                         Environment::set('request'$objItem->link);
  182.                         while ($objElement->next())
  183.                         {
  184.                             $strDescription .= $this->getContentElement($objElement->current());
  185.                         }
  186.                         Environment::set('request'$strRequest);
  187.                     }
  188.                 }
  189.                 else
  190.                 {
  191.                     $strDescription $objArticle->teaser ?? '';
  192.                 }
  193.                 $strDescription $container->get('contao.insert_tag.parser')->replaceInline($strDescription);
  194.                 $objItem->description $this->convertRelativeUrls($strDescription$strLink);
  195.                 // Add the article image as enclosure
  196.                 if ($objArticle->addImage)
  197.                 {
  198.                     $objFile FilesModel::findByUuid($objArticle->singleSRC);
  199.                     if ($objFile !== null)
  200.                     {
  201.                         $objItem->addEnclosure($objFile->path$strLink'media:content'$arrFeed['imgSize']);
  202.                     }
  203.                 }
  204.                 // Enclosures
  205.                 if ($objArticle->addEnclosure)
  206.                 {
  207.                     $arrEnclosure StringUtil::deserialize($objArticle->enclosuretrue);
  208.                     if (\is_array($arrEnclosure))
  209.                     {
  210.                         $objFile FilesModel::findMultipleByUuids($arrEnclosure);
  211.                         if ($objFile !== null)
  212.                         {
  213.                             while ($objFile->next())
  214.                             {
  215.                                 $objItem->addEnclosure($objFile->path$strLink);
  216.                             }
  217.                         }
  218.                     }
  219.                 }
  220.                 $objFeed->addItem($objItem);
  221.                 $requestStack->pop();
  222.             }
  223.             $GLOBALS['objPage'] = $origObjPage;
  224.         }
  225.         $webDir StringUtil::stripRootDir($container->getParameter('contao.web_dir'));
  226.         // Create the file
  227.         File::putContent($webDir '/share/' $strFile '.xml'$container->get('contao.insert_tag.parser')->replaceInline($objFeed->$strType()));
  228.     }
  229.     /**
  230.      * Add news items to the indexer
  231.      *
  232.      * @param array   $arrPages
  233.      * @param integer $intRoot
  234.      * @param boolean $blnIsSitemap
  235.      *
  236.      * @return array
  237.      */
  238.     public function getSearchablePages($arrPages$intRoot=0$blnIsSitemap=false)
  239.     {
  240.         $arrRoot = array();
  241.         if ($intRoot 0)
  242.         {
  243.             $arrRoot $this->Database->getChildRecords($intRoot'tl_page');
  244.         }
  245.         $arrProcessed = array();
  246.         $time time();
  247.         // Get all news archives
  248.         $objArchive NewsArchiveModel::findByProtected('');
  249.         // Walk through each archive
  250.         if ($objArchive !== null)
  251.         {
  252.             while ($objArchive->next())
  253.             {
  254.                 // Skip news archives without target page
  255.                 if (!$objArchive->jumpTo)
  256.                 {
  257.                     continue;
  258.                 }
  259.                 // Skip news archives outside the root nodes
  260.                 if (!empty($arrRoot) && !\in_array($objArchive->jumpTo$arrRoot))
  261.                 {
  262.                     continue;
  263.                 }
  264.                 // Get the URL of the jumpTo page
  265.                 if (!isset($arrProcessed[$objArchive->jumpTo]))
  266.                 {
  267.                     $objParent PageModel::findWithDetails($objArchive->jumpTo);
  268.                     // The target page does not exist
  269.                     if ($objParent === null)
  270.                     {
  271.                         continue;
  272.                     }
  273.                     // The target page has not been published (see #5520)
  274.                     if (!$objParent->published || ($objParent->start && $objParent->start $time) || ($objParent->stop && $objParent->stop <= $time))
  275.                     {
  276.                         continue;
  277.                     }
  278.                     if ($blnIsSitemap)
  279.                     {
  280.                         // The target page is protected (see #8416)
  281.                         if ($objParent->protected)
  282.                         {
  283.                             continue;
  284.                         }
  285.                         // The target page is exempt from the sitemap (see #6418)
  286.                         if ($objParent->robots == 'noindex,nofollow')
  287.                         {
  288.                             continue;
  289.                         }
  290.                     }
  291.                     // Generate the URL
  292.                     $arrProcessed[$objArchive->jumpTo] = $objParent->getAbsoluteUrl(Config::get('useAutoItem') ? '/%s' '/items/%s');
  293.                 }
  294.                 $strUrl $arrProcessed[$objArchive->jumpTo];
  295.                 // Get the items
  296.                 $objArticle NewsModel::findPublishedDefaultByPid($objArchive->id);
  297.                 if ($objArticle !== null)
  298.                 {
  299.                     while ($objArticle->next())
  300.                     {
  301.                         if ($blnIsSitemap && $objArticle->robots === 'noindex,nofollow')
  302.                         {
  303.                             continue;
  304.                         }
  305.                         $arrPages[] = $this->getLink($objArticle$strUrl);
  306.                     }
  307.                 }
  308.             }
  309.         }
  310.         return $arrPages;
  311.     }
  312.     /**
  313.      * Generate a URL and return it as string
  314.      *
  315.      * @param NewsModel $objItem
  316.      * @param boolean   $blnAddArchive
  317.      * @param boolean   $blnAbsolute
  318.      *
  319.      * @return string
  320.      */
  321.     public static function generateNewsUrl($objItem$blnAddArchive=false$blnAbsolute=false)
  322.     {
  323.         $strCacheKey 'id_' $objItem->id . ($blnAbsolute '_absolute' '');
  324.         // Load the URL from cache
  325.         if (isset(self::$arrUrlCache[$strCacheKey]))
  326.         {
  327.             return self::$arrUrlCache[$strCacheKey];
  328.         }
  329.         // Initialize the cache
  330.         self::$arrUrlCache[$strCacheKey] = null;
  331.         switch ($objItem->source)
  332.         {
  333.             // Link to an external page
  334.             case 'external':
  335.                 if (=== strncmp($objItem->url'mailto:'7))
  336.                 {
  337.                     self::$arrUrlCache[$strCacheKey] = StringUtil::encodeEmail($objItem->url);
  338.                 }
  339.                 else
  340.                 {
  341.                     self::$arrUrlCache[$strCacheKey] = StringUtil::ampersand($objItem->url);
  342.                 }
  343.                 break;
  344.             // Link to an internal page
  345.             case 'internal':
  346.                 if (($objTarget $objItem->getRelated('jumpTo')) instanceof PageModel)
  347.                 {
  348.                     /** @var PageModel $objTarget */
  349.                     self::$arrUrlCache[$strCacheKey] = StringUtil::ampersand($blnAbsolute $objTarget->getAbsoluteUrl() : $objTarget->getFrontendUrl());
  350.                 }
  351.                 break;
  352.             // Link to an article
  353.             case 'article':
  354.                 if (($objArticle ArticleModel::findByPk($objItem->articleId)) instanceof ArticleModel && ($objPid $objArticle->getRelated('pid')) instanceof PageModel)
  355.                 {
  356.                     $params '/articles/' . ($objArticle->alias ?: $objArticle->id);
  357.                     /** @var PageModel $objPid */
  358.                     self::$arrUrlCache[$strCacheKey] = StringUtil::ampersand($blnAbsolute $objPid->getAbsoluteUrl($params) : $objPid->getFrontendUrl($params));
  359.                 }
  360.                 break;
  361.         }
  362.         // Link to the default page
  363.         if (self::$arrUrlCache[$strCacheKey] === null)
  364.         {
  365.             $objPage PageModel::findByPk($objItem->getRelated('pid')->jumpTo);
  366.             if (!$objPage instanceof PageModel)
  367.             {
  368.                 self::$arrUrlCache[$strCacheKey] = StringUtil::ampersand(Environment::get('request'));
  369.             }
  370.             else
  371.             {
  372.                 $params = (Config::get('useAutoItem') ? '/' '/items/') . ($objItem->alias ?: $objItem->id);
  373.                 self::$arrUrlCache[$strCacheKey] = StringUtil::ampersand($blnAbsolute $objPage->getAbsoluteUrl($params) : $objPage->getFrontendUrl($params));
  374.             }
  375.             // Add the current archive parameter (news archive)
  376.             if ($blnAddArchive && Input::get('month'))
  377.             {
  378.                 self::$arrUrlCache[$strCacheKey] .= '?month=' Input::get('month');
  379.             }
  380.         }
  381.         return self::$arrUrlCache[$strCacheKey];
  382.     }
  383.     /**
  384.      * Return the schema.org data from a news article
  385.      *
  386.      * @param NewsModel $objArticle
  387.      *
  388.      * @return array
  389.      */
  390.     public static function getSchemaOrgData(NewsModel $objArticle): array
  391.     {
  392.         $htmlDecoder System::getContainer()->get('contao.string.html_decoder');
  393.         $jsonLd = array(
  394.             '@type' => 'NewsArticle',
  395.             'identifier' => '#/schema/news/' $objArticle->id,
  396.             'url' => self::generateNewsUrl($objArticle),
  397.             'headline' => $htmlDecoder->inputEncodedToPlainText($objArticle->headline),
  398.             'datePublished' => date('Y-m-d\TH:i:sP'$objArticle->date),
  399.         );
  400.         if ($objArticle->teaser)
  401.         {
  402.             $jsonLd['description'] = $htmlDecoder->htmlToPlainText($objArticle->teaser);
  403.         }
  404.         /** @var UserModel $objAuthor */
  405.         if (($objAuthor $objArticle->getRelated('author')) instanceof UserModel)
  406.         {
  407.             $jsonLd['author'] = array(
  408.                 '@type' => 'Person',
  409.                 'name' => $objAuthor->name,
  410.             );
  411.         }
  412.         return $jsonLd;
  413.     }
  414.     /**
  415.      * Return the link of a news article
  416.      *
  417.      * @param NewsModel $objItem
  418.      * @param string    $strUrl
  419.      * @param string    $strBase
  420.      *
  421.      * @return string
  422.      */
  423.     protected function getLink($objItem$strUrl$strBase='')
  424.     {
  425.         switch ($objItem->source)
  426.         {
  427.             // Link to an external page
  428.             case 'external':
  429.                 return $objItem->url;
  430.             // Link to an internal page
  431.             case 'internal':
  432.                 if (($objTarget $objItem->getRelated('jumpTo')) instanceof PageModel)
  433.                 {
  434.                     /** @var PageModel $objTarget */
  435.                     return $objTarget->getAbsoluteUrl();
  436.                 }
  437.                 break;
  438.             // Link to an article
  439.             case 'article':
  440.                 if (($objArticle ArticleModel::findByPk($objItem->articleId)) instanceof ArticleModel && ($objPid $objArticle->getRelated('pid')) instanceof PageModel)
  441.                 {
  442.                     /** @var PageModel $objPid */
  443.                     return StringUtil::ampersand($objPid->getAbsoluteUrl('/articles/' . ($objArticle->alias ?: $objArticle->id)));
  444.                 }
  445.                 break;
  446.         }
  447.         // Backwards compatibility (see #8329)
  448.         if ($strBase && !preg_match('#^https?://#'$strUrl))
  449.         {
  450.             $strUrl $strBase $strUrl;
  451.         }
  452.         // Link to the default page
  453.         return sprintf(preg_replace('/%(?!s)/''%%'$strUrl), ($objItem->alias ?: $objItem->id));
  454.     }
  455.     /**
  456.      * Return the names of the existing feeds so they are not removed
  457.      *
  458.      * @return array
  459.      */
  460.     public function purgeOldFeeds()
  461.     {
  462.         $arrFeeds = array();
  463.         $objFeeds NewsFeedModel::findAll();
  464.         if ($objFeeds !== null)
  465.         {
  466.             while ($objFeeds->next())
  467.             {
  468.                 $arrFeeds[] = $objFeeds->alias ?: 'news' $objFeeds->id;
  469.             }
  470.         }
  471.         return $arrFeeds;
  472.     }
  473.     /**
  474.      * Return the page object with loaded details for the given page ID
  475.      *
  476.      * @param  integer        $intPageId
  477.      * @return PageModel|null
  478.      */
  479.     private function getPageWithDetails($intPageId)
  480.     {
  481.         if (!\array_key_exists($intPageIdself::$arrPageCache))
  482.         {
  483.             $objPage self::$arrPageCache[$intPageId] = PageModel::findWithDetails($intPageId);
  484.             if (null === $objPage)
  485.             {
  486.                 return null;
  487.             }
  488.             $objLayout $objPage->getRelated('layout');
  489.             if (!$objLayout instanceof LayoutModel)
  490.             {
  491.                 return $objPage;
  492.             }
  493.             /** @var ThemeModel $objTheme */
  494.             $objTheme $objLayout->getRelated('pid');
  495.             $objPage->templateGroup $objTheme->templates ?? null;
  496.         }
  497.         return self::$arrPageCache[$intPageId];
  498.     }
  499.     /**
  500.      * Creates a sub request for the given URI.
  501.      */
  502.     private function createSubRequest(string $uriRequest $request null): Request
  503.     {
  504.         $cookies null !== $request $request->cookies->all() : array();
  505.         $server null !== $request $request->server->all() : array();
  506.         unset($server['HTTP_IF_MODIFIED_SINCE'], $server['HTTP_IF_NONE_MATCH']);
  507.         $subRequest Request::create($uri'get', array(), $cookies, array(), $server);
  508.         if (null !== $request)
  509.         {
  510.             if ($request->get('_format'))
  511.             {
  512.                 $subRequest->attributes->set('_format'$request->get('_format'));
  513.             }
  514.             if ($request->getDefaultLocale() !== $request->getLocale())
  515.             {
  516.                 $subRequest->setLocale($request->getLocale());
  517.             }
  518.         }
  519.         // Always set a session (#3856)
  520.         $subRequest->setSession(new Session(new MockArraySessionStorage()));
  521.         return $subRequest;
  522.     }
  523. }
  524. class_alias(News::class, 'News');