first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
name: Announcements
type: module
description: Displays announcements from the Drupal community.
# version: VERSION
package: Core
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,17 @@
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: {}

View File

@@ -0,0 +1,6 @@
announcements_feed.announcement:
title: Announcements
description: 'Displays announcements from the Drupal community.'
route_name: announcements_feed.announcement
weight: 10
parent: system.admin

View File

@@ -0,0 +1,123 @@
<?php
/**
* @file
* Fetch community announcements from www.drupal.org feed.
*/
use Drupal\announcements_feed\RenderCallbacks;
use Drupal\Core\Link;
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 .= '<h2>' . t('About') . '</h2>';
$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 .= '<h2>' . t('Uses') . '</h2>';
$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, or access @link, to see all announcements relevant to the Drupal version of your site.', [
'@link' => Link::createFromRoute(t('Announcements'), 'announcements_feed.announcement')->toString(),
]) . '</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_toolbar_alter().
*/
function announcements_feed_toolbar_alter(&$items) {
// As the "Announcements" link is shown already in the top toolbar bar, we
// don't need it again in the administration menu tray, so hide it.
if (!empty($items['administration']['tray'])) {
$callable = function (array $element) {
unset($element['administration_menu']['#items']['announcements_feed.announcement']);
return $element;
};
$items['administration']['tray']['toolbar_administration']['#pre_render'][] = $callable;
}
}
/**
* 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);
}
}

View File

@@ -0,0 +1,2 @@
access announcements:
title: 'View official announcements related to Drupal'

View File

@@ -0,0 +1,7 @@
announcements_feed.announcement:
path: '/admin/announcements_feed'
defaults:
_controller: '\Drupal\announcements_feed\Controller\AnnounceController::getAnnouncements'
_title: 'Community announcements'
requirements:
_permission: 'access announcements'

View File

@@ -0,0 +1,21 @@
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%']
Drupal\announcements_feed\AnnounceFetcher: '@announcements_feed.fetcher'
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']
Drupal\announcements_feed\LazyBuilders: '@announcements_feed.lazy_builders'
announcements_feed.renderer:
class: Drupal\announcements_feed\AnnounceRenderer
arguments: ['@announcements_feed.fetcher', '%announcements_feed.feed_link%']
Drupal\announcements_feed\AnnounceRenderer: '@announcements_feed.renderer'

View File

@@ -0,0 +1,53 @@
/*
* 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;
}

View File

@@ -0,0 +1,48 @@
/**
* @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;
}
}

View File

@@ -0,0 +1,24 @@
/*
* 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;
}

View File

@@ -0,0 +1,17 @@
.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;
}

View File

@@ -0,0 +1,38 @@
/*
* 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;
}

View File

@@ -0,0 +1,25 @@
/**
* @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;
}
}

View File

@@ -0,0 +1,24 @@
---
label: 'Viewing Drupal announcements'
top_level: true
---
{% set actions_link_text %}
{% trans %}Announcements{% endtrans %}
{% endset %}
{% set actions_link = render_var(help_route_link(actions_link_text, 'announcements_feed.announcement')) %}
{% set permissions_link_text %}
{% trans %}View official announcements related to Drupal{% endtrans %}
{% endset %}
{% set permissions_link = render_var(help_route_link(permissions_link_text, 'user.admin_permissions.module', {'modules': 'announcements_feed'})) %}
<h2>{% trans %}What are Drupal announcements?{% endtrans %}</h2>
<p>{% trans %}A feed of announcements about the Drupal project and Drupal Association programs.{% endtrans %}</p>
<p>{% trans %}The purpose of this feed is to provide a channel for outreach directly to Drupal site owners. This content must be highly relevant to site owners interests, serve the strategic goals of the project, and/or promote the sustainability of the project and the Drupal Association.{% endtrans %}</p>
<p>{% trans %}The module sources its content from a JSON feed generated from <a href="https://www.drupal.org/about/announcements">here</a>. The governance policy for the content is documented <a href="https://www.drupal.org/node/3274085">here</a>.{% endtrans %}</p>
<h2>{% trans %}How can I see the Announcements in my site?{% endtrans %}</h2>
<p>{% trans %}If you have the toolbar module enabled, you will see a direct link to them in the toolbar. If the toolbar module is not enabled, the content can always be accessed in the <em>{{ actions_link }}</em> page.{% endtrans %}</p>
<h2>{% trans %}Who can see the Announcements?{% endtrans %}</h2>
<p>{% trans %}Users with the <em>{{ permissions_link }}</em> permission can view Drupal announcements.{% endtrans %}</p>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/docs/core-modules-and-themes/core-modules/announcements-feed/announcements-feed-module-overview">{% trans %}Announcement module overview{% endtrans %}</a></li>
</ul>

View File

@@ -0,0 +1,4 @@
<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>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,193 @@
<?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;
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Drupal\announcements_feed;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Service to render announcements from the external feed.
*
* @internal
*/
final class AnnounceRenderer {
use StringTranslationTrait;
/**
* Constructs an AnnouncementRenderer 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,
) {
}
/**
* Generates the announcements feed render array.
*
* @return array
* Render array containing the announcements feed.
*/
public function render(): 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',
],
],
];
return $build;
}
}

View File

