Spaces:
No application file
No application file
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\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 | |
{ | |
/** | |
* @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 string $code | |
* @param array $systemEntry | |
* | |
* @throws \Doctrine\ORM\ORMException | |
* @throws \Exception | |
*/ | |
public function trackDownload($asset, $request = null, $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(); | |
} | |
$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($asset) && 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); | |
if (null !== $request) { | |
$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->execute()->fetchAllAssociative(); | |
} | |
/** | |
* Get a list of assets in a date range. | |
* | |
* @param int $limit | |
* @param array $filters | |
* @param array $options | |
* | |
* @return array | |
*/ | |
public function getAssetList($limit = 10, \DateTime $dateFrom = null, \DateTime $dateTo = null, $filters = [], $options = []) | |
{ | |
$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->execute()->fetchAllAssociative(); | |
} | |
} | |