Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Neos 9 compatibility #9

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions Classes/CatchUpHook/WorkspacePublishHook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace CodeQ\PublishNotifier\CatchUpHook;

use CodeQ\PublishNotifier\Notifier;
use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\EventStore\EventInterface;
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Event\WorkspaceWasPartiallyPublished;
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Event\WorkspaceWasPublished;
use Neos\ContentRepository\Core\Projection\CatchUpHookInterface;
use Neos\EventStore\Model\EventEnvelope;
use Neos\Flow\Configuration\Exception\InvalidConfigurationException;

final readonly class WorkspacePublishHook implements CatchUpHookInterface
{
public function __construct(
private ContentRepository $contentRepository,
) {
}

public function onBeforeCatchUp(): void
{
// Nothing to do here
}

public function onBeforeEvent(EventInterface $eventInstance, EventEnvelope $eventEnvelope): void
{
// Nothing to do here
}

/**
* @throws InvalidConfigurationException
*/
public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $eventEnvelope): void
{
match ($eventInstance::class) {
WorkspaceWasPublished::class => $this->sendNotification($eventInstance),
WorkspaceWasPartiallyPublished::class => $this->sendNotification($eventInstance),
default => null
};
}

public function onBeforeBatchCompleted(): void
{
// Nothing to do here
}

public function onAfterCatchUp(): void
{
// Nothing to do here
}

/**
* @throws InvalidConfigurationException
*/
private function sendNotification(EventInterface $event): void
{
$workspaceFinder = $this->contentRepository->getWorkspaceFinder();

/** @var WorkspaceWasPublished|WorkspaceWasPartiallyPublished $event */
(new Notifier)->notify($workspaceFinder->findOneByName($event->targetWorkspaceName));
}
}
17 changes: 17 additions & 0 deletions Classes/CatchUpHook/WorkspacePublishHookFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace CodeQ\PublishNotifier\CatchUpHook;

use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\Projection\CatchUpHookFactoryInterface;
use Neos\ContentRepository\Core\Projection\CatchUpHookInterface;

class WorkspacePublishHookFactory implements CatchUpHookFactoryInterface
{
public function build(ContentRepository $contentRepository): CatchUpHookInterface
{
return new WorkspacePublishHook(
$contentRepository,
);
}
}
172 changes: 68 additions & 104 deletions Classes/Notifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,112 +4,81 @@

namespace CodeQ\PublishNotifier;

use Neos\ContentRepository\Core\Projection\Workspace\Workspace;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Configuration\Exception\InvalidConfigurationException;
use Neos\Flow\Http\Client\Browser;
use Neos\Flow\Http\Client\CurlEngine;
use Neos\Flow\Log\Utility\LogEnvironment;
use Neos\Http\Factories\ServerRequestFactory;
use Neos\Http\Factories\StreamFactory;
use Neos\Neos\Domain\Service\UserService;
use Neos\SwiftMailer\Message;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Log\LoggerInterface;
use Neos\Flow\Configuration\Exception\InvalidConfigurationException;
use Neos\SwiftMailer\Message;
use Neos\ContentRepository\Domain\Model\Workspace;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\ContentRepository\Domain\Service\PublishingServiceInterface;
use Neos\Neos\Domain\Service\UserService;

