Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
<?php
namespace Mautic\NotificationBundle\Api;
use GuzzleHttp\Client;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\PageBundle\Model\TrackableModel;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Psr\Http\Message\ResponseInterface;
abstract class AbstractNotificationApi
{
public function __construct(
protected Client $http,
protected TrackableModel $trackableModel,
protected IntegrationHelper $integrationHelper,
) {
}
/**
* @param string $endpoint One of "apps", "players", or "notifications"
* @param array $data Array of data to send
*/
abstract public function send(string $endpoint, array $data): ResponseInterface;
/**
* @return ResponseInterface
*/
abstract public function sendNotification($id, Notification $notification);
/**
* Convert a non-tracked url to a tracked url.
*
* @param string $url
*
* @return string
*/
public function convertToTrackedUrl($url, array $clickthrough, Notification $notification)
{
/* @var \Mautic\PageBundle\Entity\Redirect $redirect */
$trackable = $this->trackableModel->getTrackableByUrl($url, 'notification', $clickthrough['notification']);
return $this->trackableModel->generateTrackableUrl($trackable, $clickthrough, [], $notification->getUtmTags());
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace Mautic\NotificationBundle\Api;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\NotificationBundle\Exception\MissingApiKeyException;
use Mautic\NotificationBundle\Exception\MissingAppIDException;
use Psr\Http\Message\ResponseInterface;
class OneSignalApi extends AbstractNotificationApi
{
/**
* @var string
*/
protected $apiUrlBase = 'https://onesignal.com/api/v1';
/**
* @throws MissingAppIDException
* @throws MissingApiKeyException
*/
public function send(string $endpoint, array $data): ResponseInterface
{
$apiKeys = $this->integrationHelper->getIntegrationObject('OneSignal')->getKeys();
$appId = $apiKeys['app_id'];
$restApiKey = $apiKeys['rest_api_key'];
if (!$restApiKey) {
throw new MissingApiKeyException();
}
if (!array_key_exists('app_id', $data)) {
if (!$appId) {
throw new MissingAppIDException();
}
$data['app_id'] = $appId;
}
return $this->http->post(
$this->apiUrlBase.$endpoint,
[
\GuzzleHttp\RequestOptions::HEADERS => [
'Authorization' => 'Basic '.$restApiKey,
'Content-Type' => 'application/json',
],
\GuzzleHttp\RequestOptions::JSON => $data,
\GuzzleHttp\RequestOptions::HTTP_ERRORS => false,
]
);
}
/**
* @param string|array $playerId Player ID as string, or an array of player ID's
*
* @throws \Exception
*/
public function sendNotification($playerId, Notification $notification): ResponseInterface
{
$data = [];
$buttonId = $notification->getHeading();
$title = $notification->getHeading();
$url = $notification->getUrl();
$button = $notification->getButton();
$message = $notification->getMessage();
if (!is_array($playerId)) {
$playerId = [$playerId];
}
$data['include_player_ids'] = $playerId;
if (!is_array($message)) {
$message = ['en' => $message];
}
$data['contents'] = $message;
if (!empty($title)) {
if (!is_array($title)) {
$title = ['en' => $title];
}
}
$data['headings'] = $title;
if ($url) {
$data['url'] = $url;
}
if ($notification->isMobile()) {
$this->addMobileData($data, $notification->getMobileSettings());
if ($button) {
$data['buttons'][] = ['id' => $buttonId, 'text' => $button];
}
} else {
if ($button && $url) {
$data['web_buttons'][] = ['id' => $buttonId, 'text' => $button, 'url' => $url];
}
}
return $this->send('/notifications', $data);
}
protected function addMobileData(array &$data, array $mobileConfig)
{
foreach ($mobileConfig as $key => $value) {
switch ($key) {
case 'ios_subtitle':
$data['subtitle'] = ['en' => $value];
break;
case 'ios_sound':
$data['ios_sound'] = $value ?: 'default';
break;
case 'ios_badges':
$data['ios_badgeType'] = $value;
break;
case 'ios_badgeCount':
$data['ios_badgeCount'] = (int) $value;
break;
case 'ios_contentAvailable':
$data['content_available'] = (bool) $value;
break;
case 'ios_media':
$data['ios_attachments'] = [uniqid('id_') => $value];
break;
case 'ios_mutableContent':
$data['mutable_content'] = (bool) $value;
break;
case 'android_sound':
$data['android_sound'] = $value;
break;
case 'android_small_icon':
$data['small_icon'] = $value;
break;
case 'android_large_icon':
$data['large_icon'] = $value;
break;
case 'android_big_picture':
$data['big_picture'] = $value;
break;
case 'android_led_color':
$data['android_led_color'] = 'FF'.strtoupper($value);
break;
case 'android_accent_color':
$data['android_accent_color'] = 'FF'.strtoupper($value);
break;
case 'android_group_key':
$data['android_group'] = $value;
break;
case 'android_lockscreen_visibility':
$data['android_visibility'] = (int) $value;
break;
case 'additional_data':
// Transforms values received from SortableListType into values acceptable by OneSignal.
if (count($value['list']) > 0) {
$result = [];
foreach ($value['list'] as $item) {
$result[$item['label']] = $item['value'];
}
$data['data'] = $result;
}
break;
}
}
}
}

View File

@@ -0,0 +1,101 @@
.notification-preview__before {
top: -275.039px;
left: calc(50% - 50vw);
position: absolute;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: -1;
background: #B27247 url('../img/sand-dunes-day-GeReAnOMiZ8.jpg') no-repeat center center / cover;
}
:root[theme="dark"] .notification-preview__before {
background: #151F33 url('../img/sand-dunes-night-0O6ZE9oX68k.jpg') no-repeat center center / cover;
}
.notification-preview {
will-change: opacity, transform;
perspective: 800px;
}
.notification-preview__body {
position: relative;
}
.notification-preview__body:nth-child(1) {
z-index: 3;
}
.notification-preview__body:nth-child(1) .notification-preview__notification {
background-color: rgba(0, 0, 0, 0.5);
}
.notification-preview__body:nth-child(1) .notification-preview__notification .notification-preview__before {
filter: saturate(160%);
}
.notification-preview__body:nth-child(2) {
margin-top: -100px;
transform: translateZ(calc(-16px*4));
z-index: 2;
}
.notification-preview__body:nth-child(2) .notification-preview__notification {
background-color: rgba(0, 0, 0, 0.4);
}
.notification-preview__body:nth-child(3) {
margin-top: -100px;
transform: translateZ(calc(-16px*8));
z-index: 1;
}
.notification-preview__body:nth-child(3) .notification-preview__notification {
background-color: rgba(0, 0, 0, 0.3);
}
.notification-preview__container {
clip-path: inset(0 0 0 0 round 16px 16px 16px 16px);
}
.notification-preview__notification {
position: relative;
overflow: hidden;
min-height: 116px;
padding: 16px;
border-radius: 16px;
background-color: rgba(0, 0, 0, 0.9);
color: white;
}
.notification-preview__header,
.notification-preview__more {
opacity: 0.75;
}
.notification-preview__header {
display: flex;
justify-content: space-between;
padding-bottom: 8px;
font-size: 12px;
}
.notification-preview__timestamp {
text-transform: lowercase;
}
.notification-preview__content span {
display: block;
line-height: 1.4;
}
.notification-preview__message {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.notification-preview__more {
margin-top: 4px;
font-size: 12px;
}

View File

@@ -0,0 +1,359 @@
body {
margin: 0;
padding: 0;
background-color: #f6f6f6;
width: 100%;
height: 100%;
}
body,
html {
height: 100%;
overflow: hidden;
}
.truncatable {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.truncatable.desktop.long {
width: 255px;
}
.truncatable.desktop.short, .truncatable.mobile {
width: 240px;
}
.truncatable.opt-out {
width: 300px;
margin: 12px auto;
}
.title {
margin: 0 auto;
text-align: center;
font-family: "Helvetica Neue", "Arial";
font-size: 24px;
font-weight: 500;
width: 90%;
word-wrap: break-word;
word-break: break-all;
overflow-wrap: break-word;
}
#black-wrapper {
width: 100%;
height: 100%;
opacity: 0;
background-color: black;
position: absolute;
top: 0;
z-index: -1;
transition: opacity .5s;
}
#white-wrapper {
width: 100%;
height: 100%;
opacity: 0;
background-color: #f6f6f6;
position: absolute;
top: 0;
z-index: -1;
}
p {
font-family: "Helvetica Neue", "Arial";
}
/* Desktop Notification */
#desktop-notification {
width: 360px;
height: 85px;
margin: 0 auto;
padding: 0;
display: block;;
border: 1px solid #ddd;
background-color: white;
-webkit-box-shadow: 0px 9px 20px -15px rgba(0, 0, 0, 0.75);
-moz-box-shadow: 0px 9px 20px -15px rgba(0, 0, 0, 0.75);
box-shadow: 0px 9px 20px -15px rgba(0, 0, 0, 0.75);
}
#desktop-notification-icon {
margin: 0;
height: 85px;
width: 85px;
float: left;
padding: 0;
}
#desktop-notification-title {
font-size: 15px;
margin-top: 10px;
margin-bottom: 0px;
position: relative;
left: 10px;
}
#desktop-notification-message {
margin-top: 10px;
margin-bottom: 0px;
font-size: 13px;
position: relative;
left: 10px;
}
#desktop-notification-url {
color: #AAA;
font-size: 13px;
margin-top: 8px;
margin-bottom: 0px;
position: relative;
left: 10px;
}
#x {
float: right;
margin-right: 8px;
margin-top: 5px;
color: #999;
}
#mobile {
width: 100%;
height: 100%;
}
#mobile-top-section {
overflow: hidden;
position: absolute;
top: 0;
bottom: 106px;
width: 100%;
}
#mobile-top-section-wrapper {
display: table;
width: 100%;
height: 100%;
}
#mobile-top-section-content {
display: table-cell;
vertical-align: middle;
width: 100%;
}
#mobile-directions {
font-size: 1.4em;
line-height: 1.6em;
padding: 0px 1em;
text-align: center;
color: #333;
font-weight: 200;
width: 310px;
margin: 0px auto;
margin-bottom: 20px;
}
/* Mobile Notification */
#mobile-notification {
width: 90%;
max-width: 350px;
height: 65px;
margin: 0 auto;
padding: 0;
display: block;
border: 1px solid #ccc;
background-color: white;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
-webkit-box-shadow: 0px 9px 20px -15px rgba(0, 0, 0, 0.75);
-moz-box-shadow: 0px 9px 20px -15px rgba(0, 0, 0, 0.75);
box-shadow: 0px 9px 20px -15px rgba(0, 0, 0, 0.75);
}
#mobile-notification-icon {
margin: 0;
height: 50px;
margin-left: 9px;
margin-top: 7px;
width: 50px;
padding: 15px;
float: left;
padding: 0;
}
#mobile-notification-title {
font-size: 18px;
position: relative;
left: 10px;
margin-top: 3px;
margin-bottom: 0;
}
#mobile-notification-message {
position: relative;
left: 10px;
margin-top: 4px;
color: #777;
font-size: 13px;
margin-bottom: 0px;
}
#mobile-notification-url {
color: #AAA;
font-size: 12px;
margin-top: 3px;
margin-bottom: 0px;
position: relative;
left: 10px;
}
#mobile-opt-out {
font-style: italic;
color: #777;
font-size: 12px;
text-align: center;
}
hr {
width: 85%;
background-color: #ccc;
border: none;
height: 1px;
margin-top: 0;
}
#mobile-footer {
width: 100%;
position: fixed;
bottom: 0;
left: 0;
display: block;
}
#mobile-button-container {
width: 100%;
display: table;
border-spacing: 10px;
}
.mobile-button-wrapper {
width: 50%;
display: table-cell;
text-align: center;
}
.mobile-button {
outline: none;
border: none;
padding: 15px 5px;
font-size: 14px;
text-align: center;
max-width: 150px;
width: 100%;
cursor: pointer;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
#show-prompt-button {
background-color: #4285f4;
color: white;
border: 1px solid #4278d2;
}
#show-prompt-button:hover {
background-color: #5392fc;
}
#show-prompt-button:active {
background-color: #3a7be7;
}
#close-button {
color: #555;
border: 1px solid #ccc;
background-color: #f6f6f6;
}
#close-button:hover {
background-color: #fafafa;
}
#close-button:active {
background-color: #eaeaea;
}
#mobile-footer-message {
text-align: center;
color: #aaa;
position: relative;
bottom: 5px;
font-size: 11px;
margin-bottom: 5px;
margin-top: 10px;
}
a:not(.default-link) {
color: inherit;
text-decoration: none;
}
#intercom-container {
display: none;
}
#error-box {
display: none;
width: 80%;
left: 10%;
top: 60%;
z-index: 20;
position: absolute;
background-color: white;
border: 1px solid #aaa;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
-webkit-box-shadow: 0px 9px 20px -15px rgba(0, 0, 0, 0.85);
-moz-box-shadow: 0px 9px 20px -15px rgba(0, 0, 0, 0.85);
box-shadow: 0px 9px 20px -15px rgba(0, 0, 0, 0.85);
}
#error-message-padding {
padding-top: 0px;
padding-right: 15px;
padding-bottom: 0px;
padding-left: 15px;
}
.error {
display: none;
}
.btn-close {
position: absolute;
right: 10px;
top: 0px;
z-index: 20;
font-size: 25px;
text-decoration: none;
font-family: 'Helvetica', arial;
color: #888;
}
.btn-close:hover {
color: #000;
}
#notification-preview {
padding:0 10px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -0,0 +1,83 @@
/** NotificationBundle **/
Mautic.notificationOnLoad = function (container, response) {
if (mQuery(container + ' #list-search').length) {
Mautic.activateSearchAutocomplete('list-search', 'notification');
}
Mautic.activatePreviewPanelUpdate();
};
Mautic.selectNotificationType = function(notificationType) {
if (notificationType == 'list') {
mQuery('#leadList').removeClass('hide');
mQuery('#publishStatus').addClass('hide');
mQuery('.page-header h3').text(mauticLang.newListNotification);
} else {
mQuery('#publishStatus').removeClass('hide');
mQuery('#leadList').addClass('hide');
mQuery('.page-header h3').text(mauticLang.newTemplateNotification);
}
mQuery('#notification_notificationType').val(notificationType);
mQuery('body').removeClass('noscroll');
mQuery('.notification-type-modal').remove();
mQuery('.notification-type-modal-backdrop').remove();
};
Mautic.standardNotificationUrl = function(options) {
if (!options) {
return;
}
var url = options.windowUrl;
if (url) {
var editEmailKey = '/notifications/edit/notificationId';
var previewEmailKey = '/notifications/preview/notificationId';
if (url.indexOf(editEmailKey) > -1 ||
url.indexOf(previewEmailKey) > -1) {
options.windowUrl = url.replace('notificationId', mQuery('#campaignevent_properties_notification').val());
}
}
return options;
};
Mautic.disabledNotificationAction = function(opener) {
if (typeof opener == 'undefined') {
opener = window;
}
var notification = opener.mQuery('#campaignevent_properties_notification').val();
var disabled = notification === '' || notification === null;
opener.mQuery('#campaignevent_properties_editNotificationButton').prop('disabled', disabled);
};
Mautic.activatePreviewPanelUpdate = function () {
var notificationPreview = mQuery('#notification-preview');
var notificationForm = mQuery('form[name="notification"]');
if (notificationPreview.length && notificationForm.length) {
var inputs = notificationForm.find('input,textarea');
inputs.on('blur', function () {
var $this = mQuery(this);
var name = $this.attr('name');
if (name === 'notification[heading]') {
notificationPreview.find('h4').text($this.val());
}
if (name === 'notification[message]') {
notificationPreview.find('p').text($this.val());
}
if (name === 'notification[url]') {
notificationPreview.find('span').not('.ri-notification-3-fill').text($this.val());
}
});
}
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,189 @@
<?php
return [
'services' => [
'events' => [
'mautic.notification.campaignbundle.subscriber' => [
'class' => Mautic\NotificationBundle\EventListener\CampaignSubscriber::class,
'arguments' => [
'mautic.helper.integration',
'mautic.notification.model.notification',
'mautic.notification.api',
'event_dispatcher',
'mautic.lead.model.dnc',
'translator',
],
],
],
'helpers' => [
'mautic.helper.notification' => [
'class' => Mautic\NotificationBundle\Helper\NotificationHelper::class,
'alias' => 'notification_helper',
'arguments' => [
'doctrine.orm.entity_manager',
'twig.helper.assets',
'mautic.helper.core_parameters',
'mautic.helper.integration',
'router',
'request_stack',
'mautic.lead.model.dnc',
],
],
],
'other' => [
'mautic.notification.api' => [
'class' => Mautic\NotificationBundle\Api\OneSignalApi::class,
'arguments' => [
'mautic.http.client',
'mautic.page.model.trackable',
'mautic.helper.integration',
],
'alias' => 'notification_api',
],
],
'integrations' => [
'mautic.integration.onesignal' => [
'class' => Mautic\NotificationBundle\Integration\OneSignalIntegration::class,
'arguments' => [
'event_dispatcher',
'mautic.helper.cache_storage',
'doctrine.orm.entity_manager',
'request_stack',
'router',
'translator',
'monolog.logger.mautic',
'mautic.helper.encryption',
'mautic.lead.model.lead',
'mautic.lead.model.company',
'mautic.helper.paths',
'mautic.core.model.notification',
'mautic.lead.model.field',
'mautic.plugin.model.integration_entity',
'mautic.lead.model.dnc',
'mautic.lead.field.fields_with_unique_identifier',
],
],
],
],
'routes' => [
'main' => [
'mautic_notification_index' => [
'path' => '/notifications/{page}',
'controller' => 'Mautic\NotificationBundle\Controller\NotificationController::indexAction',
],
'mautic_notification_action' => [
'path' => '/notifications/{objectAction}/{objectId}',
'controller' => 'Mautic\NotificationBundle\Controller\NotificationController::executeAction',
],
'mautic_notification_contacts' => [
'path' => '/notifications/view/{objectId}/contact/{page}',
'controller' => 'Mautic\NotificationBundle\Controller\NotificationController::contactsAction',
],
'mautic_mobile_notification_index' => [
'path' => '/mobile_notifications/{page}',
'controller' => 'Mautic\NotificationBundle\Controller\MobileNotificationController::indexAction',
],
'mautic_mobile_notification_action' => [
'path' => '/mobile_notifications/{objectAction}/{objectId}',
'controller' => 'Mautic\NotificationBundle\Controller\MobileNotificationController::executeAction',
],
'mautic_mobile_notification_contacts' => [
'path' => '/mobile_notifications/view/{objectId}/contact/{page}',
'controller' => 'Mautic\NotificationBundle\Controller\MobileNotificationController::contactsAction',
],
],
'public' => [
'mautic_receive_notification' => [
'path' => '/notification/receive',
'controller' => 'Mautic\NotificationBundle\Controller\Api\NotificationApiController::receiveAction',
],
'mautic_subscribe_notification' => [
'path' => '/notification/subscribe',
'controller' => 'Mautic\NotificationBundle\Controller\Api\NotificationApiController::subscribeAction',
],
'mautic_notification_popup' => [
'path' => '/notification',
'controller' => 'Mautic\NotificationBundle\Controller\PopupController::indexAction',
],
// JS / Manifest URL's
'mautic_onesignal_worker' => [
'path' => '/OneSignalSDKWorker.js',
'controller' => 'Mautic\NotificationBundle\Controller\JsController::workerAction',
],
'mautic_onesignal_updater' => [
'path' => '/OneSignalSDKUpdaterWorker.js',
'controller' => 'Mautic\NotificationBundle\Controller\JsController::updaterAction',
],
'mautic_onesignal_manifest' => [
'path' => '/manifest.json',
'controller' => 'Mautic\NotificationBundle\Controller\JsController::manifestAction',
],
'mautic_app_notification' => [
'path' => '/notification/appcallback',
'controller' => 'Mautic\NotificationBundle\Controller\AppCallbackController::indexAction',
],
],
'api' => [
'mautic_api_notificationsstandard' => [
'standard_entity' => true,
'name' => 'notifications',
'path' => '/notifications',
'controller' => Mautic\NotificationBundle\Controller\Api\NotificationApiController::class,
],
],
],
'menu' => [
'main' => [
'items' => [
'mautic.notification.notifications' => [
'route' => 'mautic_notification_index',
'access' => ['notification:notifications:viewown', 'notification:notifications:viewother'],
'checks' => [
'integration' => [
'OneSignal' => [
'enabled' => true,
],
],
],
'parent' => 'mautic.core.channels',
'priority' => 80,
],
'mautic.notification.mobile_notifications' => [
'route' => 'mautic_mobile_notification_index',
'access' => ['notification:mobile_notifications:viewown', 'notification:mobile_notifications:viewother'],
'checks' => [
'integration' => [
'OneSignal' => [
'enabled' => true,
'features' => [
'mobile',
],
],
],
],
'parent' => 'mautic.core.channels',
'priority' => 65,
],
],
],
],
// 'categories' => [
// 'notification' => null
// ],
'parameters' => [
'notification_enabled' => false,
'notification_landing_page_enabled' => true,
'notification_tracking_page_enabled' => false,
'notification_app_id' => null,
'notification_rest_api_key' => null,
'notification_safari_web_id' => null,
'gcm_sender_id' => '482941778795',
'notification_subdomain_name' => null,
'welcomenotification_enabled' => true,
'campaign_send_notification_to_author' => true,
'campaign_notification_email_addresses' => null,
'webhook_send_notification_to_author' => true,
'webhook_notification_email_addresses' => null,
],
];

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return function (ContainerConfigurator $configurator): void {
$services = $configurator->services()
->defaults()
->autowire()
->autoconfigure()
->public();
$excludes = [
];
$services->load('Mautic\\NotificationBundle\\', '../')
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
$services->load('Mautic\\NotificationBundle\\Entity\\', '../Entity/*Repository.php')
->tag(Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass::REPOSITORY_SERVICE_TAG);
$services->alias('mautic.notification.model.notification', Mautic\NotificationBundle\Model\NotificationModel::class);
$services->alias('mautic.notification.repository.stat', Mautic\NotificationBundle\Entity\StatRepository::class);
};

View File

@@ -0,0 +1,11 @@
<?php
namespace Mautic\NotificationBundle\Controller;
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
use Mautic\CoreBundle\Controller\AjaxLookupControllerTrait;
class AjaxController extends CommonAjaxController
{
use AjaxLookupControllerTrait;
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Mautic\NotificationBundle\Controller\Api;
use Doctrine\Persistence\ManagerRegistry;
use Mautic\ApiBundle\Controller\CommonApiController;
use Mautic\ApiBundle\Helper\EntityResultHelper;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Helper\AppVersion;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Tracker\ContactTracker;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\NotificationBundle\Model\NotificationModel;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\RouterInterface;
/**
* @extends CommonApiController<Notification>
*/
class NotificationApiController extends CommonApiController
{
public function __construct(
CorePermissions $security,
Translator $translator,
EntityResultHelper $entityResultHelper,
RouterInterface $router,
FormFactoryInterface $formFactory,
AppVersion $appVersion,
protected ContactTracker $contactTracker,
RequestStack $requestStack,
ManagerRegistry $doctrine,
ModelFactory $modelFactory,
EventDispatcherInterface $dispatcher,
CoreParametersHelper $coreParametersHelper,
) {
$notificationModel = $modelFactory->getModel('notification');
\assert($notificationModel instanceof NotificationModel);
$this->model = $notificationModel;
$this->entityClass = Notification::class;
$this->entityNameOne = 'notification';
$this->entityNameMulti = 'notifications';
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
}
/**
* Receive Web Push subscription request.
*/
public function subscribeAction(Request $request): JsonResponse
{
$osid = $request->get('osid');
if ($osid) {
/** @var \Mautic\LeadBundle\Model\LeadModel $leadModel */
$leadModel = $this->getModel('lead');
if ($currentLead = $this->contactTracker->getContact()) {
$currentLead->addPushIDEntry($osid);
$leadModel->saveEntity($currentLead);
}
return new JsonResponse(['success' => true, 'osid' => $osid], 200, ['Access-Control-Allow-Origin' => '*']);
}
return new JsonResponse(['success' => 'false'], 200, ['Access-Control-Allow-Origin' => '*']);
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Mautic\NotificationBundle\Controller;
use Doctrine\ORM\EntityManagerInterface;
use Mautic\CoreBundle\Controller\CommonController;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\NotificationBundle\Model\NotificationModel;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
class AppCallbackController extends CommonController
{
public function indexAction(Request $request, EntityManagerInterface $em): JsonResponse
{
$requestBody = json_decode($request->getContent(), true);
$contactRepo = $em->getRepository(Lead::class);
$matchData = [
'email' => $requestBody['email'],
];
/** @var Lead $contact */
$contact = $contactRepo->findOneBy($matchData);
if (null === $contact) {
$contact = new Lead();
$contact->setEmail($requestBody['email']);
$contact->setLastActive(new \DateTime());
}
$pushIdCreated = false;
if (array_key_exists('push_id', $requestBody) && !empty(trim($requestBody['push_id']))) {
$pushIdCreated = true;
$contact->addPushIDEntry($requestBody['push_id'], $requestBody['enabled'], true);
$contactRepo->saveEntity($contact);
}
$statCreated = false;
if (array_key_exists('stat', $requestBody)) {
$stat = $requestBody['stat'];
$notificationRepo = $em->getRepository(Notification::class);
$notification = $notificationRepo->getEntity($stat['notification_id']);
if (null !== $notification) {
$statCreated = true;
$notificationModel = $this->getModel('notification');
\assert($notificationModel instanceof NotificationModel);
$notificationModel->createStatEntry($notification, $contact, $stat['source'], $stat['source_id']);
}
}
return new JsonResponse([
'contact_id' => $contact->getId(),
'stat_recorded' => $statCreated,
'push_id_recorded' => $pushIdCreated ?: 'existing',
]);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Mautic\NotificationBundle\Controller;
use Mautic\CoreBundle\Controller\CommonController;
use Symfony\Component\HttpFoundation\Response;
class JsController extends CommonController
{
/**
* We can't user JsonResponse here, because
* it improperly encodes the data array.
*/
public function manifestAction(): Response
{
$gcmSenderId = $this->coreParametersHelper->get('gcm_sender_id', '446150739532');
$data = [
'start_url' => '/',
'gcm_sender_id' => $gcmSenderId,
'gcm_user_visible_only' => true,
];
return new Response(
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
200,
[
'Content-Type' => 'application/json',
]
);
}
public function workerAction(): Response
{
return new Response(
"importScripts('https://cdn.onesignal.com/sdks/OneSignalSDK.js');",
200,
[
'Service-Worker-Allowed' => '/',
'Content-Type' => 'application/javascript',
]
);
}
public function updaterAction(): Response
{
return new Response(
"importScripts('https://cdn.onesignal.com/sdks/OneSignalSDK.js');",
200,
[
'Service-Worker-Allowed' => '/',
'Content-Type' => 'application/javascript',
]
);
}
}

View File

@@ -0,0 +1,769 @@
<?php
namespace Mautic\NotificationBundle\Controller;
use Mautic\CoreBundle\Controller\FormController;
use Mautic\CoreBundle\Factory\PageHelperFactoryInterface;
use Mautic\CoreBundle\Form\Type\DateRangeType;
use Mautic\CoreBundle\Helper\InputHelper;
use Mautic\CoreBundle\Model\AuditLogModel;
use Mautic\LeadBundle\Controller\EntityContactsTrait;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\NotificationBundle\Model\NotificationModel;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class MobileNotificationController extends FormController
{
use EntityContactsTrait;
/**
* @param int $page
*
* @return JsonResponse|Response
*/
public function indexAction(Request $request, $page = 1)
{
/** @var NotificationModel $model */
$model = $this->getModel('notification');
// set some permissions
$permissions = $this->security->isGranted(
[
'notification:mobile_notifications:viewown',
'notification:mobile_notifications:viewother',
'notification:mobile_notifications:create',
'notification:mobile_notifications:editown',
'notification:mobile_notifications:editother',
'notification:mobile_notifications:deleteown',
'notification:mobile_notifications:deleteother',
'notification:mobile_notifications:publishown',
'notification:mobile_notifications:publishother',
],
'RETURN_ARRAY'
);
if (!$permissions['notification:mobile_notifications:viewown'] && !$permissions['notification:mobile_notifications:viewother']) {
return $this->accessDenied();
}
$session = $request->getSession();
// set limits
$limit = $session->get('mautic.mobile_notification.limit', $this->coreParametersHelper->get('default_pagelimit'));
$start = (1 === $page) ? 0 : (($page - 1) * $limit);
if ($start < 0) {
$start = 0;
}
$search = $request->get('search', $session->get('mautic.mobile_notification.filter', ''));
$session->set('mautic.mobile_notification.filter', $search);
$filter = [
'string' => $search,
'where' => [
[
'expr' => 'eq',
'col' => 'mobile',
'val' => 1,
],
[
'expr' => 'isNull',
'col' => 'translationParent',
],
],
];
if (!$permissions['notification:mobile_notifications:viewother']) {
$filter['force'][] =
['column' => 'e.createdBy', 'expr' => 'eq', 'value' => $this->user->getId()];
}
$orderBy = $session->get('mautic.mobile_notification.orderby', 'e.name');
$orderByDir = $session->get('mautic.mobile_notification.orderbydir', 'DESC');
$notifications = $model->getEntities(
[
'start' => $start,
'limit' => $limit,
'filter' => $filter,
'orderBy' => $orderBy,
'orderByDir' => $orderByDir,
]
);
$count = count($notifications);
if ($count && $count < ($start + 1)) {
// the number of entities are now less then the current page so redirect to the last page
if (1 === $count) {
$lastPage = 1;
} else {
$lastPage = (floor($count / $limit)) ?: 1;
}
$session->set('mautic.mobile_notification.page', $lastPage);
$returnUrl = $this->generateUrl('mautic_mobile_notification_index', ['page' => $lastPage]);
return $this->postActionRedirect(
[
'returnUrl' => $returnUrl,
'viewParameters' => ['page' => $lastPage],
'contentTemplate' => 'Mautic\NotificationBundle\Controller\MobileNotificationController::indexAction',
'passthroughVars' => [
'activeLink' => '#mautic_mobile_notification_index',
'mauticContent' => 'mobile_notification',
],
]
);
}
$session->set('mautic.mobile_notification.page', $page);
return $this->delegateView(
[
'viewParameters' => [
'searchValue' => $search,
'items' => $notifications,
'totalItems' => $count,
'page' => $page,
'limit' => $limit,
'tmpl' => $request->get('tmpl', 'index'),
'permissions' => $permissions,
'model' => $model,
'security' => $this->security,
],
'contentTemplate' => '@MauticNotification/MobileNotification/list.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_mobile_notification_index',
'mauticContent' => 'mobile_notification',
'route' => $this->generateUrl('mautic_mobile_notification_index', ['page' => $page]),
],
]
);
}
/**
* Loads a specific form into the detailed panel.
*
* @return JsonResponse|Response
*/
public function viewAction(Request $request, $objectId)
{
/** @var NotificationModel $model */
$model = $this->getModel('notification');
$security = $this->security;
/** @var Notification $notification */
$notification = $model->getEntity($objectId);
// set the page we came from
$page = $request->getSession()->get('mautic.mobile_notification.page', 1);
if (null === $notification) {
// set the return URL
$returnUrl = $this->generateUrl('mautic_mobile_notification_index', ['page' => $page]);
return $this->postActionRedirect(
[
'returnUrl' => $returnUrl,
'viewParameters' => ['page' => $page],
'contentTemplate' => 'Mautic\NotificationBundle\Controller\MobileNotificationController::indexAction',
'passthroughVars' => [
'activeLink' => '#mautic_mobile_notification_index',
'mauticContent' => 'mobile_notification',
],
'flashes' => [
[
'type' => 'error',
'msg' => 'mautic.notification.error.notfound',
'msgVars' => ['%id%' => $objectId],
],
],
]
);
} elseif (!$this->security->hasEntityAccess(
'notification:mobile_notifications:viewown',
'notification:mobile_notifications:viewother',
$notification->getCreatedBy()
)
) {
return $this->accessDenied();
}
// Audit Log
$auditLogModel = $this->getModel('core.auditlog');
\assert($auditLogModel instanceof AuditLogModel);
$logs = $auditLogModel->getLogForObject('notification', $notification->getId(), $notification->getDateAdded());
// Init the date range filter form
$dateRangeValues = $request->query->all()['daterange'] ?? $request->request->all()['daterange'] ?? [];
$action = $this->generateUrl('mautic_mobile_notification_action', ['objectAction' => 'view', 'objectId' => $objectId]);
$dateRangeForm = $this->formFactory->create(DateRangeType::class, $dateRangeValues, ['action' => $action]);
$entityViews = $model->getHitsLineChartData(
null,
new \DateTime($dateRangeForm->get('date_from')->getData()),
new \DateTime($dateRangeForm->get('date_to')->getData()),
null,
['notification_id' => $notification->getId()]
);
// Get click through stats
$trackableLinks = $model->getNotificationClickStats($notification->getId());
[$translationParent, $translationChildren] = $notification->getTranslations();
return $this->delegateView([
'returnUrl' => $this->generateUrl('mautic_mobile_notification_action', ['objectAction' => 'view', 'objectId' => $notification->getId()]),
'viewParameters' => [
'notification' => $notification,
'trackables' => $trackableLinks,
'logs' => $logs,
'permissions' => $security->isGranted([
'notification:mobile_notifications:viewown',
'notification:mobile_notifications:viewother',
'notification:mobile_notifications:create',
'notification:mobile_notifications:editown',
'notification:mobile_notifications:editother',
'notification:mobile_notifications:deleteown',
'notification:mobile_notifications:deleteother',
'notification:mobile_notifications:publishown',
'notification:mobile_notifications:publishother',
], 'RETURN_ARRAY'),
'security' => $security,
'entityViews' => $entityViews,
'contacts' => $this->forward(
'Mautic\NotificationBundle\Controller\MobileNotificationController::contactsAction',
[
'objectId' => $notification->getId(),
'page' => $request->getSession()->get('mautic.mobile_notification.contact.page', 1),
'ignoreAjax' => true,
]
)->getContent(),
'dateRangeForm' => $dateRangeForm->createView(),
'translations' => [
'parent' => $translationParent,
'children' => $translationChildren,
],
],
'contentTemplate' => '@MauticNotification/MobileNotification/details.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_mobile_notification_index',
'mauticContent' => 'mobile_notification',
],
]);
}
/**
* Generates new form and processes post data.
*
* @param Notification $entity
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|Response
*/
public function newAction(Request $request, IntegrationHelper $integrationHelper, $entity = null)
{
/** @var NotificationModel $model */
$model = $this->getModel('notification');
if (!$entity instanceof Notification) {
/** @var Notification $entity */
$entity = $model->getEntity();
}
$method = $request->getMethod();
$session = $request->getSession();
if (!$this->security->isGranted('notification:mobile_notifications:create')) {
return $this->accessDenied();
}
// set the page we came from
$page = $session->get('mautic.mobile_notification.page', 1);
$action = $this->generateUrl('mautic_mobile_notification_action', ['objectAction' => 'new']);
$notification = $request->request->all()['notification'] ?? [];
$updateSelect = 'POST' === $method
? ($notification['updateSelect'] ?? false)
: $request->get('updateSelect', false);
if ($updateSelect) {
$entity->setNotificationType('template');
}
// create the form
$form = $model->createForm($entity, $this->formFactory, $action, ['update_select' => $updateSelect]);
// /Check for a submitted form and process it
if ('POST' === $method) {
$valid = false;
if (!$cancelled = $this->isFormCancelled($form)) {
if ($valid = $this->isFormValid($form)) {
// form is valid so process the data
$model->saveEntity($entity);
$this->addFlashMessage(
'mautic.core.notice.created',
[
'%name%' => $entity->getName(),
'%menu_link%' => 'mautic_mobile_notification_index',
'%url%' => $this->generateUrl(
'mautic_mobile_notification_action',
[
'objectAction' => 'edit',
'objectId' => $entity->getId(),
]
),
]
);
if ($this->getFormButton($form, ['buttons', 'save'])->isClicked()) {
$viewParameters = [
'objectAction' => 'view',
'objectId' => $entity->getId(),
];
$returnUrl = $this->generateUrl('mautic_mobile_notification_action', $viewParameters);
$template = 'Mautic\NotificationBundle\Controller\MobileNotificationController::viewAction';
} else {
// return edit view so that all the session stuff is loaded
return $this->editAction($request, $integrationHelper, $entity->getId(), true);
}
}
} else {
$viewParameters = ['page' => $page];
$returnUrl = $this->generateUrl('mautic_mobile_notification_index', $viewParameters);
$template = 'Mautic\NotificationBundle\Controller\MobileNotificationController::indexAction';
// clear any modified content
$session->remove('mautic.mobile_notification.'.$entity->getId().'.content');
}
$passthrough = [
'activeLink' => 'mautic_mobile_notification_index',
'mauticContent' => 'mobile_notification',
];
// Check to see if this is a popup
if (isset($form['updateSelect'])) {
$template = false;
$passthrough = array_merge(
$passthrough,
[
'updateSelect' => $form['updateSelect']->getData(),
'id' => $entity->getId(),
'name' => $entity->getName(),
'group' => $entity->getLanguage(),
]
);
}
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
return $this->postActionRedirect(
[
'returnUrl' => $returnUrl,
'viewParameters' => $viewParameters,
'contentTemplate' => $template,
'passthroughVars' => $passthrough,
]
);
}
}
$integration = $integrationHelper->getIntegrationObject('OneSignal');
return $this->delegateView(
[
'viewParameters' => [
'form' => $form->createView(),
'notification' => $entity,
'integration' => $integration,
],
'contentTemplate' => '@MauticNotification/MobileNotification/form.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_mobile_notification_index',
'mauticContent' => 'mobile_notification',
'updateSelect' => InputHelper::clean($request->query->get('updateSelect')),
'route' => $this->generateUrl(
'mautic_mobile_notification_action',
[
'objectAction' => 'new',
]
),
],
]
);
}
/**
* @param bool $ignorePost
* @param bool $forceTypeSelection
*
* @return array|JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
*/
public function editAction(Request $request, IntegrationHelper $integrationHelper, $objectId, $ignorePost = false, $forceTypeSelection = false)
{
/** @var NotificationModel $model */
$model = $this->getModel('notification');
$method = $request->getMethod();
$entity = $model->getEntity($objectId);
$session = $request->getSession();
$page = $session->get('mautic.mobile_notification.page', 1);
// set the return URL
$returnUrl = $this->generateUrl('mautic_mobile_notification_index', ['page' => $page]);
$postActionVars = [
'returnUrl' => $returnUrl,
'viewParameters' => ['page' => $page],
'contentTemplate' => 'Mautic\NotificationBundle\Controller\MobileNotificationController::indexAction',
'passthroughVars' => [
'activeLink' => 'mautic_mobile_notification_index',
'mauticContent' => 'mobile_notification',
],
];
// not found
if (null === $entity) {
return $this->postActionRedirect(
array_merge(
$postActionVars,
[
'flashes' => [
[
'type' => 'error',
'msg' => 'mautic.notification.error.notfound',
'msgVars' => ['%id%' => $objectId],
],
],
]
)
);
} elseif (!$this->security->hasEntityAccess(
'notification:mobile_notifications:viewown',
'notification:mobile_notifications:viewother',
$entity->getCreatedBy()
)
) {
return $this->accessDenied();
} elseif ($model->isLocked($entity)) {
// deny access if the entity is locked
return $this->isLocked($postActionVars, $entity, 'notification');
}
// Create the form
$action = $this->generateUrl('mautic_mobile_notification_action', ['objectAction' => 'edit', 'objectId' => $objectId]);
$notification = $request->request->all()['notification'] ?? [];
$updateSelect = 'POST' === $method
? ($notification['updateSelect'] ?? false)
: $request->get('updateSelect', false);
$form = $model->createForm($entity, $this->formFactory, $action, ['update_select' => $updateSelect]);
// /Check for a submitted form and process it
if (!$ignorePost && 'POST' == $method) {
$valid = false;
if (!$cancelled = $this->isFormCancelled($form)) {
if ($valid = $this->isFormValid($form)) {
// form is valid so process the data
$model->saveEntity($entity, $this->getFormButton($form, ['buttons', 'save'])->isClicked());
$this->addFlashMessage(
'mautic.core.notice.updated',
[
'%name%' => $entity->getName(),
'%menu_link%' => 'mautic_mobile_notification_index',
'%url%' => $this->generateUrl(
'mautic_mobile_notification_action',
[
'objectAction' => 'edit',
'objectId' => $entity->getId(),
]
),
],
'warning'
);
}
} else {
// clear any modified content
$session->remove('mautic.mobile_notification.'.$objectId.'.content');
// unlock the entity
$model->unlockEntity($entity);
}
$template = 'Mautic\NotificationBundle\Controller\MobileNotificationController::viewAction';
$passthrough = [
'activeLink' => 'mautic_mobile_notification_index',
'mauticContent' => 'mobile_notification',
];
// Check to see if this is a popup
if (isset($form['updateSelect'])) {
$template = false;
$passthrough = array_merge(
$passthrough,
[
'updateSelect' => $form['updateSelect']->getData(),
'id' => $entity->getId(),
'name' => $entity->getName(),
'group' => $entity->getLanguage(),
]
);
}
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
$viewParameters = [
'objectAction' => 'view',
'objectId' => $entity->getId(),
];
return $this->postActionRedirect(
array_merge(
$postActionVars,
[
'returnUrl' => $this->generateUrl('mautic_mobile_notification_action', $viewParameters),
'viewParameters' => $viewParameters,
'contentTemplate' => $template,
'passthroughVars' => $passthrough,
]
)
);
}
} else {
// lock the entity
$model->lockEntity($entity);
}
$integration = $integrationHelper->getIntegrationObject('OneSignal');
return $this->delegateView(
[
'viewParameters' => [
'form' => $form->createView(),
'notification' => $entity,
'forceTypeSelection' => $forceTypeSelection,
'integration' => $integration,
],
'contentTemplate' => '@MauticNotification/MobileNotification/form.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_mobile_notification_index',
'mauticContent' => 'mobile_notification',
'updateSelect' => InputHelper::clean($request->query->get('updateSelect')),
'route' => $this->generateUrl(
'mautic_mobile_notification_action',
[
'objectAction' => 'edit',
'objectId' => $entity->getId(),
]
),
],
]
);
}
/**
* Clone an entity.
*
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
*/
public function cloneAction(Request $request, IntegrationHelper $integrationHelper, $objectId)
{
$model = $this->getModel('notification');
$entity = $model->getEntity($objectId);
if (null != $entity) {
if (!$this->security->isGranted('notification:mobile_notifications:create')
|| !$this->security->hasEntityAccess(
'notification:mobile_notifications:viewown',
'notification:mobile_notifications:viewother',
$entity->getCreatedBy()
)
) {
return $this->accessDenied();
}
$entity = clone $entity;
$session = $request->getSession();
$contentName = 'mautic.mobile_notification.'.$entity->getId().'.content';
$session->set($contentName, $entity->getContent());
}
return $this->newAction($request, $integrationHelper, $entity);
}
/**
* Deletes the entity.
*
* @return Response
*/
public function deleteAction(Request $request, $objectId)
{
$page = $request->getSession()->get('mautic.mobile_notification.page', 1);
$returnUrl = $this->generateUrl('mautic_mobile_notification_index', ['page' => $page]);
$flashes = [];
$postActionVars = [
'returnUrl' => $returnUrl,
'viewParameters' => ['page' => $page],
'contentTemplate' => 'Mautic\NotificationBundle\Controller\MobileNotificationController::indexAction',
'passthroughVars' => [
'activeLink' => 'mautic_mobile_notification_index',
'mauticContent' => 'mobile_notification',
],
];
if (Request::METHOD_POST === $request->getMethod()) {
$model = $this->getModel('notification');
\assert($model instanceof NotificationModel);
$entity = $model->getEntity($objectId);
if (null === $entity) {
$flashes[] = [
'type' => 'error',
'msg' => 'mautic.notification.error.notfound',
'msgVars' => ['%id%' => $objectId],
];
} elseif (!$this->security->hasEntityAccess(
'notification:mobile_notifications:deleteown',
'notification:mobile_notifications:deleteother',
$entity->getCreatedBy()
)
) {
return $this->accessDenied();
} elseif ($model->isLocked($entity)) {
return $this->isLocked($postActionVars, $entity, 'notification');
}
$model->deleteEntity($entity);
$flashes[] = [
'type' => 'notice',
'msg' => 'mautic.core.notice.deleted',
'msgVars' => [
'%name%' => $entity->getName(),
'%id%' => $objectId,
],
];
} // else don't do anything
return $this->postActionRedirect(
array_merge(
$postActionVars,
[
'flashes' => $flashes,
]
)
);
}
/**
* Deletes a group of entities.
*/
public function batchDeleteAction(Request $request): Response
{
$page = $request->getSession()->get('mautic.mobile_notification.page', 1);
$returnUrl = $this->generateUrl('mautic_mobile_notification_index', ['page' => $page]);
$flashes = [];
$postActionVars = [
'returnUrl' => $returnUrl,
'viewParameters' => ['page' => $page],
'contentTemplate' => 'Mautic\NotificationBundle\Controller\MobileNotificationController::indexAction',
'passthroughVars' => [
'activeLink' => '#mautic_mobile_notification_index',
'mauticContent' => 'mobile_notification',
],
];
if (Request::METHOD_POST === $request->getMethod()) {
$model = $this->getModel('notification');
\assert($model instanceof NotificationModel);
$ids = json_decode($request->query->get('ids', '{}'));
$deleteIds = [];
// Loop over the IDs to perform access checks pre-delete
foreach ($ids as $objectId) {
$entity = $model->getEntity($objectId);
if (null === $entity) {
$flashes[] = [
'type' => 'error',
'msg' => 'mautic.notification.error.notfound',
'msgVars' => ['%id%' => $objectId],
];
} elseif (!$this->security->hasEntityAccess(
'notification:mobile_notifications:viewown',
'notification:mobile_notifications:viewother',
$entity->getCreatedBy()
)
) {
$flashes[] = $this->accessDenied(true);
} elseif ($model->isLocked($entity)) {
$flashes[] = $this->isLocked($postActionVars, $entity, 'notification', true);
} else {
$deleteIds[] = $objectId;
}
}
// Delete everything we are able to
if (!empty($deleteIds)) {
$entities = $model->deleteEntities($deleteIds);
$flashes[] = [
'type' => 'notice',
'msg' => 'mautic.notification.notice.batch_deleted',
'msgVars' => [
'%count%' => count($entities),
],
];
}
} // else don't do anything
return $this->postActionRedirect(
array_merge(
$postActionVars,
[
'flashes' => $flashes,
]
)
);
}
public function previewAction($objectId): Response
{
/** @var NotificationModel $model */
$model = $this->getModel('notification');
$notification = $model->getEntity($objectId);
return $this->delegateView(
[
'viewParameters' => [
'notification' => $notification,
],
'contentTemplate' => '@MauticNotification/MobileNotification/preview.html.twig',
]
);
}
/**
* @param int $page
*
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
*/
public function contactsAction(
Request $request,
PageHelperFactoryInterface $pageHelperFactory,
$objectId,
$page = 1,
) {
return $this->generateContactsGrid(
$request,
$pageHelperFactory,
$objectId,
$page,
'notification:mobile_notifications:view',
'mobile_notification',
'push_notification_stats',
'mobile_notification',
'notification_id'
);
}
}

