. */ namespace Xibo\Controller; use Carbon\Carbon; use Illuminate\Support\Str; use Psr\Http\Message\ResponseInterface; use Slim\Http\Response as Response; use Slim\Http\ServerRequest as Request; use Symfony\Component\EventDispatcher\EventDispatcher; use Xibo\Entity\ScheduleReminder; use Xibo\Event\ScheduleCriteriaRequestEvent; use Xibo\Factory\CampaignFactory; use Xibo\Factory\CommandFactory; use Xibo\Factory\DayPartFactory; use Xibo\Factory\DisplayFactory; use Xibo\Factory\DisplayGroupFactory; use Xibo\Factory\LayoutFactory; use Xibo\Factory\ScheduleCriteriaFactory; use Xibo\Factory\ScheduleExclusionFactory; use Xibo\Factory\ScheduleFactory; use Xibo\Factory\ScheduleReminderFactory; use Xibo\Factory\SyncGroupFactory; use Xibo\Helper\DateFormatHelper; use Xibo\Helper\Session; use Xibo\Support\Exception\AccessDeniedException; use Xibo\Support\Exception\ControllerNotImplemented; use Xibo\Support\Exception\GeneralException; use Xibo\Support\Exception\InvalidArgumentException; use Xibo\Support\Exception\NotFoundException; /** * Class Schedule * @package Xibo\Controller */ class Schedule extends Base { /** * @var Session */ private $session; /** * @var ScheduleFactory */ private $scheduleFactory; /** * @var ScheduleReminderFactory */ private $scheduleReminderFactory; /** * @var ScheduleExclusionFactory */ private $scheduleExclusionFactory; /** * @var DisplayGroupFactory */ private $displayGroupFactory; /** * @var CampaignFactory */ private $campaignFactory; /** * @var CommandFactory */ private $commandFactory; /** @var DisplayFactory */ private $displayFactory; /** @var LayoutFactory */ private $layoutFactory; /** @var DayPartFactory */ private $dayPartFactory; private SyncGroupFactory $syncGroupFactory; /** * Set common dependencies. * @param Session $session * @param ScheduleFactory $scheduleFactory * @param DisplayGroupFactory $displayGroupFactory * @param CampaignFactory $campaignFactory * @param CommandFactory $commandFactory * @param DisplayFactory $displayFactory * @param LayoutFactory $layoutFactory * @param DayPartFactory $dayPartFactory * @param ScheduleReminderFactory $scheduleReminderFactory * @param ScheduleExclusionFactory $scheduleExclusionFactory */ public function __construct( $session, $scheduleFactory, $displayGroupFactory, $campaignFactory, $commandFactory, $displayFactory, $layoutFactory, $dayPartFactory, $scheduleReminderFactory, $scheduleExclusionFactory, SyncGroupFactory $syncGroupFactory, private readonly ScheduleCriteriaFactory $scheduleCriteriaFactory ) { $this->session = $session; $this->scheduleFactory = $scheduleFactory; $this->displayGroupFactory = $displayGroupFactory; $this->campaignFactory = $campaignFactory; $this->commandFactory = $commandFactory; $this->displayFactory = $displayFactory; $this->layoutFactory = $layoutFactory; $this->dayPartFactory = $dayPartFactory; $this->scheduleReminderFactory = $scheduleReminderFactory; $this->scheduleExclusionFactory = $scheduleExclusionFactory; $this->syncGroupFactory = $syncGroupFactory; } /** * @param Request $request * @param Response $response * @return \Psr\Http\Message\ResponseInterface|Response * @throws GeneralException * @throws ControllerNotImplemented */ function displayPage(Request $request, Response $response) { // get the default longitude and latitude from CMS options $defaultLat = (float)$this->getConfig()->getSetting('DEFAULT_LAT'); $defaultLong = (float)$this->getConfig()->getSetting('DEFAULT_LONG'); $data = [ 'defaultLat' => $defaultLat, 'defaultLong' => $defaultLong, 'eventTypes' => \Xibo\Entity\Schedule::getEventTypes(), ]; // Render the Theme and output $this->getState()->template = 'schedule-page'; $this->getState()->setData($data); return $this->render($request, $response); } /** * Generates the calendar that we draw events on * @deprecated - Deprecated API: This endpoint will be removed in v5.0 * @SWG\Get( * path="/schedule/data/events", * operationId="scheduleCalendarData", * description="⚠️ This endpoint is deprecated and will be removed in v5.0.", * tags={"schedule"}, * deprecated=true, * @SWG\Parameter( * name="displayGroupIds", * description="The DisplayGroupIds to return the schedule for. [-1] for All.", * in="query", * type="array", * required=true, * @SWG\Items( * type="integer" * ) * ), * @SWG\Parameter( * name="from", * in="query", * required=false, * type="string", * description="From Date in Y-m-d H:i:s format, if not provided defaults to start of the current month" * ), * @SWG\Parameter( * name="to", * in="query", * required=false, * type="string", * description="To Date in Y-m-d H:i:s format, if not provided defaults to start of the next month" * ), * @SWG\Response( * response=200, * description="successful response", * @SWG\Schema( * type="array", * @SWG\Items(ref="#/definitions/ScheduleCalendarData") * ) * ) * ) * @param Request $request * @param Response $response * @return \Psr\Http\Message\ResponseInterface|Response * @throws InvalidArgumentException * @throws NotFoundException */ public function eventData(Request $request, Response $response) { $response = $response ->withHeader( 'Warning', '299 - "Deprecated API: /schedule/data/events will be removed in v5.0"' ); $this->getLog()->error('Deprecated API called: /schedule/data/events'); $this->setNoOutput(); $sanitizedParams = $this->getSanitizer($request->getParams()); $displayGroupIds = $sanitizedParams->getIntArray('displayGroupIds', ['default' => []]); $displaySpecificDisplayGroupIds = $sanitizedParams->getIntArray('displaySpecificGroupIds', ['default' => []]); $originalDisplayGroupIds = array_merge($displayGroupIds, $displaySpecificDisplayGroupIds); $campaignId = $sanitizedParams->getInt('campaignId'); $start = $sanitizedParams->getDate('from', ['default' => Carbon::now()->startOfMonth()]); $end = $sanitizedParams->getDate('to', ['default' => Carbon::now()->addMonth()->startOfMonth()]); if (count($originalDisplayGroupIds) <= 0) { return $response->withJson(['success' => 1, 'result' => []]); } // Setting for whether we show Layouts without permissions $showLayoutName = ($this->getConfig()->getSetting('SCHEDULE_SHOW_LAYOUT_NAME') == 1); // Permissions check the list of display groups with the user accessible list of display groups $resolvedDisplayGroupIds = array_diff($originalDisplayGroupIds, [-1]); if (!$this->getUser()->isSuperAdmin()) { $userDisplayGroupIds = array_map(function ($element) { /** @var \Xibo\Entity\DisplayGroup $element */ return $element->displayGroupId; }, $this->displayGroupFactory->query(null, ['isDisplaySpecific' => -1])); // Reset the list to only those display groups that intersect and if 0 have been provided, only those from // the user list $resolvedDisplayGroupIds = (count($originalDisplayGroupIds) > 0) ? array_intersect($originalDisplayGroupIds, $userDisplayGroupIds) : $userDisplayGroupIds; $this->getLog()->debug('Resolved list of display groups [' . json_encode($resolvedDisplayGroupIds) . '] from provided list [' . json_encode($originalDisplayGroupIds) . '] and user list [' . json_encode($userDisplayGroupIds) . ']'); // If we have none, then we do not return any events. if (count($resolvedDisplayGroupIds) <= 0) { return $response->withJson(['success' => 1, 'result' => []]); } } $events = []; $filter = [ 'futureSchedulesFrom' => $start->format('U'), 'futureSchedulesTo' => $end->format('U'), 'displayGroupIds' => $resolvedDisplayGroupIds, 'geoAware' => $sanitizedParams->getInt('geoAware'), 'recurring' => $sanitizedParams->getInt('recurring'), 'eventTypeId' => $sanitizedParams->getInt('eventTypeId'), 'name' => $sanitizedParams->getString('name'), 'useRegexForName' => $sanitizedParams->getCheckbox('useRegexForName'), 'logicalOperatorName' => $sanitizedParams->getString('logicalOperatorName'), ]; if ($campaignId != null) { // Is this an ad campaign? $campaign = $this->campaignFactory->getById($campaignId); if ($campaign->type === 'ad') { $filter['parentCampaignId'] = $campaignId; } else { $filter['campaignId'] = $campaignId; } } foreach ($this->scheduleFactory->query(['FromDT'], $filter) as $row) { /* @var \Xibo\Entity\Schedule $row */ // Generate this event try { $scheduleEvents = $row->getEvents($start, $end); } catch (GeneralException $e) { $this->getLog()->error('Unable to getEvents for ' . $row->eventId); continue; } if (count($scheduleEvents) <= 0) { continue; } $this->getLog()->debug('EventId ' . $row->eventId . ' as events: ' . json_encode($scheduleEvents)); // Load the display groups $row->load(); $displayGroupList = ''; if (count($row->displayGroups) >= 0) { $array = array_map(function ($object) { return $object->displayGroup; }, $row->displayGroups); $displayGroupList = implode(', ', $array); } // Event Permissions $editable = $this->getUser()->featureEnabled('schedule.modify') && $this->isEventEditable($row); // Event Title if ($this->isSyncEvent($row->eventTypeId)) { $title = sprintf( __('%s scheduled on sync group %s'), $row->getSyncTypeForEvent(), $row->getUnmatchedProperty('syncGroupName'), ); } else if ($row->campaignId == 0) { // Command $title = __('%s scheduled on %s', $row->command, $displayGroupList); } else { // Should we show the Layout name, or not (depending on permission) // Make sure we only run the below code if we have to, it's quite expensive if (!$showLayoutName && !$this->getUser()->isSuperAdmin()) { // Campaign $campaign = $this->campaignFactory->getById($row->campaignId); if (!$this->getUser()->checkViewable($campaign)) { $row->campaign = __('Private Item'); } } $title = sprintf( __('%s scheduled on %s'), $row->getUnmatchedProperty('parentCampaignName', $row->campaign), $displayGroupList ); if ($row->eventTypeId === \Xibo\Entity\Schedule::$INTERRUPT_EVENT) { $title .= __(' with Share of Voice %d seconds per hour', $row->shareOfVoice); } } // Day diff from start date to end date $diff = $end->diff($start)->days; // Show all Hourly repeats on the day view if ($row->recurrenceType == 'Minute' || ($diff > 1 && $row->recurrenceType == 'Hour')) { $title .= __(', Repeats every %s %s', $row->recurrenceDetail, $row->recurrenceType); } // Event URL $editUrlWeb = 'schedule.edit.form'; $editUrl = ($this->isApi($request)) ? 'schedule.edit' : $editUrlWeb; $url = ($editable) ? $this->urlFor($request, $editUrl, ['id' => $row->eventId]) : '#'; $days = []; // Event scheduled events foreach ($scheduleEvents as $scheduleEvent) { $this->getLog()->debug(sprintf('Parsing event dates from %s and %s', $scheduleEvent->fromDt, $scheduleEvent->toDt)); // Get the day of schedule start $fromDtDay = Carbon::createFromTimestamp($scheduleEvent->fromDt)->format('Y-m-d'); // Handle command events which do not have a toDt if ($row->eventTypeId == \Xibo\Entity\Schedule::$COMMAND_EVENT) { $scheduleEvent->toDt = $scheduleEvent->fromDt; } // Parse our dates into a Date object, so that we convert to local time correctly. $fromDt = Carbon::createFromTimestamp($scheduleEvent->fromDt); $toDt = Carbon::createFromTimestamp($scheduleEvent->toDt); // Set the row from/to date to be an ISO date for display $scheduleEvent->fromDt = Carbon::createFromTimestamp($scheduleEvent->fromDt) ->format(DateFormatHelper::getSystemFormat()); $scheduleEvent->toDt = Carbon::createFromTimestamp($scheduleEvent->toDt) ->format(DateFormatHelper::getSystemFormat()); $this->getLog()->debug(sprintf('Start date is ' . $fromDt->toRssString() . ' ' . $scheduleEvent->fromDt)); $this->getLog()->debug(sprintf('End date is ' . $toDt->toRssString() . ' ' . $scheduleEvent->toDt)); // For a minute/hourly repeating events show only 1 event per day if ($row->recurrenceType == 'Minute' || ($diff > 1 && $row->recurrenceType == 'Hour')) { if (array_key_exists($fromDtDay, $days)) { continue; } else { $days[$fromDtDay] = $scheduleEvent->fromDt; } } /** * @SWG\Definition( * definition="ScheduleCalendarData", * @SWG\Property( * property="id", * type="integer", * description="Event ID" * ), * @SWG\Property( * property="title", * type="string", * description="Event Title" * ), * @SWG\Property( * property="sameDay", * type="integer", * description="Does this event happen only on 1 day" * ), * @SWG\Property( * property="event", * ref="#/definitions/Schedule" * ) * ) */ $events[] = [ 'id' => $row->eventId, 'title' => $title, 'url' => ($editable) ? $url : null, 'start' => $fromDt->format('U') * 1000, 'end' => $toDt->format('U') * 1000, 'sameDay' => ($fromDt->day == $toDt->day && $fromDt->month == $toDt->month && $fromDt->year == $toDt->year), 'editable' => $editable, 'event' => $row, 'scheduleEvent' => $scheduleEvent, 'recurringEvent' => $row->recurrenceType != '' ]; } } return $response->withJson(['success' => 1, 'result' => $events]); } /** * Event List * @param Request $request * @param Response $response * @param $id * @return \Psr\Http\Message\ResponseInterface|Response * @throws AccessDeniedException * @throws GeneralException * @throws NotFoundException * @throws ControllerNotImplemented * @SWG\Get( * path="/schedule/{displayGroupId}/events", * operationId="scheduleCalendarDataDisplayGroup", * tags={"schedule"}, * @SWG\Parameter( * name="displayGroupId", * description="The DisplayGroupId to return the event list for.", * in="path", * type="integer", * required=true * ), * @SWG\Parameter( * name="singlePointInTime", * in="query", * required=false, * type="integer", * ), * @SWG\Parameter( * name="date", * in="query", * required=false, * type="string", * description="Date in Y-m-d H:i:s" * ), * @SWG\Parameter( * name="startDate", * in="query", * required=false, * type="string", * description="Date in Y-m-d H:i:s" * ), * @SWG\Parameter( * name="endDate", * in="query", * required=false, * type="string", * description="Date in Y-m-d H:i:s" * ), * @SWG\Response( * response=200, * description="successful response" * ) * ) */ public function eventList(Request $request, Response $response, $id) { $displayGroup = $this->displayGroupFactory->getById($id); $sanitizedParams = $this->getSanitizer($request->getParams()); if (!$this->getUser()->checkViewable($displayGroup)) { throw new AccessDeniedException(); } // Setting for whether we show Layouts with out permissions $showLayoutName = ($this->getConfig()->getSetting('SCHEDULE_SHOW_LAYOUT_NAME') == 1); $singlePointInTime = $sanitizedParams->getInt('singlePointInTime'); if ($singlePointInTime == 1) { $startDate = $sanitizedParams->getDate('date'); $endDate = $sanitizedParams->getDate('date'); } else { $startDate = $sanitizedParams->getDate('startDate'); $endDate = $sanitizedParams->getDate('endDate'); } // Reset the seconds $startDate->second(0); $endDate->second(0); $this->getLog()->debug( sprintf( 'Generating eventList for DisplayGroupId ' . $id . ' from date ' . $startDate->format(DateFormatHelper::getSystemFormat()) . ' to ' . $endDate->format(DateFormatHelper::getSystemFormat()) ) ); // Get a list of scheduled events $events = []; $displayGroups = []; $layouts = []; $campaigns = []; // Add the displayGroupId I am filtering for to the displayGroup object $displayGroups[$displayGroup->displayGroupId] = $displayGroup; // Is this group a display specific group, or a standalone? $options = []; /** @var \Xibo\Entity\Display $display */ $display = null; if ($displayGroup->isDisplaySpecific == 1) { // We should lookup the displayId for this group. $display = $this->displayFactory->getByDisplayGroupId($id)[0]; } else { $options['useGroupId'] = true; $options['displayGroupId'] = $id; } // Get list of events $scheduleForXmds = $this->scheduleFactory->getForXmds( ($display === null) ? null : $display->displayId, $startDate, $endDate, $options ); $this->getLog()->debug(count($scheduleForXmds) . ' events returned for displaygroup and date'); foreach ($scheduleForXmds as $event) { // Ignore command events if ($event['eventTypeId'] == \Xibo\Entity\Schedule::$COMMAND_EVENT) continue; // Ignore events that have a campaignId, but no layoutId (empty Campaigns) if ($event['layoutId'] == 0 && $event['campaignId'] != 0) continue; // Assess schedules $schedule = $this->scheduleFactory->createEmpty()->hydrate($event, ['intProperties' => ['isPriority', 'syncTimezone', 'displayOrder', 'fromDt', 'toDt']]); $schedule->load(); $this->getLog()->debug('EventId ' . $schedule->eventId . ' exists in the schedule window, checking its instances for activity'); // Get scheduled events based on recurrence try { $scheduleEvents = $schedule->getEvents($startDate, $endDate); } catch (GeneralException $e) { $this->getLog()->error('Unable to getEvents for ' . $schedule->eventId); continue; } // If this event is active, collect extra information and add to the events list if (count($scheduleEvents) > 0) { // Add the link to the schedule if (!$this->isApi($request)) { $route = 'schedule.edit.form'; $schedule->setUnmatchedProperty( 'link', $this->urlFor($request, $route, ['id' => $schedule->eventId]) ); } // Add the Layout if ($event['eventTypeId'] == \Xibo\Entity\Schedule::$SYNC_EVENT) { $layoutId = $event['syncLayoutId']; } else { $layoutId = $event['layoutId']; } $this->getLog()->debug('Adding this events layoutId [' . $layoutId . '] to list'); if ($layoutId != 0 && !array_key_exists($layoutId, $layouts)) { // Look up the layout details $layout = $this->layoutFactory->getById($layoutId); // Add the link to the layout if (!$this->isApi($request)) { // do not link to Layout Designer for Full screen Media/Playlist Layout. $link = (in_array($event['eventTypeId'], [7, 8])) ? '' : $this->urlFor($request, 'layout.designer', ['id' => $layout->layoutId]); $layout->setUnmatchedProperty( 'link', $link ); } if ($showLayoutName || $this->getUser()->checkViewable($layout)) { $layouts[$layoutId] = $layout; } else { $layouts[$layoutId] = [ 'layout' => __('Private Item') ]; } // Add the Campaign $layout->campaigns = $this->campaignFactory->getByLayoutId($layout->layoutId); if (count($layout->campaigns) > 0) { // Add to the campaigns array foreach ($layout->campaigns as $campaign) { if (!array_key_exists($campaign->campaignId, $campaigns)) { $campaigns[$campaign->campaignId] = $campaign; } } } } $event['campaign'] = is_object($layouts[$layoutId]) ? $layouts[$layoutId]->layout : $layouts[$layoutId]; // Display Group details $this->getLog()->debug('Adding this events displayGroupIds to list'); $schedule->excludeProperty('displayGroups'); foreach ($schedule->displayGroups as $scheduleDisplayGroup) { if (!array_key_exists($scheduleDisplayGroup->displayGroupId, $displayGroups)) { $displayGroups[$scheduleDisplayGroup->displayGroupId] = $scheduleDisplayGroup; } } // Determine the intermediate display groups $this->getLog()->debug('Adding this events intermediateDisplayGroupIds to list'); $schedule->setUnmatchedProperty( 'intermediateDisplayGroupIds', $this->calculateIntermediates($display, $displayGroup, $event['displayGroupId']) ); foreach ($schedule->getUnmatchedProperty('intermediateDisplayGroupIds') as $intermediate) { if (!array_key_exists($intermediate, $displayGroups)) { $displayGroups[$intermediate] = $this->displayGroupFactory->getById($intermediate); } } $this->getLog()->debug(sprintf('Adding scheduled events: ' . json_encode($scheduleEvents))); // We will never save this and we need the eventId on the agenda view $eventId = $schedule->eventId; foreach ($scheduleEvents as $scheduleEvent) { $schedule = clone $schedule; $schedule->eventId = $eventId; $schedule->fromDt = $scheduleEvent->fromDt; $schedule->toDt = $scheduleEvent->toDt; $schedule->setUnmatchedProperty('layoutId', intval($layoutId)); $schedule->setUnmatchedProperty('displayGroupId', intval($event['displayGroupId'])); $events[] = $schedule; } } else { $this->getLog()->debug('No activity inside window'); } } $this->getState()->hydrate([ 'data' => [ 'events' => $events, 'displayGroups' => $displayGroups, 'layouts' => $layouts, 'campaigns' => $campaigns ] ]); return $this->render($request, $response); } /** * @param \Xibo\Entity\Display $display * @param \Xibo\Entity\DisplayGroup $displayGroup * @param int $eventDisplayGroupId * @return array * @throws NotFoundException */ private function calculateIntermediates($display, $displayGroup, $eventDisplayGroupId) { $this->getLog()->debug('Calculating intermediates for events displayGroupId ' . $eventDisplayGroupId . ' viewing displayGroupId ' . $displayGroup->displayGroupId); $intermediates = []; $eventDisplayGroup = $this->displayGroupFactory->getById($eventDisplayGroupId); // Is the event scheduled directly on the displayGroup in question? if ($displayGroup->displayGroupId == $eventDisplayGroupId) return $intermediates; // Is the event scheduled directly on the display in question? if ($eventDisplayGroup->isDisplaySpecific == 1) return $intermediates; $this->getLog()->debug('Event isnt directly scheduled to a display or to the current displaygroup '); // There are nested groups involved, so we need to trace the relationship tree. if ($display === null) { $this->getLog()->debug('We are looking at a DisplayGroup'); // We are on a group. // Get the relationship tree for this display group $tree = $this->displayGroupFactory->getRelationShipTree($displayGroup->displayGroupId); foreach ($tree as $branch) { $this->getLog()->debug( 'Branch found: ' . $branch->displayGroup . ' [' . $branch->displayGroupId . '], ' . $branch->getUnmatchedProperty('depth') . '-' . $branch->getUnmatchedProperty('level') ); if ($branch->getUnmatchedProperty('depth') < 0 && $branch->displayGroupId != $eventDisplayGroup->displayGroupId ) { $intermediates[] = $branch->displayGroupId; } } } else { // We are on a display. $this->getLog()->debug('We are looking at a Display'); // We will need to get all of this displays groups and then add only those ones that give us an eventual // match on the events display group (complicated or what!) $display->load(); foreach ($display->displayGroups as $displayDisplayGroup) { // Ignore the display specific group if ($displayDisplayGroup->isDisplaySpecific == 1) continue; // Get the relationship tree for this display group $tree = $this->displayGroupFactory->getRelationShipTree($displayDisplayGroup->displayGroupId); $found = false; $possibleIntermediates = []; foreach ($tree as $branch) { $this->getLog()->debug( 'Branch found: ' . $branch->displayGroup . ' [' . $branch->displayGroupId . '], ' . $branch->getUnmatchedProperty('depth') . '-' . $branch->getUnmatchedProperty('level') ); if ($branch->displayGroupId != $eventDisplayGroup->displayGroupId) { $possibleIntermediates[] = $branch->displayGroupId; } if ($branch->displayGroupId != $eventDisplayGroup->displayGroupId && count($possibleIntermediates) > 0) $found = true; } if ($found) { $this->getLog()->debug('We have found intermediates ' . json_encode($possibleIntermediates) . ' for display when looking at displayGroupId ' . $displayDisplayGroup->displayGroupId); $intermediates = array_merge($intermediates, $possibleIntermediates); } } } $this->getLog()->debug('Returning intermediates: ' . json_encode($intermediates)); return $intermediates; } /** * Shows a form to add an event * @param Request $request * @param Response $response * @param string|null $from * @param int|null $id * @return ResponseInterface|Response * @throws ControllerNotImplemented * @throws GeneralException * @throws InvalidArgumentException * @throws NotFoundException */ public function addForm(Request $request, Response $response, ?string $from, ?int $id): Response|ResponseInterface { $sanitizedParams = $this->getSanitizer($request->getParams()); // get the default longitude and latitude from CMS options $defaultLat = (float)$this->getConfig()->getSetting('DEFAULT_LAT'); $defaultLong = (float)$this->getConfig()->getSetting('DEFAULT_LONG'); // Dispatch an event to initialize schedule criteria $event = new ScheduleCriteriaRequestEvent(); $this->getDispatcher()->dispatch($event, ScheduleCriteriaRequestEvent::$NAME); // Retrieve the criteria data from the event $criteria = $event->getCriteria(); $criteriaDefaultCondition = $event->getCriteriaDefaultCondition(); $addFormData = [ 'dayParts' => $this->dayPartFactory->allWithSystem(['isRetired' => 0]), 'reminders' => [], 'defaultLat' => $defaultLat, 'defaultLong' => $defaultLong, 'eventTypes' => \Xibo\Entity\Schedule::getEventTypes(), 'addForm' => true, 'relativeTime' => 0, 'setDisplaysFromFilter' => true, 'scheduleCriteria' => $criteria, 'criteriaDefaultCondition' => $criteriaDefaultCondition ]; $formNowData = []; if (!empty($from) && !empty($id)) { $formNowData = [ 'event' => [ 'eventTypeId' => $this->getEventTypeId($from), ], 'campaign' => ( ($from == 'Campaign' || $from == 'Layout') ? $this->campaignFactory->getById($id) : null ), 'displayGroups' => (($from == 'DisplayGroup') ? [$this->displayGroupFactory->getById($id)] : null), 'displayGroupIds' => (($from == 'DisplayGroup') ? [$id] : [0]), 'mediaId' => (($from === 'Library') ? $id : null), 'playlistId' => (($from === 'Playlist') ? $id : null), // Lock for layout editor only 'readonlySelect' => ($from == 'Layout' && $sanitizedParams->getString('fromLayoutEditor') === '1'), // Hide event type, except for Display Groups 'hideEventType' => !($from == 'DisplayGroup'), // Skip first step, except for Display Groups 'skipFirstStep' => !($from == 'DisplayGroup'), // If coming from display page, don't show syncEvent type 'eventTypes' => \Xibo\Entity\Schedule::getEventTypes((($from === 'DisplayGroup') ? [9] : [])), 'addForm' => true, 'fromCampaign' => ($from == 'Campaign'), 'relativeTime' => 1, 'setDisplaysFromFilter' => false, ]; } $formData = array_merge($addFormData, $formNowData); $this->getState()->template = 'schedule-form-edit'; $this->getState()->setData($formData); return $this->render($request, $response); } /** * Model to use for supplying key/value pairs to arrays * @SWG\Definition( * definition="ScheduleReminderArray", * @SWG\Property( * property="reminder_value", * type="integer" * ), * @SWG\Property( * property="reminder_type", * type="integer" * ), * @SWG\Property( * property="reminder_option", * type="integer" * ), * @SWG\Property( * property="reminder_isEmailHidden", * type="integer" * ) * ) */ /** * Add Event * @SWG\Post( * path="/schedule", * operationId="scheduleAdd", * tags={"schedule"}, * summary="Add Schedule Event", * description="Add a new scheduled event for a Campaign/Layout to be shown on a Display Group/Display.", * @SWG\Parameter( * name="eventTypeId", * in="formData", * description="The Event Type Id to use for this Event. * 1=Layout, 2=Command, 3=Overlay, 4=Interrupt, 5=Campaign, 6=Action, 7=Media Library, 8=Playlist", * type="integer", * required=true * ), * @SWG\Parameter( * name="campaignId", * in="formData", * description="The Campaign ID to use for this Event. * If a Layout is needed then the Campaign specific ID for that Layout should be used.", * type="integer", * required=false * ), * @SWG\Parameter( * name="fullScreenCampaignId", * in="formData", * description="For Media or Playlist event Type. The Layout specific Campaign ID to use for this Event. * This needs to be the Layout created with layout/fullscreen function", * type="integer", * required=false * ), * @SWG\Parameter( * name="commandId", * in="formData", * description="The Command ID to use for this Event.", * type="integer", * required=false * ), * @SWG\Parameter( * name="mediaId", * in="formData", * description="The Media ID to use for this Event.", * type="integer", * required=false * ), * @SWG\Parameter( * name="displayOrder", * in="formData", * description="The display order for this event. ", * type="integer", * required=true * ), * @SWG\Parameter( * name="isPriority", * in="formData", * description="An integer indicating the priority of this event. Normal events have a priority of 0.", * type="integer", * required=true * ), * @SWG\Parameter( * name="displayGroupIds", * in="formData", * description="The Display Group IDs for this event. Display specific Group IDs should be used to schedule on single displays.", * type="array", * required=true, * @SWG\Items(type="integer") * ), * @SWG\Parameter( * name="dayPartId", * in="formData", * description="The Day Part for this event. Overrides supported are 0(custom) and 1(always). Defaulted to 0.", * type="integer", * required=false * ), * @SWG\Parameter( * name="syncTimezone", * in="formData", * description="Should this schedule be synced to the resulting Display timezone?", * type="integer", * required=false * ), * @SWG\Parameter( * name="fromDt", * in="formData", * description="The from date for this event.", * type="string", * format="date-time", * required=true * ), * @SWG\Parameter( * name="toDt", * in="formData", * description="The to date for this event.", * type="string", * format="date-time", * required=false * ), * @SWG\Parameter( * name="recurrenceType", * in="formData", * description="The type of recurrence to apply to this event.", * type="string", * required=false, * enum={"", "Minute", "Hour", "Day", "Week", "Month", "Year"} * ), * @SWG\Parameter( * name="recurrenceDetail", * in="formData", * description="The interval for the recurrence.", * type="integer", * required=false * ), * @SWG\Parameter( * name="recurrenceRange", * in="formData", * description="The end date for this events recurrence.", * type="string", * format="date-time", * required=false * ), * @SWG\Parameter( * name="recurrenceRepeatsOn", * in="formData", * description="The days of the week that this event repeats - weekly only", * type="string", * format="array", * required=false, * @SWG\Items(type="integer") * ), * @SWG\Parameter( * name="scheduleReminders", * in="formData", * description="Array of Reminders for this event", * type="array", * required=false, * @SWG\Items( * ref="#/definitions/ScheduleReminderArray" * ) * ), * @SWG\Parameter( * name="isGeoAware", * in="formData", * description="Flag (0-1), whether this event is using Geo Location", * type="integer", * required=false * ), * @SWG\Parameter( * name="geoLocation", * in="formData", * description="Array of comma separated strings each with comma separated pair of coordinates", * type="array", * required=false, * @SWG\Items(type="string") * ), * @SWG\Parameter( * name="geoLocationJson", * in="formData", * description="Valid GeoJSON string, use as an alternative to geoLocation parameter", * type="string", * required=false * ), * @SWG\Parameter( * name="actionType", * in="formData", * description="For Action eventTypeId, the type of the action - command or navLayout", * type="string", * required=false * ), * @SWG\Parameter( * name="actionTriggerCode", * in="formData", * description="For Action eventTypeId, the webhook trigger code for the Action", * type="string", * required=false * ), * @SWG\Parameter( * name="actionLayoutCode", * in="formData", * description="For Action eventTypeId and navLayout actionType, the Layout Code identifier", * type="string", * required=false * ), * @SWG\Parameter( * name="dataSetId", * in="formData", * description="For Data Connector eventTypeId, the DataSet ID", * type="integer", * required=false * ), * @SWG\Parameter( * name="dataSetParams", * in="formData", * description="For Data Connector eventTypeId, the DataSet params", * type="string", * required=false * ), * @SWG\Response( * response=201, * description="successful operation", * @SWG\Schema(ref="#/definitions/Schedule"), * @SWG\Header( * header="Location", * description="Location of the new record", * type="string" * ) * ) * ) * * @param Request $request * @param Response $response * @return \Psr\Http\Message\ResponseInterface|Response * @throws ControllerNotImplemented * @throws GeneralException * @throws NotFoundException */ public function add(Request $request, Response $response) { $this->getLog()->debug('Add Schedule'); $sanitizedParams = $this->getSanitizer($request->getParams()); $embed = ($sanitizedParams->getString('embed') != null) ? explode(',', $sanitizedParams->getString('embed')) : []; // Get the custom day part to use as a default day part $customDayPart = $this->dayPartFactory->getCustomDayPart(); $schedule = $this->scheduleFactory->createEmpty(); $schedule->userId = $this->getUser()->userId; $schedule->eventTypeId = $sanitizedParams->getInt('eventTypeId'); $schedule->campaignId = $this->isFullScreenSchedule($schedule->eventTypeId) ? $sanitizedParams->getInt('fullScreenCampaignId') : $sanitizedParams->getInt('campaignId'); $schedule->commandId = $sanitizedParams->getInt('commandId'); $schedule->displayOrder = $sanitizedParams->getInt('displayOrder', ['default' => 0]); $schedule->isPriority = $sanitizedParams->getInt('isPriority', ['default' => 0]); $schedule->dayPartId = $sanitizedParams->getInt('dayPartId', ['default' => $customDayPart->dayPartId]); $schedule->isGeoAware = $sanitizedParams->getCheckbox('isGeoAware'); $schedule->actionType = $sanitizedParams->getString('actionType'); $schedule->actionTriggerCode = $sanitizedParams->getString('actionTriggerCode'); $schedule->actionLayoutCode = $sanitizedParams->getString('actionLayoutCode'); $schedule->maxPlaysPerHour = $sanitizedParams->getInt('maxPlaysPerHour', ['default' => 0]); $schedule->syncGroupId = $sanitizedParams->getInt('syncGroupId'); $schedule->name = $sanitizedParams->getString('name'); // Set the parentCampaignId for campaign events if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$CAMPAIGN_EVENT) { $schedule->parentCampaignId = $schedule->campaignId; // Make sure we're not directly scheduling an ad campaign $campaign = $this->campaignFactory->getById($schedule->campaignId); if ($campaign->type === 'ad') { throw new InvalidArgumentException( __('Direct scheduling of an Ad Campaign is not allowed'), 'campaignId' ); } } // Fields only collected for interrupt events if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$INTERRUPT_EVENT) { $schedule->shareOfVoice = $sanitizedParams->getInt('shareOfVoice', [ 'throw' => function () { new InvalidArgumentException( __('Share of Voice must be a whole number between 0 and 3600'), 'shareOfVoice' ); } ]); } else { $schedule->shareOfVoice = null; } // Fields only collected for data connector events if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$DATA_CONNECTOR_EVENT) { $schedule->dataSetId = $sanitizedParams->getInt('dataSetId', [ 'throw' => function () { new InvalidArgumentException( __('Please select a DataSet'), 'dataSetId' ); } ]); $schedule->dataSetParams = $sanitizedParams->getString('dataSetParams'); } // Create fullscreen layout for media/playlist events if ($this->isFullScreenSchedule($schedule->eventTypeId)) { $type = $schedule->eventTypeId === \Xibo\Entity\Schedule::$MEDIA_EVENT ? 'media' : 'playlist'; $id = ($type === 'media') ? $sanitizedParams->getInt('mediaId') : $sanitizedParams->getInt('playlistId'); if (!$id) { throw new InvalidArgumentException( sprintf('%sId is required when scheduling %s events.', ucfirst($type), $type) ); } $fsLayout = $this->layoutFactory->createFullScreenLayout( $type, $id, $sanitizedParams->getInt('resolutionId'), $sanitizedParams->getString('backgroundColor'), $sanitizedParams->getInt('layoutDuration'), ); $schedule->campaignId = $this->layoutFactory->getCampaignIdFromLayoutHistory($fsLayout->layoutId); $schedule->parentCampaignId = $schedule->campaignId; } // API request can provide an array of coordinates or valid GeoJSON, handle both cases here. if ($this->isApi($request) && $schedule->isGeoAware === 1) { if ($sanitizedParams->getArray('geoLocation') != null) { // get string array from API $coordinates = $sanitizedParams->getArray('geoLocation'); // generate GeoJSON and assign to Schedule $schedule->geoLocation = $this->createGeoJson($coordinates); } else { // we were provided with GeoJSON $schedule->geoLocation = $sanitizedParams->getString('geoLocationJson'); } } else { // if we are not using API, then valid GeoJSON is created in the front end. $schedule->geoLocation = $sanitizedParams->getString('geoLocation'); } // Workaround for cases where we're supplied 0 as the dayPartId (legacy custom dayPart) if ($schedule->dayPartId === 0) { $schedule->dayPartId = $customDayPart->dayPartId; } $schedule->syncTimezone = $sanitizedParams->getCheckbox('syncTimezone'); $schedule->syncEvent = $this->isSyncEvent($schedule->eventTypeId); $schedule->recurrenceType = $sanitizedParams->getString('recurrenceType'); $schedule->recurrenceDetail = $sanitizedParams->getInt('recurrenceDetail'); $recurrenceRepeatsOn = $sanitizedParams->getIntArray('recurrenceRepeatsOn'); $schedule->recurrenceRepeatsOn = (empty($recurrenceRepeatsOn)) ? null : implode(',', $recurrenceRepeatsOn); $schedule->recurrenceMonthlyRepeatsOn = $sanitizedParams->getInt( 'recurrenceMonthlyRepeatsOn', ['default' => 0] ); foreach ($sanitizedParams->getIntArray('displayGroupIds', ['default' => []]) as $displayGroupId) { $schedule->assignDisplayGroup($this->displayGroupFactory->getById($displayGroupId)); } if (!$schedule->isAlwaysDayPart()) { // Handle the dates $fromDt = $sanitizedParams->getDate('fromDt'); $toDt = $sanitizedParams->getDate('toDt'); $recurrenceRange = $sanitizedParams->getDate('recurrenceRange'); if ($fromDt === null) { throw new InvalidArgumentException(__('Please enter a from date'), 'fromDt'); } $logToDt = $toDt?->format(DateFormatHelper::getSystemFormat()); $logRecurrenceRange = $recurrenceRange?->format(DateFormatHelper::getSystemFormat()); $this->getLog()->debug( 'Times received are: FromDt=' . $fromDt->format(DateFormatHelper::getSystemFormat()) . '. ToDt=' . $logToDt . '. recurrenceRange=' . $logRecurrenceRange ); if (!$schedule->isCustomDayPart() && !$schedule->isAlwaysDayPart()) { // Daypart selected // expect only a start date (no time) $schedule->fromDt = $fromDt->startOfDay()->format('U'); $schedule->toDt = null; if ($recurrenceRange != null) { $schedule->recurrenceRange = $recurrenceRange->format('U'); } } else if (!($this->isApi($request) || Str::contains($this->getConfig()->getSetting('DATE_FORMAT'), 's'))) { // In some circumstances we want to trim the seconds from the provided dates. // this happens when the date format provided does not include seconds and when the add // event comes from the UI. $this->getLog()->debug('Date format does not include seconds, removing them'); $schedule->fromDt = $fromDt->setTime($fromDt->hour, $fromDt->minute, 0)->format('U'); if ($toDt !== null) { $schedule->toDt = $toDt->setTime($toDt->hour, $toDt->minute, 0)->format('U'); } if ($recurrenceRange != null) { $schedule->recurrenceRange = $recurrenceRange->setTime( $recurrenceRange->hour, $recurrenceRange->minute, 0 )->format('U'); } } else { $schedule->fromDt = $fromDt->format('U'); if ($toDt !== null) { $schedule->toDt = $toDt->format('U'); } if ($recurrenceRange != null) { $schedule->recurrenceRange = $recurrenceRange->format('U'); } } $logToDt = $toDt?->format(DateFormatHelper::getSystemFormat()); $logRecurrenceRange = $recurrenceRange?->format(DateFormatHelper::getSystemFormat()); $this->getLog()->debug( 'Processed times are: FromDt=' . $fromDt->format(DateFormatHelper::getSystemFormat()) . '. ToDt=' . $logToDt . '. recurrenceRange=' . $logRecurrenceRange ); } // Schedule Criteria $criteria = $sanitizedParams->getArray('criteria'); if (is_array($criteria) && count($criteria) > 0) { foreach ($criteria as $item) { $itemParams = $this->getSanitizer($item); $criterion = $this->scheduleCriteriaFactory->createEmpty(); $criterion->metric = $itemParams->getString('metric'); $criterion->type = $itemParams->getString('type'); $criterion->condition = $itemParams->getString('condition'); $criterion->value = $itemParams->getString('value'); $schedule->addOrUpdateCriteria($criterion); } } // Ready to do the add $schedule->setDisplayNotifyService($this->displayFactory->getDisplayNotifyService()); if ($schedule->campaignId != null) { $schedule->setCampaignFactory($this->campaignFactory); } $schedule->save(); $this->getLog()->debug('Add Schedule Reminder'); // API Request $rows = []; if ($this->isApi($request)) { $reminders = $sanitizedParams->getArray('scheduleReminders', ['default' => []]); foreach ($reminders as $i => $reminder) { $rows[$i]['reminder_value'] = (int) $reminder['reminder_value']; $rows[$i]['reminder_type'] = (int) $reminder['reminder_type']; $rows[$i]['reminder_option'] = (int) $reminder['reminder_option']; $rows[$i]['reminder_isEmailHidden'] = (int) $reminder['reminder_isEmailHidden']; } } else { for ($i=0; $i < count($sanitizedParams->getIntArray('reminder_value', ['default' => []])); $i++) { $rows[$i]['reminder_value'] = $sanitizedParams->getIntArray('reminder_value')[$i]; $rows[$i]['reminder_type'] = $sanitizedParams->getIntArray('reminder_type')[$i]; $rows[$i]['reminder_option'] = $sanitizedParams->getIntArray('reminder_option')[$i]; $rows[$i]['reminder_isEmailHidden'] = $sanitizedParams->getIntArray('reminder_isEmailHidden')[$i]; } } // Save new reminders foreach ($rows as $reminder) { // Do not add reminder if empty value provided for number of minute/hour if ($reminder['reminder_value'] == 0) { continue; } $scheduleReminder = $this->scheduleReminderFactory->createEmpty(); $scheduleReminder->scheduleReminderId = null; $scheduleReminder->eventId = $schedule->eventId; $scheduleReminder->value = $reminder['reminder_value']; $scheduleReminder->type = $reminder['reminder_type']; $scheduleReminder->option = $reminder['reminder_option']; $scheduleReminder->isEmail = $reminder['reminder_isEmailHidden']; $this->saveReminder($schedule, $scheduleReminder); } // We can get schedule reminders in an array if ($this->isApi($request)) { $schedule = $this->scheduleFactory->getById($schedule->eventId); $schedule->load([ 'loadScheduleReminders' => in_array('scheduleReminders', $embed), ]); } if ($this->isSyncEvent($schedule->eventTypeId)) { $syncGroup = $this->syncGroupFactory->getById($schedule->syncGroupId); $syncGroup->validateForSchedule($sanitizedParams); $schedule->updateSyncLinks($syncGroup, $sanitizedParams); } // Return $this->getState()->hydrate([ 'httpStatus' => 201, 'message' => __('Added Event'), 'id' => $schedule->eventId, 'data' => $schedule ]); return $this->render($request, $response); } /** * Shows a form to edit an event * @param Request $request * @param Response $response * @param $id * @return \Psr\Http\Message\ResponseInterface|Response * @throws AccessDeniedException * @throws GeneralException * @throws NotFoundException * @throws ControllerNotImplemented */ function editForm(Request $request, Response $response, $id) { $sanitizedParams = $this->getSanitizer($request->getParams()); // Recurring event start/end $eventStart = $sanitizedParams->getInt('eventStart', ['default' => 1000]) / 1000; $eventEnd = $sanitizedParams->getInt('eventEnd', ['default' => 1000]) / 1000; $schedule = $this->scheduleFactory->getById($id); $schedule->load(); // Dispatch an event to retrieve criteria for scheduling from the OpenWeatherMap connector. $event = new ScheduleCriteriaRequestEvent(); $this->getDispatcher()->dispatch($event, ScheduleCriteriaRequestEvent::$NAME); // Retrieve the data from the event $criteria = $event->getCriteria(); $criteriaDefaultCondition = $event->getCriteriaDefaultCondition(); if (!$this->isEventEditable($schedule)) { throw new AccessDeniedException(); } // Fix the event dates for display if ($schedule->isAlwaysDayPart()) { $schedule->fromDt = ''; $schedule->toDt = ''; } else { $schedule->fromDt = Carbon::createFromTimestamp($schedule->fromDt) ->format(DateFormatHelper::getSystemFormat()); if ($schedule->toDt != null) { $schedule->toDt = Carbon::createFromTimestamp($schedule->toDt) ->format(DateFormatHelper::getSystemFormat()); } } if ($schedule->recurrenceRange != null) { $schedule->recurrenceRange = Carbon::createFromTimestamp($schedule->recurrenceRange) ->format(DateFormatHelper::getSystemFormat()); } // Get all reminders $scheduleReminders = $this->scheduleReminderFactory->query(null, ['eventId' => $id]); // get the default longitude and latitude from CMS options $defaultLat = (float)$this->getConfig()->getSetting('DEFAULT_LAT'); $defaultLong = (float)$this->getConfig()->getSetting('DEFAULT_LONG'); if ($this->isFullScreenSchedule($schedule->eventTypeId)) { $schedule->setUnmatchedProperty('fullScreenCampaignId', $schedule->campaignId); if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$MEDIA_EVENT) { $schedule->setUnmatchedProperty( 'mediaId', $this->layoutFactory->getLinkedFullScreenMediaId($schedule->campaignId) ); } else if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$PLAYLIST_EVENT) { $schedule->setUnmatchedProperty( 'playlistId', $this->layoutFactory->getLinkedFullScreenPlaylistId($schedule->campaignId) ); } // Get the associated fullscreen layout $fsLayout = $this->layoutFactory->getById( $this->campaignFactory->getLinkedLayouts($schedule->campaignId)[0]->layoutId ); // Set the layout properties $schedule->backgroundColor = $fsLayout->backgroundColor; $schedule->layoutDuration = $fsLayout->duration; $schedule->resolutionId = $this->layoutFactory->getLayoutResolutionId($fsLayout)->resolutionId; } $this->getState()->template = 'schedule-form-edit'; $this->getState()->setData([ 'event' => $schedule, 'dayParts' => $this->dayPartFactory->allWithSystem(['isRetired' => 0]), 'displayGroups' => $schedule->displayGroups, 'campaign' => !empty($schedule->campaignId) ? $this->campaignFactory->getById($schedule->campaignId) : null, 'displayGroupIds' => array_map(function ($element) { return $element->displayGroupId; }, $schedule->displayGroups), 'addForm' => false, 'reminders' => $scheduleReminders, 'defaultLat' => $defaultLat, 'defaultLong' => $defaultLong, 'recurringEvent' => $schedule->recurrenceType != '', 'eventStart' => $eventStart, 'eventEnd' => $eventEnd, 'eventTypes' => \Xibo\Entity\Schedule::getEventTypes(), 'scheduleCriteria' => $criteria, 'criteriaDefaultCondition' => $criteriaDefaultCondition ]); return $this->render($request, $response); } /** * Shows the Delete a Recurring Event form * @param Request $request * @param Response $response * @param $id * @return \Psr\Http\Message\ResponseInterface|Response * @throws AccessDeniedException * @throws GeneralException * @throws NotFoundException * @throws ControllerNotImplemented */ public function deleteRecurrenceForm(Request $request, Response $response, $id) { $sanitizedParams = $this->getSanitizer($request->getParams()); // Recurring event start/end $eventStart = $sanitizedParams->getInt('eventStart', ['default' => 1000]); $eventEnd = $sanitizedParams->getInt('eventEnd', ['default' => 1000]); $schedule = $this->scheduleFactory->getById($id); $schedule->load(); if (!$this->isEventEditable($schedule)) { throw new AccessDeniedException(); } $this->getState()->template = 'schedule-recurrence-form-delete'; $this->getState()->setData([ 'event' => $schedule, 'eventStart' => $eventStart, 'eventEnd' => $eventEnd, ]); return $this->render($request, $response); } /** * Deletes a recurring Event from all displays * @param Request $request * @param Response $response * @param $id * @return \Psr\Http\Message\ResponseInterface|Response * @throws AccessDeniedException * @throws GeneralException * @throws NotFoundException * @throws ControllerNotImplemented * @SWG\Delete( * path="/schedulerecurrence/{eventId}", * operationId="schedulerecurrenceDelete", * tags={"schedule"}, * summary="Delete a Recurring Event", * description="Delete a Recurring Event of a Scheduled Event", * @SWG\Parameter( * name="eventId", * in="path", * description="The Scheduled Event ID", * type="integer", * required=true * ), * @SWG\Response( * response=204, * description="successful operation" * ) * ) */ public function deleteRecurrence(Request $request, Response $response, $id) { $schedule = $this->scheduleFactory->getById($id); $schedule->load(); if (!$this->isEventEditable($schedule)) { throw new AccessDeniedException(); } $sanitizedParams = $this->getSanitizer($request->getParams()); // Recurring event start/end $eventStart = $sanitizedParams->getInt('eventStart', ['default' => 1000]); $eventEnd = $sanitizedParams->getInt('eventEnd', ['default' => 1000]); $scheduleExclusion = $this->scheduleExclusionFactory->create($schedule->eventId, $eventStart, $eventEnd); $this->getLog()->debug('Create a schedule exclusion record'); $scheduleExclusion->save(); // Return $this->getState()->hydrate([ 'httpStatus' => 204, 'message' => __('Deleted Event') ]); return $this->render($request, $response); } /** * Edits an event * @param Request $request * @param Response $response * @param $id * @return \Psr\Http\Message\ResponseInterface|Response * @throws AccessDeniedException * @throws GeneralException * @throws NotFoundException * @throws ControllerNotImplemented * @SWG\Put( * path="/schedule/{eventId}", * operationId="scheduleEdit", * tags={"schedule"}, * summary="Edit Schedule Event", * description="Edit a scheduled event for a Campaign/Layout to be shown on a Display Group/Display.", * @SWG\Parameter( * name="eventId", * in="path", * description="The Scheduled Event ID", * type="integer", * required=true * ), * @SWG\Parameter( * name="eventTypeId", * in="formData", * description="The Event Type Id to use for this Event. * 1=Layout, 2=Command, 3=Overlay, 4=Interrupt, 5=Campaign, 6=Action", * type="integer", * required=true * ), * @SWG\Parameter( * name="campaignId", * in="formData", * description="The Campaign ID to use for this Event. * If a Layout is needed then the Campaign specific ID for that Layout should be used.", * type="integer", * required=false * ), * @SWG\Parameter( * name="commandId", * in="formData", * description="The Command ID to use for this Event.", * type="integer", * required=false * ), * @SWG\Parameter( * name="mediaId", * in="formData", * description="The Media ID to use for this Event.", * type="integer", * required=false * ), * @SWG\Parameter( * name="displayOrder", * in="formData", * description="The display order for this event. ", * type="integer", * required=true * ), * @SWG\Parameter( * name="isPriority", * in="formData", * description="An integer indicating the priority of this event. Normal events have a priority of 0.", * type="integer", * required=true * ), * @SWG\Parameter( * name="displayGroupIds", * in="formData", * description="The Display Group IDs for this event. * Display specific Group IDs should be used to schedule on single displays.", * type="array", * required=true, * @SWG\Items(type="integer") * ), * @SWG\Parameter( * name="dayPartId", * in="formData", * description="The Day Part for this event. Overrides supported are 0(custom) and 1(always). Defaulted to 0.", * type="integer", * required=false * ), * @SWG\Parameter( * name="syncTimezone", * in="formData", * description="Should this schedule be synced to the resulting Display timezone?", * type="integer", * required=false * ), * @SWG\Parameter( * name="fromDt", * in="formData", * description="The from date for this event.", * type="string", * format="date-time", * required=true * ), * @SWG\Parameter( * name="toDt", * in="formData", * description="The to date for this event.", * type="string", * format="date-time", * required=false * ), * @SWG\Parameter( * name="recurrenceType", * in="formData", * description="The type of recurrence to apply to this event.", * type="string", * required=false, * enum={"", "Minute", "Hour", "Day", "Week", "Month", "Year"} * ), * @SWG\Parameter( * name="recurrenceDetail", * in="formData", * description="The interval for the recurrence.", * type="integer", * required=false * ), * @SWG\Parameter( * name="recurrenceRange", * in="formData", * description="The end date for this events recurrence.", * type="string", * format="date-time", * required=false * ), * @SWG\Parameter( * name="recurrenceRepeatsOn", * in="formData", * description="The days of the week that this event repeats - weekly only", * type="string", * format="array", * required=false, * @SWG\Items(type="integer") * ), * @SWG\Parameter( * name="scheduleReminders", * in="formData", * description="Array of Reminders for this event", * type="array", * required=false, * @SWG\Items( * ref="#/definitions/ScheduleReminderArray" * ) * ), * @SWG\Parameter( * name="isGeoAware", * in="formData", * description="Flag (0-1), whether this event is using Geo Location", * type="integer", * required=false * ), * @SWG\Parameter( * name="geoLocation", * in="formData", * description="Array of comma separated strings each with comma separated pair of coordinates", * type="array", * required=false, * @SWG\Items(type="string") * ), * @SWG\Parameter( * name="geoLocationJson", * in="formData", * description="Valid GeoJSON string, use as an alternative to geoLocation parameter", * type="string", * required=false * ), * @SWG\Parameter( * name="actionType", * in="formData", * description="For Action eventTypeId, the type of the action - command or navLayout", * type="string", * required=false * ), * @SWG\Parameter( * name="actionTriggerCode", * in="formData", * description="For Action eventTypeId, the webhook trigger code for the Action", * type="string", * required=false * ), * @SWG\Parameter( * name="actionLayoutCode", * in="formData", * description="For Action eventTypeId and navLayout actionType, the Layout Code identifier", * type="string", * required=false * ), * @SWG\Parameter( * name="dataSetId", * in="formData", * description="For Data Connector eventTypeId, the DataSet ID", * type="integer", * required=false * ), * @SWG\Parameter( * name="dataSetParams", * in="formData", * description="For Data Connector eventTypeId, the DataSet params", * type="string", * required=false * ), * @SWG\Response( * response=200, * description="successful operation", * @SWG\Schema(ref="#/definitions/Schedule") * ) * ) */ public function edit(Request $request, Response $response, $id) { $sanitizedParams = $this->getSanitizer($request->getParams()); $embed = ($sanitizedParams->getString('embed') != null) ? explode(',', $sanitizedParams->getString('embed')) : []; $schedule = $this->scheduleFactory->getById($id); $oldSchedule = clone $schedule; $schedule->load([ 'loadScheduleReminders' => in_array('scheduleReminders', $embed), ]); if (!$this->isEventEditable($schedule)) { throw new AccessDeniedException(); } $schedule->eventTypeId = $sanitizedParams->getInt('eventTypeId'); $schedule->campaignId = $this->isFullScreenSchedule($schedule->eventTypeId) ? $sanitizedParams->getInt('fullScreenCampaignId') : $sanitizedParams->getInt('campaignId'); // displayOrder and isPriority: if present but empty (""): set to 0 // if missing from form: keep existing value (fallback to 0 if unset) $schedule->displayOrder = $sanitizedParams->hasParam('displayOrder') ? $sanitizedParams->getInt('displayOrder', ['default' => 0]) : ($schedule->displayOrder ?? 0); $schedule->isPriority = $sanitizedParams->hasParam('isPriority') ? $sanitizedParams->getInt('isPriority', ['default' => 0]) : ($schedule->isPriority ?? 0); $schedule->dayPartId = $sanitizedParams->getInt('dayPartId', ['default' => $schedule->dayPartId]); $schedule->syncTimezone = $sanitizedParams->getCheckbox('syncTimezone'); $schedule->syncEvent = $this->isSyncEvent($schedule->eventTypeId); $schedule->recurrenceType = $sanitizedParams->getString('recurrenceType'); $schedule->recurrenceDetail = $sanitizedParams->getInt('recurrenceDetail'); $recurrenceRepeatsOn = $sanitizedParams->getIntArray('recurrenceRepeatsOn'); $schedule->recurrenceRepeatsOn = (empty($recurrenceRepeatsOn)) ? null : implode(',', $recurrenceRepeatsOn); $schedule->recurrenceMonthlyRepeatsOn = $sanitizedParams->getInt( 'recurrenceMonthlyRepeatsOn', ['default' => 0] ); $schedule->displayGroups = []; $schedule->isGeoAware = $sanitizedParams->getCheckbox('isGeoAware'); $schedule->maxPlaysPerHour = $sanitizedParams->getInt('maxPlaysPerHour', ['default' => 0]); $schedule->syncGroupId = $sanitizedParams->getInt('syncGroupId'); $schedule->name = $sanitizedParams->getString('name'); $schedule->modifiedBy = $this->getUser()->getId(); // collect action event relevant properties only on action event // null these properties otherwise if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$ACTION_EVENT) { $schedule->actionType = $sanitizedParams->getString('actionType'); $schedule->actionTriggerCode = $sanitizedParams->getString('actionTriggerCode'); $schedule->commandId = $sanitizedParams->getInt('commandId'); $schedule->actionLayoutCode = $sanitizedParams->getString('actionLayoutCode'); $schedule->campaignId = null; } else { $schedule->actionType = null; $schedule->actionTriggerCode = null; $schedule->commandId = null; $schedule->actionLayoutCode = null; } // collect commandId on Command event // Retain existing commandId value otherwise if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$COMMAND_EVENT) { $schedule->commandId = $sanitizedParams->getInt('commandId'); $schedule->campaignId = null; } // Set the parentCampaignId for campaign events // null parentCampaignId on other events // make sure correct Layout/Campaign is selected for relevant event. if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$CAMPAIGN_EVENT) { $schedule->parentCampaignId = $schedule->campaignId; // Make sure we're not directly scheduling an ad campaign $campaign = $this->campaignFactory->getById($schedule->campaignId); if ($campaign->type === 'ad') { throw new InvalidArgumentException( __('Direct scheduling of an Ad Campaign is not allowed'), 'campaignId' ); } if ($campaign->isLayoutSpecific === 1) { throw new InvalidArgumentException( __('Cannot schedule Layout as a Campaign, please select a Campaign instead.'), 'campaignId' ); } } else { $schedule->parentCampaignId = null; if (!empty($schedule->campaignId)) { $campaign = $this->campaignFactory->getById($schedule->campaignId); if ($campaign->isLayoutSpecific === 0) { throw new InvalidArgumentException( __('Cannot schedule Campaign in selected event type, please select a Layout instead.'), 'campaignId' ); } } } // Fields only collected for interrupt events if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$INTERRUPT_EVENT) { $schedule->shareOfVoice = $sanitizedParams->getInt('shareOfVoice', [ 'throw' => function () { new InvalidArgumentException( __('Share of Voice must be a whole number between 0 and 3600'), 'shareOfVoice' ); } ]); } else { $schedule->shareOfVoice = null; } // Fields only collected for data connector events if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$DATA_CONNECTOR_EVENT) { $schedule->dataSetId = $sanitizedParams->getInt('dataSetId', [ 'throw' => function () { new InvalidArgumentException( __('Please select a DataSet'), 'dataSetId' ); } ]); $schedule->dataSetParams = $sanitizedParams->getString('dataSetParams'); } // Get the campaignId for media/playlist events if ($this->isFullScreenSchedule($schedule->eventTypeId)) { $type = $schedule->eventTypeId === \Xibo\Entity\Schedule::$MEDIA_EVENT ? 'media' : 'playlist'; $id = ($type === 'media') ? $sanitizedParams->getInt('mediaId') : $sanitizedParams->getInt('playlistId'); if (!$id) { throw new InvalidArgumentException( sprintf('%sId is required when scheduling %s events.', ucfirst($type), $type) ); } // Create a full screen layout for this event $fsLayout = $this->layoutFactory->createFullScreenLayout( $type, $id, $sanitizedParams->getInt('resolutionId'), $sanitizedParams->getString('backgroundColor'), $sanitizedParams->getInt('layoutDuration'), ); $schedule->campaignId = $this->layoutFactory->getCampaignIdFromLayoutHistory($fsLayout->layoutId); $schedule->parentCampaignId = $schedule->campaignId; } // API request can provide an array of coordinates or valid GeoJSON, handle both cases here. if ($this->isApi($request) && $schedule->isGeoAware === 1) { if ($sanitizedParams->getArray('geoLocation') != null) { // get string array from API $coordinates = $sanitizedParams->getArray('geoLocation'); // generate GeoJSON and assign to Schedule $schedule->geoLocation = $this->createGeoJson($coordinates); } else { // we were provided with GeoJSON $schedule->geoLocation = $sanitizedParams->getString('geoLocationJson'); } } else { // if we are not using API, then valid GeoJSON is created in the front end. $schedule->geoLocation = $sanitizedParams->getString('geoLocation'); } // if we are editing Layout/Campaign event that was set with Always daypart and change it to Command event type // the daypartId will remain as always, which will then cause the event to "disappear" from calendar // https://github.com/xibosignage/xibo/issues/1982 if ($schedule->eventTypeId == \Xibo\Entity\Schedule::$COMMAND_EVENT) { $schedule->dayPartId = $this->dayPartFactory->getCustomDayPart()->dayPartId; } foreach ($sanitizedParams->getIntArray('displayGroupIds', ['default' => []]) as $displayGroupId) { $schedule->assignDisplayGroup($this->displayGroupFactory->getById($displayGroupId)); } if (!$schedule->isAlwaysDayPart()) { // Handle the dates $fromDt = $sanitizedParams->getDate('fromDt'); $toDt = $sanitizedParams->getDate('toDt'); $recurrenceRange = $sanitizedParams->getDate('recurrenceRange'); if ($fromDt === null) { throw new InvalidArgumentException(__('Please enter a from date'). 'fromDt'); } $logToDt = $toDt?->format(DateFormatHelper::getSystemFormat()); $logRecurrenceRange = $recurrenceRange?->format(DateFormatHelper::getSystemFormat()); $this->getLog()->debug( 'Times received are: FromDt=' . $fromDt->format(DateFormatHelper::getSystemFormat()) . '. ToDt=' . $logToDt . '. recurrenceRange=' . $logRecurrenceRange ); if (!$schedule->isCustomDayPart() && !$schedule->isAlwaysDayPart()) { // Daypart selected // expect only a start date (no time) $schedule->fromDt = $fromDt->startOfDay()->format('U'); $schedule->toDt = null; $schedule->recurrenceRange = ($recurrenceRange === null) ? null : $recurrenceRange->format('U'); } else if (!($this->isApi($request) || Str::contains($this->getConfig()->getSetting('DATE_FORMAT'), 's'))) { // In some circumstances we want to trim the seconds from the provided dates. // this happens when the date format provided does not include seconds and when the add // event comes from the UI. $this->getLog()->debug('Date format does not include seconds, removing them'); $schedule->fromDt = $fromDt->setTime($fromDt->hour, $fromDt->minute, 0)->format('U'); // If we have a toDt if ($toDt !== null) { $schedule->toDt = $toDt->setTime($toDt->hour, $toDt->minute, 0)->format('U'); } $schedule->recurrenceRange = ($recurrenceRange === null) ? null : $recurrenceRange->setTime($recurrenceRange->hour, $recurrenceRange->minute, 0)->format('U'); } else { $schedule->fromDt = $fromDt->format('U'); if ($toDt !== null) { $schedule->toDt = $toDt->format('U'); } $schedule->recurrenceRange = ($recurrenceRange === null) ? null : $recurrenceRange->format('U'); } $this->getLog()->debug('Processed start is: FromDt=' . $fromDt->toRssString()); } else { // This is an always day part, which cannot be recurring, make sure we clear the recurring type if it has been set $schedule->recurrenceType = null; } // Schedule Criteria $schedule->criteria = []; $criteria = $sanitizedParams->getArray('criteria'); if (is_array($criteria)) { foreach ($criteria as $item) { $itemParams = $this->getSanitizer($item); $criterion = $this->scheduleCriteriaFactory->createEmpty(); $criterion->metric = $itemParams->getString('metric'); $criterion->type = $itemParams->getString('type'); $criterion->condition = $itemParams->getString('condition'); $criterion->value = $itemParams->getString('value'); $schedule->addOrUpdateCriteria($criterion, $itemParams->getInt('id')); } } // Ready to do the add $schedule->setDisplayNotifyService($this->displayFactory->getDisplayNotifyService()); if ($schedule->campaignId != null) { $schedule->setCampaignFactory($this->campaignFactory); } $schedule->save(); if ($this->isSyncEvent($schedule->eventTypeId)) { $syncGroup = $this->syncGroupFactory->getById($schedule->syncGroupId); $syncGroup->validateForSchedule($sanitizedParams); $schedule->updateSyncLinks($syncGroup, $sanitizedParams); } // Get form reminders $rows = []; for ($i=0; $i < count($sanitizedParams->getIntArray('reminder_value',['default' => []])); $i++) { $entry = []; if ($sanitizedParams->getIntArray('reminder_scheduleReminderId')[$i] == null) { continue; } $entry['reminder_scheduleReminderId'] = $sanitizedParams->getIntArray('reminder_scheduleReminderId')[$i]; $entry['reminder_value'] = $sanitizedParams->getIntArray('reminder_value')[$i]; $entry['reminder_type'] = $sanitizedParams->getIntArray('reminder_type')[$i]; $entry['reminder_option'] = $sanitizedParams->getIntArray('reminder_option')[$i]; $entry['reminder_isEmail'] = $sanitizedParams->getIntArray('reminder_isEmailHidden')[$i]; $rows[$sanitizedParams->getIntArray('reminder_scheduleReminderId')[$i]] = $entry; } $formReminders = $rows; // Compare to delete // Get existing db reminders $scheduleReminders = $this->scheduleReminderFactory->query(null, ['eventId' => $id]); $rows = []; foreach ($scheduleReminders as $reminder) { $entry = []; $entry['reminder_scheduleReminderId'] = $reminder->scheduleReminderId; $entry['reminder_value'] = $reminder->value; $entry['reminder_type'] = $reminder->type; $entry['reminder_option'] = $reminder->option; $entry['reminder_isEmail'] = $reminder->isEmail; $rows[$reminder->scheduleReminderId] = $entry; } $dbReminders = $rows; $deleteReminders = $schedule->compareMultidimensionalArrays($dbReminders, $formReminders, false); foreach ($deleteReminders as $reminder) { $reminder = $this->scheduleReminderFactory->getById($reminder['reminder_scheduleReminderId']); $reminder->delete(); } // API Request $rows = []; if ($this->isApi($request)) { $reminders = $sanitizedParams->getArray('scheduleReminders', ['default' => []]); foreach ($reminders as $i => $reminder) { $rows[$i]['reminder_scheduleReminderId'] = isset($reminder['reminder_scheduleReminderId']) ? (int) $reminder['reminder_scheduleReminderId'] : null; $rows[$i]['reminder_value'] = (int) $reminder['reminder_value']; $rows[$i]['reminder_type'] = (int) $reminder['reminder_type']; $rows[$i]['reminder_option'] = (int) $reminder['reminder_option']; $rows[$i]['reminder_isEmailHidden'] = (int) $reminder['reminder_isEmailHidden']; } } else { for ($i=0; $i < count($sanitizedParams->getIntArray('reminder_value', ['default' => []])); $i++) { $rows[$i]['reminder_scheduleReminderId'] = $sanitizedParams->getIntArray('reminder_scheduleReminderId')[$i]; $rows[$i]['reminder_value'] = $sanitizedParams->getIntArray('reminder_value')[$i]; $rows[$i]['reminder_type'] = $sanitizedParams->getIntArray('reminder_type')[$i]; $rows[$i]['reminder_option'] = $sanitizedParams->getIntArray('reminder_option')[$i]; $rows[$i]['reminder_isEmailHidden'] = $sanitizedParams->getIntArray('reminder_isEmailHidden')[$i]; } } // Save rest of the reminders foreach ($rows as $reminder) { // Do not add reminder if empty value provided for number of minute/hour if ($reminder['reminder_value'] == 0) { continue; } $scheduleReminderId = isset($reminder['reminder_scheduleReminderId']) ? $reminder['reminder_scheduleReminderId'] : null; try { $scheduleReminder = $this->scheduleReminderFactory->getById($scheduleReminderId); $scheduleReminder->load(); } catch (NotFoundException $e) { $scheduleReminder = $this->scheduleReminderFactory->createEmpty(); $scheduleReminder->scheduleReminderId = null; $scheduleReminder->eventId = $id; } $scheduleReminder->value = $reminder['reminder_value']; $scheduleReminder->type = $reminder['reminder_type']; $scheduleReminder->option = $reminder['reminder_option']; $scheduleReminder->isEmail = $reminder['reminder_isEmailHidden']; $this->saveReminder($schedule, $scheduleReminder); } // If this is a recurring event delete all schedule exclusions if ($schedule->recurrenceType != '') { // Delete schedule exclusions $scheduleExclusions = $this->scheduleExclusionFactory->query(null, ['eventId' => $schedule->eventId]); foreach ($scheduleExclusions as $exclusion) { $exclusion->delete(); } } // Return $this->getState()->hydrate([ 'message' => __('Edited Event'), 'id' => $schedule->eventId, 'data' => $schedule ]); return $this->render($request, $response); } /** * Shows the DeleteEvent form * @param Request $request * @param Response $response * @param $id * @return \Psr\Http\Message\ResponseInterface|Response * @throws AccessDeniedException * @throws GeneralException * @throws NotFoundException * @throws ControllerNotImplemented */ function deleteForm(Request $request, Response $response, $id) { $schedule = $this->scheduleFactory->getById($id); $schedule->load(); if (!$this->isEventEditable($schedule)) { throw new AccessDeniedException(); } $this->getState()->template = 'schedule-form-delete'; $this->getState()->setData([ 'event' => $schedule, ]); return $this->render($request,$response); } /** * Deletes an Event from all displays * @param Request $request * @param Response $response * @param $id * @return \Psr\Http\Message\ResponseInterface|Response * @throws AccessDeniedException * @throws GeneralException * @throws NotFoundException * @throws ControllerNotImplemented * @SWG\Delete( * path="/schedule/{eventId}", * operationId="scheduleDelete", * tags={"schedule"}, * summary="Delete Event", * description="Delete a Scheduled Event", * @SWG\Parameter( * name="eventId", * in="path", * description="The Scheduled Event ID", * type="integer", * required=true * ), * @SWG\Response( * response=204, * description="successful operation" * ) * ) */ public function delete(Request $request, Response $response, $id) { $schedule = $this->scheduleFactory->getById($id); $schedule->load(); if (!$this->isEventEditable($schedule)) { throw new AccessDeniedException(); } $schedule ->setDisplayNotifyService($this->displayFactory->getDisplayNotifyService()) ->delete(); // Return $this->getState()->hydrate([ 'httpStatus' => 204, 'message' => __('Deleted Event') ]); return $this->render($request, $response); } /** * Is this event editable? * @param \Xibo\Entity\Schedule $event * @return bool * @throws \Xibo\Support\Exception\InvalidArgumentException */ private function isEventEditable(\Xibo\Entity\Schedule $event): bool { if (!$this->getUser()->featureEnabled('schedule.modify')) { return false; } // Is this an event coming from an ad campaign? if (!empty($event->parentCampaignId) && $event->eventTypeId === \Xibo\Entity\Schedule::$INTERRUPT_EVENT) { return false; } $scheduleWithView = ($this->getConfig()->getSetting('SCHEDULE_WITH_VIEW_PERMISSION') == 1); // Work out if this event is editable or not. To do this we need to compare the permissions // of each display group this event is associated with foreach ($event->displayGroups as $displayGroup) { // Can schedule with view, but no view permissions if ($scheduleWithView && !$this->getUser()->checkViewable($displayGroup)) { return false; } // Can't schedule with view, but no edit permissions if (!$scheduleWithView && !$this->getUser()->checkEditable($displayGroup)) { return false; } } return true; } /** * Return Schedule eventTypeId based on the grid the requests is coming from * @param $from * @return int */ private function getEventTypeId($from) { return match ($from) { 'Campaign' => \Xibo\Entity\Schedule::$CAMPAIGN_EVENT, 'Library' => \Xibo\Entity\Schedule::$MEDIA_EVENT, 'Playlist' => \Xibo\Entity\Schedule::$PLAYLIST_EVENT, default => \Xibo\Entity\Schedule::$LAYOUT_EVENT }; } /** * Generates the Schedule events grid * * @SWG\Get( * path="/schedule", * operationId="scheduleSearch", * tags={"schedule"}, * @SWG\Parameter( * name="eventTypeId", * in="query", * required=false, * type="integer", * description="Filter grid by eventTypeId. * 1=Layout, 2=Command, 3=Overlay, 4=Interrupt, 5=Campaign, 6=Action, 7=Media Library, 8=Playlist" * ), * @SWG\Parameter( * name="fromDt", * in="query", * required=false, * type="string", * description="From Date in Y-m-d H:i:s format" * ), * @SWG\Parameter( * name="toDt", * in="query", * required=false, * type="string", * description="To Date in Y-m-d H:i:s format" * ), * @SWG\Parameter( * name="geoAware", * in="query", * required=false, * type="integer", * description="Flag (0-1), whether to return events using Geo Location" * ), * @SWG\Parameter( * name="recurring", * in="query", * required=false, * type="integer", * description="Flag (0-1), whether to return Recurring events" * ), * @SWG\Parameter( * name="campaignId", * in="query", * required=false, * type="integer", * description="Filter events by specific campaignId" * ), * @SWG\Parameter( * name="displayGroupIds", * in="query", * required=false, * type="array", * description="Filter events by an array of Display Group Ids", * @SWG\Items(type="integer") * ), * @SWG\Response( * response=200, * description="successful operation", * @SWG\Schema( * type="array", * @SWG\Items(ref="#/definitions/Schedule") * ) * ) * ) * @param Request $request * @param Response $response * @return ResponseInterface|Response * @throws ControllerNotImplemented * @throws GeneralException * @throws InvalidArgumentException * @throws NotFoundException */ public function grid(Request $request, Response $response) { $params = $this->getSanitizer($request->getParams()); $displayGroupIds = $params->getIntArray('displayGroupIds', ['default' => []]); $displaySpecificDisplayGroupIds = $params->getIntArray('displaySpecificGroupIds', ['default' => []]); $originalDisplayGroupIds = array_merge($displayGroupIds, $displaySpecificDisplayGroupIds); if (!$this->getUser()->isSuperAdmin()) { $userDisplayGroupIds = array_map(function ($element) { /** @var \Xibo\Entity\DisplayGroup $element */ return $element->displayGroupId; }, $this->displayGroupFactory->query(null, ['isDisplaySpecific' => -1])); // Reset the list to only those display groups that intersect and if 0 have been provided, only those from // the user list $resolvedDisplayGroupIds = (count($originalDisplayGroupIds) > 0) ? array_intersect($originalDisplayGroupIds, $userDisplayGroupIds) : $userDisplayGroupIds; $this->getLog()->debug('Resolved list of display groups [' . json_encode($displayGroupIds) . '] from provided list [' . json_encode($originalDisplayGroupIds) . '] and user list [' . json_encode($userDisplayGroupIds) . ']'); // If we have none, then we do not return any events. if (count($resolvedDisplayGroupIds) <= 0) { $this->getState()->template = 'grid'; $this->getState()->recordsTotal = $this->scheduleFactory->countLast(); $this->getState()->setData([]); return $this->render($request, $response); } } else { $resolvedDisplayGroupIds = $originalDisplayGroupIds; } $events = $this->scheduleFactory->query( $this->gridRenderSort($params), $this->gridRenderFilter([ 'eventTypeId' => $params->getInt('eventTypeId'), 'futureSchedulesFrom' => $params->getDate('fromDt')?->format('U'), 'futureSchedulesTo' => $params->getDate('toDt')?->format('U'), 'geoAware' => $params->getInt('geoAware'), 'recurring' => $params->getInt('recurring'), 'campaignId' => $params->getInt('campaignId'), 'displayGroupIds' => $resolvedDisplayGroupIds, 'name' => $params->getString('name'), 'useRegexForName' => $params->getCheckbox('useRegexForName'), 'logicalOperatorName' => $params->getString('logicalOperatorName'), 'directSchedule' => $params->getCheckbox('directSchedule'), 'sharedSchedule' => $params->getCheckbox('sharedSchedule'), 'gridFilter' => 1, ], $params) ); // Grab some settings which determine how events are displayed. $showLayoutName = ($this->getConfig()->getSetting('SCHEDULE_SHOW_LAYOUT_NAME') == 1); $defaultTimezone = $this->getConfig()->getSetting('defaultTimezone'); foreach ($events as $event) { $event->load(); if (count($event->displayGroups) > 0) { $array = array_map(function ($object) { return $object->displayGroup; }, $event->displayGroups); $displayGroupList = implode(', ', $array); } else { $displayGroupList = ''; } $eventTypes = \Xibo\Entity\Schedule::getEventTypes(); foreach ($eventTypes as $eventType) { if ($eventType['eventTypeId'] === $event->eventTypeId) { $event->setUnmatchedProperty('eventTypeName', $eventType['eventTypeName']); } } $event->setUnmatchedProperty('displayGroupList', $displayGroupList); $event->setUnmatchedProperty('recurringEvent', !empty($event->recurrenceType)); if ($this->isSyncEvent($event->eventTypeId)) { $event->setUnmatchedProperty( 'displayGroupList', $event->getUnmatchedProperty('syncGroupName') ); $event->setUnmatchedProperty( 'syncType', $event->getSyncTypeForEvent() ); } if (!$showLayoutName && !$this->getUser()->isSuperAdmin() && !empty($event->campaignId)) { // Campaign $campaign = $this->campaignFactory->getById($event->campaignId); if (!$this->getUser()->checkViewable($campaign)) { $event->campaign = __('Private Item'); } } if (!empty($event->recurrenceType)) { $repeatsOn = ''; $repeatsUntil = ''; if ($event->recurrenceType === 'Week' && !empty($event->recurrenceRepeatsOn)) { $weekdays = Carbon::getDays(); $repeatDays = explode(',', $event->recurrenceRepeatsOn); $i = 0; foreach ($repeatDays as $repeatDay) { // Carbon getDays starts with Sunday, // return first element from that array if in our array we have 7 (Sunday) $repeatDay = ($repeatDay == 7) ? 0 : $repeatDay; $repeatsOn .= $weekdays[$repeatDay]; if ($i < count($repeatDays) - 1) { $repeatsOn .= ', '; } $i++; } } else if ($event->recurrenceType === 'Month') { // Force the timezone for this date (schedule from/to dates are timezone agnostic, but this // date still has timezone information, which could lead to use formatting as the wrong day) $date = Carbon::parse($event->fromDt)->tz($defaultTimezone); $this->getLog()->debug('grid: setting description for monthly event with date: ' . $date->toAtomString()); if ($event->recurrenceMonthlyRepeatsOn === 0) { $repeatsOn = 'the ' . $date->format('jS') . ' day of the month'; } else { // Which day of the month is this? $firstDay = Carbon::parse('first ' . $date->format('l') . ' of ' . $date->format('F')); $this->getLog()->debug('grid: the first day of the month for this date is: ' . $firstDay->toAtomString()); $nth = $firstDay->diffInDays($date) / 7 + 1; $repeatWeekDayDate = $date->copy()->setDay($nth)->format('jS'); $repeatsOn = 'the ' . $repeatWeekDayDate . ' ' . $date->format('l') . ' of the month'; } } if (!empty($event->recurrenceRange)) { $repeatsUntil = Carbon::createFromTimestamp($event->recurrenceRange) ->format(DateFormatHelper::getSystemFormat()); } $event->setUnmatchedProperty( 'recurringEventDescription', __(sprintf( 'Repeats every %d %s %s %s', $event->recurrenceDetail, $event->recurrenceType . ($event->recurrenceDetail > 1 ? 's' : ''), !empty($repeatsOn) ? 'on ' . $repeatsOn : '', !empty($repeatsUntil) ? ' until ' . $repeatsUntil : '' )) ); } else { $event->setUnmatchedProperty('recurringEventDescription', ''); } if (!$event->isAlwaysDayPart() && !$event->isCustomDayPart()) { $dayPart = $this->dayPartFactory->getById($event->dayPartId); $dayPart->adjustForDate(Carbon::createFromTimestamp($event->fromDt)); $event->fromDt = $dayPart->adjustedStart->format('U'); $event->toDt = $dayPart->adjustedEnd->format('U'); } if ($event->eventTypeId == \Xibo\Entity\Schedule::$COMMAND_EVENT) { $event->toDt = $event->fromDt; } // Set the row from/to date to be an ISO date for display (no timezone) $event->setUnmatchedProperty( 'displayFromDt', Carbon::createFromTimestamp($event->fromDt)->format(DateFormatHelper::getSystemFormat()) ); $event->setUnmatchedProperty( 'displayToDt', Carbon::createFromTimestamp($event->toDt)->format(DateFormatHelper::getSystemFormat()) ); if ($this->isApi($request)) { continue; } $event->includeProperty('buttons'); $event->setUnmatchedProperty('isEditable', $this->isEventEditable($event)); if ($this->isEventEditable($event)) { $event->buttons[] = [ 'id' => 'schedule_button_edit', 'url' => $this->urlFor( $request, 'schedule.edit.form', ['id' => $event->eventId] ), 'dataAttributes' => [ ['name' => 'event-id', 'value' => $event->eventId], ['name' => 'event-start', 'value' => $event->fromDt * 1000], ['name' => 'event-end', 'value' => $event->toDt * 1000] ], 'text' => __('Edit') ]; $event->buttons[] = [ 'id' => 'schedule_button_delete', 'url' => $this->urlFor($request, 'schedule.delete.form', ['id' => $event->eventId]), 'text' => __('Delete'), 'multi-select' => true, 'dataAttributes' => [ [ 'name' => 'commit-url', 'value' => $this->urlFor($request, 'schedule.delete', ['id' => $event->eventId]) ], ['name' => 'commit-method', 'value' => 'delete'], ['name' => 'id', 'value' => 'schedule_button_delete'], ['name' => 'text', 'value' => __('Delete')], ['name' => 'sort-group', 'value' => 1], ['name' => 'rowtitle', 'value' => $event->eventId] ] ]; } } // Store the table rows $this->getState()->template = 'grid'; $this->getState()->recordsTotal = $this->scheduleFactory->countLast(); $this->getState()->setData($events); return $this->render($request, $response); } /** * @param \Xibo\Entity\Schedule $schedule * @param ScheduleReminder $scheduleReminder * @throws GeneralException * @throws NotFoundException * @throws \Xibo\Support\Exception\ConfigurationException * @throws \Xibo\Support\Exception\InvalidArgumentException */ private function saveReminder($schedule, $scheduleReminder) { // if someone changes from custom to always // we should keep the definitions, but make sure they don't get executed in the task if ($schedule->isAlwaysDayPart()) { $scheduleReminder->reminderDt = 0; $scheduleReminder->save(); return; } switch ($scheduleReminder->type) { case ScheduleReminder::$TYPE_MINUTE: $type = ScheduleReminder::$MINUTE; break; case ScheduleReminder::$TYPE_HOUR: $type = ScheduleReminder::$HOUR; break; case ScheduleReminder::$TYPE_DAY: $type = ScheduleReminder::$DAY; break; case ScheduleReminder::$TYPE_WEEK: $type = ScheduleReminder::$WEEK; break; case ScheduleReminder::$TYPE_MONTH: $type = ScheduleReminder::$MONTH; break; default: throw new NotFoundException(__('Unknown type')); } // Remind seconds that we will subtract/add from schedule fromDt/toDt to get reminderDt $remindSeconds = $scheduleReminder->value * $type; // Set reminder date if ($scheduleReminder->option == ScheduleReminder::$OPTION_BEFORE_START) { $scheduleReminder->reminderDt = $schedule->fromDt - $remindSeconds; } elseif ($scheduleReminder->option == ScheduleReminder::$OPTION_AFTER_START) { $scheduleReminder->reminderDt = $schedule->fromDt + $remindSeconds; } elseif ($scheduleReminder->option == ScheduleReminder::$OPTION_BEFORE_END) { $scheduleReminder->reminderDt = $schedule->toDt - $remindSeconds; } elseif ($scheduleReminder->option == ScheduleReminder::$OPTION_AFTER_END) { $scheduleReminder->reminderDt = $schedule->toDt + $remindSeconds; } // Is recurring event? $now = Carbon::now(); if ($schedule->recurrenceType != '') { // find the next event from now try { $nextReminderDate = $schedule->getNextReminderDate($now, $scheduleReminder, $remindSeconds); } catch (NotFoundException $error) { $nextReminderDate = 0; $this->getLog()->debug('No next occurrence of reminderDt found. ReminderDt set to 0.'); } if ($nextReminderDate != 0) { if ($nextReminderDate < $scheduleReminder->lastReminderDt) { // handle if someone edit in frontend after notifications were created // we cannot have a reminderDt set to below the lastReminderDt // so we make the lastReminderDt 0 $scheduleReminder->lastReminderDt = 0; $scheduleReminder->reminderDt = $nextReminderDate; } else { $scheduleReminder->reminderDt = $nextReminderDate; } } else { // next event is not found // we make the reminderDt and lastReminderDt as 0 $scheduleReminder->lastReminderDt = 0; $scheduleReminder->reminderDt = 0; } // Save $scheduleReminder->save(); } else { // one off event $scheduleReminder->save(); } } private function createGeoJson($coordinates) { $properties = new \StdClass(); $convertedCoordinates = []; // coordinates come as array of strings, we need convert that to array of arrays with float values for the Geo JSON foreach ($coordinates as $coordinate) { // each $coordinate is a comma separated string with 2 coordinates // make it into an array $explodedCords = explode(',', $coordinate); // prepare a new array, we will add float values to it, need to be cleared for each set of coordinates $floatCords = []; // iterate through the exploded array, change the type to float store in a new array foreach ($explodedCords as $explodedCord) { $explodedCord = (float)$explodedCord; $floatCords[] = $explodedCord; } // each set of coordinates will be added to this new array, which we will use in the geo json $convertedCoordinates[] = $floatCords; } $geometry = [ 'type' => 'Polygon', 'coordinates' => [ $convertedCoordinates ] ]; $geoJson = [ 'type' => 'Feature', 'properties' => $properties, 'geometry' => $geometry ]; return json_encode($geoJson); } /** * Check if this is event is using full screen schedule with Media or Playlist * @param $eventTypeId * @return bool */ private function isFullScreenSchedule($eventTypeId) { return in_array($eventTypeId, [\Xibo\Entity\Schedule::$MEDIA_EVENT, \Xibo\Entity\Schedule::$PLAYLIST_EVENT]); } /** * @param $eventTypeId * @return int */ private function isSyncEvent($eventTypeId): int { return ($eventTypeId === \Xibo\Entity\Schedule::$SYNC_EVENT) ? 1 : 0; } }