/** * Handles all contexts * !TODO - handle more than one model in one request * * @param SS_HTTPRequest $req * @return SS_HTTPResponse * @throws Exception */ public function index(SS_HTTPRequest $req) { $model = $req->requestVar('model'); if (!$model) { return $this->fail(400); } // allow different date formats for different clients (eg. android) if ($df = $req->requestVar('date_format')) { self::$date_format = $df; } // this just makes the crossdomain ajax stuff simpler and // keeps anything weird from happening there. if (self::$allow_crossdomain && $_SERVER['REQUEST_METHOD'] === 'OPTIONS') { // $response = $this->getResponse(); // $response->addHeader("Access-Control-Allow-Origin", "*"); // $response->addHeader("Access-Control-Allow-Headers", "X-Requested-With"); // return $response; header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Headers: X-Requested-With"); exit; } //Debug::log(print_r($req->requestVars(),true)); // find the configuration $context = SyncContext::current(); if (!$context) { return $this->fail(400); } $cfg = $context->getConfig($model); // is syncing this model allowed? if (!$cfg || !is_array($cfg) || !isset($cfg['type']) || $cfg['type'] == SYNC_NONE) { return $this->fail(403, 'Access denied'); } $fields = isset($cfg['fields']) ? explode(',', $cfg['fields']) : array_keys(singleton($model)->db()); if (count($fields) == 0) { return $this->fail(403, 'Access denied'); } $fieldFilters = SyncFilterHelper::process_fields($fields); $fieldFilters['ID'] = false; $fieldFilters['LastEdited'] = false; $fields = array_keys($fieldFilters); // do we need to swap out for a parent table or anything? if (isset($cfg['model'])) { $model = $cfg['model']; } // build up the rest of the config with defaults if (!isset($cfg['filter'])) { $cfg['filter'] = array(); } if (!isset($cfg['join'])) { $cfg['join'] = array(); } if (!isset($cfg['sort'])) { $cfg['sort'] = ''; } if (!isset($cfg['limit'])) { $cfg['limit'] = ''; } // check authentication if (!$context->checkAuth($req->requestVars())) { return $this->fail(403, 'Incorrect or invalid authentication'); } // there are a few magic values that can be used in the filters: // :future // :last X days $cfg['filter'] = SyncFilterHelper::process_filters($cfg['filter']); // fill in any blanks in the filters based on the request input $replacements = $context->getFilterVariables($req->requestVars()); $cfg['filter'] = str_replace(array_keys($replacements), array_values($replacements), $cfg['filter']); // input arrays $insert = $req->requestVar('insert') ? json_decode($req->requestVar('insert'), true) : array(); $check = $req->requestVar('check') ? json_decode($req->requestVar('check'), true) : array(); $update = $req->requestVar('update') ? json_decode($req->requestVar('update'), true) : array(); // output arrays $clientSend = array(); $clientInsert = array(); $clientUpdate = array(); $clientDelete = array(); // check modification times on any existing records // NOTE: if update is set we assume this is the second request (#3 above) if (count($update) == 0) { if ($cfg['type'] == SYNC_DOWN || $cfg['type'] == SYNC_FULL) { $list = DataObject::get($model); if ($cfg['filter']) { $list = $list->filter($cfg['filter']); } if ($cfg['sort']) { $list = $list->sort($cfg['sort']); } if ($cfg['limit']) { $list = $list->limit($cfg['limit']); } if ($cfg['join'] && count($cfg['join']) > 0) { if (!is_array($cfg['join'])) { throw new Exception('Invalid join syntax'); } $fn = count($cfg['join']) > 2 ? $cfg['join'] . 'Join' : 'innerJoin'; $list = $list->{$fn}($cfg['join'][0], $cfg['join'][1]); } //$map = $list->map('ID', 'LastEdited'); $map = array(); $objMap = array(); foreach ($list as $rec) { $map[$rec->ID] = strtotime($rec->LastEdited); $objMap[$rec->ID] = $rec; } // take out the id's that are up-to-date form the map // also add any inserts and deletes at this point if (is_array($check)) { foreach ($check as $rec) { if (isset($map[$rec['ID']])) { $serverTS = $map[$rec['ID']]; $clientTS = max($rec['TS'], 0); if ($serverTS > $clientTS) { // the server is newer than the client // mark it to be sent back as a clientUpdate $clientUpdate[] = self::to_array($objMap[$rec['ID']], $fields, $fieldFilters); } elseif ($clientTS > $serverTS) { // the version on the client is newer than the server // add it to the clientSend list (i.e. request the data back from the client) $clientSend[] = $rec['ID']; } else { // the versions are the same, leave well enough alone } // $objMap is now our insert list, so we remove this id from it unset($objMap[$rec['ID']]); } else { // if it's present on the client WITH an ID but not present // on the server, it means we've deleted it and need to notify // the client $clientDelete[] = $rec['ID']; } } } // anything left on the $map right now needs to be inserted if (count($objMap) > 0) { foreach ($objMap as $id => $obj) { $clientInsert[] = self::to_array($obj, $fields, $fieldFilters); } } } // insert any new records if (($cfg['type'] == SYNC_FULL || $cfg['type'] == SYNC_UP) && is_array($insert)) { foreach ($insert as $rec) { unset($rec['ID']); unset($rec['LocalID']); $obj = new $model(); $obj->castedUpdate(self::filter_fields($rec, $fields)); $obj->write(); // send the object back so it gets an id, etc if ($cfg['type'] == SYNC_FULL) { $clientInsert[] = self::to_array($obj, $fields, $fieldFilters); } } } // NOTE: for SYNC_UP, if there do happen to be any records left // on the client, we want to tell it to delete them. that probably // means the model has changed from sync_full to sync_up OR // there was a bug at some point. Best to clean up the mess. if ($cfg['type'] == SYNC_UP && is_array($check) && count($check) > 0) { foreach ($check as $rec) { $clientDelete[] = $rec['ID']; } } } else { if (($cfg['type'] == SYNC_FULL || $cfg['type'] == SYNC_UP) && is_array($update)) { // update records foreach ($update as $rec) { $obj = DataObject::get_by_id($model, $rec['ID']); unset($rec['ID']); unset($rec['LocalID']); unset($rec['ClassName']); $obj->castedUpdate(self::filter_fields($rec, $fields)); $obj->write(); } } } // respond return $this->respond(array('ok' => 1, 'send' => $clientSend, 'update' => $clientUpdate, 'insert' => $clientInsert, 'del' => $clientDelete)); }
/** * create our particular contexts */ function setUpOnce() { SyncContext::add(array('test' => array('Page' => array('type' => SYNC_FULL, 'fields' => 'ID,LastEdited,Title'), 'File' => array('type' => SYNC_UP, 'fields' => 'ID,LastEdited,Name,Title,Filename')), 'test2' => array('Page' => array('type' => SYNC_NONE)))); }