diff --git a/README.md b/README.md index 0b0757264a1..b6d85da3f20 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ Installing from source is only recommended if you are comfortable using the comm ## Requirements +#### Contributors Agreement + +By contributing to this project, you accept and agree to the [Contributors Agreement](https://www.mautic.org/contributors-agreement) in its entirety. + #### Development / Build process requirements 1. Mautic uses Git as a version control system. Download and install git for your OS from https://git-scm.com/. diff --git a/app/AppKernel.php b/app/AppKernel.php index 604dbdd31af..72e00f6c9ff 100644 --- a/app/AppKernel.php +++ b/app/AppKernel.php @@ -41,7 +41,7 @@ class AppKernel extends Kernel * * @const integer */ - const PATCH_VERSION = 1; + const PATCH_VERSION = 2; /** * Extra version identifier. @@ -527,7 +527,9 @@ protected function initializeContainer() // Warm up the cache if classes.php is missing or in dev mode if (!$fresh && $this->container->has('cache_warmer')) { - $this->container->get('cache_warmer')->warmUp($this->container->getParameter('kernel.cache_dir')); + $warmer = $this->container->get('cache_warmer'); + $warmer->enableOptionalWarmers(); + $warmer->warmUp($this->container->getParameter('kernel.cache_dir')); } } diff --git a/app/bundles/CampaignBundle/Assets/js/campaign.js b/app/bundles/CampaignBundle/Assets/js/campaign.js index b3778bccea8..4c63543e844 100644 --- a/app/bundles/CampaignBundle/Assets/js/campaign.js +++ b/app/bundles/CampaignBundle/Assets/js/campaign.js @@ -1868,21 +1868,23 @@ Mautic.campaignBuilderValidateConnection = function (epDetails, targetType, targ */ Mautic.updateScheduledCampaignEvent = function(eventId, contactId) { // Convert scheduled date/time to an input - mQuery('#timeline-campaign-event-'+eventId+' .btn-edit').prop('disabled', true).addClass('disabled'); + mQuery('#timeline-campaign-event-'+eventId+' .btn-reschedule').addClass('disabled'); var converting = false; var eventWrapper = '#timeline-campaign-event-'+eventId; var eventSpan = '.timeline-campaign-event-date-'+eventId; var eventText = '#timeline-campaign-event-text-'+eventId; + var saveButton = '#timeline-campaign-event-save-' + eventId; var originalDate = mQuery(eventWrapper+' '+eventSpan).first().text(); var revertInput = function(input) { converting = true; mQuery(input).datetimepicker('destroy'); mQuery(eventSpan).text(originalDate); - mQuery(eventWrapper+' .btn').prop('disabled', false).removeClass('disabled'); + mQuery(eventWrapper+' .btn-reschedule').removeClass('disabled'); }; var date = mQuery(eventSpan).attr('data-date'); + mQuery(saveButton).show(); var input = mQuery('') .css('height', '20px') .css('color', '#000000') @@ -1902,8 +1904,8 @@ Mautic.updateScheduledCampaignEvent = function(eventId, contactId) { originalDate: date }, function (response) { mQuery(eventSpan).text(response.formattedDate); - mQuery(eventSpan).attr('data-data', response.date); - mQuery(eventWrapper+' .btn').prop('disabled', false).removeClass('disabled'); + mQuery(eventSpan).attr('data-date', response.date); + mQuery(eventWrapper+' .btn-reschedule').removeClass('disabled'); if (response.success) { mQuery(eventText).removeClass('text-warning').addClass('text-info'); @@ -1911,21 +1913,63 @@ Mautic.updateScheduledCampaignEvent = function(eventId, contactId) { mQuery('.fa.timeline-campaign-event-cancelled-'+eventId).remove(); mQuery('.timeline-campaign-event-scheduled-'+eventId).removeClass('hide'); mQuery('.timeline-campaign-event-cancelled-'+eventId).addClass('hide'); + mQuery(saveButton).hide(); } }, false ); } else if (code == 27) { e.preventDefault(); revertInput(input); + mQuery(saveButton).hide(); } }) .on('blur', function (e) { if (!converting) { revertInput(input); } + mQuery(saveButton).hide(); }); mQuery('#timeline-campaign-event-'+eventId+' '+eventSpan).html(input); Mautic.activateDateTimeInputs('#timeline-reschedule'); + mQuery('#timeline-reschedule').focus(); +}; + +/** + * + * @param eventId + * @param contactId + */ +Mautic.saveScheduledCampaignEvent = function (eventId, contactId) { + var saveButton = '#timeline-campaign-event-save-' + eventId; + mQuery(saveButton).addClass('disabled'); + + // Convert scheduled date/time to an input + var eventWrapper = '#timeline-campaign-event-' + eventId; + var eventSpan = '.timeline-campaign-event-date-' + eventId; + var eventText = '#timeline-campaign-event-text-' + eventId; + + var date = mQuery(eventSpan).attr('data-date'); + Mautic.ajaxActionRequest('campaign:updateScheduledCampaignEvent', + { + eventId: eventId, + contactId: contactId, + date: mQuery('#timeline-reschedule').val(), + originalDate: date + }, function (response) { + mQuery(eventSpan).text(response.formattedDate); + mQuery(eventSpan).attr('data-date', response.date); + + if (response.success) { + mQuery(eventText).removeClass('text-warning').addClass('text-info'); + mQuery(eventSpan).css('textDecoration', 'inherit'); + mQuery('.fa.timeline-campaign-event-cancelled-' + eventId).remove(); + mQuery('.timeline-campaign-event-scheduled-' + eventId).removeClass('hide'); + mQuery('.timeline-campaign-event-cancelled-' + eventId).addClass('hide'); + } + + mQuery(saveButton).removeClass('disabled').hide(); + mQuery(eventWrapper + ' .btn-reschedule').removeClass('disabled'); + }, false); }; /** diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index c9edeb46f38..a052fd486f0 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -274,6 +274,7 @@ 'mautic.campaign.model.event', 'mautic.campaign.model.campaign', 'mautic.helper.ip_lookup', + 'mautic.campaign.scheduler', ], ], ], diff --git a/app/bundles/CampaignBundle/Controller/CampaignController.php b/app/bundles/CampaignBundle/Controller/CampaignController.php index bed237cecfd..7563ec3a290 100644 --- a/app/bundles/CampaignBundle/Controller/CampaignController.php +++ b/app/bundles/CampaignBundle/Controller/CampaignController.php @@ -680,33 +680,54 @@ protected function getViewArguments(array $args, $action) $dateRangeForm = $this->get('form.factory')->create('daterange', $dateRangeValues, ['action' => $action]); /** @var LeadEventLogRepository $eventLogRepo */ - $eventLogRepo = $this->getDoctrine()->getManager()->getRepository('MauticCampaignBundle:LeadEventLog'); - $events = $this->getCampaignModel()->getEventRepository()->getCampaignEvents($entity->getId()); - $leadCount = $this->getCampaignModel()->getRepository()->getCampaignLeadCount($entity->getId()); - $campaignLogCounts = $eventLogRepo->getCampaignLogCounts($entity->getId(), false, false); - $sortedEvents = [ + $eventLogRepo = $this->getDoctrine()->getManager()->getRepository('MauticCampaignBundle:LeadEventLog'); + $events = $this->getCampaignModel()->getEventRepository()->getCampaignEvents($entity->getId()); + $leadCount = $this->getCampaignModel()->getRepository()->getCampaignLeadCount($entity->getId()); + $campaignLogCounts = $eventLogRepo->getCampaignLogCounts($entity->getId(), false, false, true); + $pendingCampaignLogCounts = $eventLogRepo->getCampaignLogCounts($entity->getId(), false, false); + $sortedEvents = [ 'decision' => [], 'action' => [], 'condition' => [], ]; - - foreach ($events as $event) { - $event['logCount'] = - $event['percent'] = - $event['yesPercent'] = - $event['noPercent'] = 0; - $event['leadCount'] = $leadCount; + foreach ($events as &$event) { + $event['logCount'] = + $event['logCountForPending'] = + $event['percent'] = + $event['yesPercent'] = + $event['noPercent'] = 0; + $event['leadCount'] = $leadCount; if (isset($campaignLogCounts[$event['id']])) { - $event['logCount'] = array_sum($campaignLogCounts[$event['id']]); + $event['logCount'] = array_sum($campaignLogCounts[$event['id']]); + $event['logCountForPending'] = isset($pendingCampaignLogCounts[$event['id']]) ? array_sum($pendingCampaignLogCounts[$event['id']]) : 0; + $pending = $event['leadCount'] - $event['logCountForPending']; + $totalYes = $campaignLogCounts[$event['id']][1]; + $totalNo = $campaignLogCounts[$event['id']][0]; + $total = $totalYes + $totalNo + $pending; if ($leadCount) { - $event['percent'] = round(($event['logCount'] / $leadCount) * 100, 1); - $event['yesPercent'] = round(($campaignLogCounts[$event['id']][1] / $leadCount) * 100, 1); - $event['noPercent'] = round(($campaignLogCounts[$event['id']][0] / $leadCount) * 100, 1); + $event['percent'] = round(($event['logCount'] / $total) * 100, 1); + $event['yesPercent'] = round(($campaignLogCounts[$event['id']][1] / $total) * 100, 1); + $event['noPercent'] = round(($campaignLogCounts[$event['id']][0] / $total) * 100, 1); } } + } + // rewrite stats data from parent condition if exist + foreach ($events as &$event) { + if (!empty($event['decisionPath']) && !empty($event['parent_id']) && isset($events[$event['parent_id']])) { + $parentEvent = $events[$event['parent_id']]; + $event['logCountForPending'] = $parentEvent['logCountForPending']; + $event['percent'] = $parentEvent['percent']; + $event['yesPercent'] = $parentEvent['yesPercent']; + $event['noPercent'] = $parentEvent['noPercent']; + if ($event['decisionPath'] == 'yes') { + $event['noPercent'] = 0; + } else { + $event['yesPercent'] = 0; + } + } $sortedEvents[$event['eventType']][] = $event; } diff --git a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php index 54cd3b2196d..7f2a3ac01f4 100644 --- a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php +++ b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php @@ -215,19 +215,26 @@ public function getUpcomingEvents(array $options = null) /** * @param $campaignId * @param bool $excludeScheduled + * @param bool $excludeNegative + * @param bool $all * * @return array */ - public function getCampaignLogCounts($campaignId, $excludeScheduled = false, $excludeNegative = true) + public function getCampaignLogCounts($campaignId, $excludeScheduled = false, $excludeNegative = true, $all = false) { $q = $this->getSlaveConnection()->createQueryBuilder() - ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'o') - ->innerJoin( - 'o', - MAUTIC_TABLE_PREFIX.'campaign_leads', - 'l', - 'l.campaign_id = '.(int) $campaignId.' and l.manually_removed = 0 and o.lead_id = l.lead_id and l.rotation = o.rotation' - ); + ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'o'); + + $join = 'innerJoin'; + if ($all === true) { + $join = 'leftJoin'; + } + $q->$join( + 'o', + MAUTIC_TABLE_PREFIX.'campaign_leads', + 'l', + 'l.campaign_id = '.(int) $campaignId.' and l.manually_removed = 0 and o.lead_id = l.lead_id and l.rotation = o.rotation' + ); $expr = $q->expr()->andX( $q->expr()->eq('o.campaign_id', (int) $campaignId) diff --git a/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php b/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php index 3525bda3b3c..7f89fc9c382 100644 --- a/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php +++ b/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php @@ -233,8 +233,8 @@ public function wasLogUpdatedByListener() } /** - * @param $channel - * @param null $channelId + * @param string $channel + * @param string|int|null $channelId * * @return $this */ diff --git a/app/bundles/CampaignBundle/Model/CampaignModel.php b/app/bundles/CampaignBundle/Model/CampaignModel.php index d39c6a7431c..de95b2362be 100644 --- a/app/bundles/CampaignBundle/Model/CampaignModel.php +++ b/app/bundles/CampaignBundle/Model/CampaignModel.php @@ -653,7 +653,7 @@ public function saveCampaignLead(CampaignLead $campaignLead) return true; } catch (\Exception $exception) { - $this->logger->log('error', $exception->getMessage()); + $this->logger->log('error', $exception->getMessage(), ['exception' => $exception]); return false; } diff --git a/app/bundles/CampaignBundle/Model/EventLogModel.php b/app/bundles/CampaignBundle/Model/EventLogModel.php index 2378705bd84..d337d50a79a 100644 --- a/app/bundles/CampaignBundle/Model/EventLogModel.php +++ b/app/bundles/CampaignBundle/Model/EventLogModel.php @@ -11,10 +11,9 @@ namespace Mautic\CampaignBundle\Model; -use Mautic\CampaignBundle\CampaignEvents; use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLog; -use Mautic\CampaignBundle\Event\CampaignScheduledEvent; +use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\CoreBundle\Helper\InputHelper; use Mautic\CoreBundle\Helper\IpLookupHelper; use Mautic\CoreBundle\Model\AbstractCommonModel; @@ -40,6 +39,11 @@ class EventLogModel extends AbstractCommonModel */ protected $ipLookupHelper; + /** + * @var EventScheduler + */ + protected $eventScheduler; + /** * EventLogModel constructor. * @@ -47,11 +51,12 @@ class EventLogModel extends AbstractCommonModel * @param CampaignModel $campaignModel * @param IpLookupHelper $ipLookupHelper */ - public function __construct(EventModel $eventModel, CampaignModel $campaignModel, IpLookupHelper $ipLookupHelper) + public function __construct(EventModel $eventModel, CampaignModel $campaignModel, IpLookupHelper $ipLookupHelper, EventScheduler $eventScheduler) { $this->eventModel = $eventModel; $this->campaignModel = $campaignModel; $this->ipLookupHelper = $ipLookupHelper; + $this->eventScheduler = $eventScheduler; } /** @@ -198,26 +203,16 @@ public function updateContactEvent(Event $event, Lead $contact, array $parameter } /** - * @param $entity + * @param LeadEventLog $entity */ public function saveEntity(LeadEventLog $entity) { - $eventSettings = $this->campaignModel->getEvents(); - if ($this->dispatcher->hasListeners(CampaignEvents::ON_EVENT_SCHEDULED)) { - $event = $entity->getEvent(); - $args = [ - 'eventSettings' => $eventSettings[$event->getEventType()][$event->getType()], - 'eventDetails' => null, - 'event' => $event->convertToArray(), - 'lead' => $entity->getLead(), - 'systemTriggered' => false, - 'dateScheduled' => $entity->getTriggerDate(), - ]; - - $scheduledEvent = new CampaignScheduledEvent($args, $entity); - $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_SCHEDULED, $scheduledEvent); + $triggerDate = $entity->getTriggerDate(); + if (null === $triggerDate) { + // Reschedule for now + $triggerDate = new \DateTime(); } - $this->getRepository()->saveEntity($entity); + $this->eventScheduler->reschedule($entity, $triggerDate); } } diff --git a/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql b/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql index 9cab8235b2a..4bdbab62f39 100644 --- a/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql +++ b/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql @@ -39,9 +39,9 @@ VALUES (15,1,3,'Tag EmailNotOpen Again',NULL,'lead.changetags','action',3,'a:16:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:4:\"1612\";s:8:\"droppedY\";s:3:\"374\";}s:4:\"name\";s:22:\"Tag EmailNotOpen Again\";s:11:\"triggerMode\";s:8:\"interval\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"6\";s:19:\"triggerIntervalUnit\";s:1:\"i\";s:6:\"anchor\";s:2:\"no\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"9\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:8:\"decision\";s:10:\"campaignId\";s:1:\"1\";s:6:\"_token\";s:43:\"Wd8bGtv2HJ6Nyf3K90Efoo2Rn2VkDWwXhwzCIPMiD-M\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"add_tags\";a:1:{i:0;s:12:\"EmailNotOpen\";}s:11:\"remove_tags\";a:0:{}}',NULL,6,'i','interval','no','newf16dfec5f2a65aa9c527675e7be516020a90daa6',NULL,NULL), (16,1,12,'Tag ChainedAction',NULL,'lead.changetags','action',4,'a:16:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:3:\"168\";s:8:\"droppedY\";s:3:\"439\";}s:4:\"name\";s:14:\"Chained Action\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:6:\"bottom\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:2:\"10\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:6:\"action\";s:10:\"campaignId\";s:1:\"1\";s:6:\"_token\";s:43:\"6xgHe74aRnc1V7AGzdang3-iJ0Ub5BKfbdU5NsxQmv0\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"add_tags\";a:1:{i:0;s:13:\"ChainedAction\";}s:11:\"remove_tags\";a:0:{}}',NULL,1,'d','immediate',NULL,'new60f74507aeccf217f78647e41ae29af51debe666',NULL,NULL); -INSERT INTO `#__lead_lists` (`id`,`is_published`,`date_added`,`created_by`,`created_by_user`,`date_modified`,`modified_by`,`modified_by_user`,`checked_out`,`checked_out_by`,`checked_out_by_user`,`name`,`description`,`alias`,`filters`,`is_global`) +INSERT INTO `#__lead_lists` (`id`,`is_preference_center`, `is_published`,`date_added`,`created_by`,`created_by_user`,`date_modified`,`modified_by`,`modified_by_user`,`checked_out`,`checked_out_by`,`checked_out_by_user`,`name`,`description`,`alias`,`filters`,`is_global`) VALUES - (1,1,'2018-01-04 23:41:20',1,'Admin User',NULL,NULL,NULL,NULL,NULL,NULL,'Campaign Test',NULL,'campaign-test','a:0:{}',1); + (1,0,1,'2018-01-04 23:41:20',1,'Admin User',NULL,NULL,NULL,NULL,NULL,NULL,'Campaign Test',NULL,'campaign-test','a:0:{}',1); INSERT INTO `#__campaign_leadlist_xref` (`campaign_id`,`leadlist_id`) VALUES diff --git a/app/bundles/CampaignBundle/Translations/en_US/messages.ini b/app/bundles/CampaignBundle/Translations/en_US/messages.ini index 88324607b26..09e305bcf28 100644 --- a/app/bundles/CampaignBundle/Translations/en_US/messages.ini +++ b/app/bundles/CampaignBundle/Translations/en_US/messages.ini @@ -48,6 +48,7 @@ mautic.campaign.event.timed.choice.custom="Custom" mautic.campaign.event.leadchange="contact changed campaigns" mautic.campaign.event.leadchange_descr="Trigger actions when a contact is added/removed from a campaign." mautic.campaign.event.reschedule="Reschedule this event." +mautic.campaign.event.save="Save" mautic.campaign.event.cancel="Cancel this event (it can be rescheduled later)." mautic.campaign.event.cancelled="This event has been cancelled. Reschedule it to restore." mautic.campaign.event.cancelled.time="This event was scheduled for %date% but has been cancelled." diff --git a/app/bundles/CampaignBundle/Views/Campaign/events.html.php b/app/bundles/CampaignBundle/Views/Campaign/events.html.php index c4e3dd26d39..9f8d0fcf1f5 100644 --- a/app/bundles/CampaignBundle/Views/Campaign/events.html.php +++ b/app/bundles/CampaignBundle/Views/Campaign/events.html.php @@ -29,7 +29,7 @@ - +
diff --git a/app/bundles/CampaignBundle/Views/SubscribedEvents/Timeline/index.html.php b/app/bundles/CampaignBundle/Views/SubscribedEvents/Timeline/index.html.php index 63297031a47..830c26d80b4 100644 --- a/app/bundles/CampaignBundle/Views/SubscribedEvents/Timeline/index.html.php +++ b/app/bundles/CampaignBundle/Views/SubscribedEvents/Timeline/index.html.php @@ -48,7 +48,10 @@ hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $lead->getPermissionUser())): ?> - + ' + + '
' + + '
' + + '
'), + calendar = $('
'), + timepicker = $('
'), + timeboxparent = timepicker.find('.xdsoft_time_box').eq(0), + timebox = $('
'), + applyButton = $(''), + + monthselect = $('
'), + yearselect = $('
'), + triggerAfterOpen = false, + XDSoft_datetime, + + xchangeTimer, + timerclick, + current_time_index, + setPos, + timer = 0, + _xdsoft_datetime, + forEachAncestorOf; + + if (options.id) { + datetimepicker.attr('id', options.id); + } + if (options.style) { + datetimepicker.attr('style', options.style); + } + if (options.weeks) { + datetimepicker.addClass('xdsoft_showweeks'); + } + if (options.rtl) { + datetimepicker.addClass('xdsoft_rtl'); + } + + datetimepicker.addClass('xdsoft_' + options.theme); + datetimepicker.addClass(options.className); + + month_picker + .find('.xdsoft_month span') + .after(monthselect); + month_picker + .find('.xdsoft_year span') + .after(yearselect); + + month_picker + .find('.xdsoft_month,.xdsoft_year') + .on('touchstart mousedown.xdsoft', function (event) { + var select = $(this).find('.xdsoft_select').eq(0), + val = 0, + top = 0, + visible = select.is(':visible'), + items, + i; + + month_picker + .find('.xdsoft_select') + .hide(); + if (_xdsoft_datetime.currentTime) { + val = _xdsoft_datetime.currentTime[$(this).hasClass('xdsoft_month') ? 'getMonth' : 'getFullYear'](); + } + + select[visible ? 'hide' : 'show'](); + for (items = select.find('div.xdsoft_option'), i = 0; i < items.length; i += 1) { + if (items.eq(i).data('value') === val) { + break; + } else { + top += items[0].offsetHeight; + } + } + + select.xdsoftScroller(options, top / (select.children()[0].offsetHeight - (select[0].clientHeight))); + event.stopPropagation(); + return false; + }); + + var handleTouchMoved = function (event) { + var evt = event.originalEvent; + var touchPosition = evt.touches ? evt.touches[0] : evt; + this.touchStartPosition = this.touchStartPosition || touchPosition; + var xMovement = Math.abs(this.touchStartPosition.clientX - touchPosition.clientX); + var yMovement = Math.abs(this.touchStartPosition.clientY - touchPosition.clientY); + var distance = Math.sqrt(xMovement * xMovement + yMovement * yMovement); + if(distance > options.touchMovedThreshold) { + this.touchMoved = true; + } + } + + month_picker + .find('.xdsoft_select') + .xdsoftScroller(options) + .on('touchstart mousedown.xdsoft', function (event) { + var evt = event.originalEvent; + this.touchMoved = false; + this.touchStartPosition = evt.touches ? evt.touches[0] : evt; + event.stopPropagation(); + event.preventDefault(); + }) + .on('touchmove', '.xdsoft_option', handleTouchMoved) + .on('touchend mousedown.xdsoft', '.xdsoft_option', function () { + if (!this.touchMoved) { + if (_xdsoft_datetime.currentTime === undefined || _xdsoft_datetime.currentTime === null) { + _xdsoft_datetime.currentTime = _xdsoft_datetime.now(); + } + + var year = _xdsoft_datetime.currentTime.getFullYear(); + if (_xdsoft_datetime && _xdsoft_datetime.currentTime) { + _xdsoft_datetime.currentTime[$(this).parent().parent().hasClass('xdsoft_monthselect') ? 'setMonth' : 'setFullYear']($(this).data('value')); + } + + $(this).parent().parent().hide(); + + datetimepicker.trigger('xchange.xdsoft'); + if (options.onChangeMonth && $.isFunction(options.onChangeMonth)) { + options.onChangeMonth.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); + } + + if (year !== _xdsoft_datetime.currentTime.getFullYear() && $.isFunction(options.onChangeYear)) { + options.onChangeYear.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); + } + } + }); + + datetimepicker.getValue = function () { + return _xdsoft_datetime.getCurrentTime(); + }; + + datetimepicker.setOptions = function (_options) { + var highlightedDates = {}; + + options = $.extend(true, {}, options, _options); + + if (_options.allowTimes && $.isArray(_options.allowTimes) && _options.allowTimes.length) { + options.allowTimes = $.extend(true, [], _options.allowTimes); + } + + if (_options.weekends && $.isArray(_options.weekends) && _options.weekends.length) { + options.weekends = $.extend(true, [], _options.weekends); + } + + if (_options.allowDates && $.isArray(_options.allowDates) && _options.allowDates.length) { + options.allowDates = $.extend(true, [], _options.allowDates); + } + + if (_options.allowDateRe && Object.prototype.toString.call(_options.allowDateRe)==="[object String]") { + options.allowDateRe = new RegExp(_options.allowDateRe); + } + + if (_options.highlightedDates && $.isArray(_options.highlightedDates) && _options.highlightedDates.length) { + $.each(_options.highlightedDates, function (index, value) { + var splitData = $.map(value.split(','), $.trim), + exDesc, + hDate = new HighlightedDate(dateHelper.parseDate(splitData[0], options.formatDate), splitData[1], splitData[2]), // date, desc, style + keyDate = dateHelper.formatDate(hDate.date, options.formatDate); + if (highlightedDates[keyDate] !== undefined) { + exDesc = highlightedDates[keyDate].desc; + if (exDesc && exDesc.length && hDate.desc && hDate.desc.length) { + highlightedDates[keyDate].desc = exDesc + "\n" + hDate.desc; + } + } else { + highlightedDates[keyDate] = hDate; + } + }); + + options.highlightedDates = $.extend(true, [], highlightedDates); + } + + if (_options.highlightedPeriods && $.isArray(_options.highlightedPeriods) && _options.highlightedPeriods.length) { + highlightedDates = $.extend(true, [], options.highlightedDates); + $.each(_options.highlightedPeriods, function (index, value) { + var dateTest, // start date + dateEnd, + desc, + hDate, + keyDate, + exDesc, + style; + if ($.isArray(value)) { + dateTest = value[0]; + dateEnd = value[1]; + desc = value[2]; + style = value[3]; + } + else { + var splitData = $.map(value.split(','), $.trim); + dateTest = dateHelper.parseDate(splitData[0], options.formatDate); + dateEnd = dateHelper.parseDate(splitData[1], options.formatDate); + desc = splitData[2]; + style = splitData[3]; + } + + while (dateTest <= dateEnd) { + hDate = new HighlightedDate(dateTest, desc, style); + keyDate = dateHelper.formatDate(dateTest, options.formatDate); + dateTest.setDate(dateTest.getDate() + 1); + if (highlightedDates[keyDate] !== undefined) { + exDesc = highlightedDates[keyDate].desc; + if (exDesc && exDesc.length && hDate.desc && hDate.desc.length) { + highlightedDates[keyDate].desc = exDesc + "\n" + hDate.desc; + } + } else { + highlightedDates[keyDate] = hDate; + } + } + }); + + options.highlightedDates = $.extend(true, [], highlightedDates); + } + + if (_options.disabledDates && $.isArray(_options.disabledDates) && _options.disabledDates.length) { + options.disabledDates = $.extend(true, [], _options.disabledDates); + } + + if (_options.disabledWeekDays && $.isArray(_options.disabledWeekDays) && _options.disabledWeekDays.length) { + options.disabledWeekDays = $.extend(true, [], _options.disabledWeekDays); + } + + if ((options.open || options.opened) && (!options.inline)) { + input.trigger('open.xdsoft'); + } + + if (options.inline) { + triggerAfterOpen = true; + datetimepicker.addClass('xdsoft_inline'); + input.after(datetimepicker).hide(); + } + + if (options.inverseButton) { + options.next = 'xdsoft_prev'; + options.prev = 'xdsoft_next'; + } + + if (options.datepicker) { + datepicker.addClass('active'); + } else { + datepicker.removeClass('active'); + } + + if (options.timepicker) { + timepicker.addClass('active'); + } else { + timepicker.removeClass('active'); + } + + if (options.value) { + _xdsoft_datetime.setCurrentTime(options.value); + if (input && input.val) { + input.val(_xdsoft_datetime.str); + } + } + + if (isNaN(options.dayOfWeekStart)) { + options.dayOfWeekStart = 0; + } else { + options.dayOfWeekStart = parseInt(options.dayOfWeekStart, 10) % 7; + } + + if (!options.timepickerScrollbar) { + timeboxparent.xdsoftScroller(options, 'hide'); + } + + if (options.minDate && /^[\+\-](.*)$/.test(options.minDate)) { + options.minDate = dateHelper.formatDate(_xdsoft_datetime.strToDateTime(options.minDate), options.formatDate); + } + + if (options.maxDate && /^[\+\-](.*)$/.test(options.maxDate)) { + options.maxDate = dateHelper.formatDate(_xdsoft_datetime.strToDateTime(options.maxDate), options.formatDate); + } + + if (options.minDateTime && /^\+(.*)$/.test(options.minDateTime)) { + options.minDateTime = _xdsoft_datetime.strToDateTime(options.minDateTime).dateFormat(options.formatDate); + } + + if (options.maxDateTime && /^\+(.*)$/.test(options.maxDateTime)) { + options.maxDateTime = _xdsoft_datetime.strToDateTime(options.maxDateTime).dateFormat(options.formatDate); + } + + applyButton.toggle(options.showApplyButton); + + month_picker + .find('.xdsoft_today_button') + .css('visibility', !options.todayButton ? 'hidden' : 'visible'); + + month_picker + .find('.' + options.prev) + .css('visibility', !options.prevButton ? 'hidden' : 'visible'); + + month_picker + .find('.' + options.next) + .css('visibility', !options.nextButton ? 'hidden' : 'visible'); + + setMask(options); + + if (options.validateOnBlur) { + input + .off('blur.xdsoft') + .on('blur.xdsoft', function () { + if (options.allowBlank && (!$.trim($(this).val()).length || + (typeof options.mask === "string" && $.trim($(this).val()) === options.mask.replace(/[0-9]/g, '_')))) { + $(this).val(null); + datetimepicker.data('xdsoft_datetime').empty(); + } else { + var d = dateHelper.parseDate($(this).val(), options.format); + if (d) { // parseDate() may skip some invalid parts like date or time, so make it clear for user: show parsed date/time + $(this).val(dateHelper.formatDate(d, options.format)); + } else { + var splittedHours = +([$(this).val()[0], $(this).val()[1]].join('')), + splittedMinutes = +([$(this).val()[2], $(this).val()[3]].join('')); + + // parse the numbers as 0312 => 03:12 + if (!options.datepicker && options.timepicker && splittedHours >= 0 && splittedHours < 24 && splittedMinutes >= 0 && splittedMinutes < 60) { + $(this).val([splittedHours, splittedMinutes].map(function (item) { + return item > 9 ? item : '0' + item; + }).join(':')); + } else { + $(this).val(dateHelper.formatDate(_xdsoft_datetime.now(), options.format)); + } + } + datetimepicker.data('xdsoft_datetime').setCurrentTime($(this).val()); + } + + datetimepicker.trigger('changedatetime.xdsoft'); + datetimepicker.trigger('close.xdsoft'); + }); + } + options.dayOfWeekStartPrev = (options.dayOfWeekStart === 0) ? 6 : options.dayOfWeekStart - 1; + + datetimepicker + .trigger('xchange.xdsoft') + .trigger('afterOpen.xdsoft'); + }; + + datetimepicker + .data('options', options) + .on('touchstart mousedown.xdsoft', function (event) { + event.stopPropagation(); + event.preventDefault(); + yearselect.hide(); + monthselect.hide(); + return false; + }); + + //scroll_element = timepicker.find('.xdsoft_time_box'); + timeboxparent.append(timebox); + timeboxparent.xdsoftScroller(options); + + datetimepicker.on('afterOpen.xdsoft', function () { + timeboxparent.xdsoftScroller(options); + }); + + datetimepicker + .append(datepicker) + .append(timepicker); + + if (options.withoutCopyright !== true) { + datetimepicker + .append(xdsoft_copyright); + } + + datepicker + .append(month_picker) + .append(calendar) + .append(applyButton); + + $(options.parentID) + .append(datetimepicker); + + XDSoft_datetime = function () { + var _this = this; + _this.now = function (norecursion) { + var d = new Date(), + date, + time; + + if (!norecursion && options.defaultDate) { + date = _this.strToDateTime(options.defaultDate); + d.setFullYear(date.getFullYear()); + d.setMonth(date.getMonth()); + d.setDate(date.getDate()); + } + + d.setFullYear(d.getFullYear()); + + if (!norecursion && options.defaultTime) { + time = _this.strtotime(options.defaultTime); + d.setHours(time.getHours()); + d.setMinutes(time.getMinutes()); + d.setSeconds(time.getSeconds()); + d.setMilliseconds(time.getMilliseconds()); + } + return d; + }; + + _this.isValidDate = function (d) { + if (Object.prototype.toString.call(d) !== "[object Date]") { + return false; + } + return !isNaN(d.getTime()); + }; + + _this.setCurrentTime = function (dTime, requireValidDate) { + if (typeof dTime === 'string') { + _this.currentTime = _this.strToDateTime(dTime); + } + else if (_this.isValidDate(dTime)) { + _this.currentTime = dTime; + } + else if (!dTime && !requireValidDate && options.allowBlank && !options.inline) { + _this.currentTime = null; + } + else { + _this.currentTime = _this.now(); + } + + datetimepicker.trigger('xchange.xdsoft'); + }; + + _this.empty = function () { + _this.currentTime = null; + }; + + _this.getCurrentTime = function () { + return _this.currentTime; + }; + + _this.nextMonth = function () { + + if (_this.currentTime === undefined || _this.currentTime === null) { + _this.currentTime = _this.now(); + } + + var month = _this.currentTime.getMonth() + 1, + year; + if (month === 12) { + _this.currentTime.setFullYear(_this.currentTime.getFullYear() + 1); + month = 0; + } + + year = _this.currentTime.getFullYear(); + + _this.currentTime.setDate( + Math.min( + new Date(_this.currentTime.getFullYear(), month + 1, 0).getDate(), + _this.currentTime.getDate() + ) + ); + _this.currentTime.setMonth(month); + + if (options.onChangeMonth && $.isFunction(options.onChangeMonth)) { + options.onChangeMonth.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); + } + + if (year !== _this.currentTime.getFullYear() && $.isFunction(options.onChangeYear)) { + options.onChangeYear.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); + } + + datetimepicker.trigger('xchange.xdsoft'); + return month; + }; + + _this.prevMonth = function () { + + if (_this.currentTime === undefined || _this.currentTime === null) { + _this.currentTime = _this.now(); + } + + var month = _this.currentTime.getMonth() - 1; + if (month === -1) { + _this.currentTime.setFullYear(_this.currentTime.getFullYear() - 1); + month = 11; + } + _this.currentTime.setDate( + Math.min( + new Date(_this.currentTime.getFullYear(), month + 1, 0).getDate(), + _this.currentTime.getDate() + ) + ); + _this.currentTime.setMonth(month); + if (options.onChangeMonth && $.isFunction(options.onChangeMonth)) { + options.onChangeMonth.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); + } + datetimepicker.trigger('xchange.xdsoft'); + return month; + }; + + _this.getWeekOfYear = function (datetime) { + if (options.onGetWeekOfYear && $.isFunction(options.onGetWeekOfYear)) { + var week = options.onGetWeekOfYear.call(datetimepicker, datetime); + if (typeof week !== 'undefined') { + return week; + } + } + var onejan = new Date(datetime.getFullYear(), 0, 1); + + //First week of the year is th one with the first Thursday according to ISO8601 + if (onejan.getDay() !== 4) { + onejan.setMonth(0, 1 + ((4 - onejan.getDay()+ 7) % 7)); + } + + return Math.ceil((((datetime - onejan) / 86400000) + onejan.getDay() + 1) / 7); + }; + + _this.strToDateTime = function (sDateTime) { + var tmpDate = [], timeOffset, currentTime; + + if (sDateTime && sDateTime instanceof Date && _this.isValidDate(sDateTime)) { + return sDateTime; + } + + tmpDate = /^([+-]{1})(.*)$/.exec(sDateTime); + + if (tmpDate) { + tmpDate[2] = dateHelper.parseDate(tmpDate[2], options.formatDate); + } + + if (tmpDate && tmpDate[2]) { + timeOffset = tmpDate[2].getTime() - (tmpDate[2].getTimezoneOffset()) * 60000; + currentTime = new Date((_this.now(true)).getTime() + parseInt(tmpDate[1] + '1', 10) * timeOffset); + } else { + currentTime = sDateTime ? dateHelper.parseDate(sDateTime, options.format) : _this.now(); + } + + if (!_this.isValidDate(currentTime)) { + currentTime = _this.now(); + } + + return currentTime; + }; + + _this.strToDate = function (sDate) { + if (sDate && sDate instanceof Date && _this.isValidDate(sDate)) { + return sDate; + } + + var currentTime = sDate ? dateHelper.parseDate(sDate, options.formatDate) : _this.now(true); + if (!_this.isValidDate(currentTime)) { + currentTime = _this.now(true); + } + return currentTime; + }; + + _this.strtotime = function (sTime) { + if (sTime && sTime instanceof Date && _this.isValidDate(sTime)) { + return sTime; + } + var currentTime = sTime ? dateHelper.parseDate(sTime, options.formatTime) : _this.now(true); + if (!_this.isValidDate(currentTime)) { + currentTime = _this.now(true); + } + return currentTime; + }; + + _this.str = function () { + var format = options.format; + if (options.yearOffset) { + format = format.replace('Y', _this.currentTime.getFullYear() + options.yearOffset); + format = format.replace('y', String(_this.currentTime.getFullYear() + options.yearOffset).substring(2, 4)); + } + return dateHelper.formatDate(_this.currentTime, format); + }; + _this.currentTime = this.now(); + }; + + _xdsoft_datetime = new XDSoft_datetime(); + + applyButton.on('touchend click', function (e) {//pathbrite + e.preventDefault(); + datetimepicker.data('changed', true); + _xdsoft_datetime.setCurrentTime(getCurrentValue()); + input.val(_xdsoft_datetime.str()); + datetimepicker.trigger('close.xdsoft'); + }); + month_picker + .find('.xdsoft_today_button') + .on('touchend mousedown.xdsoft', function () { + datetimepicker.data('changed', true); + _xdsoft_datetime.setCurrentTime(0, true); + datetimepicker.trigger('afterOpen.xdsoft'); + }).on('dblclick.xdsoft', function () { + var currentDate = _xdsoft_datetime.getCurrentTime(), minDate, maxDate; + currentDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate()); + minDate = _xdsoft_datetime.strToDate(options.minDate); + minDate = new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate()); + if (currentDate < minDate) { + return; + } + maxDate = _xdsoft_datetime.strToDate(options.maxDate); + maxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate()); + if (currentDate > maxDate) { + return; + } + input.val(_xdsoft_datetime.str()); + input.trigger('change'); + datetimepicker.trigger('close.xdsoft'); + }); + month_picker + .find('.xdsoft_prev,.xdsoft_next') + .on('touchend mousedown.xdsoft', function () { + var $this = $(this), + timer = 0, + stop = false; + + (function arguments_callee1(v) { + if ($this.hasClass(options.next)) { + _xdsoft_datetime.nextMonth(); + } else if ($this.hasClass(options.prev)) { + _xdsoft_datetime.prevMonth(); + } + if (options.monthChangeSpinner) { + if (!stop) { + timer = setTimeout(arguments_callee1, v || 100); + } + } + }(500)); + + $([options.ownerDocument.body, options.contentWindow]).on('touchend mouseup.xdsoft', function arguments_callee2() { + clearTimeout(timer); + stop = true; + $([options.ownerDocument.body, options.contentWindow]).off('touchend mouseup.xdsoft', arguments_callee2); + }); + }); + + timepicker + .find('.xdsoft_prev,.xdsoft_next') + .on('touchend mousedown.xdsoft', function () { + var $this = $(this), + timer = 0, + stop = false, + period = 110; + (function arguments_callee4(v) { + var pheight = timeboxparent[0].clientHeight, + height = timebox[0].offsetHeight, + top = Math.abs(parseInt(timebox.css('marginTop'), 10)); + if ($this.hasClass(options.next) && (height - pheight) - options.timeHeightInTimePicker >= top) { + timebox.css('marginTop', '-' + (top + options.timeHeightInTimePicker) + 'px'); + } else if ($this.hasClass(options.prev) && top - options.timeHeightInTimePicker >= 0) { + timebox.css('marginTop', '-' + (top - options.timeHeightInTimePicker) + 'px'); + } + /** + * Fixed bug: + * When using css3 transition, it will cause a bug that you cannot scroll the timepicker list. + * The reason is that the transition-duration time, if you set it to 0, all things fine, otherwise, this + * would cause a bug when you use jquery.css method. + * Let's say: * { transition: all .5s ease; } + * jquery timebox.css('marginTop') will return the original value which is before you clicking the next/prev button, + * meanwhile the timebox[0].style.marginTop will return the right value which is after you clicking the + * next/prev button. + * + * What we should do: + * Replace timebox.css('marginTop') with timebox[0].style.marginTop. + */ + timeboxparent.trigger('scroll_element.xdsoft_scroller', [Math.abs(parseInt(timebox[0].style.marginTop, 10) / (height - pheight))]); + period = (period > 10) ? 10 : period - 10; + if (!stop) { + timer = setTimeout(arguments_callee4, v || period); + } + }(500)); + $([options.ownerDocument.body, options.contentWindow]).on('touchend mouseup.xdsoft', function arguments_callee5() { + clearTimeout(timer); + stop = true; + $([options.ownerDocument.body, options.contentWindow]) + .off('touchend mouseup.xdsoft', arguments_callee5); + }); + }); + + xchangeTimer = 0; + // base handler - generating a calendar and timepicker + datetimepicker + .on('xchange.xdsoft', function (event) { + clearTimeout(xchangeTimer); + xchangeTimer = setTimeout(function () { + + if (_xdsoft_datetime.currentTime === undefined || _xdsoft_datetime.currentTime === null) { + _xdsoft_datetime.currentTime = _xdsoft_datetime.now(); + } + + var table = '', + start = new Date(_xdsoft_datetime.currentTime.getFullYear(), _xdsoft_datetime.currentTime.getMonth(), 1, 12, 0, 0), + i = 0, + j, + today = _xdsoft_datetime.now(), + maxDate = false, + minDate = false, + minDateTime = false, + maxDateTime = false, + hDate, + day, + d, + y, + m, + w, + classes = [], + customDateSettings, + newRow = true, + time = '', + h, + line_time, + description; + + while (start.getDay() !== options.dayOfWeekStart) { + start.setDate(start.getDate() - 1); + } + + table += ''; + + if (options.weeks) { + table += ''; + } + + for (j = 0; j < 7; j += 1) { + table += ''; + } + + table += ''; + table += ''; + + if (options.maxDate !== false) { + maxDate = _xdsoft_datetime.strToDate(options.maxDate); + maxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate(), 23, 59, 59, 999); + } + + if (options.minDate !== false) { + minDate = _xdsoft_datetime.strToDate(options.minDate); + minDate = new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate()); + } + + if (options.minDateTime !== false) { + minDateTime = _xdsoft_datetime.strToDate(options.minDateTime); + minDateTime = new Date(minDateTime.getFullYear(), minDateTime.getMonth(), minDateTime.getDate(), minDateTime.getHours(), minDateTime.getMinutes(), minDateTime.getSeconds()); + } + + if (options.maxDateTime !== false) { + maxDateTime = _xdsoft_datetime.strToDate(options.maxDateTime); + maxDateTime = new Date(maxDateTime.getFullYear(), maxDateTime.getMonth(), maxDateTime.getDate(), maxDateTime.getHours(), maxDateTime.getMinutes(), maxDateTime.getSeconds()); + } + + var maxDateTimeDay; + if (maxDateTime !== false) { + maxDateTimeDay = ((maxDateTime.getFullYear() * 12) + maxDateTime.getMonth()) * 31 + maxDateTime.getDate(); + } + + while (i < _xdsoft_datetime.currentTime.countDaysInMonth() || start.getDay() !== options.dayOfWeekStart || _xdsoft_datetime.currentTime.getMonth() === start.getMonth()) { + classes = []; + i += 1; + + day = start.getDay(); + d = start.getDate(); + y = start.getFullYear(); + m = start.getMonth(); + w = _xdsoft_datetime.getWeekOfYear(start); + description = ''; + + classes.push('xdsoft_date'); + + if (options.beforeShowDay && $.isFunction(options.beforeShowDay.call)) { + customDateSettings = options.beforeShowDay.call(datetimepicker, start); + } else { + customDateSettings = null; + } + + if(options.allowDateRe && Object.prototype.toString.call(options.allowDateRe) === "[object RegExp]"){ + if(!options.allowDateRe.test(dateHelper.formatDate(start, options.formatDate))){ + classes.push('xdsoft_disabled'); + } + } + + if(options.allowDates && options.allowDates.length>0){ + if(options.allowDates.indexOf(dateHelper.formatDate(start, options.formatDate)) === -1){ + classes.push('xdsoft_disabled'); + } + } + + var currentDay = ((start.getFullYear() * 12) + start.getMonth()) * 31 + start.getDate(); + if ((maxDate !== false && start > maxDate) || (minDateTime !== false && start < minDateTime) || (minDate !== false && start < minDate) || (maxDateTime !== false && currentDay > maxDateTimeDay) || (customDateSettings && customDateSettings[0] === false)) { + classes.push('xdsoft_disabled'); + } + + if (options.disabledDates.indexOf(dateHelper.formatDate(start, options.formatDate)) !== -1) { + classes.push('xdsoft_disabled'); + } + + if (options.disabledWeekDays.indexOf(day) !== -1) { + classes.push('xdsoft_disabled'); + } + + if (input.is('[disabled]')) { + classes.push('xdsoft_disabled'); + } + + if (customDateSettings && customDateSettings[1] !== "") { + classes.push(customDateSettings[1]); + } + + if (_xdsoft_datetime.currentTime.getMonth() !== m) { + classes.push('xdsoft_other_month'); + } + + if ((options.defaultSelect || datetimepicker.data('changed')) && dateHelper.formatDate(_xdsoft_datetime.currentTime, options.formatDate) === dateHelper.formatDate(start, options.formatDate)) { + classes.push('xdsoft_current'); + } + + if (dateHelper.formatDate(today, options.formatDate) === dateHelper.formatDate(start, options.formatDate)) { + classes.push('xdsoft_today'); + } + + if (start.getDay() === 0 || start.getDay() === 6 || options.weekends.indexOf(dateHelper.formatDate(start, options.formatDate)) !== -1) { + classes.push('xdsoft_weekend'); + } + + if (options.highlightedDates[dateHelper.formatDate(start, options.formatDate)] !== undefined) { + hDate = options.highlightedDates[dateHelper.formatDate(start, options.formatDate)]; + classes.push(hDate.style === undefined ? 'xdsoft_highlighted_default' : hDate.style); + description = hDate.desc === undefined ? '' : hDate.desc; + } + + if (options.beforeShowDay && $.isFunction(options.beforeShowDay)) { + classes.push(options.beforeShowDay(start)); + } + + if (newRow) { + table += ''; + newRow = false; + if (options.weeks) { + table += ''; + } + } + + table += ''; + + if (start.getDay() === options.dayOfWeekStartPrev) { + table += ''; + newRow = true; + } + + start.setDate(d + 1); + } + table += '
' + options.i18n[globalLocale].dayOfWeekShort[(j + options.dayOfWeekStart) % 7] + '
' + w + '' + + '
' + d + '
' + + '
'; + + calendar.html(table); + + month_picker.find('.xdsoft_label span').eq(0).text(options.i18n[globalLocale].months[_xdsoft_datetime.currentTime.getMonth()]); + month_picker.find('.xdsoft_label span').eq(1).text(_xdsoft_datetime.currentTime.getFullYear() + options.yearOffset); + + // generate timebox + time = ''; + h = ''; + m = ''; + + var minTimeMinutesOfDay = 0; + if (options.minTime !== false) { + var t = _xdsoft_datetime.strtotime(options.minTime); + minTimeMinutesOfDay = 60 * t.getHours() + t.getMinutes(); + } + var maxTimeMinutesOfDay = 24 * 60; + if (options.maxTime !== false) { + var t = _xdsoft_datetime.strtotime(options.maxTime); + maxTimeMinutesOfDay = 60 * t.getHours() + t.getMinutes(); + } + + if (options.minDateTime !== false) { + var t = _xdsoft_datetime.strToDateTime(options.minDateTime); + var currentDayIsMinDateTimeDay = dateHelper.formatDate(_xdsoft_datetime.currentTime, options.formatDate) === dateHelper.formatDate(t, options.formatDate); + if (currentDayIsMinDateTimeDay) { + var m = 60 * t.getHours() + t.getMinutes(); + if (m > minTimeMinutesOfDay) minTimeMinutesOfDay = m; + } + } + + if (options.maxDateTime !== false) { + var t = _xdsoft_datetime.strToDateTime(options.maxDateTime); + var currentDayIsMaxDateTimeDay = dateHelper.formatDate(_xdsoft_datetime.currentTime, options.formatDate) === dateHelper.formatDate(t, options.formatDate); + if (currentDayIsMaxDateTimeDay) { + var m = 60 * t.getHours() + t.getMinutes(); + if (m < maxTimeMinutesOfDay) maxTimeMinutesOfDay = m; + } + } + + line_time = function line_time(h, m) { + var now = _xdsoft_datetime.now(), current_time, + isALlowTimesInit = options.allowTimes && $.isArray(options.allowTimes) && options.allowTimes.length; + now.setHours(h); + h = parseInt(now.getHours(), 10); + now.setMinutes(m); + m = parseInt(now.getMinutes(), 10); + classes = []; + var currentMinutesOfDay = 60 * h + m; + if (input.is('[disabled]') || (currentMinutesOfDay >= maxTimeMinutesOfDay) || (currentMinutesOfDay < minTimeMinutesOfDay)) { + classes.push('xdsoft_disabled'); + } + + current_time = new Date(_xdsoft_datetime.currentTime); + current_time.setHours(parseInt(_xdsoft_datetime.currentTime.getHours(), 10)); + + if (!isALlowTimesInit) { + current_time.setMinutes(Math[options.roundTime](_xdsoft_datetime.currentTime.getMinutes() / options.step) * options.step); + } + + if ((options.initTime || options.defaultSelect || datetimepicker.data('changed')) && current_time.getHours() === parseInt(h, 10) && ((!isALlowTimesInit && options.step > 59) || current_time.getMinutes() === parseInt(m, 10))) { + if (options.defaultSelect || datetimepicker.data('changed')) { + classes.push('xdsoft_current'); + } else if (options.initTime) { + classes.push('xdsoft_init_time'); + } + } + if (parseInt(today.getHours(), 10) === parseInt(h, 10) && parseInt(today.getMinutes(), 10) === parseInt(m, 10)) { + classes.push('xdsoft_today'); + } + time += '
' + dateHelper.formatDate(now, options.formatTime) + '
'; + }; + + if (!options.allowTimes || !$.isArray(options.allowTimes) || !options.allowTimes.length) { + for (i = 0, j = 0; i < (options.hours12 ? 12 : 24); i += 1) { + for (j = 0; j < 60; j += options.step) { + var currentMinutesOfDay = i * 60 + j; + if (currentMinutesOfDay < minTimeMinutesOfDay) continue; + if (currentMinutesOfDay >= maxTimeMinutesOfDay) continue; + h = (i < 10 ? '0' : '') + i; + m = (j < 10 ? '0' : '') + j; + line_time(h, m); + } + } + } else { + for (i = 0; i < options.allowTimes.length; i += 1) { + h = _xdsoft_datetime.strtotime(options.allowTimes[i]).getHours(); + m = _xdsoft_datetime.strtotime(options.allowTimes[i]).getMinutes(); + line_time(h, m); + } + } + + timebox.html(time); + + opt = ''; + + for (i = parseInt(options.yearStart, 10); i <= parseInt(options.yearEnd, 10); i += 1) { + opt += '
' + (i + options.yearOffset) + '
'; + } + yearselect.children().eq(0) + .html(opt); + + for (i = parseInt(options.monthStart, 10), opt = ''; i <= parseInt(options.monthEnd, 10); i += 1) { + opt += '
' + options.i18n[globalLocale].months[i] + '
'; + } + monthselect.children().eq(0).html(opt); + $(datetimepicker) + .trigger('generate.xdsoft'); + }, 10); + event.stopPropagation(); + }) + .on('afterOpen.xdsoft', function () { + if (options.timepicker) { + var classType, pheight, height, top; + if (timebox.find('.xdsoft_current').length) { + classType = '.xdsoft_current'; + } else if (timebox.find('.xdsoft_init_time').length) { + classType = '.xdsoft_init_time'; + } + if (classType) { + pheight = timeboxparent[0].clientHeight; + height = timebox[0].offsetHeight; + top = timebox.find(classType).index() * options.timeHeightInTimePicker + 1; + if ((height - pheight) < top) { + top = height - pheight; + } + timeboxparent.trigger('scroll_element.xdsoft_scroller', [parseInt(top, 10) / (height - pheight)]); + } else { + timeboxparent.trigger('scroll_element.xdsoft_scroller', [0]); + } + } + }); + + timerclick = 0; + calendar + .on('touchend click.xdsoft', 'td', function (xdevent) { + xdevent.stopPropagation(); // Prevents closing of Pop-ups, Modals and Flyouts in Bootstrap + timerclick += 1; + var $this = $(this), + currentTime = _xdsoft_datetime.currentTime; + + if (currentTime === undefined || currentTime === null) { + _xdsoft_datetime.currentTime = _xdsoft_datetime.now(); + currentTime = _xdsoft_datetime.currentTime; + } + + if ($this.hasClass('xdsoft_disabled')) { + return false; + } + + currentTime.setDate(1); + currentTime.setFullYear($this.data('year')); + currentTime.setMonth($this.data('month')); + currentTime.setDate($this.data('date')); + + datetimepicker.trigger('select.xdsoft', [currentTime]); + + input.val(_xdsoft_datetime.str()); + + if (options.onSelectDate && $.isFunction(options.onSelectDate)) { + options.onSelectDate.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), xdevent); + } + + datetimepicker.data('changed', true); + datetimepicker.trigger('xchange.xdsoft'); + datetimepicker.trigger('changedatetime.xdsoft'); + if ((timerclick > 1 || (options.closeOnDateSelect === true || (options.closeOnDateSelect === false && !options.timepicker))) && !options.inline) { + datetimepicker.trigger('close.xdsoft'); + } + setTimeout(function () { + timerclick = 0; + }, 200); + }); + + timebox + .on('touchstart', 'div', function (xdevent) { + this.touchMoved = false; + }) + .on('touchmove', 'div', handleTouchMoved) + .on('touchend click.xdsoft', 'div', function (xdevent) { + if (!this.touchMoved) { + xdevent.stopPropagation(); + var $this = $(this), + currentTime = _xdsoft_datetime.currentTime; + + if (currentTime === undefined || currentTime === null) { + _xdsoft_datetime.currentTime = _xdsoft_datetime.now(); + currentTime = _xdsoft_datetime.currentTime; + } + + if ($this.hasClass('xdsoft_disabled')) { + return false; + } + currentTime.setHours($this.data('hour')); + currentTime.setMinutes($this.data('minute')); + datetimepicker.trigger('select.xdsoft', [currentTime]); + + datetimepicker.data('input').val(_xdsoft_datetime.str()); + + if (options.onSelectTime && $.isFunction(options.onSelectTime)) { + options.onSelectTime.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), xdevent); + } + datetimepicker.data('changed', true); + datetimepicker.trigger('xchange.xdsoft'); + datetimepicker.trigger('changedatetime.xdsoft'); + if (options.inline !== true && options.closeOnTimeSelect === true) { + datetimepicker.trigger('close.xdsoft'); + } + } + }); + + datepicker + .on('mousewheel.xdsoft', function (event) { + if (!options.scrollMonth) { + return true; + } + if (event.deltaY < 0) { + _xdsoft_datetime.nextMonth(); + } else { + _xdsoft_datetime.prevMonth(); + } + return false; + }); + + input + .on('mousewheel.xdsoft', function (event) { + if (!options.scrollInput) { + return true; + } + if (!options.datepicker && options.timepicker) { + current_time_index = timebox.find('.xdsoft_current').length ? timebox.find('.xdsoft_current').eq(0).index() : 0; + if (current_time_index + event.deltaY >= 0 && current_time_index + event.deltaY < timebox.children().length) { + current_time_index += event.deltaY; + } + if (timebox.children().eq(current_time_index).length) { + timebox.children().eq(current_time_index).trigger('mousedown'); + } + return false; + } + if (options.datepicker && !options.timepicker) { + datepicker.trigger(event, [event.deltaY, event.deltaX, event.deltaY]); + if (input.val) { + input.val(_xdsoft_datetime.str()); + } + datetimepicker.trigger('changedatetime.xdsoft'); + return false; + } + }); + + datetimepicker + .on('changedatetime.xdsoft', function (event) { + if (options.onChangeDateTime && $.isFunction(options.onChangeDateTime)) { + var $input = datetimepicker.data('input'); + options.onChangeDateTime.call(datetimepicker, _xdsoft_datetime.currentTime, $input, event); + delete options.value; + $input.trigger('change'); + } + }) + .on('generate.xdsoft', function () { + if (options.onGenerate && $.isFunction(options.onGenerate)) { + options.onGenerate.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); + } + if (triggerAfterOpen) { + datetimepicker.trigger('afterOpen.xdsoft'); + triggerAfterOpen = false; + } + }) + .on('click.xdsoft', function (xdevent) { + xdevent.stopPropagation(); + }); + + current_time_index = 0; + + /** + * Runs the callback for each of the specified node's ancestors. + * + * Return FALSE from the callback to stop ascending. + * + * @param {DOMNode} node + * @param {Function} callback + * @returns {undefined} + */ + forEachAncestorOf = function (node, callback) { + do { + node = node.parentNode; + + if (!node || callback(node) === false) { + break; + } + } while (node.nodeName !== 'HTML'); + }; + + /** + * Sets the position of the picker. + * + * @returns {undefined} + */ + setPos = function () { + var dateInputOffset, + dateInputElem, + verticalPosition, + left, + position, + datetimepickerElem, + dateInputHasFixedAncestor, + $dateInput, + windowWidth, + verticalAnchorEdge, + datetimepickerCss, + windowHeight, + windowScrollTop; + + $dateInput = datetimepicker.data('input'); + dateInputOffset = $dateInput.offset(); + dateInputElem = $dateInput[0]; + + verticalAnchorEdge = 'top'; + verticalPosition = (dateInputOffset.top + dateInputElem.offsetHeight) - 1; + left = dateInputOffset.left; + position = "absolute"; + + windowWidth = $(options.contentWindow).width(); + windowHeight = $(options.contentWindow).height(); + windowScrollTop = $(options.contentWindow).scrollTop(); + + if ((options.ownerDocument.documentElement.clientWidth - dateInputOffset.left) < datepicker.parent().outerWidth(true)) { + var diff = datepicker.parent().outerWidth(true) - dateInputElem.offsetWidth; + left = left - diff; + } + + if ($dateInput.parent().css('direction') === 'rtl') { + left -= (datetimepicker.outerWidth() - $dateInput.outerWidth()); + } + + if (options.fixed) { + verticalPosition -= windowScrollTop; + left -= $(options.contentWindow).scrollLeft(); + position = "fixed"; + } else { + dateInputHasFixedAncestor = false; + + forEachAncestorOf(dateInputElem, function (ancestorNode) { + if (ancestorNode === null) { + return false; + } + + if (options.contentWindow.getComputedStyle(ancestorNode).getPropertyValue('position') === 'fixed') { + dateInputHasFixedAncestor = true; + return false; + } + }); + + if (dateInputHasFixedAncestor) { + position = 'fixed'; + + //If the picker won't fit entirely within the viewport then display it above the date input. + if (verticalPosition + datetimepicker.outerHeight() > windowHeight + windowScrollTop) { + verticalAnchorEdge = 'bottom'; + verticalPosition = (windowHeight + windowScrollTop) - dateInputOffset.top; + } else { + verticalPosition -= windowScrollTop; + } + } else { + if (verticalPosition + datetimepicker[0].offsetHeight > windowHeight + windowScrollTop) { + verticalPosition = dateInputOffset.top - datetimepicker[0].offsetHeight + 1; + } + } + + if (verticalPosition < 0) { + verticalPosition = 0; + } + + if (left + dateInputElem.offsetWidth > windowWidth) { + left = windowWidth - dateInputElem.offsetWidth; + } + } + + datetimepickerElem = datetimepicker[0]; + + forEachAncestorOf(datetimepickerElem, function (ancestorNode) { + var ancestorNodePosition; + + ancestorNodePosition = options.contentWindow.getComputedStyle(ancestorNode).getPropertyValue('position'); + + if (ancestorNodePosition === 'relative' && windowWidth >= ancestorNode.offsetWidth) { + left = left - ((windowWidth - ancestorNode.offsetWidth) / 2); + return false; + } + }); + + datetimepickerCss = { + position: position, + left: left, + top: '', //Initialize to prevent previous values interfering with new ones. + bottom: '' //Initialize to prevent previous values interfering with new ones. + }; + + datetimepickerCss[verticalAnchorEdge] = verticalPosition; + + datetimepicker.css(datetimepickerCss); + }; + + datetimepicker + .on('open.xdsoft', function (event) { + var onShow = true; + if (options.onShow && $.isFunction(options.onShow)) { + onShow = options.onShow.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), event); + } + if (onShow !== false) { + datetimepicker.show(); + setPos(); + $(options.contentWindow) + .off('resize.xdsoft', setPos) + .on('resize.xdsoft', setPos); + + if (options.closeOnWithoutClick) { + $([options.ownerDocument.body, options.contentWindow]).on('touchstart mousedown.xdsoft', function arguments_callee6() { + datetimepicker.trigger('close.xdsoft'); + $([options.ownerDocument.body, options.contentWindow]).off('touchstart mousedown.xdsoft', arguments_callee6); + }); + } + } + }) + .on('close.xdsoft', function (event) { + var onClose = true; + month_picker + .find('.xdsoft_month,.xdsoft_year') + .find('.xdsoft_select') + .hide(); + if (options.onClose && $.isFunction(options.onClose)) { + onClose = options.onClose.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), event); + } + if (onClose !== false && !options.opened && !options.inline) { + datetimepicker.hide(); + } + event.stopPropagation(); + }) + .on('toggle.xdsoft', function () { + if (datetimepicker.is(':visible')) { + datetimepicker.trigger('close.xdsoft'); + } else { + datetimepicker.trigger('open.xdsoft'); + } + }) + .data('input', input); + + timer = 0; + + datetimepicker.data('xdsoft_datetime', _xdsoft_datetime); + datetimepicker.setOptions(options); + + function getCurrentValue() { + var ct = false, time; + + if (options.startDate) { + ct = _xdsoft_datetime.strToDate(options.startDate); + } else { + ct = options.value || ((input && input.val && input.val()) ? input.val() : ''); + if (ct) { + ct = _xdsoft_datetime.strToDateTime(ct); + if (options.yearOffset) { + ct = new Date(ct.getFullYear() - options.yearOffset, ct.getMonth(), ct.getDate(), ct.getHours(), ct.getMinutes(), ct.getSeconds(), ct.getMilliseconds()); + } + } else if (options.defaultDate) { + ct = _xdsoft_datetime.strToDateTime(options.defaultDate); + if (options.defaultTime) { + time = _xdsoft_datetime.strtotime(options.defaultTime); + ct.setHours(time.getHours()); + ct.setMinutes(time.getMinutes()); + } + } + } + + if (ct && _xdsoft_datetime.isValidDate(ct)) { + datetimepicker.data('changed', true); + } else { + ct = ''; + } + + return ct || 0; + } + + function setMask(options) { + + var isValidValue = function (mask, value) { + var reg = mask + .replace(/([\[\]\/\{\}\(\)\-\.\+]{1})/g, '\\$1') + .replace(/_/g, '{digit+}') + .replace(/([0-9]{1})/g, '{digit$1}') + .replace(/\{digit([0-9]{1})\}/g, '[0-$1_]{1}') + .replace(/\{digit[\+]\}/g, '[0-9_]{1}'); + return (new RegExp(reg)).test(value); + }, + getCaretPos = function (input) { + try { + if (options.ownerDocument.selection && options.ownerDocument.selection.createRange) { + var range = options.ownerDocument.selection.createRange(); + return range.getBookmark().charCodeAt(2) - 2; + } + if (input.setSelectionRange) { + return input.selectionStart; + } + } catch (e) { + return 0; + } + }, + setCaretPos = function (node, pos) { + node = (typeof node === "string" || node instanceof String) ? options.ownerDocument.getElementById(node) : node; + if (!node) { + return false; + } + if (node.createTextRange) { + var textRange = node.createTextRange(); + textRange.collapse(true); + textRange.moveEnd('character', pos); + textRange.moveStart('character', pos); + textRange.select(); + return true; + } + if (node.setSelectionRange) { + node.setSelectionRange(pos, pos); + return true; + } + return false; + }; + + if(options.mask) { + input.off('keydown.xdsoft'); + } + + if (options.mask === true) { + if (dateHelper.formatMask) { + options.mask = dateHelper.formatMask(options.format) + } else { + options.mask = options.format + .replace(/Y/g, '9999') + .replace(/F/g, '9999') + .replace(/m/g, '19') + .replace(/d/g, '39') + .replace(/H/g, '29') + .replace(/i/g, '59') + .replace(/s/g, '59'); + } + } + + if ($.type(options.mask) === 'string') { + if (!isValidValue(options.mask, input.val())) { + input.val(options.mask.replace(/[0-9]/g, '_')); + setCaretPos(input[0], 0); + } + + input.on('paste.xdsoft', function (event) { + // couple options here + // 1. return false - tell them they can't paste + // 2. insert over current characters - minimal validation + // 3. full fledged parsing and validation + // let's go option 2 for now + + // fires multiple times for some reason + + // https://stackoverflow.com/a/30496488/1366033 + var clipboardData = event.clipboardData || event.originalEvent.clipboardData || window.clipboardData, + pastedData = clipboardData.getData('text'), + val = this.value, + pos = this.selectionStart + + var valueBeforeCursor = val.substr(0, pos); + var valueAfterPaste = val.substr(pos + pastedData.length); + + val = valueBeforeCursor + pastedData + valueAfterPaste; + pos += pastedData.length; + + if (isValidValue(options.mask, val)) { + this.value = val; + setCaretPos(this, pos); + } else if ($.trim(val) === '') { + this.value = options.mask.replace(/[0-9]/g, '_'); + } else { + input.trigger('error_input.xdsoft'); + } + + event.preventDefault(); + return false; + }); + + input.on('keydown.xdsoft', function (event) { + var val = this.value, + key = event.which, + pos = this.selectionStart, + selEnd = this.selectionEnd, + hasSel = pos !== selEnd, + digit; + + // only alow these characters + if (((key >= KEY0 && key <= KEY9) || + (key >= _KEY0 && key <= _KEY9)) || + (key === BACKSPACE || key === DEL)) { + + // get char to insert which is new character or placeholder ('_') + digit = (key === BACKSPACE || key === DEL) ? '_' : + String.fromCharCode((_KEY0 <= key && key <= _KEY9) ? key - KEY0 : key); + + // we're deleting something, we're not at the start, and have normal cursor, move back one + // if we have a selection length, cursor actually sits behind deletable char, not in front + if (key === BACKSPACE && pos && !hasSel) { + pos -= 1; + } + + // don't stop on a separator, continue whatever direction you were going + // value char - keep incrementing position while on separator char and we still have room + // del char - keep decrementing position while on separator char and we still have room + while (true) { + var maskValueAtCurPos = options.mask.substr(pos, 1); + var posShorterThanMaskLength = pos < options.mask.length; + var posGreaterThanZero = pos > 0; + var notNumberOrPlaceholder = /[^0-9_]/; + var curPosOnSep = notNumberOrPlaceholder.test(maskValueAtCurPos); + var continueMovingPosition = curPosOnSep && posShorterThanMaskLength && posGreaterThanZero + + // if we hit a real char, stay where we are + if (!continueMovingPosition) break; + + // hitting backspace in a selection, you can possibly go back any further - go forward + pos += (key === BACKSPACE && !hasSel) ? -1 : 1; + + } + + + if (hasSel) { + // pos might have moved so re-calc length + var selLength = selEnd - pos + + // if we have a selection length we will wipe out entire selection and replace with default template for that range + var defaultBlank = options.mask.replace(/[0-9]/g, '_'); + var defaultBlankSelectionReplacement = defaultBlank.substr(pos, selLength); + var selReplacementRemainder = defaultBlankSelectionReplacement.substr(1) // might be empty + + var valueBeforeSel = val.substr(0, pos); + var insertChars = digit + selReplacementRemainder; + var charsAfterSelection = val.substr(pos + selLength); + + val = valueBeforeSel + insertChars + charsAfterSelection + + } else { + var valueBeforeCursor = val.substr(0, pos); + var insertChar = digit; + var valueAfterNextChar = val.substr(pos + 1); + + val = valueBeforeCursor + insertChar + valueAfterNextChar + } + + if ($.trim(val) === '') { + // if empty, set to default + val = defaultBlank + } else { + // if at the last character don't need to do anything + if (pos === options.mask.length) { + event.preventDefault(); + return false; + } + } + + // resume cursor location + pos += (key === BACKSPACE) ? 0 : 1; + // don't stop on a separator, continue whatever direction you were going + while (/[^0-9_]/.test(options.mask.substr(pos, 1)) && pos < options.mask.length && pos > 0) { + pos += (key === BACKSPACE) ? 0 : 1; + } + + if (isValidValue(options.mask, val)) { + this.value = val; + setCaretPos(this, pos); + } else if ($.trim(val) === '') { + this.value = options.mask.replace(/[0-9]/g, '_'); + } else { + input.trigger('error_input.xdsoft'); + } + } else { + if (([AKEY, CKEY, VKEY, ZKEY, YKEY].indexOf(key) !== -1 && ctrlDown) || [ESC, ARROWUP, ARROWDOWN, ARROWLEFT, ARROWRIGHT, F5, CTRLKEY, TAB, ENTER].indexOf(key) !== -1) { + return true; + } + } + + event.preventDefault(); + return false; + }); + } + } + + _xdsoft_datetime.setCurrentTime(getCurrentValue()); + + input + .data('xdsoft_datetimepicker', datetimepicker) + .on('open.xdsoft focusin.xdsoft mousedown.xdsoft touchstart', function () { + if (input.is(':disabled') || (input.data('xdsoft_datetimepicker').is(':visible') && options.closeOnInputClick)) { + return; + } + if (!options.openOnFocus) { + return; + } + clearTimeout(timer); + timer = setTimeout(function () { + if (input.is(':disabled')) { + return; + } + + triggerAfterOpen = true; + _xdsoft_datetime.setCurrentTime(getCurrentValue(), true); + if(options.mask) { + setMask(options); + } + datetimepicker.trigger('open.xdsoft'); + }, 100); + }) + .on('keydown.xdsoft', function (event) { + var elementSelector, + key = event.which; + if ([ENTER].indexOf(key) !== -1 && options.enterLikeTab) { + elementSelector = $("input:visible,textarea:visible,button:visible,a:visible"); + datetimepicker.trigger('close.xdsoft'); + elementSelector.eq(elementSelector.index(this) + 1).focus(); + return false; + } + if ([TAB].indexOf(key) !== -1) { + datetimepicker.trigger('close.xdsoft'); + return true; + } + }) + .on('blur.xdsoft', function () { + datetimepicker.trigger('close.xdsoft'); + }); + }; + destroyDateTimePicker = function (input) { + var datetimepicker = input.data('xdsoft_datetimepicker'); + if (datetimepicker) { + datetimepicker.data('xdsoft_datetime', null); + datetimepicker.remove(); + input + .data('xdsoft_datetimepicker', null) + .off('.xdsoft'); + $(options.contentWindow).off('resize.xdsoft'); + $([options.contentWindow, options.ownerDocument.body]).off('mousedown.xdsoft touchstart'); + if (input.unmousewheel) { + input.unmousewheel(); + } + } + }; + $(options.ownerDocument) + .off('keydown.xdsoftctrl keyup.xdsoftctrl') + .on('keydown.xdsoftctrl', function (e) { + if (e.keyCode === CTRLKEY) { + ctrlDown = true; + } + }) + .on('keyup.xdsoftctrl', function (e) { + if (e.keyCode === CTRLKEY) { + ctrlDown = false; + } + }); + + this.each(function () { + var datetimepicker = $(this).data('xdsoft_datetimepicker'), $input; + if (datetimepicker) { + if ($.type(opt) === 'string') { + switch (opt) { + case 'show': + $(this).select().focus(); + datetimepicker.trigger('open.xdsoft'); + break; + case 'hide': + datetimepicker.trigger('close.xdsoft'); + break; + case 'toggle': + datetimepicker.trigger('toggle.xdsoft'); + break; + case 'destroy': + destroyDateTimePicker($(this)); + break; + case 'reset': + this.value = this.defaultValue; + if (!this.value || !datetimepicker.data('xdsoft_datetime').isValidDate(dateHelper.parseDate(this.value, options.format))) { + datetimepicker.data('changed', false); + } + datetimepicker.data('xdsoft_datetime').setCurrentTime(this.value); + break; + case 'validate': + $input = datetimepicker.data('input'); + $input.trigger('blur.xdsoft'); + break; + default: + if (datetimepicker[opt] && $.isFunction(datetimepicker[opt])) { + result = datetimepicker[opt](opt2); + } + } + } else { + datetimepicker + .setOptions(opt); + } + return 0; + } + if ($.type(opt) !== 'string') { + if (!options.lazyInit || options.open || options.inline) { + createDateTimePicker($(this)); + } else { + lazyInit($(this)); + } + } + }); + + return result; + }; + + $.fn.datetimepicker.defaults = default_options; + + function HighlightedDate(date, desc, style) { + "use strict"; + this.date = date; + this.desc = desc; + this.style = style; + } +}; +;(function (factory) { + if ( typeof define === 'function' && define.amd ) { + // AMD. Register as an anonymous module. + define(['jquery', 'jquery-mousewheel'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS style for Browserify + module.exports = factory(require('jquery'));; + } else { + // Browser globals + factory(jQuery); + } +}(datetimepickerFactory)); + + +/*! + * jQuery Mousewheel 3.1.13 + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license */ -/*global document,window,jQuery,setTimeout,clearTimeout,HighlightedDate,getCurrentValue*/ -(function ($) { - 'use strict'; - var default_options = { - i18n: { - ar: { // Arabic - months: [ - "كانون الثاني", "شباط", "آذار", "نيسان", "مايو", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول" - ], - dayOfWeek: [ - "ن", "ث", "ع", "خ", "ج", "س", "ح" - ] - }, - ro: { // Romanian - months: [ - "ianuarie", "februarie", "martie", "aprilie", "mai", "iunie", "iulie", "august", "septembrie", "octombrie", "noiembrie", "decembrie" - ], - dayOfWeek: [ - "l", "ma", "mi", "j", "v", "s", "d" - ] - }, - id: { // Indonesian - months: [ - "Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember" - ], - dayOfWeek: [ - "Min", "Sen", "Sel", "Rab", "Kam", "Jum", "Sab" - ] - }, - is: { // Icelandic - months: [ - "Janúar", "Febrúar", "Mars", "Apríl", "Maí", "Júní", "Júlí", "Ágúst", "September", "Október", "Nóvember", "Desember" - ], - dayOfWeek: [ - "Sun", "Mán", "Þrið", "Mið", "Fim", "Fös", "Lau" - ] - }, - bg: { // Bulgarian - months: [ - "Януари", "Февруари", "Март", "Април", "Май", "Юни", "Юли", "Август", "Септември", "Октомври", "Ноември", "Декември" - ], - dayOfWeek: [ - "Нд", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб" - ] - }, - fa: { // Persian/Farsi - months: [ - 'فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند' - ], - dayOfWeek: [ - 'یکشنبه', 'دوشنبه', 'سه شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه', 'شنبه' - ] - }, - ru: { // Russian - months: [ - 'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь' - ], - dayOfWeek: [ - "Вск", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб" - ] - }, - uk: { // Ukrainian - months: [ - 'Січень', 'Лютий', 'Березень', 'Квітень', 'Травень', 'Червень', 'Липень', 'Серпень', 'Вересень', 'Жовтень', 'Листопад', 'Грудень' - ], - dayOfWeek: [ - "Ндл", "Пнд", "Втр", "Срд", "Чтв", "Птн", "Сбт" - ] - }, - en: { // English - months: [ - "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" - ], - dayOfWeek: [ - "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" - ] - }, - el: { // Ελληνικά - months: [ - "Ιανουάριος", "Φεβρουάριος", "Μάρτιος", "Απρίλιος", "Μάιος", "Ιούνιος", "Ιούλιος", "Αύγουστος", "Σεπτέμβριος", "Οκτώβριος", "Νοέμβριος", "Δεκέμβριος" - ], - dayOfWeek: [ - "Κυρ", "Δευ", "Τρι", "Τετ", "Πεμ", "Παρ", "Σαβ" - ] - }, - de: { // German - months: [ - 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember' - ], - dayOfWeek: [ - "So", "Mo", "Di", "Mi", "Do", "Fr", "Sa" - ] - }, - nl: { // Dutch - months: [ - "januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober", "november", "december" - ], - dayOfWeek: [ - "zo", "ma", "di", "wo", "do", "vr", "za" - ] - }, - tr: { // Turkish - months: [ - "Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık" - ], - dayOfWeek: [ - "Paz", "Pts", "Sal", "Çar", "Per", "Cum", "Cts" - ] - }, - fr: { //French - months: [ - "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre" - ], - dayOfWeek: [ - "Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam" - ] - }, - es: { // Spanish - months: [ - "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre" - ], - dayOfWeek: [ - "Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb" - ] - }, - th: { // Thai - months: [ - 'มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', 'พฤษภาคม', 'มิถุนายน', 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม' - ], - dayOfWeek: [ - 'อา.', 'จ.', 'อ.', 'พ.', 'พฤ.', 'ศ.', 'ส.' - ] - }, - pl: { // Polish - months: [ - "styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec", "lipiec", "sierpień", "wrzesień", "październik", "listopad", "grudzień" - ], - dayOfWeek: [ - "nd", "pn", "wt", "śr", "cz", "pt", "sb" - ] - }, - pt: { // Portuguese - months: [ - "Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro" - ], - dayOfWeek: [ - "Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab" - ] - }, - ch: { // Simplified Chinese - months: [ - "一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月" - ], - dayOfWeek: [ - "日", "一", "二", "三", "四", "五", "六" - ] - }, - se: { // Swedish - months: [ - "Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December" - ], - dayOfWeek: [ - "Sön", "Mån", "Tis", "Ons", "Tor", "Fre", "Lör" - ] - }, - kr: { // Korean - months: [ - "1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월" - ], - dayOfWeek: [ - "일", "월", "화", "수", "목", "금", "토" - ] - }, - it: { // Italian - months: [ - "Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre" - ], - dayOfWeek: [ - "Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab" - ] - }, - da: { // Dansk - months: [ - "January", "Februar", "Marts", "April", "Maj", "Juni", "July", "August", "September", "Oktober", "November", "December" - ], - dayOfWeek: [ - "Søn", "Man", "Tir", "Ons", "Tor", "Fre", "Lør" - ] - }, - no: { // Norwegian - months: [ - "Januar", "Februar", "Mars", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Desember" - ], - dayOfWeek: [ - "Søn", "Man", "Tir", "Ons", "Tor", "Fre", "Lør" - ] - }, - ja: { // Japanese - months: [ - "1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月" - ], - dayOfWeek: [ - "日", "月", "火", "水", "木", "金", "土" - ] - }, - vi: { // Vietnamese - months: [ - "Tháng 1", "Tháng 2", "Tháng 3", "Tháng 4", "Tháng 5", "Tháng 6", "Tháng 7", "Tháng 8", "Tháng 9", "Tháng 10", "Tháng 11", "Tháng 12" - ], - dayOfWeek: [ - "CN", "T2", "T3", "T4", "T5", "T6", "T7" - ] - }, - sl: { // Slovenščina - months: [ - "Januar", "Februar", "Marec", "April", "Maj", "Junij", "Julij", "Avgust", "September", "Oktober", "November", "December" - ], - dayOfWeek: [ - "Ned", "Pon", "Tor", "Sre", "Čet", "Pet", "Sob" - ] - }, - cs: { // Čeština - months: [ - "Leden", "Únor", "Březen", "Duben", "Květen", "Červen", "Červenec", "Srpen", "Září", "Říjen", "Listopad", "Prosinec" - ], - dayOfWeek: [ - "Ne", "Po", "Út", "St", "Čt", "Pá", "So" - ] - }, - hu: { // Hungarian - months: [ - "Január", "Február", "Március", "Április", "Május", "Június", "Július", "Augusztus", "Szeptember", "Október", "November", "December" - ], - dayOfWeek: [ - "Va", "Hé", "Ke", "Sze", "Cs", "Pé", "Szo" - ] - }, - az: { //Azerbaijanian (Azeri) - months: [ - "Yanvar", "Fevral", "Mart", "Aprel", "May", "Iyun", "Iyul", "Avqust", "Sentyabr", "Oktyabr", "Noyabr", "Dekabr" - ], - dayOfWeek: [ - "B", "Be", "Ça", "Ç", "Ca", "C", "Ş" - ] - }, - bs: { //Bosanski - months: [ - "Januar", "Februar", "Mart", "April", "Maj", "Jun", "Jul", "Avgust", "Septembar", "Oktobar", "Novembar", "Decembar" - ], - dayOfWeek: [ - "Ned", "Pon", "Uto", "Sri", "Čet", "Pet", "Sub" - ] - }, - ca: { //Català - months: [ - "Gener", "Febrer", "Març", "Abril", "Maig", "Juny", "Juliol", "Agost", "Setembre", "Octubre", "Novembre", "Desembre" - ], - dayOfWeek: [ - "Dg", "Dl", "Dt", "Dc", "Dj", "Dv", "Ds" - ] - }, - 'en-GB': { //English (British) - months: [ - "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" - ], - dayOfWeek: [ - "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" - ] - }, - et: { //"Eesti" - months: [ - "Jaanuar", "Veebruar", "Märts", "Aprill", "Mai", "Juuni", "Juuli", "August", "September", "Oktoober", "November", "Detsember" - ], - dayOfWeek: [ - "P", "E", "T", "K", "N", "R", "L" - ] - }, - eu: { //Euskara - months: [ - "Urtarrila", "Otsaila", "Martxoa", "Apirila", "Maiatza", "Ekaina", "Uztaila", "Abuztua", "Iraila", "Urria", "Azaroa", "Abendua" - ], - dayOfWeek: [ - "Ig.", "Al.", "Ar.", "Az.", "Og.", "Or.", "La." - ] - }, - fi: { //Finnish (Suomi) - months: [ - "Tammikuu", "Helmikuu", "Maaliskuu", "Huhtikuu", "Toukokuu", "Kesäkuu", "Heinäkuu", "Elokuu", "Syyskuu", "Lokakuu", "Marraskuu", "Joulukuu" - ], - dayOfWeek: [ - "Su", "Ma", "Ti", "Ke", "To", "Pe", "La" - ] - }, - gl: { //Galego - months: [ - "Xan", "Feb", "Maz", "Abr", "Mai", "Xun", "Xul", "Ago", "Set", "Out", "Nov", "Dec" - ], - dayOfWeek: [ - "Dom", "Lun", "Mar", "Mer", "Xov", "Ven", "Sab" - ] - }, - hr: { //Hrvatski - months: [ - "Siječanj", "Veljača", "Ožujak", "Travanj", "Svibanj", "Lipanj", "Srpanj", "Kolovoz", "Rujan", "Listopad", "Studeni", "Prosinac" - ], - dayOfWeek: [ - "Ned", "Pon", "Uto", "Sri", "Čet", "Pet", "Sub" - ] - }, - ko: { //Korean (한국어) - months: [ - "1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월" - ], - dayOfWeek: [ - "일", "월", "화", "수", "목", "금", "토" - ] - }, - lt: { //Lithuanian (lietuvių) - months: [ - "Sausio", "Vasario", "Kovo", "Balandžio", "Gegužės", "Birželio", "Liepos", "Rugpjūčio", "Rugsėjo", "Spalio", "Lapkričio", "Gruodžio" - ], - dayOfWeek: [ - "Sek", "Pir", "Ant", "Tre", "Ket", "Pen", "Šeš" - ] - }, - lv: { //Latvian (Latviešu) - months: [ - "Janvāris", "Februāris", "Marts", "Aprīlis ", "Maijs", "Jūnijs", "Jūlijs", "Augusts", "Septembris", "Oktobris", "Novembris", "Decembris" - ], - dayOfWeek: [ - "Sv", "Pr", "Ot", "Tr", "Ct", "Pk", "St" - ] - }, - mk: { //Macedonian (Македонски) - months: [ - "јануари", "февруари", "март", "април", "мај", "јуни", "јули", "август", "септември", "октомври", "ноември", "декември" - ], - dayOfWeek: [ - "нед", "пон", "вто", "сре", "чет", "пет", "саб" - ] - }, - mn: { //Mongolian (Монгол) - months: [ - "1-р сар", "2-р сар", "3-р сар", "4-р сар", "5-р сар", "6-р сар", "7-р сар", "8-р сар", "9-р сар", "10-р сар", "11-р сар", "12-р сар" - ], - dayOfWeek: [ - "Дав", "Мяг", "Лха", "Пүр", "Бсн", "Бям", "Ням" - ] - }, - 'pt-BR': { //Português(Brasil) - months: [ - "Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro" - ], - dayOfWeek: [ - "Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb" - ] - }, - sk: { //Slovenčina - months: [ - "Január", "Február", "Marec", "Apríl", "Máj", "Jún", "Júl", "August", "September", "Október", "November", "December" - ], - dayOfWeek: [ - "Ne", "Po", "Ut", "St", "Št", "Pi", "So" - ] - }, - sq: { //Albanian (Shqip) - months: [ - "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" - ], - dayOfWeek: [ - "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" - ] - }, - 'sr-YU': { //Serbian (Srpski) - months: [ - "Januar", "Februar", "Mart", "April", "Maj", "Jun", "Jul", "Avgust", "Septembar", "Oktobar", "Novembar", "Decembar" - ], - dayOfWeek: [ - "Ned", "Pon", "Uto", "Sre", "čet", "Pet", "Sub" - ] - }, - sr: { //Serbian Cyrillic (Српски) - months: [ - "јануар", "фебруар", "март", "април", "мај", "јун", "јул", "август", "септембар", "октобар", "новембар", "децембар" - ], - dayOfWeek: [ - "нед", "пон", "уто", "сре", "чет", "пет", "суб" - ] - }, - sv: { //Svenska - months: [ - "Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December" - ], - dayOfWeek: [ - "Sön", "Mån", "Tis", "Ons", "Tor", "Fre", "Lör" - ] - }, - 'zh-TW': { //Traditional Chinese (繁體中文) - months: [ - "一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月" - ], - dayOfWeek: [ - "日", "一", "二", "三", "四", "五", "六" - ] - }, - zh: { //Simplified Chinese (简体中文) - months: [ - "一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月" - ], - dayOfWeek: [ - "日", "一", "二", "三", "四", "五", "六" - ] - }, - he: { //Hebrew (עברית) - months: [ - 'ינואר', 'פברואר', 'מרץ', 'אפריל', 'מאי', 'יוני', 'יולי', 'אוגוסט', 'ספטמבר', 'אוקטובר', 'נובמבר', 'דצמבר' - ], - dayOfWeek: [ - 'א\'', 'ב\'', 'ג\'', 'ד\'', 'ה\'', 'ו\'', 'שבת' - ] - }, - hy: { // Armenian - months: [ - "Հունվար", "Փետրվար", "Մարտ", "Ապրիլ", "Մայիս", "Հունիս", "Հուլիս", "Օգոստոս", "Սեպտեմբեր", "Հոկտեմբեր", "Նոյեմբեր", "Դեկտեմբեր" - ], - dayOfWeek: [ - "Կի", "Երկ", "Երք", "Չոր", "Հնգ", "Ուրբ", "Շբթ" - ] - }, - kg: { // Kyrgyz - months: [ - 'Үчтүн айы', 'Бирдин айы', 'Жалган Куран', 'Чын Куран', 'Бугу', 'Кулжа', 'Теке', 'Баш Оона', 'Аяк Оона', 'Тогуздун айы', 'Жетинин айы', 'Бештин айы' - ], - dayOfWeek: [ - "Жек", "Дүй", "Шей", "Шар", "Бей", "Жум", "Ише" - ] - } - }, - value: '', - lang: 'en', - rtl: false, - - format: 'Y/m/d H:i', - formatTime: 'H:i', - formatDate: 'Y/m/d', - - startDate: false, // new Date(), '1986/12/08', '-1970/01/05','-1970/01/05', - step: 60, - monthChangeSpinner: true, - - closeOnDateSelect: false, - closeOnTimeSelect: true, - closeOnWithoutClick: true, - closeOnInputClick: true, - - timepicker: true, - datepicker: true, - weeks: false, - - defaultTime: false, // use formatTime format (ex. '10:00' for formatTime: 'H:i') - defaultDate: false, // use formatDate format (ex new Date() or '1986/12/08' or '-1970/01/05' or '-1970/01/05') - - minDate: false, - maxDate: false, - minTime: false, - maxTime: false, - disabledMinTime: false, - disabledMaxTime: false, - - allowTimes: [], - opened: false, - initTime: true, - inline: false, - theme: '', - - onSelectDate: function () {}, - onSelectTime: function () {}, - onChangeMonth: function () {}, - onChangeYear: function () {}, - onChangeDateTime: function () {}, - onShow: function () {}, - onClose: function () {}, - onGenerate: function () {}, - - withoutCopyright: true, - inverseButton: false, - hours12: false, - next: 'xdsoft_next', - prev : 'xdsoft_prev', - dayOfWeekStart: 0, - parentID: 'body', - timeHeightInTimePicker: 25, - timepickerScrollbar: true, - todayButton: true, - prevButton: true, - nextButton: true, - defaultSelect: true, - - scrollMonth: true, - scrollTime: true, - scrollInput: true, - - lazyInit: false, - mask: false, - validateOnBlur: true, - allowBlank: true, - yearStart: 1950, - yearEnd: 2050, - monthStart: 0, - monthEnd: 11, - style: '', - id: '', - fixed: false, - roundTime: 'round', // ceil, floor - className: '', - weekends: [], - highlightedDates: [], - highlightedPeriods: [], - disabledDates : [], - disabledWeekDays: [], - yearOffset: 0, - beforeShowDay: null, - - enterLikeTab: true, - showApplyButton: false - }; - // fix for ie8 - if (!window.getComputedStyle) { - window.getComputedStyle = function (el, pseudo) { - this.el = el; - this.getPropertyValue = function (prop) { - var re = /(\-([a-z]){1})/g; - if (prop === 'float') { - prop = 'styleFloat'; - } - if (re.test(prop)) { - prop = prop.replace(re, function (a, b, c) { - return c.toUpperCase(); - }); - } - return el.currentStyle[prop] || null; - }; - return this; - }; - } - if (!Array.prototype.indexOf) { - Array.prototype.indexOf = function (obj, start) { - var i, j; - for (i = (start || 0), j = this.length; i < j; i += 1) { - if (this[i] === obj) { return i; } - } - return -1; - }; - } - Date.prototype.countDaysInMonth = function () { - return new Date(this.getFullYear(), this.getMonth() + 1, 0).getDate(); - }; - $.fn.xdsoftScroller = function (percent) { - return this.each(function () { - var timeboxparent = $(this), - pointerEventToXY = function (e) { - var out = {x: 0, y: 0}, - touch; - if (e.type === 'touchstart' || e.type === 'touchmove' || e.type === 'touchend' || e.type === 'touchcancel') { - touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0]; - out.x = touch.clientX; - out.y = touch.clientY; - } else if (e.type === 'mousedown' || e.type === 'mouseup' || e.type === 'mousemove' || e.type === 'mouseover' || e.type === 'mouseout' || e.type === 'mouseenter' || e.type === 'mouseleave') { - out.x = e.clientX; - out.y = e.clientY; - } - return out; - }, - move = 0, - timebox, - parentHeight, - height, - scrollbar, - scroller, - maximumOffset = 100, - start = false, - startY = 0, - startTop = 0, - h1 = 0, - touchStart = false, - startTopScroll = 0, - calcOffset = function () {}; - if (percent === 'hide') { - timeboxparent.find('.xdsoft_scrollbar').hide(); - return; - } - if (!$(this).hasClass('xdsoft_scroller_box')) { - timebox = timeboxparent.children().eq(0); - parentHeight = timeboxparent[0].clientHeight; - height = timebox[0].offsetHeight; - scrollbar = $('
'); - scroller = $('
'); - scrollbar.append(scroller); - - timeboxparent.addClass('xdsoft_scroller_box').append(scrollbar); - calcOffset = function calcOffset(event) { - var offset = pointerEventToXY(event).y - startY + startTopScroll; - if (offset < 0) { - offset = 0; - } - if (offset + scroller[0].offsetHeight > h1) { - offset = h1 - scroller[0].offsetHeight; - } - timeboxparent.trigger('scroll_element.xdsoft_scroller', [maximumOffset ? offset / maximumOffset : 0]); - }; - - scroller - .on('touchstart.xdsoft_scroller mousedown.xdsoft_scroller', function (event) { - if (!parentHeight) { - timeboxparent.trigger('resize_scroll.xdsoft_scroller', [percent]); - } - - startY = pointerEventToXY(event).y; - startTopScroll = parseInt(scroller.css('margin-top'), 10); - h1 = scrollbar[0].offsetHeight; - - if (event.type === 'mousedown') { - if (document) { - $(document.body).addClass('xdsoft_noselect'); - } - $([document.body, window]).on('mouseup.xdsoft_scroller', function arguments_callee() { - $([document.body, window]).off('mouseup.xdsoft_scroller', arguments_callee) - .off('mousemove.xdsoft_scroller', calcOffset) - .removeClass('xdsoft_noselect'); - }); - $(document.body).on('mousemove.xdsoft_scroller', calcOffset); - } else { - touchStart = true; - event.stopPropagation(); - event.preventDefault(); - } - }) - .on('touchmove', function (event) { - if (touchStart) { - event.preventDefault(); - calcOffset(event); - } - }) - .on('touchend touchcancel', function (event) { - touchStart = false; - startTopScroll = 0; - }); - - timeboxparent - .on('scroll_element.xdsoft_scroller', function (event, percentage) { - if (!parentHeight) { - timeboxparent.trigger('resize_scroll.xdsoft_scroller', [percentage, true]); - } - percentage = percentage > 1 ? 1 : (percentage < 0 || isNaN(percentage)) ? 0 : percentage; - - scroller.css('margin-top', maximumOffset * percentage); - - setTimeout(function () { - timebox.css('marginTop', -parseInt((timebox[0].offsetHeight - parentHeight) * percentage, 10)); - }, 10); - }) - .on('resize_scroll.xdsoft_scroller', function (event, percentage, noTriggerScroll) { - var percent, sh; - parentHeight = timeboxparent[0].clientHeight; - height = timebox[0].offsetHeight; - percent = parentHeight / height; - sh = percent * scrollbar[0].offsetHeight; - if (percent > 1) { - scroller.hide(); - } else { - scroller.show(); - scroller.css('height', parseInt(sh > 10 ? sh : 10, 10)); - maximumOffset = scrollbar[0].offsetHeight - scroller[0].offsetHeight; - if (noTriggerScroll !== true) { - timeboxparent.trigger('scroll_element.xdsoft_scroller', [percentage || Math.abs(parseInt(timebox.css('marginTop'), 10)) / (height - parentHeight)]); - } - } - }); - - timeboxparent.on('mousewheel', function (event) { - var top = Math.abs(parseInt(timebox.css('marginTop'), 10)); - - top = top - (event.deltaY * 20); - if (top < 0) { - top = 0; - } - - timeboxparent.trigger('scroll_element.xdsoft_scroller', [top / (height - parentHeight)]); - event.stopPropagation(); - return false; - }); - - timeboxparent.on('touchstart', function (event) { - start = pointerEventToXY(event); - startTop = Math.abs(parseInt(timebox.css('marginTop'), 10)); - }); - - timeboxparent.on('touchmove', function (event) { - if (start) { - event.preventDefault(); - var coord = pointerEventToXY(event); - timeboxparent.trigger('scroll_element.xdsoft_scroller', [(startTop - (coord.y - start.y)) / (height - parentHeight)]); - } - }); - - timeboxparent.on('touchend touchcancel', function (event) { - start = false; - startTop = 0; - }); - } - timeboxparent.trigger('resize_scroll.xdsoft_scroller', [percent]); - }); - }; - - $.fn.datetimepicker = function (opt) { - var KEY0 = 48, - KEY9 = 57, - _KEY0 = 96, - _KEY9 = 105, - CTRLKEY = 17, - DEL = 46, - ENTER = 13, - ESC = 27, - BACKSPACE = 8, - ARROWLEFT = 37, - ARROWUP = 38, - ARROWRIGHT = 39, - ARROWDOWN = 40, - TAB = 9, - F5 = 116, - AKEY = 65, - CKEY = 67, - VKEY = 86, - ZKEY = 90, - YKEY = 89, - ctrlDown = false, - options = ($.isPlainObject(opt) || !opt) ? $.extend(true, {}, default_options, opt) : $.extend(true, {}, default_options), - - lazyInitTimer = 0, - createDateTimePicker, - destroyDateTimePicker, - - lazyInit = function (input) { - input - .on('open.xdsoft focusin.xdsoft mousedown.xdsoft', function initOnActionCallback(event) { - if (input.is(':disabled') || input.data('xdsoft_datetimepicker')) { - return; - } - clearTimeout(lazyInitTimer); - lazyInitTimer = setTimeout(function () { - - if (!input.data('xdsoft_datetimepicker')) { - createDateTimePicker(input); - } - input - .off('open.xdsoft focusin.xdsoft mousedown.xdsoft', initOnActionCallback) - .trigger('open.xdsoft'); - }, 100); - }); - }; - - createDateTimePicker = function (input) { - var datetimepicker = $('
'), - xdsoft_copyright = $(''), - datepicker = $('
'), - mounth_picker = $('
' + - '
' + - '
' + - '
'), - calendar = $('
'), - timepicker = $('
'), - timeboxparent = timepicker.find('.xdsoft_time_box').eq(0), - timebox = $('
'), - applyButton = $(''), - /*scrollbar = $('
'), - scroller = $('
'),*/ - monthselect = $('
'), - yearselect = $('
'), - triggerAfterOpen = false, - XDSoft_datetime, - //scroll_element, - xchangeTimer, - timerclick, - current_time_index, - setPos, - timer = 0, - timer1 = 0, - _xdsoft_datetime; - - if (options.id) { - datetimepicker.attr('id', options.id); - } - if (options.style) { - datetimepicker.attr('style', options.style); - } - if (options.weeks) { - datetimepicker.addClass('xdsoft_showweeks'); - } - if (options.rtl) { - datetimepicker.addClass('xdsoft_rtl'); - } - - datetimepicker.addClass('xdsoft_' + options.theme); - datetimepicker.addClass(options.className); - - mounth_picker - .find('.xdsoft_month span') - .after(monthselect); - mounth_picker - .find('.xdsoft_year span') - .after(yearselect); - - mounth_picker - .find('.xdsoft_month,.xdsoft_year') - .on('mousedown.xdsoft', function (event) { - var select = $(this).find('.xdsoft_select').eq(0), - val = 0, - top = 0, - visible = select.is(':visible'), - items, - i; - - mounth_picker - .find('.xdsoft_select') - .hide(); - if (_xdsoft_datetime.currentTime) { - val = _xdsoft_datetime.currentTime[$(this).hasClass('xdsoft_month') ? 'getMonth' : 'getFullYear'](); - } - - select[visible ? 'hide' : 'show'](); - for (items = select.find('div.xdsoft_option'), i = 0; i < items.length; i += 1) { - if (items.eq(i).data('value') === val) { - break; - } else { - top += items[0].offsetHeight; - } - } - - select.xdsoftScroller(top / (select.children()[0].offsetHeight - (select[0].clientHeight))); - event.stopPropagation(); - return false; - }); - - mounth_picker - .find('.xdsoft_select') - .xdsoftScroller() - .on('mousedown.xdsoft', function (event) { - event.stopPropagation(); - event.preventDefault(); - }) - .on('mousedown.xdsoft', '.xdsoft_option', function (event) { - - if (_xdsoft_datetime.currentTime === undefined || _xdsoft_datetime.currentTime === null) { - _xdsoft_datetime.currentTime = _xdsoft_datetime.now(); - } - - var year = _xdsoft_datetime.currentTime.getFullYear(); - if (_xdsoft_datetime && _xdsoft_datetime.currentTime) { - _xdsoft_datetime.currentTime[$(this).parent().parent().hasClass('xdsoft_monthselect') ? 'setMonth' : 'setFullYear']($(this).data('value')); - } - - $(this).parent().parent().hide(); - - datetimepicker.trigger('xchange.xdsoft'); - if (options.onChangeMonth && $.isFunction(options.onChangeMonth)) { - options.onChangeMonth.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); - } - - if (year !== _xdsoft_datetime.currentTime.getFullYear() && $.isFunction(options.onChangeYear)) { - options.onChangeYear.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); - } - }); - - datetimepicker.setOptions = function (_options) { - var highlightedDates = {}, - getCaretPos = function (input) { - try { - if (document.selection && document.selection.createRange) { - var range = document.selection.createRange(); - return range.getBookmark().charCodeAt(2) - 2; - } - if (input.setSelectionRange) { - return input.selectionStart; - } - } catch (e) { - return 0; - } - }, - setCaretPos = function (node, pos) { - node = (typeof node === "string" || node instanceof String) ? document.getElementById(node) : node; - if (!node) { - return false; - } - if (node.createTextRange) { - var textRange = node.createTextRange(); - textRange.collapse(true); - textRange.moveEnd('character', pos); - textRange.moveStart('character', pos); - textRange.select(); - return true; - } - if (node.setSelectionRange) { - node.setSelectionRange(pos, pos); - return true; - } - return false; - }, - isValidValue = function (mask, value) { - var reg = mask - .replace(/([\[\]\/\{\}\(\)\-\.\+]{1})/g, '\\$1') - .replace(/_/g, '{digit+}') - .replace(/([0-9]{1})/g, '{digit$1}') - .replace(/\{digit([0-9]{1})\}/g, '[0-$1_]{1}') - .replace(/\{digit[\+]\}/g, '[0-9_]{1}'); - return (new RegExp(reg)).test(value); - }; - options = $.extend(true, {}, options, _options); - - if (_options.allowTimes && $.isArray(_options.allowTimes) && _options.allowTimes.length) { - options.allowTimes = $.extend(true, [], _options.allowTimes); - } - - if (_options.weekends && $.isArray(_options.weekends) && _options.weekends.length) { - options.weekends = $.extend(true, [], _options.weekends); - } - - if (_options.highlightedDates && $.isArray(_options.highlightedDates) && _options.highlightedDates.length) { - $.each(_options.highlightedDates, function (index, value) { - var splitData = $.map(value.split(','), $.trim), - exDesc, - hDate = new HighlightedDate(Date.parseDate(splitData[0], options.formatDate), splitData[1], splitData[2]), // date, desc, style - keyDate = hDate.date.dateFormat(options.formatDate); - if (highlightedDates[keyDate] !== undefined) { - exDesc = highlightedDates[keyDate].desc; - if (exDesc && exDesc.length && hDate.desc && hDate.desc.length) { - highlightedDates[keyDate].desc = exDesc + "\n" + hDate.desc; - } - } else { - highlightedDates[keyDate] = hDate; - } - }); - - options.highlightedDates = $.extend(true, [], highlightedDates); - } - - if (_options.highlightedPeriods && $.isArray(_options.highlightedPeriods) && _options.highlightedPeriods.length) { - highlightedDates = $.extend(true, [], options.highlightedDates); - $.each(_options.highlightedPeriods, function (index, value) { - var dateTest, // start date - dateEnd, - desc, - hDate, - keyDate, - exDesc, - style; - if ($.isArray(value)) { - dateTest = value[0]; - dateEnd = value[1]; - desc = value[2]; - style = value[3]; - } - else { - var splitData = $.map(value.split(','), $.trim); - dateTest = Date.parseDate(splitData[0], options.formatDate); - dateEnd = Date.parseDate(splitData[1], options.formatDate); - desc = splitData[2]; - style = splitData[3]; - } - - while (dateTest <= dateEnd) { - hDate = new HighlightedDate(dateTest, desc, style); - keyDate = dateTest.dateFormat(options.formatDate); - dateTest.setDate(dateTest.getDate() + 1); - if (highlightedDates[keyDate] !== undefined) { - exDesc = highlightedDates[keyDate].desc; - if (exDesc && exDesc.length && hDate.desc && hDate.desc.length) { - highlightedDates[keyDate].desc = exDesc + "\n" + hDate.desc; - } - } else { - highlightedDates[keyDate] = hDate; - } - } - }); - - options.highlightedDates = $.extend(true, [], highlightedDates); - } - - if (_options.disabledDates && $.isArray(_options.disabledDates) && _options.disabledDates.length) { - options.disabledDates = $.extend(true, [], _options.disabledDates); - } - - if (_options.disabledWeekDays && $.isArray(_options.disabledWeekDays) && _options.disabledWeekDays.length) { - options.disabledWeekDays = $.extend(true, [], _options.disabledWeekDays); - } - - if ((options.open || options.opened) && (!options.inline)) { - input.trigger('open.xdsoft'); - } - - if (options.inline) { - triggerAfterOpen = true; - datetimepicker.addClass('xdsoft_inline'); - input.after(datetimepicker).hide(); - } - - if (options.inverseButton) { - options.next = 'xdsoft_prev'; - options.prev = 'xdsoft_next'; - } - - if (options.datepicker) { - datepicker.addClass('active'); - } else { - datepicker.removeClass('active'); - } - - if (options.timepicker) { - timepicker.addClass('active'); - } else { - timepicker.removeClass('active'); - } - - if (options.value) { - _xdsoft_datetime.setCurrentTime(options.value); - if (input && input.val) { - input.val(_xdsoft_datetime.str); - } - } - - if (isNaN(options.dayOfWeekStart)) { - options.dayOfWeekStart = 0; - } else { - options.dayOfWeekStart = parseInt(options.dayOfWeekStart, 10) % 7; - } - - if (!options.timepickerScrollbar) { - timeboxparent.xdsoftScroller('hide'); - } - - if (options.minDate && /^[\+\-](.*)$/.test(options.minDate)) { - options.minDate = _xdsoft_datetime.strToDateTime(options.minDate).dateFormat(options.formatDate); - } - - if (options.maxDate && /^[\+\-](.*)$/.test(options.maxDate)) { - options.maxDate = _xdsoft_datetime.strToDateTime(options.maxDate).dateFormat(options.formatDate); - } - - applyButton.toggle(options.showApplyButton); - - mounth_picker - .find('.xdsoft_today_button') - .css('visibility', !options.todayButton ? 'hidden' : 'visible'); - - mounth_picker - .find('.' + options.prev) - .css('visibility', !options.prevButton ? 'hidden' : 'visible'); - - mounth_picker - .find('.' + options.next) - .css('visibility', !options.nextButton ? 'hidden' : 'visible'); - - if (options.mask) { - input.off('keydown.xdsoft'); - - if (options.mask === true) { - options.mask = options.format - .replace(/Y/g, '9999') - .replace(/F/g, '9999') - .replace(/m/g, '19') - .replace(/d/g, '39') - .replace(/H/g, '29') - .replace(/i/g, '59') - .replace(/s/g, '59'); - } - - if ($.type(options.mask) === 'string') { - if (!isValidValue(options.mask, input.val())) { - input.val(options.mask.replace(/[0-9]/g, '_')); - } - - input.on('keydown.xdsoft', function (event) { - var val = this.value, - key = event.which, - pos, - digit; - - if (((key >= KEY0 && key <= KEY9) || (key >= _KEY0 && key <= _KEY9)) || (key === BACKSPACE || key === DEL)) { - pos = getCaretPos(this); - digit = (key !== BACKSPACE && key !== DEL) ? String.fromCharCode((_KEY0 <= key && key <= _KEY9) ? key - KEY0 : key) : '_'; - - if ((key === BACKSPACE || key === DEL) && pos) { - pos -= 1; - digit = '_'; - } - - while (/[^0-9_]/.test(options.mask.substr(pos, 1)) && pos < options.mask.length && pos > 0) { - pos += (key === BACKSPACE || key === DEL) ? -1 : 1; - } - - val = val.substr(0, pos) + digit + val.substr(pos + 1); - if ($.trim(val) === '') { - val = options.mask.replace(/[0-9]/g, '_'); - } else { - if (pos === options.mask.length) { - event.preventDefault(); - return false; - } - } - - pos += (key === BACKSPACE || key === DEL) ? 0 : 1; - while (/[^0-9_]/.test(options.mask.substr(pos, 1)) && pos < options.mask.length && pos > 0) { - pos += (key === BACKSPACE || key === DEL) ? -1 : 1; - } - - if (isValidValue(options.mask, val)) { - this.value = val; - setCaretPos(this, pos); - } else if ($.trim(val) === '') { - this.value = options.mask.replace(/[0-9]/g, '_'); - } else { - input.trigger('error_input.xdsoft'); - } - } else { - if (([AKEY, CKEY, VKEY, ZKEY, YKEY].indexOf(key) !== -1 && ctrlDown) || [ESC, ARROWUP, ARROWDOWN, ARROWLEFT, ARROWRIGHT, F5, CTRLKEY, TAB, ENTER].indexOf(key) !== -1) { - return true; - } - } - - event.preventDefault(); - return false; - }); - } - } - if (options.validateOnBlur) { - input - .off('blur.xdsoft') - .on('blur.xdsoft', function () { - if (options.allowBlank && !$.trim($(this).val()).length) { - $(this).val(null); - datetimepicker.data('xdsoft_datetime').empty(); - } else if (!Date.parseDate($(this).val(), options.format)) { - var splittedHours = +([$(this).val()[0], $(this).val()[1]].join('')), - splittedMinutes = +([$(this).val()[2], $(this).val()[3]].join('')); - - // parse the numbers as 0312 => 03:12 - if (!options.datepicker && options.timepicker && splittedHours >= 0 && splittedHours < 24 && splittedMinutes >= 0 && splittedMinutes < 60) { - $(this).val([splittedHours, splittedMinutes].map(function (item) { - return item > 9 ? item : '0' + item; - }).join(':')); - } else { - $(this).val((_xdsoft_datetime.now()).dateFormat(options.format)); - } - - datetimepicker.data('xdsoft_datetime').setCurrentTime($(this).val()); - } else { - datetimepicker.data('xdsoft_datetime').setCurrentTime($(this).val()); - } - - datetimepicker.trigger('changedatetime.xdsoft'); - }); - } - options.dayOfWeekStartPrev = (options.dayOfWeekStart === 0) ? 6 : options.dayOfWeekStart - 1; - - datetimepicker - .trigger('xchange.xdsoft') - .trigger('afterOpen.xdsoft'); - }; - - datetimepicker - .data('options', options) - .on('mousedown.xdsoft', function (event) { - event.stopPropagation(); - event.preventDefault(); - yearselect.hide(); - monthselect.hide(); - return false; - }); - - //scroll_element = timepicker.find('.xdsoft_time_box'); - timeboxparent.append(timebox); - timeboxparent.xdsoftScroller(); - - datetimepicker.on('afterOpen.xdsoft', function () { - timeboxparent.xdsoftScroller(); - }); - - datetimepicker - .append(datepicker) - .append(timepicker); - - if (options.withoutCopyright !== true) { - datetimepicker - .append(xdsoft_copyright); - } - - datepicker - .append(mounth_picker) - .append(calendar) - .append(applyButton); - - $(options.parentID) - .append(datetimepicker); - - XDSoft_datetime = function () { - var _this = this; - _this.now = function (norecursion) { - var d = new Date(), - date, - time; - - if (!norecursion && options.defaultDate) { - date = _this.strToDateTime(options.defaultDate); - d.setFullYear(date.getFullYear()); - d.setMonth(date.getMonth()); - d.setDate(date.getDate()); - } - - if (options.yearOffset) { - d.setFullYear(d.getFullYear() + options.yearOffset); - } - - if (!norecursion && options.defaultTime) { - time = _this.strtotime(options.defaultTime); - d.setHours(time.getHours()); - d.setMinutes(time.getMinutes()); - } - return d; - }; - - _this.isValidDate = function (d) { - if (Object.prototype.toString.call(d) !== "[object Date]") { - return false; - } - return !isNaN(d.getTime()); - }; - - _this.setCurrentTime = function (dTime) { - _this.currentTime = (typeof dTime === 'string') ? _this.strToDateTime(dTime) : _this.isValidDate(dTime) ? dTime : _this.now(); - datetimepicker.trigger('xchange.xdsoft'); - }; - - _this.empty = function () { - _this.currentTime = null; - }; - - _this.getCurrentTime = function (dTime) { - return _this.currentTime; - }; - - _this.nextMonth = function () { - - if (_this.currentTime === undefined || _this.currentTime === null) { - _this.currentTime = _this.now(); - } - - var month = _this.currentTime.getMonth() + 1, - year; - if (month === 12) { - _this.currentTime.setFullYear(_this.currentTime.getFullYear() + 1); - month = 0; - } - - year = _this.currentTime.getFullYear(); - - _this.currentTime.setDate( - Math.min( - new Date(_this.currentTime.getFullYear(), month + 1, 0).getDate(), - _this.currentTime.getDate() - ) - ); - _this.currentTime.setMonth(month); - - if (options.onChangeMonth && $.isFunction(options.onChangeMonth)) { - options.onChangeMonth.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); - } - - if (year !== _this.currentTime.getFullYear() && $.isFunction(options.onChangeYear)) { - options.onChangeYear.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); - } - - datetimepicker.trigger('xchange.xdsoft'); - return month; - }; - - _this.prevMonth = function () { - - if (_this.currentTime === undefined || _this.currentTime === null) { - _this.currentTime = _this.now(); - } - - var month = _this.currentTime.getMonth() - 1; - if (month === -1) { - _this.currentTime.setFullYear(_this.currentTime.getFullYear() - 1); - month = 11; - } - _this.currentTime.setDate( - Math.min( - new Date(_this.currentTime.getFullYear(), month + 1, 0).getDate(), - _this.currentTime.getDate() - ) - ); - _this.currentTime.setMonth(month); - if (options.onChangeMonth && $.isFunction(options.onChangeMonth)) { - options.onChangeMonth.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); - } - datetimepicker.trigger('xchange.xdsoft'); - return month; - }; - - _this.getWeekOfYear = function (datetime) { - var onejan = new Date(datetime.getFullYear(), 0, 1); - return Math.ceil((((datetime - onejan) / 86400000) + onejan.getDay() + 1) / 7); - }; - - _this.strToDateTime = function (sDateTime) { - var tmpDate = [], timeOffset, currentTime; - - if (sDateTime && sDateTime instanceof Date && _this.isValidDate(sDateTime)) { - return sDateTime; - } - - tmpDate = /^(\+|\-)(.*)$/.exec(sDateTime); - if (tmpDate) { - tmpDate[2] = Date.parseDate(tmpDate[2], options.formatDate); - } - if (tmpDate && tmpDate[2]) { - timeOffset = tmpDate[2].getTime() - (tmpDate[2].getTimezoneOffset()) * 60000; - currentTime = new Date((_this.now(true)).getTime() + parseInt(tmpDate[1] + '1', 10) * timeOffset); - } else { - currentTime = sDateTime ? Date.parseDate(sDateTime, options.format) : _this.now(); - } - - if (!_this.isValidDate(currentTime)) { - currentTime = _this.now(); - } - - return currentTime; - }; - - _this.strToDate = function (sDate) { - if (sDate && sDate instanceof Date && _this.isValidDate(sDate)) { - return sDate; - } - - var currentTime = sDate ? Date.parseDate(sDate, options.formatDate) : _this.now(true); - if (!_this.isValidDate(currentTime)) { - currentTime = _this.now(true); - } - return currentTime; - }; - - _this.strtotime = function (sTime) { - if (sTime && sTime instanceof Date && _this.isValidDate(sTime)) { - return sTime; - } - var currentTime = sTime ? Date.parseDate(sTime, options.formatTime) : _this.now(true); - if (!_this.isValidDate(currentTime)) { - currentTime = _this.now(true); - } - return currentTime; - }; - - _this.str = function () { - return _this.currentTime.dateFormat(options.format); - }; - _this.currentTime = this.now(); - }; - - _xdsoft_datetime = new XDSoft_datetime(); - - applyButton.on('click', function (e) {//pathbrite - e.preventDefault(); - datetimepicker.data('changed', true); - _xdsoft_datetime.setCurrentTime(getCurrentValue()); - input.val(_xdsoft_datetime.str()); - datetimepicker.trigger('close.xdsoft'); - }); - mounth_picker - .find('.xdsoft_today_button') - .on('mousedown.xdsoft', function () { - datetimepicker.data('changed', true); - _xdsoft_datetime.setCurrentTime(0); - datetimepicker.trigger('afterOpen.xdsoft'); - }).on('dblclick.xdsoft', function () { - var currentDate = _xdsoft_datetime.getCurrentTime(), minDate, maxDate; - currentDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate()); - minDate = _xdsoft_datetime.strToDate(options.minDate); - minDate = new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate()); - if (currentDate < minDate) { - return; - } - maxDate = _xdsoft_datetime.strToDate(options.maxDate); - maxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate()); - if (currentDate > maxDate) { - return; - } - input.val(_xdsoft_datetime.str()); - input.trigger('change'); - datetimepicker.trigger('close.xdsoft'); - }); - mounth_picker - .find('.xdsoft_prev,.xdsoft_next') - .on('mousedown.xdsoft', function () { - var $this = $(this), - timer = 0, - stop = false; - - (function arguments_callee1(v) { - if ($this.hasClass(options.next)) { - _xdsoft_datetime.nextMonth(); - } else if ($this.hasClass(options.prev)) { - _xdsoft_datetime.prevMonth(); - } - if (options.monthChangeSpinner) { - if (!stop) { - timer = setTimeout(arguments_callee1, v || 100); - } - } - }(500)); - - $([document.body, window]).on('mouseup.xdsoft', function arguments_callee2() { - clearTimeout(timer); - stop = true; - $([document.body, window]).off('mouseup.xdsoft', arguments_callee2); - }); - }); - - timepicker - .find('.xdsoft_prev,.xdsoft_next') - .on('mousedown.xdsoft', function () { - var $this = $(this), - timer = 0, - stop = false, - period = 110; - (function arguments_callee4(v) { - var pheight = timeboxparent[0].clientHeight, - height = timebox[0].offsetHeight, - top = Math.abs(parseInt(timebox.css('marginTop'), 10)); - if ($this.hasClass(options.next) && (height - pheight) - options.timeHeightInTimePicker >= top) { - timebox.css('marginTop', '-' + (top + options.timeHeightInTimePicker) + 'px'); - } else if ($this.hasClass(options.prev) && top - options.timeHeightInTimePicker >= 0) { - timebox.css('marginTop', '-' + (top - options.timeHeightInTimePicker) + 'px'); - } - timeboxparent.trigger('scroll_element.xdsoft_scroller', [Math.abs(parseInt(timebox.css('marginTop'), 10) / (height - pheight))]); - period = (period > 10) ? 10 : period - 10; - if (!stop) { - timer = setTimeout(arguments_callee4, v || period); - } - }(500)); - $([document.body, window]).on('mouseup.xdsoft', function arguments_callee5() { - clearTimeout(timer); - stop = true; - $([document.body, window]) - .off('mouseup.xdsoft', arguments_callee5); - }); - }); - - xchangeTimer = 0; - // base handler - generating a calendar and timepicker - datetimepicker - .on('xchange.xdsoft', function (event) { - clearTimeout(xchangeTimer); - xchangeTimer = setTimeout(function () { - - if (_xdsoft_datetime.currentTime === undefined || _xdsoft_datetime.currentTime === null) { - _xdsoft_datetime.currentTime = _xdsoft_datetime.now(); - } - - var table = '', - start = new Date(_xdsoft_datetime.currentTime.getFullYear(), _xdsoft_datetime.currentTime.getMonth(), 1, 12, 0, 0), - i = 0, - j, - today = _xdsoft_datetime.now(), - maxDate = false, - minDate = false, - hDate, - day, - d, - y, - m, - w, - classes = [], - customDateSettings, - newRow = true, - time = '', - h = '', - line_time, - description; - - while (start.getDay() !== options.dayOfWeekStart) { - start.setDate(start.getDate() - 1); - } - - table += ''; - - if (options.weeks) { - table += ''; - } - - for (j = 0; j < 7; j += 1) { - table += ''; - } - - table += ''; - table += ''; - - if (options.maxDate !== false) { - maxDate = _xdsoft_datetime.strToDate(options.maxDate); - maxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate(), 23, 59, 59, 999); - } - - if (options.minDate !== false) { - minDate = _xdsoft_datetime.strToDate(options.minDate); - minDate = new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate()); - } - - while (i < _xdsoft_datetime.currentTime.countDaysInMonth() || start.getDay() !== options.dayOfWeekStart || _xdsoft_datetime.currentTime.getMonth() === start.getMonth()) { - classes = []; - i += 1; - - day = start.getDay(); - d = start.getDate(); - y = start.getFullYear(); - m = start.getMonth(); - w = _xdsoft_datetime.getWeekOfYear(start); - description = ''; - - classes.push('xdsoft_date'); - - if (options.beforeShowDay && $.isFunction(options.beforeShowDay.call)) { - customDateSettings = options.beforeShowDay.call(datetimepicker, start); - } else { - customDateSettings = null; - } - - if ((maxDate !== false && start > maxDate) || (minDate !== false && start < minDate) || (customDateSettings && customDateSettings[0] === false)) { - classes.push('xdsoft_disabled'); - } else if (options.disabledDates.indexOf(start.dateFormat(options.formatDate)) !== -1) { - classes.push('xdsoft_disabled'); - } else if (options.disabledWeekDays.indexOf(day) !== -1) { - classes.push('xdsoft_disabled'); - } - - if (customDateSettings && customDateSettings[1] !== "") { - classes.push(customDateSettings[1]); - } - - if (_xdsoft_datetime.currentTime.getMonth() !== m) { - classes.push('xdsoft_other_month'); - } - - if ((options.defaultSelect || datetimepicker.data('changed')) && _xdsoft_datetime.currentTime.dateFormat(options.formatDate) === start.dateFormat(options.formatDate)) { - classes.push('xdsoft_current'); - } - - if (today.dateFormat(options.formatDate) === start.dateFormat(options.formatDate)) { - classes.push('xdsoft_today'); - } - - if (start.getDay() === 0 || start.getDay() === 6 || options.weekends.indexOf(start.dateFormat(options.formatDate)) !== -1) { - classes.push('xdsoft_weekend'); - } - - if (options.highlightedDates[start.dateFormat(options.formatDate)] !== undefined) { - hDate = options.highlightedDates[start.dateFormat(options.formatDate)]; - classes.push(hDate.style === undefined ? 'xdsoft_highlighted_default' : hDate.style); - description = hDate.desc === undefined ? '' : hDate.desc; - } - - if (options.beforeShowDay && $.isFunction(options.beforeShowDay)) { - classes.push(options.beforeShowDay(start)); - } - - if (newRow) { - table += ''; - newRow = false; - if (options.weeks) { - table += ''; - } - } - - table += ''; - - if (start.getDay() === options.dayOfWeekStartPrev) { - table += ''; - newRow = true; - } - - start.setDate(d + 1); - } - table += '
' + options.i18n[options.lang].dayOfWeek[(j + options.dayOfWeekStart) % 7] + '
' + w + '' + - '
' + d + '
' + - '
'; - - calendar.html(table); - - mounth_picker.find('.xdsoft_label span').eq(0).text(options.i18n[options.lang].months[_xdsoft_datetime.currentTime.getMonth()]); - mounth_picker.find('.xdsoft_label span').eq(1).text(_xdsoft_datetime.currentTime.getFullYear()); - - // generate timebox - time = ''; - h = ''; - m = ''; - line_time = function line_time(h, m) { - var now = _xdsoft_datetime.now(), optionDateTime, current_time; - now.setHours(h); - h = parseInt(now.getHours(), 10); - now.setMinutes(m); - m = parseInt(now.getMinutes(), 10); - optionDateTime = new Date(_xdsoft_datetime.currentTime); - optionDateTime.setHours(h); - optionDateTime.setMinutes(m); - classes = []; - if ((options.minDateTime !== false && options.minDateTime > optionDateTime) || (options.maxTime !== false && _xdsoft_datetime.strtotime(options.maxTime).getTime() < now.getTime()) || (options.minTime !== false && _xdsoft_datetime.strtotime(options.minTime).getTime() > now.getTime())) { - classes.push('xdsoft_disabled'); - } - if ((options.minDateTime !== false && options.minDateTime > optionDateTime) || ((options.disabledMinTime !== false && now.getTime() > _xdsoft_datetime.strtotime(options.disabledMinTime).getTime()) && (options.disabledMaxTime !== false && now.getTime() < _xdsoft_datetime.strtotime(options.disabledMaxTime).getTime()))) { - classes.push('xdsoft_disabled'); - } - - current_time = new Date(_xdsoft_datetime.currentTime); - current_time.setHours(parseInt(_xdsoft_datetime.currentTime.getHours(), 10)); - current_time.setMinutes(Math[options.roundTime](_xdsoft_datetime.currentTime.getMinutes() / options.step) * options.step); - - if ((options.initTime || options.defaultSelect || datetimepicker.data('changed')) && current_time.getHours() === parseInt(h, 10) && (options.step > 59 || current_time.getMinutes() === parseInt(m, 10))) { - if (options.defaultSelect || datetimepicker.data('changed')) { - classes.push('xdsoft_current'); - } else if (options.initTime) { - classes.push('xdsoft_init_time'); - } - } - if (parseInt(today.getHours(), 10) === parseInt(h, 10) && parseInt(today.getMinutes(), 10) === parseInt(m, 10)) { - classes.push('xdsoft_today'); - } - time += '
' + now.dateFormat(options.formatTime) + '
'; - }; - - if (!options.allowTimes || !$.isArray(options.allowTimes) || !options.allowTimes.length) { - for (i = 0, j = 0; i < (options.hours12 ? 12 : 24); i += 1) { - for (j = 0; j < 60; j += options.step) { - h = (i < 10 ? '0' : '') + i; - m = (j < 10 ? '0' : '') + j; - line_time(h, m); - } - } - } else { - for (i = 0; i < options.allowTimes.length; i += 1) { - h = _xdsoft_datetime.strtotime(options.allowTimes[i]).getHours(); - m = _xdsoft_datetime.strtotime(options.allowTimes[i]).getMinutes(); - line_time(h, m); - } - } - - timebox.html(time); - - opt = ''; - i = 0; - - for (i = parseInt(options.yearStart, 10) + options.yearOffset; i <= parseInt(options.yearEnd, 10) + options.yearOffset; i += 1) { - opt += '
' + i + '
'; - } - yearselect.children().eq(0) - .html(opt); - - for (i = parseInt(options.monthStart, 10), opt = ''; i <= parseInt(options.monthEnd, 10); i += 1) { - opt += '
' + options.i18n[options.lang].months[i] + '
'; - } - monthselect.children().eq(0).html(opt); - $(datetimepicker) - .trigger('generate.xdsoft'); - }, 10); - event.stopPropagation(); - }) - .on('afterOpen.xdsoft', function () { - if (options.timepicker) { - var classType, pheight, height, top; - if (timebox.find('.xdsoft_current').length) { - classType = '.xdsoft_current'; - } else if (timebox.find('.xdsoft_init_time').length) { - classType = '.xdsoft_init_time'; - } - if (classType) { - pheight = timeboxparent[0].clientHeight; - height = timebox[0].offsetHeight; - top = timebox.find(classType).index() * options.timeHeightInTimePicker + 1; - if ((height - pheight) < top) { - top = height - pheight; - } - timeboxparent.trigger('scroll_element.xdsoft_scroller', [parseInt(top, 10) / (height - pheight)]); - } else { - timeboxparent.trigger('scroll_element.xdsoft_scroller', [0]); - } - } - }); - - timerclick = 0; - calendar - .on('click.xdsoft', 'td', function (xdevent) { - xdevent.stopPropagation(); // Prevents closing of Pop-ups, Modals and Flyouts in Bootstrap - timerclick += 1; - var $this = $(this), - currentTime = _xdsoft_datetime.currentTime; - - if (currentTime === undefined || currentTime === null) { - _xdsoft_datetime.currentTime = _xdsoft_datetime.now(); - currentTime = _xdsoft_datetime.currentTime; - } - - if ($this.hasClass('xdsoft_disabled')) { - return false; - } - - currentTime.setDate(1); - currentTime.setFullYear($this.data('year')); - currentTime.setMonth($this.data('month')); - currentTime.setDate($this.data('date')); - - datetimepicker.trigger('select.xdsoft', [currentTime]); - - input.val(_xdsoft_datetime.str()); - if ((timerclick > 1 || (options.closeOnDateSelect === true || (options.closeOnDateSelect === false && !options.timepicker))) && !options.inline) { - datetimepicker.trigger('close.xdsoft'); - } - - if (options.onSelectDate && $.isFunction(options.onSelectDate)) { - options.onSelectDate.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), xdevent); - } - - datetimepicker.data('changed', true); - datetimepicker.trigger('xchange.xdsoft'); - datetimepicker.trigger('changedatetime.xdsoft'); - setTimeout(function () { - timerclick = 0; - }, 200); - }); - - timebox - .on('click.xdsoft', 'div', function (xdevent) { - xdevent.stopPropagation(); - var $this = $(this), - currentTime = _xdsoft_datetime.currentTime; - - if (currentTime === undefined || currentTime === null) { - _xdsoft_datetime.currentTime = _xdsoft_datetime.now(); - currentTime = _xdsoft_datetime.currentTime; - } - - if ($this.hasClass('xdsoft_disabled')) { - return false; - } - currentTime.setHours($this.data('hour')); - currentTime.setMinutes($this.data('minute')); - datetimepicker.trigger('select.xdsoft', [currentTime]); - - datetimepicker.data('input').val(_xdsoft_datetime.str()); - - if (options.inline !== true && options.closeOnTimeSelect === true) { - datetimepicker.trigger('close.xdsoft'); - } - - if (options.onSelectTime && $.isFunction(options.onSelectTime)) { - options.onSelectTime.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), xdevent); - } - datetimepicker.data('changed', true); - datetimepicker.trigger('xchange.xdsoft'); - datetimepicker.trigger('changedatetime.xdsoft'); - }); - - - datepicker - .on('mousewheel.xdsoft', function (event) { - if (!options.scrollMonth) { - return true; - } - if (event.deltaY < 0) { - _xdsoft_datetime.nextMonth(); - } else { - _xdsoft_datetime.prevMonth(); - } - return false; - }); - - input - .on('mousewheel.xdsoft', function (event) { - if (!options.scrollInput) { - return true; - } - if (!options.datepicker && options.timepicker) { - current_time_index = timebox.find('.xdsoft_current').length ? timebox.find('.xdsoft_current').eq(0).index() : 0; - if (current_time_index + event.deltaY >= 0 && current_time_index + event.deltaY < timebox.children().length) { - current_time_index += event.deltaY; - } - if (timebox.children().eq(current_time_index).length) { - timebox.children().eq(current_time_index).trigger('mousedown'); - } - return false; - } - if (options.datepicker && !options.timepicker) { - datepicker.trigger(event, [event.deltaY, event.deltaX, event.deltaY]); - if (input.val) { - input.val(_xdsoft_datetime.str()); - } - datetimepicker.trigger('changedatetime.xdsoft'); - return false; - } - }); - - datetimepicker - .on('changedatetime.xdsoft', function (event) { - if (options.onChangeDateTime && $.isFunction(options.onChangeDateTime)) { - var $input = datetimepicker.data('input'); - options.onChangeDateTime.call(datetimepicker, _xdsoft_datetime.currentTime, $input, event); - delete options.value; - $input.trigger('change'); - } - }) - .on('generate.xdsoft', function () { - if (options.onGenerate && $.isFunction(options.onGenerate)) { - options.onGenerate.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input')); - } - if (triggerAfterOpen) { - datetimepicker.trigger('afterOpen.xdsoft'); - triggerAfterOpen = false; - } - }) - .on('click.xdsoft', function (xdevent) { - xdevent.stopPropagation(); - }); - - current_time_index = 0; - - setPos = function () { - var offset = datetimepicker.data('input').offset(), top = offset.top + datetimepicker.data('input')[0].offsetHeight - 1, left = offset.left, position = "absolute", node; - if (datetimepicker.data('input').parent().css('direction') == 'rtl') - left -= (datetimepicker.outerWidth() - datetimepicker.data('input').outerWidth()); - if (options.fixed) { - top -= $(window).scrollTop(); - left -= $(window).scrollLeft(); - position = "fixed"; - } else { - if (top + datetimepicker[0].offsetHeight > $(window).height() + $(window).scrollTop()) { - top = offset.top - datetimepicker[0].offsetHeight + 1; - } - if (top < 0) { - top = 0; - } - if (left + datetimepicker[0].offsetWidth > $(window).width()) { - left = $(window).width() - datetimepicker[0].offsetWidth; - } - } - - node = datetimepicker[0]; - do { - node = node.parentNode; - if (window.getComputedStyle(node).getPropertyValue('position') === 'relative' && $(window).width() >= node.offsetWidth) { - left = left - (($(window).width() - node.offsetWidth) / 2); - break; - } - } while (node.nodeName !== 'HTML'); - datetimepicker.css({ - left: left, - top: top, - position: position - }); - }; - datetimepicker - .on('open.xdsoft', function (event) { - var onShow = true; - if (options.onShow && $.isFunction(options.onShow)) { - onShow = options.onShow.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), event); - } - if (onShow !== false) { - datetimepicker.show(); - setPos(); - $(window) - .off('resize.xdsoft', setPos) - .on('resize.xdsoft', setPos); - - if (options.closeOnWithoutClick) { - $([document.body, window]).on('mousedown.xdsoft', function arguments_callee6() { - datetimepicker.trigger('close.xdsoft'); - $([document.body, window]).off('mousedown.xdsoft', arguments_callee6); - }); - } - } - }) - .on('close.xdsoft', function (event) { - var onClose = true; - mounth_picker - .find('.xdsoft_month,.xdsoft_year') - .find('.xdsoft_select') - .hide(); - if (options.onClose && $.isFunction(options.onClose)) { - onClose = options.onClose.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), event); - } - if (onClose !== false && !options.opened && !options.inline) { - datetimepicker.hide(); - } - event.stopPropagation(); - }) - .on('toggle.xdsoft', function (event) { - if (datetimepicker.is(':visible')) { - datetimepicker.trigger('close.xdsoft'); - } else { - datetimepicker.trigger('open.xdsoft'); - } - }) - .data('input', input); - - timer = 0; - timer1 = 0; - - datetimepicker.data('xdsoft_datetime', _xdsoft_datetime); - datetimepicker.setOptions(options); - - function getCurrentValue() { - var ct = false, time; - - if (options.startDate) { - ct = _xdsoft_datetime.strToDate(options.startDate); - } else { - ct = options.value || ((input && input.val && input.val()) ? input.val() : ''); - if (ct) { - ct = _xdsoft_datetime.strToDateTime(ct); - } else if (options.defaultDate) { - ct = _xdsoft_datetime.strToDateTime(options.defaultDate); - if (options.defaultTime) { - time = _xdsoft_datetime.strtotime(options.defaultTime); - ct.setHours(time.getHours()); - ct.setMinutes(time.getMinutes()); - } - } - } - - if (ct && _xdsoft_datetime.isValidDate(ct)) { - datetimepicker.data('changed', true); - } else { - ct = ''; - } - - return ct || 0; - } - - _xdsoft_datetime.setCurrentTime(getCurrentValue()); - - input - .data('xdsoft_datetimepicker', datetimepicker) - .on('open.xdsoft focusin.xdsoft mousedown.xdsoft', function (event) { - if (input.is(':disabled') || (input.data('xdsoft_datetimepicker').is(':visible') && options.closeOnInputClick)) { - return; - } - clearTimeout(timer); - timer = setTimeout(function () { - if (input.is(':disabled')) { - return; - } - - triggerAfterOpen = true; - _xdsoft_datetime.setCurrentTime(getCurrentValue()); - - datetimepicker.trigger('open.xdsoft'); - }, 100); - }) - .on('keydown.xdsoft', function (event) { - var val = this.value, elementSelector, - key = event.which; - if ([ENTER].indexOf(key) !== -1 && options.enterLikeTab) { - elementSelector = $("input:visible,textarea:visible"); - datetimepicker.trigger('close.xdsoft'); - elementSelector.eq(elementSelector.index(this) + 1).focus(); - return false; - } - if ([TAB].indexOf(key) !== -1) { - datetimepicker.trigger('close.xdsoft'); - return true; - } - }); - }; - destroyDateTimePicker = function (input) { - var datetimepicker = input.data('xdsoft_datetimepicker'); - if (datetimepicker) { - datetimepicker.data('xdsoft_datetime', null); - datetimepicker.remove(); - input - .data('xdsoft_datetimepicker', null) - .off('.xdsoft'); - $(window).off('resize.xdsoft'); - $([window, document.body]).off('mousedown.xdsoft'); - if (input.unmousewheel) { - input.unmousewheel(); - } - } - }; - $(document) - .off('keydown.xdsoftctrl keyup.xdsoftctrl') - .on('keydown.xdsoftctrl', function (e) { - if (e.keyCode === CTRLKEY) { - ctrlDown = true; - } - }) - .on('keyup.xdsoftctrl', function (e) { - if (e.keyCode === CTRLKEY) { - ctrlDown = false; - } - }); - return this.each(function () { - var datetimepicker = $(this).data('xdsoft_datetimepicker'), $input; - if (datetimepicker) { - if ($.type(opt) === 'string') { - switch (opt) { - case 'show': - $(this).select().focus(); - datetimepicker.trigger('open.xdsoft'); - break; - case 'hide': - datetimepicker.trigger('close.xdsoft'); - break; - case 'toggle': - datetimepicker.trigger('toggle.xdsoft'); - break; - case 'destroy': - destroyDateTimePicker($(this)); - break; - case 'reset': - this.value = this.defaultValue; - if (!this.value || !datetimepicker.data('xdsoft_datetime').isValidDate(Date.parseDate(this.value, options.format))) { - datetimepicker.data('changed', false); - } - datetimepicker.data('xdsoft_datetime').setCurrentTime(this.value); - break; - case 'validate': - $input = datetimepicker.data('input'); - $input.trigger('blur.xdsoft'); - break; - } - } else { - datetimepicker - .setOptions(opt); - } - return 0; - } - if ($.type(opt) !== 'string') { - if (!options.lazyInit || options.open || options.inline) { - createDateTimePicker($(this)); - } else { - lazyInit($(this)); - } - } - }); - }; - $.fn.datetimepicker.defaults = default_options; -}(jQuery)); - -function HighlightedDate(date, desc, style) { - "use strict"; - this.date = date; - this.desc = desc; - this.style = style; -} - -(function () { - - /*! Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh) - * Licensed under the MIT License (LICENSE.txt). - * - * Version: 3.1.12 - * - * Requires: jQuery 1.2.2+ - */ - !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?module.exports=a:a(jQuery)}(function(a){function b(b){var g=b||window.event,h=i.call(arguments,1),j=0,l=0,m=0,n=0,o=0,p=0;if(b=a.event.fix(g),b.type="mousewheel","detail"in g&&(m=-1*g.detail),"wheelDelta"in g&&(m=g.wheelDelta),"wheelDeltaY"in g&&(m=g.wheelDeltaY),"wheelDeltaX"in g&&(l=-1*g.wheelDeltaX),"axis"in g&&g.axis===g.HORIZONTAL_AXIS&&(l=-1*m,m=0),j=0===m?l:m,"deltaY"in g&&(m=-1*g.deltaY,j=m),"deltaX"in g&&(l=g.deltaX,0===m&&(j=-1*l)),0!==m||0!==l){if(1===g.deltaMode){var q=a.data(this,"mousewheel-line-height");j*=q,m*=q,l*=q}else if(2===g.deltaMode){var r=a.data(this,"mousewheel-page-height");j*=r,m*=r,l*=r}if(n=Math.max(Math.abs(m),Math.abs(l)),(!f||f>n)&&(f=n,d(g,n)&&(f/=40)),d(g,n)&&(j/=40,l/=40,m/=40),j=Math[j>=1?"floor":"ceil"](j/f),l=Math[l>=1?"floor":"ceil"](l/f),m=Math[m>=1?"floor":"ceil"](m/f),k.settings.normalizeOffset&&this.getBoundingClientRect){var s=this.getBoundingClientRect();o=b.clientX-s.left,p=b.clientY-s.top}return b.deltaX=l,b.deltaY=m,b.deltaFactor=f,b.offsetX=o,b.offsetY=p,b.deltaMode=0,h.unshift(b,j,l,m),e&&clearTimeout(e),e=setTimeout(c,200),(a.event.dispatch||a.event.handle).apply(this,h)}}function c(){f=null}function d(a,b){return k.settings.adjustOldDeltas&&"mousewheel"===a.type&&b%120===0}var e,f,g=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],h="onwheel"in document||document.documentMode>=9?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],i=Array.prototype.slice;if(a.event.fixHooks)for(var j=g.length;j;)a.event.fixHooks[g[--j]]=a.event.mouseHooks;var k=a.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var c=h.length;c;)this.addEventListener(h[--c],b,!1);else this.onmousewheel=b;a.data(this,"mousewheel-line-height",k.getLineHeight(this)),a.data(this,"mousewheel-page-height",k.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var c=h.length;c;)this.removeEventListener(h[--c],b,!1);else this.onmousewheel=null;a.removeData(this,"mousewheel-line-height"),a.removeData(this,"mousewheel-page-height")},getLineHeight:function(b){var c=a(b),d=c["offsetParent"in a.fn?"offsetParent":"parent"]();return d.length||(d=a("body")),parseInt(d.css("fontSize"),10)||parseInt(c.css("fontSize"),10)||16},getPageHeight:function(b){return a(b).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};a.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})}); - -// Parse and Format Library -//http://www.xaprb.com/blog/2005/12/12/javascript-closures-for-runtime-efficiency/ - /* - * Copyright (C) 2004 Baron Schwartz - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by the - * Free Software Foundation, version 2.1. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - */ - Date.parseFunctions={count:0};Date.parseRegexes=[];Date.formatFunctions={count:0};Date.prototype.dateFormat=function(b){if(b=="unixtime"){return parseInt(this.getTime()/1000);}if(Date.formatFunctions[b]==null){Date.createNewFormat(b);}var a=Date.formatFunctions[b];return this[a]();};Date.createNewFormat=function(format){var funcName="format"+Date.formatFunctions.count++;Date.formatFunctions[format]=funcName;var codePrefix="Date.prototype."+funcName+" = function() {return ";var code="";var special=false;var ch="";for(var i=0;i 0) {";var regex="";var special=false;var ch="";for(var i=0;i 0 && z > 0){\nvar doyDate = new Date(y,0);\ndoyDate.setDate(z);\nm = doyDate.getMonth();\nd = doyDate.getDate();\n}";code+="if (y > 0 && m >= 0 && d > 0 && h >= 0 && i >= 0 && s >= 0)\n{return new Date(y, m, d, h, i, s);}\nelse if (y > 0 && m >= 0 && d > 0 && h >= 0 && i >= 0)\n{return new Date(y, m, d, h, i);}\nelse if (y > 0 && m >= 0 && d > 0 && h >= 0)\n{return new Date(y, m, d, h);}\nelse if (y > 0 && m >= 0 && d > 0)\n{return new Date(y, m, d);}\nelse if (y > 0 && m >= 0)\n{return new Date(y, m);}\nelse if (y > 0)\n{return new Date(y);}\n}return null;}";Date.parseRegexes[regexNum]=new RegExp("^"+regex+"$",'i');eval(code);};Date.formatCodeToRegex=function(b,a){switch(b){case"D":return{g:0,c:null,s:"(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)"};case"j":case"d":return{g:1,c:"d = parseInt(results["+a+"], 10);\n",s:"(\\d{1,2})"};case"l":return{g:0,c:null,s:"(?:"+Date.dayNames.join("|")+")"};case"S":return{g:0,c:null,s:"(?:st|nd|rd|th)"};case"w":return{g:0,c:null,s:"\\d"};case"z":return{g:1,c:"z = parseInt(results["+a+"], 10);\n",s:"(\\d{1,3})"};case"W":return{g:0,c:null,s:"(?:\\d{2})"};case"F":return{g:1,c:"m = parseInt(Date.monthNumbers[results["+a+"].substring(0, 3)], 10);\n",s:"("+Date.monthNames.join("|")+")"};case"M":return{g:1,c:"m = parseInt(Date.monthNumbers[results["+a+"]], 10);\n",s:"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)"};case"n":case"m":return{g:1,c:"m = parseInt(results["+a+"], 10) - 1;\n",s:"(\\d{1,2})"};case"t":return{g:0,c:null,s:"\\d{1,2}"};case"L":return{g:0,c:null,s:"(?:1|0)"};case"Y":return{g:1,c:"y = parseInt(results["+a+"], 10);\n",s:"(\\d{4})"};case"y":return{g:1,c:"var ty = parseInt(results["+a+"], 10);\ny = ty > Date.y2kYear ? 1900 + ty : 2000 + ty;\n",s:"(\\d{1,2})"};case"a":return{g:1,c:"if (results["+a+"] == 'am') {\nif (h == 12) { h = 0; }\n} else { if (h < 12) { h += 12; }}",s:"(am|pm)"};case"A":return{g:1,c:"if (results["+a+"] == 'AM') {\nif (h == 12) { h = 0; }\n} else { if (h < 12) { h += 12; }}",s:"(AM|PM)"};case"g":case"G":case"h":case"H":return{g:1,c:"h = parseInt(results["+a+"], 10);\n",s:"(\\d{1,2})"};case"i":return{g:1,c:"i = parseInt(results["+a+"], 10);\n",s:"(\\d{2})"};case"s":return{g:1,c:"s = parseInt(results["+a+"], 10);\n",s:"(\\d{2})"};case"O":return{g:0,c:null,s:"[+-]\\d{4}"};case"T":return{g:0,c:null,s:"[A-Z]{3}"};case"Z":return{g:0,c:null,s:"[+-]\\d{1,5}"};default:return{g:0,c:null,s:String.escape(b)};}};Date.prototype.getTimezone=function(){return this.toString().replace(/^.*? ([A-Z]{3}) [0-9]{4}.*$/,"$1").replace(/^.*?\(([A-Z])[a-z]+ ([A-Z])[a-z]+ ([A-Z])[a-z]+\)$/,"$1$2$3");};Date.prototype.getGMTOffset=function(){return(this.getTimezoneOffset()>0?"-":"+")+String.leftPad(Math.floor(Math.abs(this.getTimezoneOffset())/60),2,"0")+String.leftPad(Math.abs(this.getTimezoneOffset())%60,2,"0");};Date.prototype.getDayOfYear=function(){var a=0;Date.daysInMonth[1]=this.isLeapYear()?29:28;for(var b=0;b= 9 ) ? + ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'], + slice = Array.prototype.slice, + nullLowestDeltaTimeout, lowestDelta; + + if ( $.event.fixHooks ) { + for ( var i = toFix.length; i; ) { + $.event.fixHooks[ toFix[--i] ] = $.event.mouseHooks; + } + } + + var special = $.event.special.mousewheel = { + version: '3.1.12', + + setup: function() { + if ( this.addEventListener ) { + for ( var i = toBind.length; i; ) { + this.addEventListener( toBind[--i], handler, false ); + } + } else { + this.onmousewheel = handler; + } + // Store the line height and page height for this particular element + $.data(this, 'mousewheel-line-height', special.getLineHeight(this)); + $.data(this, 'mousewheel-page-height', special.getPageHeight(this)); + }, + + teardown: function() { + if ( this.removeEventListener ) { + for ( var i = toBind.length; i; ) { + this.removeEventListener( toBind[--i], handler, false ); + } + } else { + this.onmousewheel = null; + } + // Clean up the data we added to the element + $.removeData(this, 'mousewheel-line-height'); + $.removeData(this, 'mousewheel-page-height'); + }, + + getLineHeight: function(elem) { + var $elem = $(elem), + $parent = $elem['offsetParent' in $.fn ? 'offsetParent' : 'parent'](); + if (!$parent.length) { + $parent = $('body'); + } + return parseInt($parent.css('fontSize'), 10) || parseInt($elem.css('fontSize'), 10) || 16; + }, + + getPageHeight: function(elem) { + return $(elem).height(); + }, + + settings: { + adjustOldDeltas: true, // see shouldAdjustOldDeltas() below + normalizeOffset: true // calls getBoundingClientRect for each event + } + }; + + $.fn.extend({ + mousewheel: function(fn) { + return fn ? this.bind('mousewheel', fn) : this.trigger('mousewheel'); + }, + + unmousewheel: function(fn) { + return this.unbind('mousewheel', fn); + } + }); + + + function handler(event) { + var orgEvent = event || window.event, + args = slice.call(arguments, 1), + delta = 0, + deltaX = 0, + deltaY = 0, + absDelta = 0, + offsetX = 0, + offsetY = 0; + event = $.event.fix(orgEvent); + event.type = 'mousewheel'; + + // Old school scrollwheel delta + if ( 'detail' in orgEvent ) { deltaY = orgEvent.detail * -1; } + if ( 'wheelDelta' in orgEvent ) { deltaY = orgEvent.wheelDelta; } + if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY; } + if ( 'wheelDeltaX' in orgEvent ) { deltaX = orgEvent.wheelDeltaX * -1; } + + // Firefox < 17 horizontal scrolling related to DOMMouseScroll event + if ( 'axis' in orgEvent && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) { + deltaX = deltaY * -1; + deltaY = 0; + } + + // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy + delta = deltaY === 0 ? deltaX : deltaY; + + // New school wheel delta (wheel event) + if ( 'deltaY' in orgEvent ) { + deltaY = orgEvent.deltaY * -1; + delta = deltaY; + } + if ( 'deltaX' in orgEvent ) { + deltaX = orgEvent.deltaX; + if ( deltaY === 0 ) { delta = deltaX * -1; } + } + + // No change actually happened, no reason to go any further + if ( deltaY === 0 && deltaX === 0 ) { return; } + + // Need to convert lines and pages to pixels if we aren't already in pixels + // There are three delta modes: + // * deltaMode 0 is by pixels, nothing to do + // * deltaMode 1 is by lines + // * deltaMode 2 is by pages + if ( orgEvent.deltaMode === 1 ) { + var lineHeight = $.data(this, 'mousewheel-line-height'); + delta *= lineHeight; + deltaY *= lineHeight; + deltaX *= lineHeight; + } else if ( orgEvent.deltaMode === 2 ) { + var pageHeight = $.data(this, 'mousewheel-page-height'); + delta *= pageHeight; + deltaY *= pageHeight; + deltaX *= pageHeight; + } + + // Store lowest absolute delta to normalize the delta values + absDelta = Math.max( Math.abs(deltaY), Math.abs(deltaX) ); + + if ( !lowestDelta || absDelta < lowestDelta ) { + lowestDelta = absDelta; + + // Adjust older deltas if necessary + if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) { + lowestDelta /= 40; + } + } + + // Adjust older deltas if necessary + if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) { + // Divide all the things by 40! + delta /= 40; + deltaX /= 40; + deltaY /= 40; + } + + // Get a whole, normalized value for the deltas + delta = Math[ delta >= 1 ? 'floor' : 'ceil' ](delta / lowestDelta); + deltaX = Math[ deltaX >= 1 ? 'floor' : 'ceil' ](deltaX / lowestDelta); + deltaY = Math[ deltaY >= 1 ? 'floor' : 'ceil' ](deltaY / lowestDelta); + + // Normalise offsetX and offsetY properties + if ( special.settings.normalizeOffset && this.getBoundingClientRect ) { + var boundingRect = this.getBoundingClientRect(); + offsetX = event.clientX - boundingRect.left; + offsetY = event.clientY - boundingRect.top; + } + + // Add information to the event object + event.deltaX = deltaX; + event.deltaY = deltaY; + event.deltaFactor = lowestDelta; + event.offsetX = offsetX; + event.offsetY = offsetY; + // Go ahead and set deltaMode to 0 since we converted to pixels + // Although this is a little odd since we overwrite the deltaX/Y + // properties with normalized deltas. + event.deltaMode = 0; + + // Add event and delta to the front of the arguments + args.unshift(event, delta, deltaX, deltaY); + + // Clearout lowestDelta after sometime to better + // handle multiple device types that give different + // a different lowestDelta + // Ex: trackpad = 3 and mouse wheel = 120 + if (nullLowestDeltaTimeout) { clearTimeout(nullLowestDeltaTimeout); } + nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200); + + return ($.event.dispatch || $.event.handle).apply(this, args); + } + + function nullLowestDelta() { + lowestDelta = null; + } + + function shouldAdjustOldDeltas(orgEvent, absDelta) { + // If this is an older event and the delta is divisable by 120, + // then we are assuming that the browser is treating this as an + // older mouse wheel event and that we should divide the deltas + // by 40 to try and get a more usable deltaFactor. + // Side note, this actually impacts the reported scroll distance + // in older browsers and can cause scrolling to be slower than native. + // Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false. + return special.settings.adjustOldDeltas && orgEvent.type === 'mousewheel' && absDelta % 120 === 0; + } + +})); diff --git a/app/bundles/CoreBundle/Assets/js/libraries/8a.adminre.js b/app/bundles/CoreBundle/Assets/js/libraries/8a.adminre.js index 00b24c7a48d..c3b22e2225c 100644 --- a/app/bundles/CoreBundle/Assets/js/libraries/8a.adminre.js +++ b/app/bundles/CoreBundle/Assets/js/libraries/8a.adminre.js @@ -375,6 +375,7 @@ if (typeof jQuery === "undefined") { throw new Error("This application requires // clicker $(document).on("change", toggler, function () { + target = $(toggler).data("target"); // checked / unchecked if($(this).is(":checked")) { selectrow(this, "checked"); diff --git a/app/bundles/CoreBundle/Assets/js/libraries/ckeditor/filemanager/scripts/filemanager.config.js b/app/bundles/CoreBundle/Assets/js/libraries/ckeditor/filemanager/scripts/filemanager.config.js index 50570f661cb..fd145036aee 100644 --- a/app/bundles/CoreBundle/Assets/js/libraries/ckeditor/filemanager/scripts/filemanager.config.js +++ b/app/bundles/CoreBundle/Assets/js/libraries/ckeditor/filemanager/scripts/filemanager.config.js @@ -70,7 +70,7 @@ "svg" ], "resize": { - "enabled":true, + "enabled":false, "maxWidth": 1280, "maxHeight": 1024 } diff --git a/app/bundles/CoreBundle/Assets/json/regions.json b/app/bundles/CoreBundle/Assets/json/regions.json index 52d83833903..37753221aee 100644 --- a/app/bundles/CoreBundle/Assets/json/regions.json +++ b/app/bundles/CoreBundle/Assets/json/regions.json @@ -1417,7 +1417,7 @@ "Gash-Barka", "Maakel [Maekel]", "Semenawi Keyih Bahri [Semien-Keih-Bahri]" - ], + ], "Spain": [ "\u00c1lava", "Albacete", @@ -1435,7 +1435,7 @@ "Castell\u00f3n", "Ciudad Real", "Cuenca", - "Cord\u00f3ba", + "C\u00f3rdoba", "Girona [Gerona]", "Granada", "Guadalajara", @@ -4073,4 +4073,4 @@ "St. Eustatius", "St. Maarten" ] -} +} \ No newline at end of file diff --git a/app/bundles/CoreBundle/Command/ModeratedCommand.php b/app/bundles/CoreBundle/Command/ModeratedCommand.php index 3d679c54397..bf8b526cc89 100644 --- a/app/bundles/CoreBundle/Command/ModeratedCommand.php +++ b/app/bundles/CoreBundle/Command/ModeratedCommand.php @@ -15,12 +15,14 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\LockHandler; abstract class ModeratedCommand extends ContainerAwareCommand { - const MODE_LOCK = 'lock'; - const MODE_PID = 'pid'; + const MODE_LOCK = 'lock'; + const MODE_PID = 'pid'; + const MODE_FLOCK = 'flock'; protected $checkFile; protected $moderationKey; @@ -32,6 +34,8 @@ abstract class ModeratedCommand extends ContainerAwareCommand protected $lockFile; private $bypassLocking; + private $flockHandle; + /* @var OutputInterface $output */ protected $output; @@ -54,7 +58,7 @@ protected function configure() '--lock_mode', '-x', InputOption::VALUE_REQUIRED, - 'Force use of PID or FILE LOCK for semaphore. Allowed value are "pid" or "file_lock". By default, lock will try with pid, if not available will use file system', + 'Allowed value are "pid" , "file_lock" or "flock". By default, lock will try with pid, if not available will use file system', 'pid' ); } @@ -72,7 +76,7 @@ protected function checkRunStatus(InputInterface $input, OutputInterface $output $this->bypassLocking = $input->getOption('bypass-locking'); $lockMode = $input->getOption('lock_mode'); - if (!in_array($lockMode, ['pid', 'file_lock'])) { + if (!in_array($lockMode, ['pid', 'file_lock', 'flock'])) { $output->writeln('Unknown locking method specified.'); return false; @@ -125,6 +129,9 @@ protected function completeRun() if (self::MODE_LOCK == $this->moderationMode) { $this->lockHandler->release(); } + if (self::MODE_FLOCK == $this->moderationMode) { + fclose($this->flockHandle); + } // Attempt to keep things tidy @unlink($this->lockFile); @@ -176,6 +183,36 @@ private function checkStatus($force = false, $lockMode = null) return true; } + } elseif ($lockMode === self::MODE_FLOCK && !$force) { + $this->moderationMode = self::MODE_FLOCK; + $error = null; + // Silence error reporting + set_error_handler(function ($errno, $msg) use (&$error) { + $error = $msg; + }); + + if (!$this->flockHandle = fopen($this->lockFile, 'r+') ?: fopen($this->lockFile, 'r')) { + if ($this->flockHandle = fopen($this->lockFile, 'x')) { + chmod($this->lockFile, 0666); + } elseif (!$this->flockHandle = fopen($this->lockFile, 'r+') ?: fopen($this->lockFile, 'r')) { + usleep(100); + $this->flockHandle = fopen($this->lockFile, 'r+') ?: fopen($this->lockFile, 'r'); + } + } + + restore_error_handler(); + + if (!$this->flockHandle) { + throw new IOException($error, 0, null, $this->lockFile); + } + if (!flock($this->flockHandle, LOCK_EX | LOCK_NB)) { + fclose($this->flockHandle); + $this->flockHandle = null; + + return false; + } + + return true; } // in anycase, fallback on file system diff --git a/app/bundles/CoreBundle/Command/UnusedIpDeleteCommand.php b/app/bundles/CoreBundle/Command/UnusedIpDeleteCommand.php new file mode 100644 index 00000000000..6e84fef0e68 --- /dev/null +++ b/app/bundles/CoreBundle/Command/UnusedIpDeleteCommand.php @@ -0,0 +1,70 @@ +setName('mautic:unusedip:delete') + ->setDescription('Deletes IP addresses that are not used in any other database table') + ->addOption( + '--limit', + '-l', + InputOption::VALUE_OPTIONAL, + 'LIMIT for deleted rows', + self::DEFAULT_LIMIT + ) + ->setHelp( + <<<'EOT' + The %command.name% command is used to delete IP addresses that are not used in any other database table. + +php %command.full_name% +EOT + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $em = $this->getContainer()->get('doctrine')->getEntityManager(); + $ipAddressRepo = $em->getRepository('MauticCoreBundle:IpAddress'); + + try { + $limit = $input->getOption('limit'); + $deletedRows = $ipAddressRepo->deleteUnusedIpAddresses($limit); + $output->writeln(sprintf('%s unused IP addresses has been deleted', $deletedRows)); + } catch (DBALException $e) { + $output->writeln(sprintf('Deletion of unused IP addresses failed because of database error: %s', $e->getMessage())); + + return 1; + } + + return 0; + } +} diff --git a/app/bundles/CoreBundle/Config/config.php b/app/bundles/CoreBundle/Config/config.php index bca4b797075..1575ef6ab1c 100644 --- a/app/bundles/CoreBundle/Config/config.php +++ b/app/bundles/CoreBundle/Config/config.php @@ -213,6 +213,17 @@ 'templating.helper.assets', ], ], + 'mautic.core.subscriber.router' => [ + 'class' => \Mautic\CoreBundle\EventListener\RouterSubscriber::class, + 'arguments' => [ + 'router', + '%router.request_context.scheme%', + '%router.request_context.host%', + '%request_listener.https_port%', + '%request_listener.http_port%', + '%router.request_context.base_url%', + ], + ], ], 'forms' => [ 'mautic.form.type.spacer' => [ @@ -277,8 +288,8 @@ ], ], 'mautic.form.type.theme_list' => [ - 'class' => 'Mautic\CoreBundle\Form\Type\ThemeListType', - 'arguments' => 'mautic.factory', + 'class' => \Mautic\CoreBundle\Form\Type\ThemeListType::class, + 'arguments' => ['mautic.helper.theme'], 'alias' => 'theme_list', ], 'mautic.form.type.daterange' => [ @@ -1025,6 +1036,7 @@ 'ip_lookup_service' => 'maxmind_download', 'ip_lookup_auth' => '', 'ip_lookup_config' => [], + 'ip_lookup_create_organization' => false, 'transifex_username' => '', 'transifex_password' => '', 'update_stability' => 'stable', diff --git a/app/bundles/CoreBundle/Controller/ThemeController.php b/app/bundles/CoreBundle/Controller/ThemeController.php index 8b80451d793..08f0cc87320 100644 --- a/app/bundles/CoreBundle/Controller/ThemeController.php +++ b/app/bundles/CoreBundle/Controller/ThemeController.php @@ -122,7 +122,7 @@ public function downloadAction($themeName) $flashes = []; $error = false; - if (!$this->get('mautic.security')->isGranted('core:themes:edit')) { + if (!$this->get('mautic.security')->isGranted('core:themes:view')) { return $this->accessDenied(); } diff --git a/app/bundles/CoreBundle/DependencyInjection/Compiler/EventPass.php b/app/bundles/CoreBundle/DependencyInjection/Compiler/EventPass.php index bf32f823d5d..633f5d9a0d4 100644 --- a/app/bundles/CoreBundle/DependencyInjection/Compiler/EventPass.php +++ b/app/bundles/CoreBundle/DependencyInjection/Compiler/EventPass.php @@ -15,8 +15,8 @@ use Mautic\WebhookBundle\EventListener\WebhookSubscriberBase; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\ExpressionLanguage\Expression; /** * Class EventPass. @@ -40,7 +40,7 @@ public function process(ContainerBuilder $container) $definition->addMethodCall('setRequest', [new Reference('request_stack')]); $definition->addMethodCall('setSecurity', [new Reference('mautic.security')]); $definition->addMethodCall('setSerializer', [new Reference('jms_serializer')]); - $definition->addMethodCall('setSystemParameters', [new Parameter('mautic.parameters')]); + $definition->addMethodCall('setSystemParameters', [new Expression("parameter('mautic.parameters')")]); $definition->addMethodCall('setDispatcher', [new Reference('event_dispatcher')]); $definition->addMethodCall('setTranslator', [new Reference('translator')]); $definition->addMethodCall('setEntityManager', [new Reference('doctrine.orm.entity_manager')]); diff --git a/app/bundles/CoreBundle/Doctrine/Mapping/ClassMetadataBuilder.php b/app/bundles/CoreBundle/Doctrine/Mapping/ClassMetadataBuilder.php index da19428ecbb..f3634e13a10 100644 --- a/app/bundles/CoreBundle/Doctrine/Mapping/ClassMetadataBuilder.php +++ b/app/bundles/CoreBundle/Doctrine/Mapping/ClassMetadataBuilder.php @@ -312,9 +312,9 @@ public function addIpAddress($nullable = false) /** * Add a nullable field. * - * @param $name - * @param string $type - * @param null $columnName + * @param string $name + * @param string $type + * @param string|null $columnName * * @return $this */ diff --git a/app/bundles/CoreBundle/Entity/FormEntity.php b/app/bundles/CoreBundle/Entity/FormEntity.php index 82ab60d73ba..68806d6eb2a 100644 --- a/app/bundles/CoreBundle/Entity/FormEntity.php +++ b/app/bundles/CoreBundle/Entity/FormEntity.php @@ -387,9 +387,9 @@ public function getCheckedOutBy() */ public function setIsPublished($isPublished) { - $this->isChanged('isPublished', $isPublished); + $this->isChanged('isPublished', (bool) $isPublished); - $this->isPublished = $isPublished; + $this->isPublished = (bool) $isPublished; return $this; } diff --git a/app/bundles/CoreBundle/Entity/IpAddress.php b/app/bundles/CoreBundle/Entity/IpAddress.php index e13cde73294..7b7664222f4 100644 --- a/app/bundles/CoreBundle/Entity/IpAddress.php +++ b/app/bundles/CoreBundle/Entity/IpAddress.php @@ -185,22 +185,24 @@ public function isTrackable() { if (!empty($this->doNotTrack)) { foreach ($this->doNotTrack as $ip) { - if (strpos($ip, '/') == false) { - if (preg_match('/'.str_replace('.', '\\.', $ip).'/', $this->ipAddress)) { - return false; - } - } else { + if (strpos($ip, '/') !== false) { // has a netmask range // https://gist.github.com/tott/7684443 list($range, $netmask) = explode('/', $ip, 2); $range_decimal = ip2long($range); - $ip_decimal = ip2long($ip); + $ip_decimal = ip2long($this->ipAddress); $wildcard_decimal = pow(2, (32 - $netmask)) - 1; $netmask_decimal = ~$wildcard_decimal; if ((($ip_decimal & $netmask_decimal) == ($range_decimal & $netmask_decimal))) { return false; } + + continue; + } + + if (preg_match('/'.str_replace('.', '\\.', $ip).'/', $this->ipAddress)) { + return false; } } } diff --git a/app/bundles/CoreBundle/Entity/IpAddressRepository.php b/app/bundles/CoreBundle/Entity/IpAddressRepository.php index 9054c7fd6ef..e0e4abfd338 100644 --- a/app/bundles/CoreBundle/Entity/IpAddressRepository.php +++ b/app/bundles/CoreBundle/Entity/IpAddressRepository.php @@ -33,4 +33,76 @@ public function countIpAddresses() return (int) $results['unique']; } + + /** + * Deletes duplicate IP addresses that are not being used in any other table. + * + * @param int $limit + * + * @return int Number of deleted rows + * + * @throws \Doctrine\DBAL\DBALException + */ + public function deleteUnusedIpAddresses($limit) + { + $prefix = MAUTIC_TABLE_PREFIX; + + $sql = << (int) $limit]; + $types = [':limit' => \PDO::PARAM_INT]; + $stmt = $this->_em->getConnection()->executeQuery($sql, $params, $types); + + $deletedCount = $stmt->rowCount(); + + return $deletedCount; + } } diff --git a/app/bundles/CoreBundle/ErrorHandler/ErrorHandler.php b/app/bundles/CoreBundle/ErrorHandler/ErrorHandler.php index fe6fc5fe308..875785bbba7 100644 --- a/app/bundles/CoreBundle/ErrorHandler/ErrorHandler.php +++ b/app/bundles/CoreBundle/ErrorHandler/ErrorHandler.php @@ -419,11 +419,6 @@ public function setMainLogger($mainLogger) */ protected function log($logLevel, $message, $context = [], $debugTrace = null) { - if ('dev' !== self::$environment) { - // Don't clutter the logs - $context = []; - } - $message = strip_tags($message); if ($this->logger) { if (LogLevel::DEBUG === $logLevel) { @@ -434,7 +429,7 @@ protected function log($logLevel, $message, $context = [], $debugTrace = null) if ($this->debugLogger) { if ($debugTrace) { // Just a snippet - $context['trace'] = array_slice($debugTrace, 1, 5); + $context['trace'] = array_slice($debugTrace, 0, 50); } $this->debugLogger->log($logLevel, $message, $context); } diff --git a/app/bundles/CoreBundle/Event/CustomButtonEvent.php b/app/bundles/CoreBundle/Event/CustomButtonEvent.php index 25dce0b209f..8ca365553ee 100644 --- a/app/bundles/CoreBundle/Event/CustomButtonEvent.php +++ b/app/bundles/CoreBundle/Event/CustomButtonEvent.php @@ -100,9 +100,9 @@ public function addButtons(array $buttons, $location = null, $route = null) /** * Add a single button. * - * @param array $button - * @param null $location - * @param null $route + * @param array $button + * @param string|null $location + * @param string|null $route * * @return $this */ diff --git a/app/bundles/CoreBundle/Event/CustomContentEvent.php b/app/bundles/CoreBundle/Event/CustomContentEvent.php index 002462039ca..6fc06a78f2a 100644 --- a/app/bundles/CoreBundle/Event/CustomContentEvent.php +++ b/app/bundles/CoreBundle/Event/CustomContentEvent.php @@ -84,7 +84,10 @@ public function addContent($content) */ public function addTemplate($template, array $vars = []) { - $this->templates[$template] = $vars; + $this->templates[] = [ + 'template' => $template, + 'vars' => $vars, + ]; } /** diff --git a/app/bundles/CoreBundle/EventListener/BuildJsSubscriber.php b/app/bundles/CoreBundle/EventListener/BuildJsSubscriber.php index 75e8c6b1e04..605c89e854d 100644 --- a/app/bundles/CoreBundle/EventListener/BuildJsSubscriber.php +++ b/app/bundles/CoreBundle/EventListener/BuildJsSubscriber.php @@ -110,7 +110,7 @@ function CustomEvent ( event, params ) { }; MauticJS.setCookie = function(name, value) { - document.cookie = name+"="+value+";"; + document.cookie = name+"="+value+"; path=/"; }; MauticJS.createCORSRequest = function(method, url) { diff --git a/app/bundles/CoreBundle/EventListener/MaintenanceSubscriber.php b/app/bundles/CoreBundle/EventListener/MaintenanceSubscriber.php index d3ed921047c..268a58ea963 100644 --- a/app/bundles/CoreBundle/EventListener/MaintenanceSubscriber.php +++ b/app/bundles/CoreBundle/EventListener/MaintenanceSubscriber.php @@ -83,11 +83,31 @@ private function cleanupData(MaintenanceEvent $event, $table) ->execute() ->fetchColumn(); } else { - $rows = (int) $qb->delete(MAUTIC_TABLE_PREFIX.$table) - ->where( - $qb->expr()->lte('date_added', ':date') - ) - ->execute(); + $qb->select('log.id') + ->from(MAUTIC_TABLE_PREFIX.$table, 'log') + ->where( + $qb->expr()->lte('log.date_added', ':date') + ); + + $rows = 0; + $qb->setMaxResults(10000)->setFirstResult(0); + + $qb2 = $this->db->createQueryBuilder(); + while (true) { + $ids = array_column($qb->execute()->fetchAll(), 'id'); + + if (sizeof($ids) === 0) { + break; + } + + $rows += $qb2->delete(MAUTIC_TABLE_PREFIX.$table) + ->where( + $qb2->expr()->in( + 'id', $ids + ) + ) + ->execute(); + } } $event->setStat($this->translator->trans('mautic.maintenance.'.$table), $rows, $qb->getSQL(), $qb->getParameters()); diff --git a/app/bundles/CoreBundle/EventListener/RouterSubscriber.php b/app/bundles/CoreBundle/EventListener/RouterSubscriber.php new file mode 100644 index 00000000000..134c51f0985 --- /dev/null +++ b/app/bundles/CoreBundle/EventListener/RouterSubscriber.php @@ -0,0 +1,109 @@ +router = $router; + $this->scheme = $scheme; + $this->host = $host; + $this->httpsPort = $httpsPort; + $this->httpPort = $httpPort; + $this->baseUrl = $baseUrl; + } + + /** + * @return array + */ + public static function getSubscribedEvents() + { + return [ + KernelEvents::REQUEST => ['setRouterRequestContext', 1], + ]; + } + + /** + * This forces generated routes to be the same as what is configured as Mautic's site_url + * in order to prevent mismatches between cached URLs generated during web requests and URLs generated + * via CLI/cron jobs. + * + * @param GetResponseEvent $event + */ + public function setRouterRequestContext(GetResponseEvent $event) + { + if (empty($this->host)) { + return; + } + + if (!$event->isMasterRequest()) { + return; + } + + if ('dev' === MAUTIC_ENV) { + $this->baseUrl = $this->baseUrl.'/index_dev.php'; + } + + $context = $this->router->getContext(); + $context->setBaseUrl($this->baseUrl); + $context->setScheme($this->scheme); + $context->setHost($this->host); + $context->setHttpPort($this->httpPort); + $context->setHttpsPort($this->httpsPort); + } +} diff --git a/app/bundles/CoreBundle/Form/RequestTrait.php b/app/bundles/CoreBundle/Form/RequestTrait.php index c038145e671..f245413f78f 100644 --- a/app/bundles/CoreBundle/Form/RequestTrait.php +++ b/app/bundles/CoreBundle/Form/RequestTrait.php @@ -82,7 +82,7 @@ protected function prepareParametersFromRequest(Form $form, array &$params, $ent if ($timestamp) { switch ($type) { case 'datetime': - $params[$name] = (new \DateTime(date('Y-m-d H:i:s', $timestamp)))->format('Y-m-d H:i:s'); + $params[$name] = (new \DateTime(date('Y-m-d H:i:s', $timestamp)))->format('Y-m-d H:i'); break; case 'date': $params[$name] = (new \DateTime(date('Y-m-d', $timestamp)))->format('Y-m-d'); diff --git a/app/bundles/CoreBundle/Form/Type/ConfigType.php b/app/bundles/CoreBundle/Form/Type/ConfigType.php index 636738f895d..5a7719d3c28 100644 --- a/app/bundles/CoreBundle/Form/Type/ConfigType.php +++ b/app/bundles/CoreBundle/Form/Type/ConfigType.php @@ -101,6 +101,8 @@ public function __construct( */ public function buildForm(FormBuilderInterface $builder, array $options) { + $builder->add('last_shown_tab', 'hidden'); + $builder->add( 'site_url', 'text', @@ -500,6 +502,21 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); + $builder->add( + 'ip_lookup_create_organization', + YesNoButtonGroupType::class, + [ + 'label' => 'mautic.core.config.create.organization.from.ip.lookup', + 'label_attr' => ['class' => 'control-label'], + 'attr' => [ + 'class' => 'form-control', + 'tooltip' => 'mautic.core.config.create.organization.from.ip.lookup.tooltip', + ], + 'data' => isset($options['data']['ip_lookup_create_organization']) ? (bool) $options['data']['ip_lookup_create_organization'] : false, + 'required' => false, + ] + ); + $ipLookupFactory = $this->ipLookupFactory; $formModifier = function (FormEvent $event) use ($ipLookupFactory) { $data = $event->getData(); diff --git a/app/bundles/CoreBundle/Form/Type/ThemeListType.php b/app/bundles/CoreBundle/Form/Type/ThemeListType.php index cdcf08734a9..c80a731a814 100644 --- a/app/bundles/CoreBundle/Form/Type/ThemeListType.php +++ b/app/bundles/CoreBundle/Form/Type/ThemeListType.php @@ -11,52 +11,56 @@ namespace Mautic\CoreBundle\Form\Type; -use Mautic\CoreBundle\Factory\MauticFactory; +use Mautic\CoreBundle\Helper\ThemeHelper; use Symfony\Component\Form\AbstractType; use Symfony\Component\OptionsResolver\Options; -use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; /** * Class ThemeListType. */ class ThemeListType extends AbstractType { - private $factory; + /** + * @var ThemeHelper + */ + private $themeHelper; /** - * @param MauticFactory $factory + * ThemeListType constructor. + * + * @param ThemeHelper $helper */ - public function __construct(MauticFactory $factory) + public function __construct(ThemeHelper $helper) { - $this->factory = $factory; + $this->themeHelper = $helper; } /** - * @param OptionsResolverInterface $resolver + * @param OptionsResolver $resolver */ - public function setDefaultOptions(OptionsResolverInterface $resolver) + public function configureOptions(OptionsResolver $resolver) { - $factory = $this->factory; - $resolver->setDefaults([ - 'choices' => function (Options $options) use ($factory) { - $themes = $factory->getInstalledThemes($options['feature']); - $themes['mautic_code_mode'] = 'Code Mode'; - - return $themes; - }, - 'expanded' => false, - 'multiple' => false, - 'label' => 'mautic.core.form.theme', - 'label_attr' => ['class' => 'control-label'], - 'empty_value' => false, - 'required' => false, - 'attr' => [ - 'class' => 'form-control', - ], - 'feature' => 'all', - ]); + $resolver->setDefaults( + [ + 'choices' => function (Options $options) { + $themes = $this->themeHelper->getInstalledThemes($options['feature']); + $themes['mautic_code_mode'] = 'Code Mode'; - $resolver->setOptional(['feature']); + return $themes; + }, + 'expanded' => false, + 'multiple' => false, + 'label' => 'mautic.core.form.theme', + 'label_attr' => ['class' => 'control-label'], + 'empty_value' => false, + 'required' => false, + 'attr' => [ + 'class' => 'form-control', + ], + 'feature' => 'all', + ] + ); } /** diff --git a/app/bundles/CoreBundle/Helper/CacheStorageHelper.php b/app/bundles/CoreBundle/Helper/CacheStorageHelper.php index dacd47893c9..0486ca31e5c 100644 --- a/app/bundles/CoreBundle/Helper/CacheStorageHelper.php +++ b/app/bundles/CoreBundle/Helper/CacheStorageHelper.php @@ -101,6 +101,14 @@ public function __construct($adaptor, $namespace = null, Connection $connection $this->setCacheAdaptor(); } + /** + * @return string|false + */ + public function getAdaptorClassName() + { + return get_class($this->cacheAdaptor); + } + /** * @param $name * @param $data diff --git a/app/bundles/CoreBundle/Helper/ClickthroughHelper.php b/app/bundles/CoreBundle/Helper/ClickthroughHelper.php index c979d4ba746..704e35432e0 100644 --- a/app/bundles/CoreBundle/Helper/ClickthroughHelper.php +++ b/app/bundles/CoreBundle/Helper/ClickthroughHelper.php @@ -31,13 +31,17 @@ public static function encodeArrayForUrl(array $array) * @param $string * @param bool $urlDecode * - * @return mixed + * @return array */ public static function decodeArrayFromUrl($string, $urlDecode = true) { $raw = $urlDecode ? urldecode($string) : $string; $decoded = base64_decode($raw); + if (empty($decoded)) { + return []; + } + if (strpos(strtolower($decoded), 'a') !== 0) { throw new \InvalidArgumentException(sprintf('The string %s is not a serialized array.', $decoded)); } diff --git a/app/bundles/CoreBundle/Helper/DateTimeHelper.php b/app/bundles/CoreBundle/Helper/DateTimeHelper.php index 4f80f938293..8f7eee18fe5 100644 --- a/app/bundles/CoreBundle/Helper/DateTimeHelper.php +++ b/app/bundles/CoreBundle/Helper/DateTimeHelper.php @@ -11,9 +11,6 @@ namespace Mautic\CoreBundle\Helper; -/** - * Class DateTimeHelper. - */ class DateTimeHelper { /** @@ -42,14 +39,14 @@ class DateTimeHelper private $local; /** - * @var \DateTime + * @var \DateTimeInterface */ private $datetime; /** - * @param \DateTime|string $string - * @param string $fromFormat Format the string is in - * @param string $timezone Timezone the string is in + * @param \DateTimeInterface|string $string + * @param string $fromFormat Format the string is in + * @param string $timezone Timezone the string is in */ public function __construct($string = '', $fromFormat = 'Y-m-d H:i:s', $timezone = 'UTC') { @@ -59,9 +56,9 @@ public function __construct($string = '', $fromFormat = 'Y-m-d H:i:s', $timezone /** * Sets date/time. * - * @param \DateTime|string $datetime - * @param string $fromFormat - * @param string $timezone + * @param \DateTimeInterface|string $datetime + * @param string $fromFormat + * @param string $timezone */ public function setDateTime($datetime = '', $fromFormat = 'Y-m-d H:i:s', $timezone = 'local') { @@ -75,9 +72,9 @@ public function setDateTime($datetime = '', $fromFormat = 'Y-m-d H:i:s', $timezo $this->timezone = $timezone; $this->utc = new \DateTimeZone('UTC'); - $this->local = new \DateTimeZone(date_default_timezone_get()); + $this->local = new \DateTimeZone($timezone); - if ($datetime instanceof \DateTime) { + if ($datetime instanceof \DateTimeInterface) { $this->datetime = $datetime; $this->timezone = $datetime->getTimezone()->getName(); $this->string = $this->datetime->format($fromFormat); @@ -244,7 +241,7 @@ public function getDiff($compare = 'now', $format = null, $resetTime = false) * @param $intervalString * @param bool|false $clone If true, return a new \DateTime rather than update current one * - * @return \DateTime + * @return \DateTimeInterface */ public function add($intervalString, $clone = false) { @@ -266,7 +263,7 @@ public function add($intervalString, $clone = false) * @param $intervalString * @param bool|false $clone If true, return a new \DateTime rather than update current one * - * @return \DateTime + * @return \DateTimeInterface */ public function sub($intervalString, $clone = false) { @@ -322,7 +319,7 @@ public function buildInterval($interval, $unit) * @param $string * @param bool|false $clone If true, return a new \DateTime rather than update current one * - * @return \DateTime + * @return \DateTimeInterface */ public function modify($string, $clone = false) { diff --git a/app/bundles/CoreBundle/Helper/ThemeHelper.php b/app/bundles/CoreBundle/Helper/ThemeHelper.php index 0d674d46897..5412d1e1c46 100644 --- a/app/bundles/CoreBundle/Helper/ThemeHelper.php +++ b/app/bundles/CoreBundle/Helper/ThemeHelper.php @@ -15,6 +15,8 @@ use Mautic\CoreBundle\Templating\Helper\ThemeHelper as TemplatingThemeHelper; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; +use Symfony\Component\Templating\EngineInterface; +use Symfony\Component\Templating\TemplateReference; use Symfony\Component\Translation\TranslatorInterface; class ThemeHelper @@ -122,9 +124,12 @@ public function setDefaultTheme($defaultTheme) } /** - * @param string $themeName + * @param $themeName + * + * @return TemplatingThemeHelper * - * @return ThemeHelper + * @throws MauticException\BadConfigurationException + * @throws MauticException\FileNotFoundException */ public function createThemeHelper($themeName) { @@ -291,11 +296,20 @@ public function checkForTwigTemplate($template) $twigTemplate = clone $template; $twigTemplate->set('engine', 'twig'); + // Does a twig version exist? if ($templating->exists($twigTemplate)) { return $twigTemplate->getLogicalName(); } - return $template->getLogicalName(); + // Does a PHP version exist? + if ($templating->exists($template)) { + return $template->getLogicalName(); + } + + // Try any theme as a fall back starting with default + $this->findThemeWithTemplate($templating, $twigTemplate); + + return $twigTemplate->getLogicalName(); } /** @@ -308,31 +322,21 @@ public function checkForTwigTemplate($template) */ public function getInstalledThemes($specificFeature = 'all', $extended = false, $ignoreCache = false, $includeDirs = true) { - if (empty($this->themes[$specificFeature]) || $ignoreCache === true) { - $dir = $this->pathsHelper->getSystemPath('themes', true); - $addTheme = false; - + if (empty($this->themes[$specificFeature]) || $ignoreCache) { + $dir = $this->pathsHelper->getSystemPath('themes', true); $finder = new Finder(); $finder->directories()->depth('0')->ignoreDotFiles(true)->in($dir); $this->themes[$specificFeature] = []; $this->themesInfo[$specificFeature] = []; foreach ($finder as $theme) { - if (file_exists($theme->getRealPath().'/config.json')) { - $config = json_decode(file_get_contents($theme->getRealPath().'/config.json'), true); - } else { + if (!file_exists($theme->getRealPath().'/config.json')) { continue; } - if ($specificFeature != 'all') { - if (isset($config['features']) && in_array($specificFeature, $config['features'])) { - $addTheme = true; - } - } else { - $addTheme = true; - } + $config = json_decode(file_get_contents($theme->getRealPath().'/config.json'), true); - if ($addTheme) { + if ('all' === $specificFeature || (isset($config['features']) && in_array($specificFeature, $config['features']))) { $this->themes[$specificFeature][$theme->getBasename()] = $config['name']; $this->themesInfo[$specificFeature][$theme->getBasename()] = []; $this->themesInfo[$specificFeature][$theme->getBasename()]['name'] = $config['name']; @@ -341,7 +345,10 @@ public function getInstalledThemes($specificFeature = 'all', $extended = false, if ($includeDirs) { $this->themesInfo[$specificFeature][$theme->getBasename()]['dir'] = $theme->getRealPath(); - $this->themesInfo[$specificFeature][$theme->getBasename()]['themesLocalDir'] = $this->pathsHelper->getSystemPath('themes', false); + $this->themesInfo[$specificFeature][$theme->getBasename()]['themesLocalDir'] = $this->pathsHelper->getSystemPath( + 'themes', + false + ); } } } @@ -349,9 +356,9 @@ public function getInstalledThemes($specificFeature = 'all', $extended = false, if ($extended) { return $this->themesInfo[$specificFeature]; - } else { - return $this->themes[$specificFeature]; } + + return $this->themes[$specificFeature]; } /** @@ -409,7 +416,7 @@ public function getTheme($theme = 'current', $throwException = false) * @return bool * * @throws MauticException\FileNotFoundException - * @throws Exception + * @throws \Exception */ public function install($zipFile) { @@ -435,45 +442,75 @@ public function install($zipFile) if ($archive !== true) { throw new \Exception($this->getExtractError($archive)); - } else { - $containsConfig = false; - $allowedExtensions = $this->coreParametersHelper->getParameter('theme_import_allowed_extensions'); - $allowedFiles = []; - for ($i = 0; $i < $zipper->numFiles; ++$i) { - $entry = $zipper->getNameIndex($i); - $extension = pathinfo($entry, PATHINFO_EXTENSION); - - // Check if the config.json exists in the zip file at the root level - if ($entry == 'config.json' || $entry == '/config.json') { - $containsConfig = true; - } + } - // Filter out dangerous files like .php - if (empty($extension) || in_array(strtolower($extension), $allowedExtensions)) { - $allowedFiles[] = $entry; - } + $requiredFiles = ['config.json', 'html/message.html.twig']; + $foundRequiredFiles = []; + $allowedFiles = []; + $allowedExtensions = $this->coreParametersHelper->getParameter('theme_import_allowed_extensions'); + + $config = []; + for ($i = 0; $i < $zipper->numFiles; ++$i) { + $entry = $zipper->getNameIndex($i); + if (strpos($entry, '/') === 0) { + $entry = substr($entry, 1); } - if (!$containsConfig) { - throw new \Exception('mautic.core.theme.missing.config'); + $extension = pathinfo($entry, PATHINFO_EXTENSION); + + // Check for required files + if (in_array($entry, $requiredFiles)) { + $foundRequiredFiles[] = $entry; + } + + // Filter out dangerous files like .php + if (empty($extension) || in_array(strtolower($extension), $allowedExtensions)) { + $allowedFiles[] = $entry; } - // Extract the archive file now - if (!$zipper->extractTo($themePath, $allowedFiles)) { - throw new \Exception('mautic.core.update.error_extracting_package'); - } else { - $zipper->close(); - unlink($zipFile); + if ('config.json' === $entry) { + $config = json_decode($zipper->getFromName($entry), true); + } + } + + if (!empty($config['features'])) { + foreach ($config['features'] as $feature) { + $featureFile = sprintf('html/%s.html.twig', strtolower($feature)); + $requiredFiles[] = $featureFile; - return true; + if (in_array($featureFile, $allowedFiles)) { + $foundRequiredFiles[] = $featureFile; + } } } + + if ($missingFiles = array_diff($requiredFiles, $foundRequiredFiles)) { + throw new MauticException\FileNotFoundException( + $this->translator->trans( + 'mautic.core.theme.missing.files', + [ + '%files%' => implode(', ', $missingFiles), + ], + 'validators' + ) + ); + } + + // Extract the archive file now + if (!$zipper->extractTo($themePath, $allowedFiles)) { + throw new \Exception('mautic.core.update.error_extracting_package'); + } else { + $zipper->close(); + unlink($zipFile); + + return true; + } } /** * Get the error message from the zip archive. * - * @param ZipArchive $archive + * @param \ZipArchive $archive * * @return string */ @@ -544,4 +581,42 @@ public function zip($themeName) return false; } + + /** + * @param EngineInterface $templating + * @param TemplateReference $template + * + * @throws MauticException\BadConfigurationException + * @throws MauticException\FileNotFoundException + */ + private function findThemeWithTemplate(EngineInterface $templating, TemplateReference $template) + { + preg_match('/^:(.*?):(.*?)$/', $template->getLogicalName(), $match); + $requestedThemeName = $match[1]; + + // Try the default theme first + $defaultTheme = $this->getTheme(); + if ($requestedThemeName !== $defaultTheme->getTheme()) { + $template->set('controller', $defaultTheme->getTheme()); + if ($templating->exists($template)) { + return; + } + } + + // Find any theme as a fallback + $themes = $this->getInstalledThemes('all', true); + foreach ($themes as $theme) { + // Already handled the default + if ($theme['name'] === $defaultTheme->getTheme()) { + continue; + } + + // Theme name is stored in the controller parameter + $template->set('controller', $theme['key']); + + if ($templating->exists($template)) { + return; + } + } + } } diff --git a/app/bundles/CoreBundle/Helper/TrackingPixelHelper.php b/app/bundles/CoreBundle/Helper/TrackingPixelHelper.php index fd5c3168f18..a4e4b01e3b3 100644 --- a/app/bundles/CoreBundle/Helper/TrackingPixelHelper.php +++ b/app/bundles/CoreBundle/Helper/TrackingPixelHelper.php @@ -38,7 +38,9 @@ public static function getResponse(Request $request) return $response; } - ignore_user_abort(true); + if (ini_get('ignore_user_abort')) { + ignore_user_abort(true); + } //turn off gzip compression if (function_exists('apache_setenv')) { diff --git a/app/bundles/CoreBundle/Helper/UrlHelper.php b/app/bundles/CoreBundle/Helper/UrlHelper.php index 6b5fffb05dd..b138aa26de9 100644 --- a/app/bundles/CoreBundle/Helper/UrlHelper.php +++ b/app/bundles/CoreBundle/Helper/UrlHelper.php @@ -89,16 +89,14 @@ public static function rel2abs($rel) { $path = $host = $scheme = ''; - $base = 'http'; - if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') { - $base .= 's'; - } - $base .= '://'; - if ($_SERVER['SERVER_PORT'] != '80') { - $base .= $_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT'].$_SERVER['REQUEST_URI']; - } else { - $base .= $_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']; - } + $ssl = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'; + $scheme = strtolower($_SERVER['SERVER_PROTOCOL']); + $scheme = substr($scheme, 0, strpos($scheme, '/')).($ssl ? 's' : ''); + $port = $_SERVER['SERVER_PORT']; + $port = ((!$ssl && $port == '80') || ($ssl && $port == '443')) ? '' : ":$port"; + $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : null; + $host = isset($host) ? $host : $_SERVER['SERVER_NAME'].$port; + $base = "$scheme://$host".$_SERVER['REQUEST_URI']; $base = str_replace('/index_dev.php', '', $base); $base = str_replace('/index.php', '', $base); @@ -179,7 +177,7 @@ public static function getUrlsFromPlaintext($text, array $contactUrlFields = []) // We don't want to match URLs in token default values // like {contactfield=website|http://ignore.this.url} - if (preg_match_all("#{(.*?)\|".preg_quote($url).'}#', $text, $matches)) { + if (preg_match_all("/{(.*?)\|".preg_quote($url, '/').'}/', $text, $matches)) { unset($urls[$key]); // We know this is a URL due to the default so let's include it as a trackable diff --git a/app/bundles/CoreBundle/IpLookup/AbstractLookup.php b/app/bundles/CoreBundle/IpLookup/AbstractLookup.php index 656244c657a..4b039af5ae3 100644 --- a/app/bundles/CoreBundle/IpLookup/AbstractLookup.php +++ b/app/bundles/CoreBundle/IpLookup/AbstractLookup.php @@ -27,6 +27,11 @@ abstract class AbstractLookup public $timezone = ''; public $extra = ''; + /** + * @var Http|null + */ + protected $connector; + /** * @var string IP Address */ @@ -106,14 +111,17 @@ public function setIpAddress($ip) */ public function getDetails() { - $reflect = new \ReflectionClass($this); - $props = $reflect->getProperties(\ReflectionProperty::IS_PUBLIC); - - $details = []; - foreach ($props as $prop) { - $details[$prop->getName()] = $prop->getValue($this); - } - - return $details; + return [ + 'city' => $this->city, + 'region' => $this->region, + 'zipcode' => $this->zipcode, + 'country' => $this->country, + 'latitude' => $this->latitude, + 'longitude' => $this->longitude, + 'isp' => $this->isp, + 'organization' => $this->organization, + 'timezone' => $this->timezone, + 'extra' => $this->extra, + ]; } } diff --git a/app/bundles/CoreBundle/Model/AuditLogModel.php b/app/bundles/CoreBundle/Model/AuditLogModel.php index f12c75f4116..8b9984159a5 100644 --- a/app/bundles/CoreBundle/Model/AuditLogModel.php +++ b/app/bundles/CoreBundle/Model/AuditLogModel.php @@ -70,11 +70,11 @@ public function writeToLog(array $args) /** * Get the audit log for specific object. * - * @param $object - * @param $id - * @param null $afterDate - * @param int $limit - * @param null $bundle + * @param string $object + * @param string|int $id + * @param \DateTimeInterface|null $afterDate + * @param int $limit + * @param string|null $bundle * * @return mixed */ diff --git a/app/bundles/CoreBundle/Templating/Helper/ContentHelper.php b/app/bundles/CoreBundle/Templating/Helper/ContentHelper.php index 07e8d459e6e..b115720b5a9 100644 --- a/app/bundles/CoreBundle/Templating/Helper/ContentHelper.php +++ b/app/bundles/CoreBundle/Templating/Helper/ContentHelper.php @@ -61,7 +61,7 @@ public function getCustomContent($context = null, array $vars = [], $viewName = $viewName = $vars['mauticTemplate']; } - /** @var ContentEvent $event */ + /** @var CustomContentEvent $event */ $event = $this->dispatcher->dispatch( CoreEvents::VIEW_INJECT_CUSTOM_CONTENT, new CustomContentEvent($viewName, $context, $vars) @@ -69,9 +69,9 @@ public function getCustomContent($context = null, array $vars = [], $viewName = $content = $event->getContent(); - if ($templates = $event->getTemplates()) { - foreach ($templates as $template => $templateVars) { - $content[] = $this->templating->render($template, array_merge($vars, $templateVars)); + if ($templatProps = $event->getTemplates()) { + foreach ($templatProps as $props) { + $content[] = $this->templating->render($props['template'], array_merge($vars, $props['vars'])); } } diff --git a/app/bundles/CoreBundle/Templating/Helper/DateHelper.php b/app/bundles/CoreBundle/Templating/Helper/DateHelper.php index b191b65023c..f273a648370 100644 --- a/app/bundles/CoreBundle/Templating/Helper/DateHelper.php +++ b/app/bundles/CoreBundle/Templating/Helper/DateHelper.php @@ -170,12 +170,6 @@ public function toText($datetime, $timezone = 'local', $fromFormat = 'Y-m-d H:i: return ''; } - if ($datetime instanceof \DateTime) { - // Fix time shift with timezone conversion into string representation - $timezone = ($datetime->getTimezone()->getName() === 'UTC') ? 'utc' : $timezone; - $datetime = $datetime->format($fromFormat); - } - $this->helper->setDateTime($datetime, $fromFormat, $timezone); $textDate = $this->helper->getTextDate(); diff --git a/app/bundles/CoreBundle/Tests/unit/Entity/IpAddressTest.php b/app/bundles/CoreBundle/Tests/unit/Entity/IpAddressTest.php new file mode 100644 index 00000000000..c6689953927 --- /dev/null +++ b/app/bundles/CoreBundle/Tests/unit/Entity/IpAddressTest.php @@ -0,0 +1,71 @@ +setDoNotTrackList( + [ + '192.168.0.1', + ] + ); + $ipAddress->setIpAddress('192.168.0.1'); + $this->assertFalse($ipAddress->isTrackable()); + + $ipAddress->setIpAddress('192.168.0.2'); + $this->assertTrue($ipAddress->isTrackable()); + } + + public function testIpRange() + { + // HostMin: 172.16.0.1 + // HostMax: 172.31.255.255 + $ipAddress = new IpAddress(); + $ipAddress->setDoNotTrackList( + [ + '172.16.0.0/12', + ] + ); + + $ipAddress->setIpAddress('172.16.0.1'); + $this->assertFalse($ipAddress->isTrackable()); + + $ipAddress->setIpAddress('172.31.255.254'); + $this->assertFalse($ipAddress->isTrackable()); + + $ipAddress->setIpAddress('172.15.1.32'); + $this->assertTrue($ipAddress->isTrackable()); + + $ipAddress->setIpAddress('172.32.0.0'); + $this->assertTrue($ipAddress->isTrackable()); + } + + public function testIpWildcard() + { + $ipAddress = new IpAddress(); + $ipAddress->setDoNotTrackList( + [ + '172.15.1.*', + ] + ); + $ipAddress->setIpAddress('172.15.1.1'); + $this->assertFalse($ipAddress->isTrackable()); + + $ipAddress->setIpAddress('172.16.1.1'); + $this->assertTrue($ipAddress->isTrackable()); + } +} diff --git a/app/bundles/CoreBundle/Tests/unit/Helper/ClickthroughHelperTest.php b/app/bundles/CoreBundle/Tests/unit/Helper/ClickthroughHelperTest.php index 08eaff387a1..adff89b9f79 100644 --- a/app/bundles/CoreBundle/Tests/unit/Helper/ClickthroughHelperTest.php +++ b/app/bundles/CoreBundle/Tests/unit/Helper/ClickthroughHelperTest.php @@ -28,4 +28,11 @@ public function testOnlyArraysCanBeDecodedToPreventObjectWakeupVulnerability() ClickthroughHelper::decodeArrayFromUrl(urlencode(base64_encode(serialize(new \stdClass())))); } + + public function testEmptyStringDoesNotThrowException() + { + $array = []; + + $this->assertEquals($array, ClickthroughHelper::decodeArrayFromUrl('')); + } } diff --git a/app/bundles/CoreBundle/Tests/unit/Helper/ThemeHelperTest.php b/app/bundles/CoreBundle/Tests/unit/Helper/ThemeHelperTest.php new file mode 100644 index 00000000000..fafa0e19503 --- /dev/null +++ b/app/bundles/CoreBundle/Tests/unit/Helper/ThemeHelperTest.php @@ -0,0 +1,252 @@ +pathsHelper = $this->createMock(PathsHelper::class); + $this->templatingHelper = $this->createMock(TemplatingHelper::class); + $this->translator = $this->createMock(TranslatorInterface::class); + $this->coreParameterHelper = $this->createMock(CoreParametersHelper::class); + $this->coreParameterHelper->method('getParameter') + ->with('theme_import_allowed_extensions') + ->willReturn(['json', 'twig', 'css', 'js', 'htm', 'html', 'txt', 'jpg', 'jpeg', 'png', 'gif']); + } + + public function testExceptionThrownWithMissingConfig() + { + $this->expectException(FileNotFoundException::class); + + $this->pathsHelper->method('getSystemPath') + ->with('themes', true) + ->willReturn(__DIR__.'/themes'); + + $this->translator->expects($this->once()) + ->method('trans') + ->with('mautic.core.theme.missing.files', $this->anything(), 'validators') + ->willReturnCallback( + function ($key, array $parameters) { + $this->assertContains('config.json', $parameters['%files%']); + } + ); + + $this->getThemeHelper()->install(__DIR__.'/themes/missing-config.zip'); + } + + public function testExceptionThrownWithMissingMessage() + { + $this->expectException(FileNotFoundException::class); + + $this->pathsHelper->method('getSystemPath') + ->with('themes', true) + ->willReturn(__DIR__.'/themes'); + + $this->translator->expects($this->once()) + ->method('trans') + ->with('mautic.core.theme.missing.files', $this->anything(), 'validators') + ->willReturnCallback( + function ($key, array $parameters) { + $this->assertContains('message.html.twig', $parameters['%files%']); + } + ); + + $this->getThemeHelper()->install(__DIR__.'/themes/missing-message.zip'); + } + + public function testExceptionThrownWithMissingFeature() + { + $this->expectException(FileNotFoundException::class); + + $this->pathsHelper->method('getSystemPath') + ->with('themes', true) + ->willReturn(__DIR__.'/themes'); + + $this->translator->expects($this->once()) + ->method('trans') + ->with('mautic.core.theme.missing.files', $this->anything(), 'validators') + ->willReturnCallback( + function ($key, array $parameters) { + $this->assertContains('page.html.twig', $parameters['%files%']); + } + ); + + $this->getThemeHelper()->install(__DIR__.'/themes/missing-feature.zip'); + } + + public function testThemeIsInstalled() + { + $fs = new Filesystem(); + $fs->copy(__DIR__.'/themes/good.zip', __DIR__.'/themes/good-tmp.zip'); + + $this->pathsHelper->method('getSystemPath') + ->with('themes', true) + ->willReturn(__DIR__.'/themes'); + + $this->getThemeHelper()->install(__DIR__.'/themes/good-tmp.zip'); + + $this->assertFileExists(__DIR__.'/themes/good-tmp'); + + $fs->remove(__DIR__.'/themes/good-tmp'); + } + + public function testThemeFallbackToDefaultIfTemplateIsMissing() + { + $templateNameParser = $this->createMock(TemplateNameParser::class); + $this->templatingHelper->expects($this->once()) + ->method('getTemplateNameParser') + ->willReturn($templateNameParser); + $templateNameParser->expects($this->once()) + ->method('parse') + ->willReturn( + new TemplateReference('', 'goldstar', 'page', 'html') + ); + + $templating = $this->createMock(DelegatingEngine::class); + + // twig does not exist + $templating->expects($this->at(0)) + ->method('exists') + ->willReturn(false); + + // php does not exist + $templating->expects($this->at(1)) + ->method('exists') + ->willReturn(false); + + // default themes twig exists + $templating->expects($this->at(2)) + ->method('exists') + ->willReturn(true); + + $this->templatingHelper->expects($this->once()) + ->method('getTemplating') + ->willReturn($templating); + + $this->pathsHelper->method('getSystemPath') + ->willReturnCallback( + function ($path, $absolute) { + switch ($path) { + case 'themes': + return ($absolute) ? __DIR__.'/../../../../../../themes' : 'themes'; + case 'themes_root': + return __DIR__.'/../../../../../..'; + } + } + ); + + $themeHelper = $this->getThemeHelper(); + $themeHelper->setDefaultTheme('nature'); + + $template = $themeHelper->checkForTwigTemplate(':goldstar:page.html.twig'); + $this->assertEquals(':nature:page.html.twig', $template); + } + + public function testThemeFallbackToNextBestIfTemplateIsMissingForBothRequestedAndDefaultThemes() + { + $templateNameParser = $this->createMock(TemplateNameParser::class); + $this->templatingHelper->expects($this->once()) + ->method('getTemplateNameParser') + ->willReturn($templateNameParser); + $templateNameParser->expects($this->once()) + ->method('parse') + ->willReturn( + new TemplateReference('', 'goldstar', 'page', 'html') + ); + + $templating = $this->createMock(DelegatingEngine::class); + + // twig does not exist + $templating->expects($this->at(0)) + ->method('exists') + ->willReturn(false); + + // php does not exist + $templating->expects($this->at(1)) + ->method('exists') + ->willReturn(false); + + // default theme twig does not exist + $templating->expects($this->at(2)) + ->method('exists') + ->willReturn(false); + + // next theme exists + $templating->expects($this->at(3)) + ->method('exists') + ->willReturn(true); + + $this->templatingHelper->expects($this->once()) + ->method('getTemplating') + ->willReturn($templating); + + $this->pathsHelper->method('getSystemPath') + ->willReturnCallback( + function ($path, $absolute) { + switch ($path) { + case 'themes': + return ($absolute) ? __DIR__.'/../../../../../../themes' : 'themes'; + case 'themes_root': + return __DIR__.'/../../../../../..'; + } + } + ); + + $themeHelper = $this->getThemeHelper(); + $themeHelper->setDefaultTheme('nature'); + + $template = $themeHelper->checkForTwigTemplate(':goldstar:page.html.twig'); + $this->assertNotEquals(':nature:page.html.twig', $template); + $this->assertNotEquals(':goldstar:page.html.twig', $template); + $this->assertContains(':page.html.twig', $template); + } + + /** + * @return ThemeHelper + */ + private function getThemeHelper() + { + return new ThemeHelper($this->pathsHelper, $this->templatingHelper, $this->translator, $this->coreParameterHelper); + } +} diff --git a/app/bundles/CoreBundle/Tests/unit/Helper/UrlHelperTest.php b/app/bundles/CoreBundle/Tests/unit/Helper/UrlHelperTest.php index a728e9c1fb3..fba8cdcec6e 100644 --- a/app/bundles/CoreBundle/Tests/unit/Helper/UrlHelperTest.php +++ b/app/bundles/CoreBundle/Tests/unit/Helper/UrlHelperTest.php @@ -124,6 +124,7 @@ public function testGetUrlsFromPlaintextWithSymbols() 'https://example.org/with/query?utm_campaign=hello', 'https://example.org/with/tokenized-query?foo={contactfield=bar}&bar=foo', 'https://example.org/with/just-tokenized-query?foo={contactfield=bar}', + 'https://example.org/with/query?utm_campaign=_hello#_underscore-test', ], UrlHelper::getUrlsFromPlaintext( <<translator = $this->createMock(TranslatorInterface::class); + $this->helper = new DateHelper( + 'F j, Y g:i a T', + 'D, M d', + 'F j, Y', + 'g:i a', + $this->translator + ); + } + + public function testToTextWithPragueTimezone() + { + $dateTime = new \DateTime('2016-01-27 13:30:00', new \DateTimeZone('UTC')); + $regexForDst = '/^January 27, 2016 [1,2]:30 pm$/'; + + $this->assertRegExp($regexForDst, $this->helper->toText($dateTime, 'Europe/Prague', 'Y-m-d H:i:s', true)); + } + + public function testToTextWithUtcTimezone() + { + $dateTime = new \DateTime('2017-11-20 15:45:00', new \DateTimeZone('UTC')); + + $this->assertSame('November 20, 2017 3:45 pm', $this->helper->toText($dateTime, 'UTC', 'Y-m-d H:i:s', true)); + } +} diff --git a/app/bundles/CoreBundle/Translations/en_US/flashes.ini b/app/bundles/CoreBundle/Translations/en_US/flashes.ini index 20a60784bba..62e22db7895 100644 --- a/app/bundles/CoreBundle/Translations/en_US/flashes.ini +++ b/app/bundles/CoreBundle/Translations/en_US/flashes.ini @@ -11,8 +11,8 @@ mautic.core.notice.batch_deleted="%count% items deleted" mautic.core.notice.created="%name% has been created!" mautic.core.notice.deleted="%name% has been deleted!" mautic.core.notice.updated="%name% has been updated!" -mautic.core.notice.used.field="Field %name% (#%id%) is used and cannot be deleted." -mautic.core.notice.used.fields="Some fields are used and cannot be deleted." +mautic.core.notice.used.field="Field "%name%" (#%id%) cannot be deleted because it's used in the following Segment(s): %segments%." +mautic.core.notice.used.fields="Field(s) "%fields%" cannot be deleted because they are used in the following Segment(s): %segments%." mautic.core.language.helper.error.fetching.package="An error occurred while downloading the language package." mautic.core.language.helper.error.follow.redirects="Whoops, either safe_mode or open_basedir is turned on. Download the language from here. Unzip and upload it to the /translations directory." mautic.core.language.helper.invalid.language="Requested language '%language%' does not exist among the available Mautic languages. The language was reset to the default one." diff --git a/app/bundles/CoreBundle/Translations/en_US/messages.ini b/app/bundles/CoreBundle/Translations/en_US/messages.ini index 09c866b9035..f00ddcf6179 100644 --- a/app/bundles/CoreBundle/Translations/en_US/messages.ini +++ b/app/bundles/CoreBundle/Translations/en_US/messages.ini @@ -97,6 +97,8 @@ mautic.core.config.form.image.path="Relative path to the images directory" mautic.core.config.form.image.path.tooltip="Set the relative path from the public web root to where images uploaded through the editors should be stored." mautic.core.config.form.ip.lookup.auth="IP lookup service authentication" mautic.core.config.form.ip.lookup.auth.tooltip="Set authentication credential(s) required by the selected service. For services that require a username and password, use the format username:password." +mautic.core.config.create.organization.from.ip.lookup="Create company from IP lookup" +mautic.core.config.create.organization.from.ip.lookup.tooltip="Although it may be useful to get the company from IP lookup, Mautic will consider such contact to be identified which may not be what you want." mautic.core.config.form.ip.lookup.service="IP lookup service" mautic.core.config.form.ip.lookup.service.tooltip="Set the service to use to lookup geographical information from an IP address. Note that some of the services listed are commercial services. Mautic is not affiliated with the listed services but provide them for convenience." mautic.core.config.form.link.shortener="URL Shortener" diff --git a/app/bundles/CoreBundle/Translations/en_US/validators.ini b/app/bundles/CoreBundle/Translations/en_US/validators.ini index 67ed1276dea..4633bc754fc 100644 --- a/app/bundles/CoreBundle/Translations/en_US/validators.ini +++ b/app/bundles/CoreBundle/Translations/en_US/validators.ini @@ -9,7 +9,7 @@ mautic.core.subject.required="A subject is required." mautic.core.variant_weights_invalid="The sum of weights between all variants cannot be more than 100%" mautic.form.lists.count="At least one list value is required." mautic.form.lists.notblank="List values cannot be blank." -mautic.core.theme.missing.config="The theme you tried to install doesn't have the config.json file in the root folder. The theme could not be installed." +mautic.core.theme.missing.files="The theme you tried to install is missing the following required files and thus could not be installed: %files%" mautic.core.theme.default.cannot.overwrite="%name% is the default theme and therefore cannot be overwritten." mautic.core.valid_url_required="A valid URL is required." mautic.core.theme.upload.empty="The file was not selected. Select a ZIP file to upload." diff --git a/app/bundles/CoreBundle/Views/FormTheme/Config/_config_coreconfig_widget.html.php b/app/bundles/CoreBundle/Views/FormTheme/Config/_config_coreconfig_widget.html.php index 646372df373..313665a523b 100644 --- a/app/bundles/CoreBundle/Views/FormTheme/Config/_config_coreconfig_widget.html.php +++ b/app/bundles/CoreBundle/Views/FormTheme/Config/_config_coreconfig_widget.html.php @@ -13,7 +13,7 @@ $template = '
{content}
'; ?> - +

trans('mautic.core.config.header.general'); ?>

@@ -27,6 +27,7 @@ rowIfExists($fields, 'log_path', $template); ?> rowIfExists($fields, 'theme', $template); ?> rowIfExists($fields, 'image_path', $template); ?> + rowIfExists($fields, 'last_shown_tab'); ?>
@@ -90,6 +91,7 @@
rowIfExists($fields, 'ip_lookup_service', $template); ?> rowIfExists($fields, 'ip_lookup_auth', $template); ?> + rowIfExists($fields, 'ip_lookup_create_organization', $template); ?>
rowIfExists($fields, 'ip_lookup_config', '
{content}
'); ?>
diff --git a/app/bundles/CoreBundle/Views/Helper/pagination.html.php b/app/bundles/CoreBundle/Views/Helper/pagination.html.php index f4ae2ff6c52..f1bf7ab5829 100644 --- a/app/bundles/CoreBundle/Views/Helper/pagination.html.php +++ b/app/bundles/CoreBundle/Views/Helper/pagination.html.php @@ -102,7 +102,7 @@
- $label): ?> value="escape($value); ?>"> diff --git a/app/bundles/CoreBundle/Views/Helper/tableheader.html.php b/app/bundles/CoreBundle/Views/Helper/tableheader.html.php index e5b125bada1..baf1d196fc8 100644 --- a/app/bundles/CoreBundle/Views/Helper/tableheader.html.php +++ b/app/bundles/CoreBundle/Views/Helper/tableheader.html.php @@ -37,21 +37,20 @@ endswitch; ?> data-toggle="tooltip" title="" data-placement="top" data-original-title="trans($tooltip); ?>"> - getButtonCount()): ?>
- - - + + + -
- - +
+ + +
- > diff --git a/app/bundles/CoreBundle/Views/Menu/main.html.php b/app/bundles/CoreBundle/Views/Menu/main.html.php index bc859850097..2f333759401 100644 --- a/app/bundles/CoreBundle/Views/Menu/main.html.php +++ b/app/bundles/CoreBundle/Views/Menu/main.html.php @@ -60,7 +60,7 @@ if (!isset($labelAttributes['class'])) { $labelAttributes['class'] = 'nav-item-name'; } - $labelPull = $extras['depth'] === 0 ? ' pull-left' : ''; + $labelPull = empty($extras['depth']) ? ' pull-left' : ''; $labelAttributes['class'] .= ' text'.$labelPull; echo "parseAttributes($labelAttributes)}>{$view['translator']->trans($child->getLabel())}"; diff --git a/app/bundles/CoreBundle/Views/Slots/channelfrequency.html.php b/app/bundles/CoreBundle/Views/Slots/channelfrequency.html.php index 4831977631f..8052e7ca72d 100644 --- a/app/bundles/CoreBundle/Views/Slots/channelfrequency.html.php +++ b/app/bundles/CoreBundle/Views/Slots/channelfrequency.html.php @@ -36,7 +36,7 @@ -
+
value]) && isset($form['lead_channels']['frequency_time_'.$channel->value])):?>
@@ -81,7 +81,7 @@ -
+
diff --git a/app/bundles/CoreBundle/Views/Slots/saveprefsbutton.html.php b/app/bundles/CoreBundle/Views/Slots/saveprefsbutton.html.php index 39856ba38e9..ed1c70710a9 100644 --- a/app/bundles/CoreBundle/Views/Slots/saveprefsbutton.html.php +++ b/app/bundles/CoreBundle/Views/Slots/saveprefsbutton.html.php @@ -8,16 +8,19 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ +$style = isset($saveprefsbutton['style']) ? $saveprefsbutton['style'] : 'display:inline-block;text-decoration:none;border-color:#4e5d9d;border-width: 10px 20px;border-style:solid; text-decoration: none; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; background-color: #4e5d9d; display: inline-block;font-size: 16px; color: #ffffff;'; +$background = isset($saveprefsbutton['background']) ? $saveprefsbutton['background'] : ''; + if (isset($form)) { // add form tag echo ''; - $view['assets']->addCustomDeclaration($view['form']->start($form), 'bodyOpen'); } ?> onclick="saveUnsubscribePreferences('vars['id']; ?>')" - style="display:inline-block;text-decoration:none;border-color:#4e5d9d;border-width: 10px 20px;border-style:solid; text-decoration: none; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; background-color: #4e5d9d; display: inline-block;font-size: 16px; color: #ffffff; "> + class="button btn btn-default btn-save" + onclick="saveUnsubscribePreferences('vars['id']; ?>')" + style="" + background=""> trans('mautic.page.form.saveprefs'); ?>
diff --git a/app/bundles/EmailBundle/Assets/js/email.js b/app/bundles/EmailBundle/Assets/js/email.js index 0e4a1b57821..bb6dfe393d8 100644 --- a/app/bundles/EmailBundle/Assets/js/email.js +++ b/app/bundles/EmailBundle/Assets/js/email.js @@ -533,7 +533,7 @@ Mautic.addDynamicContentFilter = function (selectedFilter, jQueryVariant) { var prototype = mQuery('#filterSelectPrototype').data('prototype'); var fieldObject = selectedOption.data('field-object'); var fieldType = selectedOption.data('field-type'); - var isSpecial = (mQuery.inArray(fieldType, ['leadlist', 'lead_email_received', 'tags', 'multiselect', 'boolean', 'select', 'country', 'timezone', 'region', 'stage', 'locale']) != -1); + var isSpecial = (mQuery.inArray(fieldType, ['leadlist', 'assets', 'lead_email_received', 'tags', 'multiselect', 'boolean', 'select', 'country', 'timezone', 'region', 'stage', 'locale']) != -1); // Update the prototype settings prototype = prototype.replace(/__name__/g, filterNum) @@ -619,6 +619,7 @@ Mautic.addDynamicContentFilter = function (selectedFilter, jQueryVariant) { lazyInit: true, validateOnBlur: false, allowBlank: true, + scrollMonth: false, scrollInput: false }); } else if (fieldType == 'date') { @@ -628,6 +629,7 @@ Mautic.addDynamicContentFilter = function (selectedFilter, jQueryVariant) { lazyInit: true, validateOnBlur: false, allowBlank: true, + scrollMonth: false, scrollInput: false, closeOnDateSelect: true }); @@ -638,6 +640,7 @@ Mautic.addDynamicContentFilter = function (selectedFilter, jQueryVariant) { lazyInit: true, validateOnBlur: false, allowBlank: true, + scrollMonth: false, scrollInput: false }); } else if (fieldType == 'lookup_id') { diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index afa09ec3097..31676f72bd5 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -127,6 +127,7 @@ 'mautic.transport.momentum.callback', 'mautic.queue.service', 'mautic.email.helper.request.storage', + 'monolog.logger.mautic', ], ], 'mautic.email.monitored.bounce.subscriber' => [ @@ -554,6 +555,8 @@ '%mautic.mailer_api_key%', 'translator', 'mautic.email.model.transport_callback', + 'mautic.sparkpost.factory', + 'monolog.logger.mautic', ], ], 'mautic.sparkpost.factory' => [ diff --git a/app/bundles/EmailBundle/Controller/AjaxController.php b/app/bundles/EmailBundle/Controller/AjaxController.php index 67e8d9f388a..01c60915dfe 100644 --- a/app/bundles/EmailBundle/Controller/AjaxController.php +++ b/app/bundles/EmailBundle/Controller/AjaxController.php @@ -185,7 +185,7 @@ protected function testMonitoredEmailServerConnectionAction(Request $request) $dataArray['success'] = 1; $dataArray['message'] = $this->translator->trans('mautic.core.success'); } catch (\Exception $e) { - $dataArray['message'] = $e->getMessage(); + $dataArray['message'] = $this->translator->trans($e->getMessage()); } } diff --git a/app/bundles/EmailBundle/Controller/PublicController.php b/app/bundles/EmailBundle/Controller/PublicController.php index 766825e09ca..bf6b5d6e00f 100644 --- a/app/bundles/EmailBundle/Controller/PublicController.php +++ b/app/bundles/EmailBundle/Controller/PublicController.php @@ -169,6 +169,8 @@ public function unsubscribeAction($idHash) } $contentTemplate = $this->factory->getHelper('theme')->checkForTwigTemplate(':'.$template.':message.html.php'); if (!empty($stat)) { + $successSessionName = 'mautic.email.prefscenter.success'; + if ($lead = $stat->getLead()) { // Set the lead as current lead $leadModel->setCurrentLead($lead); @@ -177,9 +179,11 @@ public function unsubscribeAction($idHash) if ($lead->getPreferredLocale()) { $translator->setLocale($lead->getPreferredLocale()); } - } - $successSessionName = 'mautic.email.prefscenter.success.'.$lead->getId(); + // Add contact ID to the session name in case more contacts + // share the same session/device and the contact is known. + $successSessionName .= ".{$lead->getId()}"; + } if (!$this->get('mautic.helper.core_parameters')->getParameter('show_contact_preferences')) { $message = $this->getUnsubscribeMessage($idHash, $model, $stat, $translator); @@ -219,10 +223,13 @@ public function unsubscribeAction($idHash) if ($savePrefsPresent) { // set custom tag to inject end form // update show pref center slots by looking for their presence in the html - $params = array_merge( + /** @var \Mautic\CoreBundle\Templating\Helper\FormHelper $formHelper */ + $formHelper =$this->get('templating.helper.form'); + $params = array_merge( $viewParameters, [ 'form' => $formView, + 'startform' => $formHelper->start($formView), 'custom_tag' => '', 'showContactFrequency' => false !== strpos($html, 'data-slot="channelfrequency"') || false !== strpos($html, BuilderSubscriber::channelfrequency), 'showContactSegments' => false !== strpos($html, 'data-slot="segmentlist"') || false !== strpos($html, BuilderSubscriber::segmentListRegex), diff --git a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php index 9c86889d737..66837d5ab90 100644 --- a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php @@ -339,7 +339,8 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) foreach ($stats as $contactId => $sentCount) { /** @var LeadEventLog $log */ $log = $event->findLogByContactId($contactId); - $event->fail( + // Pass with a note to the UI because no use retrying + $event->passWithError( $log, $this->translator->trans('mautic.email.contact_already_received_marketing_email', ['%contact%' => $credentialArray[$log->getId()]['primaryIdentifier']]) ); diff --git a/app/bundles/EmailBundle/EventListener/MatchFilterForLeadTrait.php b/app/bundles/EmailBundle/EventListener/MatchFilterForLeadTrait.php index 428a3f30b86..704cba3c530 100644 --- a/app/bundles/EmailBundle/EventListener/MatchFilterForLeadTrait.php +++ b/app/bundles/EmailBundle/EventListener/MatchFilterForLeadTrait.php @@ -122,10 +122,18 @@ protected function matchFilterForLead(array $filter, array $lead) switch ($data['operator']) { case '=': - $groups[$groupNum] = $leadVal == $filterVal; + if ($data['type'] === 'boolean') { + $groups[$groupNum] = $leadVal === $filterVal; + } else { + $groups[$groupNum] = $leadVal == $filterVal; + } break; case '!=': - $groups[$groupNum] = $leadVal != $filterVal; + if ($data['type'] === 'boolean') { + $groups[$groupNum] = $leadVal !== $filterVal; + } else { + $groups[$groupNum] = $leadVal != $filterVal; + } break; case 'gt': $groups[$groupNum] = $leadVal > $filterVal; diff --git a/app/bundles/EmailBundle/EventListener/MomentumSubscriber.php b/app/bundles/EmailBundle/EventListener/MomentumSubscriber.php index fb5f065903f..e06d2efd630 100644 --- a/app/bundles/EmailBundle/EventListener/MomentumSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/MomentumSubscriber.php @@ -22,6 +22,7 @@ use Mautic\QueueBundle\Queue\QueueName; use Mautic\QueueBundle\Queue\QueueService; use Mautic\QueueBundle\QueueEvents; +use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; /** @@ -48,15 +49,18 @@ class MomentumSubscriber extends CommonSubscriber * @param MomentumCallbackInterface $momentumCallback * @param QueueService $queueService * @param RequestStorageHelper $requestStorageHelper + * @param LoggerInterface $logger */ public function __construct( MomentumCallbackInterface $momentumCallback, QueueService $queueService, - RequestStorageHelper $requestStorageHelper + RequestStorageHelper $requestStorageHelper, + LoggerInterface $logger ) { $this->momentumCallback = $momentumCallback; $this->queueService = $queueService; $this->requestStorageHelper = $requestStorageHelper; + $this->logger = $logger; } /** @@ -80,9 +84,15 @@ public function onMomentumWebhookQueueProcessing(QueueConsumerEvent $event) if ($event->checkTransport(MomentumTransport::class)) { $payload = $event->getPayload(); $key = $payload['key']; - $request = $this->requestStorageHelper->getRequest($key); - $this->momentumCallback->processCallbackRequest($request); - $this->requestStorageHelper->deleteCachedRequest($key); + + try { + $request = $this->requestStorageHelper->getRequest($key); + $this->momentumCallback->processCallbackRequest($request); + $this->requestStorageHelper->deleteCachedRequest($key); + } catch (\UnexpectedValueException $e) { + $this->logger->error($e->getMessage()); + } + $event->setResult(QueueConsumerResults::ACKNOWLEDGE); } } @@ -96,7 +106,7 @@ public function onMomentumWebhookRequest(TransportWebhookEvent $event) if ($this->queueService->isQueueEnabled() && $event->transportIsInstanceOf($transport)) { // Beanstalk jobs are limited to 65,535 kB. Momentum can send up to 10.000 items per request. // One item has about 1,6 kB. Lets store the request to the cache storage instead of the job itself. - $key = $this->requestStorageHelper->storeRequest($transport, $event->getRequest()); + $key = $this->requestStorageHelper->storeRequest($transport, $event->getRequest()); $this->queueService->publishToQueue(QueueName::TRANSPORT_WEBHOOK, ['transport' => $transport, 'key' => $key]); $event->stopPropagation(); } diff --git a/app/bundles/EmailBundle/EventListener/ReportSubscriber.php b/app/bundles/EmailBundle/EventListener/ReportSubscriber.php index 00eed94b4f0..4c72c9f1979 100644 --- a/app/bundles/EmailBundle/EventListener/ReportSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/ReportSubscriber.php @@ -373,6 +373,13 @@ public function onReportGenerate(ReportGeneratorEvent $event) ) ->where('cut2.channel = \'email\' AND ph.source = \'email\'') ->groupBy('cut2.channel_id, ph.lead_id'); + + if ($event->hasFilter('e.id')) { + $filterParam = $event->createParameterName(); + $qbcut->andWhere("cut2.channel_id = :{$filterParam}"); + $qb->setParameter($filterParam, $event->getFilterValue('e.id'), \PDO::PARAM_INT); + } + $qb->leftJoin( 'e', sprintf('(%s)', $qbcut->getSQL()), diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index 474927345ce..3137cdbc81d 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -1015,9 +1015,17 @@ public function getContentHash() */ public function setTo($addresses, $name = null) { + $name = $this->cleanName($name); + if (!is_array($addresses)) { - $name = $this->cleanName($name); $addresses = [$addresses => $name]; + } elseif (array_keys($addresses)[0] === 0) { + // We need an array of $email => $name pairs + $addresses = array_reduce($addresses, function ($address, $item) use ($name) { + $address[$item] = $name; + + return $address; + }, []); } $this->checkBatchMaxRecipients(count($addresses)); @@ -1631,15 +1639,17 @@ public function dispatchSendEvent() protected function logError($error, $context = null) { if ($error instanceof \Exception) { - $errorMessage = $error->getMessage(); - $error = ('dev' === MAUTIC_ENV) ? (string) $error : $errorMessage; + $exceptionContext = ['exception' => $error]; + $errorMessage = $error->getMessage(); + $error = ('dev' === MAUTIC_ENV) ? (string) $error : $errorMessage; // Clean up the error message $errorMessage = trim(preg_replace('/(.*?)Log data:(.*)$/is', '$1', $errorMessage)); $this->fatal = true; } else { - $errorMessage = trim($error); + $exceptionContext = []; + $errorMessage = trim($error); } $logDump = $this->logger->dump(); @@ -1659,7 +1669,7 @@ protected function logError($error, $context = null) $this->logger->clear(); - $this->factory->getLogger()->log('error', '[MAIL ERROR] '.$error); + $this->factory->getLogger()->log('error', '[MAIL ERROR] '.$error, $exceptionContext); } /** diff --git a/app/bundles/EmailBundle/Helper/RequestStorageHelper.php b/app/bundles/EmailBundle/Helper/RequestStorageHelper.php index 58c366c4d6c..d443321908f 100644 --- a/app/bundles/EmailBundle/Helper/RequestStorageHelper.php +++ b/app/bundles/EmailBundle/Helper/RequestStorageHelper.php @@ -60,10 +60,19 @@ public function storeRequest($transportName, Request $request) * @param string $key * * @return Request + * + * @throws \UnexpectedValueException */ public function getRequest($key) { - return new Request([], $this->cacheStorage->get($key)); + $key = $this->removeCachePrefix($key); + $cachedRequest = $this->cacheStorage->get($key); + + if (false === $cachedRequest) { + throw new \UnexpectedValueException("Request with key '{$key}' was not found in the cache store '{$this->cacheStorage->getAdaptorClassName()}'."); + } + + return new Request([], $cachedRequest); } /** @@ -71,6 +80,8 @@ public function getRequest($key) */ public function deleteCachedRequest($key) { + $key = $this->removeCachePrefix($key); + $this->cacheStorage->delete($key); } @@ -83,11 +94,33 @@ public function deleteCachedRequest($key) */ public function getTransportNameFromKey($key) { - list($transportName) = explode(self::KEY_SEPARATOR, $key); + $key = $this->removeCachePrefix($key); + + // Take the part before the key separator as the serialized transpot name. + list($serializedTransportName) = explode(self::KEY_SEPARATOR, $key); + + // Unserialize transport name to the standard full class name. + $transportName = str_replace('|', '\\', $serializedTransportName); return $transportName; } + /** + * Remove the default cache key prefix if set. + * + * @param string $key + * + * @return string + */ + private function removeCachePrefix($key) + { + if (strpos($key, 'mautic:') === 0) { + $key = ltrim($key, 'mautic:'); + } + + return $key; + } + /** * Generates unique hash in format $transportName:webhook_request:unique.hash. * diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index dd10b04437d..0be2163124e 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -401,11 +401,11 @@ public function getEntities(array $args = []) $queued = $this->cacheStorageHelper->get(sprintf('%s|%s|%s', 'email', $entity->getId(), 'queued')); $pending = $this->cacheStorageHelper->get(sprintf('%s|%s|%s', 'email', $entity->getId(), 'pending')); - if ($queued) { + if ($queued !== false) { $entity->setQueuedCount($queued); } - if ($pending) { + if ($pending !== false) { $entity->setPendingCount($pending); } } @@ -977,7 +977,7 @@ public function getPendingLeads( $countWithMaxMin ); - if ($storeToCache && !empty($total)) { + if ($storeToCache) { if ($countOnly && $countWithMaxMin) { $toStore = $total['count']; } elseif ($countOnly) { @@ -1006,10 +1006,7 @@ public function getQueuedCounts(Email $email, $includeVariants = true) } $queued = (int) $this->messageQueueModel->getQueuedChannelCount('email', $ids); - - if ($queued) { - $this->cacheStorageHelper->set(sprintf('%s|%s|%s', 'email', $email->getId(), 'queued'), $queued); - } + $this->cacheStorageHelper->set(sprintf('%s|%s|%s', 'email', $email->getId(), 'queued'), $queued); return $queued; } @@ -1563,7 +1560,10 @@ public function sendEmailToUser( return false; } - $mailer = $this->mailHelper->getMailer(); + $mailer = $this->mailHelper->getMailer(); + if (!isset($lead['companies'])) { + $lead['companies'] = $this->companyModel->getRepository()->getCompaniesByLeadId($lead['id']); + } $mailer->setLead($lead, true); $mailer->setTokens($tokens); $mailer->setEmail($email, false, $emailSettings[$emailId]['slots'], $assetAttachments, (!$saveStat)); @@ -1607,7 +1607,12 @@ public function sendEmailToUser( } if (!isset($user['email'])) { - $userEntity = $this->userModel->getEntity($id); + $userEntity = $this->userModel->getEntity($id); + + if ($userEntity === null) { + continue; + } + $user['email'] = $userEntity->getEmail(); $user['firstname'] = $userEntity->getFirstName(); $user['lastname'] = $userEntity->getLastName(); diff --git a/app/bundles/EmailBundle/MonitoredEmail/Exception/NotConfiguredException.php b/app/bundles/EmailBundle/MonitoredEmail/Exception/NotConfiguredException.php new file mode 100644 index 00000000000..fb9ea2b1908 --- /dev/null +++ b/app/bundles/EmailBundle/MonitoredEmail/Exception/NotConfiguredException.php @@ -0,0 +1,16 @@ +imapFullPath, $this->settings['user'], @@ -478,9 +483,11 @@ public function statusMailbox() */ public function getListingFolders() { - static $folders = []; + if (!$this->isConfigured()) { + throw new NotConfiguredException('mautic.email.config.monitored_email.not_configured'); + } - if (!isset($folders[$this->imapFullPath]) && $this->isConfigured()) { + if (!isset($this->folders[$this->imapFullPath])) { $tempFolders = @imap_list($this->getImapStream(), $this->imapPath, '*'); if (!empty($tempFolders)) { @@ -492,10 +499,10 @@ public function getListingFolders() $tempFolders = []; } - $folders[$this->imapFullPath] = $tempFolders; + $this->folders[$this->imapFullPath] = $tempFolders; } - return $folders[$this->imapFullPath]; + return $this->folders[$this->imapFullPath]; } /** diff --git a/app/bundles/EmailBundle/MonitoredEmail/Processor/Bounce/DsnParser.php b/app/bundles/EmailBundle/MonitoredEmail/Processor/Bounce/DsnParser.php index bb8c3507a19..cce9e0fa6a6 100644 --- a/app/bundles/EmailBundle/MonitoredEmail/Processor/Bounce/DsnParser.php +++ b/app/bundles/EmailBundle/MonitoredEmail/Processor/Bounce/DsnParser.php @@ -460,7 +460,7 @@ public function parse($dsnMessage, $dsnReport) * sample 2: * Diagnostic-Code: SMTP; 550 sorry, that recipient doesn't exist (#5.7.1) */ - elseif (preg_match("/(?:alias|account|recipient|address|email|mailbox|user).*(?:n't|not) exist/is", $diagnosisCode)) { + elseif (preg_match("/(?:alias|account|recipient|address|email|mailbox|user).*(?:n't|not).*exist/is", $diagnosisCode)) { $result['rule_cat'] = Category::UNKNOWN; $result['rule_no'] = '0205'; } diff --git a/app/bundles/EmailBundle/MonitoredEmail/Processor/Reply.php b/app/bundles/EmailBundle/MonitoredEmail/Processor/Reply.php index 8d72c57adea..fa9849e8512 100644 --- a/app/bundles/EmailBundle/MonitoredEmail/Processor/Reply.php +++ b/app/bundles/EmailBundle/MonitoredEmail/Processor/Reply.php @@ -112,6 +112,7 @@ public function process(Message $message) // A stat has been found so let's compare to the From address for the contact to prevent false positives $contactEmail = $this->cleanEmail($stat->getLead()->getEmail()); $fromEmail = $this->cleanEmail($repliedEmail->getFromAddress()); + if ($contactEmail !== $fromEmail) { // We can't reliably assume this email was from the originating contact $this->logger->debug('MONITORED EMAIL: '.$contactEmail.' != '.$fromEmail.' so cannot confirm match'); diff --git a/app/bundles/EmailBundle/MonitoredEmail/Processor/Reply/Parser.php b/app/bundles/EmailBundle/MonitoredEmail/Processor/Reply/Parser.php index 5ae423e91b4..81b14341b6b 100644 --- a/app/bundles/EmailBundle/MonitoredEmail/Processor/Reply/Parser.php +++ b/app/bundles/EmailBundle/MonitoredEmail/Processor/Reply/Parser.php @@ -40,7 +40,7 @@ public function __construct(Message $message) */ public function parse() { - if (!preg_match('/email\/(.*?)\.gif/', $this->message->textHtml, $parts)) { + if (!preg_match('/email\/([a-zA-Z0-9]+)\.gif/', $this->message->textHtml, $parts)) { throw new ReplyNotFound(); } diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO.php index 6a142b6789f..f54e9b21fc9 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO.php @@ -122,9 +122,15 @@ public function jsonSerialize() if (count($this->metadata) !== 0) { $json['metadata'] = $this->metadata; } - if (count($this->substitutionData) !== 0) { + + if (count($this->substitutionData) === 0) { + // `substitution_data` is required but Sparkpost will return the following error with empty arrays: + // field 'substitution_data' is of type 'json_array', but needs to be of type 'json_object' + $json['substitution_data'] = new \stdClass(); + } else { $json['substitution_data'] = $this->substitutionData; } + if ($this->returnPath !== null) { $json['return_path'] = $this->returnPath; } diff --git a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php index 9dc372aaa7a..bd3736ef149 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php +++ b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php @@ -5,9 +5,6 @@ use Http\Adapter\Guzzle6\Client as GuzzleAdapter; use SparkPost\SparkPost; -/** - * Class SparkpostFactory. - */ final class SparkpostFactory implements SparkpostFactoryInterface { /** @@ -16,8 +13,6 @@ final class SparkpostFactory implements SparkpostFactoryInterface private $client; /** - * SparkpostFactory constructor. - * * @param GuzzleAdapter $client */ public function __construct(GuzzleAdapter $client) @@ -26,11 +21,11 @@ public function __construct(GuzzleAdapter $client) } /** - * @param $host - * @param $apiKey - * @param null $port + * @param string $host + * @param string $apiKey + * @param int|null $port * - * @return mixed|SparkPost + * @return SparkPost */ public function create($host, $apiKey, $port = null) { @@ -39,12 +34,13 @@ public function create($host, $apiKey, $port = null) } $options = [ - 'host' => '', - 'protocol' => 'https', - 'port' => $port, - 'key' => ($apiKey) ?: 1234, // prevent Exception: You must provide an API key + 'key' => ($apiKey) ?: 1234, // prevent Exception: You must provide an API key ]; + if ($port) { + $options['port'] = $port; + } + $hostInfo = parse_url($host); if ($hostInfo) { $options['protocol'] = $hostInfo['scheme']; diff --git a/app/bundles/EmailBundle/Swiftmailer/Transport/SparkpostTransport.php b/app/bundles/EmailBundle/Swiftmailer/Transport/SparkpostTransport.php index c093c4f406d..2075d998aa4 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Transport/SparkpostTransport.php +++ b/app/bundles/EmailBundle/Swiftmailer/Transport/SparkpostTransport.php @@ -14,9 +14,10 @@ namespace Mautic\EmailBundle\Swiftmailer\Transport; use GuzzleHttp\Client; -use Http\Adapter\Guzzle6\Client as GuzzleAdapter; use Mautic\EmailBundle\Model\TransportCallback; +use Mautic\EmailBundle\Swiftmailer\Sparkpost\SparkpostFactoryInterface; use Mautic\LeadBundle\Entity\DoNotContact; +use Psr\Log\LoggerInterface; use SparkPost\SparkPost; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Translation\TranslatorInterface; @@ -42,16 +43,35 @@ class SparkpostTransport extends AbstractTokenArrayTransport implements \Swift_T private $transportCallback; /** - * @param string $apiKey - * @param TranslatorInterface $translator - * @param TransportCallback $transportCallback + * @var SparkpostFactoryInterface */ - public function __construct($apiKey, TranslatorInterface $translator, TransportCallback $transportCallback) - { + private $sparkpostFactory; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param string $apiKey + * @param TranslatorInterface $translator + * @param TransportCallback $transportCallback + * @param SparkpostFactoryInterface $sparkpostFactory + * @param LoggerInterface $logger + */ + public function __construct( + $apiKey, + TranslatorInterface $translator, + TransportCallback $transportCallback, + SparkpostFactoryInterface $sparkpostFactory, + LoggerInterface $logger + ) { $this->setApiKey($apiKey); $this->translator = $translator; $this->transportCallback = $transportCallback; + $this->sparkpostFactory = $sparkpostFactory; + $this->logger = $logger; } /** @@ -83,14 +103,20 @@ public function start() } /** + * Creates new SparkPost HTTP client. + * If no API key is provided then the default one is used. + * + * @param string $apiKey + * * @return SparkPost */ - protected function createSparkPost() + protected function createSparkPost($apiKey = null) { - $httpAdapter = new GuzzleAdapter(new Client()); - $sparky = new SparkPost($httpAdapter, ['key' => $this->apiKey]); + if (null === $apiKey) { + $apiKey = $this->apiKey; + } - return $sparky; + return $this->sparkpostFactory->create('', $apiKey); } /** @@ -112,17 +138,21 @@ public function send(\Swift_Mime_Message $message, &$failedRecipients = null) } try { - $sparkPost = $this->createSparkPost(); $sparkPostMessage = $this->getSparkPostMessage($message); - $response = $sparkPost->transmissions->post($sparkPostMessage); + $sparkPostClient = $this->createSparkPost(); - $response = $response->wait(); - if (200 == (int) $response->getStatusCode()) { - $results = $response->getBody(); - if (!$sendCount = $results['results']['total_accepted_recipients']) { - $this->processImmediateSendFeedback($sparkPostMessage, $results); - } + $this->checkTemplateIsValid($sparkPostClient, $sparkPostMessage); + + $promise = $sparkPostClient->transmissions->post($sparkPostMessage); + $response = $promise->wait(); + $body = $response->getBody(); + + if ($errorMessage = $this->getErrorMessageFromResponseBody($body)) { + $this->processImmediateSendFeedback($sparkPostMessage, $body); + throw new \Exception($errorMessage); } + + $sendCount = $body['results']['total_accepted_recipients']; } catch (\Exception $e) { $this->throwException($e->getMessage()); } @@ -192,7 +222,7 @@ public function getSparkPostMessage(\Swift_Mime_Message $message) 'metadata' => [], ]; - if (isset($metadata[$to['email']])) { + if (isset($metadata[$to['email']]['tokens'])) { foreach ($metadata[$to['email']]['tokens'] as $token => $value) { $recipient['substitution_data'][$mergeVars[$token]] = $value; } @@ -201,10 +231,14 @@ public function getSparkPostMessage(\Swift_Mime_Message $message) $recipient['metadata'] = $metadata[$to['email']]; } - // Apparently Sparkpost doesn't like empty substitution_data or metadata + // Sparkpost requires substitution_data which can be byspassed by using MailHelper::setTo() rather than a Lead via MailHelper::setLead() + // Without it, Sparkpost returns the error: "field 'substitution_data' is required" + // But, it can't be an empty array or Sparkpost will return error: field 'substitution_data' is of type 'json_array', but needs to be of type 'json_object' if (empty($recipient['substitution_data'])) { - unset($recipient['substitution_data']); + $recipient['substitution_data'] = new \stdClass(); } + + // Sparkpost doesn't like empty metadata if (empty($recipient['metadata'])) { unset($recipient['metadata']); } @@ -214,30 +248,20 @@ public function getSparkPostMessage(\Swift_Mime_Message $message) // CC and BCC fields need to be included as a normal TO address with token duplication // https://www.sparkpost.com/docs/faq/cc-bcc-with-rest-api/ - token duplication is not mentioned here // See test for CC and BCC too - $substitution_data = $recipient['substitution_data']; - if (!empty($message['recipients']['cc'])) { - foreach ($message['recipients']['cc'] as $cc => $content) { - $recipient = [ - 'address' => [ - 'email' => $cc, - ], - 'header_to' => $to['email'], - 'substitution_data' => $substitution_data, - ]; - $recipients[] = $recipient; - } - } - - if (!empty($message['recipients']['bcc'])) { - foreach ($message['recipients']['bcc'] as $bcc => $content) { - $recipient = [ - 'address' => [ - 'email' => $bcc, - ], - 'header_to' => $to['email'], - 'substitution_data' => $substitution_data, - ]; - $recipients[] = $recipient; + foreach (['cc', 'bcc'] as $copyType) { + if (!empty($message['recipients'][$copyType])) { + foreach ($message['recipients'][$copyType] as $email => $content) { + $copyRecipient = [ + 'address' => ['email' => $email], + 'header_to' => $to['email'], + ]; + + if (!empty($recipient['substitution_data'])) { + $copyRecipient['substitution_data'] = $recipient['substitution_data']; + } + + $recipients[] = $copyRecipient; + } } } } @@ -371,7 +395,40 @@ public function processCallbackRequest(Request $request) } /** - * Check for SparkPost rejection as they will not send a webhook for a single recipient rejected immediately. + * Checks with Sparkpost whether the email template is valid. + * Substitution data are taken from the first recipient. + * + * @param Sparkpost $sparkPostClient + * @param array $sparkPostMessage + * + * @throws \UnexpectedValueException + */ + protected function checkTemplateIsValid(Sparkpost $sparkPostClient, array $sparkPostMessage) + { + // Take substitution_data from the first recipient. + if (empty($sparkPostMessage['substitution_data']) && isset($sparkPostMessage['recipients'][0]['substitution_data'])) { + $sparkPostMessage['substitution_data'] = $sparkPostMessage['recipients'][0]['substitution_data']; + unset($sparkPostMessage['recipients']); + } + + $promise = $sparkPostClient->request('POST', 'utils/content-previewer', $sparkPostMessage); + $response = $promise->wait(); + $body = $response->getBody(); + + if ($response->getStatusCode() === 403) { + // We cannot fail as it would be a BC break. Throw a warning and continue. + $this->logger->warning("The permission 'Templates: Preview' is not enabled. Enable it to let Mautic check email template validity before send."); + + return; + } + + if ($errorMessage = $this->getErrorMessageFromResponseBody($body)) { + throw new \UnexpectedValueException($errorMessage); + } + } + + /** + * Check for SparkPost rejection for immediate error messages. * * @param array $message * @param array $response @@ -379,8 +436,8 @@ public function processCallbackRequest(Request $request) private function processImmediateSendFeedback(array $message, array $response) { if (!empty($response['errors'][0]['code']) && 1902 == (int) $response['errors'][0]['code']) { - $comments = $response['errors'][0]['description']; - $emailAddress = $message['recipients']['to'][0]['email']; + $comments = $this->getErrorMessageFromResponseBody($response); + $emailAddress = $message['recipients'][0]['address']['email']; $metadata = $this->getMetadata(); if (isset($metadata[$emailAddress]) && isset($metadata[$emailAddress]['leadId'])) { @@ -390,6 +447,27 @@ private function processImmediateSendFeedback(array $message, array $response) } } + /** + * Sparkpost renamed the error message property name from 'description' to 'message'. + * Ensure that we get the error message before and after the change is made. + * + * @see https://www.sparkpost.com/blog/error-handling-transmissions-api + * + * @param array $response + * + * @return string + */ + private function getErrorMessageFromResponseBody(array $response) + { + if (isset($response['errors'][0]['description'])) { + return $response['errors'][0]['description']; + } elseif (isset($response['errors'][0]['message'])) { + return $response['errors'][0]['message']; + } + + return null; + } + /** * @param $hashId * @param array $event diff --git a/app/bundles/EmailBundle/Tests/EventListener/MomentumSubscriberTest.php b/app/bundles/EmailBundle/Tests/EventListener/MomentumSubscriberTest.php index 297b2c282a5..c3a9b8c3b8a 100644 --- a/app/bundles/EmailBundle/Tests/EventListener/MomentumSubscriberTest.php +++ b/app/bundles/EmailBundle/Tests/EventListener/MomentumSubscriberTest.php @@ -20,12 +20,16 @@ use Mautic\QueueBundle\Queue\QueueConsumerResults; use Mautic\QueueBundle\Queue\QueueName; use Mautic\QueueBundle\Queue\QueueService; +use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; class MomentumSubscriberTest extends \PHPUnit_Framework_TestCase { private $queueServiceMock; private $momentumCallbackMock; + private $requestStorageHelperMock; + private $loggerMock; + private $momentumSubscriber; protected function setUp() { @@ -34,7 +38,13 @@ protected function setUp() $this->momentumCallbackMock = $this->createMock(MomentumCallbackInterface::class); $this->queueServiceMock = $this->createMock(QueueService::class); $this->requestStorageHelperMock = $this->createMock(RequestStorageHelper::class); - $this->momentumSubscriber = new MomentumSubscriber($this->momentumCallbackMock, $this->queueServiceMock, $this->requestStorageHelperMock); + $this->loggerMock = $this->createMock(LoggerInterface::class); + $this->momentumSubscriber = new MomentumSubscriber( + $this->momentumCallbackMock, + $this->queueServiceMock, + $this->requestStorageHelperMock, + $this->loggerMock + ); } public function testOnMomentumWebhookQueueProcessingForNonMomentumTransport() @@ -95,6 +105,41 @@ public function testOnMomentumWebhookQueueProcessingForMomentumTransport() $this->momentumSubscriber->onMomentumWebhookQueueProcessing($queueConsumerEvent); } + public function testOnMomentumWebhookQueueProcessingForMomentumTransportIfRequestNotFounc() + { + $queueConsumerEvent = $this->createMock(QueueConsumerEvent::class); + + $queueConsumerEvent->expects($this->once()) + ->method('getPayload') + ->willReturn([ + 'transport' => MomentumTransport::class, + 'key' => 'value', + ]); + + $queueConsumerEvent->expects($this->once()) + ->method('checkTransport') + ->with(MomentumTransport::class) + ->willReturn(true); + + $this->requestStorageHelperMock->expects($this->once()) + ->method('getRequest') + ->with('value') + ->will($this->throwException(new \UnexpectedValueException('Error message'))); + + $this->momentumCallbackMock->expects($this->never()) + ->method('processCallbackRequest'); + + $this->loggerMock->expects($this->once()) + ->method('error') + ->with('Error message'); + + $queueConsumerEvent->expects($this->once()) + ->method('setResult') + ->with(QueueConsumerResults::ACKNOWLEDGE); + + $this->momentumSubscriber->onMomentumWebhookQueueProcessing($queueConsumerEvent); + } + public function testOnMomentumWebhookRequestWhenQueueIsDisabled() { $transportWebhookEvent = $this->createMock(TransportWebhookEvent::class); diff --git a/app/bundles/EmailBundle/Tests/Helper/MailHelperTest.php b/app/bundles/EmailBundle/Tests/Helper/MailHelperTest.php index 38d9fa4d469..80c24c9d9e9 100644 --- a/app/bundles/EmailBundle/Tests/Helper/MailHelperTest.php +++ b/app/bundles/EmailBundle/Tests/Helper/MailHelperTest.php @@ -674,4 +674,34 @@ protected function getMockFactory($mailIsOwner = true, $parameterMap = []) return $mockFactory; } + + public function testArrayOfAddressesAreRemappedIntoEmailToNameKeyValuePair() + { + $mockFactory = $this->getMockBuilder(MauticFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $mockFactory->method('getParameter') + ->will( + $this->returnValueMap( + [ + ['mailer_return_path', false, null], + ['mailer_spool_type', false, 'memory'], + ] + ) + ); + + $swiftMailer = new \Swift_Mailer(new SmtpTransport()); + + $mailer = new MailHelper($mockFactory, $swiftMailer, ['nobody@nowhere.com' => 'No Body']); + + $mailer->setTo(['sombody@somewhere.com', 'sombodyelse@somewhere.com'], 'test'); + + $this->assertEquals( + [ + 'sombody@somewhere.com' => 'test', + 'sombodyelse@somewhere.com' => 'test', + ], + $mailer->message->getTo() + ); + } } diff --git a/app/bundles/EmailBundle/Tests/Helper/RequestStorageHelperTest.php b/app/bundles/EmailBundle/Tests/Helper/RequestStorageHelperTest.php index b5d8657b895..0ada348cfa2 100644 --- a/app/bundles/EmailBundle/Tests/Helper/RequestStorageHelperTest.php +++ b/app/bundles/EmailBundle/Tests/Helper/RequestStorageHelperTest.php @@ -72,8 +72,30 @@ public function testGetRequest() $this->assertEquals($payload, $request->request->all()); } + public function testGetRequestIfNotFound() + { + $payload = ['some' => 'values']; + $key = MomentumTransport::class.':webhook_request:5b43832134cfb0.36545510'; + + $this->cacheStorageMock->expects($this->once()) + ->method('get') + ->with($key) + ->willReturn(false); + + $this->expectException(\UnexpectedValueException::class); + $this->helper->getRequest($key); + } + public function testGetTransportNameFromKey() { $this->assertEquals(MomentumTransport::class, $this->helper->getTransportNameFromKey('Mautic\EmailBundle\Swiftmailer\Transport\MomentumTransport:webhook_request:5b43832134cfb0.36545510')); } + + /** + * The StorageHelper will add '%mautic.db_table_prefix%' as a prefix to each cache key. + */ + public function testGetTransportNameFromKeyWithGlobalPrefix() + { + $this->assertEquals(MomentumTransport::class, $this->helper->getTransportNameFromKey('mautic:Mautic|EmailBundle|Swiftmailer|Transport|MomentumTransport:webhook_request:5bfbe8ce671198.00044461')); + } } diff --git a/app/bundles/EmailBundle/Tests/MonitoredEmail/Processor/Bounce/DsnParserTest.php b/app/bundles/EmailBundle/Tests/MonitoredEmail/Processor/Bounce/DsnParserTest.php index 502e2e405ba..6b50586a0d4 100644 --- a/app/bundles/EmailBundle/Tests/MonitoredEmail/Processor/Bounce/DsnParserTest.php +++ b/app/bundles/EmailBundle/Tests/MonitoredEmail/Processor/Bounce/DsnParserTest.php @@ -46,6 +46,35 @@ public function testBouncedEmailIsReturnedFromParsedDsnReport() $this->assertTrue($bounce->isFinal()); } + /** + * @testdox Test a Postfix BouncedEmail is returned from a dsn report + * + * @covers \Mautic\EmailBundle\MonitoredEmail\Processor\Bounce\DsnParser::getBounce() + * @covers \Mautic\EmailBundle\MonitoredEmail\Processor\Bounce\DsnParser::parse() + */ + public function testPostfixBouncedEmailIsReturnedFromParsedDsnReport() + { + $message = new Message(); + $message->dsnReport = <<<'DSN' +Final-Recipient: rfc822; aaaaaaaaaaaaa@yoursite.com +Original-Recipient: rfc822;aaaaaaaaaaaaa@yoursite.com +Action: failed +Status: 5.1.1 +Remote-MTA: dns; mail-server.yoursite.com +Diagnostic-Code: smtp; 550 5.1.1 User doesn't + exist: aaaaaaaaaaaaa@yoursite.com +DSN; + + $parser = new DsnParser(); + $bounce = $parser->getBounce($message); + + $this->assertInstanceOf(BouncedEmail::class, $bounce); + $this->assertEquals('aaaaaaaaaaaaa@yoursite.com', $bounce->getContactEmail()); + $this->assertEquals(Category::UNKNOWN, $bounce->getRuleCategory()); + $this->assertEquals(Type::HARD, $bounce->getType()); + $this->assertTrue($bounce->isFinal()); + } + /** * @testdox Test that an exception is thrown if a bounce cannot be found in a dsn report * diff --git a/app/bundles/EmailBundle/Tests/MonitoredEmail/Processor/Reply/ParserTest.php b/app/bundles/EmailBundle/Tests/MonitoredEmail/Processor/Reply/ParserTest.php index aadb07ecc4b..f2d5996cd22 100644 --- a/app/bundles/EmailBundle/Tests/MonitoredEmail/Processor/Reply/ParserTest.php +++ b/app/bundles/EmailBundle/Tests/MonitoredEmail/Processor/Reply/ParserTest.php @@ -39,6 +39,27 @@ public function testThatReplyIsDetectedThroughTrackingPixel() $this->assertEquals('123abc', $replyEmail->getStatHash()); } + /** + * @testdox Test that an email is found inside a feedback report + * + * @covers \Mautic\EmailBundle\MonitoredEmail\Processor\Reply\Parser::parse() + * @covers \Mautic\EmailBundle\MonitoredEmail\Processor\Reply\RepliedEmail::getStatHash() + */ + public function testThatReplyIsDetectedThroughTrackingPixelWithUnsubcribeLink() + { + $message = new Message(); + $message->textHtml = <<<'BODY' +

On Mar 13, 2019, at 16:31, Alan Hartless <email@test.com> wrote:


Hello there!


We haven\'t heard from you for a while... 

Unsubscribe to no longer receive emails from us. | Having trouble reading this email? Click here. 
 

+BODY; + + $parser = new Parser($message); + + $replyEmail = $parser->parse(); + $this->assertInstanceOf(RepliedEmail::class, $replyEmail); + + $this->assertEquals('5c897694957a7581067884', $replyEmail->getStatHash()); + } + /** * @testdox Test that an exeption is thrown if the hash is not found * diff --git a/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php index 628e6c79876..6127d00c3c7 100644 --- a/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php +++ b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php @@ -72,40 +72,46 @@ private function geTransformToTransmissionComplexData() "email":"to1@test.local", "name":"To1 test", "header_to":"to1@test.local" - } + }, + "substitution_data": {} }, { "address":{ "email":"to2@test.local", "name":"To2 test", "header_to":"to2@test.local" - } + }, + "substitution_data": {} }, { "address":{ "email":"cc1@test.local", "name":"CC1 test", "header_to":"cc1@test.local" - } + }, + "substitution_data": {} }, { "address":{ "email":"cc2@test.local", "name":"CC2 test", "header_to":"cc2@test.local" - } + }, + "substitution_data": {} }, { "address":{ "email":"bcc1@test.local", "name":"BCC1 test" - } + }, + "substitution_data": {} }, { "address":{ "email":"bcc2@test.local", "name":"BCC2 test" - } + }, + "substitution_data": {} } ], "content":{ @@ -208,26 +214,30 @@ private function geTransformToTransmissionComplexDataWithEmailName() "email":"cc1@test.local", "name":"CC1 test", "header_to":"cc1@test.local" - } + }, + "substitution_data": {} }, { "address":{ "email":"cc2@test.local", "name":"CC2 test", "header_to":"cc2@test.local" - } + }, + "substitution_data": {} }, { "address":{ "email":"bcc1@test.local", "name":"BCC1 test" - } + }, + "substitution_data": {} }, { "address":{ "email":"bcc2@test.local", "name":"BCC2 test" - } + }, + "substitution_data": {} } ], "content":{ @@ -344,26 +354,30 @@ private function geTransformToTransmissionComplexDataWithUtmTag() "email":"cc1@test.local", "name":"CC1 test", "header_to":"cc1@test.local" - } + }, + "substitution_data": {} }, { "address":{ "email":"cc2@test.local", "name":"CC2 test", "header_to":"cc2@test.local" - } + }, + "substitution_data": {} }, { "address":{ "email":"bcc1@test.local", "name":"BCC1 test" - } + }, + "substitution_data": {} }, { "address":{ "email":"bcc2@test.local", "name":"BCC2 test" - } + }, + "substitution_data": {} } ], "content":{ diff --git a/app/bundles/EmailBundle/Tests/Transport/SparkpostTransportMessageTest.php b/app/bundles/EmailBundle/Tests/Transport/SparkpostTransportMessageTest.php index b63da2f8a96..87eab83ef1d 100644 --- a/app/bundles/EmailBundle/Tests/Transport/SparkpostTransportMessageTest.php +++ b/app/bundles/EmailBundle/Tests/Transport/SparkpostTransportMessageTest.php @@ -14,7 +14,9 @@ use Mautic\CoreBundle\Translation\Translator; use Mautic\EmailBundle\Model\TransportCallback; use Mautic\EmailBundle\Swiftmailer\Message\MauticMessage; +use Mautic\EmailBundle\Swiftmailer\Sparkpost\SparkpostFactoryInterface; use Mautic\EmailBundle\Swiftmailer\Transport\SparkpostTransport; +use Psr\Log\LoggerInterface; class SparkpostTransportMessageTest extends \PHPUnit_Framework_TestCase { @@ -22,6 +24,8 @@ public function testCcAndBccFields() { $translator = $this->createMock(Translator::class); $transportCallback = $this->createMock(TransportCallback::class); + $sparkpostFactory = $this->createMock(SparkpostFactoryInterface::class); + $logger = $this->createMock(LoggerInterface::class); $message = new MauticMessage('Test subject', 'First Name: {formfield=first_name}'); $message->addFrom('from@xx.xx'); @@ -53,7 +57,7 @@ public function testCcAndBccFields() ] ); - $sparkpost = new SparkpostTransport('1234', $translator, $transportCallback); + $sparkpost = new SparkpostTransport('1234', $translator, $transportCallback, $sparkpostFactory, $logger); $sparkpostMessage = $sparkpost->getSparkPostMessage($message); diff --git a/app/bundles/EmailBundle/Tests/Transport/SparkpostTransportTest.php b/app/bundles/EmailBundle/Tests/Transport/SparkpostTransportTest.php index 76bf5ec99a9..9182e03ca09 100644 --- a/app/bundles/EmailBundle/Tests/Transport/SparkpostTransportTest.php +++ b/app/bundles/EmailBundle/Tests/Transport/SparkpostTransportTest.php @@ -11,31 +11,75 @@ namespace Mautic\EmailBundle\Tests\Transport; -use Mautic\CoreBundle\Translation\Translator; +use GuzzleHttp\Psr7\Response; +use GuzzleHttp\Psr7\Stream; +use Http\Adapter\Guzzle6\Client; +use Http\Promise\Promise; use Mautic\EmailBundle\Model\TransportCallback; +use Mautic\EmailBundle\Swiftmailer\Message\MauticMessage; +use Mautic\EmailBundle\Swiftmailer\Sparkpost\SparkpostFactoryInterface; use Mautic\EmailBundle\Swiftmailer\Transport\SparkpostTransport; use Mautic\LeadBundle\Entity\DoNotContact; +use Psr\Log\LoggerInterface; +use SparkPost\SparkPost; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Translation\TranslatorInterface; class SparkpostTransportTest extends \PHPUnit_Framework_TestCase { - public function testWebhookPayloadIsProcessed() + private $translator; + private $transportCallback; + private $httpClient; + private $promise; + private $response; + private $stream; + private $message; + private $headers; + private $sparkpostFactory; + private $sparkpostClient; + private $sparkpostTransport; + private $logger; + + protected function setUp() { - $translator = $this->getMockBuilder(Translator::class) - ->disableOriginalConstructor() - ->getMock(); - $translator->method('trans') - ->willReturnCallback( - function ($key) { - return $key; - } - ); + parent::setUp(); - $transportCallback = $this->getMockBuilder(TransportCallback::class) - ->disableOriginalConstructor() - ->getMock(); + $this->translator = $this->createMock(TranslatorInterface::class); + $this->transportCallback = $this->createMock(TransportCallback::class); + $this->httpClient = $this->createMock(Client::class); + $this->promise = $this->createMock(Promise::class); + $this->response = $this->createMock(Response::class); + $this->stream = $this->createMock(Stream::class); + $this->message = $this->createMock(MauticMessage::class); + $this->headers = $this->createMock(\Swift_Mime_HeaderSet::class); + $this->sparkpostFactory = $this->createMock(SparkpostFactoryInterface::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->sparkpostClient = new SparkPost($this->httpClient, ['key' => '1234']); + $this->sparkpostTransport = new SparkpostTransport( + '1234', + $this->translator, + $this->transportCallback, + $this->sparkpostFactory, + $this->logger + ); + + $this->translator->method('trans') + ->willReturnCallback(function ($key) { + return $key; + }); + + $this->httpClient->method('sendAsyncRequest')->willReturn($this->promise); + $this->promise->method('wait')->willReturn($this->response); + $this->message->method('getChildren')->willReturn([]); + $this->message->method('getHeaders')->willReturn($this->headers); + $this->headers->method('getAll')->willReturn([]); + $this->response->method('getBody')->willReturn($this->stream); + $this->sparkpostFactory->method('create')->willReturn($this->sparkpostClient); + } - $transportCallback->expects($this->exactly(6)) + public function testWebhookPayloadIsProcessed() + { + $this->transportCallback->expects($this->exactly(6)) ->method('addFailureByHashId') ->withConsecutive( [$this->equalTo('1'), 'MAIL REFUSED - IP (17.99.99.99) is in black list', DoNotContact::BOUNCED], @@ -45,9 +89,9 @@ function ($key) { [$this->equalTo('5'), 'unsubscribed', DoNotContact::UNSUBSCRIBED], [$this->equalTo('6'), 'unsubscribed', DoNotContact::UNSUBSCRIBED] // cc recipient type is ignored so addFailureByHashId should not be called - ); + ); - $transportCallback->expects($this->once()) + $this->transportCallback->expects($this->once()) ->method('addFailureByAddress') ->with( 'bounce@example.com', @@ -55,9 +99,92 @@ function ($key) { DoNotContact::BOUNCED ); - $sparkpost = new SparkpostTransport('1234', $translator, $transportCallback); + $this->sparkpostTransport->processCallbackRequest($this->getRequestWithPayload()); + } + + /** + * @see https://www.sparkpost.com/blog/error-handling-transmissions-api/ + */ + public function testSendWithOldErrorResponse() + { + $templateCheckPayload = '{ + "results": { + "subject": "Summer deals for Natalie", + "html": "Check out these deals Natalie!" + } + }'; + $transmissionPayload = '{ + "errors":[{ + "description":"Unconfigured or unverified sending domain.", + "code":"1902", + "message":"Invalid domain" + }] + }'; + + $this->message->method('getMetadata')->willReturn(['jane@doe.email' => ['leadId' => 21]]); + $this->message->method('getSubject')->willReturn('Top secret'); + $this->message->method('getFrom')->willReturn(['john@doe.email' => 'John']); + $this->message->method('getTo')->willReturn(['jane@doe.email' => 'Jane']); + $this->response->method('getStatusCode')->willReturn(200); + + $this->stream->expects($this->at(0)) + ->method('__toString') + ->willReturn($templateCheckPayload); + + $this->stream->expects($this->at(1)) + ->method('__toString') + ->willReturn($transmissionPayload); + + $this->transportCallback + ->expects($this->once()) + ->method('addFailureByContactId') + ->with(21, 'Unconfigured or unverified sending domain.', DoNotContact::BOUNCED, null); + + $this->expectExceptionMessage('Unconfigured or unverified sending domain.'); + $this->sparkpostTransport->send($this->message); + } + + /** + * @see https://www.sparkpost.com/blog/error-handling-transmissions-api/ + */ + public function testSendWithNewErrorResponse() + { + $templateCheckPayload = '{ + "results": { + "subject": "Summer deals for Natalie", + "html": "Check out these deals Natalie!" + } + }'; + $transmissionPayload = '{ + "errors":[ + { + "code":"1902", + "message":"Invalid domain" + } + ] + }'; + + $this->message->method('getMetadata')->willReturn(['jane@doe.email' => ['leadId' => 21]]); + $this->message->method('getSubject')->willReturn('Top secret'); + $this->message->method('getFrom')->willReturn(['john@doe.email' => 'John']); + $this->message->method('getTo')->willReturn(['jane@doe.email' => 'Jane']); + $this->response->method('getStatusCode')->willReturn(200); + + $this->stream->expects($this->at(0)) + ->method('__toString') + ->willReturn($templateCheckPayload); + + $this->stream->expects($this->at(1)) + ->method('__toString') + ->willReturn($transmissionPayload); + + $this->transportCallback + ->expects($this->once()) + ->method('addFailureByContactId') + ->with(21, 'Invalid domain', DoNotContact::BOUNCED, null); - $sparkpost->processCallbackRequest($this->getRequestWithPayload()); + $this->expectExceptionMessage('Invalid domain'); + $this->sparkpostTransport->send($this->message); } private function getRequestWithPayload() diff --git a/app/bundles/EmailBundle/Translations/en_US/messages.ini b/app/bundles/EmailBundle/Translations/en_US/messages.ini index 6059375f9a8..125238f705b 100644 --- a/app/bundles/EmailBundle/Translations/en_US/messages.ini +++ b/app/bundles/EmailBundle/Translations/en_US/messages.ini @@ -151,6 +151,7 @@ mautic.email.config.mailer_transport.sendmail="Sendmail" mautic.email.config.mailer_transport.smtp="Other SMTP Server" mautic.email.config.mailer_transport.sparkpost="Sparkpost" mautic.email.config.mailer_transport.momentum="Momentum" +mautic.email.config.monitored_email.not_configured="IMAP account is not configured." mautic.email.config.monitored_email.bounce_folder.tooltip="Folder to monitor for new bounce messages. Leave blank to disable. NOTE: Gmail will rewrite Return-Path headers when sending through their SMTP servers. Mautic will still attempt to analyze new messages for bounces but it is best to use another sending method or configure a unique mailbox." mautic.email.config.monitored_email.bounce_folder="Bounces" mautic.email.config.monitored_email.general="Default Mailbox" diff --git a/app/bundles/EmailBundle/Views/Lead/preference_options.html.php b/app/bundles/EmailBundle/Views/Lead/preference_options.html.php index 896109e4654..ff8588a6484 100644 --- a/app/bundles/EmailBundle/Views/Lead/preference_options.html.php +++ b/app/bundles/EmailBundle/Views/Lead/preference_options.html.php @@ -69,7 +69,7 @@ function togglePreferredChannel(channel){ -
+
diff --git a/app/bundles/FormBundle/Controller/Api/FormApiController.php b/app/bundles/FormBundle/Controller/Api/FormApiController.php index df4c762a240..e703cf6ff8b 100644 --- a/app/bundles/FormBundle/Controller/Api/FormApiController.php +++ b/app/bundles/FormBundle/Controller/Api/FormApiController.php @@ -13,7 +13,6 @@ use FOS\RestBundle\Util\Codes; use Mautic\ApiBundle\Controller\CommonApiController; -use Mautic\CoreBundle\Helper\InputHelper; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpKernel\Event\FilterControllerEvent; @@ -97,15 +96,7 @@ public function deleteActionsAction($formId) return $this->badRequest('The actions attribute must be array.'); } - $currentActions = $entity->getActions(); - - foreach ($currentActions as $currentAction) { - if (in_array($currentAction->getId(), $actionsToDelete)) { - $entity->removeAction($currentAction); - } - } - - $this->model->saveEntity($entity); + $this->model->deleteActions($entity, $actionsToDelete); $view = $this->view([$this->entityNameOne => $entity]); @@ -177,7 +168,7 @@ protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit') $fieldEntityArray['formId'] = $formId; if (!empty($fieldParams['alias'])) { - $fieldParams['alias'] = InputHelper::filename($fieldParams['alias']); + $fieldParams['alias'] = $fieldModel->cleanAlias($fieldParams['alias'], '', 25); if (!in_array($fieldParams['alias'], $aliases)) { $fieldEntityArray['alias'] = $fieldParams['alias']; @@ -249,11 +240,17 @@ protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit') // Remove actions which weren't in the PUT request if (!$isNew && $method === 'PUT') { + $actionsToDelete = []; + foreach ($currentActions as $currentAction) { if (!in_array($currentAction->getId(), $requestActionIds)) { - $entity->removeAction($currentAction); + $actionsToDelete[] = $currentAction->getId(); } } + + if ($actionsToDelete) { + $this->model->deleteActions($entity, $actionsToDelete); + } } } diff --git a/app/bundles/LeadBundle/Assets/js/lead.js b/app/bundles/LeadBundle/Assets/js/lead.js index ca59765f7f6..b7c899c63f2 100644 --- a/app/bundles/LeadBundle/Assets/js/lead.js +++ b/app/bundles/LeadBundle/Assets/js/lead.js @@ -306,6 +306,7 @@ Mautic.leadlistOnLoad = function(container) { 'fast', function () { mQuery(this).remove(); + Mautic.reorderSegmentFilters(); } ); @@ -389,7 +390,7 @@ Mautic.reorderSegmentFilters = function() { } var newName = prefix+'[filters]['+counter+']['+suffix+']'; - if (name.slice(-2) === '[]') { + if (typeof name !== 'undefined' && name.slice(-2) === '[]') { newName += '[]'; } @@ -408,7 +409,6 @@ Mautic.reorderSegmentFilters = function() { mQuery('#' + prefix + '_filters .panel-heading').removeClass('hide'); mQuery('#' + prefix + '_filters .panel-heading').first().addClass('hide'); - mQuery('#' + prefix + '_filters .panel').first().removeClass('in-group'); }; Mautic.convertLeadFilterInput = function(el) { @@ -521,7 +521,7 @@ Mautic.addLeadListFilter = function (elId, elObj) { var prototypeStr = mQuery('.available-filters').data('prototype'); var fieldType = filterOption.data('field-type'); var fieldObject = filterOption.data('field-object'); - var isSpecial = (mQuery.inArray(fieldType, ['leadlist', 'device_type', 'device_brand', 'device_os', 'lead_email_received', 'lead_email_sent', 'tags', 'multiselect', 'boolean', 'select', 'country', 'timezone', 'region', 'stage', 'locale', 'globalcategory']) != -1); + var isSpecial = (mQuery.inArray(fieldType, ['leadlist', 'assets', 'device_type', 'device_brand', 'device_os', 'lead_email_received', 'lead_email_sent', 'tags', 'multiselect', 'boolean', 'select', 'country', 'timezone', 'region', 'stage', 'locale', 'globalcategory']) != -1); prototypeStr = prototypeStr.replace(/__name__/g, filterNum); prototypeStr = prototypeStr.replace(/__label__/g, label); @@ -568,6 +568,7 @@ Mautic.addLeadListFilter = function (elId, elObj) { 'fast', function () { mQuery(this).remove(); + Mautic.reorderSegmentFilters(); } ); }); @@ -614,6 +615,7 @@ Mautic.addLeadListFilter = function (elId, elObj) { lazyInit: true, validateOnBlur: false, allowBlank: true, + scrollMonth: false, scrollInput: false }); } else if (fieldType == 'date') { @@ -623,6 +625,7 @@ Mautic.addLeadListFilter = function (elId, elObj) { lazyInit: true, validateOnBlur: false, allowBlank: true, + scrollMonth: false, scrollInput: false, closeOnDateSelect: true }); @@ -633,6 +636,7 @@ Mautic.addLeadListFilter = function (elId, elObj) { lazyInit: true, validateOnBlur: false, allowBlank: true, + scrollMonth: false, scrollInput: false }); } else if (fieldType == 'lookup_id') { @@ -1438,10 +1442,11 @@ Mautic.initUniqueIdentifierFields = function() { }; Mautic.updateFilterPositioning = function (el) { - var $el = mQuery(el); + var $el = mQuery(el); var $parentEl = $el.closest('.panel'); + var list = $parentEl.parent().children('.panel'); - if ($el.val() == 'and') { + if ($el.val() == 'and' && list.index($parentEl) !== 0) { $parentEl.addClass('in-group'); } else { $parentEl.removeClass('in-group'); diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 351fc90a4f5..1f793c635d0 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -362,11 +362,12 @@ 'services' => [ 'events' => [ 'mautic.lead.subscriber' => [ - 'class' => 'Mautic\LeadBundle\EventListener\LeadSubscriber', + 'class' => Mautic\LeadBundle\EventListener\LeadSubscriber::class, 'arguments' => [ 'mautic.helper.ip_lookup', 'mautic.core.model.auditlog', 'mautic.lead.event.dispatcher', + 'mautic.helper.template.dnc_reason', ], 'methodCalls' => [ 'setModelFactory' => ['mautic.model.factory'], @@ -531,6 +532,7 @@ 'mautic.category.model.category', 'mautic.helper.user', 'mautic.campaign.model.campaign', + 'mautic.asset.model.asset', ], 'alias' => 'leadlist', ], @@ -828,6 +830,10 @@ 'mautic.lead.repository.company_lead', ], ], + 'mautic.lead.validator.length' => [ + 'class' => Mautic\LeadBundle\Validator\Constraints\LengthValidator::class, + 'tag' => 'validator.constraint_validator', + ], ], 'repositories' => [ 'mautic.lead.repository.company' => [ @@ -940,7 +946,7 @@ ], 'helpers' => [ 'mautic.helper.template.avatar' => [ - 'class' => 'Mautic\LeadBundle\Templating\Helper\AvatarHelper', + 'class' => Mautic\LeadBundle\Templating\Helper\AvatarHelper::class, 'arguments' => ['mautic.factory'], 'alias' => 'lead_avatar', ], @@ -948,6 +954,11 @@ 'class' => \Mautic\LeadBundle\Helper\FieldAliasHelper::class, 'arguments' => ['mautic.lead.model.field'], ], + 'mautic.helper.template.dnc_reason' => [ + 'class' => Mautic\LeadBundle\Templating\Helper\DncReasonHelper::class, + 'arguments' => ['translator'], + 'alias' => 'lead_dnc_reason', + ], ], 'models' => [ 'mautic.lead.model.lead' => [ @@ -1088,6 +1099,13 @@ 'arguments' => [ 'mautic.lead.model.lead_segment_decorator_date', 'mautic.lead.model.relative_date', + 'mautic.lead.model.lead_segment.timezoneResolver', + ], + ], + 'mautic.lead.model.lead_segment.timezoneResolver' => [ + 'class' => \Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver::class, + 'arguments' => [ + 'mautic.helper.core_parameters', ], ], 'mautic.lead.model.random_parameter_name' => [ diff --git a/app/bundles/LeadBundle/Controller/Api/CustomFieldsApiControllerTrait.php b/app/bundles/LeadBundle/Controller/Api/CustomFieldsApiControllerTrait.php index a9169fa4f5e..6a55aff3448 100644 --- a/app/bundles/LeadBundle/Controller/Api/CustomFieldsApiControllerTrait.php +++ b/app/bundles/LeadBundle/Controller/Api/CustomFieldsApiControllerTrait.php @@ -105,9 +105,9 @@ protected function getEntityFormOptions() * @param Lead|Company $entity * @param Form $form * @param array $parameters - * @param bool $isPost + * @param bool $isPostOrPatch */ - protected function setCustomFieldValues($entity, $form, $parameters, $isPost = false) + protected function setCustomFieldValues($entity, $form, $parameters, $isPostOrPatch = false) { //set the custom field values //pull the data from the form in order to apply the form's formatting @@ -115,14 +115,14 @@ protected function setCustomFieldValues($entity, $form, $parameters, $isPost = f $parameters[$f->getName()] = $f->getData(); } - if ($isPost) { + if ($isPostOrPatch) { // Don't overwrite the contacts accumulated points if (isset($parameters['points']) && empty($parameters['points'])) { unset($parameters['points']); } - // When merging a contact because of a unique identifier match in POST /api/contacts//new, all 0 values must be unset because - // we have to assume 0 was not meant to overwrite an existing value. Other empty values will be caught by LeadModel::setCustomFieldValues + // When merging a contact because of a unique identifier match in POST /api/contacts//new or PATCH /api/contacts//edit all 0 values must be unset because + // we have to assume 0 was not meant to overwrite an existing value. Other empty values will be caught by LeadModel::setFieldValues $parameters = array_filter( $parameters, function ($value) { @@ -135,6 +135,6 @@ function ($value) { ); } - $this->model->setFieldValues($entity, $parameters, !$isPost); + $this->model->setFieldValues($entity, $parameters, !$isPostOrPatch); } } diff --git a/app/bundles/LeadBundle/Controller/Api/LeadApiController.php b/app/bundles/LeadBundle/Controller/Api/LeadApiController.php index d4822847d62..a1cb504b784 100644 --- a/app/bundles/LeadBundle/Controller/Api/LeadApiController.php +++ b/app/bundles/LeadBundle/Controller/Api/LeadApiController.php @@ -14,6 +14,7 @@ use FOS\RestBundle\Util\Codes; use JMS\Serializer\SerializationContext; use Mautic\ApiBundle\Controller\CommonApiController; +use Mautic\CoreBundle\Helper\ArrayHelper; use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\CoreBundle\Helper\InputHelper; use Mautic\LeadBundle\Controller\FrequencyRuleTrait; @@ -651,12 +652,21 @@ protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit') unset($this->entityRequestParameters['lastActive']); } + // Batch DNC settings if (!empty($parameters['doNotContact']) && is_array($parameters['doNotContact'])) { foreach ($parameters['doNotContact'] as $dnc) { $channel = !empty($dnc['channel']) ? $dnc['channel'] : 'email'; $comments = !empty($dnc['comments']) ? $dnc['comments'] : ''; - $reason = !empty($dnc['reason']) ? $dnc['reason'] : DoNotContact::MANUAL; - $this->model->addDncForLead($entity, $channel, $comments, $reason, false); + + $reason = (int) ArrayHelper::getValue('reason', $dnc, DoNotContact::MANUAL); + + if ($reason === DoNotContact::IS_CONTACTABLE) { + // Remove DNC record + $this->model->removeDncForLead($entity, $channel, false); + } else { + // Add DNC record + $this->model->addDncForLead($entity, $channel, $comments, $reason, false); + } } unset($parameters['doNotContact']); } @@ -679,7 +689,8 @@ protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit') unset($parameters['frequencyRules']); } - $this->setCustomFieldValues($entity, $form, $parameters, 'POST' === $this->request->getMethod()); + $isPostOrPatch = 'POST' === $this->request->getMethod() || 'PATCH' === $this->request->getMethod(); + $this->setCustomFieldValues($entity, $form, $parameters, $isPostOrPatch); } /** diff --git a/app/bundles/LeadBundle/Controller/CompanyController.php b/app/bundles/LeadBundle/Controller/CompanyController.php index 37c1d1469a0..ebdc54cfd99 100644 --- a/app/bundles/LeadBundle/Controller/CompanyController.php +++ b/app/bundles/LeadBundle/Controller/CompanyController.php @@ -35,6 +35,7 @@ public function indexAction($page = 1) 'lead:leads:viewother', 'lead:leads:create', 'lead:leads:editother', + 'lead:leads:editown', 'lead:leads:deleteother', ], 'RETURN_ARRAY' diff --git a/app/bundles/LeadBundle/Controller/EntityContactsTrait.php b/app/bundles/LeadBundle/Controller/EntityContactsTrait.php index 457705cf239..3da78fb565e 100644 --- a/app/bundles/LeadBundle/Controller/EntityContactsTrait.php +++ b/app/bundles/LeadBundle/Controller/EntityContactsTrait.php @@ -19,17 +19,18 @@ trait EntityContactsTrait { /** - * @param $entityId - * @param $page - * @param $permission - * @param $sessionVar - * @param $entityJoinTable Table to join to obtain list of related contacts or a DBAL QueryBuilder object defining custom joins - * @param null $dncChannel Channel for this entity to get do not contact records for - * @param null $entityIdColumnName If the entity ID in $joinTable is not "id", set the column name here - * @param array $contactFilter Array of additional filters for the getEntityContactsWithFields() function - * @param array $additionalJoins [ ['type' => 'join|leftJoin', 'from_alias' => '', 'table' => '', 'condition' => ''], ... ] - * @param string $contactColumnName Column of the contact in the join table - * @param string $paginationTarget DOM seletor for injecting new content when pagination is used + * @param string|int $entityId + * @param int $page + * @param string $permission + * @param string $sessionVar + * @param string $entityJoinTable Table to join to obtain list of related contacts or a DBAL QueryBuilder object defining custom joins + * @param string|null $dncChannel Channel for this entity to get do not contact records for + * @param string|null $entityIdColumnName If the entity ID in $joinTable is not "id", set the column name here + * @param array|null $contactFilter Array of additional filters for the getEntityContactsWithFields() function + * @param array|null $additionalJoins [ ['type' => 'join|leftJoin', 'from_alias' => '', 'table' => '', 'condition' => ''], ... ] + * @param string|null $contactColumnName Column of the contact in the join table + * @param array|null $routeParameters + * @param string|null $paginationTarget DOM seletor for injecting new content when pagination is used * * @return mixed */ diff --git a/app/bundles/LeadBundle/Controller/FieldController.php b/app/bundles/LeadBundle/Controller/FieldController.php index cdc16262665..c5cac1e4c00 100644 --- a/app/bundles/LeadBundle/Controller/FieldController.php +++ b/app/bundles/LeadBundle/Controller/FieldController.php @@ -415,13 +415,19 @@ public function deleteAction($objectId) return $this->accessDenied(); } - if ($model->isUsedField($field)) { + $segments = []; + foreach ($model->getFieldSegments($field) as $segment) { + $segments[] = sprintf('"%s" (%d)', $segment->getName(), $segment->getId()); + } + + if (count($segments)) { $flashMessage = [ 'type' => 'error', 'msg' => 'mautic.core.notice.used.field', 'msgVars' => [ - '%name%' => $field->getLabel(), - '%id%' => $objectId, + '%name%' => $field->getLabel(), + '%id%' => $objectId, + '%segments%' => implode(', ', $segments), ], ]; } else { @@ -497,16 +503,34 @@ public function batchDeleteAction() // Delete everything we are able to if (!empty($deleteIds)) { $filteredDeleteIds = $model->filterUsedFieldIds($deleteIds); + $usedFieldIds = array_diff($deleteIds, $filteredDeleteIds); + $segments = []; + $usedFieldsNames = []; + + if ($usedFieldIds) { + // Iterating through all used fileds to get segments they are used in + foreach ($usedFieldIds as $usedFieldId) { + $fieldEntity = $model->getEntity($usedFieldId); + foreach ($model->getFieldSegments($fieldEntity) as $segment) { + $segments[$segment->getId()] = sprintf('"%s" (%d)', $segment->getName(), $segment->getId()); + $usedFieldsNames[] = sprintf('"%s"', $fieldEntity->getName()); + } + } + } if ($filteredDeleteIds !== $deleteIds) { $flashes[] = [ 'type' => 'error', 'msg' => 'mautic.core.notice.used.fields', + 'msgVars' => [ + '%segments%' => implode(', ', $segments), + '%fields%' => implode(', ', array_unique($usedFieldsNames)), + ], ]; } if (count($filteredDeleteIds)) { - $entities = $model->deleteEntities($deleteIds); + $entities = $model->deleteEntities($filteredDeleteIds); $flashes[] = [ 'type' => 'notice', diff --git a/app/bundles/LeadBundle/Controller/FrequencyRuleTrait.php b/app/bundles/LeadBundle/Controller/FrequencyRuleTrait.php index db8bf6674a2..9f063917073 100644 --- a/app/bundles/LeadBundle/Controller/FrequencyRuleTrait.php +++ b/app/bundles/LeadBundle/Controller/FrequencyRuleTrait.php @@ -174,18 +174,17 @@ protected function getFrequencyRuleFormData(Lead $lead, array $allChannels = nul */ protected function persistFrequencyRuleFormData(Lead $lead, array $formData, array $allChannels, $leadChannels, $currentChannelId = null) { - /** @var LeadModel $model */ - $model = $this->getModel('lead'); + /** @var LeadModel $leadModel */ + $leadModel = $this->getModel('lead.lead'); + + /** @var \Mautic\LeadBundle\Model\DoNotContact $dncModel */ + $dncModel = $this->getModel('lead.dnc'); // iF subscribed_channels are enabled in form, then touch DNC - if (isset($this->request->request->get('lead_contact_frequency_rules')['lead_channels']['subscribed_channels'])) { + if (isset($this->request->request->get('lead_contact_frequency_rules')['lead_channels'])) { foreach ($formData['lead_channels']['subscribed_channels'] as $contactChannel) { if (!isset($leadChannels[$contactChannel])) { - $contactable = $model->isContactable($lead, $contactChannel); - if ($contactable == DoNotContact::UNSUBSCRIBED) { - // Only resubscribe if the contact did not opt out themselves - $model->removeDncForLead($lead, $contactChannel); - } + $dncModel->removeDncForContact($lead->getId(), $contactChannel); } } $dncChannels = array_diff($allChannels, $formData['lead_channels']['subscribed_channels']); @@ -194,15 +193,10 @@ protected function persistFrequencyRuleFormData(Lead $lead, array $formData, arr if ($currentChannelId) { $channel = [$channel => $currentChannelId]; } - $model->addDncForLead( - $lead, - $channel, - 'user', - ($this->isPublicView) ? DoNotContact::UNSUBSCRIBED : DoNotContact::MANUAL - ); + $dncModel->addDncForContact($lead->getId(), $channel, ($this->isPublicView) ? DoNotContact::UNSUBSCRIBED : DoNotContact::MANUAL, 'user'); } } } - $model->setFrequencyRules($lead, $formData, $this->leadLists); + $leadModel->setFrequencyRules($lead, $formData, $this->leadLists); } } diff --git a/app/bundles/LeadBundle/Controller/LeadController.php b/app/bundles/LeadBundle/Controller/LeadController.php index 2bdc6132e39..ed936517303 100644 --- a/app/bundles/LeadBundle/Controller/LeadController.php +++ b/app/bundles/LeadBundle/Controller/LeadController.php @@ -367,6 +367,7 @@ public function viewAction($objectId) 'engagementData' => $this->getEngagementData($lead), 'noteCount' => $this->getModel('lead.note')->getNoteCount($lead, true), 'integrations' => $integrationRepo->getIntegrationEntityByLead($lead->getId()), + 'devices' => $this->get('mautic.lead.repository.lead_device')->getLeadDevices($lead), 'auditlog' => $this->getAuditlogs($lead), 'doNotContact' => end($dnc), 'leadNotes' => $this->forward( diff --git a/app/bundles/LeadBundle/Controller/ListController.php b/app/bundles/LeadBundle/Controller/ListController.php index 7cdf19cd711..6fea80e6eae 100644 --- a/app/bundles/LeadBundle/Controller/ListController.php +++ b/app/bundles/LeadBundle/Controller/ListController.php @@ -80,7 +80,7 @@ public function indexAction($page = 1) $translator = $this->get('translator'); $mine = $translator->trans('mautic.core.searchcommand.ismine'); $global = $translator->trans('mautic.lead.list.searchcommand.isglobal'); - $filter['force'] = " ($mine or $global)"; + $filter['force'] = "($mine or $global)"; } $items = $model->getEntities( @@ -455,6 +455,8 @@ private function getPostActionVars($objectId = null) */ public function deleteAction($objectId) { + /** @var ListModel $model */ + $model = $this->getModel('lead.list'); $page = $this->get('session')->get('mautic.segment.page', 1); $returnUrl = $this->generateUrl('mautic_segment_index', ['page' => $page]); $flashes = []; @@ -469,6 +471,22 @@ public function deleteAction($objectId) ], ]; + $dependents = $model->getSegmentsWithDependenciesOnSegment($objectId); + + if (!empty($dependents)) { + $flashes[] = [ + 'type' => 'error', + 'msg' => 'mautic.lead.list.error.cannot.delete', + 'msgVars' => ['%segments%' => implode(', ', $dependents)], + ]; + + return $this->postActionRedirect( + array_merge($postActionVars, [ + 'flashes' => $flashes, + ]) + ); + } + if ($this->request->getMethod() == 'POST') { /** @var ListModel $model */ $model = $this->getModel('lead.list'); @@ -531,12 +549,23 @@ public function batchDeleteAction() if ($this->request->getMethod() == 'POST') { /** @var ListModel $model */ - $model = $this->getModel('lead.list'); - $ids = json_decode($this->request->query->get('ids', '{}')); - $deleteIds = []; + $model = $this->getModel('lead.list'); + $ids = json_decode($this->request->query->get('ids', '{}')); + $canNotBeDeleted = $model->canNotBeDeleted($ids); + + if (!empty($canNotBeDeleted)) { + $flashes[] = [ + 'type' => 'error', + 'msg' => 'mautic.lead.list.error.cannot.delete.batch', + 'msgVars' => ['%segments%' => implode(', ', $canNotBeDeleted)], + ]; + } + + $toBeDeleted = array_diff($ids, array_keys($canNotBeDeleted)); + $deleteIds = []; // Loop over the IDs to perform access checks pre-delete - foreach ($ids as $objectId) { + foreach ($toBeDeleted as $objectId) { $entity = $model->getEntity($objectId); if ($entity === null) { @@ -728,9 +757,8 @@ public function viewAction($objectId) ], ]); } elseif (!$this->get('mautic.security')->hasEntityAccess( + 'lead:leads:viewown', 'lead:lists:viewother', - 'lead:lists:editother', - 'lead:lists:deleteother', $list->getCreatedBy() ) ) { diff --git a/app/bundles/LeadBundle/Entity/Lead.php b/app/bundles/LeadBundle/Entity/Lead.php index ba0d1c8860e..f7ec67d45ba 100644 --- a/app/bundles/LeadBundle/Entity/Lead.php +++ b/app/bundles/LeadBundle/Entity/Lead.php @@ -294,8 +294,7 @@ public static function loadMetadata(ORM\ClassMetadata $metadata) ->addLifecycleEvent('checkAttributionDate', 'prePersist') ->addLifecycleEvent('checkDateAdded', 'prePersist') ->addIndex(['date_added'], 'lead_date_added') - ->addIndex(['date_identified'], 'date_identified') - ->addIndex(['last_active'], 'last_active_search'); + ->addIndex(['date_identified'], 'date_identified'); $builder->createField('id', 'integer') ->makePrimaryKey() diff --git a/app/bundles/LeadBundle/Entity/LeadDeviceRepository.php b/app/bundles/LeadBundle/Entity/LeadDeviceRepository.php index 8c0741c1f36..9311b12a718 100644 --- a/app/bundles/LeadBundle/Entity/LeadDeviceRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadDeviceRepository.php @@ -196,4 +196,22 @@ public function isAnyLeadDeviceTracked(Lead $lead) return !empty($devices); } + + /** + * @param Lead $lead + * + * @return array + */ + public function getLeadDevices(Lead $lead) + { + $qb = $this->getEntityManager()->getConnection()->createQueryBuilder(); + + return $qb->select('*') + ->from(MAUTIC_TABLE_PREFIX.'lead_devices', 'es') + ->where('lead_id = :leadId') + ->setParameter('leadId', (int) $lead->getId()) + ->orderBy('date_added', 'desc') + ->execute() + ->fetchAll(); + } } diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index 470778951c3..6a47c1b5039 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -1539,6 +1539,7 @@ public function getListFilterExpr($filters, &$parameters, QueryBuilder $q, $isNo case 'tags': case 'globalcategory': case 'campaign': + case 'lead_asset_download': case 'lead_email_received': case 'lead_email_sent': case 'device_type': @@ -1588,6 +1589,10 @@ public function getListFilterExpr($filters, &$parameters, QueryBuilder $q, $isNo $table = 'lead_devices'; $column = 'device_brand'; break; + case 'lead_asset_download': + $table = 'asset_downloads'; + $column = 'asset_id'; + break; case 'device_os': $table = 'lead_devices'; $column = 'device_os_name'; @@ -1603,7 +1608,6 @@ public function getListFilterExpr($filters, &$parameters, QueryBuilder $q, $isNo $leadId, $subQueryFilters ); - $groupExpr->add( sprintf('%s (%s)', $func, $subQb->getSQL()) ); diff --git a/app/bundles/LeadBundle/Entity/LeadRepository.php b/app/bundles/LeadBundle/Entity/LeadRepository.php index 5176ce74033..ac9f1910b8e 100755 --- a/app/bundles/LeadBundle/Entity/LeadRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadRepository.php @@ -112,9 +112,8 @@ public function getLeadsByFieldValue($field, $value, $ignoreId = null, $indexByC ->from(MAUTIC_TABLE_PREFIX.'leads', 'l'); if (is_array($value)) { - $q->where( - $q->expr()->in($col, $value) - ); + $q->where($col.' IN (:value)') + ->setParameter('value', $value); } else { $q->where("$col = :search") ->setParameter('search', $value); @@ -245,7 +244,7 @@ public function getLeadIdsByUniqueFields($uniqueFieldsWithData, $leadId = null) // loop through the fields and foreach ($uniqueFieldsWithData as $col => $val) { - $q->andWhere("l.$col = :".$col) + $q->orWhere("l.$col = :".$col) ->setParameter($col, $val); } diff --git a/app/bundles/LeadBundle/Entity/StagesChangeLogRepository.php b/app/bundles/LeadBundle/Entity/StagesChangeLogRepository.php index e7620a4b713..25570eb08ae 100644 --- a/app/bundles/LeadBundle/Entity/StagesChangeLogRepository.php +++ b/app/bundles/LeadBundle/Entity/StagesChangeLogRepository.php @@ -152,6 +152,6 @@ public function getCurrentLeadStage($leadId) $result = $query->execute()->fetch(); - return (isset($result['stage'])) ? $result['stage'] : null; + return (isset($result['stage'])) ? (int) $result['stage'] : null; } } diff --git a/app/bundles/LeadBundle/EventListener/LeadSubscriber.php b/app/bundles/LeadBundle/EventListener/LeadSubscriber.php index 390af0ffe20..80a800a34f6 100644 --- a/app/bundles/LeadBundle/EventListener/LeadSubscriber.php +++ b/app/bundles/LeadBundle/EventListener/LeadSubscriber.php @@ -15,12 +15,12 @@ use Mautic\CoreBundle\EventListener\CommonSubscriber; use Mautic\CoreBundle\Helper\IpLookupHelper; use Mautic\CoreBundle\Model\AuditLogModel; -use Mautic\LeadBundle\Entity\DoNotContact; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Event as Events; use Mautic\LeadBundle\Helper\LeadChangeEventDispatcher; use Mautic\LeadBundle\LeadEvents; use Mautic\LeadBundle\Model\ChannelTimelineInterface; +use Mautic\LeadBundle\Templating\Helper\DncReasonHelper; /** * Class LeadSubscriber. @@ -45,16 +45,26 @@ class LeadSubscriber extends CommonSubscriber private $leadEventDispatcher; /** - * LeadSubscriber constructor. - * - * @param IpLookupHelper $ipLookupHelper - * @param AuditLogModel $auditLogModel + * @var DncReasonHelper */ - public function __construct(IpLookupHelper $ipLookupHelper, AuditLogModel $auditLogModel, LeadChangeEventDispatcher $eventDispatcher) - { + private $dncReasonHelper; + + /** + * @param IpLookupHelper $ipLookupHelper + * @param AuditLogModel $auditLogModel + * @param LeadChangeEventDispatcher $eventDispatcher + * @param DncReasonHelper $dncReasonHelper + */ + public function __construct( + IpLookupHelper $ipLookupHelper, + AuditLogModel $auditLogModel, + LeadChangeEventDispatcher $eventDispatcher, + DncReasonHelper $dncReasonHelper + ) { $this->ipLookupHelper = $ipLookupHelper; $this->auditLogModel = $auditLogModel; $this->leadEventDispatcher = $eventDispatcher; + $this->dncReasonHelper = $dncReasonHelper; } /** @@ -535,17 +545,7 @@ protected function addTimelineDoNotContactEntries(Events\LeadTimelineEvent $even if (!$event->isEngagementCount()) { foreach ($rows['results'] as $row) { - switch ($row['reason']) { - case DoNotContact::UNSUBSCRIBED: - $row['reason'] = $this->translator->trans('mautic.lead.event.donotcontact_unsubscribed'); - break; - case DoNotContact::BOUNCED: - $row['reason'] = $this->translator->trans('mautic.lead.event.donotcontact_bounced'); - break; - case DoNotContact::MANUAL: - $row['reason'] = $this->translator->trans('mautic.lead.event.donotcontact_manual'); - break; - } + $row['reason'] = $this->dncReasonHelper->toText($row['reason']); $template = 'MauticLeadBundle:SubscribedEvents\Timeline:donotcontact.html.php'; $icon = 'fa-ban'; diff --git a/app/bundles/LeadBundle/EventListener/MaintenanceSubscriber.php b/app/bundles/LeadBundle/EventListener/MaintenanceSubscriber.php index 69faebd08b7..eb0e2b1effb 100644 --- a/app/bundles/LeadBundle/EventListener/MaintenanceSubscriber.php +++ b/app/bundles/LeadBundle/EventListener/MaintenanceSubscriber.php @@ -71,21 +71,38 @@ public function onDataCleanup(MaintenanceEvent $event) } $rows = $qb->execute()->fetchColumn(); } else { - $qb->delete(MAUTIC_TABLE_PREFIX.'leads') - ->where($qb->expr()->lte('last_active', ':date')); + $qb->select('l.id')->from(MAUTIC_TABLE_PREFIX.'leads', 'l') + ->where($qb->expr()->lte('l.last_active', ':date')); if ($event->isGdpr() === false) { - $qb->andWhere($qb->expr()->isNull('date_identified')); + $qb->andWhere($qb->expr()->isNull('l.date_identified')); } else { $qb->orWhere( $qb->expr()->andX( - $qb->expr()->lte('date_added', ':date2'), - $qb->expr()->isNull('last_active') + $qb->expr()->lte('l.date_added', ':date2'), + $qb->expr()->isNull('l.last_active') )); $qb->setParameter('date2', $event->getDate()->format('Y-m-d H:i:s')); } - $rows = $qb->execute(); + $rows = 0; + $qb->setMaxResults(10000)->setFirstResult(0); + + $qb2 = $this->db->createQueryBuilder(); + while (true) { + $leadsIds = array_column($qb->execute()->fetchAll(), 'id'); + if (sizeof($leadsIds) === 0) { + break; + } + foreach ($leadsIds as $leadId) { + $rows += $qb2->delete(MAUTIC_TABLE_PREFIX.'leads') + ->where( + $qb2->expr()->eq( + 'id', $leadId + ) + )->execute(); + } + } } $event->setStat($this->translator->trans('mautic.maintenance.visitors'), $rows, $qb->getSQL(), $qb->getParameters()); diff --git a/app/bundles/LeadBundle/EventListener/SegmentReportSubscriber.php b/app/bundles/LeadBundle/EventListener/SegmentReportSubscriber.php index f71bc7eef8f..407cb7b3d96 100644 --- a/app/bundles/LeadBundle/EventListener/SegmentReportSubscriber.php +++ b/app/bundles/LeadBundle/EventListener/SegmentReportSubscriber.php @@ -95,7 +95,8 @@ public function onReportGenerate(ReportGeneratorEvent $event) $qb = $event->getQueryBuilder(); $qb->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'lll') ->leftJoin('lll', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = lll.lead_id') - ->leftJoin('lll', MAUTIC_TABLE_PREFIX.'lead_lists', 's', 's.id = lll.leadlist_id'); + ->leftJoin('lll', MAUTIC_TABLE_PREFIX.'lead_lists', 's', 's.id = lll.leadlist_id') + ->andWhere('lll.manually_removed = 0'); if ($event->hasColumn(['u.first_name', 'u.last_name']) || $event->hasFilter(['u.first_name', 'u.last_name'])) { $qb->leftJoin('l', MAUTIC_TABLE_PREFIX.'users', 'u', 'u.id = l.owner_id'); diff --git a/app/bundles/LeadBundle/EventListener/WebhookSubscriber.php b/app/bundles/LeadBundle/EventListener/WebhookSubscriber.php index 756a33917b0..312e34db6aa 100644 --- a/app/bundles/LeadBundle/EventListener/WebhookSubscriber.php +++ b/app/bundles/LeadBundle/EventListener/WebhookSubscriber.php @@ -118,6 +118,7 @@ public function onLeadNewUpdate(LeadEvent $event) 'userList', 'publishDetails', 'ipAddress', + 'tagList', ] ); } @@ -142,6 +143,7 @@ public function onLeadPointChange(PointsChangeEvent $event) 'userList', 'publishDetails', 'ipAddress', + 'tagList', ] ); } @@ -186,6 +188,7 @@ public function onChannelSubscriptionChange(ChannelSubscriptionChange $event) 'userList', 'publishDetails', 'ipAddress', + 'tagList', ] ); } diff --git a/app/bundles/LeadBundle/Exception/UnknownDncReasonException.php b/app/bundles/LeadBundle/Exception/UnknownDncReasonException.php new file mode 100644 index 00000000000..fb425dc6a68 --- /dev/null +++ b/app/bundles/LeadBundle/Exception/UnknownDncReasonException.php @@ -0,0 +1,16 @@ +relativeDateStrings = LeadListRepository::getRelativeDateTranslationKeys(); foreach ($this->relativeDateStrings as &$string) { $string = $translator->trans($string); } + $this->default = $default; } /** @@ -44,6 +51,9 @@ public function transform($rawFilters) } foreach ($rawFilters as $k => $f) { + if (!empty($this->default)) { + $rawFilters[$k] = array_merge($this->default, $rawFilters[$k]); + } if ($f['type'] == 'datetime') { if (in_array($f['filter'], $this->relativeDateStrings) or stristr($f['filter'][0], '-') or stristr($f['filter'][0], '+')) { continue; diff --git a/app/bundles/LeadBundle/Form/Type/ContactChannelsType.php b/app/bundles/LeadBundle/Form/Type/ContactChannelsType.php index 12a69d2f4a2..a07686bb85b 100644 --- a/app/bundles/LeadBundle/Form/Type/ContactChannelsType.php +++ b/app/bundles/LeadBundle/Form/Type/ContactChannelsType.php @@ -5,6 +5,7 @@ use Mautic\CoreBundle\Helper\CoreParametersHelper; use Mautic\LeadBundle\Entity\FrequencyRule; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -16,8 +17,6 @@ class ContactChannelsType extends AbstractType private $coreParametersHelper; /** - * ContactFrequencyType constructor. - * * @param CoreParametersHelper $coreParametersHelper */ public function __construct(CoreParametersHelper $coreParametersHelper) @@ -123,7 +122,6 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'frequency-date form-control', ] ); - $type = 'datetime'; } else { $attributes = array_merge( $attr, @@ -131,30 +129,32 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', ] ); - $type = 'date'; } + if (!$options['public_view'] || $showContactPauseDates) { $builder->add( 'contact_pause_start_date_'.$channel, - $type, + DateType::class, [ 'widget' => 'single_text', - 'label' => false, //'mautic.lead.frequency.contact.start.date', + 'label' => false, 'label_attr' => ['class' => 'text-muted fw-n label3'], 'attr' => $attributes, - 'format' => 'yyyy-MM-dd', + 'format' => $options['public_view'] ? DateType::HTML5_FORMAT : 'yyyy-MM-dd', + 'html5' => $options['public_view'], 'required' => false, ] ); $builder->add( 'contact_pause_end_date_'.$channel, - $type, + DateType::class, [ 'widget' => 'single_text', 'label' => 'mautic.lead.frequency.contact.end.date', 'label_attr' => ['class' => 'frequency-label text-muted fw-n label4'], 'attr' => $attributes, - 'format' => 'yyyy-MM-dd', + 'format' => $options['public_view'] ? DateType::HTML5_FORMAT : 'yyyy-MM-dd', + 'html5' => $options['public_view'], 'required' => false, ] ); diff --git a/app/bundles/LeadBundle/Form/Type/EntityFieldsBuildFormTrait.php b/app/bundles/LeadBundle/Form/Type/EntityFieldsBuildFormTrait.php index 8327b92cb8f..b505cf8454a 100644 --- a/app/bundles/LeadBundle/Form/Type/EntityFieldsBuildFormTrait.php +++ b/app/bundles/LeadBundle/Form/Type/EntityFieldsBuildFormTrait.php @@ -13,10 +13,10 @@ use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Helper\FormFieldHelper; +use Mautic\LeadBundle\Validator\Constraints\Length; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; -use Symfony\Component\Validator\Constraints\Date; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\NotBlank; @@ -169,6 +169,10 @@ function (FormEvent $event) use ($alias, $type) { case 'select': case 'multiselect': case 'boolean': + if ($type == 'multiselect') { + $constraints[] = new Length(['max' => 255]); + } + $typeProperties = [ 'required' => $required, 'label' => $field['label'], @@ -268,6 +272,10 @@ function (FormEvent $event) use ($alias, $type) { ] ); break; + case 'multiselect': + if ($type == 'multiselect') { + $constraints[] = new Length(['max' => 255]); + } } $builder->add( diff --git a/app/bundles/LeadBundle/Form/Type/FilterTrait.php b/app/bundles/LeadBundle/Form/Type/FilterTrait.php index a2edba921ec..b00e69fd2c6 100644 --- a/app/bundles/LeadBundle/Form/Type/FilterTrait.php +++ b/app/bundles/LeadBundle/Form/Type/FilterTrait.php @@ -66,6 +66,18 @@ public function buildFiltersForm($eventName, FormEvent $event, TranslatorInterfa $customOptions = []; switch ($fieldType) { + case 'assets': + if (!isset($data['filter'])) { + $data['filter'] = []; + } elseif (!is_array($data['filter'])) { + $data['filter'] = [$data['filter']]; + } + + $customOptions['choices'] = $options['assets']; + $customOptions['multiple'] = true; + $customOptions['choice_translation_domain'] = false; + $type = 'choice'; + break; case 'leadlist': if (!isset($data['filter'])) { $data['filter'] = []; diff --git a/app/bundles/LeadBundle/Form/Type/FilterType.php b/app/bundles/LeadBundle/Form/Type/FilterType.php index 28cdf709da1..600f0c4bffb 100644 --- a/app/bundles/LeadBundle/Form/Type/FilterType.php +++ b/app/bundles/LeadBundle/Form/Type/FilterType.php @@ -101,6 +101,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) 'deviceTypes', 'deviceBrands', 'deviceOs', + 'assets', 'tags', 'stage', 'locales', diff --git a/app/bundles/LeadBundle/Form/Type/ListType.php b/app/bundles/LeadBundle/Form/Type/ListType.php index 9a51f0db0fb..99c410a39ea 100644 --- a/app/bundles/LeadBundle/Form/Type/ListType.php +++ b/app/bundles/LeadBundle/Form/Type/ListType.php @@ -13,6 +13,7 @@ use DeviceDetector\Parser\Device\DeviceParserAbstract as DeviceParser; use DeviceDetector\Parser\OperatingSystem; +use Mautic\AssetBundle\Model\AssetModel; use Mautic\CampaignBundle\Model\CampaignModel; use Mautic\CategoryBundle\Model\CategoryModel; use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber; @@ -51,6 +52,7 @@ class ListType extends AbstractType private $deviceOsChoices = []; private $tagChoices = []; private $stageChoices = []; + private $assetChoices = []; private $localeChoices = []; private $categoriesChoices = []; @@ -66,8 +68,9 @@ class ListType extends AbstractType * @param CategoryModel $categoryModel * @param UserHelper $userHelper * @param CampaignModel $campaignModel + * @param AssetModel $assetModel */ - public function __construct(TranslatorInterface $translator, ListModel $listModel, EmailModel $emailModel, CorePermissions $security, LeadModel $leadModel, StageModel $stageModel, CategoryModel $categoryModel, UserHelper $userHelper, CampaignModel $campaignModel) + public function __construct(TranslatorInterface $translator, ListModel $listModel, EmailModel $emailModel, CorePermissions $security, LeadModel $leadModel, StageModel $stageModel, CategoryModel $categoryModel, UserHelper $userHelper, CampaignModel $campaignModel, AssetModel $assetModel) { $this->translator = $translator; @@ -104,6 +107,12 @@ public function __construct(TranslatorInterface $translator, ListModel $listMode } ksort($this->emailChoices); + $assets = $assetModel->getLookupResults('asset'); + foreach ($assets as $asset) { + $this->assetChoices[$asset['language']][$asset['id']] = $asset['title']; + } + ksort($this->assetChoices); + $tags = $leadModel->getTagList(); foreach ($tags as $tag) { $this->tagChoices[$tag['value']] = $tag['label']; @@ -193,7 +202,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) $builder->add('isPublished', 'yesno_button_group'); - $filterModalTransformer = new FieldFilterTransformer($this->translator); + $filterModalTransformer = new FieldFilterTransformer($this->translator, ['object'=>'lead']); $builder->add( $builder->create( 'filters', @@ -212,6 +221,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'deviceTypes' => $this->deviceTypesChoices, 'deviceBrands' => $this->deviceBrandsChoices, 'deviceOs' => $this->deviceOsChoices, + 'assets' => $this->assetChoices, 'tags' => $this->tagChoices, 'stage' => $this->stageChoices, 'locales' => $this->localeChoices, @@ -265,6 +275,7 @@ public function buildView(FormView $view, FormInterface $form, array $options) $view->vars['deviceTypes'] = $this->deviceTypesChoices; $view->vars['deviceBrands'] = $this->deviceBrandsChoices; $view->vars['deviceOs'] = $this->deviceOsChoices; + $view->vars['assets'] = $this->assetChoices; $view->vars['tags'] = $this->tagChoices; $view->vars['stage'] = $this->stageChoices; $view->vars['locales'] = $this->localeChoices; diff --git a/app/bundles/LeadBundle/Helper/ContactRequestHelper.php b/app/bundles/LeadBundle/Helper/ContactRequestHelper.php index 0506fd99e4e..da17c770b37 100644 --- a/app/bundles/LeadBundle/Helper/ContactRequestHelper.php +++ b/app/bundles/LeadBundle/Helper/ContactRequestHelper.php @@ -178,8 +178,7 @@ private function getContactFromUrl() true, true ); - - if ($foundContact->getId() !== $this->trackedContact->getId()) { + if (is_null($this->trackedContact) or $foundContact->getId() !== $this->trackedContact->getId()) { // A contact was found by a publicly updatable field return $foundContact; } diff --git a/app/bundles/LeadBundle/Model/CompanyModel.php b/app/bundles/LeadBundle/Model/CompanyModel.php index c133b332721..4f6d34735d9 100644 --- a/app/bundles/LeadBundle/Model/CompanyModel.php +++ b/app/bundles/LeadBundle/Model/CompanyModel.php @@ -132,7 +132,8 @@ public function getCompanyLeadRepository() */ public function getPermissionBase() { - return 'company:companies'; + // We are using lead:leads in the CompanyController so this should match to prevent a BC break + return 'lead:leads'; } /** diff --git a/app/bundles/LeadBundle/Model/FieldModel.php b/app/bundles/LeadBundle/Model/FieldModel.php index 6c583099b6b..abaaa787aba 100644 --- a/app/bundles/LeadBundle/Model/FieldModel.php +++ b/app/bundles/LeadBundle/Model/FieldModel.php @@ -602,6 +602,18 @@ public function isUsedField(LeadField $field) return $this->leadListModel->isFieldUsed($field); } + /** + * Returns list of all segments that use $field. + * + * @param LeadField $field + * + * @return \Doctrine\ORM\Tools\Pagination\Paginator + */ + public function getFieldSegments(LeadField $field) + { + return $this->leadListModel->getFieldSegments($field); + } + /** * Filter used field ids. * diff --git a/app/bundles/LeadBundle/Model/IpAddressModel.php b/app/bundles/LeadBundle/Model/IpAddressModel.php index 1f52fb9d0d6..70137c05b52 100644 --- a/app/bundles/LeadBundle/Model/IpAddressModel.php +++ b/app/bundles/LeadBundle/Model/IpAddressModel.php @@ -53,6 +53,16 @@ public function saveIpAddressesReferencesForContact(Lead $contact) } } + /** + * @param string $ip + * + * @return IpAddress|null + */ + public function findOneByIpAddress($ip) + { + return $this->entityManager->getRepository(IpAddress::class)->findOneByIpAddress($ip); + } + /** * Tries to insert the Lead/IP relation and continues even if UniqueConstraintViolationException is thrown. * diff --git a/app/bundles/LeadBundle/Model/LeadModel.php b/app/bundles/LeadBundle/Model/LeadModel.php index 8622ed93ec7..0033c0f5d8e 100644 --- a/app/bundles/LeadBundle/Model/LeadModel.php +++ b/app/bundles/LeadBundle/Model/LeadModel.php @@ -527,7 +527,7 @@ public function saveEntity($entity, $unlock = true) } } - if (!$entity->getCompany() && !empty($details['organization'])) { + if (!$entity->getCompany() && !empty($details['organization']) && $this->coreParametersHelper->getParameter('ip_lookup_create_organization', false)) { $entity->addUpdatedField('company', $details['organization']); } } @@ -617,7 +617,8 @@ public function setFieldValues(Lead $lead, array $data, $overwriteWithBlank = fa $stagesChangeLogRepo = $this->getStagesChangeLogRepository(); $currentLeadStage = $stagesChangeLogRepo->getCurrentLeadStage($lead->getId()); - if ($data['stage'] !== $currentLeadStage) { + $previousId = is_object($data['stage']) ? $data['stage']->getId() : (int) $data['stage']; + if ($previousId !== $currentLeadStage) { $stage = $this->em->getRepository('MauticStageBundle:Stage')->find($data['stage']); $lead->stageChangeLogEntry( $stage, @@ -1438,8 +1439,11 @@ public function import($fields, $data, $owner = null, $list = null, $tags = null if (!empty($fields['ip']) && !empty($data[$fields['ip']])) { $addresses = explode(',', $data[$fields['ip']]); foreach ($addresses as $address) { - $ipAddress = new IpAddress(); - $ipAddress->setIpAddress(trim($address)); + $address = trim($address); + if (!$ipAddress = $this->ipAddressModel->findOneByIpAddress($address)) { + $ipAddress = new IpAddress(); + $ipAddress->setIpAddress($address); + } $lead->addIpAddress($ipAddress); } } @@ -1500,18 +1504,23 @@ public function import($fields, $data, $owner = null, $list = null, $tags = null unset($fieldData['stage']); // Set unsubscribe status - if (!empty($fields['doNotEmail']) && !empty($data[$fields['doNotEmail']]) && (!empty($fields['email']) && !empty($data[$fields['email']]))) { - $doNotEmail = filter_var($data[$fields['doNotEmail']], FILTER_VALIDATE_BOOLEAN); - if ($doNotEmail) { + if (!empty($fields['doNotEmail']) && isset($data[$fields['doNotEmail']]) && (!empty($fields['email']) && !empty($data[$fields['email']]))) { + $doNotEmail = filter_var($data[$fields['doNotEmail']], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + if (null !== $doNotEmail) { $reason = $this->translator->trans('mautic.lead.import.by.user', [ '%user%' => $this->userHelper->getUser()->getUsername(), ]); // The email must be set for successful unsubscribtion $lead->addUpdatedField('email', $data[$fields['email']]); - $this->addDncForLead($lead, 'email', $reason, DNC::MANUAL); + if ($doNotEmail) { + $this->addDncForLead($lead, 'email', $reason, DNC::MANUAL); + } else { + $this->removeDncForLead($lead, 'email', true); + } } } + unset($fieldData['doNotEmail']); if (!empty($fields['ownerusername']) && !empty($data[$fields['ownerusername']])) { diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index f951bac54a3..ead7ebf9e84 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -309,6 +309,12 @@ public function getChoiceFields() 'operators' => $this->getOperatorsForFieldType('multiselect'), 'object' => 'lead', ], + 'lead_asset_download' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.lead_asset_download'), + 'properties' => ['type' => 'assets'], + 'operators' => $this->getOperatorsForFieldType('multiselect'), + 'object' => 'lead', + ], 'lead_email_received' => [ 'label' => $this->translator->trans('mautic.lead.list.filter.lead_email_received'), 'properties' => [ @@ -461,6 +467,18 @@ public function getChoiceFields() 'operators' => $this->getOperatorsForFieldType('bool'), 'object' => 'lead', ], + 'dnc_manual_email' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.dnc_manual_email'), + 'properties' => [ + 'type' => 'boolean', + 'list' => [ + 0 => $this->translator->trans('mautic.core.form.no'), + 1 => $this->translator->trans('mautic.core.form.yes'), + ], + ], + 'operators' => $this->getOperatorsForFieldType('bool'), + 'object' => 'lead', + ], 'dnc_bounced_sms' => [ 'label' => $this->translator->trans('mautic.lead.list.filter.dnc_bounced_sms'), 'properties' => [ @@ -1722,6 +1740,13 @@ public function getSegmentContactsLineChartData($unit, \DateTime $dateFrom, \Dat * @return bool */ public function isFieldUsed(LeadField $field) + { + $segments = $this->getFieldSegments($field); + + return 0 < $segments->count(); + } + + public function getFieldSegments(LeadField $field) { $alias = $field->getAlias(); $aliasLength = mb_strlen($alias); @@ -1733,6 +1758,105 @@ public function isFieldUsed(LeadField $field) ], ]; - return $this->getEntities(['filter' => $filter])->count() !== 0; + return $this->getEntities(['filter' => $filter]); + } + + /** + * Get segments which are dependent on given segment. + * + * @param int $segmentId + * + * @return array + */ + public function getSegmentsWithDependenciesOnSegment($segmentId) + { + $page = 1; + $limit = 1000; + $start = 0; + + $filter = [ + 'force' => [ + ['column' => 'l.filters', 'expr' => 'LIKE', 'value'=>'%s:8:"leadlist"%'], + ['column' => 'l.id', 'expr' => 'neq', 'value'=>$segmentId], + ], + ]; + + $entities = $this->getEntities( + [ + 'start' => $start, + 'limit' => $limit, + 'filter' => $filter, + ] + ); + $dependents = []; + + foreach ($entities as $entity) { + $retrFilters = $entity->getFilters(); + foreach ($retrFilters as $eachFilter) { + if ($eachFilter['type'] === 'leadlist' && in_array($segmentId, $eachFilter['filter'])) { + $dependents[] = $entity->getName(); + } + } + } + + return $dependents; + } + + /** + * Get segments which are used as a dependent by other segments to prevent batch deletion of them. + * + * @param array $segmentIds + * + * @return array + */ + public function canNotBeDeleted($segmentIds) + { + $filter = [ + 'force' => [ + ['column' => 'l.filters', 'expr' => 'LIKE', 'value'=>'%s:8:"leadlist"%'], + ], + ]; + + $entities = $this->getEntities( + [ + 'filter' => $filter, + ] + ); + + $idsNotToBeDeleted = []; + $namesNotToBeDeleted = []; + $dependency = []; + + foreach ($entities as $entity) { + $retrFilters = $entity->getFilters(); + foreach ($retrFilters as $eachFilter) { + if ($eachFilter['type'] !== 'leadlist') { + continue; + } + + $idsNotToBeDeleted = array_unique(array_merge($idsNotToBeDeleted, $eachFilter['filter'])); + foreach ($eachFilter['filter'] as $val) { + if (!empty($dependency[$val])) { + $dependency[$val] = array_merge($dependency[$val], [$entity->getId()]); + $dependency[$val] = array_unique($dependency[$val]); + } else { + $dependency[$val] = [$entity->getId()]; + } + } + } + } + foreach ($dependency as $key => $value) { + if (array_intersect($value, $segmentIds) === $value) { + $idsNotToBeDeleted = array_unique(array_diff($idsNotToBeDeleted, [$key])); + } + } + + $idsNotToBeDeleted = array_intersect($segmentIds, $idsNotToBeDeleted); + + foreach ($idsNotToBeDeleted as $val) { + $namesNotToBeDeleted[$val] = $this->getEntity($val)->getName(); + } + + return $namesNotToBeDeleted; } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php index d192ccdfb74..26de76478fe 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php @@ -125,7 +125,7 @@ public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilt */ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - $dateTimeHelper = $this->dateDecorator->getDefaultDate(); + $dateTimeHelper = $this->dateOptionParameters->getDefaultDate(); $this->modifyBaseDate($dateTimeHelper); @@ -141,7 +141,7 @@ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilte $dateTimeHelper->modify($modifier); } - return $dateTimeHelper->toUtcString($dateFormat); + return $dateTimeHelper->toLocalString($dateFormat); } /** diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php index 4a889f350da..a443610dada 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php @@ -43,12 +43,19 @@ class DateOptionFactory */ private $relativeDate; + /** + * @var TimezoneResolver + */ + private $timezoneResolver; + public function __construct( DateDecorator $dateDecorator, - RelativeDate $relativeDate + RelativeDate $relativeDate, + TimezoneResolver $timezoneResolver ) { - $this->dateDecorator = $dateDecorator; - $this->relativeDate = $relativeDate; + $this->dateDecorator = $dateDecorator; + $this->relativeDate = $relativeDate; + $this->timezoneResolver = $timezoneResolver; } /** @@ -60,7 +67,8 @@ public function getDateOption(ContactSegmentFilterCrate $leadSegmentFilterCrate) { $originalValue = $leadSegmentFilterCrate->getFilter(); $relativeDateStrings = $this->relativeDate->getRelativeDateStrings(); - $dateOptionParameters = new DateOptionParameters($leadSegmentFilterCrate, $relativeDateStrings); + + $dateOptionParameters = new DateOptionParameters($leadSegmentFilterCrate, $relativeDateStrings, $this->timezoneResolver); $timeframe = $dateOptionParameters->getTimeframe(); @@ -71,11 +79,7 @@ public function getDateOption(ContactSegmentFilterCrate $leadSegmentFilterCrate) switch ($timeframe) { case 'birthday': case 'anniversary': - case $timeframe && ( - false !== strpos($timeframe, 'anniversary') || - false !== strpos($timeframe, 'birthday') - ): - return new DateAnniversary($this->dateDecorator); + return new DateAnniversary($this->dateDecorator, $dateOptionParameters); case 'today': return new DateDayToday($this->dateDecorator, $dateOptionParameters); case 'tomorrow': @@ -105,7 +109,7 @@ public function getDateOption(ContactSegmentFilterCrate $leadSegmentFilterCrate) false !== strpos($timeframe[0], '+') || // +5 days false !== strpos($timeframe, ' ago') // 5 days ago ): - return new DateRelativeInterval($this->dateDecorator, $originalValue); + return new DateRelativeInterval($this->dateDecorator, $originalValue, $dateOptionParameters); default: return new DateDefault($this->dateDecorator, $originalValue); } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php index 47103a01c4a..006544ab891 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php @@ -11,6 +11,7 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; +use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; class DateOptionParameters @@ -35,16 +36,27 @@ class DateOptionParameters */ private $shouldUseLastDayOfRange; + /** + * @var DateTimeHelper + */ + private $dateTimeHelper; + /** * @param ContactSegmentFilterCrate $leadSegmentFilterCrate * @param array $relativeDateStrings + * @param TimezoneResolver $timezoneResolver */ - public function __construct(ContactSegmentFilterCrate $leadSegmentFilterCrate, array $relativeDateStrings) - { + public function __construct( + ContactSegmentFilterCrate $leadSegmentFilterCrate, + array $relativeDateStrings, + TimezoneResolver $timezoneResolver + ) { $this->hasTimePart = $leadSegmentFilterCrate->hasTimeParts(); $this->timeframe = $this->parseTimeFrame($leadSegmentFilterCrate, $relativeDateStrings); $this->requiresBetween = in_array($leadSegmentFilterCrate->getOperator(), ['=', '!='], true); $this->shouldUseLastDayOfRange = in_array($leadSegmentFilterCrate->getOperator(), ['gt', 'lte'], true); + + $this->setDateTimeHelper($timezoneResolver); } /** @@ -83,6 +95,14 @@ public function shouldUseLastDayOfRange() return $this->shouldUseLastDayOfRange; } + /** + * @return DateTimeHelper + */ + public function getDefaultDate() + { + return $this->dateTimeHelper; + } + /** * @param ContactSegmentFilterCrate $leadSegmentFilterCrate * @param array $relativeDateStrings @@ -100,4 +120,9 @@ private function parseTimeFrame(ContactSegmentFilterCrate $leadSegmentFilterCrat return str_replace('mautic.lead.list.', '', $key); } + + private function setDateTimeHelper(TimezoneResolver $timezoneResolver) + { + $this->dateTimeHelper = $timezoneResolver->getDefaultDate($this->hasTimePart()); + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php index bc236c43def..372d7134b2f 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php @@ -30,7 +30,7 @@ protected function getModifierForBetweenRange() */ protected function getValueForBetweenRange(DateTimeHelper $dateTimeHelper) { - return $dateTimeHelper->toUtcString('Y-m-d%'); + return $dateTimeHelper->toLocalString('Y-m-d%'); } /** diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php index 29b3cca34c7..30ba833213c 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php @@ -30,7 +30,7 @@ protected function getModifierForBetweenRange() */ protected function getValueForBetweenRange(DateTimeHelper $dateTimeHelper) { - return $dateTimeHelper->toUtcString('Y-m-%'); + return $dateTimeHelper->toLocalString('Y-m-%'); } /** diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php index a0537f662ee..c0b3edaa873 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php @@ -12,6 +12,7 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Other; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; +use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; @@ -23,11 +24,18 @@ class DateAnniversary implements FilterDecoratorInterface private $dateDecorator; /** - * @param DateDecorator $dateDecorator + * @var DateOptionParameters */ - public function __construct(DateDecorator $dateDecorator) + private $dateOptionParameters; + + /** + * @param DateDecorator $dateDecorator + * @param DateOptionParameters $dateOptionParameters + */ + public function __construct(DateDecorator $dateDecorator, DateOptionParameters $dateOptionParameters) { - $this->dateDecorator = $dateDecorator; + $this->dateDecorator = $dateDecorator; + $this->dateOptionParameters = $dateOptionParameters; } /** @@ -78,11 +86,9 @@ public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilt */ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - $filter = $contactSegmentFilterCrate->getFilter(); - $relativeFilter = trim(str_replace(['anniversary', 'birthday'], '', $filter)); - $dateTimeHelper = $this->dateDecorator->getDefaultDate($relativeFilter); + $dateTimeHelper = $this->dateOptionParameters->getDefaultDate(); - return $dateTimeHelper->toUtcString('%-m-d'); + return $dateTimeHelper->toLocalString('%-m-d'); } /** diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php index 823c787f85d..e09923fb5fb 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php @@ -12,6 +12,7 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Other; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; +use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; @@ -28,13 +29,23 @@ class DateRelativeInterval implements FilterDecoratorInterface private $originalValue; /** - * @param DateDecorator $dateDecorator - * @param string $originalValue + * @var DateOptionParameters */ - public function __construct(DateDecorator $dateDecorator, $originalValue) - { - $this->dateDecorator = $dateDecorator; - $this->originalValue = $originalValue; + private $dateOptionParameters; + + /** + * @param DateDecorator $dateDecorator + * @param string $originalValue + * @param DateOptionParameters $dateOptionParameters + */ + public function __construct( + DateDecorator $dateDecorator, + $originalValue, + DateOptionParameters $dateOptionParameters + ) { + $this->dateDecorator = $dateDecorator; + $this->originalValue = $originalValue; + $this->dateOptionParameters = $dateOptionParameters; } /** @@ -92,7 +103,7 @@ public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilt */ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - $date = $this->dateDecorator->getDefaultDate(); + $date = $this->dateOptionParameters->getDefaultDate(); $date->modify($this->originalValue); $operator = $this->getOperator($contactSegmentFilterCrate); @@ -101,7 +112,7 @@ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilte $format .= '%'; } - return $date->toUtcString($format); + return $date->toLocalString($format); } /** diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/TimezoneResolver.php b/app/bundles/LeadBundle/Segment/Decorator/Date/TimezoneResolver.php new file mode 100644 index 00000000000..2a7ce836cc6 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/TimezoneResolver.php @@ -0,0 +1,54 @@ +coreParametersHelper = $coreParametersHelper; + } + + /** + * @param bool $hasTimePart + * + * @return DateTimeHelper + */ + public function getDefaultDate($hasTimePart) + { + /** + * $hasTimePart tells us if field in a database is date or datetime + * All datetime fields are stored in UTC + * Date field, however, is always stored in a local time (there is no time information, so it cannot be converted to UTC). + * + * We will generate default date according to this. We need midnight as a default date (for relative intervals like "today" or "-1 day" + * 1) in UTC for datetime fields + * 2) in the local timezone for date fields + * + * Later we use toLocalString() method - it gives us midnight in UTC for first condition and midnight in local timezone for second option. + */ + $timezone = $hasTimePart ? 'UTC' : $this->coreParametersHelper->getParameter('default_timezone', 'UTC'); + + $date = new \DateTime('midnight today', new \DateTimeZone($timezone)); + + return new DateTimeHelper($date, null, $timezone); + } +} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php index 82b4c5caab1..a5d19bdddbb 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php @@ -31,11 +31,11 @@ protected function getModifierForBetweenRange() protected function getValueForBetweenRange(DateTimeHelper $dateTimeHelper) { $dateFormat = $this->dateOptionParameters->hasTimePart() ? 'Y-m-d H:i:s' : 'Y-m-d'; - $startWith = $dateTimeHelper->toUtcString($dateFormat); + $startWith = $dateTimeHelper->toLocalString($dateFormat); $modifier = $this->getModifierForBetweenRange().' -1 second'; $dateTimeHelper->modify($modifier); - $endWith = $dateTimeHelper->toUtcString($dateFormat); + $endWith = $dateTimeHelper->toLocalString($dateFormat); return [$startWith, $endWith]; } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php index 4db321b7a81..d640fcea677 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php @@ -30,7 +30,7 @@ protected function getModifierForBetweenRange() */ protected function getValueForBetweenRange(DateTimeHelper $dateTimeHelper) { - return $dateTimeHelper->toUtcString('Y-%'); + return $dateTimeHelper->toLocalString('Y-%'); } /** diff --git a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php index 1f3748ac9cb..3c1fe84087f 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php @@ -54,6 +54,8 @@ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilte } /** + * @deprecated Use DateOptionParameters->getDefaultDate() which takes timezone into account + * * @param null|string $relativeDate * * @return DateTimeHelper @@ -64,8 +66,8 @@ public function getDefaultDate($relativeDate = null) if ($relativeDate) { return new DateTimeHelper($relativeDate, null, $timezone); - } else { - return new DateTimeHelper('midnight today', null, $timezone); } + + return new DateTimeHelper('midnight today', null, $timezone); } } diff --git a/app/bundles/LeadBundle/Segment/DoNotContact/DoNotContactParts.php b/app/bundles/LeadBundle/Segment/DoNotContact/DoNotContactParts.php index da5faa3b377..7f8f5ad27ee 100644 --- a/app/bundles/LeadBundle/Segment/DoNotContact/DoNotContactParts.php +++ b/app/bundles/LeadBundle/Segment/DoNotContact/DoNotContactParts.php @@ -48,6 +48,16 @@ public function getChannel() */ public function getParameterType() { - return $this->type === 'bounced' ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED; + switch ($this->type) { + case 'bounced': + return DoNotContact::BOUNCED; + break; + case 'manual': + return DoNotContact::MANUAL; + break; + default: + return DoNotContact::UNSUBSCRIBED; + break; + } } } diff --git a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php index e47e93f9924..eb17b52a28c 100644 --- a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php +++ b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php @@ -84,6 +84,10 @@ public function __construct() 'type' => DoNotContactFilterQueryBuilder::getServiceId(), ]; + $this->translations['dnc_manual_email'] = [ + 'type' => DoNotContactFilterQueryBuilder::getServiceId(), + ]; + $this->translations['dnc_unsubscribed_sms'] = [ 'type' => DoNotContactFilterQueryBuilder::getServiceId(), ]; @@ -231,6 +235,12 @@ public function __construct() 'where' => 'campaign_leads.manually_removed = 0', ]; + $this->translations['lead_asset_download'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'asset_downloads', + 'field' => 'asset_id', + ]; + parent::__construct($this->translations); } } diff --git a/app/bundles/LeadBundle/Templating/Helper/DncReasonHelper.php b/app/bundles/LeadBundle/Templating/Helper/DncReasonHelper.php new file mode 100644 index 00000000000..0312ea3d48b --- /dev/null +++ b/app/bundles/LeadBundle/Templating/Helper/DncReasonHelper.php @@ -0,0 +1,79 @@ +translator = $translator; + } + + /** + * Convert DNC reason ID to text. + * + * @param int $reasonId + * + * @return string + * + * @throws UnknownDncReasonException + */ + public function toText($reasonId) + { + switch ($reasonId) { + case DoNotContact::IS_CONTACTABLE: + $reasonKey = 'mautic.lead.event.donotcontact_contactable'; + break; + case DoNotContact::UNSUBSCRIBED: + $reasonKey = 'mautic.lead.event.donotcontact_unsubscribed'; + break; + case DoNotContact::BOUNCED: + $reasonKey = 'mautic.lead.event.donotcontact_bounced'; + break; + case DoNotContact::MANUAL: + $reasonKey = 'mautic.lead.event.donotcontact_manual'; + break; + default: + throw new UnknownDncReasonException( + sprintf("Unknown DNC reason ID '%c'", $reasonId) + ); + } + + return $this->translator->trans($reasonKey); + } + + /** + * Returns the canonical name of this helper. + * + * @return string The canonical name + */ + public function getName() + { + return 'lead_dnc_reason'; + } +} diff --git a/app/bundles/LeadBundle/Tests/Controller/Api/LeadApiControllerFunctionalTest.php b/app/bundles/LeadBundle/Tests/Controller/Api/LeadApiControllerFunctionalTest.php index 3126be51df6..18b30a21c7f 100644 --- a/app/bundles/LeadBundle/Tests/Controller/Api/LeadApiControllerFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Controller/Api/LeadApiControllerFunctionalTest.php @@ -13,6 +13,8 @@ use FOS\RestBundle\Util\Codes; use Mautic\CoreBundle\Test\MauticMysqlTestCase; +use Mautic\LeadBundle\Entity\DoNotContact; +use Symfony\Component\HttpFoundation\Response; class LeadApiControllerFunctionalTest extends MauticMysqlTestCase { @@ -146,4 +148,61 @@ public function testSingleNewEndpointCreateAndUpdate() $this->assertEquals(4, $response['contact']['points']); $this->assertEquals(2, count($response['contact']['tags'])); } + + public function testBachdDncAddAndRemove() + { + // Create contact + $emailAddress = uniqid('', false).'@mautic.com'; + + $payload = [ + 'id' => 80, + 'email'=> $emailAddress, + ]; + + $this->client->request('POST', '/api/contacts/new', $payload); + $clientResponse = $this->client->getResponse(); + $response = json_decode($clientResponse->getContent(), true); + $contactId = $response['contact']['id']; + + // Batch update contact with new DNC record + $payload = [[ + 'id' => $contactId, + 'email' => $emailAddress, + 'doNotContact' => [[ + 'reason' => DoNotContact::MANUAL, + 'comments' => 'manually', + 'channel' => 'email', + 'channelId' => null, + ]], + ]]; + + $this->client->request('PUT', '/api/contacts/batch/edit', $payload); + $clientResponse = $this->client->getResponse(); + $response = json_decode($clientResponse->getContent(), true); + + $this->assertSame(3, $response['contacts'][0]['doNotContact'][0]['reason']); + + // Batch update contact and remove DNC record + $payload = [[ + 'id' => $contactId, + 'email' => $emailAddress, + 'doNotContact' => [[ + 'reason' => DoNotContact::IS_CONTACTABLE, + 'comments' => 'manually', + 'channel' => 'email', + 'channelId' => null, + ]], + ]]; + + $this->client->request('PUT', '/api/contacts/batch/edit', $payload); + $clientResponse = $this->client->getResponse(); + $response = json_decode($clientResponse->getContent(), true); + + $this->assertSame(null, $response['contacts'][0]['doNotContact'][0]['reason']); + + // Remove contact + $this->client->request('DELETE', "/api/contacts/$contactId/delete"); + $clientResponse = $this->client->getResponse(); + $this->assertSame(Response::HTTP_OK, $clientResponse->getStatusCode()); + } } diff --git a/app/bundles/LeadBundle/Tests/EventListener/LeadSubscriberTest.php b/app/bundles/LeadBundle/Tests/EventListener/LeadSubscriberTest.php index 8aeced422a9..e0a1371f13f 100644 --- a/app/bundles/LeadBundle/Tests/EventListener/LeadSubscriberTest.php +++ b/app/bundles/LeadBundle/Tests/EventListener/LeadSubscriberTest.php @@ -18,6 +18,7 @@ use Mautic\LeadBundle\Event\LeadEvent; use Mautic\LeadBundle\EventListener\LeadSubscriber; use Mautic\LeadBundle\Helper\LeadChangeEventDispatcher; +use Mautic\LeadBundle\Templating\Helper\DncReasonHelper; class LeadSubscriberTest extends CommonMocks { @@ -77,7 +78,9 @@ public function testOnLeadPostSaveWillNotProcessTheSameLeadTwice() ->disableOriginalConstructor() ->getMock(); - $subscriber = new LeadSubscriber($ipLookupHelper, $auditLogModel, $leadEventDispatcher); + $dncReasonHelper = $this->createMock(DncReasonHelper::class); + + $subscriber = new LeadSubscriber($ipLookupHelper, $auditLogModel, $leadEventDispatcher, $dncReasonHelper); $leadEvent = $this->getMockBuilder(LeadEvent::class) ->disableOriginalConstructor() diff --git a/app/bundles/LeadBundle/Tests/EventListener/WebhookSubscriberTest.php b/app/bundles/LeadBundle/Tests/EventListener/WebhookSubscriberTest.php index 2c672cfc70e..5ed142515e1 100644 --- a/app/bundles/LeadBundle/Tests/EventListener/WebhookSubscriberTest.php +++ b/app/bundles/LeadBundle/Tests/EventListener/WebhookSubscriberTest.php @@ -154,6 +154,7 @@ public function testChannelChangeIsPickedUpByWebhook() 'userList', 'publishDetails', 'ipAddress', + 'tagList', ] ); diff --git a/app/bundles/LeadBundle/Tests/Model/LeadListModelTest.php b/app/bundles/LeadBundle/Tests/Model/LeadListModelTest.php new file mode 100644 index 00000000000..8046ca09be2 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Model/LeadListModelTest.php @@ -0,0 +1,121 @@ +getMockBuilder(ListModel::class) + ->disableOriginalConstructor() + ->setMethods(['getEntities', 'getEntity']) + ->getMock(); + + $mockListModel->expects($this->any()) + ->method('getEntity') + ->willReturnCallback(function ($id) { + $mockEntity = $this->getMockBuilder(LeadList::class) + ->disableOriginalConstructor() + ->setMethods(['getName']) + ->getMock(); + + $mockEntity->expects($this->once()) + ->method('getName') + ->willReturn((string) $id); + + return $mockEntity; + }); + + $filters = 'a:1:{i:0;a:7:{s:4:"glue";s:3:"and";s:5:"field";s:8:"leadlist";s:6:"object";s:4:"lead";s:4:"type";s:8:"leadlist";s:6:"filter";a:2:{i:0;i:1;i:1;i:3;}s:7:"display";N;s:8:"operator";s:2:"in";}}'; + + $filters4 = 'a:1:{i:0;a:7:{s:4:"glue";s:3:"and";s:5:"field";s:8:"leadlist";s:6:"object";s:4:"lead";s:4:"type";s:8:"leadlist";s:6:"filter";a:1:{i:0;i:3;}s:7:"display";N;s:8:"operator";s:2:"in";}}'; + + $mockEntity = $this->getMockBuilder(LeadList::class) + ->disableOriginalConstructor() + ->getMock(); + + $mockEntity1 = clone $mockEntity; + $mockEntity1->expects($this->once()) + ->method('getFilters') + ->willReturn([]); + $mockEntity1->expects($this->any()) + ->method('getId') + ->willReturn(1); + + $mockEntity2 = clone $mockEntity; + $mockEntity2->expects($this->once()) + ->method('getFilters') + ->willReturn(unserialize($filters)); + $mockEntity2->expects($this->any()) + ->method('getId') + ->willReturn(2); + + $mockEntity3 = clone $mockEntity; + $mockEntity3->expects($this->once()) + ->method('getFilters') + ->willReturn([]); + $mockEntity3->expects($this->any()) + ->method('getId') + ->willReturn(3); + + $mockEntity4 = clone $mockEntity; + $mockEntity4->expects($this->once()) + ->method('getFilters') + ->willReturn(unserialize($filters4)); + $mockEntity4->expects($this->any()) + ->method('getId') + ->willReturn(4); + + $mockListModel->expects($this->once()) + ->method('getEntities') + ->willReturn([ + 1 => $mockEntity1, + 2 => $mockEntity2, + 3 => $mockEntity3, + 4 => $mockEntity4, + ]); + + $this->fixture = $mockListModel; + } + + /** + * @dataProvider segmentTestDataProvider + */ + public function testSegmentsCanBeDeletedCorrecty(array $arg, array $expected, $message) + { + $result = $this->fixture->canNotBeDeleted($arg); + + $this->assertEquals($expected, $result, $message); + } + + public function segmentTestDataProvider() + { + return [ + [ + [1], + [1 => '1'], + '2 is dependent on 1, so 1 cannot be deleted.', + ], + [ + [1, 3], + [1 => '1', 3 => '3'], + '2 is dependent on 1 & 3, so 1 & 3 cannot be deleted.', + ], + [ + [1, 2, 3, 4], + [], + 'Since we are deleting all segments, it should not prevent any from being deleted.', + ], + [ + [2], + [], + 'Segments without any other segment dependent on them should always be able to be deleted.', + ], + ]; + } +} diff --git a/app/bundles/LeadBundle/Tests/Model/LeadModelTest.php b/app/bundles/LeadBundle/Tests/Model/LeadModelTest.php index 509b1430263..79eb7eba7ed 100644 --- a/app/bundles/LeadBundle/Tests/Model/LeadModelTest.php +++ b/app/bundles/LeadBundle/Tests/Model/LeadModelTest.php @@ -123,6 +123,27 @@ protected function setUp() $this->companyModelMock->method('getCompanyLeadRepository')->willReturn($this->companyLeadRepositoryMock); } + public function testIpLookupDoesNotAddCompanyIfConfiguredSo() + { + $entity = new Lead(); + $ipAddress = new IpAddress(); + + $ipAddress->setIpDetails(['organization' => 'Doctors Without Borders']); + + $entity->addIpAddress($ipAddress); + + $this->coreParametersHelperMock->expects($this->once())->method('getParameter')->with('ip_lookup_create_organization', false)->willReturn(false); + $this->fieldModelMock->method('getFieldListWithProperties')->willReturn([]); + $this->fieldModelMock->method('getFieldList')->willReturn([]); + $this->companyLeadRepositoryMock->expects($this->never())->method('getEntitiesByLead'); + $this->companyModelMock->expects($this->never())->method('getEntities'); + + $this->leadModel->saveEntity($entity); + + $this->assertNull($entity->getCompany()); + $this->assertTrue(empty($entity->getUpdatedFields()['company'])); + } + public function testIpLookupAddsCompanyIfDoesNotExistInEntity() { $companyFromIpLookup = 'Doctors Without Borders'; @@ -133,9 +154,11 @@ public function testIpLookupAddsCompanyIfDoesNotExistInEntity() $entity->addIpAddress($ipAddress); + $this->coreParametersHelperMock->expects($this->once())->method('getParameter')->with('ip_lookup_create_organization', false)->willReturn(true); $this->fieldModelMock->method('getFieldListWithProperties')->willReturn([]); $this->fieldModelMock->method('getFieldList')->willReturn([]); $this->companyLeadRepositoryMock->method('getEntitiesByLead')->willReturn([]); + $this->companyModelMock->expects($this->once())->method('getEntities')->willReturn([]); $this->leadModel->saveEntity($entity); @@ -155,6 +178,7 @@ public function testIpLookupAddsCompanyIfExistsInEntity() $entity->addIpAddress($ipAddress); + $this->coreParametersHelperMock->expects($this->never())->method('getParameter'); $this->fieldModelMock->method('getFieldListWithProperties')->willReturn([]); $this->fieldModelMock->method('getFieldList')->willReturn([]); $this->companyLeadRepositoryMock->method('getEntitiesByLead')->willReturn([]); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php index a9c93502825..42f86491837 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php @@ -22,6 +22,7 @@ use Mautic\LeadBundle\Segment\Decorator\Date\Other\DateAnniversary; use Mautic\LeadBundle\Segment\Decorator\Date\Other\DateDefault; use Mautic\LeadBundle\Segment\Decorator\Date\Other\DateRelativeInterval; +use Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver; use Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast; use Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekNext; use Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekThis; @@ -262,8 +263,9 @@ public function testNullValue() */ private function getFilterDecorator($filterName) { - $dateDecorator = $this->createMock(DateDecorator::class); - $relativeDate = $this->createMock(RelativeDate::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $relativeDate = $this->createMock(RelativeDate::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $relativeDate->method('getRelativeDateStrings') ->willReturn( @@ -285,7 +287,7 @@ private function getFilterDecorator($filterName) ] ); - $dateOptionFactory = new DateOptionFactory($dateDecorator, $relativeDate); + $dateOptionFactory = new DateOptionFactory($dateDecorator, $relativeDate, $timezoneResolver); $filter = [ 'glue' => 'and', diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTodayTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTodayTest.php index 23de4fbae02..ceb4cc39c8a 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTodayTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTodayTest.php @@ -15,6 +15,7 @@ use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; use Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayToday; +use Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; class DateDayTodayTest extends \PHPUnit_Framework_TestCase @@ -24,13 +25,14 @@ class DateDayTodayTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $filter = [ 'operator' => '=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateDayToday($dateDecorator, $dateOptionParameters); @@ -42,7 +44,8 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $dateDecorator->method('getOperator') ->with() @@ -52,7 +55,7 @@ public function testGetOperatorLessOrEqual() 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateDayToday($dateDecorator, $dateOptionParameters); @@ -64,11 +67,12 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -76,7 +80,7 @@ public function testGetParameterValueBetween() 'operator' => '!=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateDayToday($dateDecorator, $dateOptionParameters); @@ -88,11 +92,12 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -100,7 +105,7 @@ public function testGetParameterValueSingle() 'operator' => 'lt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateDayToday($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTomorrowTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTomorrowTest.php index 2f1e3e41c6a..82acd0b5006 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTomorrowTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTomorrowTest.php @@ -15,6 +15,7 @@ use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; use Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayTomorrow; +use Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; class DateDayTomorrowTest extends \PHPUnit_Framework_TestCase @@ -24,13 +25,14 @@ class DateDayTomorrowTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $filter = [ 'operator' => '=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateDayTomorrow($dateDecorator, $dateOptionParameters); @@ -42,7 +44,8 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $dateDecorator->method('getOperator') ->with() @@ -52,7 +55,7 @@ public function testGetOperatorLessOrEqual() 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateDayTomorrow($dateDecorator, $dateOptionParameters); @@ -64,11 +67,12 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -76,7 +80,7 @@ public function testGetParameterValueBetween() 'operator' => '!=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateDayTomorrow($dateDecorator, $dateOptionParameters); @@ -88,11 +92,12 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -100,7 +105,7 @@ public function testGetParameterValueSingle() 'operator' => 'lt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateDayTomorrow($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayYesterdayTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayYesterdayTest.php index d3da77c713a..91f3d1c195e 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayYesterdayTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayYesterdayTest.php @@ -15,6 +15,7 @@ use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; use Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayYesterday; +use Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; class DateDayYesterdayTest extends \PHPUnit_Framework_TestCase @@ -24,13 +25,14 @@ class DateDayYesterdayTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $filter = [ 'operator' => '=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateDayYesterday($dateDecorator, $dateOptionParameters); @@ -42,7 +44,8 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $dateDecorator->method('getOperator') ->with() @@ -52,7 +55,7 @@ public function testGetOperatorLessOrEqual() 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateDayYesterday($dateDecorator, $dateOptionParameters); @@ -64,11 +67,12 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -76,7 +80,7 @@ public function testGetParameterValueBetween() 'operator' => '!=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateDayYesterday($dateDecorator, $dateOptionParameters); @@ -88,11 +92,12 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -100,7 +105,7 @@ public function testGetParameterValueSingle() 'operator' => 'lt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateDayYesterday($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php index aa6b7904ad3..70e9920bac8 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php @@ -15,6 +15,7 @@ use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; use Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthLast; +use Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; class DateMonthLastTest extends \PHPUnit_Framework_TestCase @@ -24,13 +25,14 @@ class DateMonthLastTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $filter = [ 'operator' => '=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateMonthLast($dateDecorator, $dateOptionParameters); @@ -42,7 +44,8 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $dateDecorator->method('getOperator') ->with() @@ -52,7 +55,7 @@ public function testGetOperatorLessOrEqual() 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateMonthLast($dateDecorator, $dateOptionParameters); @@ -64,11 +67,12 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -76,7 +80,7 @@ public function testGetParameterValueBetween() 'operator' => '!=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateMonthLast($dateDecorator, $dateOptionParameters); @@ -90,11 +94,12 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -102,7 +107,7 @@ public function testGetParameterValueSingle() 'operator' => 'lt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateMonthLast($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php index a26cd0c915b..22336d6c928 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php @@ -15,6 +15,7 @@ use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; use Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthNext; +use Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; class DateMonthNextTest extends \PHPUnit_Framework_TestCase @@ -24,13 +25,14 @@ class DateMonthNextTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $filter = [ 'operator' => '=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateMonthNext($dateDecorator, $dateOptionParameters); @@ -42,7 +44,8 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $dateDecorator->method('getOperator') ->with() @@ -52,7 +55,7 @@ public function testGetOperatorLessOrEqual() 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateMonthNext($dateDecorator, $dateOptionParameters); @@ -64,11 +67,12 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -76,7 +80,7 @@ public function testGetParameterValueBetween() 'operator' => '!=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateMonthNext($dateDecorator, $dateOptionParameters); @@ -90,11 +94,12 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -102,7 +107,7 @@ public function testGetParameterValueSingle() 'operator' => 'lt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateMonthNext($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php index 132bf7a2024..c3a56ab7735 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php @@ -15,6 +15,7 @@ use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; use Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthThis; +use Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; class DateMonthThisTest extends \PHPUnit_Framework_TestCase @@ -24,13 +25,14 @@ class DateMonthThisTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $filter = [ 'operator' => '=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateMonthThis($dateDecorator, $dateOptionParameters); @@ -42,7 +44,8 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $dateDecorator->method('getOperator') ->with() @@ -52,7 +55,7 @@ public function testGetOperatorLessOrEqual() 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateMonthThis($dateDecorator, $dateOptionParameters); @@ -64,11 +67,12 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -76,7 +80,7 @@ public function testGetParameterValueBetween() 'operator' => '!=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateMonthThis($dateDecorator, $dateOptionParameters); @@ -90,11 +94,12 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -102,7 +107,7 @@ public function testGetParameterValueSingle() 'operator' => 'lt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateMonthThis($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateAnniversaryTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateAnniversaryTest.php index 00df452c1c1..f5579a8c334 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateAnniversaryTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateAnniversaryTest.php @@ -13,7 +13,9 @@ use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; +use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; use Mautic\LeadBundle\Segment\Decorator\Date\Other\DateAnniversary; +use Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; class DateAnniversaryTest extends \PHPUnit_Framework_TestCase @@ -24,9 +26,17 @@ class DateAnniversaryTest extends \PHPUnit_Framework_TestCase public function testGetOperator() { $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); + + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); - $filterDecorator = new DateAnniversary($dateDecorator); + $filterDecorator = new DateAnniversary($dateDecorator, $dateOptionParameters); $this->assertEquals('like', $filterDecorator->getOperator($contactSegmentFilterCrate)); } @@ -36,17 +46,24 @@ public function testGetOperator() */ public function testGetParameterValue() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); - $filterDecorator = new DateAnniversary($dateDecorator); + $filterDecorator = new DateAnniversary($dateDecorator, $dateOptionParameters); $this->assertEquals('%-03-02', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php index 9da19592f94..c625bc15644 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php @@ -13,7 +13,9 @@ use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; +use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; use Mautic\LeadBundle\Segment\Decorator\Date\Other\DateRelativeInterval; +use Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; class DateRelativeIntervalTest extends \PHPUnit_Framework_TestCase @@ -23,13 +25,16 @@ class DateRelativeIntervalTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); + $filter = [ 'operator' => '=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); - $filterDecorator = new DateRelativeInterval($dateDecorator, '+5 days'); + $filterDecorator = new DateRelativeInterval($dateDecorator, '+5 days', $dateOptionParameters); $this->assertEquals('like', $filterDecorator->getOperator($contactSegmentFilterCrate)); } @@ -39,13 +44,16 @@ public function testGetOperatorEqual() */ public function testGetOperatorNotEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); + $filter = [ 'operator' => '!=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); - $filterDecorator = new DateRelativeInterval($dateDecorator, '+5 days'); + $filterDecorator = new DateRelativeInterval($dateDecorator, '+5 days', $dateOptionParameters); $this->assertEquals('notLike', $filterDecorator->getOperator($contactSegmentFilterCrate)); } @@ -55,7 +63,8 @@ public function testGetOperatorNotEqual() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $dateDecorator->method('getOperator') ->with() @@ -65,8 +74,9 @@ public function testGetOperatorLessOrEqual() 'operator' => '=<', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); - $filterDecorator = new DateRelativeInterval($dateDecorator, '+5 days'); + $filterDecorator = new DateRelativeInterval($dateDecorator, '+5 days', $dateOptionParameters); $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); } @@ -76,11 +86,12 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValuePlusDaysWithGreaterOperator() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -88,8 +99,9 @@ public function testGetParameterValuePlusDaysWithGreaterOperator() 'operator' => '>', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); - $filterDecorator = new DateRelativeInterval($dateDecorator, '+5 days'); + $filterDecorator = new DateRelativeInterval($dateDecorator, '+5 days', $dateOptionParameters); $this->assertEquals('2018-03-07', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } @@ -99,11 +111,12 @@ public function testGetParameterValuePlusDaysWithGreaterOperator() */ public function testGetParameterValueMinusMonthWithNotEqualOperator() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -111,8 +124,9 @@ public function testGetParameterValueMinusMonthWithNotEqualOperator() 'operator' => '!=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); - $filterDecorator = new DateRelativeInterval($dateDecorator, '-3 months'); + $filterDecorator = new DateRelativeInterval($dateDecorator, '-3 months', $dateOptionParameters); $this->assertEquals('2017-12-02%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } @@ -122,11 +136,12 @@ public function testGetParameterValueMinusMonthWithNotEqualOperator() */ public function testGetParameterValueDaysAgoWithNotEqualOperator() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -134,8 +149,9 @@ public function testGetParameterValueDaysAgoWithNotEqualOperator() 'operator' => '!=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); - $filterDecorator = new DateRelativeInterval($dateDecorator, '5 days ago'); + $filterDecorator = new DateRelativeInterval($dateDecorator, '5 days ago', $dateOptionParameters); $this->assertEquals('2018-02-25%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } @@ -145,11 +161,12 @@ public function testGetParameterValueDaysAgoWithNotEqualOperator() */ public function testGetParameterValueYearsAgoWithGreaterOperator() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -157,8 +174,9 @@ public function testGetParameterValueYearsAgoWithGreaterOperator() 'operator' => '>', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); - $filterDecorator = new DateRelativeInterval($dateDecorator, '2 years ago'); + $filterDecorator = new DateRelativeInterval($dateDecorator, '2 years ago', $dateOptionParameters); $this->assertEquals('2016-03-02', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } @@ -168,11 +186,12 @@ public function testGetParameterValueYearsAgoWithGreaterOperator() */ public function testGetParameterValueDaysWithEqualOperator() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -180,8 +199,9 @@ public function testGetParameterValueDaysWithEqualOperator() 'operator' => '=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); - $filterDecorator = new DateRelativeInterval($dateDecorator, '5 days'); + $filterDecorator = new DateRelativeInterval($dateDecorator, '5 days', $dateOptionParameters); $this->assertEquals('2018-03-07%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php index 50df38a9512..725d1e367f0 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php @@ -160,7 +160,7 @@ private function createLead($name, $initialTime, $dateModifier) /** @var LeadRepository $leadRepository */ $leadRepository = $this->container->get('doctrine.orm.default_entity_manager')->getRepository(Lead::class); - $date = new \DateTime($initialTime); + $date = new \DateTime($initialTime, new \DateTimeZone('UTC')); $date->modify($dateModifier); $lead = new Lead(); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php index 4573ac40346..eb8ba6141f4 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php @@ -14,6 +14,7 @@ use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; +use Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver; use Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; @@ -24,13 +25,14 @@ class DateWeekLastTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $filter = [ 'operator' => '=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); @@ -42,7 +44,8 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $dateDecorator->method('getOperator') ->with() ->willReturn('=<'); @@ -51,7 +54,7 @@ public function testGetOperatorLessOrEqual() 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); @@ -63,11 +66,12 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -75,7 +79,7 @@ public function testGetParameterValueBetween() 'operator' => '!=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); @@ -96,11 +100,12 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -108,7 +113,7 @@ public function testGetParameterValueSingle() 'operator' => 'lt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); @@ -122,10 +127,11 @@ public function testGetParameterValueSingle() */ public function testGetParameterValueforGreaterOperatorIncludesSunday() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -133,7 +139,7 @@ public function testGetParameterValueforGreaterOperatorIncludesSunday() 'operator' => 'gt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); @@ -147,10 +153,11 @@ public function testGetParameterValueforGreaterOperatorIncludesSunday() */ public function testGetParameterValueForLessThanOperatorIncludesSunday() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -158,7 +165,7 @@ public function testGetParameterValueForLessThanOperatorIncludesSunday() 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php index e294ca41576..fd7fa3bf72b 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php @@ -14,6 +14,7 @@ use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; +use Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver; use Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekNext; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; @@ -24,13 +25,14 @@ class DateWeekNextTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $filter = [ 'operator' => '=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); @@ -42,7 +44,8 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $dateDecorator->method('getOperator') ->with() ->willReturn('=<'); @@ -51,7 +54,7 @@ public function testGetOperatorLessOrEqual() 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); @@ -63,11 +66,12 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -75,7 +79,7 @@ public function testGetParameterValueBetween() 'operator' => '!=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); @@ -96,11 +100,12 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -108,7 +113,7 @@ public function testGetParameterValueSingle() 'operator' => 'lt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); @@ -122,10 +127,11 @@ public function testGetParameterValueSingle() */ public function testGetParameterValueforGreaterOperatorIncludesSunday() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -133,7 +139,7 @@ public function testGetParameterValueforGreaterOperatorIncludesSunday() 'operator' => 'gt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); @@ -147,10 +153,11 @@ public function testGetParameterValueforGreaterOperatorIncludesSunday() */ public function testGetParameterValueForLessThanOperatorIncludesSunday() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -158,7 +165,7 @@ public function testGetParameterValueForLessThanOperatorIncludesSunday() 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php index 90ac4ba1a8c..1e89418abf7 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php @@ -14,6 +14,7 @@ use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; +use Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver; use Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekThis; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; @@ -24,13 +25,14 @@ class DateWeekThisTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $filter = [ 'operator' => '=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); @@ -42,7 +44,8 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $dateDecorator->method('getOperator') ->with() ->willReturn('=<'); @@ -51,7 +54,7 @@ public function testGetOperatorLessOrEqual() 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); @@ -63,11 +66,12 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -75,7 +79,7 @@ public function testGetParameterValueBetween() 'operator' => '!=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); @@ -96,11 +100,12 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -108,7 +113,7 @@ public function testGetParameterValueSingle() 'operator' => 'lt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); @@ -122,10 +127,11 @@ public function testGetParameterValueSingle() */ public function testGetParameterValueforGreaterOperatorIncludesSunday() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -133,7 +139,7 @@ public function testGetParameterValueforGreaterOperatorIncludesSunday() 'operator' => 'gt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); @@ -147,10 +153,11 @@ public function testGetParameterValueforGreaterOperatorIncludesSunday() */ public function testGetParameterValueForLessThanOperatorIncludesSunday() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -158,7 +165,7 @@ public function testGetParameterValueForLessThanOperatorIncludesSunday() 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php index 7afeb6d8cdf..dbd38491826 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php @@ -14,6 +14,7 @@ use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; +use Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver; use Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearLast; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; @@ -24,13 +25,14 @@ class DateYearLastTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $filter = [ 'operator' => '=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateYearLast($dateDecorator, $dateOptionParameters); @@ -42,7 +44,8 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $dateDecorator->method('getOperator') ->with() @@ -52,7 +55,7 @@ public function testGetOperatorLessOrEqual() 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateYearLast($dateDecorator, $dateOptionParameters); @@ -64,11 +67,12 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -76,7 +80,7 @@ public function testGetParameterValueBetween() 'operator' => '!=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateYearLast($dateDecorator, $dateOptionParameters); @@ -90,11 +94,12 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -102,7 +107,7 @@ public function testGetParameterValueSingle() 'operator' => 'lt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateYearLast($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php index 3505132ef80..e2aacf45cf5 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php @@ -14,6 +14,7 @@ use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; +use Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver; use Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearNext; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; @@ -24,13 +25,14 @@ class DateYearNextTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $filter = [ 'operator' => '=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateYearNext($dateDecorator, $dateOptionParameters); @@ -42,7 +44,8 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $dateDecorator->method('getOperator') ->with() @@ -52,7 +55,7 @@ public function testGetOperatorLessOrEqual() 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateYearNext($dateDecorator, $dateOptionParameters); @@ -64,11 +67,12 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -76,7 +80,7 @@ public function testGetParameterValueBetween() 'operator' => '!=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateYearNext($dateDecorator, $dateOptionParameters); @@ -90,11 +94,12 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -102,7 +107,7 @@ public function testGetParameterValueSingle() 'operator' => 'lt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateYearNext($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php index 90838428eae..96f3e144c07 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php @@ -14,6 +14,7 @@ use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionParameters; +use Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver; use Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearThis; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; @@ -24,13 +25,14 @@ class DateYearThisTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $filter = [ 'operator' => '=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateYearThis($dateDecorator, $dateOptionParameters); @@ -42,7 +44,8 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $dateDecorator->method('getOperator') ->with() @@ -52,7 +55,7 @@ public function testGetOperatorLessOrEqual() 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateYearThis($dateDecorator, $dateOptionParameters); @@ -64,11 +67,12 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -76,7 +80,7 @@ public function testGetParameterValueBetween() 'operator' => '!=', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateYearThis($dateDecorator, $dateOptionParameters); @@ -90,11 +94,12 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); + $dateDecorator = $this->createMock(DateDecorator::class); + $timezoneResolver = $this->createMock(TimezoneResolver::class); $date = new DateTimeHelper('', null, 'local'); - $dateDecorator->method('getDefaultDate') + $timezoneResolver->method('getDefaultDate') ->with() ->willReturn($date); @@ -102,7 +107,7 @@ public function testGetParameterValueSingle() 'operator' => 'lt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, [], $timezoneResolver); $filterDecorator = new DateYearThis($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Templating/DncReasonHelperTest.php b/app/bundles/LeadBundle/Tests/Templating/DncReasonHelperTest.php new file mode 100644 index 00000000000..6d4c5329e5d --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Templating/DncReasonHelperTest.php @@ -0,0 +1,63 @@ + 'mautic.lead.event.donotcontact_contactable', + DoNotContact::UNSUBSCRIBED => 'mautic.lead.event.donotcontact_unsubscribed', + DoNotContact::BOUNCED => 'mautic.lead.event.donotcontact_bounced', + DoNotContact::MANUAL => 'mautic.lead.event.donotcontact_manual', + ]; + + private $translations = [ + 'mautic.lead.event.donotcontact_contactable' => 'a', + 'mautic.lead.event.donotcontact_unsubscribed' => 'b', + 'mautic.lead.event.donotcontact_bounced' => 'c', + 'mautic.lead.event.donotcontact_manual' => 'd', + ]; + + public function testToText() + { + foreach ($this->reasonTo as $reasonId => $translationKey) { + $translationResult = $this->translations[$translationKey]; + + $translator = $this->createMock(TranslatorInterface::class); + $translator->expects($this->once()) + ->method('trans') + ->with($translationKey) + ->willReturn($translationResult); + + $dncReasonHelper = new DncReasonHelper($translator); + + $this->assertSame($translationResult, $dncReasonHelper->toText($reasonId)); + } + + $translator = $this->createMock(TranslatorInterface::class); + $dncReasonHelper = new DncReasonHelper($translator); + $this->expectException(UnknownDncReasonException::class); + $dncReasonHelper->toText(999); + } + + public function testGetName() + { + $translator = $this->createMock(TranslatorInterface::class); + $dncReasonHelper = new DncReasonHelper($translator); + $this->assertSame('lead_dnc_reason', $dncReasonHelper->getName()); + } +} diff --git a/app/bundles/LeadBundle/Tests/Validator/Constraints/LengthTest.php b/app/bundles/LeadBundle/Tests/Validator/Constraints/LengthTest.php new file mode 100644 index 00000000000..53c96dcd4f0 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Validator/Constraints/LengthTest.php @@ -0,0 +1,24 @@ + 3]); + $this->assertEquals(LengthValidator::class, $constraint->validatedBy()); + } +} diff --git a/app/bundles/LeadBundle/Tests/Validator/Constraints/LengthValidatorTest.php b/app/bundles/LeadBundle/Tests/Validator/Constraints/LengthValidatorTest.php new file mode 100644 index 00000000000..15ed2b5ae29 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Validator/Constraints/LengthValidatorTest.php @@ -0,0 +1,27 @@ + 3]); + $validator = new LengthValidator(); + $this->assertNull($validator->validate('valid', $constraint)); + // Not thrownig Symfony\Component\Validator\Exception\UnexpectedTypeException + $this->assertNull($validator->validate(['0', '1'], $constraint)); + } +} diff --git a/app/bundles/LeadBundle/Translations/en_US/flashes.ini b/app/bundles/LeadBundle/Translations/en_US/flashes.ini index a7af39600b5..0b46456d883 100644 --- a/app/bundles/LeadBundle/Translations/en_US/flashes.ini +++ b/app/bundles/LeadBundle/Translations/en_US/flashes.ini @@ -10,6 +10,8 @@ mautic.lead.lead.notice.addedtolists="
+ hasEntityAccess( + $permissions['lead:leads:editown'], + $permissions['lead:leads:editother'], + $item->getCreatedBy() + ) + ): ?> + + + + trans('mautic.lead.device.header'); ?> + trans('mautic.lead.device_os_name.header'); ?> + trans('mautic.lead.device_os_version.header'); ?> + trans('mautic.lead.device_browser.header'); ?> + trans('mautic.lead.device_brand.header'); ?> + trans('mautic.core.date.added'); ?> + + + + + + + + + transConditional('mautic.lead.device.'.$device['device'], ucfirst($device['device'])); ?> + + + + + + + toText($device['date_added'], 'utc'); ?> + + + + \ No newline at end of file diff --git a/app/bundles/LeadBundle/Views/Lead/lead.html.php b/app/bundles/LeadBundle/Views/Lead/lead.html.php index 0abd054b1dd..5bca2269c5a 100644 --- a/app/bundles/LeadBundle/Views/Lead/lead.html.php +++ b/app/bundles/LeadBundle/Views/Lead/lead.html.php @@ -31,8 +31,8 @@ $view['slots']->set( 'headerTitle', - $avatar.'
'.$leadName.'' - .$lead->getSecondaryIdentifier().'
' + $avatar.'
'.$view->escape($leadName).'' + .$view->escape($lead->getSecondaryIdentifier()).'
' ); $groups = array_keys($fields); @@ -72,7 +72,7 @@ 'data-target' => '#MauticSharedModal', 'data-header' => $view['translator']->trans( 'mautic.lead.lead.header.contact.frequency', - ['%name%' => $lead->getPrimaryIdentifier()] + ['%name%' => $view->escape($lead->getPrimaryIdentifier())] ), 'href' => $view['router']->path( 'mautic_contact_action', @@ -117,7 +117,7 @@ 'data-target' => '#MauticSharedModal', 'data-header' => $view['translator']->trans( 'mautic.lead.lead.header.merge', - ['%name%' => $lead->getPrimaryIdentifier()] + ['%name%' => $view->escape($lead->getPrimaryIdentifier())] ), 'href' => $view['router']->path( 'mautic_contact_action', @@ -188,9 +188,7 @@ -
  • +
  • trans('mautic.lead.field.group.'.$g); ?> @@ -198,35 +196,41 @@ + +
  • + + trans('mautic.lead.devices'); ?> + +
  • +
    -
    +
    - @@ -239,6 +243,11 @@ + +
    + render('MauticLeadBundle:Lead:devices.html.php', ['devices' => $devices]); ?> +
    + @@ -248,10 +257,10 @@ @@ -425,13 +434,14 @@
    + class="arrow text-muted text-center" + data-toggle="collapse" + data-target="#lead-avatar-block"> + +
    -
    - <?php echo $leadName; ?> +
    + <?php echo $view->escape($leadName); ?>
    @@ -450,7 +460,7 @@ class="caret">
    getStage()): ?> - getStage()->getName(); ?> + escape($lead->getStage()->getName()); ?>
    @@ -459,26 +469,22 @@ class="caret">

    getReason()): ?> - + trans('mautic.lead.do.not.contact'); ?> getReason()): ?> - + trans('mautic.lead.do.not.contact'); ?> - + getReason()): ?> - + trans('mautic.lead.do.not.contact_bounced'); ?> - + @@ -496,7 +502,7 @@ class="caret">
    getOwner()) : ?>
    trans('mautic.lead.lead.field.owner'); ?>
    -

    getOwner()->getName(); ?>

    +

    escape($lead->getOwner()->getName()); ?>

    @@ -504,26 +510,25 @@ class="caret">
    -
    + escape($fields['core']['address1']['value']); ?>
    - '; endif ?> - getLocation(); ?>
    + escape($fields['core']['address2']['value']).'
    ' : ''; ?> + escape($lead->getLocation()); ?> + escape($fields['core']['zipcode']['value']) : '' ?> +
    trans('mautic.core.type.email'); ?>
    -

    +

    escape($fields['core']['email']['value']); ?>

    trans('mautic.lead.field.type.tel.home'); ?>
    -

    +

    escape($fields['core']['phone']['value']); ?>

    trans('mautic.lead.field.type.tel.mobile'); ?>
    -

    +

    escape($fields['core']['mobile']['value']); ?>

    @@ -560,7 +565,7 @@ class="caret"> ['%event%' => $event['event_name'], '%link%' => $link] ); ?> - +

    toFull($event['trigger_date'], 'utc'); ?>

    @@ -573,19 +578,28 @@ class="caret">
    getTags(); ?> -
    getTag(); ?> +
    escape($tag->getTag()); ?>
    -
    trans( - 'mautic.lead.lead.companies'); ?>
    +
    + trans('mautic.lead.lead.companies'); ?> +
    $company): ?> -
    - - -
    +
    + + + + + escape($company['companyname']); ?> + + +
    diff --git a/app/bundles/LeadBundle/Views/List/form.html.php b/app/bundles/LeadBundle/Views/List/form.html.php index dac8cb91d02..5fb9a48130d 100644 --- a/app/bundles/LeadBundle/Views/List/form.html.php +++ b/app/bundles/LeadBundle/Views/List/form.html.php @@ -33,6 +33,7 @@ 'deviceBrands' => 'device_brand-template', 'deviceOs' => 'device_os-template', 'emails' => 'lead_email_received-template', + 'assets' => 'assets-template', 'tags' => 'tags-template', 'stage' => 'stage-template', 'locales' => 'locale-template', diff --git a/app/bundles/PageBundle/EventListener/BuilderSubscriber.php b/app/bundles/PageBundle/EventListener/BuilderSubscriber.php index b6b377e708c..5679b5db939 100644 --- a/app/bundles/PageBundle/EventListener/BuilderSubscriber.php +++ b/app/bundles/PageBundle/EventListener/BuilderSubscriber.php @@ -358,6 +358,7 @@ public function onPageDisplay(Events\PageDisplayEvent $event) for ($i = 0; $i < $divContent->length; ++$i) { $slot = $divContent->item($i); $slot->nodeValue = self::segmentListRegex; + $slot->setAttribute('data-prefs-center', '1'); $content = $dom->saveHTML(); } @@ -365,6 +366,7 @@ public function onPageDisplay(Events\PageDisplayEvent $event) for ($i = 0; $i < $divContent->length; ++$i) { $slot = $divContent->item($i); $slot->nodeValue = self::categoryListRegex; + $slot->setAttribute('data-prefs-center', '1'); $content = $dom->saveHTML(); } @@ -372,6 +374,7 @@ public function onPageDisplay(Events\PageDisplayEvent $event) for ($i = 0; $i < $divContent->length; ++$i) { $slot = $divContent->item($i); $slot->nodeValue = self::preferredchannel; + $slot->setAttribute('data-prefs-center', '1'); $content = $dom->saveHTML(); } @@ -379,14 +382,22 @@ public function onPageDisplay(Events\PageDisplayEvent $event) for ($i = 0; $i < $divContent->length; ++$i) { $slot = $divContent->item($i); $slot->nodeValue = self::channelfrequency; + $slot->setAttribute('data-prefs-center', '1'); $content = $dom->saveHTML(); } $divContent = $xpath->query('//*[@data-slot="saveprefsbutton"]'); for ($i = 0; $i < $divContent->length; ++$i) { $slot = $divContent->item($i); + $saveButton = $xpath->query('//*[@data-slot="saveprefsbutton"]//a')->item(0); $slot->nodeValue = self::saveprefsRegex; + $slot->setAttribute('data-prefs-center', '1'); $content = $dom->saveHTML(); + + $params['saveprefsbutton'] = [ + 'style' => $saveButton->getAttribute('style'), + 'background' => $saveButton->getAttribute('background'), + ]; } unset($slot, $xpath, $dom); @@ -416,6 +427,26 @@ public function onPageDisplay(Events\PageDisplayEvent $event) $savePrefs = $this->renderSavePrefs($params); $content = str_ireplace(self::saveprefsRegex, $savePrefs, $content); } + // add form before first block of prefs center + if (isset($params['startform']) && strpos($content, 'data-prefs-center') !== false) { + $dom = new DOMDocument('1.0', 'utf-8'); + $dom->loadHTML(mb_convert_encoding($content, 'HTML-ENTITIES', 'UTF-8'), LIBXML_NOERROR); + $xpath = new DOMXPath($dom); + // If use slots + $divContent = $xpath->query('//*[@data-prefs-center="1"]'); + if (!$divContent->length) { + // If use tokens + $divContent = $xpath->query('//*[@data-prefs-center-first="1"]'); + } + + if ($divContent->length) { + $slot = $divContent->item(0); + $newnode = $dom->createElement('startform'); + $slot->parentNode->insertBefore($newnode, $slot); + $content = $dom->saveHTML(); + $content = str_replace('', $params['startform'], $content); + } + } if (false !== strpos($content, self::successmessage)) { $successMessage = $this->renderSuccessMessage($params); @@ -458,6 +489,14 @@ protected function renderSocialShareButtons() return $content; } + /** + * @return string + */ + private function getAttributeForFirtSlot() + { + return 'data-prefs-center-first="1"'; + } + /** * Renders the HTML for the segment list. * @@ -470,7 +509,7 @@ protected function renderSegmentList(array $params = []) static $content = ''; if (empty($content)) { - $content = "
    \n"; + $content = "
    getAttributeForFirtSlot().">\n"; $content .= $this->templating->render('MauticCoreBundle:Slots:segmentlist.html.php', $params); $content .= "
    \n"; } @@ -488,7 +527,7 @@ protected function renderCategoryList(array $params = []) static $content = ''; if (empty($content)) { - $content = "
    \n"; + $content = "
    getAttributeForFirtSlot().">\n"; $content .= $this->templating->render('MauticCoreBundle:Slots:categorylist.html.php', $params); $content .= "
    \n"; } @@ -542,7 +581,7 @@ protected function renderSavePrefs(array $params = []) static $content = ''; if (empty($content)) { - $content = "
    \n"; + $content = "
    getAttributeForFirtSlot().">\n"; $content .= $this->templating->render('MauticCoreBundle:Slots:saveprefsbutton.html.php', $params); $content .= "
    \n"; } @@ -606,7 +645,7 @@ protected function renderLanguageBar($page) $related[$parent->getId()] = [ 'lang' => $trans, // Add ntrd to not auto redirect to another language - 'url' => $this->pageModel->generateUrl($parent, false).'?ntrd=1', + 'url' => $this->pageModel->generateUrl($parent, false).'?ntrd=1', ]; foreach ($children as $c) { $lang = $c->getLanguage(); @@ -617,7 +656,7 @@ protected function renderLanguageBar($page) $related[$c->getId()] = [ 'lang' => $trans, // Add ntrd to not auto redirect to another language - 'url' => $this->pageModel->generateUrl($c, false).'?ntrd=1', + 'url' => $this->pageModel->generateUrl($c, false).'?ntrd=1', ]; } } diff --git a/app/bundles/PageBundle/EventListener/MaintenanceSubscriber.php b/app/bundles/PageBundle/EventListener/MaintenanceSubscriber.php index 835ae6e9726..4314cca8192 100644 --- a/app/bundles/PageBundle/EventListener/MaintenanceSubscriber.php +++ b/app/bundles/PageBundle/EventListener/MaintenanceSubscriber.php @@ -91,18 +91,30 @@ private function cleanData(MaintenanceEvent $event, $table) $subQb->expr()->lte('l.date_added', ':date2'), $subQb->expr()->isNull('l.last_active') )); - $qb->setParameter('date2', $event->getDate()->format('Y-m-d H:i:s')); + $subQb->setParameter('date2', $event->getDate()->format('Y-m-d H:i:s')); } - $rows = $qb->delete(MAUTIC_TABLE_PREFIX.$table) - ->where( + $rows = 0; + $loop = 0; + $subQb->setParameter('date', $event->getDate()->format('Y-m-d H:i:s')); + while (true) { + $subQb->setMaxResults(10000)->setFirstResult($loop * 10000); + + $leadsIds = array_column($subQb->execute()->fetchAll(), 'id'); + + if (sizeof($leadsIds) === 0) { + break; + } + + $rows += $qb->delete(MAUTIC_TABLE_PREFIX.$table) + ->where( $qb->expr()->in( - 'lead_id', - $subQb->getSQL() + 'lead_id', $leadsIds ) - ) - ->execute(); + ) + ->execute(); + ++$loop; + } } - $event->setStat($this->translator->trans('mautic.maintenance.'.$table), $rows, $qb->getSQL(), $qb->getParameters()); } } diff --git a/app/bundles/PageBundle/EventListener/PageSubscriber.php b/app/bundles/PageBundle/EventListener/PageSubscriber.php index 5d7d8f22eeb..570f648c5a5 100644 --- a/app/bundles/PageBundle/EventListener/PageSubscriber.php +++ b/app/bundles/PageBundle/EventListener/PageSubscriber.php @@ -188,13 +188,20 @@ public function onPageHit(QueueConsumerEvent $event) $trackingNewlyGenerated = $payload['isNew']; $pageId = $payload['pageId']; $leadId = $payload['leadId']; + $isRedirect = !empty($payload['isRedirect']); $hitRepo = $this->em->getRepository('MauticPageBundle:Hit'); $pageRepo = $this->em->getRepository('MauticPageBundle:Page'); + $redirectRepo = $this->em->getRepository('MauticPageBundle:Redirect'); $leadRepo = $this->em->getRepository('MauticLeadBundle:Lead'); $hit = $hitRepo->find((int) $payload['hitId']); - $page = $pageId ? $pageRepo->find((int) $pageId) : null; $lead = $leadId ? $leadRepo->find((int) $leadId) : null; + if ($isRedirect) { + $page = $pageId ? $redirectRepo->find((int) $pageId) : null; + } else { + $page = $pageId ? $pageRepo->find((int) $pageId) : null; + } + $this->pageModel->processPageHit($hit, $page, $request, $lead, $trackingNewlyGenerated, false); $event->setResult(QueueConsumerResults::ACKNOWLEDGE); } diff --git a/app/bundles/PageBundle/Form/Type/ConfigTrackingPageType.php b/app/bundles/PageBundle/Form/Type/ConfigTrackingPageType.php index 3cb2a77bae9..21a0aa20d65 100644 --- a/app/bundles/PageBundle/Form/Type/ConfigTrackingPageType.php +++ b/app/bundles/PageBundle/Form/Type/ConfigTrackingPageType.php @@ -58,7 +58,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'yesno_button_group', [ 'label' => 'mautic.page.config.form.track_contact_by_ip', - 'data' => (bool) $options['data']['track_contact_by_ip'], + 'data' => isset($options['data']['track_contact_by_ip']) ? (bool) $options['data']['track_contact_by_ip'] : false, 'attr' => [ 'tooltip' => 'mautic.page.config.form.track_contact_by_ip.tooltip', 'data-show-on' => '{"config_trackingconfig_anonymize_ip_1":"checked"}', diff --git a/app/bundles/PageBundle/Model/PageModel.php b/app/bundles/PageBundle/Model/PageModel.php index 3db3a19142c..6e6e46b3271 100644 --- a/app/bundles/PageBundle/Model/PageModel.php +++ b/app/bundles/PageBundle/Model/PageModel.php @@ -533,11 +533,12 @@ public function hitPage($page, Request $request, $code = '200', Lead $lead = nul if ($this->queueService->isQueueEnabled()) { $msg = [ - 'hitId' => $hit->getId(), - 'pageId' => $page ? $page->getId() : null, - 'request' => $request, - 'leadId' => $lead ? $lead->getId() : null, - 'isNew' => $this->deviceTracker->wasDeviceChanged(), + 'hitId' => $hit->getId(), + 'pageId' => $page ? $page->getId() : null, + 'request' => $request, + 'leadId' => $lead ? $lead->getId() : null, + 'isNew' => $this->deviceTracker->wasDeviceChanged(), + 'isRedirect' => ($page instanceof Redirect), ]; $this->queueService->publishToQueue(QueueName::PAGE_HIT, $msg); } else { diff --git a/app/bundles/PointBundle/Entity/Point.php b/app/bundles/PointBundle/Entity/Point.php index 01e45c2f859..970abf3cd61 100644 --- a/app/bundles/PointBundle/Entity/Point.php +++ b/app/bundles/PointBundle/Entity/Point.php @@ -44,8 +44,10 @@ class Point extends FormEntity */ private $type; - /** @var bool */ - private $repeatable; + /** + * @var bool + */ + private $repeatable = false; /** * @var \DateTime diff --git a/app/bundles/PointBundle/Translations/en_US/messages.ini b/app/bundles/PointBundle/Translations/en_US/messages.ini index 35b0441fe00..da96630b171 100644 --- a/app/bundles/PointBundle/Translations/en_US/messages.ini +++ b/app/bundles/PointBundle/Translations/en_US/messages.ini @@ -7,7 +7,7 @@ mautic.point.event.gained="Point gained" mautic.point.form.addaction="Use the list to the right to add an action." mautic.point.form.confirmbatchdelete="Delete the selected point actions?" mautic.point.form.confirmdelete="Delete the point action, %name%?" -mautic.point.form.repeat="Allow repeat the action" +mautic.point.form.repeat="Is repeatable" mautic.point.form.type="When a contact..." mautic.point.menu.edit="Edit Point Action" mautic.point.menu.index="Manage Actions" diff --git a/app/bundles/PointBundle/Views/Point/list.html.php b/app/bundles/PointBundle/Views/Point/list.html.php index 80b173f4123..3dd2870c7fe 100644 --- a/app/bundles/PointBundle/Views/Point/list.html.php +++ b/app/bundles/PointBundle/Views/Point/list.html.php @@ -102,12 +102,16 @@ 'MauticCoreBundle:Helper:publishstatus_icon.html.php', ['item' => $item, 'model' => 'point'] ); ?> - + + + getName(); ?> + + getName(); ?> - +
    getDescription()): ?>
    diff --git a/app/bundles/ReportBundle/Entity/Report.php b/app/bundles/ReportBundle/Entity/Report.php index 53ee42e9519..5def6fa46f3 100644 --- a/app/bundles/ReportBundle/Entity/Report.php +++ b/app/bundles/ReportBundle/Entity/Report.php @@ -133,7 +133,7 @@ public static function loadMetadata(ORM\ClassMetadata $metadata) $builder->addIdColumns(); - $builder->addField('system', Type::BOOLEAN); + $builder->addField('system', Type::BOOLEAN, ['columnName'=>'`system`']); $builder->addField('source', Type::STRING); @@ -364,6 +364,26 @@ public function getFilters() return $this->filters; } + /** + * Get filter value from a specific filter. + * + * @param string $column + * + * @return mixed + * + * @throws \UnexpectedValueException + */ + public function getFilterValue($column) + { + foreach ($this->getFilters() as $field) { + if ($column === $field['column']) { + return $field['value']; + } + } + + throw new \UnexpectedValueException("Column {$column} doesn't have any filter."); + } + /** * @return mixed */ diff --git a/app/bundles/ReportBundle/Event/ReportGeneratorEvent.php b/app/bundles/ReportBundle/Event/ReportGeneratorEvent.php index 6a0b0fa2a9a..9c1f08684b7 100644 --- a/app/bundles/ReportBundle/Event/ReportGeneratorEvent.php +++ b/app/bundles/ReportBundle/Event/ReportGeneratorEvent.php @@ -433,6 +433,20 @@ public function hasFilter($column) return isset($sorted[$column]); } + /** + * Get filter value from a specific filter. + * + * @param string $column + * + * @return mixed + * + * @throws \UnexpectedValueException + */ + public function getFilterValue($column) + { + return $this->getReport()->getFilterValue($column); + } + /** * Check if the report has a groupBy columns selected. * diff --git a/app/bundles/ReportBundle/Form/Type/TableOrderType.php b/app/bundles/ReportBundle/Form/Type/TableOrderType.php index ccf4f0e9729..a762a918fa4 100644 --- a/app/bundles/ReportBundle/Form/Type/TableOrderType.php +++ b/app/bundles/ReportBundle/Form/Type/TableOrderType.php @@ -48,7 +48,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'empty_value' => false, 'required' => false, 'attr' => [ - 'class' => 'form-control filter-columns', + 'class' => 'form-control', ], ]); diff --git a/app/bundles/ReportBundle/Model/ReportExporter.php b/app/bundles/ReportBundle/Model/ReportExporter.php index cb06b98105c..d2daa25869c 100644 --- a/app/bundles/ReportBundle/Model/ReportExporter.php +++ b/app/bundles/ReportBundle/Model/ReportExporter.php @@ -125,8 +125,6 @@ private function processReport(Scheduler $scheduler) $event = new ReportScheduleSendEvent($scheduler, $file); $this->eventDispatcher->dispatch(ReportEvents::REPORT_SCHEDULE_SEND, $event); - $this->reportFileWriter->clear($scheduler); - $this->schedulerModel->reportWasScheduled($report); } } diff --git a/app/bundles/ReportBundle/Model/ReportModel.php b/app/bundles/ReportBundle/Model/ReportModel.php index d33e84391c2..4097ad0f09d 100644 --- a/app/bundles/ReportBundle/Model/ReportModel.php +++ b/app/bundles/ReportBundle/Model/ReportModel.php @@ -11,6 +11,7 @@ namespace Mautic\ReportBundle\Model; +use Doctrine\DBAL\Connections\MasterSlaveConnection; use Doctrine\DBAL\Query\QueryBuilder; use Mautic\ChannelBundle\Helper\ChannelListHelper; use Mautic\CoreBundle\Helper\Chart\ChartQuery; @@ -546,7 +547,7 @@ public function getReportData(Report $entity, FormFactoryInterface $formFactory $paginate = !empty($options['paginate']); $reportPage = isset($options['reportPage']) ? $options['reportPage'] : 1; $data = $graphs = []; - $reportGenerator = new ReportGenerator($this->dispatcher, $this->em->getConnection(), $entity, $this->channelListHelper, $formFactory); + $reportGenerator = new ReportGenerator($this->dispatcher, $this->getConnection(), $entity, $this->channelListHelper, $formFactory); $selectedColumns = $entity->getColumns(); $totalResults = $limit = 0; @@ -772,4 +773,16 @@ private function getTotalCount(QueryBuilder $qb, array &$debugData) return (int) $countQb->execute()->fetchColumn(); } + + /** + * @return \Doctrine\DBAL\Connection + */ + private function getConnection() + { + if ($this->em->getConnection() instanceof MasterSlaveConnection) { + $this->em->getConnection()->connect('slave'); + } + + return $this->em->getConnection(); + } } diff --git a/app/bundles/ReportBundle/Scheduler/Model/SendSchedule.php b/app/bundles/ReportBundle/Scheduler/Model/SendSchedule.php index 9465ca6f516..9c45a9ca2e8 100644 --- a/app/bundles/ReportBundle/Scheduler/Model/SendSchedule.php +++ b/app/bundles/ReportBundle/Scheduler/Model/SendSchedule.php @@ -39,6 +39,8 @@ public function __construct(MailHelper $mailer, MessageSchedule $messageSchedule */ public function send(Scheduler $scheduler, $filePath) { + $this->mailer->reset(true); + $transformer = new ArrayStringTransformer(); $report = $scheduler->getReport(); $emails = $transformer->reverseTransform($report->getToAddress()); diff --git a/app/bundles/ReportBundle/Tests/Entity/ReportTest.php b/app/bundles/ReportBundle/Tests/Entity/ReportTest.php index 72671d74d9f..8230adf470c 100644 --- a/app/bundles/ReportBundle/Tests/Entity/ReportTest.php +++ b/app/bundles/ReportBundle/Tests/Entity/ReportTest.php @@ -86,6 +86,41 @@ public function testInvalidWeeklyScheduled() $report->ensureIsWeeklyScheduled(); } + public function testGetFilterValueIfFIltersAreEmpty() + { + $report = $this->getInvalidReport(); + + $this->expectException(\UnexpectedValueException::class); + $this->assertSame('1234', $report->getFilterValue('e.test')); + } + + public function testGetFilterValueIfExists() + { + $report = $this->getInvalidReport(); + $report->setFilters([ + [ + 'column' => 'e.test', + 'value' => '1234', + ], + ]); + + $this->assertSame('1234', $report->getFilterValue('e.test')); + } + + public function testGetFilterValueIfDoesNotExist() + { + $report = $this->getInvalidReport(); + $report->setFilters([ + [ + 'column' => 'e.test', + 'value' => '1234', + ], + ]); + + $this->expectException(\UnexpectedValueException::class); + $report->getFilterValue('I need coffee'); + } + /** * @return Report */ diff --git a/app/bundles/ReportBundle/Tests/Model/ReportExporterTest.php b/app/bundles/ReportBundle/Tests/Model/ReportExporterTest.php index 51d4004241f..afc822201c3 100644 --- a/app/bundles/ReportBundle/Tests/Model/ReportExporterTest.php +++ b/app/bundles/ReportBundle/Tests/Model/ReportExporterTest.php @@ -93,9 +93,6 @@ public function testProcessExport() $eventDispatcher->expects($this->exactly(2)) ->method('dispatch'); - $reportFileWriter->expects($this->exactly(2)) - ->method('clear'); - $schedulerModel->expects($this->exactly(2)) ->method('reportWasScheduled'); diff --git a/app/bundles/ReportBundle/Views/Report/details_data.html.php b/app/bundles/ReportBundle/Views/Report/details_data.html.php index 29a7c942ad6..bd54756ca14 100644 --- a/app/bundles/ReportBundle/Views/Report/details_data.html.php +++ b/app/bundles/ReportBundle/Views/Report/details_data.html.php @@ -213,14 +213,18 @@ function getTotal($a, $f, $t, $allrows, $ac) mQuery('.datetimepicker').datetimepicker({ format:'Y-m-d H:i:s', closeOnDateSelect: true, - validateOnBlur: false + validateOnBlur: false, + scrollMonth: false, + scrollInput: false }); }); mQuery(document).ready(function() { mQuery('.datepicker').datetimepicker({ format:'Y-m-d', closeOnDateSelect: true, - validateOnBlur: false + validateOnBlur: false, + scrollMonth: false, + scrollInput: false }); }); diff --git a/app/bundles/StageBundle/Views/Stage/list.html.php b/app/bundles/StageBundle/Views/Stage/list.html.php index b1c1d81427e..8432cc0499b 100644 --- a/app/bundles/StageBundle/Views/Stage/list.html.php +++ b/app/bundles/StageBundle/Views/Stage/list.html.php @@ -90,12 +90,16 @@ 'MauticCoreBundle:Helper:publishstatus_icon.html.php', ['item' => $item, 'model' => 'stage'] ); ?> + getName(); ?> + + getName(); ?> +
    getDescription()): ?>
    diff --git a/app/bundles/WebhookBundle/Controller/AjaxController.php b/app/bundles/WebhookBundle/Controller/AjaxController.php index 8b7f9953b00..f8c21ffa03b 100644 --- a/app/bundles/WebhookBundle/Controller/AjaxController.php +++ b/app/bundles/WebhookBundle/Controller/AjaxController.php @@ -62,8 +62,8 @@ protected function sendHookTestAction(Request $request) .'
    ', ]; - // if we get a 200 response convert to success message - if ($response->code == 200) { + // if we get a 2xx response convert to success message + if (substr($response->code, 0, 1) == 2) { $dataArray['html'] = '
    ' .$this->translator->trans('mautic.webhook.label.success') diff --git a/app/migrations/Version20181017154600.php b/app/migrations/Version20181017154600.php new file mode 100644 index 00000000000..f60a3bae322 --- /dev/null +++ b/app/migrations/Version20181017154600.php @@ -0,0 +1,75 @@ + Córdoba). + */ +class Version20181017154600 extends AbstractMauticMigration +{ + /** + * @param Schema $schema + */ + public function up(Schema $schema) + { + $oldRegionName = 'Cordóba'; + $newRegionName = 'Córdoba'; + + // Fix region name for leads. + $queryBuilder = $this->connection->createQueryBuilder(); + $queryBuilder->update("{$this->prefix}leads") + ->set('state', ':newRegionName') + ->where( + $queryBuilder->expr()->eq('state', $queryBuilder->expr()->literal($oldRegionName)) + ) + ->setParameter('newRegionName', $newRegionName, 'string') + ->execute(); + + // Fix region name for companies. + $queryBuilder = $this->connection->createQueryBuilder(); + $queryBuilder->update("{$this->prefix}companies") + ->set('companystate', ':newRegionName') + ->where( + $queryBuilder->expr()->eq('companystate', $queryBuilder->expr()->literal($oldRegionName)) + ) + ->setParameter('newRegionName', $newRegionName, 'string') + ->execute(); + + // Fix region name for page hits. + $queryBuilder = $this->connection->createQueryBuilder(); + $queryBuilder->update("{$this->prefix}page_hits") + ->set('region', ':newRegionName') + ->where( + $queryBuilder->expr()->eq('region', $queryBuilder->expr()->literal($oldRegionName)) + ) + ->setParameter('newRegionName', $newRegionName, 'string') + ->execute(); + + // Fix region name for video hits. + $queryBuilder = $this->connection->createQueryBuilder(); + $queryBuilder->update("{$this->prefix}video_hits") + ->set('region', ':newRegionName') + ->where( + $queryBuilder->expr()->eq('region', $queryBuilder->expr()->literal($oldRegionName)) + ) + ->setParameter('newRegionName', $newRegionName, 'string') + ->execute(); + + // Fix region name for 'filters' field of lead_lists, dynamic_content & reports. + // As the old string & the new one are of the same length, a general 'REPLACE' is OK even on (DC2Type:array). + $this->addSql("UPDATE `{$this->prefix}lead_lists` SET filters = REPLACE(filters, '$oldRegionName', '$newRegionName') WHERE filters LIKE '%$oldRegionName%'"); + $this->addSql("UPDATE `{$this->prefix}dynamic_content` SET filters = REPLACE(filters, '$oldRegionName', '$newRegionName') WHERE filters LIKE '%$oldRegionName%'"); + $this->addSql("UPDATE `{$this->prefix}reports` SET filters = REPLACE(filters, '$oldRegionName', '$newRegionName') WHERE filters LIKE '%$oldRegionName%'"); + } +} diff --git a/app/version.txt b/app/version.txt index 22f73a03a7a..8c8363ecc82 100644 --- a/app/version.txt +++ b/app/version.txt @@ -1 +1 @@ -2.15.1-dev +2.15.2-dev diff --git a/composer.json b/composer.json index 984a81dddfb..4fc32a72591 100644 --- a/composer.json +++ b/composer.json @@ -134,6 +134,10 @@ { "type": "git", "url": "https://github.com/mautic/BazingaOAuthServerBundle.git" + }, + { + "type": "git", + "url": "https://github.com/mautic/intl.git" } ], "scripts": { diff --git a/composer.lock b/composer.lock index 1f0abb2bb3b..dd983752b7a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d865a55c8f4f76edea7f41ffb0d5223f", + "content-hash": "76ca1f3e88950d50351be721720c3e0a", "packages": [ { "name": "aws/aws-sdk-php", @@ -827,7 +827,7 @@ "cache", "caching" ], - "time": "2017-09-29T14:39:10+00:00" + "time": "2017-10-12T17:23:29+00:00" }, { "name": "doctrine/doctrine-fixtures-bundle", @@ -2731,7 +2731,7 @@ "homepage": "http://github.com/knplabs/Gaufrette/contributors" }, { - "name": "KNPLabs Team", + "name": "KnpLabs Team", "homepage": "http://knplabs.com" } ], @@ -3145,7 +3145,7 @@ "geolocation", "maxmind" ], - "time": "2017-01-19T23:49:38+00:00" + "time": "2017-10-27T19:15:33+00:00" }, { "name": "maxmind/web-service-common", @@ -6541,17 +6541,11 @@ }, { "name": "symfony/intl", - "version": "v2.8.34", + "version": "v2.8.51", "source": { "type": "git", - "url": "https://github.com/symfony/intl.git", - "reference": "847da8b0460d6119e571982037093df43aed9b21" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/847da8b0460d6119e571982037093df43aed9b21", - "reference": "847da8b0460d6119e571982037093df43aed9b21", - "shasum": "" + "url": "https://github.com/mautic/intl.git", + "reference": "236c4eb3d12813f766d6b52d23b5677bf0a9840f" }, "require": { "php": ">=5.3.9", @@ -6581,7 +6575,6 @@ "/Tests/" ] }, - "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -6613,7 +6606,7 @@ "l10n", "localization" ], - "time": "2018-01-03T07:36:31+00:00" + "time": "2019-04-25T07:50:25+00:00" }, { "name": "symfony/monolog-bridge", @@ -7755,7 +7748,7 @@ ], "description": "Symfony SwiftmailerBundle", "homepage": "http://symfony.com", - "time": "2017-07-22T07:18:13+00:00" + "time": "2017-10-19T01:06:41+00:00" }, { "name": "symfony/templating", @@ -9009,7 +9002,7 @@ "object", "object graph" ], - "time": "2017-04-12T18:52:22+00:00" + "time": "2017-10-19T19:58:43+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -9273,6 +9266,7 @@ "testing", "xunit" ], + "abandoned": true, "time": "2016-12-02T14:39:14+00:00" }, { @@ -9663,6 +9657,7 @@ "mock", "xunit" ], + "abandoned": true, "time": "2017-06-30T09:13:00+00:00" }, { diff --git a/media/js/app.js b/media/js/app.js index b6fb7d9ed8b..36f6fbcb085 100644 --- a/media/js/app.js +++ b/media/js/app.js @@ -23,7 +23,7 @@ container.appendTo('body');}},deactivateBackgroup:function(){if(mQuery('#mautic- Mautic.activeActions[action]=true;Mautic.dismissConfirmation();if(action.indexOf('batchExport')>=0){Mautic.initiateFileDownload(action);return;} mQuery.ajax({showLoadingBar:true,url:action,type:"POST",dataType:"json",success:function(response){Mautic.processPageContent(response);if(typeof callback=='function'){callback(response);}},error:function(request,textStatus,errorThrown){Mautic.processAjaxError(request,textStatus,errorThrown);},complete:function(){delete Mautic.activeActions[action]}});},processAjaxError:function(request,textStatus,errorThrown,mainContent){if(textStatus=='abort'){Mautic.stopPageLoadingBar();Mautic.stopCanvasLoadingBar();Mautic.stopIconSpinPostEvent();return;} var inDevMode=typeof mauticEnv!=='undefined'&&mauticEnv=='dev';if(inDevMode){console.log(request);} -if(typeof request.responseJSON!=='undefined'){response=request.responseJSON;}else{var errorStart=request.responseText.indexOf('{"newContent');var jsonString=request.responseText.slice(errorStart);if(jsonString){try{var response=mQuery.parseJSON(jsonString);if(inDevMode){console.log(response);}}catch(err){if(inDevMode){console.log(err);}}}else{response={};}} +if(typeof request.responseJSON!=='undefined'){response=request.responseJSON;}else if(typeof(request.responseText)!=='undefined'){var errorStart=request.responseText.indexOf('{"newContent');var jsonString=request.responseText.slice(errorStart);if(jsonString){try{var response=mQuery.parseJSON(jsonString);if(inDevMode){console.log(response);}}catch(err){if(inDevMode){console.log(err);}}}else{response={};}} if(response){if(response.newContent&&mainContent){mQuery('#app-content .content-body').html(response.newContent);if(response.route&&response.route.indexOf("ajax")==-1){MauticVars.manualStateChange=false;History.pushState(null,"Mautic",response.route);}}else if(response.newContent&&mQuery('.modal.in').length){mQuery('.modal.in .modal-body-content').html(response.newContent);mQuery('.modal.in .modal-body-content').removeClass('hide');if(mQuery('.modal.in .loading-placeholder').length){mQuery('.modal.in .loading-placeholder').addClass('hide');}}else if(inDevMode){console.log(response);if(response.errors&&response.errors[0]&&response.errors[0].message){alert(response.errors[0].message);}}} Mautic.stopPageLoadingBar();Mautic.stopCanvasLoadingBar();Mautic.stopIconSpinPostEvent();},setModeratedInterval:function(key,callback,timeout,params){if(typeof MauticVars.intervalsInProgress[key]!='undefined'){clearTimeout(MauticVars.moderatedIntervals[key]);}else{MauticVars.intervalsInProgress[key]=true;if(typeof params=='undefined'){params=[];} if(typeof callback=='function'){callback(params);}else{window["Mautic"][callback].apply('window',params);}} @@ -57,7 +57,7 @@ mQuery('body').animate({scrollTop:0},0);}else{var overflow=mQuery(response.targe if(response.overlayEnabled){mQuery(response.overlayTarget+' .content-overlay').remove();} Mautic.onPageLoad(response.target,response);}};Mautic.onPageLoad=function(container,response,inModal){Mautic.initDateRangePicker(container+' #daterange_date_from',container+' #daterange_date_to');Mautic.makeLinksAlive(mQuery(container+" a[data-toggle='ajax']"));mQuery(container+" form[data-toggle='ajax']").each(function(index){Mautic.ajaxifyForm(mQuery(this).attr('name'));});Mautic.makeModalsAlive(mQuery(container+" *[data-toggle='ajaxmodal']")) Mautic.activateModalEmbeddedForms(container);mQuery(container+" *[data-toggle='livesearch']").each(function(index){Mautic.activateLiveSearch(mQuery(this),"lastSearchStr","liveCache");});mQuery(container+" *[data-toggle='listfilter']").each(function(index){Mautic.activateListFilterSelect(mQuery(this));});var pageTooltips=mQuery(container+" *[data-toggle='tooltip']");pageTooltips.tooltip({html:true,container:'body'});pageTooltips.each(function(i){var thisTooltip=mQuery(pageTooltips.get(i));var elementParent=thisTooltip.parent();if(elementParent.get(0).tagName==='LABEL'){elementParent.append('');elementParent.hover(function(){thisTooltip.tooltip('show')},function(){thisTooltip.tooltip('hide');});}});mQuery(container+" *[data-toggle='sortablelist']").each(function(index){Mautic.activateSortable(this);});mQuery(container+" div.sortable-panels").each(function(){Mautic.activateSortablePanels(this);});mQuery(container+" a[data-toggle='download']").off('click.download');mQuery(container+" a[data-toggle='download']").on('click.download',function(event){event.preventDefault();Mautic.initiateFileDownload(mQuery(this).attr('href'));});Mautic.makeConfirmationsAlive(mQuery(container+" a[data-toggle='confirmation']"));mQuery(container+" *[data-toggle='datetime']").each(function(){Mautic.activateDateTimeInputs(this,'datetime');});mQuery(container+" *[data-toggle='date']").each(function(){Mautic.activateDateTimeInputs(this,'date');});mQuery(container+" *[data-toggle='time']").each(function(){Mautic.activateDateTimeInputs(this,'time');});mQuery(container+" *[data-onload-callback]").each(function(){var callback=function(el){if(typeof window["Mautic"][mQuery(el).attr('data-onload-callback')]=='function'){window["Mautic"][mQuery(el).attr('data-onload-callback')].apply('window',[el]);}} -mQuery(document).ready(callback(this));});mQuery(container+" input[data-toggle='color']").each(function(){Mautic.activateColorPicker(this);});mQuery(container+" select").not('.multiselect, .not-chosen').each(function(){Mautic.activateChosenSelect(this);});mQuery(container+" select.multiselect").each(function(){Mautic.activateMultiSelect(this);});mQuery(container+" *[data-toggle='field-lookup']").each(function(index){var target=mQuery(this).attr('data-target');var options=mQuery(this).attr('data-options');var field=mQuery(this).attr('id');var action=mQuery(this).attr('data-action');Mautic.activateFieldTypeahead(field,target,options,action);});mQuery(container+" .table-responsive").on('shown.bs.dropdown',function(e){var table=mQuery(this),menu=mQuery(e.target).find(".dropdown-menu"),tableOffsetHeight=table.offset().top+table.height(),menuOffsetHeight=menu.offset().top+menu.outerHeight(true);if(menuOffsetHeight>tableOffsetHeight) +mQuery(document).ready(callback(this));});mQuery(container+" input[data-toggle='color']").each(function(){Mautic.activateColorPicker(this);});mQuery(container+" select").not('.multiselect, .not-chosen').each(function(){Mautic.activateChosenSelect(this);});mQuery(container+" select.multiselect").each(function(){Mautic.activateMultiSelect(this);});Mautic.activateLookupTypeahead(mQuery(container));mQuery(container+" .table-responsive").on('shown.bs.dropdown',function(e){var table=mQuery(this),menu=mQuery(e.target).find(".dropdown-menu"),tableOffsetHeight=table.offset().top+table.height(),menuOffsetHeight=menu.offset().top+menu.outerHeight(true);if(menuOffsetHeight>tableOffsetHeight) table.css("padding-bottom",menuOffsetHeight-tableOffsetHeight+16)});mQuery(container+" .table-responsive").on("hide.bs.dropdown",function(){mQuery(this).css("padding-bottom",0);}) mQuery(container+" .nav-tabs[data-toggle='tab-hash']").each(function(){var hash=document.location.hash;var prefix='tab-';if(hash){var hashPieces=hash.split('?');hash=hashPieces[0].replace("#","#"+prefix);var activeTab=mQuery(this).find('a[href='+hash+']').first();if(mQuery(activeTab).length){mQuery('.nav-tabs li').removeClass('active');mQuery('.tab-pane').removeClass('in active');mQuery(activeTab).parent().addClass('active');mQuery(hash).addClass('in active');}} mQuery(this).find('a').on('shown.bs.tab',function(e){window.location.hash=e.target.hash.replace("#"+prefix,"#");});});mQuery(container+" .nav-overflow-tabs ul").each(function(){Mautic.activateOverflowTabs(this);});mQuery(container+" .nav.sortable").each(function(){Mautic.activateSortableTabs(this);});Mautic.activateTabDeleteButtons(container);mQuery(container+' .btn:not(.btn-nospin)').on('click.spinningicons',function(event){Mautic.startIconSpinOnEvent(event);});mQuery(container+' input[class=list-checkbox]').on('change',function(){var disabled=Mautic.batchActionPrecheck(container)?false:true;var color=(disabled)?'btn-default':'btn-info';var button=container+' th.col-actions .input-group-btn button';mQuery(button).prop('disabled',disabled);mQuery(button).removeClass('btn-default btn-info').addClass(color);});mQuery(container+" .bottom-form-buttons").each(function(){if(inModal||mQuery(this).closest('.modal').length){var modal=(inModal)?container:mQuery(this).closest('.modal');if(mQuery(modal).find('.modal-form-buttons').length){mQuery(modal).find('.bottom-form-buttons').addClass('hide');var buttons=mQuery(modal).find('.bottom-form-buttons').html();mQuery(modal).find('.modal-form-buttons').html('');mQuery(buttons).filter("button").each(function(i,v){var id=mQuery(this).attr('id');var button=mQuery("'+'
    '+'
    '+'
    '),calendar=$('
    '),timepicker=$('
    '),timeboxparent=timepicker.find('.xdsoft_time_box').eq(0),timebox=$('
    '),applyButton=$(''),monthselect=$('
    '),yearselect=$('
    '),triggerAfterOpen=false,XDSoft_datetime,xchangeTimer,timerclick,current_time_index,setPos,timer=0,timer1=0,_xdsoft_datetime;if(options.id){datetimepicker.attr('id',options.id);} +input.off('open.xdsoft focusin.xdsoft mousedown.xdsoft touchstart',initOnActionCallback).trigger('open.xdsoft');},100);});};createDateTimePicker=function(input){var datetimepicker=$('
    '),xdsoft_copyright=$(''),datepicker=$('
    '),month_picker=$('
    '+'
    '+'
    '+'
    '),calendar=$('
    '),timepicker=$('
    '),timeboxparent=timepicker.find('.xdsoft_time_box').eq(0),timebox=$('
    '),applyButton=$(''),monthselect=$('
    '),yearselect=$('
    '),triggerAfterOpen=false,XDSoft_datetime,xchangeTimer,timerclick,current_time_index,setPos,timer=0,_xdsoft_datetime,forEachAncestorOf;if(options.id){datetimepicker.attr('id',options.id);} if(options.style){datetimepicker.attr('style',options.style);} if(options.weeks){datetimepicker.addClass('xdsoft_showweeks');} if(options.rtl){datetimepicker.addClass('xdsoft_rtl');} -datetimepicker.addClass('xdsoft_'+options.theme);datetimepicker.addClass(options.className);mounth_picker.find('.xdsoft_month span').after(monthselect);mounth_picker.find('.xdsoft_year span').after(yearselect);mounth_picker.find('.xdsoft_month,.xdsoft_year').on('mousedown.xdsoft',function(event){var select=$(this).find('.xdsoft_select').eq(0),val=0,top=0,visible=select.is(':visible'),items,i;mounth_picker.find('.xdsoft_select').hide();if(_xdsoft_datetime.currentTime){val=_xdsoft_datetime.currentTime[$(this).hasClass('xdsoft_month')?'getMonth':'getFullYear']();} +datetimepicker.addClass('xdsoft_'+options.theme);datetimepicker.addClass(options.className);month_picker.find('.xdsoft_month span').after(monthselect);month_picker.find('.xdsoft_year span').after(yearselect);month_picker.find('.xdsoft_month,.xdsoft_year').on('touchstart mousedown.xdsoft',function(event){var select=$(this).find('.xdsoft_select').eq(0),val=0,top=0,visible=select.is(':visible'),items,i;month_picker.find('.xdsoft_select').hide();if(_xdsoft_datetime.currentTime){val=_xdsoft_datetime.currentTime[$(this).hasClass('xdsoft_month')?'getMonth':'getFullYear']();} select[visible?'hide':'show']();for(items=select.find('div.xdsoft_option'),i=0;ioptions.touchMovedThreshold){this.touchMoved=true;}} +month_picker.find('.xdsoft_select').xdsoftScroller(options).on('touchstart mousedown.xdsoft',function(event){var evt=event.originalEvent;this.touchMoved=false;this.touchStartPosition=evt.touches?evt.touches[0]:evt;event.stopPropagation();event.preventDefault();}).on('touchmove','.xdsoft_option',handleTouchMoved).on('touchend mousedown.xdsoft','.xdsoft_option',function(){if(!this.touchMoved){if(_xdsoft_datetime.currentTime===undefined||_xdsoft_datetime.currentTime===null){_xdsoft_datetime.currentTime=_xdsoft_datetime.now();} var year=_xdsoft_datetime.currentTime.getFullYear();if(_xdsoft_datetime&&_xdsoft_datetime.currentTime){_xdsoft_datetime.currentTime[$(this).parent().parent().hasClass('xdsoft_monthselect')?'setMonth':'setFullYear']($(this).data('value'));} $(this).parent().parent().hide();datetimepicker.trigger('xchange.xdsoft');if(options.onChangeMonth&&$.isFunction(options.onChangeMonth)){options.onChangeMonth.call(datetimepicker,_xdsoft_datetime.currentTime,datetimepicker.data('input'));} -if(year!==_xdsoft_datetime.currentTime.getFullYear()&&$.isFunction(options.onChangeYear)){options.onChangeYear.call(datetimepicker,_xdsoft_datetime.currentTime,datetimepicker.data('input'));}});datetimepicker.setOptions=function(_options){var highlightedDates={},getCaretPos=function(input){try{if(document.selection&&document.selection.createRange){var range=document.selection.createRange();return range.getBookmark().charCodeAt(2)-2;} -if(input.setSelectionRange){return input.selectionStart;}}catch(e){return 0;}},setCaretPos=function(node,pos){node=(typeof node==="string"||node instanceof String)?document.getElementById(node):node;if(!node){return false;} -if(node.createTextRange){var textRange=node.createTextRange();textRange.collapse(true);textRange.moveEnd('character',pos);textRange.moveStart('character',pos);textRange.select();return true;} -if(node.setSelectionRange){node.setSelectionRange(pos,pos);return true;} -return false;},isValidValue=function(mask,value){var reg=mask.replace(/([\[\]\/\{\}\(\)\-\.\+]{1})/g,'\\$1').replace(/_/g,'{digit+}').replace(/([0-9]{1})/g,'{digit$1}').replace(/\{digit([0-9]{1})\}/g,'[0-$1_]{1}').replace(/\{digit[\+]\}/g,'[0-9_]{1}');return(new RegExp(reg)).test(value);};options=$.extend(true,{},options,_options);if(_options.allowTimes&&$.isArray(_options.allowTimes)&&_options.allowTimes.length){options.allowTimes=$.extend(true,[],_options.allowTimes);} +if(year!==_xdsoft_datetime.currentTime.getFullYear()&&$.isFunction(options.onChangeYear)){options.onChangeYear.call(datetimepicker,_xdsoft_datetime.currentTime,datetimepicker.data('input'));}}});datetimepicker.getValue=function(){return _xdsoft_datetime.getCurrentTime();};datetimepicker.setOptions=function(_options){var highlightedDates={};options=$.extend(true,{},options,_options);if(_options.allowTimes&&$.isArray(_options.allowTimes)&&_options.allowTimes.length){options.allowTimes=$.extend(true,[],_options.allowTimes);} if(_options.weekends&&$.isArray(_options.weekends)&&_options.weekends.length){options.weekends=$.extend(true,[],_options.weekends);} -if(_options.highlightedDates&&$.isArray(_options.highlightedDates)&&_options.highlightedDates.length){$.each(_options.highlightedDates,function(index,value){var splitData=$.map(value.split(','),$.trim),exDesc,hDate=new HighlightedDate(Date.parseDate(splitData[0],options.formatDate),splitData[1],splitData[2]),keyDate=hDate.date.dateFormat(options.formatDate);if(highlightedDates[keyDate]!==undefined){exDesc=highlightedDates[keyDate].desc;if(exDesc&&exDesc.length&&hDate.desc&&hDate.desc.length){highlightedDates[keyDate].desc=exDesc+"\n"+hDate.desc;}}else{highlightedDates[keyDate]=hDate;}});options.highlightedDates=$.extend(true,[],highlightedDates);} +if(_options.allowDates&&$.isArray(_options.allowDates)&&_options.allowDates.length){options.allowDates=$.extend(true,[],_options.allowDates);} +if(_options.allowDateRe&&Object.prototype.toString.call(_options.allowDateRe)==="[object String]"){options.allowDateRe=new RegExp(_options.allowDateRe);} +if(_options.highlightedDates&&$.isArray(_options.highlightedDates)&&_options.highlightedDates.length){$.each(_options.highlightedDates,function(index,value){var splitData=$.map(value.split(','),$.trim),exDesc,hDate=new HighlightedDate(dateHelper.parseDate(splitData[0],options.formatDate),splitData[1],splitData[2]),keyDate=dateHelper.formatDate(hDate.date,options.formatDate);if(highlightedDates[keyDate]!==undefined){exDesc=highlightedDates[keyDate].desc;if(exDesc&&exDesc.length&&hDate.desc&&hDate.desc.length){highlightedDates[keyDate].desc=exDesc+"\n"+hDate.desc;}}else{highlightedDates[keyDate]=hDate;}});options.highlightedDates=$.extend(true,[],highlightedDates);} if(_options.highlightedPeriods&&$.isArray(_options.highlightedPeriods)&&_options.highlightedPeriods.length){highlightedDates=$.extend(true,[],options.highlightedDates);$.each(_options.highlightedPeriods,function(index,value){var dateTest,dateEnd,desc,hDate,keyDate,exDesc,style;if($.isArray(value)){dateTest=value[0];dateEnd=value[1];desc=value[2];style=value[3];} -else{var splitData=$.map(value.split(','),$.trim);dateTest=Date.parseDate(splitData[0],options.formatDate);dateEnd=Date.parseDate(splitData[1],options.formatDate);desc=splitData[2];style=splitData[3];} -while(dateTest<=dateEnd){hDate=new HighlightedDate(dateTest,desc,style);keyDate=dateTest.dateFormat(options.formatDate);dateTest.setDate(dateTest.getDate()+1);if(highlightedDates[keyDate]!==undefined){exDesc=highlightedDates[keyDate].desc;if(exDesc&&exDesc.length&&hDate.desc&&hDate.desc.length){highlightedDates[keyDate].desc=exDesc+"\n"+hDate.desc;}}else{highlightedDates[keyDate]=hDate;}}});options.highlightedDates=$.extend(true,[],highlightedDates);} +else{var splitData=$.map(value.split(','),$.trim);dateTest=dateHelper.parseDate(splitData[0],options.formatDate);dateEnd=dateHelper.parseDate(splitData[1],options.formatDate);desc=splitData[2];style=splitData[3];} +while(dateTest<=dateEnd){hDate=new HighlightedDate(dateTest,desc,style);keyDate=dateHelper.formatDate(dateTest,options.formatDate);dateTest.setDate(dateTest.getDate()+1);if(highlightedDates[keyDate]!==undefined){exDesc=highlightedDates[keyDate].desc;if(exDesc&&exDesc.length&&hDate.desc&&hDate.desc.length){highlightedDates[keyDate].desc=exDesc+"\n"+hDate.desc;}}else{highlightedDates[keyDate]=hDate;}}});options.highlightedDates=$.extend(true,[],highlightedDates);} if(_options.disabledDates&&$.isArray(_options.disabledDates)&&_options.disabledDates.length){options.disabledDates=$.extend(true,[],_options.disabledDates);} if(_options.disabledWeekDays&&$.isArray(_options.disabledWeekDays)&&_options.disabledWeekDays.length){options.disabledWeekDays=$.extend(true,[],_options.disabledWeekDays);} if((options.open||options.opened)&&(!options.inline)){input.trigger('open.xdsoft');} @@ -4934,80 +4950,91 @@ if(options.datepicker){datepicker.addClass('active');}else{datepicker.removeClas if(options.timepicker){timepicker.addClass('active');}else{timepicker.removeClass('active');} if(options.value){_xdsoft_datetime.setCurrentTime(options.value);if(input&&input.val){input.val(_xdsoft_datetime.str);}} if(isNaN(options.dayOfWeekStart)){options.dayOfWeekStart=0;}else{options.dayOfWeekStart=parseInt(options.dayOfWeekStart,10)%7;} -if(!options.timepickerScrollbar){timeboxparent.xdsoftScroller('hide');} -if(options.minDate&&/^[\+\-](.*)$/.test(options.minDate)){options.minDate=_xdsoft_datetime.strToDateTime(options.minDate).dateFormat(options.formatDate);} -if(options.maxDate&&/^[\+\-](.*)$/.test(options.maxDate)){options.maxDate=_xdsoft_datetime.strToDateTime(options.maxDate).dateFormat(options.formatDate);} -applyButton.toggle(options.showApplyButton);mounth_picker.find('.xdsoft_today_button').css('visibility',!options.todayButton?'hidden':'visible');mounth_picker.find('.'+options.prev).css('visibility',!options.prevButton?'hidden':'visible');mounth_picker.find('.'+options.next).css('visibility',!options.nextButton?'hidden':'visible');if(options.mask){input.off('keydown.xdsoft');if(options.mask===true){options.mask=options.format.replace(/Y/g,'9999').replace(/F/g,'9999').replace(/m/g,'19').replace(/d/g,'39').replace(/H/g,'29').replace(/i/g,'59').replace(/s/g,'59');} -if($.type(options.mask)==='string'){if(!isValidValue(options.mask,input.val())){input.val(options.mask.replace(/[0-9]/g,'_'));} -input.on('keydown.xdsoft',function(event){var val=this.value,key=event.which,pos,digit;if(((key>=KEY0&&key<=KEY9)||(key>=_KEY0&&key<=_KEY9))||(key===BACKSPACE||key===DEL)){pos=getCaretPos(this);digit=(key!==BACKSPACE&&key!==DEL)?String.fromCharCode((_KEY0<=key&&key<=_KEY9)?key-KEY0:key):'_';if((key===BACKSPACE||key===DEL)&&pos){pos-=1;digit='_';} -while(/[^0-9_]/.test(options.mask.substr(pos,1))&&pos0){pos+=(key===BACKSPACE||key===DEL)?-1:1;} -val=val.substr(0,pos)+digit+val.substr(pos+1);if($.trim(val)===''){val=options.mask.replace(/[0-9]/g,'_');}else{if(pos===options.mask.length){event.preventDefault();return false;}} -pos+=(key===BACKSPACE||key===DEL)?0:1;while(/[^0-9_]/.test(options.mask.substr(pos,1))&&pos0){pos+=(key===BACKSPACE||key===DEL)?-1:1;} -if(isValidValue(options.mask,val)){this.value=val;setCaretPos(this,pos);}else if($.trim(val)===''){this.value=options.mask.replace(/[0-9]/g,'_');}else{input.trigger('error_input.xdsoft');}}else{if(([AKEY,CKEY,VKEY,ZKEY,YKEY].indexOf(key)!==-1&&ctrlDown)||[ESC,ARROWUP,ARROWDOWN,ARROWLEFT,ARROWRIGHT,F5,CTRLKEY,TAB,ENTER].indexOf(key)!==-1){return true;}} -event.preventDefault();return false;});}} -if(options.validateOnBlur){input.off('blur.xdsoft').on('blur.xdsoft',function(){if(options.allowBlank&&!$.trim($(this).val()).length){$(this).val(null);datetimepicker.data('xdsoft_datetime').empty();}else if(!Date.parseDate($(this).val(),options.format)){var splittedHours=+([$(this).val()[0],$(this).val()[1]].join('')),splittedMinutes=+([$(this).val()[2],$(this).val()[3]].join(''));if(!options.datepicker&&options.timepicker&&splittedHours>=0&&splittedHours<24&&splittedMinutes>=0&&splittedMinutes<60){$(this).val([splittedHours,splittedMinutes].map(function(item){return item>9?item:'0'+item;}).join(':'));}else{$(this).val((_xdsoft_datetime.now()).dateFormat(options.format));} -datetimepicker.data('xdsoft_datetime').setCurrentTime($(this).val());}else{datetimepicker.data('xdsoft_datetime').setCurrentTime($(this).val());} -datetimepicker.trigger('changedatetime.xdsoft');});} -options.dayOfWeekStartPrev=(options.dayOfWeekStart===0)?6:options.dayOfWeekStart-1;datetimepicker.trigger('xchange.xdsoft').trigger('afterOpen.xdsoft');};datetimepicker.data('options',options).on('mousedown.xdsoft',function(event){event.stopPropagation();event.preventDefault();yearselect.hide();monthselect.hide();return false;});timeboxparent.append(timebox);timeboxparent.xdsoftScroller();datetimepicker.on('afterOpen.xdsoft',function(){timeboxparent.xdsoftScroller();});datetimepicker.append(datepicker).append(timepicker);if(options.withoutCopyright!==true){datetimepicker.append(xdsoft_copyright);} -datepicker.append(mounth_picker).append(calendar).append(applyButton);$(options.parentID).append(datetimepicker);XDSoft_datetime=function(){var _this=this;_this.now=function(norecursion){var d=new Date(),date,time;if(!norecursion&&options.defaultDate){date=_this.strToDateTime(options.defaultDate);d.setFullYear(date.getFullYear());d.setMonth(date.getMonth());d.setDate(date.getDate());} -if(options.yearOffset){d.setFullYear(d.getFullYear()+options.yearOffset);} -if(!norecursion&&options.defaultTime){time=_this.strtotime(options.defaultTime);d.setHours(time.getHours());d.setMinutes(time.getMinutes());} +if(!options.timepickerScrollbar){timeboxparent.xdsoftScroller(options,'hide');} +if(options.minDate&&/^[\+\-](.*)$/.test(options.minDate)){options.minDate=dateHelper.formatDate(_xdsoft_datetime.strToDateTime(options.minDate),options.formatDate);} +if(options.maxDate&&/^[\+\-](.*)$/.test(options.maxDate)){options.maxDate=dateHelper.formatDate(_xdsoft_datetime.strToDateTime(options.maxDate),options.formatDate);} +if(options.minDateTime&&/^\+(.*)$/.test(options.minDateTime)){options.minDateTime=_xdsoft_datetime.strToDateTime(options.minDateTime).dateFormat(options.formatDate);} +if(options.maxDateTime&&/^\+(.*)$/.test(options.maxDateTime)){options.maxDateTime=_xdsoft_datetime.strToDateTime(options.maxDateTime).dateFormat(options.formatDate);} +applyButton.toggle(options.showApplyButton);month_picker.find('.xdsoft_today_button').css('visibility',!options.todayButton?'hidden':'visible');month_picker.find('.'+options.prev).css('visibility',!options.prevButton?'hidden':'visible');month_picker.find('.'+options.next).css('visibility',!options.nextButton?'hidden':'visible');setMask(options);if(options.validateOnBlur){input.off('blur.xdsoft').on('blur.xdsoft',function(){if(options.allowBlank&&(!$.trim($(this).val()).length||(typeof options.mask==="string"&&$.trim($(this).val())===options.mask.replace(/[0-9]/g,'_')))){$(this).val(null);datetimepicker.data('xdsoft_datetime').empty();}else{var d=dateHelper.parseDate($(this).val(),options.format);if(d){$(this).val(dateHelper.formatDate(d,options.format));}else{var splittedHours=+([$(this).val()[0],$(this).val()[1]].join('')),splittedMinutes=+([$(this).val()[2],$(this).val()[3]].join(''));if(!options.datepicker&&options.timepicker&&splittedHours>=0&&splittedHours<24&&splittedMinutes>=0&&splittedMinutes<60){$(this).val([splittedHours,splittedMinutes].map(function(item){return item>9?item:'0'+item;}).join(':'));}else{$(this).val(dateHelper.formatDate(_xdsoft_datetime.now(),options.format));}} +datetimepicker.data('xdsoft_datetime').setCurrentTime($(this).val());} +datetimepicker.trigger('changedatetime.xdsoft');datetimepicker.trigger('close.xdsoft');});} +options.dayOfWeekStartPrev=(options.dayOfWeekStart===0)?6:options.dayOfWeekStart-1;datetimepicker.trigger('xchange.xdsoft').trigger('afterOpen.xdsoft');};datetimepicker.data('options',options).on('touchstart mousedown.xdsoft',function(event){event.stopPropagation();event.preventDefault();yearselect.hide();monthselect.hide();return false;});timeboxparent.append(timebox);timeboxparent.xdsoftScroller(options);datetimepicker.on('afterOpen.xdsoft',function(){timeboxparent.xdsoftScroller(options);});datetimepicker.append(datepicker).append(timepicker);if(options.withoutCopyright!==true){datetimepicker.append(xdsoft_copyright);} +datepicker.append(month_picker).append(calendar).append(applyButton);$(options.parentID).append(datetimepicker);XDSoft_datetime=function(){var _this=this;_this.now=function(norecursion){var d=new Date(),date,time;if(!norecursion&&options.defaultDate){date=_this.strToDateTime(options.defaultDate);d.setFullYear(date.getFullYear());d.setMonth(date.getMonth());d.setDate(date.getDate());} +d.setFullYear(d.getFullYear());if(!norecursion&&options.defaultTime){time=_this.strtotime(options.defaultTime);d.setHours(time.getHours());d.setMinutes(time.getMinutes());d.setSeconds(time.getSeconds());d.setMilliseconds(time.getMilliseconds());} return d;};_this.isValidDate=function(d){if(Object.prototype.toString.call(d)!=="[object Date]"){return false;} -return!isNaN(d.getTime());};_this.setCurrentTime=function(dTime){_this.currentTime=(typeof dTime==='string')?_this.strToDateTime(dTime):_this.isValidDate(dTime)?dTime:_this.now();datetimepicker.trigger('xchange.xdsoft');};_this.empty=function(){_this.currentTime=null;};_this.getCurrentTime=function(dTime){return _this.currentTime;};_this.nextMonth=function(){if(_this.currentTime===undefined||_this.currentTime===null){_this.currentTime=_this.now();} +return!isNaN(d.getTime());};_this.setCurrentTime=function(dTime,requireValidDate){if(typeof dTime==='string'){_this.currentTime=_this.strToDateTime(dTime);} +else if(_this.isValidDate(dTime)){_this.currentTime=dTime;} +else if(!dTime&&!requireValidDate&&options.allowBlank&&!options.inline){_this.currentTime=null;} +else{_this.currentTime=_this.now();} +datetimepicker.trigger('xchange.xdsoft');};_this.empty=function(){_this.currentTime=null;};_this.getCurrentTime=function(){return _this.currentTime;};_this.nextMonth=function(){if(_this.currentTime===undefined||_this.currentTime===null){_this.currentTime=_this.now();} var month=_this.currentTime.getMonth()+1,year;if(month===12){_this.currentTime.setFullYear(_this.currentTime.getFullYear()+1);month=0;} year=_this.currentTime.getFullYear();_this.currentTime.setDate(Math.min(new Date(_this.currentTime.getFullYear(),month+1,0).getDate(),_this.currentTime.getDate()));_this.currentTime.setMonth(month);if(options.onChangeMonth&&$.isFunction(options.onChangeMonth)){options.onChangeMonth.call(datetimepicker,_xdsoft_datetime.currentTime,datetimepicker.data('input'));} if(year!==_this.currentTime.getFullYear()&&$.isFunction(options.onChangeYear)){options.onChangeYear.call(datetimepicker,_xdsoft_datetime.currentTime,datetimepicker.data('input'));} datetimepicker.trigger('xchange.xdsoft');return month;};_this.prevMonth=function(){if(_this.currentTime===undefined||_this.currentTime===null){_this.currentTime=_this.now();} var month=_this.currentTime.getMonth()-1;if(month===-1){_this.currentTime.setFullYear(_this.currentTime.getFullYear()-1);month=11;} _this.currentTime.setDate(Math.min(new Date(_this.currentTime.getFullYear(),month+1,0).getDate(),_this.currentTime.getDate()));_this.currentTime.setMonth(month);if(options.onChangeMonth&&$.isFunction(options.onChangeMonth)){options.onChangeMonth.call(datetimepicker,_xdsoft_datetime.currentTime,datetimepicker.data('input'));} -datetimepicker.trigger('xchange.xdsoft');return month;};_this.getWeekOfYear=function(datetime){var onejan=new Date(datetime.getFullYear(),0,1);return Math.ceil((((datetime-onejan)/86400000)+onejan.getDay()+1)/7);};_this.strToDateTime=function(sDateTime){var tmpDate=[],timeOffset,currentTime;if(sDateTime&&sDateTime instanceof Date&&_this.isValidDate(sDateTime)){return sDateTime;} -tmpDate=/^(\+|\-)(.*)$/.exec(sDateTime);if(tmpDate){tmpDate[2]=Date.parseDate(tmpDate[2],options.formatDate);} -if(tmpDate&&tmpDate[2]){timeOffset=tmpDate[2].getTime()-(tmpDate[2].getTimezoneOffset())*60000;currentTime=new Date((_this.now(true)).getTime()+parseInt(tmpDate[1]+'1',10)*timeOffset);}else{currentTime=sDateTime?Date.parseDate(sDateTime,options.format):_this.now();} +datetimepicker.trigger('xchange.xdsoft');return month;};_this.getWeekOfYear=function(datetime){if(options.onGetWeekOfYear&&$.isFunction(options.onGetWeekOfYear)){var week=options.onGetWeekOfYear.call(datetimepicker,datetime);if(typeof week!=='undefined'){return week;}} +var onejan=new Date(datetime.getFullYear(),0,1);if(onejan.getDay()!==4){onejan.setMonth(0,1+((4-onejan.getDay()+7)%7));} +return Math.ceil((((datetime-onejan)/86400000)+onejan.getDay()+1)/7);};_this.strToDateTime=function(sDateTime){var tmpDate=[],timeOffset,currentTime;if(sDateTime&&sDateTime instanceof Date&&_this.isValidDate(sDateTime)){return sDateTime;} +tmpDate=/^([+-]{1})(.*)$/.exec(sDateTime);if(tmpDate){tmpDate[2]=dateHelper.parseDate(tmpDate[2],options.formatDate);} +if(tmpDate&&tmpDate[2]){timeOffset=tmpDate[2].getTime()-(tmpDate[2].getTimezoneOffset())*60000;currentTime=new Date((_this.now(true)).getTime()+parseInt(tmpDate[1]+'1',10)*timeOffset);}else{currentTime=sDateTime?dateHelper.parseDate(sDateTime,options.format):_this.now();} if(!_this.isValidDate(currentTime)){currentTime=_this.now();} return currentTime;};_this.strToDate=function(sDate){if(sDate&&sDate instanceof Date&&_this.isValidDate(sDate)){return sDate;} -var currentTime=sDate?Date.parseDate(sDate,options.formatDate):_this.now(true);if(!_this.isValidDate(currentTime)){currentTime=_this.now(true);} +var currentTime=sDate?dateHelper.parseDate(sDate,options.formatDate):_this.now(true);if(!_this.isValidDate(currentTime)){currentTime=_this.now(true);} return currentTime;};_this.strtotime=function(sTime){if(sTime&&sTime instanceof Date&&_this.isValidDate(sTime)){return sTime;} -var currentTime=sTime?Date.parseDate(sTime,options.formatTime):_this.now(true);if(!_this.isValidDate(currentTime)){currentTime=_this.now(true);} -return currentTime;};_this.str=function(){return _this.currentTime.dateFormat(options.format);};_this.currentTime=this.now();};_xdsoft_datetime=new XDSoft_datetime();applyButton.on('click',function(e){e.preventDefault();datetimepicker.data('changed',true);_xdsoft_datetime.setCurrentTime(getCurrentValue());input.val(_xdsoft_datetime.str());datetimepicker.trigger('close.xdsoft');});mounth_picker.find('.xdsoft_today_button').on('mousedown.xdsoft',function(){datetimepicker.data('changed',true);_xdsoft_datetime.setCurrentTime(0);datetimepicker.trigger('afterOpen.xdsoft');}).on('dblclick.xdsoft',function(){var currentDate=_xdsoft_datetime.getCurrentTime(),minDate,maxDate;currentDate=new Date(currentDate.getFullYear(),currentDate.getMonth(),currentDate.getDate());minDate=_xdsoft_datetime.strToDate(options.minDate);minDate=new Date(minDate.getFullYear(),minDate.getMonth(),minDate.getDate());if(currentDatemaxDate){return;} -input.val(_xdsoft_datetime.str());input.trigger('change');datetimepicker.trigger('close.xdsoft');});mounth_picker.find('.xdsoft_prev,.xdsoft_next').on('mousedown.xdsoft',function(){var $this=$(this),timer=0,stop=false;(function arguments_callee1(v){if($this.hasClass(options.next)){_xdsoft_datetime.nextMonth();}else if($this.hasClass(options.prev)){_xdsoft_datetime.prevMonth();} -if(options.monthChangeSpinner){if(!stop){timer=setTimeout(arguments_callee1,v||100);}}}(500));$([document.body,window]).on('mouseup.xdsoft',function arguments_callee2(){clearTimeout(timer);stop=true;$([document.body,window]).off('mouseup.xdsoft',arguments_callee2);});});timepicker.find('.xdsoft_prev,.xdsoft_next').on('mousedown.xdsoft',function(){var $this=$(this),timer=0,stop=false,period=110;(function arguments_callee4(v){var pheight=timeboxparent[0].clientHeight,height=timebox[0].offsetHeight,top=Math.abs(parseInt(timebox.css('marginTop'),10));if($this.hasClass(options.next)&&(height-pheight)-options.timeHeightInTimePicker>=top){timebox.css('marginTop','-'+(top+options.timeHeightInTimePicker)+'px');}else if($this.hasClass(options.prev)&&top-options.timeHeightInTimePicker>=0){timebox.css('marginTop','-'+(top-options.timeHeightInTimePicker)+'px');} -timeboxparent.trigger('scroll_element.xdsoft_scroller',[Math.abs(parseInt(timebox.css('marginTop'),10)/(height-pheight))]);period=(period>10)?10:period-10;if(!stop){timer=setTimeout(arguments_callee4,v||period);}}(500));$([document.body,window]).on('mouseup.xdsoft',function arguments_callee5(){clearTimeout(timer);stop=true;$([document.body,window]).off('mouseup.xdsoft',arguments_callee5);});});xchangeTimer=0;datetimepicker.on('xchange.xdsoft',function(event){clearTimeout(xchangeTimer);xchangeTimer=setTimeout(function(){if(_xdsoft_datetime.currentTime===undefined||_xdsoft_datetime.currentTime===null){_xdsoft_datetime.currentTime=_xdsoft_datetime.now();} -var table='',start=new Date(_xdsoft_datetime.currentTime.getFullYear(),_xdsoft_datetime.currentTime.getMonth(),1,12,0,0),i=0,j,today=_xdsoft_datetime.now(),maxDate=false,minDate=false,hDate,day,d,y,m,w,classes=[],customDateSettings,newRow=true,time='',h='',line_time,description;while(start.getDay()!==options.dayOfWeekStart){start.setDate(start.getDate()-1);} +input.val(_xdsoft_datetime.str());input.trigger('change');datetimepicker.trigger('close.xdsoft');});month_picker.find('.xdsoft_prev,.xdsoft_next').on('touchend mousedown.xdsoft',function(){var $this=$(this),timer=0,stop=false;(function arguments_callee1(v){if($this.hasClass(options.next)){_xdsoft_datetime.nextMonth();}else if($this.hasClass(options.prev)){_xdsoft_datetime.prevMonth();} +if(options.monthChangeSpinner){if(!stop){timer=setTimeout(arguments_callee1,v||100);}}}(500));$([options.ownerDocument.body,options.contentWindow]).on('touchend mouseup.xdsoft',function arguments_callee2(){clearTimeout(timer);stop=true;$([options.ownerDocument.body,options.contentWindow]).off('touchend mouseup.xdsoft',arguments_callee2);});});timepicker.find('.xdsoft_prev,.xdsoft_next').on('touchend mousedown.xdsoft',function(){var $this=$(this),timer=0,stop=false,period=110;(function arguments_callee4(v){var pheight=timeboxparent[0].clientHeight,height=timebox[0].offsetHeight,top=Math.abs(parseInt(timebox.css('marginTop'),10));if($this.hasClass(options.next)&&(height-pheight)-options.timeHeightInTimePicker>=top){timebox.css('marginTop','-'+(top+options.timeHeightInTimePicker)+'px');}else if($this.hasClass(options.prev)&&top-options.timeHeightInTimePicker>=0){timebox.css('marginTop','-'+(top-options.timeHeightInTimePicker)+'px');} +timeboxparent.trigger('scroll_element.xdsoft_scroller',[Math.abs(parseInt(timebox[0].style.marginTop,10)/(height-pheight))]);period=(period>10)?10:period-10;if(!stop){timer=setTimeout(arguments_callee4,v||period);}}(500));$([options.ownerDocument.body,options.contentWindow]).on('touchend mouseup.xdsoft',function arguments_callee5(){clearTimeout(timer);stop=true;$([options.ownerDocument.body,options.contentWindow]).off('touchend mouseup.xdsoft',arguments_callee5);});});xchangeTimer=0;datetimepicker.on('xchange.xdsoft',function(event){clearTimeout(xchangeTimer);xchangeTimer=setTimeout(function(){if(_xdsoft_datetime.currentTime===undefined||_xdsoft_datetime.currentTime===null){_xdsoft_datetime.currentTime=_xdsoft_datetime.now();} +var table='',start=new Date(_xdsoft_datetime.currentTime.getFullYear(),_xdsoft_datetime.currentTime.getMonth(),1,12,0,0),i=0,j,today=_xdsoft_datetime.now(),maxDate=false,minDate=false,minDateTime=false,maxDateTime=false,hDate,day,d,y,m,w,classes=[],customDateSettings,newRow=true,time='',h,line_time,description;while(start.getDay()!==options.dayOfWeekStart){start.setDate(start.getDate()-1);} table+='
    + escape($field['label']); ?> - + escape($field['value']); ?> - + - + escape($field['value']); ?>
    ';if(options.weeks){table+='';} -for(j=0;j<7;j+=1){table+='';} +for(j=0;j<7;j+=1){table+='';} table+='';table+='';if(options.maxDate!==false){maxDate=_xdsoft_datetime.strToDate(options.maxDate);maxDate=new Date(maxDate.getFullYear(),maxDate.getMonth(),maxDate.getDate(),23,59,59,999);} if(options.minDate!==false){minDate=_xdsoft_datetime.strToDate(options.minDate);minDate=new Date(minDate.getFullYear(),minDate.getMonth(),minDate.getDate());} +if(options.minDateTime!==false){minDateTime=_xdsoft_datetime.strToDate(options.minDateTime);minDateTime=new Date(minDateTime.getFullYear(),minDateTime.getMonth(),minDateTime.getDate(),minDateTime.getHours(),minDateTime.getMinutes(),minDateTime.getSeconds());} +if(options.maxDateTime!==false){maxDateTime=_xdsoft_datetime.strToDate(options.maxDateTime);maxDateTime=new Date(maxDateTime.getFullYear(),maxDateTime.getMonth(),maxDateTime.getDate(),maxDateTime.getHours(),maxDateTime.getMinutes(),maxDateTime.getSeconds());} +var maxDateTimeDay;if(maxDateTime!==false){maxDateTimeDay=((maxDateTime.getFullYear()*12)+maxDateTime.getMonth())*31+maxDateTime.getDate();} while(i<_xdsoft_datetime.currentTime.countDaysInMonth()||start.getDay()!==options.dayOfWeekStart||_xdsoft_datetime.currentTime.getMonth()===start.getMonth()){classes=[];i+=1;day=start.getDay();d=start.getDate();y=start.getFullYear();m=start.getMonth();w=_xdsoft_datetime.getWeekOfYear(start);description='';classes.push('xdsoft_date');if(options.beforeShowDay&&$.isFunction(options.beforeShowDay.call)){customDateSettings=options.beforeShowDay.call(datetimepicker,start);}else{customDateSettings=null;} -if((maxDate!==false&&start>maxDate)||(minDate!==false&&start0){if(options.allowDates.indexOf(dateHelper.formatDate(start,options.formatDate))===-1){classes.push('xdsoft_disabled');}} +var currentDay=((start.getFullYear()*12)+start.getMonth())*31+start.getDate();if((maxDate!==false&&start>maxDate)||(minDateTime!==false&&startmaxDateTimeDay)||(customDateSettings&&customDateSettings[0]===false)){classes.push('xdsoft_disabled');} +if(options.disabledDates.indexOf(dateHelper.formatDate(start,options.formatDate))!==-1){classes.push('xdsoft_disabled');} +if(options.disabledWeekDays.indexOf(day)!==-1){classes.push('xdsoft_disabled');} +if(input.is('[disabled]')){classes.push('xdsoft_disabled');} if(customDateSettings&&customDateSettings[1]!==""){classes.push(customDateSettings[1]);} if(_xdsoft_datetime.currentTime.getMonth()!==m){classes.push('xdsoft_other_month');} -if((options.defaultSelect||datetimepicker.data('changed'))&&_xdsoft_datetime.currentTime.dateFormat(options.formatDate)===start.dateFormat(options.formatDate)){classes.push('xdsoft_current');} -if(today.dateFormat(options.formatDate)===start.dateFormat(options.formatDate)){classes.push('xdsoft_today');} -if(start.getDay()===0||start.getDay()===6||options.weekends.indexOf(start.dateFormat(options.formatDate))!==-1){classes.push('xdsoft_weekend');} -if(options.highlightedDates[start.dateFormat(options.formatDate)]!==undefined){hDate=options.highlightedDates[start.dateFormat(options.formatDate)];classes.push(hDate.style===undefined?'xdsoft_highlighted_default':hDate.style);description=hDate.desc===undefined?'':hDate.desc;} +if((options.defaultSelect||datetimepicker.data('changed'))&&dateHelper.formatDate(_xdsoft_datetime.currentTime,options.formatDate)===dateHelper.formatDate(start,options.formatDate)){classes.push('xdsoft_current');} +if(dateHelper.formatDate(today,options.formatDate)===dateHelper.formatDate(start,options.formatDate)){classes.push('xdsoft_today');} +if(start.getDay()===0||start.getDay()===6||options.weekends.indexOf(dateHelper.formatDate(start,options.formatDate))!==-1){classes.push('xdsoft_weekend');} +if(options.highlightedDates[dateHelper.formatDate(start,options.formatDate)]!==undefined){hDate=options.highlightedDates[dateHelper.formatDate(start,options.formatDate)];classes.push(hDate.style===undefined?'xdsoft_highlighted_default':hDate.style);description=hDate.desc===undefined?'':hDate.desc;} if(options.beforeShowDay&&$.isFunction(options.beforeShowDay)){classes.push(options.beforeShowDay(start));} if(newRow){table+='';newRow=false;if(options.weeks){table+='';}} table+='';if(start.getDay()===options.dayOfWeekStartPrev){table+='';newRow=true;} start.setDate(d+1);} -table+='
    '+options.i18n[options.lang].dayOfWeek[(j+options.dayOfWeekStart)%7]+''+options.i18n[globalLocale].dayOfWeekShort[(j+options.dayOfWeekStart)%7]+'
    '+w+''+'
    '+d+'
    '+'
    ';calendar.html(table);mounth_picker.find('.xdsoft_label span').eq(0).text(options.i18n[options.lang].months[_xdsoft_datetime.currentTime.getMonth()]);mounth_picker.find('.xdsoft_label span').eq(1).text(_xdsoft_datetime.currentTime.getFullYear());time='';h='';m='';line_time=function line_time(h,m){var now=_xdsoft_datetime.now(),optionDateTime,current_time;now.setHours(h);h=parseInt(now.getHours(),10);now.setMinutes(m);m=parseInt(now.getMinutes(),10);optionDateTime=new Date(_xdsoft_datetime.currentTime);optionDateTime.setHours(h);optionDateTime.setMinutes(m);classes=[];if((options.minDateTime!==false&&options.minDateTime>optionDateTime)||(options.maxTime!==false&&_xdsoft_datetime.strtotime(options.maxTime).getTime()now.getTime())){classes.push('xdsoft_disabled');} -if((options.minDateTime!==false&&options.minDateTime>optionDateTime)||((options.disabledMinTime!==false&&now.getTime()>_xdsoft_datetime.strtotime(options.disabledMinTime).getTime())&&(options.disabledMaxTime!==false&&now.getTime()<_xdsoft_datetime.strtotime(options.disabledMaxTime).getTime()))){classes.push('xdsoft_disabled');} -current_time=new Date(_xdsoft_datetime.currentTime);current_time.setHours(parseInt(_xdsoft_datetime.currentTime.getHours(),10));current_time.setMinutes(Math[options.roundTime](_xdsoft_datetime.currentTime.getMinutes()/options.step)*options.step);if((options.initTime||options.defaultSelect||datetimepicker.data('changed'))&¤t_time.getHours()===parseInt(h,10)&&(options.step>59||current_time.getMinutes()===parseInt(m,10))){if(options.defaultSelect||datetimepicker.data('changed')){classes.push('xdsoft_current');}else if(options.initTime){classes.push('xdsoft_init_time');}} +table+='';calendar.html(table);month_picker.find('.xdsoft_label span').eq(0).text(options.i18n[globalLocale].months[_xdsoft_datetime.currentTime.getMonth()]);month_picker.find('.xdsoft_label span').eq(1).text(_xdsoft_datetime.currentTime.getFullYear()+options.yearOffset);time='';h='';m='';var minTimeMinutesOfDay=0;if(options.minTime!==false){var t=_xdsoft_datetime.strtotime(options.minTime);minTimeMinutesOfDay=60*t.getHours()+t.getMinutes();} +var maxTimeMinutesOfDay=24*60;if(options.maxTime!==false){var t=_xdsoft_datetime.strtotime(options.maxTime);maxTimeMinutesOfDay=60*t.getHours()+t.getMinutes();} +if(options.minDateTime!==false){var t=_xdsoft_datetime.strToDateTime(options.minDateTime);var currentDayIsMinDateTimeDay=dateHelper.formatDate(_xdsoft_datetime.currentTime,options.formatDate)===dateHelper.formatDate(t,options.formatDate);if(currentDayIsMinDateTimeDay){var m=60*t.getHours()+t.getMinutes();if(m>minTimeMinutesOfDay)minTimeMinutesOfDay=m;}} +if(options.maxDateTime!==false){var t=_xdsoft_datetime.strToDateTime(options.maxDateTime);var currentDayIsMaxDateTimeDay=dateHelper.formatDate(_xdsoft_datetime.currentTime,options.formatDate)===dateHelper.formatDate(t,options.formatDate);if(currentDayIsMaxDateTimeDay){var m=60*t.getHours()+t.getMinutes();if(m=maxTimeMinutesOfDay)||(currentMinutesOfDay59)||current_time.getMinutes()===parseInt(m,10))){if(options.defaultSelect||datetimepicker.data('changed')){classes.push('xdsoft_current');}else if(options.initTime){classes.push('xdsoft_init_time');}} if(parseInt(today.getHours(),10)===parseInt(h,10)&&parseInt(today.getMinutes(),10)===parseInt(m,10)){classes.push('xdsoft_today');} -time+='
    '+now.dateFormat(options.formatTime)+'
    ';};if(!options.allowTimes||!$.isArray(options.allowTimes)||!options.allowTimes.length){for(i=0,j=0;i<(options.hours12?12:24);i+=1){for(j=0;j<60;j+=options.step){h=(i<10?'0':'')+i;m=(j<10?'0':'')+j;line_time(h,m);}}}else{for(i=0;i'+i+'
    ';} -yearselect.children().eq(0).html(opt);for(i=parseInt(options.monthStart,10),opt='';i<=parseInt(options.monthEnd,10);i+=1){opt+='
    '+options.i18n[options.lang].months[i]+'
    ';} +time+='
    '+dateHelper.formatDate(now,options.formatTime)+'
    ';};if(!options.allowTimes||!$.isArray(options.allowTimes)||!options.allowTimes.length){for(i=0,j=0;i<(options.hours12?12:24);i+=1){for(j=0;j<60;j+=options.step){var currentMinutesOfDay=i*60+j;if(currentMinutesOfDay=maxTimeMinutesOfDay)continue;h=(i<10?'0':'')+i;m=(j<10?'0':'')+j;line_time(h,m);}}}else{for(i=0;i'+(i+options.yearOffset)+'
    ';} +yearselect.children().eq(0).html(opt);for(i=parseInt(options.monthStart,10),opt='';i<=parseInt(options.monthEnd,10);i+=1){opt+='
    '+options.i18n[globalLocale].months[i]+'
    ';} monthselect.children().eq(0).html(opt);$(datetimepicker).trigger('generate.xdsoft');},10);event.stopPropagation();}).on('afterOpen.xdsoft',function(){if(options.timepicker){var classType,pheight,height,top;if(timebox.find('.xdsoft_current').length){classType='.xdsoft_current';}else if(timebox.find('.xdsoft_init_time').length){classType='.xdsoft_init_time';} if(classType){pheight=timeboxparent[0].clientHeight;height=timebox[0].offsetHeight;top=timebox.find(classType).index()*options.timeHeightInTimePicker+1;if((height-pheight)1||(options.closeOnDateSelect===true||(options.closeOnDateSelect===false&&!options.timepicker)))&&!options.inline){datetimepicker.trigger('close.xdsoft');} -if(options.onSelectDate&&$.isFunction(options.onSelectDate)){options.onSelectDate.call(datetimepicker,_xdsoft_datetime.currentTime,datetimepicker.data('input'),xdevent);} -datetimepicker.data('changed',true);datetimepicker.trigger('xchange.xdsoft');datetimepicker.trigger('changedatetime.xdsoft');setTimeout(function(){timerclick=0;},200);});timebox.on('click.xdsoft','div',function(xdevent){xdevent.stopPropagation();var $this=$(this),currentTime=_xdsoft_datetime.currentTime;if(currentTime===undefined||currentTime===null){_xdsoft_datetime.currentTime=_xdsoft_datetime.now();currentTime=_xdsoft_datetime.currentTime;} +currentTime.setDate(1);currentTime.setFullYear($this.data('year'));currentTime.setMonth($this.data('month'));currentTime.setDate($this.data('date'));datetimepicker.trigger('select.xdsoft',[currentTime]);input.val(_xdsoft_datetime.str());if(options.onSelectDate&&$.isFunction(options.onSelectDate)){options.onSelectDate.call(datetimepicker,_xdsoft_datetime.currentTime,datetimepicker.data('input'),xdevent);} +datetimepicker.data('changed',true);datetimepicker.trigger('xchange.xdsoft');datetimepicker.trigger('changedatetime.xdsoft');if((timerclick>1||(options.closeOnDateSelect===true||(options.closeOnDateSelect===false&&!options.timepicker)))&&!options.inline){datetimepicker.trigger('close.xdsoft');} +setTimeout(function(){timerclick=0;},200);});timebox.on('touchstart','div',function(xdevent){this.touchMoved=false;}).on('touchmove','div',handleTouchMoved).on('touchend click.xdsoft','div',function(xdevent){if(!this.touchMoved){xdevent.stopPropagation();var $this=$(this),currentTime=_xdsoft_datetime.currentTime;if(currentTime===undefined||currentTime===null){_xdsoft_datetime.currentTime=_xdsoft_datetime.now();currentTime=_xdsoft_datetime.currentTime;} if($this.hasClass('xdsoft_disabled')){return false;} -currentTime.setHours($this.data('hour'));currentTime.setMinutes($this.data('minute'));datetimepicker.trigger('select.xdsoft',[currentTime]);datetimepicker.data('input').val(_xdsoft_datetime.str());if(options.inline!==true&&options.closeOnTimeSelect===true){datetimepicker.trigger('close.xdsoft');} -if(options.onSelectTime&&$.isFunction(options.onSelectTime)){options.onSelectTime.call(datetimepicker,_xdsoft_datetime.currentTime,datetimepicker.data('input'),xdevent);} -datetimepicker.data('changed',true);datetimepicker.trigger('xchange.xdsoft');datetimepicker.trigger('changedatetime.xdsoft');});datepicker.on('mousewheel.xdsoft',function(event){if(!options.scrollMonth){return true;} +currentTime.setHours($this.data('hour'));currentTime.setMinutes($this.data('minute'));datetimepicker.trigger('select.xdsoft',[currentTime]);datetimepicker.data('input').val(_xdsoft_datetime.str());if(options.onSelectTime&&$.isFunction(options.onSelectTime)){options.onSelectTime.call(datetimepicker,_xdsoft_datetime.currentTime,datetimepicker.data('input'),xdevent);} +datetimepicker.data('changed',true);datetimepicker.trigger('xchange.xdsoft');datetimepicker.trigger('changedatetime.xdsoft');if(options.inline!==true&&options.closeOnTimeSelect===true){datetimepicker.trigger('close.xdsoft');}}});datepicker.on('mousewheel.xdsoft',function(event){if(!options.scrollMonth){return true;} if(event.deltaY<0){_xdsoft_datetime.nextMonth();}else{_xdsoft_datetime.prevMonth();} return false;});input.on('mousewheel.xdsoft',function(event){if(!options.scrollInput){return true;} if(!options.datepicker&&options.timepicker){current_time_index=timebox.find('.xdsoft_current').length?timebox.find('.xdsoft_current').eq(0).index():0;if(current_time_index+event.deltaY>=0&¤t_time_index+event.deltaY$(window).height()+$(window).scrollTop()){top=offset.top-datetimepicker[0].offsetHeight+1;} -if(top<0){top=0;} -if(left+datetimepicker[0].offsetWidth>$(window).width()){left=$(window).width()-datetimepicker[0].offsetWidth;}} -node=datetimepicker[0];do{node=node.parentNode;if(window.getComputedStyle(node).getPropertyValue('position')==='relative'&&$(window).width()>=node.offsetWidth){left=left-(($(window).width()-node.offsetWidth)/2);break;}}while(node.nodeName!=='HTML');datetimepicker.css({left:left,top:top,position:position});};datetimepicker.on('open.xdsoft',function(event){var onShow=true;if(options.onShow&&$.isFunction(options.onShow)){onShow=options.onShow.call(datetimepicker,_xdsoft_datetime.currentTime,datetimepicker.data('input'),event);} -if(onShow!==false){datetimepicker.show();setPos();$(window).off('resize.xdsoft',setPos).on('resize.xdsoft',setPos);if(options.closeOnWithoutClick){$([document.body,window]).on('mousedown.xdsoft',function arguments_callee6(){datetimepicker.trigger('close.xdsoft');$([document.body,window]).off('mousedown.xdsoft',arguments_callee6);});}}}).on('close.xdsoft',function(event){var onClose=true;mounth_picker.find('.xdsoft_month,.xdsoft_year').find('.xdsoft_select').hide();if(options.onClose&&$.isFunction(options.onClose)){onClose=options.onClose.call(datetimepicker,_xdsoft_datetime.currentTime,datetimepicker.data('input'),event);} +if(triggerAfterOpen){datetimepicker.trigger('afterOpen.xdsoft');triggerAfterOpen=false;}}).on('click.xdsoft',function(xdevent){xdevent.stopPropagation();});current_time_index=0;forEachAncestorOf=function(node,callback){do{node=node.parentNode;if(!node||callback(node)===false){break;}}while(node.nodeName!=='HTML');};setPos=function(){var dateInputOffset,dateInputElem,verticalPosition,left,position,datetimepickerElem,dateInputHasFixedAncestor,$dateInput,windowWidth,verticalAnchorEdge,datetimepickerCss,windowHeight,windowScrollTop;$dateInput=datetimepicker.data('input');dateInputOffset=$dateInput.offset();dateInputElem=$dateInput[0];verticalAnchorEdge='top';verticalPosition=(dateInputOffset.top+dateInputElem.offsetHeight)-1;left=dateInputOffset.left;position="absolute";windowWidth=$(options.contentWindow).width();windowHeight=$(options.contentWindow).height();windowScrollTop=$(options.contentWindow).scrollTop();if((options.ownerDocument.documentElement.clientWidth-dateInputOffset.left)windowHeight+windowScrollTop){verticalAnchorEdge='bottom';verticalPosition=(windowHeight+windowScrollTop)-dateInputOffset.top;}else{verticalPosition-=windowScrollTop;}}else{if(verticalPosition+datetimepicker[0].offsetHeight>windowHeight+windowScrollTop){verticalPosition=dateInputOffset.top-datetimepicker[0].offsetHeight+1;}} +if(verticalPosition<0){verticalPosition=0;} +if(left+dateInputElem.offsetWidth>windowWidth){left=windowWidth-dateInputElem.offsetWidth;}} +datetimepickerElem=datetimepicker[0];forEachAncestorOf(datetimepickerElem,function(ancestorNode){var ancestorNodePosition;ancestorNodePosition=options.contentWindow.getComputedStyle(ancestorNode).getPropertyValue('position');if(ancestorNodePosition==='relative'&&windowWidth>=ancestorNode.offsetWidth){left=left-((windowWidth-ancestorNode.offsetWidth)/2);return false;}});datetimepickerCss={position:position,left:left,top:'',bottom:''};datetimepickerCss[verticalAnchorEdge]=verticalPosition;datetimepicker.css(datetimepickerCss);};datetimepicker.on('open.xdsoft',function(event){var onShow=true;if(options.onShow&&$.isFunction(options.onShow)){onShow=options.onShow.call(datetimepicker,_xdsoft_datetime.currentTime,datetimepicker.data('input'),event);} +if(onShow!==false){datetimepicker.show();setPos();$(options.contentWindow).off('resize.xdsoft',setPos).on('resize.xdsoft',setPos);if(options.closeOnWithoutClick){$([options.ownerDocument.body,options.contentWindow]).on('touchstart mousedown.xdsoft',function arguments_callee6(){datetimepicker.trigger('close.xdsoft');$([options.ownerDocument.body,options.contentWindow]).off('touchstart mousedown.xdsoft',arguments_callee6);});}}}).on('close.xdsoft',function(event){var onClose=true;month_picker.find('.xdsoft_month,.xdsoft_year').find('.xdsoft_select').hide();if(options.onClose&&$.isFunction(options.onClose)){onClose=options.onClose.call(datetimepicker,_xdsoft_datetime.currentTime,datetimepicker.data('input'),event);} if(onClose!==false&&!options.opened&&!options.inline){datetimepicker.hide();} -event.stopPropagation();}).on('toggle.xdsoft',function(event){if(datetimepicker.is(':visible')){datetimepicker.trigger('close.xdsoft');}else{datetimepicker.trigger('open.xdsoft');}}).data('input',input);timer=0;timer1=0;datetimepicker.data('xdsoft_datetime',_xdsoft_datetime);datetimepicker.setOptions(options);function getCurrentValue(){var ct=false,time;if(options.startDate){ct=_xdsoft_datetime.strToDate(options.startDate);}else{ct=options.value||((input&&input.val&&input.val())?input.val():'');if(ct){ct=_xdsoft_datetime.strToDateTime(ct);}else if(options.defaultDate){ct=_xdsoft_datetime.strToDateTime(options.defaultDate);if(options.defaultTime){time=_xdsoft_datetime.strtotime(options.defaultTime);ct.setHours(time.getHours());ct.setMinutes(time.getMinutes());}}} +event.stopPropagation();}).on('toggle.xdsoft',function(){if(datetimepicker.is(':visible')){datetimepicker.trigger('close.xdsoft');}else{datetimepicker.trigger('open.xdsoft');}}).data('input',input);timer=0;datetimepicker.data('xdsoft_datetime',_xdsoft_datetime);datetimepicker.setOptions(options);function getCurrentValue(){var ct=false,time;if(options.startDate){ct=_xdsoft_datetime.strToDate(options.startDate);}else{ct=options.value||((input&&input.val&&input.val())?input.val():'');if(ct){ct=_xdsoft_datetime.strToDateTime(ct);if(options.yearOffset){ct=new Date(ct.getFullYear()-options.yearOffset,ct.getMonth(),ct.getDate(),ct.getHours(),ct.getMinutes(),ct.getSeconds(),ct.getMilliseconds());}}else if(options.defaultDate){ct=_xdsoft_datetime.strToDateTime(options.defaultDate);if(options.defaultTime){time=_xdsoft_datetime.strtotime(options.defaultTime);ct.setHours(time.getHours());ct.setMinutes(time.getMinutes());}}} if(ct&&_xdsoft_datetime.isValidDate(ct)){datetimepicker.data('changed',true);}else{ct='';} return ct||0;} -_xdsoft_datetime.setCurrentTime(getCurrentValue());input.data('xdsoft_datetimepicker',datetimepicker).on('open.xdsoft focusin.xdsoft mousedown.xdsoft',function(event){if(input.is(':disabled')||(input.data('xdsoft_datetimepicker').is(':visible')&&options.closeOnInputClick)){return;} +function setMask(options){var isValidValue=function(mask,value){var reg=mask.replace(/([\[\]\/\{\}\(\)\-\.\+]{1})/g,'\\$1').replace(/_/g,'{digit+}').replace(/([0-9]{1})/g,'{digit$1}').replace(/\{digit([0-9]{1})\}/g,'[0-$1_]{1}').replace(/\{digit[\+]\}/g,'[0-9_]{1}');return(new RegExp(reg)).test(value);},getCaretPos=function(input){try{if(options.ownerDocument.selection&&options.ownerDocument.selection.createRange){var range=options.ownerDocument.selection.createRange();return range.getBookmark().charCodeAt(2)-2;} +if(input.setSelectionRange){return input.selectionStart;}}catch(e){return 0;}},setCaretPos=function(node,pos){node=(typeof node==="string"||node instanceof String)?options.ownerDocument.getElementById(node):node;if(!node){return false;} +if(node.createTextRange){var textRange=node.createTextRange();textRange.collapse(true);textRange.moveEnd('character',pos);textRange.moveStart('character',pos);textRange.select();return true;} +if(node.setSelectionRange){node.setSelectionRange(pos,pos);return true;} +return false;};if(options.mask){input.off('keydown.xdsoft');} +if(options.mask===true){if(dateHelper.formatMask){options.mask=dateHelper.formatMask(options.format)}else{options.mask=options.format.replace(/Y/g,'9999').replace(/F/g,'9999').replace(/m/g,'19').replace(/d/g,'39').replace(/H/g,'29').replace(/i/g,'59').replace(/s/g,'59');}} +if($.type(options.mask)==='string'){if(!isValidValue(options.mask,input.val())){input.val(options.mask.replace(/[0-9]/g,'_'));setCaretPos(input[0],0);} +input.on('paste.xdsoft',function(event){var clipboardData=event.clipboardData||event.originalEvent.clipboardData||window.clipboardData,pastedData=clipboardData.getData('text'),val=this.value,pos=this.selectionStart +var valueBeforeCursor=val.substr(0,pos);var valueAfterPaste=val.substr(pos+pastedData.length);val=valueBeforeCursor+pastedData+valueAfterPaste;pos+=pastedData.length;if(isValidValue(options.mask,val)){this.value=val;setCaretPos(this,pos);}else if($.trim(val)===''){this.value=options.mask.replace(/[0-9]/g,'_');}else{input.trigger('error_input.xdsoft');} +event.preventDefault();return false;});input.on('keydown.xdsoft',function(event){var val=this.value,key=event.which,pos=this.selectionStart,selEnd=this.selectionEnd,hasSel=pos!==selEnd,digit;if(((key>=KEY0&&key<=KEY9)||(key>=_KEY0&&key<=_KEY9))||(key===BACKSPACE||key===DEL)){digit=(key===BACKSPACE||key===DEL)?'_':String.fromCharCode((_KEY0<=key&&key<=_KEY9)?key-KEY0:key);if(key===BACKSPACE&&pos&&!hasSel){pos-=1;} +while(true){var maskValueAtCurPos=options.mask.substr(pos,1);var posShorterThanMaskLength=pos0;var notNumberOrPlaceholder=/[^0-9_]/;var curPosOnSep=notNumberOrPlaceholder.test(maskValueAtCurPos);var continueMovingPosition=curPosOnSep&&posShorterThanMaskLength&&posGreaterThanZero +if(!continueMovingPosition)break;pos+=(key===BACKSPACE&&!hasSel)?-1:1;} +if(hasSel){var selLength=selEnd-pos +var defaultBlank=options.mask.replace(/[0-9]/g,'_');var defaultBlankSelectionReplacement=defaultBlank.substr(pos,selLength);var selReplacementRemainder=defaultBlankSelectionReplacement.substr(1) +var valueBeforeSel=val.substr(0,pos);var insertChars=digit+selReplacementRemainder;var charsAfterSelection=val.substr(pos+selLength);val=valueBeforeSel+insertChars+charsAfterSelection}else{var valueBeforeCursor=val.substr(0,pos);var insertChar=digit;var valueAfterNextChar=val.substr(pos+1);val=valueBeforeCursor+insertChar+valueAfterNextChar} +if($.trim(val)===''){val=defaultBlank}else{if(pos===options.mask.length){event.preventDefault();return false;}} +pos+=(key===BACKSPACE)?0:1;while(/[^0-9_]/.test(options.mask.substr(pos,1))&&pos0){pos+=(key===BACKSPACE)?0:1;} +if(isValidValue(options.mask,val)){this.value=val;setCaretPos(this,pos);}else if($.trim(val)===''){this.value=options.mask.replace(/[0-9]/g,'_');}else{input.trigger('error_input.xdsoft');}}else{if(([AKEY,CKEY,VKEY,ZKEY,YKEY].indexOf(key)!==-1&&ctrlDown)||[ESC,ARROWUP,ARROWDOWN,ARROWLEFT,ARROWRIGHT,F5,CTRLKEY,TAB,ENTER].indexOf(key)!==-1){return true;}} +event.preventDefault();return false;});}} +_xdsoft_datetime.setCurrentTime(getCurrentValue());input.data('xdsoft_datetimepicker',datetimepicker).on('open.xdsoft focusin.xdsoft mousedown.xdsoft touchstart',function(){if(input.is(':disabled')||(input.data('xdsoft_datetimepicker').is(':visible')&&options.closeOnInputClick)){return;} +if(!options.openOnFocus){return;} clearTimeout(timer);timer=setTimeout(function(){if(input.is(':disabled')){return;} -triggerAfterOpen=true;_xdsoft_datetime.setCurrentTime(getCurrentValue());datetimepicker.trigger('open.xdsoft');},100);}).on('keydown.xdsoft',function(event){var val=this.value,elementSelector,key=event.which;if([ENTER].indexOf(key)!==-1&&options.enterLikeTab){elementSelector=$("input:visible,textarea:visible");datetimepicker.trigger('close.xdsoft');elementSelector.eq(elementSelector.index(this)+1).focus();return false;} -if([TAB].indexOf(key)!==-1){datetimepicker.trigger('close.xdsoft');return true;}});};destroyDateTimePicker=function(input){var datetimepicker=input.data('xdsoft_datetimepicker');if(datetimepicker){datetimepicker.data('xdsoft_datetime',null);datetimepicker.remove();input.data('xdsoft_datetimepicker',null).off('.xdsoft');$(window).off('resize.xdsoft');$([window,document.body]).off('mousedown.xdsoft');if(input.unmousewheel){input.unmousewheel();}}};$(document).off('keydown.xdsoftctrl keyup.xdsoftctrl').on('keydown.xdsoftctrl',function(e){if(e.keyCode===CTRLKEY){ctrlDown=true;}}).on('keyup.xdsoftctrl',function(e){if(e.keyCode===CTRLKEY){ctrlDown=false;}});return this.each(function(){var datetimepicker=$(this).data('xdsoft_datetimepicker'),$input;if(datetimepicker){if($.type(opt)==='string'){switch(opt){case'show':$(this).select().focus();datetimepicker.trigger('open.xdsoft');break;case'hide':datetimepicker.trigger('close.xdsoft');break;case'toggle':datetimepicker.trigger('toggle.xdsoft');break;case'destroy':destroyDateTimePicker($(this));break;case'reset':this.value=this.defaultValue;if(!this.value||!datetimepicker.data('xdsoft_datetime').isValidDate(Date.parseDate(this.value,options.format))){datetimepicker.data('changed',false);} -datetimepicker.data('xdsoft_datetime').setCurrentTime(this.value);break;case'validate':$input=datetimepicker.data('input');$input.trigger('blur.xdsoft');break;}}else{datetimepicker.setOptions(opt);} +triggerAfterOpen=true;_xdsoft_datetime.setCurrentTime(getCurrentValue(),true);if(options.mask){setMask(options);} +datetimepicker.trigger('open.xdsoft');},100);}).on('keydown.xdsoft',function(event){var elementSelector,key=event.which;if([ENTER].indexOf(key)!==-1&&options.enterLikeTab){elementSelector=$("input:visible,textarea:visible,button:visible,a:visible");datetimepicker.trigger('close.xdsoft');elementSelector.eq(elementSelector.index(this)+1).focus();return false;} +if([TAB].indexOf(key)!==-1){datetimepicker.trigger('close.xdsoft');return true;}}).on('blur.xdsoft',function(){datetimepicker.trigger('close.xdsoft');});};destroyDateTimePicker=function(input){var datetimepicker=input.data('xdsoft_datetimepicker');if(datetimepicker){datetimepicker.data('xdsoft_datetime',null);datetimepicker.remove();input.data('xdsoft_datetimepicker',null).off('.xdsoft');$(options.contentWindow).off('resize.xdsoft');$([options.contentWindow,options.ownerDocument.body]).off('mousedown.xdsoft touchstart');if(input.unmousewheel){input.unmousewheel();}}};$(options.ownerDocument).off('keydown.xdsoftctrl keyup.xdsoftctrl').on('keydown.xdsoftctrl',function(e){if(e.keyCode===CTRLKEY){ctrlDown=true;}}).on('keyup.xdsoftctrl',function(e){if(e.keyCode===CTRLKEY){ctrlDown=false;}});this.each(function(){var datetimepicker=$(this).data('xdsoft_datetimepicker'),$input;if(datetimepicker){if($.type(opt)==='string'){switch(opt){case'show':$(this).select().focus();datetimepicker.trigger('open.xdsoft');break;case'hide':datetimepicker.trigger('close.xdsoft');break;case'toggle':datetimepicker.trigger('toggle.xdsoft');break;case'destroy':destroyDateTimePicker($(this));break;case'reset':this.value=this.defaultValue;if(!this.value||!datetimepicker.data('xdsoft_datetime').isValidDate(dateHelper.parseDate(this.value,options.format))){datetimepicker.data('changed',false);} +datetimepicker.data('xdsoft_datetime').setCurrentTime(this.value);break;case'validate':$input=datetimepicker.data('input');$input.trigger('blur.xdsoft');break;default:if(datetimepicker[opt]&&$.isFunction(datetimepicker[opt])){result=datetimepicker[opt](opt2);}}}else{datetimepicker.setOptions(opt);} return 0;} -if($.type(opt)!=='string'){if(!options.lazyInit||options.open||options.inline){createDateTimePicker($(this));}else{lazyInit($(this));}}});};$.fn.datetimepicker.defaults=default_options;}(jQuery));function HighlightedDate(date,desc,style){"use strict";this.date=date;this.desc=desc;this.style=style;} -(function(){ -/*! Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh) - * Licensed under the MIT License (LICENSE.txt). - * - * Version: 3.1.12 - * - * Requires: jQuery 1.2.2+ - */ -!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?module.exports=a:a(jQuery)}(function(a){function b(b){var g=b||window.event,h=i.call(arguments,1),j=0,l=0,m=0,n=0,o=0,p=0;if(b=a.event.fix(g),b.type="mousewheel","detail"in g&&(m=-1*g.detail),"wheelDelta"in g&&(m=g.wheelDelta),"wheelDeltaY"in g&&(m=g.wheelDeltaY),"wheelDeltaX"in g&&(l=-1*g.wheelDeltaX),"axis"in g&&g.axis===g.HORIZONTAL_AXIS&&(l=-1*m,m=0),j=0===m?l:m,"deltaY"in g&&(m=-1*g.deltaY,j=m),"deltaX"in g&&(l=g.deltaX,0===m&&(j=-1*l)),0!==m||0!==l){if(1===g.deltaMode){var q=a.data(this,"mousewheel-line-height");j*=q,m*=q,l*=q}else if(2===g.deltaMode){var r=a.data(this,"mousewheel-page-height");j*=r,m*=r,l*=r}if(n=Math.max(Math.abs(m),Math.abs(l)),(!f||f>n)&&(f=n,d(g,n)&&(f/=40)),d(g,n)&&(j/=40,l/=40,m/=40),j=Math[j>=1?"floor":"ceil"](j/f),l=Math[l>=1?"floor":"ceil"](l/f),m=Math[m>=1?"floor":"ceil"](m/f),k.settings.normalizeOffset&&this.getBoundingClientRect){var s=this.getBoundingClientRect();o=b.clientX-s.left,p=b.clientY-s.top}return b.deltaX=l,b.deltaY=m,b.deltaFactor=f,b.offsetX=o,b.offsetY=p,b.deltaMode=0,h.unshift(b,j,l,m),e&&clearTimeout(e),e=setTimeout(c,200),(a.event.dispatch||a.event.handle).apply(this,h)}}function c(){f=null}function d(a,b){return k.settings.adjustOldDeltas&&"mousewheel"===a.type&&b%120===0}var e,f,g=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],h="onwheel"in document||document.documentMode>=9?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],i=Array.prototype.slice;if(a.event.fixHooks)for(var j=g.length;j;)a.event.fixHooks[g[--j]]=a.event.mouseHooks;var k=a.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var c=h.length;c;)this.addEventListener(h[--c],b,!1);else this.onmousewheel=b;a.data(this,"mousewheel-line-height",k.getLineHeight(this)),a.data(this,"mousewheel-page-height",k.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var c=h.length;c;)this.removeEventListener(h[--c],b,!1);else this.onmousewheel=null;a.removeData(this,"mousewheel-line-height"),a.removeData(this,"mousewheel-page-height")},getLineHeight:function(b){var c=a(b),d=c["offsetParent"in a.fn?"offsetParent":"parent"]();return d.length||(d=a("body")),parseInt(d.css("fontSize"),10)||parseInt(c.css("fontSize"),10)||16},getPageHeight:function(b){return a(b).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};a.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})});Date.parseFunctions={count:0};Date.parseRegexes=[];Date.formatFunctions={count:0};Date.prototype.dateFormat=function(b){if(b=="unixtime"){return parseInt(this.getTime()/1000);}if(Date.formatFunctions[b]==null){Date.createNewFormat(b);}var a=Date.formatFunctions[b];return this[a]();};Date.createNewFormat=function(format){var funcName="format"+Date.formatFunctions.count++;Date.formatFunctions[format]=funcName;var codePrefix="Date.prototype."+funcName+" = function() {return ";var code="";var special=false;var ch="";for(var i=0;i 0) {";var regex="";var special=false;var ch="";for(var i=0;i 0 && z > 0){\nvar doyDate = new Date(y,0);\ndoyDate.setDate(z);\nm = doyDate.getMonth();\nd = doyDate.getDate();\n}";code+="if (y > 0 && m >= 0 && d > 0 && h >= 0 && i >= 0 && s >= 0)\n{return new Date(y, m, d, h, i, s);}\nelse if (y > 0 && m >= 0 && d > 0 && h >= 0 && i >= 0)\n{return new Date(y, m, d, h, i);}\nelse if (y > 0 && m >= 0 && d > 0 && h >= 0)\n{return new Date(y, m, d, h);}\nelse if (y > 0 && m >= 0 && d > 0)\n{return new Date(y, m, d);}\nelse if (y > 0 && m >= 0)\n{return new Date(y, m);}\nelse if (y > 0)\n{return new Date(y);}\n}return null;}";Date.parseRegexes[regexNum]=new RegExp("^"+regex+"$",'i');eval(code);};Date.formatCodeToRegex=function(b,a){switch(b){case"D":return{g:0,c:null,s:"(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)"};case"j":case"d":return{g:1,c:"d = parseInt(results["+a+"], 10);\n",s:"(\\d{1,2})"};case"l":return{g:0,c:null,s:"(?:"+Date.dayNames.join("|")+")"};case"S":return{g:0,c:null,s:"(?:st|nd|rd|th)"};case"w":return{g:0,c:null,s:"\\d"};case"z":return{g:1,c:"z = parseInt(results["+a+"], 10);\n",s:"(\\d{1,3})"};case"W":return{g:0,c:null,s:"(?:\\d{2})"};case"F":return{g:1,c:"m = parseInt(Date.monthNumbers[results["+a+"].substring(0, 3)], 10);\n",s:"("+Date.monthNames.join("|")+")"};case"M":return{g:1,c:"m = parseInt(Date.monthNumbers[results["+a+"]], 10);\n",s:"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)"};case"n":case"m":return{g:1,c:"m = parseInt(results["+a+"], 10) - 1;\n",s:"(\\d{1,2})"};case"t":return{g:0,c:null,s:"\\d{1,2}"};case"L":return{g:0,c:null,s:"(?:1|0)"};case"Y":return{g:1,c:"y = parseInt(results["+a+"], 10);\n",s:"(\\d{4})"};case"y":return{g:1,c:"var ty = parseInt(results["+a+"], 10);\ny = ty > Date.y2kYear ? 1900 + ty : 2000 + ty;\n",s:"(\\d{1,2})"};case"a":return{g:1,c:"if (results["+a+"] == 'am') {\nif (h == 12) { h = 0; }\n} else { if (h < 12) { h += 12; }}",s:"(am|pm)"};case"A":return{g:1,c:"if (results["+a+"] == 'AM') {\nif (h == 12) { h = 0; }\n} else { if (h < 12) { h += 12; }}",s:"(AM|PM)"};case"g":case"G":case"h":case"H":return{g:1,c:"h = parseInt(results["+a+"], 10);\n",s:"(\\d{1,2})"};case"i":return{g:1,c:"i = parseInt(results["+a+"], 10);\n",s:"(\\d{2})"};case"s":return{g:1,c:"s = parseInt(results["+a+"], 10);\n",s:"(\\d{2})"};case"O":return{g:0,c:null,s:"[+-]\\d{4}"};case"T":return{g:0,c:null,s:"[A-Z]{3}"};case"Z":return{g:0,c:null,s:"[+-]\\d{1,5}"};default:return{g:0,c:null,s:String.escape(b)};}};Date.prototype.getTimezone=function(){return this.toString().replace(/^.*? ([A-Z]{3}) [0-9]{4}.*$/,"$1").replace(/^.*?\(([A-Z])[a-z]+ ([A-Z])[a-z]+ ([A-Z])[a-z]+\)$/,"$1$2$3");};Date.prototype.getGMTOffset=function(){return(this.getTimezoneOffset()>0?"-":"+")+String.leftPad(Math.floor(Math.abs(this.getTimezoneOffset())/60),2,"0")+String.leftPad(Math.abs(this.getTimezoneOffset())%60,2,"0");};Date.prototype.getDayOfYear=function(){var a=0;Date.daysInMonth[1]=this.isLeapYear()?29:28;for(var b=0;b=9)?['wheel']:['mousewheel','DomMouseScroll','MozMousePixelScroll'],slice=Array.prototype.slice,nullLowestDeltaTimeout,lowestDelta;if($.event.fixHooks){for(var i=toFix.length;i;){$.event.fixHooks[toFix[--i]]=$.event.mouseHooks;}} +var special=$.event.special.mousewheel={version:'3.1.12',setup:function(){if(this.addEventListener){for(var i=toBind.length;i;){this.addEventListener(toBind[--i],handler,false);}}else{this.onmousewheel=handler;} +$.data(this,'mousewheel-line-height',special.getLineHeight(this));$.data(this,'mousewheel-page-height',special.getPageHeight(this));},teardown:function(){if(this.removeEventListener){for(var i=toBind.length;i;){this.removeEventListener(toBind[--i],handler,false);}}else{this.onmousewheel=null;} +$.removeData(this,'mousewheel-line-height');$.removeData(this,'mousewheel-page-height');},getLineHeight:function(elem){var $elem=$(elem),$parent=$elem['offsetParent'in $.fn?'offsetParent':'parent']();if(!$parent.length){$parent=$('body');} +return parseInt($parent.css('fontSize'),10)||parseInt($elem.css('fontSize'),10)||16;},getPageHeight:function(elem){return $(elem).height();},settings:{adjustOldDeltas:true,normalizeOffset:true}};$.fn.extend({mousewheel:function(fn){return fn?this.bind('mousewheel',fn):this.trigger('mousewheel');},unmousewheel:function(fn){return this.unbind('mousewheel',fn);}});function handler(event){var orgEvent=event||window.event,args=slice.call(arguments,1),delta=0,deltaX=0,deltaY=0,absDelta=0,offsetX=0,offsetY=0;event=$.event.fix(orgEvent);event.type='mousewheel';if('detail'in orgEvent){deltaY=orgEvent.detail*-1;} +if('wheelDelta'in orgEvent){deltaY=orgEvent.wheelDelta;} +if('wheelDeltaY'in orgEvent){deltaY=orgEvent.wheelDeltaY;} +if('wheelDeltaX'in orgEvent){deltaX=orgEvent.wheelDeltaX*-1;} +if('axis'in orgEvent&&orgEvent.axis===orgEvent.HORIZONTAL_AXIS){deltaX=deltaY*-1;deltaY=0;} +delta=deltaY===0?deltaX:deltaY;if('deltaY'in orgEvent){deltaY=orgEvent.deltaY*-1;delta=deltaY;} +if('deltaX'in orgEvent){deltaX=orgEvent.deltaX;if(deltaY===0){delta=deltaX*-1;}} +if(deltaY===0&&deltaX===0){return;} +if(orgEvent.deltaMode===1){var lineHeight=$.data(this,'mousewheel-line-height');delta*=lineHeight;deltaY*=lineHeight;deltaX*=lineHeight;}else if(orgEvent.deltaMode===2){var pageHeight=$.data(this,'mousewheel-page-height');delta*=pageHeight;deltaY*=pageHeight;deltaX*=pageHeight;} +absDelta=Math.max(Math.abs(deltaY),Math.abs(deltaX));if(!lowestDelta||absDelta=1?'floor':'ceil'](delta/lowestDelta);deltaX=Math[deltaX>=1?'floor':'ceil'](deltaX/lowestDelta);deltaY=Math[deltaY>=1?'floor':'ceil'](deltaY/lowestDelta);if(special.settings.normalizeOffset&&this.getBoundingClientRect){var boundingRect=this.getBoundingClientRect();offsetX=event.clientX-boundingRect.left;offsetY=event.clientY-boundingRect.top;} +event.deltaX=deltaX;event.deltaY=deltaY;event.deltaFactor=lowestDelta;event.offsetX=offsetX;event.offsetY=offsetY;event.deltaMode=0;args.unshift(event,delta,deltaX,deltaY);if(nullLowestDeltaTimeout){clearTimeout(nullLowestDeltaTimeout);} +nullLowestDeltaTimeout=setTimeout(nullLowestDelta,200);return($.event.dispatch||$.event.handle).apply(this,args);} +function nullLowestDelta(){lowestDelta=null;} +function shouldAdjustOldDeltas(orgEvent,absDelta){return special.settings.adjustOldDeltas&&orgEvent.type==='mousewheel'&&absDelta%120===0;}}));; /*! * Shuffle.js by @Vestride * Categorize, sort, and filter a responsive grid of items. diff --git a/plugins/MauticCitrixBundle/Config/config.php b/plugins/MauticCitrixBundle/Config/config.php index d4116d01ff1..c1f9c25421d 100644 --- a/plugins/MauticCitrixBundle/Config/config.php +++ b/plugins/MauticCitrixBundle/Config/config.php @@ -66,6 +66,10 @@ 'doctrine.orm.entity_manager', ], ], + 'mautic.citrix.integration.request' => [ + 'class' => \MauticPlugin\MauticCitrixBundle\EventListener\IntegrationRequestSubscriber::class, + 'arguments' => [], + ], ], 'forms' => [ 'mautic.form.type.fieldslist.citrixlist' => [ diff --git a/plugins/MauticCitrixBundle/Controller/PublicController.php b/plugins/MauticCitrixBundle/Controller/PublicController.php index 169fb3f0cbe..19cf5eb53cd 100644 --- a/plugins/MauticCitrixBundle/Controller/PublicController.php +++ b/plugins/MauticCitrixBundle/Controller/PublicController.php @@ -46,7 +46,7 @@ public function proxyAction(Request $request) } $ch = curl_init($url); - if (strtolower($request->server->get('REQUEST_METHOD', '')) === 'post') { + if ('post' === strtolower($request->server->get('REQUEST_METHOD', ''))) { $headers = [ 'Content-type: application/json', 'Accept: application/json', @@ -73,7 +73,7 @@ public function proxyAction(Request $request) $response = new Response($json, $status['http_code']); // Generate appropriate content-type header. - $is_xhr = strtolower($request->server->get('HTTP_X_REQUESTED_WITH', null)) === 'xmlhttprequest'; + $is_xhr = 'xmlhttprequest' === strtolower($request->server->get('HTTP_X_REQUESTED_WITH', null)); $response->headers->set('Content-type', 'application/'.($is_xhr ? 'json' : 'x-javascript')); // Allow CORS requests only from dev machines diff --git a/plugins/MauticCitrixBundle/EventListener/FormSubscriber.php b/plugins/MauticCitrixBundle/EventListener/FormSubscriber.php index 1329f9442eb..68cfaaab1bc 100644 --- a/plugins/MauticCitrixBundle/EventListener/FormSubscriber.php +++ b/plugins/MauticCitrixBundle/EventListener/FormSubscriber.php @@ -131,11 +131,11 @@ private function _doRegistration(SubmissionEvent $event, $product, $startType = } $productsToRegister = self::_getProductsFromPost($actions, $fields, $post, $product); - if ($product === 'assist' || (0 !== count($productsToRegister))) { + if ('assist' === $product || (0 !== count($productsToRegister))) { $results = $submission->getResults(); // persist the new values - if ($product !== 'assist') { + if ('assist' !== $product) { // replace the submitted value with something more legible foreach ($productsToRegister as $productToRegister) { $results[$productToRegister['fieldName']] = $productToRegister['productTitle'].' ('.$productToRegister['productId'].')'; diff --git a/plugins/MauticCitrixBundle/EventListener/IntegrationRequestSubscriber.php b/plugins/MauticCitrixBundle/EventListener/IntegrationRequestSubscriber.php new file mode 100644 index 00000000000..72ef67d3c7c --- /dev/null +++ b/plugins/MauticCitrixBundle/EventListener/IntegrationRequestSubscriber.php @@ -0,0 +1,68 @@ + [ + 'getParameters', + 0, + ], + ]; + } + + /** + * @param PluginIntegrationRequestEvent $requestEvent + * + * @throws \Exception + */ + public function getParameters(PluginIntegrationRequestEvent $requestEvent) + { + if (false !== strpos($requestEvent->getUrl(), 'oauth/v2/token')) { + $authorization = $this->getAuthorization($requestEvent->getParameters()); + $requestEvent->setHeaders([ + 'Authorization' => sprintf('Basic %s', base64_encode($authorization)), + 'Content-Type' => 'application/x-www-form-urlencoded', + ]); + } + } + + /** + * @param array $parameters + * + * @return string + * + * @throws \Exception + */ + protected function getAuthorization(array $parameters) + { + if (!isset($parameters['client_id']) || empty($parameters['client_id'])) { + throw new \Exception('No client ID given.', 1554211764); + } + + if (!isset($parameters['client_secret']) || empty($parameters['client_secret'])) { + throw new \Exception('No client secret given.', 1554211808); + } + + return sprintf('%s:%s', $parameters['client_id'], $parameters['client_secret']); + } +} diff --git a/plugins/MauticCitrixBundle/EventListener/LeadSubscriber.php b/plugins/MauticCitrixBundle/EventListener/LeadSubscriber.php index b019ecdeaf7..ed9edc1e21d 100644 --- a/plugins/MauticCitrixBundle/EventListener/LeadSubscriber.php +++ b/plugins/MauticCitrixBundle/EventListener/LeadSubscriber.php @@ -12,7 +12,6 @@ namespace MauticPlugin\MauticCitrixBundle\EventListener; use Mautic\CoreBundle\EventListener\CommonSubscriber; -use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Event\LeadListFilteringEvent; use Mautic\LeadBundle\Event\LeadListFiltersChoicesEvent; use Mautic\LeadBundle\Event\LeadListFiltersOperatorsEvent; diff --git a/plugins/MauticCitrixBundle/Form/Type/CitrixCampaignActionType.php b/plugins/MauticCitrixBundle/Form/Type/CitrixCampaignActionType.php index 644b6d6d8dc..3b3bc20fd82 100644 --- a/plugins/MauticCitrixBundle/Form/Type/CitrixCampaignActionType.php +++ b/plugins/MauticCitrixBundle/Form/Type/CitrixCampaignActionType.php @@ -65,7 +65,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) $newChoices = []; foreach ($choices as $k => $c) { - if (strpos($k, $product) === 0) { + if (0 === strpos($k, $product)) { $newChoices[$k] = $c; } } diff --git a/plugins/MauticCitrixBundle/Helper/BasicEnum.php b/plugins/MauticCitrixBundle/Helper/BasicEnum.php index 98d99fbbd7e..6034588da1c 100644 --- a/plugins/MauticCitrixBundle/Helper/BasicEnum.php +++ b/plugins/MauticCitrixBundle/Helper/BasicEnum.php @@ -19,7 +19,7 @@ abstract class BasicEnum private static function getConstants() { - if (self::$constCacheArray === null) { + if (null === self::$constCacheArray) { self::$constCacheArray = []; } $calledClass = get_called_class(); diff --git a/plugins/MauticCitrixBundle/Helper/CitrixHelper.php b/plugins/MauticCitrixBundle/Helper/CitrixHelper.php index 4ef8afb4142..772cb3c15a8 100644 --- a/plugins/MauticCitrixBundle/Helper/CitrixHelper.php +++ b/plugins/MauticCitrixBundle/Helper/CitrixHelper.php @@ -319,7 +319,7 @@ public static function registerToProduct($product, $productId, $email, $firstnam { try { $response = []; - if ($product === CitrixProducts::GOTOWEBINAR) { + if (CitrixProducts::GOTOWEBINAR === $product) { $params = [ 'email' => $email, 'firstName' => $firstname, @@ -332,7 +332,7 @@ public static function registerToProduct($product, $productId, $email, $firstnam 'POST' ); } else { - if ($product === CitrixProducts::GOTOTRAINING) { + if (CitrixProducts::GOTOTRAINING === $product) { $params = [ 'email' => $email, 'givenName' => $firstname, @@ -368,21 +368,21 @@ public static function registerToProduct($product, $productId, $email, $firstnam public static function startToProduct($product, $productId, $email, $firstname, $lastname) { try { - if ($product === CitrixProducts::GOTOMEETING) { + if (CitrixProducts::GOTOMEETING === $product) { $response = self::getG2mApi()->request( 'meetings/'.$productId.'/start' ); return (is_array($response) && array_key_exists('hostURL', $response)) ? $response['hostURL'] : ''; } else { - if ($product === CitrixProducts::GOTOTRAINING) { + if (CitrixProducts::GOTOTRAINING === $product) { $response = self::getG2tApi()->request( 'trainings/'.$productId.'/start' ); return (is_array($response) && array_key_exists('hostURL', $response)) ? $response['hostURL'] : ''; } else { - if ($product === CitrixProducts::GOTOASSIST) { + if (CitrixProducts::GOTOASSIST === $product) { // TODO: use the sessioncallback to update attendance status /** @var Router $router */ $router = self::getContainer()->get('router'); diff --git a/plugins/MauticCitrixBundle/Integration/CitrixAbstractIntegration.php b/plugins/MauticCitrixBundle/Integration/CitrixAbstractIntegration.php index 5a551bb8601..7b6afee935b 100644 --- a/plugins/MauticCitrixBundle/Integration/CitrixAbstractIntegration.php +++ b/plugins/MauticCitrixBundle/Integration/CitrixAbstractIntegration.php @@ -36,7 +36,7 @@ public function setIntegrationSettings(Integration $settings) { //make sure URL does not have ending / $keys = $this->getDecryptedApiKeys($settings); - if (array_key_exists('url', $keys) && substr($keys['url'], -1) === '/') { + if (array_key_exists('url', $keys) && '/' === substr($keys['url'], -1)) { $keys['url'] = substr($keys['url'], 0, -1); $this->encryptAndSetApiKeys($keys, $settings); } @@ -62,8 +62,9 @@ public function getAuthenticationType() public function getRequiredKeyFields() { return [ - 'app_name' => 'mautic.citrix.form.appname', - 'client_id' => 'mautic.citrix.form.consumerkey', + 'app_name' => 'mautic.citrix.form.appname', + 'client_id' => 'mautic.citrix.form.clientid', + 'client_secret' => 'mautic.citrix.form.clientsecret', ]; } @@ -117,7 +118,7 @@ public function getApiUrl() */ public function getAccessTokenUrl() { - return $this->getApiUrl().'/oauth/access_token'; + return $this->getApiUrl().'/oauth/v2/token'; } /** @@ -127,7 +128,7 @@ public function getAccessTokenUrl() */ public function getAuthenticationUrl() { - return $this->getApiUrl().'/oauth/authorize'; + return $this->getApiUrl().'/oauth/v2/authorize'; } /** diff --git a/plugins/MauticCitrixBundle/Integration/GotomeetingIntegration.php b/plugins/MauticCitrixBundle/Integration/GotomeetingIntegration.php index fd47fd8e0bc..2ffcd95699c 100644 --- a/plugins/MauticCitrixBundle/Integration/GotomeetingIntegration.php +++ b/plugins/MauticCitrixBundle/Integration/GotomeetingIntegration.php @@ -1,6 +1,5 @@
    diff --git a/plugins/MauticCrmBundle/Controller/PipedriveController.php b/plugins/MauticCrmBundle/Controller/PipedriveController.php index 73767a5f729..54d65640ed3 100644 --- a/plugins/MauticCrmBundle/Controller/PipedriveController.php +++ b/plugins/MauticCrmBundle/Controller/PipedriveController.php @@ -84,8 +84,8 @@ public function webhookAction(Request $request) $companyImport->delete($params['previous']); break; case self::USER_UPDATE_EVENT: - $ownerImport = $this->getOwnerImport($pipedriveIntegration); - $ownerImport->create($data[0]); + $ownerImport = $this->getOwnerImport($pipedriveIntegration); + $ownerImport->create($data[0]); break; default: $response = [ @@ -96,12 +96,26 @@ public function webhookAction(Request $request) return new JsonResponse([ 'status' => 'error', 'message' => $e->getMessage(), - ], $e->getCode()); + ], $this->getErrorCodeFromException($e)); } return new JsonResponse($response, Response::HTTP_OK); } + /** + * Transform unknown Exception codes into 500 code. + * + * @param \Exception $e + * + * @return int + */ + private function getErrorCodeFromException(\Exception $e) + { + $code = $e->getCode(); + + return (is_int($code) && $code >= 400 && $code < 600) ? $code : 500; + } + /** * @param $integration * diff --git a/plugins/MauticCrmBundle/Integration/Pipedrive/Import/CompanyImport.php b/plugins/MauticCrmBundle/Integration/Pipedrive/Import/CompanyImport.php index b5ad6463115..905034f4e69 100644 --- a/plugins/MauticCrmBundle/Integration/Pipedrive/Import/CompanyImport.php +++ b/plugins/MauticCrmBundle/Integration/Pipedrive/Import/CompanyImport.php @@ -101,7 +101,7 @@ public function update(array $data = []) } /** @var Company $company */ - $company = $this->em->getRepository(Company::class)->findOneById($integrationEntity->getInternalEntityId()); + $company = $this->companyModel->getEntity($integrationEntity->getInternalEntityId()); // prevent listeners from exporting $company->setEventData('pipedrive.webhook', 1); @@ -113,7 +113,7 @@ public function update(array $data = []) $mappedData = $this->getMappedCompanyData($data); - $this->companyModel->setFieldValues($company, $mappedData); + $this->companyModel->setFieldValues($company, $mappedData, true); $this->companyModel->saveEntity($company); $integrationEntity->setLastSyncDate(new \DateTime()); diff --git a/plugins/MauticCrmBundle/Integration/Pipedrive/Import/LeadImport.php b/plugins/MauticCrmBundle/Integration/Pipedrive/Import/LeadImport.php index ee485475dc5..43f75d66b22 100644 --- a/plugins/MauticCrmBundle/Integration/Pipedrive/Import/LeadImport.php +++ b/plugins/MauticCrmBundle/Integration/Pipedrive/Import/LeadImport.php @@ -102,7 +102,7 @@ public function update(array $data = []) } /** @var Lead $lead * */ - $lead = $this->em->getRepository(Lead::class)->findOneById($integrationEntity->getInternalEntityId()); + $lead = $this->leadModel->getEntity($integrationEntity->getInternalEntityId()); // prevent listeners from exporting $lead->setEventData('pipedrive.webhook', 1); @@ -118,7 +118,7 @@ public function update(array $data = []) } //Do not push lead if contact was modified in Mautic, and we don't wanna mofify it $lead->setDateModified(new \DateTime()); - $this->leadModel->setFieldValues($lead, $dataToUpdate); + $this->leadModel->setFieldValues($lead, $dataToUpdate, true); if (!isset($data['owner_id']) && $lead->getOwner()) { $lead->setOwner(null); diff --git a/plugins/MauticCrmBundle/Integration/SugarcrmIntegration.php b/plugins/MauticCrmBundle/Integration/SugarcrmIntegration.php index c6d061f0ebe..dc38e0e3cc5 100644 --- a/plugins/MauticCrmBundle/Integration/SugarcrmIntegration.php +++ b/plugins/MauticCrmBundle/Integration/SugarcrmIntegration.php @@ -1254,7 +1254,7 @@ public function pushLeads($params = []) $config = $this->mergeConfigToFeatureSettings(); $integrationEntityRepo = $this->em->getRepository('MauticPluginBundle:IntegrationEntity'); $mauticData = $leadsToUpdate = $fields = []; - $fieldsToUpdateInSugar = isset($config['update_mautic']) ? array_keys($config['update_mautic'], 1) : []; + $fieldsToUpdateInSugar = isset($config['update_mautic']) ? array_keys($config['update_mautic'], 0) : []; $leadFields = $config['leadFields']; if (!empty($leadFields)) { if ($keys = array_keys($leadFields, 'mauticContactTimelineLink')) {