src/Cms/Modules/CalendarBundle/Service/CalendarModuleConfig.php line 154

Open in your IDE?
  1. <?php
  2. namespace Cms\Modules\CalendarBundle\Service;
  3. use App\Doctrine\Repository\Content\Events\Event\EventObjectRepository;
  4. use App\Entity\Content\Events\Event\EventObject;
  5. use Cms\AssetsBundle\Model\Structure\TitleStructure;
  6. use Cms\ContainerBundle\Doctrine\ContainerRepository;
  7. use Cms\ContainerBundle\Entity\Container;
  8. use Cms\CoreBundle\Model\Search\AbstractSearcher;
  9. use Cms\CoreBundle\Util\DateTimeUtils;
  10. use Cms\FrontendBundle\Model\FrontendGlobals;
  11. use Cms\ModuleBundle\Entity\ModuleEntity;
  12. use Cms\ModuleBundle\Entity\Proxy;
  13. use Cms\ModuleBundle\Exception\ModuleConfigException;
  14. use Cms\ModuleBundle\Model\ModuleConfig;
  15. use Cms\ModuleBundle\Model\Traits\FrontendActions\CascadingSharesTrait;
  16. use Cms\ModuleBundle\Model\Traits\FrontendActions\FeedSecurityTrait;
  17. use Cms\ModuleBundle\Model\Traits\FrontendActions\FeedTrait;
  18. use Cms\ModuleBundle\Model\Traits\FrontendActions\IcalFeedTrait;
  19. use Cms\ModuleBundle\Model\Traits\FrontendActions\SocialMetaTrait;
  20. use Cms\Modules\CalendarBundle\Doctrine\Event\EventProxyRepository;
  21. use Cms\Modules\CalendarBundle\Entity\Event\EventProxy;
  22. use Cms\Modules\CalendarBundle\Entity\ModuleSettings;
  23. use Cms\Modules\CalendarBundle\Model\CalendarBuilder;
  24. use Cms\Modules\CalendarBundle\Model\CalendarBuilder\Event;
  25. use Cms\Modules\CalendarBundle\Service\Search\EventSearcher;
  26. use Cms\Modules\CalendarBundle\Service\Social\ShareService;
  27. use Cms\Modules\CalendarBundle\Service\Social\Types\Google;
  28. use Cms\Modules\CalendarBundle\Service\Social\Types\Microsoft;
  29. use Cms\Widgets\Html\Html;
  30. use DateTime;
  31. use Doctrine\Common\Util\ClassUtils;
  32. use Platform\SecurityBundle\Model\OAuth\OAuthOptions;
  33. use Platform\SecurityBundle\Service\OAuth\OAuthProviderService;
  34. use Platform\SecurityBundle\Service\OAuth\Providers\MicrosoftOAuthProvider;
  35. use Platform\SecurityBundle\Util\OAuth\ServiceFactory\MicrosoftOAuth2ServiceFactory;
  36. use SplObjectStorage;
  37. use Symfony\Component\HttpFoundation\RedirectResponse;
  38. use Symfony\Component\HttpFoundation\Response;
  39. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  40. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  41. use Laminas\Uri\Uri;
  42. /**
  43.  * Class CalendarModuleConfig
  44.  * @package Cms\Modules\CalendarBundle\Service
  45.  */
  46. final class CalendarModuleConfig extends ModuleConfig
  47. {
  48.     const NAME 'Calendar';
  49.     const COOKIES__VIEW_MODE 'campussuite__cms__modules__calendar__view_mode';
  50.     use IcalFeedTrait;
  51.     use FeedTrait;
  52.     use FeedSecurityTrait;
  53.     use SocialMetaTrait;
  54.     use CascadingSharesTrait;
  55.     /**
  56.      * @return EventProxyRepository
  57.      */
  58.     private function getProxyRepository(): EventProxyRepository
  59.     {
  60.         return $this->getEntityManager()->getRepository(EventProxy::class);
  61.     }
  62.     /**
  63.      * @return EventObjectRepository
  64.      */
  65.     private function getObjectRepository(): EventObjectRepository
  66.     {
  67.         return $this->getEntityManager()->getRepository(EventObject::class);
  68.     }
  69.     /**
  70.      * @param FrontendGlobals $globals
  71.      * @return bool
  72.      */
  73.     private function isGraphicalView(FrontendGlobals $globals)
  74.     {
  75.         $viewFromCookies $globals->getRequest()->cookies->get(self::COOKIES__VIEW_MODE);
  76.         if ($viewFromCookies) {
  77.             return $viewFromCookies == ModuleSettings::VIEW_MODE_GRAPHICAL;
  78.         }
  79.         /** @var ModuleSettings $moduleSettings */
  80.         $moduleSettings $globals->getModuleSettings();
  81.         return $moduleSettings->getViewMode() == ModuleSettings::VIEW_MODE_GRAPHICAL;
  82.     }
  83.     /**
  84.      * {@inheritdoc}
  85.      * @param EventProxy $proxy
  86.      */
  87.     public function urlify(Proxy $proxy null)
  88.     {
  89.         if ($proxy === null) {
  90.             return '/' $this->key();
  91.         }
  92.         return sprintf(
  93.             '/%s/%s/%s',
  94.             $this->key(),
  95.             $proxy->getId(),
  96.             $proxy->getData()->getSlug()
  97.         );
  98.     }
  99.     /**
  100.      * {@inheritdoc}
  101.      */
  102.     public function frontend(FrontendGlobals $globals)
  103.     {
  104.         $extra $globals->getExtra();
  105.         //override inner layout from module settings
  106.         /** @var ModuleSettings $settings */
  107.         $settings $globals->getModuleSettings();
  108.         if (empty($settings->getInnerLayout()) === false) {
  109.             $globals->setInnerLayout(
  110.                 $settings->getInnerLayout(),
  111.                 true
  112.             );
  113.         }
  114.         // Maybe is there a better way to set/get departments data via a method
  115.         // in the frontend globals object (e.g. getQuery() the same way as getExtra())
  116.         // instead of getting query data from the request? */
  117.         $departments $globals->getRequest()->query->has('departments')
  118.             ? explode(','$globals->getRequest()->query->get('departments'))
  119.             : [];
  120.         switch (true) {
  121.             case preg_match('/^\\/$/'$extra$matches):
  122.                 return $this->frontendList($globals);
  123.             // SCHOOLNOW: modify regex to allow for ulid values
  124.             case preg_match('/^\\/details\\/([0-9]+|[0-7][0-9a-hA-HjJkKmMnNp-tP-Tv-zV-Z]{25})$/'$extra$matches):
  125.                 return $this->ajaxDetailsView($globals, array(
  126.                     'event' => $matches[1]
  127.                 ));
  128.             case preg_match('/^\\/monthly\\/([0-9]{4})\\/(0[1-9]|1[0-2])$/'$extra$matches):
  129.                 return $this->frontendMonthView($globals, array(
  130.                     'year' => $matches[1],
  131.                     'month' => $matches[2],
  132.                     'departments' => $departments,
  133.                 ));
  134.             // SCHOOLNOW: modify regex to allow for ulid values
  135.             case preg_match('/^\\/([0-9]+|[0-7][0-9a-hA-HjJkKmMnNp-tP-Tv-zV-Z]{25})\\/([-a-zA-Z0-9]+)$/'$extra$matches):
  136.                 return $this->frontendView($globals, array(
  137.                     'id' => $matches[1],
  138.                     'slug' => $matches[2],
  139.                 ));
  140.             case preg_match('/^\\/monthly\\/([0-9]{4})\\/(0[1-9]|1[0-2])\\/print$/'$extra$matches):
  141.                 return $this->frontendMonthView($globals, array(
  142.                     'year' => $matches[1],
  143.                     'month' => $matches[2],
  144.                     'departments' => $departments,
  145.                 ), true);
  146.             case preg_match('/\\/print$/'$extra):
  147.                 $now DateTimeUtils::current();
  148.                 return $this->frontendMonthView(
  149.                     $globals,
  150.                     array(
  151.                         'year' => $now->format('Y'),
  152.                         'month' => $now->format('m'),
  153.                         'departments' => $departments,
  154.                     ),
  155.                     true
  156.                 );
  157.             case $this->isPrivateFeed($globals->getContainer(), $this->name()):
  158.                 return $this->loginRedirect($globals->getRequest());
  159.             case preg_match('/\\/subscribe$/'$extra):
  160.                 return $this->frontendSubscribe($globals);
  161.             case preg_match('/\\/feed\\/ical(?:\\.ics)?$/'$extra):
  162.                 return $this->icalFeed(
  163.                     $this->getFeedBuilder(),
  164.                     $globals,
  165.                     array(
  166.                         'departments' => $departments,
  167.                     )
  168.                 );
  169.             case preg_match('/^\\/download\\/(\\d+)\\/ical$/'$extra$matches):
  170.                 return $this->icalFeed(
  171.                     $this->getFeedBuilder(),
  172.                     $globals,
  173.                     array(
  174.                         'event' => $matches[1]
  175.                     )
  176.                 );
  177.             case preg_match('/^\\/add\\/(\\d+)$/'$extra$matches):
  178.                 return $this->addToCalendar(
  179.                     $globals,
  180.                     array(
  181.                         'event' => $matches[1],
  182.                         'provider' => $globals->getRequest()->query->get('provider')
  183.                     )
  184.                 );
  185.             case preg_match('/^\\/share\\/(\\d+)$/'$extra$matches):
  186.                 return $this->shareTo(
  187.                     $matches[1],
  188.                     $globals
  189.                 );
  190.             default:
  191.                 throw new NotFoundHttpException();
  192.         }
  193.     }
  194.     /**
  195.      * @param FrontendGlobals $globals
  196.      * @param array $params
  197.      * @param bool $print
  198.      * @return Response|array
  199.      * @throws ModuleConfigException
  200.      */
  201.     private function frontendMonthView(FrontendGlobals $globals, array $params$print false)
  202.     {
  203.         // extract params
  204.         $year intval($params['year']);
  205.         $month intval($params['month']);
  206.         // get selected departments from the query search string
  207.         $selectedDepsIds array_key_exists('departments'$params) ? array_filter($params['departments']) : [];
  208.         // generate the dates, based on locale stuff
  209.         $start DateTimeUtils::make(
  210.             sprintf(
  211.                 '%s-%s-01 00:00:00',
  212.                 $year,
  213.                 $month
  214.             ),
  215.             null,
  216.             $this->getLocaleManager()->effectiveTimezone()
  217.         );
  218.         // generate some important dates based on the input
  219.         $monthStart DateTimeUtils::firstOfMonth($start);
  220.         $monthEnd DateTimeUtils::lastOfMonth($start);
  221.         $monthBefore DateTimeUtils::firstOfMonth($start)->modify('-1 month');
  222.         $monthAfter DateTimeUtils::firstOfMonth($start)->modify('+1 month');
  223.         if ($print === true) {
  224.             // after should be the full number of months we want to cover
  225.             // end is one less than that
  226.             $monthEnd DateTimeUtils::lastOfMonth(DateTimeUtils::firstOfMonth($start)->modify('+0 month'));
  227.             $monthAfter DateTimeUtils::firstOfMonth($start)->modify('+1 month');
  228.         }
  229.         // make sure that we don't extend beyond a range
  230.         // this ensures that the calendar stuff does not loop indefinitely to past or future months
  231.         $min DateTimeUtils::beforeCurrent('P8Y');
  232.         $max DateTimeUtils::afterCurrent('P2Y');
  233.         if ($start $min || $start $max) {
  234.             throw new NotFoundHttpException();
  235.         }
  236.         // set title
  237.         if (method_exists($globals->getModuleSettings(), 'getTitle')
  238.             && ! empty($globals->getModuleSettings()->getTitle())
  239.         ) {
  240.             $globals->getAssetsOrganizer()->getTitles()
  241.                 ->add(new TitleStructure($globals->getModuleSettings()->getTitle()))
  242.                 ->add(new TitleStructure($monthStart->format('F Y')));
  243.         } else {
  244.             $globals->getAssetsOrganizer()->getTitles()
  245.                 ->add(new TitleStructure($globals->getModule()->name()))
  246.                 ->add(new TitleStructure($monthStart->format('F Y')))
  247.                 ->add(new TitleStructure($globals->getContainer()->getName()));
  248.         }
  249.         // get the module settings for this department
  250.         /** @var ModuleSettings $settings */
  251.         $settings $this->loadSettings($globals->getContainer());
  252.         /** @var ContainerRepository $containerRepo */
  253.         $containerRepo $this->getEntityManager()
  254.             ->getRepository(
  255.                 Container::class
  256.             );
  257.         // get all the departments that need to show in the calendar selections
  258.         if ( ! empty($settings->getSelections())) {
  259.             $departments $containerRepo->findBy(
  260.                 array(
  261.                     'id' => $settings->getSelections(),
  262.                 ),
  263.                 array(
  264.                     'name' => 'ASC',
  265.                 )
  266.             );
  267.         } else {
  268.             $departments $containerRepo->findModuleViewableHierarchy(
  269.                 $globals->getContainers()[count($globals->getContainers()) - 1],
  270.                 $this->key()
  271.             );
  272.         }
  273.         $shares $containerRepo->findBy(
  274.             array(
  275.                 'id' => $this->flattenContainers($globals->getContainer(), ModuleSettings::class),
  276.             ),
  277.             array(
  278.                 'name' => 'ASC',
  279.             )
  280.         );
  281.         $expansions = new SplObjectStorage();
  282.         if (count($selectedDepsIds)) {
  283.             $queryDepartments $selectedDepartments $containerRepo->searchContainers($selectedDepsIds);
  284.             $expansionDepartments = [];
  285.             foreach ($selectedDepartments as $selectedDepartment) {
  286.                 if ($selectedDepartment === $globals->getContainer()) {
  287.                     continue;
  288.                 }
  289.                 $expandedDepartment $this->flattenContainers($selectedDepartmentModuleSettings::class);
  290.                 if ( ! is_array($expandedDepartment)) {
  291.                     continue;
  292.                 }
  293.                 array_shift($expandedDepartment);
  294.                 $expandedDepartments $containerRepo->findBy(
  295.                     array(
  296.                         'id' => $expandedDepartment,
  297.                     ),
  298.                     array(
  299.                         'name' => 'ASC',
  300.                     )
  301.                 );
  302.                 $expansionDepartments array_merge(
  303.                     $expansionDepartments,
  304.                     $expandedDepartments
  305.                 );
  306.                 foreach ($expandedDepartments as $dept) {
  307.                     if ( ! in_array($dept$selectedDepartmentstrue)) {
  308.                         $expansions->offsetSet($dept$selectedDepartment);
  309.                     }
  310.                 }
  311.             }
  312.             if (in_array($globals->getContainer(), $queryDepartmentstrue)) {
  313.                 $queryDepartments array_values(array_filter(array_unique(array_merge(
  314.                     $queryDepartments,
  315.                     $shares
  316.                 ))));
  317.             }
  318.             $queryDepartments array_values(array_filter(array_unique(array_merge(
  319.                 $queryDepartments,
  320.                 $expansionDepartments
  321.             ))));
  322.         } else {
  323.             // get the departments that are already showing in the calendar
  324.             $selectedDepartments = array(
  325.                 $globals->getContainer(),
  326.             );
  327.             $queryDepartments $shares;
  328.         }
  329.         // get the diff of the calendars that are yet to be selected from the possible options
  330.         $unselectedDepartments array_diff($departments$selectedDepartments);
  331.         // find the items
  332.         /** @var array|EventProxy[] $items */
  333.         $items $this->getProxyRepository()->createQueryBuilder('items')
  334.             ->andWhere('items.container IN (:container)')
  335.             ->setParameter('container'$queryDepartments)
  336.             ->andWhere('((items.data_startDate >= :monthStart AND items.data_startDate < :monthAfter) 
  337.                       OR (items.data_endDate > :monthStart AND items.data_endDate < :monthAfter) 
  338.                       OR (items.data_startDate < :monthStart AND items.data_endDate >= :monthAfter))')
  339.             // TODO: these values are in local times, do we need to convert to utc before using in query?
  340.             ->setParameter('monthStart'$monthStart)
  341.             ->setParameter('monthAfter'$monthAfter)
  342.             ->addOrderBy('items.data_startDate''ASC')
  343.             ->addOrderBy('items.data_endDate''ASC')
  344.             ->getQuery()
  345.             ->getResult();
  346.         // create a calendar builder to help us process and display the events easier
  347.         $builder = (new CalendarBuilder($start, array(
  348.             'container' => $globals->getContainer(),
  349.             'shares' => (in_array($globals->getContainer(), $selectedDepartmentstrue)) ? array_values(array_diff($shares$selectedDepartments)) : [],
  350.             'expansions' => $expansions,
  351.         )))
  352.             ->mergeEvents($items);
  353.         // get today's date
  354.         $today DateTimeUtils::today(
  355.             $this->getLocaleManager()->effectiveTimezone(),
  356.             false
  357.         );
  358.         // if doing print view, show the print ui
  359.         if ($print) {
  360.             $content $this->getTwig()->render(
  361.                 $this->getDatabaseLoader()->determine(
  362.                     $globals->getTheme(),
  363.                     sprintf(
  364.                         '/modules/%s/Print/build/tpl.html.twig',
  365.                         $globals->getModule()->name()
  366.                     )
  367.                 ),
  368.                 array(
  369.                     '_globals' => $globals,
  370.                     'today' => $today,
  371.                     'monthStart' => $monthStart,
  372.                     'monthEnd' => $monthEnd,
  373.                     'monthBefore' => ($monthBefore >= $min) ? $monthBefore null,
  374.                     'monthAfter' => ($monthAfter <= $max) ? $monthAfter null,
  375.                     'builder' => $builder,
  376.                     'departments' => $departments,
  377.                     'selectedDepartments' => $selectedDepartments,
  378.                     'unselectedDepartments' => $unselectedDepartments,
  379.                     'sharedDepartments' => $shares,
  380.                 )
  381.             );
  382.             return new Response($content);
  383.         }
  384.         // render content from twig file
  385.         $content $this->getTwig()->render(
  386.             $this->getDatabaseLoader()->determine(
  387.                 $globals->getTheme(),
  388.                 sprintf(
  389.                     '/modules/%s/Graphical/build/tpl.html.twig',
  390.                     $globals->getModule()->name()
  391.                 )
  392.             ),
  393.             array(
  394.                 '_globals' => $globals,
  395.                 '_share' => $this->getShareService(),
  396.                 'today' => $today,
  397.                 'monthStart' => $monthStart,
  398.                 'monthEnd' => $monthEnd,
  399.                 'monthBefore' => ($monthBefore >= $min) ? $monthBefore null,
  400.                 'monthAfter' => ($monthAfter <= $max) ? $monthAfter null,
  401.                 'builder' => $builder,
  402.                 'departments' => $departments,
  403.                 'selectedDepartments' => $selectedDepartments,
  404.                 'unselectedDepartments' => $unselectedDepartments,
  405.                 'sharedDepartments' => $shares,
  406.                 'isGraphicalView' => $this->isGraphicalView($globals),
  407.             )
  408.         );
  409.         // done
  410.         return array(
  411.             array(
  412.                 'content' => array(
  413.                     (new Html())
  414.                         ->setContent($content),
  415.                 ),
  416.             ),
  417.         );
  418.     }
  419.     /**
  420.      * @param FrontendGlobals $globals
  421.      * @return RedirectResponse
  422.      */
  423.     private function frontendList(FrontendGlobals $globals)
  424.     {
  425.         // HACK: jump to graphical view
  426.         $now DateTimeUtils::current();
  427.         return new RedirectResponse(sprintf(
  428.             '%s://%s/%s/monthly/%s/%s',
  429.             $globals->getRequest()->getScheme(),
  430.             $globals->getRequest()->getHost(),
  431.             trim($globals->getPath(), '/'),
  432.             $now->format('Y'),
  433.             $now->format('m')
  434.         ));
  435.     }
  436.     /**
  437.      * @param FrontendGlobals $globals
  438.      * @param array $params
  439.      * @return array|RedirectResponse
  440.      * @throws ModuleConfigException
  441.      */
  442.     private function frontendView(FrontendGlobals $globals, array $params)
  443.     {
  444.         // need to grab only the item requested
  445.         /** @var EventProxyRepository $repo */
  446.         $repo $this->getEntityManager()->getRepository(EventProxy::class);
  447.         /** @var EventProxy|null $item */
  448.         $item $repo->find($params['id']);
  449.         // check for item, if none, throw 404
  450.         if ($item === null) {
  451.             throw new NotFoundHttpException();
  452.         }
  453.         // set the thing on the globals
  454.         $globals->setThing($item);
  455.         // handle non-proxy preview
  456.         if ($globals->getThingOverride() instanceof ModuleEntity) {
  457.             $globals->setThing($item->setData(
  458.                 $globals->getThingOverride()->getData()
  459.             ));
  460.         }
  461.         // set social meta tags
  462.         $this->setSocialMeta(
  463.             $globals,
  464.             $item->getData()->getSocialMetadata()
  465.         );
  466.         //set title
  467.         $globals->getAssetsOrganizer()->getTitles()
  468.             ->add(new TitleStructure($item->getData()->getTitle()))
  469.             ->add(new TitleStructure(
  470.                 $this->getFormattedDatePeriod(
  471.                     $item->getData()->getStartDate(),
  472.                     $item->getData()->getEndDate(),
  473.                     $item->getData()->getAllDay()
  474.                 )
  475.             ))
  476.             ->add(new TitleStructure($globals->getContainer()->getName()));
  477.         // check slugs, and if incorrect, redirect to proper url with slug
  478.         if ($item->getData()->getSlug() !== $params['slug']) {
  479.             return new RedirectResponse(sprintf(
  480.                 '%s/%s/%s/%s',
  481.                 $globals->pathPrefix(),
  482.                 $globals->getModule()->key(),
  483.                 $item->getId(),
  484.                 $item->getData()->getSlug()
  485.             ));
  486.         }
  487.         // generate the dates, based on locale stuff
  488.         $start DateTimeUtils::changeTimezone(
  489.             $item->getData()->getStartDate(),
  490.             $this->getLocaleManager()->effectiveTimezone()
  491.         );
  492.         // render the content
  493.         $content $this->getTwig()->render(
  494.             $this->getDatabaseLoader()->determine(
  495.                 $globals->getTheme(),
  496.                 sprintf(
  497.                     '/modules/%s/View/build/tpl.html.twig',
  498.                     $globals->getModule()->name()
  499.                 )
  500.             ),
  501.             array(
  502.                 '_globals' => $globals,
  503.                 '_share' => $this->getShareService(),
  504.                 'event' => new Event(
  505.                     array(
  506.                         'timezone' => $start->getTimezone(),
  507.                         'start' => DateTimeUtils::firstOfMonth($start),
  508.                         'end' => DateTimeUtils::lastOfMonth($start),
  509.                         'container' => $globals->getContainer(),
  510.                         'shares' => $this->getEntityManager()
  511.                             ->getRepository(ClassUtils::getClass($globals->getContainer()))
  512.                             ->findBy(
  513.                                 array(
  514.                                     'id' => $this->flattenContainers($globals->getContainer(), ModuleSettings::class),
  515.                                 ),
  516.                                 array(
  517.                                     'name' => 'ASC',
  518.                                 )
  519.                             )
  520.                         ,
  521.                     ),
  522.                     $item
  523.                 )
  524.             )
  525.         );
  526.         // pass back as we need it
  527.         return array(
  528.             array(
  529.                 'content' => array(
  530.                     (new Html())
  531.                         ->setContent($content),
  532.                 ),
  533.             ),
  534.         );
  535.     }
  536.     /**
  537.      * {@inheritdoc}
  538.      */
  539.     public function types()
  540.     {
  541.         return array(EventProxy::TYPE);
  542.     }
  543.     /**
  544.      * {@inheritdoc}
  545.      */
  546.     public function typesRoots()
  547.     {
  548.         return array(EventProxy::TYPE);
  549.     }
  550.     /**
  551.      * {@inheritdoc}
  552.      */
  553.     public function typesChildren($type)
  554.     {
  555.         return [];
  556.     }
  557.     /**
  558.      * {@inheritdoc}
  559.      */
  560.     public function collectionClass($classname)
  561.     {
  562.     }
  563.     /**
  564.      * @param DateTime $eventStartDate
  565.      * @param DateTime|null $eventEndDate
  566.      * @param bool $allDay
  567.      * @return string
  568.      */
  569.     private function getFormattedDatePeriod(
  570.         DateTime $eventStartDate,
  571.         DateTime $eventEndDate null,
  572.         bool $allDay false
  573.     ) {
  574.         $timezone DateTimeUtils::timezone($this->getLocaleManager()->effectiveTimezone());
  575.         $startDate = clone $eventStartDate;
  576.         $startDate->setTimezone($timezone);
  577.         if (empty($eventEndDate)) {
  578.             // example: May 23, 2017
  579.             $dateString =  $startDate->format('F j, Y');
  580.         } else {
  581.             $endDate = clone $eventEndDate;
  582.             $endDate->setTimezone($timezone);
  583.             // For all day multi day events, the last day ends the next day, so we have to subtract one day
  584.             if ($allDay) {
  585.                 $endDate $endDate
  586.                     ->sub(new \DateInterval('P1D'));
  587.             }
  588.             $sameYear = ($startDate->format('Y') === $endDate->format('Y'));
  589.             $sameMonth = ($startDate->format('m') === $endDate->format('m'));
  590.             $sameDay = ($startDate->format('d') === $endDate->format('d'));
  591.             switch (true) {
  592.                 case ! $sameYear:
  593.                     // example: December 23, 2017 - January 10, 2018
  594.                     $dateString sprintf(
  595.                         '%s - %s',
  596.                         $startDate->format('F j, Y'),
  597.                         $endDate->format('F j, Y')
  598.                     );
  599.                     break;
  600.                 case ! $sameMonth:
  601.                     // example: May 23 - June 16, 2017
  602.                     $dateString sprintf(
  603.                         '%s - %s, %s',
  604.                         $startDate->format('F j'),
  605.                         $endDate->format('F j'),
  606.                         $startDate->format('Y')
  607.                     );
  608.                     break;
  609.                 case ! $sameDay:
  610.                     // example: May 23 - 28, 2017
  611.                     $dateString sprintf(
  612.                         '%s %s - %s, %s',
  613.                         $startDate->format('F'),
  614.                         $startDate->format('j'),
  615.                         $endDate->format('j'),
  616.                         $startDate->format('Y')
  617.                     );
  618.                     break;
  619.                 default:
  620.                     // example: May 23, 2017
  621.                     $dateString $startDate->format('F j, Y');
  622.                     break;
  623.             }
  624.         }
  625.         return $dateString;
  626.     }
  627.     /**
  628.      * Return details html content based on the event id
  629.      * @param FrontendGlobals $globals
  630.      * @param array $params
  631.      * @return mixed|string
  632.      */
  633.     private function ajaxDetailsView(FrontendGlobals $globals, array $params)
  634.     {
  635.         if ( ! $globals->getRequest()->isXmlHttpRequest()) {
  636.             throw new BadRequestHttpException();
  637.         }
  638.         $eventId intval($params['event']);
  639.         /** @var EventProxy $event */
  640.         $event $this->getEntityManager()
  641.             ->getRepository(EventProxy::class)->findExact($eventId);
  642.         if (empty($event)) {
  643.             throw new NotFoundHttpException(sprintf('Event #%s not found.'$eventId));
  644.         }
  645.         // generate the dates, based on locale stuff
  646.         $start DateTimeUtils::changeTimezone(
  647.             $event->getData()->getStartDate(),
  648.             $this->getLocaleManager()->effectiveTimezone()
  649.         );
  650.         $content =  $this->getTwig()->render(
  651.             $this->getDatabaseLoader()->determine(
  652.                 $globals->getTheme(),
  653.                 sprintf(
  654.                     '/modules/%s/GraphicalDetails/build/tpl.html.twig',
  655.                     $globals->getModule()->name()
  656.                 )
  657.             ),
  658.             array(
  659.                 '_globals' => $globals,
  660.                 '_share' => $this->getShareService(),
  661.                 'event' => new Event(array(
  662.                     'timezone' => $start->getTimezone(),
  663.                     'start' => DateTimeUtils::firstOfMonth($start),
  664.                     'end' => DateTimeUtils::lastOfMonth($start),
  665.                     'container' => $globals->getContainer(),
  666.                     'shares' => $this->getEntityManager()
  667.                         ->getRepository(ClassUtils::getClass($globals->getContainer()))
  668.                         ->findBy(
  669.                             array(
  670.                                 'id' => $this->flattenContainers($globals->getContainer(), ModuleSettings::class),
  671.                             ),
  672.                             array(
  673.                                 'name' => 'ASC',
  674.                             )
  675.                         )
  676.                     ,
  677.                 ), $event),
  678.             )
  679.         );
  680.         return new Response($content);
  681.     }
  682.     /**
  683.      * Handle add to calendar events via providers (google, outlook, etc.)
  684.      *
  685.      * @param FrontendGlobals $globals
  686.      * @param array $params
  687.      * @return Response
  688.      * @throws \Exception
  689.      */
  690.     private function addToCalendar(FrontendGlobals $globals, array $params)
  691.     {
  692.         $provider $params['provider'];
  693.         $eventId intval($params['event']);
  694.         if (empty($provider)) {
  695.             throw new \Exception('Type cannot be empty.');
  696.         }
  697.         /** @var EventProxy $event */
  698.         $event $this->getEntityManager()
  699.             ->getRepository(EventProxy::class)->findExact($eventId);
  700.         switch ($provider) {
  701.             case Microsoft::TYPE:
  702.                 return $this->getMicrosoft()->handler(
  703.                     $globals,
  704.                     $event,
  705.                     $globals->getRequest()->query->get('code'),
  706.                     $globals->getRequest()->query->get('state')
  707.                 );
  708.             case Google::TYPE:
  709.                 return $this->getGoogle()->handler($event);
  710.             default:
  711.                 throw new \Exception(sprintf('Invalid provider: `%s`'$provider));
  712.         }
  713.     }
  714.     /**
  715.      * Just wrap creating an external link (reduce the count of OAuth2 API calls)
  716.      *
  717.      * @param $eventId
  718.      * @param FrontendGlobals $globals
  719.      * @return Response
  720.      * @throws \Exception
  721.      */
  722.     private function shareTo($eventIdFrontendGlobals $globals)
  723.     {
  724.         $provider $globals->getRequest()->query->get('provider');
  725.         /** @var EventProxy $event */
  726.         $event $this->getEntityManager()
  727.             ->getRepository(EventProxy::class)
  728.             ->findExact($eventId);
  729.         switch ($provider) {
  730.             case Microsoft::TYPE:
  731.                 return new RedirectResponse($this->getOAuthProviderService()->getProvider(MicrosoftOAuthProvider::ID)->getAuthorizationUri(
  732.                     $globals->getTenant(),
  733.                     new OAuthOptions([
  734.                         'scopes' => [
  735.                             MicrosoftOAuth2ServiceFactory::SCOPE_CALENDARS_READ,
  736.                             MicrosoftOAuth2ServiceFactory::SCOPE_CALENDARS_READ_WRITE,
  737.                         ],
  738.                         'state' => [
  739.                             'redirect' => (new Uri(sprintf(
  740.                                 '%s/calendar/add/%s?provider=%s',
  741.                                 $this->getContainerService()->getFrontLink(
  742.                                     $globals->getContainer()
  743.                                 ),
  744.                                 $event->getId(),
  745.                                 Microsoft::TYPE
  746.                             )))->toString()
  747.                         ],
  748.                     ])
  749.                 ));
  750.             case Google::TYPE:
  751.                 return new RedirectResponse((new Uri(sprintf(
  752.                     '%s/calendar/add/%s?provider=%s',
  753.                     $this->getContainerService()->getFrontLink(
  754.                         $event->getContainer()
  755.                     ),
  756.                     $event->getId(),
  757.                     Google::TYPE
  758.                 )))->toString());
  759.             default:
  760.                 throw new \Exception(sprintf('Invalid provider: `%s`'$provider));
  761.         }
  762.     }
  763.     /**
  764.      * @param array|Container[] $selectedDeps
  765.      * @param int $limit
  766.      * @param int $offset
  767.      * @return array|EventProxy[]
  768.      */
  769.     public function findCalendarItems(array $selectedDepsint $limitint $offset): array
  770.     {
  771.         // check paging
  772.         if ($limit <= 0) {
  773.             throw new \Exception();
  774.         }
  775.         if ($offset 0) {
  776.             throw new \Exception();
  777.         }
  778.         // get the department ids for the schools we are searching for
  779.         if ( ! $selectedDeps) {
  780.             return [];
  781.         }
  782.         $selectedDepsIds array_map(
  783.             static function (Container $dep) {
  784.                 return $dep->getId();
  785.             },
  786.             $selectedDeps
  787.         );
  788.         // start date should be today
  789.         $start DateTimeUtils::today(
  790.             $this->getLocaleManager()->effectiveTimezone()
  791.         );
  792.         // make sure that we don't extend beyond a range
  793.         // this ensures that the calendar stuff does not loop indefinitely to past or future months
  794.         $max DateTimeUtils::afterCurrent('P2Y');
  795.         if ($start $max) {
  796.             throw new NotFoundHttpException();
  797.         }
  798.         // get the module settings for this department
  799.         /** @var ModuleSettings $settings */
  800.         $settings $this->loadSettings($selectedDeps[0]);
  801.         /** @var ContainerRepository $containerRepo */
  802.         $containerRepo $this->getEntityManager()->getRepository(
  803.             ClassUtils::getClass($selectedDeps[0])
  804.         );
  805.         // get all the departments that need to show in the calendar selections
  806.         if ( ! empty($settings->getSelections())) {
  807.             $departments $containerRepo->findBy(
  808.                 array(
  809.                     'id' => $settings->getSelections(),
  810.                 ),
  811.                 array(
  812.                     'name' => 'ASC',
  813.                 )
  814.             );
  815.         } else {
  816.             $departments $containerRepo->findModuleViewableHierarchy(
  817.                 $selectedDeps[0],
  818.                 $this->key()
  819.             );
  820.         }
  821.         $shares $containerRepo->findBy(
  822.             array(
  823.                 'id' => $this->flattenContainers($selectedDeps[0], ModuleSettings::class),
  824.             ),
  825.             array(
  826.                 'name' => 'ASC',
  827.             )
  828.         );
  829.         $expansions = new SplObjectStorage();
  830.         if (count($selectedDepsIds)) {
  831.             $queryDepartments $selectedDepartments $containerRepo->searchContainers($selectedDepsIds);
  832.             $expansionDepartments = [];
  833.             foreach ($selectedDepartments as $selectedDepartment) {
  834.                 if ($selectedDepartment === $selectedDeps[0]) {
  835.                     continue;
  836.                 }
  837.                 $expandedDepartment $this->flattenContainers($selectedDepartmentModuleSettings::class);
  838.                 if ( ! is_array($expandedDepartment)) {
  839.                     continue;
  840.                 }
  841.                 array_shift($expandedDepartment);
  842.                 $expandedDepartments $containerRepo->findBy(
  843.                     array(
  844.                         'id' => $expandedDepartment,
  845.                     ),
  846.                     array(
  847.                         'name' => 'ASC',
  848.                     )
  849.                 );
  850.                 $expansionDepartments array_merge(
  851.                     $expansionDepartments,
  852.                     $expandedDepartments
  853.                 );
  854.                 foreach ($expandedDepartments as $dept) {
  855.                     if ( ! in_array($dept$selectedDepartmentstrue)) {
  856.                         $expansions->offsetSet($dept$selectedDepartment);
  857.                     }
  858.                 }
  859.             }
  860.             if (in_array($selectedDeps[0], $queryDepartmentstrue)) {
  861.                 $queryDepartments array_values(array_filter(array_unique(array_merge(
  862.                     $queryDepartments,
  863.                     $shares
  864.                 ))));
  865.             }
  866.             $queryDepartments array_values(array_filter(array_unique(array_merge(
  867.                 $queryDepartments,
  868.                 $expansionDepartments
  869.             ))));
  870.         } else {
  871.             // get the departments that are already showing in the calendar
  872.             $selectedDepartments = array(
  873.                 $selectedDeps[0],
  874.             );
  875.             $queryDepartments $shares;
  876.         }
  877.         // get the diff of the calendars that are yet to be selected from the possible options
  878.         $unselectedDepartments array_diff($departments$selectedDepartments);
  879.         // find the items
  880.         /** @var array|EventProxy[] $items */
  881.         $items $this->getProxyRepository()->createQueryBuilder('items')
  882.             ->andWhere('items.container IN (:container)')
  883.             ->setParameter('container'$queryDepartments)
  884.             ->andWhere('((items.data_startDate >= :start)
  885.                       OR (items.data_endDate > :start) 
  886.                       OR (items.data_startDate < :start AND items.data_endDate >= :start))')
  887.             // TODO: these values are in local times, do we need to convert to utc before using in query?
  888.             ->setParameter('start'$start)
  889.             ->addOrderBy('items.data_startDate''ASC')
  890.             ->addOrderBy('items.data_endDate''ASC')
  891.             ->getQuery()
  892.             ->setMaxResults($limit)
  893.             ->setFirstResult($offset)
  894.             ->getResult();
  895.         return $items;
  896.     }
  897.     /**
  898.      * @return OAuthProviderService|object
  899.      */
  900.     private function getOAuthProviderService(): OAuthProviderService
  901.     {
  902.         return $this->container->get(__METHOD__);
  903.     }
  904.     /**
  905.      * @return ShareService|object
  906.      */
  907.     private function getShareService(): ShareService
  908.     {
  909.         return $this->container->get(__METHOD__);
  910.     }
  911.     /**
  912.      * @return Google|object
  913.      */
  914.     private function getGoogle(): Google
  915.     {
  916.         return $this->container->get(__METHOD__);
  917.     }
  918.     /**
  919.      * @return Microsoft|object
  920.      */
  921.     private function getMicrosoft(): Microsoft
  922.     {
  923.         return $this->container->get(__METHOD__);
  924.     }
  925.     /**
  926.      * @return EventSearcher|object
  927.      */
  928.     public function getEventSearcher(): EventSearcher
  929.     {
  930.         return $this->container->get(__METHOD__);
  931.     }
  932.     /**
  933.      * {@inheritDoc}
  934.      * @return EventSearcher
  935.      */
  936.     public function getSearcher(): AbstractSearcher
  937.     {
  938.         return $this->getEventSearcher();
  939.     }
  940. }