arlo-1.x-dev/src/Controller/ArloAPIController.php
src/Controller/ArloAPIController.php
<?php namespace Drupal\arlo\Controller; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Database\Connection; use Drupal\node\Entity\Node; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; /** * Class ArloAPIController. */ class ArloAPIController extends ControllerBase { /** * Service endpoint function. Handles the submission from the Arlo * webhook. */ public function endpoint(Request $request) { $config = $this->config('arlo.settings'); $webhook_key = $config->get('webhook_key'); // Get the input from our posted data. If no data was posted, then we can // bail on the operation. $content = $request->getContent(); $headers = $_SERVER; /** * We need to figure out how to verify the signature of each incoming message * as it relates to the webhook key for the webhook. We have an example of * how it works here: * * https://github.com/ArloSoftware/webhookclients/blob/master/python-flask/app.py */ $auth = FALSE; foreach ($headers as $header => $value) { if (strtolower($header) == 'http_x_arlo_signature') { $key_bytes = base64_decode($webhook_key); $calculated = base64_encode(hash_hmac('sha512', $content, $key_bytes, TRUE)); if ($value == $calculated) { $auth = TRUE; } } } if ($auth === TRUE) { $data = json_decode($content, FALSE); // What we get from the API is metadata only. For "Event" resource types, the // resourceID is the event id. This needs to then be used to fetch the event // information from Arlo and either create a new event or update an existing // one as it exists in Drupal. if (!empty($data)) { // Handle event events. if (!empty($data->events)) { foreach ($data->events as $event) { switch ($event->type) { case 'Event.Created': $fetchedEvent = $this->fetchEvent($event->resourceId); if (!empty($fetchedEvent)) { $this->createEvent($fetchEvent); } break; case 'Event.Updated': $fetchedEvent = $this->fetchEvent($event->resourceId); if (!empty($fetchedEvent)) { $this->updateEvents($fetchedEvent); } else { // TODO: Not sure yet if we should delete the reference if there is no data // or handle the error differently. Right now we will mark this issue // as to be done. } break; default: break; } } } $response = [ 'data' => 'Success', 'method' => 'GET' ]; } else { $response = [ 'data' => 'Failure', 'method' => 'GET' ]; } } else { $response = [ 'data' => 'Failure', 'method' => 'GET' ]; } return new JsonResponse($response); } /** * Get a specific event from the Arlo API and return the result. * * @param int eventID * The EventID (or sometimes known as resourceID) of the event that we wish to * get from the Arlo API. If left blank wil return all events. * * @return array $items * An array of returned items. A single item if the eventID is specified or all * of the events if it is not. */ public function fetchEvent($eventID = NULL) { /** * Sample URL structure to fetch the data we need from an event. A full * list of fields can be found at: * * https://developer.arlo.co/doc/api/2012-02-01/pub/resources/eventsearch * * Note that if no eventID is passed, it will return all events which will * then need to be iterated through - so always be prepared for > 1 result. */ $config = $this->config('arlo.settings'); $platform_id = $config->get('platform_id'); $filter = (!empty($eventID)) ? '&filter=eventID=' . $eventID : NULL; $url = 'https://' . $platform_id . '/api/2012-02-01/pub/resources/eventsearch?fields=eventid,name,description,summary,sessionsdescription,presenters,viewuri' . $filter; try { $request = \Drupal::httpClient()->get($url, [ 'headers' => [ 'Accept' => 'application/json', ], ]); $status = $request->getStatusCode(); $response = $request->getBody(); $data = json_decode($response); if (!empty($data->Items)) { return $data->Items; } else { return []; } } catch (RequestException $e) { // TODO: Handle the error. } } /** * * Update an existing event when new information is presented via the update * webhook. * * @param array $payload * The payload array of objects that comes back from the Arlo API. * * @return array $status * Returns an array with the count of SAVED_NEW and SAVED_UPDATED counts. */ public function createEvent($payload) { $status = [ SAVED_NEW => 0, SAVED_UPDATED => 0, ]; foreach ($payload as $key => $event) { $node = Node::create(['type' => 'arlo_event']); $node->set('title', htmlspecialchars($event->Name)); $node->set('field_arlo_link', $event->ViewUri); $node->set('field_arlo_summary', $event->Summary); $node->set('field_arlo_event_id', $event->EventID); $node->set('body', $event->Description->Text); $node->enforceIsNew(); $return = $node->save(); $status[$return]++; } return $status; } /** * Synchronize events from Arlo into our Arlo Events content type. * * @return void */ public function syncEvents() { $events = $this->fetchEvent(); $this->updateEvents($events); \Drupal::messenger()->addStatus('Arlo Events/Template have been synchronized.'); (new RedirectResponse('/admin'))->send(); } /** * Update an existing event when new information is presented via the update * webhook. * * @param array $payload * The payload array of objects that comes back from the Arlo API. * * @return array $status * Returns an array with the count of SAVED_NEW and SAVED_UPDATED counts. */ public function updateEvents($payload) { $status = [ SAVED_NEW => 0, SAVED_UPDATED => 0, ]; foreach ($payload as $key => $event) { // We need to determine from the EventID if this is a new entry or // an update to an existing one. $nids = \Drupal::entityTypeManager()->getStorage('node')->getQuery() ->condition('field_arlo_event_id', $event->EventID) ->condition('type', 'arlo_event') ->accessCheck(TRUE) ->execute(); if (!empty($nids)) { if (count($nids) > 1) { // Now this would be a problem if we have more than one node of a // particular event. If this is the case, we need to remove all of // the existing nodes and save a new one. // // This is a failsafe for duplicate arlo_event nodes. \Drupal::logger('arlo')->notice('Arlo EventID ' . $event->EventID . ' has more than one node. Destroying existing nodes and creating a single new one.'); $handler = \Drupal::entityTypeManager()->getStorage("node"); $entities = $handler->loadMultiple($nids); $handler->delete($entities); $status = $this->createEvent(array($event)); } else { // Yes, we only have one nid, but it comes in an array, so iterate // to make it easy. foreach ($nids as $nid) { $node = \Drupal::entityTypeManager()->getStorage('node')->load($nid); $node->set('title', htmlspecialchars($event->Name)); $node->set('field_arlo_link', $event->ViewUri); $node->set('field_arlo_summary', $event->Summary); $node->set('field_arlo_event_id', $event->EventID); $node->set('body', $event->Description->Text); $node->save(); } } } else { $node = Node::create(['type' => 'arlo_event']); $node->set('title', htmlspecialchars($event->Name)); $node->set('field_arlo_link', $event->ViewUri); $node->set('field_arlo_summary', $event->Summary); $node->set('field_arlo_event_id', $event->EventID); $node->set('body', $event->Description->Text); $node->enforceIsNew(); $return = $node->save(); $status[$return]++; } } return $status; } }