View File

@@ -0,0 +1,757 @@
<?php
namespace Mautic\NotificationBundle\Controller;
use Mautic\CoreBundle\Controller\AbstractFormController;
use Mautic\CoreBundle\Factory\PageHelperFactoryInterface;
use Mautic\CoreBundle\Form\Type\DateRangeType;
use Mautic\CoreBundle\Helper\InputHelper;
use Mautic\CoreBundle\Model\AuditLogModel;
use Mautic\LeadBundle\Controller\EntityContactsTrait;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\NotificationBundle\Model\NotificationModel;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class NotificationController extends AbstractFormController
{
use EntityContactsTrait;
/**
* @param int $page
*
* @return JsonResponse|Response
*/
public function indexAction(Request $request, $page = 1)
{
/** @var NotificationModel $model */
$model = $this->getModel('notification');
// set some permissions
$permissions = $this->security->isGranted(
[
'notification:notifications:viewown',
'notification:notifications:viewother',
'notification:notifications:create',
'notification:notifications:editown',
'notification:notifications:editother',
'notification:notifications:deleteown',
'notification:notifications:deleteother',
'notification:notifications:publishown',
'notification:notifications:publishother',
],
'RETURN_ARRAY'
);
if (!$permissions['notification:notifications:viewown'] && !$permissions['notification:notifications:viewother']) {
return $this->accessDenied();
}
if ('POST' == $request->getMethod()) {
$this->setListFilters();
}
$session = $request->getSession();
$limit = $session->get('mautic.notification.limit', $this->coreParametersHelper->get('default_pagelimit'));
$start = (1 === $page) ? 0 : (($page - 1) * $limit);
if ($start < 0) {
$start = 0;
}
$search = $request->get('search', $session->get('mautic.notification.filter', ''));
$session->set('mautic.notification.filter', $search);
$filter = [
'string' => $search,
'where' => [
[
'expr' => 'eq',
'col' => 'mobile',
'val' => 0,
],
],
];
if (!$permissions['notification:notifications:viewother']) {
$filter['force'][] =
['column' => 'e.createdBy', 'expr' => 'eq', 'value' => $this->user->getId()];
}
$orderBy = $session->get('mautic.notification.orderby', 'e.name');
$orderByDir = $session->get('mautic.notification.orderbydir', 'DESC');
$notifications = $model->getEntities(
[
'start' => $start,
'limit' => $limit,
'filter' => $filter,
'orderBy' => $orderBy,
'orderByDir' => $orderByDir,
]
);
$count = count($notifications);
if ($count && $count < ($start + 1)) {
// the number of entities are now less then the current page so redirect to the last page
if (1 === $count) {
$lastPage = 1;
} else {
$lastPage = (floor($count / $limit)) ?: 1;
}
$session->set('mautic.notification.page', $lastPage);
$returnUrl = $this->generateUrl('mautic_notification_index', ['page' => $lastPage]);
return $this->postActionRedirect(
[
'returnUrl' => $returnUrl,
'viewParameters' => ['page' => $lastPage],
'contentTemplate' => 'Mautic\NotificationBundle\Controller\NotificationController::indexAction',
'passthroughVars' => [
'activeLink' => '#mautic_notification_index',
'mauticContent' => 'notification',
],
]
);
}
$session->set('mautic.notification.page', $page);
return $this->delegateView(
[
'viewParameters' => [
'searchValue' => $search,
'items' => $notifications,
'totalItems' => $count,
'page' => $page,
'limit' => $limit,
'tmpl' => $request->get('tmpl', 'index'),
'permissions' => $permissions,
'model' => $model,
'security' => $this->security,
],
'contentTemplate' => '@MauticNotification/Notification/list.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_notification_index',
'mauticContent' => 'notification',
'route' => $this->generateUrl('mautic_notification_index', ['page' => $page]),
],
]
);
}
/**
* Loads a specific form into the detailed panel.
*
* @return JsonResponse|Response
*/
public function viewAction(Request $request, FormFactoryInterface $formFactory, $objectId)
{
/** @var NotificationModel $model */
$model = $this->getModel('notification');
$security = $this->security;
/** @var Notification $notification */
$notification = $model->getEntity($objectId);
// set the page we came from
$page = $request->getSession()->get('mautic.notification.page', 1);
if (null === $notification) {
// set the return URL
$returnUrl = $this->generateUrl('mautic_notification_index', ['page' => $page]);
return $this->postActionRedirect(
[
'returnUrl' => $returnUrl,
'viewParameters' => ['page' => $page],
'contentTemplate' => 'Mautic\NotificationBundle\Controller\NotificationController::indexAction',
'passthroughVars' => [
'activeLink' => '#mautic_notification_index',
'mauticContent' => 'notification',
],
'flashes' => [
[
'type' => 'error',
'msg' => 'mautic.notification.error.notfound',
'msgVars' => ['%id%' => $objectId],
],
],
]
);
} elseif (!$this->security->hasEntityAccess(
'notification:notifications:viewown',
'notification:notifications:viewother',
$notification->getCreatedBy()
)
) {
return $this->accessDenied();
}
// Audit Log
$auditLog = $this->getModel('core.auditlog');
\assert($auditLog instanceof AuditLogModel);
$logs = $auditLog->getLogForObject('notification', $notification->getId(), $notification->getDateAdded());
// Init the date range filter form
$dateRangeValues = $request->query->all()['daterange'] ?? $request->request->all()['daterange'] ?? [];
$action = $this->generateUrl('mautic_notification_action', ['objectAction' => 'view', 'objectId' => $objectId]);
$dateRangeForm = $formFactory->create(DateRangeType::class, $dateRangeValues, ['action' => $action]);
$entityViews = $model->getHitsLineChartData(
null,
new \DateTime($dateRangeForm->get('date_from')->getData()),
new \DateTime($dateRangeForm->get('date_to')->getData()),
null,
['notification_id' => $notification->getId()]
);
// Get click through stats
$trackableLinks = $model->getNotificationClickStats($notification->getId());
return $this->delegateView([
'returnUrl' => $this->generateUrl('mautic_notification_action', ['objectAction' => 'view', 'objectId' => $notification->getId()]),
'viewParameters' => [
'notification' => $notification,
'trackables' => $trackableLinks,
'logs' => $logs,
'permissions' => $security->isGranted([
'notification:notifications:viewown',
'notification:notifications:viewother',
'notification:notifications:create',
'notification:notifications:editown',
'notification:notifications:editother',
'notification:notifications:deleteown',
'notification:notifications:deleteother',
'notification:notifications:publishown',
'notification:notifications:publishother',
], 'RETURN_ARRAY'),
'security' => $security,
'entityViews' => $entityViews,
'contacts' => $this->forward(
'Mautic\NotificationBundle\Controller\NotificationController::contactsAction',
[
'objectId' => $notification->getId(),
'page' => $request->getSession()->get('mautic.notification.contact.page', 1),
'ignoreAjax' => true,
]
)->getContent(),
'dateRangeForm' => $dateRangeForm->createView(),
],
'contentTemplate' => '@MauticNotification/Notification/details.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_notification_index',
'mauticContent' => 'notification',
],
]);
}
/**
* Generates new form and processes post data.
*
* @param Notification $entity
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|Response
*/
public function newAction(Request $request, FormFactoryInterface $formFactory, $entity = null)
{
/** @var NotificationModel $model */
$model = $this->getModel('notification');
if (!$entity instanceof Notification) {
/** @var Notification $entity */
$entity = $model->getEntity();
}
$method = $request->getMethod();
$session = $request->getSession();
if (!$this->security->isGranted('notification:notifications:create')) {
return $this->accessDenied();
}
// set the page we came from
$page = $session->get('mautic.notification.page', 1);
$action = $this->generateUrl('mautic_notification_action', ['objectAction' => 'new']);
$notification = $request->request->all()['notification'] ?? [];
$updateSelect = ('POST' == $method)
? ($notification['updateSelect'] ?? false)
: $request->get('updateSelect', false);
if ($updateSelect) {
$entity->setNotificationType('template');
}
// create the form
$form = $model->createForm($entity, $formFactory, $action, ['update_select' => $updateSelect]);
// /Check for a submitted form and process it
if ('POST' === $method) {
$valid = false;
if (!$cancelled = $this->isFormCancelled($form)) {
if ($valid = $this->isFormValid($form)) {
// form is valid so process the data
$model->saveEntity($entity);
$this->addFlashMessage(
'mautic.core.notice.created',
[
'%name%' => $entity->getName(),
'%menu_link%' => 'mautic_notification_index',
'%url%' => $this->generateUrl(
'mautic_notification_action',
[
'objectAction' => 'edit',
'objectId' => $entity->getId(),
]
),
]
);
if ($this->getFormButton($form, ['buttons', 'save'])->isClicked()) {
$viewParameters = [
'objectAction' => 'view',
'objectId' => $entity->getId(),
];
$returnUrl = $this->generateUrl('mautic_notification_action', $viewParameters);
$template = 'Mautic\NotificationBundle\Controller\NotificationController::viewAction';
} else {
// return edit view so that all the session stuff is loaded
return $this->editAction($request, $formFactory, $entity->getId(), true);
}
}
} else {
$viewParameters = ['page' => $page];
$returnUrl = $this->generateUrl('mautic_notification_index', $viewParameters);
$template = 'Mautic\NotificationBundle\Controller\NotificationController::indexAction';
// clear any modified content
$session->remove('mautic.notification.'.$entity->getId().'.content');
}
$passthrough = [
'activeLink' => 'mautic_notification_index',
'mauticContent' => 'notification',
];
// Check to see if this is a popup
if (isset($form['updateSelect'])) {
$template = false;
$passthrough = array_merge(
$passthrough,
[
'updateSelect' => $form['updateSelect']->getData(),
'id' => $entity->getId(),
'name' => $entity->getName(),
'group' => $entity->getLanguage(),
]
);
}
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
return $this->postActionRedirect(
[
'returnUrl' => $returnUrl,
'viewParameters' => $viewParameters,
'contentTemplate' => $template,
'passthroughVars' => $passthrough,
]
);
}
}
return $this->delegateView(
[
'viewParameters' => [
'form' => $form->createView(),
'notification' => $entity,
],
'contentTemplate' => '@MauticNotification/Notification/form.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_notification_index',
'mauticContent' => 'notification',
'updateSelect' => InputHelper::clean($request->query->get('updateSelect')),
'route' => $this->generateUrl(
'mautic_notification_action',
[
'objectAction' => 'new',
]
),
],
]
);
}
/**
* @param bool $ignorePost
* @param bool $forceTypeSelection
*
* @return array|JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
*/
public function editAction(Request $request, FormFactoryInterface $formFactory, $objectId, $ignorePost = false, $forceTypeSelection = false)
{
/** @var NotificationModel $model */
$model = $this->getModel('notification');
$method = $request->getMethod();
$entity = $model->getEntity($objectId);
$session = $request->getSession();
$page = $session->get('mautic.notification.page', 1);
// set the return URL
$returnUrl = $this->generateUrl('mautic_notification_index', ['page' => $page]);
$postActionVars = [
'returnUrl' => $returnUrl,
'viewParameters' => ['page' => $page],
'contentTemplate' => 'Mautic\NotificationBundle\Controller\NotificationController::indexAction',
'passthroughVars' => [
'activeLink' => 'mautic_notification_index',
'mauticContent' => 'notification',
],
];
// not found
if (null === $entity) {
return $this->postActionRedirect(
array_merge(
$postActionVars,
[
'flashes' => [
[
'type' => 'error',
'msg' => 'mautic.notification.error.notfound',
'msgVars' => ['%id%' => $objectId],
],
],
]
)
);
} elseif (!$this->security->hasEntityAccess(
'notification:notifications:viewown',
'notification:notifications:viewother',
$entity->getCreatedBy()
)
) {
return $this->accessDenied();
} elseif ($model->isLocked($entity)) {
// deny access if the entity is locked
return $this->isLocked($postActionVars, $entity, 'notification');
}
// Create the form
$action = $this->generateUrl('mautic_notification_action', ['objectAction' => 'edit', 'objectId' => $objectId]);
$notification = $request->request->all()['notification'] ?? [];
$updateSelect = 'POST' === $method
? ($notification['updateSelect'] ?? false)
: $request->get('updateSelect', false);
$form = $model->createForm($entity, $formFactory, $action, ['update_select' => $updateSelect]);
// /Check for a submitted form and process it
if (!$ignorePost && 'POST' === $method) {
$valid = false;
if (!$cancelled = $this->isFormCancelled($form)) {
if ($valid = $this->isFormValid($form)) {
// form is valid so process the data
$model->saveEntity($entity, $this->getFormButton($form, ['buttons', 'save'])->isClicked());
$this->addFlashMessage(
'mautic.core.notice.updated',
[
'%name%' => $entity->getName(),
'%menu_link%' => 'mautic_notification_index',
'%url%' => $this->generateUrl(
'mautic_notification_action',
[
'objectAction' => 'edit',
'objectId' => $entity->getId(),
]
),
],
'warning'
);
}
} else {
// clear any modified content
$session->remove('mautic.notification.'.$objectId.'.content');
// unlock the entity
$model->unlockEntity($entity);
}
$template = 'Mautic\NotificationBundle\Controller\NotificationController::viewAction';
$passthrough = [
'activeLink' => 'mautic_notification_index',
'mauticContent' => 'notification',
];
// Check to see if this is a popup
if (isset($form['updateSelect'])) {
$template = false;
$passthrough = array_merge(
$passthrough,
[
'updateSelect' => $form['updateSelect']->getData(),
'id' => $entity->getId(),
'name' => $entity->getName(),
'group' => $entity->getLanguage(),
]
);
}
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
$viewParameters = [
'objectAction' => 'view',
'objectId' => $entity->getId(),
];
return $this->postActionRedirect(
array_merge(
$postActionVars,
[
'returnUrl' => $this->generateUrl('mautic_notification_action', $viewParameters),
'viewParameters' => $viewParameters,
'contentTemplate' => $template,
'passthroughVars' => $passthrough,
]
)
);
}
} else {
// lock the entity
$model->lockEntity($entity);
}
return $this->delegateView(
[
'viewParameters' => [
'form' => $form->createView(),
'notification' => $entity,
'forceTypeSelection' => $forceTypeSelection,
],
'contentTemplate' => '@MauticNotification/Notification/form.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_notification_index',
'mauticContent' => 'notification',
'updateSelect' => InputHelper::clean($request->query->get('updateSelect')),
'route' => $this->generateUrl(
'mautic_notification_action',
[
'objectAction' => 'edit',
'objectId' => $entity->getId(),
]
),
],
]
);
}
/**
* Clone an entity.
*
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
*/
public function cloneAction(Request $request, FormFactoryInterface $formFactory, $objectId)
{
$model = $this->getModel('notification');
$entity = $model->getEntity($objectId);
if (null != $entity) {
if (!$this->security->isGranted('notification:notifications:create')
|| !$this->security->hasEntityAccess(
'notification:notifications:viewown',
'notification:notifications:viewother',
$entity->getCreatedBy()
)
) {
return $this->accessDenied();
}
$entity = clone $entity;
$session = $request->getSession();
$contentName = 'mautic.notification.'.$entity->getId().'.content';
$session->set($contentName, $entity->getContent());
}
return $this->newAction($request, $formFactory, $entity);
}
/**
* Deletes the entity.
*
* @return Response
*/
public function deleteAction(Request $request, $objectId)
{
$page = $request->getSession()->get('mautic.notification.page', 1);
$returnUrl = $this->generateUrl('mautic_notification_index', ['page' => $page]);
$flashes = [];
$postActionVars = [
'returnUrl' => $returnUrl,
'viewParameters' => ['page' => $page],
'contentTemplate' => 'Mautic\NotificationBundle\Controller\NotificationController::indexAction',
'passthroughVars' => [
'activeLink' => 'mautic_notification_index',
'mauticContent' => 'notification',
],
];
if (Request::METHOD_POST === $request->getMethod()) {
$model = $this->getModel('notification');
\assert($model instanceof NotificationModel);
$entity = $model->getEntity($objectId);
if (null === $entity) {
$flashes[] = [
'type' => 'error',
'msg' => 'mautic.notification.error.notfound',
'msgVars' => ['%id%' => $objectId],
];
} elseif (!$this->security->hasEntityAccess(
'notification:notifications:deleteown',
'notification:notifications:deleteother',
$entity->getCreatedBy()
)
) {
return $this->accessDenied();
} elseif ($model->isLocked($entity)) {
return $this->isLocked($postActionVars, $entity, 'notification');
}
$model->deleteEntity($entity);
$flashes[] = [
'type' => 'notice',
'msg' => 'mautic.core.notice.deleted',
'msgVars' => [
'%name%' => $entity->getName(),
'%id%' => $objectId,
],
];
} // else don't do anything
return $this->postActionRedirect(
array_merge(
$postActionVars,
[
'flashes' => $flashes,
]
)
);
}
/**
* Deletes a group of entities.
*/
public function batchDeleteAction(Request $request): Response
{
$page = $request->getSession()->get('mautic.notification.page', 1);
$returnUrl = $this->generateUrl('mautic_notification_index', ['page' => $page]);
$flashes = [];
$postActionVars = [
'returnUrl' => $returnUrl,
'viewParameters' => ['page' => $page],
'contentTemplate' => 'Mautic\NotificationBundle\Controller\NotificationController::indexAction',
'passthroughVars' => [
'activeLink' => '#mautic_notification_index',
'mauticContent' => 'notification',
],
];
if (Request::METHOD_POST === $request->getMethod()) {
$model = $this->getModel('notification');
\assert($model instanceof NotificationModel);
$ids = json_decode($request->query->get('ids', '{}'));
$deleteIds = [];
// Loop over the IDs to perform access checks pre-delete
foreach ($ids as $objectId) {
$entity = $model->getEntity($objectId);
if (null === $entity) {
$flashes[] = [
'type' => 'error',
'msg' => 'mautic.notification.error.notfound',
'msgVars' => ['%id%' => $objectId],
];
} elseif (!$this->security->hasEntityAccess(
'notification:notifications:viewown',
'notification:notifications:viewother',
$entity->getCreatedBy()
)
) {
$flashes[] = $this->accessDenied(true);
} elseif ($model->isLocked($entity)) {
$flashes[] = $this->isLocked($postActionVars, $entity, 'notification', true);
} else {
$deleteIds[] = $objectId;
}
}
// Delete everything we are able to
if (!empty($deleteIds)) {
$entities = $model->deleteEntities($deleteIds);
$flashes[] = [
'type' => 'notice',
'msg' => 'mautic.notification.notice.batch_deleted',
'msgVars' => [
'%count%' => count($entities),
],
];
}
} // else don't do anything
return $this->postActionRedirect(
array_merge(
$postActionVars,
[
'flashes' => $flashes,
]
)
);
}
public function previewAction($objectId): Response
{
/** @var NotificationModel $model */
$model = $this->getModel('notification');
$notification = $model->getEntity($objectId);
return $this->delegateView(
[
'viewParameters' => [
'notification' => $notification,
],
'contentTemplate' => '@MauticNotification/Notification/preview.html.twig',
]
);
}
/**
* @param int $page
*
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
*/
public function contactsAction(
Request $request,
PageHelperFactoryInterface $pageHelperFactory,
$objectId,
$page = 1,
) {
return $this->generateContactsGrid(
$request,
$pageHelperFactory,
$objectId,
$page,
'notification:notifications:view',
'notification',
'push_notification_stats',
'notification',
'notification_id'
);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Mautic\NotificationBundle\Controller;
use Mautic\CoreBundle\Controller\CommonController;
use Mautic\CoreBundle\Twig\Helper\AssetsHelper;
use Mautic\PageBundle\Entity\Page;
use Mautic\PageBundle\Event\PageDisplayEvent;
use Mautic\PageBundle\PageEvents;
use Symfony\Component\HttpFoundation\Response;
class PopupController extends CommonController
{
public function indexAction(AssetsHelper $assetsHelper): Response
{
$assetsHelper->addStylesheet('/app/bundles/NotificationBundle/Assets/css/popup/popup.css');
$response = $this->render(
'@MauticNotification/Popup/index.html.twig',
[
'siteUrl' => $this->coreParametersHelper->get('site_url'),
]
);
$content = $response->getContent();
$event = new PageDisplayEvent($content, new Page());
$this->dispatcher->dispatch($event, PageEvents::PAGE_ON_DISPLAY);
$content = $event->getContent();
return $response->setContent($content);
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
class MauticNotificationExtension extends Extension
{
/**
* @param mixed[] $configs
*/
public function load(array $configs, ContainerBuilder $container): void
{
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Config'));
$loader->load('services.php');
}
}

View File

@@ -0,0 +1,673 @@
<?php
namespace Mautic\NotificationBundle\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\FormEntity;
use Mautic\CoreBundle\Entity\TranslationEntityInterface;
use Mautic\CoreBundle\Entity\TranslationEntityTrait;
use Mautic\CoreBundle\Entity\UuidInterface;
use Mautic\CoreBundle\Entity\UuidTrait;
use Mautic\LeadBundle\Entity\LeadList;
use Mautic\LeadBundle\Form\Validator\Constraints\LeadListAccess;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Mapping\ClassMetadata;
#[ApiResource(
operations: [
new GetCollection(security: "is_granted('notification:notifications:viewown')"),
new Post(security: "is_granted('notification:notifications:create')"),
new Get(security: "is_granted('notification:notifications:viewown')"),
new Put(security: "is_granted('notification:notifications:editown')"),
new Patch(security: "is_granted('notification:notifications:editother')"),
new Delete(security: "is_granted('notification:notifications:deleteown')"),
],
normalizationContext: [
'groups' => ['notification:read'],
'swagger_definition_name' => 'Read',
'api_included' => ['category'],
],
denormalizationContext: [
'groups' => ['notification:write'],
'swagger_definition_name' => 'Write',
]
)]
class Notification extends FormEntity implements UuidInterface, TranslationEntityInterface
{
use UuidTrait;
use TranslationEntityTrait;
/**
* @var int
*/
#[Groups(['notification:read'])]
private $id;
/**
* @var string
*/
#[Groups(['notification:read', 'notification:write'])]
private $name;
/**
* @var string|null
*/
#[Groups(['notification:read', 'notification:write'])]
private $description;
/**
* @var string|null
*/
#[Groups(['notification:read', 'notification:write'])]
private $url;
/**
* @var string
*/
#[Groups(['notification:read', 'notification:write'])]
private $heading;
/**
* @var string
*/
#[Groups(['notification:read', 'notification:write'])]
private $message;
/**
* @var string|null
*/
#[Groups(['notification:read', 'notification:write'])]
private $button;
/**
* @var array
*/
#[Groups(['notification:read', 'notification:write'])]
private $utmTags = [];
/**
* @var \DateTimeInterface
*/
#[Groups(['notification:read', 'notification:write'])]
private $publishUp;
/**
* @var \DateTimeInterface
*/
#[Groups(['notification:read', 'notification:write'])]
private $publishDown;
/**
* @var int
*/
#[Groups(['notification:read'])]
private $readCount = 0;
/**
* @var int
*/
#[Groups(['notification:read'])]
private $sentCount = 0;
/**
* @var \Mautic\CategoryBundle\Entity\Category|null
**/
#[Groups(['notification:read', 'notification:write'])]
private $category;
/**
* @var ArrayCollection<int, LeadList>
*/
#[Groups(['notification:read', 'notification:write'])]
private $lists;
/**
* @var ArrayCollection<int, Stat>
*/
private $stats;
/**
* @var string|null
*/
#[Groups(['notification:read', 'notification:write'])]
private $notificationType = 'template';
/**
* @var bool
*/
#[Groups(['notification:read', 'notification:write'])]
private $mobile = false;
/**
* @var ?array
*/
#[Groups(['notification:read', 'notification:write'])]
private $mobileSettings;
public function __clone()
{
$this->id = null;
$this->stats = new ArrayCollection();
$this->sentCount = 0;
$this->readCount = 0;
parent::__clone();
}
public function __construct()
{
$this->lists = new ArrayCollection();
$this->stats = new ArrayCollection();
$this->translationChildren = new ArrayCollection();
}
/**
* Clear stats.
*/
public function clearStats(): void
{
$this->stats = new ArrayCollection();
}
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('push_notifications')
->setCustomRepositoryClass(NotificationRepository::class);
$builder->addIdColumns();
$builder->createField('url', 'text')
->nullable()
->build();
$builder->createField('heading', 'text')
->build();
$builder->createField('message', 'text')
->build();
$builder->createField('button', 'text')
->nullable()
->build();
$builder->createField('utmTags', 'array')
->columnName('utm_tags')
->nullable()
->build();
$builder->createField('notificationType', 'text')
->columnName('notification_type')
->nullable()
->build();
$builder->addPublishDates();
$builder->createField('readCount', 'integer')
->columnName('read_count')
->build();
$builder->createField('sentCount', 'integer')
->columnName('sent_count')
->build();
$builder->addCategory();
$builder->createManyToMany('lists', LeadList::class)
->setJoinTable('push_notification_list_xref')
->setIndexBy('id')
->addInverseJoinColumn('leadlist_id', 'id', false, false, 'CASCADE')
->addJoinColumn('notification_id', 'id', false, false, 'CASCADE')
->fetchExtraLazy()
->build();
$builder->createOneToMany('stats', 'Stat')
->setIndexBy('id')
->mappedBy('notification')
->cascadePersist()
->fetchExtraLazy()
->build();
$builder->createField('mobile', 'boolean')->build();
$builder->createField('mobileSettings', 'array')->build();
static::addUuidField($builder);
self::addTranslationMetadata($builder, self::class);
}
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$metadata->addPropertyConstraint(
'name',
new NotBlank(
[
'message' => 'mautic.core.name.required',
]
)
);
$metadata->addPropertyConstraint(
'heading',
new NotBlank(
[
'message' => 'mautic.core.heading.required',
]
)
);
$metadata->addPropertyConstraint(
'message',
new NotBlank(
[
'message' => 'mautic.core.message.required',
]
)
);
$metadata->addConstraint(new Callback(
function (Notification $notification, ExecutionContextInterface $context): void {
$type = $notification->getNotificationType();
if ('list' == $type) {
$validator = $context->getValidator();
$violations = $validator->validate(
$notification->getLists(),
[
new LeadListAccess(
[
'message' => 'mautic.lead.lists.required',
]
),
new NotBlank(
[
'message' => 'mautic.lead.lists.required',
]
),
]
);
if (count($violations) > 0) {
$string = (string) $violations;
$context->buildViolation($string)
->atPath('lists')
->addViolation();
}
}
},
));
}
/**
* Prepares the metadata for API usage.
*/
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
{
$metadata->setGroupPrefix('notification')
->addListProperties(
[
'id',
'name',
'heading',
'message',
'url',
'language',
'category',
'button',
]
)
->addProperties(
[
'utmTags',
'publishUp',
'publishDown',
'readCount',
'sentCount',
]
)
->build();
}
protected function isChanged($prop, $val)
{
$getter = 'get'.ucfirst($prop);
$current = $this->$getter();
if ('category' == $prop || 'list' == $prop) {
$currentId = ($current) ? $current->getId() : '';
$newId = ($val) ? $val->getId() : null;
if ($currentId != $newId) {
$this->changes[$prop] = [$currentId, $newId];
}
} else {
parent::isChanged($prop, $val);
}
}
/**
* @return mixed
*/
public function getName()
{
return $this->name;
}
/**
* @param string $name
*
* @return $this
*/
public function setName($name)
{
$this->isChanged('name', $name);
$this->name = $name;
return $this;
}
/**
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* @param string $description
*/
public function setDescription($description): void
{
$this->isChanged('description', $description);
$this->description = $description;
}
/**
* Get id.
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return mixed
*/
public function getCategory()
{
return $this->category;
}
/**
* @return $this
*/
public function setCategory($category)
{
$this->isChanged('category', $category);
$this->category = $category;
return $this;
}
/**
* @return string
*/
public function getHeading()
{
return $this->heading;
}
/**
* @param string $heading
*/
public function setHeading($heading): void
{
$this->isChanged('heading', $heading);
$this->heading = $heading;
}
/**
* @return string
*/
public function getButton()
{
return $this->button;
}
public function setButton($button): void
{
$this->isChanged('button', $button);
$this->button = $button;
}
/**
* @return string
*/
public function getMessage()
{
return $this->message;
}
/**
* @param string $message
*/
public function setMessage($message): void
{
$this->isChanged('message', $message);
$this->message = $message;
}
/**
* @return array
*/
public function getUtmTags()
{
return $this->utmTags;
}
/**
* @param array $utmTags
*/
public function setUtmTags($utmTags)
{
$this->isChanged('utmTags', $utmTags);
$this->utmTags = $utmTags;
return $this;
}
/**
* @return string
*/
public function getUrl()
{
return $this->url;
}
/**
* @param string $url
*/
public function setUrl($url): void
{
$this->isChanged('url', $url);
$this->url = $url;
}
/**
* @return mixed
*/
public function getReadCount()
{
return $this->readCount;
}
/**
* @return $this
*/
public function setReadCount($readCount)
{
$this->readCount = $readCount;
return $this;
}
/**
* @return mixed
*/
public function getPublishDown()
{
return $this->publishDown;
}
/**
* @return $this
*/
public function setPublishDown($publishDown)
{
$this->isChanged('publishDown', $publishDown);
$this->publishDown = $publishDown;
return $this;
}
/**
* @return mixed
*/
public function getPublishUp()
{
return $this->publishUp;
}
/**
* @return $this
*/
public function setPublishUp($publishUp)
{
$this->isChanged('publishUp', $publishUp);
$this->publishUp = $publishUp;
return $this;
}
public function getSentCount(bool $includeVariants = false): mixed
{
return ($includeVariants) ? $this->getAccumulativeTranslationCount('getSentCount') : $this->sentCount;
}
/**
* @return $this
*/
public function setSentCount($sentCount)
{
$this->sentCount = $sentCount;
return $this;
}
/**
* @return mixed
*/
public function getLists()
{
return $this->lists;
}
/**
* Add list.
*
* @return Notification
*/
public function addList(LeadList $list)
{
$this->lists[] = $list;
return $this;
}
/**
* Remove list.
*/
public function removeList(LeadList $list): void
{
$this->lists->removeElement($list);
}
/**
* @return mixed
*/
public function getStats()
{
return $this->stats;
}
/**
* @return string
*/
public function getNotificationType()
{
return $this->notificationType;
}
/**
* @param string $notificationType
*/
public function setNotificationType($notificationType): void
{
$this->isChanged('notificationType', $notificationType);
$this->notificationType = $notificationType;
}
/**
* @return bool
*/
public function isMobile()
{
return $this->mobile;
}
/**
* @param bool $mobile
*
* @return $this
*/
public function setMobile($mobile)
{
$this->mobile = $mobile;
return $this;
}
/**
* @return array
*/
public function getMobileSettings()
{
return $this->mobileSettings ?? [];
}
/**
* @return $this
*/
public function setMobileSettings(array $mobileSettings)
{
$this->mobileSettings = $mobileSettings;
return $this;
}
}

