From 923f986e67413ac548cc98d6d59fa01de9681035 Mon Sep 17 00:00:00 2001 From: Devlin Junker Date: Sat, 5 Aug 2023 19:16:39 -0700 Subject: upmerged from master Signed-off-by: Devlin Junker --- lib/AppInfo/Application.php | 3 + lib/Config/FetcherConfig.php | 6 +- lib/Controller/PageController.php | 1 + lib/Db/Feed.php | 8 ++- lib/Db/ItemMapperV2.php | 7 ++- lib/Fetcher/FeedFetcher.php | 67 +++++++++++++--------- lib/Fetcher/Fetcher.php | 7 ++- lib/Fetcher/IFeedFetcher.php | 4 +- lib/Migration/RemoveUnusedJob.php | 50 +++++++++++++++++ lib/Scraper/Scraper.php | 7 ++- lib/Search/FeedSearchProvider.php | 6 +- lib/Search/FolderSearchProvider.php | 4 +- lib/Search/ItemSearchProvider.php | 109 ++++++++++++++++++++++++++++++++++++ lib/Service/FeedServiceV2.php | 57 ++++++++++++------- lib/Service/StatusService.php | 27 ++++++++- lib/Settings/AdminSettings.php | 14 ++++- 16 files changed, 317 insertions(+), 60 deletions(-) create mode 100644 lib/Migration/RemoveUnusedJob.php create mode 100644 lib/Search/ItemSearchProvider.php (limited to 'lib') diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 950d44383..bc5e1b476 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -23,6 +23,7 @@ use OCA\News\Config\FetcherConfig; use OCA\News\Hooks\UserDeleteHook; use OCA\News\Search\FeedSearchProvider; use OCA\News\Search\FolderSearchProvider; +use OCA\News\Search\ItemSearchProvider; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -82,6 +83,8 @@ class Application extends App implements IBootstrap $context->registerSearchProvider(FolderSearchProvider::class); $context->registerSearchProvider(FeedSearchProvider::class); + $context->registerSearchProvider(ItemSearchProvider::class); + $context->registerEventListener(BeforeUserDeletedEvent::class, UserDeleteHook::class); diff --git a/lib/Config/FetcherConfig.php b/lib/Config/FetcherConfig.php index ce2e7db8d..797dae49e 100644 --- a/lib/Config/FetcherConfig.php +++ b/lib/Config/FetcherConfig.php @@ -103,7 +103,11 @@ class FetcherConfig { $config = [ 'timeout' => $this->client_timeout, - 'headers' => ['User-Agent' => static::DEFAULT_USER_AGENT, 'Accept' => static::DEFAULT_ACCEPT], + 'headers' => [ + 'User-Agent' => static::DEFAULT_USER_AGENT, + 'Accept' => static::DEFAULT_ACCEPT, + 'Accept-Encoding' => 'gzip, deflate', + ], ]; if (!is_null($this->proxy)) { diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index abdd3f2f7..41d0e7f89 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -96,6 +96,7 @@ class PageController extends Controller $csp->addAllowedImageDomain('*') ->addAllowedMediaDomain('*') ->addAllowedConnectDomain('*')// chrome breaks on audio elements + ->allowEvalScript(true) ->addAllowedFrameDomain('https://youtube.com') ->addAllowedFrameDomain('https://www.youtube.com') ->addAllowedFrameDomain('https://player.vimeo.com') diff --git a/lib/Db/Feed.php b/lib/Db/Feed.php index 3c371087b..d949ea5f5 100644 --- a/lib/Db/Feed.php +++ b/lib/Db/Feed.php @@ -326,13 +326,16 @@ class Feed extends Entity implements IAPI, \JsonSerializable 'basicAuthPassword' ]); + if (is_null($this->link)) { + return $serialized; + } + $url = parse_url($this->link, PHP_URL_HOST); // strip leading www. to avoid css class confusion if (strpos($url, 'www.') === 0) { $url = substr($url, 4); } - $serialized['cssClass'] = 'custom-' . str_replace('.', '-', $url); return $serialized; @@ -488,6 +491,9 @@ class Feed extends Entity implements IAPI, \JsonSerializable */ public function setLink(?string $link = null): Feed { + if (is_null($link)) { + return $this; + } $link = trim($link); if (strpos($link, 'http') === 0 && $this->link !== $link) { $this->link = $link; diff --git a/lib/Db/ItemMapperV2.php b/lib/Db/ItemMapperV2.php index 704a8214d..972295ee3 100644 --- a/lib/Db/ItemMapperV2.php +++ b/lib/Db/ItemMapperV2.php @@ -285,12 +285,15 @@ class ItemMapperV2 extends NewsMapperV2 return intval($value['id']); }, $this->db->executeQuery($idBuilder->getSQL(), $idBuilder->getParameters())->fetchAll()); + $time = new Time(); $builder = $this->db->getQueryBuilder(); $builder->update(self::TABLE_NAME) ->set('unread', $builder->createParameter('unread')) + ->set('last_modified', $builder->createParameter('last_modified')) ->andWhere('id IN (:idList)') ->setParameter('idList', $idList, IQueryBuilder::PARAM_INT_ARRAY) - ->setParameter('unread', false, IQueryBuilder::PARAM_BOOL); + ->setParameter('unread', false, IQueryBuilder::PARAM_BOOL) + ->setParameter('last_modified', $time->getMicroTime(), IQueryBuilder::PARAM_STR); return $this->db->executeStatement( $builder->getSQL(), @@ -311,7 +314,7 @@ class ItemMapperV2 extends NewsMapperV2 { $builder = $this->db->getQueryBuilder(); - $builder->select('items.*') + $builder->select('items.id') ->from($this->tableName, 'items') ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id') ->where('feeds.user_id = :userId') diff --git a/lib/Fetcher/FeedFetcher.php b/lib/Fetcher/FeedFetcher.php index 86e0edd18..60d798889 100755 --- a/lib/Fetcher/FeedFetcher.php +++ b/lib/Fetcher/FeedFetcher.php @@ -31,6 +31,7 @@ use OCA\News\Db\Item; use OCA\News\Db\Feed; use OCA\News\Utility\Time; use OCA\News\Scraper\Scraper; +use OCA\News\Config\FetcherConfig; use Psr\Log\LoggerInterface; use SimpleXMLElement; @@ -113,15 +114,21 @@ class FeedFetcher implements IFeedFetcher string $url, bool $fullTextEnabled, ?string $user, - ?string $password + ?string $password, + ?string $httpLastModified ): array { $url2 = new Net_URL2($url); if (!is_null($user) && trim($user) !== '') { $url2->setUserinfo(rawurlencode($user), rawurlencode($password)); } + if (!is_null($httpLastModified) && trim($httpLastModified) !== '') { + $lastModified = new DateTime($httpLastModified); + } else { + $lastModified = null; + } $url = $url2->getNormalizedURL(); $this->reader->resetFilters(); - $resource = $this->reader->read($url); + $resource = $this->reader->read($url, null, $lastModified); $location = $resource->getUrl(); $parsedFeed = $resource->getFeed(); @@ -301,22 +308,24 @@ class FeedFetcher implements IFeedFetcher } // purification is done in the service layer - $body = mb_convert_encoding( - $body, - 'HTML-ENTITIES', - mb_detect_encoding($body) - ); - if (strpos($body, 'CDATA') !== false) { - libxml_use_internal_errors(true); - $data = simplexml_load_string( - "$body", - SimpleXMLElement::class, - LIBXML_NOCDATA + if (!is_null($body)) { + $body = mb_convert_encoding( + $body, + 'HTML-ENTITIES', + mb_detect_encoding($body) ); - if ($data !== false && libxml_get_last_error() === false) { - $body = (string) $data; + if (strpos($body, 'CDATA') !== false) { + libxml_use_internal_errors(true); + $data = simplexml_load_string( + "$body", + SimpleXMLElement::class, + LIBXML_NOCDATA + ); + if ($data !== false && libxml_get_last_error() === false) { + $body = (string) $data; + } + libxml_clear_errors(); } - libxml_clear_errors(); } $item->setBody($body); @@ -350,9 +359,9 @@ class FeedFetcher implements IFeedFetcher * @param FeedInterface $feed Feed to check for a logo * @param string $url Original URL for the feed * - * @return string|mixed|bool + * @return string|null */ - protected function getFavicon(FeedInterface $feed, string $url) + protected function getFavicon(FeedInterface $feed, string $url): ?string { $favicon = null; // trim the string because authors do funny things @@ -362,15 +371,21 @@ class FeedFetcher implements IFeedFetcher $favicon = trim($feed_logo); } - ini_set('user_agent', 'NextCloud-News/1.0'); + ini_set('user_agent', FetcherConfig::DEFAULT_USER_AGENT); $base_url = new Net_URL2($url); $base_url->setPath(""); $base_url = $base_url->getNormalizedURL(); + // Return if the URL is empty + if ($base_url === null || trim($base_url) === '') { + return null; + } + // check if feed has a logo entry - if (is_null($favicon) || $favicon === '') { - return $this->faviconFactory->get($base_url); + if ($favicon === null || $favicon === '') { + $return = $this->faviconFactory->get($base_url); + return is_string($return) ? $return : null; } // logo will be saved in the tmp folder provided by Nextcloud, file is named as md5 of the url @@ -392,7 +407,7 @@ class FeedFetcher implements IFeedFetcher [ 'sink' => $favicon_path, 'headers' => [ - 'User-Agent' => 'NextCloud-News/1.0', + 'User-Agent' => FetcherConfig::DEFAULT_USER_AGENT, 'Accept' => 'image/*', 'If-Modified-Since' => date(DateTime::RFC7231, $last_modified) ] @@ -422,16 +437,18 @@ class FeedFetcher implements IFeedFetcher // check if file is actually an image if (!$is_image) { - return $this->faviconFactory->get($base_url); + $return = $this->faviconFactory->get($base_url); + return is_string($return) ? $return : null; } list($width, $height, $type, $attr) = getimagesize($favicon_path); // check if image is square else fall back to favicon if ($width !== $height) { - return $this->faviconFactory->get($base_url); + $return = $this->faviconFactory->get($base_url); + return is_string($return) ? $return : null; } - return $favicon; + return is_string($favicon) ? $favicon : null; } /** diff --git a/lib/Fetcher/Fetcher.php b/lib/Fetcher/Fetcher.php index 8c755bc85..7d4690a6e 100644 --- a/lib/Fetcher/Fetcher.php +++ b/lib/Fetcher/Fetcher.php @@ -47,6 +47,7 @@ class Fetcher * @param bool $fullTextEnabled If true use a scraper to download the full article * @param string|null $user if given, basic auth is set for this feed * @param string|null $password if given, basic auth is set for this feed. Ignored if user is empty + * @param string|null $httpLastModified if given, will be used when sending a request to servers * * @throws ReadErrorException if FeedIO fails * @return array an array containing the new feed and its items, first @@ -56,7 +57,8 @@ class Fetcher string $url, bool $fullTextEnabled = false, ?string $user = null, - ?string $password = null + ?string $password = null, + ?string $httpLastModified = null ): array { foreach ($this->fetchers as $fetcher) { if (!$fetcher->canHandle($url)) { @@ -66,7 +68,8 @@ class Fetcher $url, $fullTextEnabled, $user, - $password + $password, + $httpLastModified ); } diff --git a/lib/Fetcher/IFeedFetcher.php b/lib/Fetcher/IFeedFetcher.php index 45e0915f0..7f2bbe31c 100644 --- a/lib/Fetcher/IFeedFetcher.php +++ b/lib/Fetcher/IFeedFetcher.php @@ -27,6 +27,7 @@ interface IFeedFetcher * @param bool $fullTextEnabled If true use a scraper to download the full article * @param string|null $user if given, basic auth is set for this feed * @param string|null $password if given, basic auth is set for this feed. Ignored if user is empty + * @param string|null $httpLastModified if given, will be used when sending a request to servers * * @return array an array containing the new feed and its items, first * element being the Feed and second element being an array of Items @@ -37,7 +38,8 @@ interface IFeedFetcher string $url, bool $fullTextEnabled, ?string $user, - ?string $password + ?string $password, + ?string $httpLastModified ): array; /** diff --git a/lib/Migration/RemoveUnusedJob.php b/lib/Migration/RemoveUnusedJob.php new file mode 100644 index 000000000..cb5c5fecc --- /dev/null +++ b/lib/Migration/RemoveUnusedJob.php @@ -0,0 +1,50 @@ +logger = $logger; + $this->joblist = $jobList; + } + + /** + * Returns the step's name + */ + public function getName() + { + return 'Remove the unused News update job'; + } + + /** + * @param IOutput $output + */ + public function run(IOutput $output) + { + if ($this->joblist->has("OCA\News\Cron\Updater", null)) { + $output->info("Job exists, attempting to remove"); + $this->joblist->remove("OCA\News\Cron\Updater"); + $output->info("Job removed"); + } else { + $output->info("Job does not exist, all good"); + } + } +} diff --git a/lib/Scraper/Scraper.php b/lib/Scraper/Scraper.php index 5deac358e..998c4464c 100644 --- a/lib/Scraper/Scraper.php +++ b/lib/Scraper/Scraper.php @@ -14,6 +14,7 @@ namespace OCA\News\Scraper; use fivefilters\Readability\Readability; use fivefilters\Readability\Configuration; use fivefilters\Readability\ParseException; +use League\Uri\Exceptions\SyntaxError; use Psr\Log\LoggerInterface; class Scraper implements IScraper @@ -74,10 +75,14 @@ class Scraper implements IScraper try { $this->readability->parse($content); - } catch (ParseException $e) { + } catch (ParseException | SyntaxError $e) { $this->logger->error('Unable to parse content from {url}', [ 'url' => $url, ]); + $this->logger->debug('Error during parsing of {url} ran into {error}', [ + 'url' => $url, + 'error' => $e, + ]); } return true; } diff --git a/lib/Search/FeedSearchProvider.php b/lib/Search/FeedSearchProvider.php index bbcd466e4..36b21eaba 100644 --- a/lib/Search/FeedSearchProvider.php +++ b/lib/Search/FeedSearchProvider.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace OCA\News\Search; use OCA\News\Service\FeedServiceV2; -use OCA\News\Service\FolderServiceV2; +use OCA\News\AppInfo\Application; use OCP\IL10N; use OCP\IURLGenerator; use OCP\IUser; @@ -48,7 +48,7 @@ class FeedSearchProvider implements IProvider public function getOrder(string $route, array $routeParameters): int { - if ($route === 'news.page.index') { + if (strpos($route, Application::NAME . '.') === 0) { // Active app, prefer my results return -1; } @@ -67,7 +67,7 @@ class FeedSearchProvider implements IProvider } $list[] = new SearchResultEntry( - $this->urlGenerator->imagePath('core', 'filetypes/text.svg'), + $this->urlGenerator->imagePath('core', 'rss.svg'), $feed->getTitle(), $this->l10n->t('Unread articles') . ': ' . $feed->getUnreadCount(), $this->urlGenerator->linkToRoute('news.page.index') . '#/items/feeds/' . $feed->getId() diff --git a/lib/Search/FolderSearchProvider.php b/lib/Search/FolderSearchProvider.php index 24439d73d..7f6783c68 100644 --- a/lib/Search/FolderSearchProvider.php +++ b/lib/Search/FolderSearchProvider.php @@ -49,9 +49,9 @@ class FolderSearchProvider implements IProvider public function getOrder(string $route, array $routeParameters): int { - if ($route === 'news.page.index') { + if (strpos($route, Application::NAME . '.') === 0) { // Active app, prefer my results - return -1; + return 0; } return 55; diff --git a/lib/Search/ItemSearchProvider.php b/lib/Search/ItemSearchProvider.php new file mode 100644 index 000000000..29b4bf209 --- /dev/null +++ b/lib/Search/ItemSearchProvider.php @@ -0,0 +1,109 @@ +l10n = $l10n; + $this->urlGenerator = $urlGenerator; + $this->service = $service; + } + + public function getId(): string + { + return 'news_item'; + } + + public function getName(): string + { + return $this->l10n->t('News articles'); + } + + public function getOrder(string $route, array $routeParameters): int + { + if (strpos($route, Application::NAME . '.') === 0) { + // Active app, prefer my results + return 1; + } + + return 65; + } + + private function stripTruncate(string $string, int $length = 50): string + { + $string = strip_tags(trim($string)); + + if (strlen($string) > $length) { + $string = wordwrap($string, $length); + $string = explode("\n", $string, 2); + $string = $string[0]; + } + + return $string; + } + + public function search(IUser $user, ISearchQuery $query): SearchResult + { + $list = []; + $offset = (int) ($query->getCursor() ?? 0); + $limit = $query->getLimit(); + + $search_result = $this->service->findAllWithFilters( + $user->getUID(), + ListType::ALL_ITEMS, + $limit, + $offset, + false, + [$query->getTerm()] + ); + + $last = end($search_result); + if ($last === false) { + return SearchResult::complete( + $this->l10n->t('News'), + [] + ); + } + + $icon = $this->urlGenerator->imagePath('core', 'filetypes/text.svg'); + + foreach ($search_result as $item) { + $list[] = new SearchResultEntry( + $icon, + $item->getTitle(), + $this->stripTruncate($item->getBody(), 50), + $this->urlGenerator->linkToRoute('news.page.index') . '#/items/feeds/' . $item->getFeedId() + ); + } + + return SearchResult::paginated($this->l10n->t('News'), $list, $last->getId()); + } +} diff --git a/lib/Service/FeedServiceV2.php b/lib/Service/FeedServiceV2.php index 3e02b3971..16ca9a60a 100644 --- a/lib/Service/FeedServiceV2.php +++ b/lib/Service/FeedServiceV2.php @@ -13,6 +13,7 @@ namespace OCA\News\Service; +use DateTime; use FeedIo\Explorer; use FeedIo\Reader\ReadErrorException; use HTMLPurifier; @@ -37,6 +38,7 @@ class FeedServiceV2 extends Service { /** * Class to fetch feeds. + * * @var FeedFetcher */ protected $feedFetcher; @@ -48,11 +50,13 @@ class FeedServiceV2 extends Service protected $itemService; /** * HTML Purifier + * * @var HTMLPurifier */ protected $purifier; /** * Feed Explorer + * * @var Explorer */ protected $explorer; @@ -109,7 +113,7 @@ class FeedServiceV2 extends Service /** * Finds all feeds of a user and all items in it * - * @param string $userId the name of the user + * @param string $userId the name of the user * * @return Feed[] */ @@ -169,13 +173,14 @@ class FeedServiceV2 extends Service /** * Creates a new feed * - * @param string $userId Feed owner - * @param string $feedUrl Feed URL - * @param int|null $folderId Target folder, defaults to root - * @param bool $full_text Scrape the feed for full text - * @param string|null $title The feed title - * @param string|null $user Basic auth username, if set - * @param string|null $password Basic auth password if username is set + * @param string $userId Feed owner + * @param string $feedUrl Feed URL + * @param int|null $folderId Target folder, defaults to root + * @param bool $full_text Scrape the feed for full text + * @param string|null $title The feed title + * @param string|null $user Basic auth username, if set + * @param string|null $password Basic auth password if username is set + * @param string|null $httpLastModified timestamp send when fetching the feed * * @return Feed|Entity * @@ -190,14 +195,16 @@ class FeedServiceV2 extends Service ?string $title = null, ?string $user = null, ?string $password = null, - bool $full_discover = true + bool $full_discover = true, + ?string $httpLastModified = null ): Entity { + $httpLastModified ??= (new DateTime("-1 year"))->format(DateTime::RSS); try { /** * @var Feed $feed * @var Item[] $items */ - list($feed, $items) = $this->feedFetcher->fetch($feedUrl, $full_text, $user, $password); + list($feed, $items) = $this->feedFetcher->fetch($feedUrl, $full_text, $user, $password, $httpLastModified); } catch (ReadErrorException $ex) { $this->logger->debug($ex->getMessage()); if ($full_discover === false) { @@ -209,7 +216,13 @@ class FeedServiceV2 extends Service $feedUrl = array_shift($feeds); } try { - list($feed, $items) = $this->feedFetcher->fetch($feedUrl, $full_text, $user, $password); + list($feed, $items) = $this->feedFetcher->fetch( + $feedUrl, + $full_text, + $user, + $password, + $httpLastModified + ); } catch (ReadErrorException $ex) { throw new ServiceNotFoundException($ex->getMessage()); } @@ -234,7 +247,7 @@ class FeedServiceV2 extends Service if (!is_null($user)) { $feed->setBasicAuthUser($user) - ->setBasicAuthPassword($password); + ->setBasicAuthPassword($password); } return $this->mapper->insert($feed); @@ -267,7 +280,8 @@ class FeedServiceV2 extends Service $location, $feed->getFullTextEnabled(), $feed->getBasicAuthUser(), - $feed->getBasicAuthPassword() + $feed->getBasicAuthPassword(), + $feed->getHttpLastModified() ); } catch (ReadErrorException $ex) { $feed->setUpdateErrorCount($feed->getUpdateErrorCount() + 1); @@ -294,11 +308,11 @@ class FeedServiceV2 extends Service } $feed->setHttpLastModified($fetchedFeed->getHttpLastModified()) - ->setLocation($fetchedFeed->getLocation()); + ->setLocation($fetchedFeed->getLocation()); foreach (array_reverse($items) as &$item) { $item->setFeedId($feed->getId()) - ->setBody($this->purifier->purify($item->getBody())); + ->setBody($this->purifier->purify($item->getBody())); // update modes: 0 nothing, 1 set unread if ($feed->getUpdateMode() === Feed::UPDATE_MODE_NORMAL) { @@ -314,11 +328,14 @@ class FeedServiceV2 extends Service $feed->setLastUpdateError(null); $unreadCount = 0; - array_map(function (Item $item) use (&$unreadCount): void { - if ($item->isUnread()) { - $unreadCount++; - } - }, $items); + array_map( + function (Item $item) use (&$unreadCount): void { + if ($item->isUnread()) { + $unreadCount++; + } + }, + $items + ); return $this->mapper->update($feed)->setUnreadCount($unreadCount); } diff --git a/lib/Service/StatusService.php b/lib/Service/StatusService.php index a15f30c76..69a621e3c 100644 --- a/lib/Service/StatusService.php +++ b/lib/Service/StatusService.php @@ -16,6 +16,9 @@ namespace OCA\News\Service; use OCA\News\AppInfo\Application; use OCP\IConfig; use OCP\IDBConnection; +use OCP\BackgroundJob\IJobList; +use OCP\Util; +use OCA\News\Cron\UpdaterJob; class StatusService { @@ -25,14 +28,18 @@ class StatusService private $appName; /** @var IDBConnection */ private $connection; + /** @var IJobList */ + private $jobList; public function __construct( IConfig $settings, - IDBConnection $connection + IDBConnection $connection, + IJobList $jobList ) { $this->settings = $settings; $this->connection = $connection; $this->appName = Application::NAME; + $this->jobList = $jobList; } /** @@ -76,4 +83,22 @@ class StatusService ] ]; } + + /** + * Get last update time + */ + public function getUpdateTime(): int + { + + $time = 0; + + [$major, $minor, $micro] = Util::getVersion(); + + if ($major >= 26) { + $myJobList = $this->jobList->getJobsIterator(UpdaterJob::class, 1, 0); + $time = $myJobList->current()->getLastRun(); + } + + return $time; + } } diff --git a/lib/Settings/AdminSettings.php b/lib/Settings/AdminSettings.php index c29b5d46c..5e4e83920 100644 --- a/lib/Settings/AdminSettings.php +++ b/lib/Settings/AdminSettings.php @@ -3,6 +3,7 @@ namespace OCA\News\Settings; use OCA\News\AppInfo\Application; +use OCA\News\Service\StatusService; use OCP\AppFramework\Http\TemplateResponse; use OCP\IConfig; use OCP\Settings\ISettings; @@ -17,11 +18,14 @@ class AdminSettings implements ISettings private $config; /** @var IInitialState */ private $initialState; + /** @var StatusService */ + private $service; - public function __construct(IConfig $config, IInitialState $initialState) + public function __construct(IConfig $config, IInitialState $initialState, StatusService $service) { $this->config = $config; $this->initialState = $initialState; + $this->service = $service; } public function getForm() @@ -33,6 +37,14 @@ class AdminSettings implements ISettings (string)Application::DEFAULT_SETTINGS[$setting] )); } + + if ($this->service->isCronProperlyConfigured()) { + $lastUpdate = $this->service->getUpdateTime(); + } else { + $lastUpdate = 0; + } + + $this->initialState->provideInitialState("lastCron", $lastUpdate); return new TemplateResponse(Application::NAME, 'admin', []); } -- cgit v1.2.3