@@ -0,0 +1,68 @@
<?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();
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Drupal\announcements_feed\Controller;
use Drupal\announcements_feed\AnnounceRenderer;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Controller for community announcements.
*
* @internal
*/
class AnnounceController extends ControllerBase implements ContainerInjectionInterface {
/**
* Constructs an AnnounceController object.
*
* @param \Drupal\announcements_feed\AnnounceRenderer $announceRenderer
* The AnnounceRenderer service.
*/
public function __construct(
protected AnnounceRenderer $announceRenderer,
) {
}
/**
* 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 {
$build = $this->announceRenderer->render();
if ($request->query->get('_wrapper_format') != 'drupal_dialog.off_canvas') {
$build['#theme'] = 'announcements_feed_admin';
$build['#attached'] = [];
}
return $build;
}
}

View File

@@ -0,0 +1,93 @@
<?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'];
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Drupal\announcements_feed\Plugin\Block;
use Drupal\announcements_feed\AnnounceRenderer;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides an 'Announcements Feed' block.
*
* @Block(
* id = "announce_block",
* admin_label = @Translation("Announcements Feed"),
* )
*
* @internal
*/
class AnnounceBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* Constructs a new AnnouncementsFeedBlock instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\announcements_feed\AnnounceRenderer $announceRenderer
* The AnnounceRenderer service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, protected AnnounceRenderer $announceRenderer) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('announcements_feed.renderer')
);
}
/**
* {@inheritdoc}
*/
public function blockAccess(AccountInterface $account): AccessResultInterface {
return AccessResult::allowedIfHasPermission($account, 'access announcements');
}
/**
* {@inheritdoc}
*/
public function build(): array {
return $this->announceRenderer->render();
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\announcements_feed;
use Drupal\Core\Security\TrustedCallbackInterface;
/**
* Defines a class for render callbacks.
*
* @internal
*/
final class RenderCallbacks implements TrustedCallbackInterface {
/**
* Render callback.
*/
public static function removeTabAttributes(array $element): array {
unset($element['tab']['#attributes']);
return $element;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks(): array {
return ['removeTabAttributes'];
}
}

View File

@@ -0,0 +1,27 @@
{#
/**
* @file
* Template file for the theming of announcement_feed admin page.
*
* This template will get rendered when the user navigates to the announcements_feed.announcement route.
*
* Available variables:
* - count: Contains the total number of announcements.
* - featured: A list of featured announcement objects.
* - standard: A list of non-featured announcement objects.
*
* Announcement objects have the following variables:
* - id: Unique id of the announcement.
* - title: Title of the standard announcement.
* - content: Short description of the announcement.
* - datePublishedTimestamp: Timestamp of the announcement.
* - url: Learn more link of the standard announcement.
*
* @see announcements_feed_theme()
*
* @ingroup themeable
*/
#}
{{ attach_library('announcements_feed/drupal.announcements_feed.page') }}
{% include '@announcements_feed/announcements.html.twig' %}

View File

@@ -0,0 +1,25 @@
{#
/**
* @file
* Template file for the theming of announcement_feed off-canvas dialog.
*
* This template will get rendered when the user clicks the announcement button in the toolbar.
*
* Available variables:
* - count: Contains the total number of announcements.
* - featured: A list of featured announcement objects.
* - standard: A list of non-featured announcement objects.
*
* Announcement objects have the following variables:
* - id: Unique id of the announcement.
* - title: Title of the standard announcement.
* - content: Short description of the announcement.
* - datePublishedTimestamp: Timestamp of the announcement.
* - url: Learn more link of the standard announcement.
*
* @see announcements_feed_theme()
*
* @ingroup themeable
*/
#}
{% include '@announcements_feed/announcements.html.twig' %}

View File

@@ -0,0 +1,37 @@
{% if count %}
<nav class="announcements">
<ul>
{% if featured|length %}
{% for announcement in featured %}
<li class="announcement announcement--featured" data-drupal-featured>
<div class="announcement__title">
<h4>{{ announcement.title }}</h4>
</div>
<div class="announcement__teaser">
{{ announcement.content }}
</div>
<div class="announcement__link">
<a href="{{ announcement.url }}">{{ 'Learn More'|t }}</a>
</div>
</li>
{% endfor %}
{% endif %}
{% for announcement in standard %}
<li class="announcement announcement--standard">
<div class="announcement__title">
<a href="{{ announcement.url }}">{{ announcement.title }}</a>
<div class="announcement__date">{{ announcement.datePublishedTimestamp|format_date('short') }}</div>
</div>
</li>
{% endfor %}
</ul>
</nav>
{% if feed_link %}
<p class="announcements--view-all">
<a target="_blank" href="{{ feed_link }}">{{ 'View all announcements'|t }}</a>
</p>
{% endif %}
{% else %}
<div class="announcements announcements--empty"><p> {{ 'No announcements available'|t }}</p></div>
{% endif %}

View File

@@ -0,0 +1,57 @@
{
"version": "https://jsonfeed.org/version/1.1",
"title": "Drupal Announcements Feed",
"home_page_url": "https://www.drupal.org",
"feed_url": "https://www.drupal.org/announcements.json",
"favicon": "https://www.drupal.org/favicon.ico",
"items": [
{
"id": "201",
"title": "new 9 - 10 Drupal 9.1.3 is available",
"content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
"url": "https://www.drupal.org/project/announce",
"date_modified": "2021-01-19T07:29:38+00:00",
"date_published": "2021-01-18T07:29:38+00:00",
"_drupalorg": {
"featured": false,
"version": "^9 | ^10"
}
},
{
"id": "2021",
"title": "updated 10 - DrupalCon is here",
"content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
"url": "https://www.drupal.org/project/announce",
"date_modified": "2021-01-19T07:29:38+00:00",
"date_published": "2021-01-18T07:29:38+00:00",
"_drupalorg": {
"featured": true,
"version": "^10"
}
},
{
"id": "2031",
"title": "new 9 only - Download latest drupal here",
"content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
"url": "https://www.drupal.org/project/announce",
"date_modified": "2021-01-19T07:29:38+00:00",
"date_published": "2021-01-18T07:29:38+00:00",
"_drupalorg": {
"featured": false,
"version": "^9"
}
},
{
"id": "2043",
"title": "Only 10 - Drupal 106 is available",
"content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
"url": "https://www.drupal.org/project/announce",
"date_modified": "2021-01-19T07:29:39+00:00",
"date_published": "2021-01-18T07:29:39+00:00",
"_drupalorg": {
"featured": false,
"version": "^10"
}
}
]
}

View File

@@ -0,0 +1,8 @@
{
"version": "https://jsonfeed.org/version/1.1",
"title": "Drupal Announcements Feed",
"home_page_url": "https://www.drupal.org",
"feed_url": "https://www.drupal.org/announcements.json",
"favicon": "https://www.drupal.org/favicon.ico",
"items": []
}

View File

@@ -0,0 +1,45 @@
{
"version": "https://jsonfeed.org/version/1.1",
"title": "Drupal Announcements Feed",
"home_page_url": "https://www.drupal.org",
"feed_url": "https://www.drupal.org/announcements.json",
"favicon": "https://www.drupal.org/favicon.ico",
"items": [
{
"id": "201",
"title": "new 9 - 10 Drupal 9.1.3 is available",
"content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
"url": "https://www.drupal.org/project/announce",
"date_modified": "2021-01-19T07:29:38+00:00",
"date_published": "2021-01-18T07:29:38+00:00",
"_drupalorg": {
"featured": true,
"version": "^9 | ^10"
}
},
{
"id": "2021",
"title": "updated 10 - DrupalCon is here",
"content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
"url": "https://www.drupal.org/project/announce",
"date_modified": "2021-01-19T07:29:38+00:00",
"date_published": "2021-01-18T07:29:38+00:00",
"_drupalorg": {
"featured": false,
"version": "^10"
}
},
{
"id": "2031",
"title": "new 9 only - Download latest drupal here",
"content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
"url": "https://www.drupal.org/project/announce",
"date_modified": "2021-01-19T07:29:38+00:00",
"date_published": "2021-01-18T07:29:38+00:00",
"_drupalorg": {
"featured": false,
"version": "^9"
}
}
]
}

View File

@@ -0,0 +1,69 @@
{
"version": "https://jsonfeed.org/version/1.1",
"title": "Drupal Announcements Feed",
"home_page_url": "https://www.drupal.org",
"feed_url": "https://www.drupal.org/announcements.json",
"favicon": "https://www.drupal.org/favicon.ico",
"items": [
{
"id": "201",
"title": "new 9 - 10 Drupal 9.1.3 is available",
"content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
"url": "https://www.drupal.org/project/announce",
"date_modified": "2021-01-19T07:29:38+00:00",
"date_published": "2021-01-18T07:29:38+00:00",
"_drupalorg":{
"featured": true,
"version": "^9 | ^10"
}
},
{
"id": "2021",
"title": "updated 10 - DrupalCon is here",
"content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
"url": "https://www.drupal.org/project/announce",
"date_modified": "2021-01-19T07:29:38+00:00",
"date_published": "2021-01-18T07:29:38+00:00",
"_drupalorg": {
"featured": false,
"version": "^10"
}
},
{
"id": "2031",
"title": "new 9 only - Download latest drupal here",
"content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
"url": "https://www.drupal.org/project/announce",
"date_modified": "2021-01-19T07:29:38+00:00",
"date_published": "2021-01-18T07:29:38+00:00",
"_drupalorg": {
"featured": false,
"version": "^9"
}
},
{
"id": "2043",
"title": "announce title updated",
"content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
"url": "https://www.drupal.org/project/announce",
"date_modified": "2021-01-19T07:29:39+00:00",
"date_published": "2021-01-18T07:29:39+00:00",
"_drupalorg": {
"featured": false,
"version": "^10"
}
},
{
"id": "2044",
"title": "Only 10 - Drupal 106 is available and this feed is Updated",
"content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
"url": "https://www.drupal.org/project/announce-updated",
"date_modified": "2021-01-19T07:29:39+00:00",
"date_published": "2021-01-18T07:29:39+00:00",
"_drupalorg": {
"featured": false,
"version": "^10"
}
}
]
}

View File

@@ -0,0 +1,9 @@
name: 'Announce feed test'
type: module
description: 'Support module for announce feed testing.'
package: Testing
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,7 @@
announce_feed_test.json_test:
path: '/announce-feed-json/{json_name}'
defaults:
_title: 'Announce Feed test'
_controller: '\Drupal\announce_feed_test\Controller\AnnounceTestController::setFeedConfig'
requirements:
_access: 'TRUE'

View File

@@ -0,0 +1,5 @@
services:
announce_feed_test.announce_client_middleware:
class: Drupal\announce_feed_test\AnnounceTestHttpClientMiddleware
tags:
- { name: http_client_middleware }

View File

@@ -0,0 +1,48 @@
<?php
namespace Drupal\announce_feed_test;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Url;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\RequestInterface;
/**
* Overrides the requested endpoint when running tests.
*/
class AnnounceTestHttpClientMiddleware {
/**
* HTTP middleware that replaces request endpoint for a test one.
*/
public function __invoke(): \Closure {
return function ($handler) {
return function (RequestInterface $request, array $options) use ($handler): PromiseInterface {
$test_end_point = \Drupal::state()->get('announce_test_endpoint');
if ($test_end_point && str_contains($request->getUri(), '://www.drupal.org/announcements.json')) {
// Only override $uri if it matches the advisories JSON feed to avoid
// changing any other uses of the 'http_client' service during tests with
// this module installed.
$request = $request->withUri(new Uri($test_end_point));
}
return $handler($request, $options);
};
};
}
/**
* Sets the test endpoint for the advisories JSON feed.
*
* @param string $test_endpoint
* The test endpoint.
*/
public static function setAnnounceTestEndpoint(string $test_endpoint): void {
// Convert the endpoint to an absolute URL.
$test_endpoint = Url::fromUri('base:/' . $test_endpoint)->setAbsolute()->toString();
\Drupal::state()->set('announce_test_endpoint', $test_endpoint);
\Drupal::service('keyvalue.expirable')->get('announcements_feed')->delete('announcements');
Cache::invalidateTags(['announcements_feed:feed']);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Drupal\announce_feed_test\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
/**
* Defines a controller to return JSON for security advisory tests.
*/
class AnnounceTestController {
/**
* Reads a JSON file and returns the contents as a Response.
*
* This method will replace the string '[CORE_VERSION]' with the current core
* version to allow testing core version matches.
*
* @param string $json_name
* The name of the JSON file without the file extension.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\Response
* If a fixture file with the name $json_name + '.json' is found a
* JsonResponse will be returned using the contents of the file, otherwise a
* Response will be returned with a 404 status code.
*/
public function setFeedConfig(string $json_name): JsonResponse|Response {
$file = __DIR__ . "/../../../../announce_feed/$json_name.json";
$headers = ['Content-Type' => 'application/json; charset=utf-8'];
if (!is_file($file)) {
// Return an empty response.
return new Response('', 404, $headers);
}
return new JsonResponse(file_get_contents($file), 200, $headers, TRUE);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\announcements_feed\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber;
/**
* Defines a class for testing pages are still cacheable with dynamic page cache.
*
* @group announcements_feed
*/
final class AnnouncementsCacheTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = [
'announcements_feed',
'dynamic_page_cache',
'toolbar',
];
/**
* Tests dynamic page cache.
*/
public function testDynamicPageCache(): void {
$this->drupalLogin($this->drupalCreateUser([
'access toolbar',
'access announcements',
]));
// Front-page is visited right after login.
$this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'MISS');
// Reload the page, it should be cached now.
$this->drupalGet(Url::fromRoute('<front>'));
$this->assertSession()->elementExists('css', '[data-drupal-announce-trigger]');
$this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'HIT');
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\announcements_feed\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for announcements_feed.
*
* @group announcements_feed
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\announcements_feed\FunctionalJavascript;
use Drupal\Tests\system\FunctionalJavascript\OffCanvasTestBase;
use Drupal\announce_feed_test\AnnounceTestHttpClientMiddleware;
/**
* Test the access announcement permissions to get access announcement icon.
*
* @group announcements_feed
*/
class AccessAnnouncementTest extends OffCanvasTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'user',
'toolbar',
'announcements_feed',
'announce_feed_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
public function setUp():void {
parent::setUp();
AnnounceTestHttpClientMiddleware::setAnnounceTestEndpoint('/announce-feed-json/community-feeds');
}
/**
* Test of viewing announcements by a user with appropriate permission.
*/
public function testAnnounceFirstLogin(): void {
$this->drupalLogin(
$this->drupalCreateUser(
[
'access toolbar',
'access announcements',
]
)
);
$this->drupalGet('<front>');
// Check that the user can see the toolbar.
$this->assertSession()->elementExists('css', '#toolbar-bar');
// And the announcements.
$this->assertSession()->elementExists('css', '.toolbar-icon-announce');
}
/**
* Testing announce icon without announce permission.
*/
public function testAnnounceWithoutPermission(): void {
// User without "access announcements" permission.
$account = $this->drupalCreateUser(
[
'access toolbar',
]
);
$this->drupalLogin($account);
$this->drupalGet('<front>');
// Check that the user can see the toolbar.
$this->assertSession()->elementExists('css', '#toolbar-bar');
// But not the announcements.
$this->assertSession()->elementNotExists('css', '.toolbar-icon-announce');
$this->drupalGet('admin/announcements_feed');
$this->assertSession()->responseContains('You are not authorized to access this page.');
}
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\announcements_feed\FunctionalJavascript;
use Drupal\Tests\system\FunctionalJavascript\OffCanvasTestBase;
use Drupal\announce_feed_test\AnnounceTestHttpClientMiddleware;
use Drupal\user\UserInterface;
/**
* Test the access announcement according to json feed changes.
*
* @group announcements_feed
*/
class AlertsJsonFeedTest extends OffCanvasTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'user',
'toolbar',
'announcements_feed',
'announce_feed_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to access toolbar and access announcements.
*
* @var \Drupal\user\UserInterface
*/
protected UserInterface $user;
/**
* {@inheritdoc}
*/
public function setUp():void {
parent::setUp();
$this->user = $this->drupalCreateUser(
[
'access toolbar',
'access announcements',
]
);
AnnounceTestHttpClientMiddleware::setAnnounceTestEndpoint('/announce-feed-json/community-feeds');
}
/**
* Check the status of the announcements when the feed is updated and removed.
*/
public function testAnnounceFeedUpdatedAndRemoved(): void {
$this->markTestSkipped('Skipped due to major version-specific logic. See https://www.drupal.org/project/drupal/issues/3359322');
$this->drupalLogin($this->user);
$this->drupalGet('<front>');
$this->clickLink('Announcements');
$this->waitForOffCanvasToOpen();
$page_html = $this->getSession()->getPage()->getHtml();
$this->assertStringNotContainsString('Only 10 - Drupal 106 is available and this feed is Updated', $page_html);
// Change the feed url and reset temp storage.
AnnounceTestHttpClientMiddleware::setAnnounceTestEndpoint('/announce-feed-json/updated');
$this->drupalGet('<front>');
$this->clickLink('Announcements');
$this->waitForOffCanvasToOpen();
$page_html = $this->getSession()->getPage()->getHtml();
$this->assertStringContainsString('Only 10 - Drupal 106 is available and this feed is Updated', $page_html);
$this->drupalLogout();
// Change the feed url and reset temp storage.
AnnounceTestHttpClientMiddleware::setAnnounceTestEndpoint('/announce-feed-json/removed');
$this->drupalLogin($this->user);
$this->drupalGet('<front>');
$this->clickLink('Announcements');
$this->waitForOffCanvasToOpen();
$page_html = $this->getSession()->getPage()->getHtml();
$this->assertStringNotContainsString('Only 10 - Drupal 106 is available and this feed is Updated', $page_html);
}
/**
* Check with an empty JSON feed.
*/
public function testAnnounceFeedEmpty(): void {
// Change the feed url and reset temp storage.
AnnounceTestHttpClientMiddleware::setAnnounceTestEndpoint('/announce-feed-json/empty');
$this->drupalLogin($this->user);
$this->drupalGet('<front>');
// Removed items should not display in the announcement model.
$this->clickLink('Announcements');
$this->waitForOffCanvasToOpen();
$this->assertStringContainsString('No announcements available', $this->getSession()->getPage()->getHtml());
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\announcements_feed\FunctionalJavascript;
use Drupal\announce_feed_test\AnnounceTestHttpClientMiddleware;
use Drupal\block\BlockInterface;
use Drupal\Core\Access\AccessResultAllowed;
use Drupal\Core\Access\AccessResultNeutral;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Test the announcement block test visibility.
*
* @group announcements_feed
*/
class AnnounceBlockTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'announcements_feed',
'block',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The announce block instance.
*
* @var \Drupal\block\BlockInterface
*/
protected BlockInterface $announceBlock;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
AnnounceTestHttpClientMiddleware::setAnnounceTestEndpoint('/announce-feed-json/community-feeds');
$this->announceBlock = $this->placeBlock('announce_block', [
'label' => 'Announcements Feed',
]);
}
/**
* Testing announce feed block visibility.
*/
public function testAnnounceWithoutPermission(): void {
// User with "access announcements" permission and anonymous session.
$account = $this->drupalCreateUser([
'access announcements',
]);
$anonymous_account = new AnonymousUserSession();
$this->drupalLogin($account);
$this->drupalGet('<front>');
$assert_session = $this->assertSession();
// Block should be visible for the user.
$assert_session->pageTextContains('Announcements Feed');
// Block is not accessible without permission.
$this->drupalLogout();
$assert_session->pageTextNotContains('Announcements Feed');
// Test access() method return type.
$this->assertTrue($this->announceBlock->getPlugin()->access($account));
$this->assertInstanceOf(AccessResultAllowed::class, $this->announceBlock->getPlugin()->access($account, TRUE));
$this->assertFalse($this->announceBlock->getPlugin()->access($anonymous_account));
$this->assertInstanceOf(AccessResultNeutral::class, $this->announceBlock->getPlugin()->access($anonymous_account, TRUE));
}
}

View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\announcements_feed\Kernel;
use GuzzleHttp\Psr7\Response;
/**
* @coversDefaultClass \Drupal\announcements_feed\AnnounceFetcher
*
* @group announcements_feed
*/
class AnnounceFetcherTest extends AnnounceTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['announcements_feed']);
}
/**
* Tests announcement that should be displayed.
*
* @param mixed[] $feed_item
* The feed item to test. 'title' and 'url' are omitted from this array
* because they do not need to vary between test cases.
*
* @dataProvider providerShowAnnouncements
*/
public function testShowAnnouncements(array $feed_item): void {
$this->markTestSkipped('Skipped due to major version-specific logic. See https://www.drupal.org/project/drupal/issues/3359322');
$this->setFeedItems([$feed_item]);
$feeds = $this->fetchFeedItems();
$this->assertCount(1, $feeds);
$this->assertSame('https://www.drupal.org/project/announce', $feeds[0]->url);
$this->assertSame('Drupal security update Test', $feeds[0]->title);
$this->assertSame('^10', $feeds[0]->version);
$this->assertCount(1, $this->history);
}
/**
* Tests feed fields.
*/
public function testFeedFields(): void {
$this->markTestSkipped('Skipped due to major version-specific logic. See https://www.drupal.org/project/drupal/issues/3359322');
$feed_item_1 = [
'id' => '1001',
'content_html' => 'Test teaser 1',
'url' => 'https://www.drupal.org/project/announce',
'_drupalorg' => [
'featured' => TRUE,
'version' => '^10',
],
'date_modified' => "2021-09-02T15:09:42+00:00",
'date_published' => "2021-09-01T15:09:42+00:00",
];
$this->setFeedItems([$feed_item_1]);
$feeds = $this->fetchFeedItems();
$this->assertCount(1, $feeds);
$this->assertSame($feed_item_1['id'], $feeds[0]->id);
$this->assertSame($feed_item_1['content_html'], $feeds[0]->content_html);
$this->assertSame($feed_item_1['_drupalorg']['featured'], $feeds[0]->featured);
$this->assertSame($feed_item_1['date_published'], $feeds[0]->date_published);
$this->assertSame($feed_item_1['_drupalorg']['version'], $feeds[0]->version);
}
/**
* Data provider for testShowAnnouncements().
*/
public static function providerShowAnnouncements(): array {
return [
'1' => [
'feed_item' => [
'id' => '1001',
'content_html' => 'Test teaser 1',
'_drupalorg' => [
'featured' => 1,
'version' => '^10',
],
'date_modified' => "2021-09-02T15:09:42+00:00",
'date_published' => "2021-09-01T15:09:42+00:00",
],
],
'2' => [
'feed_item' => [
'id' => '1002',
'content_html' => 'Test teaser 2',
'_drupalorg' => [
'featured' => 1,
'version' => '^10',
],
'date_modified' => "2021-09-02T15:09:42+00:00",
'date_published' => "2021-09-01T15:09:42+00:00",
],
],
'3' => [
'feed_item' => [
'id' => '1003',
'content_html' => 'Test teaser 3',
'_drupalorg' => [
'featured' => 1,
'version' => '^10',
],
'date_modified' => "2021-09-02T15:09:42+00:00",
'date_published' => "2021-09-01T15:09:42+00:00",
],
],
'4' => [
'feed_item' => [
'id' => '1004',
'content_html' => 'Test teaser 4',
'_drupalorg' => [
'featured' => 1,
'version' => '^10',
],
'date_modified' => "2021-09-02T15:09:42+00:00",
'date_published' => "2021-09-01T15:09:42+00:00",
],
],
];
}
/**
* Sets the feed items to be returned for the test.
*
* @param mixed[][] $feed_items
* The feeds items to test. Every time the http_client makes a request the
* next item in this array will be returned. For each feed item 'title' and
* 'url' are omitted because they do not need to vary between test cases.
*/
protected function setFeedItems(array $feed_items): void {
$responses = [];
foreach ($feed_items as $feed_item) {
$feed_item += [
'title' => 'Drupal security update Test',
'url' => 'https://www.drupal.org/project/announce',
];
$responses[] = new Response(200, [], json_encode(['items' => [$feed_item]]));
}
$this->setTestFeedResponses($responses);
}
/**
* Gets the announcements from the 'announce.fetcher' service.
*
* @return \Drupal\announcements_feed\Announcement[]
* The return value of AnnounceFetcher::fetch().
*/
protected function fetchFeedItems(): array {
return $this->container->get('announcements_feed.fetcher')->fetch();
}
}

View File

@@ -0,0 +1,228 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\announcements_feed\Kernel;
use Drupal\Tests\user\Traits\UserCreationTrait;
use GuzzleHttp\Psr7\Response;
/**
* @coversDefaultClass \Drupal\announcements_feed\AnnounceFetcher
*
* @group announcements_feed
*/
class AnnounceFetcherUserTest extends AnnounceTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'toolbar',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installSchema('user', ['users_data']);
// Setting current user.
$permissions = [
'access toolbar',
'access announcements',
];
$this->setUpCurrentUser(['uid' => 1], $permissions);
}
/**
* Tests testAllAnnouncements should get all announcements.
*
* First time accessing the announcements.
*/
public function testAllAnnouncementsFirst(): void {
$this->markTestSkipped('Skipped due to major version-specific logic. See https://www.drupal.org/project/drupal/issues/3359322');
$feed_items = $this->providerShowAnnouncements();
// First time access.
$this->setFeedItems($feed_items);
$all_items = $this->container->get('announcements_feed.fetcher')->fetch();
$this->assertCount(4, $all_items);
$this->assertCount(1, $this->history);
// Second time access.
$this->setFeedItems($feed_items);
$all_items = $this->container->get('announcements_feed.fetcher')->fetch();
$this->assertCount(4, $all_items);
$this->assertCount(2, $this->history);
// Create another user and test again.
$permissions = [
'access toolbar',
'access announcements',
];
$this->setUpCurrentUser(['uid' => 2], $permissions);
$this->setFeedItems($feed_items);
// First time access.
$all_items = $this->container->get('announcements_feed.fetcher')->fetch();
$this->assertCount(4, $all_items);
$this->assertCount(3, $this->history);
// Check after adding new record.
$feed_items = $this->providerShowUpdatedAnnouncements();
$this->setFeedItems($feed_items);
$all_items = $this->container->get('announcements_feed.fetcher')->fetch();
$this->assertCount(5, $all_items);
$this->assertSame('1005', $all_items[0]->id);
$this->assertCount(4, $this->history);
}
/**
* Data provider for testAllAnnouncements().
*/
public function providerShowAnnouncements(): array {
return [
[
'id' => '1001',
'title' => 'Drupal security update Test',
'url' => 'https://www.drupal.org/project/announce',
'content_html' => 'Test teaser 1',
'_drupalorg' => [
'featured' => TRUE,
'version' => '^10',
],
'date_modified' => date('c', 1611041378),
'date_published' => date('c', 1610958578),
],
[
'id' => '1002',
'title' => 'Drupal security update Test',
'url' => 'https://www.drupal.org/project/announce',
'content_html' => 'Test teaser 2',
'_drupalorg' => [
'featured' => TRUE,
'version' => '^10',
],
'date_modified' => date('c', 1611041378),
'date_published' => date('c', 1610958578),
],
[
'id' => '1003',
'title' => 'Drupal security update Test',
'url' => 'https://www.drupal.org/project/announce',
'content_html' => 'Test teaser 3',
'_drupalorg' => [
'featured' => TRUE,
'version' => '^10',
],
'date_modified' => date('c', 1611041378),
'date_published' => date('c', 1610958578),
],
[
'id' => '1004',
'title' => 'Drupal security update Test',
'url' => 'https://www.drupal.org/project/announce',
'content_html' => 'Test teaser 4',
'_drupalorg' => [
'featured' => TRUE,
'version' => '^10',
],
'date_modified' => date('c', 1611041378),
'date_published' => date('c', 1610958578),
],
];
}
/**
* Data provider for testAllAnnouncements().
*/
public function providerShowUpdatedAnnouncements(): array {
return [
[
'id' => '1005',
'title' => 'Drupal security update Test new',
'url' => 'https://www.drupal.org/project/announce',
'content_html' => 'Test teaser 1',
'_drupalorg' => [
'featured' => TRUE,
'version' => '^10',
],
'date_modified' => date('c', 1611041378),
'date_published' => date('c', 1610958578),
],
[
'id' => '1001',
'title' => 'Drupal security update Test',
'url' => 'https://www.drupal.org/project/announce',
'content_html' => 'Test teaser 1',
'_drupalorg' => [
'featured' => TRUE,
'version' => '^10',
],
'date_modified' => date('c', 1611041378),
'date_published' => date('c', 1610958578),
],
[
'id' => '1002',
'title' => 'Drupal security update Test',
'url' => 'https://www.drupal.org/project/announce',
'content_html' => 'Test teaser 2',
'_drupalorg' => [
'featured' => TRUE,
'version' => '^10',
],
'date_modified' => date('c', 1611041378),
'date_published' => date('c', 1610958578),
],
[
'id' => '1003',
'title' => 'Drupal security update Test',
'url' => 'https://www.drupal.org/project/announce',
'content_html' => 'Test teaser 3',
'_drupalorg' => [
'featured' => TRUE,
'version' => '^10',
],
'date_modified' => date('c', 1611041378),
'date_published' => date('c', 1610958578),
],
[
'id' => '1004',
'title' => 'Drupal security update Test',
'url' => 'https://www.drupal.org/project/announce',
'content_html' => 'Test teaser 4',
'_drupalorg' => [
'featured' => TRUE,
'version' => '^10',
],
'date_modified' => date('c', 1611041378),
'date_published' => date('c', 1610958578),
],
];
}
/**
* Sets the feed items to be returned for the test.
*
* @param mixed[][] $feed_items
* The feeds items to test. Every time the http_client makes a request the
* next item in this array will be returned. For each feed item 'title' and
* 'url' are omitted because they do not need to vary between test cases.
*/
protected function setFeedItems(array $feed_items): void {
$responses[] = new Response(200, [], json_encode(['items' => $feed_items]));
$responses[] = new Response(200, [], json_encode(['items' => $feed_items]));
$responses[] = new Response(200, [], json_encode(['items' => $feed_items]));
$this->setTestFeedResponses($responses);
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\announcements_feed\Kernel;
use GuzzleHttp\Psr7\Response;
/**
* @coversDefaultClass \Drupal\announcements_feed\AnnounceRenderer
*
* @group announcements_feed
*/
class AnnounceRendererTest extends AnnounceTestBase {
/**
* Tests rendered valid when something goes wrong.
*/
public function testRendererException(): void {
$this->setTestFeedResponses([
new Response(403),
]);
$render = $this->container->get('announcements_feed.renderer')->render();
$this->assertEquals('status_messages', $render['#theme']);
$this->assertEquals('An error occurred while parsing the announcements feed, check the logs for more information.', $render['#message_list']['error'][0]);
}
/**
* Tests rendered valid content.
*/
public function testRendererContent(): void {
$feed_item_1 = [
'id' => '1001',
'content_html' => 'Test teaser 1',
'url' => 'https://www.drupal.org/project/announce',
'_drupalorg' => [
'featured' => TRUE,
'version' => '^10||^11',
],
'date_modified' => "2021-09-02T15:09:42+00:00",
'date_published' => "2021-09-01T15:09:42+00:00",
];
$feed_item_2 = [
'id' => '1002',
'content_html' => 'Test teaser 1',
'url' => 'https://www.drupal.org/project/announce',
'_drupalorg' => [
'featured' => FALSE,
'version' => '^10||^11',
],
'date_modified' => "2021-09-02T15:09:42+00:00",
'date_published' => "2021-09-01T15:09:42+00:00",
];
$this->setFeedItems([$feed_item_1, $feed_item_2]);
$render = $this->container->get('announcements_feed.renderer')->render();
$this->assertEquals('announcements_feed', $render['#theme']);
$this->assertEquals(1, $render['#count']);
$this->assertEquals(1001, $render['#featured'][0]->id);
$render = $this->container->get('announcements_feed.renderer')->render();
$this->assertEquals('announcements_feed', $render['#theme']);
$this->assertEquals(1, $render['#count']);
$this->assertEquals(1002, $render['#standard'][0]->id);
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\announcements_feed\Kernel;
use Drupal\KernelTests\KernelTestBase;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Response;
/**
* Base class for Announce Kernel tests.
*/
class AnnounceTestBase extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'user',
'system',
'announcements_feed',
];
/**
* History of requests/responses.
*
* @var array
*/
protected array $history = [];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig('system');
$this->installConfig(['user']);
}
/**
* Sets the feed items to be returned for the test.
*
* @param mixed[][] $feed_items
* The feeds items to test. Every time the http_client makes a request the
* next item in this array will be returned. For each feed item 'title' and
* 'url' are omitted because they do not need to vary between test cases.
*/
protected function setFeedItems(array $feed_items): void {
$responses = [];
foreach ($feed_items as $feed_item) {
$feed_item += [
'title' => 'Drupal security update Test',
'url' => 'https://www.drupal.org/project/announce',
];
$responses[] = new Response(200, [], json_encode(['items' => [$feed_item]]));
}
$this->setTestFeedResponses($responses);
}
/**
* Sets test feed responses.
*
* @param \GuzzleHttp\Psr7\Response[] $responses
* The responses for the http_client service to return.
*/
protected function setTestFeedResponses(array $responses): void {
// Create a mock and queue responses.
$mock = new MockHandler($responses);
$handler_stack = HandlerStack::create($mock);
$history = Middleware::history($this->history);
$handler_stack->push($history);
// Rebuild the container because the 'system.sa_fetcher' service and other
// services may already have an instantiated instance of the 'http_client'
// service without these changes.
$this->container->get('kernel')->rebuildContainer();
$this->container = $this->container->get('kernel')->getContainer();
$this->container->set('http_client', new Client(['handler' => $handler_stack]));
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\announcements_feed\Unit;
use Drupal\Tests\UnitTestCase;
use Drupal\announcements_feed\AnnounceFetcher;
/**
* Simple test to ensure that asserts pass.
*
* @group announcements_feed
*/
class AnnounceFetcherUnitTest extends UnitTestCase {
/**
* The Fetcher service object.
*
* @var \Drupal\announcements_feed\AnnounceFetcher
*/
protected AnnounceFetcher $fetcher;
/**
* {@inheritdoc}
*/
public function setUp():void {
parent::setUp();
$httpClient = $this->createMock('GuzzleHttp\ClientInterface');
$config = $this->getConfigFactoryStub([
'announcements_feed.settings' => [
'max_age' => 86400,
'cron_interval' => 21600,
'limit' => 10,
],
]);
$tempStore = $this->createMock('Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface');
$tempStore->expects($this->once())
->method('get')
->willReturn($this->createMock('Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface'));
$logger = $this->createMock('Psr\Log\LoggerInterface');
$this->fetcher = new AnnounceFetcher($httpClient, $config, $tempStore, $logger, 'https://www.drupal.org/announcements.json');
}
/**
* Test the ValidateUrl() method.
*
* @covers \Drupal\announcements_feed\AnnounceFetcher::validateUrl
*
* @dataProvider urlProvider
*/
public function testValidateUrl($url, $isValid): void {
$this->assertEquals($isValid, $this->fetcher->validateUrl($url));
}
/**
* Data for the testValidateUrl.
*/
public static function urlProvider(): array {
return [
['https://www.drupal.org', TRUE],
['https://drupal.org', TRUE],
['https://api.drupal.org', TRUE],
['https://a.drupal.org', TRUE],
['https://123.drupal.org', TRUE],
['https://api-new.drupal.org', TRUE],
['https://api_new.drupal.org', TRUE],
['https://api-.drupal.org', TRUE],
['https://www.example.org', FALSE],
['https://example.org', FALSE],
['https://api.example.org/project/announce', FALSE],
['https://-api.drupal.org', FALSE],
['https://a.example.org/project/announce', FALSE],
['https://test.drupaal.com', FALSE],
['https://api.drupal.org.example.com', FALSE],
['https://example.org/drupal.org', FALSE],
];
}
}