View File

@@ -0,0 +1,254 @@
<?php
namespace Mautic\NotificationBundle\Entity;
use Doctrine\ORM\Query;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<Notification>
*/
class NotificationRepository extends CommonRepository
{
/**
* Get a list of entities.
*
* @return Paginator
*/
public function getEntities(array $args = [])
{
$q = $this->_em
->createQueryBuilder()
->select('e')
->from(Notification::class, 'e', 'e.id');
if (empty($args['iterable_mode'])) {
$q->leftJoin('e.category', 'c');
}
$args['qb'] = $q;
return parent::getEntities($args);
}
/**
* Get amounts of sent and read notifications.
*
* @return array
*/
public function getSentReadCount()
{
$q = $this->_em->createQueryBuilder();
$q->select('SUM(e.sentCount) as sent_count, SUM(e.readCount) as read_count')
->from(Notification::class, 'e');
$results = $q->getQuery()->getSingleResult(Query::HYDRATE_ARRAY);
if (!isset($results['sent_count'])) {
$results['sent_count'] = 0;
}
if (!isset($results['read_count'])) {
$results['read_count'] = 0;
}
return $results;
}
/**
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
*/
protected function addSearchCommandWhereClause($q, $filter): array
{
[$expr, $parameters] = $this->addStandardSearchCommandWhereClause($q, $filter);
if ($expr) {
return [$expr, $parameters];
}
$command = $filter->command;
$unique = $this->generateRandomParameterName();
$returnParameter = false; // returning a parameter that is not used will lead to a Doctrine error
switch ($command) {
case $this->translator->trans('mautic.core.searchcommand.lang'):
case $this->translator->trans('mautic.core.searchcommand.lang', [], null, 'en_US'):
$langUnique = $this->generateRandomParameterName();
$langValue = $filter->string.'_%';
$forceParameters = [
$langUnique => $langValue,
$unique => $filter->string,
];
$expr = $q->expr()->or(
$q->expr()->eq('e.language', ":$unique"),
$q->expr()->like('e.language', ":$langUnique")
);
$returnParameter = true;
break;
}
if ($expr && $filter->not) {
$expr = $q->expr()->not($expr);
}
if (!empty($forceParameters)) {
$parameters = $forceParameters;
} elseif ($returnParameter) {
$string = ($filter->strict) ? $filter->string : "%{$filter->string}%";
$parameters = ["$unique" => $string];
}
return [$expr, $parameters];
}
/**
* @return string[]
*/
public function getSearchCommands(): array
{
$commands = [
'mautic.core.searchcommand.ispublished',
'mautic.core.searchcommand.isunpublished',
'mautic.core.searchcommand.isuncategorized',
'mautic.core.searchcommand.ismine',
'mautic.core.searchcommand.category',
'mautic.core.searchcommand.lang',
];
return array_merge($commands, parent::getSearchCommands());
}
/**
* @return array<array<string>>
*/
protected function getDefaultOrder(): array
{
return [
['e.name', 'ASC'],
];
}
public function getTableAlias(): string
{
return 'e';
}
/**
* Up the click/sent counts.
*
* @param string $type
* @param int $increaseBy
*/
public function upCount($id, $type = 'sent', $increaseBy = 1): void
{
try {
$q = $this->_em->getConnection()->createQueryBuilder();
$q->update(MAUTIC_TABLE_PREFIX.'push_notifications')
->set($type.'_count', $type.'_count + '.(int) $increaseBy)
->where('id = '.(int) $id);
$q->executeStatement();
} catch (\Exception) {
// not important
}
}
/**
* @param string $search
* @param int $limit
* @param int $start
* @param bool $viewOther
* @param string $notificationType
*
* @return array
*/
public function getNotificationList($search = '', $limit = 10, $start = 0, $viewOther = false, $notificationType = null)
{
$q = $this->createQueryBuilder('e');
$q->select('partial e.{id, name, language}');
if (!empty($search)) {
if (is_array($search)) {
$search = array_map('intval', $search);
$q->andWhere($q->expr()->in('e.id', ':search'))
->setParameter('search', $search);
} else {
$q->andWhere($q->expr()->like('e.name', ':search'))
->setParameter('search', "%{$search}%");
}
}
if (!$viewOther) {
$q->andWhere($q->expr()->eq('e.createdBy', ':id'))
->setParameter('id', $this->currentUser->getId());
}
if (!empty($notificationType)) {
$q->andWhere(
$q->expr()->eq('e.notificationType', $q->expr()->literal($notificationType))
);
}
$q->andWhere('e.mobile != 1');
$q->orderBy('e.name');
if (!empty($limit)) {
$q->setFirstResult($start)
->setMaxResults($limit);
}
return $q->getQuery()->getArrayResult();
}
/**
* @param string|array<mixed> $search
* @param array<mixed> $options
*
* @return array<int, array<string, int|string>>
*/
public function getMobileNotificationList(string|array $search = '', int $limit = 10, int $start = 0, bool $viewOther = false, array $options = []): array
{
$q = $this->createQueryBuilder('e');
$q->select('partial e.{id, name, language}');
if (!empty($search)) {
if (is_array($search)) {
$search = array_map('intval', $search);
$q->andWhere($q->expr()->in('e.id', ':search'))
->setParameter('search', $search);
} else {
$q->andWhere($q->expr()->like('e.name', ':search'))
->setParameter('search', "%{$search}%");
}
}
if (!$viewOther) {
$q->andWhere($q->expr()->eq('e.createdBy', ':id'))
->setParameter('id', $this->currentUser->getId());
}
if (!empty($options['notification_type'])) {
$q->andWhere(
$q->expr()->eq('e.notificationType', $q->expr()->literal($options['notification_type']))
);
}
if (!empty($options['top_level']) && 'translation' === $options['top_level']) {
$q->andWhere($q->expr()->isNull('e.translationParent'));
}
if (!empty($options['ignore_ids'])) {
$q->andWhere($q->expr()->notIn('e.id', $options['ignore_ids']));
}
$q->andWhere('e.mobile = 1');
$q->orderBy('e.name');
if (!empty($limit)) {
$q->setFirstResult($start)
->setMaxResults($limit);
}
return $q->getQuery()->getArrayResult();
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace Mautic\NotificationBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\LeadBundle\Entity\Lead;
class PushID
{
/**
* @var int
*/
private $id;
/**
* @var Lead|null
*/
private $lead;
/**
* @var string
*/
private $pushID;
/**
* @var bool
*/
private $enabled;
/**
* @var bool
*/
private $mobile;
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('push_ids')
->setCustomRepositoryClass(PushIDRepository::class);
$builder->createField('id', 'integer')
->makePrimaryKey()
->generatedValue()
->build();
$builder->createField('pushID', 'string')
->columnName('push_id')
->nullable(false)
->build();
$builder->createManyToOne('lead', Lead::class)
->addJoinColumn('lead_id', 'id', true, false, 'SET NULL')
->inversedBy('pushIds')
->build();
$builder->createField('enabled', 'boolean')->build();
$builder->createField('mobile', 'boolean')->build();
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @param int $id
*
* @return $this
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* @return Lead
*/
public function getLead()
{
return $this->lead;
}
/**
* @return $this
*/
public function setLead(Lead $lead)
{
$this->lead = $lead;
return $this;
}
/**
* @return string
*/
public function getPushID()
{
return $this->pushID;
}
/**
* @param string $pushID
*
* @return $this
*/
public function setPushID($pushID)
{
$this->pushID = $pushID;
return $this;
}
/**
* @return bool
*/
public function isEnabled()
{
return $this->enabled;
}
/**
* @return $this
*/
public function setEnabled($enabled)
{
$this->enabled = $enabled;
return $this;
}
/**
* @return bool
*/
public function isMobile()
{
return $this->mobile;
}
/**
* @param bool $mobile
*
* @return $this
*/
public function setMobile($mobile)
{
$this->mobile = $mobile;
return $this;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Mautic\NotificationBundle\Entity;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<PushID>
*/
class PushIDRepository extends CommonRepository
{
}

View File

@@ -0,0 +1,495 @@
<?php
namespace Mautic\NotificationBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\IpAddress;
use Mautic\LeadBundle\Entity\Lead;
class Stat
{
public const TABLE_NAME = 'push_notification_stats';
/**
* @var string
*/
private $id;
/**
* @var Notification|null
*/
private $notification;
/**
* @var Lead|null
*/
private $lead;
/**
* @var \Mautic\LeadBundle\Entity\LeadList|null
*/
private $list;
/**
* @var IpAddress|null
*/
private $ipAddress;
/**
* @var \DateTimeInterface
*/
private $dateSent;
/**
* @var \DateTimeInterface
*/
private $dateRead;
/**
* @var bool
*/
private $isClicked = false;
/**
* @var \DateTimeInterface
*/
private $dateClicked;
/**
* @var string|null
*/
private $trackingHash;
/**
* @var int|null
*/
private $retryCount = 0;
/**
* @var string|null
*/
private $source;
/**
* @var int|null
*/
private $sourceId;
/**
* @var array
*/
private $tokens = [];
/**
* @var int|null
*/
private $clickCount;
/**
* @var array
*/
private $clickDetails = [];
/**
* @var \DateTimeInterface
*/
private $lastClicked;
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable(self::TABLE_NAME)
->setCustomRepositoryClass(StatRepository::class)
->addIndex(['notification_id', 'lead_id'], 'stat_notification_search')
->addIndex(['is_clicked'], 'stat_notification_clicked_search')
->addIndex(['tracking_hash'], 'stat_notification_hash_search')
->addIndex(['source', 'source_id'], 'stat_notification_source_search');
$builder->addBigIntIdField();
$builder->createManyToOne('notification', 'Notification')
->inversedBy('stats')
->addJoinColumn('notification_id', 'id', true, false, 'SET NULL')
->build();
$builder->addLead(true, 'SET NULL');
$builder->createManyToOne('list', \Mautic\LeadBundle\Entity\LeadList::class)
->addJoinColumn('list_id', 'id', true, false, 'SET NULL')
->build();
$builder->addIpAddress(true);
$builder->createField('dateSent', 'datetime')
->columnName('date_sent')
->build();
$builder->createField('dateRead', 'datetime')
->columnName('date_read')
->nullable()
->build();
$builder->createField('isClicked', 'boolean')
->columnName('is_clicked')
->build();
$builder->createField('dateClicked', 'datetime')
->columnName('date_clicked')
->nullable()
->build();
$builder->createField('trackingHash', 'string')
->columnName('tracking_hash')
->nullable()
->build();
$builder->createField('retryCount', 'integer')
->columnName('retry_count')
->nullable()
->build();
$builder->createField('source', 'string')
->nullable()
->build();
$builder->createField('sourceId', 'integer')
->columnName('source_id')
->nullable()
->build();
$builder->createField('tokens', 'array')
->nullable()
->build();
$builder->addNullableField('clickCount', 'integer', 'click_count');
$builder->addNullableField('lastClicked', 'datetime', 'last_clicked');
$builder->addNullableField('clickDetails', 'array', 'click_details');
}
/**
* Prepares the metadata for API usage.
*/
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
{
$metadata->setGroupPrefix('stat')
->addProperties(
[
'id',
'ipAddress',
'dateSent',
'isClicked',
'dateClicked',
'retryCount',
'source',
'clickCount',
'lastClicked',
'sourceId',
'trackingHash',
'lead',
'notification',
]
)
->build();
}
/**
* @return mixed
*/
public function getDateClicked()
{
return $this->dateClicked;
}
/**
* @param mixed $dateClicked
*/
public function setDateClicked($dateClicked): void
{
$this->dateClicked = $dateClicked;
}
/**
* @return mixed
*/
public function getDateSent()
{
return $this->dateSent;
}
/**
* @param mixed $dateSent
*/
public function setDateSent($dateSent): void
{
$this->dateSent = $dateSent;
}
/**
* @return Notification
*/
public function getNotification()
{
return $this->notification;
}
public function setNotification(?Notification $notification = null): void
{
$this->notification = $notification;
}
public function getId(): int
{
return (int) $this->id;
}
/**
* @return IpAddress|null
*/
public function getIpAddress()
{
return $this->ipAddress;
}
/**
* @param mixed $ip
*/
public function setIpAddress(IpAddress $ip): void
{
$this->ipAddress = $ip;
}
/**
* @return mixed
*/
public function getIsClicked()
{
return $this->isClicked;
}
/**
* @param mixed $isClicked
*/
public function setIsClicked($isClicked): void
{
$this->isClicked = $isClicked;
}
/**
* @return Lead
*/
public function getLead()
{
return $this->lead;
}
/**
* @param mixed $lead
*/
public function setLead(?Lead $lead = null): void
{
$this->lead = $lead;
}
/**
* @return mixed
*/
public function getTrackingHash()
{
return $this->trackingHash;
}
/**
* @param mixed $trackingHash
*/
public function setTrackingHash($trackingHash): void
{
$this->trackingHash = $trackingHash;
}
/**
* @return \Mautic\LeadBundle\Entity\LeadList
*/
public function getList()
{
return $this->list;
}
/**
* @param mixed $list
*/
public function setList($list): void
{
$this->list = $list;
}
/**
* @return mixed
*/
public function getRetryCount()
{
return $this->retryCount;
}
/**
* @param mixed $retryCount
*/
public function setRetryCount($retryCount): void
{
$this->retryCount = $retryCount;
}
public function upRetryCount(): void
{
++$this->retryCount;
}
/**
* @return mixed
*/
public function getSource()
{
return $this->source;
}
/**
* @param mixed $source
*/
public function setSource($source): void
{
$this->source = $source;
}
/**
* @return mixed
*/
public function getSourceId()
{
return $this->sourceId;
}
/**
* @param mixed $sourceId
*/
public function setSourceId($sourceId): void
{
$this->sourceId = (int) $sourceId;
}
/**
* @return mixed
*/
public function getTokens()
{
return $this->tokens;
}
/**
* @param mixed $tokens
*/
public function setTokens($tokens): void
{
$this->tokens = $tokens;
}
/**
* @return mixed
*/
public function getClickCount()
{
return $this->clickCount;
}
/**
* @param mixed $clickCount
*
* @return Stat
*/
public function setClickCount($clickCount)
{
$this->clickCount = $clickCount;
return $this;
}
public function addClickDetails($details): void
{
$this->clickDetails[] = $details;
++$this->clickCount;
}
/**
* Up the sent count.
*
* @return Stat
*/
public function upClickCount()
{
$count = (int) $this->clickCount + 1;
$this->clickCount = $count;
return $this;
}
/**
* @return mixed
*/
public function getLastClicked()
{
return $this->lastClicked;
}
/**
* @return Stat
*/
public function setLastClicked(\DateTime $lastClicked)
{
$this->lastClicked = $lastClicked;
return $this;
}
/**
* @return mixed
*/
public function getClickDetails()
{
return $this->clickDetails;
}
/**
* @param mixed $clickDetails
*
* @return Stat
*/
public function setClickDetails($clickDetails)
{
$this->clickDetails = $clickDetails;
return $this;
}
/**
* @return \DateTimeInterface
*/
public function getDateRead()
{
return $this->dateRead;
}
/**
* @param \DateTime $dateRead
*
* @return Stat
*/
public function setDateRead($dateRead)
{
$this->dateRead = $dateRead;
return $this;
}
}

View File

@@ -0,0 +1,208 @@
<?php
namespace Mautic\NotificationBundle\Entity;
use Doctrine\DBAL\Query\QueryBuilder;
use Mautic\CoreBundle\Entity\CommonRepository;
use Mautic\CoreBundle\Helper\DateTimeHelper;
/**
* @extends CommonRepository<Stat>
*/
class StatRepository extends CommonRepository
{
/**
* @return mixed
*
* @throws \Doctrine\ORM\NoResultException
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function getNotificationStatus($trackingHash)
{
$q = $this->createQueryBuilder('s');
$q->select('s')
->leftJoin('s.lead', 'l')
->leftJoin('s.notification', 'e')
->where(
$q->expr()->eq('s.trackingHash', ':hash')
)
->setParameter('hash', $trackingHash);
$result = $q->getQuery()->getResult();
return (!empty($result)) ? $result[0] : null;
}
/**
* Updates lead ID (e.g. after a lead merge).
*/
public function getSentStats($notificationId, $listId = null): array
{
$q = $this->_em->getConnection()->createQueryBuilder();
$q->select('s.lead_id')
->from(MAUTIC_TABLE_PREFIX.'push_notification_stats', 's')
->where('s.notification_id = :notification')
->setParameter('notification', $notificationId);
if ($listId) {
$q->andWhere('s.list_id = :list')
->setParameter('list', $listId);
}
$result = $q->executeQuery()->fetchAllAssociative();
// index by lead
$stats = [];
foreach ($result as $r) {
$stats[$r['lead_id']] = $r['lead_id'];
}
unset($result);
return $stats;
}
/**
* @param int|array $notificationIds
* @param int $listId
*
* @return int
*/
public function getSentCount($notificationIds = null, $listId = null)
{
$q = $this->_em->getConnection()->createQueryBuilder();
$q->select('count(s.id) as sent_count')
->from(MAUTIC_TABLE_PREFIX.'push_notification_stats', 's');
if ($notificationIds) {
if (!is_array($notificationIds)) {
$notificationIds = [(int) $notificationIds];
}
$q->where(
$q->expr()->in('s.notification_id', $notificationIds)
);
}
if ($listId) {
$q->andWhere('s.list_id = '.(int) $listId);
}
$q->andWhere('s.is_failed = :false')
->setParameter('false', false, 'boolean');
$results = $q->executeQuery()->fetchAllAssociative();
return (isset($results[0])) ? $results[0]['sent_count'] : 0;
}
/**
* @param array|int $notificationIds
* @param int $listId
*
* @return int
*/
public function getReadCount($notificationIds = null, $listId = null)
{
$q = $this->_em->getConnection()->createQueryBuilder();
$q->select('count(s.id) as read_count')
->from(MAUTIC_TABLE_PREFIX.'push_notification_stats', 's');
if ($notificationIds) {
if (!is_array($notificationIds)) {
$notificationIds = [(int) $notificationIds];
}
$q->where(
$q->expr()->in('s.notification_id', $notificationIds)
);
}
if ($listId) {
$q->andWhere('s.list_id = '.(int) $listId);
}
$q->andWhere('is_read = :true')
->setParameter('true', true, 'boolean');
$results = $q->executeQuery()->fetchAllAssociative();
return (isset($results[0])) ? $results[0]['read_count'] : 0;
}
/**
* Get pie graph data for Sent, Read and Failed notifications count.
*
* @param QueryBuilder $query
*
* @throws \Doctrine\ORM\NoResultException
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function getMostNotifications($query, $limit = 10, $offset = 0): array
{
$query
->setMaxResults($limit)
->setFirstResult($offset);
return $query->executeQuery()->fetchAllAssociative();
}
/**
* Get sent counts based grouped by notification Id.
*
* @param array $notificationIds
*/
public function getSentCounts($notificationIds = [], ?\DateTime $fromDate = null): array
{
$q = $this->_em->getConnection()->createQueryBuilder();
$q->select('s.notification_id, count(n.id) as sentcount')
->from(MAUTIC_TABLE_PREFIX.'push_notification_stats', 's')
->where(
$q->expr()->in('s.notification_id', $notificationIds)
);
if (null !== $fromDate) {
// make sure the date is UTC
$dt = new DateTimeHelper($fromDate);
$q->andWhere(
$q->expr()->gte('s.date_read', $q->expr()->literal($dt->toUtcString()))
);
}
$q->groupBy('s.notification_id');
// get a total number of sent notifications first
$results = $q->executeQuery()->fetchAllAssociative();
$counts = [];
foreach ($results as $r) {
$counts[$r['notification_id']] = $r['sentcount'];
}
return $counts;
}
/**
* Updates lead ID (e.g. after a lead merge).
*/
public function updateLead($fromLeadId, $toLeadId): void
{
$q = $this->_em->getConnection()->createQueryBuilder();
$q->update(MAUTIC_TABLE_PREFIX.'push_notification_stats')
->set('notification_id', (int) $toLeadId)
->where('notification_id = '.(int) $fromLeadId)
->executeStatement();
}
/**
* Delete a stat.
*/
public function deleteStat($id): void
{
$this->_em->getConnection()->delete(MAUTIC_TABLE_PREFIX.'push_notification_stats', ['id' => (int) $id]);
}
public function getTableAlias(): string
{
return 's';
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Mautic\NotificationBundle\Event;
use Mautic\CoreBundle\Event\CommonEvent;
use Mautic\NotificationBundle\Entity\Notification;
class NotificationEvent extends CommonEvent
{
/**
* @param bool $isNew
*/
public function __construct(Notification $notification, $isNew = false)
{
$this->entity = $notification;
$this->isNew = $isNew;
}
/**
* Returns the Notification entity.
*
* @return Notification
*/
public function getNotification()
{
return $this->entity;
}
/**
* Sets the Notification entity.
*/
public function setNotification(Notification $notification): void
{
$this->entity = $notification;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Mautic\NotificationBundle\Event;
use Mautic\CoreBundle\Event\CommonEvent;
use Mautic\LeadBundle\Entity\Lead;
class NotificationSendEvent extends CommonEvent
{
/**
* @param string $message
*/
public function __construct(
protected $message,
protected $heading,
protected Lead $lead,
) {
}
/**
* @return string
*/
public function getMessage()
{
return $this->message;
}
/**
* @param string $message
*/
public function setMessage($message): void
{
$this->message = $message;
}
/**
* @return mixed
*/
public function getHeading()
{
return $this->heading;
}
/**
* @param mixed $heading
*
* @return NotificationSendEvent
*/
public function setHeading($heading)
{
$this->heading = $heading;
return $this;
}
/**
* @return Lead
*/
public function getLead()
{
return $this->lead;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Mautic\NotificationBundle\EventListener;
use Mautic\CoreBundle\CoreEvents;
use Mautic\CoreBundle\Event\BuildJsEvent;
use Mautic\NotificationBundle\Helper\NotificationHelper;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
class BuildJsSubscriber implements EventSubscriberInterface
{
public function __construct(
private NotificationHelper $notificationHelper,
private IntegrationHelper $integrationHelper,
private RouterInterface $router,
) {
}
public static function getSubscribedEvents(): array
{
return [
CoreEvents::BUILD_MAUTIC_JS => ['onBuildJs', 254],
];
}
public function onBuildJs(BuildJsEvent $event): void
{
$integration = $this->integrationHelper->getIntegrationObject('OneSignal');
if (!$integration || false === $integration->getIntegrationSettings()->getIsPublished()) {
return;
}
$subscribeUrl = $this->router->generate('mautic_notification_popup', [], UrlGeneratorInterface::ABSOLUTE_URL);
$subscribeTitle = 'Subscribe To Notifications';
$width = 450;
$height = 450;
$js = <<<JS
{$this->notificationHelper->getHeaderScript()}
MauticJS.notification = {
init: function () {
{$this->notificationHelper->getScript()}
var subscribeButton = document.getElementById('mautic-notification-subscribe');
if (subscribeButton) {
subscribeButton.addEventListener('click', MauticJS.notification.popup);
}
},
popup: function () {
var subscribeUrl = '{$subscribeUrl}';
var subscribeTitle = '{$subscribeTitle}';
var w = {$width};
var h = {$height};
// Fixes dual-screen position Most browsers Firefox
var dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : screen.left;
var dualScreenTop = window.screenTop != undefined ? window.screenTop : screen.top;
var width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
var height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;
var left = ((width / 2) - (w / 2)) + dualScreenLeft;
var top = ((height / 2) - (h / 2)) + dualScreenTop;
var subscribeWindow = window.open(
subscribeUrl,
subscribeTitle,
'scrollbars=yes, width=' + w + ',height=' + h + ',top=' + top + ',left=' + left + ',directories=0,titlebar=0,toolbar=0,location=0,status=0,menubar=0,scrollbars=no,resizable=no'
);
if (window.focus) {
subscribeWindow.focus();
}
window.closeSubscribeWindow = function() { subscribeWindow.close(); };
}
};
MauticJS.documentReady(MauticJS.notification.init);
JS;
$event->appendJs($js, 'Mautic Notification JS');
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Mautic\NotificationBundle\EventListener;
use Mautic\CampaignBundle\CampaignEvents;
use Mautic\CampaignBundle\Event\CampaignBuilderEvent;
use Mautic\CampaignBundle\Event\CampaignExecutionEvent;
use Mautic\NotificationBundle\Entity\PushID;
use Mautic\NotificationBundle\NotificationEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class CampaignConditionSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
CampaignEvents::CAMPAIGN_ON_BUILD => ['onCampaignBuild', 0],
NotificationEvents::ON_CAMPAIGN_TRIGGER_CONDITION => ['onCampaignTriggerHasActiveCondition', 0],
];
}
public function onCampaignBuild(CampaignBuilderEvent $event): void
{
$event->addCondition(
'notification.has.active',
[
'label' => 'mautic.notification.campaign.event.notification.has.active',
'description' => 'mautic.notification.campaign.event.notification.has.active.desc',
'eventName' => NotificationEvents::ON_CAMPAIGN_TRIGGER_CONDITION,
]
);
}
public function onCampaignTriggerHasActiveCondition(CampaignExecutionEvent $event)
{
if (!$event->checkContext('notification.has.active')) {
return;
}
$pushIds = $event->getLead()->getPushIDs();
/** @var PushID $pushID */
foreach ($pushIds as $pushID) {
if ($pushID->isEnabled()) {
return $event->setResult(true);
}
}
return $event->setResult(false);
}
}

View File

@@ -0,0 +1,317 @@
<?php
namespace Mautic\NotificationBundle\EventListener;
use Mautic\CampaignBundle\CampaignEvents;
use Mautic\CampaignBundle\Entity\LeadEventLog;
use Mautic\CampaignBundle\Event\CampaignBuilderEvent;
use Mautic\CampaignBundle\Event\PendingEvent;
use Mautic\CoreBundle\Event\TokenReplacementEvent;
use Mautic\LeadBundle\Entity\DoNotContact;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Model\DoNotContact as DoNotContactModel;
use Mautic\NotificationBundle\Api\AbstractNotificationApi;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\NotificationBundle\Event\NotificationSendEvent;
use Mautic\NotificationBundle\Form\Type\MobileNotificationSendType;
use Mautic\NotificationBundle\Form\Type\NotificationSendType;
use Mautic\NotificationBundle\Model\NotificationModel;
use Mautic\NotificationBundle\NotificationEvents;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class CampaignSubscriber implements EventSubscriberInterface
{
/**
* @var string
*/
public const EVENT_ACTION_SEND_MOBILE_NOTIFICATION = 'notification.send_mobile_notification';
/**
* @var string
*/
public const EVENT_ACTION_SEND_NOTIFICATION = 'notification.send_notification';
/**
* The maximum number of `include_player_ids` that can be sent within a single request.
*
* @var int
*/
protected const MAX_PLAYER_IDS_PER_REQUEST = 2000;
public function __construct(
private IntegrationHelper $integrationHelper,
private NotificationModel $notificationModel,
private AbstractNotificationApi $notificationApi,
private EventDispatcherInterface $dispatcher,
private DoNotContactModel $doNotContact,
private TranslatorInterface $translator,
) {
}
public static function getSubscribedEvents(): array
{
return [
CampaignEvents::CAMPAIGN_ON_BUILD => ['onCampaignBuild', 0],
NotificationEvents::ON_CAMPAIGN_BATCH_ACTION => ['onCampaignBatchAction', 0],
];
}
public function onCampaignBuild(CampaignBuilderEvent $event): void
{
$integration = $this->integrationHelper->getIntegrationObject('OneSignal');
if (!$integration || false === $integration->getIntegrationSettings()->getIsPublished()) {
return;
}
$features = $integration->getSupportedFeatures();
if (in_array('mobile', $features)) {
$event->addAction(
static::EVENT_ACTION_SEND_MOBILE_NOTIFICATION,
[
'label' => 'mautic.notification.campaign.send_mobile_notification',
'description' => 'mautic.notification.campaign.send_mobile_notification.tooltip',
'batchEventName' => NotificationEvents::ON_CAMPAIGN_BATCH_ACTION,
'formType' => MobileNotificationSendType::class,
'formTypeOptions' => ['update_select' => 'campaignevent_properties_notification'],
'formTheme' => '@MauticNotification/FormTheme/NotificationSendList/_notificationsend_list_row.html.twig',
'timelineTemplate' => '@MauticNotification/SubscribedEvents/Timeline/index.html.twig',
'channel' => 'mobile_notification',
'channelIdField' => 'mobile_notification',
]
);
}
$event->addAction(
static::EVENT_ACTION_SEND_NOTIFICATION,
[
'label' => 'mautic.notification.campaign.send_notification',
'description' => 'mautic.notification.campaign.send_notification.tooltip',
'batchEventName' => NotificationEvents::ON_CAMPAIGN_BATCH_ACTION,
'formType' => NotificationSendType::class,
'formTypeOptions' => ['update_select' => 'campaignevent_properties_notification'],
'formTheme' => '@MauticNotification/FormTheme/NotificationSendList/_notificationsend_list_row.html.twig',
'timelineTemplate' => '@MauticNotification/SubscribedEvents/Timeline/index.html.twig',
'channel' => 'notification',
'channelIdField' => 'notification',
]
);
}
public function onCampaignBatchAction(PendingEvent $event): void
{
if (!$event->checkContext(static::EVENT_ACTION_SEND_NOTIFICATION) && !$event->checkContext(static::EVENT_ACTION_SEND_MOBILE_NOTIFICATION)) {
return;
}
$notificationId = $event->getEvent()->getProperties()['notification'] ?? null;
$notification = $notificationId ? $this->notificationModel->getEntity((int) $notificationId) : null;
if (!$notification) {
$event->passAllWithError($this->translator->trans('mautic.notification.campaign.failed.missing_entity'));
return;
}
if (!$notification->getIsPublished()) {
$event->passAllWithError($this->translator->trans('mautic.notification.campaign.failed.unpublished'));
return;
}
$event->setChannel('notification', $notification->getId());
if ($notification->getUrl()) {
$this->sendNotificationPerLead($notification, $event);
} else {
$this->sendNotificationsInBatches($notification, $event);
}
}
private function sendNotificationPerLead(Notification $notification, PendingEvent $event): void
{
foreach ($event->getPending() as $log) {
if (!$this->isLeadContactable($event, $log)) {
continue;
}
$playerIds = $this->getLeadPlayerIds($event, $log);
if (!$playerIds) {
continue;
}
$sendNotification = $this->buildNotificationToSend($notification, $log->getLead());
$response = $this->notificationApi->sendNotification($playerIds, $sendNotification);
$this->processResponse($response, $event, $log, $notification, $sendNotification);
}
}
private function sendNotificationsInBatches(Notification $notification, PendingEvent $event): void
{
$batches = $this->buildBatches($event, $notification);
$processedLogs = [];
foreach ($batches as $batch) {
$sendNotification = $batch['sendNotification'];
$playerIdsChunks = array_chunk($batch['playerIds'], static::MAX_PLAYER_IDS_PER_REQUEST, true);
foreach ($playerIdsChunks as $playerIdsChunk) {
$playerIds = array_keys($playerIdsChunk);
$response = $this->notificationApi->sendNotification($playerIds, $sendNotification);
foreach ($playerIdsChunk as $log) {
if (!isset($processedLogs[$log->getId()])) {
$processedLogs[$log->getId()] = $log;
$this->processResponse($response, $event, $log, $notification, $sendNotification);
}
}
}
}
}
private function isLeadContactable(PendingEvent $event, LeadEventLog $log): bool
{
$contactable = DoNotContact::IS_CONTACTABLE === $this->doNotContact->isContactable($log->getLead(), 'notification');
if (!$contactable) {
$event->passWithError($log, $this->translator->trans('mautic.notification.campaign.failed.not_contactable'));
}
return $contactable;
}
/**
* @return string[]
*/
private function getLeadPlayerIds(PendingEvent $event, LeadEventLog $log): array
{
$playerIds = [];
foreach ($log->getLead()->getPushIDs() as $pushID) {
// Skip non-mobile PushIDs if this is a mobile event
if ($event->checkContext(static::EVENT_ACTION_SEND_MOBILE_NOTIFICATION) && !$pushID->isMobile()) {
continue;
}
// Skip mobile PushIDs if this is a non-mobile event
if ($event->checkContext(static::EVENT_ACTION_SEND_NOTIFICATION) && $pushID->isMobile()) {
continue;
}
$playerIds[] = $pushID->getPushID();
}
if (!$playerIds) {
$event->passWithError($log, $this->translator->trans('mautic.notification.campaign.failed.not_subscribed'));
}
return $playerIds;
}
private function buildNotificationToSend(Notification $notification, Lead $lead): Notification
{
[$ignore, $notification] = $this->notificationModel->getTranslatedEntity($notification, $lead);
\assert($notification instanceof Notification);
/** @var TokenReplacementEvent $tokenEvent */
$tokenEvent = $this->dispatcher->dispatch(
new TokenReplacementEvent(
$notification->getMessage(),
$lead,
['channel' => ['notification', $notification->getId()]]
),
NotificationEvents::TOKEN_REPLACEMENT
);
/** @var NotificationSendEvent $sendEvent */
$sendEvent = $this->dispatcher->dispatch(
new NotificationSendEvent($tokenEvent->getContent(), $notification->getHeading(), $lead),
NotificationEvents::NOTIFICATION_ON_SEND
);
if ($url = $notification->getUrl()) {
$url = $this->notificationApi->convertToTrackedUrl(
$url,
[
'notification' => $notification->getId(),
'lead' => $lead->getId(),
],
$notification
);
}
// prevent rewrite notification entity
$sendNotification = clone $notification;
$sendNotification->setUrl($url);
$sendNotification->setMessage($sendEvent->getMessage());
$sendNotification->setHeading($sendEvent->getHeading());
return $sendNotification;
}
private function processResponse(ResponseInterface $response, PendingEvent $event, LeadEventLog $log, Notification $notification, Notification $sendNotification): void
{
// if for some reason the call failed, tell mautic to try again
if (200 !== $response->getStatusCode()) {
$event->fail($log, sprintf('%s (%s)', (string) $response->getBody(), $response->getStatusCode()));
return;
}
$this->notificationModel->createStatEntry($notification, $log->getLead(), 'campaign.event', $event->getEvent()->getId());
$this->notificationModel->getRepository()->upCount($notification->getId());
$result = [
'status' => 'mautic.notification.timeline.status.delivered',
'type' => 'mautic.notification.notification',
'id' => $notification->getId(),
'name' => $notification->getName(),
'heading' => $sendNotification->getHeading(),
'content' => $sendNotification->getMessage(),
];
$log->appendToMetadata($result);
$event->pass($log);
}
/**
* @return array<string,mixed[]>
*/
private function buildBatches(PendingEvent $event, Notification $notification): array
{
$batches = [];
foreach ($event->getPending() as $log) {
if (!$this->isLeadContactable($event, $log)) {
continue;
}
$playerIds = $this->getLeadPlayerIds($event, $log);
if (!$playerIds) {
continue;
}
$sendNotification = $this->buildNotificationToSend($notification, $log->getLead());
$uniqueKey = md5(sprintf('[%s][%s]', $sendNotification->getHeading(), $sendNotification->getMessage()));
if (!isset($batches[$uniqueKey])) {
$batches[$uniqueKey] = [
'sendNotification' => $sendNotification,
'playerIds' => [],
];
}
foreach ($playerIds as $playerId) {
$batches[$uniqueKey]['playerIds'][$playerId] = $log;
}
}
return $batches;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Mautic\NotificationBundle\EventListener;
use Mautic\ChannelBundle\ChannelEvents;
use Mautic\ChannelBundle\Event\ChannelEvent;
use Mautic\ChannelBundle\Model\MessageModel;
use Mautic\NotificationBundle\Form\Type\NotificationListType;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Mautic\ReportBundle\Model\ReportModel;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ChannelSubscriber implements EventSubscriberInterface
{
public function __construct(
private IntegrationHelper $integrationHelper,
) {
}
public static function getSubscribedEvents(): array
{
return [
ChannelEvents::ADD_CHANNEL => ['onAddChannel', 70],
];
}
public function onAddChannel(ChannelEvent $event): void
{
$integration = $this->integrationHelper->getIntegrationObject('OneSignal');
if ($integration && $integration->getIntegrationSettings()->getIsPublished()) {
$event->addChannel(
'notification',
[
MessageModel::CHANNEL_FEATURE => [
'campaignAction' => CampaignSubscriber::EVENT_ACTION_SEND_NOTIFICATION,
'campaignDecisionsSupported' => [
'page.pagehit',
'asset.download',
'form.submit',
],
'lookupFormType' => NotificationListType::class,
'repository' => \Mautic\NotificationBundle\Entity\Notification::class,
'lookupOptions' => [
'mobile' => false,
'desktop' => true,
],
],
ReportModel::CHANNEL_FEATURE => [
'table' => 'push_notifications',
],
]
);
$supportedFeatures = $integration->getSupportedFeatures();
if (in_array('mobile', $supportedFeatures)) {
$event->addChannel(
'mobile_notification',
[
MessageModel::CHANNEL_FEATURE => [
'campaignAction' => CampaignSubscriber::EVENT_ACTION_SEND_MOBILE_NOTIFICATION,
'campaignDecisionsSupported' => [
'page.pagehit',
'asset.download',
'form.submit',
],
'lookupFormType' => NotificationListType::class,
'repository' => \Mautic\NotificationBundle\Entity\Notification::class,
'lookupOptions' => [
'mobile' => true,
'desktop' => false,
],
],
]
);
}
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\EventListener;
use Mautic\ConfigBundle\ConfigEvents;
use Mautic\ConfigBundle\Event\ConfigBuilderEvent;
use Mautic\NotificationBundle\Form\Type\NotificationConfigType;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ConfigSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
ConfigEvents::CONFIG_ON_GENERATE => ['onConfigGenerate', 0],
];
}
public function onConfigGenerate(ConfigBuilderEvent $event): void
{
$event->addForm([
'bundle' => 'NotificationBundle',
'formAlias' => 'notification_config',
'formType' => NotificationConfigType::class,
'formTheme' => '@MauticNotification/FormTheme/Config/_config_notification_config_widget.html.twig',
'parameters' => $event->getParametersFromConfig('MauticNotificationBundle'),
]);
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace Mautic\NotificationBundle\EventListener;
use Mautic\AssetBundle\Helper\TokenHelper as AssetTokenHelper;
use Mautic\CoreBundle\Event\TokenReplacementEvent;
use Mautic\CoreBundle\Model\AuditLogModel;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Helper\TokenHelper;
use Mautic\NotificationBundle\Event\NotificationEvent;
use Mautic\NotificationBundle\NotificationEvents;
use Mautic\PageBundle\Entity\Trackable;
use Mautic\PageBundle\Helper\TokenHelper as PageTokenHelper;
use Mautic\PageBundle\Model\TrackableModel;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class NotificationSubscriber implements EventSubscriberInterface
{
public function __construct(
private AuditLogModel $auditLogModel,
private TrackableModel $trackableModel,
private PageTokenHelper $pageTokenHelper,
private AssetTokenHelper $assetTokenHelper,
) {
}
public static function getSubscribedEvents(): array
{
return [
NotificationEvents::NOTIFICATION_POST_SAVE => ['onPostSave', 0],
NotificationEvents::NOTIFICATION_POST_DELETE => ['onDelete', 0],
NotificationEvents::TOKEN_REPLACEMENT => ['onTokenReplacement', 0],
];
}
/**
* Add an entry to the audit log.
*/
public function onPostSave(NotificationEvent $event): void
{
$entity = $event->getNotification();
if ($details = $event->getChanges()) {
$log = [
'bundle' => 'notification',
'object' => 'notification',
'objectId' => $entity->getId(),
'action' => ($event->isNew()) ? 'create' : 'update',
'details' => $details,
];
$this->auditLogModel->writeToLog($log);
}
}
/**
* Add a delete entry to the audit log.
*/
public function onDelete(NotificationEvent $event): void
{
$entity = $event->getNotification();
$log = [
'bundle' => 'notification',
'object' => 'notification',
'objectId' => $entity->deletedId,
'action' => 'delete',
'details' => ['name' => $entity->getName()],
];
$this->auditLogModel->writeToLog($log);
}
public function onTokenReplacement(TokenReplacementEvent $event): void
{
/** @var Lead $lead */
$lead = $event->getLead();
$content = $event->getContent();
$clickthrough = $event->getClickthrough();
if ($content) {
$tokens = array_merge(
TokenHelper::findLeadTokens($content, $lead->getProfileFields()),
$this->pageTokenHelper->findPageTokens($content, $clickthrough),
$this->assetTokenHelper->findAssetTokens($content, $clickthrough)
);
[$content, $trackables] = $this->trackableModel->parseContentForTrackables(
$content,
$tokens,
'notification',
$clickthrough['channel'][1]
);
/**
* @var string $token
* @var Trackable $trackable
*/
foreach ($trackables as $token => $trackable) {
$tokens[$token] = $this->trackableModel->generateTrackableUrl($trackable, $clickthrough);
}
$content = str_replace(array_keys($tokens), array_values($tokens), $content);
$event->setContent($content);
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Mautic\NotificationBundle\EventListener;
use Mautic\CoreBundle\Twig\Helper\AssetsHelper;
use Mautic\PageBundle\Event\PageDisplayEvent;
use Mautic\PageBundle\PageEvents;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class PageSubscriber implements EventSubscriberInterface
{
public function __construct(
private AssetsHelper $assetsHelper,
private IntegrationHelper $integrationHelper,
) {
}
public static function getSubscribedEvents(): array
{
return [
PageEvents::PAGE_ON_DISPLAY => ['onPageDisplay', 0],
];
}
public function onPageDisplay(PageDisplayEvent $event): void
{
$integrationObject = $this->integrationHelper->getIntegrationObject('OneSignal');
$settings = $integrationObject->getIntegrationSettings();
$features = $settings->getFeatureSettings();
$script = '';
if (!in_array('landing_page_enabled', $features)) {
$script = 'disable_notification = true;';
}
$this->assetsHelper->addScriptDeclaration($script, 'onPageDisplay_headClose');
}
}

View File

@@ -0,0 +1,325 @@
<?php
namespace Mautic\NotificationBundle\EventListener;
use Doctrine\DBAL\Connection;
use Mautic\CoreBundle\Helper\Chart\LineChart;
use Mautic\LeadBundle\Model\CompanyReportData;
use Mautic\NotificationBundle\Entity\StatRepository;
use Mautic\ReportBundle\Event\ReportBuilderEvent;
use Mautic\ReportBundle\Event\ReportGeneratorEvent;
use Mautic\ReportBundle\Event\ReportGraphEvent;
use Mautic\ReportBundle\ReportEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ReportSubscriber implements EventSubscriberInterface
{
public const MOBILE_NOTIFICATIONS = 'mobile_notifications';
public const MOBILE_NOTIFICATIONS_STATS = 'mobile_notifications.stats';
public function __construct(
private Connection $db,
private CompanyReportData $companyReportData,
private StatRepository $statRepository,
) {
}
public static function getSubscribedEvents(): array
{
return [
ReportEvents::REPORT_ON_BUILD => ['onReportBuilder', 0],
ReportEvents::REPORT_ON_GENERATE => ['onReportGenerate', 0],
ReportEvents::REPORT_ON_GRAPH_GENERATE => ['onReportGraphGenerate', 0],
];
}
/**
* Add available tables and columns to the report builder lookup.
*/
public function onReportBuilder(ReportBuilderEvent $event): void
{
if (!$event->checkContext([self::MOBILE_NOTIFICATIONS, self::MOBILE_NOTIFICATIONS_STATS])) {
return;
}
$prefix = 'pn.';
$channelUrlTrackables = 'cut.';
$columns = [
$prefix.'heading' => [
'label' => 'mautic.notification.mobile_notification.heading',
'type' => 'string',
],
$prefix.'lang' => [
'label' => 'mautic.core.language',
'type' => 'string',
],
$prefix.'read_count' => [
'label' => 'mautic.mobile_notification.report.read_count',
'type' => 'int',
],
'read_ratio' => [
'alias' => 'read_ratio',
'label' => 'mautic.mobile_notification.report.read_ratio',
'type' => 'string',
'formula' => 'CONCAT(ROUND(('.$prefix.'read_count/'.$prefix.'sent_count)*100),\'%\')',
],
$prefix.'sent_count' => [
'label' => 'mautic.mobile_notification.report.sent_count',
'type' => 'int',
],
'hits' => [
'alias' => 'hits',
'label' => 'mautic.mobile_notification.report.hits_count',
'type' => 'string',
'formula' => $channelUrlTrackables.'hits',
],
'unique_hits' => [
'alias' => 'unique_hits',
'label' => 'mautic.mobile_notification.report.unique_hits_count',
'type' => 'string',
'formula' => $channelUrlTrackables.'unique_hits',
],
'hits_ratio' => [
'alias' => 'hits_ratio',
'label' => 'mautic.mobile_notification.report.hits_ratio',
'type' => 'string',
'formula' => 'CONCAT(ROUND('.$channelUrlTrackables.'hits/('.$prefix.'sent_count * '.$channelUrlTrackables
.'trackable_count)*100),\'%\')',
],
'unique_ratio' => [
'alias' => 'unique_ratio',
'label' => 'mautic.mobile_notification.report.unique_ratio',
'type' => 'string',
'formula' => 'CONCAT(ROUND('.$channelUrlTrackables.'unique_hits/('.$prefix.'sent_count * '.$channelUrlTrackables
.'trackable_count)*100),\'%\')',
],
];
$columns = array_merge(
$columns,
$event->getStandardColumns($prefix, [], 'mautic_mobile_notification_action'),
$event->getCategoryColumns()
);
$data = [
'display_name' => 'mautic.notification.mobile_notifications',
'columns' => $columns,
];
$event->addTable(self::MOBILE_NOTIFICATIONS, $data);
if ($event->checkContext(self::MOBILE_NOTIFICATIONS_STATS)) {
// Ratios are not applicable for individual stats
unset($columns['read_ratio'], $columns['unsubscribed_ratio'], $columns['hits_ratio'], $columns['unique_ratio']);
// Mobile Notification counts are not applicable for individual stats
unset($columns[$prefix.'read_count']);
$statPrefix = 'pns.';
$statColumns = [
$statPrefix.'date_sent' => [
'label' => 'mautic.mobile_notifications.report.stat.date_sent',
'type' => 'datetime',
'groupByFormula' => 'DATE('.$statPrefix.'date_sent)',
],
$statPrefix.'date_read' => [
'label' => 'mautic.mobile_notifications.report.stat.date_read',
'type' => 'datetime',
'groupByFormula' => 'DATE('.$statPrefix.'date_read)',
],
$statPrefix.'source' => [
'label' => 'mautic.report.field.source',
'type' => 'string',
],
$statPrefix.'source_id' => [
'label' => 'mautic.report.field.source_id',
'type' => 'int',
],
];
$companyColumns = $this->companyReportData->getCompanyData();
$mobileStatsColumns = array_merge(
$columns,
$statColumns,
$event->getLeadColumns(),
$event->getIpColumn(),
$companyColumns
);
$data = [
'display_name' => 'mautic.mobile_notification.stats.report.table',
'columns' => $mobileStatsColumns,
];
$context = self::MOBILE_NOTIFICATIONS_STATS;
// Register table
$event->addTable($context, $data, self::MOBILE_NOTIFICATIONS);
// Register Graphs
$event->addGraph($context, 'line', 'mautic.mobile_notification.graph.line.stats');
$event->addGraph($context, 'table', 'mautic.mobile_notification.table.most.mobile_notifications.sent');
$event->addGraph($context, 'table', 'mautic.mobile_notification.table.most.mobile_notifications.read');
$event->addGraph($context, 'table', 'mautic.mobile_notification.table.most.mobile_notifications.read.percent');
}
}
/**
* Initialize the QueryBuilder object to generate reports from.
*/
public function onReportGenerate(ReportGeneratorEvent $event): void
{
if (!$event->checkContext([self::MOBILE_NOTIFICATIONS, self::MOBILE_NOTIFICATIONS_STATS])) {
return;
}
$qb = $event->getQueryBuilder();
// channel_url_trackables subquery
$qbcut = $this->db->createQueryBuilder();
$clickColumns = ['hits', 'unique_hits', 'hits_ratio', 'unique_ratio'];
// Ensure this only stats mobile notifications
$qb->andWhere('pn.mobile = 1');
switch ($event->getContext()) {
case self::MOBILE_NOTIFICATIONS:
$qb->from(MAUTIC_TABLE_PREFIX.'push_notifications', 'pn');
$event->addCategoryLeftJoin($qb, 'pn');
if ($event->usesColumn($clickColumns)) {
$qbcut->select(
'COUNT(cut2.channel_id) AS trackable_count, SUM(cut2.hits) AS hits',
'SUM(cut2.unique_hits) AS unique_hits',
'cut2.channel_id'
)
->from(MAUTIC_TABLE_PREFIX.'channel_url_trackables', 'cut2')
->where('cut2.channel = \'notification\'')
->groupBy('cut2.channel_id');
$qb->leftJoin('pn', sprintf('(%s)', $qbcut->getSQL()), 'cut', 'pn.id = cut.channel_id');
}
break;
case self::MOBILE_NOTIFICATIONS_STATS:
$qb->from(MAUTIC_TABLE_PREFIX.'push_notification_stats', 'pns')
->leftJoin('pns', MAUTIC_TABLE_PREFIX.'push_notifications', 'pn', 'pn.id = pns.notification_id');
$event->addCategoryLeftJoin($qb, 'pn')
->addLeadLeftJoin($qb, 'pns')
->addIpAddressLeftJoin($qb, 'pns')
->applyDateFilters($qb, 'date_sent', 'pns');
if ($event->usesColumn($clickColumns)) {
$qbcut->select('COUNT(ph.id) AS hits', 'COUNT(DISTINCT(ph.redirect_id)) AS unique_hits', 'cut2.channel_id', 'ph.lead_id')
->from(MAUTIC_TABLE_PREFIX.'channel_url_trackables', 'cut2')
->join(
'cut2',
MAUTIC_TABLE_PREFIX.'page_hits',
'ph',
'cut2.redirect_id = ph.redirect_id AND cut2.channel_id = ph.source_id'
)
->where('cut2.channel = \'email\' AND ph.source = \'email\'')
->groupBy('cut2.channel_id, ph.lead_id');
$qb->leftJoin('pn', sprintf('(%s)', $qbcut->getSQL()), 'cut', 'pn.id = cut.channel_id AND pns.lead_id = cut.lead_id');
}
if ($this->companyReportData->eventHasCompanyColumns($event)) {
$event->addCompanyLeftJoin($qb);
}
break;
}
$event->setQueryBuilder($qb);
}
/**
* Initialize the QueryBuilder object to generate reports from.
*/
public function onReportGraphGenerate(ReportGraphEvent $event): void
{
// Context check, we only want to fire for Mobile Notification reports
if (!$event->checkContext(self::MOBILE_NOTIFICATIONS_STATS)) {
return;
}
$graphs = $event->getRequestedGraphs();
$qb = $event->getQueryBuilder();
foreach ($graphs as $g) {
$options = $event->getOptions($g);
$queryBuilder = clone $qb;
$chartQuery = clone $options['chartQuery'];
$origQuery = clone $queryBuilder;
$chartQuery->applyDateFilters($queryBuilder, 'date_sent', 'pns');
switch ($g) {
case 'mautic.mobile_notification.graph.line.stats':
$chart = new LineChart(null, $options['dateFrom'], $options['dateTo']);
$sendQuery = clone $queryBuilder;
$readQuery = clone $origQuery;
$readQuery->andWhere($qb->expr()->isNotNull('date_read'));
$chartQuery->applyDateFilters($readQuery, 'date_read', 'pns');
$chartQuery->modifyTimeDataQuery($sendQuery, 'date_sent', 'pns');
$chartQuery->modifyTimeDataQuery($readQuery, 'date_read', 'pns');
$sends = $chartQuery->loadAndBuildTimeData($sendQuery);
$reads = $chartQuery->loadAndBuildTimeData($readQuery);
$chart->setDataset($options['translator']->trans('mautic.mobile_notification.sent.mobile_notifications'), $sends);
$chart->setDataset($options['translator']->trans('mautic.mobile_notification.read.mobile_notifications'), $reads);
$data = $chart->render();
$data['name'] = $g;
$event->setGraph($g, $data);
break;
case 'mautic.mobile_notification.table.most.mobile_notifications.sent':
$queryBuilder->select('pn.id, pn.heading as title, count(pns.id) as sent')
->groupBy('pn.id, pn.heading')
->orderBy('sent', 'DESC');
$limit = 10;
$offset = 0;
$items = $this->statRepository->getMostNotifications($queryBuilder, $limit, $offset);
$graphData = [];
$graphData['data'] = $items;
$graphData['name'] = $g;
$graphData['iconClass'] = 'ri-send-plane-line';
$graphData['link'] = 'mautic_mobile_notification_action';
$event->setGraph($g, $graphData);
break;
case 'mautic.mobile_notification.table.most.mobile_notifications.read':
$queryBuilder->select('pn.id, pn.heading as title, count(CASE WHEN pns.date_read THEN 1 ELSE null END) as "read"')
->groupBy('pn.id, pn.heading')
->orderBy('"read"', 'DESC');
$limit = 10;
$offset = 0;
$items = $this->statRepository->getMostNotifications($queryBuilder, $limit, $offset);
$graphData = [];
$graphData['data'] = $items;
$graphData['name'] = $g;
$graphData['iconClass'] = 'ri-eye-line';
$graphData['link'] = 'mautic_mobile_notification_action';
$event->setGraph($g, $graphData);
break;
case 'mautic.mobile_notification.table.most.mobile_notifications.read.percent':
$queryBuilder->select('pn.id, pn.heading as title, round(pn.read_count / pn.sent_count * 100) as ratio')
->groupBy('pn.id, pn.heading')
->orderBy('ratio', 'DESC');
$limit = 10;
$offset = 0;
$items = $this->statRepository->getMostNotifications($queryBuilder, $limit, $offset);
$graphData = [];
$graphData['data'] = $items;
$graphData['name'] = $g;
$graphData['iconClass'] = 'ri-speed-up-line';
$graphData['link'] = 'mautic_mobile_notification_action';
$event->setGraph($g, $graphData);
break;
}
unset($queryBuilder);
}
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\EventListener;
use Mautic\CoreBundle\CoreEvents;
use Mautic\CoreBundle\DTO\GlobalSearchFilterDTO;
use Mautic\CoreBundle\Event as MauticEvents;
use Mautic\CoreBundle\Service\GlobalSearch;
use Mautic\NotificationBundle\Model\NotificationModel;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SearchSubscriber implements EventSubscriberInterface
{
public function __construct(
private NotificationModel $model,
private GlobalSearch $globalSearch,
) {
}
public static function getSubscribedEvents(): array
{
return [
CoreEvents::GLOBAL_SEARCH => [
['onGlobalSearchWebNotification', 0],
['onGlobalSearchMobileNotification', 0],
],
];
}
public function onGlobalSearchWebNotification(MauticEvents\GlobalSearchEvent $event): void
{
$filterDTO = new GlobalSearchFilterDTO($event->getSearchString());
$filterDTO->setFilters([
'where' => [
[
'expr' => 'eq',
'col' => 'mobile',
'val' => 0,
],
],
]);
$results = $this->globalSearch->performSearch(
$filterDTO,
$this->model,
'@MauticNotification/SubscribedEvents/Search/global-web.html.twig'
);
if (!empty($results)) {
$event->addResults('mautic.notification.notification.header', $results);
}
}
public function onGlobalSearchMobileNotification(MauticEvents\GlobalSearchEvent $event): void
{
$filterDTO = new GlobalSearchFilterDTO($event->getSearchString());
$filterDTO->setFilters([
'where' => [
[
'expr' => 'eq',
'col' => 'mobile',
'val' => 1,
],
],
]);
$results = $this->globalSearch->performSearch(
$filterDTO,
$this->model,
'@MauticNotification/SubscribedEvents/Search/global-mobile.html.twig'
);
if (!empty($results)) {
$event->addResults('mautic.notification.mobile_notification.header', $results);
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Mautic\NotificationBundle\EventListener;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\EventListener\CommonStatsSubscriber;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\NotificationBundle\Entity\Stat;
class StatsSubscriber extends CommonStatsSubscriber
{
public function __construct(CorePermissions $security, EntityManager $entityManager)
{
parent::__construct($security, $entityManager);
$this->addContactRestrictedRepositories([Stat::class]);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Mautic\NotificationBundle\Exception;
class MissingApiKeyException extends \Exception
{
protected $message = 'Missing Notification API Key';
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Mautic\NotificationBundle\Exception;
class MissingAppIDException extends \Exception
{
protected $message = 'Missing Notification App ID';
}

View File

@@ -0,0 +1,257 @@
<?php
namespace Mautic\NotificationBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\ButtonGroupType;
use Mautic\CoreBundle\Form\Type\SortableListType;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<array<mixed>>
*/
class MobileNotificationDetailsType extends AbstractType
{
public function __construct(
protected IntegrationHelper $integrationHelper,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$integration = $this->integrationHelper->getIntegrationObject('OneSignal');
$settings = $integration->getIntegrationSettings()->getFeatureSettings();
$builder->add(
'additional_data',
SortableListType::class,
[
'required' => false,
'label' => 'mautic.notification.tab.data',
'option_required' => false,
'with_labels' => true,
]
);
if (!isset($settings['platforms'])) {
return;
}
if (in_array('ios', $settings['platforms'], true)) {
$builder->add(
'ios_subtitle',
TextType::class,
[
'label' => 'mautic.notification.form.mobile.ios_subtitle',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.form.mobile.ios_subtitle.tooltip',
],
'required' => false,
]
);
$builder->add(
'ios_sound',
TextType::class,
[
'label' => 'mautic.notification.form.mobile.ios_sound',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.form.mobile.ios_sound.tooltip',
],
'required' => false,
]
);
$builder->add(
'ios_badges',
ButtonGroupType::class,
[
'choices' => [
'mautic.notification.form.mobile.ios_badges.set' => 'SetTo',
'mautic.notification.form.mobile.ios_badges.increment' => 'Increase',
],
'attr' => [
'tooltip' => 'mautic.notification.form.mobile.ios_badges.tooltip',
],
'label' => 'mautic.notification.form.mobile.ios_badges',
'empty_data' => 'None',
'required' => false,
'placeholder' => 'mautic.notification.form.mobile.ios_badges.placeholder',
'expanded' => true,
'multiple' => false,
]
);
$builder->add(
'ios_badgeCount',
IntegerType::class,
[
'label' => 'mautic.notification.form.mobile.ios_badgecount',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.form.mobile.ios_badgecount.tooltip',
'data-show-on' => '{"mobile_notification_mobileSettings_ios_badges_placeholder":""}',
],
'required' => false,
]
);
$builder->add(
'ios_contentAvailable',
CheckboxType::class,
[
'label' => 'mautic.notification.form.mobile.ios_contentavailable',
'attr' => [
'tooltip' => 'mautic.notification.form.mobile.ios_contentavailable.tooltip',
],
'required' => false,
]
);
$builder->add(
'ios_media',
FileType::class,
[
'label' => 'mautic.notification.form.mobile.ios_media',
'attr' => [
'tooltip' => 'mautic.notification.form.mobile.ios_media.tooltip',
],
'required' => false,
]
);
$builder->add(
'ios_mutableContent',
CheckboxType::class,
[
'label' => 'mautic.notification.form.mobile.ios_mutablecontent',
'attr' => [
'tooltip' => 'mautic.notification.form.mobile.mutablecontent.tooltip',
],
'required' => false,
]
);
}
if (in_array('android', $settings['platforms'], true)) {
$builder->add(
'android_sound',
TextType::class,
[
'label' => 'mautic.notification.form.mobile.android_sound',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.form.mobile.android_sound.tooltip',
],
'required' => false,
]
);
$builder->add(
'android_small_icon',
TextType::class,
[
'label' => 'mautic.notification.form.mobile.android_small_icon',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.form.mobile.android_small_icon.tooltip',
],
'required' => false,
]
);
$builder->add(
'android_large_icon',
TextType::class,
[
'label' => 'mautic.notification.form.mobile.android_large_icon',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.form.mobile.android_large_icon.tooltip',
],
'required' => false,
]
);
$builder->add(
'android_big_picture',
TextType::class,
[
'label' => 'mautic.notification.form.mobile.android_big_picture',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.form.mobile.android_big_picture.tooltip',
],
'required' => false,
]
);
$builder->add(
'android_led_color',
TextType::class,
[
'label' => 'mautic.notification.form.mobile.android_led_color',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.form.mobile.android_led_color.tooltip',
'data-toggle' => 'color',
],
'required' => false,
]
);
$builder->add(
'android_accent_color',
TextType::class,
[
'label' => 'mautic.notification.form.mobile.android_accent_color',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.form.mobile.android_accent_color.tooltip',
'data-toggle' => 'color',
],
'required' => false,
]
);
$builder->add(
'android_group_key',
TextType::class,
[
'label' => 'mautic.notification.form.mobile.android_group_key',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.form.mobile.android_group_key.tooltip',
],
'required' => false,
]
);
$builder->add(
'android_lockscreen_visibility',
ButtonGroupType::class,
[
'choices' => [
'mautic.notification.form.mobile.android_lockscreen_visibility.private' => '0',
'mautic.notification.form.mobile.android_lockscreen_visibility.secret' => '-1',
],
'attr' => [
'tooltip' => 'mautic.notification.form.mobile.android_lockscreen_visibility.tooltip',
],
'label' => 'mautic.notification.form.mobile.android_lockscreen_visibility',
'empty_data' => '1',
'required' => false,
'placeholder' => 'mautic.notification.form.mobile.android_lockscreen_visibility.placeholder',
'expanded' => true,
'multiple' => false,
]
);
}
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Mautic\NotificationBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\EntityLookupType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<array<mixed>>
*/
class MobileNotificationListType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'modal_route' => 'mautic_mobile_notification_action',
'modal_header' => 'mautic.notification.mobile.header.new',
'model' => 'notification',
'model_lookup_method' => 'getLookupResults',
'lookup_arguments' => fn (Options $options): array => [
'type' => 'mobile_notification',
'filter' => '$data',
'limit' => 0,
'start' => 0,
'options' => [
'notification_type' => $options['notification_type'],
'top_level' => $options['top_level'],
'ignore_ids' => $options['ignore_ids'],
],
],
'ajax_lookup_action' => function (Options $options): string {
$query = [
'notification_type' => $options['notification_type'],
'top_level' => $options['top_level'],
'ignore_ids' => $options['ignore_ids'],
];
return 'notification:getLookupChoiceList&'.http_build_query($query);
},
'expanded' => false,
'multiple' => true,
'required' => false,
'notification_type' => 'template',
'top_level' => 'translation',
'ignore_ids' => [],
]
);
}
public function getBlockPrefix(): string
{
return 'mobilenotification_list';
}
public function getParent(): ?string
{
return EntityLookupType::class;
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Mautic\NotificationBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ButtonType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @extends AbstractType<array<mixed>>
*/
class MobileNotificationSendType extends AbstractType
{
public function __construct(
protected RouterInterface $router,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'notification',
MobileNotificationListType::class,
[
'label' => 'mautic.notification.send.selectnotifications',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.choose.notifications',
'onchange' => 'Mautic.disabledNotificationAction()',
],
'multiple' => false,
'constraints' => [
new NotBlank(
['message' => 'mautic.notification.choosenotification.notblank']
),
],
]
);
if (!empty($options['update_select'])) {
$windowUrl = $this->router->generate('mautic_mobile_notification_action', [
'objectAction' => 'new',
'contentOnly' => 1,
'updateSelect' => $options['update_select'],
]);
$builder->add(
'newNotificationButton',
ButtonType::class,
[
'attr' => [
'class' => 'btn btn-primary btn-nospin',
'onclick' => 'Mautic.loadNewWindow({
"windowUrl": "'.$windowUrl.'"
})',
'icon' => 'ri-add-line',
],
'label' => 'mautic.notification.send.new.notification',
]
);
if (array_key_exists('data', $options)) {
if (is_array($options['data']) && array_key_exists('notification', $options['data'])) {
$notification = $options['data']['notification'];
}
}
// create button edit notification
$windowUrlEdit = $this->router->generate('mautic_mobile_notification_action', [
'objectAction' => 'edit',
'objectId' => 'notificationId',
'contentOnly' => 1,
'updateSelect' => $options['update_select'],
]);
$builder->add(
'editNotificationButton',
ButtonType::class,
[
'attr' => [
'class' => 'btn btn-primary btn-nospin',
'onclick' => 'Mautic.loadNewWindow(Mautic.standardNotificationUrl({"windowUrl": "'.$windowUrlEdit.'"}))',
'disabled' => !isset($notification),
'icon' => 'ri-edit-line',
],
'label' => 'mautic.notification.send.edit.notification',
]
);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefined(['update_select']);
}
public function getBlockPrefix(): string
{
return 'mobilenotificationsend_list';
}
}

View File

@@ -0,0 +1,231 @@
<?php
namespace Mautic\NotificationBundle\Form\Type;
use Doctrine\ORM\EntityManager;
use Mautic\CategoryBundle\Form\Type\CategoryListType;
use Mautic\CoreBundle\Form\DataTransformer\IdToEntityModelTransformer;
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\CoreBundle\Form\Type\PublishDownDateType;
use Mautic\CoreBundle\Form\Type\PublishUpDateType;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Mautic\EmailBundle\Form\Type\EmailUtmTagsType;
use Mautic\NotificationBundle\Entity\Notification;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\LocaleType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<Notification>
*/
class MobileNotificationType extends AbstractType
{
public function __construct(private readonly EntityManager $entityManager)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventSubscriber(new CleanFormSubscriber(['content' => 'html', 'customHtml' => 'html']));
$builder->addEventSubscriber(new FormExitSubscriber('notification.notification', $options));
$builder->add(
'name',
TextType::class,
[
'label' => 'mautic.notification.form.internal.name',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]
);
$builder->add(
'description',
TextareaType::class,
[
'label' => 'mautic.notification.form.internal.description',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'required' => false,
]
);
$builder->add(
'heading',
TextType::class,
[
'label' => 'mautic.notification.form.mobile.heading',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]
);
$builder->add(
'message',
TextareaType::class,
[
'label' => 'mautic.notification.form.message',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'rows' => 6,
],
]
);
$builder->add(
'url',
UrlType::class,
[
'label' => 'mautic.notification.form.mobile.url',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.form.mobile.url.tooltip',
],
'required' => false,
]
);
$builder->add(
'utmTags',
EmailUtmTagsType::class,
[
'label' => 'mautic.email.utm_tags',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.email.utm_tags.tooltip',
],
'required' => false,
]
);
$builder->add('isPublished', YesNoButtonGroupType::class);
$builder->add('publishUp', PublishUpDateType::class);
$builder->add('publishDown', PublishDownDateType::class);
// add category
$builder->add(
'category',
CategoryListType::class,
[
'bundle' => 'notification',
]
);
$builder->add(
'language',
LocaleType::class,
[
'label' => 'mautic.core.language',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'required' => false,
]
);
$transformer = new IdToEntityModelTransformer($this->entityManager, Notification::class);
$builder->add(
$builder->create(
'translationParent',
HiddenType::class
)->addModelTransformer($transformer)
);
$builder->add(
'translationParentSelector', // This is a non-mapped field
MobileNotificationListType::class,
[
'label' => 'mautic.core.form.translation_parent',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.core.form.translation_parent.help',
],
'required' => false,
'multiple' => false,
'placeholder' => 'mautic.core.form.translation_parent.empty',
'top_level' => 'translation',
'ignore_ids' => [(int) $options['data']->getId()],
'mapped' => false,
'data' => ($options['data']->getTranslationParent()) ? $options['data']->getTranslationParent()->getId() : null,
]
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) {
$data = $event->getData();
if (isset($data['translationParentSelector'])) {
$data['translationParent'] = $data['translationParentSelector'];
}
$event->setData($data);
}
);
$builder->add('buttons', FormButtonsType::class);
if (!empty($options['update_select'])) {
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
]
);
$builder->add(
'updateSelect',
HiddenType::class,
[
'data' => $options['update_select'],
'mapped' => false,
]
);
} else {
$builder->add(
'buttons',
FormButtonsType::class
);
}
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
$builder->add(
'mobile',
HiddenType::class,
[
'data' => 1,
]
);
$builder->add(
'mobileSettings',
MobileNotificationDetailsType::class
);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'data_class' => Notification::class,
]
);
$resolver->setDefined(['update_select']);
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Mautic\EmailBundle\Validator\MultipleEmailsValid;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @extends AbstractType<array<mixed>>
*/
class NotificationConfigType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'campaign_send_notification_to_author',
YesNoButtonGroupType::class,
[
'label' => 'mautic.notification.form.config.send_notification_to_author',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.form.config.send_notification_to_author.tooltip',
'onchange' => 'Mautic.resetEmailsToNotification(this)',
],
'required' => true,
]
);
$builder->add(
'campaign_notification_email_addresses',
TextType::class,
[
'label' => 'mautic.notification.form.config.notification_email_addresses',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control notification_email_addresses',
'tooltip' => 'mautic.notification.form.config.notification_email_addresses.tooltip',
'data-show-on' => '{"config_notification_config_campaign_send_notification_to_author_0":"checked"}',
],
'constraints' => [
new NotBlank(['groups' => ['campaign_email_list']]),
new MultipleEmailsValid(),
],
]
);
$builder->add(
'webhook_send_notification_to_author',
YesNoButtonGroupType::class,
[
'label' => 'mautic.notification.form.config.send_notification_to_author',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.form.config.send_notification_to_author.tooltip',
'onchange' => 'Mautic.resetEmailsToNotification(this)',
],
'required' => true,
]
);
$builder->add(
'webhook_notification_email_addresses',
TextType::class,
[
'label' => 'mautic.notification.form.config.notification_email_addresses',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control notification_email_addresses',
'tooltip' => 'mautic.notification.form.config.notification_email_addresses.tooltip',
'data-show-on' => '{"config_notification_config_webhook_send_notification_to_author_0":"checked"}',
],
'constraints' => [
new MultipleEmailsValid(),
new NotBlank(['groups' => ['webhook_email_list']]),
],
]
);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'validation_groups' => function (FormInterface $form): array {
$data = $form->getData();
$groups = ['Default'];
if (!$data['webhook_send_notification_to_author']) {
$groups[] = 'webhook_email_list';
}
if (!$data['campaign_send_notification_to_author']) {
$groups[] = 'campaign_email_list';
}
return $groups;
},
]);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Mautic\NotificationBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\EntityLookupType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<array<mixed>>
*/
class NotificationListType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'modal_route' => 'mautic_notification_action',
'modal_header' => 'mautic.notification.header.new',
'model' => 'notification',
'model_lookup_method' => 'getLookupResults',
'lookup_arguments' => fn (Options $options): array => [
'type' => 'notification',
'filter' => '$data',
'limit' => 0,
'start' => 0,
'options' => [
'notification_type' => $options['notification_type'],
],
],
'ajax_lookup_action' => function (Options $options): string {
$query = [
'notification_type' => $options['notification_type'],
];
return 'notification:getLookupChoiceList&'.http_build_query($query);
},
'expanded' => false,
'multiple' => true,
'required' => false,
'notification_type' => 'template',
]
);
}
public function getParent(): ?string
{
return EntityLookupType::class;
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Mautic\NotificationBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ButtonType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @extends AbstractType<array<mixed>>
*/
class NotificationSendType extends AbstractType
{
public function __construct(
protected RouterInterface $router,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'notification',
NotificationListType::class,
[
'label' => 'mautic.notification.send.selectnotifications',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.choose.notifications',
'onchange' => 'Mautic.disabledNotificationAction()',
],
'multiple' => false,
'constraints' => [
new NotBlank(
['message' => 'mautic.notification.choosenotification.notblank']
),
],
]
);
if (!empty($options['update_select'])) {
$windowUrl = $this->router->generate('mautic_notification_action', [
'objectAction' => 'new',
'contentOnly' => 1,
'updateSelect' => $options['update_select'],
]);
$builder->add(
'newNotificationButton',
ButtonType::class,
[
'attr' => [
'class' => 'btn btn-primary btn-nospin',
'onclick' => 'Mautic.loadNewWindow({
"windowUrl": "'.$windowUrl.'"
})',
'icon' => 'ri-add-line',
],
'label' => 'mautic.notification.send.new.notification',
]
);
if (array_key_exists('data', $options)) {
if (is_array($options['data']) && array_key_exists('notification', $options['data'])) {
$notification = $options['data']['notification'];
}
}
// create button edit notification
$windowUrlEdit = $this->router->generate('mautic_notification_action', [
'objectAction' => 'edit',
'objectId' => 'notificationId',
'contentOnly' => 1,
'updateSelect' => $options['update_select'],
]);
$builder->add(
'editNotificationButton',
ButtonType::class,
[
'attr' => [
'class' => 'btn btn-primary btn-nospin',
'onclick' => 'Mautic.loadNewWindow(Mautic.standardNotificationUrl({"windowUrl": "'.$windowUrlEdit.'"}))',
'disabled' => !isset($notification),
'icon' => 'ri-edit-line',
],
'label' => 'mautic.notification.send.edit.notification',
]
);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefined(['update_select']);
}
public function getBlockPrefix(): string
{
return 'notificationsend_list';
}
}

View File

@@ -0,0 +1,187 @@
<?php
namespace Mautic\NotificationBundle\Form\Type;
use Mautic\CategoryBundle\Form\Type\CategoryListType;
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\CoreBundle\Form\Type\PublishDownDateType;
use Mautic\CoreBundle\Form\Type\PublishUpDateType;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Mautic\EmailBundle\Form\Type\EmailUtmTagsType;
use Mautic\NotificationBundle\Entity\Notification;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\LocaleType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<Notification>
*/
class NotificationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventSubscriber(new CleanFormSubscriber(['content' => 'html', 'customHtml' => 'html']));
$builder->addEventSubscriber(new FormExitSubscriber('notification.notification', $options));
$builder->add(
'name',
TextType::class,
[
'label' => 'mautic.notification.form.internal.name',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]
);
$builder->add(
'description',
TextareaType::class,
[
'label' => 'mautic.notification.form.internal.description',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'required' => false,
]
);
$builder->add(
'utmTags',
EmailUtmTagsType::class,
[
'label' => 'mautic.email.utm_tags',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.email.utm_tags.tooltip',
],
'required' => false,
]
);
$builder->add(
'heading',
TextType::class,
[
'label' => 'mautic.notification.form.heading',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'required' => true,
]
);
$builder->add(
'message',
TextareaType::class,
[
'label' => 'mautic.notification.form.message',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'rows' => 6,
],
'required' => true,
]
);
$builder->add(
'url',
UrlType::class,
[
'label' => 'mautic.notification.form.url',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.form.url.tooltip',
],
'required' => false,
]
);
$builder->add(
'button',
TextType::class,
[
'label' => 'mautic.notification.form.button',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.notification.form.button.tooltip',
],
'required' => false,
]
);
$builder->add('isPublished', YesNoButtonGroupType::class);
$builder->add('publishUp', PublishUpDateType::class);
$builder->add('publishDown', PublishDownDateType::class);
// add category
$builder->add(
'category',
CategoryListType::class,
[
'bundle' => 'notification',
]
);
$builder->add(
'language',
LocaleType::class,
[
'label' => 'mautic.core.language',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'required' => false,
]
);
$builder->add('buttons', FormButtonsType::class);
if (!empty($options['update_select'])) {
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
]
);
$builder->add(
'updateSelect',
HiddenType::class,
[
'data' => $options['update_select'],
'mapped' => false,
]
);
} else {
$builder->add(
'buttons',
FormButtonsType::class
);
}
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'data_class' => Notification::class,
]
);
$resolver->setDefined(['update_select']);
}
}

View File

@@ -0,0 +1,193 @@
<?php
namespace Mautic\NotificationBundle\Helper;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Twig\Helper\AssetsHelper;
use Mautic\LeadBundle\Entity\DoNotContact;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class NotificationHelper
{
public function __construct(
protected EntityManager $em,
protected AssetsHelper $assetsHelper,
protected CoreParametersHelper $coreParametersHelper,
protected IntegrationHelper $integrationHelper,
protected Router $router,
protected RequestStack $requestStack,
private \Mautic\LeadBundle\Model\DoNotContact $doNotContact,
) {
}
/**
* @param string $notification
*
* @return bool
*/
public function unsubscribe($notification)
{
/** @var \Mautic\LeadBundle\Entity\LeadRepository $repo */
$repo = $this->em->getRepository(\Mautic\LeadBundle\Entity\Lead::class);
$lead = $repo->getLeadByEmail($notification);
return $this->doNotContact->addDncForContact($lead->getId(), 'notification', DoNotContact::UNSUBSCRIBED);
}
public function getHeaderScript()
{
if ($this->hasScript()) {
return 'MauticJS.insertScript(\'https://cdn.onesignal.com/sdks/OneSignalSDK.js\');
var OneSignal = OneSignal || [];';
}
}
public function getScript()
{
if ($this->hasScript()) {
$integration = $this->integrationHelper->getIntegrationObject('OneSignal');
if (!$integration || false === $integration->getIntegrationSettings()->getIsPublished()) {
return;
}
$settings = $integration->getIntegrationSettings();
$keys = $integration->getDecryptedApiKeys();
$supported = $settings->getSupportedFeatures();
$featureSettings = $settings->getFeatureSettings();
$appId = $keys['app_id'];
$safariWebId = $keys['safari_web_id'];
$welcomenotificationEnabled = in_array('welcome_notification_enabled', $supported);
$notificationSubdomainName = $featureSettings['subdomain_name'];
$leadAssociationUrl = $this->router->generate(
'mautic_subscribe_notification',
[],
UrlGeneratorInterface::ABSOLUTE_URL
);
$welcomenotificationText = '';
if (!$welcomenotificationEnabled) {
$welcomenotificationText = 'welcomeNotification: { "disable": true },';
}
$server = $this->requestStack->getCurrentRequest()->server;
$https = ('https' == parse_url($server->get('HTTP_REFERER'), PHP_URL_SCHEME)) ? true : false;
$subdomainName = '';
if (!$https && $notificationSubdomainName) {
$subdomainName = 'subdomainName: "'.$notificationSubdomainName.'",
httpPermissionRequest: {
enable: true,
useCustomModal: true
},';
}
$oneSignalInit = <<<JS
var scrpt = document.createElement('link');
scrpt.rel ='manifest';
scrpt.href ='/manifest.json';
var head = document.getElementsByTagName('head')[0];
head.appendChild(scrpt);
OneSignal.push(["init", {
appId: "{$appId}",
safari_web_id: "{$safariWebId}",
autoRegister: true,
{$welcomenotificationText}
{$subdomainName}
notifyButton: {
enable: false // Set to false to hide
}
}]);
var postUserIdToMautic = function(userId) {
var data = [];
data['osid'] = userId;
MauticJS.makeCORSRequest('GET', '{$leadAssociationUrl}', data);
};
OneSignal.push(function() {
OneSignal.getUserId(function(userId) {
if (! userId) {
OneSignal.on('subscriptionChange', function(isSubscribed) {
if (isSubscribed) {
OneSignal.getUserId(function(newUserId) {
postUserIdToMautic(newUserId);
});
}
});
} else {
postUserIdToMautic(userId);
}
});
// Just to be sure we've grabbed the ID
window.onbeforeunload = function() {
OneSignal.getUserId(function(userId) {
if (userId) {
postUserIdToMautic(userId);
}
});
};
});
JS;
if (!$https && $notificationSubdomainName) {
$oneSignalInit .= <<<'JS'
OneSignal.push(function() {
OneSignal.on('notificationPermissionChange', function(permissionChange) {
if(currentPermission == 'granted'){
setTimeout(function(){
OneSignal.registerForPushNotifications({httpPermissionRequest: true});
}, 100);
}
});
});
JS;
}
return $oneSignalInit;
}
}
private function hasScript(): bool
{
$landingPage = true;
$server = $this->requestStack->getCurrentRequest()->server;
$cookies = $this->requestStack->getCurrentRequest()->cookies;
// already exist
if ($cookies->get('mtc_osid')) {
return false;
}
if (!str_contains($server->get('HTTP_REFERER'), $this->coreParametersHelper->get('site_url'))) {
$landingPage = false;
}
$integration = $this->integrationHelper->getIntegrationObject('OneSignal');
if (!$integration || false === $integration->getIntegrationSettings()->getIsPublished()) {
return false;
}
$supportedFeatures = $integration->getIntegrationSettings()->getSupportedFeatures();
// disable on Landing pages
if (true === $landingPage && !in_array('landing_page_enabled', $supportedFeatures)) {
return false;
}
// disable on Landing pages
if (false === $landingPage && !in_array('tracking_page_enabled', $supportedFeatures)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace Mautic\NotificationBundle\Integration;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilder;
class OneSignalIntegration extends AbstractIntegration
{
protected bool $coreIntegration = true;
public function getName(): string
{
return 'OneSignal';
}
public function getIcon(): string
{
return 'app/bundles/NotificationBundle/Assets/img/OneSignal.png';
}
public function getSupportedFeatures(): array
{
return [
'mobile',
'landing_page_enabled',
'welcome_notification_enabled',
'tracking_page_enabled',
];
}
public function getSupportedFeatureTooltips(): array
{
return [
'landing_page_enabled' => 'mautic.integration.form.features.landing_page_enabled.tooltip',
'tracking_page_enabled' => 'mautic.integration.form.features.tracking_page_enabled.tooltip',
];
}
/**
* @return array<string, string>
*/
public function getRequiredKeyFields(): array
{
return [
'app_id' => 'mautic.notification.config.form.notification.app_id',
'safari_web_id' => 'mautic.notification.config.form.notification.safari_web_id',
'rest_api_key' => 'mautic.notification.config.form.notification.rest_api_key',
'gcm_sender_id' => 'mautic.notification.config.form.notification.gcm_sender_id',
];
}
public function getAuthenticationType(): string
{
return 'none';
}
/**
* @param \Mautic\PluginBundle\Integration\Form|FormBuilder $builder
* @param array $data
* @param string $formArea
*/
public function appendToForm(&$builder, $data, $formArea): void
{
if ('features' == $formArea) {
/* @var FormBuilder $builder */
$builder->add(
'subdomain_name',
TextType::class,
[
'label' => 'mautic.notification.form.subdomain_name.label',
'required' => false,
'attr' => [
'class' => 'form-control',
],
]
);
$builder->add(
'platforms',
ChoiceType::class,
[
'choices' => [
'mautic.integration.form.platforms.ios' => 'ios',
'mautic.integration.form.platforms.android' => 'android',
],
'attr' => [
'tooltip' => 'mautic.integration.form.platforms.tooltip',
'data-show-on' => '{"integration_details_supportedFeatures_0":"checked"}',
],
'expanded' => true,
'multiple' => true,
'label' => 'mautic.integration.form.platforms',
'placeholder' => false,
'required' => false,
]
);
}
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Mautic\NotificationBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class MauticNotificationBundle extends Bundle
{
}

View File

@@ -0,0 +1,328 @@
<?php
namespace Mautic\NotificationBundle\Model;
use Doctrine\DBAL\Query\QueryBuilder;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
use Mautic\CoreBundle\Helper\Chart\LineChart;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Model\AjaxLookupModelInterface;
use Mautic\CoreBundle\Model\FormModel;
use Mautic\CoreBundle\Model\GlobalSearchInterface;
use Mautic\CoreBundle\Model\TranslationModelTrait;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\NotificationBundle\Entity\Stat;
use Mautic\NotificationBundle\Event\NotificationEvent;
use Mautic\NotificationBundle\Form\Type\MobileNotificationType;
use Mautic\NotificationBundle\Form\Type\NotificationType;
use Mautic\NotificationBundle\NotificationEvents;
use Mautic\PageBundle\Model\TrackableModel;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* @extends FormModel<Notification>
*
* @implements AjaxLookupModelInterface<Notification>
*/
class NotificationModel extends FormModel implements AjaxLookupModelInterface, GlobalSearchInterface
{
use TranslationModelTrait;
public function __construct(
protected TrackableModel $pageTrackableModel,
EntityManager $em,
CorePermissions $security,
EventDispatcherInterface $dispatcher,
UrlGeneratorInterface $router,
Translator $translator,
UserHelper $userHelper,
LoggerInterface $mauticLogger,
CoreParametersHelper $coreParametersHelper,
) {
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
}
/**
* @return \Mautic\NotificationBundle\Entity\NotificationRepository
*/
public function getRepository()
{
return $this->em->getRepository(Notification::class);
}
/**
* @return \Mautic\NotificationBundle\Entity\StatRepository
*/
public function getStatRepository()
{
return $this->em->getRepository(Stat::class);
}
public function getPermissionBase(): string
{
return 'notification:notifications';
}
public function saveEntities($entities, $unlock = true): void
{
// iterate over the results so the events are dispatched on each delete
$batchSize = 20;
$i = 0;
foreach ($entities as $entity) {
$isNew = ($entity->getId()) ? false : true;
// set some defaults
$this->setTimestamps($entity, $isNew, $unlock);
if ($dispatchEvent = $entity instanceof Notification) {
$event = $this->dispatchEvent('pre_save', $entity, $isNew);
}
$this->getRepository()->saveEntity($entity, false);
if ($dispatchEvent) {
$this->dispatchEvent('post_save', $entity, $isNew, $event);
}
if (0 === ++$i % $batchSize) {
$this->em->flush();
}
}
$this->em->flush();
}
/**
* @param Notification|null $entity
* @param string|null $action
* @param array $options
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* @throws MethodNotAllowedHttpException
*/
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): FormInterface
{
if (!$entity instanceof Notification) {
throw new MethodNotAllowedHttpException(['Notification']);
}
if (!empty($action)) {
$options['action'] = $action;
}
$type = str_contains($action, 'mobile_') ? MobileNotificationType::class : NotificationType::class;
return $formFactory->create($type, $entity, $options);
}
/**
* Get a specific entity or generate a new one if id is empty.
*/
public function getEntity($id = null): ?Notification
{
if (null === $id) {
$entity = new Notification();
} else {
$entity = parent::getEntity($id);
}
return $entity;
}
public function saveEntity($entity, $unlock = true): void
{
parent::saveEntity($entity, $unlock);
$this->postTranslationEntitySave($entity);
}
/**
* @param string $source
* @param int $sourceId
*/
public function createStatEntry(Notification $notification, Lead $lead, $source = null, $sourceId = null): void
{
$stat = new Stat();
$stat->setDateSent(new \DateTime());
$stat->setLead($lead);
$stat->setNotification($notification);
$stat->setSource($source);
$stat->setSourceId($sourceId);
$this->getStatRepository()->saveEntity($stat);
}
/**
* @throws MethodNotAllowedHttpException
*/
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
{
if (!$entity instanceof Notification) {
throw new MethodNotAllowedHttpException(['Notification']);
}
switch ($action) {
case 'pre_save':
$name = NotificationEvents::NOTIFICATION_PRE_SAVE;
break;
case 'post_save':
$name = NotificationEvents::NOTIFICATION_POST_SAVE;
break;
case 'pre_delete':
$name = NotificationEvents::NOTIFICATION_PRE_DELETE;
break;
case 'post_delete':
$name = NotificationEvents::NOTIFICATION_POST_DELETE;
break;
default:
return null;
}
if ($this->dispatcher->hasListeners($name)) {
if (empty($event)) {
$event = new NotificationEvent($entity, $isNew);
$event->setEntityManager($this->em);
}
$this->dispatcher->dispatch($event, $name);
return $event;
}
return null;
}
/**
* Joins the page table and limits created_by to currently logged in user.
*/
public function limitQueryToCreator(QueryBuilder &$q): void
{
$q->join('t', MAUTIC_TABLE_PREFIX.'push_notifications', 'p', 'p.id = t.notification_id')
->andWhere('p.created_by = :userId')
->setParameter('userId', $this->userHelper->getUser()->getId());
}
/**
* Get line chart data of hits.
*
* @param char $unit {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters}
* @param string $dateFormat
* @param array $filter
* @param bool $canViewOthers
*/
public function getHitsLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true): array
{
$flag = null;
if (isset($filter['flag'])) {
$flag = $filter['flag'];
unset($filter['flag']);
}
$chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
if (!$flag || 'total_and_unique' === $flag) {
$q = $query->prepareTimeDataQuery('push_notification_stats', 'date_sent', $filter);
if (!$canViewOthers) {
$this->limitQueryToCreator($q);
}
$data = $query->loadAndBuildTimeData($q);
$chart->setDataset($this->translator->trans('mautic.notification.show.total.sent'), $data);
}
return $chart->render();
}
/**
* @return Stat
*/
public function getNotificationStatus($idHash)
{
return $this->getStatRepository()->getNotificationStatus($idHash);
}
/**
* Search for an notification stat by notification and lead IDs.
*
* @return array
*/
public function getNotificationStatByLeadId($notificationId, $leadId)
{
return $this->getStatRepository()->findBy(
[
'notification' => (int) $notificationId,
'lead' => (int) $leadId,
],
['dateSent' => 'DESC']
);
}
/**
* Get an array of tracked links.
*/
public function getNotificationClickStats($notificationId): array
{
return $this->pageTrackableModel->getTrackableList('notification', $notificationId);
}
/**
* @param string $filter
* @param int $limit
* @param int $start
* @param array $options
*/
public function getLookupResults($type, $filter = '', $limit = 10, $start = 0, $options = []): array
{
$results = [];
switch ($type) {
case 'notification':
$entities = $this->getRepository()->getNotificationList(
$filter,
$limit,
$start,
$this->security->isGranted($this->getPermissionBase().':viewother'),
$options['notification_type'] ?? null
);
foreach ($entities as $entity) {
$results[$entity['language']][$entity['id']] = $entity['name'];
}
// sort by language
ksort($results);
break;
case 'mobile_notification':
$entities = $this->getRepository()->getMobileNotificationList(
$filter,
$limit,
$start,
$this->security->isGranted($this->getPermissionBase().':viewother'),
$options
);
foreach ($entities as $entity) {
$results[$entity['language']][$entity['id']] = $entity['name'];
}
// sort by language
ksort($results);
break;
}
return $results;
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Mautic\NotificationBundle;
/**
* Events available for NotificationBundle.
*/
final class NotificationEvents
{
/**
* The mautic.notification_token_replacement event is thrown right before the content is returned.
*
* The event listener receives a
* Mautic\CoreBundle\Event\TokenReplacementEvent instance.
*
* @var string
*/
public const TOKEN_REPLACEMENT = 'mautic.notification_token_replacement';
/**
* The mautic.notification_form_action_send event is thrown when a notification is sent
* as part of a form action.
*
* The event listener receives a
* Mautic\NotificationBundle\Event\SendingNotificationEvent instance.
*
* @var string
*/
public const NOTIFICATION_ON_FORM_ACTION_SEND = 'mautic.notification_form_action_send';
/**
* The mautic.notification_on_send event is thrown when a notification is sent.
*
* The event listener receives a
* Mautic\NotificationBundle\Event\NotificationSendEvent instance.
*
* @var string
*/
public const NOTIFICATION_ON_SEND = 'mautic.notification_on_send';
/**
* The mautic.notification_pre_save event is thrown right before a notification is persisted.
*
* The event listener receives a
* Mautic\NotificationBundle\Event\NotificationEvent instance.
*
* @var string
*/
public const NOTIFICATION_PRE_SAVE = 'mautic.notification_pre_save';
/**
* The mautic.notification_post_save event is thrown right after a notification is persisted.
*
* The event listener receives a
* Mautic\NotificationBundle\Event\NotificationEvent instance.
*
* @var string
*/
public const NOTIFICATION_POST_SAVE = 'mautic.notification_post_save';
/**
* The mautic.notification_pre_delete event is thrown prior to when a notification is deleted.
*
* The event listener receives a
* Mautic\NotificationBundle\Event\NotificationEvent instance.
*
* @var string
*/
public const NOTIFICATION_PRE_DELETE = 'mautic.notification_pre_delete';
/**
* The mautic.notification_post_delete event is thrown after a notification is deleted.
*
* The event listener receives a
* Mautic\NotificationBundle\Event\NotificationEvent instance.
*
* @var string
*/
public const NOTIFICATION_POST_DELETE = 'mautic.notification_post_delete';
/**
* The mautic.notification.on_batch_trigger_action event is fired when the campaign action triggers.
*
* The event listener receives a
* Mautic\CampaignBundle\Event\PendingEvent
*
* @var string
*/
public const ON_CAMPAIGN_BATCH_ACTION = 'mautic.notification.on_batch_trigger_action';
/**
* The mautic.notification.on_campaign_trigger_condition event is fired when the campaign condition triggers.
*
* The event listener receives a
* Mautic\CampaignBundle\Event\CampaignExecutionEvent
*
* @var string
*/
public const ON_CAMPAIGN_TRIGGER_CONDITION = 'mautic.notification.on_campaign_trigger_notification';
}

View File

@@ -0,0 +1,45 @@
{#
Form Theme is used at "Configuration" > "Notification Settings"
Variables
- form
#}
{% block _config_notification_config_widget %}
<h4 class="fw-sb mt-48 mb-xs">{{ 'mautic.config.tab.campaign_notification_config'|trans }}</h4>
<div class="text-muted small pb-md">{{ 'mautic.core.config.header.campaign_notification_config.description'|trans }}</div>
<div class="row">
<div class="panel panel-default mb-md">
<div class="panel-body">
<div class="row">
<div class="col-xs-12">
{{ form_row(form.campaign_send_notification_to_author) }}
</div>
</div>
<div class="row">
<div class="col-xs-12">
{{ form_row(form.campaign_notification_email_addresses) }}
</div>
</div>
</div>
</div>
</div>
<h4 class="fw-sb mt-48 mb-xs">{{ 'mautic.config.tab.webhook_notification_config'|trans }}</h4>
<div class="text-muted small pb-md">{{ 'mautic.core.config.header.webhook_notification_config.description'|trans }}</div>
<div class="row">
<div class="panel panel-default mb-md">
<div class="panel-body">
<div class="row">
<div class="col-xs-12">
{{ form_row(form.webhook_send_notification_to_author) }}
</div>
</div>
<div class="row">
<div class="col-xs-12">
{{ form_row(form.webhook_notification_email_addresses) }}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,47 @@
{% block _mobile_notification_mobileSettings_widget %}
{% if 'ios' in integrationSettings.platforms %}
<div class="tab-pane fade in bdr-w-0" id="ios-notification-container">
<div class="row">
<div class="col-md-6">
{{ form_row(form.ios_subtitle) }}
{{ form_row(form.ios_media) }}
</div>
<div class="col-md-6">
<div class="well">
<h4>{{ 'Advanced Settings'|trans }}</h4>
<hr />
{{ form_row(form.ios_badges) }}
{{ form_row(form.ios_badgeCount) }}
{{ form_row(form.ios_sound) }}
{{ form_label(form.ios_contentAvailable) }}
{{ form_row(form.ios_contentAvailable) }}
{{ form_label(form.ios_mutableContent) }}
{{ form_row(form.ios_mutableContent) }}
</div>
</div>
</div>
</div>
{% endif %}
{% if 'android' in integrationSettings.platforms %}
<div class="tab-pane fade in bdr-w-0" id="android-notification-container">
<div class="row">
<div class="col-md-6">
{{ form_row(form.android_small_icon) }}
{{ form_row(form.android_large_icon) }}
{{ form_row(form.android_big_picture) }}
</div>
<div class="col-md-6">
<div class="well">
<h4>{{ 'Advanced Settings'|trans }}</h4>
<hr />
{{ form_row(form.android_sound) }}
{{ form_row(form.android_group_key) }}
{{ form_row(form.android_led_color) }}
{{ form_row(form.android_accent_color) }}
{{ form_row(form.android_lockscreen_visibility) }}
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% block _notificationsend_list_row %}
<div class="row">
<div class="col-xs-8">
{{ form_row(form.notification) }}
</div>
<div class="col-xs-4 mt-lg">
<div class="mt-3">
{{ form_row(form.newNotificationButton) }}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,122 @@
{#
Variables
- searchValue
- items
- totalItems
- page
- limit
- tmpl
- permissions
- model
- security
#}
{% if items|length > 0 %}
<div class="table-responsive">
<table class="table table-hover notification-list">
<thead>
<tr>
{{ include('@MauticCore/Helper/tableheader.html.twig', {
'checkall': 'true',
}) }}
{{ include('@MauticCore/Helper/tableheader.html.twig', {
'sessionVar': 'mobile_notification',
'orderBy': 'e.name',
'text': 'mautic.core.name',
'class': 'col-notification-name',
'default': true,
}) }}
{{ include('@MauticCore/Helper/tableheader.html.twig', {
'sessionVar': 'mobile_notification',
'orderBy': 'c.title',
'text': 'mautic.core.category',
'class': 'visible-md visible-lg col-notification-category',
}) }}
<th class="visible-sm visible-md visible-lg col-notification-stats">{{ 'mautic.core.stats'|trans }}</th>
{{ include('@MauticCore/Helper/tableheader.html.twig', {
'sessionVar': 'mobile_notification',
'orderBy': 'e.id',
'text': 'mautic.core.id',
'class': 'visible-md visible-lg col-notification-id',
}) }}
</tr>
</thead>
<tbody>
{# @var \Mautic\NotificationBundle\Entity\Notification $item #}
{% for item in items %}
{% set type = item.notificationType %}
<tr>
<td>
{{ include('@MauticCore/Helper/list_actions.html.twig', {
'item': item,
'templateButtons': {
'edit': securityHasEntityAccess(permissions['notification:mobile_notifications:editown'], permissions['notification:mobile_notifications:editother'], item.createdBy),
'delete': securityHasEntityAccess(permissions['notification:mobile_notifications:deleteown'], permissions['notification:mobile_notifications:deleteother'], item.createdBy),
},
'routeBase': 'mobile_notification',
'customButtons': [
{
'attr': {
'data-toggle': 'ajaxmodal',
'data-target': '#MauticSharedModal',
'data-header': 'mautic.notification.mobile_notification.preview'|trans,
'data-footer': 'false',
'href': path('mautic_mobile_notification_action', {'objectId': item.id, 'objectAction': 'preview'}),
},
'btnText': 'mautic.notification.mobile_notification.preview'|trans,
'iconClass': 'ri-share-forward-box-fill',
},
]
}) }}
</td>
<td>
<div>
{% if 'template' == type %}
{{ include('@MauticCore/Helper/publishstatus_icon.html.twig', {'item': item, 'model': 'notification'}) }}
{% else %}
<i class="ri-fw ri-lg ri-toggle-fill text-secondary disabled"></i>
{% endif %}
<a href="{{ path('mautic_mobile_notification_action', {'objectAction': 'view', 'objectId': item.id}) }}" data-toggle="ajax">
{{ item.name }}
{% if 'list' == type %}
<span data-toggle="tooltip" title="{{ 'mautic.notification.icon_tooltip.list_notification'|trans }}"><i class="ri-fw ri-list-check"></i></span>
{% endif %}
{% if item.isTranslation() %}
<span data-toggle="tooltip" title="{{ 'mautic.core.icon_tooltip.translation'|trans }}"><i class="ri-fw ri-translate fs-14"></i></span>
{% endif %}
</a>
</div>
</td>
<td class="visible-md visible-lg">
{{ include('@MauticCore/Modules/category--expanded.html.twig', {'category': item.category}) }}
</td>
<td class="visible-sm visible-md visible-lg col-stats">
<span class="mt-xs label label-green"
data-toggle="tooltip"
title="{{ 'mautic.channel.stat.leadcount.tooltip'|trans }}">
<a href="{{ path('mautic_contact_index', {'search': 'mautic.lead.lead.searchcommand.mobile_sent'|trans ~ ':' ~ item.id}) }}">
{{- 'mautic.notification.stat.sentcount'|trans({'%count%': item.getSentCount(true)}) -}}
</a>
</span>
</td>
<td class="visible-md visible-lg">{{ item.id }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="panel-footer">
{{ include('@MauticCore/Helper/pagination.html.twig', {
'totalItems': totalItems,
'page': page,
'limit': limit,
'baseUrl': path('mautic_mobile_notification_index'),
'sessionVar': 'mobile_notification',
}) }}
</div>
{% else %}
{{ include('@MauticCore/Helper/noresults.html.twig') }}
{% endif %}

View File

@@ -0,0 +1,198 @@
{#
Variables
- notification
- trackables
- logs
- permissions
- security
- entityViews
- contacts
- dateRangeForm
#}
{% extends '@MauticCore/Default/content.html.twig' %}
{% block mauticContent %}notification{% endblock %}
{% block headerTitle %}{{ notification.name }}{% endblock %}
{% block preHeader %}
{{- include('@MauticCore/Helper/page_actions.html.twig',
{
'item' : notification,
'templateButtons' : {
'close' : securityHasEntityAccess(permissions['notification:mobile_notifications:viewown'], permissions['notification:mobile_notifications:viewother'], notification.createdBy),
},
'routeBase' : 'mobile_notification',
'targetLabel' : 'mautic.notification.mobile_notifications'|trans
}
) -}}
{{ include('@MauticCore/Modules/category--inline.html.twig', {'category': notification.category}) }}
{% endblock %}
{% block actions %}
{{- include('@MauticCore/Helper/page_actions.html.twig', {
'item': notification,
'templateButtons': {
'edit': securityHasEntityAccess(permissions['notification:mobile_notifications:editown'], permissions['notification:mobile_notifications:editother'], notification.createdBy),
'delete': permissions['notification:mobile_notifications:create'],
},
'routeBase': 'mobile_notification',
}) -}}
{% endblock %}
{% block publishStatus %}
{{ include('@MauticCore/Helper/publishstatus_badge.html.twig', {'entity': notification}, with_context=false) }}
{% set tags = [] %}
{# Translation tags #}
{% set tags = tags
|merge(notification.isTranslation and not notification.isTranslation(true)
? [{ color: 'warm-gray', label: 'mautic.core.icon_tooltip.translation' }]
: [])
|merge(notification.isTranslation(true)
? [{ color: 'warm-gray', label: 'mautic.core.translation_of'|trans({'%parent%' : translations.parent.name}), icon: 'ri-translate' }]
: [])
%}
{% include '@MauticCore/Helper/_tag.html.twig' with { tags: tags } %}
{% endblock %}
{% set translationContent = include('@MauticCore/Translation/index.html.twig',
{
'activeEntity' : notification,
'translations' : translations,
'model' : 'notifications',
'actionRoute' : 'mautic_mobile_notification_action',
}
) %}
{% set showTranslations = translationContent|trim is not empty %}
{% block content %}
<!-- start: box layout -->
<div class="box-layout">
<!-- left section -->
<div class="col-lg-9 col-md-8 height-auto">
<div>
<!-- notification detail collapseable -->
<div class="collapse pr-md pl-md" id="notification-details">
<div class="pr-md pl-md pb-md">
<div class="panel shd-none mb-0">
<table class="table table-hover mb-0">
<tbody>
{{ include('@MauticCore/Helper/details.html.twig', {'entity': notification}, with_context=false) }}
</tbody>
</table>
</div>
</div>
</div>
<!--/ notification detail collapseable -->
<!-- notification detail collapseable toggler -->
<div class="hr-expand nm">
<span data-toggle="tooltip" title="{{ 'mautic.core.details'|trans }}">
<a href="javascript:void(0)" class="arrow text-secondary collapsed" data-toggle="collapse" data-target="#notification-details"><span class="caret"></span> {{ 'mautic.core.details'|trans }}</a>
</span>
</div>
<!--/ notification detail collapseable toggler -->
<!-- some stats -->
<div class="pa-md">
<div class="row">
<div class="col-sm-12">
{% if security.isGranted('lead:leads:viewown') %}
{{ include('@MauticCore/Modules/stat--icon.html.twig', {'stats': [
{
'title': 'mautic.lead.lead.contacts.mobile_sent',
'value': notification.getSentCount(true),
'link': path('mautic_contact_index', {
'search': ('mautic.lead.lead.searchcommand.mobile_sent'|trans) ~ ':' ~ notification.id
}),
'icon': 'ri-smartphone-line'
}
]}) }}
{% endif %}
<div class="panel">
<div class="panel-body box-layout">
<div class="col-md-3 va-m">
<h5 class="text-white dark-md fw-sb mb-xs">
<span class="ri-line-chart-fill"></span>
{{ 'mautic.core.stats'|trans }}
</h5>
</div>
<div class="col-md-9 va-m">
{{ include('@MauticCore/Helper/graph_dateselect.html.twig', {'dateRangeForm': dateRangeForm, 'class': 'pull-right'}) }}
</div>
</div>
<div class="d-flex fd-column pt-0 pl-15 pb-15 pr-15 min-h-256">
{{ include('@MauticCore/Helper/chart.html.twig', {'chartData': entityViews, 'chartType': 'line', 'chartHeight': 300}) }}
</div>
</div>
</div>
</div>
</div>
<!--/ stats -->
<!-- tabs controls -->
<ul class="nav nav-tabs nav-tabs-contained">
<li class="active">
<a href="#clicks-container" role="tab" data-toggle="tab">
{{ 'mautic.trackable.click_counts'|trans }}
</a>
</li>
<li class="">
<a href="#contacts-container" role="tab" data-toggle="tab">
{{ 'mautic.lead.leads'|trans }}
</a>
</li>
{% if showTranslations %}
<li>
<a href="#translation-container" role="tab" data-toggle="tab">
{{ 'mautic.core.translations'|trans }}
</a>
</li>
{% endif %}
</ul>
<!--/ tabs controls -->
</div>
<!-- start: tab-content -->
<div class="tab-content pa-md">
<div class="tab-pane active bdr-w-0" id="clicks-container">
{{ include('@MauticPage/Trackable/click_counts.html.twig', {
'trackables': trackables,
'entity': notification,
'channel': 'notification',
}) }}
</div>
<div class="tab-pane fade in bdr-w-0 page-list" id="contacts-container">
{{ contacts|raw }}
</div>
{% if showTranslations %}
<div class="tab-pane bdr-w-0" id="translation-container">
{{ translationContent|raw }}
</div>
{% endif %}
</div>
<!--/ tab-content -->
</div>
<!--/ left section -->
<!-- right section -->
<div class="col-lg-3 col-md-4 height-auto">
<div class="pa-md mb-32 animation--slide-in-down">
{{ include('@MauticNotification/MobileNotification/preview.html.twig') }}
</div>
<!-- activity feed -->
{{ include('@MauticCore/Helper/recentactivity.html.twig', {'logs': logs}, with_context=false) }}
</div>
<!--/ right section -->
<input name="entityId" id="entityId" type="hidden" value="{{ notification.id|e }}" />
</div>
<!--/ end: box layout -->
{% endblock %}

View File

@@ -0,0 +1,87 @@
{#
Variables
- form
- notification
- integration (\Mautic\NotificationBundle\Integration\OneSignalIntegration)
- forceTypeSelection (optional, bool, default = false)
Only defined when editing notification
#}
{% form_theme form '@MauticNotification/FormTheme/MobileNotification/_mobile_notification_mobileSettings_widget.html.twig' %}
{% extends '@MauticCore/Default/content.html.twig' %}
{% block mauticContent %}mobile_notification{% endblock %}
{% block headerTitle %}
{% if notification.id %}
{{ 'mautic.notification.mobile.header.edit'|trans({'%name%': notification.name}) }}
{% else %}
{{ 'mautic.notification.mobile.header.new'|trans }}
{% endif %}
{% endblock %}
{% block content %}
{% set integrationSettings = integration.integrationSettings.featureSettings %}
{{ form_start(form) }}
<div class="box-layout">
<div class="col-md-9 height-auto">
<div class="row">
<div class="col-xs-12">
<!-- tabs controls -->
<ul class="nav nav-tabs nav-tabs-contained">
<li class="active"><a href="#notification-container" role="tab" data-toggle="tab">{{ 'mautic.core.details'|trans }}</a></li>
<li><a href="#data-notification-container" role="tab" data-toggle="tab">{{ 'mautic.notification.tab.data'|trans }}</a></li>
{% if integrationSettings.platforms is defined and 'ios' in integrationSettings.platforms %}
<li><a href="#ios-notification-container" role="tab" data-toggle="tab">{{ 'mautic.notification.tab.ios'|trans }}</a></li>
{% endif %}
{% if integrationSettings.platforms is defined and 'android' in integrationSettings.platforms %}
<li><a href="#android-notification-container" role="tab" data-toggle="tab">{{ 'mautic.notification.tab.android'|trans }}</a></li>
{% endif %}
</ul>
<!--/ tabs controls -->
<!-- tabs content -->
<div class="tab-content pa-md">
<div class="tab-pane fade in active bdr-w-0" id="notification-container">
<div class="row">
<div class="col-md-6">
{{- form_row(form.name) -}}
{{- form_row(form.heading) -}}
{{- form_row(form.url) -}}
</div>
<div class="col-md-6">{{ form_row(form.message) }}</div>
</div>
</div>
<div class="tab-pane fade in bdr-w-0" id="data-notification-container">
<div class="row">
<div class="col-md-6">{{ form_row(form.mobileSettings.additional_data) }}</div>
</div>
</div>
{% if form.mobileSettings.ios_sound is defined or form.mobileSettings.android_sound is defined %}
{{ form_widget(form.mobileSettings, {'integrationSettings': integrationSettings}) }}
{% endif %}
</div>
<!--/ tabs content -->
</div>
</div>
</div>
<div class="col-md-3 height-auto bdr-l">
<div class="pr-lg pl-lg pt-md pb-md">
{{ form_row(form.category) }}
{{ form_row(form.language) }}
{{ form_row(form.translationParentSelector) }}
<hr />
{% include '@MauticCore/FormTheme/Fields/_utm_tags_fields.html.twig' %}
<div class="hide">
{{ form_row(form.isPublished) }}
{{ form_row(form.publishUp) }}
{{ form_row(form.publishDown) }}
{{ form_rest(form) }}
</div>
</div>
</div>
</div>
{{ form_end(form) }}
{% endblock %}

View File

@@ -0,0 +1,99 @@
{#
Variables
- searchValue
- items
- totalItems
- page
- limit
- tmpl
- permissions
- model
- security
#}
{% set isIndex = 'index' == tmpl ? true : false %}
{% set tmpl = 'list' %}
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
{% block mauticContent %}mobile_notification{% endblock %}
{% block headerTitle %}{{ 'mautic.notification.mobile_notifications'|trans }}{% endblock %}
{% block content %}
{% if isIndex %}
<div id="page-list-wrapper" class="{% if items|length > 0 or searchValue is not empty %}panel {% endif %}panel-default">
{{ include('@MauticCore/Helper/list_toolbar.html.twig', {
'searchValue': searchValue,
'searchId': 'mobile-notification-search',
'action': currentRoute,
'page_actions': {
'templateButtons': {
'new': permissions['notification:mobile_notifications:create'],
},
'routeBase': 'mobile_notification',
},
'bulk_actions': {
'routeBase': 'mobile_notification',
'templateButtons': {
'delete': permissions['notification:mobile_notifications:deleteown'] or permissions['notification:mobile_notifications:deleteother'],
},
},
'quickFilters': [
{
'search': 'mautic.core.searchcommand.ispublished',
'label': 'mautic.core.form.available',
'tooltip': 'mautic.core.searchcommand.ispublished.description',
'icon': 'ri-check-line'
},
{
'search': 'mautic.core.searchcommand.isunpublished',
'label': 'mautic.core.form.unavailable',
'tooltip': 'mautic.core.searchcommand.isunpublished.description',
'icon': 'ri-close-line'
},
{
'search': 'mautic.core.searchcommand.isuncategorized',
'label': 'mautic.core.form.uncategorized',
'tooltip': 'mautic.core.searchcommand.isuncategorized.description',
'icon': 'ri-folder-unknow-line'
},
{
'search': 'mautic.core.searchcommand.ismine',
'label': 'mautic.core.searchcommand.ismine.label',
'tooltip': 'mautic.core.searchcommand.ismine.description',
'icon': 'ri-user-line'
}
]
}) }}
<div class="page-list">
{% endif %}
{% if items|length > 0 %}
{{ include('@MauticNotification/MobileNotification/_list.html.twig') }}
{% else %}
{% if searchValue is not empty %}
{{- include('@MauticCore/Helper/noresults.html.twig', {'tip' : 'mautic.category.noresults.tip'}) -}}
{% else %}
<div class="mt-80 col-md-offset-2 col-lg-offset-3 col-md-8 col-lg-5 height-auto">
{% set childContainer %}
<div class="mt-32 mb-md">
{% include '@MauticCore/Components/pictogram.html.twig' with {
'pictogram': 'mobile',
'size': '80'
} %}
</div>
{% endset %}
{{ include('@MauticCore/Components/content-block.html.twig', {
heading: 'mautic.notification.mobile.contentblock.heading',
subheading: 'mautic.notification.mobile.contentblock.subheading',
copy: 'mautic.notification.mobile.contentblock.copy',
childContainer: childContainer
}) }}
</div>
{% endif %}
{% endif %}
{% if isIndex %}
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,47 @@
{#
some stats: need more input on what type of form data to show.
delete if it is not require
#}
<div class="pa-md">
<div class="row">
<div class="col-sm-12">
{% if showVariants %}
<div class="text-right small">
<span>
{% if isVariant %}
<span data-chart="variant">{{ 'mautic.notification.variant.graph.variant'|trans }}</span>
{% else %}
<a data-chart="variant" href="javascript:void(0)">{{ 'mautic.notification.variant.graph.variant'|trans }}</a>
{% endif %}
</span>
</span> | </span>
<span>
{% if isVariant %}
<a data-chart="all" href="javascript:void(0)">{{ 'mautic.notification.variant.graph.all'|trans }}</a>
{% else %}
<span data-chart="all">{{ 'mautic.notification.variant.graph.all'|trans }}</span>
{% endif %}
</span>
</div>
{% endif %}
<div class="panel">
<div class="panel-body box-layout">
<div class="col-xs-4 va-m">
<h5 class="text-white dark-md fw-sb mb-xs">
<span class="ri-mail-line"></span>
{{ 'mautic.notification.lead.list.comparison'|trans }}
</h5>
</div>
<div class="col-xs-8 va-m" id="legend"></div>
</div>
<div class="pt-0 pl-15 pb-10 pr-15">
<div>
<canvas id="list-compare-chart" height="300"></canvas>
</div>
</div>
<div id="list-compare-chart-data" class="hide">{{ stats|json_encode|raw }}</div>
</div>
</div>
</div>
</div>
<!--/ some stats -->

View File

@@ -0,0 +1,50 @@
{#
Variables
- notification
#}
{% set notificationWrapper %}
<div class="notification-preview">
<div class="notification-preview__body animation--grow">
<div class="notification-preview__container">
<div class="notification-preview__notification">
<span class="notification-preview__before"></span>
<header class="notification-preview__header">
<div class="notification-preview__heading tt-u">{{ configGetParameter('brand_name')|default('') }}</div>
<span class="notification-preview__timestamp">{{ 'mautic.core.now'|trans }}</span>
</header>
<div class="notification-preview__content">
<span class="notification-preview__title fw-sb">{{ notification.heading }}</span>
{% if notification.mobileSettings.ios_subtitle is defined %}
<span class="notification-preview__subtitle fw-sb">{{ notification.mobileSettings.ios_subtitle }}</span>
{% endif %}
<span class="notification-preview__message">{{ notification.message }}</span>
<span class="notification-preview__more">{{ 'mautic.notification.preview.more_notifications'|trans }}</span>
</div>
</div>
</div>
</div>
<div class="notification-preview__body">
<div class="notification-preview__container">
<div class="notification-preview__notification">
<span class="notification-preview__before" style="top: -294.15px;"></span>
</div>
</div>
</div>
<div class="notification-preview__body">
<div class="notification-preview__container">
<div class="notification-preview__notification">
<span class="notification-preview__before" style="top: -310.625px;"></span>
</div>
</div>
</div>
</div>
{% endset %}
{% if notification.url %}
<a href="{{ notification.url }}" class="notification-wrapper" target="_blank">
{{ notificationWrapper }}
</a>
{% else %}
{{ notificationWrapper }}
{% endif %}

View File

@@ -0,0 +1,31 @@
{#
some stats: need more input on what type of form data to show.
delete if it is not require
#}
<div class="pa-md">
<div class="row">
<div class="col-sm-12">
<div class="panel">
<div class="panel-body box-layout">
<div class="col-xs-4 va-m">
<h5 class="text-white dark-md fw-sb mb-xs">
<span class="ri-mail-line"></span>
{{ 'mautic.notification.stats'|trans }}
</h5>
</div>
<div class="col-xs-6 va-m" id="legend"></div>
<div class="col-xs-2 va-m">
{{ include('@MauticCore/Helper/graph_dateselect.html.twig', {'callback': 'updateNotificationStatsChart'}) }}
</div>
</div>
<div class="pt-0 pl-15 pb-10 pr-15">
<div>
<canvas id="stat-chart" height="300"></canvas>
</div>
</div>
<div id="stat-chart-data" class="hide">{{ stats|json_encode|raw }}</div>
</div>
</div>
</div>
</div>
<!--/ some stats -->

View File

@@ -0,0 +1,120 @@
{#
Variables
- searchValue
- items
- totalItems
- page
- limit
- tmpl
- permissions
- model
- security
#}
{% if items|length > 0 %}
<div class="table-responsive">
<table class="table table-hover notification-list">
<thead>
<tr>
{{ include('@MauticCore/Helper/tableheader.html.twig', {
'checkall': 'true',
}) }}
{{ include('@MauticCore/Helper/tableheader.html.twig', {
'sessionVar': 'notification',
'orderBy': 'e.name',
'text': 'mautic.core.name',
'class': 'col-notification-name',
'default': true,
}) }}
{{ include('@MauticCore/Helper/tableheader.html.twig', {
'sessionVar': 'notification',
'orderBy': 'c.title',
'text': 'mautic.core.category',
'class': 'visible-md visible-lg col-notification-category',
}) }}
<th class="visible-sm visible-md visible-lg col-notification-stats">{{ 'mautic.core.stats'|trans }}</th>
{{ include('@MauticCore/Helper/tableheader.html.twig', {
'sessionVar': 'notification',
'orderBy': 'e.id',
'text': 'mautic.core.id',
'class': 'visible-md visible-lg col-notification-id',
}) }}
</tr>
</thead>
<tbody>
{# @var \Mautic\NotificationBundle\Entity\Notification $item #}
{% for item in items %}
{% set type = item.notificationType %}
<tr>
<td>
{{ include('@MauticCore/Helper/list_actions.html.twig', {
'item': item,
'templateButtons': {
'edit': securityHasEntityAccess(permissions['notification:notifications:editown'], permissions['notification:notifications:editother'], item.createdBy),
'delete': securityHasEntityAccess(permissions['notification:notifications:deleteown'], permissions['notification:notifications:deleteother'], item.createdBy),
},
'routeBase': 'notification',
'customButtons': [
{
'attr': {
'data-toggle': 'ajaxmodal',
'data-target': '#MauticSharedModal',
'data-header': 'mautic.notification.notification.header.preview'|trans,
'data-footer': 'false',
'href': path('mautic_notification_action', {'objectId': item.id, 'objectAction': 'preview'}),
},
'btnText': 'mautic.notification.preview'|trans,
'iconClass': 'ri-share-forward-box-fill',
},
],
}) }}
</td>
<td>
<div>
{% if 'template' == type %}
{{ include('@MauticCore/Helper/publishstatus_icon.html.twig', {'item': item, 'model': 'notification'}) }}
{% else %}
<i class="ri-fw ri-lg ri-toggle-fill text-secondary disabled"></i>
{% endif %}
<a href="{{ path('mautic_notification_action', {'objectAction': 'view', 'objectId': item.id}) }}" data-toggle="ajax">
{{ item.name }}
{% if 'list' == type %}
<span data-toggle="tooltip" title="{{ 'mautic.notification.icon_tooltip.list_notification'|trans }}"><i class="ri-fw ri-list-check"></i></span>
{% endif %}
</a>
{{ customContent('notification.name', _context) }}
</div>
</td>
<td class="visible-md visible-lg">
{{ include('@MauticCore/Modules/category--expanded.html.twig', {'category': item.category}) }}
</td>
<td class="visible-sm visible-md visible-lg col-stats">
<span class="mt-xs label label-green"
data-toggle="tooltip"
title="{{ 'mautic.channel.stat.leadcount.tooltip'|trans }}">
<a href="{{ path('mautic_contact_index', {'search': 'mautic.lead.lead.searchcommand.web_sent'|trans ~ ':' ~ item.id}) }}">
{{- 'mautic.notification.stat.sentcount'|trans({'%count%': item.getSentCount(true)}) -}}
</a>
</span>
</td>
<td class="visible-md visible-lg">{{ item.id }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="panel-footer">
{{ include('@MauticCore/Helper/pagination.html.twig', {
'totalItems': totalItems,
'page': page,
'limit': limit,
'baseUrl': path('mautic_notification_index'),
'sessionVar': 'notification',
}) }}
</div>
{% else %}
{{ include('@MauticCore/Helper/noresults.html.twig') }}
{% endif %}

View File

@@ -0,0 +1,159 @@
{#
Variables
- notification
- trackables
- logs
- permissions
- security
- entityViews
- contacts
- dateRangeForm
#}
{% extends '@MauticCore/Default/content.html.twig' %}
{% block mauticContent %}notification{% endblock %}
{% block headerTitle %}{{ notification.name }}{% endblock %}
{% block preHeader %}
{{- include('@MauticCore/Helper/page_actions.html.twig',
{
'item' : notification,
'templateButtons' : {
'close' : securityHasEntityAccess(permissions['notification:notifications:viewown'], permissions['notification:notifications:viewother'], notification.createdBy),
},
'routeBase' : 'notification',
'targetLabel' : 'mautic.notification.notifications'|trans
}
) -}}
{{ include('@MauticCore/Modules/category--inline.html.twig', {'category': notification.category}) }}
{% endblock %}
{% block actions %}
{{- include('@MauticCore/Helper/page_actions.html.twig', {
'item': notification,
'templateButtons': {
'edit': securityHasEntityAccess(permissions['notification:notifications:editown'], permissions['notification:notifications:editother'], notification.createdBy),
'delete': permissions['notification:notifications:create'],
},
'routeBase': 'notification',
}) -}}
{% endblock %}
{% block publishStatus %}
{{ include('@MauticCore/Helper/publishstatus_badge.html.twig', {'entity': notification}) }}
{% endblock %}
{% block content %}
<!-- start: box layout -->
<div class="box-layout">
<!-- left section -->
<div class="col-md-9 height-auto">
<div>
<!-- notification detail collapseable -->
<div class="collapse pr-md pl-md" id="notification-details">
<div class="pr-md pl-md pb-md">
<div class="panel shd-none mb-0">
<table class="table table-hover mb-0">
<tbody>
{{ include('@MauticCore/Helper/details.html.twig', {'entity': notification}) }}
</tbody>
</table>
</div>
</div>
</div>
<!--/ notification detail collapseable -->
<!-- notification detail collapseable toggler -->
<div class="hr-expand nm">
<span data-toggle="tooltip" title="{{ 'mautic.core.details'|trans }}">
<a href="javascript:void(0)" class="arrow text-secondary collapsed" data-toggle="collapse" data-target="#notification-details">
<span class="caret"></span>
{{ 'mautic.core.details'|trans }}
</a>
</span>
</div>
<!--/ notification detail collapseable toggler -->
<!-- some stats -->
<div class="pa-md">
<div class="row">
<div class="col-sm-12">
{% if security.isGranted('lead:leads:viewown') %}
{{ include('@MauticCore/Modules/stat--icon.html.twig', {'stats': [
{
'title': 'mautic.lead.lead.contacts.web_sent',
'value': notification.getSentCount(true),
'link': path('mautic_contact_index', {
'search': ('mautic.lead.lead.searchcommand.web_sent'|trans) ~ ':' ~ notification.id
}),
'icon': 'ri-notification-3-line'
}
]}) }}
{% endif %}
<div class="panel">
<div class="panel-body box-layout">
<div class="col-md-3 va-m">
<h5 class="text-white dark-md fw-sb mb-xs">
<span class="ri-line-chart-fill"></span>
{{ 'mautic.core.stats'|trans }}
</h5>
</div>
<div class="col-md-9 va-m">
{{ include('@MauticCore/Helper/graph_dateselect.html.twig', {'dateRangeForm': dateRangeForm, 'class': 'pull-right'}, with_context=false) }}
</div>
</div>
<div class="d-flex fd-column pt-0 pl-15 pb-15 pr-15 min-h-256">
{{ include('@MauticCore/Helper/chart.html.twig', {'chartData': entityViews, 'chartType': 'line', 'chartHeight': 300}, with_context=false) }}
</div>
</div>
</div>
</div>
</div>
<!--/ stats -->
{{ customContent('details.stats.graph.below', _context) }}
<!-- tabs controls -->
<ul class="nav nav-tabs nav-tabs-contained">
<li class="active">
<a href="#clicks-container" role="tab" data-toggle="tab">
{{ 'mautic.trackable.click_counts'|trans }}
</a>
</li>
<li class="">
<a href="#contacts-container" role="tab" data-toggle="tab">
{{ 'mautic.lead.leads'|trans }}
</a>
</li>
</ul>
<!--/ tabs controls -->
</div>
<!-- start: tab-content -->
<div class="tab-content pa-md">
<div class="tab-pane active bdr-w-0" id="clicks-container">
{{ include('@MauticPage/Trackable/click_counts.html.twig', {
'trackables': trackables,
'entity': notification,
'channel': 'notification'
}) }}
</div>
<div class="tab-pane fade in bdr-w-0 page-list" id="contacts-container">
{{ contacts|purify }}
</div>
</div>
<!--/ tab-content -->
</div>
<!--/ left section -->
<!-- right section -->
<div class="col-md-3 bdr-l height-auto">
<!-- activity feed -->
{{ include('@MauticCore/Helper/recentactivity.html.twig', {'logs': logs}) }}
</div>
<!--/ right section -->
<input name="entityId" id="entityId" type="hidden" value="{{ notification.id|e }}" />
</div>
<!--/ end: box layout -->
{% endblock %}

View File

@@ -0,0 +1,63 @@
{#
Variables
- form
- notification
- forceTypeSelection (optional, bool, default=false)
This will be defined when the notification entity is being edited
#}
{% extends '@MauticCore/Default/content.html.twig' %}
{% block mauticContent %}notification{% endblock %}
{% block headerTitle %}
{% if notification.id %}
{{ 'mautic.notification.header.edit'|trans({'%name%': notification.name}) }}
{% else %}
{{ 'mautic.notification.header.new'|trans }}
{% endif %}
{% endblock %}
{% block content %}
{{ form_start(form) }}
<div class="box-layout">
<div class="col-md-9 height-auto">
<div class="row">
<div class="col-xs-12">
<div class="tab-content pa-md">
<div class="tab-pane fade in active bdr-w-0" id="notification-container">
<div class="row">
<div class="col-md-6">
{{ form_row(form.name) }}
{{ form_row(form.heading) }}
{{ form_row(form.message) }}
{{ form_row(form.url) }}
{{ form_row(form.button) }}
</div>
<div class="col-md-6">
{{ include('@MauticNotification/Notification/preview.html.twig', {
'notification': notification,
}, with_context=false) }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 height-auto bdr-l">
<div class="pr-lg pl-lg pt-md pb-md">
{{ form_row(form.category) }}
{{ form_row(form.language) }}
<hr />
{% include '@MauticCore/FormTheme/Fields/_utm_tags_fields.html.twig' %}
<div class="hide">
{{ form_row(form.isPublished) }}
{{ form_row(form.publishUp) }}
{{ form_row(form.publishDown) }}
{{ form_rest(form) }}
</div>
</div>
</div>
</div>
{{ form_end(form) }}
{% endblock %}

View File

@@ -0,0 +1,99 @@
{#
Variables
- searchValue
- items
- totalItems
- page
- limit
- tmpl
- permissions
- model
- security
#}
{% set isIndex = 'index' == tmpl ? true : false %}
{% set tmpl = 'list' %}
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
{% block mauticContent %}notification{% endblock %}
{% block headerTitle %}{{ 'mautic.notification.notifications'|trans }}{% endblock %}
{% block content %}
{% if isIndex %}
<div id="page-list-wrapper" class="{% if items|length > 0 or searchValue is not empty %}panel {% endif %}panel-default">
{{ include('@MauticCore/Helper/list_toolbar.html.twig', {
'searchValue': searchValue,
'searchId': 'notification-search',
'action': currentRoute,
'page_actions': {
'templateButtons': {
'new': permissions['notification:notifications:create'],
},
'routeBase': 'notification',
},
'bulk_actions': {
'routeBase': 'notification',
'templateButtons': {
'delete': permissions['notification:notifications:deleteown'] or permissions['notification:notifications:deleteother'],
},
},
'quickFilters': [
{
'search': 'mautic.core.searchcommand.ispublished',
'label': 'mautic.core.form.available',
'tooltip': 'mautic.core.searchcommand.ispublished.description',
'icon': 'ri-check-line'
},
{
'search': 'mautic.core.searchcommand.isunpublished',
'label': 'mautic.core.form.unavailable',
'tooltip': 'mautic.core.searchcommand.isunpublished.description',
'icon': 'ri-close-line'
},
{
'search': 'mautic.core.searchcommand.isuncategorized',
'label': 'mautic.core.form.uncategorized',
'tooltip': 'mautic.core.searchcommand.isuncategorized.description',
'icon': 'ri-folder-unknow-line'
},
{
'search': 'mautic.core.searchcommand.ismine',
'label': 'mautic.core.searchcommand.ismine.label',
'tooltip': 'mautic.core.searchcommand.ismine.description',
'icon': 'ri-user-line'
}
]
}) }}
<div class="page-list">
{% endif %}
{% if items|length > 0 %}
{{ include('@MauticNotification/Notification/_list.html.twig') }}
{% else %}
{% if searchValue is not empty %}
{{- include('@MauticCore/Helper/noresults.html.twig', {'tip' : 'mautic.category.noresults.tip'}) -}}
{% else %}
<div class="mt-80 col-md-offset-2 col-lg-offset-3 col-md-8 col-lg-5 height-auto">
{% set childContainer %}
<div class="mt-32 mb-md">
{% include '@MauticCore/Components/pictogram.html.twig' with {
'pictogram': 'websites',
'size': '80'
} %}
</div>
{% endset %}
{{ include('@MauticCore/Components/content-block.html.twig', {
heading: 'mautic.notification.contentblock.heading',
subheading: 'mautic.notification.contentblock.subheading',
copy: 'mautic.notification.contentblock.copy',
childContainer: childContainer
}) }}
</div>
{% endif %}
{% endif %}
{% if isIndex %}
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,48 @@
{#
some stats: need more input on what type of form data to show.
delete if it is not require
#}
<!-- some stats -->
<div class="pa-md">
<div class="row">
<div class="col-sm-12">
{% if showVariants %}
<div class="text-right small">
<span>
{% if isVariant %}
<span data-chart="variant">{{ 'mautic.notification.variant.graph.variant'|trans }}</span>
{% else %}
<a data-chart="variant" href="javascript:void(0)">{{ 'mautic.notification.variant.graph.variant'|trans }}</a>
{% endif %}
</span>
</span> | </span>
<span>
{% if isVariant %}
<a data-chart="all" href="javascript:void(0)">{{ 'mautic.notification.variant.graph.all'|trans }}</a>
{% else %}
<span data-chart="all">{{ 'mautic.notification.variant.graph.all'|trans }}</span>
{% endif %}
</span>
</div>
{% endif %}
<div class="panel">
<div class="panel-body box-layout">
<div class="col-xs-4 va-m">
<h5 class="text-white dark-md fw-sb mb-xs">
<span class="ri-mail-line"></span>
{{ 'mautic.notification.lead.list.comparison'|trans }}
</h5>
</div>
<div class="col-xs-8 va-m" id="legend"></div>
</div>
<div class="pt-0 pl-15 pb-10 pr-15">
<div>
<canvas id="list-compare-chart" height="300"></canvas>
</div>
</div>
<div id="list-compare-chart-data" class="hide">{{ stats|json_encode|raw }}</div>
</div>
</div>
</div>
</div>
<!--/ some stats -->

View File

@@ -0,0 +1,35 @@
{#
Variables
- notification (\Mautic\NotificationBundle\Entity\Notification)
#}
<label>Preview</label>
<div id="notification-preview" class="panel panel-default">
<div class="panel-body">
<div class="row">
<div class="icon height-auto text-center">
<span class="ri-notification-3-fill fs-48"></span>
</div>
<div class="text height-auto">
<h4>
{%- if notification.heading -%}
{{- notification.heading -}}
{%- else -%}
Your notification header
{%- endif -%}
</h4>
<p>
{%- if notification.message %}
{{- notification.message -}}
{%- else -%}
The message body of your notification
{%- endif -%}
</p>
<span>{{ app.request.server.get('HTTP_HOST') }}</span>
</div>
</div>
{% if notification.url and notification.button %}
<hr>
<a href="{{ notification.url }}">{{ notification.button }}</a>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,32 @@
{#
some stats: need more input on what type of form data to show.
delete if it is not require
#}
<!-- some stats -->
<div class="pa-md">
<div class="row">
<div class="col-sm-12">
<div class="panel">
<div class="panel-body box-layout">
<div class="col-xs-4 va-m">
<h5 class="text-white dark-md fw-sb mb-xs">
<span class="ri-mail-line"></span>
{{ 'mautic.notification.stats'|trans }}
</h5>
</div>
<div class="col-xs-6 va-m" id="legend"></div>
<div class="col-xs-2 va-m">
{{ include('@MauticCore/Helper/graph_dateselect.html.twig', {'callback': 'updateNotificationStatsChart'}) }}
</div>
</div>
<div class="pt-0 pl-15 pb-10 pr-15">
<div>
<canvas id="stat-chart" height="300"></canvas>
</div>
</div>
<div id="stat-chart-data" class="hide">{{ stats|json_encode|raw }}</div>
</div>
</div>
</div>
</div>
<!--/ some stats -->

View File

@@ -0,0 +1,214 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Mautic</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="icon" type="image/x-icon" href="{{ getOverridableUrl('images/favicon.ico') }}" />
<link rel="icon" sizes="192x192" href="{{ getOverridableUrl('images/favicon.ico') }}">
<link rel="apple-touch-icon" href="{{ getOverridableUrl('images/apple-touch-icon.png') }}" />
{{ outputStyles() }}
<script src="/app/bundles/NotificationBundle/Assets/js/popup/usparser.min.js" type="text/javascript"></script>
</head>
<body>
<!-- Directions -->
<!-- overlay -->
<div id="black-wrapper">
</div>
<div id="white-wrapper">
</div>
<div id="mobile">
<div id="mobile-top-section">
<div id="mobile-top-section-wrapper">
<div id="mobile-top-section-content">
<div class="title domainName">This website</div>
<p id="mobile-directions">wants to show notifications:</p>
<div style="display: none;" id="mobile-notification">
<img id="mobile-notification-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RDdBOEVEMjU3RTgwMTFFNUIzMjFCOUQ0QjUzN0Q0NDYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RDdBOEVEMjY3RTgwMTFFNUIzMjFCOUQ0QjUzN0Q0NDYiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpDODdGODRDMDdEMTMxMUU1QjMyMUI5RDRCNTM3RDQ0NiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpEN0E4RUQyNDdFODAxMUU1QjMyMUI5RDRCNTM3RDQ0NiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pttyb1cAAACySURBVHja7N2xDUMhDEBBO0rJ33++SNkAJKqET1YwTYp7hUV9smtyzvmKiCtUqecG/OzHg0Wp7w9ucCg3bN5hAAECBAhQAAECBCiAAAECFECAAAEKIECAAAUQIECAAggQIEABBAgQoAACBAhQZ4CZSe4EcK1FzgkDBAhQAAECBAhQAAECBCiAAAECFECAAAEKIECAAAUQIECAAggQIEABBPivgA1Dufbc4x2+w6jWbwEGAJZEES0DZiYyAAAAAElFTkSuQmCC">
<p id="mobile-notification-title" class="truncatable long desktop message">Example Notification</p>
<p id="mobile-notification-message" class="truncatable short desktop message">Notifications will appear on your device</p>
<p id="mobile-notification-url" class="truncatable short desktop message">{{ siteUrl }}</p>
</div>
<div id="desktop-notification">
<img id="desktop-notification-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RDdBOEVEMjU3RTgwMTFFNUIzMjFCOUQ0QjUzN0Q0NDYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RDdBOEVEMjY3RTgwMTFFNUIzMjFCOUQ0QjUzN0Q0NDYiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpDODdGODRDMDdEMTMxMUU1QjMyMUI5RDRCNTM3RDQ0NiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpEN0E4RUQyNDdFODAxMUU1QjMyMUI5RDRCNTM3RDQ0NiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pttyb1cAAACySURBVHja7N2xDUMhDEBBO0rJ33++SNkAJKqET1YwTYp7hUV9smtyzvmKiCtUqecG/OzHg0Wp7w9ucCg3bN5hAAECBAhQAAECBCiAAAECFECAAAEKIECAAAUQIECAAggQIEABBAgQoAACBAhQZ4CZSe4EcK1FzgkDBAhQAAECBAhQAAECBCiAAAECFECAAAEKIECAAAUQIECAAggQIEABBPivgA1Dufbc4x2+w6jWbwEGAJZEES0DZiYyAAAAAElFTkSuQmCC">
<p id="x">x</p>
<p id="desktop-notification-title" class="truncatable mobile message">This is an example notification</p>
<p id="desktop-notification-message" class="truncatable mobile message">Notifications will appear on your desktop</p>
<p id="desktop-notification-url" class="truncatable mobile message">{{ siteUrl }}</p>
</div>
<p id="mobile-opt-out" class="truncatable opt-out message">(you can unsubscribe anytime in your browser settings)</p>
</div>
</div>
</div>
</div>
<div id="error-box">
<div id="error-message-padding">
<!-- if on ios -->
<div class="error" id="ios">
<p> Web Push Notifications are not supported by iOS. </p>
</div>
<!-- if not on chrome (Desktop) -->
<div class="error" id="not-chrome-desktop">
<p> Web Push Notifications are not supported by your browser. Please install
<a class="default-link" href="https://www.google.com/chrome/browser/desktop" target="_blank">Chrome</a> to get
notifications. </p>
</div>
<!-- if not on chrome (Android) -->
<div class="error" id="not-chrome-Android">
<p> Please install Chrome web browser to get notifications. </p>
<p><a class="default-link" href="https://play.google.com/store/apps/details?id=com.android.chrome">Tap here</a> to
download from the Google Play Store.</p>
</div>
<!-- not have latest version of chrome (desktop) -->
<div class="error" id="outdated-chrome-desktop">
<p> Please update your Chrome web browser to get notifications. </p>
</div>
<!-- not have latest version of chrome (mobile) -->
<div class="error" id="outdated-chrome-mobile">
<p> Please update your Chrome web browser to get notifications. </p>
<p><a class="default-link" href="https://play.google.com/store/apps/details?id=com.android.chrome">Tap here</a> to
download from the Google Play Store.</p>
</div>
<!-- if notifications are disabled (desktop)-->
<div class="error" id="disabled-notifications-desktop">
<p> Notifications are currently disabled.</p>
<p>Please re-enable them by clicking on the lock icon in the top left of this window. </p>
</div>
<!-- if notifications are disabled (mobile) -->
<div class="error" id="disabled-notifications-mobile">
<p> Notifications are currently disabled.</p>
<p>Please re-enable them by tapping on the lock icon on the top left. </p>
</div>
<!-- if notifications are already enabled -->
<div class="error" id="notifications-already-enabled">
<p> Notifications are already enabled, you may close this window. </p>
<p style="font-size: 12px">If you would like to unsubscribe from all notifications from
<span class="domainName">{{ siteUrl }}</span> click on the lock icon to the left of the address. </p>
</div>
</div>
</div>
<script>
/* returns true if device is mobile or tablet */
function detectmob() {
return navigator.userAgent.match(/Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone/i) != null;
}
/* show mobile example notification on mobile, desktop notification on desktop */
if (detectmob()) {
document.getElementById("desktop-notification").style.display = 'none';
} else {
document.getElementById("mobile-notification").style.display = "none";
}
/* ERROR MESSAGES */
/* instantiate parser */
var parser = new UAParser();
var isHttpsPrompt = false;
// get the UA string result
var result = parser.getResult();
// get user agent info
var browser = result.browser.name;
var browser_version = result.browser.version;
var os = result.os.name;
var engine = result.engine.name;
if (OneSignal.isPushNotificationsSupported());
else if (os == "iOS")
showError("ios");
else if (browser != "Chrome") {
if (os == "Android")
showError("not-chrome-Android");
else
showError("not-chrome-desktop");
} // TODO: Show generic error if SDK reports push notifications not supported.
else { // They are on Chrome
if (parseInt(browser_version.substring(0, 2)) < 42) // Check Chrome version
showError(detectmob() ? "outdated-chrome-mobile" : "outdated-chrome-desktop");
else if (isHttpsPrompt) {
if (!isPushEnabled) {
if (isPermissionBlocked)
showError(detectmob() ? "disabled-notifications-mobile" : "disabled-notifications-desktop");
} else
showError("notifications-already-enabled");
} else { // HTTP
if (Notification.permission == "denied") // Check if the Notification permission is disabled.
showError(detectmob() ? "disabled-notifications-mobile" : "disabled-notifications-desktop");
else if (Notification.permission == "granted") {
navigator.serviceWorker.ready.then(function (event) {
if (event) {
OneSignal.getIdsAvailable(function (ids) {
if (ids.registrationId != null) {
OneSignal._getSubscription(function (isSet) {
if (isSet)
showError("notifications-already-enabled");
});
}
});
}
});
}
}
}
if (!isHttpsPrompt) {
if (Notification.permission == "denied") // Check if the Notification permission is disabled.
showError(detectmob() ? "disabled-notifications-mobile" : "disabled-notifications-desktop");
else if (Notification.permission == "granted") {
OneSignal._initOptions = {};
(OneSignal.isPushNotificationsEnabled(function (enabled) {
if (enabled) {
showError("notifications-already-enabled");
}
}));
}
} else {
if (isPermissionBlocked) // Check if the Notification permission is disabled.
showError(detectmob() ? "disabled-notifications-mobile" : "disabled-notifications-desktop");
else if (isPushEnabled) {
showError("notifications-already-enabled");
}
}
function showError(error) {
// put a white overlay over all existing content
// this also disables all functionality
document.getElementById("white-wrapper").style.zIndex = "10";
document.getElementById("white-wrapper").style.opacity = ".75";
document.getElementById("error-box").style.opacity = "1";
document.getElementById("error-box").style.display = "block";
document.getElementById(error).style.display = "block";
}
</script>
</body></html>

View File

@@ -0,0 +1,16 @@
{% if showMore is defined %}
<a href="{{ url('mautic_mobile_notification_index', {'search': searchString}) }}" data-toggle="ajax">
<span>{{ 'mautic.core.search.more'|trans({'%count%': remaining}) }}</span>
</a>
{% else %}
<a href="{{ url('mautic_mobile_notification_action', {'objectAction': 'edit', 'objectId': item.id}) }}" data-toggle="ajax">
<span class="fw-sb">{{ item.name }}</span>
<span class="ml-4 mr-sm">#{{ item.id }}</span>
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {
'entity': item,
'status': 'active',
'simplified': 'true'
}) -}}
</a>
<div class="clearfix"></div>
{% endif %}

View File

@@ -0,0 +1,16 @@
{% if showMore is defined %}
<a href="{{ url('mautic_notification_index', {'search': searchString}) }}" data-toggle="ajax">
<span>{{ 'mautic.core.search.more'|trans({'%count%': remaining}) }}</span>
</a>
{% else %}
<a href="{{ url('mautic_notification_action', {'objectAction': 'view', 'objectId': item.id}) }}" data-toggle="ajax">
<span class="fw-sb">{{ item.name }}</span>
<span class="ml-4 mr-sm">#{{ item.id }}</span>
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {
'entity': item,
'status': 'active',
'simplified': 'true'
}) -}}
</a>
<div class="clearfix"></div>
{% endif %}

View File

@@ -0,0 +1,19 @@
{#
Variables
- event
#}
{%- set data = event.extra.log.metadata -%}
{%- if data.failed is not defined -%}
<dl class="dl-horizontal">
<dt>{{ 'mautic.notification.timeline.status'|trans }}</dt>
<dd>{{ data.status|trans }}</dd>
<dt>{{ 'mautic.notification.timeline.type'|trans }}</dt>
<dd>{{ data.type|trans }}</dd>
</dl>
<div class="small">
<hr />
<strong>{{ data.heading }}</strong>
<br />
{{ data.content }}
</div>
{%- endif -%}

View File

@@ -0,0 +1,29 @@
<?php
namespace Mautic\NotificationBundle\Security\Permissions;
use Mautic\CoreBundle\Security\Permissions\AbstractPermissions;
use Symfony\Component\Form\FormBuilderInterface;
class NotificationPermissions extends AbstractPermissions
{
public function __construct($params)
{
parent::__construct($params);
$this->addStandardPermissions('categories');
$this->addExtendedPermissions('notifications');
$this->addExtendedPermissions('mobile_notifications');
}
public function getName(): string
{
return 'notification';
}
public function buildForm(FormBuilderInterface &$builder, array $options, array $data): void
{
$this->addStandardFormFields('notification', 'categories', $builder, $data);
$this->addExtendedFormFields('notification', 'notifications', $builder, $data);
$this->addExtendedFormFields('notification', 'mobile_notifications', $builder, $data);
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Form\Type;
use Mautic\NotificationBundle\Form\Type\MobileNotificationDetailsType;
use Mautic\PluginBundle\Entity\Integration;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\FormExtensionInterface;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Validator\Validation;
class MobileNotificationDetailsTypeTest extends TypeTestCase
{
/**
* @var MockObject&Integration
*/
private MockObject $integrationSettings;
/**
* @return array<FormExtensionInterface>
*/
protected function getExtensions(): array
{
$validatorBuilder = Validation::createValidatorBuilder();
$validatorBuilder->addMethodMapping('loadValidatorMetadata');
$this->integrationSettings = $this->createMock(Integration::class);
// @phpstan-ignore-next-line
$integration = $this->createMock(AbstractIntegration::class);
$integration->method('getIntegrationSettings')
->willReturn($this->integrationSettings);
$integrationHelper = $this->createMock(IntegrationHelper::class);
$integrationHelper->method('getIntegrationObject')
->with('OneSignal')
->willReturn($integration);
return [
new ValidatorExtension($validatorBuilder->getValidator()),
new PreloadedExtension([
new MobileNotificationDetailsType($integrationHelper),
], []),
];
}
public function testNoPlatformsSelected(): void
{
$this->integrationSettings->method('getFeatureSettings')
->willReturn([]);
$form = $this->factory->create(MobileNotificationDetailsType::class);
$view = $form->createView();
// test only field is "additional_data"
self::assertCount(1, $view->children);
self::assertArrayHasKey('additional_data', $view->children);
}
/**
* @param array<int, string> $platforms
* @param array<int, string> $settings
*/
#[\PHPUnit\Framework\Attributes\DataProvider('platformProvider')]
public function testPlatformSelected(array $platforms, array $settings): void
{
$this->integrationSettings->method('getFeatureSettings')
->willReturn(['platforms' => $platforms]);
$form = $this->factory->create(MobileNotificationDetailsType::class);
$view = $form->createView();
self::assertCount(1 + count($settings), $view->children);
self::assertArrayHasKey('additional_data', $view->children);
foreach ($settings as $settingField) {
self::assertArrayHasKey($settingField, $view->children);
}
}
public static function platformProvider(): \Generator
{
$iosSettings = [
'ios_subtitle',
'ios_sound',
'ios_badges',
'ios_badgeCount',
'ios_contentAvailable',
'ios_media',
'ios_mutableContent',
];
$androidSettings = [
'android_sound',
'android_small_icon',
'android_large_icon',
'android_big_picture',
'android_led_color',
'android_accent_color',
'android_group_key',
'android_lockscreen_visibility',
];
yield 'ios' => [['ios'], $iosSettings];
yield 'android' => [['android'], $androidSettings];
yield 'both' => [['android', 'ios'], array_merge($androidSettings, $iosSettings)];
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Form\Type;
use Doctrine\DBAL\Connection;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Form\Type\EntityLookupType;
use Mautic\NotificationBundle\Form\Type\MobileNotificationSendType;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Validator\Validation;
use Symfony\Contracts\Translation\TranslatorInterface;
final class MobileNotificationSendTypeTest extends TypeTestCase
{
private RouterInterface $router;
private TranslatorInterface $translator;
private Connection $connection;
/**
* @var ModelFactory<object>&MockObject
*/
private ModelFactory $modelFactory;
protected function setUp(): void
{
$this->router = $this->createMock(RouterInterface::class);
$this->translator = $this->createMock(TranslatorInterface::class);
$this->modelFactory = $this->createMock(ModelFactory::class);
$this->connection = $this->createMock(Connection::class);
parent::setup();
}
/**
* @return array<mixed>
*/
protected function getExtensions()
{
return [
new ValidatorExtension(Validation::createValidator()),
new PreloadedExtension([
new MobileNotificationSendType($this->router),
new EntityLookupType($this->modelFactory, $this->translator, $this->connection, $this->router),
], []),
];
}
public function testSubmitValidData(): void
{
$form = $this->factory->create(MobileNotificationSendType::class);
$expected = [
'notification' => '1',
];
$form->submit([
'notification' => '1',
]);
// This check ensures there are no transformation failures
$this->assertTrue($form->isSynchronized());
// check that $model was modified as expected when the form was submitted
$this->assertEquals($expected, $form->getData());
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Form\Type;
use Doctrine\DBAL\Connection;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Form\Type\EntityLookupType;
use Mautic\NotificationBundle\Form\Type\NotificationSendType;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Validator\Validation;
use Symfony\Contracts\Translation\TranslatorInterface;
final class NotificationSendTypeTest extends TypeTestCase
{
private RouterInterface $router;
private TranslatorInterface $translator;
private Connection $connection;
/**
* @var ModelFactory<object>&MockObject
*/
private ModelFactory $modelFactory;
protected function setUp(): void
{
$this->router = $this->createMock(RouterInterface::class);
$this->translator = $this->createMock(TranslatorInterface::class);
$this->modelFactory = $this->createMock(ModelFactory::class);
$this->connection = $this->createMock(Connection::class);
parent::setup();
}
/**
* @return array<mixed>
*/
protected function getExtensions()
{
return [
new ValidatorExtension(Validation::createValidator()),
new PreloadedExtension([
new NotificationSendType($this->router),
new EntityLookupType($this->modelFactory, $this->translator, $this->connection, $this->router),
], []),
];
}
public function testSubmitValidData(): void
{
$form = $this->factory->create(NotificationSendType::class);
$expected = [
'notification' => '1',
];
$form->submit([
'notification' => '1',
]);
// This check ensures there are no transformation failures
$this->assertTrue($form->isSynchronized());
// check that $model was modified as expected when the form was submitted
$this->assertEquals($expected, $form->getData());
}
}

View File

@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Form\Type;
use Doctrine\ORM\EntityManager;
use Mautic\CategoryBundle\Form\Type\CategoryListType;
use Mautic\CategoryBundle\Model\CategoryModel;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\NotificationBundle\Form\Type\NotificationType;
use PHPUnit\Framework\Assert;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\FormErrorIterator;
use Symfony\Component\Form\FormExtensionInterface;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Validator\Validation;
use Symfony\Contracts\Translation\TranslatorInterface;
class NotificationTypeTest extends TypeTestCase
{
/**
* @return array<FormExtensionInterface>
*/
protected function getExtensions(): array
{
$validatorBuilder = Validation::createValidatorBuilder();
$validatorBuilder->addMethodMapping('loadValidatorMetadata');
return [
new ValidatorExtension($validatorBuilder->getValidator()),
new PreloadedExtension([
new CategoryListType(
$this->createMock(EntityManager::class),
$this->createMock(TranslatorInterface::class),
$this->createMock(CategoryModel::class),
$this->createMock(RouterInterface::class),
),
], []),
];
}
public function testSubmitInvalidData(): void
{
$form = $this->factory->create(NotificationType::class);
$expected = new Notification();
$expected->setLanguage('en');
$expected->setUtmTags([
'utmSource' => null,
'utmMedium' => null,
'utmCampaign' => null,
'utmContent' => null,
]);
$expected->setIsPublished(false);
$form->submit([
'language' => 'en',
]);
Assert::assertTrue($form->isSynchronized());
$formData = $form->getData();
\assert($formData instanceof Notification);
$expected->setChanges($formData->getChanges());
Assert::assertEquals($expected, $formData);
Assert::assertFalse($form->isValid());
$view = $form->createView();
$invalidFields = ['name', 'heading', 'message'];
$errorCount = 0;
foreach ($view->children as $fieldName => $child) {
$errors = $view->children[$fieldName]->vars['errors'];
\assert($errors instanceof FormErrorIterator);
if (in_array($fieldName, $invalidFields, true)) {
++$errorCount;
self::assertCount(1, $errors);
continue;
}
self::assertCount(0, $errors);
}
self::assertCount($errorCount, $invalidFields);
self::assertCount(0, $view->vars['errors']);
}
public function testSubmitValidData(): void
{
$form = $this->factory->create(NotificationType::class);
$expected = new Notification();
$expected->setLanguage('en');
$expected->setName('The name');
$expected->setHeading('The heading');
$expected->setMessage('The message');
$expected->setUtmTags([
'utmSource' => null,
'utmMedium' => null,
'utmCampaign' => null,
'utmContent' => null,
]);
$expected->setIsPublished(false);
$form->submit([
'name' => 'The name',
'heading' => 'The heading',
'message' => 'The message',
'language' => 'en',
]);
Assert::assertTrue($form->isSynchronized());
$formData = $form->getData();
\assert($formData instanceof Notification);
$expected->setChanges($formData->getChanges());
Assert::assertEquals($expected, $formData);
Assert::assertTrue($form->isValid());
$view = $form->createView();
foreach ($view->children as $fieldName => $child) {
$errors = $view->children[$fieldName]->vars['errors'];
\assert($errors instanceof FormErrorIterator);
self::assertCount(0, $errors);
}
self::assertCount(0, $view->vars['errors']);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class MobileNotificationControllerTest extends MauticMysqlTestCase
{
/**
* Smoke test to ensure the '/s/mobile_notifications' route loads.
*/
public function testIndexRouteSuccessfullyLoads(): void
{
$this->client->request(Request::METHOD_GET, '/s/mobile_notifications');
$response = $this->client->getResponse();
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
}
public function testCreateRouteSuccessfullyLoads(): void
{
$this->client->request(Request::METHOD_GET, '/s/mobile_notifications/new');
$response = $this->client->getResponse();
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
}
}

View File

@@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\NotificationBundle\Entity\Stat;
use Symfony\Component\HttpFoundation\Request;
final class MobileNotificationTranslationFunctionalTest extends MauticMysqlTestCase
{
public function testNotificationCanBeCreatedWithTranslationParent(): void
{
// Arrange
$parentNotification = $this->createAndPersistNotification('Parent Notification', 'Parent Notification message');
// Act
$crawler = $this->client->request(Request::METHOD_GET, '/s/mobile_notifications/new');
$this->assertResponseIsSuccessful();
$form = $crawler->selectButton('Save')->form();
$form['mobile_notification[name]'] = 'Child Notification';
$form['mobile_notification[message]'] = 'Child Notification message';
$form['mobile_notification[heading]'] = 'Child Notification';
$form['mobile_notification[translationParentSelector]'] = (string) $parentNotification->getId();
$this->client->submit($form);
$this->assertResponseIsSuccessful();
// Assert
$childNotification = $this->em->getRepository(Notification::class)->findOneBy(['name' => 'Child Notification']);
$this->assertInstanceOf(Notification::class, $childNotification);
$this->assertInstanceOf(Notification::class, $childNotification->getTranslationParent());
$this->assertSame($parentNotification->getId(), $childNotification->getTranslationParent()->getId());
}
public function testNotificationCannotBeItsOwnTranslationParent(): void
{
// Arrange
$notification = $this->createAndPersistNotification('Test Notification', 'Test Notification message');
// Act
$crawler = $this->client->request(Request::METHOD_GET, '/s/mobile_notifications/edit/'.$notification->getId());
$this->assertResponseIsSuccessful();
// Assert
$options = $crawler->filter('#mobile_notification_translationParentSelector option');
$this->assertCount(2, $options);
$this->assertSame('Choose a translated item...', $options->eq(0)->text());
$this->assertSame('Create new...', $options->eq(1)->text());
// Ensure the Notification itself is not in the dropdown
$optionValues = $options->each(fn ($node) => $node->attr('value'));
$this->assertNotContains((string) $notification->getId(), $optionValues);
}
public function testNotificationWithTranslationParentCanBeEdited(): void
{
// Arrange
$parentNotification = $this->createAndPersistNotification('Parent Notification', 'Parent Notification message');
$childNotification = $this->createAndPersistNotification('Child Notification', 'Child Notification message');
$childNotification->setTranslationParent($parentNotification);
$newParentNotification = $this->createAndPersistNotification('New Parent Notification', 'New Parent Notification message');
$this->em->flush();
// Act
$crawler = $this->client->request(Request::METHOD_GET, '/s/mobile_notifications/edit/'.$childNotification->getId());
$this->assertResponseIsSuccessful();
// Assert original parent is selected
$this->assertSame(
(string) $parentNotification->getId(),
$crawler->filter('#mobile_notification_translationParentSelector option[selected]')->attr('value')
);
// Change parent
$form = $crawler->selectButton('Save')->form();
$form['mobile_notification[translationParentSelector]'] = (string) $newParentNotification->getId();
$this->client->submit($form);
$this->assertResponseIsSuccessful();
// Assert parent updated
$this->em->refresh($childNotification);
$this->assertInstanceOf(Notification::class, $childNotification->getTranslationParent());
$this->assertSame($newParentNotification->getId(), $childNotification->getTranslationParent()->getId());
}
public function testTranslationParentCanBeRemovedFromNotification(): void
{
// Arrange
$parentNotification = $this->createAndPersistNotification('Parent Notification', 'Parent Notification message');
$childNotification = $this->createAndPersistNotification('Child Notification', 'Child Notification message');
$childNotification->setTranslationParent($parentNotification);
$this->em->flush();
// Act
$crawler = $this->client->request(Request::METHOD_GET, '/s/mobile_notifications/edit/'.$childNotification->getId());
$this->assertResponseIsSuccessful();
$form = $crawler->selectButton('Save')->form();
$form['mobile_notification[translationParentSelector]'] = '';
$this->client->submit($form);
$this->assertResponseIsSuccessful();
// Assert
$this->em->refresh($childNotification);
$this->assertNull($childNotification->getTranslationParent());
}
public function testTranslationsAreDisplayedOnViewPage(): void
{
// Arrange
$parentNotification = $this->createAndPersistNotification('Parent Notification', 'Parent Notification message', 'en');
$childNotification = $this->createAndPersistNotification('Child Notification', 'Child Notification message', 'fr');
$childNotification->setTranslationParent($parentNotification);
$parentNotification->addTranslationChild($childNotification);
$this->em->flush();
// Act & Assert - Parent view
$crawler = $this->client->request(Request::METHOD_GET, '/s/mobile_notifications/view/'.$parentNotification->getId());
$this->assertResponseIsSuccessful();
$this->assertCount(1, $crawler->filter('a[href="#translation-container"]'));
$this->client->click($crawler->selectLink('Translations')->link());
$this->assertSelectorTextContains('#translation-container', 'Child Notification');
// Act & Assert - Child view
$crawler = $this->client->request(Request::METHOD_GET, '/s/mobile_notifications/view/'.$childNotification->getId());
$this->assertResponseIsSuccessful();
$this->assertCount(1, $crawler->filter('a[href="#translation-container"]'));
$this->client->click($crawler->selectLink('Translations')->link());
$this->assertSelectorTextContains('#translation-container', 'Parent Notification');
}
public function testListPageWithSentStats(): void
{
// Arrange
$parentNotification = $this->createAndPersistNotification('Parent Notification', 'Parent Notification message', 'en');
$childNotification = $this->createAndPersistNotification('Child Notification', 'Child Notification message', 'fr');
$childNotification->setTranslationParent($parentNotification);
$parentNotification->addTranslationChild($childNotification);
$this->em->flush();
// Create a stat
$this->createStatEntry($parentNotification, $this->createContact('user', 'one'));
$this->createStatEntry($childNotification, $this->createContact('user', 'two'));
// Act & Assert - list view
$crawler = $this->client->request(Request::METHOD_GET, '/s/mobile_notifications');
$this->assertResponseIsSuccessful();
$this->assertCount(2, $crawler->filterXPath("//td[contains(@class, 'col-stats')]"));
}
private function createANotification(string $name, string $message, bool $isPublished = true, string $locale = 'en'): Notification
{
$notification = new Notification();
$notification->setName($name);
$notification->setMessage($message);
$notification->setHeading($name);
$notification->setLanguage($locale);
$notification->setIsPublished($isPublished);
$notification->setMobile(true);
return $notification;
}
private function createAndPersistNotification(string $name, string $message, string $locale = 'en'): Notification
{
$notification = $this->createANotification($name, $message, true, $locale);
$this->em->persist($notification);
$this->em->flush();
return $notification;
}
public function createStatEntry(Notification $notification, Lead $lead): void
{
$stat = new Stat();
$stat->setDateSent(new \DateTime());
$stat->setLead($lead);
$stat->setNotification($notification);
$stat->setSource(null);
$stat->setSourceId(null);
$this->em->persist($stat);
$this->em->flush();
}
private function createContact(string $firstname, string $lastname): Lead
{
$contact = new Lead();
$contact->setFirstname($firstname);
$contact->setLastname($lastname);
$this->em->persist($contact);
$this->em->flush();
return $contact;
}
}

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\NotificationBundle\Tests\NotificationTrait;
use PHPUnit\Framework\Assert;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class NotificationControllerTest extends MauticMysqlTestCase
{
use NotificationTrait;
/**
* @var string
*/
private const REST_API_ID = 'restApiID';
/**
* @var string
*/
private const API_ID = 'apiID';
/**
* Smoke test to ensure the '/s/notifications' route loads.
*/
public function testIndexRouteSuccessfullyLoads(): void
{
$this->client->request(Request::METHOD_GET, '/s/notifications');
$response = $this->client->getResponse();
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
}
/**
* Smoke test to ensure the '/s/notifications/new' route loads.
*/
public function testNewRouteSuccessfullyLoads(): void
{
$this->client->request(Request::METHOD_GET, '/s/notifications/new');
$response = $this->client->getResponse();
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
}
public function testNewWebNotificationValidSubmit(): void
{
$crawler = $this->client->request(Request::METHOD_GET, '/s/notifications/new');
$formCrawler = $crawler->filter('form[name=notification]');
$this->assertCount(1, $formCrawler);
$form = $formCrawler->form();
$form->setValues([
'notification[name]' => 'Some Name',
'notification[heading]' => 'Some Heading',
'notification[message]' => 'some message',
]);
$crawler = $this->client->submit($form);
Assert::assertStringContainsString('Some Name has been created!', $crawler->text());
}
public function testNewWebNotificationValidationErrors(): void
{
$crawler = $this->client->request(Request::METHOD_GET, '/s/notifications/new');
$this->assertValidationErrors($crawler);
}
public function testEditWebNotificationValidationErrors(): void
{
$notification = $this->createNotification($this->em);
$this->em->flush();
$this->em->clear();
$crawler = $this->client->request(Request::METHOD_GET, '/s/notifications/edit/'.$notification->getid());
$this->assertValidationErrors($crawler);
}
private function assertValidationErrors(Crawler $crawler): void
{
$formCrawler = $crawler->filter('form[name=notification]');
$this->assertCount(1, $formCrawler);
// test blank errors
$form = $formCrawler->form();
$form->setValues([
'notification[name]' => '',
'notification[heading]' => '',
'notification[message]' => '',
]);
$crawler = $this->client->submit($form);
$formCrawler = $crawler->filter('form[name=notification]');
$this->assertCount(1, $formCrawler);
Assert::assertMatchesRegularExpression('/A name is required\./', $formCrawler->text());
Assert::assertMatchesRegularExpression('/A heading is required\./', $formCrawler->text());
Assert::assertMatchesRegularExpression('/A message is required\./', $formCrawler->text());
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class PopupControllerTest extends MauticMysqlTestCase
{
/**
* Smoke test to ensure the '/s/notifications' route loads.
*/
public function testIndexRouteSuccessfullyLoads(): void
{
$this->client->request(Request::METHOD_GET, '/notification');
$response = $this->client->getResponse();
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
}
}

View File

@@ -0,0 +1,617 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Functional\EventListener;
use GuzzleHttp\Psr7\Response;
use Mautic\CampaignBundle\Entity\Campaign;
use Mautic\CampaignBundle\Entity\Event as CampaignEvent;
use Mautic\CampaignBundle\Entity\Lead as CampaignLead;
use Mautic\CampaignBundle\Entity\LeadEventLog;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\LeadBundle\Entity\DoNotContact;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\NotificationBundle\Api\AbstractNotificationApi;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\NotificationBundle\EventListener\CampaignSubscriber;
use Mautic\NotificationBundle\Tests\NotificationTrait;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\RequestInterface;
class CampaignSubscriberTest extends MauticMysqlTestCase
{
use NotificationTrait;
/**
* @var string
*/
private const REST_API_ID = 'restApiID';
/**
* @var string
*/
private const API_ID = 'apiID';
/**
* @var string
*/
private const ONESIGNAL_API_BASE_URL = 'https://onesignal.com/api/v1/notifications';
public function testLeadNotContactable(): void
{
$notification = $this->createNotification($this->em);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadTwo = $this->createLeadInCampaign($campaign, ['web-2']);
$leadThree = $this->createLeadInCampaign($campaign, ['web-3a', 'web-3b']);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$this->createDoNotContact($leadOne, $notification);
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(['web-2', 'web-3a', 'web-3b'], $notification),
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$this->assertEventLogFailed($event, $leadOne, 'Contact is not contactable on the Web Notification channel.');
$this->assertEventLogPassed($event, $leadTwo);
$this->assertEventLogPassed($event, $leadThree);
}
public function testNotificationMissing(): void
{
$notification = $this->createNotification($this->em);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadTwo = $this->createLeadInCampaign($campaign, ['web-2']);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$event->setProperties([]);
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$reason = 'The specified Web Notification entity does not exist.';
$this->assertEventLogFailed($event, $leadOne, $reason);
$this->assertEventLogFailed($event, $leadTwo, $reason);
}
public function testNotificationUnpublished(): void
{
$notification = $this->createNotification($this->em);
$notification->setIsPublished(false);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadTwo = $this->createLeadInCampaign($campaign, ['web-2']);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$reason = 'The specified Web Notification is unpublished.';
$this->assertEventLogFailed($event, $leadOne, $reason);
$this->assertEventLogFailed($event, $leadTwo, $reason);
}
public function testNotificationWithEmptyPushIds(): void
{
$notification = $this->createNotification($this->em);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, []);
$leadTwo = $this->createLeadInCampaign($campaign, []);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$reason = 'The contact has not subscribed to the Web Notification channel.';
$this->assertEventLogFailed($event, $leadOne, $reason);
$this->assertEventLogFailed($event, $leadTwo, $reason);
}
public function testWebNotificationsAreSent(): void
{
$notification = $this->createNotification($this->em);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadTwo = $this->createLeadInCampaign($campaign, ['mobile-1'], true);
$leadThree = $this->createLeadInCampaign($campaign, ['web-2a', 'web-2b']);
$leadFour = $this->createLeadInCampaign($campaign, ['web-3']);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(['web-1', 'web-2a', 'web-2b', 'web-3'], $notification),
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$this->assertEventLogPassed($event, $leadOne);
$this->assertEventLogFailed($event, $leadTwo, 'The contact has not subscribed to the Web Notification channel.');
$this->assertEventLogPassed($event, $leadThree);
$this->assertEventLogPassed($event, $leadFour);
}
public function testMobileNotificationsAreSent(): void
{
$notification = $this->createNotification($this->em);
$notification->setMobile(true);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['mobile-1'], true);
$leadTwo = $this->createLeadInCampaign($campaign, ['web-1']);
$leadThree = $this->createLeadInCampaign($campaign, ['mobile-2a', 'mobile-2b'], true);
$leadFour = $this->createLeadInCampaign($campaign, ['mobile-3a', 'mobile-3b'], true);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_mobile_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(
['mobile-1', 'mobile-2a', 'mobile-2b', 'mobile-3a', 'mobile-3b'],
$notification
),
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$this->assertEventLogPassed($event, $leadOne);
$this->assertEventLogFailed($event, $leadTwo, 'The contact has not subscribed to the Web Notification channel.');
$this->assertEventLogPassed($event, $leadThree);
$this->assertEventLogPassed($event, $leadFour);
}
public function testWebAndMobileNotificationsAreSent(): void
{
$webNotification = $this->createNotification($this->em);
$webNotification->setHeading('Web heading 1');
$webNotification->setMessage('Web message 1');
$mobileNotification = $this->createNotification($this->em);
$mobileNotification->setHeading('Mobile heading 1');
$mobileNotification->setMessage('Mobile message 1');
$mobileNotification->setMobile(true);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadTwo = $this->createLeadInCampaign($campaign, ['mobile-1'], true);
$leadThree = $this->createLeadInCampaign($campaign, ['mobile-2a', 'mobile-2b'], true);
$leadFour = $this->createLeadInCampaign($campaign, ['web-2a', 'web-2b']);
$webEvent = $this->createCampaignEvent($campaign, $webNotification, 'notification.send_notification');
$mobileEvent = $this->createCampaignEvent($campaign, $mobileNotification, 'notification.send_mobile_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(['web-1', 'web-2a', 'web-2b'], $webNotification),
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(['mobile-1', 'mobile-2a', 'mobile-2b'], $mobileNotification),
'POST',
self::ONESIGNAL_API_BASE_URL,
500,
'Internal server error'
));
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$reason = 'Internal server error (500)';
$this->assertEventLogPassed($webEvent, $leadOne);
$this->assertEventLogFailed($mobileEvent, $leadTwo, $reason, true);
$this->assertEventLogFailed($mobileEvent, $leadThree, $reason, true);
$this->assertEventLogPassed($webEvent, $leadFour);
}
public function testNotificationsWithToken(): void
{
$notification = $this->createNotification($this->em);
$notification->setMessage('Message {contactfield=email}');
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadOne->setEmail('one@domain.tld');
$leadTwo = $this->createLeadInCampaign($campaign, ['web-2a', 'web-2b']);
$leadTwo->setEmail('two@domain.tld');
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->responseDataAssertion(
[
'include_player_ids' => ['web-1'],
'contents' => ['en' => 'Message '.$leadOne->getEmail()],
'headings' => ['en' => $notification->getHeading()],
'app_id' => self::API_ID,
],
'POST',
self::ONESIGNAL_API_BASE_URL,
400,
'Bad Request'
));
$this->transportMock->append($this->responseDataAssertion(
[
'include_player_ids' => ['web-2a', 'web-2b'],
'contents' => ['en' => 'Message '.$leadTwo->getEmail()],
'headings' => ['en' => $notification->getHeading()],
'app_id' => self::API_ID,
],
'POST',
self::ONESIGNAL_API_BASE_URL,
));
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$this->assertEventLogFailed($event, $leadOne, 'Bad Request (400)', true);
$this->assertEventLogPassed($event, $leadTwo);
}
public function testWebNotificationsWithUrlAndButtons(): void
{
$notification = $this->createNotification($this->em);
$notification->setUrl('https://some-url.tld');
$notification->setButton('Some button');
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadTwo = $this->createLeadInCampaign($campaign, []);
$leadThree = $this->createLeadInCampaign($campaign, ['web-2']);
$leadFour = $this->createLeadInCampaign($campaign, ['web-3a', 'web-3b']);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$this->createDoNotContact($leadOne, $notification);
$this->em->flush();
$this->em->clear();
$urlThree = $this->convertToTrackedUrl($notification, $leadThree);
$urlFour = $this->convertToTrackedUrl($notification, $leadFour);
$this->transportMock->append($this->responseDataAssertion(
[
'include_player_ids' => ['web-2'],
'contents' => ['en' => $notification->getMessage()],
'headings' => ['en' => $notification->getHeading()],
'url' => $urlThree,
'web_buttons' => [
[
'id' => $notification->getHeading(),
'text' => $notification->getButton(),
'url' => $urlThree,
],
],
'app_id' => self::API_ID,
],
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->responseDataAssertion(
[
'include_player_ids' => ['web-3a', 'web-3b'],
'contents' => ['en' => $notification->getMessage()],
'headings' => ['en' => $notification->getHeading()],
'url' => $urlFour,
'web_buttons' => [
[
'id' => $notification->getHeading(),
'text' => $notification->getButton(),
'url' => $urlFour,
],
],
'app_id' => self::API_ID,
],
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->triggerCampaigns();
$this->assertEventLogFailed($event, $leadOne, 'Contact is not contactable on the Web Notification channel.');
$this->assertEventLogFailed($event, $leadTwo, 'The contact has not subscribed to the Web Notification channel.');
$this->assertEventLogPassed($event, $leadThree);
$this->assertEventLogPassed($event, $leadFour);
}
public function testMobileNotificationsWithButtonsAndSettings(): void
{
$notification = $this->createNotification($this->em);
$notification->setMobile(true);
$notification->setButton('Some button');
$notification->setMobileSettings([
'ios_subtitle' => 'iOS Subtitle',
'android_led_color' => 'FF00DD',
]);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['mobile-1'], true);
$leadTwo = $this->createLeadInCampaign($campaign, ['mobile-2a', 'mobile-2b'], true);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_mobile_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->responseDataAssertion(
[
'include_player_ids' => ['mobile-1', 'mobile-2a', 'mobile-2b'],
'contents' => ['en' => $notification->getMessage()],
'headings' => ['en' => $notification->getHeading()],
'subtitle' => ['en' => $notification->getMobileSettings()['ios_subtitle']],
'android_led_color' => 'FF'.$notification->getMobileSettings()['android_led_color'],
'buttons' => [
[
'id' => $notification->getHeading(),
'text' => $notification->getButton(),
],
],
'app_id' => self::API_ID,
],
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$this->assertEventLogPassed($event, $leadOne);
$this->assertEventLogPassed($event, $leadTwo);
}
public function testNotificationsSentInBatches(): void
{
$subscriber = new class(static::getContainer()->get('mautic.helper.integration'), static::getContainer()->get('mautic.notification.model.notification'), static::getContainer()->get('mautic.notification.api'), static::getContainer()->get('event_dispatcher'), static::getContainer()->get('mautic.lead.model.dnc'), static::getContainer()->get('translator')) extends CampaignSubscriber {
protected const MAX_PLAYER_IDS_PER_REQUEST = 2;
};
static::getContainer()->set('mautic.notification.campaignbundle.subscriber', $subscriber);
$notification = $this->createNotification($this->em);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadTwo = $this->createLeadInCampaign($campaign, ['mobile-1'], true);
$leadThree = $this->createLeadInCampaign($campaign, ['web-2a', 'web-2b']);
$leadFour = $this->createLeadInCampaign($campaign, ['web-3']);
$leadFive = $this->createLeadInCampaign($campaign, ['web-4']);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(['web-1', 'web-2a'], $notification),
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(['web-2b', 'web-3'], $notification),
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(['web-4'], $notification),
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$this->assertEventLogPassed($event, $leadOne);
$this->assertEventLogFailed($event, $leadTwo, 'The contact has not subscribed to the Web Notification channel.');
$this->assertEventLogPassed($event, $leadThree);
$this->assertEventLogPassed($event, $leadFour);
$this->assertEventLogPassed($event, $leadFive);
}
/**
* @param string[] $pushIds
*/
private function createLeadInCampaign(Campaign $campaign, array $pushIds, bool $mobile = false): Lead
{
$lead = new Lead();
foreach ($pushIds as $pushId) {
$lead->addPushIDEntry($pushId, true, $mobile);
}
$this->em->persist($lead);
$campaignLead = new CampaignLead();
$campaignLead->setCampaign($campaign);
$campaignLead->setLead($lead);
$campaignLead->setDateAdded(new \DateTime());
$this->em->persist($campaignLead);
return $lead;
}
private function createCampaignEvent(Campaign $campaign, Notification $notification, string $type): CampaignEvent
{
$campaignEvent = new CampaignEvent();
$campaignEvent->setCampaign($campaign);
$campaignEvent->setName('Send notification');
$campaignEvent->setType($type);
$campaignEvent->setEventType('action');
$campaignEvent->setProperties(['notification' => $notification->getId()]);
$this->em->persist($campaignEvent);
return $campaignEvent;
}
private function triggerCampaigns(): void
{
$this->testSymfonyCommand('mautic:campaigns:trigger');
$this->em->clear();
}
/**
* @param mixed[] $expectedData
*/
private function responseDataAssertion(
array $expectedData,
string $expectedMethod = 'GET',
string $expectedUri = '',
int $status = 200,
?string $body = null,
): callable {
return static function (RequestInterface $request) use ($expectedData, $expectedMethod, $expectedUri, $status, $body) {
Assert::assertSame($expectedMethod, $request->getMethod());
Assert::assertSame($expectedUri, $request->getUri()->__toString());
Assert::assertSame(json_encode($expectedData), $request->getBody()->getContents());
$headers = $request->getHeaders();
unset($headers['Content-Length']);
Assert::assertSame([
'User-Agent' => ['GuzzleHttp/7'],
'Host' => ['onesignal.com'],
'Authorization' => ['Basic '.self::REST_API_ID],
'Content-Type' => ['application/json'],
], $headers);
return new Response($status, [], $body);
};
}
/**
* @param array<string> $pushIds
*
* @return array<mixed>
*/
private function getExpectedResponsePushIds(array $pushIds, Notification $notification): array
{
return array_merge(
['include_player_ids' => $pushIds],
[
'contents' => ['en' => $notification->getMessage()],
'headings' => ['en' => $notification->getHeading()],
'app_id' => self::API_ID,
]
);
}
private function noMoreRequestAssertion(): callable
{
return function () {
$this->fail('No other request was expected');
};
}
private function convertToTrackedUrl(Notification $notification, Lead $leadOne): string
{
/** @var AbstractNotificationApi $api */
$api = static::getContainer()->get('mautic.notification.api');
$clickThrough = [
'notification' => $notification->getId(),
'lead' => $leadOne->getId(),
];
return $api->convertToTrackedUrl($notification->getUrl(), $clickThrough, $notification);
}
private function assertEventLogPassed(CampaignEvent $event, Lead $leadOne): void
{
$log = $this->findEventLog($event, $leadOne);
Assert::assertFalse($log->getIsScheduled());
$metadata = $log->getMetadata();
Assert::assertIsArray($metadata);
Assert::assertArrayHasKey('status', $metadata);
Assert::assertSame('mautic.notification.timeline.status.delivered', $metadata['status']);
}
private function assertEventLogFailed(CampaignEvent $event, Lead $leadOne, ?string $reason, bool $isScheduled = false): void
{
$log = $this->findEventLog($event, $leadOne);
Assert::assertSame($isScheduled, $log->getIsScheduled());
$metadata = $log->getMetadata();
Assert::assertIsArray($metadata);
Assert::assertArrayHasKey('failed', $metadata);
Assert::assertSame(1, $metadata['failed']);
Assert::assertArrayHasKey('reason', $metadata);
Assert::assertSame($reason, $metadata['reason']);
}
private function findEventLog(CampaignEvent $event, Lead $leadOne): LeadEventLog
{
$log = $this->em->getRepository(LeadEventLog::class)->findOneBy([
'event' => $event->getId(),
'lead' => $leadOne,
'rotation' => 1,
]);
Assert::assertNotNull($log);
return $log;
}
private function createDoNotContact(Lead $lead, Notification $notification): DoNotContact
{
$doNotContact = new DoNotContact();
$doNotContact->setLead($lead);
$doNotContact->setChannel('notification');
$doNotContact->setChannelId($notification->getId());
$doNotContact->setReason(DoNotContact::UNSUBSCRIBED);
$doNotContact->setDateAdded(new \DateTime());
$this->em->persist($doNotContact);
return $doNotContact;
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests;
use Doctrine\ORM\EntityManagerInterface;
use GuzzleHttp\Handler\MockHandler;
use Mautic\CampaignBundle\Entity\Campaign;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use Symfony\Component\DependencyInjection\ContainerInterface;
trait NotificationTrait
{
private MockHandler $transportMock;
protected function setUp(): void
{
parent::setUp();
$this->transportMock = $this->getMockHandler(static::getContainer());
$this->setupIntegration(static::getContainer(), $this->em, self::API_ID, self::REST_API_ID);
}
private function getMockHandler(ContainerInterface $container): MockHandler
{
return $container->get(MockHandler::class);
}
private function createNotification(EntityManagerInterface $em): Notification
{
$notification = new Notification();
$notification->setName('Name 1');
$notification->setHeading('Heading 1');
$notification->setMessage('Message 1');
$em->persist($notification);
return $notification;
}
private function createCampaign(EntityManagerInterface $em): Campaign
{
$campaign = new Campaign();
$campaign->setName('Notification');
$em->persist($campaign);
return $campaign;
}
private function setupIntegration(ContainerInterface $container, EntityManagerInterface $em, string $apiId, string $restApiId): void
{
/** @var AbstractIntegration $integration */
$integration = $container->get('mautic.helper.integration')
->getIntegrationObject('OneSignal');
$integrationSettings = $integration->getIntegrationSettings();
$integrationSettings->setIsPublished(true);
$integration->encryptAndSetApiKeys([
'app_id' => $apiId,
'rest_api_key' => $restApiId,
], $integrationSettings);
$em->persist($integrationSettings);
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Unit\Api;
use Mautic\NotificationBundle\Api\OneSignalApi;
use PHPUnit\Framework\TestCase;
class OneSignalApiTest extends TestCase
{
public function testAddMobileData(): void
{
$mockOneSignalApi = $this->createMock(OneSignalApi::class);
$controllerReflection = (new \ReflectionClass(OneSignalApi::class));
$method = $controllerReflection->getMethod('addMobileData');
$method->setAccessible(true);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['ios_subtitle' => 'test']]);
$this->assertEquals(['subtitle' => ['en' => 'test']], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['ios_sound' => 'test']]);
$this->assertEquals(['ios_sound' => 'test'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['ios_sound' => '']]);
$this->assertEquals(['ios_sound' => 'default'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['ios_badges' => 'test']]);
$this->assertEquals(['ios_badgeType' => 'test'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['ios_badgeCount' => '5']]);
$this->assertEquals(['ios_badgeCount' => 5], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['ios_contentAvailable' => true]]);
$this->assertEquals(['content_available' => true], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['ios_mutableContent' => true]]);
$this->assertEquals(['mutable_content' => true], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_sound' => 'test']]);
$this->assertEquals(['android_sound' => 'test'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_small_icon' => 'test']]);
$this->assertEquals(['small_icon' => 'test'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_large_icon' => 'test']]);
$this->assertEquals(['large_icon' => 'test'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_big_picture' => 'test']]);
$this->assertEquals(['big_picture' => 'test'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_led_color' => 'test']]);
$this->assertEquals(['android_led_color' => 'FFTEST'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_accent_color' => 'test']]);
$this->assertEquals(['android_accent_color' => 'FFTEST'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_group_key' => 'test']]);
$this->assertEquals(['android_group' => 'test'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_lockscreen_visibility' => 1]]);
$this->assertEquals(['android_visibility' => 1], $data);
$data = [];
$mobileConfig = ['additional_data' => ['list' => [
['label' => 'a', 'value' => 1],
['label' => 'b', 'value' => 2],
],
],
];
$method->invokeArgs($mockOneSignalApi, [&$data, $mobileConfig]);
$this->assertEquals(['data' => ['a' => 1, 'b' => 2]], $data);
}
}

View File

@@ -0,0 +1,2 @@
mautic.notification.notice.batch_deleted="%count% notifications have been deleted!"
mautic.notification.error.notfound="No notification with id %id% was found!"

View File

@@ -0,0 +1,209 @@
mautic.campaign.notification.send_notification="Send Notification"
mautic.channel.mobile_notification="Mobile Push Notification"
mautic.notification.notification="Web Notification"
mautic.notification.notifications="Web Notifications"
mautic.notification.campaign.send_notification="Send web notification"
mautic.notification.campaign.send_notification.tooltip="Sends a web notification to the contact."
mautic.notification.mobile_notification="Mobile Notification"
mautic.notification.mobile_notifications="Mobile Notifications"
mautic.notification.campaign.send_mobile_notification="Send mobile notification"
mautic.notification.campaign.send_mobile_notification.tooltip="Sends a mobile notification to the contact if they have enabled notifications on their mobile device in your app."
mautic.notification.tab.ios="iOS"
mautic.notification.tab.android="Android"
mautic.notification.tab.data="Additional Data"
mautic.notification.tab.mobile="Mobile Settings"
mautic.integration.form.feature.mobile="Mobile app notifications"
mautic.integration.form.feature.landing_page_enabled="Enabled on landing pages?"
mautic.integration.form.features.landing_page_enabled.tooltip="Enable OneSignal on your Mautic landing pages?"
mautic.integration.form.feature.tracking_page_enabled="Enable on tracked pages?"
mautic.integration.form.features.tracking_page_enabled.tooltip="Enable OneSignal on websites that have embedded the mtc.js from this Mautic installation?"
mautic.integration.form.feature.welcome_notification_enabled="Welcome notification"
mautic.integration.form.platforms="Supported Platforms For Mobile Push"
mautic.integration.form.platforms.tooltip="Select the platforms that your OneSignal configuration will support. Only select platforms for which you have an app and have integrated the OneSignal SDK."
mautic.integration.form.platforms.ios="iOS"
mautic.integration.form.platforms.android="Android"
mautic.integration.form.platforms.error="If the mobile notifications feature is enabled, you must select at least one platform."
mautic.notification.form.mobile.url="URL"
mautic.notification.form.mobile.url.tooltip="Opens a URL when the notification is clicked."
mautic.notification.form.mobile.heading="Title"
mautic.notification.form.mobile.ios_subtitle="Message Subtitle"
mautic.notification.form.mobile.ios_subtitle.tooltip="This is an iOS 10 only feature"
mautic.notification.form.mobile.ios_sound="Sound"
mautic.notification.form.mobile.ios_sound.tooltip="Sound file will play when the notification is received by the device. This should either be left blank for the default sound or set to the name of a sound file in your app bundle."
mautic.notification.form.mobile.ios_badges="Badges"
mautic.notification.form.mobile.ios_badges.tooltip="Small number on the app icon on the home screen indicating the number of notifications received for your app. Clears when the app is opened."
mautic.notification.form.mobile.ios_badges.placeholder="Don't set or change"
mautic.notification.form.mobile.ios_badges.set="Set to"
mautic.notification.form.mobile.ios_badges.increment="Increase by"
mautic.notification.form.mobile.ios_badgecount="Badge count"
mautic.notification.form.mobile.ios_badgecount.tooltip="This will either set or increase the badge count depending on your previous selection."
mautic.notification.form.mobile.ios_contentavailable="Content Available"
mautic.notification.form.mobile.ios_contentavailable.tooltip="Only for native iOS apps. Wakes your app when the notification is received so you can do work in the background. See Apple's 'content-available' documentation for more details."
mautic.notification.form.mobile.ios_media="Media"
mautic.notification.form.mobile.ios_media.tooltip="Rich media attachment. Image, sound, or video to show when 3D touching the notification. Requires the OneSignal iOS 2.1.1 SDK or newer."
mautic.notification.form.mobile.ios_mutablecontent="Mutable Content"
mautic.notification.form.mobile.ios_mutablecontent.tooltip="Native only code running on iOS 10+. Allows you to modify the notification from your app before it is displayed. See Apple's 'mutable-content' documentation for more details."
mautic.notification.form.mobile.android_sound="Sound"
mautic.notification.form.mobile.android_small_icon="Small Icon"
mautic.notification.form.mobile.android_large_icon="Large Icon"
mautic.notification.form.mobile.android_group_key="Group Key"
mautic.notification.form.mobile.android_lockscreen_visibility="Lockscreen Visibility"
mautic.notification.form.mobile.android_lockscreen_visibility.placeholder="Public"
mautic.notification.form.mobile.android_lockscreen_visibility.private="Private"
mautic.notification.form.mobile.android_lockscreen_visibility.secret="Secret"
mautic.notification.form.mobile.android_big_picture="Big Picture"
mautic.notification.form.mobile.android_led_color="LED Color"
mautic.notification.form.mobile.android_accent_color="Accent Color"
mautic.notification.form.mobile.android_sound.tooltip="Sound resource will play when the notification is received by the device."
mautic.notification.form.mobile.android_small_icon.tooltip="Icon shows in the status bar. Also show to the left of the notification text unless a large icon is set."
mautic.notification.form.mobile.android_large_icon.tooltip="Requires Android 3.0+. Icon shows up to the left of the notification text."
mautic.notification.form.mobile.android_group_key.tooltip="Notifications with the same Group Key will be stacked together as a single summary notification with the number of unopened notifications."
mautic.notification.form.mobile.android_lockscreen_visibility.tooltip="Only applies to apps targeting Android API level 21+ running on Android 5.0+ devices."
mautic.notification.form.mobile.android_big_picture.tooltip="Requires Android 4.1+ Shows up in an expandable view below the notification text."
mautic.notification.form.mobile.android_led_color.tooltip="Sets the device's LED notification light if the device has one. Uses ARGB Hex value. The placeholder text shown is blue."
mautic.notification.form.mobile.android_accent_color.tooltip="Sets the circle color around your small icon that shows to the left of your notification text. Uses ARGB Hex value. The placeholder text is shown red. Only applies to apps targeting Android API level 21+ running on Android 5.0+ devices."
mautic.config.tab.notificationconfig="Web Notification Settings"
mautic.notification.config.form.notification.trackinggpage.enabled.tooltip="Enable Web Notifications on tracking pages?"
mautic.notification.config.form.notification.app_id="OneSignal App ID"
mautic.notification.config.form.notification.rest_api_key="OneSignal Rest API Key"
mautic.notification.config.form.notification.gcm_sender_id="Shared key for push notifications"
mautic.notification.config.form.notification.notification_safari_web_id="Web Notifications Provider Safari Web ID"
mautic.notification.config.form.notification.notification_safari_web_id.tooltip="One Signal Safari Web ID for your One Signal App"
mautic.notification.config.form.notification.icon="Web Notification Icon"
mautic.notification.config.form.notification.icon.tooltip="The icon that will be shown on the left side of your web notifications."
mautic.notification.form.action.sendnotification.admin="Send web notification to user"
mautic.notification.form.action.sendnotification.admin.descr="Send the selected web notification to the selected user(s) upon form submission."
mautic.notification.form.action.sendnotification.lead="Send web notification to contact"
mautic.notification.form.action.sendnotification.lead.descr="Send the web selected notification to the contact upon form submission."
mautic.notification.form.body="Body"
mautic.notification.form.confirmbatchdelete="Delete the selected web notifications?"
mautic.notification.form.confirmdelete="Delete the web notification, %name%?"
mautic.notification.form.confirmsend="Queue, %name%, for sending?"
mautic.notification.form.internal.name="Name"
mautic.notification.form.list="Contact list"
mautic.notification.header.edit="Edit Web Notification"
mautic.notification.header.new="New Web Notification"
mautic.notification.mobile.header.edit="Edit Mobile Notification"
mautic.notification.mobile.header.new="New Mobile Notification"
mautic.notification.text="Web Notification Content"
mautic.notification.text.tooltip="Your web notification content"
mautic.notification.headings="Web Notification Title"
mautic.notification.headings.tooltip="Your web notification title"
mautic.notification.link="Link"
mautic.notification.link.placeholder="http://"
mautic.notification.link.tooltip="When the user clicks the web notification, where do you want to send them?"
mautic.notification.permissions.mobile_notifications="Mobile Notifications - User has access to"
mautic.notification.permissions.header="Push Notification Permissions"
mautic.notification.permissions.notifications="Web Notifications - User has access to"
mautic.notification.contentblock.heading="Bring contacts back with timely web push"
mautic.notification.contentblock.subheading="Cut through the noise. Send timely, clickable messages directly to your contacts' desktop or mobile browsers even when they aren't actively on your site."
mautic.notification.contentblock.copy="These push notifications are perfect for announcements, alerts, and re-engagement efforts, grabbing attention immediately."
mautic.notification.mobile.contentblock.heading="Deepen engagement within your mobile app"
mautic.notification.mobile.contentblock.subheading="Send push notifications to users who have installed your iOS/Android app and opted-in to receive them."
mautic.notification.mobile.contentblock.copy="It's the perfect channel for driving feature adoption, promoting in-app actions, and keeping users engaged with your mobile application."
mautic.notification.stats="Notification Stats"
mautic.notification.stats.report.table="Notifications Sent"
mautic.notification.stat.leadcount="%count% Pending"
mautic.notification.stat.readcount="%count% Read"
mautic.notification.stat.sentcount="%count% Sent"
mautic.notification.type.header="What type of notification do you want to create?"
mautic.notification.type.list="Segment Notifications"
mautic.notification.type.list.header="New Segment Notification"
mautic.notification.type.list.description="A segment notification can be manually sent to selected contact segments. Once the notification has been sent, it cannot be edited. However, it can be sent to new contacts as they are added to the associated segments."
mautic.notification.type.template="Triggered Notifications"
mautic.notification.type.template.header="New Triggered Notification"
mautic.notification.type.template.description="A triggered notification is automatically sent by campaigns, forms, point events, etc. These can be edited but cannot be sent to a contact segments."
mautic.notification.form.internal.description="Description"
mautic.notification.form.heading="Heading"
mautic.notification.form.url="Link"
mautic.notification.form.url.tooltip="The destination the user is sent to when they click the notification."
mautic.notification.form.button="Action Button Text"
mautic.notification.form.button.tooltip="Add action button to the notification (Chrome only)"
mautic.notification.form.message="Message"
mautic.notification.send.selectnotifications="Select Notification"
mautic.notification.choose.notifications="Select the notification to send to the user."
mautic.notification.send.new.notification="New Notification"
mautic.notification.send.edit.notification="Edit Notification"
mautic.notification.send.preview.notification="Preview Notification"
mautic.notification.preview="Preview"
mautic.notification.notification.header.preview="Web Notification Preview"
mautic.notification.timeline.status="Status"
mautic.notification.timeline.type="Type"
mautic.notification.timeline.status.delivered="Delivered"
mautic.notification.timeline.status.failed="Failed"
mautic.notification.config.form.notification.safari_web_id="Safari Web ID"
mautic.notification.campaign.failed.not_contactable="Contact is not contactable on the Web Notification channel."
mautic.notification.campaign.failed.not_subscribed="The contact has not subscribed to the Web Notification channel."
mautic.notification.campaign.failed.missing_entity="The specified Web Notification entity does not exist."
mautic.notification.campaign.failed.unpublished="The specified Web Notification is unpublished."
mautic.notification.show.total.sent="Total sent"
mautic.notification.campaign.event.notification.has.active="Has active notification"
mautic.campaign.notification.has.active="Has active notification"
mautic.notification.campaign.event.notification.has.active.desc="Condition check If contact has active notification."
mautic.report.group.mobile_notifications="Mobile Notifications"
mautic.mobile_notification.stats.report.table="Mobile Notifications sent"
mautic.mobile_notification.report.hits_count="Hits Count"
mautic.mobile_notification.report.hits_ratio="Hits Ratio"
mautic.mobile_notification.report.read_count="Read Count"
mautic.mobile_notification.report.read_ratio="Read Ratio"
mautic.mobile_notification.report.sent_count="Sent Count"
mautic.mobile_notifications.report.stat.date_read="Date Read"
mautic.mobile_notifications.report.stat.date_sent="Date Sent"
mautic.mobile_notification.report.unique_hits_count="Unique Hits"
mautic.mobile_notification.report.unique_ratio="Unique Ratio"
mautic.notification.mobile_notification.heading="Title"
mautic.notification.mobile_notification.preview="Preview notification"
mautic.notification.mobile_notification.header.preview="Mobile notification"
mautic.mobile_notification.graph.line.stats.read="Read"
mautic.mobile_notification.graph.line.stats.sent="Sent"
mautic.mobile_notification.graph.line.stats="Mobile Notifications sent"
mautic.mobile_notification.graph.pie.ignored.read.failed.ignored="Ignored"
mautic.mobile_notification.graph.pie.ignored.read.failed.read="Read"
mautic.mobile_notification.graph.pie.ignored.read.failed="Ignored / Read / Failed mobile notifications"
mautic.notification.actions="Notification actions"
mautic.notification.actions.mobile_tooltip="Send the selected mobile notification to the user when a form is filled out if they have mobile notifications enabled."
mautic.notification.actions.send_mobile_notification="Send mobile notification"
mautic.notification.form.subdomain_name.label="Subdomain name"
mautic.campaign.notification.send_mobile_notification="Send mobile notification"
mautic.config.tab.notification_config="Notification Settings"
mautic.config.tab.campaign_notification_config="Campaign Notification Settings"
mautic.config.tab.webhook_notification_config="Webhook Notification Settings"
mautic.core.config.header.campaign_notification_config.description="Specify who receives alerts and updates about campaign activities."
mautic.core.config.header.webhook_notification_config.description="Define recipients for webhook-related notifications and alerts."
mautic.notification.form.config.send_notification_to_author ="Send notification to author"
mautic.notification.form.config.send_notification_to_author.tooltip = "Send notification to the author or other users email addresses."
mautic.notification.form.config.notification_email_addresses ="Email addresses to receive notifications"
mautic.notification.form.config.notification_email_addresses.tooltip = "Add comma separated list of email addresses to receive notifications"
mautic.notification.notification.header="Web Notification"
mautic.notification.mobile_notification.header="Mobile Notification"
mautic.report.source.mobile_notifications="Mobile Notifications"
mautic.report.source.mobile_notifications.stats="Mobile Notifications sent"
mautic.notification.preview.more_notifications="2 more notifications"

View File

@@ -0,0 +1 @@
mautic.notification.choosenotification.notblank="Please select a notification"