/** * Parse the given skiptoken, validate it using the given InternalOrderByInfo * and generates instance of InternalSkipTokenInfo. * * @param ResourceType &$resourceType The resource type of the * resource targeted by the * resource path. * @param InternalOrderByInfo &$internalOrderByInfo The $orderby details. * @param string $skipToken The $skiptoken value. * * @return InternalSkipTokenInfo * * @throws ODataException */ public static function parseSkipTokenClause(ResourceType &$resourceType, InternalOrderByInfo &$internalOrderByInfo, $skipToken) { $tokenValueDescriptor = null; if (!KeyDescriptor::tryParseValuesFromSkipToken($skipToken, $tokenValueDescriptor)) { throw ODataException::createSyntaxError(Messages::skipTokenParserSyntaxError($skipToken)); } $orderByPathSegments = null; //$positionalValues are of type array(int, array(string, IType)) $positionalValues =& $tokenValueDescriptor->getPositionalValuesByRef(); $count = count($positionalValues); $orderByPathSegments = $internalOrderByInfo->getOrderByPathSegments(); $orderByPathCount = count($orderByPathSegments); if ($count != $orderByPathCount) { throw ODataException::createBadRequestError(Messages::skipTokenParserSkipTokenNotMatchingOrdering($count, $skipToken, $orderByPathCount)); } $i = 0; foreach ($orderByPathSegments as $orderByPathSegment) { $typeProvidedInSkipToken = $positionalValues[$i][1]; if (!$typeProvidedInSkipToken instanceof Null1) { $orderBySubPathSegments = $orderByPathSegment->getSubPathSegments(); $j = count($orderBySubPathSegments) - 1; $expectedType = $orderBySubPathSegments[$j]->getInstanceType(); if (!$expectedType->isCompatibleWith($typeProvidedInSkipToken)) { throw ODataException::createSyntaxError(Messages::skipTokenParserInCompatibleTypeAtPosition($skipToken, $expectedType->getFullTypeName(), $i, $typeProvidedInSkipToken->getFullTypeName())); } } $i++; } return new InternalSkipTokenInfo($internalOrderByInfo, $positionalValues, $resourceType); }
/** * Process the request Uri and creates an instance of * RequestDescription from the processed uri. * * @param IService $service Reference to the data service instance. * * @return RequestDescription * * @throws ODataException If any exception occurs while processing the segments * or in case of any version incompatibility. */ public static function process(IService $service) { $host = $service->getHost(); $absoluteRequestUri = $host->getAbsoluteRequestUri(); $absoluteServiceUri = $host->getAbsoluteServiceUri(); $requestUriSegments = array_slice($absoluteRequestUri->getSegments(), $absoluteServiceUri->getSegmentCount()); $segments = SegmentParser::parseRequestUriSegments($requestUriSegments, $service->getProvidersWrapper(), true); $request = new RequestDescription($segments, $absoluteRequestUri, $service->getConfiguration()->getMaxDataServiceVersion(), $host->getRequestVersion(), $host->getRequestMaxVersion()); $kind = $request->getTargetKind(); if ($kind == TargetKind::METADATA() || $kind == TargetKind::BATCH() || $kind == TargetKind::SERVICE_DIRECTORY()) { return $request; } if ($kind == TargetKind::PRIMITIVE_VALUE() || $kind == TargetKind::MEDIA_RESOURCE()) { // http://odata/NW.svc/Orders/$count // http://odata/NW.svc/Orders(123)/Customer/CustomerID/$value // http://odata/NW.svc/Employees(1)/$value // http://odata/NW.svc/Employees(1)/ThumbNail_48X48/$value $request->setContainerName($segments[count($segments) - 2]->getIdentifier()); } else { $request->setContainerName($request->getIdentifier()); } if ($request->getIdentifier() === ODataConstants::URI_COUNT_SEGMENT) { if (!$service->getConfiguration()->getAcceptCountRequests()) { throw ODataException::createBadRequestError(Messages::configurationCountNotAccepted()); } $request->queryType = QueryType::COUNT(); // use of $count requires request DataServiceVersion // and MaxDataServiceVersion greater than or equal to 2.0 $request->raiseResponseVersion(2, 0); $request->raiseMinVersionRequirement(2, 0); } else { if ($request->isNamedStream()) { $request->raiseMinVersionRequirement(3, 0); } else { if ($request->getTargetKind() == TargetKind::RESOURCE()) { if (!$request->isLinkUri()) { $resourceSetWrapper = $request->getTargetResourceSetWrapper(); //assert($resourceSetWrapper != null) $hasNamedStream = $resourceSetWrapper->hasNamedStreams($service->getProvidersWrapper()); $hasBagProperty = $resourceSetWrapper->hasBagProperty($service->getProvidersWrapper()); if ($hasNamedStream || $hasBagProperty) { $request->raiseResponseVersion(3, 0); } } } else { if ($request->getTargetKind() == TargetKind::BAG()) { $request->raiseResponseVersion(3, 0); } } } } return $request; }
/** * For the given entry object compare it's eTag (if it has eTag properties) * with current eTag request headers (if it present). * * @param mixed &$entryObject entity resource for which etag * needs to be checked. * @param ResourceType &$resourceType Resource type of the entry * object. * @param boolean &$needToSerializeResponse On return, this will contain * True if response needs to be * serialized, False otherwise. * * @return string|null The ETag for the entry object if it has eTag properties * NULL otherwise. */ protected function compareETag(&$entryObject, ResourceType &$resourceType, &$needToSerializeResponse) { $needToSerializeResponse = true; $eTag = null; $ifMatch = $this->_serviceHost->getRequestIfMatch(); $ifNoneMatch = $this->_serviceHost->getRequestIfNoneMatch(); if (is_null($entryObject)) { if (!is_null($ifMatch)) { throw ODataException::createPreConditionFailedError(Messages::eTagNotAllowedForNonExistingResource()); } return null; } if ($this->config->getValidateETagHeader() && !$resourceType->hasETagProperties()) { if (!is_null($ifMatch) || !is_null($ifNoneMatch)) { // No eTag properties but request has eTag headers, bad request throw ODataException::createBadRequestError(Messages::noETagPropertiesForType()); } // We need write the response but no eTag header return null; } if (!$this->config->getValidateETagHeader()) { // Configuration says do not validate ETag so we will not write ETag header in the // response even though the requested resource support it return null; } if (is_null($ifMatch) && is_null($ifNoneMatch)) { // No request eTag header, we need to write the response // and eTag header } else { if (strcmp($ifMatch, '*') == 0) { // If-Match:* => we need to write the response and eTag header } else { if (strcmp($ifNoneMatch, '*') == 0) { // if-None-Match:* => Do not write the response (304 not modified), // but write eTag header $needToSerializeResponse = false; } else { $eTag = $this->getETagForEntry($entryObject, $resourceType); // Note: The following code for attaching the prefix W\" // and the suffix " can be done in getETagForEntry function // but that is causing an issue in Linux env where the // firefix browser is unable to parse the ETag in this case. // Need to follow up PHP core devs for this. $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"'; if (!is_null($ifMatch)) { if (strcmp($eTag, $ifMatch) != 0) { // Requested If-Match value does not match with current // eTag Value then pre-condition error // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html throw ODataException::createPreConditionFailedError(Messages::eTagValueDoesNotMatch()); } } else { if (strcmp($eTag, $ifNoneMatch) == 0) { //304 not modified, but in write eTag header $needToSerializeResponse = false; } } } } } if (is_null($eTag)) { $eTag = $this->getETagForEntry($entryObject, $resourceType); // Note: The following code for attaching the prefix W\" // and the suffix " can be done in getETagForEntry function // but that is causing an issue in Linux env where the // firefix browser is unable to parse the ETag in this case. // Need to follow up PHP core devs for this. $eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"'; } return $eTag; }
/** * Validates the given version in string format and returns the version as instance of Version * * @param string $versionHeader The DataServiceVersion or MaxDataServiceVersion header value * @param string $headerName The name of the header * * @return Version * * @throws ODataException If the version is malformed or not supported */ private static function parseVersionHeader($versionHeader, $headerName) { $libName = null; $versionHeader = trim($versionHeader); $libNameIndex = strpos($versionHeader, ';'); if ($libNameIndex !== false) { $libName = substr($versionHeader, $libNameIndex); } else { $libNameIndex = strlen($versionHeader); } $dotIndex = -1; for ($i = 0; $i < $libNameIndex; $i++) { if ($versionHeader[$i] == '.') { //Throw an exception if we find more than 1 dot if ($dotIndex != -1) { throw ODataException::createBadRequestError(Messages::requestDescriptionInvalidVersionHeader($versionHeader, $headerName)); } $dotIndex = $i; } else { if ($versionHeader[$i] < '0' || $versionHeader[$i] > '9') { throw ODataException::createBadRequestError(Messages::requestDescriptionInvalidVersionHeader($versionHeader, $headerName)); } } } $major = intval(substr($versionHeader, 0, $dotIndex)); $minor = 0; //Apparently the . is optional if ($dotIndex != -1) { if ($dotIndex == 0) { //If it starts with a ., throw an exception throw ODataException::createBadRequestError(Messages::requestDescriptionInvalidVersionHeader($versionHeader, $headerName)); } $minor = intval(substr($versionHeader, $dotIndex + 1, $libNameIndex)); } $version = new Version($major, $minor); //TODO: move this somewhere... /* $isSupportedVersion = false; foreach (self::getKnownDataServiceVersions() as $version1) { if ($version->compare($version1) == 0) { $isSupportedVersion = true; break; } } if (!$isSupportedVersion) { $availableVersions = null; foreach (self::getKnownDataServiceVersions() as $version1) { $availableVersions .= $version1->toString() . ', '; } $availableVersions = rtrim($availableVersions, ', '); throw ODataException::createBadRequestError( Messages::requestDescriptionUnSupportedVersion( $headerName, $versionHeader, $availableVersions ) ); } */ return $version; }
/** * This method verfies the client provided url query parameters and check whether * any of the odata query option specified more than once or check any of the * non-odata query parameter start will $ symbol or check any of the odata query * option specified with out value. If any of the above check fails throws * ODataException, else set _queryOptions member variable * * @return void * * @throws ODataException */ public function validateQueryParameters() { $queryOptions = $this->_operationContext->incomingRequest()->getQueryParameters(); reset($queryOptions); $namesFound = array(); while ($queryOption = current($queryOptions)) { $optionName = key($queryOption); $optionValue = current($queryOption); if (empty($optionName)) { if (!empty($optionValue)) { if ($optionValue[0] == '$') { if ($this->_isODataQueryOption($optionValue)) { throw ODataException::createBadRequestError(Messages::hostODataQueryOptionFoundWithoutValue($optionValue)); } else { throw ODataException::createBadRequestError(Messages::hostNonODataOptionBeginsWithSystemCharacter($optionValue)); } } } } else { if ($optionName[0] == '$') { if (!$this->_isODataQueryOption($optionName)) { throw ODataException::createBadRequestError(Messages::hostNonODataOptionBeginsWithSystemCharacter($optionName)); } if (array_search($optionName, $namesFound) !== false) { throw ODataException::createBadRequestError(Messages::hostODataQueryOptionCannotBeSpecifiedMoreThanOnce($optionName)); } if (empty($optionValue) && $optionValue !== '0') { throw ODataException::createBadRequestError(Messages::hostODataQueryOptionFoundWithoutValue($optionName)); } $namesFound[] = $optionName; } } next($queryOptions); } $this->_queryOptions = $queryOptions; }
/** * Modify the 'Projection Tree' to include selection details * * @param array(array(string)) $selectPathSegments Collection of select * paths. * * @return void * * @throws ODataException If any error occurs while processing select * path segments */ private function _applySelectionToProjectionTree($selectPathSegments) { foreach ($selectPathSegments as $selectSubPathSegments) { $currentNode = $this->_rootProjectionNode; $subPathCount = count($selectSubPathSegments); foreach ($selectSubPathSegments as $index => $selectSubPathSegment) { if (!$currentNode instanceof RootProjectionNode && !$currentNode instanceof ExpandedProjectionNode) { throw ODataException::createBadRequestError(Messages::expandProjectionParserPropertyWithoutMatchingExpand($currentNode->getPropertyName())); } $currentNode->setSelectionFound(); $isLastSegment = $index == $subPathCount - 1; if ($selectSubPathSegment === '*') { $currentNode->setSelectAllImmediateProperties(); break; } $currentResourceType = $currentNode->getResourceType(); $resourceProperty = $currentResourceType->resolveProperty($selectSubPathSegment); if (is_null($resourceProperty)) { throw ODataException::createSyntaxError(Messages::expandProjectionParserPropertyNotFound($currentResourceType->getFullName(), $selectSubPathSegment, true)); } if (!$isLastSegment) { if ($resourceProperty->isKindOf(ResourcePropertyKind::BAG)) { throw ODataException::createBadRequestError(Messages::expandProjectionParserBagPropertyAsInnerSelectSegment($currentResourceType->getFullName(), $selectSubPathSegment)); } else { if ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE)) { throw ODataException::createBadRequestError(Messages::expandProjectionParserPrimitivePropertyUsedAsNavigationProperty($currentResourceType->getFullName(), $selectSubPathSegment)); } else { if ($resourceProperty->isKindOf(ResourcePropertyKind::COMPLEX_TYPE)) { throw ODataException::createBadRequestError(Messages::expandProjectionParserComplexPropertyAsInnerSelectSegment($currentResourceType->getFullName(), $selectSubPathSegment)); } else { if ($resourceProperty->getKind() != ResourcePropertyKind::RESOURCE_REFERENCE && $resourceProperty->getKind() != ResourcePropertyKind::RESOURCESET_REFERENCE) { throw ODataException::createInternalServerError(Messages::expandProjectionParserUnexpectedPropertyType()); } } } } } $node = $currentNode->findNode($selectSubPathSegment); if (is_null($node)) { if (!$isLastSegment) { throw ODataException::createBadRequestError(Messages::expandProjectionParserPropertyWithoutMatchingExpand($selectSubPathSegment)); } $node = new ProjectionNode($selectSubPathSegment, $resourceProperty); $currentNode->addNode($node); } $currentNode = $node; if ($currentNode instanceof ExpandedProjectionNode && $isLastSegment) { $currentNode->setSelectionFound(); $currentNode->markSubtreeAsSelected(); } } } }
/** * Create SegmentDescriptor for the first segment * * @param string $segmentIdentifier The identifier part of the first segment * @param string $keyPredicate The predicate part of the first segment if any else NULL * @param boolean $checkRights Whether to check the rights on this segment * * @return SegmentDescriptor Descriptor for the first segment * * @throws ODataException Exception if any validation fails */ private function _createFirstSegmentDescriptor($segmentIdentifier, $keyPredicate, $checkRights) { $descriptor = new SegmentDescriptor(); $descriptor->setIdentifier($segmentIdentifier); if ($segmentIdentifier === ODataConstants::URI_METADATA_SEGMENT) { $this->_assertion(is_null($keyPredicate)); $descriptor->setTargetKind(TargetKind::METADATA()); return $descriptor; } if ($segmentIdentifier === ODataConstants::URI_BATCH_SEGMENT) { $this->_assertion(is_null($keyPredicate)); $descriptor->setTargetKind(TargetKind::BATCH()); return $descriptor; } if ($segmentIdentifier === ODataConstants::URI_COUNT_SEGMENT) { throw ODataException::createBadRequestError(Messages::segmentParserSegmentNotAllowedOnRoot(ODataConstants::URI_COUNT_SEGMENT)); } if ($segmentIdentifier === ODataConstants::URI_LINK_SEGMENT) { throw ODataException::createBadRequestError(Messages::segmentParserSegmentNotAllowedOnRoot(ODataConstants::URI_LINK_SEGMENT)); } $resourceSetWrapper = $this->providerWrapper->resolveResourceSet($segmentIdentifier); if ($resourceSetWrapper === null) { throw ODataException::createResourceNotFoundError($segmentIdentifier); } $descriptor->setTargetResourceSetWrapper($resourceSetWrapper); $descriptor->setTargetResourceType($resourceSetWrapper->getResourceType()); $descriptor->setTargetSource(TargetSource::ENTITY_SET); $descriptor->setTargetKind(TargetKind::RESOURCE()); if ($keyPredicate !== null) { $keyDescriptor = $this->_createKeyDescriptor($segmentIdentifier . '(' . $keyPredicate . ')', $resourceSetWrapper->getResourceType(), $keyPredicate); $descriptor->setKeyDescriptor($keyDescriptor); if (!$keyDescriptor->isEmpty()) { $descriptor->setSingleResult(true); } } if ($checkRights) { $resourceSetWrapper->checkResourceSetRightsForRead($descriptor->isSingleResult()); } return $descriptor; }
/** * To check whether the the query options $select, $expand * is applicable for the current requested resource. * * @param string $queryItem The query option to check. * * @return void * * @throws ODataException Throws bad request error if the query * options $select, $expand cannot be * applied to the requested resource. */ private function _checkExpandOrSelectApplicable($queryItem) { if (!$this->_expandSelectApplicable) { throw ODataException::createBadRequestError(Messages::queryProcessorSelectOrExpandOptionNotApplicable($queryItem)); } }
/** * Build 'OrderBy Tree' from the given orderby path segments, also build * comparsion function for each path segment. * * @param array(array) &$orderByPathSegments Collection of orderby path segments, * this is passed by reference * since we need this function to * modify this array in two cases: * 1. if asc or desc present, then the * corresponding sub path segment * should be removed * 2. remove duplicate orderby path * segment * * @return void * * @throws ODataException If any error occurs while processing the orderby path * segments */ private function _buildOrderByTree(&$orderByPathSegments) { foreach ($orderByPathSegments as $index1 => &$orderBySubPathSegments) { $currentNode = $this->_rootOrderByNode; $currentObject = $this->_dummyObject; $ascending = true; $subPathCount = count($orderBySubPathSegments); // Check sort order is specified in the path, if so set a // flag and remove that segment if ($subPathCount > 1) { if ($orderBySubPathSegments[$subPathCount - 1] === '*desc') { $ascending = false; unset($orderBySubPathSegments[$subPathCount - 1]); $subPathCount--; } else { if ($orderBySubPathSegments[$subPathCount - 1] === '*asc') { unset($orderBySubPathSegments[$subPathCount - 1]); $subPathCount--; } } } $ancestors = array($this->_rootOrderByNode->getResourceSetWrapper()->getName()); foreach ($orderBySubPathSegments as $index2 => $orderBySubPathSegment) { $isLastSegment = $index2 == $subPathCount - 1; $resourceSetWrapper = null; $resourceType = $currentNode->getResourceType(); $resourceProperty = $resourceType->resolveProperty($orderBySubPathSegment); if (is_null($resourceProperty)) { throw ODataException::createSyntaxError(Messages::orderByParserPropertyNotFound($resourceType->getFullName(), $orderBySubPathSegment)); } if ($resourceProperty->isKindOf(ResourcePropertyKind::BAG)) { throw ODataException::createBadRequestError(Messages::orderByParserBagPropertyNotAllowed($resourceProperty->getName())); } else { if ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE)) { if (!$isLastSegment) { throw ODataException::createBadRequestError(Messages::orderByParserPrimitiveAsIntermediateSegment($resourceProperty->getName())); } $type = $resourceProperty->getInstanceType(); if ($type instanceof Binary) { throw ODataException::createBadRequestError(Messages::orderByParserSortByBinaryPropertyNotAllowed($resourceProperty->getName())); } } else { if ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCESET_REFERENCE || $resourceProperty->getKind() == ResourcePropertyKind::RESOURCE_REFERENCE) { $this->_assertion($currentNode instanceof OrderByRootNode || $currentNode instanceof OrderByNode); $resourceSetWrapper = $currentNode->getResourceSetWrapper(); $this->_assertion(!is_null($resourceSetWrapper)); $resourceSetWrapper = $this->_providerWrapper->getResourceSetWrapperForNavigationProperty($resourceSetWrapper, $resourceType, $resourceProperty); if (is_null($resourceSetWrapper)) { throw ODataException::createBadRequestError(Messages::badRequestInvalidPropertyNameSpecified($resourceType->getFullName(), $orderBySubPathSegment)); } if ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCESET_REFERENCE) { throw ODataException::createBadRequestError(Messages::orderByParserResourceSetReferenceNotAllowed($resourceProperty->getName(), $resourceType->getFullName())); } $resourceSetWrapper->checkResourceSetRightsForRead(true); if ($isLastSegment) { throw ODataException::createBadRequestError(Messages::orderByParserSortByNavigationPropertyIsNotAllowed($resourceProperty->getName())); } $ancestors[] = $orderBySubPathSegment; } else { if ($resourceProperty->isKindOf(ResourcePropertyKind::COMPLEX_TYPE)) { if ($isLastSegment) { throw ODataException::createBadRequestError(Messages::orderByParserSortByComplexPropertyIsNotAllowed($resourceProperty->getName())); } $ancestors[] = $orderBySubPathSegment; } else { throw ODataException::createInternalServerError(Messages::orderByParserUnexpectedPropertyType()); } } } } $node = $currentNode->findNode($orderBySubPathSegment); if (is_null($node)) { if ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE)) { $node = new OrderByLeafNode($orderBySubPathSegment, $resourceProperty, $ascending); $this->_comparisonFunctions[] = $node->buildComparisonFunction($ancestors); } else { if ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCE_REFERENCE) { $node = new OrderByNode($orderBySubPathSegment, $resourceProperty, $resourceSetWrapper); // Initialize this member variable (identified by // $resourceProperty) of parent object. try { $dummyProperty = new \ReflectionProperty($currentObject, $resourceProperty->getName()); $object = $resourceProperty->getInstanceType()->newInstance(); $dummyProperty->setValue($currentObject, $object); $currentObject = $object; } catch (\ReflectionException $reflectionException) { throw ODataException::createInternalServerError(Messages::orderByParserFailedToAccessOrInitializeProperty($resourceProperty->getName(), $resourceType->getName())); } } else { if ($resourceProperty->getKind() == ResourcePropertyKind::COMPLEX_TYPE) { $node = new OrderByNode($orderBySubPathSegment, $resourceProperty, null); // Initialize this member variable // (identified by $resourceProperty)of parent object. try { $dummyProperty = new \ReflectionProperty($currentObject, $resourceProperty->getName()); $object = $resourceProperty->getInstanceType()->newInstance(); $dummyProperty->setValue($currentObject, $object); $currentObject = $object; } catch (\ReflectionException $reflectionException) { throw ODataException::createInternalServerError(Messages::orderByParserFailedToAccessOrInitializeProperty($resourceProperty->getName(), $resourceType->getName())); } } } } $currentNode->addNode($node); } else { try { $reflectionClass = new \ReflectionClass(get_class($currentObject)); $reflectionProperty = $reflectionClass->getProperty($resourceProperty->getName()); $reflectionProperty->setAccessible(true); $currentObject = $reflectionProperty->getValue($currentObject); //$dummyProperty = new \ReflectionProperty( // $currentObject, $resourceProperty->getName() //); //$currentObject = $dummyProperty->getValue($currentObject); } catch (\ReflectionException $reflectionException) { throw ODataException::createInternalServerError(Messages::orderByParserFailedToAccessOrInitializeProperty($resourceProperty->getName(), $resourceType->getName())); } if ($node instanceof OrderByLeafNode) { //remove duplicate orderby path unset($orderByPathSegments[$index1]); } } $currentNode = $node; } } }