Initial Drupal 11 with DDEV setup
This commit is contained in:
@ -0,0 +1,5 @@
|
||||
name: Announcements
|
||||
type: module
|
||||
description: Displays announcements from the Drupal community.
|
||||
version: VERSION
|
||||
package: Core
|
||||
@ -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: {}
|
||||
@ -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
|
||||
@ -0,0 +1,2 @@
|
||||
access announcements:
|
||||
title: 'View official announcements related to Drupal'
|
||||
@ -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'
|
||||
@ -0,0 +1,24 @@
|
||||
parameters:
|
||||
announcements_feed.feed_json_url: https://www.drupal.org/announcements.json
|
||||
announcements_feed.feed_link: https://www.drupal.org/about/announcements
|
||||
announcements_feed.skip_procedural_hook_scan: true
|
||||
|
||||
services:
|
||||
_defaults:
|
||||
autoconfigure: true
|
||||
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'
|
||||
@ -0,0 +1,3 @@
|
||||
max_age: 86400
|
||||
cron_interval: 21600
|
||||
limit: 10
|
||||
@ -0,0 +1,24 @@
|
||||
announcements_feed.settings:
|
||||
type: config_object
|
||||
label: 'Announcements Settings'
|
||||
constraints:
|
||||
FullyValidatable: ~
|
||||
mapping:
|
||||
max_age:
|
||||
type: integer
|
||||
label: 'Cache announcements for max-age seconds.'
|
||||
constraints:
|
||||
Range:
|
||||
min: 0
|
||||
cron_interval:
|
||||
type: integer
|
||||
label: 'Cron interval for fetching announcements in seconds.'
|
||||
constraints:
|
||||
Range:
|
||||
min: 0
|
||||
limit:
|
||||
type: integer
|
||||
label: 'Number of announcements that will be displayed.'
|
||||
constraints:
|
||||
Range:
|
||||
min: 0
|
||||
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 {
|
||||
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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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;
|
||||
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-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 */
|
||||
}
|
||||
|
||||
.toolbar .toolbar-bar .announce-toolbar-tab.toolbar-tab:dir(rtl) {
|
||||
float: left;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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 |
193
web/core/modules/announcements_feed/src/AnnounceFetcher.php
Normal file
193
web/core/modules/announcements_feed/src/AnnounceFetcher.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
84
web/core/modules/announcements_feed/src/AnnounceRenderer.php
Normal file
84
web/core/modules/announcements_feed/src/AnnounceRenderer.php
Normal 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
68
web/core/modules/announcements_feed/src/Announcement.php
Normal file
68
web/core/modules/announcements_feed/src/Announcement.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\announcements_feed\Hook;
|
||||
|
||||
use Drupal\announcements_feed\RenderCallbacks;
|
||||
use Drupal\Core\Link;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Hook\Attribute\Hook;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Hook implementations for announcements_feed.
|
||||
*/
|
||||
class AnnouncementsFeedHooks {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
#[Hook('help')]
|
||||
public function help($route_name, RouteMatchInterface $route_match): ?string {
|
||||
switch ($route_name) {
|
||||
case 'help.page.announcements_feed':
|
||||
$output = '';
|
||||
$output .= '<h2>' . $this->t('About') . '</h2>';
|
||||
$output .= '<p>' . $this->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>' . $this->t('Uses') . '</h2>';
|
||||
$output .= '<dl><dt>' . $this->t('Accessing announcements') . '</dt>';
|
||||
$output .= '<dd>' . $this->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($this->t('Announcements'), 'announcements_feed.announcement')->toString(),
|
||||
]) . '</dd>';
|
||||
$output .= '</dl>';
|
||||
return $output;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_toolbar().
|
||||
*/
|
||||
#[Hook('toolbar')]
|
||||
public function toolbar(): array {
|
||||
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().
|
||||
*/
|
||||
#[Hook('toolbar_alter')]
|
||||
public function toolbarAlter(&$items): void {
|
||||
// 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().
|
||||
*/
|
||||
#[Hook('theme')]
|
||||
public function theme($existing, $type, $theme, $path) : array {
|
||||
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().
|
||||
*/
|
||||
#[Hook('cron')]
|
||||
public function cron(): void {
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
96
web/core/modules/announcements_feed/src/LazyBuilders.php
Normal file
96
web/core/modules/announcements_feed/src/LazyBuilders.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?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\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Defines a class for lazy building render arrays.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class LazyBuilders implements TrustedCallbackInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* 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' => $this->t('Announcements'),
|
||||
'#url' => Url::fromRoute('announcements_feed.announcement'),
|
||||
'#id' => Html::getId('toolbar-item-announcement'),
|
||||
'#attributes' => [
|
||||
'title' => $this->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,0 +1,70 @@
|
||||
<?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\Attribute\Block;
|
||||
use Drupal\Core\Block\BlockBase;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides an 'Announcements Feed' block.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
#[Block(
|
||||
id: 'announce_block',
|
||||
admin_label: new TranslatableMarkup('Announcements Feed')),
|
||||
]
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
31
web/core/modules/announcements_feed/src/RenderCallbacks.php
Normal file
31
web/core/modules/announcements_feed/src/RenderCallbacks.php
Normal 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'];
|
||||
}
|
||||
|
||||
}
|
||||
@ -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' %}
|
||||
@ -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' %}
|
||||
@ -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 %}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
name: 'Announce feed test'
|
||||
type: module
|
||||
description: 'Support module for announce feed testing.'
|
||||
package: Testing
|
||||
@ -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'
|
||||
@ -0,0 +1,5 @@
|
||||
services:
|
||||
announce_feed_test.announce_client_middleware:
|
||||
class: Drupal\announce_feed_test\AnnounceTestHttpClientMiddleware
|
||||
tags:
|
||||
- { name: http_client_middleware }
|
||||
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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((string) $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']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\announcements_feed\Functional;
|
||||
|
||||
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',
|
||||
'node',
|
||||
'toolbar',
|
||||
];
|
||||
|
||||
/**
|
||||
* Tests dynamic page cache.
|
||||
*/
|
||||
public function testDynamicPageCache(): void {
|
||||
$node_type = $this->drupalCreateContentType();
|
||||
$node = $this->drupalCreateNode(['type' => $node_type->id()]);
|
||||
$this->drupalLogin($this->drupalCreateUser([
|
||||
'access toolbar',
|
||||
'access announcements',
|
||||
]));
|
||||
$this->drupalGet($node->toUrl());
|
||||
$this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'MISS');
|
||||
// Reload the page, it should be cached now.
|
||||
$this->drupalGet($node->toUrl());
|
||||
$this->assertSession()->elementExists('css', '[data-drupal-announce-trigger]');
|
||||
$this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'HIT');
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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.');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
<?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 {
|
||||
if ($this->name() === 'testAnnounceFeedUpdatedAndRemoved') {
|
||||
$this->markTestSkipped('Skipped due to major version-specific logic. See https://www.drupal.org/project/drupal/issues/3359322');
|
||||
}
|
||||
|
||||
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->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());
|
||||
}
|
||||
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,155 @@
|
||||
<?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 {
|
||||
$this->markTestSkipped('Skipped due to major version-specific logic. See https://www.drupal.org/project/drupal/issues/3359322');
|
||||
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->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 {
|
||||
$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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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.
|
||||
*/
|
||||
abstract 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]));
|
||||
}
|
||||
|
||||
}
|
||||
@ -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],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user