getElementsByTagName('*'); if ($dataTags->length > 0) { $dataPipeliningTags = array(); $namespaceErrorTags = array('httprequest', 'datarequest', 'peoplerequest', 'personappdatarequest', 'viewerrequest', 'ownerrequest', 'activitiesrequest'); foreach ($dataTags as $dataTag) { $tag = array(); $tag['type'] = $dataTag->tagName; $supportedDataAttributes = array('key', 'method', 'userId', 'groupId', 'fields', 'startIndex', 'count', 'sortBy', 'sortOrder', 'filterBy', 'filterOp', 'filterValue', 'activityIds', 'href', 'params'); foreach ($supportedDataAttributes as $dataAttribute) { $val = $dataTag->getAttribute($dataAttribute); if (! empty($val)) { $tag[$dataAttribute] = $val; } } // Make sure the proper name space decleration was used, either parsing would fail miserably if (in_array(strtolower($tag['type']), $namespaceErrorTags)) { throw new ExpressionException("Invalid os-data namespace, please use xmlns:os=\"http://ns.opensocial.org/2008/markup\" in the script tag"); } // normalize the methods so that os:PeopleRequest becomes a os:DataRequest with a people.get method, and os:ViewerRequest becomes a people.get with a userId = @viewer & groupId = @self, this // makes it a whole lot simpler to implement the actual data fetching in the renderer switch ($tag['type']) { case 'os:PeopleRequest': $tag['type'] = 'os:DataRequest'; $tag['method'] = 'people.get'; break; case 'os:PersonAppDataRequest': $tag['type'] = 'os:DataRequest'; $tag['method'] = 'appdata.get'; break; case 'os:ViewerRequest': case 'os:OwnerRequest': $tag['method'] = 'people.get'; $tag['userId'] = $tag['type'] == 'os:ViewerRequest' ? '@viewer' : '@owner'; $tag['groupId'] = '@self'; $tag['type'] = 'os:DataRequest'; break; case 'os:ActivitiesRequest': $tag['type'] = 'os:DataRequest'; $tag['method'] = 'activities.get'; break; } $dataPipeliningTags[] = $tag; } return $dataPipeliningTags; } return null; } /** * Fetches the requested data-pipeling info * * @param array $dataPipelining contains the parsed data-pipelining tags * @param GadgetContext $context context to use for fetching * @param array $dataContext the data context to use while resolving expressions in requests (it'll use the combined results + context to resolve) * @return array result */ static public function fetch($dataPipeliningRequests, GadgetContext $context, $dataContext = array()) { $result = array(); if (is_array($dataPipeliningRequests) && count($dataPipeliningRequests)) { do { // See which requests we can batch together, that either don't use dynamic tags or who's tags are resolvable $requestQueue = array(); foreach ($dataPipeliningRequests as $key => $request) { if (($resolved = self::resolveRequest($request, $result)) !== false) { $requestQueue[] = $resolved; unset($dataPipeliningRequests[$key]); } } if (count($requestQueue)) { $returnedResults = self::performRequests($requestQueue, $context); if (is_array($returnedResults)) { $dataContext = self::addResultToContext($returnedResults, $dataContext); $result = array_merge($returnedResults, $result); } } } while (count($requestQueue)); } return $result; } /** * Adds the fetched results to the data context, used by the fetch() function to * add the performed requests to the data context that's used to resolve expressions * * @param array $returnedResults * @param array $dataContext * @return array */ static private function addResultToContext($returnedResults, $dataContext) { foreach ($returnedResults as $val) { // we really only accept entries with a request id, otherwise it can't be referenced by context anyhow if (isset($val['id'])) { $key = $val['id']; // Pick up only the actual data part of the response, so we can do direct variable resolution if (isset($val['result']['list'])) { $dataContext[$key] = $val['result']['list']; } elseif (isset($val['result']['entry'])) { $dataContext[$key] = $val['result']['entry']; } elseif (isset($val['result'])) { $dataContext[$key] = $val['result']; } } } return $dataContext; } /** * Peforms the actual http fetching of the data-pipelining requests, all social requests * are made to $_SERVER['HTTP_HOST'] (the virtual host name of this server) / (optional) web_prefix / social / rpc, and * the httpRequest's are made to $_SERVER['HTTP_HOST'] (the virtual host name of this server) / (optional) web_prefix / gadgets / makeRequest * both request types use the current security token ($_GET['st']) when performing the requests so they happen in the correct context * * @param array $requests * @param GadgetContext $context * @return array response */ static private function performRequests($requests, GadgetContext $context) { $jsonRequests = array(); $httpRequests = array(); $decodedResponse = array(); // Using the same gadget security token for all social & http requests so everything happens in the right context if (! BasicSecurityToken::getTokenStringFromRequest()) { throw new ExpressionException("No security token set, required for data-pipeling"); } $securityToken = $_GET['st']; foreach ($requests as $request) { switch ($request['type']) { case 'os:DataRequest': // Add to the social request batch $id = $request['key']; $method = $request['method']; // remove our internal fields so we can use the remainder as params unset($request['key']); unset($request['method']); unset($request['type']); if (isset($request['fields'])) { $request['fields'] = explode(',', $request['fields']); } $jsonRequests[] = array('method' => $method, 'id' => $id, 'params' => $request); break; case 'os:HttpRequest': $id = $request['key']; $url = $request['href']; $format = isset($request['format']) ? $request['format'] : 'json'; unset($request['key']); unset($request['type']); unset($request['href']); $httpRequests[$url] = array('id' => $id, 'url' => $url, 'format' => $format, 'queryStr' => implode('&', $request)); break; } } if (count($jsonRequests)) { // perform social api requests $request = new RemoteContentRequest('http://'.$_SERVER['HTTP_HOST'] . Config::get('web_prefix') . '/rpc?st=' . urlencode($securityToken) . '&format=json', "Content-Type: application/json\n", json_encode($jsonRequests)); $request->setMethod('POST'); $remoteFetcherClass = Config::get('remote_content_fetcher'); $remoteFetcher = new $remoteFetcherClass(); $basicRemoteContent = new BasicRemoteContent($remoteFetcher); $response = $basicRemoteContent->fetch($request); $decodedResponse = json_decode($response->getResponseContent(), true); } if (count($httpRequests)) { $requestQueue = array(); foreach ($httpRequests as $request) { $req = new RemoteContentRequest($_SERVER['HTTP_HOST'] . Config::get('web_prefix') . '/gadgets/makeRequest?url=' . urlencode($request['url']) . '&st=' . urlencode($securityToken) . (! empty($request['queryStr']) ? '&' . $request['queryStr'] : '')); $req->getOptions()->ignoreCache = $context->getIgnoreCache(); $req->setNotSignedUri($request['url']); $requestQueue[] = $req; } $basicRemoteContent = new BasicRemoteContent(); $resps = $basicRemoteContent->multiFetch($requestQueue); foreach ($resps as $response) { //FIXME: this isn't completely correct yet since this picks up the status code and headers // as they are returned by the makeRequest handler and not the ones from the original request $url = $response->getNotSignedUrl(); $id = $httpRequests[$url]['id']; // strip out the UNPARSEABLE_CRUFT (see makeRequestHandler.php) on assigning the body $resp = json_decode(str_replace("throw 1; < don't be evil' >", '', $response->getResponseContent()), true); if (is_array($resp)) { $statusCode = $response->getHttpCode(); $statusCodeMessage = $response->getHttpCodeMsg(); $headers = $response->getHeaders(); if (intval($statusCode) == 200) { $content = $httpRequests[$url]['format'] == 'json' ? json_decode($resp[$url]['body'], true) : $resp[$url]['body']; $toAdd = array( 'result' => array( 'content' => $content, 'status' => $statusCode, 'headers' => $headers ) ); } else { $content = $resp[$url]['body']; $toAdd = array( 'error' => array( 'code' => $statusCode, 'message' => $statusCodeMessage, 'result' => array( 'content' => $content, 'headers' => $headers ) ) ); } //$toAdd[$id] = array('id' => $id, 'result' => $httpRequests[$url]['format'] == 'json' ? json_decode($resp[$url]['body'], true) : $resp[$url]['body']); $decodedResponse[] = array('id' => $id, 'result' => $toAdd); } } } return $decodedResponse; } /** * If a request (data-pipelining tag) doesn't include any dynamic tags, it's returned as is. If * however it does contain said tag, this function will attempt to resolve it using the $result * array, returning the parsed request on success, or FALSE on failure to resolve. * * @param array $request * @param array $result * @return array */ static private function resolveRequest($request, $result) { $dataContext = self::makeContextData($result); foreach ($request as $key => $val) { $expressions = array(); preg_match_all('/\$\{(.*)\}/imxsU', $val, $expressions); $expressionCount = count($expressions[0]); if ($expressionCount) { for ($i = 0; $i < $expressionCount; $i ++) { $toReplace = $expressions[0][$i]; $expression = $expressions[1][$i]; try { $expressionResult = ExpressionParser::evaluate($expression, $dataContext); $request[$key] = str_replace($toReplace, $expressionResult, $request[$key]); } catch (\Exception $e) { // ignore, maybe on the next pass we can resolve this return false; } } } } return $request; } /** * Makes a data context array out of the current data pipelining results that can be used * by the expression parser to resolve the request attributes * * @param array $array current data pipelining results * @return array $dataContext a dataContext array */ static private function makeContextData($array) { $result = array(); foreach ($array as $val) { if (isset($val['id'])) { $key = $val['id']; if (isset($val['result']['list'])) { $result[$key] = $val['result']['list']; } elseif (isset($val['result']['entry'])) { $result[$key] = $val['result']['entry']; } elseif (isset($val['result'])) { $result[$key] = $val['result']; } } } $dataContext = array(); $dataContext['Top'] = $result; $dataContext['Cur'] = array(); $dataContext['My'] = array(); $dataContext['Context'] = array('UniqueId' => uniqid()); return $dataContext; } }