Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle;
|
||||
|
||||
/**
|
||||
* Events available for AssetBundle.
|
||||
*/
|
||||
final class AssetEvents
|
||||
{
|
||||
/**
|
||||
* The mautic.asset_on_load event is dispatched when a public asset is downloaded, publicly viewed, or redirected to (remote).
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\AssetBundle\Event\AssetLoadEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ASSET_ON_LOAD = 'mautic.asset_on_load';
|
||||
|
||||
/**
|
||||
* The mautic.asset_on_remote_browse event is dispatched when browsing a remote provider.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\AssetBundle\Event\RemoteAssetBrowseEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ASSET_ON_REMOTE_BROWSE = 'mautic.asset_on_remote_browse';
|
||||
|
||||
/**
|
||||
* The mautic.asset_on_upload event is dispatched before uploading a file.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\AssetBundle\Event\AssetEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ASSET_ON_UPLOAD = 'mautic.asset_on_upload';
|
||||
|
||||
/**
|
||||
* The mautic.asset_pre_save event is dispatched right before a asset is persisted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\AssetBundle\Event\AssetEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ASSET_PRE_SAVE = 'mautic.asset_pre_save';
|
||||
|
||||
/**
|
||||
* The mautic.asset_post_save event is dispatched right after a asset is persisted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\AssetBundle\Event\AssetEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ASSET_POST_SAVE = 'mautic.asset_post_save';
|
||||
|
||||
/**
|
||||
* The mautic.asset_pre_delete event is dispatched prior to when a asset is deleted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\AssetBundle\Event\AssetEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ASSET_PRE_DELETE = 'mautic.asset_pre_delete';
|
||||
|
||||
/**
|
||||
* The mautic.asset_post_delete event is dispatched after a asset is deleted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\AssetBundle\Event\AssetEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ASSET_POST_DELETE = 'mautic.asset_post_delete';
|
||||
|
||||
/**
|
||||
* The mautic.asset.on_campaign_trigger_decision event is fired when the campaign action triggers.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\CampaignBundle\Event\CampaignExecutionEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ON_CAMPAIGN_TRIGGER_DECISION = 'mautic.asset.on_campaign_trigger_decision';
|
||||
|
||||
/**
|
||||
* The mautic.asset.on_download_rate_winner event is fired when there is a need to determine download rate winner.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\CoreBundles\Event\DetermineWinnerEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ON_DETERMINE_DOWNLOAD_RATE_WINNER = 'mautic.asset.on_download_rate_winner';
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
.thumbnail-preview img {
|
||||
height: 100px;
|
||||
}
|
||||
.modal-body-content iframe, .preview-detail iframe {
|
||||
height: 400px;
|
||||
}
|
||||
.form-group.preview img {
|
||||
max-height: 260px;
|
||||
}
|
||||
|
||||
@-webkit-keyframes passing-through { 0% { opacity: 0; -webkit-transform: translateY(40px); -moz-transform: translateY(40px); -ms-transform: translateY(40px); -o-transform: translateY(40px); transform: translateY(40px); }
|
||||
30%, 70% { opacity: 1; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -ms-transform: translateY(0px); -o-transform: translateY(0px); transform: translateY(0px); }
|
||||
100% { opacity: 0; -webkit-transform: translateY(-40px); -moz-transform: translateY(-40px); -ms-transform: translateY(-40px); -o-transform: translateY(-40px); transform: translateY(-40px); } }
|
||||
@-moz-keyframes passing-through { 0% { opacity: 0; -webkit-transform: translateY(40px); -moz-transform: translateY(40px); -ms-transform: translateY(40px); -o-transform: translateY(40px); transform: translateY(40px); }
|
||||
30%, 70% { opacity: 1; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -ms-transform: translateY(0px); -o-transform: translateY(0px); transform: translateY(0px); }
|
||||
100% { opacity: 0; -webkit-transform: translateY(-40px); -moz-transform: translateY(-40px); -ms-transform: translateY(-40px); -o-transform: translateY(-40px); transform: translateY(-40px); } }
|
||||
@keyframes passing-through { 0% { opacity: 0; -webkit-transform: translateY(40px); -moz-transform: translateY(40px); -ms-transform: translateY(40px); -o-transform: translateY(40px); transform: translateY(40px); }
|
||||
30%, 70% { opacity: 1; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -ms-transform: translateY(0px); -o-transform: translateY(0px); transform: translateY(0px); }
|
||||
100% { opacity: 0; -webkit-transform: translateY(-40px); -moz-transform: translateY(-40px); -ms-transform: translateY(-40px); -o-transform: translateY(-40px); transform: translateY(-40px); } }
|
||||
@-webkit-keyframes slide-in { 0% { opacity: 0; -webkit-transform: translateY(40px); -moz-transform: translateY(40px); -ms-transform: translateY(40px); -o-transform: translateY(40px); transform: translateY(40px); }
|
||||
30% { opacity: 1; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -ms-transform: translateY(0px); -o-transform: translateY(0px); transform: translateY(0px); } }
|
||||
@-moz-keyframes slide-in { 0% { opacity: 0; -webkit-transform: translateY(40px); -moz-transform: translateY(40px); -ms-transform: translateY(40px); -o-transform: translateY(40px); transform: translateY(40px); }
|
||||
30% { opacity: 1; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -ms-transform: translateY(0px); -o-transform: translateY(0px); transform: translateY(0px); } }
|
||||
@keyframes slide-in { 0% { opacity: 0; -webkit-transform: translateY(40px); -moz-transform: translateY(40px); -ms-transform: translateY(40px); -o-transform: translateY(40px); transform: translateY(40px); }
|
||||
30% { opacity: 1; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -ms-transform: translateY(0px); -o-transform: translateY(0px); transform: translateY(0px); } }
|
||||
@-webkit-keyframes pulse { 0% { -webkit-transform: scale(1); -moz-transform: scale(1); -ms-transform: scale(1); -o-transform: scale(1); transform: scale(1); }
|
||||
10% { -webkit-transform: scale(1.1); -moz-transform: scale(1.1); -ms-transform: scale(1.1); -o-transform: scale(1.1); transform: scale(1.1); }
|
||||
20% { -webkit-transform: scale(1); -moz-transform: scale(1); -ms-transform: scale(1); -o-transform: scale(1); transform: scale(1); } }
|
||||
@-moz-keyframes pulse { 0% { -webkit-transform: scale(1); -moz-transform: scale(1); -ms-transform: scale(1); -o-transform: scale(1); transform: scale(1); }
|
||||
10% { -webkit-transform: scale(1.1); -moz-transform: scale(1.1); -ms-transform: scale(1.1); -o-transform: scale(1.1); transform: scale(1.1); }
|
||||
20% { -webkit-transform: scale(1); -moz-transform: scale(1); -ms-transform: scale(1); -o-transform: scale(1); transform: scale(1); } }
|
||||
@keyframes pulse { 0% { -webkit-transform: scale(1); -moz-transform: scale(1); -ms-transform: scale(1); -o-transform: scale(1); transform: scale(1); }
|
||||
10% { -webkit-transform: scale(1.1); -moz-transform: scale(1.1); -ms-transform: scale(1.1); -o-transform: scale(1.1); transform: scale(1.1); }
|
||||
20% { -webkit-transform: scale(1); -moz-transform: scale(1); -ms-transform: scale(1); -o-transform: scale(1); transform: scale(1); } }
|
||||
.mdropzone, .mdropzone * { box-sizing: border-box; }
|
||||
|
||||
.mdropzone { min-height: 150px; border: none; background: var(--field); padding: 30px 30px; transition: var(--transition-all-productive); border-radius: var(--border-radius-md);}
|
||||
.mdropzone:hover { background: var(--field-hover); }
|
||||
.has-error .mdropzone {border: 2px solid #a94442;}
|
||||
.is-success .mdropzone {border: 2px solid #00a08a;}
|
||||
.mdropzone.dz-clickable { cursor: pointer; }
|
||||
.mdropzone.dz-clickable * { cursor: default; }
|
||||
.mdropzone.dz-clickable .dz-message, .mdropzone.dz-clickable .dz-message * { cursor: pointer; }
|
||||
.mdropzone.dz-started .dz-message { display: none; }
|
||||
.mdropzone.dz-drag-hover { border-style: solid; }
|
||||
.mdropzone.dz-drag-hover .dz-message { opacity: 0.5; }
|
||||
.mdropzone .dz-message { text-align: center; margin: 2em 0; }
|
||||
.mdropzone .dz-preview { position: relative; display: inline-block; vertical-align: top; margin: 16px; min-height: 100px; }
|
||||
.mdropzone .dz-preview:hover { z-index: 1000; }
|
||||
.mdropzone .dz-preview:hover .dz-details { opacity: 1; }
|
||||
.mdropzone .dz-preview.dz-file-preview .dz-image { border-radius: 20px; background: #999; background: linear-gradient(to bottom, #eee, #ddd); }
|
||||
.mdropzone .dz-preview.dz-file-preview .dz-details { opacity: 1; }
|
||||
.mdropzone .dz-preview.dz-image-preview { background: white; }
|
||||
.mdropzone .dz-preview.dz-image-preview .dz-details { -webkit-transition: opacity 0.2s linear; -moz-transition: opacity 0.2s linear; -ms-transition: opacity 0.2s linear; -o-transition: opacity 0.2s linear; transition: opacity 0.2s linear; }
|
||||
.mdropzone .dz-preview .dz-remove { font-size: 14px; text-align: center; display: block; cursor: pointer; border: none; }
|
||||
.mdropzone .dz-preview .dz-remove:hover { text-decoration: underline; }
|
||||
.mdropzone .dz-preview:hover .dz-details { opacity: 1; }
|
||||
.mdropzone .dz-preview .dz-details { z-index: 20; position: absolute; top: 0; left: 0; opacity: 0; font-size: 13px; min-width: 100%; max-width: 100%; padding: 2em 1em; text-align: center; color: rgba(0, 0, 0, 0.9); line-height: 150%; }
|
||||
.mdropzone .dz-preview .dz-details .dz-size { margin-bottom: 1em; font-size: 16px; }
|
||||
.mdropzone .dz-preview .dz-details .dz-filename { white-space: nowrap; }
|
||||
.mdropzone .dz-preview .dz-details .dz-filename:hover span { border: 1px solid rgba(200, 200, 200, 0.8); background-color: rgba(255, 255, 255, 0.8); }
|
||||
.mdropzone .dz-preview .dz-details .dz-filename:not(:hover) { overflow: hidden; text-overflow: ellipsis; }
|
||||
.mdropzone .dz-preview .dz-details .dz-filename:not(:hover) span { border: 1px solid transparent; }
|
||||
.mdropzone .dz-preview .dz-details .dz-filename span, .mdropzone .dz-preview .dz-details .dz-size span { background-color: rgba(255, 255, 255, 0.4); padding: 0 0.4em; border-radius: 3px; }
|
||||
.mdropzone .dz-preview:hover .dz-image img { -webkit-transform: scale(1.05, 1.05); -moz-transform: scale(1.05, 1.05); -ms-transform: scale(1.05, 1.05); -o-transform: scale(1.05, 1.05); transform: scale(1.05, 1.05); -webkit-filter: blur(8px); filter: blur(8px); }
|
||||
.mdropzone .dz-preview .dz-image { border-radius: 20px; overflow: hidden; width: 120px; height: 120px; position: relative; display: block; z-index: 10; }
|
||||
.mdropzone .dz-preview .dz-image img { display: block; }
|
||||
.mdropzone .dz-preview.dz-success .dz-success-mark { -webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); -moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); -ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); -o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); }
|
||||
.mdropzone .dz-preview.dz-error .dz-error-mark { opacity: 1; -webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); -moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); -ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); -o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); }
|
||||
.mdropzone .dz-preview .dz-success-mark, .mdropzone .dz-preview .dz-error-mark { pointer-events: none; opacity: 0; z-index: 500; position: absolute; display: block; top: 50%; left: 50%; margin-left: -27px; margin-top: -27px; }
|
||||
.mdropzone .dz-preview .dz-success-mark svg, .mdropzone .dz-preview .dz-error-mark svg { display: block; width: 54px; height: 54px; }
|
||||
.mdropzone .dz-preview.dz-processing .dz-progress { opacity: 1; -webkit-transition: all 0.2s linear; -moz-transition: all 0.2s linear; -ms-transition: all 0.2s linear; -o-transition: all 0.2s linear; transition: all 0.2s linear; }
|
||||
.mdropzone .dz-preview.dz-complete .dz-progress { opacity: 0; -webkit-transition: opacity 0.4s ease-in; -moz-transition: opacity 0.4s ease-in; -ms-transition: opacity 0.4s ease-in; -o-transition: opacity 0.4s ease-in; transition: opacity 0.4s ease-in; }
|
||||
.mdropzone .dz-preview:not(.dz-processing) .dz-progress { -webkit-animation: pulse 6s ease infinite; -moz-animation: pulse 6s ease infinite; -ms-animation: pulse 6s ease infinite; -o-animation: pulse 6s ease infinite; animation: pulse 6s ease infinite; }
|
||||
.mdropzone .dz-preview .dz-progress { opacity: 1; z-index: 1000; pointer-events: none; position: absolute; height: 16px; left: 50%; top: 50%; margin-top: -8px; width: 80px; margin-left: -40px; background: rgba(255, 255, 255, 0.9); -webkit-transform: scale(1); border-radius: 8px; overflow: hidden; }
|
||||
.mdropzone .dz-preview .dz-progress .dz-upload { background: #333; background: linear-gradient(to bottom, #666, #444); position: absolute; top: 0; left: 0; bottom: 0; width: 0; -webkit-transition: width 300ms ease-in-out; -moz-transition: width 300ms ease-in-out; -ms-transition: width 300ms ease-in-out; -o-transition: width 300ms ease-in-out; transition: width 300ms ease-in-out; }
|
||||
.mdropzone .dz-preview.dz-error .dz-error-message { display: block; }
|
||||
.mdropzone .dz-preview.dz-error:hover .dz-error-message { opacity: 1; pointer-events: auto; }
|
||||
.mdropzone .dz-preview .dz-error-message { pointer-events: none; z-index: 1000; position: absolute; display: block; display: none; opacity: 0; -webkit-transition: opacity 0.3s ease; -moz-transition: opacity 0.3s ease; -ms-transition: opacity 0.3s ease; -o-transition: opacity 0.3s ease; transition: opacity 0.3s ease; border-radius: 8px; font-size: 13px; top: 130px; left: -10px; width: 140px; background: #be2626; background: linear-gradient(to bottom, #be2626, #a92222); padding: 0.5em 1.2em; color: white; }
|
||||
.mdropzone .dz-preview .dz-error-message:after { content: ''; position: absolute; top: -6px; left: 64px; width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-bottom: 6px solid #a94442; }
|
||||
|
||||
/*
|
||||
* The MIT License
|
||||
* Copyright (c) 2012 Matias Meno <m@tias.me>
|
||||
*/
|
||||
.mdropzone, .mdropzone * {
|
||||
box-sizing: border-box; }
|
||||
|
||||
.mdropzone {
|
||||
position: relative;
|
||||
}
|
||||
.mdropzone .dz-preview {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
margin: 0.5em;
|
||||
}
|
||||
.mdropzone .dz-preview .dz-progress {
|
||||
display: block;
|
||||
height: 15px;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
.mdropzone .dz-preview .dz-progress .dz-upload {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: green;
|
||||
}
|
||||
.mdropzone .dz-preview .dz-error-message {
|
||||
color: #fff;
|
||||
display: none;
|
||||
}
|
||||
.mdropzone .dz-preview.dz-error .dz-error-message, .mdropzone .dz-preview.dz-error .dz-error-mark {
|
||||
display: block;
|
||||
}
|
||||
.mdropzone .dz-preview.dz-success .dz-success-mark {
|
||||
display: block;
|
||||
}
|
||||
.mdropzone .dz-preview .dz-error-mark, .mdropzone .dz-preview .dz-success-mark {
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 30px;
|
||||
top: 30px;
|
||||
width: 54px;
|
||||
height: 58px;
|
||||
left: 50%;
|
||||
margin-left: -27px;
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
//AssetBundle
|
||||
Mautic.assetOnLoad = function (container) {
|
||||
if (typeof mauticAssetUploadEndpoint !== 'undefined' && typeof Mautic.assetDropzone == 'undefined' && mQuery('div#dropzone').length) {
|
||||
Mautic.initializeDropzone();
|
||||
}
|
||||
};
|
||||
|
||||
Mautic.assetOnUnload = function(id) {
|
||||
if (id === '#app-content') {
|
||||
delete Mautic.assetDropzone;
|
||||
}
|
||||
};
|
||||
|
||||
Mautic.updateRemoteBrowser = function(provider, path) {
|
||||
path = typeof path !== 'undefined' ? path : '';
|
||||
|
||||
var spinner = mQuery('<i class="ri-loader-3-line ri-spin ri-fw"></i>');
|
||||
spinner.appendTo('#tab' + provider + ' a');
|
||||
|
||||
mQuery.ajax({
|
||||
url: mauticAjaxUrl,
|
||||
type: "POST",
|
||||
data: "action=asset:fetchRemoteFiles&provider=" + provider + "&path=" + path,
|
||||
dataType: "json",
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
mQuery('div#remoteFileBrowser').html(response.output);
|
||||
|
||||
mQuery('.remote-file-search').quicksearch('#remoteFileBrowser .remote-file-list a');
|
||||
} else {
|
||||
const flashMessage = Mautic.addErrorFlashMessage(response.message);
|
||||
Mautic.setFlashes(flashMessage);
|
||||
}
|
||||
},
|
||||
error: function (request, textStatus, errorThrown) {
|
||||
Mautic.processAjaxError(request, textStatus, errorThrown);
|
||||
},
|
||||
complete: function() {
|
||||
spinner.remove();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
Mautic.selectRemoteFile = function(url) {
|
||||
mQuery('#asset_remotePath').val(url);
|
||||
mQuery('#RemoteFileModal').modal('hide');
|
||||
};
|
||||
|
||||
Mautic.changeAssetStorageLocation = function() {
|
||||
if (mQuery('#asset_storageLocation_0').prop('checked')) {
|
||||
mQuery('#storage-local').removeClass('hide');
|
||||
mQuery('#storage-remote').addClass('hide');
|
||||
mQuery('#remote-button').addClass('hide');
|
||||
} else {
|
||||
mQuery('#storage-local').addClass('hide');
|
||||
mQuery('#storage-remote').removeClass('hide');
|
||||
mQuery('#remote-button').removeClass('hide');
|
||||
}
|
||||
};
|
||||
|
||||
Mautic.initializeDropzone = function() {
|
||||
var options = {
|
||||
url: mauticAssetUploadEndpoint,
|
||||
uploadMultiple: false,
|
||||
filesizeBase: 1024,
|
||||
init: function() {
|
||||
this.on("addedfile", function() {
|
||||
if (this.files[1] != null) {
|
||||
this.removeFile(this.files[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof mauticAssetUploadMaxSize !== 'undefined') {
|
||||
options.maxFilesize = mauticAssetUploadMaxSize;
|
||||
}
|
||||
|
||||
if (typeof mauticAssetUploadMaxSizeError !== 'undefined') {
|
||||
options.dictFileTooBig = mauticAssetUploadMaxSizeError;
|
||||
}
|
||||
|
||||
if (typeof mauticAssetUploadExtensions !== 'undefined') {
|
||||
options.acceptedFiles = mauticAssetUploadExtensions;
|
||||
}
|
||||
|
||||
if (typeof mauticAssetUploadExtensionError !== 'undefined') {
|
||||
options.dictInvalidFileType = mauticAssetUploadExtensionError;
|
||||
}
|
||||
|
||||
Mautic.assetDropzone = new Dropzone("div#dropzone", options);
|
||||
var preview = mQuery('.preview div.text-center');
|
||||
|
||||
Mautic.assetDropzone.on("sending", function (file, request, formData) {
|
||||
request.setRequestHeader('X-CSRF-Token', mauticAjaxCsrf);
|
||||
formData.append('tempId', mQuery('#asset_tempId').val());
|
||||
}).on("addedfile", function (file) {
|
||||
preview.fadeOut('fast');
|
||||
}).on("success", function (file, response, progress) {
|
||||
if (response.tmpFileName) {
|
||||
mQuery('#asset_tempName').val(response.tmpFileName);
|
||||
}
|
||||
|
||||
var messageArea = mQuery('.mdropzone-error');
|
||||
if (response.error || !response.tmpFileName) {
|
||||
if (!response.error) {
|
||||
var errorText = '';
|
||||
} else {
|
||||
var errorText = (typeof response.error == 'object') ? response.error.text : response.error;
|
||||
}
|
||||
|
||||
messageArea.text(errorText);
|
||||
messageArea.closest('.form-group').addClass('has-error').removeClass('is-success');
|
||||
|
||||
// invoke the error
|
||||
var node, _i, _len, _ref, _results;
|
||||
file.previewElement.classList.add('dz-error');
|
||||
_ref = file.previewElement.querySelectorAll('data-dz-errormessage');
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
node = _ref[_i];
|
||||
_results.push(node.textContent = errorText);
|
||||
}
|
||||
return _results;
|
||||
} else {
|
||||
messageArea.text('');
|
||||
messageArea.closest('.form-group').removeClass('has-error').addClass('is-success');
|
||||
}
|
||||
|
||||
var titleInput = mQuery('#asset_title');
|
||||
if (file.name && !titleInput.val()) {
|
||||
titleInput.val(file.name);
|
||||
}
|
||||
|
||||
if (file.name) {
|
||||
mQuery('#asset_originalFileName').val(file.name);
|
||||
}
|
||||
}).on("error", function (file, response) {
|
||||
preview.fadeIn('fast');
|
||||
var messageArea = mQuery('.mdropzone-error');
|
||||
|
||||
// Dropzone error is just a text in the response var
|
||||
if (typeof response == "string") {
|
||||
response = {'error': response};
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
if (!response.error) {
|
||||
var errorText = '';
|
||||
} else {
|
||||
var errorText = (typeof response.error == 'object') ? response.error.text : response.error;
|
||||
}
|
||||
|
||||
messageArea.text(errorText);
|
||||
messageArea.closest('.form-group').addClass('has-error').removeClass('is-success');
|
||||
|
||||
// invoke the error
|
||||
var node, _i, _len, _ref, _results;
|
||||
file.previewElement.classList.add('dz-error');
|
||||
_ref = file.previewElement.querySelectorAll('[data-dz-errormessage]');
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
node = _ref[_i];
|
||||
_results.push(node.textContent = errorText);
|
||||
}
|
||||
return _results;
|
||||
}
|
||||
}).on("thumbnail", function (file, url) {
|
||||
if (file.accepted === true) {
|
||||
var extension = file.name.substr((file.name.lastIndexOf('.') +1)).toLowerCase();
|
||||
var previewContent = '';
|
||||
|
||||
if (mQuery.inArray(extension, ['jpg', 'jpeg', 'gif', 'png']) !== -1) {
|
||||
previewContent = mQuery('<img />').addClass('img-thumbnail').attr('src', url);
|
||||
} else if (extension === 'pdf') {
|
||||
previewContent = mQuery('<iframe />').attr('src', url);
|
||||
}
|
||||
|
||||
preview.empty().html(previewContent);
|
||||
preview.fadeIn('fast');
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'routes' => [
|
||||
'main' => [
|
||||
'mautic_asset_index' => [
|
||||
'path' => '/assets/{page}',
|
||||
'controller' => 'Mautic\AssetBundle\Controller\AssetController::indexAction',
|
||||
],
|
||||
'mautic_asset_remote' => [
|
||||
'path' => '/assets/remote',
|
||||
'controller' => 'Mautic\AssetBundle\Controller\AssetController::remoteAction',
|
||||
],
|
||||
'mautic_asset_action' => [
|
||||
'path' => '/assets/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\AssetBundle\Controller\AssetController::executeAction',
|
||||
],
|
||||
],
|
||||
'api' => [
|
||||
'mautic_api_assetsstandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'assets',
|
||||
'path' => '/assets',
|
||||
'controller' => Mautic\AssetBundle\Controller\Api\AssetApiController::class,
|
||||
],
|
||||
],
|
||||
'public' => [
|
||||
'mautic_asset_download' => [
|
||||
'path' => '/asset/{slug}',
|
||||
'controller' => 'Mautic\AssetBundle\Controller\PublicController::downloadAction',
|
||||
'defaults' => [
|
||||
'slug' => '',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'menu' => [
|
||||
'main' => [
|
||||
'items' => [
|
||||
'mautic.asset.assets' => [
|
||||
'route' => 'mautic_asset_index',
|
||||
'access' => ['asset:assets:viewown', 'asset:assets:viewother'],
|
||||
'parent' => 'mautic.core.components',
|
||||
'priority' => 300,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'categories' => [
|
||||
'asset' => [
|
||||
'class' => Mautic\AssetBundle\Entity\Asset::class,
|
||||
],
|
||||
],
|
||||
|
||||
'services' => [
|
||||
'permissions' => [
|
||||
'mautic.asset.permissions' => [
|
||||
'class' => Mautic\AssetBundle\Security\Permissions\AssetPermissions::class,
|
||||
'arguments' => [
|
||||
'mautic.helper.core_parameters',
|
||||
],
|
||||
],
|
||||
],
|
||||
'others' => [
|
||||
'mautic.asset.upload.error.handler' => [
|
||||
'class' => Mautic\AssetBundle\ErrorHandler\DropzoneErrorHandler::class,
|
||||
],
|
||||
// Override the DropzoneController
|
||||
'oneup_uploader.controller.dropzone.class' => [
|
||||
'class' => Mautic\AssetBundle\Controller\UploadController::class,
|
||||
],
|
||||
],
|
||||
'fixtures' => [
|
||||
'mautic.asset.fixture.asset' => [
|
||||
'class' => Mautic\AssetBundle\DataFixtures\ORM\LoadAssetData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'parameters' => [
|
||||
'upload_dir' => '%mautic.application_dir%/media/files',
|
||||
'max_size' => '6',
|
||||
'allowed_extensions' => ['csv', 'doc', 'docx', 'epub', 'gif', 'jpg', 'jpeg', 'mpg', 'mpeg', 'mp3', 'odt', 'odp', 'ods', 'pdf', 'png', 'ppt', 'pptx', 'tif', 'tiff', 'txt', 'xls', 'xlsx', 'wav'],
|
||||
'streamed_extensions' => ['gif', 'jpg', 'jpeg', 'mpg', 'mpeg', 'mp3', 'pdf', 'png', 'wav'],
|
||||
'allowed_mimetypes' => [
|
||||
'csv' => 'text/csv',
|
||||
'doc' => 'application/msword',
|
||||
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'epub' => 'application/epub+zip',
|
||||
'gif' => 'image/gif',
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'mpg' => 'video/mpeg',
|
||||
'mpeg' => 'video/mpeg',
|
||||
'mp3' => 'audio/mpeg',
|
||||
'odt' => 'application/vnd.oasis.opendocument.text',
|
||||
'odp' => 'application/vnd.oasis.opendocument.presentation',
|
||||
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
|
||||
'pdf' => 'application/pdf',
|
||||
'png' => 'image/png',
|
||||
'ppt' => 'application/vnd.ms-powerpoint',
|
||||
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'tif' => 'image/tiff',
|
||||
'tiff' => 'image/tiff',
|
||||
'txt' => 'text/plain',
|
||||
'xls' => 'application/vnd.ms-excel',
|
||||
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'wav' => 'audio/wav',
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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 = [
|
||||
'Controller/UploadController.php',
|
||||
];
|
||||
|
||||
$services->load('Mautic\\AssetBundle\\', '../')
|
||||
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
|
||||
|
||||
$services->load('Mautic\\AssetBundle\\Entity\\', '../Entity/*Repository.php')
|
||||
->tag(Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass::REPOSITORY_SERVICE_TAG);
|
||||
$services->alias('mautic.asset.helper.token', Mautic\AssetBundle\Helper\TokenHelper::class);
|
||||
$services->alias('mautic.asset.model.asset', Mautic\AssetBundle\Model\AssetModel::class);
|
||||
$services->alias(Oneup\UploaderBundle\Templating\Helper\UploaderHelper::class, 'oneup_uploader.templating.uploader_helper');
|
||||
$services->alias('mautic.asset.repository.download', Mautic\AssetBundle\Entity\DownloadRepository::class);
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Controller;
|
||||
|
||||
use Gaufrette\Filesystem;
|
||||
use Mautic\AssetBundle\AssetEvents;
|
||||
use Mautic\AssetBundle\Event\RemoteAssetBrowseEvent;
|
||||
use Mautic\AssetBundle\Model\AssetModel;
|
||||
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class AjaxController extends CommonAjaxController
|
||||
{
|
||||
public function categoryListAction(Request $request): \Symfony\Component\HttpFoundation\JsonResponse
|
||||
{
|
||||
$assetModel = $this->getModel('asset');
|
||||
\assert($assetModel instanceof AssetModel);
|
||||
$filter = InputHelper::clean($request->query->get('filter'));
|
||||
$results = $assetModel->getLookupResults('category', $filter, 10);
|
||||
$dataArray = [];
|
||||
foreach ($results as $r) {
|
||||
$dataArray[] = [
|
||||
'label' => $r['title']." ({$r['id']})",
|
||||
'value' => $r['id'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function fetchRemoteFilesAction(Request $request, IntegrationHelper $integrationHelper): \Symfony\Component\HttpFoundation\JsonResponse
|
||||
{
|
||||
$provider = InputHelper::string($request->request->get('provider'));
|
||||
$path = InputHelper::string($request->request->get('path', ''));
|
||||
$dispatcher = $this->dispatcher;
|
||||
$name = AssetEvents::ASSET_ON_REMOTE_BROWSE;
|
||||
if (!$dispatcher->hasListeners($name)) {
|
||||
return $this->sendJsonResponse(['success' => 0]);
|
||||
}
|
||||
|
||||
/** @var \Mautic\PluginBundle\Integration\AbstractIntegration $integration */
|
||||
$integration = $integrationHelper->getIntegrationObject($provider);
|
||||
|
||||
$event = new RemoteAssetBrowseEvent($integration);
|
||||
$dispatcher->dispatch($event, $name);
|
||||
|
||||
if (!$adapter = $event->getAdapter()) {
|
||||
return $this->sendJsonResponse([
|
||||
'success' => 0,
|
||||
'message' => $event->getFailureMessage() ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
$connector = new Filesystem($adapter);
|
||||
|
||||
$output = $this->renderView(
|
||||
'@MauticAsset/Remote/list.html.twig',
|
||||
[
|
||||
'connector' => $connector,
|
||||
'integration' => $integration,
|
||||
'items' => $connector->listKeys($path),
|
||||
]
|
||||
);
|
||||
|
||||
return $this->sendJsonResponse(['success' => 1, 'output' => $output]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\AssetBundle\Model\AssetModel;
|
||||
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 Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<Asset>
|
||||
*/
|
||||
class AssetApiController extends CommonApiController
|
||||
{
|
||||
/**
|
||||
* @var AssetModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
public function __construct(
|
||||
CorePermissions $security,
|
||||
Translator $translator,
|
||||
EntityResultHelper $entityResultHelper,
|
||||
RouterInterface $router,
|
||||
FormFactoryInterface $formFactory,
|
||||
AppVersion $appVersion,
|
||||
RequestStack $requestStack,
|
||||
private CoreParametersHelper $parametersHelper,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
$assetModel = $modelFactory->getModel('asset');
|
||||
\assert($assetModel instanceof AssetModel);
|
||||
|
||||
$this->model = $assetModel;
|
||||
$this->entityClass = Asset::class;
|
||||
$this->entityNameOne = 'asset';
|
||||
$this->entityNameMulti = 'assets';
|
||||
$this->serializerGroups = ['assetDetails', 'categoryList', 'publishDetails'];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives child controllers opportunity to analyze and do whatever to an entity before going through serializer.
|
||||
*/
|
||||
protected function preSerializeEntity(object $entity, string $action = 'view'): void
|
||||
{
|
||||
$entity->setDownloadUrl(
|
||||
$this->model->generateUrl($entity, true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert posted parameters into what the form needs in order to successfully bind.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function prepareParametersForBinding(Request $request, $parameters, $entity, $action)
|
||||
{
|
||||
$assetDir = $this->parametersHelper->get('upload_dir');
|
||||
$entity->setUploadDir($assetDir);
|
||||
|
||||
if (isset($parameters['file'])) {
|
||||
if ('local' === $parameters['storageLocation']) {
|
||||
$entity->setPath($parameters['file']);
|
||||
$entity->setFileInfoFromFile();
|
||||
|
||||
if (null === $entity->loadFile()) {
|
||||
return $this->returnError('File '.$parameters['file'].' was not found in the asset directory.', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
} elseif ('remote' === $parameters['storageLocation']) {
|
||||
$parameters['remotePath'] = $parameters['file'];
|
||||
$entity->setTitle($parameters['title']);
|
||||
$entity->setStorageLocation('remote');
|
||||
$entity->setRemotePath($parameters['remotePath']);
|
||||
$entity->preUpload();
|
||||
$entity->upload();
|
||||
}
|
||||
|
||||
unset($parameters['file']);
|
||||
} elseif ('new' === $action) {
|
||||
return $this->returnError('File of the asset is required.', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,754 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Controller;
|
||||
|
||||
use Mautic\AssetBundle\Model\AssetModel;
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
use Mautic\CoreBundle\Form\Type\DateRangeType;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\FileHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Oneup\UploaderBundle\Templating\Helper\UploaderHelper;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AssetController extends FormController
|
||||
{
|
||||
/**
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function indexAction(Request $request, CoreParametersHelper $parametersHelper, AssetModel $assetModel, int $page = 1)
|
||||
{
|
||||
// set some permissions
|
||||
$permissions = $this->security->isGranted([
|
||||
'asset:assets:viewown',
|
||||
'asset:assets:viewother',
|
||||
'asset:assets:create',
|
||||
'asset:assets:editown',
|
||||
'asset:assets:editother',
|
||||
'asset:assets:deleteown',
|
||||
'asset:assets:deleteother',
|
||||
'asset:assets:publishown',
|
||||
'asset:assets:publishother',
|
||||
], 'RETURN_ARRAY');
|
||||
|
||||
if (!$permissions['asset:assets:viewown'] && !$permissions['asset:assets:viewother']) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$limit = $request->getSession()->get('mautic.asset.limit', $parametersHelper->get('default_assetlimit'));
|
||||
$start = (1 === $page) ? 0 : (($page - 1) * $limit);
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
}
|
||||
|
||||
$search = $request->get('search', $request->getSession()->get('mautic.asset.filter', ''));
|
||||
$request->getSession()->set('mautic.asset.filter', $search);
|
||||
|
||||
$filter = ['string' => $search, 'force' => []];
|
||||
|
||||
if (!$permissions['asset:assets:viewother']) {
|
||||
$filter['force'][] =
|
||||
['column' => 'a.createdBy', 'expr' => 'eq', 'value' => $this->user->getId()];
|
||||
}
|
||||
|
||||
$orderBy = $request->getSession()->get('mautic.asset.orderby', 'a.dateModified');
|
||||
$orderByDir = $request->getSession()->get('mautic.asset.orderbydir', $this->getDefaultOrderDirection());
|
||||
|
||||
$assets = $assetModel->getEntities(
|
||||
[
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
'filter' => $filter,
|
||||
'orderBy' => $orderBy,
|
||||
'orderByDir' => $orderByDir,
|
||||
]
|
||||
);
|
||||
|
||||
$count = count($assets);
|
||||
if ($count && $count < ($start + 1)) {
|
||||
// the number of entities are now less then the current asset so redirect to the last asset
|
||||
if (1 === $count) {
|
||||
$lastPage = 1;
|
||||
} else {
|
||||
$lastPage = (ceil($count / $limit)) ?: 1;
|
||||
}
|
||||
$request->getSession()->set('mautic.asset.asset', $lastPage);
|
||||
$returnUrl = $this->generateUrl('mautic_asset_index', ['page' => $lastPage]);
|
||||
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['asset' => $lastPage],
|
||||
'contentTemplate' => 'Mautic\AssetBundle\Controller\AssetController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_asset_index',
|
||||
'mauticContent' => 'asset',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// set what asset currently on so that we can return here after form submission/cancellation
|
||||
$request->getSession()->set('mautic.asset.page', $page);
|
||||
|
||||
$tmpl = $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index';
|
||||
|
||||
// retrieve a list of categories
|
||||
$categories = $assetModel->getLookupResults('category', '', 0);
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'searchValue' => $search,
|
||||
'items' => $assets,
|
||||
'categories' => $categories,
|
||||
'limit' => $limit,
|
||||
'permissions' => $permissions,
|
||||
'model' => $assetModel,
|
||||
'tmpl' => $tmpl,
|
||||
'page' => $page,
|
||||
'security' => $this->security,
|
||||
],
|
||||
'contentTemplate' => '@MauticAsset/Asset/list.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_asset_index',
|
||||
'mauticContent' => 'asset',
|
||||
'route' => $this->generateUrl('mautic_asset_index', ['page' => $page]),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a specific form into the detailed panel.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function viewAction(Request $request, AssetModel $model, $objectId)
|
||||
{
|
||||
$activeAsset = $model->getEntity($objectId);
|
||||
|
||||
// set the asset we came from
|
||||
$page = $request->getSession()->get('mautic.asset.page', 1);
|
||||
|
||||
$tmpl = $request->isXmlHttpRequest() ? $request->get('tmpl', 'details') : 'details';
|
||||
|
||||
// Init the date range filter form
|
||||
$dateRangeValues = $request->get('daterange', []);
|
||||
$action = $this->generateUrl('mautic_asset_action', ['objectAction' => 'view', 'objectId' => $objectId]);
|
||||
$dateRangeForm = $this->formFactory->create(DateRangeType::class, $dateRangeValues, ['action' => $action]);
|
||||
|
||||
if (null === $activeAsset) {
|
||||
// set the return URL
|
||||
$returnUrl = $this->generateUrl('mautic_asset_index', ['page' => $page]);
|
||||
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\AssetBundle\Controller\AssetController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_asset_index',
|
||||
'mauticContent' => 'asset',
|
||||
],
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.asset.asset.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
],
|
||||
],
|
||||
]);
|
||||
} elseif (!$this->security->hasEntityAccess('asset:assets:viewown', 'asset:assets:viewother', $activeAsset->getCreatedBy())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
// Audit Log
|
||||
$auditLogModel = $this->getModel('core.auditlog');
|
||||
\assert($auditLogModel instanceof AuditLogModel);
|
||||
$logs = $auditLogModel->getLogForObject('asset', $activeAsset->getId(), $activeAsset->getDateAdded());
|
||||
|
||||
return $this->delegateView([
|
||||
'returnUrl' => $action,
|
||||
'viewParameters' => [
|
||||
'activeAsset' => $activeAsset,
|
||||
'tmpl' => $tmpl,
|
||||
'permissions' => $this->security->isGranted([
|
||||
'asset:assets:viewown',
|
||||
'asset:assets:viewother',
|
||||
'asset:assets:create',
|
||||
'asset:assets:editown',
|
||||
'asset:assets:editother',
|
||||
'asset:assets:deleteown',
|
||||
'asset:assets:deleteother',
|
||||
'asset:assets:publishown',
|
||||
'asset:assets:publishother',
|
||||
], 'RETURN_ARRAY'),
|
||||
'stats' => [
|
||||
'downloads' => [
|
||||
'total' => $activeAsset->getDownloadCount(),
|
||||
'unique' => $activeAsset->getUniqueDownloadCount(),
|
||||
'timeStats' => $model->getDownloadsLineChartData(
|
||||
null,
|
||||
new \DateTime($dateRangeForm->get('date_from')->getData()),
|
||||
new \DateTime($dateRangeForm->get('date_to')->getData()),
|
||||
null,
|
||||
['asset_id' => $activeAsset->getId()]
|
||||
),
|
||||
],
|
||||
],
|
||||
'security' => $this->security,
|
||||
'assetDownloadUrl' => $model->generateUrl($activeAsset, true),
|
||||
'logs' => $logs,
|
||||
'dateRangeForm' => $dateRangeForm->createView(),
|
||||
],
|
||||
'contentTemplate' => '@MauticAsset/Asset/'.$tmpl.'.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_asset_index',
|
||||
'mauticContent' => 'asset',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a preview of the file.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function previewAction(Request $request, AssetModel $model, $objectId)
|
||||
{
|
||||
$activeAsset = $model->getEntity($objectId);
|
||||
|
||||
if (null === $activeAsset || !$this->security->hasEntityAccess('asset:assets:viewown', 'asset:assets:viewother', $activeAsset->getCreatedBy())) {
|
||||
return $this->modalAccessDenied();
|
||||
}
|
||||
|
||||
$download = $request->query->get('download', 0);
|
||||
|
||||
// Display the file directly in the browser just for selected extensions
|
||||
$defaultStream = in_array($activeAsset->getExtension(), $this->coreParametersHelper->get('streamed_extensions')) ? '1' : null;
|
||||
$stream = $request->query->get('stream', $defaultStream);
|
||||
|
||||
if ('1' === $download || '1' === $stream) {
|
||||
try {
|
||||
// set the uploadDir
|
||||
$activeAsset->setUploadDir($this->coreParametersHelper->get('upload_dir'));
|
||||
$contents = $activeAsset->getFileContents();
|
||||
} catch (\Exception) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$response = new Response();
|
||||
$response->headers->set('Content-Type', $activeAsset->getFileMimeType());
|
||||
if ('1' === $download) {
|
||||
$response->headers->set('Content-Disposition', 'attachment;filename="'.$activeAsset->getOriginalFileName());
|
||||
}
|
||||
$response->setContent($contents);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'activeAsset' => $activeAsset,
|
||||
'assetDownloadUrl' => $model->generateUrl($activeAsset),
|
||||
],
|
||||
'contentTemplate' => '@MauticAsset/Modules/preview.html.twig',
|
||||
'passthroughVars' => [
|
||||
'route' => false,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates new form and processes post data.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function newAction(Request $request, CoreParametersHelper $parametersHelper, UploaderHelper $uploaderHelper, IntegrationHelper $integrationHelper, AssetModel $model, $entity = null)
|
||||
{
|
||||
if (null == $entity) {
|
||||
$entity = $model->getEntity();
|
||||
}
|
||||
|
||||
$entity->setMaxSize(FileHelper::convertMegabytesToBytes($this->coreParametersHelper->get('max_size')));
|
||||
|
||||
$method = $request->getMethod();
|
||||
$session = $request->getSession();
|
||||
|
||||
if (!$this->security->isGranted('asset:assets:create')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$maxSize = $model->getMaxUploadSize();
|
||||
$extensions = '.'.implode(', .', $this->coreParametersHelper->get('allowed_extensions'));
|
||||
|
||||
$maxSizeError = $this->translator->trans('mautic.asset.asset.error.file.size', [
|
||||
'%fileSize%' => '{{filesize}}',
|
||||
'%maxSize%' => '{{maxFilesize}}',
|
||||
], 'validators');
|
||||
|
||||
$extensionError = $this->translator->trans('mautic.asset.asset.error.file.extension.js', [
|
||||
'%extensions%' => $extensions,
|
||||
], 'validators');
|
||||
|
||||
// Create temporary asset ID
|
||||
$asset = $request->request->all()['asset'] ?? [];
|
||||
$tempId = 'POST' === $method ? ($asset['tempId'] ?? '') : uniqid('tmp_');
|
||||
$entity->setTempId($tempId);
|
||||
|
||||
// Set the page we came from
|
||||
$page = $session->get('mautic.asset.page', 1);
|
||||
$action = $this->generateUrl('mautic_asset_action', ['objectAction' => 'new']);
|
||||
|
||||
$uploadEndpoint = $uploaderHelper->endpoint('asset');
|
||||
|
||||
// create the form
|
||||
$form = $model->createForm($entity, $this->formFactory, $action);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if ('POST' == $method) {
|
||||
$valid = false;
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
$entity->setUploadDir($parametersHelper->get('upload_dir'));
|
||||
$entity->preUpload();
|
||||
$entity->upload();
|
||||
$entity->setDateModified(new \DateTime());
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($entity);
|
||||
|
||||
// remove the asset from request
|
||||
$request->files->remove('asset');
|
||||
|
||||
$this->addFlashMessage('mautic.core.notice.created', [
|
||||
'%name%' => $entity->getTitle(),
|
||||
'%menu_link%' => 'mautic_asset_index',
|
||||
'%url%' => $this->generateUrl('mautic_asset_action', [
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(),
|
||||
]),
|
||||
]);
|
||||
|
||||
if (!$this->getFormButton($form, ['buttons', 'save'])->isClicked()) {
|
||||
// return edit view so that all the session stuff is loaded
|
||||
return $this->editAction($request, $uploaderHelper, $integrationHelper, $model, $entity->getId(), true);
|
||||
}
|
||||
|
||||
$viewParameters = [
|
||||
'objectAction' => 'view',
|
||||
'objectId' => $entity->getId(),
|
||||
];
|
||||
$returnUrl = $this->generateUrl('mautic_asset_action', $viewParameters);
|
||||
$template = 'Mautic\AssetBundle\Controller\AssetController::viewAction';
|
||||
}
|
||||
} else {
|
||||
$viewParameters = ['page' => $page];
|
||||
$returnUrl = $this->generateUrl('mautic_asset_index', $viewParameters);
|
||||
$template = 'Mautic\AssetBundle\Controller\AssetController::indexAction';
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => $viewParameters,
|
||||
'contentTemplate' => $template,
|
||||
'passthroughVars' => [
|
||||
'activeLink' => 'mautic_asset_index',
|
||||
'mauticContent' => 'asset',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for integrations to cloud providers
|
||||
$integrations = $integrationHelper->getIntegrationObjects(null, ['cloud_storage']);
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'activeAsset' => $entity,
|
||||
'assetDownloadUrl' => $model->generateUrl($entity),
|
||||
'integrations' => $integrations,
|
||||
'startOnLocal' => $entity->isLocal(),
|
||||
'uploadEndpoint' => $uploadEndpoint,
|
||||
'maxSize' => $maxSize,
|
||||
'maxSizeError' => $maxSizeError,
|
||||
'extensions' => $extensions,
|
||||
'extensionError' => $extensionError,
|
||||
],
|
||||
'contentTemplate' => '@MauticAsset/Asset/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_asset_index',
|
||||
'mauticContent' => 'asset',
|
||||
'route' => $this->generateUrl('mautic_asset_action', [
|
||||
'objectAction' => 'new',
|
||||
]),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates edit form and processes post data.
|
||||
*
|
||||
* @param int $objectId
|
||||
* @param bool $ignorePost
|
||||
*
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Request $request, UploaderHelper $uploaderHelper, IntegrationHelper $integrationHelper, AssetModel $model, $objectId, $ignorePost = false)
|
||||
{
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (!$this->security->hasEntityAccess('asset:assets:editown', 'asset:assets:editother', $entity->getCreatedBy())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$entity->setMaxSize(FileHelper::convertMegabytesToBytes($this->coreParametersHelper->get('max_size')));
|
||||
|
||||
$session = $request->getSession();
|
||||
$page = $session->get('mautic.asset.page', 1);
|
||||
$method = $request->getMethod();
|
||||
$maxSize = $model->getMaxUploadSize();
|
||||
$extensions = '.'.implode(', .', $this->coreParametersHelper->get('allowed_extensions'));
|
||||
|
||||
$maxSizeError = $this->translator->trans('mautic.asset.asset.error.file.size', [
|
||||
'%fileSize%' => '{{filesize}}',
|
||||
'%maxSize%' => '{{maxFilesize}}',
|
||||
], 'validators');
|
||||
|
||||
$extensionError = $this->translator->trans('mautic.asset.asset.error.file.extension.js', [
|
||||
'%extensions%' => $extensions,
|
||||
], 'validators');
|
||||
|
||||
// set the return URL
|
||||
$returnUrl = $this->generateUrl('mautic_asset_index', ['page' => $page]);
|
||||
|
||||
$uploadEndpoint = $uploaderHelper->endpoint('asset');
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\AssetBundle\Controller\AssetController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => 'mautic_asset_index',
|
||||
'mauticContent' => 'asset',
|
||||
],
|
||||
];
|
||||
|
||||
// not found
|
||||
if (null === $entity) {
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.asset.asset.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
],
|
||||
],
|
||||
])
|
||||
);
|
||||
} elseif (!$this->security->hasEntityAccess(
|
||||
'asset:assets:viewown', 'asset:assets:viewother', $entity->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
// deny access if the entity is locked
|
||||
return $this->isLocked($postActionVars, $entity, 'asset.asset');
|
||||
}
|
||||
|
||||
// Create temporary asset ID
|
||||
$asset = $request->request->all()['asset'] ?? [];
|
||||
$tempId = 'POST' === $method ? ($asset['tempId'] ?? '') : uniqid('tmp_');
|
||||
$entity->setTempId($tempId);
|
||||
|
||||
// Create the form
|
||||
$action = $this->generateUrl('mautic_asset_action', ['objectAction' => 'edit', 'objectId' => $objectId]);
|
||||
$form = $model->createForm($entity, $this->formFactory, $action);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if (!$ignorePost && 'POST' == $method) {
|
||||
$valid = false;
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
$entity->setUploadDir($this->coreParametersHelper->get('upload_dir'));
|
||||
$entity->preUpload();
|
||||
$entity->upload();
|
||||
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($entity, $this->getFormButton($form, ['buttons', 'save'])->isClicked());
|
||||
|
||||
// remove the asset from request
|
||||
$request->files->remove('asset');
|
||||
|
||||
$this->addFlashMessage('mautic.core.notice.updated', [
|
||||
'%name%' => $entity->getTitle(),
|
||||
'%menu_link%' => 'mautic_asset_index',
|
||||
'%url%' => $this->generateUrl('mautic_asset_action', [
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(),
|
||||
]),
|
||||
]);
|
||||
|
||||
$returnUrl = $this->generateUrl('mautic_asset_action', [
|
||||
'objectAction' => 'view',
|
||||
'objectId' => $entity->getId(),
|
||||
]);
|
||||
$viewParams = ['objectId' => $entity->getId()];
|
||||
$template = 'Mautic\AssetBundle\Controller\AssetController::viewAction';
|
||||
}
|
||||
} else {
|
||||
// clear any modified content
|
||||
$session->remove('mautic.asestbuilder.'.$objectId.'.content');
|
||||
// unlock the entity
|
||||
$model->unlockEntity($entity);
|
||||
|
||||
$returnUrl = $this->generateUrl('mautic_asset_index', ['page' => $page]);
|
||||
$viewParams = ['page' => $page];
|
||||
$template = 'Mautic\AssetBundle\Controller\AssetController::indexAction';
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => $viewParams,
|
||||
'contentTemplate' => $template,
|
||||
])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// lock the entity
|
||||
$model->lockEntity($entity);
|
||||
}
|
||||
|
||||
// Check for integrations to cloud providers
|
||||
$integrations = $integrationHelper->getIntegrationObjects(null, ['cloud_storage']);
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'activeAsset' => $entity,
|
||||
'assetDownloadUrl' => $model->generateUrl($entity),
|
||||
'integrations' => $integrations,
|
||||
'startOnLocal' => $entity->isLocal(),
|
||||
'uploadEndpoint' => $uploadEndpoint,
|
||||
'maxSize' => $maxSize,
|
||||
'maxSizeError' => $maxSizeError,
|
||||
'extensions' => $extensions,
|
||||
'extensionError' => $extensionError,
|
||||
],
|
||||
'contentTemplate' => '@MauticAsset/Asset/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_asset_index',
|
||||
'mauticContent' => 'asset',
|
||||
'route' => $this->generateUrl('mautic_asset_action', [
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(),
|
||||
]),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone an entity.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function cloneAction(Request $request, CoreParametersHelper $parametersHelper, UploaderHelper $uploaderHelper, IntegrationHelper $integrationHelper, AssetModel $model, $objectId)
|
||||
{
|
||||
$entity = $model->getEntity($objectId);
|
||||
$clone = null;
|
||||
|
||||
if (null != $entity) {
|
||||
if (!$this->security->isGranted('asset:assets:create')
|
||||
|| !$this->security->hasEntityAccess(
|
||||
'asset:assets:viewown', 'asset:assets:viewother', $entity->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$clone = clone $entity;
|
||||
$clone->setDownloadCount(0);
|
||||
$clone->setUniqueDownloadCount(0);
|
||||
$clone->setRevision(0);
|
||||
$clone->setIsPublished(false);
|
||||
}
|
||||
|
||||
return $this->newAction($request, $parametersHelper, $uploaderHelper, $integrationHelper, $model, $clone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the entity.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAction(Request $request, AssetModel $model, $objectId)
|
||||
{
|
||||
$page = $request->getSession()->get('mautic.asset.page', 1);
|
||||
$returnUrl = $this->generateUrl('mautic_asset_index', ['page' => $page]);
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\AssetBundle\Controller\AssetController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => 'mautic_asset_index',
|
||||
'mauticContent' => 'asset',
|
||||
],
|
||||
];
|
||||
|
||||
if ('POST' === $request->getMethod()) {
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null === $entity) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.asset.asset.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif (!$this->security->hasEntityAccess(
|
||||
'asset:assets:deleteown',
|
||||
'asset:assets:deleteother',
|
||||
$entity->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
return $this->isLocked($postActionVars, $entity, 'asset.asset');
|
||||
}
|
||||
|
||||
$entity->removeUpload();
|
||||
$model->deleteEntity($entity);
|
||||
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.core.notice.deleted',
|
||||
'msgVars' => [
|
||||
'%name%' => $entity->getTitle(),
|
||||
'%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, AssetModel $model): Response
|
||||
{
|
||||
$page = $request->getSession()->get('mautic.asset.page', 1);
|
||||
$returnUrl = $this->generateUrl('mautic_asset_index', ['page' => $page]);
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\AssetBundle\Controller\AssetController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => 'mautic_asset_index',
|
||||
'mauticContent' => 'asset',
|
||||
],
|
||||
];
|
||||
|
||||
if ('POST' === $request->getMethod()) {
|
||||
$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.asset.asset.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif (!$this->security->hasEntityAccess(
|
||||
'asset:assets:deleteown', 'asset:assets:deleteother', $entity->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
$flashes[] = $this->accessDenied(true);
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
$flashes[] = $this->isLocked($postActionVars, $entity, 'asset', true);
|
||||
} else {
|
||||
$deleteIds[] = $objectId;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete everything we are able to
|
||||
if (!empty($deleteIds)) {
|
||||
$entities = $model->deleteEntities($deleteIds);
|
||||
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.asset.asset.notice.batch_deleted',
|
||||
'msgVars' => [
|
||||
'%count%' => count($entities),
|
||||
],
|
||||
];
|
||||
}
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => $flashes,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the container for the remote file browser.
|
||||
*
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
|
||||
*/
|
||||
public function remoteAction(Request $request, IntegrationHelper $integrationHelper): Response
|
||||
{
|
||||
// Check for integrations to cloud providers
|
||||
$integrations = $integrationHelper->getIntegrationObjects(null, ['cloud_storage']);
|
||||
|
||||
$tmpl = $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index';
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'integrations' => $integrations,
|
||||
'tmpl' => $tmpl,
|
||||
],
|
||||
'contentTemplate' => '@MauticAsset/Remote/browse.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_asset_index',
|
||||
'mauticContent' => 'asset',
|
||||
'route' => $this->generateUrl('mautic_asset_index', ['page' => $request->getSession()->get('mautic.asset.page', 1)]),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getModelName(): string
|
||||
{
|
||||
return 'asset';
|
||||
}
|
||||
|
||||
protected function getDefaultOrderDirection(): string
|
||||
{
|
||||
return 'DESC';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\FormController as CommonFormController;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class PublicController extends CommonFormController
|
||||
{
|
||||
/**
|
||||
* @param string $slug
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function downloadAction(Request $request, CoreParametersHelper $parametersHelper, $slug)
|
||||
{
|
||||
// find the asset
|
||||
/** @var \Mautic\AssetBundle\Model\AssetModel $model */
|
||||
$model = $this->getModel('asset');
|
||||
|
||||
/** @var \Mautic\AssetBundle\Entity\Asset $entity */
|
||||
$entity = $model->getEntityBySlugs($slug);
|
||||
|
||||
if (!empty($entity)) {
|
||||
$published = $entity->isPublished();
|
||||
|
||||
// make sure the asset is published or deny access if not
|
||||
if ((!$published) && (!$this->security->hasEntityAccess('asset:assets:viewown', 'asset:assets:viewother', $entity->getCreatedBy()))) {
|
||||
$model->trackDownload($entity, $request, 401);
|
||||
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
// make sure URLs match up
|
||||
$url = $model->generateUrl($entity, false);
|
||||
$requestUri = $request->getRequestUri();
|
||||
// remove query
|
||||
$query = $request->getQueryString();
|
||||
|
||||
if (!empty($query)) {
|
||||
$requestUri = str_replace("?{$query}", '', $url);
|
||||
}
|
||||
|
||||
// redirect if they don't match
|
||||
if ($requestUri != $url) {
|
||||
$model->trackDownload($entity, $request, 301);
|
||||
|
||||
return $this->redirect($url, 301);
|
||||
}
|
||||
|
||||
if ($entity->isRemote()) {
|
||||
$model->trackDownload($entity, $request, 200);
|
||||
|
||||
// Redirect to remote URL
|
||||
$response = new RedirectResponse($entity->getRemotePath());
|
||||
} else {
|
||||
try {
|
||||
// set the uploadDir
|
||||
$entity->setUploadDir($parametersHelper->get('upload_dir'));
|
||||
$contents = $entity->getFileContents();
|
||||
$model->trackDownload($entity, $request, 200);
|
||||
} catch (\Exception) {
|
||||
$model->trackDownload($entity, $request, 404);
|
||||
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$response = new Response();
|
||||
|
||||
if ($entity->getDisallow()) {
|
||||
$response->headers->set('X-Robots-Tag', 'noindex, nofollow, noarchive');
|
||||
}
|
||||
|
||||
$response->headers->set('Content-Type', $entity->getFileMimeType());
|
||||
|
||||
// Display the file directly in the browser just for selected extensions
|
||||
$stream = $request->get('stream', in_array($entity->getExtension(), $this->coreParametersHelper->get('streamed_extensions')));
|
||||
if (!$stream) {
|
||||
$response->headers->set('Content-Disposition', 'attachment;filename="'.$entity->getOriginalFileName());
|
||||
}
|
||||
$response->setContent($contents);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->notFound();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Controller;
|
||||
|
||||
use Oneup\UploaderBundle\Controller\DropzoneController;
|
||||
use Oneup\UploaderBundle\Uploader\Response\EmptyResponse;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\UploadException;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class UploadController extends DropzoneController
|
||||
{
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
public function upload(): JsonResponse
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
$response = new EmptyResponse();
|
||||
$files = $this->getFiles($request->files);
|
||||
$this->setTranslator($this->container->get('translator'));
|
||||
|
||||
if (!empty($files)) {
|
||||
foreach ($files as $file) {
|
||||
try {
|
||||
$this->handleUpload($file, $response, $request);
|
||||
} catch (UploadException $e) {
|
||||
$this->errorHandler->addException($response, $e);
|
||||
} catch (\Exception $e) {
|
||||
error_log($e);
|
||||
$error = new UploadException($this->translator->trans('mautic.asset.error.file.failed'));
|
||||
$this->errorHandler->addException($response, $error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$error = new UploadException($this->translator->trans('mautic.asset.error.file.failed'));
|
||||
$this->errorHandler->addException($response, $error);
|
||||
}
|
||||
|
||||
return $this->createSupportedJsonResponse($response->assemble());
|
||||
}
|
||||
|
||||
#[\Symfony\Contracts\Service\Attribute\Required]
|
||||
public function setTranslator(TranslatorInterface $translator): void
|
||||
{
|
||||
$this->translator = $translator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
|
||||
class LoadAssetData extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$asset = new Asset();
|
||||
$asset
|
||||
->setTitle('@TOCHANGE: Asset1 Title')
|
||||
->setAlias('asset1')
|
||||
->setOriginalFileName('@TOCHANGE: Asset1 Original File Name')
|
||||
->setPath('fdb8e28357b02d12d068de3e5661832e21bc08ec.doc')
|
||||
->setDownloadCount(1)
|
||||
->setUniqueDownloadCount(1)
|
||||
->setRevision(1)
|
||||
->setLanguage('en');
|
||||
|
||||
$manager->persist($asset);
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\AssetBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
|
||||
class MauticAssetExtension 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');
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Asset>
|
||||
*/
|
||||
class AssetRepository extends CommonRepository
|
||||
{
|
||||
use ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* Get a list of entities.
|
||||
*
|
||||
* @return Paginator
|
||||
*/
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$q = $this
|
||||
->createQueryBuilder('a')
|
||||
->select('a')
|
||||
->leftJoin('a.category', 'c');
|
||||
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $search
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
* @param bool|false $viewOther
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAssetList($search = '', $limit = 10, $start = 0, $viewOther = false)
|
||||
{
|
||||
$q = $this->createQueryBuilder('a');
|
||||
$q->select('partial a.{id, title, path, alias, language}');
|
||||
|
||||
if (!empty($search)) {
|
||||
$q->andWhere($q->expr()->like('a.title', ':search'))
|
||||
->setParameter('search', "%{$search}%");
|
||||
}
|
||||
|
||||
if (!$viewOther) {
|
||||
$q->andWhere($q->expr()->eq('a.createdBy', ':id'))
|
||||
->setParameter('id', $this->currentUser->getId());
|
||||
}
|
||||
|
||||
$q->orderBy('a.title');
|
||||
|
||||
if (!empty($limit)) {
|
||||
$q->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
*/
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardCatchAllWhereClause($q, $filter, [
|
||||
'a.title',
|
||||
'a.alias',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 = $field = $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.asset.asset.searchcommand.isexpired'):
|
||||
case $this->translator->trans('mautic.asset.asset.searchcommand.isexpired', [], null, 'en_US'):
|
||||
$expr = sprintf(
|
||||
"(a.isPublished = :%1\$s AND a.publishDown IS NOT NULL AND a.publishDown <> '' AND a.publishDown < CURRENT_TIMESTAMP())",
|
||||
$unique
|
||||
);
|
||||
$forceParameters = [$unique => true];
|
||||
break;
|
||||
case $this->translator->trans('mautic.asset.asset.searchcommand.ispending'):
|
||||
case $this->translator->trans('mautic.asset.asset.searchcommand.ispending', [], null, 'en_US'):
|
||||
$expr = sprintf(
|
||||
"(a.isPublished = :%1\$s AND a.publishUp IS NOT NULL AND a.publishUp <> '' AND a.publishUp > CURRENT_TIMESTAMP())",
|
||||
$unique
|
||||
);
|
||||
$forceParameters = [$unique => true];
|
||||
break;
|
||||
case $this->translator->trans('mautic.asset.asset.searchcommand.lang'):
|
||||
$langUnique = $this->generateRandomParameterName();
|
||||
$langValue = $filter->string.'_%';
|
||||
$forceParameters = [
|
||||
$langUnique => $langValue,
|
||||
$unique => $filter->string,
|
||||
];
|
||||
$expr = '('.$q->expr()->eq('a.language', ":$unique").' OR '.$q->expr()->like('a.language', ":$langUnique").')';
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.project.searchcommand.name'):
|
||||
case $this->translator->trans('mautic.project.searchcommand.name', [], null, 'en_US'):
|
||||
return $this->handleProjectFilter(
|
||||
$this->_em->getConnection()->createQueryBuilder(),
|
||||
'asset_id',
|
||||
'asset_projects_xref',
|
||||
$this->getTableAlias(),
|
||||
$filter->string,
|
||||
$filter->not
|
||||
);
|
||||
}
|
||||
|
||||
if ($expr && $filter->not) {
|
||||
$expr = $q->expr()->not($expr);
|
||||
}
|
||||
|
||||
if (!empty($forceParameters)) {
|
||||
$parameters = $forceParameters;
|
||||
} elseif (!$returnParameter) {
|
||||
$parameters = [];
|
||||
} else {
|
||||
$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.asset.asset.searchcommand.isexpired',
|
||||
'mautic.asset.asset.searchcommand.ispending',
|
||||
'mautic.core.searchcommand.category',
|
||||
'mautic.asset.asset.searchcommand.lang',
|
||||
'mautic.project.searchcommand.name',
|
||||
];
|
||||
|
||||
return array_merge($commands, parent::getSearchCommands());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array<string>>
|
||||
*/
|
||||
protected function getDefaultOrder(): array
|
||||
{
|
||||
return [
|
||||
['a.title', 'ASC'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'a';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sum size of assets.
|
||||
*/
|
||||
public function getAssetSize(array $assets): int
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('sum(a.size) as total_size')
|
||||
->from(MAUTIC_TABLE_PREFIX.'assets', 'a')
|
||||
->where('a.id IN (:assetIds)')
|
||||
->setParameter('assetIds', $assets, \Doctrine\DBAL\ArrayParameterType::INTEGER);
|
||||
|
||||
$result = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
return (int) $result[0]['total_size'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $increaseBy
|
||||
* @param bool|false $unique
|
||||
*/
|
||||
public function upDownloadCount($id, $increaseBy = 1, $unique = false): void
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'assets')
|
||||
->set('download_count', 'download_count + '.(int) $increaseBy)
|
||||
->where('id = '.(int) $id);
|
||||
|
||||
if ($unique) {
|
||||
$q->set('unique_download_count', 'unique_download_count + '.(int) $increaseBy);
|
||||
}
|
||||
|
||||
$q->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $categoryId
|
||||
*
|
||||
* @return Asset
|
||||
*
|
||||
* @throws NoResultException
|
||||
* @throws NonUniqueResultException
|
||||
*/
|
||||
public function getLatestAssetForCategory($categoryId)
|
||||
{
|
||||
$q = $this->createQueryBuilder($this->getTableAlias());
|
||||
$q->where($this->getTableAlias().'.category = :categoryId');
|
||||
$q->andWhere($this->getTableAlias().'.isPublished = TRUE');
|
||||
$q->setParameter('categoryId', $categoryId);
|
||||
$q->orderBy($this->getTableAlias().'.dateAdded', Order::Descending->value);
|
||||
$q->setMaxResults(1);
|
||||
|
||||
return $q->getQuery()->getSingleResult();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(security: "is_granted('asset:assets:viewown')"),
|
||||
new Get(security: "is_granted('asset:assets:viewown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['download:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
'api_included' => ['asset', 'ipaddress', 'email'],
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['download:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class Download
|
||||
{
|
||||
public const TABLE_NAME = 'asset_downloads';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['download:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
#[Groups(['download:read', 'download:write'])]
|
||||
private $dateDownload;
|
||||
|
||||
/**
|
||||
* @var Asset|null
|
||||
*/
|
||||
#[Groups(['download:read', 'download:write'])]
|
||||
private $asset;
|
||||
|
||||
/**
|
||||
* @var IpAddress|null
|
||||
*/
|
||||
#[Groups(['download:read', 'download:write'])]
|
||||
private $ipAddress;
|
||||
|
||||
#[Groups(['download:read', 'download:write'])]
|
||||
private ?Lead $lead;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['download:read', 'download:write'])]
|
||||
private $code;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['download:read', 'download:write'])]
|
||||
private $referer;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['download:read', 'download:write'])]
|
||||
private $trackingId;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['download:read', 'download:write'])]
|
||||
private $source;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
#[Groups(['download:read', 'download:write'])]
|
||||
private $sourceId;
|
||||
|
||||
/**
|
||||
* @var Email|null
|
||||
*/
|
||||
#[Groups(['download:read', 'download:write'])]
|
||||
private $email;
|
||||
|
||||
private ?string $utmCampaign = null;
|
||||
|
||||
private ?string $utmContent = null;
|
||||
|
||||
private ?string $utmMedium = null;
|
||||
|
||||
private ?string $utmSource = null;
|
||||
|
||||
private ?string $utmTerm = null;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(DownloadRepository::class)
|
||||
->addIndex(['tracking_id'], 'download_tracking_search')
|
||||
->addIndex(['source', 'source_id'], 'download_source_search')
|
||||
->addIndex(['date_download'], 'asset_date_download');
|
||||
|
||||
$builder->addBigIntIdField();
|
||||
|
||||
$builder->createField('dateDownload', 'datetime')
|
||||
->columnName('date_download')
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('asset', 'Asset')
|
||||
->addJoinColumn('asset_id', 'id', true, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->addIpAddress(true);
|
||||
|
||||
$builder->addLead(true, 'SET NULL');
|
||||
|
||||
$builder->addField('code', 'integer');
|
||||
|
||||
$builder->createField('referer', 'text')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('trackingId', 'string')
|
||||
->columnName('tracking_id')
|
||||
->build();
|
||||
|
||||
$builder->createField('source', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('sourceId', 'integer')
|
||||
->columnName('source_id')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('email', Email::class)
|
||||
->addJoinColumn('email_id', 'id', true, false, 'SET NULL')
|
||||
->build();
|
||||
|
||||
$builder->createField('utmCampaign', Types::STRING)
|
||||
->columnName('utm_campaign')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('utmContent', Types::STRING)
|
||||
->columnName('utm_content')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('utmMedium', Types::STRING)
|
||||
->columnName('utm_medium')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('utmSource', Types::STRING)
|
||||
->columnName('utm_source')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('utmTerm', Types::STRING)
|
||||
->columnName('utm_term')
|
||||
->nullable()
|
||||
->build();
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return (int) $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $dateDownload
|
||||
*
|
||||
* @return Download
|
||||
*/
|
||||
public function setDateDownload($dateDownload)
|
||||
{
|
||||
$this->dateDownload = $dateDownload;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateDownload()
|
||||
{
|
||||
return $this->dateDownload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $code
|
||||
*
|
||||
* @return Download
|
||||
*/
|
||||
public function setCode($code)
|
||||
{
|
||||
$this->code = $code;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCode()
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $referer
|
||||
*
|
||||
* @return Download
|
||||
*/
|
||||
public function setReferer($referer)
|
||||
{
|
||||
$this->referer = $referer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getReferer()
|
||||
{
|
||||
return $this->referer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Download
|
||||
*/
|
||||
public function setAsset(?Asset $asset = null)
|
||||
{
|
||||
$this->asset = $asset;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Asset
|
||||
*/
|
||||
public function getAsset()
|
||||
{
|
||||
return $this->asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Download
|
||||
*/
|
||||
public function setIpAddress(IpAddress $ipAddress)
|
||||
{
|
||||
$this->ipAddress = $ipAddress;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IpAddress
|
||||
*/
|
||||
public function getIpAddress()
|
||||
{
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $trackingId
|
||||
*
|
||||
* @return Download
|
||||
*/
|
||||
public function setTrackingId($trackingId)
|
||||
{
|
||||
$this->trackingId = $trackingId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getTrackingId()
|
||||
{
|
||||
return $this->trackingId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $lead
|
||||
*/
|
||||
public function setLead($lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getSource()
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $source
|
||||
*/
|
||||
public function setSource($source): void
|
||||
{
|
||||
$this->source = $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getSourceId()
|
||||
{
|
||||
return $this->sourceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $sourceId
|
||||
*/
|
||||
public function setSourceId($sourceId): void
|
||||
{
|
||||
$this->sourceId = (int) $sourceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getEmail()
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $email
|
||||
*/
|
||||
public function setEmail(Email $email): void
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
public function getUtmCampaign(): ?string
|
||||
{
|
||||
return $this->utmCampaign;
|
||||
}
|
||||
|
||||
public function setUtmCampaign(?string $utmCampaign): static
|
||||
{
|
||||
$this->utmCampaign = $utmCampaign;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUtmContent(): ?string
|
||||
{
|
||||
return $this->utmContent;
|
||||
}
|
||||
|
||||
public function setUtmContent(?string $utmContent): static
|
||||
{
|
||||
$this->utmContent = $utmContent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUtmMedium(): ?string
|
||||
{
|
||||
return $this->utmMedium;
|
||||
}
|
||||
|
||||
public function setUtmMedium(?string $utmMedium): static
|
||||
{
|
||||
$this->utmMedium = $utmMedium;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUtmSource(): ?string
|
||||
{
|
||||
return $this->utmSource;
|
||||
}
|
||||
|
||||
public function setUtmSource(?string $utmSource): static
|
||||
{
|
||||
$this->utmSource = $utmSource;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUtmTerm(): ?string
|
||||
{
|
||||
return $this->utmTerm;
|
||||
}
|
||||
|
||||
public function setUtmTerm(?string $utmTerm): static
|
||||
{
|
||||
$this->utmTerm = $utmTerm;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\CoreBundle\Helper\Chart\PieChart;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\LeadBundle\Entity\TimelineTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Download>
|
||||
*/
|
||||
class DownloadRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
|
||||
/**
|
||||
* Determine if the download is a unique download.
|
||||
*/
|
||||
public function isUniqueDownload($assetId, $trackingId): bool
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q2 = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$q2->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'asset_downloads', 'd');
|
||||
|
||||
$q2->where(
|
||||
$q2->expr()->and(
|
||||
$q2->expr()->eq('d.tracking_id', ':id'),
|
||||
$q2->expr()->eq('d.asset_id', (int) $assetId)
|
||||
)
|
||||
);
|
||||
|
||||
$q->select('u.is_unique')
|
||||
->from(sprintf('(SELECT (NOT EXISTS (%s)) is_unique)', $q2->getSQL()), 'u'
|
||||
)
|
||||
->setParameter('id', $trackingId);
|
||||
|
||||
return (bool) $q->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a lead's page downloads.
|
||||
*
|
||||
* @param int|null $leadId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLeadDownloads($leadId = null, array $options = [])
|
||||
{
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('a.id as asset_id, d.date_download as dateDownload, a.title, d.id as download_id, d.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'asset_downloads', 'd')
|
||||
->leftJoin('d', MAUTIC_TABLE_PREFIX.'assets', 'a', 'd.asset_id = a.id');
|
||||
|
||||
if ($leadId) {
|
||||
$query->where('d.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
}
|
||||
|
||||
if (isset($options['search']) && $options['search']) {
|
||||
$query->andWhere('a.title LIKE :search')
|
||||
->setParameter('search', '%'.$options['search'].'%');
|
||||
}
|
||||
|
||||
return $this->getTimelineResults($query, $options, 'a.title', 'd.date_download', [], ['date_download'], null, 'd.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of assets ordered by it's download count.
|
||||
*
|
||||
* @param QueryBuilder $query
|
||||
* @param int $limit
|
||||
* @param int $offset
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getMostDownloaded($query, $limit = 10, $offset = 0): array
|
||||
{
|
||||
$query->select('a.title, a.id, count(ad.id) as downloads')
|
||||
->groupBy('a.id, a.title')
|
||||
->orderBy('downloads', 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $query->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of asset referrals ordered by it's count.
|
||||
*
|
||||
* @param QueryBuilder $query
|
||||
* @param int $limit
|
||||
* @param int $offset
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getTopReferrers($query, $limit = 10, $offset = 0): array
|
||||
{
|
||||
$query->select('ad.referer, count(ad.referer) as downloads')
|
||||
->groupBy('ad.referer')
|
||||
->orderBy('downloads', 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $query->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pie graph data for http statuses.
|
||||
*
|
||||
* @param QueryBuilder $query
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getHttpStatuses($query): array
|
||||
{
|
||||
$query->select('ad.code as status, count(ad.code) as count')
|
||||
->groupBy('ad.code')
|
||||
->orderBy('count', 'DESC');
|
||||
|
||||
$results = $query->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$chart = new PieChart();
|
||||
|
||||
foreach ($results as $result) {
|
||||
$chart->setDataset($result['status'], $result['count']);
|
||||
}
|
||||
|
||||
return $chart->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed, array<string, mixed>>
|
||||
*/
|
||||
public function getDownloadCountsByPage($pageId, ?\DateTime $fromDate = null): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('count(distinct(a.tracking_id)) as count, a.source_id as id, p.title as name, p.hits as total')
|
||||
->from(MAUTIC_TABLE_PREFIX.'asset_downloads', 'a')
|
||||
->join('a', MAUTIC_TABLE_PREFIX.'pages', 'p', 'a.source_id = p.id');
|
||||
|
||||
if (is_array($pageId)) {
|
||||
$q->where($q->expr()->in('p.id', $pageId))
|
||||
->groupBy('p.id, a.source_id, p.title, p.hits');
|
||||
} else {
|
||||
$q->where($q->expr()->eq('p.id', ':page'))
|
||||
->setParameter('page', (int) $pageId);
|
||||
}
|
||||
|
||||
$q->andWhere('a.source = "page"')
|
||||
->andWhere('a.code = 200');
|
||||
|
||||
if (null != $fromDate) {
|
||||
$dh = new DateTimeHelper($fromDate);
|
||||
$q->andWhere($q->expr()->gte('a.date_download', ':date'))
|
||||
->setParameter('date', $dh->toUtcString());
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$downloads = [];
|
||||
foreach ($results as $r) {
|
||||
$downloads[$r['id']] = $r;
|
||||
}
|
||||
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get download count by email by linking emails that have been associated with a page hit that has the
|
||||
* same tracking ID as an asset download tracking ID and thus assumed happened in the same session.
|
||||
*
|
||||
* @return array<mixed, array<string, mixed>>
|
||||
*/
|
||||
public function getDownloadCountsByEmail($emailId, ?\DateTime $fromDate = null): array
|
||||
{
|
||||
// link email to page hit tracking id to download tracking id
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('count(distinct(a.tracking_id)) as count, e.id, e.subject as name, e.variant_sent_count as total')
|
||||
->from(MAUTIC_TABLE_PREFIX.'asset_downloads', 'a')
|
||||
->join('a', MAUTIC_TABLE_PREFIX.'emails', 'e', 'a.email_id = e.id');
|
||||
|
||||
if (is_array($emailId)) {
|
||||
$q->where($q->expr()->in('e.id', $emailId))
|
||||
->groupBy('e.id, e.subject, e.variant_sent_count');
|
||||
} else {
|
||||
$q->where($q->expr()->eq('e.id', ':email'))
|
||||
->setParameter('email', (int) $emailId);
|
||||
}
|
||||
|
||||
$q->andWhere('a.code = 200');
|
||||
|
||||
if (null != $fromDate) {
|
||||
$dh = new DateTimeHelper($fromDate);
|
||||
$q->andWhere($q->expr()->gte('a.date_download', ':date'))
|
||||
->setParameter('date', $dh->toUtcString());
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$downloads = [];
|
||||
foreach ($results as $r) {
|
||||
$downloads[$r['id']] = $r;
|
||||
}
|
||||
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
public function updateLeadByTrackingId($leadId, $newTrackingId, $oldTrackingId): void
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'asset_downloads')
|
||||
->set('lead_id', (int) $leadId)
|
||||
->set('tracking_id', ':newTrackingId')
|
||||
->where(
|
||||
$q->expr()->eq('tracking_id', ':oldTrackingId')
|
||||
)
|
||||
->setParameters([
|
||||
'newTrackingId' => $newTrackingId,
|
||||
'oldTrackingId' => $oldTrackingId,
|
||||
])
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.'asset_downloads')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\ErrorHandler;
|
||||
|
||||
use Oneup\UploaderBundle\Uploader\ErrorHandler\ErrorHandlerInterface;
|
||||
use Oneup\UploaderBundle\Uploader\Response\AbstractResponse;
|
||||
|
||||
class DropzoneErrorHandler implements ErrorHandlerInterface
|
||||
{
|
||||
public function addException(AbstractResponse $response, \Exception $exception): void
|
||||
{
|
||||
// HTTP status between 400 and 500 should be set here.
|
||||
// Dropzone will handle error messages itself then.
|
||||
// Unfortunatelly UploaderBundle will have this option in v 3.
|
||||
$response['error'] = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Event;
|
||||
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
|
||||
class AssetEvent extends CommonEvent
|
||||
{
|
||||
/**
|
||||
* @param bool $isNew
|
||||
*/
|
||||
public function __construct(Asset $asset, $isNew = false)
|
||||
{
|
||||
$this->entity = $asset;
|
||||
$this->isNew = $isNew;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Asset entity.
|
||||
*
|
||||
* @return Asset
|
||||
*/
|
||||
public function getAsset()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Asset entity.
|
||||
*/
|
||||
public function setAsset(Asset $asset): void
|
||||
{
|
||||
$this->entity = $asset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\AssetBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
|
||||
final class AssetExportListEvent extends CommonEvent
|
||||
{
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
private array $list = [];
|
||||
|
||||
/**
|
||||
* @param list<array<string, array<string, mixed>>> $data
|
||||
*/
|
||||
public function __construct(private array $data)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, array<string, mixed>>>
|
||||
*/
|
||||
public function getEntityData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function setList(string $item): void
|
||||
{
|
||||
if (!in_array($item, $this->list)) {
|
||||
$this->list[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>|null
|
||||
*/
|
||||
public function getList(): ?array
|
||||
{
|
||||
return $this->list ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Event;
|
||||
|
||||
use Mautic\AssetBundle\Entity\Download;
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
|
||||
class AssetLoadEvent extends CommonEvent
|
||||
{
|
||||
public function __construct(
|
||||
Download $download,
|
||||
protected bool $unique,
|
||||
) {
|
||||
$this->entity = $download;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Download entity.
|
||||
*
|
||||
* @return Download
|
||||
*/
|
||||
public function getRecord()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\AssetBundle\Entity\Asset
|
||||
*/
|
||||
public function getAsset()
|
||||
{
|
||||
return $this->entity->getAsset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if this is the first download for the session.
|
||||
*/
|
||||
public function isUnique(): bool
|
||||
{
|
||||
return $this->unique;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Event;
|
||||
|
||||
use Gaufrette\Adapter;
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\PluginBundle\Integration\UnifiedIntegrationInterface;
|
||||
|
||||
class RemoteAssetBrowseEvent extends CommonEvent
|
||||
{
|
||||
private ?Adapter $adapter = null;
|
||||
private ?string $failureMessage = null;
|
||||
|
||||
public function __construct(
|
||||
private UnifiedIntegrationInterface $integration,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getAdapter(): ?Adapter
|
||||
{
|
||||
return $this->adapter;
|
||||
}
|
||||
|
||||
public function getIntegration(): UnifiedIntegrationInterface
|
||||
{
|
||||
return $this->integration;
|
||||
}
|
||||
|
||||
public function setAdapter(Adapter $adapter): void
|
||||
{
|
||||
$this->adapter = $adapter;
|
||||
}
|
||||
|
||||
public function setFailed(string $message): void
|
||||
{
|
||||
$this->failureMessage = $message;
|
||||
}
|
||||
|
||||
public function getFailureMessage(): ?string
|
||||
{
|
||||
return $this->failureMessage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\AssetBundle\Event\AssetExportListEvent;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
final class AssetExportListEventSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private PathsHelper $pathsHelper)
|
||||
{
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
AssetExportListEvent::class => ['onExportList', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onExportList(AssetExportListEvent $event): void
|
||||
{
|
||||
$data = $event->getEntityData();
|
||||
|
||||
if (empty($data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($event->getEntityData() as $section) {
|
||||
if (!is_array($section)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($section[Asset::ENTITY_NAME]) || !is_array($section[Asset::ENTITY_NAME])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($section[Asset::ENTITY_NAME] as $asset) {
|
||||
$location = $asset['storage_location'] ?? null;
|
||||
$path = $asset['path'] ?? null;
|
||||
|
||||
if ('local' === $location && !empty($path)) {
|
||||
$assetPath = $this->pathsHelper->getSystemPath('media').'/files/'.$path;
|
||||
$event->setList($assetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\AssetBundle\Model\AssetModel;
|
||||
use Mautic\CoreBundle\Event\EntityExportEvent;
|
||||
use Mautic\CoreBundle\Event\EntityImportAnalyzeEvent;
|
||||
use Mautic\CoreBundle\Event\EntityImportEvent;
|
||||
use Mautic\CoreBundle\Event\EntityImportUndoEvent;
|
||||
use Mautic\CoreBundle\EventListener\ImportExportTrait;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
|
||||
final class AssetImportExportSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
use ImportExportTrait;
|
||||
|
||||
public function __construct(
|
||||
private AssetModel $assetModel,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private AuditLogModel $auditLogModel,
|
||||
private IpLookupHelper $ipLookupHelper,
|
||||
private DenormalizerInterface $serializer,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
EntityExportEvent::class => ['onAssetExport', 0],
|
||||
EntityImportEvent::class => ['onAssetImport', 0],
|
||||
EntityImportUndoEvent::class => ['onUndoImport', 0],
|
||||
EntityImportAnalyzeEvent::class => ['onDuplicationCheck', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onAssetExport(EntityExportEvent $event): void
|
||||
{
|
||||
if (Asset::ENTITY_NAME !== $event->getEntityName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$assetId = $event->getEntityId();
|
||||
$asset = $this->assetModel->getEntity($assetId);
|
||||
if (!$asset) {
|
||||
return;
|
||||
}
|
||||
|
||||
$assetData = [
|
||||
'id' => $asset->getId(),
|
||||
'is_published' => $asset->isPublished(),
|
||||
'title' => $asset->getTitle(),
|
||||
'description' => $asset->getDescription(),
|
||||
'alias' => $asset->getAlias(),
|
||||
'storage_location' => $asset->getStorageLocation(),
|
||||
'path' => $asset->getPath(),
|
||||
'remote_path' => $asset->getRemotePath(),
|
||||
'original_file_name' => $asset->getOriginalFileName(),
|
||||
'lang' => $asset->getLanguage(),
|
||||
'publish_up' => $asset->getPublishUp() ? $asset->getPublishUp()->format(DATE_ATOM) : null,
|
||||
'publish_down' => $asset->getPublishDown() ? $asset->getPublishDown()->format(DATE_ATOM) : null,
|
||||
'extension' => $asset->getExtension(),
|
||||
'mime' => $asset->getMime(),
|
||||
'size' => (int) $asset->getSize(),
|
||||
'disallow' => $asset->getDisallow(),
|
||||
'uuid' => $asset->getUuid(),
|
||||
];
|
||||
|
||||
$event->addEntity(Asset::ENTITY_NAME, $assetData);
|
||||
$log = [
|
||||
'bundle' => 'asset',
|
||||
'object' => 'asset',
|
||||
'objectId' => $asset->getId(),
|
||||
'action' => 'export',
|
||||
'details' => $assetData,
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
|
||||
public function onAssetImport(EntityImportEvent $event): void
|
||||
{
|
||||
if (Asset::ENTITY_NAME !== $event->getEntityName() || !$event->getEntityData()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stats = [
|
||||
EntityImportEvent::NEW => ['names' => [], 'ids' => [], 'count' => 0],
|
||||
EntityImportEvent::UPDATE => ['names' => [], 'ids' => [], 'count' => 0],
|
||||
];
|
||||
|
||||
foreach ($event->getEntityData() as $element) {
|
||||
$object = $this->entityManager->getRepository(Asset::class)->findOneBy(['uuid' => $element['uuid']]);
|
||||
$isNew = !$object;
|
||||
|
||||
$object ??= new Asset();
|
||||
if (isset($element['size'])) {
|
||||
$element['size'] = (int) $element['size'];
|
||||
}
|
||||
$this->serializer->denormalize(
|
||||
$element,
|
||||
Asset::class,
|
||||
null,
|
||||
['object_to_populate' => $object]
|
||||
);
|
||||
$this->assetModel->saveEntity($object);
|
||||
|
||||
$event->addEntityIdMap((int) $element['id'], $object->getId());
|
||||
|
||||
$status = $isNew ? EntityImportEvent::NEW : EntityImportEvent::UPDATE;
|
||||
$stats[$status]['names'][] = $object->getTitle();
|
||||
$stats[$status]['ids'][] = $object->getId();
|
||||
++$stats[$status]['count'];
|
||||
|
||||
$this->logAction('import', $object->getId(), $element);
|
||||
}
|
||||
foreach ($stats as $status => $info) {
|
||||
if ($info['count'] > 0) {
|
||||
$event->setStatus($status, [Asset::ENTITY_NAME => $info]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onUndoImport(EntityImportUndoEvent $event): void
|
||||
{
|
||||
if (Asset::ENTITY_NAME !== $event->getEntityName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$summary = $event->getSummary();
|
||||
|
||||
if (!isset($summary['ids']) || empty($summary['ids'])) {
|
||||
return;
|
||||
}
|
||||
foreach ($summary['ids'] as $id) {
|
||||
$entity = $this->entityManager->getRepository(Asset::class)->find($id);
|
||||
|
||||
if ($entity) {
|
||||
$this->entityManager->remove($entity);
|
||||
$this->logAction('undo_import', $id, ['deletedEntity' => Asset::class]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function onDuplicationCheck(EntityImportAnalyzeEvent $event): void
|
||||
{
|
||||
$this->performDuplicationCheck(
|
||||
$event,
|
||||
Asset::ENTITY_NAME,
|
||||
Asset::class,
|
||||
'title',
|
||||
$this->entityManager
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $details
|
||||
*/
|
||||
private function logAction(string $action, int $objectId, array $details): void
|
||||
{
|
||||
$this->auditLogModel->writeToLog([
|
||||
'bundle' => 'asset',
|
||||
'object' => 'asset',
|
||||
'objectId' => $objectId,
|
||||
'action' => $action,
|
||||
'details' => $details,
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\AssetEvents;
|
||||
use Mautic\AssetBundle\Event as Events;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class AssetSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private IpLookupHelper $ipLookupHelper,
|
||||
private AuditLogModel $auditLogModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
AssetEvents::ASSET_POST_SAVE => ['onAssetPostSave', 0],
|
||||
AssetEvents::ASSET_POST_DELETE => ['onAssetDelete', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry to the audit log.
|
||||
*/
|
||||
public function onAssetPostSave(Events\AssetEvent $event): void
|
||||
{
|
||||
$asset = $event->getAsset();
|
||||
if ($details = $event->getChanges()) {
|
||||
$log = [
|
||||
'bundle' => 'asset',
|
||||
'object' => 'asset',
|
||||
'objectId' => $asset->getId(),
|
||||
'action' => ($event->isNew()) ? 'create' : 'update',
|
||||
'details' => $details,
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a delete entry to the audit log.
|
||||
*/
|
||||
public function onAssetDelete(Events\AssetEvent $event): void
|
||||
{
|
||||
$asset = $event->getAsset();
|
||||
$log = [
|
||||
'bundle' => 'asset',
|
||||
'object' => 'asset',
|
||||
'objectId' => $asset->deletedId,
|
||||
'action' => 'delete',
|
||||
'details' => ['name' => $asset->getTitle()],
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
|
||||
// In case of batch delete, this method call remove the uploaded file
|
||||
$asset->removeUpload();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\Helper\TokenHelper;
|
||||
use Mautic\CoreBundle\Event\BuilderEvent;
|
||||
use Mautic\CoreBundle\Helper\BuilderTokenHelperFactory;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\EmailBundle\EmailEvents;
|
||||
use Mautic\EmailBundle\Event\EmailSendEvent;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\PageBundle\Event\PageDisplayEvent;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class BuilderSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private string $assetToken = '{assetlink=(.*?)}';
|
||||
|
||||
public function __construct(
|
||||
private CorePermissions $security,
|
||||
private TokenHelper $tokenHelper,
|
||||
private ContactTracker $contactTracker,
|
||||
private BuilderTokenHelperFactory $builderTokenHelperFactory,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
EmailEvents::EMAIL_ON_BUILD => ['onBuilderBuild', 0],
|
||||
EmailEvents::EMAIL_ON_SEND => ['onEmailGenerate', 0],
|
||||
EmailEvents::EMAIL_ON_DISPLAY => ['onEmailGenerate', 0],
|
||||
PageEvents::PAGE_ON_BUILD => ['onBuilderBuild', 0],
|
||||
PageEvents::PAGE_ON_DISPLAY => ['onPageDisplay', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onBuilderBuild(BuilderEvent $event): void
|
||||
{
|
||||
if ($event->tokensRequested($this->assetToken)) {
|
||||
$tokenHelper = $this->builderTokenHelperFactory->getBuilderTokenHelper('asset');
|
||||
$event->addTokensFromHelper($tokenHelper, $this->assetToken, 'title', 'id', true);
|
||||
}
|
||||
}
|
||||
|
||||
public function onEmailGenerate(EmailSendEvent $event): void
|
||||
{
|
||||
$lead = $event->getLead();
|
||||
$leadId = (int) (null !== $lead ? $lead['id'] : null);
|
||||
$email = $event->getEmail();
|
||||
$tokens = $this->generateTokensFromContent($event, $leadId, $event->getSource(), null === $email ? null : $email->getId());
|
||||
$event->addTokens($tokens);
|
||||
}
|
||||
|
||||
public function onPageDisplay(PageDisplayEvent $event): void
|
||||
{
|
||||
if (!$lead = $event->getLead()) {
|
||||
$lead = $this->security->isAnonymous() ? $this->contactTracker->getContact() : null;
|
||||
}
|
||||
|
||||
$leadId = $lead ? $lead->getId() : null;
|
||||
$page = $event->getPage();
|
||||
$tokens = $this->generateTokensFromContent($event, $leadId, ['page', $page->getId()]);
|
||||
$content = $event->getContent();
|
||||
|
||||
if ([] !== $tokens) {
|
||||
$content = str_ireplace(array_keys($tokens), $tokens, $content);
|
||||
}
|
||||
$event->setContent($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PageDisplayEvent|EmailSendEvent $event
|
||||
* @param array $source
|
||||
* @param int|null $emailId
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function generateTokensFromContent($event, ?int $leadId, $source = [], $emailId = null): array
|
||||
{
|
||||
if ($event instanceof PageDisplayEvent || ($event instanceof EmailSendEvent && $event->shouldAppendClickthrough())) {
|
||||
$clickthrough = [
|
||||
'source' => $source,
|
||||
'lead' => $leadId ?? false,
|
||||
'email' => $emailId ?? false,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->tokenHelper->findAssetTokens($event->getContent(), array_filter($clickthrough ?? []));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\AssetEvents;
|
||||
use Mautic\AssetBundle\Event\AssetLoadEvent;
|
||||
use Mautic\AssetBundle\Form\Type\CampaignEventAssetDownloadType;
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Event\CampaignBuilderEvent;
|
||||
use Mautic\CampaignBundle\Event\CampaignExecutionEvent;
|
||||
use Mautic\CampaignBundle\Executioner\RealTimeExecutioner;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class CampaignSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private RealTimeExecutioner $realTimeExecutioner,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CampaignEvents::CAMPAIGN_ON_BUILD => ['onCampaignBuild', 0],
|
||||
AssetEvents::ASSET_ON_LOAD => ['onAssetDownload', 0],
|
||||
AssetEvents::ON_CAMPAIGN_TRIGGER_DECISION => ['onCampaignTriggerDecision', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onCampaignBuild(CampaignBuilderEvent $event): void
|
||||
{
|
||||
$trigger = [
|
||||
'label' => 'mautic.asset.campaign.event.download',
|
||||
'description' => 'mautic.asset.campaign.event.download_descr',
|
||||
'eventName' => AssetEvents::ON_CAMPAIGN_TRIGGER_DECISION,
|
||||
'formType' => CampaignEventAssetDownloadType::class,
|
||||
'channel' => 'asset',
|
||||
'channelIdField' => 'assets',
|
||||
];
|
||||
|
||||
$event->addDecision('asset.download', $trigger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger point actions for asset download.
|
||||
*/
|
||||
public function onAssetDownload(AssetLoadEvent $event): void
|
||||
{
|
||||
$asset = $event->getRecord()->getAsset();
|
||||
|
||||
if (null !== $asset) {
|
||||
$this->realTimeExecutioner->execute('asset.download', $asset, 'asset', $asset->getId());
|
||||
}
|
||||
}
|
||||
|
||||
public function onCampaignTriggerDecision(CampaignExecutionEvent $event)
|
||||
{
|
||||
$eventDetails = $event->getEventDetails();
|
||||
|
||||
if (null == $eventDetails) {
|
||||
return $event->setResult(true);
|
||||
}
|
||||
|
||||
$assetId = $eventDetails->getId();
|
||||
$limitToAssets = $event->getConfig()['assets'];
|
||||
|
||||
if (!empty($limitToAssets) && !in_array($assetId, $limitToAssets)) {
|
||||
// no points change
|
||||
return $event->setResult(false);
|
||||
}
|
||||
|
||||
$event->setResult(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\Form\Type\ConfigType;
|
||||
use Mautic\ConfigBundle\ConfigEvents;
|
||||
use Mautic\ConfigBundle\Event\ConfigBuilderEvent;
|
||||
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' => 'AssetBundle',
|
||||
'formAlias' => 'assetconfig',
|
||||
'formType' => ConfigType::class,
|
||||
'formTheme' => '@MauticAsset/FormTheme/Config/_config_assetconfig_widget.html.twig',
|
||||
'parameters' => $event->getParametersFromConfig('MauticAssetBundle'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\Model\AssetModel;
|
||||
use Mautic\DashboardBundle\Event\WidgetDetailEvent;
|
||||
use Mautic\DashboardBundle\EventListener\DashboardSubscriber as MainDashboardSubscriber;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
class DashboardSubscriber extends MainDashboardSubscriber
|
||||
{
|
||||
/**
|
||||
* Define the name of the bundle/category of the widget(s).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bundle = 'asset';
|
||||
|
||||
/**
|
||||
* Define the widget(s).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $types = [
|
||||
'asset.downloads.in.time' => [],
|
||||
'unique.vs.repetitive.downloads' => [],
|
||||
'popular.assets' => [],
|
||||
'created.assets' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* Define permissions to see those widgets.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'asset:assets:viewown',
|
||||
'asset:assets:viewother',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected AssetModel $assetModel,
|
||||
protected RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a widget detail when needed.
|
||||
*/
|
||||
public function onWidgetDetailGenerate(WidgetDetailEvent $event): void
|
||||
{
|
||||
$this->checkPermissions($event);
|
||||
$canViewOthers = $event->hasPermission('asset:assets:viewother');
|
||||
|
||||
if ('asset.downloads.in.time' == $event->getType()) {
|
||||
$widget = $event->getWidget();
|
||||
$params = $widget->getParams();
|
||||
|
||||
if (!$event->isCached()) {
|
||||
$event->setTemplateData([
|
||||
'chartType' => 'line',
|
||||
'chartHeight' => $widget->getHeight() - 80,
|
||||
'chartData' => $this->assetModel->getDownloadsLineChartData(
|
||||
$params['timeUnit'],
|
||||
$params['dateFrom'],
|
||||
$params['dateTo'],
|
||||
$params['dateFormat'],
|
||||
$canViewOthers
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
$event->setTemplate('@MauticCore/Helper/chart.html.twig');
|
||||
$event->stopPropagation();
|
||||
}
|
||||
|
||||
if ('unique.vs.repetitive.downloads' == $event->getType()) {
|
||||
if (!$event->isCached()) {
|
||||
$params = $event->getWidget()->getParams();
|
||||
$event->setTemplateData([
|
||||
'chartType' => 'pie',
|
||||
'chartHeight' => $event->getWidget()->getHeight() - 80,
|
||||
'chartData' => $this->assetModel->getUniqueVsRepetitivePieChartData($params['dateFrom'], $params['dateTo'], $canViewOthers),
|
||||
]);
|
||||
}
|
||||
|
||||
$event->setTemplate('@MauticCore/Helper/chart.html.twig');
|
||||
$event->stopPropagation();
|
||||
}
|
||||
|
||||
if ('popular.assets' == $event->getType()) {
|
||||
if (!$event->isCached()) {
|
||||
$params = $event->getWidget()->getParams();
|
||||
|
||||
if (empty($params['limit'])) {
|
||||
// Count the pages limit from the widget height
|
||||
$limit = round((($event->getWidget()->getHeight() - 80) / 35) - 1);
|
||||
} else {
|
||||
$limit = $params['limit'];
|
||||
}
|
||||
|
||||
$assets = $this->assetModel->getPopularAssets($limit, $params['dateFrom'], $params['dateTo'], $canViewOthers);
|
||||
$items = [];
|
||||
|
||||
// Build table rows with links
|
||||
foreach ($assets as &$asset) {
|
||||
$assetUrl = $this->router->generate('mautic_asset_action', ['objectAction' => 'view', 'objectId' => $asset['id']]);
|
||||
$row = [
|
||||
[
|
||||
'value' => $asset['title'],
|
||||
'type' => 'link',
|
||||
'link' => $assetUrl,
|
||||
],
|
||||
[
|
||||
'value' => $asset['download_count'],
|
||||
],
|
||||
];
|
||||
$items[] = $row;
|
||||
}
|
||||
|
||||
$event->setTemplateData([
|
||||
'headItems' => [
|
||||
'mautic.dashboard.label.title',
|
||||
'mautic.dashboard.label.downloads',
|
||||
],
|
||||
'bodyItems' => $items,
|
||||
'raw' => $assets,
|
||||
]);
|
||||
}
|
||||
|
||||
$event->setTemplate('@MauticCore/Helper/table.html.twig');
|
||||
$event->stopPropagation();
|
||||
}
|
||||
|
||||
if ('created.assets' == $event->getType()) {
|
||||
if (!$event->isCached()) {
|
||||
$params = $event->getWidget()->getParams();
|
||||
|
||||
if (empty($params['limit'])) {
|
||||
// Count the assets limit from the widget height
|
||||
$limit = round((($event->getWidget()->getHeight() - 80) / 35) - 1);
|
||||
} else {
|
||||
$limit = $params['limit'];
|
||||
}
|
||||
|
||||
$assets = $this->assetModel->getAssetList($limit, $params['dateFrom'], $params['dateTo'], [], ['canViewOthers' => $canViewOthers]);
|
||||
$items = [];
|
||||
|
||||
// Build table rows with links
|
||||
foreach ($assets as &$asset) {
|
||||
$assetUrl = $this->router->generate('mautic_asset_action', ['objectAction' => 'view', 'objectId' => $asset['id']]);
|
||||
$row = [
|
||||
[
|
||||
'value' => $asset['name'],
|
||||
'type' => 'link',
|
||||
'link' => $assetUrl,
|
||||
],
|
||||
];
|
||||
$items[] = $row;
|
||||
}
|
||||
|
||||
$event->setTemplateData([
|
||||
'headItems' => [
|
||||
'mautic.dashboard.label.title',
|
||||
],
|
||||
'bodyItems' => $items,
|
||||
'raw' => $assets,
|
||||
]);
|
||||
}
|
||||
|
||||
$event->setTemplate('@MauticCore/Helper/table.html.twig');
|
||||
$event->stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\AssetBundle\AssetEvents;
|
||||
use Mautic\AssetBundle\Entity\Download;
|
||||
use Mautic\CoreBundle\Event\DetermineWinnerEvent;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class DetermineWinnerSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
AssetEvents::ON_DETERMINE_DOWNLOAD_RATE_WINNER => ['onDetermineDownloadRateWinner', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the winner of A/B test based on number of asset downloads.
|
||||
*/
|
||||
public function onDetermineDownloadRateWinner(DetermineWinnerEvent $event): void
|
||||
{
|
||||
$repo = $this->em->getRepository(Download::class);
|
||||
$parameters = $event->getParameters();
|
||||
$parent = $parameters['parent'];
|
||||
$children = $parameters['children'];
|
||||
|
||||
// if this is an email A/B test, then link email to page to form submission
|
||||
// if it is a page A/B test, then link form submission to page
|
||||
$type = ($parent instanceof Email) ? 'email' : 'page';
|
||||
|
||||
$ids = [$parent->getId()];
|
||||
|
||||
foreach ($children as $c) {
|
||||
if ($c->isPublished()) {
|
||||
$id = $c->getId();
|
||||
$ids[] = $id;
|
||||
}
|
||||
}
|
||||
|
||||
$startDate = $parent->getVariantStartDate();
|
||||
if (null != $startDate && !empty($ids)) {
|
||||
$counts = ('page' == $type) ? $repo->getDownloadCountsByPage($ids, $startDate) : $repo->getDownloadCountsByEmail($ids, $startDate);
|
||||
|
||||
$translator = $this->translator;
|
||||
if ($counts) {
|
||||
$downloads = $support = $data = [];
|
||||
$hasResults = [];
|
||||
|
||||
$downloadsLabel = $translator->trans('mautic.asset.abtest.label.downloads');
|
||||
$hitsLabel = ('page' == $type) ? $translator->trans('mautic.asset.abtest.label.hits') : $translator->trans('mautic.asset.abtest.label.sentemils');
|
||||
foreach ($counts as $stats) {
|
||||
$rate = ($stats['total']) ? round(($stats['count'] / $stats['total']) * 100, 2) : 0;
|
||||
$downloads[$stats['id']] = $rate;
|
||||
$data[$downloadsLabel][] = $stats['count'];
|
||||
$data[$hitsLabel][] = $stats['total'];
|
||||
$support['labels'][] = $stats['id'].':'.$stats['name'].' ('.$rate.'%)';
|
||||
$hasResults[] = $stats['id'];
|
||||
}
|
||||
|
||||
// make sure that parent and published children are included
|
||||
if (!in_array($parent->getId(), $hasResults)) {
|
||||
$data[$downloadsLabel][] = 0;
|
||||
$data[$hitsLabel][] = 0;
|
||||
$support['labels'][] = $parent->getId().':'.(('page' == $type) ? $parent->getTitle() : $parent->getName()).' (0%)';
|
||||
}
|
||||
|
||||
foreach ($children as $c) {
|
||||
if ($c->isPublished()) {
|
||||
if (!in_array($c->getId(), $hasResults)) {
|
||||
$data[$downloadsLabel][] = 0;
|
||||
$data[$hitsLabel][] = 0;
|
||||
$support['labels'][] = $c->getId().':'.(('page' == $type) ? $c->getTitle() : $c->getName()).' (0%)';
|
||||
}
|
||||
}
|
||||
}
|
||||
$support['data'] = $data;
|
||||
|
||||
// set max for scales
|
||||
$maxes = [];
|
||||
foreach ($support['data'] as $data) {
|
||||
$maxes[] = max($data);
|
||||
}
|
||||
$top = max($maxes);
|
||||
$support['step_width'] = (ceil($top / 10) * 10);
|
||||
|
||||
// put in order from least to greatest just because
|
||||
asort($downloads);
|
||||
|
||||
// who's the winner?
|
||||
$max = max($downloads);
|
||||
|
||||
// get the page ids with the most number of downloads
|
||||
$winners = ($max > 0) ? array_keys($downloads, $max) : [];
|
||||
|
||||
$event->setAbTestResults([
|
||||
'winners' => $winners,
|
||||
'support' => $support,
|
||||
'basedOn' => 'asset.downloads',
|
||||
'supportTemplate' => '@MauticPage/SubscribedEvents/AbTest/bargraph.html.twig',
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$event->setAbTestResults([
|
||||
'winners' => [],
|
||||
'support' => [],
|
||||
'basedOn' => 'asset.downloads',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\AssetEvents;
|
||||
use Mautic\EmailBundle\EmailEvents;
|
||||
use Mautic\EmailBundle\Event\EmailBuilderEvent;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class EmailSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
EmailEvents::EMAIL_ON_BUILD => ['onEmailBuild', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onEmailBuild(EmailBuilderEvent $event): void
|
||||
{
|
||||
if ($event->abTestWinnerCriteriaRequested()) {
|
||||
// add AB Test Winner Criteria
|
||||
$formSubmissions = [
|
||||
'group' => 'mautic.asset.abtest.criteria',
|
||||
'label' => 'mautic.asset.abtest.criteria.downloads',
|
||||
'event' => AssetEvents::ON_DETERMINE_DOWNLOAD_RATE_WINNER,
|
||||
];
|
||||
$event->addAbTestWinnerCriteria('asset.downloads', $formSubmissions);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\AssetBundle\Form\Type\FormSubmitActionDownloadFileType;
|
||||
use Mautic\AssetBundle\Model\AssetModel;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\ThemeHelperInterface;
|
||||
use Mautic\CoreBundle\Twig\Helper\AnalyticsHelper;
|
||||
use Mautic\CoreBundle\Twig\Helper\AssetsHelper;
|
||||
use Mautic\FormBundle\Entity\Form;
|
||||
use Mautic\FormBundle\Event\FormBuilderEvent;
|
||||
use Mautic\FormBundle\Event\SubmissionEvent;
|
||||
use Mautic\FormBundle\FormEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
class FormSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private AssetModel $assetModel,
|
||||
protected TranslatorInterface $translator,
|
||||
private AnalyticsHelper $analyticsHelper,
|
||||
private AssetsHelper $assetsHelper,
|
||||
private ThemeHelperInterface $themeHelper,
|
||||
private Environment $twig,
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
FormEvents::FORM_ON_BUILD => ['onFormBuilder', 0],
|
||||
FormEvents::ON_EXECUTE_SUBMIT_ACTION => [
|
||||
['onFormSubmitActionAssetDownload', 0],
|
||||
['onFormSubmitActionDownloadFile', 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a lead generation action to available form submit actions.
|
||||
*/
|
||||
public function onFormBuilder(FormBuilderEvent $event): void
|
||||
{
|
||||
$event->addSubmitAction('asset.download', [
|
||||
'group' => 'mautic.asset.actions',
|
||||
'label' => 'mautic.asset.asset.submitaction.downloadfile',
|
||||
'description' => 'mautic.asset.asset.submitaction.downloadfile_descr',
|
||||
'formType' => FormSubmitActionDownloadFileType::class,
|
||||
'formTypeCleanMasks' => ['message' => 'html'],
|
||||
'eventName' => FormEvents::ON_EXECUTE_SUBMIT_ACTION,
|
||||
'allowCampaignForm' => true,
|
||||
'template' => '@MauticAsset/Action/asset.html.twig',
|
||||
]);
|
||||
}
|
||||
|
||||
public function onFormSubmitActionAssetDownload(SubmissionEvent $event): void
|
||||
{
|
||||
if (false === $event->checkContext('asset.download')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$properties = $event->getAction()->getProperties();
|
||||
$assetId = $properties['asset'];
|
||||
$categoryId = $properties['category'] ?? null;
|
||||
$asset = null;
|
||||
|
||||
if (null !== $assetId) {
|
||||
$asset = $this->assetModel->getEntity($assetId);
|
||||
} elseif (null !== $categoryId) {
|
||||
try {
|
||||
$asset = $this->assetModel->getRepository()->getLatestAssetForCategory($categoryId);
|
||||
} catch (NoResultException|NonUniqueResultException) {
|
||||
$asset = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($asset instanceof Asset && $asset->isPublished()) {
|
||||
$event->setPostSubmitCallback('asset.download_file', [
|
||||
'eventName' => FormEvents::ON_EXECUTE_SUBMIT_ACTION,
|
||||
'form' => $event->getAction()->getForm(),
|
||||
'asset' => $asset,
|
||||
'message' => $properties['message'] ?? '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function onFormSubmitActionDownloadFile(SubmissionEvent $event): void
|
||||
{
|
||||
if (false === $event->checkContext('asset.download_file')) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* No further actions can run after this, as we need to send the
|
||||
* download response to the client.
|
||||
*/
|
||||
$event->stopPropagation();
|
||||
|
||||
/**
|
||||
* @var Form $form
|
||||
* @var Asset $asset
|
||||
* @var string $message
|
||||
* @var bool $messengerMode
|
||||
*/
|
||||
[
|
||||
'form' => $form,
|
||||
'asset' => $asset,
|
||||
'message' => $message,
|
||||
'messengerMode' => $messengerMode,
|
||||
] = $event->getPostSubmitCallback('asset.download_file');
|
||||
|
||||
$url = $this->assetModel->generateUrl($asset, true, [
|
||||
'lead' => $event->getLead() ? $event->getLead()->getId() : null,
|
||||
'channel' => ['form' => $form->getId()],
|
||||
]).'&stream=0';
|
||||
|
||||
if ($messengerMode) {
|
||||
$event->setPostSubmitResponse(['download' => $url]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$msg = $message.$this->translator->trans('mautic.asset.asset.submitaction.downloadfile.msg', [
|
||||
'%url%' => $url,
|
||||
]);
|
||||
|
||||
$analytics = $this->analyticsHelper->getCode();
|
||||
|
||||
if (!empty($analytics)) {
|
||||
$this->assetsHelper->addCustomDeclaration($analytics);
|
||||
}
|
||||
|
||||
$event->setPostSubmitResponse(new Response(
|
||||
$this->twig->render(
|
||||
$this->themeHelper->checkForTwigTemplate('@themes/'.$this->coreParametersHelper->get('theme').'/html/message.html.twig'),
|
||||
[
|
||||
'message' => $msg,
|
||||
'type' => 'notice',
|
||||
'template' => $this->coreParametersHelper->get('theme'),
|
||||
]
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\Entity\DownloadRepository;
|
||||
use Mautic\AssetBundle\Model\AssetModel;
|
||||
use Mautic\LeadBundle\Event\LeadChangeEvent;
|
||||
use Mautic\LeadBundle\Event\LeadMergeEvent;
|
||||
use Mautic\LeadBundle\Event\LeadTimelineEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class LeadSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private AssetModel $assetModel,
|
||||
private TranslatorInterface $translator,
|
||||
private RouterInterface $router,
|
||||
private DownloadRepository $downloadRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
LeadEvents::TIMELINE_ON_GENERATE => ['onTimelineGenerate', 0],
|
||||
LeadEvents::CURRENT_LEAD_CHANGED => ['onLeadChange', 0],
|
||||
LeadEvents::LEAD_POST_MERGE => ['onLeadMerge', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile events for the lead timeline.
|
||||
*/
|
||||
public function onTimelineGenerate(LeadTimelineEvent $event): void
|
||||
{
|
||||
// Set available event types
|
||||
$eventTypeKey = 'asset.download';
|
||||
$eventTypeName = $this->translator->trans('mautic.asset.event.download');
|
||||
$event->addEventType($eventTypeKey, $eventTypeName);
|
||||
$event->addSerializerGroup('assetList');
|
||||
|
||||
// Decide if those events are filtered
|
||||
if (!$event->isApplicable($eventTypeKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$downloads = $this->downloadRepository->getLeadDownloads($event->getLeadId(), $event->getQueryOptions());
|
||||
|
||||
// Add total number to counter
|
||||
$event->addToCounter($eventTypeKey, $downloads);
|
||||
|
||||
if (!$event->isEngagementCount()) {
|
||||
// Add the downloads to the event array
|
||||
foreach ($downloads['results'] as $download) {
|
||||
$asset = $this->assetModel->getEntity($download['asset_id']);
|
||||
$event->addEvent(
|
||||
[
|
||||
'event' => $eventTypeKey,
|
||||
'eventId' => $eventTypeKey.$download['download_id'],
|
||||
'eventLabel' => [
|
||||
'label' => $download['title'],
|
||||
'href' => $this->router->generate('mautic_asset_action', ['objectAction' => 'view', 'objectId' => $download['asset_id']]),
|
||||
],
|
||||
'extra' => [
|
||||
'asset' => $asset,
|
||||
'assetDownloadUrl' => $this->assetModel->generateUrl($asset),
|
||||
],
|
||||
'eventType' => $eventTypeName,
|
||||
'timestamp' => $download['dateDownload'],
|
||||
'icon' => 'ri-download-line',
|
||||
'contentTemplate' => '@MauticAsset/SubscribedEvents/Timeline/index.html.twig',
|
||||
'contactId' => $download['lead_id'],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onLeadChange(LeadChangeEvent $event): void
|
||||
{
|
||||
$this->assetModel->getDownloadRepository()->updateLeadByTrackingId(
|
||||
$event->getNewLead()->getId(),
|
||||
$event->getNewTrackingId(),
|
||||
$event->getOldTrackingId()
|
||||
);
|
||||
}
|
||||
|
||||
public function onLeadMerge(LeadMergeEvent $event): void
|
||||
{
|
||||
$this->assetModel->getDownloadRepository()->updateLead($event->getLoser()->getId(), $event->getVictor()->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\AssetEvents;
|
||||
use Mautic\PageBundle\Event\PageBuilderEvent;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class PageSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
PageEvents::PAGE_ON_BUILD => ['OnPageBuild', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add forms to available page tokens.
|
||||
*/
|
||||
public function onPageBuild(PageBuilderEvent $event): void
|
||||
{
|
||||
if ($event->abTestWinnerCriteriaRequested()) {
|
||||
// add AB Test Winner Criteria
|
||||
$assetDownloads = [
|
||||
'group' => 'mautic.asset.abtest.criteria',
|
||||
'label' => 'mautic.asset.abtest.criteria.downloads',
|
||||
'event' => AssetEvents::ON_DETERMINE_DOWNLOAD_RATE_WINNER,
|
||||
];
|
||||
$event->addAbTestWinnerCriteria('asset.downloads', $assetDownloads);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\AssetEvents;
|
||||
use Mautic\AssetBundle\Event\AssetLoadEvent;
|
||||
use Mautic\AssetBundle\Form\Type\PointActionAssetDownloadType;
|
||||
use Mautic\PointBundle\Event\PointBuilderEvent;
|
||||
use Mautic\PointBundle\Model\PointModel;
|
||||
use Mautic\PointBundle\PointEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class PointSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private PointModel $pointModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
PointEvents::POINT_ON_BUILD => ['onPointBuild', 0],
|
||||
AssetEvents::ASSET_ON_LOAD => ['onAssetDownload', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onPointBuild(PointBuilderEvent $event): void
|
||||
{
|
||||
$action = [
|
||||
'group' => 'mautic.asset.actions',
|
||||
'label' => 'mautic.asset.point.action.download',
|
||||
'description' => 'mautic.asset.point.action.download_descr',
|
||||
'callback' => [\Mautic\AssetBundle\Helper\PointActionHelper::class, 'validateAssetDownload'],
|
||||
'formType' => PointActionAssetDownloadType::class,
|
||||
];
|
||||
|
||||
$event->addAction('asset.download', $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger point actions for asset download.
|
||||
*/
|
||||
public function onAssetDownload(AssetLoadEvent $event): void
|
||||
{
|
||||
$asset = $event->getRecord()->getAsset();
|
||||
|
||||
if (null !== $asset) {
|
||||
$this->pointModel->triggerAction('asset.download', $asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\Entity\DownloadRepository;
|
||||
use Mautic\CoreBundle\Helper\Chart\LineChart;
|
||||
use Mautic\LeadBundle\Model\CompanyReportData;
|
||||
use Mautic\LeadBundle\Report\DncReportService;
|
||||
use Mautic\ReportBundle\Event\ReportBuilderEvent;
|
||||
use Mautic\ReportBundle\Event\ReportDataEvent;
|
||||
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 CONTEXT_ASSET = 'assets';
|
||||
|
||||
public const CONTEXT_ASSET_DOWNLOAD = 'asset.downloads';
|
||||
|
||||
public function __construct(
|
||||
private CompanyReportData $companyReportData,
|
||||
private DownloadRepository $downloadRepository,
|
||||
private DncReportService $dncReportService,
|
||||
) {
|
||||
}
|
||||
|
||||
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],
|
||||
ReportEvents::REPORT_ON_DISPLAY => ['onReportDisplay', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add available tables and columns to the report builder lookup.
|
||||
*/
|
||||
public function onReportBuilder(ReportBuilderEvent $event): void
|
||||
{
|
||||
if (!$event->checkContext([self::CONTEXT_ASSET, self::CONTEXT_ASSET_DOWNLOAD])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Assets
|
||||
$prefix = 'a.';
|
||||
$columns = [
|
||||
$prefix.'download_count' => [
|
||||
'alias' => 'download_count',
|
||||
'label' => 'mautic.asset.report.download_count',
|
||||
'type' => 'int',
|
||||
],
|
||||
$prefix.'unique_download_count' => [
|
||||
'alias' => 'unique_download_count',
|
||||
'label' => 'mautic.asset.report.unique_download_count',
|
||||
'type' => 'int',
|
||||
],
|
||||
$prefix.'alias' => [
|
||||
'label' => 'mautic.core.alias',
|
||||
'type' => 'string',
|
||||
],
|
||||
$prefix.'lang' => [
|
||||
'label' => 'mautic.core.language',
|
||||
'type' => 'string',
|
||||
],
|
||||
$prefix.'title' => [
|
||||
'label' => 'mautic.core.title',
|
||||
'type' => 'string',
|
||||
],
|
||||
];
|
||||
|
||||
$columns = array_merge(
|
||||
$columns,
|
||||
$event->getStandardColumns($prefix, ['name'], 'mautic_asset_action'),
|
||||
$event->getCategoryColumns()
|
||||
);
|
||||
|
||||
$event->addTable(
|
||||
self::CONTEXT_ASSET,
|
||||
[
|
||||
'display_name' => 'mautic.asset.assets',
|
||||
'columns' => $columns,
|
||||
]
|
||||
);
|
||||
|
||||
if ($event->checkContext([self::CONTEXT_ASSET_DOWNLOAD])) {
|
||||
// asset downloads calculate this columns
|
||||
$columns[$prefix.'download_count']['formula'] = 'COUNT(ad.id)';
|
||||
$columns[$prefix.'unique_download_count']['formula'] = 'COUNT(DISTINCT ad.lead_id)';
|
||||
|
||||
// Downloads
|
||||
$downloadPrefix = 'ad.';
|
||||
$downloadColumns = [
|
||||
$downloadPrefix.'date_download' => [
|
||||
'label' => 'mautic.asset.report.download.date_download',
|
||||
'type' => 'datetime',
|
||||
'groupByFormula' => 'DATE('.$downloadPrefix.'date_download)',
|
||||
],
|
||||
$downloadPrefix.'code' => [
|
||||
'label' => 'mautic.asset.report.download.code',
|
||||
'type' => 'string',
|
||||
],
|
||||
$downloadPrefix.'referer' => [
|
||||
'label' => 'mautic.core.referer',
|
||||
'type' => 'string',
|
||||
],
|
||||
$downloadPrefix.'source' => [
|
||||
'label' => 'mautic.report.field.source',
|
||||
'type' => 'string',
|
||||
],
|
||||
$downloadPrefix.'source_id' => [
|
||||
'label' => 'mautic.report.field.source_id',
|
||||
'type' => 'int',
|
||||
],
|
||||
$downloadPrefix.'utm_campaign' => [
|
||||
'label' => 'mautic.report.field.utm_campaign',
|
||||
'type' => 'string',
|
||||
],
|
||||
$downloadPrefix.'utm_content' => [
|
||||
'label' => 'mautic.report.field.utm_content',
|
||||
'type' => 'string',
|
||||
],
|
||||
$downloadPrefix.'utm_medium' => [
|
||||
'label' => 'mautic.report.field.utm_medium',
|
||||
'type' => 'string',
|
||||
],
|
||||
$downloadPrefix.'utm_source' => [
|
||||
'label' => 'mautic.report.field.utm_source',
|
||||
'type' => 'string',
|
||||
],
|
||||
$downloadPrefix.'utm_term' => [
|
||||
'label' => 'mautic.report.field.utm_term',
|
||||
'type' => 'string',
|
||||
],
|
||||
];
|
||||
|
||||
$companyColumns = $this->companyReportData->getCompanyData();
|
||||
$commonColumnsAndFilters = array_merge(
|
||||
$columns,
|
||||
$downloadColumns,
|
||||
$event->getCampaignByChannelColumns(),
|
||||
$event->getLeadColumns(),
|
||||
$event->getIpColumn(),
|
||||
$companyColumns
|
||||
);
|
||||
|
||||
$assetDownloadColumns = array_merge($commonColumnsAndFilters, $this->dncReportService->getDncColumns());
|
||||
$assetDownloadFilters = array_merge($commonColumnsAndFilters, $this->dncReportService->getDncFilters());
|
||||
|
||||
$event->addTable(
|
||||
self::CONTEXT_ASSET_DOWNLOAD,
|
||||
[
|
||||
'display_name' => 'mautic.asset.report.downloads.table',
|
||||
'columns' => $assetDownloadColumns,
|
||||
'filters' => $assetDownloadFilters,
|
||||
],
|
||||
self::CONTEXT_ASSET
|
||||
);
|
||||
|
||||
// Add Graphs
|
||||
$context = self::CONTEXT_ASSET_DOWNLOAD;
|
||||
$event->addGraph($context, 'line', 'mautic.asset.graph.line.downloads');
|
||||
$event->addGraph($context, 'table', 'mautic.asset.table.most.downloaded');
|
||||
$event->addGraph($context, 'table', 'mautic.asset.table.top.referrers');
|
||||
$event->addGraph($context, 'pie', 'mautic.asset.graph.pie.statuses', ['translate' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the QueryBuilder object to generate reports from.
|
||||
*/
|
||||
public function onReportGenerate(ReportGeneratorEvent $event): void
|
||||
{
|
||||
if (!$event->checkContext([self::CONTEXT_ASSET, self::CONTEXT_ASSET_DOWNLOAD])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queryBuilder = $event->getQueryBuilder();
|
||||
|
||||
if ($event->checkContext(self::CONTEXT_ASSET)) {
|
||||
$queryBuilder->from(MAUTIC_TABLE_PREFIX.'assets', 'a');
|
||||
$event->addCategoryLeftJoin($queryBuilder, 'a');
|
||||
} elseif ($event->checkContext(self::CONTEXT_ASSET_DOWNLOAD)) {
|
||||
$event->applyDateFilters($queryBuilder, 'date_download', 'ad');
|
||||
|
||||
$queryBuilder->from(MAUTIC_TABLE_PREFIX.'asset_downloads', 'ad')
|
||||
->leftJoin('ad', MAUTIC_TABLE_PREFIX.'assets', 'a', 'a.id = ad.asset_id');
|
||||
$event->addCategoryLeftJoin($queryBuilder, 'a');
|
||||
$event->addLeadLeftJoin($queryBuilder, 'ad');
|
||||
$event->addIpAddressLeftJoin($queryBuilder, 'ad');
|
||||
$event->addCampaignByChannelJoin($queryBuilder, 'a', 'asset');
|
||||
|
||||
if ($this->companyReportData->eventHasCompanyColumns($event)) {
|
||||
$event->addCompanyLeftJoin($queryBuilder);
|
||||
}
|
||||
|
||||
if (!$event->hasGroupBy()) {
|
||||
$queryBuilder->groupBy('ad.id');
|
||||
}
|
||||
}
|
||||
|
||||
$event->setQueryBuilder($queryBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the QueryBuilder object to generate reports from.
|
||||
*/
|
||||
public function onReportGraphGenerate(ReportGraphEvent $event): void
|
||||
{
|
||||
// Context check, we only want to fire for Lead reports
|
||||
if (!$event->checkContext(self::CONTEXT_ASSET_DOWNLOAD)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$graphs = $event->getRequestedGraphs();
|
||||
$qb = $event->getQueryBuilder();
|
||||
|
||||
foreach ($graphs as $g) {
|
||||
$options = $event->getOptions($g);
|
||||
$queryBuilder = clone $qb;
|
||||
$chartQuery = clone $options['chartQuery'];
|
||||
$chartQuery->applyDateFilters($queryBuilder, 'date_download', 'ad');
|
||||
|
||||
switch ($g) {
|
||||
case 'mautic.asset.graph.line.downloads':
|
||||
$chart = new LineChart(null, $options['dateFrom'], $options['dateTo']);
|
||||
$chartQuery->modifyTimeDataQuery($queryBuilder, 'date_download', 'ad');
|
||||
$downloads = $chartQuery->loadAndBuildTimeData($queryBuilder);
|
||||
$chart->setDataset($options['translator']->trans($g), $downloads);
|
||||
$data = $chart->render();
|
||||
$data['name'] = $g;
|
||||
|
||||
$event->setGraph($g, $data);
|
||||
break;
|
||||
case 'mautic.asset.table.most.downloaded':
|
||||
$limit = 10;
|
||||
$offset = 0;
|
||||
$items = $this->downloadRepository->getMostDownloaded($queryBuilder, $limit, $offset);
|
||||
$graphData = [];
|
||||
$graphData['data'] = $items;
|
||||
$graphData['name'] = $g;
|
||||
$graphData['iconClass'] = 'ri-download-line';
|
||||
$graphData['link'] = 'mautic_asset_action';
|
||||
$event->setGraph($g, $graphData);
|
||||
break;
|
||||
case 'mautic.asset.table.top.referrers':
|
||||
$limit = 10;
|
||||
$offset = 0;
|
||||
$items = $this->downloadRepository->getTopReferrers($queryBuilder, $limit, $offset);
|
||||
$graphData = [];
|
||||
$graphData['data'] = $items;
|
||||
$graphData['name'] = $g;
|
||||
$graphData['iconClass'] = 'ri-download-line';
|
||||
$graphData['link'] = 'mautic_asset_action';
|
||||
$event->setGraph($g, $graphData);
|
||||
break;
|
||||
case 'mautic.asset.graph.pie.statuses':
|
||||
$items = $this->downloadRepository->getHttpStatuses($queryBuilder);
|
||||
$graphData = [];
|
||||
$graphData['data'] = $items;
|
||||
$graphData['name'] = $g;
|
||||
$graphData['iconClass'] = 'ri-earth-line';
|
||||
$event->setGraph($g, $graphData);
|
||||
break;
|
||||
}
|
||||
|
||||
unset($queryBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
public function onReportDisplay(ReportDataEvent $event): void
|
||||
{
|
||||
$data = $event->getData();
|
||||
|
||||
if ($event->checkContext([self::CONTEXT_ASSET_DOWNLOAD])) {
|
||||
$data = $this->dncReportService->processDncStatusDisplay($data);
|
||||
}
|
||||
|
||||
$event->setData($data);
|
||||
unset($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\Model\AssetModel;
|
||||
use Mautic\CoreBundle\CoreEvents;
|
||||
use Mautic\CoreBundle\DTO\GlobalSearchFilterDTO;
|
||||
use Mautic\CoreBundle\Event as MauticEvents;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\GlobalSearch;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class SearchSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private AssetModel $assetModel,
|
||||
private CorePermissions $security,
|
||||
private GlobalSearch $globalSearch,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CoreEvents::GLOBAL_SEARCH => ['onGlobalSearch', 0],
|
||||
CoreEvents::BUILD_COMMAND_LIST => ['onBuildCommandList', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onGlobalSearch(MauticEvents\GlobalSearchEvent $event): void
|
||||
{
|
||||
$filterDTO = new GlobalSearchFilterDTO($event->getSearchString());
|
||||
$results = $this->globalSearch->performSearch(
|
||||
$filterDTO,
|
||||
$this->assetModel,
|
||||
'@MauticAsset/SubscribedEvents/Search/global.html.twig'
|
||||
);
|
||||
|
||||
if (!empty($results)) {
|
||||
$event->addResults('mautic.asset.assets', $results);
|
||||
}
|
||||
}
|
||||
|
||||
public function onBuildCommandList(MauticEvents\CommandListEvent $event): void
|
||||
{
|
||||
if ($this->security->isGranted(['asset:assets:viewown', 'asset:assets:viewother'], 'MATCH_ONE')) {
|
||||
$event->addCommands(
|
||||
'mautic.asset.assets',
|
||||
$this->assetModel->getCommandList()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\AssetBundle\Entity\Download;
|
||||
use Mautic\CoreBundle\EventListener\CommonStatsSubscriber;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
|
||||
class StatsSubscriber extends CommonStatsSubscriber
|
||||
{
|
||||
public function __construct(CorePermissions $security, EntityManager $entityManager)
|
||||
{
|
||||
parent::__construct($security, $entityManager);
|
||||
$this->addContactRestrictedRepositories([Download::class]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\Model\AssetModel;
|
||||
use Mautic\CoreBundle\Exception\FileInvalidException;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\CoreBundle\Validator\FileUploadValidator;
|
||||
use Oneup\UploaderBundle\Event\PostUploadEvent;
|
||||
use Oneup\UploaderBundle\Event\ValidationEvent;
|
||||
use Oneup\UploaderBundle\Uploader\Exception\ValidationException;
|
||||
use Oneup\UploaderBundle\UploadEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class UploadSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
private AssetModel $assetModel,
|
||||
protected Translator $translator,
|
||||
private FileUploadValidator $fileUploadValidator,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
UploadEvents::POST_UPLOAD => ['onPostUpload', 0],
|
||||
UploadEvents::VALIDATION => ['onUploadValidation', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves upladed file to temporary directory where it can be found later
|
||||
* and all uploaded files in there cleared. Also sets file name to the response.
|
||||
*/
|
||||
public function onPostUpload(PostUploadEvent $event): void
|
||||
{
|
||||
$request = $event->getRequest()->request;
|
||||
$response = $event->getResponse();
|
||||
$tempId = basename($request->get('tempId'));
|
||||
$file = $event->getFile();
|
||||
$config = $event->getConfig();
|
||||
$uploadDir = $config['storage']['directory'];
|
||||
$tmpDir = $uploadDir.'/tmp/'.$tempId;
|
||||
|
||||
// Move uploaded file to temporary folder
|
||||
$file->move($tmpDir);
|
||||
|
||||
// Set resposnse data
|
||||
$response['state'] = 1;
|
||||
$response['tmpFileName'] = $file->getBasename();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates file before upload.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function onUploadValidation(ValidationEvent $event): void
|
||||
{
|
||||
$file = $event->getFile();
|
||||
$extensions = $this->coreParametersHelper->get('allowed_extensions');
|
||||
$configuredMimeTypes = $this->coreParametersHelper->get('allowed_mimetypes');
|
||||
$allowedMimeTypes = array_intersect_key($configuredMimeTypes, array_flip($extensions));
|
||||
$maxSize = $this->assetModel->getMaxUploadSize('B');
|
||||
|
||||
if (null === $file) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->fileUploadValidator->checkFileSize($file->getSize(), $maxSize, 'mautic.asset.asset.error.file.size');
|
||||
} catch (FileInvalidException $e) {
|
||||
throw new ValidationException($e->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$this->fileUploadValidator->checkExtension($file->getExtension(), $extensions, 'mautic.asset.asset.error.file.extension');
|
||||
} catch (FileInvalidException $e) {
|
||||
throw new ValidationException($e->getMessage());
|
||||
}
|
||||
|
||||
if (array_key_exists(strtolower($file->getExtension()), array_change_key_case($configuredMimeTypes, CASE_LOWER))) {
|
||||
try {
|
||||
$this->checkMimeType($file->getMimeType(), $allowedMimeTypes, 'mautic.asset.asset.error.file.mimetype');
|
||||
} catch (FileInvalidException $e) {
|
||||
throw new ValidationException($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string> $allowedMimeTypes
|
||||
*/
|
||||
private function checkMimeType(string $mimeType, array $allowedMimeTypes, string $extensionErrorMsg): void
|
||||
{
|
||||
if (!in_array(strtolower($mimeType), array_map('strtolower', $allowedMimeTypes), true)) {
|
||||
$error = $this->translator->trans($extensionErrorMsg, [
|
||||
'%fileMimetype%' => $mimeType,
|
||||
'%mimetypes%' => implode(', ', $allowedMimeTypes),
|
||||
], 'validators');
|
||||
|
||||
throw new FileInvalidException($error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Form\Type;
|
||||
|
||||
use Mautic\AssetBundle\Model\AssetModel;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class AssetListType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private CorePermissions $corePermissions,
|
||||
private AssetModel $assetModel,
|
||||
private UserHelper $userHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'choices' => $this->getAssetChoices(),
|
||||
'placeholder' => false,
|
||||
'expanded' => false,
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getParent(): ?string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
|
||||
private function getAssetChoices(): array
|
||||
{
|
||||
$choices = [];
|
||||
$viewOther = $this->corePermissions->isGranted('asset:assets:viewother');
|
||||
$repo = $this->assetModel->getRepository();
|
||||
$repo->setCurrentUser($this->userHelper->getUser());
|
||||
$assets = $repo->getAssetList('', 0, 0, $viewOther);
|
||||
|
||||
foreach ($assets as $asset) {
|
||||
$choices[$asset['language']][$asset['title']] = $asset['id'];
|
||||
}
|
||||
|
||||
// sort by language
|
||||
ksort($choices);
|
||||
|
||||
return $choices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Form\Type;
|
||||
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\AssetBundle\Model\AssetModel;
|
||||
use Mautic\CategoryBundle\Form\Type\CategoryListType;
|
||||
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
|
||||
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
|
||||
use Mautic\CoreBundle\Form\Type\ButtonGroupType;
|
||||
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\CoreBundle\Loader\ParameterLoader;
|
||||
use Mautic\ProjectBundle\Form\Type\ProjectType;
|
||||
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\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Callback;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Constraints\Url;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<Asset>
|
||||
*/
|
||||
class AssetType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
private AssetModel $assetModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addEventSubscriber(new CleanFormSubscriber(['description' => 'html']));
|
||||
$builder->addEventSubscriber(new FormExitSubscriber('asset.asset', $options));
|
||||
|
||||
$builder->add('storageLocation', ButtonGroupType::class, [
|
||||
'label' => 'mautic.asset.asset.form.storageLocation',
|
||||
'choices' => [
|
||||
'mautic.asset.asset.form.storageLocation.local' => 'local',
|
||||
'mautic.asset.asset.form.storageLocation.remote' => 'remote',
|
||||
],
|
||||
'attr' => [
|
||||
'onchange' => 'Mautic.changeAssetStorageLocation();',
|
||||
],
|
||||
]);
|
||||
|
||||
$maxUploadSize = $this->assetModel->getMaxUploadSize('', true);
|
||||
$builder->add(
|
||||
'tempName',
|
||||
HiddenType::class,
|
||||
[
|
||||
'label' => $this->translator->trans('mautic.asset.asset.form.file.upload', ['%max%' => $maxUploadSize]),
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'required' => false,
|
||||
'constraints' => [
|
||||
new Callback([$this, 'validateExtension']),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'originalFileName',
|
||||
HiddenType::class,
|
||||
[
|
||||
'required' => false,
|
||||
'constraints' => [
|
||||
new Callback([$this, 'validateExtension']),
|
||||
],
|
||||
],
|
||||
);
|
||||
$builder->add(
|
||||
'disallow',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.asset.asset.form.disallow.crawlers',
|
||||
'attr' => [
|
||||
'tooltip' => 'mautic.asset.asset.form.disallow.crawlers.descr',
|
||||
'data-show-on' => '{"asset_storageLocation_0":"checked"}',
|
||||
],
|
||||
'data'=> empty($options['data']->getDisallow()) ? false : true,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'remotePath',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.asset.asset.form.remotePath',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'required' => false,
|
||||
'constraints' => [
|
||||
new Url(
|
||||
[
|
||||
'message' => 'mautic.asset.validation.error.url',
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'title',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.title',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'alias',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.alias',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.asset.asset.help.alias',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'description',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.core.description',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control editor'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'category',
|
||||
CategoryListType::class,
|
||||
[
|
||||
'bundle' => 'asset',
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('projects', ProjectType::class);
|
||||
|
||||
$builder->add('language', LocaleType::class, [
|
||||
'label' => 'mautic.core.language',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.asset.asset.form.language.help',
|
||||
],
|
||||
'required' => true,
|
||||
'constraints' => [
|
||||
new NotBlank(
|
||||
[
|
||||
'message' => 'mautic.core.value.required',
|
||||
]
|
||||
),
|
||||
],
|
||||
]);
|
||||
|
||||
$builder->add('isPublished', YesNoButtonGroupType::class, [
|
||||
'label' => 'mautic.core.form.available',
|
||||
]);
|
||||
$builder->add('publishUp', PublishUpDateType::class);
|
||||
$builder->add('publishDown', PublishDownDateType::class);
|
||||
|
||||
$builder->add(
|
||||
'tempId',
|
||||
HiddenType::class,
|
||||
[
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('buttons', FormButtonsType::class, []);
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Asset|string|null $object
|
||||
*/
|
||||
public function validateExtension($object, ExecutionContextInterface $context): void
|
||||
{
|
||||
if (empty($object)) {
|
||||
return;
|
||||
}
|
||||
$parameters = (new ParameterLoader())->getParameterBag();
|
||||
$extensions = $parameters->get('allowed_extensions');
|
||||
$fileName = $object;
|
||||
if (!is_string($object) && $object instanceof Asset) {
|
||||
$fileName = $object->getOriginalFileName();
|
||||
}
|
||||
$fileExtension = pathinfo($fileName, PATHINFO_EXTENSION);
|
||||
if (!in_array(strtolower($fileExtension), array_map('strtolower', $extensions), true)) {
|
||||
$context->buildViolation('mautic.asset.asset.error.file.extension', [
|
||||
'%fileExtension%'=> $fileExtension,
|
||||
'%extensions%' => implode(', ', $extensions),
|
||||
])
|
||||
->atPath('file')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(['data_class' => Asset::class]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class CampaignEventAssetDownloadType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'assets',
|
||||
AssetListType::class,
|
||||
[
|
||||
'label' => 'mautic.asset.campaign.event.assets',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.asset.campaign.event.assets.descr',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'campaignevent_assetdownload';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\DataTransformer\ArrayStringTransformer;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class ConfigType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'upload_dir',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.asset.config.form.upload.dir',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.asset.config.form.upload.dir.tooltip',
|
||||
],
|
||||
'constraints' => [
|
||||
new NotBlank([
|
||||
'message' => 'mautic.core.value.required',
|
||||
]),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'max_size',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.asset.config.form.max.size',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.asset.config.form.max.size.tooltip',
|
||||
],
|
||||
'constraints' => [
|
||||
new NotBlank([
|
||||
'message' => 'mautic.core.value.required',
|
||||
]),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$arrayStringTransformer = new ArrayStringTransformer();
|
||||
$builder->add(
|
||||
$builder->create(
|
||||
'allowed_extensions',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.asset.config.form.allowed.extensions',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.asset.config.form.allowed.extensions.tooltip',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
)->addViewTransformer($arrayStringTransformer)
|
||||
);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'assetconfig';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Form\Type;
|
||||
|
||||
use Mautic\CategoryBundle\Form\Type\CategoryListType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class FormSubmitActionDownloadFileType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'asset',
|
||||
AssetListType::class,
|
||||
[
|
||||
'expanded' => false,
|
||||
'multiple' => false,
|
||||
'label' => 'mautic.asset.form.submit.assets',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'placeholder' => 'mautic.asset.form.submit.latest.category',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.asset.form.submit.assets_descr',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'category',
|
||||
CategoryListType::class,
|
||||
[
|
||||
'label' => 'mautic.asset.form.submit.latest.category',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'placeholder' => false,
|
||||
'required' => false,
|
||||
'bundle' => 'asset',
|
||||
'return_entity' => false,
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.asset.form.submit.latest.category_descr',
|
||||
'data-show-on' => '{"formaction_properties_asset":""}',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'asset_submitaction_downloadfile';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class PointActionAssetDownloadType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'assets',
|
||||
AssetListType::class,
|
||||
[
|
||||
'expanded' => false,
|
||||
'multiple' => true,
|
||||
'label' => 'mautic.asset.point.action.assets',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'placeholder' => false,
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.asset.point.action.assets.descr',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'pointaction_assetdownload';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Helper;
|
||||
|
||||
class PointActionHelper
|
||||
{
|
||||
public static function validateAssetDownload($eventDetails, $action): bool
|
||||
{
|
||||
$assetId = $eventDetails->getId();
|
||||
$limitToAssets = $action['properties']['assets'];
|
||||
|
||||
if (!empty($limitToAssets) && !in_array($assetId, $limitToAssets)) {
|
||||
// no points change
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Helper;
|
||||
|
||||
use Mautic\AssetBundle\Model\AssetModel;
|
||||
|
||||
class TokenHelper
|
||||
{
|
||||
public const REGEX = '/{assetlink=(.*?)}/';
|
||||
|
||||
public function __construct(
|
||||
protected AssetModel $model,
|
||||
) {
|
||||
}
|
||||
|
||||
public function findAssetTokens($content, $clickthrough = []): array
|
||||
{
|
||||
$tokens = [];
|
||||
|
||||
preg_match_all(self::REGEX, $content, $matches);
|
||||
foreach ($matches[1] as $key => $assetId) {
|
||||
$token = $matches[0][$key];
|
||||
|
||||
if (isset($tokens[$token])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$asset = $this->model->getEntity($assetId);
|
||||
$tokens[$token] = (null !== $asset) ? $this->model->generateUrl($asset, true, $clickthrough) : '';
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class MauticAssetBundle extends Bundle
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,623 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Mautic\AssetBundle\AssetEvents;
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\AssetBundle\Entity\Download;
|
||||
use Mautic\AssetBundle\Event\AssetEvent;
|
||||
use Mautic\AssetBundle\Event\AssetLoadEvent;
|
||||
use Mautic\AssetBundle\Form\Type\AssetType;
|
||||
use Mautic\CategoryBundle\Model\CategoryModel;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\CoreBundle\Helper\Chart\LineChart;
|
||||
use Mautic\CoreBundle\Helper\Chart\PieChart;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\FileHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\FormModel;
|
||||
use Mautic\CoreBundle\Model\GlobalSearchInterface;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\LeadBundle\Tracker\Factory\DeviceDetectorFactory\DeviceDetectorFactoryInterface;
|
||||
use Mautic\LeadBundle\Tracker\Service\DeviceCreatorService\DeviceCreatorServiceInterface;
|
||||
use Mautic\LeadBundle\Tracker\Service\DeviceTrackingService\DeviceTrackingServiceInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* @extends FormModel<Asset>
|
||||
*/
|
||||
class AssetModel extends FormModel implements GlobalSearchInterface
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $maxAssetSize;
|
||||
|
||||
public function __construct(
|
||||
protected LeadModel $leadModel,
|
||||
protected CategoryModel $categoryModel,
|
||||
private RequestStack $requestStack,
|
||||
protected IpLookupHelper $ipLookupHelper,
|
||||
private DeviceCreatorServiceInterface $deviceCreatorService,
|
||||
private DeviceDetectorFactoryInterface $deviceDetectorFactory,
|
||||
private DeviceTrackingServiceInterface $deviceTrackingService,
|
||||
private ContactTracker $contactTracker,
|
||||
EntityManager $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $logger,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
$this->maxAssetSize = $coreParametersHelper->get('max_size');
|
||||
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $logger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
public function saveEntity($entity, $unlock = true): void
|
||||
{
|
||||
if (empty($this->inConversion)) {
|
||||
$alias = $entity->getAlias();
|
||||
if (empty($alias)) {
|
||||
$alias = $entity->getTitle();
|
||||
}
|
||||
$alias = $this->cleanAlias($alias, '', 0, '-');
|
||||
|
||||
// make sure alias is not already taken
|
||||
$repo = $this->getRepository();
|
||||
$testAlias = $alias;
|
||||
$count = $repo->checkUniqueAlias($testAlias, $entity);
|
||||
$aliasTag = $count;
|
||||
|
||||
while ($count) {
|
||||
$testAlias = $alias.$aliasTag;
|
||||
$count = $repo->checkUniqueAlias($testAlias, $entity);
|
||||
++$aliasTag;
|
||||
}
|
||||
if ($testAlias != $alias) {
|
||||
$alias = $testAlias;
|
||||
}
|
||||
$entity->setAlias($alias);
|
||||
}
|
||||
|
||||
if (!$entity->isNew()) {
|
||||
// increase the revision
|
||||
$revision = $entity->getRevision();
|
||||
++$revision;
|
||||
$entity->setRevision($revision);
|
||||
}
|
||||
|
||||
parent::saveEntity($entity, $unlock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $systemEntry
|
||||
*
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function trackDownload($asset, $request = null, int $code = 200, $systemEntry = []): void
|
||||
{
|
||||
// Don't skew results with in-house downloads
|
||||
if (empty($systemEntry) && !$this->security->isAnonymous()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null == $request) {
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
}
|
||||
|
||||
if (!($request instanceof Request)) {
|
||||
// likely this download came via a cron (no request), do not bother logging the download.
|
||||
// https://github.com/mautic/mautic/issues/13577
|
||||
return;
|
||||
}
|
||||
|
||||
$download = new Download();
|
||||
$download->setDateDownload(new \DateTime());
|
||||
$download->setUtmCampaign($request->get('utm_campaign'));
|
||||
$download->setUtmContent($request->get('utm_content'));
|
||||
$download->setUtmMedium($request->get('utm_medium'));
|
||||
$download->setUtmSource($request->get('utm_source'));
|
||||
$download->setUtmTerm($request->get('utm_term'));
|
||||
|
||||
// Download triggered by lead
|
||||
if (empty($systemEntry)) {
|
||||
// check for any clickthrough info
|
||||
$clickthrough = $request->get('ct', false);
|
||||
if (!empty($clickthrough)) {
|
||||
$clickthrough = $this->decodeArrayFromUrl($clickthrough);
|
||||
|
||||
if (!empty($clickthrough['lead'])) {
|
||||
$lead = $this->leadModel->getEntity($clickthrough['lead']);
|
||||
if (null !== $lead) {
|
||||
$wasTrackedAlready = $this->deviceTrackingService->isTracked();
|
||||
$deviceDetector = $this->deviceDetectorFactory->create($request->server->get('HTTP_USER_AGENT'));
|
||||
$deviceDetector->parse();
|
||||
$currentDevice = $this->deviceCreatorService->getCurrentFromDetector($deviceDetector, $lead);
|
||||
$trackedDevice = $this->deviceTrackingService->trackCurrentDevice($currentDevice, false);
|
||||
$trackingId = $trackedDevice->getTrackingId();
|
||||
$trackingNewlyGenerated = !$wasTrackedAlready;
|
||||
$leadClickthrough = true;
|
||||
|
||||
$this->contactTracker->setTrackedContact($lead);
|
||||
}
|
||||
}
|
||||
if (!empty($clickthrough['channel'])) {
|
||||
if (1 === count($clickthrough['channel'])) {
|
||||
$channelId = reset($clickthrough['channel']);
|
||||
$channel = key($clickthrough['channel']);
|
||||
} else {
|
||||
$channel = $clickthrough['channel'][0];
|
||||
$channelId = (int) $clickthrough['channel'][1];
|
||||
}
|
||||
$download->setSource($channel);
|
||||
$download->setSourceId($channelId);
|
||||
} elseif (!empty($clickthrough['source'])) {
|
||||
$download->setSource($clickthrough['source'][0]);
|
||||
$download->setSourceId($clickthrough['source'][1]);
|
||||
}
|
||||
|
||||
if (!empty($clickthrough['email'])) {
|
||||
$emailRepo = $this->em->getRepository(Email::class);
|
||||
if ($emailEntity = $emailRepo->getEntity($clickthrough['email'])) {
|
||||
$download->setEmail($emailEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($leadClickthrough)) {
|
||||
$wasTrackedAlready = $this->deviceTrackingService->isTracked();
|
||||
$lead = $this->contactTracker->getContact();
|
||||
$trackedDevice = $this->deviceTrackingService->getTrackedDevice();
|
||||
$trackingId = null;
|
||||
$trackingNewlyGenerated = false;
|
||||
if (null !== $trackedDevice) {
|
||||
$trackingId = $trackedDevice->getTrackingId();
|
||||
$trackingNewlyGenerated = !$wasTrackedAlready;
|
||||
}
|
||||
}
|
||||
|
||||
$download->setLead($lead);
|
||||
} else {
|
||||
$trackingId = '';
|
||||
|
||||
if (isset($systemEntry['lead'])) {
|
||||
$lead = $systemEntry['lead'];
|
||||
if (!$lead instanceof Lead) {
|
||||
$leadId = is_array($lead) ? $lead['id'] : $lead;
|
||||
$lead = $this->em->getReference(Lead::class, $leadId);
|
||||
}
|
||||
|
||||
$download->setLead($lead);
|
||||
}
|
||||
|
||||
if (!empty($systemEntry['source'])) {
|
||||
$download->setSource($systemEntry['source'][0]);
|
||||
$download->setSourceId($systemEntry['source'][1]);
|
||||
}
|
||||
|
||||
if (isset($systemEntry['email'])) {
|
||||
$email = $systemEntry['email'];
|
||||
if (!$email instanceof Email) {
|
||||
$emailId = is_array($email) ? $email['id'] : $email;
|
||||
$email = $this->em->getReference(Email::class, $emailId);
|
||||
}
|
||||
|
||||
$download->setEmail($email);
|
||||
}
|
||||
|
||||
if (isset($systemEntry['tracking_id'])) {
|
||||
$trackingId = $systemEntry['tracking_id'];
|
||||
$trackingNewlyGenerated = false;
|
||||
} elseif ($this->security->isAnonymous() && !defined('IN_MAUTIC_CONSOLE')) {
|
||||
// If the session is anonymous and not triggered via CLI, assume the lead did something to trigger the
|
||||
// system forced download such as an email
|
||||
$deviceWasTracked = $this->deviceTrackingService->isTracked();
|
||||
$deviceDetector = $this->deviceDetectorFactory->create($request->server->get('HTTP_USER_AGENT'));
|
||||
$deviceDetector->parse();
|
||||
$currentDevice = $this->deviceCreatorService->getCurrentFromDetector($deviceDetector, $lead);
|
||||
$trackedDevice = $this->deviceTrackingService->trackCurrentDevice($currentDevice, false);
|
||||
$trackingId = $trackedDevice->getTrackingId();
|
||||
$trackingNewlyGenerated = !$deviceWasTracked;
|
||||
}
|
||||
}
|
||||
|
||||
$isUnique = true;
|
||||
if (!empty($trackingNewlyGenerated)) {
|
||||
// Cookie was just generated so this is definitely a unique download
|
||||
$isUnique = $trackingNewlyGenerated;
|
||||
} elseif (!empty($trackingId)) {
|
||||
// Determine if this is a unique download
|
||||
$isUnique = $this->getDownloadRepository()->isUniqueDownload($asset->getId(), $trackingId);
|
||||
}
|
||||
|
||||
$download->setTrackingId($trackingId);
|
||||
|
||||
if (empty($systemEntry)) {
|
||||
$download->setAsset($asset);
|
||||
|
||||
$this->getRepository()->upDownloadCount($asset->getId(), 1, $isUnique);
|
||||
}
|
||||
|
||||
// check for existing IP
|
||||
$ipAddress = $this->ipLookupHelper->getIpAddress();
|
||||
|
||||
$download->setCode($code);
|
||||
$download->setIpAddress($ipAddress);
|
||||
$download->setReferer($request->server->get('HTTP_REFERER'));
|
||||
|
||||
// Dispatch event
|
||||
if ($this->dispatcher->hasListeners(AssetEvents::ASSET_ON_LOAD)) {
|
||||
$event = new AssetLoadEvent($download, $isUnique);
|
||||
$this->dispatcher->dispatch($event, AssetEvents::ASSET_ON_LOAD);
|
||||
}
|
||||
|
||||
// Wrap in a try/catch to prevent deadlock errors on busy servers
|
||||
try {
|
||||
$this->em->persist($download);
|
||||
$this->em->flush();
|
||||
} catch (\Exception $e) {
|
||||
if (MAUTIC_ENV === 'dev') {
|
||||
throw $e;
|
||||
} else {
|
||||
error_log($e);
|
||||
}
|
||||
}
|
||||
|
||||
$this->em->detach($download);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase the download count.
|
||||
*
|
||||
* @param int $increaseBy
|
||||
* @param bool|false $unique
|
||||
*/
|
||||
public function upDownloadCount($asset, $increaseBy = 1, $unique = false): void
|
||||
{
|
||||
$id = ($asset instanceof Asset) ? $asset->getId() : (int) $asset;
|
||||
|
||||
$this->getRepository()->upDownloadCount($id, $increaseBy, $unique);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\AssetBundle\Entity\AssetRepository
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
return $this->em->getRepository(Asset::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\AssetBundle\Entity\DownloadRepository
|
||||
*/
|
||||
public function getDownloadRepository()
|
||||
{
|
||||
return $this->em->getRepository(Download::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'asset:assets';
|
||||
}
|
||||
|
||||
public function getNameGetter(): string
|
||||
{
|
||||
return 'getTitle';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFoundHttpException
|
||||
*/
|
||||
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
if (!$entity instanceof Asset) {
|
||||
throw new MethodNotAllowedHttpException(['Asset']);
|
||||
}
|
||||
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
return $formFactory->create(AssetType::class, $entity, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific entity or generate a new one if id is empty.
|
||||
*/
|
||||
public function getEntity($id = null): ?Asset
|
||||
{
|
||||
if (null === $id) {
|
||||
$entity = new Asset();
|
||||
} else {
|
||||
$entity = parent::getEntity($id);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
||||
{
|
||||
if (!$entity instanceof Asset) {
|
||||
throw new MethodNotAllowedHttpException(['Asset']);
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'pre_save':
|
||||
$name = AssetEvents::ASSET_PRE_SAVE;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = AssetEvents::ASSET_POST_SAVE;
|
||||
break;
|
||||
case 'pre_delete':
|
||||
$name = AssetEvents::ASSET_PRE_DELETE;
|
||||
break;
|
||||
case 'post_delete':
|
||||
$name = AssetEvents::ASSET_POST_DELETE;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new AssetEvent($entity, $isNew);
|
||||
$event->setEntityManager($this->em);
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of entities for autopopulate fields.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLookupResults($type, $filter = '', $limit = 10)
|
||||
{
|
||||
$results = [];
|
||||
switch ($type) {
|
||||
case 'asset':
|
||||
$viewOther = $this->security->isGranted('asset:assets:viewother');
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
$repo = $this->getRepository();
|
||||
$repo->setCurrentUser($this->userHelper->getUser());
|
||||
// During the form submit & edit, make sure that the data is checked against available assets
|
||||
if ('mautic_segment_action' === $request->get('_route')
|
||||
&& (Request::METHOD_POST === $request->getMethod() || 'edit' === $request->get('objectAction'))
|
||||
) {
|
||||
$limit = 0;
|
||||
}
|
||||
$results = $repo->getAssetList($filter, $limit, 0, $viewOther);
|
||||
break;
|
||||
case 'category':
|
||||
$results = $this->categoryModel->getRepository()->getCategoryList($filter, $limit, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate url for an asset.
|
||||
*
|
||||
* @param Asset $entity
|
||||
* @param bool $absolute
|
||||
* @param array $clickthrough
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateUrl($entity, $absolute = true, $clickthrough = [])
|
||||
{
|
||||
$assetSlug = $entity->getId().':'.$entity->getAlias();
|
||||
|
||||
$slugs = [
|
||||
'slug' => $assetSlug,
|
||||
];
|
||||
|
||||
return $this->buildUrl('mautic_asset_download', $slugs, $absolute, $clickthrough);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the max upload size based on PHP restrictions and config.
|
||||
*
|
||||
* @param string $unit If '', determine the best unit based on the number
|
||||
* @param bool|false $humanReadable Return as a human readable filesize
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getMaxUploadSize($unit = 'M', $humanReadable = false)
|
||||
{
|
||||
$maxAssetSize = $this->maxAssetSize;
|
||||
$maxAssetSize = (-1 == $maxAssetSize || 0 === $maxAssetSize) ? PHP_INT_MAX : FileHelper::convertMegabytesToBytes($maxAssetSize);
|
||||
$maxPostSize = Asset::getIniValue('post_max_size');
|
||||
$maxUploadSize = Asset::getIniValue('upload_max_filesize');
|
||||
$memoryLimit = Asset::getIniValue('memory_limit');
|
||||
$maxAllowed = min(array_filter([$maxAssetSize, $maxPostSize, $maxUploadSize, $memoryLimit]));
|
||||
|
||||
if ($humanReadable) {
|
||||
$number = Asset::convertBytesToHumanReadable($maxAllowed);
|
||||
} else {
|
||||
[$number, $unit] = Asset::convertBytesToUnit($maxAllowed, $unit);
|
||||
}
|
||||
|
||||
return $number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|string
|
||||
*/
|
||||
public function getTotalFilesize($assets)
|
||||
{
|
||||
$firstAsset = is_array($assets) ? reset($assets) : false;
|
||||
if ($assets instanceof PersistentCollection || is_object($firstAsset)) {
|
||||
$assetIds = [];
|
||||
foreach ($assets as $asset) {
|
||||
$assetIds[] = $asset->getId();
|
||||
}
|
||||
$assets = $assetIds;
|
||||
}
|
||||
|
||||
if (!is_array($assets)) {
|
||||
$assets = [$assets];
|
||||
}
|
||||
|
||||
if (empty($assets)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$repo = $this->getRepository();
|
||||
$size = $repo->getAssetSize($assets);
|
||||
|
||||
if ($size) {
|
||||
$size = Asset::convertBytesToHumanReadable($size);
|
||||
}
|
||||
|
||||
return $size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line chart data of downloads.
|
||||
*
|
||||
* @param string|null $unit {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters}
|
||||
* @param string $dateFormat
|
||||
* @param array $filter
|
||||
* @param bool $canViewOthers
|
||||
*/
|
||||
public function getDownloadsLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true): array
|
||||
{
|
||||
$chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
|
||||
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
||||
$q = $query->prepareTimeDataQuery('asset_downloads', 'date_download', $filter);
|
||||
|
||||
if (!$canViewOthers) {
|
||||
$q->join('t', MAUTIC_TABLE_PREFIX.'assets', 'a', 'a.id = t.asset_id')
|
||||
->andWhere('a.created_by = :userId')
|
||||
->setParameter('userId', $this->userHelper->getUser()->getId());
|
||||
}
|
||||
|
||||
$data = $query->loadAndBuildTimeData($q);
|
||||
|
||||
$chart->setDataset($this->translator->trans('mautic.asset.downloadcount'), $data);
|
||||
|
||||
return $chart->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pie chart data of unique vs repetitive downloads.
|
||||
* Repetitive in this case mean if a lead downloaded any of the assets more than once.
|
||||
*
|
||||
* @param string $dateFrom
|
||||
* @param string $dateTo
|
||||
* @param array $filters
|
||||
* @param bool $canViewOthers
|
||||
*/
|
||||
public function getUniqueVsRepetitivePieChartData($dateFrom, $dateTo, $filters = [], $canViewOthers = true): array
|
||||
{
|
||||
$chart = new PieChart();
|
||||
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
||||
$allQ = $query->getCountQuery('asset_downloads', 'id', 'date_download', $filters);
|
||||
$uniqueQ = $query->getCountQuery('asset_downloads', 'lead_id', 'date_download', $filters, ['getUnique' => true]);
|
||||
|
||||
if (!$canViewOthers) {
|
||||
$allQ->join('t', MAUTIC_TABLE_PREFIX.'assets', 'a', 'a.id = t.asset_id')
|
||||
->andWhere('a.created_by = :userId')
|
||||
->setParameter('userId', $this->userHelper->getUser()->getId());
|
||||
$uniqueQ->join('t', MAUTIC_TABLE_PREFIX.'assets', 'a', 'a.id = t.asset_id')
|
||||
->andWhere('a.created_by = :userId')
|
||||
->setParameter('userId', $this->userHelper->getUser()->getId());
|
||||
}
|
||||
|
||||
$all = $query->fetchCount($allQ);
|
||||
$unique = $query->fetchCount($uniqueQ);
|
||||
|
||||
$repetitive = $all - $unique;
|
||||
$chart->setDataset($this->translator->trans('mautic.asset.unique'), $unique);
|
||||
$chart->setDataset($this->translator->trans('mautic.asset.repetitive'), $repetitive);
|
||||
|
||||
return $chart->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of popular (by downloads) assets.
|
||||
*
|
||||
* @param int $limit
|
||||
* @param string $dateFrom
|
||||
* @param string $dateTo
|
||||
* @param array $filters
|
||||
* @param bool $canViewOthers
|
||||
*/
|
||||
public function getPopularAssets($limit = 10, $dateFrom = null, $dateTo = null, $filters = [], $canViewOthers = true): array
|
||||
{
|
||||
$q = $this->em->getConnection()->createQueryBuilder();
|
||||
$q->select('COUNT(DISTINCT t.id) AS download_count, a.id, a.title')
|
||||
->from(MAUTIC_TABLE_PREFIX.'asset_downloads', 't')
|
||||
->join('t', MAUTIC_TABLE_PREFIX.'assets', 'a', 'a.id = t.asset_id')
|
||||
->orderBy('download_count', 'DESC')
|
||||
->groupBy('a.id')
|
||||
->setMaxResults($limit);
|
||||
|
||||
if (!$canViewOthers) {
|
||||
$q->andWhere('a.created_by = :userId')
|
||||
->setParameter('userId', $this->userHelper->getUser()->getId());
|
||||
}
|
||||
|
||||
$chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
||||
$chartQuery->applyFilters($q, $filters);
|
||||
$chartQuery->applyDateFilters($q, 'date_download');
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of assets in a date range.
|
||||
*
|
||||
* @param int $limit
|
||||
* @param array $filters
|
||||
* @param array $options
|
||||
*/
|
||||
public function getAssetList($limit = 10, ?\DateTime $dateFrom = null, ?\DateTime $dateTo = null, $filters = [], $options = []): array
|
||||
{
|
||||
$q = $this->em->getConnection()->createQueryBuilder();
|
||||
$q->select('t.id, t.title as name, t.date_added, t.date_modified')
|
||||
->from(MAUTIC_TABLE_PREFIX.'assets', 't')
|
||||
->setMaxResults($limit);
|
||||
|
||||
if (!empty($options['canViewOthers'])) {
|
||||
$q->andWhere('t.created_by = :userId')
|
||||
->setParameter('userId', $this->userHelper->getUser()->getId());
|
||||
}
|
||||
|
||||
$chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
||||
$chartQuery->applyFilters($q, $filters);
|
||||
$chartQuery->applyDateFilters($q, 'date_added');
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{% extends '@MauticForm/Action/base_form_action.html.twig' %}
|
||||
|
||||
{% set footerContent %}
|
||||
{% if action.properties.asset is defined %}
|
||||
{% if action.properties.asset is null and action.properties.category is defined %}
|
||||
<!-- Using last asset from category -->
|
||||
{% set category = getEntity('Mautic\\CategoryBundle\\Entity\\Category', action.properties.category) %}
|
||||
{% set categoryName = category ? category.title : '' %}
|
||||
|
||||
{% include '@MauticCore/Helper/_tag.html.twig' with {
|
||||
tags: [
|
||||
{
|
||||
label: 'mautic.form.field.asset.use_category'|trans({'%category_name%': categoryName}),
|
||||
icon: 'ri-folder-line',
|
||||
color: 'warm-gray',
|
||||
attributes: {
|
||||
href: path('mautic_asset_index'),
|
||||
'target': '_blank'
|
||||
}
|
||||
}
|
||||
]
|
||||
} %}
|
||||
{% elseif action.properties.asset is not null %}
|
||||
<!-- Specific asset selected -->
|
||||
{% set asset = getEntity('Mautic\\AssetBundle\\Entity\\Asset', action.properties.asset) %}
|
||||
{% if asset %}
|
||||
{% include '@MauticCore/Helper/_tag.html.twig' with {
|
||||
tags: [
|
||||
{
|
||||
label: asset.title,
|
||||
icon: 'ri-file-line',
|
||||
color: 'warm-gray',
|
||||
attributes: securityIsGranted('asset:assets:viewother') ? {
|
||||
href: path('mautic_asset_action', {'objectAction': 'view', 'objectId': asset.id}),
|
||||
'target': '_blank'
|
||||
} : {}
|
||||
}
|
||||
]
|
||||
} %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endset %}
|
||||
|
||||
{% block action_label %}
|
||||
{{ footerContent|raw }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,258 @@
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
{% block mauticContent %}asset{% endblock %}
|
||||
|
||||
{% block preHeader %}
|
||||
{{- include('@MauticCore/Helper/page_actions.html.twig',
|
||||
{
|
||||
'item' : activeAsset,
|
||||
'templateButtons' : {
|
||||
'close' : securityHasEntityAccess(
|
||||
permissions['asset:assets:viewown'],
|
||||
permissions['asset:assets:viewother'],
|
||||
activeAsset.getCreatedBy()
|
||||
),
|
||||
},
|
||||
'routeBase' : 'asset',
|
||||
'langVar' : 'asset.asset',
|
||||
'nameGetter' : 'getTitle',
|
||||
'targetLabel': 'mautic.asset.assets'|trans
|
||||
}
|
||||
) -}}
|
||||
{{ include('@MauticCore/Modules/category--inline.html.twig', {'category': activeAsset.category}) }}
|
||||
{{ include('@MauticProject/Modules/projects.html.twig', {'item': activeAsset}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block headerTitle %} {{ activeAsset.getTitle() }} {% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
{{- include('@MauticCore/Helper/page_actions.html.twig',
|
||||
{
|
||||
'item' : activeAsset,
|
||||
'templateButtons' : {
|
||||
'edit' : securityHasEntityAccess(
|
||||
permissions['asset:assets:editown'],
|
||||
permissions['asset:assets:editother'],
|
||||
activeAsset.getCreatedBy()
|
||||
),
|
||||
'clone' : permissions['asset:assets:create'],
|
||||
'delete' : securityHasEntityAccess(
|
||||
permissions['asset:assets:deleteown'],
|
||||
permissions['asset:assets:deleteother'],
|
||||
activeAsset.getCreatedBy()
|
||||
),
|
||||
},
|
||||
'routeBase' : 'asset',
|
||||
'langVar' : 'asset.asset',
|
||||
'nameGetter' : 'getTitle',
|
||||
}) -}}
|
||||
{% endblock %}
|
||||
|
||||
{% block publishStatus %}
|
||||
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {
|
||||
'entity': activeAsset,
|
||||
'status': 'available'
|
||||
}) -}}
|
||||
<div class="label__divider"></div>
|
||||
|
||||
{# Asset type #}
|
||||
{% if activeAsset.getFileType() is defined and activeAsset.getFileType() is not empty %}
|
||||
{% set fileType = activeAsset.getFileType()|lower %}
|
||||
{% set extensionGroups = activeAsset.getFileExtensions()|default([]) %}
|
||||
{% set type = 'fallback' %}
|
||||
{% for group, exts in extensionGroups %}
|
||||
{% if fileType in exts %}
|
||||
{% set type = group %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% include '@MauticCore/Helper/_tag.html.twig' with {
|
||||
tags: [{
|
||||
label: ('mautic.asset.type.' ~ type)|trans,
|
||||
icon: activeAsset.getIconClass()|default(''),
|
||||
color: 'high-contrast'
|
||||
}]
|
||||
} %}
|
||||
{% endif %}
|
||||
|
||||
{# Disallow indexing #}
|
||||
{% if activeAsset.getDisallow() is defined and activeAsset.getDisallow() == 1 %}
|
||||
{% include '@MauticCore/Helper/_tag.html.twig' with {
|
||||
tags: [{
|
||||
label: 'mautic.asset.tag.disallow.label'|trans,
|
||||
icon: 'ri-eye-off-fill',
|
||||
color: 'blue',
|
||||
icon_only: true
|
||||
}]
|
||||
} %}
|
||||
{% endif %}
|
||||
|
||||
{# Storage location #}
|
||||
{% if activeAsset.getStorageLocation() is defined %}
|
||||
{% include '@MauticCore/Helper/_tag.html.twig' with {
|
||||
tags: [{
|
||||
label: ('mautic.asset.tag.storage.' ~ activeAsset.getStorageLocation())|trans,
|
||||
icon: activeAsset.getStorageLocation() == 'local' ? 'ri-hard-drive-2-fill' : 'ri-cloud-fill',
|
||||
color: 'blue',
|
||||
icon_only: true
|
||||
}]
|
||||
} %}
|
||||
{% endif %}
|
||||
|
||||
{# Language #}
|
||||
{% if activeAsset.getLanguage() is defined and activeAsset.getLanguage() is not empty %}
|
||||
{% include '@MauticCore/Helper/_tag.html.twig' with {
|
||||
tags: [{
|
||||
label: activeAsset.getLanguage()|language_name|capitalize,
|
||||
icon: 'ri-translate-2',
|
||||
color: 'warm-gray',
|
||||
attributes: {
|
||||
'data-toggle': 'tooltip',
|
||||
'data-placement': 'top',
|
||||
'title': 'mautic.core.language'|trans
|
||||
}
|
||||
}]
|
||||
} %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- start: box layout -->
|
||||
<div class="box-layout">
|
||||
<!-- left section -->
|
||||
<div class="col-md-9 height-auto">
|
||||
<div>
|
||||
<!-- asset detail header -->
|
||||
{% include '@MauticCore/Helper/description--expanded.html.twig' with {'description': activeAsset.description} %}
|
||||
<!--/ asset detail header -->
|
||||
<!-- asset detail collapseable -->
|
||||
<div class="collapse pr-md pl-md" id="asset-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' : activeAsset}
|
||||
) -}}
|
||||
<tr>
|
||||
<td width="20%">
|
||||
<span class="fw-b textTitle">
|
||||
{% trans %}mautic.asset.asset.size{% endtrans %}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ activeAsset.getSize() }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="20%"><span class="fw-b textTitle">{% trans %}mautic.asset.asset.url{% endtrans %}</span></td>
|
||||
<td>{{ assetDownloadUrl }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="20%"><span class="fw-b textTitle">{% trans %}mautic.asset.filename.original{% endtrans %}</span></td>
|
||||
<td>{{ activeAsset.getOriginalFilename() }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{% set location = activeAsset.getStorageLocation() %}
|
||||
<td width="20%"><span class="fw-b textTitle">{{ ('mautic.asset.filename.' ~ location)|trans }}</span></td>
|
||||
<td>{{ ('local' == location) ? activeAsset.getPath() : activeAsset.getRemotePath() }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--/ asset detail collapseable -->
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- asset detail collapseable toggler -->
|
||||
<div class="hr-expand nm">
|
||||
<span data-toggle="tooltip" title="Detail">
|
||||
<a href="javascript:void(0)" class="arrow text-secondary collapsed" data-toggle="collapse"
|
||||
data-target="#asset-details"><span class="caret"></span> {% trans %}mautic.core.details{% endtrans %}</a>
|
||||
</span>
|
||||
</div>
|
||||
<!--/ asset detail collapseable toggler -->
|
||||
|
||||
<!-- some stats -->
|
||||
<div class="pa-md">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% include '@MauticCore/Modules/stat--icon.html.twig' with {
|
||||
'stats': [
|
||||
{
|
||||
'title': 'mautic.asset.asset.downloads.total',
|
||||
'value': stats.downloads.total,
|
||||
'tooltip': 'mautic.asset.asset.downloads.total.all_time',
|
||||
'icon': 'ri-download-line'
|
||||
},
|
||||
{
|
||||
'title': 'mautic.asset.asset.downloads.unique',
|
||||
'value': stats.downloads.unique,
|
||||
'tooltip': 'mautic.asset.asset.downloads.unique.all_time',
|
||||
'icon': 'ri-user-6-line'
|
||||
}
|
||||
]
|
||||
} %}
|
||||
<div class="panel">
|
||||
<div class="panel-body box-layout">
|
||||
<div class="col-md-4 va-m">
|
||||
<h5 class="text-white dark-md fw-sb mb-xs">
|
||||
<span class="ri-download-line"></span>
|
||||
{% trans %}mautic.asset.graph.line.downloads{% endtrans %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-md-8 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' : stats.downloads.timeStats, 'chartType' : 'line', 'chartHeight' : 300}) -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--/ stats -->
|
||||
</div>
|
||||
|
||||
{{ customContent('details.stats.graph.below', _context) }}
|
||||
</div>
|
||||
<!--/ left section -->
|
||||
|
||||
<!-- right section -->
|
||||
<div class="col-md-3 bdr-l height-auto">
|
||||
<!-- preview URL -->
|
||||
<div class="panel shd-none bdr-rds-0 bdr-w-0 mt-sm mb-0">
|
||||
<div class="panel-body pt-xs">
|
||||
{% include '@MauticCore/Components/card.html.twig' with {
|
||||
type: 'link',
|
||||
href: assetDownloadUrl,
|
||||
ctaType: 'external',
|
||||
heading: 'mautic.core.open_link',
|
||||
attributes: {
|
||||
'target': '_blank'
|
||||
}
|
||||
} %}
|
||||
{{- include('@MauticAsset/Modules/preview.html.twig',
|
||||
{
|
||||
'variant': 'dialog',
|
||||
'activeAsset' : activeAsset,
|
||||
'assetDownloadUrl' : url(
|
||||
'mautic_asset_action',
|
||||
{'objectAction' : 'preview', 'objectId' : activeAsset.getId()}
|
||||
)}) -}}
|
||||
</div>
|
||||
</div>
|
||||
<!--/ preview URL -->
|
||||
|
||||
<hr class="hr-w-2" style="width:50%">
|
||||
|
||||
<!-- activity feed -->
|
||||
{{- include('@MauticCore/Helper/recentactivity.html.twig', {'logs' : logs}) -}}
|
||||
</div>
|
||||
<!--/ right section -->
|
||||
<input name="entityId" id="entityId" type="hidden" value="{{ activeAsset.getId() }}"/>
|
||||
</div>
|
||||
<!--/ end: box layout -->
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,125 @@
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
{% set header = (activeAsset.getId()) ? 'mautic.asset.asset.menu.edit'|trans({'%name%' : activeAsset.getTitle()}) :
|
||||
'mautic.asset.asset.menu.new'|trans %}
|
||||
{% block headerTitle %}{{ header }}{% endblock %}
|
||||
{% block mauticContent %}asset{% endblock %}
|
||||
{% block content %}
|
||||
<script>
|
||||
mauticAssetUploadEndpoint = "{{ uploadEndpoint }}";
|
||||
mauticAssetUploadMaxSize = {{ maxSize }};
|
||||
mauticAssetUploadMaxSizeError = "{{ maxSizeError }}";
|
||||
mauticAssetUploadExtensions = "{{ extensions }}";
|
||||
mauticAssetUploadExtensionError = "{{ extensionError }}";
|
||||
</script>
|
||||
{{ form_start(form) }}
|
||||
<!-- start: box layout -->
|
||||
<div class="box-layout">
|
||||
<!-- container -->
|
||||
<div class="col-md-8 col-lg-9 height-auto bdr-r">
|
||||
<div class="pa-md">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="col-md-7 pl-0">
|
||||
{{ form_row(form.storageLocation) }}
|
||||
</div>
|
||||
<div class="text-left mt-lg mb-lg{% if startOnLocal %} hide {% endif %}" id="remote-button">
|
||||
{% if integrations %}
|
||||
{% include '@MauticCore/Helper/button.html.twig' with {
|
||||
buttons: [
|
||||
{
|
||||
href: path('mautic_asset_remote') ~ '?tmpl=modal',
|
||||
icon: 'ri-file-search-line',
|
||||
label: 'mautic.asset.remote.file.browse',
|
||||
size: 'sm',
|
||||
spin: true,
|
||||
variant: 'tertiary',
|
||||
attributes: {
|
||||
'data-toggle': 'ajaxmodal',
|
||||
'data-target': '#RemoteFileModal',
|
||||
'data-header': 'mautic.asset.remote.file.browse'|trans,
|
||||
'role': 'button'
|
||||
}
|
||||
}
|
||||
]
|
||||
} %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="storage-local"{% if not startOnLocal %} class="hide"{% endif %}>
|
||||
<div class="row">
|
||||
<div class="form-group col-xs-12 ">
|
||||
{{ form_label(form.tempName) }}
|
||||
{{ form_widget(form.tempName) }}
|
||||
{{ form_errors(form.tempName) }}
|
||||
<div class="help-block mdropzone-error"></div>
|
||||
<div class="mdropzone text-center" id="dropzone">
|
||||
<div class="dz-message">
|
||||
{% trans %}mautic.asset.drop.file.here{% endtrans %}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="storage-remote"{% if startOnLocal %} class="hide"{% endif %}>
|
||||
{{ form_row(form.remotePath) }}
|
||||
</div>
|
||||
<div>
|
||||
{{ form_row(form.title) }}
|
||||
</div>
|
||||
<div>
|
||||
{{ form_row(form.alias) }}
|
||||
</div>
|
||||
<div>
|
||||
{{ form_row(form.description) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="tile jc-center">
|
||||
<div class="form-group col-xs-12 ">
|
||||
{{- include('@MauticAsset/Modules/preview.html.twig', {
|
||||
'variant': 'interactive',
|
||||
'activeAsset' : activeAsset,
|
||||
'assetDownloadUrl' : url('mautic_asset_action',
|
||||
{'objectAction' : 'preview', 'objectId' : activeAsset.getId()}
|
||||
)}) -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-lg-3 height-auto">
|
||||
<div class="pr-lg pl-lg pt-md pb-md">
|
||||
{{ form_row(form.category) }}
|
||||
{{ form_row(form.projects) }}
|
||||
{{ form_row(form.language) }}
|
||||
{{ form_row(form.isPublished, {
|
||||
'attr': {
|
||||
'data-none': 'mautic.core.form.unavailable_regardless_of_scheduling',
|
||||
'data-start': 'mautic.core.form.available_on_scheduled_date',
|
||||
'data-both': 'mautic.core.form.available_during_scheduled_period',
|
||||
'data-end': 'mautic.core.form.available_until_scheduled_end'
|
||||
}
|
||||
}) }}
|
||||
{{ form_row(form.publishUp, {'label': 'mautic.core.form.available.available_from'}) }}
|
||||
{{ form_row(form.publishDown, {'label': 'mautic.core.form.available.unavailable_from'}) }}
|
||||
{{ form_row(form.disallow) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% if integrations %}
|
||||
{{- include('@MauticCore/Helper/modal.html.twig', {
|
||||
'id' : 'RemoteFileModal',
|
||||
'size' : 'lg',
|
||||
'footerButtons' : true,
|
||||
}) -}}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,292 @@
|
||||
{% set isIndex = tmpl == 'index' ? true : false %}
|
||||
{% set tmpl = 'list' %}
|
||||
|
||||
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
|
||||
{% block mauticContent %}asset
|
||||
{% endblock %}
|
||||
{% block headerTitle %}
|
||||
{% trans %}mautic.asset.assets{% endtrans %}
|
||||
{% 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,
|
||||
'action': currentRoute,
|
||||
'page_actions': {
|
||||
'templateButtons': {
|
||||
'new': permissions['asset:assets:create'],
|
||||
},
|
||||
'routeBase': 'asset',
|
||||
'langVar': 'asset.asset',
|
||||
},
|
||||
'bulk_actions': {
|
||||
'langVar': 'asset.asset',
|
||||
'routeBase': 'asset',
|
||||
'templateButtons': {
|
||||
'delete': permissions['asset:assets:deleteown'] or permissions['asset:assets:deleteother'],
|
||||
},
|
||||
},
|
||||
'quickFilters': [
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.isuncategorized',
|
||||
'label': 'mautic.core.form.uncategorized',
|
||||
'tooltip': 'mautic.core.search.quickfilter.is_uncategorized',
|
||||
'icon': 'ri-folder-unknow-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.ispublished',
|
||||
'label': 'mautic.core.form.available',
|
||||
'tooltip': 'mautic.core.search.quickfilter.is_published',
|
||||
'icon': 'ri-check-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.isunpublished',
|
||||
'label': 'mautic.core.form.unavailable',
|
||||
'tooltip': 'mautic.core.search.quickfilter.is_unpublished',
|
||||
'icon': 'ri-close-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.ismine',
|
||||
'label': 'mautic.core.searchcommand.ismine.label',
|
||||
'tooltip': 'mautic.core.searchcommand.ismine.description',
|
||||
'icon': 'ri-user-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.asset.asset.searchcommand.isexpired',
|
||||
'label': 'mautic.core.form.no_longer_available',
|
||||
'tooltip': 'mautic.asset.asset.searchcommand.isexpired.description',
|
||||
'icon': 'ri-time-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.asset.asset.searchcommand.ispending',
|
||||
'label': 'mautic.core.form.not_yet_available',
|
||||
'tooltip': 'mautic.asset.asset.searchcommand.ispending.description',
|
||||
'icon': 'ri-timer-line'
|
||||
}
|
||||
]
|
||||
}) -}}
|
||||
<div class="page-list">
|
||||
{{ block('listResults') }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ block('listResults') }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block listResults %}
|
||||
{% if items|length %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover asset-list" id="assetTable">
|
||||
<thead>
|
||||
<tr>
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'checkall': 'true',
|
||||
'target': '#assetTable',
|
||||
}) }}
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'sessionVar' : 'asset',
|
||||
'orderBy' : 'a.title',
|
||||
'text' : 'mautic.core.title',
|
||||
'class' : 'col-asset-title',
|
||||
}
|
||||
) -}}
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'sessionVar' : 'asset',
|
||||
'orderBy' : 'c.title',
|
||||
'text' : 'mautic.core.category',
|
||||
'class' : 'visible-md visible-lg col-asset-category',
|
||||
}
|
||||
) -}}
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'sessionVar' : 'asset',
|
||||
'orderBy' : 'a.downloadCount',
|
||||
'text' : 'mautic.asset.asset.thead.download.count',
|
||||
'class' : 'visible-md visible-lg col-asset-download-count',
|
||||
}
|
||||
) -}}
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'sessionVar' : 'asset',
|
||||
'orderBy' : 'a.dateAdded',
|
||||
'text' : 'mautic.lead.import.label.dateAdded',
|
||||
'class' : 'visible-md visible-lg col-asset-dateAdded',
|
||||
}
|
||||
) -}}
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'sessionVar' : 'asset',
|
||||
'orderBy' : 'a.dateModified',
|
||||
'text' : 'mautic.lead.import.label.dateModified',
|
||||
'class' : 'visible-md visible-lg col-asset-dateModified',
|
||||
'default' : true,
|
||||
}
|
||||
) -}}
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'sessionVar' : 'asset',
|
||||
'orderBy' : 'a.createdByUser',
|
||||
'text' : 'mautic.core.createdby',
|
||||
'class' : 'visible-md visible-lg col-asset-createdByUser',
|
||||
}
|
||||
) -}}
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'sessionVar' : 'asset',
|
||||
'orderBy' : 'a.id',
|
||||
'text' : 'mautic.core.id',
|
||||
'class' : 'visible-md visible-lg col-asset-id',
|
||||
}
|
||||
) -}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for k, item in items %}
|
||||
<tr>
|
||||
<td>
|
||||
{{- include(
|
||||
'@MauticCore/Helper/list_actions.html.twig',
|
||||
{
|
||||
'item' : item,
|
||||
'templateButtons' : {
|
||||
'edit' : securityHasEntityAccess(
|
||||
permissions['asset:assets:editown'],
|
||||
permissions['asset:assets:editother'],
|
||||
item.getCreatedBy()
|
||||
),
|
||||
'delete' : securityHasEntityAccess(
|
||||
permissions['asset:assets:deleteown'],
|
||||
permissions['asset:assets:deleteother'],
|
||||
item.getCreatedBy()
|
||||
),
|
||||
'clone' : permissions['asset:assets:create'],
|
||||
},
|
||||
'routeBase' : 'asset',
|
||||
'langVar' : 'asset.asset',
|
||||
'nameGetter' : 'getTitle',
|
||||
'customButtons' : {
|
||||
0: {
|
||||
'attr' : {
|
||||
'data-toggle' : 'ajaxmodal',
|
||||
'data-target' : '#AssetPreviewModal',
|
||||
'href' : path(
|
||||
'mautic_asset_action',
|
||||
{'objectAction' : 'preview', 'objectId' : item.getId(), 'stream': 0}
|
||||
),
|
||||
},
|
||||
'btnText' : 'mautic.asset.asset.preview'|trans,
|
||||
'iconClass' : 'ri-image-circle-line',
|
||||
},
|
||||
1: {
|
||||
'attr' : {
|
||||
'data-copy' : url('mautic_asset_download', {'slug': item.getId() ~ ':' ~ item.getAlias()}),
|
||||
'data-toggle' : 'none',
|
||||
},
|
||||
'btnText' : 'mautic.core.copy_download_link'|trans,
|
||||
'iconClass' : 'ri-clipboard-line',
|
||||
},
|
||||
},
|
||||
}
|
||||
) -}}
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
{{- include(
|
||||
'@MauticCore/Helper/publishstatus_icon.html.twig',
|
||||
{
|
||||
'item' : item,
|
||||
'model' : 'asset.asset',
|
||||
}
|
||||
) -}}
|
||||
<a href="{{ path(
|
||||
'mautic_asset_action',
|
||||
{'objectAction' : 'view', 'objectId' : item.getId()}
|
||||
) }}"
|
||||
data-toggle="ajax">
|
||||
{{ item.getTitle() }} ({{ item.getAlias() }})
|
||||
</a>
|
||||
<i class="{{ item.getIconClass() }}"></i>
|
||||
{{ customContent('asset.name', _context) }}
|
||||
{{ include('@MauticProject/Modules/projects.html.twig') }}
|
||||
</div>
|
||||
{% set description = item.getDescription() %}
|
||||
{% if description %}
|
||||
{{ include('@MauticCore/Helper/description--inline.html.twig', {
|
||||
'description': description
|
||||
}) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">
|
||||
{{ include('@MauticCore/Modules/category--expanded.html.twig', {'category': item.getCategory()}) }}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">{{ item.getDownloadCount() }}</td>
|
||||
<td class="visible-md visible-lg" title="{{ item.getDateAdded() ? dateToFullConcat(item.getDateAdded()) : '' }}">
|
||||
{{ item.getDateAdded() ? dateToDate(item.getDateAdded()) : '' }}
|
||||
</td>
|
||||
<td class="visible-md visible-lg" title="{{ item.getDateModified() ? dateToFullConcat(item.getDateModified()) : '' }}">
|
||||
{{ item.getDateModified() ? dateToDate(item.getDateModified()) : '' }}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">{{ item.getCreatedByUser() }}</td>
|
||||
<td class="visible-md visible-lg">{{ item.getId() }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="panel-footer">
|
||||
{{- include('@MauticCore/Helper/pagination.html.twig', {
|
||||
'totalItems' : items|length,
|
||||
'page' : page,
|
||||
'limit' : limit,
|
||||
'menuLinkId' : 'mautic_asset_index',
|
||||
'baseUrl' : path('mautic_asset_index'),
|
||||
'sessionVar' : 'asset',
|
||||
}) -}}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if searchValue is not empty %}
|
||||
{{- include('@MauticCore/Helper/noresults.html.twig', {'tip' : 'mautic.asset.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="mb-md">
|
||||
{% include '@MauticCore/Components/pictogram.html.twig' with {
|
||||
'pictogram': 'cloud--assets',
|
||||
'size': '80'
|
||||
} %}
|
||||
</div>
|
||||
{% endset %}
|
||||
|
||||
{{ include('@MauticCore/Components/content-block.html.twig', {
|
||||
heading: 'mautic.asset.onboarding.heading',
|
||||
subheading: 'mautic.asset.onboarding.subheading',
|
||||
copy: 'mautic.asset.onboarding.copy',
|
||||
childContainer: childContainer,
|
||||
}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{{- include('@MauticCore/Helper/modal.html.twig', {
|
||||
'id' : 'AssetPreviewModal',
|
||||
'header' : false,
|
||||
}) -}}
|
||||
|
||||
{{ include('@MauticCore/Modules/protip.html.twig', {
|
||||
tip: random(['mautic.protip.assets.gating', 'mautic.protip.assets.naming', 'mautic.protip.assets.repurpose', 'mautic.protip.assets.track'])
|
||||
}) }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,17 @@
|
||||
{% block _config_assetconfig_widget %}
|
||||
<h4 class="fw-sb mt-48 mb-xs">{% trans %}mautic.config.tab.assetconfig{% endtrans %}</h4>
|
||||
<div class="text-muted small pb-md">{{ 'mautic.core.config.header.assetconfig.description'|trans }}</div>
|
||||
<div class="row">
|
||||
<div class="panel panel-default mb-md">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
{% for f in form.children %}
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(f) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,113 @@
|
||||
{% set variant = variant|default('') %}
|
||||
{% set isPreviewable = activeAsset.isImage() or 'pdf' == activeAsset.getFileType()|lower or activeAsset.getMime() starts with 'video' or activeAsset.getMime() starts with 'audio' %}
|
||||
|
||||
{% macro renderPreviewContent(activeAsset, assetDownloadUrl) %}
|
||||
{% if activeAsset.isImage() %}
|
||||
<img src="{{ assetDownloadUrl ~ '?stream=1' }}" alt="{{ activeAsset.getTitle()|escape }}" class="img-thumbnail" />
|
||||
{% elseif 'pdf' == activeAsset.getFileType()|lower %}
|
||||
<iframe src="{{ assetDownloadUrl ~ '?stream=1#view=FitH' }}" style="width: 100%; height: 70vh; border: none;" title="{{ 'mautic.asset.preview.pdf_iframe_title'|trans({'%title%': activeAsset.getTitle()|escape}) }}"></iframe>
|
||||
{% elseif activeAsset.getMime() starts with 'video' or activeAsset.getExtension() in ['mpg', 'mpeg', 'mp4', 'webm'] %}
|
||||
<video src="{{ assetDownloadUrl ~ '?stream=1' }}" controls style="width: 100%;">
|
||||
{{ 'mautic.asset.no_video_support'|trans }}
|
||||
</video>
|
||||
{% elseif activeAsset.getMime() starts with 'audio' or activeAsset.getExtension() in ['mp3', 'ogg', 'wav'] %}
|
||||
<audio controls>
|
||||
<source src="{{ assetDownloadUrl ~ '?stream=1' }}" type="{{ activeAsset.getMime() }}">
|
||||
{{ 'mautic.asset.no_audio_support'|trans }}
|
||||
</audio>
|
||||
{% else %}
|
||||
<div class="d-flex jc-center ai-center text-helper">
|
||||
<i class="{{ activeAsset.getIconClass() }} ri-lg mr-xs"></i>
|
||||
<span>{{ 'mautic.asset.no_preview'|trans }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% if variant == 'interactive' %}
|
||||
{% if isPreviewable %}
|
||||
<h5 class="fw-sb mb-xs">{{ 'mautic.asset.asset.preview'|trans }}</h5>
|
||||
|
||||
<div class="asset-preview">
|
||||
{% if activeAsset.isImage() %}
|
||||
<div class="asset-preview--image">
|
||||
<div class="asset-preview__label">
|
||||
{% include '@MauticCore/Helper/_tag.html.twig' with {
|
||||
tags: [
|
||||
{
|
||||
label: 'mautic.asset.click_to_zoom',
|
||||
color: 'gray',
|
||||
size: 'sm'
|
||||
}
|
||||
]
|
||||
} %}
|
||||
</div>
|
||||
{{ _self.renderPreviewContent(activeAsset, assetDownloadUrl) }}
|
||||
</div>
|
||||
{% elseif 'pdf' == activeAsset.getFileType()|lower %}
|
||||
<div class="asset-preview__label">
|
||||
{% include '@MauticCore/Helper/_tag.html.twig' with {
|
||||
tags: [
|
||||
{
|
||||
label: 'mautic.asset.click_to_view_full_size',
|
||||
color: 'gray',
|
||||
size: 'sm',
|
||||
attributes: {
|
||||
'href': '#',
|
||||
'data-toggle': 'modal',
|
||||
'data-target': '#pdf-preview-modal'
|
||||
}
|
||||
}
|
||||
]
|
||||
} %}
|
||||
</div>
|
||||
<div class="pdf-preview-thumbnail">
|
||||
{{ _self.renderPreviewContent(activeAsset, assetDownloadUrl) }}
|
||||
</div>
|
||||
|
||||
{% include '@MauticCore/Components/modal.html.twig' with {
|
||||
id: 'pdf-preview-modal',
|
||||
size: 'xl',
|
||||
type: 'productive',
|
||||
modalHeading: activeAsset.getTitle()|escape,
|
||||
modalAriaLabel: 'PDF Preview',
|
||||
modalContent: _self.renderPreviewContent(activeAsset, assetDownloadUrl),
|
||||
hasScrollingContent: false
|
||||
} %}
|
||||
{% else %}
|
||||
{{ _self.renderPreviewContent(activeAsset, assetDownloadUrl) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="d-flex jc-center ai-center text-helper">
|
||||
<i class="{{ activeAsset.getIconClass() }} ri-lg mr-xs"></i>
|
||||
<span>{{ 'mautic.asset.no_preview'|trans }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
|
||||
{% elseif variant == 'dialog' %}
|
||||
{% set modalId = 'asset-dialog-preview-modal-' ~ activeAsset.id|default('new') %}
|
||||
{% if isPreviewable %}
|
||||
{% include '@MauticCore/Components/tile.html.twig' with {
|
||||
type: 'mini',
|
||||
href: '#',
|
||||
title: 'mautic.asset.open_preview',
|
||||
icon: 'ri-rectangle-line',
|
||||
attributes: {
|
||||
'data-toggle': 'modal',
|
||||
'data-target': '#' ~ modalId
|
||||
}
|
||||
} %}
|
||||
{% endif %}
|
||||
|
||||
{% include '@MauticCore/Components/modal.html.twig' with {
|
||||
id: modalId,
|
||||
size: 'xl',
|
||||
type: 'productive',
|
||||
modalHeading: activeAsset.getTitle()|escape,
|
||||
modalAriaLabel: 'mautic.asset.preview.ariaLabel'|trans({'%title%': activeAsset.getTitle()|escape}),
|
||||
modalContent: _self.renderPreviewContent(activeAsset, assetDownloadUrl)
|
||||
} %}
|
||||
{% else %}
|
||||
{{ _self.renderPreviewContent(activeAsset, assetDownloadUrl) }}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,54 @@
|
||||
{% set isIndex = tmpl == 'index' ? true : false %}
|
||||
{% set tmpl = 'list' %}
|
||||
|
||||
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
|
||||
{% block mauticContent %}asset{% endblock %}
|
||||
{% block headerTitle %}{% trans %}mautic.asset.remote.file.browse{% endtrans %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if isIndex %}
|
||||
<div id="page-list-wrapper" class="panel panel-default">
|
||||
<div class="page-list">
|
||||
{{ block('mainContent') }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ block('mainContent') }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block mainContent %}
|
||||
{% if integrations|length %}
|
||||
<!-- start: box layout -->
|
||||
<div class="box-layout">
|
||||
<!-- step container -->
|
||||
<div class="col-md-3">
|
||||
<div class="pt-md pr-md pb-md">
|
||||
<ul class="list-group list-group-tabs">
|
||||
{% for integration in integrations %}
|
||||
<li class="list-group-item{% if loop.index0 is same as(0) %} active{% endif %}" id="tab{{ integration.getName() }}">
|
||||
<a href="javascript: void(0);" class="list-group-item-heading steps" onclick="Mautic.updateRemoteBrowser('{{ integration.getName() }}');">
|
||||
{{ integration.getDisplayName() }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!--/ step container -->
|
||||
|
||||
<!-- container -->
|
||||
<div class="col-md-9 bdr-l">
|
||||
<div id="remoteFileBrowser">
|
||||
<div class="alert alert-warning col-md-6 col-md-offset-3 mt-md">
|
||||
<p>{% trans %}mautic.asset.remote.select_service{% endtrans %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--/ end: container -->
|
||||
</div>
|
||||
<!--/ end: box layout -->
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,36 @@
|
||||
{% if items|length %}
|
||||
<div class="panel panel-primary mb-0">
|
||||
<div class="panel-body">
|
||||
<input type='text' class='remote-file-search form-control mb-lg' autocomplete='off' placeholder="{% trans %}mautic.core.search.placeholder{% endtrans %}" />
|
||||
|
||||
<div class="list-group remote-file-list">
|
||||
{% if items.dirs is defined %}
|
||||
{% for item in items.dirs %}
|
||||
<a class="list-group-item" href="javascript: void(0);" onclick="Mautic.updateRemoteBrowser('{{ integration.getName() }}', '/{{ item|trim('/','right') }}');">
|
||||
{{ item }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% for item in items.keys %}
|
||||
<a class="list-group-item" href="javascript: void(0);" onclick="Mautic.selectRemoteFile('{{ integration.getPublicUrl(item) }}');">
|
||||
{{ item }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for item in items %}
|
||||
{% if connector.getAdapter().isDirectory(item) %}
|
||||
<a class="list-group-item" href="javascript: void(0);" onclick="Mautic.updateRemoteBrowser('{{ integration.getName() }}', '/{{ item|trim('/', 'right') }}');">
|
||||
{{ item }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="list-group-item" href="javascript: void(0);" onclick="Mautic.selectRemoteFile('{{ integration.getPublicUrl(item) }}');">
|
||||
{{ item }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{- include('@MauticCore/Helper/noresults.html.twig', {'message' : 'mautic.asset.remote.no_results'}) -}}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% if showMore is defined and showMore is not empty %}
|
||||
<a href="{{ url('mautic_asset_index', {'search' : searchString}) }}" data-toggle="ajax">
|
||||
<span>{{ 'mautic.core.search.more'|trans({'%count%' : remaining})|escape }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="gsearch--results-common" href="{{ url('mautic_asset_action', {'objectAction' : 'view', 'objectId' : item.getId()}) }}" data-toggle="ajax">
|
||||
<i class="{{ item.getIconClass() }} gsearch--results-common__icon mr-3"></i>
|
||||
<span class="fw-sb">{{ item.getTitle()|escape }}</span>
|
||||
<span class="ml-4 mr-4">#{{ item.getId() }}</span>
|
||||
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {
|
||||
'entity': item,
|
||||
'status': 'available',
|
||||
'simplified': 'true'
|
||||
}) -}}
|
||||
|
||||
<span size="sm" class="pull-right" data-toggle="tooltip" title="{% trans %}mautic.asset.downloadcount{% endtrans %}" data-placement="left">
|
||||
<i class="ri-download-line"></i>
|
||||
{{ item.getDownloadCount() }}
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,7 @@
|
||||
<div>
|
||||
Hello
|
||||
{{- include('@MauticAsset/Asset/preview.html.twig', {'activeAsset' : event.extra.asset, 'assetDownloadUrl' : url(
|
||||
'mautic_asset_action',
|
||||
{'objectAction' : 'preview', 'objectId' : event.extra.asset.getId()}
|
||||
)}) -}}
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Security\Permissions;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\AbstractPermissions;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class AssetPermissions extends AbstractPermissions
|
||||
{
|
||||
public function __construct(CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
parent::__construct($coreParametersHelper->all());
|
||||
}
|
||||
|
||||
public function definePermissions(): void
|
||||
{
|
||||
$this->addExtendedPermissions('assets');
|
||||
$this->addStandardPermissions('categories');
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'asset';
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface &$builder, array $options, array $data): void
|
||||
{
|
||||
$this->addStandardFormFields('asset', 'categories', $builder, $data);
|
||||
$this->addExtendedFormFields('asset', 'assets', $builder, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\AssetBundle\Tests\Asset;
|
||||
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\Persistence\Mapping\MappingException;
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\CoreBundle\Helper\CsvHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
|
||||
abstract class AbstractAssetTestCase extends MauticMysqlTestCase
|
||||
{
|
||||
protected Asset $asset;
|
||||
|
||||
protected string $expectedMimeType;
|
||||
|
||||
protected string $expectedContentDisposition;
|
||||
|
||||
protected string $expectedPngContent;
|
||||
|
||||
protected string $csvPath;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->generateCsv();
|
||||
|
||||
$assetData = [
|
||||
'title' => 'Asset controller test. Preview action',
|
||||
'alias' => 'Test',
|
||||
'createdAt' => new \DateTime('2021-05-05 22:30:00'),
|
||||
'updatedAt' => new \DateTime('2022-05-05 22:30:00'),
|
||||
'createdBy' => 'User',
|
||||
'storage' => 'local',
|
||||
'path' => basename($this->csvPath),
|
||||
'extension' => 'png',
|
||||
];
|
||||
$this->asset = $this->createAsset($assetData);
|
||||
|
||||
$this->expectedMimeType = 'text/plain; charset=UTF-8';
|
||||
$this->expectedContentDisposition = 'attachment;filename="';
|
||||
$this->expectedPngContent = file_get_contents($this->csvPath);
|
||||
}
|
||||
|
||||
protected function beforeTearDown(): void
|
||||
{
|
||||
if (file_exists($this->csvPath)) {
|
||||
unlink($this->csvPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an asset entity in the DB.
|
||||
*
|
||||
* @param array<string, string|mixed> $assetData
|
||||
*
|
||||
* @throws ORMException
|
||||
* @throws MappingException
|
||||
*/
|
||||
protected function createAsset(array $assetData): Asset
|
||||
{
|
||||
$asset = new Asset();
|
||||
$asset->setTitle($assetData['title']);
|
||||
$asset->setAlias($assetData['alias']);
|
||||
$asset->setDateAdded($assetData['createdAt'] ?? new \DateTime());
|
||||
$asset->setDateModified($assetData['updatedAt'] ?? new \DateTime());
|
||||
$asset->setCreatedByUser($assetData['createdBy'] ?? 'User');
|
||||
$asset->setStorageLocation($assetData['storage'] ?? 'local');
|
||||
$asset->setPath($assetData['path'] ?? '');
|
||||
$asset->setExtension($assetData['extension'] ?? '');
|
||||
$asset->setSize($this->csvPath ? filesize($this->csvPath) : 0);
|
||||
|
||||
$this->em->persist($asset);
|
||||
$this->em->flush();
|
||||
$this->em->detach($asset);
|
||||
|
||||
return $asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the csv asset and return the path of the asset.
|
||||
*/
|
||||
protected function generateCsv(): void
|
||||
{
|
||||
$uploadDir = static::getContainer()->get('mautic.helper.core_parameters')->get('upload_dir') ?? sys_get_temp_dir();
|
||||
$tmpFile = tempnam($uploadDir, 'mautic_asset_test_');
|
||||
$file = fopen($tmpFile, 'w');
|
||||
|
||||
$initialList = [
|
||||
['email', 'firstname', 'lastname'],
|
||||
['john.doe@his-site.com.email', 'John', 'Doe'],
|
||||
['john.smith@his-site.com.email', 'John', 'Smith'],
|
||||
['jim.doe@his-site.com.email', 'Jim', 'Doe'],
|
||||
[''],
|
||||
['jim.smith@his-site.com.email', 'Jim', 'Smith'],
|
||||
];
|
||||
|
||||
foreach ($initialList as $line) {
|
||||
CsvHelper::putCsv($file, $line);
|
||||
}
|
||||
|
||||
fclose($file);
|
||||
|
||||
$this->csvPath = $tmpFile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Tests\Controller\Api;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
|
||||
class AssetApiControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testCreateNewRemoteAsset(): void
|
||||
{
|
||||
$payload = [
|
||||
'file' => 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf',
|
||||
'storageLocation' => 'remote',
|
||||
'title' => 'title',
|
||||
];
|
||||
$this->client->request('POST', 'api/assets/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseStatusCodeSame(201, $clientResponse->getContent());
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertEquals($payload['title'], $response['asset']['title']);
|
||||
$this->assertEquals($payload['storageLocation'], $response['asset']['storageLocation']);
|
||||
$this->assertStringContainsString('application/pdf', $response['asset']['mime']);
|
||||
$this->assertStringContainsString('pdf', $response['asset']['extension']);
|
||||
$this->assertNotNull($response['asset']['size']);
|
||||
}
|
||||
|
||||
public function testCreateNewRemoteAssetWithVulnerableFile(): void
|
||||
{
|
||||
$payload = [
|
||||
'file' => 'file:///etc/passwd',
|
||||
'storageLocation' => 'remote',
|
||||
'title' => 'title',
|
||||
];
|
||||
$this->client->request('POST', 'api/assets/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseStatusCodeSame(400, $clientResponse->getContent());
|
||||
$this->assertEquals('{"errors":[{"code":400,"message":"remotePath: The remote should be a valid URL.","details":{"remotePath":["The remote should be a valid URL."]}}]}', $clientResponse->getContent());
|
||||
}
|
||||
|
||||
public function testCreateNewLocalAsset(): void
|
||||
{
|
||||
$assetsPath = $this->client->getKernel()->getContainer()->getParameter('mautic.upload_dir');
|
||||
file_put_contents($assetsPath.'/file.txt', 'test');
|
||||
|
||||
$payload = [
|
||||
'file' => 'file.txt',
|
||||
'storageLocation' => 'local',
|
||||
'title' => 'title',
|
||||
];
|
||||
$this->client->request('POST', 'api/assets/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseStatusCodeSame(201, $clientResponse->getContent());
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertEquals($payload['title'], $response['asset']['title']);
|
||||
$this->assertEquals($payload['storageLocation'], $response['asset']['storageLocation']);
|
||||
$this->assertStringContainsString('text/plain', $response['asset']['mime']);
|
||||
$this->assertNotNull($response['asset']['size']);
|
||||
$this->assertStringContainsString('txt', $response['asset']['extension']);
|
||||
unlink($assetsPath.'/file.txt');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\AssetBundle\Tests\Controller;
|
||||
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\AssetBundle\Tests\Asset\AbstractAssetTestCase;
|
||||
use Mautic\CoreBundle\Tests\Traits\ControllerTrait;
|
||||
use Mautic\PageBundle\Tests\Controller\PageControllerTest;
|
||||
use Mautic\ProjectBundle\Entity\Project;
|
||||
use Mautic\UserBundle\Entity\Permission;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Model\RoleModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AssetControllerFunctionalTest extends AbstractAssetTestCase
|
||||
{
|
||||
use ControllerTrait;
|
||||
|
||||
private const SALES_USER = 'sales';
|
||||
private const ADMIN_USER = 'admin';
|
||||
|
||||
/**
|
||||
* Index action should return status code 200.
|
||||
*/
|
||||
public function testIndexAction(): void
|
||||
{
|
||||
$asset = new Asset();
|
||||
$asset->setTitle('test');
|
||||
$asset->setAlias('test');
|
||||
$asset->setDateAdded(new \DateTime('2020-02-07 20:29:02'));
|
||||
$asset->setDateModified(new \DateTime('2020-03-21 20:29:02'));
|
||||
$asset->setCreatedByUser('Test User');
|
||||
|
||||
$this->em->persist($asset);
|
||||
$this->em->flush();
|
||||
$this->em->detach($asset);
|
||||
|
||||
$urlAlias = 'assets';
|
||||
$routeAlias = 'asset';
|
||||
$column = 'dateModified';
|
||||
$column2 = 'title';
|
||||
$tableAlias = 'a.';
|
||||
|
||||
$this->getControllerColumnTests($urlAlias, $routeAlias, $column, $tableAlias, $column2);
|
||||
}
|
||||
|
||||
public function testAssetSizes(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/ajax?action=email:getAttachmentsSize&assets%5B%5D='.$this->asset->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
Assert::assertSame('{"size":"178 bytes"}', $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview action should return the file content.
|
||||
*/
|
||||
public function testPreviewActionStreamByDefault(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/assets/preview/'.$this->asset->getId());
|
||||
ob_start();
|
||||
$response = $this->client->getResponse();
|
||||
$response->sendContent();
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$this->assertSame(Response::HTTP_OK, $response->getStatusCode());
|
||||
$this->assertSame($this->expectedMimeType, $response->headers->get('Content-Type'));
|
||||
$this->assertNotSame($this->expectedContentDisposition.$this->asset->getOriginalFileName(), $response->headers->get('Content-Disposition'));
|
||||
$this->assertEquals($this->expectedPngContent, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview action should return the file content.
|
||||
*/
|
||||
public function testPreviewActionStreamIsZero(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/assets/preview/'.$this->asset->getId().'?stream=0&download=1');
|
||||
ob_start();
|
||||
$response = $this->client->getResponse();
|
||||
$response->sendContent();
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$this->assertSame(Response::HTTP_OK, $response->getStatusCode());
|
||||
$this->assertSame($this->expectedContentDisposition.$this->asset->getOriginalFileName(), $response->headers->get('Content-Disposition'));
|
||||
$this->assertEquals($this->expectedPngContent, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview action should return the html code.
|
||||
*/
|
||||
public function testPreviewActionStreamDownloadAreZero(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/assets/preview/'.$this->asset->getId().'?stream=0&download=0');
|
||||
ob_start();
|
||||
$response = $this->client->getResponse();
|
||||
$response->sendContent();
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$this->assertSame(Response::HTTP_OK, $response->getStatusCode(), $content);
|
||||
$this->assertNotEquals($this->expectedPngContent, $content);
|
||||
PageControllerTest::assertTrue($response->isOk());
|
||||
|
||||
$assetSlug = $this->asset->getId().':'.$this->asset->getAlias();
|
||||
PageControllerTest::assertStringContainsString(
|
||||
'/asset/'.$assetSlug,
|
||||
$content,
|
||||
'The return must contain the assert slug'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string[]> $permission
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('getValuesProvider')]
|
||||
public function testEditWithPermissions(string $route, array $permission, int $expectedStatusCode, string $userCreatorUN): void
|
||||
{
|
||||
$userCreator = $this->getUser($userCreatorUN);
|
||||
$userEditor = $this->getUser(self::SALES_USER);
|
||||
$this->setPermission($userEditor, ['asset:assets' => $permission]);
|
||||
|
||||
$asset = new Asset();
|
||||
$asset->setTitle('Asset A');
|
||||
$asset->setAlias('asset-a');
|
||||
$asset->setStorageLocation('local');
|
||||
$asset->setPath('broken-image.jpg');
|
||||
$asset->setExtension('jpg');
|
||||
$asset->setCreatedByUser($userCreator->getUserIdentifier());
|
||||
$asset->setCreatedBy($userCreator->getId());
|
||||
$this->em->persist($asset);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->logoutUser();
|
||||
|
||||
$this->loginUser($userEditor);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, "/s/assets/{$route}/{$asset->getId()}");
|
||||
|
||||
Assert::assertSame($expectedStatusCode, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<string, mixed[]>
|
||||
*/
|
||||
public static function getValuesProvider(): \Generator
|
||||
{
|
||||
yield 'The sales user with edit own permission can edits its own asset' => [
|
||||
'route' => 'edit',
|
||||
'permission' => ['editown'],
|
||||
'expectedStatusCode' => Response::HTTP_OK,
|
||||
'userCreatorUN' => self::SALES_USER,
|
||||
];
|
||||
|
||||
yield 'The sales user with edit own permission cannot edit asset created by admin' => [
|
||||
'route' => 'edit',
|
||||
'permission' => ['editown'],
|
||||
'expectedStatusCode' => Response::HTTP_FORBIDDEN,
|
||||
'userCreatorUN' => self::ADMIN_USER,
|
||||
];
|
||||
|
||||
yield 'The sales user with edit other permission can edit asset created by admin' => [
|
||||
'route' => 'edit',
|
||||
'permission' => ['editown', 'editother'],
|
||||
'expectedStatusCode' => Response::HTTP_OK,
|
||||
'userCreatorUN' => self::ADMIN_USER,
|
||||
];
|
||||
|
||||
yield 'The sales user with view own permission cannot edit or asset created by admin' => [
|
||||
'route' => 'edit',
|
||||
'permission' => ['viewown'],
|
||||
'expectedStatusCode' => Response::HTTP_FORBIDDEN,
|
||||
'userCreatorUN' => self::ADMIN_USER,
|
||||
];
|
||||
|
||||
yield 'The sales user with view other permission cannot edit asset created by admin' => [
|
||||
'route' => 'edit',
|
||||
'permission' => ['viewown', 'viewother'],
|
||||
'expectedStatusCode' => Response::HTTP_FORBIDDEN,
|
||||
'userCreatorUN' => self::ADMIN_USER,
|
||||
];
|
||||
|
||||
yield 'The sales user with view own permission cannot view asset created by admin' => [
|
||||
'route' => 'view',
|
||||
'permission' => ['viewown'],
|
||||
'expectedStatusCode' => Response::HTTP_FORBIDDEN,
|
||||
'userCreatorUN' => self::ADMIN_USER,
|
||||
];
|
||||
|
||||
yield 'The sales user with view others permission can view asset created by admin' => [
|
||||
'route' => 'view',
|
||||
'permission' => ['viewown', 'viewother'],
|
||||
'expectedStatusCode' => Response::HTTP_OK,
|
||||
'userCreatorUN' => self::ADMIN_USER,
|
||||
];
|
||||
|
||||
yield 'The sales user with view own permission can view its own asset' => [
|
||||
'route' => 'view',
|
||||
'permission' => ['viewown'],
|
||||
'expectedStatusCode' => Response::HTTP_OK,
|
||||
'userCreatorUN' => self::SALES_USER,
|
||||
];
|
||||
}
|
||||
|
||||
public function testAssetUploadPathTraversal(): void
|
||||
{
|
||||
$client = $this->client;
|
||||
$container = $this->getContainer();
|
||||
|
||||
// Get CSRF token
|
||||
$csrfToken = $container->get('security.csrf.token_manager')->getToken('mautic_ajax_post')->getValue();
|
||||
|
||||
// Create a temporary file
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'test_');
|
||||
file_put_contents($tempFile, '111');
|
||||
|
||||
// Prepare the file for upload
|
||||
$uploadedFile = new UploadedFile(
|
||||
$tempFile,
|
||||
'test.txt',
|
||||
'text/plain',
|
||||
null,
|
||||
true
|
||||
);
|
||||
|
||||
$tmpDir = 'tmp_'.substr(md5(uniqid()), 0, 13);
|
||||
$client->request(
|
||||
'POST',
|
||||
'/s/_uploader/asset/upload',
|
||||
['tempId' => '../../'.$tmpDir],
|
||||
['file' => $uploadedFile],
|
||||
[
|
||||
'HTTP_X-Requested-With' => 'XMLHttpRequest',
|
||||
'HTTP_X-CSRF-Token' => $csrfToken,
|
||||
]
|
||||
);
|
||||
|
||||
$response = $client->getResponse();
|
||||
|
||||
// Assert response is successful
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
|
||||
// Decode JSON response
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
|
||||
// Assert the response contains expected keys
|
||||
$this->assertArrayHasKey('tmpFileName', $responseData);
|
||||
|
||||
// Assert file was created in the correct directory
|
||||
$expectedDir = $container->getParameter('mautic.upload_dir').join('/', ['', 'tmp', $tmpDir]);
|
||||
$expectedFilePath = join('/', [$expectedDir, $responseData['tmpFileName']]);
|
||||
$this->assertFileExists($expectedFilePath);
|
||||
|
||||
// Clean up
|
||||
if (file_exists($expectedFilePath)) {
|
||||
unlink($expectedFilePath);
|
||||
}
|
||||
if (is_dir($expectedDir)) {
|
||||
rmdir($expectedDir);
|
||||
}
|
||||
if (file_exists($tempFile)) {
|
||||
unlink($tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
private function getUser(string $username): User
|
||||
{
|
||||
$repository = $this->em->getRepository(User::class);
|
||||
|
||||
return $repository->findOneBy(['username' => $username]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, array<string>>> $permissions
|
||||
*/
|
||||
private function setPermission(User $user, array $permissions): void
|
||||
{
|
||||
$role = $user->getRole();
|
||||
|
||||
// Delete previous permissions
|
||||
$this->em->createQueryBuilder()
|
||||
->delete(Permission::class, 'p')
|
||||
->where('p.bundle = :bundle')
|
||||
->andWhere('p.role = :role_id')
|
||||
->setParameters(['bundle' => 'asset', 'role_id' => $role->getId()])
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
// Set new permissions
|
||||
$role->setIsAdmin(false);
|
||||
$roleModel = static::getContainer()->get('mautic.user.model.role');
|
||||
\assert($roleModel instanceof RoleModel);
|
||||
$roleModel->setRolePermissions($role, $permissions);
|
||||
$this->em->persist($role);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function testPostRequestWithWrongTempNameAndOriginalFileNameFileExtension(): void
|
||||
{
|
||||
$response = $this->client->request(
|
||||
Request::METHOD_GET,
|
||||
'/s/assets/new',
|
||||
);
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_OK);
|
||||
$form = $response->filter('form[name="asset"]')->form();
|
||||
$data = $form->getPhpValues();
|
||||
$data['asset']['tempName'] = 'image2.php';
|
||||
$data['asset']['originalFileName'] = 'originalImage2.php';
|
||||
$data['asset']['storageLocation'] = 'local';
|
||||
$data['asset']['title'] = 'title';
|
||||
$data['asset']['description'] = 'description';
|
||||
$this->client->submit($form, $data);
|
||||
preg_match_all('/Upload failed as the file extension, php/', $this->client->getResponse()->getContent(), $matches);
|
||||
$this->assertCount(2, $matches[0]);
|
||||
$this->assertStringContainsString('Upload failed as the file extension, php', $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testPostRequestWithWrongTempNameFileExtension(): void
|
||||
{
|
||||
$response = $this->client->request(
|
||||
Request::METHOD_GET,
|
||||
'/s/assets/new',
|
||||
);
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_OK);
|
||||
$form = $response->filter('form[name="asset"]')->form();
|
||||
$data = $form->getPhpValues();
|
||||
$data['asset']['tempName'] = 'image2.php';
|
||||
$data['asset']['originalFileName'] = 'originalImage2.png';
|
||||
$data['asset']['storageLocation'] = 'local';
|
||||
$data['asset']['title'] = 'title';
|
||||
$data['asset']['description'] = 'description';
|
||||
$this->client->submit($form, $data);
|
||||
preg_match_all('/Upload failed as the file extension, php/', $this->client->getResponse()->getContent(), $matches);
|
||||
$this->assertCount(1, $matches[0]);
|
||||
$this->assertStringContainsString('Upload failed as the file extension, php', $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testPostResquetSuccessWithCorrectFileExtension(): void
|
||||
{
|
||||
$response = $this->client->request(
|
||||
Request::METHOD_GET,
|
||||
'/s/assets/new',
|
||||
);
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_OK);
|
||||
$form = $response->filter('form[name="asset"]')->form();
|
||||
$data = $form->getPhpValues();
|
||||
$data['asset']['tempName'] = 'image.png';
|
||||
$data['asset']['originalFileName'] = 'originalImage.png';
|
||||
$data['asset']['storageLocation'] = 'local';
|
||||
$data['asset']['title'] = 'title';
|
||||
$data['asset']['description'] = 'description';
|
||||
$this->client->submit($form, $data);
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_OK);
|
||||
$this->assertStringNotContainsString('Upload failed as the file extension, php', $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testAssetWithProject(): void
|
||||
{
|
||||
$asset = new Asset();
|
||||
$asset->setTitle('test');
|
||||
$asset->setAlias('test');
|
||||
$this->em->persist($asset);
|
||||
|
||||
$project = new Project();
|
||||
$project->setName('Test Project');
|
||||
$this->em->persist($project);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/assets/edit/'.$asset->getId());
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form['asset[projects]']->setValue((string) $project->getId());
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$savedAsset = $this->em->find(Asset::class, $asset->getId());
|
||||
Assert::assertSame($project->getId(), $savedAsset->getProjects()->first()->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Tests\Controller;
|
||||
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class AssetDetailFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testLeadViewPreventsXSS(): void
|
||||
{
|
||||
$title = 'aaa" onerror=alert(1) a="';
|
||||
$asset = new Asset();
|
||||
$asset->setTitle($title);
|
||||
$asset->setAlias('dummy-alias');
|
||||
$asset->setStorageLocation('local');
|
||||
$asset->setPath('broken-image.jpg');
|
||||
$asset->setExtension('jpg');
|
||||
$this->em->persist($asset);
|
||||
$this->em->flush();
|
||||
$this->em->detach($asset);
|
||||
|
||||
$crawler = $this->client->request('GET', sprintf('/s/assets/view/%d', $asset->getId()));
|
||||
$imageTag = $crawler->filter('.img-thumbnail');
|
||||
|
||||
$onError = $imageTag->attr('onerror');
|
||||
$altProp = $imageTag->attr('alt');
|
||||
|
||||
Assert::assertNull($onError);
|
||||
Assert::assertSame($title, $altProp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\AssetBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class AssetDownloadFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testDownloadOfNotFoundAsset(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/logout');
|
||||
|
||||
// The 500 error happened only on the second request.
|
||||
// It happened only if the device was already tracked.
|
||||
$this->client->request(Request::METHOD_GET, '/asset/unicorn'); // returns 404 correctly
|
||||
$this->client->request(Request::METHOD_GET, '/asset/unicorn'); // returned 500 but it should return 404
|
||||
|
||||
Assert::assertSame(Response::HTTP_NOT_FOUND, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\AssetBundle\Tests\Controller;
|
||||
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
|
||||
|
||||
final class AssetProjectSearchFunctionalTest extends AbstractProjectSearchTestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
|
||||
public function testProjectSearch(string $searchTerm, array $expectedEntities, array $unexpectedEntities): void
|
||||
{
|
||||
$projectOne = $this->createProject('Project One');
|
||||
$projectTwo = $this->createProject('Project Two');
|
||||
$projectThree = $this->createProject('Project Three');
|
||||
|
||||
$assetAlpha = $this->createAsset('Asset Alpha');
|
||||
$assetBeta = $this->createAsset('Asset Beta');
|
||||
$this->createAsset('Asset Gamma');
|
||||
$this->createAsset('Asset Delta');
|
||||
|
||||
$assetAlpha->addProject($projectOne);
|
||||
$assetAlpha->addProject($projectTwo);
|
||||
$assetBeta->addProject($projectTwo);
|
||||
$assetBeta->addProject($projectThree);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/assets', '/s/assets']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<string, array{searchTerm: string, expectedEntities: array<string>, unexpectedEntities: array<string>}>
|
||||
*/
|
||||
public static function searchDataProvider(): \Generator
|
||||
{
|
||||
yield 'search by one project' => [
|
||||
'searchTerm' => 'project:"Project Two"',
|
||||
'expectedEntities' => ['Asset Alpha', 'Asset Beta'],
|
||||
'unexpectedEntities' => ['Asset Gamma', 'Asset Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project AND asset name' => [
|
||||
'searchTerm' => 'project:"Project Two" AND Beta',
|
||||
'expectedEntities' => ['Asset Beta'],
|
||||
'unexpectedEntities' => ['Asset Alpha', 'Asset Gamma', 'Asset Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project OR asset name' => [
|
||||
'searchTerm' => 'project:"Project Two" OR Gamma',
|
||||
'expectedEntities' => ['Asset Alpha', 'Asset Beta', 'Asset Gamma'],
|
||||
'unexpectedEntities' => ['Asset Delta'],
|
||||
];
|
||||
|
||||
yield 'search by NOT one project' => [
|
||||
'searchTerm' => '!project:"Project Two"',
|
||||
'expectedEntities' => ['Asset Gamma', 'Asset Delta'],
|
||||
'unexpectedEntities' => ['Asset Alpha', 'Asset Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with AND' => [
|
||||
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
|
||||
'expectedEntities' => ['Asset Beta'],
|
||||
'unexpectedEntities' => ['Asset Alpha', 'Asset Gamma', 'Asset Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT AND' => [
|
||||
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
|
||||
'expectedEntities' => ['Asset Gamma', 'Asset Delta'],
|
||||
'unexpectedEntities' => ['Asset Alpha', 'Asset Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with OR' => [
|
||||
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
|
||||
'expectedEntities' => ['Asset Alpha', 'Asset Beta'],
|
||||
'unexpectedEntities' => ['Asset Gamma', 'Asset Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT OR' => [
|
||||
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
|
||||
'expectedEntities' => ['Asset Alpha', 'Asset Gamma', 'Asset Delta'],
|
||||
'unexpectedEntities' => ['Asset Beta'],
|
||||
];
|
||||
}
|
||||
|
||||
private function createAsset(string $name): Asset
|
||||
{
|
||||
$asset = new Asset();
|
||||
$asset->setTitle($name);
|
||||
$asset->setAlias($name);
|
||||
$this->em->persist($asset);
|
||||
|
||||
return $asset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Tests\Controller;
|
||||
|
||||
use Mautic\AssetBundle\Entity\Download;
|
||||
use Mautic\AssetBundle\Tests\Asset\AbstractAssetTestCase;
|
||||
|
||||
class PublicControllerFunctionalTest extends AbstractAssetTestCase
|
||||
{
|
||||
/**
|
||||
* Download action should return the file content.
|
||||
*/
|
||||
public function testDownloadActionStreamByDefault(): void
|
||||
{
|
||||
$assetSlug = $this->asset->getId().':'.$this->asset->getAlias();
|
||||
|
||||
$this->client->request('GET', '/asset/'.$assetSlug);
|
||||
ob_start();
|
||||
$response = $this->client->getResponse();
|
||||
$response->sendContent();
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertSame($this->expectedMimeType, $response->headers->get('Content-Type'));
|
||||
$this->assertNotSame($this->expectedContentDisposition.$this->asset->getOriginalFileName(), $response->headers->get('Content-Disposition'));
|
||||
$this->assertEquals($this->expectedPngContent, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download action should return the file content.
|
||||
*/
|
||||
public function testDownloadActionStreamIsZero(): void
|
||||
{
|
||||
$assetSlug = $this->asset->getId().':'.$this->asset->getAlias();
|
||||
|
||||
$this->client->request('GET', '/asset/'.$assetSlug.'?stream=0');
|
||||
ob_start();
|
||||
$response = $this->client->getResponse();
|
||||
$response->sendContent();
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertSame($this->expectedContentDisposition.$this->asset->getOriginalFileName(), $response->headers->get('Content-Disposition'));
|
||||
$this->assertEquals($this->expectedPngContent, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download action with UTM should return the file content.
|
||||
*/
|
||||
public function testDownloadActionWithUTM(): void
|
||||
{
|
||||
$this->logoutUser();
|
||||
$assetSlug = $this->asset->getId().':'.$this->asset->getAlias().'?utm_source=test2&utm_medium=test3&utm_campaign=test6&utm_term=test4&utm_content=test5';
|
||||
|
||||
$this->client->request('GET', '/asset/'.$assetSlug);
|
||||
ob_start();
|
||||
$response = $this->client->getResponse();
|
||||
$response->sendContent();
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertSame($this->expectedMimeType, $response->headers->get('Content-Type'));
|
||||
$this->assertNotSame($this->expectedContentDisposition.$this->asset->getOriginalFileName(), $response->headers->get('Content-Disposition'));
|
||||
$this->assertEquals($this->expectedPngContent, $content);
|
||||
|
||||
$downloadRepo = $this->em->getRepository(Download::class);
|
||||
|
||||
$download = $downloadRepo->findOneBy(['asset' => $this->asset]);
|
||||
\assert($download instanceof Download);
|
||||
$this->assertSame('test2', $download->getUtmSource());
|
||||
$this->assertSame('test3', $download->getUtmMedium());
|
||||
$this->assertSame('test4', $download->getUtmTerm());
|
||||
$this->assertSame('test5', $download->getUtmContent());
|
||||
$this->assertSame('test6', $download->getUtmCampaign());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\AssetBundle\Tests\Controller;
|
||||
|
||||
use Mautic\AssetBundle\Tests\Asset\AbstractAssetTestCase;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class UploadControllerFunctionalTest extends AbstractAssetTestCase
|
||||
{
|
||||
public function testUploadWithWrongMimetype(): void
|
||||
{
|
||||
// Create a php file with the content of phpinfo
|
||||
$assetsPath = $this->client->getKernel()->getContainer()->getParameter('mautic.upload_dir');
|
||||
|
||||
$fileName = 'image2.png';
|
||||
$filePath = $assetsPath.'/'.$fileName;
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
copy('index.php', $filePath);
|
||||
|
||||
$binaryFile = new UploadedFile($filePath, $fileName, 'application/x-httpd-php', null, true);
|
||||
|
||||
$tmpId = 'tempId_'.time();
|
||||
// Upload the file
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/s/_uploader/asset/upload',
|
||||
[
|
||||
'tempId' => $tmpId,
|
||||
],
|
||||
[
|
||||
'file' => $binaryFile,
|
||||
]
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertStringContainsString('Upload failed as the file mimetype', $response->getContent());
|
||||
$this->assertStringContainsString('text\/x-php is not allowed', $response->getContent());
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
public function testSuccessUploadWithPng(): void
|
||||
{
|
||||
// Create a temporary PNG file
|
||||
// Create a php file with the content of phpinfo
|
||||
$assetsPath = $this->client->getKernel()->getContainer()->getParameter('mautic.upload_dir');
|
||||
$assetsPathFrom = $this->client->getKernel()->getContainer()->getParameter('mautic.application_dir').'/app/assets/images/mautic_logo_db64.png';
|
||||
|
||||
$fileName = 'image3.png';
|
||||
$filePath = $assetsPath.'/'.$fileName;
|
||||
|
||||
copy($assetsPathFrom, $filePath);
|
||||
// Create an UploadedFile instance with the correct MIME type
|
||||
$uploadedFile = new UploadedFile($filePath, $fileName, 'image/png', null, true);
|
||||
|
||||
$tmpId = 'tempId_'.time();
|
||||
// Perform the request with the file
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/s/_uploader/asset/upload',
|
||||
['tempId' => $tmpId],
|
||||
['file' => $uploadedFile]
|
||||
);
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_OK);
|
||||
$this->assertStringContainsString('state":1', $this->client->getResponse()->getContent());
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
$data = json_decode($this->client->getResponse()->getContent(), true);
|
||||
unlink($assetsPath.'/tmp/'.$tmpId.'/'.$data['tmpFileName']);
|
||||
rmdir($assetsPath.'/tmp/'.$tmpId);
|
||||
}
|
||||
|
||||
public function testUploadWithWrongExtension(): void
|
||||
{
|
||||
// Create a php file with the content of phpinfo
|
||||
$assetsPath = $this->client->getKernel()->getContainer()->getParameter('mautic.upload_dir');
|
||||
$assetsPathFrom = $this->client->getKernel()->getContainer()->getParameter('mautic.application_dir').'/app/assets/images/mautic_logo_db64.png';
|
||||
|
||||
$fileName = 'image2.php';
|
||||
$filePath = $assetsPath.'/'.$fileName;
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
copy($assetsPathFrom, $filePath);
|
||||
|
||||
$binaryFile = new UploadedFile($filePath, $fileName, 'image/png', null, true);
|
||||
|
||||
$tmpId = 'tempId_'.time();
|
||||
// Upload the file
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/s/_uploader/asset/upload',
|
||||
[
|
||||
'tempId' => $tmpId,
|
||||
],
|
||||
[
|
||||
'file' => $binaryFile,
|
||||
]
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertStringContainsString('Upload failed as the file extension', $response->getContent());
|
||||
$this->assertStringContainsString('Upload failed as the file extension, php,', $response->getContent());
|
||||
unlink($filePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\AssetBundle\Tests\DataFixtures;
|
||||
|
||||
use Mautic\AssetBundle\DataFixtures\ORM\LoadAssetData;
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
|
||||
class LoadAssetDataTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testLoadFixtures(): void
|
||||
{
|
||||
$this->loadFixtures([LoadAssetData::class]);
|
||||
$asset = $this->em->getRepository(Asset::class)->findOneBy(
|
||||
['title' => '@TOCHANGE: Asset1 Title'],
|
||||
['id' => 'DESC']
|
||||
);
|
||||
self::assertInstanceOf(Asset::class, $asset);
|
||||
self::assertEquals('asset1', $asset->getAlias());
|
||||
self::assertEquals('@TOCHANGE: Asset1 Original File Name', $asset->getOriginalFileName());
|
||||
self::assertEquals('fdb8e28357b02d12d068de3e5661832e21bc08ec.doc', $asset->getPath());
|
||||
self::assertEquals(1, $asset->getDownloadCount());
|
||||
self::assertEquals(1, $asset->getUniqueDownloadCount());
|
||||
self::assertEquals(1, $asset->getRevision());
|
||||
self::assertEquals('en', $asset->getLanguage());
|
||||
}
|
||||
|
||||
public function testLoadFixturesOrder(): void
|
||||
{
|
||||
$loadAssetData = new LoadAssetData();
|
||||
self::assertEquals(10, $loadAssetData->getOrder());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\AssetBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\AssetBundle\Entity\AssetRepository;
|
||||
use Mautic\CoreBundle\Test\Doctrine\RepositoryConfiguratorTrait;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class AssetRepositoryTest extends TestCase
|
||||
{
|
||||
use RepositoryConfiguratorTrait;
|
||||
|
||||
private function getRepository(): AssetRepository
|
||||
{
|
||||
$repository = $this->configureRepository(Asset::class);
|
||||
$this->connection->method('createQueryBuilder')->willReturnCallback(fn () => new QueryBuilder($this->connection));
|
||||
|
||||
$translator = $this->createMock(TranslatorInterface::class);
|
||||
$translator->method('trans')->willReturnCallback(fn ($id) => match ($id) {
|
||||
'mautic.asset.asset.searchcommand.isexpired' => 'is:expired',
|
||||
'mautic.asset.asset.searchcommand.ispending' => 'is:pending',
|
||||
default => $id,
|
||||
});
|
||||
$repository->setTranslator($translator);
|
||||
|
||||
return $repository;
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataExpirationFilters')]
|
||||
public function testAddSearchCommandWhereClauseHandlesExpirationFilters(string $command, string $expected): void
|
||||
{
|
||||
$repository = $this->getRepository();
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$filter = (object) ['command' => $command, 'string' => '', 'not' => false, 'strict' => false];
|
||||
|
||||
$method = new \ReflectionMethod(AssetRepository::class, 'addSearchCommandWhereClause');
|
||||
$method->setAccessible(true);
|
||||
|
||||
[$expr, $params] = $method->invoke($repository, $qb, $filter);
|
||||
|
||||
self::assertSame($expected, (string) $expr);
|
||||
self::assertSame(['par1' => true], $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array{0: string, 1: string}>
|
||||
*/
|
||||
public static function dataExpirationFilters(): iterable
|
||||
{
|
||||
yield ['is:expired', "(a.isPublished = :par1 AND a.publishDown IS NOT NULL AND a.publishDown <> '' AND a.publishDown < CURRENT_TIMESTAMP())"];
|
||||
yield ['is:pending', "(a.isPublished = :par1 AND a.publishUp IS NOT NULL AND a.publishUp <> '' AND a.publishUp > CURRENT_TIMESTAMP())"];
|
||||
}
|
||||
|
||||
public function testGetSearchCommandsContainsExpirationFilters(): void
|
||||
{
|
||||
$repository = $this->getRepository();
|
||||
$commands = $repository->getSearchCommands();
|
||||
self::assertContains('mautic.asset.asset.searchcommand.isexpired', $commands);
|
||||
self::assertContains('mautic.asset.asset.searchcommand.ispending', $commands);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\AssetBundle\Tests\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\AssetBundle\Entity\DownloadRepository;
|
||||
use Mautic\AssetBundle\EventListener\DetermineWinnerSubscriber;
|
||||
use Mautic\CoreBundle\Event\DetermineWinnerEvent;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class DetermineWinnerSubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject|EntityManagerInterface
|
||||
*/
|
||||
private MockObject $em;
|
||||
|
||||
/**
|
||||
* @var MockObject|TranslatorInterface
|
||||
*/
|
||||
private MockObject $translator;
|
||||
|
||||
private DetermineWinnerSubscriber $subscriber;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->em = $this->createMock(EntityManagerInterface::class);
|
||||
$this->translator = $this->createMock(TranslatorInterface::class);
|
||||
$this->subscriber = new DetermineWinnerSubscriber($this->em, $this->translator);
|
||||
}
|
||||
|
||||
public function testOnDetermineDownloadRateWinner(): void
|
||||
{
|
||||
$parentMock = $this->createMock(Page::class);
|
||||
$childMock = $this->createMock(Page::class);
|
||||
$children = [2 => $childMock];
|
||||
$repoMock = $this->createMock(DownloadRepository::class);
|
||||
$parameters = ['parent' => $parentMock, 'children' => $children];
|
||||
$event = new DetermineWinnerEvent($parameters);
|
||||
$startDate = new \DateTime();
|
||||
|
||||
$transDownloads = 'downloads';
|
||||
$transHits = 'hits';
|
||||
|
||||
$counts = [
|
||||
1 => [
|
||||
'count' => 20,
|
||||
'id' => 1,
|
||||
'name' => 'Test 5',
|
||||
'total' => 100,
|
||||
],
|
||||
2 => [
|
||||
'count' => 25,
|
||||
'id' => 2,
|
||||
'name' => 'Test 6',
|
||||
'total' => 150,
|
||||
],
|
||||
];
|
||||
|
||||
$this->translator->method('trans')
|
||||
->willReturnOnConsecutiveCalls($transDownloads, $transHits);
|
||||
|
||||
$this->em->expects($this->once())
|
||||
->method('getRepository')
|
||||
->willReturn($repoMock);
|
||||
|
||||
$parentMock->expects($this->any())
|
||||
->method('isPublished')
|
||||
->willReturn(true);
|
||||
|
||||
$childMock->expects($this->any())
|
||||
->method('isPublished')
|
||||
->willReturn(true);
|
||||
|
||||
$parentMock->expects($this->any())
|
||||
->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$childMock->expects($this->any())
|
||||
->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
$parentMock->expects($this->once())
|
||||
->method('getVariantStartDate')
|
||||
->willReturn($startDate);
|
||||
|
||||
$repoMock->expects($this->once())
|
||||
->method('getDownloadCountsByPage')
|
||||
->with([1, 2], $startDate)
|
||||
->willReturn($counts);
|
||||
|
||||
$this->subscriber->onDetermineDownloadRateWinner($event);
|
||||
|
||||
$expectedData = [
|
||||
$transDownloads => [$counts[1]['count'], $counts[2]['count']],
|
||||
$transHits => [$counts[1]['total'], $counts[2]['total']],
|
||||
];
|
||||
|
||||
$abTestResults = $event->getAbTestResults();
|
||||
|
||||
$this->assertEquals($abTestResults['winners'], [1]);
|
||||
$this->assertEquals($abTestResults['support']['data'], $expectedData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\AssetBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\AssetBundle\Entity\Download;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\ReportBundle\Tests\Functional\AbstractReportSubscriberTestCase;
|
||||
|
||||
class ReportSubscriberFunctionalTest extends AbstractReportSubscriberTestCase
|
||||
{
|
||||
public function testAssetDownloadReportWithDncListColumn(): void
|
||||
{
|
||||
$leads[] = $this->createContact('test1@example.com');
|
||||
$leads[] = $this->createContact('test2@example.com');
|
||||
$leads[] = $this->createContact('test3@example.com');
|
||||
$this->em->flush();
|
||||
|
||||
$this->createDnc('email', $leads[0], DoNotContact::BOUNCED);
|
||||
$this->createDnc('email', $leads[1], DoNotContact::MANUAL);
|
||||
$this->createDnc('email', $leads[2], DoNotContact::UNSUBSCRIBED);
|
||||
$this->createDnc('sms', $leads[2], DoNotContact::MANUAL);
|
||||
$this->em->flush();
|
||||
|
||||
$asset = $this->createAsset();
|
||||
$this->emulateAssetDownload($asset, $leads[0]);
|
||||
$this->emulateAssetDownload($asset, $leads[1]);
|
||||
$this->emulateAssetDownload($asset, $leads[2]);
|
||||
|
||||
$report = $this->createReport(
|
||||
source: 'asset.downloads',
|
||||
columns: ['l.id', 'a.id', 'a.title', 'dnc_preferences'],
|
||||
filters: [
|
||||
[
|
||||
'column' => 'dnc_preferences',
|
||||
'glue' => 'and',
|
||||
'dynamic' => null,
|
||||
'condition' => 'in',
|
||||
'value' => [
|
||||
'email:'.DoNotContact::UNSUBSCRIBED,
|
||||
'email:'.DoNotContact::BOUNCED,
|
||||
],
|
||||
],
|
||||
],
|
||||
order: [['column' => 'l.id', 'direction' => 'ASC']]
|
||||
);
|
||||
|
||||
$expectedReport = [
|
||||
[(string) $leads[0]->getId(), (string) $asset->getId(), $asset->getTitle(), 'DNC Bounced: Email'],
|
||||
[(string) $leads[2]->getId(), (string) $asset->getId(), $asset->getTitle(), 'DNC Manually Unsubscribed: Text Message, DNC Unsubscribed: Email'],
|
||||
];
|
||||
$this->verifyReport($report->getId(), $expectedReport);
|
||||
$this->verifyApiReport($report->getId(), $expectedReport);
|
||||
}
|
||||
|
||||
private function createAsset(): Asset
|
||||
{
|
||||
$asset = new Asset();
|
||||
$asset->setTitle('test');
|
||||
$asset->setAlias('test');
|
||||
$asset->setDateAdded(new \DateTime('2020-02-07 20:29:02'));
|
||||
$asset->setDateModified(new \DateTime('2020-03-21 20:29:02'));
|
||||
$asset->setCreatedByUser('Test User');
|
||||
|
||||
$this->em->persist($asset);
|
||||
$this->em->flush();
|
||||
|
||||
return $asset;
|
||||
}
|
||||
|
||||
private function emulateAssetDownload(Asset $asset, Lead $contact): Download
|
||||
{
|
||||
$assetDownload = new Download();
|
||||
$assetDownload->setAsset($asset);
|
||||
$assetDownload->setLead($contact);
|
||||
$assetDownload->setDateDownload(new \DateTime());
|
||||
$assetDownload->setCode(200);
|
||||
$assetDownload->setTrackingId(random_int(1, 99999));
|
||||
$this->em->persist($assetDownload);
|
||||
$this->em->flush();
|
||||
|
||||
return $assetDownload;
|
||||
}
|
||||
|
||||
private function createContact(string $email): Lead
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setEmail($email);
|
||||
$this->em->persist($contact);
|
||||
|
||||
return $contact;
|
||||
}
|
||||
|
||||
public function createDnc(string $channel, Lead $contact, int $reason): DoNotContact
|
||||
{
|
||||
$dnc = new DoNotContact();
|
||||
$dnc->setChannel($channel);
|
||||
$dnc->setLead($contact);
|
||||
$dnc->setReason($reason);
|
||||
$dnc->setDateAdded(new \DateTime());
|
||||
$this->em->persist($dnc);
|
||||
|
||||
return $dnc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\AssetBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\Entity\DownloadRepository;
|
||||
use Mautic\AssetBundle\EventListener\ReportSubscriber;
|
||||
use Mautic\ChannelBundle\Helper\ChannelListHelper;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Model\CompanyReportData;
|
||||
use Mautic\LeadBundle\Report\DncReportService;
|
||||
use Mautic\LeadBundle\Segment\Query\QueryBuilder;
|
||||
use Mautic\ReportBundle\Entity\Report;
|
||||
use Mautic\ReportBundle\Event\ReportBuilderEvent;
|
||||
use Mautic\ReportBundle\Event\ReportGeneratorEvent;
|
||||
use Mautic\ReportBundle\Helper\ReportHelper;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ReportSubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
private ChannelListHelper $channelListHelper;
|
||||
|
||||
/**
|
||||
* @var CompanyReportData|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $companyReportData;
|
||||
|
||||
/**
|
||||
* @var DownloadRepository|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $downloadRepository;
|
||||
|
||||
/**
|
||||
* @var QueryBuilder|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $queryBuilder;
|
||||
|
||||
/**
|
||||
* @var DncReportService|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $dncReportService;
|
||||
|
||||
private ReportHelper $reportHelper;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->queryBuilder = $this->createMock(QueryBuilder::class);
|
||||
$this->channelListHelper = new ChannelListHelper($this->createMock(EventDispatcherInterface::class), $this->createMock(Translator::class));
|
||||
$this->reportHelper = new ReportHelper($this->createMock(EventDispatcherInterface::class));
|
||||
$this->companyReportData = $this->createMock(CompanyReportData::class);
|
||||
$this->downloadRepository = $this->createMock(DownloadRepository::class);
|
||||
$this->dncReportService = $this->createMock(DncReportService::class);
|
||||
}
|
||||
|
||||
public function testOnReportBuilderWithUnknownContext(): void
|
||||
{
|
||||
$companyReportData = new class extends CompanyReportData {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
$downloadRepository = new class extends DownloadRepository {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
$event = new class extends ReportBuilderEvent {
|
||||
public function __construct()
|
||||
{
|
||||
$this->context = 'unicorn';
|
||||
}
|
||||
};
|
||||
|
||||
$reportSubscriber = new ReportSubscriber($companyReportData, $downloadRepository, $this->dncReportService);
|
||||
|
||||
$reportSubscriber->onReportBuilder($event);
|
||||
|
||||
Assert::assertSame([], $event->getTables());
|
||||
}
|
||||
|
||||
public function testOnReportBuilderWithAssetDownloadContext(): void
|
||||
{
|
||||
$companyReportData = new class extends CompanyReportData {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function getCompanyData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
$downloadRepository = new class extends DownloadRepository {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
$event = new ReportBuilderEvent($this->createTranslatorMock(), $this->channelListHelper, ReportSubscriber::CONTEXT_ASSET_DOWNLOAD, [], $this->reportHelper);
|
||||
|
||||
$reportSubscriber = new ReportSubscriber($companyReportData, $downloadRepository, $this->dncReportService);
|
||||
|
||||
$reportSubscriber->onReportBuilder($event);
|
||||
|
||||
Assert::assertSame(
|
||||
[
|
||||
'alias' => 'download_count',
|
||||
'label' => '[trans]mautic.asset.report.download_count[/trans]',
|
||||
'type' => 'int',
|
||||
],
|
||||
$event->getTables()['assets']['columns']['a.download_count']
|
||||
);
|
||||
|
||||
Assert::assertSame(
|
||||
[
|
||||
'alias' => 'unique_download_count',
|
||||
'label' => '[trans]mautic.asset.report.unique_download_count[/trans]',
|
||||
'type' => 'int',
|
||||
],
|
||||
$event->getTables()['assets']['columns']['a.unique_download_count']
|
||||
);
|
||||
|
||||
Assert::assertSame(
|
||||
[
|
||||
'alias' => 'download_count',
|
||||
'label' => '[trans]mautic.asset.report.download_count[/trans]',
|
||||
'type' => 'int',
|
||||
'formula' => 'COUNT(ad.id)',
|
||||
],
|
||||
$event->getTables()['asset.downloads']['columns']['a.download_count']
|
||||
);
|
||||
|
||||
Assert::assertSame(
|
||||
[
|
||||
'alias' => 'unique_download_count',
|
||||
'label' => '[trans]mautic.asset.report.unique_download_count[/trans]',
|
||||
'type' => 'int',
|
||||
'formula' => 'COUNT(DISTINCT ad.lead_id)',
|
||||
],
|
||||
$event->getTables()['asset.downloads']['columns']['a.unique_download_count']
|
||||
);
|
||||
}
|
||||
|
||||
private function createTranslatorMock(): TranslatorInterface
|
||||
{
|
||||
return new class implements TranslatorInterface {
|
||||
/**
|
||||
* @param array<int|string> $parameters
|
||||
*/
|
||||
public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
|
||||
{
|
||||
return '[trans]'.$id.'[/trans]';
|
||||
}
|
||||
|
||||
public function getLocale(): string
|
||||
{
|
||||
return 'en';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public function testGroupByDefaultConfigured(): void
|
||||
{
|
||||
$report = new Report();
|
||||
$report->setSource(ReportSubscriber::CONTEXT_ASSET_DOWNLOAD);
|
||||
$event = new ReportGeneratorEvent($report, [], $this->queryBuilder, $this->channelListHelper);
|
||||
$subscriber = new ReportSubscriber($this->companyReportData, $this->downloadRepository, $this->dncReportService);
|
||||
$this->queryBuilder->method('from')->willReturn($this->queryBuilder);
|
||||
|
||||
$this->queryBuilder->expects($this->once())
|
||||
->method('groupBy')
|
||||
->with('ad.id');
|
||||
|
||||
$this->assertFalse($event->hasGroupBy());
|
||||
|
||||
$subscriber->onReportGenerate($event);
|
||||
}
|
||||
|
||||
public function testGroupByNotDefaultConfigured(): void
|
||||
{
|
||||
$report = new Report();
|
||||
$report->setSource(ReportSubscriber::CONTEXT_ASSET_DOWNLOAD);
|
||||
$this->queryBuilder->method('from')->willReturn($this->queryBuilder);
|
||||
$report->setGroupBy(['a.id' => 'desc']);
|
||||
$event = new ReportGeneratorEvent($report, [], $this->queryBuilder, $this->channelListHelper);
|
||||
$subscriber = new ReportSubscriber($this->companyReportData, $this->downloadRepository, $this->dncReportService);
|
||||
$subscriber->onReportGenerate($event);
|
||||
$this->assertTrue($event->hasGroupBy());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\AssetBundle\Tests\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\AssetBundle\AssetEvents;
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\AssetBundle\Entity\AssetRepository;
|
||||
use Mautic\AssetBundle\Entity\Download;
|
||||
use Mautic\AssetBundle\Model\AssetModel;
|
||||
use Mautic\CacheBundle\Cache\CacheProvider;
|
||||
use Mautic\CategoryBundle\Model\CategoryModel;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\LeadBundle\Tracker\Factory\DeviceDetectorFactory\DeviceDetectorFactory;
|
||||
use Mautic\LeadBundle\Tracker\Service\DeviceCreatorService\DeviceCreatorService;
|
||||
use Mautic\LeadBundle\Tracker\Service\DeviceTrackingService\DeviceTrackingServiceInterface;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\ServerBag;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class AssetModelTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
private AssetModel $assetModel;
|
||||
|
||||
private CoreParametersHelper&MockObject $coreParametersHelper;
|
||||
|
||||
private ContainerInterface&MockObject $container;
|
||||
|
||||
private CacheProvider $cacheProvider;
|
||||
|
||||
private LeadModel&MockObject $leadModel;
|
||||
|
||||
private CategoryModel&MockObject $categoryModel;
|
||||
|
||||
private RequestStack&MockObject $requestStack;
|
||||
|
||||
private IpLookupHelper&MockObject $ipLookupHelper;
|
||||
|
||||
private DeviceDetectorFactory $deviceDetectorFactory;
|
||||
|
||||
private DeviceCreatorService $deviceCreatorService;
|
||||
|
||||
private DeviceTrackingServiceInterface&MockObject $deviceTrackingService;
|
||||
|
||||
private ContactTracker&MockObject $contactTracker;
|
||||
|
||||
private EntityManager&MockObject $entityManager;
|
||||
|
||||
private CorePermissions&MockObject $corePermissions;
|
||||
|
||||
private EventDispatcherInterface&MockObject $eventDispatcher;
|
||||
|
||||
private MockObject&UrlGeneratorInterface $urlGenerator;
|
||||
|
||||
private Translator&MockObject $translator;
|
||||
|
||||
private UserHelper&MockObject $userHelper;
|
||||
|
||||
private LoggerInterface&MockObject $logger;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
|
||||
$this->coreParametersHelper->expects($this->once())
|
||||
->method('get')
|
||||
->with($this->equalTo('max_size'))
|
||||
->willReturn('2MB');
|
||||
|
||||
$this->container = $this->createMock(ContainerInterface::class);
|
||||
$this->cacheProvider = new CacheProvider($this->coreParametersHelper, $this->container);
|
||||
$this->leadModel = $this->createMock(LeadModel::class);
|
||||
$this->categoryModel = $this->createMock(CategoryModel::class);
|
||||
$this->requestStack = $this->createMock(RequestStack::class);
|
||||
$this->ipLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$this->deviceDetectorFactory = new DeviceDetectorFactory($this->cacheProvider);
|
||||
$this->deviceCreatorService = new DeviceCreatorService();
|
||||
$this->deviceTrackingService = $this->createMock(DeviceTrackingServiceInterface::class);
|
||||
$this->contactTracker = $this->createMock(ContactTracker::class);
|
||||
$this->entityManager = $this->createMock(EntityManager::class);
|
||||
$this->corePermissions = $this->createMock(CorePermissions::class);
|
||||
$this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$this->translator = $this->createMock(Translator::class);
|
||||
$this->userHelper = $this->createMock(UserHelper::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
|
||||
$this->assetModel = new AssetModel(
|
||||
$this->leadModel,
|
||||
$this->categoryModel,
|
||||
$this->requestStack,
|
||||
$this->ipLookupHelper,
|
||||
$this->deviceCreatorService,
|
||||
$this->deviceDetectorFactory,
|
||||
$this->deviceTrackingService,
|
||||
$this->contactTracker,
|
||||
$this->entityManager,
|
||||
$this->corePermissions,
|
||||
$this->eventDispatcher,
|
||||
$this->urlGenerator,
|
||||
$this->translator,
|
||||
$this->userHelper,
|
||||
$this->logger,
|
||||
$this->coreParametersHelper,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that TrackDownload works only with a request.
|
||||
*/
|
||||
public function testTrackDownloadRequest(): void
|
||||
{
|
||||
$asset = new Asset();
|
||||
|
||||
$this->corePermissions->expects($this->once())
|
||||
->method('isAnonymous')
|
||||
->willReturn(true);
|
||||
|
||||
$this->requestStack->expects($this->once())
|
||||
->method('getCurrentRequest')
|
||||
->willReturn(null);
|
||||
|
||||
$this->entityManager->expects($this->never())
|
||||
->method('persist');
|
||||
|
||||
$this->entityManager->expects($this->never())
|
||||
->method('flush');
|
||||
|
||||
$this->entityManager->expects($this->never())
|
||||
->method('detach');
|
||||
|
||||
$this->assetModel->trackDownload($asset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that TrackDownload works successfully.
|
||||
*/
|
||||
public function testTrackDownload(): void
|
||||
{
|
||||
$asset = new Asset();
|
||||
$lead = new Lead();
|
||||
|
||||
$this->corePermissions->expects($this->once())
|
||||
->method('isAnonymous')
|
||||
->willReturn(true);
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
||||
$serverBag = $this->createMock(ServerBag::class);
|
||||
|
||||
$serverBag->expects($this->once())
|
||||
->method('get')
|
||||
->with($this->equalTo('HTTP_REFERER'))
|
||||
->willReturn('http://localhost');
|
||||
|
||||
$request->server = $serverBag;
|
||||
$matcher = $this->exactly(6);
|
||||
|
||||
$request->expects($matcher)
|
||||
->method('get')->willReturnCallback(function (...$parameters) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertEquals('utm_campaign', $parameters[0]);
|
||||
|
||||
return 'test_utm_campaign';
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertEquals('utm_content', $parameters[0]);
|
||||
|
||||
return 'test_utm_content';
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertEquals('utm_medium', $parameters[0]);
|
||||
|
||||
return 'test_utm_medium';
|
||||
}
|
||||
if (4 === $matcher->numberOfInvocations()) {
|
||||
$this->assertEquals('utm_source', $parameters[0]);
|
||||
|
||||
return 'test_utm_source';
|
||||
}
|
||||
if (5 === $matcher->numberOfInvocations()) {
|
||||
$this->assertEquals('utm_term', $parameters[0]);
|
||||
|
||||
return 'test_utm_term';
|
||||
}
|
||||
if (6 === $matcher->numberOfInvocations()) {
|
||||
$this->assertEquals('ct', $parameters[0]);
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$this->requestStack->expects($this->once())
|
||||
->method('getCurrentRequest')
|
||||
->willReturn($request);
|
||||
|
||||
$this->deviceTrackingService->expects($this->once())
|
||||
->method('isTracked')
|
||||
->willReturn(false);
|
||||
|
||||
$this->contactTracker->expects($this->once())
|
||||
->method('getContact')
|
||||
->willReturn($lead);
|
||||
|
||||
$this->deviceTrackingService->expects($this->once())
|
||||
->method('getTrackedDevice')
|
||||
->willReturn(null);
|
||||
|
||||
$assetRepository = $this->createMock(AssetRepository::class);
|
||||
|
||||
$this->entityManager->expects($this->once())
|
||||
->method('getRepository')
|
||||
->with($this->equalTo(Asset::class))
|
||||
->willReturn($assetRepository);
|
||||
|
||||
$assetRepository->expects($this->once())
|
||||
->method('upDownloadCount')
|
||||
->with(
|
||||
$this->equalTo($asset->getId()),
|
||||
$this->equalTo(1),
|
||||
$this->equalTo(true),
|
||||
);
|
||||
|
||||
$ipAddress = new IpAddress('127.0.0.1');
|
||||
|
||||
$this->ipLookupHelper->expects($this->once())
|
||||
->method('getIpAddress')
|
||||
->willReturn($ipAddress);
|
||||
|
||||
$this->eventDispatcher->expects($this->once())
|
||||
->method('hasListeners')
|
||||
->with($this->equalTo(AssetEvents::ASSET_ON_LOAD))
|
||||
->willReturn(false);
|
||||
|
||||
/** @var ?Download $download */
|
||||
$download = null;
|
||||
|
||||
$this->entityManager->expects($this->once())
|
||||
->method('persist')
|
||||
->with($this->callback(function ($downloadPersist) use (&$download) {
|
||||
$download = $downloadPersist;
|
||||
|
||||
return $download instanceof Download;
|
||||
}));
|
||||
|
||||
$this->entityManager->expects($this->once())
|
||||
->method('flush');
|
||||
|
||||
$this->entityManager->expects($this->once())
|
||||
->method('detach')
|
||||
->with($this->callback(function ($downloadDetach) use (&$download) {
|
||||
$this->assertSame($downloadDetach, $download);
|
||||
|
||||
return true;
|
||||
}));
|
||||
|
||||
$this->assetModel->trackDownload($asset);
|
||||
|
||||
$this->assertEquals('test_utm_campaign', $download->getUtmCampaign());
|
||||
$this->assertEquals('test_utm_content', $download->getUtmContent());
|
||||
$this->assertEquals('test_utm_medium', $download->getUtmMedium());
|
||||
$this->assertEquals('test_utm_source', $download->getUtmSource());
|
||||
$this->assertEquals('test_utm_term', $download->getUtmTerm());
|
||||
$this->assertEquals('200', $download->getCode());
|
||||
$this->assertEquals($ipAddress, $download->getIpAddress());
|
||||
$this->assertEquals($lead, $download->getLead());
|
||||
$this->assertEquals($asset, $download->getAsset());
|
||||
$this->assertEquals('http://localhost', $download->getReferer());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mautic.asset.asset.error.notfound="No asset with an id of %id% was found!"
|
||||
mautic.asset.asset.notice.batch_deleted="%count% assets have been deleted!"
|
||||
@@ -0,0 +1,127 @@
|
||||
mautic.asset.abtest.criteria="Asset Stats"
|
||||
mautic.asset.abtest.criteria.downloads="Download rate"
|
||||
mautic.asset.abtest.downloads="%count% downloads"
|
||||
mautic.asset.abtest.label.downloads="Number of downloads"
|
||||
mautic.asset.abtest.label.hits="Number of unique asset downloads"
|
||||
mautic.asset.abtest.label.sentemils="Number of sent emails"
|
||||
mautic.asset.actions="Asset actions"
|
||||
mautic.asset.asset="Asset"
|
||||
mautic.asset.click_to_zoom="Click on the image to zoom"
|
||||
mautic.asset.click_to_view_full_size="View full size"
|
||||
mautic.asset.no_preview="No preview available"
|
||||
mautic.asset.open_preview="Open preview"
|
||||
mautic.protip.assets.gating="Gate valuable assets behind a form to capture lead information before allowing the download."
|
||||
mautic.protip.assets.naming="Use consistent naming conventions to keep assets organized"
|
||||
mautic.protip.assets.repurpose="Repurpose high-performing assets across different channels and campaigns"
|
||||
mautic.protip.assets.track="Track asset performance metrics in the dashboard widgets to identify top-performing content"
|
||||
mautic.asset.asset.searchcommand.lang="lang"
|
||||
mautic.asset.asset.searchcommand.lang.description="Filters assets by a specific language code"
|
||||
mautic.asset.asset.searchcommand.isexpired.description="Filters for assets that have passed their expiration date"
|
||||
mautic.asset.asset.searchcommand.isexpired="is:expired"
|
||||
mautic.asset.asset.searchcommand.ispending.description="Filters for assets scheduled for future availability"
|
||||
mautic.asset.asset.searchcommand.ispending="is:pending"
|
||||
mautic.asset.asset.downloads.total="Total downloads"
|
||||
mautic.asset.asset.downloads.unique="Unique downloads"
|
||||
mautic.asset.asset.downloads.total.all_time="All downloads over time since this asset was created"
|
||||
mautic.asset.asset.downloads.unique.all_time="Unlike total downloads, this counts each contact only once"
|
||||
mautic.asset.asset.error.missing.remote.path="A remote URL must be specified when remote storage is selected."
|
||||
mautic.asset.asset.form.confirmbatchdelete="Delete the selected assets?"
|
||||
mautic.asset.asset.form.confirmdelete="Delete the asset, %name%?"
|
||||
mautic.asset.asset.form.file.upload="Upload a file (max filesize allowed = %max%)"
|
||||
mautic.asset.asset.form.language.help="Select language of the asset."
|
||||
mautic.asset.asset.form.remotePath="Remote URL"
|
||||
mautic.asset.asset.form.storageLocation="Storage Location"
|
||||
mautic.asset.asset.form.storageLocation.local="Local"
|
||||
mautic.asset.asset.form.storageLocation.remote="Remote"
|
||||
mautic.asset.asset.form.disallow.crawlers="Block search engines from indexing this file"
|
||||
mautic.asset.asset.form.disallow.crawlers.descr="If you don't want to index files like PDF, DOCX etc, this option will disallow search bots by using the X-Robots-Tag HTTP header."
|
||||
mautic.asset.asset.help.alias="Letters and numbers (hyphens allowed) used for URL generation of this asset. A unique alias based on the title will be autogenerated if left empty."
|
||||
mautic.asset.asset.menu.edit="Edit Asset"
|
||||
mautic.asset.asset.menu.new="New Asset"
|
||||
mautic.asset.asset.preview="Preview"
|
||||
mautic.asset.asset.size="Filesize"
|
||||
mautic.asset.asset.submitaction.downloadfile="Download an asset"
|
||||
mautic.asset.asset.submitaction.downloadfile.msg="<br />Your download should start within 5 seconds. If it does not, <a href='%url%'>click here</a>.<script>setTimeout(function(){window.location='%url%';}, 5000);</script>"
|
||||
mautic.asset.asset.submitaction.downloadfile_descr="Download the selected asset upon submitting the form."
|
||||
mautic.asset.asset.thead.download.count="Download count"
|
||||
mautic.asset.asset.url="Download URL"
|
||||
mautic.asset.assets="Assets"
|
||||
mautic.asset.campaign.event.assets="Limit to Assets"
|
||||
mautic.asset.campaign.event.assets.descr="Select the assets this trigger applies to. If none are selected, the event will trigger for any asset."
|
||||
mautic.asset.campaign.event.download="Downloads asset"
|
||||
mautic.asset.campaign.event.download_descr="Trigger actions upon downloading an asset."
|
||||
mautic.asset.config.form.allowed.extensions="Allowed file extensions"
|
||||
mautic.asset.config.form.allowed.extensions.tooltip="Comma separated list of file extensions. Only files with specified file extensions will be able to upload."
|
||||
mautic.asset.config.form.upload.dir="Path to the asset directory"
|
||||
mautic.asset.config.form.upload.dir.tooltip="Set the absolute path to where assets should be uploaded to. %kernel.project_dir%/app can be used as a placeholder for the app directory in the public web root. It is advised to use a directory outside of the public web root to prevent assets from being accessible by the public."
|
||||
mautic.asset.config.form.max.size="Maximum size (MB)"
|
||||
mautic.asset.config.form.max.size.tooltip="Set the maximum size of uploaded assets in MB (Megabytes). Default value is 6MB."
|
||||
mautic.asset.downloadcount="Download count"
|
||||
mautic.asset.drop.file.here="Drop the file here or click to browse and select the file."
|
||||
mautic.asset.error.file.failed="File failed to upload."
|
||||
mautic.asset.event.download="Asset downloaded"
|
||||
mautic.asset.filename.local="Local filename"
|
||||
mautic.asset.filename.original="Original filename"
|
||||
mautic.asset.filename.remote="Remote filename"
|
||||
mautic.asset.form.submit.assets="Asset"
|
||||
mautic.asset.form.submit.assets_descr="Choose the asset to be downloaded."
|
||||
mautic.asset.form.submit.latest.category="Use the latest asset from the category"
|
||||
mautic.asset.form.submit.latest.category_descr="If 'Use latest file from the category' Asset option is selected then the action will download the latest asset from the selected category."
|
||||
mautic.asset.graph.line.downloads="Downloads"
|
||||
mautic.asset.graph.pie.statuses="Download HTTP statuses"
|
||||
mautic.asset.no_audio_support="Your browser does not support audio."
|
||||
mautic.asset.no_video_support="Your browser does not support video."
|
||||
mautic.asset.noresults.tip="Assets can be white papers, PDFs, images, docs, eBooks, or pretty much any electronic document that you want to distribute to customers. Want to offer an asset after a customer has submitted a form? Easy! Add the 'Download an asset' action when building the form."
|
||||
mautic.asset.permissions.assets="Assets - User has access to"
|
||||
mautic.asset.permissions.header="Asset Permissions"
|
||||
mautic.asset.onboarding.heading="Share & monitor your digital resources"
|
||||
mautic.asset.onboarding.subheading="Assets are downloadable files – like PDFs, ebooks, guides, or videos – that you offer to your contacts."
|
||||
mautic.asset.onboarding.copy="Use them to deliver value, generate leads, and understand your audience's interests based on what they choose to download."
|
||||
mautic.asset.point.action.assets="Limit to the selected assets"
|
||||
mautic.asset.point.action.assets.descr="Select the assets this action applies to. If none are selected, it'll apply to any asset."
|
||||
mautic.asset.point.action.download="Downloads an asset"
|
||||
mautic.asset.point.action.download_descr="Update the contact's points when an asset is downloaded."
|
||||
mautic.asset.remote.file.browse="Browse Remote Files"
|
||||
mautic.asset.remote.no_results="No remote files found."
|
||||
mautic.asset.remote.select_service="Select the service to the left. If none are listed, configure the Cloud Storage addon."
|
||||
mautic.asset.report.download.code="Response code"
|
||||
mautic.asset.report.download.date_download="Date downloaded"
|
||||
mautic.asset.report.download_count="Download count"
|
||||
mautic.asset.report.downloads.table="Asset Downloads"
|
||||
mautic.asset.report.unique_download_count="Unique download count"
|
||||
mautic.asset.stage.action.download="Download asset"
|
||||
mautic.asset.table.most.downloaded="Most downloaded assets"
|
||||
mautic.asset.table.top.referrers="Top referrers"
|
||||
mautic.campaign.asset.download="Asset downloaded"
|
||||
mautic.config.AssetBundle.upload_dir="Upload Directory"
|
||||
mautic.config.tab.assetconfig="Asset Settings"
|
||||
mautic.core.config.header.assetconfig.description="Manage file storage, size limits, and allowed file types for uploaded assets."
|
||||
mautic.asset.dashboard.widgets="Asset Widgets"
|
||||
mautic.asset.unique="Unique"
|
||||
mautic.asset.repetitive="Repetitive"
|
||||
mautic.widget.asset.downloads.in.time="Downloads in time"
|
||||
mautic.widget.unique.vs.repetitive.downloads="Unique vs repetitive downloads"
|
||||
mautic.widget.popular.assets="Popular assets"
|
||||
mautic.widget.created.assets="Created assets"
|
||||
mautic.report.group.assets="Assets"
|
||||
mautic.asset.asset.help.searchcommands="<strong>Search commands</strong><br />ids:ID1,ID2 (comma separated IDs, no spaces)<br />is:mine<br />is:published<br />is:unpublished<br />is:expired<br />is:pending<br />name:*<br />is:uncategorized<br />category:{category alias}<br />project:\"Project Name\""
|
||||
mautic.asset.asset.error.file.mimetype="The file type is not allowed."
|
||||
mautic.asset.type.excel="Excel"
|
||||
mautic.asset.type.word="Word"
|
||||
mautic.asset.type.pdf="PDF"
|
||||
mautic.asset.type.audio="Audio"
|
||||
mautic.asset.type.zip="Zip"
|
||||
mautic.asset.type.image="Image"
|
||||
mautic.asset.type.code="Code"
|
||||
mautic.asset.type.text="Text"
|
||||
mautic.asset.type.ppt="Presentation"
|
||||
mautic.asset.type.video="Video"
|
||||
mautic.asset.type.fallback="File"
|
||||
mautic.asset.tag.disallow.label="Block search engines from indexing this file"
|
||||
mautic.asset.tag.storage.local="Local storage"
|
||||
mautic.asset.tag.storage.remote="Remote storage"
|
||||
mautic.asset.tag.language="Language: %lang%"
|
||||
mautic.report.source.assets="Assets"
|
||||
mautic.report.source.asset.downloads="Asset Downloads"
|
||||
mautic.asset.preview.ariaLabel="Preview for %title%"
|
||||
mautic.asset.preview.pdf_iframe_title = "PDF preview for %title%"
|
||||
@@ -0,0 +1,9 @@
|
||||
mautic.asset.asset.error.missing.file="A file must be uploaded before Asset is saved when local storage is selected."
|
||||
mautic.asset.asset.error.missing.title="Assset Title is required."
|
||||
mautic.asset.asset.error.missing.remote.path="A remote URL must be specified when remote storage is selected."
|
||||
mautic.asset.asset.error.file.size="Upload failed as the file is %fileSize% MB which exceeds the maximum allowed file size of %maxSize% MB. This setting can be changed in the Configuration."
|
||||
mautic.asset.asset.error.file.extension="Upload failed as the file extension, %fileExtension%, is not in the list of allowed extensions (%extensions%). This setting can be changed in the Configuration."
|
||||
mautic.asset.asset.error.file.extension.js="Upload failed as the file extension is not in the list of allowed extensions (%extensions%). This setting can be changed in the Configuration."
|
||||
mautic.asset.validation.error.url="The remote should be a valid URL."
|
||||
mautic.asset.asset.error.file.mimetype="Upload failed as the file mimetype, %fileMimetype% is not allowed. Allowed file types are %mimetypes%."
|
||||
mautic.asset.asset.error.invalid.mimetype="Upload failed as the file mimetype, %fileMimetype% is not allowed. Allowed file types are %mimetypes%."
|
||||
Reference in New Issue
Block a user