Skip to content
Snippets Groups Projects
Verified Commit 492062df authored by Dave Long's avatar Dave Long
Browse files

Issue #3206643 by fjgarlin, Lal_, AJV009, quietone, benjifisher, mohit_aghera,...

Issue #3206643 by fjgarlin, Lal_, AJV009, quietone, benjifisher, mohit_aghera, larowlan, VladimirAus, mherchel, xjm, alexpott, Rishabh Vishwakarma, ckrina, rkoller, Gábor Hojtsy, smustgrave, AkshayAdhav, hestenet, irinaz, AaronMcHale, andrewmacpherson, longwave, catch, shaal, nod_, drumm: Project messaging channel in core (as experimental)
parent 60bd67fa
No related branches found
No related tags found
25 merge requests!54479.5.x SF update,!5014Issue #3071143: Table Render Array Example Is Incorrect,!4868Issue #1428520: Improve menu parent link selection,!4289Issue #1344552 by marcingy, Niklas Fiekas, Ravi.J, aleevas, Eduardo Morales...,!4114Issue #2707291: Disable body-level scrolling when a dialog is open as a modal,!4100Issue #3249600: Add support for PHP 8.1 Enums as allowed values for list_* data types,!2378Issue #2875033: Optimize joins and table selection in SQL entity query implementation,!2334Issue #3228209: Add hasRole() method to AccountInterface,!2062Issue #3246454: Add weekly granularity to views date sort,!1591Issue #3199697: Add JSON:API Translation experimental module,!1484Exposed filters get values from URL when Ajax is on,!1255Issue #3238922: Refactor (if feasible) uses of the jQuery serialize function to use vanillaJS,!1105Issue #3025039: New non translatable field on translatable content throws error,!1073issue #3191727: Focus states on mobile second level navigation items fixed,!10223132456: Fix issue where views instances are emptied before an ajax request is complete,!925Issue #2339235: Remove taxonomy hard dependency on node module,!877Issue #2708101: Default value for link text is not saved,!872Draft: Issue #3221319: Race condition when creating menu links and editing content deletes menu links,!844Resolve #3036010 "Updaters",!617Issue #3043725: Provide a Entity Handler for user cancelation,!579Issue #2230909: Simple decimals fail to pass validation,!560Move callback classRemove outside of the loop,!555Issue #3202493,!485Sets the autocomplete attribute for username/password input field on login form.,!30Issue #3182188: Updates composer usage to point at ./vendor/bin/composer
Showing
with 836 additions and 0 deletions
......@@ -358,6 +358,7 @@ drupallink
drupalmedia
drupalmediaediting
drupalmediatoolbar
drupalorg
drupaltest
druplicon
drush
......
name: Announcements
type: module
description: Displays announcements from the Drupal community.
version: VERSION
package: Core (Experimental)
lifecycle: experimental
drupal.announcements_feed.dialog:
version: VERSION
css:
component:
css/announcements_feed.dialog.css: {}
drupal.announcements_feed.toolbar:
version: VERSION
css:
component:
css/announcements_feed.toolbar.css: {}
drupal.announcements_feed.page:
version: VERSION
css:
component:
css/announcements_feed.page.css: {}
<?php
/**
* @file
* Fetch community announcements from www.drupal.org feed.
*/
use Drupal\announcements_feed\RenderCallbacks;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function announcements_feed_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.announcements_feed':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Announcements module displays announcements from the Drupal community. For more information, see the <a href=":documentation">online documentation for the Announcements module</a>.', [':documentation' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/announcements-feed']) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl><dt>' . t('Accessing announcements') . '</dt>';
$output .= '<dd>' . t('Users with the "View drupal.org announcements" permission may click on the "Announcements" item in the administration toolbar to see all announcements relevant to the Drupal version of your site.') . '</dd>';
$output .= '</dl>';
return $output;
}
}
/**
* Implements hook_toolbar().
*/
function announcements_feed_toolbar() {
if (!\Drupal::currentUser()->hasPermission('access announcements')) {
return [
'#cache' => ['contexts' => ['user.permissions']],
];
}
$items['announcement'] = [
'#type' => 'toolbar_item',
'tab' => [
'#lazy_builder' => [
'announcements_feed.lazy_builders:renderAnnouncements',
[],
],
'#create_placeholder' => TRUE,
'#cache' => [
'tags' => [
'announcements_feed:feed',
],
],
],
'#wrapper_attributes' => [
'class' => ['announce-toolbar-tab'],
],
'#cache' => ['contexts' => ['user.permissions']],
'#weight' => 3399,
];
// \Drupal\toolbar\Element\ToolbarItem::preRenderToolbarItem adds an
// #attributes property to each toolbar item's tab child automatically.
// Lazy builders don't support an #attributes property so we need to
// add another render callback to remove the #attributes property. We start by
// adding the defaults, and then we append our own pre render callback.
$items['announcement'] += \Drupal::service('plugin.manager.element_info')->getInfo('toolbar_item');
$items['announcement']['#pre_render'][] = [RenderCallbacks::class, 'removeTabAttributes'];
return $items;
}
/**
* Implements hook_theme().
*/
function announcements_feed_theme($existing, $type, $theme, $path) {
return [
'announcements_feed' => [
'variables' => [
'featured' => NULL,
'standard' => NULL,
'count' => 0,
'feed_link' => '',
],
],
'announcements_feed_admin' => [
'variables' => [
'featured' => NULL,
'standard' => NULL,
'count' => 0,
'feed_link' => '',
],
],
];
}
/**
* Implements hook_cron().
*/
function announcements_feed_cron() {
$config = \Drupal::config('announcements_feed.settings');
$interval = $config->get('cron_interval');
$last_check = \Drupal::state()->get('announcements_feed.last_fetch', 0);
$time = \Drupal::time()->getRequestTime();
if (($time - $last_check) > $interval) {
\Drupal::service('announcements_feed.fetcher')->fetch(TRUE);
\Drupal::state()->set('announcements_feed.last_fetch', $time);
}
}
access announcements:
title: 'View official announcements related to Drupal'
announcements_feed.announcement:
path: '/admin/announcements_feed'
defaults:
_controller: '\Drupal\announcements_feed\Controller\AnnounceController::getAnnouncements'
_title: 'Community announcements'
requirements:
_permission: 'access announcements'
parameters:
announcements_feed.feed_json_url: https://www.drupal.org/announcements.json
announcements_feed.feed_link: https://www.drupal.org/about/announcements
services:
announcements_feed.fetcher:
class: Drupal\announcements_feed\AnnounceFetcher
arguments: ['@http_client', '@config.factory', '@keyvalue.expirable', '@logger.channel.announcements_feed', '%announcements_feed.feed_json_url%']
logger.channel.announcements_feed:
parent: logger.channel_base
arguments: ['announcements_feed']
public: false
announcements_feed.lazy_builders:
class: Drupal\announcements_feed\LazyBuilders
arguments: [ '@plugin.manager.element_info']
max_age: 86400
cron_interval: 21600
limit: 10
announcements_feed.settings:
type: config_object
label: 'Announcements Settings'
mapping:
max_age:
type: integer
label: 'Cache announcements for max-age seconds.'
cron_interval:
type: integer
label: 'Cron interval for fetching announcements in seconds.'
limit:
type: integer
label: 'Number of announcements that will be displayed.'
/*
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/3084859
* @preserve
*/
/**
* @file
*
* Styles for the announcements feed within the off-canvas dialog.
*/
#drupal-off-canvas-wrapper .ui-dialog-titlebar.announce-titlebar::before {
-webkit-mask-image: url("data:image/svg+xml,%3csvg width='20' height='19' viewBox='0 0 20 19' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.73047 16.7648C6.00143 17.4831 6.6872 18 7.50009 18C8.31299 18 8.99876 17.4865 9.26972 16.7682C8.71107 16.8118 8.12231 16.8387 7.50009 16.8387C6.87788 16.8353 6.28912 16.8085 5.73047 16.7648Z' fill='white'/%3e%3cpath d='M14.331 13.4118H14.0801L12.4074 11.3979L11.5143 6.69897H11.5042C11.2333 5.05433 9.97881 3.74869 8.36976 3.39627C8.3731 3.38955 8.37979 3.38284 8.37979 3.37613L8.624 2.63772C8.74108 2.28529 8.53702 2 8.16905 2H6.83095C6.46298 2 6.25892 2.28529 6.37266 2.63772L6.61686 3.37613C6.62021 3.38284 6.62355 3.38955 6.6269 3.39627C5.01784 3.74869 3.76673 5.05433 3.49242 6.69897H3.48238L2.59255 11.3979L0.919938 13.4118H0.669046C0.30107 13.4118 0 13.7139 0 14.0831C0 14.4523 0.280999 14.8618 0.625558 14.996C0.625558 14.996 3.48573 16.0969 7.5 16.0969C11.5143 16.0969 14.3744 14.996 14.3744 14.996C14.719 14.8618 15 14.4523 15 14.0831C15 13.7139 14.6989 13.4118 14.331 13.4118ZM4.58296 6.95742L3.70317 11.8611L1.75624 14.0831H1.23439L3.21811 11.6933L4.15477 6.82652C4.28189 6.0579 4.68332 5.3799 5.24532 4.8798L5.49955 5.19866C5.03122 5.60478 4.68666 6.32305 4.58296 6.95742Z' fill='white'/%3e%3c/svg%3e");
mask-image: url("data:image/svg+xml,%3csvg width='20' height='19' viewBox='0 0 20 19' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.73047 16.7648C6.00143 17.4831 6.6872 18 7.50009 18C8.31299 18 8.99876 17.4865 9.26972 16.7682C8.71107 16.8118 8.12231 16.8387 7.50009 16.8387C6.87788 16.8353 6.28912 16.8085 5.73047 16.7648Z' fill='white'/%3e%3cpath d='M14.331 13.4118H14.0801L12.4074 11.3979L11.5143 6.69897H11.5042C11.2333 5.05433 9.97881 3.74869 8.36976 3.39627C8.3731 3.38955 8.37979 3.38284 8.37979 3.37613L8.624 2.63772C8.74108 2.28529 8.53702 2 8.16905 2H6.83095C6.46298 2 6.25892 2.28529 6.37266 2.63772L6.61686 3.37613C6.62021 3.38284 6.62355 3.38955 6.6269 3.39627C5.01784 3.74869 3.76673 5.05433 3.49242 6.69897H3.48238L2.59255 11.3979L0.919938 13.4118H0.669046C0.30107 13.4118 0 13.7139 0 14.0831C0 14.4523 0.280999 14.8618 0.625558 14.996C0.625558 14.996 3.48573 16.0969 7.5 16.0969C11.5143 16.0969 14.3744 14.996 14.3744 14.996C14.719 14.8618 15 14.4523 15 14.0831C15 13.7139 14.6989 13.4118 14.331 13.4118ZM4.58296 6.95742L3.70317 11.8611L1.75624 14.0831H1.23439L3.21811 11.6933L4.15477 6.82652C4.28189 6.0579 4.68332 5.3799 5.24532 4.8798L5.49955 5.19866C5.03122 5.60478 4.68666 6.32305 4.58296 6.95742Z' fill='white'/%3e%3c/svg%3e");
}
#drupal-off-canvas-wrapper .announcements {
padding-block-start: var(--off-canvas-padding);
}
#drupal-off-canvas-wrapper .announcements ul {
margin: 0;
padding-inline-start: 0;
list-style: none;
}
#drupal-off-canvas-wrapper .announcement {
font-size: 0.875rem;
}
#drupal-off-canvas-wrapper .announcement--featured {
position: relative;
margin-inline: calc(-1 * var(--off-canvas-padding));
padding: 0 var(--off-canvas-padding) var(--off-canvas-padding);
}
#drupal-off-canvas-wrapper .announcement.announcement--featured + .announcement.announcement--standard {
border-block-start: 1px solid var(--off-canvas-border-color);
}
#drupal-off-canvas-wrapper .announcement--standard {
padding-block-start: var(--off-canvas-padding);
}
#drupal-off-canvas-wrapper .announcement__title {
font-size: 1rem;
}
#drupal-off-canvas-wrapper .announcements--view-all {
margin-block-start: 3rem;
}
/**
* @file
*
* Styles for the announcements feed within the off-canvas dialog.
*/
#drupal-off-canvas-wrapper {
& .ui-dialog-titlebar.announce-titlebar::before {
-webkit-mask-image: url("../images/announcement-bell.svg");
mask-image: url("../images/announcement-bell.svg");
}
& .announcements {
padding-block-start: var(--off-canvas-padding);
}
& .announcements ul {
margin: 0;
padding-inline-start: 0;
list-style: none;
}
& .announcement {
font-size: 0.875rem;
}
& .announcement--featured {
position: relative;
margin-inline: calc(-1 * var(--off-canvas-padding));
padding: 0 var(--off-canvas-padding) var(--off-canvas-padding);
}
& .announcement.announcement--featured + .announcement.announcement--standard {
border-block-start: 1px solid var(--off-canvas-border-color);
}
& .announcement--standard {
padding-block-start: var(--off-canvas-padding);
}
& .announcement__title {
font-size: 1rem;
}
& .announcements--view-all {
margin-block-start: 3rem;
}
}
/*
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/3084859
* @preserve
*/
.announcements ul {
margin-inline-start: 0;
list-style: none;
}
.announcement:not(.announcement:last-child) {
margin-block-end: 1rem;
}
.announcement.announcement--featured + .announcement.announcement--standard {
padding-block-start: 1rem;
border-top: 1px solid #aaa;
}
.announcements--view-all {
margin-block-start: 3rem;
}
.announcements ul {
margin-inline-start: 0;
list-style: none;
}
.announcement:not(.announcement:last-child) {
margin-block-end: 1rem;
}
.announcement.announcement--featured + .announcement.announcement--standard {
padding-block-start: 1rem;
border-top: 1px solid #aaa;
}
.announcements--view-all {
margin-block-start: 3rem;
}
/*
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/3084859
* @preserve
*/
/**
* @file
*
* Styles for the announcements toolbar item.
*/
.toolbar .toolbar-icon.announce-canvas-link::before {
background-image: url("data:image/svg+xml,%3csvg width='20' height='19' viewBox='0 0 20 19' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.73047 16.7648C6.00143 17.4831 6.6872 18 7.50009 18C8.31299 18 8.99876 17.4865 9.26972 16.7682C8.71107 16.8118 8.12231 16.8387 7.50009 16.8387C6.87788 16.8353 6.28912 16.8085 5.73047 16.7648Z' fill='white'/%3e%3cpath d='M14.331 13.4118H14.0801L12.4074 11.3979L11.5143 6.69897H11.5042C11.2333 5.05433 9.97881 3.74869 8.36976 3.39627C8.3731 3.38955 8.37979 3.38284 8.37979 3.37613L8.624 2.63772C8.74108 2.28529 8.53702 2 8.16905 2H6.83095C6.46298 2 6.25892 2.28529 6.37266 2.63772L6.61686 3.37613C6.62021 3.38284 6.62355 3.38955 6.6269 3.39627C5.01784 3.74869 3.76673 5.05433 3.49242 6.69897H3.48238L2.59255 11.3979L0.919938 13.4118H0.669046C0.30107 13.4118 0 13.7139 0 14.0831C0 14.4523 0.280999 14.8618 0.625558 14.996C0.625558 14.996 3.48573 16.0969 7.5 16.0969C11.5143 16.0969 14.3744 14.996 14.3744 14.996C14.719 14.8618 15 14.4523 15 14.0831C15 13.7139 14.6989 13.4118 14.331 13.4118ZM4.58296 6.95742L3.70317 11.8611L1.75624 14.0831H1.23439L3.21811 11.6933L4.15477 6.82652C4.28189 6.0579 4.68332 5.3799 5.24532 4.8798L5.49955 5.19866C5.03122 5.60478 4.68666 6.32305 4.58296 6.95742Z' fill='white'/%3e%3c/svg%3e");
}
@media (forced-colors: active) {
.toolbar .toolbar-icon.announce-canvas-link::before {
background: linktext;
-webkit-mask-image: url("data:image/svg+xml,%3csvg width='20' height='19' viewBox='0 0 20 19' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.73047 16.7648C6.00143 17.4831 6.6872 18 7.50009 18C8.31299 18 8.99876 17.4865 9.26972 16.7682C8.71107 16.8118 8.12231 16.8387 7.50009 16.8387C6.87788 16.8353 6.28912 16.8085 5.73047 16.7648Z' fill='white'/%3e%3cpath d='M14.331 13.4118H14.0801L12.4074 11.3979L11.5143 6.69897H11.5042C11.2333 5.05433 9.97881 3.74869 8.36976 3.39627C8.3731 3.38955 8.37979 3.38284 8.37979 3.37613L8.624 2.63772C8.74108 2.28529 8.53702 2 8.16905 2H6.83095C6.46298 2 6.25892 2.28529 6.37266 2.63772L6.61686 3.37613C6.62021 3.38284 6.62355 3.38955 6.6269 3.39627C5.01784 3.74869 3.76673 5.05433 3.49242 6.69897H3.48238L2.59255 11.3979L0.919938 13.4118H0.669046C0.30107 13.4118 0 13.7139 0 14.0831C0 14.4523 0.280999 14.8618 0.625558 14.996C0.625558 14.996 3.48573 16.0969 7.5 16.0969C11.5143 16.0969 14.3744 14.996 14.3744 14.996C14.719 14.8618 15 14.4523 15 14.0831C15 13.7139 14.6989 13.4118 14.331 13.4118ZM4.58296 6.95742L3.70317 11.8611L1.75624 14.0831H1.23439L3.21811 11.6933L4.15477 6.82652C4.28189 6.0579 4.68332 5.3799 5.24532 4.8798L5.49955 5.19866C5.03122 5.60478 4.68666 6.32305 4.58296 6.95742Z' fill='white'/%3e%3c/svg%3e");
mask-image: url("data:image/svg+xml,%3csvg width='20' height='19' viewBox='0 0 20 19' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.73047 16.7648C6.00143 17.4831 6.6872 18 7.50009 18C8.31299 18 8.99876 17.4865 9.26972 16.7682C8.71107 16.8118 8.12231 16.8387 7.50009 16.8387C6.87788 16.8353 6.28912 16.8085 5.73047 16.7648Z' fill='white'/%3e%3cpath d='M14.331 13.4118H14.0801L12.4074 11.3979L11.5143 6.69897H11.5042C11.2333 5.05433 9.97881 3.74869 8.36976 3.39627C8.3731 3.38955 8.37979 3.38284 8.37979 3.37613L8.624 2.63772C8.74108 2.28529 8.53702 2 8.16905 2H6.83095C6.46298 2 6.25892 2.28529 6.37266 2.63772L6.61686 3.37613C6.62021 3.38284 6.62355 3.38955 6.6269 3.39627C5.01784 3.74869 3.76673 5.05433 3.49242 6.69897H3.48238L2.59255 11.3979L0.919938 13.4118H0.669046C0.30107 13.4118 0 13.7139 0 14.0831C0 14.4523 0.280999 14.8618 0.625558 14.996C0.625558 14.996 3.48573 16.0969 7.5 16.0969C11.5143 16.0969 14.3744 14.996 14.3744 14.996C14.719 14.8618 15 14.4523 15 14.0831C15 13.7139 14.6989 13.4118 14.331 13.4118ZM4.58296 6.95742L3.70317 11.8611L1.75624 14.0831H1.23439L3.21811 11.6933L4.15477 6.82652C4.28189 6.0579 4.68332 5.3799 5.24532 4.8798L5.49955 5.19866C5.03122 5.60478 4.68666 6.32305 4.58296 6.95742Z' fill='white'/%3e%3c/svg%3e");
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-position: center;
}
}
/* Pushes the tab to the opposite side of the page. */
.toolbar .toolbar-bar .announce-toolbar-tab.toolbar-tab {
float: right; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-bar .announce-toolbar-tab.toolbar-tab {
float: left;
}
/**
* @file
*
* Styles for the announcements toolbar item.
*/
.toolbar .toolbar-icon.announce-canvas-link::before {
background-image: url("../images/announcement-bell.svg");
@media (forced-colors: active) {
background: linktext;
mask-image: url("../images/announcement-bell.svg");
mask-repeat: no-repeat;
mask-position: center;
}
}
/* Pushes the tab to the opposite side of the page. */
.toolbar .toolbar-bar .announce-toolbar-tab.toolbar-tab {
float: right; /* LTR */
&:dir(rtl) {
float: left;
}
}
<svg width="20" height="19" viewBox="0 0 20 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.73047 16.7648C6.00143 17.4831 6.6872 18 7.50009 18C8.31299 18 8.99876 17.4865 9.26972 16.7682C8.71107 16.8118 8.12231 16.8387 7.50009 16.8387C6.87788 16.8353 6.28912 16.8085 5.73047 16.7648Z" fill="white"/>
<path d="M14.331 13.4118H14.0801L12.4074 11.3979L11.5143 6.69897H11.5042C11.2333 5.05433 9.97881 3.74869 8.36976 3.39627C8.3731 3.38955 8.37979 3.38284 8.37979 3.37613L8.624 2.63772C8.74108 2.28529 8.53702 2 8.16905 2H6.83095C6.46298 2 6.25892 2.28529 6.37266 2.63772L6.61686 3.37613C6.62021 3.38284 6.62355 3.38955 6.6269 3.39627C5.01784 3.74869 3.76673 5.05433 3.49242 6.69897H3.48238L2.59255 11.3979L0.919938 13.4118H0.669046C0.30107 13.4118 0 13.7139 0 14.0831C0 14.4523 0.280999 14.8618 0.625558 14.996C0.625558 14.996 3.48573 16.0969 7.5 16.0969C11.5143 16.0969 14.3744 14.996 14.3744 14.996C14.719 14.8618 15 14.4523 15 14.0831C15 13.7139 14.6989 13.4118 14.331 13.4118ZM4.58296 6.95742L3.70317 11.8611L1.75624 14.0831H1.23439L3.21811 11.6933L4.15477 6.82652C4.28189 6.0579 4.68332 5.3799 5.24532 4.8798L5.49955 5.19866C5.03122 5.60478 4.68666 6.32305 4.58296 6.95742Z" fill="white"/>
</svg>
<?php
declare(strict_types=1);
namespace Drupal\announcements_feed;
use Composer\Semver\Semver;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\Utility\Error;
use GuzzleHttp\ClientInterface;
use Psr\Log\LoggerInterface;
/**
* Service to fetch announcements from the external feed.
*
* @internal
*/
final class AnnounceFetcher {
/**
* The configuration settings of this module.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected ImmutableConfig $config;
/**
* The tempstore service.
*
* @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactory
*/
protected KeyValueStoreInterface $tempStore;
/**
* Construct an AnnounceFetcher service.
*
* @param \GuzzleHttp\ClientInterface $httpClient
* The http client.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config
* The config factory service.
* @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $temp_store
* The tempstore factory service.
* @param \Psr\Log\LoggerInterface $logger
* The logger service.
* @param string $feedUrl
* The feed url path.
*/
public function __construct(
protected ClientInterface $httpClient,
ConfigFactoryInterface $config,
KeyValueExpirableFactoryInterface $temp_store,
protected LoggerInterface $logger,
protected string $feedUrl
) {
$this->config = $config->get('announcements_feed.settings');
$this->tempStore = $temp_store->get('announcements_feed');
}
/**
* Fetch ids of announcements.
*
* @return array
* An array with ids of all announcements in the feed.
*/
public function fetchIds(): array {
return array_column($this->fetch(), 'id');
}
/**
* Check whether the version given is relevant to the Drupal version used.
*
* @param string $version
* Version to check.
*
* @return bool
* Return True if the version matches Drupal version.
*/
protected static function isRelevantItem(string $version): bool {
return !empty($version) && Semver::satisfies(\Drupal::VERSION, $version);
}
/**
* Check whether a link is controlled by D.O.
*
* @param string $url
* URL to check.
*
* @return bool
* Return True if the URL is controlled by the D.O.
*/
public static function validateUrl(string $url): bool {
if (empty($url)) {
return FALSE;
}
$host = parse_url($url, PHP_URL_HOST);
// First character can only be a letter or a digit.
// @see https://www.rfc-editor.org/rfc/rfc1123#page-13
return $host && preg_match('/^([a-zA-Z0-9][a-zA-Z0-9\-_]*\.)?drupal\.org$/', $host);
}
/**
* Fetches the feed either from a local cache or fresh remotely.
*
* The feed follows the "JSON Feed" format:
* - https://www.jsonfeed.org/version/1.1/
*
* The structure of an announcement item in the feed is:
* - id: Id.
* - title: Title of the announcement.
* - content_html: Announcement teaser.
* - url: URL
* - date_modified: Last updated timestamp.
* - date_published: Created timestamp.
* - _drupalorg.featured: 1 if featured, 0 if not featured.
* - _drupalorg.version: Target version of Drupal, as a Composer version.
*
* @param bool $force
* (optional) Whether to always fetch new items or not. Defaults to FALSE.
*
* @return \Drupal\announcements_feed\Announcement[]
* An array of announcements from the feed relevant to the Drupal version.
* The array is empty if there were no matching announcements. If an error
* occurred while fetching/decoding the feed, it is thrown as an exception.
*
* @throws \Exception
*/
public function fetch(bool $force = FALSE): array {
$announcements = $this->tempStore->get('announcements');
if ($force || $announcements === NULL) {
try {
$feed_content = (string) $this->httpClient->get($this->feedUrl)->getBody();
}
catch (\Exception $e) {
$this->logger->error(Error::DEFAULT_ERROR_MESSAGE, Error::decodeException($e));
throw $e;
}
$announcements = Json::decode($feed_content);
if (!isset($announcements['items'])) {
$this->logger->error('The feed format is not valid.');
throw new \Exception('Invalid format');
}
$announcements = $announcements['items'] ?? [];
// Ensure that announcements reference drupal.org and are applicable to
// the current Drupal version.
$announcements = array_filter($announcements, function (array $announcement) {
return static::validateUrl($announcement['url'] ?? '') && static::isRelevantItem($announcement['_drupalorg']['version'] ?? '');
});
// Save the raw decoded and filtered array to temp store.
$this->tempStore->setWithExpire('announcements', $announcements,
$this->config->get('max_age'));
}
// The drupal.org endpoint is sorted by created date in descending order.
// We will limit the announcements based on the configuration limit.
$announcements = array_slice($announcements, 0, $this->config->get('limit') ?? 10);
// For the remaining announcements, put all the featured announcements
// before the rest.
uasort($announcements, function ($a, $b) {
$a_value = (int) $a['_drupalorg']['featured'];
$b_value = (int) $b['_drupalorg']['featured'];
if ($a_value == $b_value) {
return 0;
}
return ($a_value < $b_value) ? -1 : 1;
});
// Map the multidimensional array into an array of Announcement objects.
$announcements = array_map(function ($announcement) {
return new Announcement(
$announcement['id'],
$announcement['title'],
$announcement['url'],
$announcement['date_modified'],
$announcement['date_published'],
$announcement['content_html'],
$announcement['_drupalorg']['version'],
(bool) $announcement['_drupalorg']['featured'],
);
}, $announcements);
return $announcements;
}
}
<?php
declare(strict_types=1);
namespace Drupal\announcements_feed;
use Drupal\Core\Datetime\DrupalDateTime;
/**
* Object containing a single announcement from the feed.
*
* @internal
*/
final class Announcement {
/**
* Construct an Announcement object.
*
* @param string $id
* Unique identifier of the announcement.
* @param string $title
* Title of the announcement.
* @param string $url
* URL where the announcement can be seen.
* @param string $date_modified
* When was the announcement last modified.
* @param string $date_published
* When was the announcement published.
* @param string $content_html
* HTML content of the announcement.
* @param string $version
* Target Drupal version of the announcement.
* @param bool $featured
* Whether this announcement is featured or not.
*/
public function __construct(
public readonly string $id,
public readonly string $title,
public readonly string $url,
public readonly string $date_modified,
public readonly string $date_published,
public readonly string $content_html,
public readonly string $version,
public readonly bool $featured
) {
}
/**
* Returns the content of the announcement with no markup.
*
* @return string
* Content of the announcement without markup.
*/
public function getContent() {
return strip_tags($this->content_html);
}
/**
* Gets the published date in timestamp format.
*
* @return int
* Date published timestamp.
*/
public function getDatePublishedTimestamp() {
return DrupalDateTime::createFromFormat(DATE_ATOM, $this->date_published)->getTimestamp();
}
}
<?php
declare(strict_types=1);
namespace Drupal\announcements_feed\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\announcements_feed\AnnounceFetcher;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Controller for community announcements.
*
* @internal
*/
class AnnounceController extends ControllerBase implements ContainerInjectionInterface {
/**
* Constructs an AnnounceController object.
*
* @param \Drupal\announcements_feed\AnnounceFetcher $announceFetcher
* The AnnounceFetcher service.
* @param string $feedLink
* The feed url path.
*/
public function __construct(
protected AnnounceFetcher $announceFetcher,
protected string $feedLink
) {
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): AnnounceController {
return new static(
$container->get('announcements_feed.fetcher'),
$container->getParameter('announcements_feed.feed_link')
);
}
/**
* Returns the list of Announcements.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return array
* A build array with announcements.
*/
public function getAnnouncements(Request $request): array {
try {
$announcements = $this->announceFetcher->fetch();
}
catch (\Exception $e) {
return [
'#theme' => 'status_messages',
'#message_list' => [
'error' => [
$this->t('An error occurred while parsing the announcements feed, check the logs for more information.'),
],
],
'#status_headings' => [
'error' => $this->t('Error Message'),
],
];
}
$build = [];
foreach ($announcements as $announcement) {
$key = $announcement->featured ? '#featured' : '#standard';
$build[$key][] = $announcement;
}
$build += [
'#theme' => 'announcements_feed',
'#count' => count($announcements),
'#feed_link' => $this->feedLink,
'#cache' => [
'contexts' => [
'url.query_args:_wrapper_format',
],
'tags' => [
'announcements_feed:feed',
],
],
'#attached' => [
'library' => [
'announcements_feed/drupal.announcements_feed.dialog',
],
],
];
if ($request->query->get('_wrapper_format') != 'drupal_dialog.off_canvas') {
$build['#theme'] = 'announcements_feed_admin';
$build['#attached'] = [];
}
return $build;
}
}
<?php
declare(strict_types=1);
namespace Drupal\announcements_feed;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Url;
/**
* Defines a class for lazy building render arrays.
*
* @internal
*/
final class LazyBuilders implements TrustedCallbackInterface {
/**
* Constructs LazyBuilders object.
*
* @param \Drupal\Core\Render\ElementInfoManagerInterface $elementInfo
* Element info.
*/
public function __construct(
protected ElementInfoManagerInterface $elementInfo,
) {
}
/**
* Render announcements.
*
* @return array
* Render array.
*/
public function renderAnnouncements(): array {
$build = [
'#type' => 'link',
'#cache' => [
'context' => ['user.permissions'],
],
'#title' => t('Announcements'),
'#url' => Url::fromRoute('announcements_feed.announcement'),
'#id' => Html::getId('toolbar-item-announcement'),
'#attributes' => [
'title' => t('Announcements'),
'data-drupal-announce-trigger' => '',
'class' => [
'toolbar-icon',
'toolbar-item',
'toolbar-icon-announce',
'use-ajax',
'announce-canvas-link',
'announce-default',
],
'data-dialog-renderer' => 'off_canvas',
'data-dialog-type' => 'dialog',
'data-dialog-options' => Json::encode(
[
'announce' => TRUE,
'width' => '25%',
'classes' => [
'ui-dialog' => 'announce-dialog',
'ui-dialog-titlebar' => 'announce-titlebar',
'ui-dialog-title' => 'announce-title',
'ui-dialog-titlebar-close' => 'announce-close',
'ui-dialog-content' => 'announce-body',
],
]),
],
'#attached' => [
'library' => [
'announcements_feed/drupal.announcements_feed.toolbar',
],
],
];
// The renderer has already added element defaults by the time the lazy
// builder is run.
// @see https://www.drupal.org/project/drupal/issues/2609250
$build += $this->elementInfo->getInfo('link');
return $build;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks(): array {
return ['renderAnnouncements'];
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment