Expand error condition checking on json_decode result
[shindig.git] / php / src / social / sample / JsonDbOpensocialService.php
blobbdd7d54dba979401f95b16dbccce311566792903
1 <?php
2 /**
3 * Licensed to the Apache Software Foundation (ASF) under one
4 * or more contributor license agreements. See the NOTICE file
5 * distributed with this work for additional information
6 * regarding copyright ownership. The ASF licenses this file
7 * to you under the Apache License, Version 2.0 (the
8 * "License"); you may not use this file except in compliance
9 * with the License. You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing,
14 * software distributed under the License is distributed on an
15 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 * KIND, either express or implied. See the License for the
17 * specific language governing permissions and limitations
18 * under the License.
21 /**
22 * Implementation of supported services backed by a JSON DB
24 class JsonDbOpensocialService implements ActivityService, PersonService, AppDataService, MessagesService {
26 /**
27 * The DB
29 private $db;
31 /**
32 * db["activities"] -> Array<Person>
34 private static $PEOPLE_TABLE = "people";
36 /**
37 * db["people"] -> Map<Person.Id, Array<Activity>>
39 private static $ACTIVITIES_TABLE = "activities";
41 /**
42 * db["data"] -> Map<Person.Id, Map<String, String>>
44 private static $DATA_TABLE = "data";
46 /**
47 * db["friendLinks"] -> Map<Person.Id, Array<Person.Id>>
49 private static $FRIEND_LINK_TABLE = "friendLinks";
51 /**
52 * db["userApplications"] -> Map<Person.Id, Array<Application Ids>>
54 private static $USER_APPLICATIONS_TABLE = "userApplications";
56 private $allPeople = null;
58 private $allData = null;
60 private $allActivities = null;
62 private $jsonDbFileName = 'ShindigDb.json';
64 public function getDb() {
65 try {
66 $fileName = sys_get_temp_dir() . '/' . $this->jsonDbFileName;
67 if (file_exists($fileName)) {
68 if (! is_readable($fileName)) {
69 throw new SocialSpiException("Could not read temp json db file: $fileName, check permissions", ResponseError::$INTERNAL_ERROR);
71 $cachedDb = file_get_contents($fileName);
72 $jsonDecoded = json_decode($cachedDb, true);
73 if ($jsonDecoded == $cachedDb || $jsonDecoded == null) {
74 throw new SocialSpiException("Failed to decode the json db", ResponseError::$INTERNAL_ERROR);
76 return $jsonDecoded;
77 } else {
78 $jsonDb = Config::get('jsondb_path');
79 if (! file_exists($jsonDb) || ! is_readable($jsonDb)) {
80 throw new SocialSpiException("Could not read json db file: $jsonDb, check if the file exists & has proper permissions", ResponseError::$INTERNAL_ERROR);
82 $dbConfig = @file_get_contents($jsonDb);
83 $contents = preg_replace('/(?<!http:|https:)\/\/.*$/m', '', preg_replace('@/\\*(?:.|[\\n\\r])*?\\*/@', '', $dbConfig));
84 $jsonDecoded = json_decode($contents, true);
85 if ($jsonDecoded == $contents || $jsonDecoded == null) {
86 throw new SocialSpiException("Failed to decode the json db", ResponseError::$INTERNAL_ERROR);
88 $this->saveDb($jsonDecoded);
89 return $jsonDecoded;
91 } catch (Exception $e) {
92 throw new SocialSpiException("An error occured while reading/writing the json db: " . $e->getMessage(), ResponseError::$INTERNAL_ERROR);
96 private function saveDb($db) {
97 if (! @file_put_contents(sys_get_temp_dir() . '/' . $this->jsonDbFileName, json_encode($db))) {
98 throw new Exception("Could not save json db: " . sys_get_temp_dir() . '/' . $this->jsonDbFileName);
102 private function getAllPeople() {
103 $db = $this->getDb();
104 $peopleTable = $db[self::$PEOPLE_TABLE];
105 foreach ($peopleTable as $people) {
106 $this->allPeople[$people['id']] = $people;
108 $db[self::$PEOPLE_TABLE] = $this->allPeople;
109 return $this->allPeople;
112 private function getAllData() {
113 $db = $this->getDb();
114 $dataTable = $db[self::$DATA_TABLE];
115 foreach ($dataTable as $key => $value) {
116 $this->allData[$key] = $value;
118 $db[self::$DATA_TABLE] = $this->allData;
119 return $this->allData;
122 private function getAllActivities() {
123 $db = $this->getDb();
124 $activitiesTable = $db[self::$ACTIVITIES_TABLE];
125 foreach ($activitiesTable as $key => $value) {
126 $this->allActivities[$key] = $value;
128 $db[self::$ACTIVITIES_TABLE] = $this->allActivities;
129 return $this->allActivities;
132 private function getPeopleWithApp($appId) {
133 $peopleWithApp = array();
134 $db = $this->getDb();
135 $userApplicationsTable = $db[self::$USER_APPLICATIONS_TABLE];
136 foreach ($userApplicationsTable as $key => $value) {
137 if (in_array($appId, $userApplicationsTable[$key])) {
138 $peopleWithApp[] = $key;
141 return $peopleWithApp;
144 public function getPerson($userId, $groupId, $fields, SecurityToken $token) {
145 if (! is_object($groupId)) {
146 throw new SocialSpiException("Not Implemented", ResponseError::$NOT_IMPLEMENTED);
148 $person = $this->getPeople($userId, $groupId, new CollectionOptions(), $fields, $token);
149 if (is_array($person->getEntry())) {
150 $person = $person->getEntry();
151 if (is_array($person) && count($person) == 1) {
152 return array_pop($person);
155 throw new SocialSpiException("Person not found", ResponseError::$BAD_REQUEST);
158 public function getPeople($userId, $groupId, CollectionOptions $options, $fields, SecurityToken $token) {
160 $sortOrder = $options->getSortOrder();
161 $filter = $options->getFilterBy();
162 $first = $options->getStartIndex();
163 $max = $options->getCount();
164 $networkDistance = $options->getNetworkDistance();
165 $ids = $this->getIdSet($userId, $groupId, $token);
166 $allPeople = $this->getAllPeople();
167 if (! $token->isAnonymous() && $filter == "hasApp") {
168 $appId = $token->getAppId();
169 $peopleWithApp = $this->getPeopleWithApp($appId);
171 $people = array();
172 foreach ($ids as $id) {
173 if ($filter == "hasApp" && ! in_array($id, $peopleWithApp)) {
174 continue;
176 $person = null;
177 if (is_array($allPeople) && isset($allPeople[$id])) {
178 $person = $allPeople[$id];
179 if (! $token->isAnonymous() && $id == $token->getViewerId()) {
180 $person['isViewer'] = true;
182 if (! $token->isAnonymous() && $id == $token->getOwnerId()) {
183 $person['isOwner'] = true;
185 if ($fields[0] != '@all') {
186 $newPerson = array();
187 $newPerson['isOwner'] = isset($person['isOwner']) ? $person['isOwner'] : false;
188 $newPerson['isViewer'] = isset($person['isViewer']) ? $person['isViewer'] : false;
189 $newPerson['name'] = $person['name'];
190 $newPerson['displayName'] = $person['displayName'];
191 foreach ($fields as $field => $present) {
192 $present = strtolower($present);
193 if (isset($person[$present]) && ! isset($newPerson[$present])) {
194 $newPerson[$present] = $person[$present];
197 $person = $newPerson;
199 $people[$id] = $person;
202 if ($sortOrder == 'name') {
203 usort($people, array($this, 'comparator'));
206 try {
207 $people = $this->filterResults($people, $options);
208 } catch(Exception $e) {
209 $people['filtered'] = 'false';
212 //TODO: The samplecontainer doesn't support any filters yet. We should fix this.
213 $totalSize = count($people);
214 $collection = new RestfulCollection($people, $options->getStartIndex(), $totalSize);
215 $collection->setItemsPerPage($options->getCount());
216 return $collection;
219 private function filterResults($peopleById, $options) {
220 if (! $options->getFilterBy()) {
221 return $peopleById; // no filtering specified
223 $filterBy = $options->getFilterBy();
224 $op = $options->getFilterOperation();
225 if (! $op) {
226 $op = CollectionOptions::FILTER_OP_EQUALS; // use this container-specific default
228 $value = $options->getFilterValue();
229 $filteredResults = array();
230 $numFilteredResults = 0;
231 foreach ($peopleById as $id => $person) {
232 if ($this->passesFilter($person, $filterBy, $op, $value)) {
233 $filteredResults[$id] = $person;
234 $numFilteredResults ++;
237 return $filteredResults;
240 private function passesFilter($person, $filterBy, $op, $value) {
241 $fieldValue = $person[$filterBy];
242 if (! $fieldValue || (is_array($fieldValue) && ! count($fieldValue))) {
243 return false; // person is missing the field being filtered for
245 if ($op == CollectionOptions::FILTER_OP_PRESENT) {
246 return true; // person has a non-empty value for the requested field
248 if (!$value) {
249 return false; // can't do an equals/startswith/contains filter on an empty filter value
251 // grab string value for comparison
252 if (is_array($fieldValue)) {
253 // plural fields match if any instance of that field matches
254 foreach ($fieldValue as $field) {
255 if ($this->passesStringFilter($field, $op, $value)) {
256 return true;
259 } else {
260 return $this->passesStringFilter($fieldValue, $op, $value);
263 return false;
266 public function getPersonData($userId, GroupId $groupId, $appId, $fields, SecurityToken $token) {
267 if (! isset($fields[0])) {
268 $fields[0] = '@all';
270 $db = $this->getDb();
271 $allData = $this->getAllData();
272 $friendsTable = $db[self::$FRIEND_LINK_TABLE];
273 $data = array();
274 $ids = $this->getIdSet($userId, $groupId, $token);
275 foreach ($ids as $id) {
276 if (isset($allData[$id])) {
277 $allPersonData = $allData[$id];
278 $personData = array();
279 foreach (array_keys($allPersonData) as $key) {
280 if (in_array($key, $fields) || in_array("@all", $fields)) {
281 $personData[$key] = $allPersonData[$key];
284 $data[$id] = $personData;
287 return new DataCollection($data);
290 public function updatePersonData(UserId $userId, GroupId $groupId, $appId, $fields, $values, SecurityToken $token) {
291 $db = $this->getDb();
292 foreach ($fields as $key => $present) {
293 if (! $this->isValidKey($present)) {
294 throw new SocialSpiException("The person app data key had invalid characters", ResponseError::$BAD_REQUEST);
297 $allData = $this->getAllData();
298 $person = $allData[$userId->getUserId($token)];
299 switch ($groupId->getType()) {
300 case 'self':
301 foreach ($fields as $key => $present) {
302 $value = isset($values[$present]) ? @$values[$present] : null;
303 $person[$present] = $value;
305 break;
306 default:
307 throw new SocialSpiException("We don't support updating data in batches yet", ResponseError::$NOT_IMPLEMENTED);
308 break;
310 $allData[$userId->getUserId($token)] = $person;
311 $db[self::$DATA_TABLE] = $allData;
312 $this->saveDb($db);
313 return null;
316 public function deletePersonData($userId, GroupId $groupId, $appId, $fields, SecurityToken $token) {
317 $db = $this->getDb();
318 $allData = $this->getAllData();
319 if ($fields == null || $fields[0] == '*') {
320 $allData[$userId->getUserId($token)] = null;
321 $db[self::$DATA_TABLE] = $allData;
322 $this->saveDb($db);
323 return null;
325 foreach ($fields as $key => $present) {
326 if (! $this->isValidKey($key)) {
327 throw new SocialSpiException("The person app data key had invalid characters", ResponseError::$BAD_REQUEST);
330 switch ($groupId->getType()) {
331 case 'self':
332 foreach ($fields as $key => $present) {
333 $value = isset($values[$key]) ? null : @$values[$key];
334 $person[$key] = $value;
336 $allData[$userId->getUserId($token)] = $person;
337 $db[self::$DATA_TABLE] = $allData;
338 $this->saveDb($db);
339 break;
340 default:
341 throw new SocialSpiException("We don't support updating data in batches yet", ResponseError::$NOT_IMPLEMENTED);
342 break;
344 return null;
347 public function getActivity($userId, $groupId, $appdId, $fields, $activityId, SecurityToken $token) {
348 $activities = $this->getActivities($userId, $groupId, $appdId, null, null, null, null, $fields, array($activityId), $token);
349 if ($activities instanceof RestfulCollection) {
350 $activities = $activities->getEntry();
351 foreach ($activities as $activity) {
352 if ($activity->getId() == $activityId) {
353 return $activity;
357 throw new SocialSpiException("Activity not found", ResponseError::$NOT_FOUND);
360 public function getActivities($userIds, $groupId, $appId, $sortBy, $filterBy, $filterOp, $filterValue, $startIndex, $count, $fields, $activityIds, $token) {
361 $db = $this->getDb();
362 $friendsTable = $db[self::$FRIEND_LINK_TABLE];
363 $ids = array();
364 $ids = $this->getIdSet($userIds, $groupId, $token);
365 $allActivities = $this->getAllActivities();
366 $activities = array();
367 foreach ($ids as $id) {
368 if (isset($allActivities[$id])) {
369 $personsActivities = $allActivities[$id];
370 $activities = array_merge($activities, $personsActivities);
371 if ($fields) {
372 $newPersonsActivities = array();
373 foreach ($personsActivities as $activity) {
374 $newActivity = array();
375 foreach ($fields as $field => $present) {
376 $newActivity[$present] = $activity[$present];
378 $newPersonsActivities[] = $newActivity;
380 $personsActivities = $newPersonsActivities;
381 $activities = $personsActivities;
383 if ($filterBy && $filterValue) {
384 $newActivities = array();
385 foreach ($activities as $activity) {
386 if (array_key_exists($filterBy, $activity)) {
387 if ($this->passesStringFilter($activity[$filterBy], $filterOp, $filterValue)) {
388 $newActivities[] = $activity;
390 } else {
391 throw new SocialSpiException("Invalid filterby parameter", ResponseError::$NOT_FOUND);
394 $activities = $newActivities;
398 $totalResults = count($activities);
399 if (! $totalResults) {
400 throw new SocialSpiException("Activity not found", ResponseError::$NOT_FOUND);
402 $activities = array_slice($activities, $startIndex, $count);
403 $ret = new RestfulCollection($activities, $startIndex, $totalResults);
404 $ret->setItemsPerPage($count);
405 return $ret;
410 * to check the activity against filter
413 private function passesStringFilter($fieldValue, $filterOp, $filterValue) {
414 switch ($filterOp) {
415 case CollectionOptions::FILTER_OP_EQUALS:
416 return $fieldValue == $filterValue;
417 case CollectionOptions::FILTER_OP_CONTAINS:
418 return strpos($fieldValue, $filterValue) !== false;
419 case CollectionOptions::FILTER_OP_STARTSWITH:
420 return strpos($fieldValue, $filterValue) === 0;
421 default:
422 throw new Exception('unrecognized filterOp');
426 public function createActivity($userId, $groupId, $appId, $fields, $activity, SecurityToken $token) {
427 $db = $this->getDb();
428 $activitiesTable = $this->getAllActivities();
429 $activity['appId'] = $token->getAppId();
430 try {
431 array_push($activitiesTable[$userId->getUserId($token)], $activity);
432 $db[self::$ACTIVITIES_TABLE] = $activitiesTable;
433 $this->saveDb($db);
434 // Should this return something to show success?
435 } catch (Exception $e) {
436 throw new SocialSpiException("Activity can't be created: " . $e->getMessage(), ResponseError::$INTERNAL_ERROR);
440 public function deleteActivities($userId, $groupId, $appId, $activityIds, SecurityToken $token) {
441 throw new SocialSpiException("Not implemented", ResponseError::$NOT_IMPLEMENTED);
444 public function createMessage($userId, $appId, $message, $optionalMessageId, SecurityToken $token) {
445 throw new SocialSpiException("Not implemented", ResponseError::$NOT_IMPLEMENTED);
449 * Determines whether the input is a valid key.
451 * @param key the key to validate.
452 * @return true if the key is a valid appdata key, false otherwise.
454 public static function isValidKey($key) {
455 if (empty($key)) {
456 return false;
458 for ($i = 0; $i < strlen($key); ++ $i) {
459 $c = substr($key, $i, 1);
460 if (($c >= 'a' && $c <= 'z') || ($c >= 'A' && $c <= 'Z') || ($c >= '0' && $c <= '9') || ($c == '-') || ($c == '_') || ($c == '.')) {
461 continue;
463 return false;
465 return true;
468 private function comparator($person, $person1) {
469 $name = $person['name']['unstructured'];
470 $name1 = $person1['name']['unstructured'];
471 if ($name == $name1) {
472 return 0;
474 return ($name < $name1) ? - 1 : 1;
478 * Get the set of user id's from a user or collection of users, and group
479 * Code taken from http://code.google.com/p/partuza/source/browse/trunk/Shindig/PartuzaService.php
481 private function getIdSet($user, GroupId $group, SecurityToken $token) {
482 $ids = array();
483 $db = $this->getDb();
484 $friendsTable = $db[self::$FRIEND_LINK_TABLE];
485 if ($user instanceof UserId) {
486 $userId = $user->getUserId($token);
487 if ($group == null) {
488 return array($userId);
490 switch ($group->getType()) {
491 case 'self':
492 $ids[] = $userId;
493 break;
494 case 'all':
495 case 'friends':
496 if (is_array($friendsTable) && count($friendsTable) && isset($friendsTable[$userId])) {
497 $ids = $friendsTable[$userId];
499 break;
500 default:
501 return new ResponseItem(NOT_IMPLEMENTED, "We don't support fetching data in batches yet", null);
502 break;
504 } elseif (is_array($user)) {
505 $ids = array();
506 foreach ($user as $id) {
507 $ids = array_merge($ids, $this->getIdSet($id, $group, $token));
510 return $ids;