/**
* @Flow\Scope("singleton")
*/
#[Flow\Scope("singleton")]
class Notifier
{
/**
* @Flow\Inject
* @var LoggerInterface
*/
protected $systemLogger;
#[Flow\Inject]
protected LoggerInterface $systemLogger;

/**
* @Flow\Inject
* @var UserService
*/
protected $userService;
#[Flow\Inject]
protected UserService $userService;

/**
* @Flow\Inject
* @var PublishingServiceInterface
*/
protected $publishingService;
#[Flow\Inject]
protected ServerRequestFactory $serverRequestFactory;

/**
* @Flow\Inject
* @var ServerRequestFactory
*/
protected $serverRequestFactory;
#[Flow\Inject]
protected StreamFactory $streamFactory;

/**
* @Flow\Inject
* @var StreamFactory
*/
protected $streamFactory;
#[Flow\InjectConfiguration(path: 'http.baseUri', package: 'Neos.Flow')]
protected ?string $baseUri;

/**
* @Flow\InjectConfiguration(package="Neos.Flow", path="http.baseUri")
* @var string
*/
protected $baseUri;
#[Flow\InjectConfiguration(path: 'notify', package: 'CodeQ.PublishNotifier')]
protected ?array $notifySettings;

/**
* @var array
*/
protected $settings;
#[Flow\InjectConfiguration(path: 'email', package: 'CodeQ.PublishNotifier')]
protected ?array $mailSettings;

/**
* @var bool
*/
protected $notificationHasBeenSentInCurrentInstance = false;
#[Flow\InjectConfiguration(path: 'slack', package: 'CodeQ.PublishNotifier')]
protected ?array $slackSettings;

/**
* Inject the settings
*
* @param array $settings
* @return void
*/
public function injectSettings(array $settings) {
$this->settings = $settings;
}
protected bool $notificationHasBeenSentInCurrentInstance = false;

/**
* Send out emails for a change in a workspace.
*
* @param Workspace $targetWorkspace
* @return void
* @throws InvalidConfigurationException
*/
protected function sendEmails($targetWorkspace)
protected function sendEmails(Workspace $targetWorkspace): void
{
if(!$this->settings['email']['enabled']) {
if(!$this->mailSettings['enabled']) {
return;
}
if(!$this->settings['email']['senderAddress']) {
if(!$this->mailSettings['senderAddress']) {
throw new InvalidConfigurationException('The CodeQ.PublishNotifier email.senderAddress configuration does not exist.');
}
if(!$this->settings['email']['notifyEmails']) {
if(!$this->mailSettings['notifyEmails']) {
throw new InvalidConfigurationException('The CodeQ.PublishNotifier email.notifyEmails configuration does not exist.');
}

// TODO: why current user `null`?
$currentUser = $this->userService->getCurrentUser();
$currentUserName = $currentUser->getLabel();
$targetWorkspaceName = $targetWorkspace->getTitle();
$reviewUrl = sprintf('%1$s/neos/management/workspaces/show?moduleArguments[workspace][__identity]=%2$s', $this->baseUri, $targetWorkspace->getName());

$senderAddress = $this->settings['email']['senderAddress'];
$senderName = $this->settings['email']['senderName'];
$subject = sprintf($this->settings['email']['subject'], $currentUserName);
$body = sprintf($this->settings['email']['body'], $currentUserName, $targetWorkspaceName, $reviewUrl);
$currentUserName = $currentUser?->getLabel() ?: 'null';
$targetWorkspaceName = $targetWorkspace->workspaceTitle->jsonSerialize();
$reviewUrl = sprintf('%1$s/neos/management/workspaces/show?moduleArguments[workspace][__identity]=%2$s', $this->baseUri, $targetWorkspace->workspaceTitle->jsonSerialize());

$senderAddress = $this->mailSettings['senderAddress'];
$senderName = $this->mailSettings['senderName'];
$subject = sprintf($this->mailSettings['subject'], $currentUserName);
$body = sprintf($this->mailSettings['body'], $currentUserName, $targetWorkspaceName, $reviewUrl);

foreach ($this->mailSettings['notifyEmails'] as $email) {
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidConfigurationException('The CodeQ.PublishNotifier email.notifyEmails entry `' . $email . '` is not valid.');
}

foreach ($this->settings['email']['notifyEmails'] as $email) {
try {
$mail = new Message();
$mail
Expand All @@ -119,37 +88,36 @@ protected function sendEmails($targetWorkspace)
$mail->setBody($body, 'text/plain');
$mail->send();
} catch (\Exception $exception) {
$this->systemLogger->logException($exception);
$this->systemLogger->alert($exception->getMessage());
}
}
}

/**
* Send out a Slack message for a change in a workspace.
*
* @param Workspace $targetWorkspace
* @return void
* @throws InvalidConfigurationException
*/
protected function sendSlackMessages($targetWorkspace)
protected function sendSlackMessages(Workspace $targetWorkspace): void
{
if(!$this->settings['slack']['enabled']) {
if (!$this->slackSettings['enabled']) {
return;
}
if(empty($this->settings['slack']['postTo'])) {
if (empty($this->slackSettings['postTo'])) {
throw new InvalidConfigurationException('The CodeQ.PublishNotifier slack.postTo configuration expects at least one target if enabled.');
}

// TODO: why current user `null`?
$currentUser = $this->userService->getCurrentUser();
$currentUserName = $currentUser->getLabel();
$targetWorkspaceName = $targetWorkspace->getTitle();
$reviewUrl = sprintf('%1$s/neos/management/workspaces/show?moduleArguments[workspace][__identity]=%2$s', $this->baseUri, $targetWorkspace->getName());
$currentUserName = $currentUser?->getLabel() ?: 'null';
$targetWorkspaceName = $targetWorkspace->workspaceTitle->jsonSerialize();
$reviewUrl = sprintf('%1$s/neos/management/workspaces/show?moduleArguments[workspace][__identity]=%2$s', $this->baseUri, $targetWorkspace->workspaceTitle->jsonSerialize());

$message = sprintf($this->settings['slack']['message'], $currentUserName, $targetWorkspaceName, $reviewUrl);
$message = sprintf($this->slackSettings['message'], $currentUserName, $targetWorkspaceName, $reviewUrl);

foreach ($this->settings['slack']['postTo'] as $postToKey => $postTo) {
if (empty($postTo['webhookUrl'])) {
throw new InvalidConfigurationException('The CodeQ.PublishNotifier slack.postTo ' . $postToKey . ' requires a webhookUrl.');
foreach ($this->slackSettings['postTo'] as $postToKey => $postTo) {
if (empty($postTo['webhookUrl']) || !filter_var($postTo['webhookUrl'], FILTER_VALIDATE_URL)) {
throw new InvalidConfigurationException('The CodeQ.PublishNotifier slack.postTo ' . $postToKey . ' requires a valid webhookUrl.');
}

try {
Expand All @@ -158,28 +126,23 @@ protected function sendSlackMessages($targetWorkspace)
$engine->setOption(CURLOPT_TIMEOUT, 0);
$browser->setRequestEngine($engine);

$requestBody = array(
"text" => $message
);
$requestBody = ["text" => $message];

$slackRequest = $this->serverRequestFactory->createServerRequest('POST', $postTo['webhookUrl'])
->withHeader('Content-Type', 'application/json')
->withBody($this->streamFactory->createStream(json_encode($requestBody)));

$browser->sendRequest($slackRequest);
} catch (ClientExceptionInterface $e) {
} catch (ClientExceptionInterface) {
$this->systemLogger->warning(sprintf('Could not send message to Slack webhook %s with message "%s"', $postTo['webhookUrl'], $message), LogEnvironment::fromMethodName(__METHOD__));
}
}
}

/**
* @param NodeInterface $node
* @param Workspace $targetWorkspace
* @return void
* @throws InvalidConfigurationException
*/
public function notify($node, $targetWorkspace)
public function notify(Workspace $targetWorkspace): void
{
// skip sending another notification if more than one node is to be published
if ($this->notificationHasBeenSentInCurrentInstance) {
Expand All @@ -192,21 +155,22 @@ public function notify($node, $targetWorkspace)
}

// skip changes to public/live workspace
if ($targetWorkspace->isPublicWorkspace() && !$this->settings['notify']['publicWorkspace']) {
if ($targetWorkspace->isPublicWorkspace() && !$this->notifySettings['publicWorkspace']) {
return;
}

if($targetWorkspace->isInternalWorkspace()) {
$isFirstChangeInWorkspace = !$this->publishingService->getUnpublishedNodes($targetWorkspace);

if ($isFirstChangeInWorkspace && !$this->settings['notify']['internalWorkspace']['onFirstChange']) {
return;
}

if (!$isFirstChangeInWorkspace && !$this->settings['notify']['internalWorkspace']['onAdditionalChange']) {
return;
}
}
// TODO: implement diff check between Workspaces aka `$isFirstChangeInWorkspace`
// if($targetWorkspace->isInternalWorkspace()) {
// $isFirstChangeInWorkspace = !$this->publishingService->getUnpublishedNodes($targetWorkspace);
//
// if ($isFirstChangeInWorkspace && !$this->notifySettings['internalWorkspace']['onFirstChange']) {
// return;
// }
//
// if (!$isFirstChangeInWorkspace && !$this->notifySettings['internalWorkspace']['onAdditionalChange']) {
// return;
// }
// }

$this->sendEmails($targetWorkspace);
$this->sendSlackMessages($targetWorkspace);
Expand Down
21 changes: 0 additions & 21 deletions Classes/Package.php

This file was deleted.

10 changes: 10 additions & 0 deletions Configuration/Settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,13 @@ CodeQ:
message: |+
*%1$s* has published changes to the workspace *%2$s*.
Please review the changes and publish to live: <%3$s|here>

Neos:
ContentRepositoryRegistry:
presets:
'default':
projections:
'Neos.ContentRepository:Workspace':
catchUpHooks:
'CodeQ.PublishNotifier:WorkspacePublishHook':
factoryObjectName: CodeQ\PublishNotifier\CatchUpHook\WorkspacePublishHookFactory
Loading