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(); | |
| } | |
| } | |