clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

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'];
}
}