Repository: RESTful-Drupal/restful Branch: 7.x-2.x Commit: d996c6ee6043 Files: 271 Total size: 1011.1 KB Directory structure: gitextract_cruhgny1/ ├── .travis.yml ├── README.md ├── docs/ │ ├── api_drupal.md │ ├── api_url.md │ ├── documentation.md │ └── plugin.md ├── help/ │ ├── problem-instances-bad-request.html │ ├── problem-instances-flood.html │ ├── problem-instances-forbidden.html │ ├── problem-instances-gone.html │ ├── problem-instances-incompatible-field-definition.html │ ├── problem-instances-not-found.html │ ├── problem-instances-not-implemented.html │ ├── problem-instances-server-configuration.html │ ├── problem-instances-server-error.html │ ├── problem-instances-service-unavailable.html │ ├── problem-instances-unauthorized.html │ ├── problem-instances-unprocessable-entity.html │ ├── problem-instances-unsupported-media-type.html │ ├── problem-instances.html │ ├── readme.html │ └── restful.help.ini ├── modules/ │ ├── restful_example/ │ │ ├── restful_example.info │ │ ├── restful_example.module │ │ └── src/ │ │ └── Plugin/ │ │ ├── formatter/ │ │ │ └── FormatterHalXml.php │ │ └── resource/ │ │ ├── Tags__1_0.php │ │ ├── comment/ │ │ │ ├── Comments__1_0.php │ │ │ └── DataProviderComment.php │ │ ├── node/ │ │ │ └── article/ │ │ │ ├── v1/ │ │ │ │ ├── Articles__1_0.php │ │ │ │ ├── Articles__1_1.php │ │ │ │ ├── Articles__1_4.php │ │ │ │ ├── Articles__1_5.php │ │ │ │ ├── Articles__1_6.php │ │ │ │ └── Articles__1_7.php │ │ │ └── v2/ │ │ │ ├── Articles__2_0.php │ │ │ └── Articles__2_1.php │ │ └── variables/ │ │ ├── DataInterpreterVariable.php │ │ ├── DataProviderVariable.php │ │ ├── DataProviderVariableInterface.php │ │ └── Variables__1_0.php │ └── restful_token_auth/ │ ├── modules/ │ │ └── restful_token_auth_test/ │ │ ├── restful_token_auth_test.info │ │ ├── restful_token_auth_test.module │ │ └── src/ │ │ └── Plugin/ │ │ └── resource/ │ │ └── Articles__1_3.php │ ├── restful_token_auth.admin.inc │ ├── restful_token_auth.info │ ├── restful_token_auth.install │ ├── restful_token_auth.module │ ├── src/ │ │ ├── Entity/ │ │ │ ├── RestfulTokenAuth.php │ │ │ └── RestfulTokenAuthController.php │ │ └── Plugin/ │ │ ├── authentication/ │ │ │ └── TokenAuthentication.php │ │ └── resource/ │ │ ├── AccessToken__1_0.php │ │ ├── RefreshToken__1_0.php │ │ └── TokenAuthenticationBase.php │ └── tests/ │ └── RestfulTokenAuthenticationTestCase.test ├── restful.admin.inc ├── restful.api.php ├── restful.cache.inc ├── restful.entity.inc ├── restful.info ├── restful.install ├── restful.module ├── src/ │ ├── Annotation/ │ │ ├── Authentication.php │ │ ├── Formatter.php │ │ ├── RateLimit.php │ │ └── Resource.php │ ├── Authentication/ │ │ ├── AuthenticationManager.php │ │ ├── AuthenticationManagerInterface.php │ │ ├── AuthenticationPluginCollection.php │ │ ├── UserSessionState.php │ │ └── UserSessionStateInterface.php │ ├── Exception/ │ │ ├── BadRequestException.php │ │ ├── FloodException.php │ │ ├── ForbiddenException.php │ │ ├── GoneException.php │ │ ├── InaccessibleRecordException.php │ │ ├── IncompatibleFieldDefinitionException.php │ │ ├── InternalServerErrorException.php │ │ ├── NotFoundException.php │ │ ├── NotImplementedException.php │ │ ├── RestfulException.php │ │ ├── ServerConfigurationException.php │ │ ├── ServiceUnavailableException.php │ │ ├── UnauthorizedException.php │ │ ├── UnprocessableEntityException.php │ │ └── UnsupportedMediaTypeException.php │ ├── Formatter/ │ │ ├── FormatterManager.php │ │ ├── FormatterManagerInterface.php │ │ └── FormatterPluginCollection.php │ ├── Http/ │ │ ├── HttpHeader.php │ │ ├── HttpHeaderBag.php │ │ ├── HttpHeaderBagInterface.php │ │ ├── HttpHeaderInterface.php │ │ ├── HttpHeaderNull.php │ │ ├── Request.php │ │ ├── RequestInterface.php │ │ ├── Response.php │ │ └── ResponseInterface.php │ ├── Plugin/ │ │ ├── AuthenticationPluginManager.php │ │ ├── ConfigurablePluginTrait.php │ │ ├── FormatterPluginManager.php │ │ ├── RateLimitPluginManager.php │ │ ├── ResourcePluginManager.php │ │ ├── SemiSingletonTrait.php │ │ ├── authentication/ │ │ │ ├── Authentication.php │ │ │ ├── AuthenticationInterface.php │ │ │ ├── BasicAuthentication.php │ │ │ ├── CookieAuthentication.php │ │ │ └── OAuth2ServerAuthentication.php │ │ ├── formatter/ │ │ │ ├── Formatter.php │ │ │ ├── FormatterHalJson.php │ │ │ ├── FormatterInterface.php │ │ │ ├── FormatterJson.php │ │ │ ├── FormatterJsonApi.php │ │ │ └── FormatterSingleJson.php │ │ ├── rate_limit/ │ │ │ ├── RateLimit.php │ │ │ ├── RateLimitGlobal.php │ │ │ ├── RateLimitInterface.php │ │ │ └── RateLimitRequest.php │ │ └── resource/ │ │ ├── AuthenticatedResource.php │ │ ├── AuthenticatedResourceInterface.php │ │ ├── CrudInterface.php │ │ ├── CsrfToken.php │ │ ├── DataInterpreter/ │ │ │ ├── ArrayWrapper.php │ │ │ ├── ArrayWrapperInterface.php │ │ │ ├── DataInterpreterArray.php │ │ │ ├── DataInterpreterBase.php │ │ │ ├── DataInterpreterEMW.php │ │ │ ├── DataInterpreterInterface.php │ │ │ ├── DataInterpreterPlug.php │ │ │ ├── PluginWrapper.php │ │ │ └── PluginWrapperInterface.php │ │ ├── DataProvider/ │ │ │ ├── CacheDecoratedDataProvider.php │ │ │ ├── CacheDecoratedDataProviderInterface.php │ │ │ ├── DataProvider.php │ │ │ ├── DataProviderDbQuery.php │ │ │ ├── DataProviderDbQueryInterface.php │ │ │ ├── DataProviderDecorator.php │ │ │ ├── DataProviderEntity.php │ │ │ ├── DataProviderEntityDecorator.php │ │ │ ├── DataProviderEntityInterface.php │ │ │ ├── DataProviderFile.php │ │ │ ├── DataProviderInterface.php │ │ │ ├── DataProviderNode.php │ │ │ ├── DataProviderNull.php │ │ │ ├── DataProviderPlug.php │ │ │ ├── DataProviderResource.php │ │ │ ├── DataProviderResourceInterface.php │ │ │ └── DataProviderTaxonomyTerm.php │ │ ├── Decorators/ │ │ │ ├── CacheDecoratedResource.php │ │ │ ├── CacheDecoratedResourceInterface.php │ │ │ ├── RateLimitDecoratedResource.php │ │ │ ├── ResourceDecoratorBase.php │ │ │ └── ResourceDecoratorInterface.php │ │ ├── Discovery.php │ │ ├── Field/ │ │ │ ├── PublicFieldInfo/ │ │ │ │ ├── PublicFieldInfoBase.php │ │ │ │ ├── PublicFieldInfoEntity.php │ │ │ │ ├── PublicFieldInfoEntityInterface.php │ │ │ │ ├── PublicFieldInfoInterface.php │ │ │ │ └── PublicFieldInfoNull.php │ │ │ ├── ResourceField.php │ │ │ ├── ResourceFieldBase.php │ │ │ ├── ResourceFieldCollection.php │ │ │ ├── ResourceFieldCollectionInterface.php │ │ │ ├── ResourceFieldDbColumn.php │ │ │ ├── ResourceFieldDbColumnInterface.php │ │ │ ├── ResourceFieldEntity.php │ │ │ ├── ResourceFieldEntityAlterableInterface.php │ │ │ ├── ResourceFieldEntityFile.php │ │ │ ├── ResourceFieldEntityInterface.php │ │ │ ├── ResourceFieldEntityReference.php │ │ │ ├── ResourceFieldEntityReferenceInterface.php │ │ │ ├── ResourceFieldEntityText.php │ │ │ ├── ResourceFieldFileEntityReference.php │ │ │ ├── ResourceFieldInterface.php │ │ │ ├── ResourceFieldKeyValue.php │ │ │ ├── ResourceFieldReference.php │ │ │ ├── ResourceFieldResource.php │ │ │ └── ResourceFieldResourceInterface.php │ │ ├── FilesUpload__1_0.php │ │ ├── LoginCookie__1_0.php │ │ ├── Resource.php │ │ ├── ResourceDbQuery.php │ │ ├── ResourceEntity.php │ │ ├── ResourceInterface.php │ │ ├── ResourceNode.php │ │ └── Users__1_0.php │ ├── RateLimit/ │ │ ├── Entity/ │ │ │ ├── RateLimit.php │ │ │ └── RateLimitController.php │ │ ├── RateLimitManager.php │ │ ├── RateLimitManagerInterface.php │ │ └── RateLimitPluginCollection.php │ ├── RenderCache/ │ │ ├── Entity/ │ │ │ ├── CacheFragment.php │ │ │ └── CacheFragmentController.php │ │ ├── RenderCache.php │ │ └── RenderCacheInterface.php │ ├── Resource/ │ │ ├── EnabledArrayIterator.php │ │ ├── ResourceManager.php │ │ ├── ResourceManagerInterface.php │ │ └── ResourcePluginCollection.php │ ├── RestfulManager.php │ └── Util/ │ ├── EntityFieldQuery.php │ ├── EntityFieldQueryRelationalConditionsInterface.php │ ├── ExplorableDecoratorInterface.php │ ├── PersistableCache.php │ ├── PersistableCacheInterface.php │ ├── RelationalFilter.php │ ├── RelationalFilterInterface.php │ └── StringHelper.php └── tests/ ├── RestfulAuthenticationTestCase.test ├── RestfulAutoCompleteTestCase.test ├── RestfulCommentTestCase.test ├── RestfulCreateEntityTestCase.test ├── RestfulCreateNodeTestCase.test ├── RestfulCreatePrivateNodeTestCase.test ├── RestfulCreateTaxonomyTermTestCase.test ├── RestfulCsrfTokenTestCase.test ├── RestfulCurlBaseTestCase.test ├── RestfulDataProviderPlugPluginsTestCase.test ├── RestfulDbQueryTestCase.test ├── RestfulDiscoveryTestCase.test ├── RestfulEntityAndPropertyAccessTestCase.test ├── RestfulEntityUserAccessTestCase.test ├── RestfulEntityValidatorTestCase.test ├── RestfulExceptionHandleTestCase.test ├── RestfulForbiddenItemsTestCase.test ├── RestfulGetHandlersTestCase.test ├── RestfulHalJsonTestCase.test ├── RestfulHookMenuTestCase.test ├── RestfulJsonApiTestCase.test ├── RestfulListEntityMultipleBundlesTestCase.test ├── RestfulListTestCase.test ├── RestfulPassThroughTestCase.test ├── RestfulRateLimitTestCase.test ├── RestfulReferenceTestCase.test ├── RestfulRenderCacheTestCase.test ├── RestfulSimpleJsonTestCase.test ├── RestfulSubResourcesCreateEntityTestCase.test ├── RestfulUpdateEntityCurlTestCase.test ├── RestfulUpdateEntityTestCase.test ├── RestfulUserLoginCookieTestCase.test ├── RestfulVariableTestCase.test ├── RestfulViewEntityMultiLingualTestCase.test ├── RestfulViewEntityTestCase.test ├── RestfulViewModeAndFormatterTestCase.test └── modules/ ├── restful_node_access_test/ │ ├── restful_node_access_test.info │ └── restful_node_access_test.module └── restful_test/ ├── restful_test.info ├── restful_test.install ├── restful_test.module └── src/ └── Plugin/ └── resource/ ├── DataProvider/ │ └── DataProviderFileTest.php ├── db_query_test/ │ └── v1/ │ └── DbQueryTest__1_0.php ├── entity_test/ │ ├── EntityTests__1_0.php │ ├── main/ │ │ └── v1/ │ │ ├── Main__1_0.php │ │ ├── Main__1_1.php │ │ ├── Main__1_2.php │ │ ├── Main__1_3.php │ │ ├── Main__1_4.php │ │ ├── Main__1_5.php │ │ ├── Main__1_6.php │ │ ├── Main__1_7.php │ │ └── Main__1_8.php │ └── tests/ │ └── Tests__1_0.php ├── file/ │ └── file_upload_test/ │ └── v1/ │ └── FilesUploadTest__1_0.php ├── node/ │ └── test_article/ │ └── v1/ │ ├── TestArticles__1_0.php │ ├── TestArticles__1_1.php │ ├── TestArticles__1_2.php │ ├── TestArticles__1_3.php │ └── TestArticles__1_4.php ├── restful_test_translatable_entity/ │ └── v1/ │ └── RestfulTestTranslatableEntityResource__1_0.php └── taxonomy_term/ └── v1/ ├── DataProviderTaxonomyTerm.php └── TestTags__1_0.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .travis.yml ================================================ language: php php: - 5.5 - 5.6 env: - PATH=$PATH:/home/travis/.composer/vendor/bin # This will create the database mysql: database: drupal username: root encoding: utf8 install: # Grab Drush - composer global require "drush/drush:7.*" - phpenv rehash # Make sure we don't fail when checking out projects - echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config - sudo cat /etc/apt/sources.list - echo "deb http://archive.ubuntu.com/ubuntu/ precise multiverse" | sudo tee -a /etc/apt/sources.list - echo "deb-src http://archive.ubuntu.com/ubuntu/ precise multiverse" | sudo tee -a /etc/apt/sources.list - echo "deb http://archive.ubuntu.com/ubuntu/ precise-updates multiverse" | sudo tee -a /etc/apt/sources.list - echo "deb-src http://archive.ubuntu.com/ubuntu/ precise-updates multiverse" | sudo tee -a /etc/apt/sources.list - echo "deb http://security.ubuntu.com/ubuntu precise-security multiverse" | sudo tee -a /etc/apt/sources.list - echo "deb-src http://security.ubuntu.com/ubuntu precise-security multiverse" | sudo tee -a /etc/apt/sources.list # LAMP package installation (mysql is already started) - sudo apt-get update - sudo apt-get install apache2 libapache2-mod-fastcgi # enable php-fpm, travis does not support any other method with php and apache - sudo cp ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf.default ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf - sudo a2enmod rewrite actions fastcgi alias - echo "cgi.fix_pathinfo = 1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - ~/.phpenv/versions/$(phpenv version-name)/sbin/php-fpm # Make sure the apache root is in our wanted directory - echo "$(curl -fsSL https://gist.githubusercontent.com/nickveenhof/11386315/raw/b8abaf9304fe12b5cc7752d39c29c1edae8ac2e6/gistfile1.txt)" | sed -e "s,PATH,$TRAVIS_BUILD_DIR/../drupal,g" | sudo tee /etc/apache2/sites-available/default > /dev/null # Set sendmail so drush doesn't throw an error during site install. - echo "sendmail_path='true'" >> `php --ini | grep "Loaded Configuration" | awk '{print $4}'` # Forward the errors to the syslog so we can print them - echo "error_log=syslog" >> `php --ini | grep "Loaded Configuration" | awk '{print $4}'` # Get latest drupal 8 core - cd $TRAVIS_BUILD_DIR/.. - git clone --depth 1 --branch 7.x http://git.drupal.org/project/drupal.git # Restart apache and test it - sudo service apache2 restart - curl -v "http://localhost" # Re-enable when trying to get CodeSniffer doesn't return a 403 anymore. #- composer global require drupal/coder:\>7 before_script: - cd $TRAVIS_BUILD_DIR/../drupal # Update drupal core - git pull origin 7.x # Install the site - drush -v site-install minimal --db-url=mysql://root:@localhost/drupal --yes # Increase max_allowed_packet to avoid MySQL errors - echo -e "[server]\nmax_allowed_packet=64M" | sudo tee -a /etc/mysql/conf.d/drupal.cnf - sudo service mysql restart - phpenv rehash script: # Go to our Drupal module directory - mkdir $TRAVIS_BUILD_DIR/../drupal/sites/all/modules/restful - cp -R $TRAVIS_BUILD_DIR/* $TRAVIS_BUILD_DIR/../drupal/sites/all/modules/restful/ # Go to our Drupal main directory - cd $TRAVIS_BUILD_DIR/../drupal # Download and enable module and its dependencies - drush --yes dl registry_autoload - drush --yes dl entity_validator-7.x-2.0 - drush --yes dl plug - drush --yes dl entity - drush --yes dl entityreference - drush --yes dl uuid - drush --yes pm-enable plug # Patch Entity API. - curl -O https://www.drupal.org/files/issues/2086225-entity-access-check-node-create-3.patch - patch -p1 $TRAVIS_BUILD_DIR/../drupal/sites/all/modules/entity/modules/callbacks.inc < 2086225-entity-access-check-node-create-3.patch # Enable the RESTful modules - drush --yes pm-enable simpletest restful restful_token_auth # Run the tests - cd $TRAVIS_BUILD_DIR/../drupal - php ./scripts/run-tests.sh --php $(which php) --concurrency 4 --verbose --color --url http://localhost RESTful 2>&1 | tee /tmp/simpletest-result.txt - egrep -i "([1-9]+ fail)|(Fatal error)|([1-9]+ exception)" /tmp/simpletest-result.txt && exit 1 - exit 0 ================================================ FILE: README.md ================================================ [![Build Status](https://travis-ci.org/RESTful-Drupal/restful.svg?branch=7.x-2.x)](https://travis-ci.org/RESTful-Drupal/restful) # RESTful best practices for Drupal This module allows Drupal to be operated via RESTful HTTP requests, using best practices for security, performance, and usability. ## Concept Here are the differences between RESTful and other modules, such as RestWs and Services Entity: * RESTful requires explicitly declaring the exposed API. When enabling the module, nothing happens until a plugin declares it. * Resources are exposed by bundle, rather than by entity. This would allow a developer to expose only nodes of a certain type, for example. * The exposed properties need to be explicitly declared. This allows a _clean_ output without Drupal's internal implementation leaking out. This means the consuming client doesn't need to know if an entity is a node or a term, nor will they be presented with the ``field_`` prefix. * Resource versioning is built-in, so that resources can be reused with multiple consumers. The versions are at the resource level, for more flexibility and control. * It has configurable output formats. It ships with JSON (the default one), JSON+HAL and as an example also XML. * Audience is developers and not site builders. * Provide a key tool for a headless Drupal. See the [AngularJs form](https://github.com/Gizra/restful/blob/7.x-1.x/modules/restful_angular_example/README.md) example module. ## Module dependencies * [Entity API](https://drupal.org/project/entity), with the following patches: * [Prevent notice in entity_metadata_no_hook_node_access() when node is not saved](https://drupal.org/node/2086225#comment-8768373) ## Recipes Read even more examples on how to use the RESTful module in the [module documentation node](https://www.drupal.org/node/2380679) in Drupal.org. Make sure you read the _Recipes_ section. If you have any to share, feel free to add your own recipes. ## Declaring a REST Endpoint A RESTful endpoint is declared via a custom module that includes a plugin which describes the resource you want to make available. Here are the bare essentials from one of the multiple examples in [the example module](./modules/restful_example): ####restful\_custom/restful\_custom.info ```ini name = RESTful custom description = Custom RESTful resource. core = 7.x dependencies[] = restful registry_autoload[] = PSR-4 ``` ####restful\_custom/src/Plugin/resource/Custom__1_0.php ```php namespace Drupal\restful_custom\Plugin\resource; use Drupal\restful\Plugin\resource\ResourceEntity; use Drupal\restful\Plugin\resource\ResourceInterface; /** * Class Custom__1_0 * @package Drupal\restful_custom\Plugin\resource * * @Resource( * name = "custom:1.0", * resource = "custom", * label = "Custom", * description = "My custom resource!", * authenticationTypes = TRUE, * authenticationOptional = TRUE, * dataProvider = { * "entityType": "node", * "bundles": { * "article" * }, * }, * majorVersion = 1, * minorVersion = 0 * ) */ class Custom__1_0 extends ResourceEntity implements ResourceInterface { /** * Overrides EntityNode::publicFields(). */ public function publicFields() { $public_fields = parent::publicFields(); $public_fields['body'] = array( 'property' => 'body', 'sub_property' => 'value', ); return $public_fields; } } ``` After declaring this plugin, the resource could be accessed at its root URL, which would be `http://example.com/api/v1.0/custom`. ### Security, caching, output, and customization See the [Defining a RESTful Plugin](./docs/plugin.md) document for more details. ## Using your API from within Drupal The following examples use the _articles_ resource from the _restful\_example_ module. #### Getting a specific version of a RESTful handler for a resource ```php // Get handler v1.1 $handler = restful()->getResourceManager()->getPlugin('articles:1.1'); ``` #### Create and update an entity ```php $handler = restful() ->getResourceManager() ->getPlugin('articles:1.0'); // POST method, to create. $result = restful() ->getFormatterManager() ->format($handler->doPost(array('label' => 'example title'))); $id = $result['id']; // PATCH method to update only the title. $request['label'] = 'new title'; restful() ->getFormatterManager() ->format($handler->doPatch($id, $request)); ``` #### List entities ```php $handler = restful()->getResourceManager()->getPlugin('articles:1.0'); $handler->setRequest(Request::create('')); $result = restful()->getFormatterManager()->format($handler->process(), 'json'); // Output: array( 'data' => array( array( 'id' => 1, 'label' => 'example title', 'self' => 'https://example.com/node/1', ); array( 'id' => 2, 'label' => 'another title', 'self' => 'https://example.com/node/2', ); ), ); ``` ### Sort, Filter, Range, and Sub Requests See the [Using your API within drupal](./docs/api_drupal.md) documentation for more details. ## Consuming your API The following examples use the _articles_ resource from the _restful\_example_ module. #### Consuming specific versions of your API ```shell # Handler v1.0 curl https://example.com/api/articles/1 \ -H "X-API-Version: v1.0" # or curl https://example.com/api/v1.0/articles/1 # Handler v1.1 curl https://example.com/api/articles/1 \ -H "X-API-Version: v1.1" # or curl https://example.com/api/v1.1/articles/1 ``` #### View multiple articles at once ```shell # Handler v1.1 curl https://example.com/api/articles/1,2 \ -H "X-API-Version: v1.1" ``` #### Returning autocomplete results ```shell curl https://example.com/api/articles?autocomplete[string]=mystring ``` #### URL Query strings, HTTP headers, and HTTP requests See the [Consuming Your API](./docs/api_url.md) document for more details. ## CORS RESTful provides support for preflight requests (see the [Wikipedia example](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing#Preflight_example) for more details). To configure the allowed domains, you can: - Go to `admin/config/services/restful` and set _CORS Preflight_ to the allowed domain. This will apply globally unless overridden with the method below. - Set the `allowOrigin` key in your resource definition (in the annotation) to the allowed domain. This setting will only apply to this resource. Bear in mind that this check is only performed to the top-level resource. If you are composing resources with competing `allowOrigin` settings, the top-level resource will be applied. ## Documenting your API Clients can access documentation about a resource by making an `OPTIONS` HTTP request to its root URL. The resource will respond with the field information in the body, and the information about the available output formats and the permitted HTTP methods will be contained in the headers. ### Automatic documentation If your resource is an entity, then it will be partially self-documented, without you needing to do anything else. This information is automatically derived from the Entity API and Field API. Here is a snippet from a typical JSON response using only the automatic documentation: ```json { "myfield": { "info": { "label": "My Field", "description": "A field within my resource." }, "data": { "type": "string", "read_only": false, "cardinality": 1, "required": false }, "form_element": { "type": "textfield", "default_value": "", "placeholder": "", "size": 255, "allowed_values": null } } // { ... other fields would follow ... } } ``` Each field you've defined in `publicFields` will output an object similar to the one listed above. ### Manual documentation In addition to the automatic documentation provided to you out of the box, you have the ability to manually document your resources. See the [Documenting your API](./docs/documentation.md) documentation for more details. ## Modules integration * [Entity validator 2.x](https://www.drupal.org/project/entity_validator): Integrate with a robust entity validation (RESTful 1.x requires Entity Validator 1.x). ## Credits * [Gizra](http://gizra.com) * [Mateu Aguiló Bosch](https://github.com/e0ipso) ================================================ FILE: docs/api_drupal.md ================================================ # Using Your API Within Drupal The RESTful module allows your resources to be used within Drupal itself. For example, you could define a resource, and then operate it within another custom module. In general, this is accomplished by using the resource manager in order to get a handler for your resource, and then calling methods such as `get` or `post` to make a request, which will operate the resource. The request itself can be customized by passing in an array of key/value pairs. ## Read Contexts The following keys apply to read contexts, in which you are using the `get` method to return results from a resource. ### Sort You can use the `'sort'` key to sort the list of entities by multiple properties. List every property in a comma-separated string, in the order that you want to sort by. Prefixing the property name with a dash (``-``) will sort by that property in a descending order; the default is ascending. Bear in mind that for entity based resources, only those fields with a `'property'` (matching to an entity property or a Field API field) can be used for sorting. If no sorting is specified the default sorting is by the entity ID. ```php $handler = restful() ->getResourceManager() ->getPlugin('articles:1.0'); // Define the sorting by ID (descending) and label (ascending). $query['sort'] = '-id,label'; $result = restful() ->getFormatterManager() ->format($handler->doGet('', $query)); // Output: array( 'data' => array( array( 'id' => 2, 'label' => 'another title', 'self' => 'https://example.com/node/2', ), array( 'id' => 1, 'label' => 'example title', 'self' => 'https://example.com/node/1', ), ), ); ``` ### Filter Use the `'filter'` key to filter the list. You can provide as many filters as you need. ```php $handler = restful() ->getResourceManager() ->getPlugin('articles:1.0'); // Single value property. $query['filter'] = array('label' => 'abc'); $result = restful() ->getFormatterManager() ->format($handler->doGet('', $query)); ``` Bear in mind that for entity based resources, only those fields with a `'property'` (matching to an entity property or a Field API field) can be used for filtering. Additionally you can provide multiple filters for the same field. That is specially useful when filtering on multiple value fields. The following example will get all the articles with the integer multiple field that contains all 1, 3 and 5. ```php $handler = restful() ->getResourceManager() ->getPlugin('articles:1.0'); // Single value property. $query['filter'] = array('integer_multiple' => array( 'values' => array(1, 3, 5), )); $result = restful() ->getFormatterManager() ->format($handler->doGet('', $query)); ``` You can do more advanced filtering by providing values and operators. The following example will get all the articles with an integer value more than 5 and another equal to 10. ```php $handler = restful() ->getResourceManager() ->getPlugin('articles:1.0'); // Single value property. $query['filter'] = array('integer_multiple' => array( 'values' => array(5, 10), 'operator' => array('>', '='), )); $result = restful() ->getFormatterManager() ->format($handler->doGet('', $query)); ``` ### Autocomplete By using the `'autocomplete'` key and supplying a query string, it is possible to change the normal listing behavior into autocomplete. This also changes the normal output objects into key/value pairs which can be fed directly into a Drupal autocomplete field. The following is the API equivalent of `https://example.com?autocomplete[string]=foo&autocomplete[operator]=STARTS_WITH` ```php $handler = restful() ->getResourceManager() ->getPlugin('articles:1.0'); $query = array( 'autocomplete' => array( 'string' => 'foo', // Optional, defaults to "CONTAINS". 'operator' => 'STARTS_WITH', ), ); $handler->get('', $query); ``` ### Range Using the `'range'` key, you can control the number of elements per page you want to show. This value will always be limited by the `$range` variable in your resource class. This variable defaults to 50. ```php $handler = restful() ->getResourceManager() ->getPlugin('articles:1.0'); // Single value property. $query['range'] = 25; $result = $handler->get('', $query); ``` ## Write Contexts The following techniques apply to write contexts, in which you are using the `post` method to create an entity defined by a resource. ### Sub-requests It is possible to create multiple referencing entities in a single request. A typical example would be a node referencing a new taxonomy term. For example if there was a taxonomy reference or entity reference field called ``field_tags`` on the Article bundle (node) with an ``articles`` and a Tags bundle (taxonomy term) with a ``tags`` resource, we would define the relation via the ``ResourceEntity::publicFields()`` ```php public function publicFields() { $public_fields = parent::publicFields(); // ... $public_fields['tags'] = array( 'property' => 'field_tags', 'resource' => array( 'name' => 'tags', 'minorVersion' => 1, 'majorVersion' => 0, ), ); // ... return $public_fields; } ``` And create both entities with a single request: ```php $handler = restful() ->getResourceManager() ->getPlugin('articles:1.0'); $parsed_body = array( 'label' => 'parent', 'body' => 'Drupal', 'tags' => array( array( // Create a new term. 'body' => array( 'label' => 'child1', ), 'request' => array( 'method' => 'POST', 'headers' => array( 'X-CSRF-Token' => 'my-csrf-token', ), ), ), array( // PATCH an existing term. 'body' => array( 'label' => 'new title by PATCH', ), 'id' => 12, 'request' => array( 'method' => 'PATCH', ), ), array( // PATCH an existing term. 'body' => array( 'label' => 'new title by PUT', ), 'id' => 9, 'request' => array( 'method' => 'PUT', ), ), // Use an existing item. array( 'id' => 21, ), ), ); $handler->doPost($parsed_body); ``` ## Error handling If an error occurs while using the API within Drupal, a custom exception is thrown. All the exceptions thrown by the RESTful module extend the `\Drupal\restful\Exception\RestfulException` class. ================================================ FILE: docs/api_url.md ================================================ # Consuming your API The RESTful module allows your resources to be used by external clients via HTTP requests. This is the module's primary purpose. You can manipulate the resources using different HTTP request types (e.g. `POST`, `GET`, `DELETE`), HTTP headers, and special query strings passed in the URL itself. ## Write operations Write operations can be performed via the `POST` (to create items), `PUT` or `PATCH` (to update items) HTTP methods. ### Basic example The following request will create an article using the `articles` resource: ```http POST /articles HTTP/1.1 Content-Type: application/json Accept: application/json { "title": "My article", "body": "

This is a short one

", "tags": [1, 6, 12] } ``` Note how we are setting the properties that we want to set using JSON. The provided payload format needs to match the contents of the `Content-Type` header (in this case _application/json_). It's also worth noting that when setting reference fields with multiple values, you can submit an array of IDs or a string of IDs separated by commas. ### Advanced example You use sub-requests to manipulate (create or alter) the relationships in a single request. The following example will: 1. Update the title of the article to be _To TDD or Not_. 1. Update the contents of tag 6 to replace it with the provided content. 1. Create a new tag and assign it to the updated article. ``` PATCH /articles/1 HTTP/1.1 Content-Type: application/vnd.api+json Accept: application/vnd.api+json { "title": "To TDD or Not", "tags": [ { "id": "6", "body": { "label": "Batman!", "description": "The gadget owner." }, "request": { "method": "PATCH" } }, { "body": { "label": "everything", "description": "I can only say: 42." }, "request": { "method": "POST", "headers": {"Authorization": "Basic Yoasdkk1="} } } ] } ``` See the [extension specification](https://gist.github.com/e0ipso/cc95bfce66a5d489bb8a) for an example using JSON API. ## Getting information about the resource ### Exploring the resource Using a HTTP `GET` request on a resource's root URL will return information about that resource, in addition to the data itself. ``` shell curl https://example.com/api/ ``` This will output all the available **latest** resources (of course, if you have enabled the "Discovery Resource" option). For example, if there are 3 different API version plugins for content type Article (1.0, 1.1, 2.0) it will display the latest only (2.0 in this case). If you want to display all the versions of all the resources declared, then add the query **?all=true** like this. ``` shell curl https://example.com/api?all=true ``` The data results are stored in the `data` property of the JSON response, while the `self` and `next` objects contain information about the resource. ```javascript { "data": [ { "self": "https://example.com/api/v1.0/articles/123", "field": "A field value", "field2": "Another field value" }, // { ... more results follow ... } ], "count": 100, "self": { "title": "Self", "href": "https://example.com/api/v1.0/articles" }, "next": { "title": "Next", "href": "https://example.com/api/v1.0/articles?page=2" } } ``` ### Returning documentation about the resource Using an HTTP `OPTIONS` request, you can return documentation about the resource. To do so, make an `OPTIONS` request to the resource's root URL. ```shell curl -X OPTIONS -i https://example.com/api/v1.0/articles ``` The resource will respond with a JSON object that contains documentation for each field defined by the resource. See the _Documenting your API_ section of the [README file](../README.md) for examples of the types of information returned by such a request. ## Returning specific fields Using the ``?fields`` query string, you can declare which fields should be returned. Note that you can only return fields already being returned by `publicFields()`. This is used, for example, if you have many fields in `publicFields()`, but your client only needs a few specific ones. ```shell # Handler v1.0 curl https://example.com/api/v1/articles/2?fields=id ``` Returns: ```javascript { "data": [{ "id": "2", "label": "Foo" }] } ``` ## Applying a query filter RESTful allows applying filters to the database query used to generate the list. Bear in mind that for entity based resources, only those fields with a `'property'` (matching to an entity property or a Field API field) can be used for filtering. ```php # Handler v1.0 curl https://example.com/api/v1/articles?filter[label]=abc ``` You can even filter results using basic operators. For instance to get all the articles after a certain date: ```shell # Handler v1.0 curl https://example.com/api/articles?filter[created][value]=1417591992&filter[created][operator]=">=" ``` Additionally you can provide multiple filters for the same field. That is especially useful when filtering on multiple value fields. The following example will get all the articles with the `integer_multiple` field that contains all 1, 3 and 5. ``` curl https://example.com/api/articles?filter[integer_multiple][value][0]=1&filter[integer_multiple][value][1]=3&filter[integer_multiple][value][2]=5 ``` You can do more advanced filtering by providing values and operators. The following example will get all the articles with an `integer_multiple` value less than 5 and another equal to 10. ``` curl https://example.com/api/articles?filter[integer_multiple][value][0]=5&filter[integer_multiple][value][1]=10&filter[integer_multiple][operator][0]=">"&filter[integer_multiple][operator][1]="=" ``` ## Loading by an alternate ID. Sometimes you need to load an entity by an alternate ID that is not the regular entity ID, for example a unique ID title. All that you need to do is provide the alternate ID as the regular resource ID and inform that the passed in ID is not the regular entity ID but a different field. To do so use the `loadByFieldName` query parameter. ``` curl -H 'X-API-version: v1.5' https://www.example.org/articles/1234-abcd-5678-efg0?loadByFieldName=uuid ``` That will load the article node and output it as usual. Since every REST resource object has a canonical URL (and we are using a different one) a _Link_ header will be added to the response with the canonical URL so the consumer can use it in future requests. ``` HTTP/1.1 200 OK Date: Mon, 22 Dec 2014 08:08:53 GMT Content-Type: application/hal+json; charset=utf-8 ... Link: https://www.example.org/articles/12; rel="canonical" { ... } ``` The only requirement to use this feature is that the value for your `loadByFieldName` field needs to be one of your exposed fields. It is also up to you to make sure that that field is unique. Note that in case that more than one entity matches the provided ID, the first record will be loaded. ## Working with authentication providers RESTful comes with ``cookie``, ``base_auth`` (user name and password in the HTTP header) authentications providers, as well as a "RESTful token auth" module that has a `token` authentication provider. Note: if you use cookie-based authentication then you also need to set the HTTP ``X-CSRF-Token`` header on all writing requests (`POST`, `PUT` and `DELETE`). You can retrieve the token from ``/api/session/token`` with a standard HTTP `GET` request. See [this](https://github.com/Gizra/angular-restful-auth) AngularJs example that shows a login from a fully decoupled web app to a Drupal backend. Note: If you use basic auth under `.htaccess` password you might hit a flood exception, as the server is sending the `.htaccess` user name and password as the authentication. In such a case you may set the ``restful_skip_basic_auth`` to TRUE, in order to avoid using it. This will allow enabling and disabling the basic auth on different environments. ```bash # (Change username and password) curl -u "username:password" https://example.com/api/login-token # Response has access token. {"access_token":"YOUR_TOKEN","refresh_token":"OTHER_TOKEN",...} # Call a "protected" with token resource (Articles resource version 1.3 in "RESTful example") curl https://example.com/api/v1.3/articles/1?access_token=YOUR_TOKEN # Or use access-token instead of access_token for ensuring header is not going to be # dropped out from $_SERVER so it remains compatible with other webservers different than apache. curl -H "access-token: YOUR_TOKEN" https://example.com/api/v1.3/articles/1 ``` ## Error handling If an error occurs when operating the REST endpoint via URL, a valid JSON object with ``code``, ``message`` and ``description`` would be returned. The RESTful module adheres to the [Problem Details for HTTP APIs](http://tools.ietf.org/html/draft-nottingham-http-problem-06) draft to improve DX when dealing with HTTP API errors. Download and enable the [Advanced Help](https://drupal.org/project/advanced_help) module for more information about the errors. For example, trying to sort a list by an invalid key ```shell curl https://example.com/api/v1/articles?sort=wrong_key ``` Will result with an HTTP code 400, and the following JSON: ```javascript { 'type' => 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1', 'title' => 'The sort wrong_key is not allowed for this path.', 'status' => 400, 'detail' => 'Bad Request.', } ``` ================================================ FILE: docs/documentation.md ================================================ # Documenting your API ## Documenting your fields When declaring a public field and its mappings, you can also provide information about the field itself. This includes basic information about the field, information about the data the field holds, and information about the form element to generate on the client side for this field. By declaring this information, you make it possible for clients to provide form elements for your API using reusable form components. ```php $public_fields['text_multiple'] = array( 'property' => 'text_multiple', 'discovery' => array( // Basic information about the field for human consumption. 'info' => array( // The name of the field. Defaults to: ''. 'name' => t('Text multiple'), // The description of the field. Defaults to: ''. 'description' => t('This field holds different text inputs.'), // A custom piece of information we want to add to the documentation. 'custom' => t('This is custom documentation'), ), // Information about the data that the field holds. Typically used to help the client to manage the data appropriately. 'data' => array( // The type of data. For instance: 'int', 'string', 'boolean', 'object', 'array', ... Defaults to: NULL. 'type' => 'string', // The number of elements that this field can contain. Defaults to: 1. 'cardinality' => FIELD_CARDINALITY_UNLIMITED, // Avoid updating/setting this field. Typically used in fields representing the ID for the resource. Defaults to: FALSE. 'read_only' => FALSE, ), 'form_element' => array( // The type of the input element as in Form API. Defaults to: NULL. 'type' => 'textfield', // The default value for the form element. Defaults to: ''. 'default_value' => '', // The placeholder text for the form element. Defaults to: ''. 'placeholder' => t('This is helpful.'), // The size of the form element (if applies). Defaults to: NULL. 'size' => 255, // The allowed values for form elements with a limited set of options. Defaults to: NULL. 'allowed_values' => NULL, ), ), ); ``` Note the `'custom'` key; you can add your own information to the `'discovery'` property and it will be exposed as well. Here is a snippet from the JSON response to an HTTP OPTIONS request made to the above resource: ```json "text_multiple": { "info": { "label": "", "description": "This field holds different text inputs.", "name": "Text multiple", "custom": "This is custom documentation" }, "data": { "type": "string", "read_only": false, "cardinality": -1, "required": false }, "form_element": { "type": "textfield", "default_value": "", "placeholder": "This is helpful.", "size": 255, "allowed_values": null } }, ``` ================================================ FILE: docs/plugin.md ================================================ # Defining a RESTful Plugin ## Resources with multiple bundles One of the things that your API design should have is entity uniformity. That means that you should be able to describe the contents of a single item with a schema. That in turn, refers to the ability to set expectation in the fields that are going to be present for a given item, by explaining «I'm going to have fields a, b and c and they are all strings». If you have a resource for a Drupal entity type (ex: a node) that should output different bundles (ex: article and page), then you have to make sure that you only expose fields that are common to the both of them so the resulting payload is uniform. Another way to expose them, in case that you need to expose different fields, is to treat them as references. In that case you would have a public field per bundle that contains a reference to each specific bundle (one for the page and another one for the article, linked to the page and article resources). You will need to set the field class manually for that field. See an example of that in [EntityTests__1_0](../tests/modules/restful_test/src/Plugin/resource/entity_test/EntityTests__1_0.php). ## Defining the exposed fields By default the RESTful module will expose the ID, label and URL of the entity. You probably want to expose more than that. To do so you will need to implement the `publicFields` method defining the names in the output array and how those are mapped to the queried entity. For instance the following example will retrieve the basic fields plus the body, tags and images from an article node. The RESTful module will know to use the `MyArticlesResource` class because your plugin definition will say so. ```php class MyArticles__1_0 extends ResourceEntity { /** * Overrides ResourceEntity::publicFields(). */ public function publicFields() { $public_fields = parent::publicFields(); $public_fields['body'] = array( 'property' => 'body', 'sub_property' => 'value', ); $public_fields['tags'] = array( 'property' => 'field_tags', 'resource' => array( 'tags' => 'tags', ), ); $public_fields['image'] = array( 'property' => 'field_image', 'process_callbacks' => array( array($this, 'imageProcess'), ), // This will add 3 image variants in the output. 'image_styles' => array('thumbnail', 'medium', 'large'), ); return $public_fields; } } ``` See [the inline documentation](https://github.com/RESTful-Drupal/restful/blob/7.x-1.x/plugins/restful/RestfulEntityBase.php) for `publicFields` to get more details on exposing field data to your resource. If you need even more flexibility, you can use the `'callback'` key to name a custom function to compute the field data. ## Defining a view mode You can leverage Drupal core's view modes to render an entity and expose it as a resource with RESTful. All you need is to set up a view mode that renders the output you want to expose and tell RESTful to use it. This simplifies the workflow of exposing your resource a lot, since you don't even need to create a resource class, but it also offers you less features that are configured in the `publicFields` method. Use this method when you don't need any of the extra features that are added via `publicFields` (like the discovery metadata, image styles for images, process callbacks, custom access callbacks for properties, etc.). This is also a good way to stub a resource really quick and then move to the more fine grained method. To use this method, set the `'view_mode'` key in the plugin definition file: ```php $plugin = array( 'label' => t('Articles'), 'resource' => 'articles', 'name' => 'articles__1_7', 'entity_type' => 'node', 'bundle' => 'article', 'description' => t('Export the article content type using view modes.'), 'class' => 'RestfulEntityBaseNode', 'authentication_types' => TRUE, 'authentication_optional' => TRUE, 'minor_version' => 7, // Add the view mode information. 'view_mode' => array( 'name' => 'default', 'field_map' => array( 'body' => 'body', 'field_tags' => 'tags', 'field_image' => 'image', ), ), ); ``` ## Disable filter capability The filter parameter can be disabled in your resource plugin definition: ```php $plugin = array( ... 'url_params' => array( 'filter' => FALSE, ), ); ``` ## Defining a default sort You can also define default sort fields in your plugin, by overriding `defaultSortInfo()` in your class definition. This method should return an associative array, with each element having a key that matches a field from `publicFields()`, and a value of either 'ASC' or 'DESC'. Bear in mind that for entity based resources, only those fields with a `'property'` (matching to an entity property or a Field API field) can be used for sorting. This default sort will be ignored if the request URL contains a sort query. ```php class MyPlugin extends \RestfulEntityBaseTaxonomyTerm { /** * Overrides \RestfulEntityBase::defaultSortInfo(). */ public function defaultSortInfo() { // Sort by 'id' in descending order. return array('id' => 'DESC'); } } ``` ## Disabling sort capability The sort parameter can be disabled in your resource plugin definition: ```php $plugin = array( ... 'url_params' => array( 'sort' => FALSE, ), ); ``` ## Setting the default range The range can be specified by setting `$this->range` in your plugin definition. ### Disabling the range parameter The range parameter can be disabled in your resource plugin definition: ```php $plugin = array( ... 'url_params' => array( 'range' => FALSE, ), ); ``` ## Image derivatives Many client side technologies have lots of problems resizing images to serve them optimized and thus avoiding browser scaling. For that reason the RESTful module will let you specify an array of image style names to get an array of image derivatives for your image fields. Just add an `'image_styles'` key in your public field info (as shown above) with the list of styles to use and be done with it. ## Reference fields and properties It is considered a best practice to map a reference field (i.e. entity reference or taxonomy term reference) or a reference property (e.g. the ``uid`` property on the node entity) to the resource it belongs to. ```php public function publicFields() { $public_fields = parent::publicFields(); // ... $public_fields['user'] = array( 'property' => 'author', 'resource' => array( // The bundle of the entity. 'user' => array( // The name of the resource to map to. 'name' => 'users', // Determines if the entire resource should appear, or only the ID. 'fullView' => TRUE, ), ); // ... return $public_fields; } ``` Note that when you use the ``resource`` property, behind the scenes RESTful initializes a second handler and calls that resource. In order to pass information to the second handler (e.g. the access token), we pipe the original request array with some parameters removed. If you need to strip further parameters you can override ``\RestfulBase::getRequestForSubRequest``. ## Output formats The RESTful module outputs all resources by using HAL+JSON encoding by default. That means that when you have the following data: ```php array( array( 'id' => 2, 'label' => 'another title', 'self' => 'https://example.com/node/2', ), array( 'id' => 1, 'label' => 'example title', 'self' => 'https://example.com/node/1', ), ); ``` Then the following output is generated (using the header `ContentType:application/hal+json; charset=utf-8`): ```javascript { "data": [ { "id": 2, "label": "another title", "self": "https:\/\/example.com\/node\/2" }, { "id": 1, "label": "example title", "self": "https:\/\/example.com\/node\/1" } ], "count": 2, "_links": [] } ``` You can change that to be anything that you need. You have a plugin that will allow you to output XML instead of JSON in [the example module](./modules/restful_example/plugins/formatter). Take that example and create you custom module that contains the formatter plugin the you need (maybe you need to output JSON but following a different data structure, you may even want to use YAML, ...). All that you will need is to create a formatter plugin and tell your restful resource to use that in the restful plugin definition: ```php $plugin = array( 'label' => t('Articles'), 'resource' => 'articles', 'description' => t('Export the article content type in my cool format.'), ... 'formatter' => 'my_formatter', // <-- The name of the formatter plugin. ); ``` ### Changing the default output format If you need to change the output format for everything at once then you just have to set a special variable with the name of the new output format plugin. When you do that all the resources that don't specify a `'formatter'` key in the plugin definition will use that output format by default. Ex: ```php variable_set('restful_default_output_formatter', 'my_formatter'); ``` ## Render cache In addition to providing its own basic caching, the RESTful module is compatible with the [Entity Cache](https://drupal.org/project/entitycache) module. Two requests made by the same user requesting the same fields on the same entity will benefit from the render cache layer. This means that no entity will need to be loaded if it was rendered in the past under the same conditions. Developers have absolute control where the cache is stored and the expiration for every resource, meaning that very volatile resources can skip cache entirely while other resources can have its cache in MemCached or the database. To configure this developers just have to specify the following keys in their _restful_ plugin definition: ```php $plugin = array( ... 'render_cache' => array( // Enables the render cache. 'render' => TRUE, // Defaults to 'cache_restful' (optional). 'bin' => 'cache_bin_name', // Expiration logic. Defaults to CACHE_PERMANENT (optional). 'expire' => CACHE_TEMPORARY, // Enable cache invalidation for entity based resources. Defaults to TRUE (optional). 'simpleInvalidate' => TRUE, // Use a different cache backend for this resource. Defaults to variable_get('cache_default_class', 'DrupalDatabaseCache') (optional). 'class' => 'MemCacheDrupal', // Account cache granularity. Instead of caching per user you can choose to cache per role. Default: DRUPAL_CACHE_PER_USER. 'granularity' => DRUPAL_CACHE_PER_ROLE, ), ); ``` Additionally you can define a cache backend for a given cache bin by setting the variable `cache_class_` to the class to be used. This way all the resouces caching to that particular bin will use that cache backend instead of the default one. ## Rate limit RESTful provides rate limit functionality out of the box. A rate limit is a way to protect your API service from flooding, basically consisting on checking is the number of times an event has happened in a given period is greater that the maximum allowed. ### Rate limit events You can define your own rate limit events for your resources and define the limit an period for those, for that you only need to create a new _rate\_limit_ CTools plugin and implement the `isRequestedEvent` method. Every request the `isRequestedEvent` will be evaluated and if it returns true that request will increase the number of hits -for that particular user- for that event. If the number of hits is bigger than the allowed limit an exception will be raised. Two events are provided out of the box: the request event -that is always true for every request- and the global event -that is always true and is not contained for a given resource, all resources will increment the hit counter-. This way, for instance, you could define different limit for read operations than for write operations by checking the HTTP method in `isRequestedEvent`. ### Configuring your rate limits You can configure the declared Rate Limit events in every resource by providing a configuration array. The following is taken from the example resource articles 1.4 (articles\_\_1\_4.inc): ```php … 'rate_limit' => array( // The 'request' event is the basic event. You can declare your own events. 'request' => array( 'event' => 'request', // Rate limit is cleared every day. 'period' => new \DateInterval('P1D'), 'limits' => array( 'authenticated user' => 3, 'anonymous user' => 2, 'administrator' => \Drupal\restful\RateLimit\RateLimitManager::UNLIMITED_RATE_LIMIT, ), ), ), … ``` As you can see in the example you can set the rate limit differently depending on the role of the visiting user. Since the global event is not tied to any resource the limit and period is specified by setting the following variables: - `restful_global_rate_limit`: The number of allowed hits. This is global for all roles. - `restful_global_rate_period`: The period string compatible with \DateInterval. ## Documenting your resources. A resource can be documented in the plugin definition using the `'label'` and `'description'` keys: ```php $plugin = array( // This is the human readable name of the resource. 'label' => t('User'), // Use de description to provide more extended information about the resource. 'description' => t('Export the "User" entity.'), 'resource' => 'users', 'class' => 'RestfulEntityBaseUser', ... ); ``` This should not include any information about the endpoints or the allowed HTTP methods on them, since those will be accessed directly on the aforementioned endpoint. This information aims to describe what the accessed resource represents. To access this information just use the `discovery` resource at the api homepage: ```shell # List resources curl -u user:password https://example.org/api ``` ================================================ FILE: help/problem-instances-bad-request.html ================================================

Malformed syntax.

The request could not be understood by the server due to malformed syntax.

================================================ FILE: help/problem-instances-flood.html ================================================

Rate limit exceeded.

The user has sent too many requests in a given amount of time. Intended for use with rate limiting schemes.

================================================ FILE: help/problem-instances-forbidden.html ================================================

Forbidden resource access.

The request was a valid request, but the server is refusing to respond to it. Unlike a 401 Unauthorized response, authenticating will make no difference.

================================================ FILE: help/problem-instances-gone.html ================================================

Resource no longer available.

Indicates that the resource requested is no longer available and will not be available again. This should be used when a resource has been intentionally removed and the resource should be purged. Upon receiving a 410 status code, the client should not request the resource again in the future. Clients such as search engines should remove the resource from their indices. Most use cases do not require clients and search engines to purge the resource, and a "404 Not Found" may be used instead.

================================================ FILE: help/problem-instances-incompatible-field-definition.html ================================================

Incompatible field definition.

The field configuration contains incompatible or conflicting field definitions. Please make sure to read the documentation on how to declare your resource fields.

================================================ FILE: help/problem-instances-not-found.html ================================================

Document not found.

The requested resource could not be found but may be available again in the future. Subsequent requests by the client are permissible.

================================================ FILE: help/problem-instances-not-implemented.html ================================================

Operation not supported.

This error means that the operation you are attempting on the selected resource is not implemented in the server. If you think this operation should be supported, contact the support team.

================================================ FILE: help/problem-instances-server-configuration.html ================================================

Server configuration error.

Some configuration for the RESTful module is causing an unrecoverable error. Please check your configuration.

================================================ FILE: help/problem-instances-server-error.html ================================================

Internal server error exception.

Generic exception thrown when there is an error in the server side.

================================================ FILE: help/problem-instances-service-unavailable.html ================================================

Server is unavailable.

The server is currently unavailable (because it is overloaded or down for maintenance). Generally, this is a temporary state.

================================================ FILE: help/problem-instances-unauthorized.html ================================================

Authentication needed.

Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. The response must include a WWW-Authenticate header field containing a challenge applicable to the requested resource. See the authentication methods documentation for more information.

================================================ FILE: help/problem-instances-unprocessable-entity.html ================================================

Semantic error.

The request was well-formed but was unable to be followed due to semantic errors.

================================================ FILE: help/problem-instances-unsupported-media-type.html ================================================

Type not supported by the server.

The request entity has a media type which the server or resource does not support. For example, the client uploads an image as image/svg+xml, but the server requires that images use a different format.

================================================ FILE: help/problem-instances.html ================================================

Index

================================================ FILE: help/readme.html ================================================ Go to the project page in GitHub. ================================================ FILE: help/restful.help.ini ================================================ [advanced help settings] line break = TRUE [readme] title = README weight = -10 [problem-instances] title = RESTful problem instances weight = 0 [problem-instances-bad-request] title = Bad Request weight = -90 parent = problem-instances [problem-instances-flood] title = Flood weight = -80 parent = problem-instances [problem-instances-forbidden] title = Bad Request weight = -70 parent = problem-instances [problem-instances-gone] title = Gone weight = -60 parent = problem-instances [problem-instances-service-unavailable] title = Service unavailable weight = -55 parent = problem-instances [problem-instances-unauthorized] title = Unauthorized weight = -50 parent = problem-instances [problem-instances-unprocessable-entity] title = Unprocessable entity weight = -40 parent = problem-instances [problem-instances-unsupported-media-type] title = Unsupported media type weight = -30 parent = problem-instances ================================================ FILE: modules/restful_example/restful_example.info ================================================ name = RESTful example description = Example module for the RESTful module. core = 7.x dependencies[] = restful registry_autoload[] = PSR-0 registry_autoload[] = PSR-4 ================================================ FILE: modules/restful_example/restful_example.module ================================================ arrayToXML($structured_data, new \SimpleXMLElement(''))->asXML(); } /** * Converts the input array into an XML formatted string. * * @param array $data * The input array. * @param \SimpleXMLElement $xml * The object that will perform the conversion. * * @return \SimpleXMLElement */ protected function arrayToXML(array $data, \SimpleXMLElement $xml) { foreach ($data as $key => $value) { if(is_array($value)) { if(!is_numeric($key)){ $subnode = $xml->addChild("$key"); $this->arrayToXML($value, $subnode); } else{ $subnode = $xml->addChild("item$key"); $this->arrayToXML($value, $subnode); } } else { $xml->addChild("$key", htmlspecialchars("$value")); } } return $xml; } } ================================================ FILE: modules/restful_example/src/Plugin/resource/Tags__1_0.php ================================================ getAccount(); return user_access('create article content', $account); } } ================================================ FILE: modules/restful_example/src/Plugin/resource/comment/Comments__1_0.php ================================================ 'node', 'sub_property' => 'nid', ); // Add a custom field for test only. if (field_info_field('comment_text')) { $public_fields['comment_text'] = array( 'property' => 'comment_text', 'sub_property' => 'value', ); } return $public_fields; } /** * {@inheritdoc} */ protected function dataProviderClassName() { return '\Drupal\restful_example\Plugin\resource\comment\DataProviderComment'; } } ================================================ FILE: modules/restful_example/src/Plugin/resource/comment/DataProviderComment.php ================================================ value(); if (empty($comment->nid) && !empty($object['nid'])) { // Comment nid must be set manually, as the nid property setter requires // 'administer comments' permission. $comment->nid = $object['nid']; unset($object['nid']); // Make sure we have a bundle name. $node = node_load($comment->nid); $comment->node_type = 'comment_node_' . $node->type; } parent::setPropertyValues($wrapper, $object, $replace); } /** * Overrides DataProviderEntity::getQueryForList(). * * Expose only published comments. */ public function getQueryForList() { $query = parent::getQueryForList(); $query->propertyCondition('status', COMMENT_PUBLISHED); return $query; } /** * Overrides DataProviderEntity::getQueryCount(). * * Only count published comments. */ public function getQueryCount() { $query = parent::getQueryCount(); $query->propertyCondition('status', COMMENT_PUBLISHED); return $query; } /** * {@inheritdoc} */ public function entityPreSave(\EntityDrupalWrapper $wrapper) { $comment = $wrapper->value(); if (!empty($comment->cid)) { // Comment is already saved. return; } $comment->uid = $this->getAccount()->uid; } } ================================================ FILE: modules/restful_example/src/Plugin/resource/node/article/v1/Articles__1_0.php ================================================ pluginDefinition['rateLimit'] = array( // The 'request' event is the basic event. You can declare your own // events. 'request' => array( 'event' => 'request', // Rate limit is cleared every day. 'period' => 'P1D', 'limits' => array( 'authenticated user' => 3, 'anonymous user' => 2, 'administrator' => RateLimitManager::UNLIMITED_RATE_LIMIT, ), ), ); } } ================================================ FILE: modules/restful_example/src/Plugin/resource/node/article/v1/Articles__1_5.php ================================================ 'body', 'sub_property' => 'value', ); $public_fields['tags'] = array( 'property' => 'field_tags', 'resource' => array( 'name' => 'tags', 'majorVersion' => 1, 'minorVersion' => 0, ), ); $public_fields['image'] = array( 'property' => 'field_image', 'process_callbacks' => array( array($this, 'imageProcess'), ), 'image_styles' => array('thumbnail', 'medium', 'large'), ); // By checking that the field exists, we allow re-using this class on // different tests, where different fields exist. if (field_info_field('field_images')) { $public_fields['images'] = array( 'property' => 'field_images', 'process_callbacks' => array( array($this, 'imageProcess'), ), 'image_styles' => array('thumbnail', 'medium', 'large'), ); } $public_fields['user'] = array( 'property' => 'author', 'resource' => array( // The name of the resource to map to. 'name' => 'users', // Determines if the entire resource should appear, or only the ID. 'fullView' => TRUE, 'majorVersion' => 1, 'minorVersion' => 0, ), ); $public_fields['static'] = array( 'callback' => '\Drupal\restful_example\Plugin\resource\node\article\v1\Articles__1_5::randomNumber', ); return $public_fields; } /** * Process callback, Remove Drupal specific items from the image array. * * @param array $value * The image array. * * @return array * A cleaned image array. */ public function imageProcess($value) { if (ResourceFieldBase::isArrayNumeric($value)) { $output = array(); foreach ($value as $item) { $output[] = $this->imageProcess($item); } return $output; } return array( 'id' => $value['fid'], 'self' => file_create_url($value['uri']), 'filemime' => $value['filemime'], 'filesize' => $value['filesize'], 'width' => $value['width'], 'height' => $value['height'], 'styles' => $value['image_styles'], ); } /** * Callback, Generate a random number. * * @param DataInterpreterInterface $interpreter * The data interpreter containing the wrapper. * * @return int * A random integer. */ public static function randomNumber(DataInterpreterInterface $interpreter) { return mt_rand(); } } ================================================ FILE: modules/restful_example/src/Plugin/resource/node/article/v1/Articles__1_6.php ================================================ 'body', 'sub_property' => 'value', ); return $public_fields; } } ================================================ FILE: modules/restful_example/src/Plugin/resource/node/article/v1/Articles__1_7.php ================================================ array(), 'property' => 'uuid', ); return $fields; } } ================================================ FILE: modules/restful_example/src/Plugin/resource/variables/DataInterpreterVariable.php ================================================ options['urlParams'])) { $this->options['urlParams'] = array( 'filter' => TRUE, 'sort' => TRUE, 'fields' => TRUE, ); } } /** * {@inheritdoc} */ public function count() { return count($this->getIndexIds()); } /** * {@inheritdoc} */ public function create($object) { // Overly simplified update method. Search for the name and value fields, // and set the variable. $name_key = $this->searchPublicFieldByProperty('name'); $value_key = $this->searchPublicFieldByProperty('value'); if (empty($object[$name_key]) || empty($object[$value_key])) { throw new BadRequestException('You need to provide the variable name and value.'); } $identifier = $object[$name_key]; if (!empty($GLOBALS['conf'][$identifier])) { throw new UnprocessableEntityException('The selected variable already exists.'); } variable_set($identifier, $object[$value_key]); return array($this->view($identifier)); } /** * {@inheritdoc} */ public function view($identifier) { $resource_field_collection = $this->initResourceFieldCollection($identifier); $input = $this->getRequest()->getParsedInput(); $limit_fields = !empty($input['fields']) ? explode(',', $input['fields']) : array(); foreach ($this->fieldDefinitions as $resource_field_name => $resource_field) { /* @var \Drupal\restful\Plugin\resource\Field\ResourceFieldInterface $resource_field */ if ($limit_fields && !in_array($resource_field_name, $limit_fields)) { // Limit fields doesn't include this property. continue; } if (!$this->methodAccess($resource_field) || !$resource_field->access('view', $resource_field_collection->getInterpreter())) { // The field does not apply to the current method or has denied // access. continue; } $resource_field_collection->set($resource_field->id(), $resource_field); } return $resource_field_collection; } /** * {@inheritdoc} */ public function viewMultiple(array $identifiers) { $return = array(); foreach ($identifiers as $identifier) { try { $row = $this->view($identifier); } catch (InaccessibleRecordException $e) { $row = NULL; } $return[] = $row; } return array_values(array_filter($return)); } /** * {@inheritdoc} */ public function update($identifier, $object, $replace = FALSE) { // Overly simplified update method. Search for the name and value fields, // and set the variable. $name_key = $this->searchPublicFieldByProperty('name'); $value_key = $this->searchPublicFieldByProperty('value'); if (empty($object[$value_key])) { if (!$replace) { return array($this->view($identifier)); } $object[$value_key] = NULL; } if (!empty($object[$name_key]) && $object[$name_key] != $identifier) { // If the variable name is changed, then remove the old one. $this->remove($identifier); $identifier = $object[$name_key]; } variable_set($identifier, $object[$value_key]); return array($this->view($identifier)); } /** * {@inheritdoc} */ public function remove($identifier) { variable_del($identifier); } /** * {@inheritdoc} */ public function getIndexIds() { $output = array(); foreach ($GLOBALS['conf'] as $key => $value) { $output[] = array('name' => $key, 'value' => $value); } // Apply filters. $output = $this->applyFilters($output); $output = $this->applySort($output); return array_map(function ($item) { return $item['name']; }, $output); } /** * Removes plugins from the list based on the request options. * * @param \Drupal\restful\Plugin\resource\ResourceInterface[] $variables * The array of resource plugins keyed by instance ID. * * @return \Drupal\restful\Plugin\resource\ResourceInterface[] * The same array minus the filtered plugins. * * @throws \Drupal\restful\Exception\BadRequestException * @throws \Drupal\restful\Exception\ServiceUnavailableException */ protected function applyFilters(array $variables) { $filters = $this->parseRequestForListFilter(); // Apply the filter to the list of plugins. foreach ($variables as $delta => $variable) { $variable_name = $variable['name']; // A filter on a result needs the ResourceFieldCollection representing the // result to return. $interpreter = $this->initDataInterpreter($variable_name); $this->fieldDefinitions->setInterpreter($interpreter); foreach ($filters as $filter) { if (!$this->fieldDefinitions->evalFilter($filter)) { unset($variables[$delta]); } } } $this->fieldDefinitions->setInterpreter(NULL); return $variables; } /** * Sorts plugins on the list based on the request options. * * @param \Drupal\restful\Plugin\resource\ResourceInterface[] $variables * The array of resource plugins keyed by instance ID. * * @return \Drupal\restful\Plugin\resource\ResourceInterface[] * The sorted array. * * @throws \Drupal\restful\Exception\BadRequestException * @throws \Drupal\restful\Exception\ServiceUnavailableException */ protected function applySort(array $variables) { if ($sorts = $this->parseRequestForListSort()) { uasort($variables, function ($variable1, $variable2) use ($sorts) { $interpreter1 = $this->initDataInterpreter($variable1['name']); $interpreter2 = $this->initDataInterpreter($variable2['name']); foreach ($sorts as $key => $order) { $property = $this->fieldDefinitions->get($key)->getProperty(); $value1 = $interpreter1->getWrapper()->get($property); $value2 = $interpreter2->getWrapper()->get($property); if ($value1 == $value2) { continue; } return ($order == 'DESC' ? -1 : 1) * strcmp($value1, $value2); } return 0; }); } return $variables; } /** * {@inheritdoc} */ protected function initDataInterpreter($identifier) { return new DataInterpreterVariable($this->getAccount(), new ArrayWrapper(array( 'name' => $identifier, 'value' => variable_get($identifier), ))); } /** * Finds the public field name that has the provided property. * * @param string $property * The property to find. * * @return string * The name of the public name. */ protected function searchPublicFieldByProperty($property) { foreach ($this->fieldDefinitions as $public_name => $resource_field) { if ($resource_field->getProperty() == $property) { return $public_name; } } return NULL; } } ================================================ FILE: modules/restful_example/src/Plugin/resource/variables/DataProviderVariableInterface.php ================================================ array('property' => 'name'), 'variable_value' => array('property' => 'value'), ); } /** * {@inheritdoc} */ protected function dataProviderClassName() { return '\Drupal\restful_example\Plugin\resource\variables\DataProviderVariable'; } } ================================================ FILE: modules/restful_token_auth/modules/restful_token_auth_test/restful_token_auth_test.info ================================================ name = RESTful Token Authentication tests description = Test module that provides some example resources. core = 7.x dependencies[] = restful_token_auth registry_autoload[] = PSR-0 registry_autoload[] = PSR-4 ================================================ FILE: modules/restful_token_auth/modules/restful_token_auth_test/restful_token_auth_test.module ================================================ 'checkbox', '#title' => t('Delete expired tokens.'), '#description' => t('Enable to delete expired tokens when trying to use an expired token and during cron runs.'), '#default_value' => variable_get('restful_token_auth_delete_expired_tokens', TRUE), ); $form['advanced'] = array( '#type' => 'fieldset', '#title' => t('Advanced'), '#description' => t('Advanced configuration for the token authentication.'), '#collapsible' => TRUE, '#collapsed' => TRUE, ); $form['advanced']['restful_token_auth_expiration_period'] = array( '#type' => 'textfield', '#title' => t('Expiration time'), '#description' => t('The period string compatible with \DateInterval.', array('@url' => 'http://php.net/manual/en/class.dateinterval.php')), '#default_value' => variable_get('restful_token_auth_expiration_period', 'P1D'), '#element_validate' => array('restful_date_time_format_element_validate'), ); return system_settings_form($form); } ================================================ FILE: modules/restful_token_auth/restful_token_auth.info ================================================ name = RESTful token authentication description = Authenticate a REST call using a token. core = 7.x dependencies[] = restful dependencies[] = entityreference configure = admin/config/services/restful/token-auth registry_autoload[] = PSR-0 registry_autoload[] = PSR-4 files[] = tests/RestfulTokenAuthenticationTestCase.test ================================================ FILE: modules/restful_token_auth/restful_token_auth.install ================================================ 'The authentication token table.', 'fields' => array( 'id' => array( 'description' => 'The authentication token unique ID.', 'type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE, ), 'type' => array( 'description' => 'The authentication token type.', 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '', ), 'uid' => array( 'description' => 'The user the authentication token belongs to.', 'type' => 'int', 'not null' => TRUE, 'default' => 0, ), 'name' => array( 'description' => 'The authentication token name.', 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'default' => '', ), 'token' => array( 'description' => 'The authentication token security token.', 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '', ), 'created' => array( 'description' => 'The Unix timestamp when the authentication token was created.', 'type' => 'int', 'not null' => TRUE, 'default' => 0, ), 'expire' => array( 'description' => 'The Unix timestamp when the authentication token will expire.', 'type' => 'int', 'not null' => FALSE, 'default' => NULL, ), ), 'foreign keys' => array( 'uid' => array( 'table' => 'users', 'columns' => array('uid' => 'uid'), ), ), 'primary key' => array('id'), ); // Cache bins for Entity-cache module. $cache_schema = drupal_get_schema_unprocessed('system', 'cache'); $types = array('restful_token_auth'); foreach ($types as $type) { $schema["cache_entity_$type"] = $cache_schema; $schema["cache_entity_$type"]['description'] = "Cache table used to store $type entity records."; } return $schema; } /** * Implements hook_install(). */ function restful_token_auth_install() { restful_token_auth_create_field_refresh_token(); } /** * Implements hook_uninstall(). */ function restful_token_auth_uninstall() { variable_del('restful_token_auth_delete_expired_tokens'); field_delete_field('refresh_token_reference'); } /** * Adds the refresh token entity reference. */ function restful_token_auth_update_7100(&$sandbox) { // Change the type column for existing tokens. db_update('restful_token_auth') ->fields(array( 'type' => 'access_token', )) ->execute(); // Attach the new field. restful_token_auth_create_field_refresh_token(); } /** * Helper function to create the refresh token entity reference. */ function restful_token_auth_create_field_refresh_token() { // Add an entity reference field to the access_token bundle to link to the // corresponding refresh token. $field_name = 'refresh_token_reference'; $field = array( 'entity_types' => array('restful_token_auth'), 'settings' => array( 'handler' => 'base', 'target_type' => 'restful_token_auth', 'handler_settings' => array( 'target_bundles' => array( 'refresh_token' => 'refresh_token', ), ), ), 'field_name' => $field_name, 'type' => 'entityreference', 'cardinality' => 1, ); field_create_field($field); $instance = array( 'field_name' => $field_name, 'bundle' => 'access_token', 'entity_type' => 'restful_token_auth', 'label' => t('Refresh token'), 'description' => t('Token used to get a new access token once it is expired.'), 'required' => FALSE, ); field_create_instance($instance); } ================================================ FILE: modules/restful_token_auth/restful_token_auth.module ================================================ 'Token Authentication', 'description' => 'Administer the RESTful Token Authentication module.', 'type' => MENU_LOCAL_TASK, 'page callback' => 'drupal_get_form', 'page arguments' => array('restful_token_auth_admin_settings'), 'access arguments' => array('administer restful'), 'file' => 'restful_token_auth.admin.inc', ); return $items; } /** * Implements hook_restful_parse_request_alter(). */ function restful_token_auth_restful_parse_request_alter(RequestInterface &$request) { // In this hook we cannot rely on the service to be ready. $authentication_manager = new AuthenticationManager(); try { // If the the authentication provider have not been added yet, add it. $authentication_manager->addAuthenticationProvider('token'); $plugin = $authentication_manager->getPlugin('token'); } catch (PluginNotFoundException $e) { watchdog_exception('restful_token_auth', $e); return; } $plugin_definition = $plugin->getPluginDefinition(); $param_name = $plugin_definition['options']['paramName']; $header = $request->getHeaders()->get($param_name); $request->setApplicationData($param_name, $header->getValueString()); } /** * Implements hook_entity_info(). */ function restful_token_auth_entity_info() { $items['restful_token_auth'] = array( 'label' => t('Authentication token'), 'entity class' => '\\Drupal\\restful_token_auth\\Entity\\RestfulTokenAuth', 'controller class' => '\\Drupal\\restful_token_auth\\Entity\\RestfulTokenAuthController', 'base table' => 'restful_token_auth', 'fieldable' => TRUE, 'entity keys' => array( 'id' => 'id', 'label' => 'name', 'bundle' => 'type', ), 'bundles' => array( 'access_token' => array( 'label' => t('Access token'), ), 'refresh_token' => array( 'label' => t('Refresh token'), ), ), 'bundle keys' => array( 'bundle' => 'type', ), 'module' => 'restful_token_auth', 'entity cache' => module_exists('entitycache'), ); return $items; } /** * Implements hook_cron(). * * Delete expired token auth entities. */ function restful_token_auth_cron() { if (!variable_get('restful_token_auth_delete_expired_tokens', TRUE)) { // We should not delete expired tokens. return; } $query = new \EntityFieldQuery(); $result = $query ->entityCondition('entity_type', 'restful_token_auth') ->propertyCondition('expire', REQUEST_TIME, '<') ->range(0, 50) ->execute(); if (empty($result['restful_token_auth'])) { // No expired tokens. return; } $ids = array_keys($result['restful_token_auth']); entity_delete_multiple('restful_token_auth', $ids); } /** * Implements hook_restful_resource_alter(). */ function restful_token_auth_restful_resource_alter(ResourceInterface &$resource) { $plugin_definition = $resource->getPluginDefinition(); if ( empty($plugin_definition['dataProvider']['entityType']) || $plugin_definition['dataProvider']['entityType'] != 'restful_token_auth' || !empty($plugin_definition['formatter']) ) { return; } // If this resource is based on access token entities and does not have an // explicit formatter attached to it, then use the single_json formatter. $plugin_definition['formatter'] = 'single_json'; $resource->setPluginDefinition($plugin_definition); } /** * Implements hook_user_update(). */ function restful_token_auth_user_update(&$edit, $account, $category) { if ($edit['status']) { return; } $query = new EntityFieldQuery(); $result = $query ->entityCondition('entity_type', 'restful_token_auth') ->propertyCondition('uid', $account->uid) ->execute(); if (empty($result['restful_token_auth'])) { return; } entity_delete_multiple('restful_token_auth', array_keys($result['restful_token_auth'])); } ================================================ FILE: modules/restful_token_auth/src/Entity/RestfulTokenAuth.php ================================================ generateRefreshToken($uid); // Create a new access token. $values = array( 'uid' => $uid, 'type' => 'access_token', 'created' => REQUEST_TIME, 'name' => t('Access token for: @uid', array( '@uid' => $uid, )), 'token' => drupal_random_key(), 'expire' => $this->getExpireTime(), 'refresh_token_reference' => array( LANGUAGE_NONE => array(array( 'target_id' => $refresh_token->id, )), ), ); $access_token = $this->create($values); $this->save($access_token); return $access_token; } /** * Create a refresh token for the current user. * * It will delete all the existing refresh tokens for that same user as well. * * @param int $uid * The user ID. * * @return RestfulTokenAuth * The token entity. */ private function generateRefreshToken($uid) { // Check if there are other refresh tokens for the user. $query = new \EntityFieldQuery(); $results = $query ->entityCondition('entity_type', 'restful_token_auth') ->entityCondition('bundle', 'refresh_token') ->propertyCondition('uid', $uid) ->execute(); if (!empty($results['restful_token_auth'])) { // Delete the tokens. entity_delete_multiple('restful_token_auth', array_keys($results['restful_token_auth'])); } // Create a new refresh token. $values = array( 'uid' => $uid, 'type' => 'refresh_token', 'created' => REQUEST_TIME, 'name' => t('Refresh token for: @uid', array( '@uid' => $uid, )), 'token' => drupal_random_key(), ); $refresh_token = $this->create($values); $this->save($refresh_token); return $refresh_token; } /** * Return the expiration time. * * @throws ServerConfigurationException * * @return int * Timestamp with the expiration time. */ protected function getExpireTime() { $now = new \DateTime(); try { $expiration = $now->add(new \DateInterval(variable_get('restful_token_auth_expiration_period', 'P1D'))); } catch (\Exception $e) { throw new ServerConfigurationException('Invalid DateInterval format provided.'); } return $expiration->format('U'); } } ================================================ FILE: modules/restful_token_auth/src/Plugin/authentication/TokenAuthentication.php ================================================ extractToken($request); } /** * {@inheritdoc} */ public function authenticate(RequestInterface $request) { // Access token may be on the request, or in the headers. if (!$token = $this->extractToken($request)) { return NULL; } // Check if there is a token that has not expired yet. $query = new \EntityFieldQuery(); $result = $query ->entityCondition('entity_type', 'restful_token_auth') ->entityCondition('bundle', 'access_token') ->propertyCondition('token', $token) ->range(0, 1) ->execute(); if (empty($result['restful_token_auth'])) { // No token exists. return NULL; } $id = key($result['restful_token_auth']); $auth_token = entity_load_single('restful_token_auth', $id); if (!empty($auth_token->expire) && $auth_token->expire < REQUEST_TIME) { // Token is expired. if (variable_get('restful_token_auth_delete_expired_tokens', TRUE)) { // Token has expired, so we can delete this token. $auth_token->delete(); } return NULL; } return user_load($auth_token->uid); } /** * Extract the token from the request. * * @param RequestInterface $request * The request. * * @return string * The extracted token. */ protected function extractToken(RequestInterface $request) { $plugin_definition = $this->getPluginDefinition(); $options = $plugin_definition['options']; $key_name = !empty($options['paramName']) ? $options['paramName'] : 'access_token'; // Access token may be on the request, or in the headers. $input = $request->getParsedInput(); // If we don't have a $key_name on either the URL or the in the headers, // then check again using a hyphen instead of an underscore. This is due to // new versions of Apache not accepting headers with underscores. if (empty($input[$key_name]) && !$request->getHeaders()->get($key_name)->getValueString()) { $key_name = str_replace('_', '-', $key_name); } return empty($input[$key_name]) ? $request->getHeaders()->get($key_name)->getValueString() : $input[$key_name]; } } ================================================ FILE: modules/restful_token_auth/src/Plugin/resource/AccessToken__1_0.php ================================================ array( // Get or create a new token. RequestInterface::METHOD_GET => 'getOrCreateToken', RequestInterface::METHOD_OPTIONS => 'discover', ), ); } /** * Create a token for a user, and return its value. */ public function getOrCreateToken() { $entity_type = $this->getEntityType(); $account = $this->getAccount(); // Check if there is a token that did not expire yet. /* @var DataProviderEntityInterface $data_provider */ $data_provider = $this->getDataProvider(); $query = $data_provider->EFQObject(); $result = $query ->entityCondition('entity_type', $entity_type) ->entityCondition('bundle', 'access_token') ->propertyCondition('uid', $account->uid) ->range(0, 1) ->execute(); $token_exists = FALSE; if (!empty($result[$entity_type])) { $id = key($result[$entity_type]); $access_token = entity_load_single($entity_type, $id); $token_exists = TRUE; if (!empty($access_token->expire) && $access_token->expire < REQUEST_TIME) { if (variable_get('restful_token_auth_delete_expired_tokens', TRUE)) { // Token has expired, so we can delete this token. $access_token->delete(); } $token_exists = FALSE; } } if (!$token_exists) { /* @var \Drupal\restful_token_auth\Entity\RestfulTokenAuthController $controller */ $controller = entity_get_controller($this->getEntityType()); $access_token = $controller->generateAccessToken($account->uid); $id = $access_token->id; } $output = $this->view($id); return $output; } } ================================================ FILE: modules/restful_token_auth/src/Plugin/resource/RefreshToken__1_0.php ================================================ array( // Get or create a new token. RequestInterface::METHOD_GET => 'refreshToken', ), ); } /** * Create a token for a user, and return its value. * * @param string $token * The refresh token. * * @throws BadRequestException * * @return RestfulTokenAuth * The new access token. */ public function refreshToken($token) { // Check if there is a token that did not expire yet. /* @var \Drupal\restful\Plugin\resource\DataProvider\DataProviderEntityInterface $data_provider */ $data_provider = $this->getDataProvider(); $query = $data_provider->EFQObject(); $results = $query ->entityCondition('entity_type', $this->entityType) ->entityCondition('bundle', 'refresh_token') ->propertyCondition('token', $token) ->range(0, 1) ->execute(); if (empty($results['restful_token_auth'])) { throw new BadRequestException('Invalid refresh token.'); } // Remove the refresh token once used. $refresh_token = entity_load_single('restful_token_auth', key($results['restful_token_auth'])); $uid = $refresh_token->uid; // Get the access token linked to this refresh token then do some cleanup. $access_token_query = new EntityFieldQuery(); $access_token_reference = $access_token_query ->entityCondition('entity_type', 'restful_token_auth') ->entityCondition('bundle', 'access_token') ->fieldCondition('refresh_token_reference', 'target_id', $refresh_token->id) ->range(0, 1) ->execute(); if (!empty($access_token_reference['restful_token_auth'])) { $access_token = key($access_token_reference['restful_token_auth']); entity_delete('restful_token_auth', $access_token); } $refresh_token->delete(); // Create the new access token and return it. /* @var \Drupal\restful_token_auth\Entity\RestfulTokenAuthController $controller */ $controller = entity_get_controller($this->getEntityType()); $token = $controller->generateAccessToken($uid); return $this->view($token->id); } } ================================================ FILE: modules/restful_token_auth/src/Plugin/resource/TokenAuthenticationBase.php ================================================ 'token', ); $public_fields['type'] = array( 'callback' => array('\Drupal\restful\RestfulManager::echoMessage', array('Bearer')), ); $public_fields['expires_in'] = array( 'property' => 'expire', 'process_callbacks' => array( '\Drupal\restful_token_auth\Plugin\resource\TokenAuthenticationBase::intervalInSeconds', ), ); $public_fields['refresh_token'] = array( 'property' => 'refresh_token_reference', 'process_callbacks' => array( '\Drupal\restful_token_auth\Plugin\resource\TokenAuthenticationBase::getTokenFromEntity', ), ); return $public_fields; } /** * Process callback helper to get the time difference in seconds. * * @param int $value * The expiration timestamp in the access token. * * @return int * Number of seconds before expiration. */ public static function intervalInSeconds($value) { $interval = $value - time(); return $interval < 0 ? 0 : $interval; } /** * Get the token string from the token entity. * * @param int $token_id * The restful_token_auth entity. * * @return string * The token string. */ public static function getTokenFromEntity($token_id) { if ($token = entity_load_single('restful_token_auth', $token_id)) { return $token->token; } return NULL; } } ================================================ FILE: modules/restful_token_auth/tests/RestfulTokenAuthenticationTestCase.test ================================================ 'Token Authentication', 'description' => 'Test the request authentication with a token.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_example', 'restful_token_auth', 'restful_token_auth_test'); $this->user = $this->drupalCreateUser(); } /** * Testing the user's access token will be invalidate one the user is blocked. */ function testTokenInvalidating() { $this->drupalLogin($this->user); $resource_manager = restful()->getResourceManager(); $handler = $resource_manager->getPlugin('access_token:1.0'); // Generating token. $handler->doGet(); // Blocking the user. user_save($this->user, array('status' => FALSE)); // Verify the token removed. $query = new EntityFieldQuery(); $result = $query ->entityCondition('entity_type', 'restful_token_auth') ->propertyCondition('uid', $this->user->uid) ->execute(); $this->assertTrue(empty($result), 'The access tokens invalidated when blocking the user.'); } /** * Test authenticating a user. */ function testAuthentication() { // Create user. $this->drupalLogin($this->user); // Create "Article" node. $title1 = $this->randomName(); $settings = array( 'type' => 'article', 'title' => $title1, 'uid' => $this->user->uid, ); $node1 = $this->drupalCreateNode($settings); $id = $node1->nid; $resource_manager = restful()->getResourceManager(); $formatter = restful()->getFormatterManager()->getPlugin('single_json'); // Get a token for the user, using the handler. $handler = $resource_manager->getPlugin('access_token:1.0'); $formatter->setResource($handler); $result = $formatter->prepare($handler->doGet()); $access_token = $result['access_token']; $refresh_token = $result['refresh_token']; $this->assertNotNull($access_token); $this->assertNotNull($refresh_token); // Assert the token did not change. $result = $formatter->prepare($handler->doGet()); $this->assertEqual($access_token, $result['access_token'], 'Access token did not change.'); // Get a "protected" resource without the access token. $handler = $resource_manager->getPlugin('articles:1.3'); $handler->setRequest(Request::create('api/v1.3/articles')); $handler->setPath(''); $formatter->setResource($handler); try { // Reset the account to trigger the auth process. $handler->setAccount(NULL); $handler->getAccount(); $this->fail('"Unauthorized" exception not thrown.'); } catch (UnauthorizedException $e) { $this->pass('"Unauthorized" exception was thrown.'); } // Get a "protected" resource with invalid access token. $handler->setRequest(Request::create('api/v1.3/articles', array( 'access_token' => 'invalid', ))); try { // Reset the account to trigger the auth process. $handler->setAccount(NULL); $handler->getAccount(); $this->fail('"Unauthorized" exception not thrown.'); } catch (UnauthorizedException $e) { $this->pass('"Unauthorized" exception was thrown.'); } // Get a "protected" resource with refresh token as access token. $handler->setRequest(Request::create('api/v1.3/articles/' . $id, array( 'access_token' => 'invalid', ))); $handler->setPath($id); try { // Reset the account to trigger the auth process. $handler->setAccount(NULL); $handler->getAccount(); $this->fail('"Unauthorized" exception not thrown.'); } catch (UnauthorizedException $e) { $this->pass('"Unauthorized" exception was thrown.'); } // Get a "protected" resource with refresh token. $handler->setRequest(Request::create('api/v1.3/articles/' . $id, array( 'refresh_token' => $refresh_token, ))); $handler->setPath($id); try { // Reset the account to trigger the auth process. $handler->setAccount(NULL); $handler->getAccount(); $this->fail('"Unauthorized" exception not thrown.'); } catch (UnauthorizedException $e) { $this->pass('"Unauthorized" exception was thrown.'); } // Get a "protected" resource with the access token. $response = restful() ->getFormatterManager() ->negotiateFormatter(NULL) ->prepare($handler->doGet($id, array('access_token' => $access_token))); $handler->setAccount(NULL); $handler->getAccount(); // Validate the returned content. $result = $response['data'][0]; $this->assertEqual($result['label'], $title1, 'Article resource can be accessed with valid access token.'); // Set the expiration token to the past. $query = new \EntityFieldQuery(); $result = $query ->entityCondition('entity_type', 'restful_token_auth') ->entityCondition('bundle', 'access_token') ->propertyCondition('token', $access_token) ->execute(); if (empty($result['restful_token_auth'])) { $this->fail('No token was found.'); } // Load the token. $access_id = key($result['restful_token_auth']); $token = entity_load_single('restful_token_auth', $access_id); $token->expire = REQUEST_TIME - 60 * 24; $token->save(); // Make a GET request to trigger a deletion of the token. $handler = $resource_manager->getPlugin('articles:1.3'); $formatter->setResource($handler); $handler->setRequest(Request::create('api/v1.3/articles/' . $id, array( 'access_token' => $access_token, ))); $handler->setPath($id); try { // Reset the account to trigger the auth process. $handler->setAccount(NULL); $handler->getAccount(); $this->fail('"Unauthorized" exception not thrown for expired token.'); } catch (UnauthorizedException $e) { $this->pass('"Unauthorized" exception was thrown for expired token.'); } // Make sure the token was deleted. $query = new \EntityFieldQuery(); $count = $query ->entityCondition('entity_type', 'restful_token_auth') ->entityCondition('bundle', 'access_token') ->propertyCondition('token', $access_token) ->count() ->execute(); $this->assertFalse($count, 'The token was deleted.'); // Test the refresh capabilities. $handler = $resource_manager->getPlugin('refresh_token:1.0'); $formatter->setResource($handler); $result = $formatter->prepare($handler->doGet($refresh_token)); $this->assertNotNull($result['access_token'], 'A new access token granted for a valid refresh token.'); $this->assertNotNull($result['refresh_token'], 'A new refresh token granted for a valid refresh token.'); $this->assertNotEqual($refresh_token, $result['refresh_token']); // Test invalid refresh token. try { $handler->doGet('invalid'); $this->fail('"Bad Request" exception not thrown.'); } catch (BadRequestException $e) { $this->pass('"Bad Request" exception was thrown.'); } } } ================================================ FILE: restful.admin.inc ================================================ 'radios', '#title' => t('Default formatter'), '#description' => t('Determine the default formatter that would be used.'), '#options' => array(), '#default_value' => variable_get('restful_default_output_formatter', 'json'), '#required' => TRUE, ); $element = &$form['restful_default_output_formatter']; $formatter_manager = FormatterPluginManager::create(); foreach ($formatter_manager->getDefinitions() as $plugin_name => $plugin) { $element['#options'][$plugin_name] = check_plain($plugin['label']); // Add description for each formatter. if (!$plugin['description']) { continue; } $element[$plugin_name]['#description'] = check_plain($plugin['description']); } $params = array( '@api' => variable_get('restful_hook_menu_base_path', 'api'), ); $form['file_upload'] = array( '#type' => 'fieldset', '#title' => t('File upload'), ); $form['file_upload']['restful_file_upload'] = array( '#type' => 'checkbox', '#title' => t('File upload'), '#description' => t('When enabled a file upload resource will be available.'), '#default_value' => variable_get('restful_file_upload', FALSE), ); $form['file_upload']['restful_file_upload_allow_anonymous_user'] = array( '#type' => 'checkbox', '#title' => t('Anonymous file upload'), '#description' => t('When enabled a file upload resource will be available also for anonymous users.'), '#default_value' => variable_get('restful_file_upload_allow_anonymous_user', FALSE), '#states' => array( 'visible' => array( ':input[name=restful_file_upload]' => array('checked' => TRUE), ), ), ); $form['restful_show_access_denied'] = array( '#type' => 'checkbox', '#title' => t('Show access denied records'), '#description' => t('Check this box to get an HTTP 403 error when requesting entities with access denied. Listing denied entities will remove them from the output and have a flag indicating the access violation. Leave it unchecked to hide access denied records. If you do not care about unpriviledged users knowing that records they do not have access to exist, you can check this box.'), '#default_value' => variable_get('restful_show_access_denied', FALSE), ); $form['restful_allowed_origin'] = array( '#type' => 'textfield', '#title' => t('Allowed origin'), '#description' => t('When you make an XHR (AJAX) call to a resource under a different host, most modern browsers will make an initial OPTIONS request to the resource. The response to that can contain the Access-Control-Allow-Origin, if that includes the domain making the request the browser will allow the cross-domain request. The contents of this field will be used in that header. Use * to allow any origin.'), '#default_value' => variable_get('restful_allowed_origin', ''), ); $form['advanced'] = array( '#type' => 'fieldset', '#title' => t('Advanced'), '#collapsible' => TRUE, '#collapsed' => TRUE, ); $form['advanced']['restful_hijack_api_pages'] = array( '#type' => 'checkbox', '#title' => t('Hijack API pages'), '#description' => t('When enabled all URLS under @api will be handled by RESTful module.', $params), '#default_value' => variable_get('restful_hijack_api_pages', TRUE), ); $form['advanced']['restful_hook_menu_base_path'] = array( '#type' => 'textfield', '#title' => t('API Base path'), '#description' => t('Determines the base path of all resources.'), '#default_value' => variable_get('restful_hook_menu_base_path', 'api'), ); $form['advanced']['restful_enable_user_login_resource'] = array( '#type' => 'checkbox', '#title' => t('Login resource'), '#description' => t('Determines if the default user login resource should be enabled.'), '#default_value' => variable_get('restful_enable_user_login_resource', TRUE), ); $form['advanced']['restful_enable_users_resource'] = array( '#type' => 'checkbox', '#title' => t('User resource'), '#description' => t('Determines if the default user resource should be enabled.'), '#default_value' => variable_get('restful_enable_users_resource', TRUE), ); $form['advanced']['restful_enable_discovery_resource'] = array( '#type' => 'checkbox', '#title' => t('Discovery resource'), '#description' => t('Enable discovery resource which shows all accessible resources under @api URL.', $params), '#default_value' => variable_get('restful_enable_discovery_resource', TRUE), ); $form['advanced']['restful_global_rate_limit'] = array( '#type' => 'textfield', '#title' => t('Rate limit - hits'), '#description' => t('The number of allowed hits. This is global for all roles. 0 means no global rate limit should be applied.'), '#default_value' => variable_get('restful_global_rate_limit', 0), '#element_validate' => array('element_validate_integer'), ); $form['advanced']['restful_global_rate_period'] = array( '#type' => 'textfield', '#title' => t('Rate limit - period'), '#description' => t('The period string compatible with \DateInterval. After this period the module will restart counting hits.', array('@url' => 'http://php.net/manual/en/dateinterval.createfromdatestring.php')), '#default_value' => variable_get('restful_global_rate_period', 'P1D'), '#element_validate' => array('restful_date_time_format_element_validate'), ); return system_settings_form($form); } ================================================ FILE: restful.api.php ================================================ setApplicationData('csrf_token', 'token'); } /** * Allow altering the request before it is processed. * * @param \Drupal\restful\Plugin\resource\ResourceInterface &$resource * The resource object to alter. */ function hook_restful_resource_alter(\Drupal\restful\Plugin\resource\ResourceInterface &$resource) { // Chain a decorator with the passed in resource based on the resource // annotation definition. $plugin_definition = $resource->getPluginDefinition(); if (!empty($plugin_definition['renderCache']) && !empty($plugin_definition['renderCache']['render'])) { $resource = new \Drupal\restful\Plugin\resource\CachedResource($resource); } } /** * @} End of "addtogroup hooks". */ ================================================ FILE: restful.cache.inc ================================================ wipe(); cache_clear_all('*', RenderCache::CACHE_BIN, TRUE); drupal_set_message(t('RESTful caches were successfully cleared.')); } /** * Implements hook_flush_caches(). */ function restful_flush_caches() { if (!variable_get('restful_clear_on_cc_all', FALSE)) { return array(); } // Delete all the cache fragments. /* @var \Drupal\restful\RenderCache\Entity\CacheFragmentController $controller */ $controller = entity_get_controller('cache_fragment'); $controller->wipe(); return array(RenderCache::CACHE_BIN); } /** * Menu callback; Admin settings form. */ function restful_admin_cache_settings($form_state) { $form = array(); $form['restful_page_cache'] = array( '#type' => 'checkbox', '#title' => t('Page cache'), '#description' => t('RESTful can leverage page cache, this will boost your performace for anonymous traffic. !link to start caching responses. Status: @status. CAUTION: If your resources are using authentication providers other than cookie, you will want to turn this off. Otherwise you may get cached anonymous values for your authenticated GET requests.', array( '!link' => l(t('Enable page cache'), 'admin/config/development/performance'), '@status' => variable_get('cache', FALSE) ? t('Enabled') : t('Disabled'), )), '#disabled' => !variable_get('cache', FALSE), '#default_value' => variable_get('restful_page_cache', FALSE) && variable_get('cache', FALSE), ); $form['restful_render_cache'] = array( '#type' => 'checkbox', '#title' => t('Cache results'), '#description' => t('When enabled any resource that has not explicitly disabled the caching will be cached. Note that the first hit may result with slower response, although the next ones would be significantly faster. This is different from the page cache in the sense that it acts at the row level (a single entity, a single database row, ...), therefore allowing you to assemble non cached pages with the cached bits faster.'), '#default_value' => variable_get('restful_render_cache', FALSE), ); $form['clear_restful'] = array( '#submit' => array('restful_clear_caches'), '#type' => 'submit', '#value' => t('Clear render caches'), '#disabled' => !variable_get('restful_render_cache', FALSE) && user_access('restful clear render caches'), ); $form['restful_clear_on_cc_all'] = array( '#type' => 'checkbox', '#title' => t('Clear on global flush'), '#description' => t("Check this box to clear the render caches when clearing Drupal's caches. In general the render caches are more robust than the TTL based caches. The recommended value is unchecked."), '#default_value' => variable_get('restful_clear_on_cc_all', FALSE), ); $form['restful_fast_cache_clear'] = array( '#type' => 'checkbox', '#title' => t('Fast cache clear'), '#description' => t('A lot of cache fragment entries may be created by default. This may cause your cache clears to be slow. By checking this checkbox the cache fragments are deleted from the database in a fast manner. As a trade-in, no hook_entity_delete will be fired for the cache fragment entities. This is OK in the vast majority of the cases. You can mitigate the number of generated fragments by overriding the "getCacheContext" method in your data provider.'), '#default_value' => variable_get('restful_fast_cache_clear', TRUE), ); return system_settings_form($form); } ================================================ FILE: restful.entity.inc ================================================ t('Rate limit'), 'entity class' => '\\Drupal\\restful\\RateLimit\\Entity\\RateLimit', 'controller class' => '\\Drupal\\restful\\RateLimit\\Entity\\RateLimitController', 'base table' => 'restful_rate_limit', 'fieldable' => TRUE, 'entity keys' => array( 'id' => 'rlid', 'label' => 'identifier', 'bundle' => 'event', ), 'bundles' => array(), 'bundle keys' => array( 'bundle' => 'type', ), 'module' => 'restful', 'entity cache' => module_exists('entitycache'), ); $items['cache_fragment'] = array( 'label' => t('Cache fragment'), 'entity class' => '\\Drupal\\restful\\RenderCache\\Entity\\CacheFragment', 'controller class' => '\\Drupal\\restful\\RenderCache\\Entity\\CacheFragmentController', 'base table' => 'restful_cache_fragment', 'fieldable' => FALSE, 'entity keys' => array( 'id' => 'tid', 'label' => 'identifier', 'bundle' => 'type', ), 'bundles' => array(), 'bundle keys' => array( 'bundle' => 'type', ), 'module' => 'restful', 'entity cache' => FALSE, ); return $items; } /** * Helper function that extract cache hashes from an entity. */ function _restful_entity_cache_hashes($entity, $type) { if ($type == 'cache_fragment') { return array(); } // Limit to the fragments for our entity type. list($entity_id) = entity_extract_ids($type, $entity); $query = new \EntityFieldQuery(); $query ->entityCondition('entity_type', 'cache_fragment') ->propertyCondition('type', 'entity') ->propertyCondition('value', CacheDecoratedResource::serializeKeyValue($type, $entity_id)); return CacheFragmentController::lookUpHashes($query); } /** * Implements hook_entity_update(). */ function restful_entity_update($entity, $type) { $hashes = &drupal_static('restful_entity_clear_hashes', array()); $new_hashes = _restful_entity_cache_hashes($entity, $type); array_walk($new_hashes, '_restful_entity_clear_all_resources'); $hashes += $new_hashes; restful_register_shutdown_function_once('restful_entity_clear_render_cache'); } /** * Implements hook_entity_delete(). */ function restful_entity_delete($entity, $type) { $hashes = &drupal_static('restful_entity_clear_hashes', array()); $new_hashes = _restful_entity_cache_hashes($entity, $type); array_walk($new_hashes, '_restful_entity_clear_all_resources'); $hashes += $new_hashes; restful_register_shutdown_function_once('restful_entity_clear_render_cache'); } /** * Implements hook_user_update(). */ function restful_user_update(&$edit, $account, $category) { // Search for all the cache fragments with our entity id. $query = new \EntityFieldQuery(); $query ->entityCondition('entity_type', 'cache_fragment') ->propertyCondition('type', 'user_id') ->propertyCondition('value', $account->uid); $hashes = &drupal_static('restful_entity_clear_hashes', array()); $new_hashes = CacheFragmentController::lookUpHashes($query); array_walk($new_hashes, '_restful_entity_clear_all_resources'); $hashes += $new_hashes; restful_register_shutdown_function_once('restful_entity_clear_render_cache'); } /** * Implements hook_user_delete(). */ function restful_user_delete($account) { // Search for all the cache fragments with our entity id. $query = new \EntityFieldQuery(); $query ->entityCondition('entity_type', 'cache_fragment') ->propertyCondition('type', 'user_id') ->propertyCondition('value', $account->uid); $hashes = &drupal_static('restful_entity_clear_hashes', array()); $new_hashes = CacheFragmentController::lookUpHashes($query); array_walk($new_hashes, '_restful_entity_clear_all_resources'); $hashes += $new_hashes; restful_register_shutdown_function_once('restful_entity_clear_render_cache'); } /** * Helper function to schedule a shutdown once. * * @param callable $callback * The callback. */ function restful_register_shutdown_function_once($callback) { $existing_callbacks = drupal_register_shutdown_function(); $added = (bool) array_filter($existing_callbacks, function ($item) use ($callback) { return $item['callback'] == $callback; }); if (!$added) { drupal_register_shutdown_function($callback); } } /** * Clear the cache back ends for the given hash. * * @param string $cid * The cache ID to clear. */ function _restful_entity_clear_all_resources($cid) { if (!$instance_id = CacheFragmentController::resourceIdFromHash($cid)) { return; } try { $handler = restful()->getResourceManager()->getPlugin($instance_id); } catch (ServerConfigurationException $e) { watchdog_exception('restful', $e); return; } if (!$handler instanceof CacheDecoratedResourceInterface) { return; } // Clear the cache bin. $handler->getCacheController()->clear($cid); } /** * Delete the scheduled fragments and caches on shutdown. */ function restful_entity_clear_render_cache() { if ($hashes = drupal_static('restful_entity_clear_hashes', array())) { $hashes = array_unique($hashes); drupal_static_reset('restful_entity_clear_hashes'); $resource_manager = restful()->getResourceManager(); foreach ($hashes as $hash) { if (!$instance_id = CacheFragmentController::resourceIdFromHash($hash)) { continue; } $handler = $resource_manager->getPlugin($instance_id); if (!$handler instanceof CacheDecoratedResourceInterface) { continue; } if (!$handler->hasSimpleInvalidation()) { continue; } // You can get away without the fragments for a clear. $cache_object = new RenderCache(new ArrayCollection(), $hash, $handler->getCacheController()); // Do a clear with the RenderCache object to also remove the cache // fragment entities. $cache_object->clear(); } } } ================================================ FILE: restful.info ================================================ name = RESTful description = Turn Drupal to a RESTful server, following best practices. core = 7.x php = 5.5.9 dependencies[] = entity dependencies[] = plug configure = admin/config/services/restful registry_autoload[] = PSR-0 registry_autoload[] = PSR-4 ; Temporary workaround to allow RESTful to work fine on PHP7. registry_autoload_files[] = src/Util/ExplorableDecoratorInterface.php ; Tests files[] = tests/RestfulAuthenticationTestCase.test files[] = tests/RestfulCommentTestCase.test files[] = tests/RestfulCreateEntityTestCase.test files[] = tests/RestfulCreateNodeTestCase.test files[] = tests/RestfulCreatePrivateNodeTestCase.test files[] = tests/RestfulCreateTaxonomyTermTestCase.test files[] = tests/RestfulCsrfTokenTestCase.test files[] = tests/RestfulCurlBaseTestCase.test files[] = tests/RestfulDataProviderPlugPluginsTestCase.test files[] = tests/RestfulDbQueryTestCase.test files[] = tests/RestfulVariableTestCase.test files[] = tests/RestfulDiscoveryTestCase.test files[] = tests/RestfulEntityAndPropertyAccessTestCase.test files[] = tests/RestfulEntityUserAccessTestCase.test files[] = tests/RestfulEntityValidatorTestCase.test files[] = tests/RestfulExceptionHandleTestCase.test files[] = tests/RestfulForbiddenItemsTestCase.test files[] = tests/RestfulGetHandlersTestCase.test files[] = tests/RestfulHalJsonTestCase.test files[] = tests/RestfulHookMenuTestCase.test files[] = tests/RestfulJsonApiTestCase.test files[] = tests/RestfulListEntityMultipleBundlesTestCase.test files[] = tests/RestfulListTestCase.test files[] = tests/RestfulRateLimitTestCase.test files[] = tests/RestfulReferenceTestCase.test files[] = tests/RestfulRenderCacheTestCase.test files[] = tests/RestfulSimpleJsonTestCase.test files[] = tests/RestfulUpdateEntityTestCase.test files[] = tests/RestfulSubResourcesCreateEntityTestCase.test files[] = tests/RestfulUpdateEntityCurlTestCase.test files[] = tests/RestfulUserLoginCookieTestCase.test files[] = tests/RestfulViewEntityMultiLingualTestCase.test files[] = tests/RestfulViewEntityTestCase.test files[] = tests/RestfulViewModeAndFormatterTestCase.test ================================================ FILE: restful.install ================================================ 'Rate limit base table', 'fields' => array( 'rlid' => array( 'description' => 'The rate limit unique ID.', 'type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE, ), 'event' => array( 'description' => 'The event name.', 'type' => 'varchar', 'length' => 64, 'not null' => TRUE, 'default' => '', ), 'identifier' => array( 'description' => 'The user & request identifier.', 'type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => '', ), 'timestamp' => array( 'description' => 'The Unix timestamp when the rate limit window started.', 'type' => 'int', 'not null' => FALSE, 'default' => NULL, ), 'expiration' => array( 'description' => 'The Unix timestamp when the rate limit window expires.', 'type' => 'int', 'not null' => FALSE, 'default' => NULL, ), 'hits' => array( 'description' => 'The number of hits.', 'type' => 'int', 'not null' => FALSE, 'default' => 0, ), ), 'unique keys' => array( 'identifier' => array('identifier'), ), 'indexes' => array( 'rate_limit_identifier' => array('identifier'), ), 'primary key' => array('rlid'), ); // Cache fragment entity base table. $schema['restful_cache_fragment'] = array( 'description' => 'Cache fragment base table', 'fields' => array( 'tid' => array( 'description' => 'The cache fragment unique ID.', 'type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE, ), 'type' => array( 'description' => 'The type of the cache fragment, e.g "entity_id".', 'type' => 'varchar', 'length' => 64, 'not null' => TRUE, 'default' => '', ), 'value' => array( 'description' => 'The value for the tag. Example: 182.', 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '', ), 'hash' => array( 'description' => 'The hash used as a cache ID.', 'type' => 'char', 'length' => 40, 'not null' => FALSE, 'default' => NULL, ), ), 'indexes' => array( 'cache_fragment_hash' => array('hash'), ), 'unique keys' => array( 'hash_key_val' => array('type', 'value', 'hash'), ), 'primary key' => array('tid'), ); return $schema; } /** * Implements hook_uninstall(). */ function restful_uninstall() { variable_del('restful_default_output_formatter'); variable_del('restful_enable_discovery_resource'); variable_del('restful_file_upload'); variable_del('restful_file_upload_allow_anonymous_user'); variable_del('restful_hijack_api_pages'); variable_del('restful_hook_menu_base_path'); variable_del('restful_enable_user_login_resource'); variable_del('restful_global_rate_limit'); variable_del('restful_global_rate_period'); variable_del('restful_enable_users_resource'); variable_del('restful_render_cache'); variable_del('restful_skip_basic_auth'); variable_del('restful_allowed_origin'); } /** * Create the cache fragments schema. */ function restful_update_7200() { $table_schema = drupal_get_schema('restful_cache_fragment', TRUE); db_create_table('restful_cache_fragment', $table_schema); } /** * Clear RESTful cache on cache flush. */ function restful_update_7201() { // Even if the recommended value is FALSE, there might be some deploy // workflows that assume cache clearing. variable_set('restful_clear_on_cc_all', TRUE); } ================================================ FILE: restful.module ================================================ getResourceManager() ->getPlugins(); foreach ($plugins->getIterator() as $plugin) { if (!$plugin instanceof ResourceInterface) { // If the plugin is disabled $plugin gets set to NULL. If that is the case // do not set any menu values based on it. continue; } $plugin_definition = $plugin->getPluginDefinition(); if (!$plugin_definition['hookMenu']) { // Plugin explicitly declared no hook menu should be created automatically // for it. continue; } $item = array( 'title' => $plugin_definition['name'], 'access callback' => RestfulManager::FRONT_CONTROLLER_ACCESS_CALLBACK, 'access arguments' => array($plugin_definition['resource']), 'page callback' => RestfulManager::FRONT_CONTROLLER_CALLBACK, 'page arguments' => array($plugin_definition['resource']), 'delivery callback' => 'restful_delivery', 'type' => MENU_CALLBACK, ); // If there is no specific menu item allow the different version variations. if (!isset($plugin_definition['menuItem'])) { // Add the version string to the arguments. $item['access arguments'][] = 1; $item['page arguments'][] = 1; // Ex: api/v1.2/articles $items[$base_path . '/v' . $plugin_definition['majorVersion'] . '.' . $plugin_definition['minorVersion'] . '/' . $plugin_definition['resource']] = $item; // Ex: api/v1/articles will use the latest minor version. $items[$base_path . '/v' . $plugin_definition['majorVersion'] . '/' . $plugin_definition['resource']] = $item; // Ex: api/articles will use the header or the latest version. // Do not add the version string to the arguments. $item['access arguments'] = $item['page arguments'] = array(1); $items[$base_path . '/' . $plugin_definition['resource']] = $item; } else { $path = implode('/', array($base_path, $plugin_definition['menuItem'])); // Remove trailing slashes that can lead to 404 errors. $path = rtrim($path, '/'); $items[$path] = $item; } } // Make sure the Login endpoint has the correct access callback. if (!empty($items[$base_path . '/login'])) { $items[$base_path . '/login']['access callback'] = 'user_is_anonymous'; } // Add administration page. $items['admin/config/services/restful'] = array( 'title' => 'RESTful', 'description' => 'Administer the RESTful module.', 'page callback' => 'drupal_get_form', 'page arguments' => array('restful_admin_settings'), 'access arguments' => array('administer restful'), 'file' => 'restful.admin.inc', ); $items['admin/config/services/restful/restful'] = $items['admin/config/services/restful']; $items['admin/config/services/restful/restful']['type'] = MENU_DEFAULT_LOCAL_TASK; // Add cache administration page. $items['admin/config/services/restful/cache'] = array( 'title' => 'Cache', 'description' => 'Administer the RESTful module cache system.', 'page callback' => 'drupal_get_form', 'page arguments' => array('restful_admin_cache_settings'), 'access arguments' => array('administer restful'), 'file' => 'restful.cache.inc', 'type' => MENU_LOCAL_TASK, 'weight' => 2, ); return $items; } /** * Implements hook_permission(). */ function restful_permission() { return array( 'administer restful' => array( 'title' => t('Administer the RESTful module'), 'description' => t('Access the administration pages for the RESTful module.'), ), 'administer restful resources' => array( 'title' => t('Administer the resources'), 'description' => t('Perform operations on the resources.'), ), 'restful clear render caches' => array( 'title' => t('Clear RESTful render caches'), 'description' => t('Clear the render caches and their correspoding cache fragments.'), ), ); } /** * Implements hook_help(). */ function restful_help($path, $arg) { switch ($path) { case 'admin/config/services/restful': case 'admin/help#restful': $message = t('This module is managed in GitHub. Please make sure to read the files in the !link folder for more help.', array( '!link' => l(t('Docs'), 'https://github.com/RESTful-Drupal/restful/tree/7.x-2.x/docs'), )); return '

' . $message . '

'; case 'admin/config/services/restful/cache': $message = t('The RESTful module contains several layers of caching for enhanced performance: (1) page cache (aka URL level caching) for anonymous users. This cache is extremely fast, but not very flexible. (2) The render cache can be configured for each resource and allows you to serve cached versions of your records (even to authenticated users!). The render cache also contains smart invalidation, which means that you do not need to have a TTL based cache system. Instead the caches are evicted when automatically when necessary.'); return '

' . $message . '

'; } } /** * Get the RestfulManager. * * Calling restful() from anywhere in the code will give you access to the * RestfulManager. That in turn will provide you access to all the elements * involved. * * @return RestfulManager * The manager. */ function restful() { static $manager; if (!isset($manager)) { $manager = RestfulManager::createFromGlobals(); } return $manager; } /** * Access callback; Determine access for an API call. * * @param string $resource_name * The name of the resource (e.g. "articles"). * * @param string $version_string * The version array. * * @return bool * TRUE if user is allowed to access resource. */ function restful_menu_access_callback($resource_name, $version_string = NULL) { $resource_manager = restful()->getResourceManager(); if (!empty($version_string) && preg_match('/v[0-9]+(\.[0-9]+)?/', $version_string)) { $version_string = substr($version_string, 1); $parsed_versions = explode('.', $version_string); if (count($parsed_versions) == 2) { // If there is only the major we need to get the version from the request, // to get the latest version within the major version. $versions = $parsed_versions; } } if (empty($versions) && !$versions = $resource_manager->getVersionFromRequest()) { // No version could be found. return FALSE; } try { $instance_id = $resource_name . PluginBase::DERIVATIVE_SEPARATOR . implode('.', $versions); $resource = $resource_manager->getPlugin($instance_id, restful()->getRequest()); if (!$resource) { // Throw a PluginNotFoundException exception instead of a denied access. throw new PluginNotFoundException($instance_id); } return $resource->access(); } catch (RestfulException $e) { // We can get here if the request method is not valid or if no resource can // be negotiated. $response = restful()->getResponse(); $output = _restful_build_http_api_error($e, $response); $response->setStatusCode($e->getCode()); $response->setContent(drupal_json_encode($output)); $response->send(); exit(); } catch (PluginNotFoundException $e) { restful_delivery(MENU_NOT_FOUND); exit(); } } /** * Page callback; Return the response for an API call. * * @param string $resource_name * The name of the resource (e.g. "articles"). * @param string $version * The version, prefixed with v (e.g. v1, v2.2). * * @throws \Drupal\restful\Exception\ServiceUnavailableException * * @return string * JSON output with the result of the API call. * * @see http://tools.ietf.org/html/draft-nottingham-http-problem-06 */ function restful_menu_process_callback($resource_name, $version = NULL) { $path = func_get_args(); array_shift($path); if (preg_match('/^v\d+(\.\d+)?$/', $version)) { array_shift($path); } $resource_manager = restful()->getResourceManager(); list($major_version, $minor_version) = $resource_manager->getVersionFromRequest(); $request = restful()->getRequest(); $request->setViaRouter(TRUE); $resource = $resource_manager->getPlugin($resource_name . PluginBase::DERIVATIVE_SEPARATOR . $major_version . '.' . $minor_version, $request); $response_headers = restful() ->getResponse() ->getHeaders(); $version_array = $resource->getVersion(); $version_string = 'v' . $version_array['major'] . '.' . $version_array['minor']; $response_headers->add(HttpHeader::create('X-API-Version', $version_string)); // Vary the response with the presence of the X-API-Version or Accept headers. $vary = $request ->getHeaders() ->get('Vary') ->getValueString() ?: ''; $additional_variations = array($vary, 'Accept'); if ($x_api_version = $request ->getHeaders() ->get('X-API-Version') ->getValueString()) { $additional_variations[] = 'X-API-Version'; } if ($additional_variations) { $response_headers->append(HttpHeader::create('Vary', implode(',', $additional_variations))); } // Always add the allow origin if configured. $plugin_definition = $resource->getPluginDefinition(); if (!empty($plugin_definition['allowOrigin'])) { $response_headers->append(HttpHeader::create('Access-Control-Allow-Origin', $plugin_definition['allowOrigin'])); } try { $resource->setPath(implode('/', $path)); $result = $resource->process(); } catch (RestfulException $e) { $result = _restful_build_http_api_error($e); } catch (Exception $e) { $result = array( 'type' => 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1', 'title' => $e->getMessage(), 'status' => 500, ); } // If the user was switched during the execution thread, then switch it back. $resource->switchUserBack(); return $result; } /** * Returns data in JSON format. * * We do not use drupal_json_output(), in order to maintain the "Content-Type" * header. * * @param mixed $var * (optional) If set, the variable will be converted to JSON and output. * * @see restful_menu_process_callback() */ function restful_delivery($var = NULL) { if (!isset($var)) { return; } $response = _restful_delivery_fill_reponse($var, restful()->getResponse()); $response->send(); } /** * Gets the response object to be send. * * @param mixed $var * If set, the variable will be converted to JSON and output. * @param ResponseInterface $response * If set, this response will be used to add the response data. * * @return ResponseInterface * A prepared response for sending. */ function _restful_delivery_fill_reponse($var, ResponseInterface $response = NULL) { $request = restful()->getRequest(); $response = $response ? $response : new Response(); if (!empty($var['status'])) { $response->setStatusCode($var['status']); } if (is_int($var)) { _restful_get_data_from_menu_status($var); if (!empty($var['status'])) { $response->setStatusCode($var['status']); } try { // Adhere to the API Problem draft proposal. $formatter_id = variable_get('restful_default_output_formatter', 'json'); // Get the data in the default output format. $data = restful() ->getFormatterManager() ->negotiateFormatter(NULL, $formatter_id) ->format($var); $response->setContent($data); $response->prepare($request); return $response; } catch (RestfulException $e) { // If there is an exception during delivery, just JSON encode this. $output = _restful_build_http_api_error($e, $response); $response->setStatusCode($e->getCode()); $response->setContent(drupal_json_encode($output)); return $response; } } try { // Get the formatter for the current resource. $resource = restful() ->getResourceManager() ->negotiate(); // Get a new formatter manager. $formatter_manager = restful() ->getFormatterManager(); $formatter_manager->setResource($resource); $plugin_definition = $resource->getPluginDefinition(); if ($request->getMethod() == RequestInterface::METHOD_OPTIONS) { // There is no guarantee that other formatters can process the // auto-discovery output correctly. $formatter_name = 'json'; } else { $formatter_name = isset($plugin_definition['formatter']) ? $plugin_definition['formatter'] : NULL; } $output = $formatter_manager->format($var, $formatter_name); $response->setContent($output); } catch (RestfulException $e) { // Handle if the formatter does not exist. $output = _restful_build_http_api_error($e, $response); $response->setStatusCode($e->getCode()); $response->setContent(drupal_json_encode($output)); return $response; } $response->prepare($request); return $response; } /** * Convert a menu status response to a valid JSON. * * @param int $var * The integer value of the menu status, passed by reference. */ function _restful_get_data_from_menu_status(&$var) { switch ($var) { case MENU_NOT_FOUND: $class_name = '\Drupal\restful\Exception\NotFoundException'; $message = 'Invalid URL path.'; break; case MENU_ACCESS_DENIED: $class_name = '\Drupal\restful\Exception\ForbiddenException'; $message = 'Access denied.'; break; case MENU_SITE_OFFLINE: $class_name = '\Drupal\restful\Exception\ServiceUnavailableException'; $message = 'Site is offline.'; break; default: $class_name = '\Drupal\restful\Exception\RestfulException'; $message = 'Unknown exception'; } $var = _restful_build_http_api_error(new $class_name($message)); } /** * Helper function to build the structured array for the error output. * * @param RestfulException $exception * The exception. * @param ResponseInterface $response * The response object to alter. * * @return array * The structured output. */ function _restful_build_http_api_error(RestfulException $exception, ResponseInterface $response = NULL) { $response = $response ?: restful()->getResponse(); // Adhere to the API Problem draft proposal. $exception->setHeader('Content-Type', 'application/problem+json; charset=utf-8'); $result = array( 'type' => $exception->getType(), 'title' => $exception->getMessage(), 'status' => $exception->getCode(), 'detail' => $exception->getDescription(), ); if ($instance = $exception->getInstance()) { $result['instance'] = $instance; } if ($errors = $exception->getFieldErrors()) { $result['errors'] = $errors; } $headers = $response->getHeaders(); foreach ($exception->getHeaders() as $header_name => $header_value) { $headers->add(HttpHeader::create($header_name, $header_value)); } drupal_page_is_cacheable(FALSE); // Add a log entry with the error / warning. if ($exception->getCode() < 500) { // Even though it's an exception, it's in fact not a server error - it // might be just access denied, or a bad request, so we just want to log // it, but without marking it as an actual exception. watchdog('restful', $exception->getMessage()); } else { watchdog_exception('restful', $exception); } return $result; } /** * Implements hook_page_delivery_callback_alter(). * * Hijack api/* to be under RESTful. We make sure that any call to api/* pages * that isn't valid, will still return with a well formatted error, instead of * a 404 HTML page. */ function restful_page_delivery_callback_alter(&$callback) { if (!variable_get('restful_hijack_api_pages', TRUE)) { return; } $base_path = variable_get('restful_hook_menu_base_path', 'api'); if (strpos($_GET['q'], $base_path . '/') !== 0 && $_GET['q'] != $base_path) { // Page doesn't start with the base path (e.g. "api" or "api/"). return; } if (menu_get_item()) { // Path is valid (i.e. not 404). return; } $callback = 'restful_deliver_menu_not_found'; } /** * Delivers a not found (404) error. */ function restful_deliver_menu_not_found($page_callback_result) { restful_delivery(MENU_NOT_FOUND); } /** * Implements hook_cron(). */ function restful_cron() { \Drupal\restful\RateLimit\RateLimitManager::deleteExpired(); } /** * Page callback: returns a session token for the currently active user. */ function restful_csrf_session_token() { return array('X-CSRF-Token' => drupal_get_token(\Drupal\restful\Plugin\authentication\Authentication::TOKEN_VALUE)); } /** * Element validate \DateTime format function. */ function restful_date_time_format_element_validate($element, &$form_state) { $value = $element['#value']; try { new \DateInterval($value); } catch (\Exception $e) { form_error($element, t('%name must be compatible with the !link.', array( '%name' => $element['#title'], '!link' => l(t('\DateInterval format'), 'http://php.net/manual/en/class.dateinterval.php'), ))); } } /** * Implements hook_restful_resource_alter(). * * Decorate an existing resource with other services (e.g. rate limit and render * cache). */ function restful_restful_resource_alter(ResourceInterface &$resource) { // Disable any plugin in the disabled plugins variable. $disabled_plugins = array( // Disable the Files Upload resource based on the settings variable. 'files_upload:1.0' => (bool) !variable_get('restful_file_upload', FALSE), // Disable the Users resources based on the settings variable. 'users:1.0' => (bool) !variable_get('restful_enable_users_resource', TRUE), // Disable the Login Cookie resources based on the settings variable. 'login_cookie:1.0' => (bool) !variable_get('restful_enable_user_login_resource', TRUE), // Disable the Discovery resource based on the settings variable. 'discovery:1.0' => (bool) !variable_get('restful_enable_discovery_resource', TRUE), ) + variable_get('restful_disabled_plugins', array()); if (!empty($disabled_plugins[$resource->getResourceName()])) { $resource->disable(); } elseif ( isset($disabled_plugins[$resource->getResourceName()]) && $disabled_plugins[$resource->getResourceName()] === FALSE && !$resource->isEnabled() ) { $resource->enable(); } $plugin_definition = $resource->getPluginDefinition(); // If render cache is enabled for the current resource, or there is no render // cache information for the resource but render cache is enabled globally, // then decorate the resource with cache capabilities. if ( !empty($plugin_definition['renderCache']['render']) || (!isset($plugin_definition['renderCache']['render']) && variable_get('restful_render_cache', FALSE)) ) { $resource = new CacheDecoratedResource($resource); } // Check for the rate limit configuration. if (!empty($plugin_definition['rateLimit']) || variable_get('restful_global_rate_limit', 0)) { $resource = new RateLimitDecoratedResource($resource); } // Disable the discovery endpoint if it's disabled. if ( $resource->getResourceMachineName() == 'discovery' && !variable_get('restful_enable_discovery_resource', TRUE) && $resource->isEnabled() ) { $resource->disable(); } } ================================================ FILE: src/Annotation/Authentication.php ================================================ 'foo', * 'operator' => 'STARTS_WITH', * * @var array */ public $autocomplete = array(); /** * Access control using the HTTP Access-Control-Allow-Origin header. * * @var string */ public $allowOrigin; /** * Determines if a resource should be discoverable, and appear under /api. * * @var bool */ public $discoverable = TRUE; /** * URL parameters. * * @var array */ public $urlParams = array(); /** * {@inheritdoc} */ public function getId() { // The ID of the resource plugin is its name. return $this->definition['name']; } } ================================================ FILE: src/Authentication/AuthenticationManager.php ================================================ plugins = new AuthenticationPluginCollection($manager ?: AuthenticationPluginManager::create()); $this->userSessionState = $user_session_state ?: new UserSessionState(); } /** * {@inheritdoc} */ public function setIsOptional($is_optional) { $this->isOptional = $is_optional; } /** * {@inheritdoc} */ public function getIsOptional() { return $this->isOptional; } /** * {@inheritdoc} */ public function addAuthenticationProvider($plugin_id) { $manager = AuthenticationPluginManager::create(); $instance = $manager->createInstance($plugin_id); // The get method will instantiate a plugin if not there. $this->plugins->setInstanceConfiguration($plugin_id, $manager->getDefinition($plugin_id)); $this->plugins->set($plugin_id, $instance); } /** * {@inheritdoc} */ public function addAllAuthenticationProviders() { $manager = AuthenticationPluginManager::create(); foreach ($manager->getDefinitions() as $id => $plugin) { $this->addAuthenticationProvider($id); } } /** * {@inheritdoc} */ public function getAccount(RequestInterface $request, $cache = TRUE) { global $user; // Return the previously resolved user, if any. if (!empty($this->account)) { return $this->account; } // Resolve the user based on the providers in the manager. $account = NULL; foreach ($this->plugins as $provider) { /* @var \Drupal\restful\Plugin\authentication\AuthenticationInterface $provider */ if ($provider->applies($request) && ($account = $provider->authenticate($request)) && $account->uid && $account->status) { // The account has been loaded, we can stop looking. break; } } if (empty($account->uid) || !$account->status) { if (RestfulManager::isRestfulPath($request) && $this->plugins->count() && !$this->getIsOptional()) { // Allow caching pages for anonymous users. drupal_page_is_cacheable(variable_get('restful_page_cache', FALSE)); // User didn't authenticate against any provider, so we throw an error. throw new UnauthorizedException('Bad credentials. Anonymous user resolved for a resource that requires authentication.'); } // If the account could not be authenticated default to the global user. // Most of the cases the cookie provider will do this for us. $account = drupal_anonymous_user(); if (!$request->isViaRouter()) { // If we are using the API from within Drupal and we have not tried to // authenticate using the 'cookie' provider, then we expect to be logged // in using the cookie authentication as a last resort. $account = $user->uid ? user_load($user->uid) : $account; } } if ($cache) { $this->setAccount($account); } // Disable page caching for security reasons so that an authenticated user // response never gets into the page cache for anonymous users. // This is necessary because the page cache system only looks at session // cookies, but not at HTTP Basic Auth headers. drupal_page_is_cacheable(!$account->uid && variable_get('restful_page_cache', FALSE)); // Record the access time of this request. $this->setAccessTime($account); return $account; } /** * {@inheritdoc} */ public function setAccount($account) { $this->account = $account; if (!empty($account->uid)) { $this->userSessionState->switchUser($account); } } /** * {@inheritdoc} */ public function switchUserBack() { return $this->userSessionState->switchUserBack(); } /** * {@inheritdoc} */ public function getPlugins() { return $this->plugins; } /** * {@inheritdoc} */ public function getPlugin($instance_id) { return $this->plugins->get($instance_id); } /** * Set the user's last access time. * * @param object $account * A user account. * * @see _drupal_session_write() */ protected function setAccessTime($account) { // This logic is pulled directly from _drupal_session_write(). if ($account->uid && REQUEST_TIME - $account->access > variable_get('session_write_interval', 180)) { db_update('users')->fields(array( 'access' => REQUEST_TIME, ))->condition('uid', $account->uid)->execute(); } } } ================================================ FILE: src/Authentication/AuthenticationManagerInterface.php ================================================ originalUser && !$this->needsSaving) { // This is the first time a user switched, and there isn't an original // user session. $this->needsSaving = drupal_save_session(); $this->originalUser = $user; // Don't allow a session to be saved. Provider that require a session to // be saved, like the cookie provider, need to explicitly set // drupal_save_session(TRUE). // @see LoginCookie__1_0::loginUser(). drupal_save_session(FALSE); } // Set the global user. $user = $account; } /** * Switch the user to the authenticated user, and back. * * This should be called only for an API call. It should not be used for calls * via the menu system, as it might be a login request, so we avoid switching * back to the anonymous user. */ public function switchUserBack() { global $user; if (!$this->originalUser) { return; } $user = $this->originalUser; drupal_save_session($this->needsSaving); $this->reset(); } /** * Reset the initial values. */ protected function reset() { // Reset initial values. static::$isSwitched = FALSE; $this->originalUser = NULL; $this->needsSaving = FALSE; } } ================================================ FILE: src/Authentication/UserSessionStateInterface.php ================================================ message = $show_access_denied ? $message : static::ERROR_404_MESSAGE; $this->code = $show_access_denied ? 403 : 404; $this->instance = $show_access_denied ? 'help/restful/problem-instances-forbidden' : 'help/restful/problem-instances-not-found'; } } ================================================ FILE: src/Exception/IncompatibleFieldDefinitionException.php ================================================ description ? $this->description : Response::$statusTexts[$this->getCode()]; } /** * Return a string to the common problem type. * * @return string * URL pointing to the specific RFC-2616 section. */ public function getType() { // Depending on the error code we'll return a different URL. $url = 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html'; $sections = array( '100' => '#sec10.1.1', '101' => '#sec10.1.2', '200' => '#sec10.2.1', '201' => '#sec10.2.2', '202' => '#sec10.2.3', '203' => '#sec10.2.4', '204' => '#sec10.2.5', '205' => '#sec10.2.6', '206' => '#sec10.2.7', '300' => '#sec10.3.1', '301' => '#sec10.3.2', '302' => '#sec10.3.3', '303' => '#sec10.3.4', '304' => '#sec10.3.5', '305' => '#sec10.3.6', '307' => '#sec10.3.8', '400' => '#sec10.4.1', '401' => '#sec10.4.2', '402' => '#sec10.4.3', '403' => '#sec10.4.4', '404' => '#sec10.4.5', '405' => '#sec10.4.6', '406' => '#sec10.4.7', '407' => '#sec10.4.8', '408' => '#sec10.4.9', '409' => '#sec10.4.10', '410' => '#sec10.4.11', '411' => '#sec10.4.12', '412' => '#sec10.4.13', '413' => '#sec10.4.14', '414' => '#sec10.4.15', '415' => '#sec10.4.16', '416' => '#sec10.4.17', '417' => '#sec10.4.18', '500' => '#sec10.5.1', '501' => '#sec10.5.2', '502' => '#sec10.5.3', '503' => '#sec10.5.4', '504' => '#sec10.5.5', '505' => '#sec10.5.6', ); return empty($sections[$this->getCode()]) ? $url : $url . $sections[$this->getCode()]; } /** * Get the URL to the error for the particular case. * * @return string * The url or NULL if empty. */ public function getInstance() { // Handle all instances using the advanced help module. if (!module_exists('advanced_help') || empty($this->instance)) { return NULL; } return url($this->instance, array( 'absolute' => TRUE, )); } /** * Return an array with all the errors. */ public function getFieldErrors() { return $this->fieldErrors; } /** * Add an error per field. * * @param string $field_name * The field name. * @param string $error_message * The error message. */ public function addFieldError($field_name, $error_message) { $this->fieldErrors[$field_name][] = $error_message; } /** * Get the associative array of headers. * * @return array * The associated headers to the error exception. */ public function getHeaders() { return $this->headers; } /** * Set a header. * * @param string $key * The header name. * @param string $value * The header value. */ public function setHeader($key, $value) { $this->headers[$key] = $value; } } ================================================ FILE: src/Exception/ServerConfigurationException.php ================================================ resource = $resource; $manager = FormatterPluginManager::create(); $options = array(); foreach ($manager->getDefinitions() as $plugin_id => $plugin_definition) { // Since there is only one instance per plugin_id use the plugin_id as // instance_id. $options[$plugin_id] = $plugin_definition; } $this->plugins = new FormatterPluginCollection($manager, $options); } /** * {@inheritdoc} */ public function setResource($resource) { $this->resource = $resource; } /** * {@inheritdoc} */ public function format(array $data, $formatter_name = NULL) { return $this->processData('format', $data, $formatter_name); } /** * {@inheritdoc} */ public function render(array $data, $formatter_name = NULL) { return $this->processData('render', $data, $formatter_name); } /** * {@inheritdoc} */ public function negotiateFormatter($accept, $formatter_name = NULL) { $message = 'Formatter plugin class was not found.'; $default_formatter_name = variable_get('restful_default_output_formatter', 'json'); try { if ($formatter_name) { return $this->getPluginByName($formatter_name); } // Sometimes we will get a default Accept: */* in that case we want to // return the default content type and not just any. if (empty($accept) || $accept == '*/*') { // Return the default formatter. return $this->getPluginByName($default_formatter_name); } foreach (explode(',', $accept) as $accepted_content_type) { // Loop through all the formatters and find the first one that matches // the Content-Type header. $accepted_content_type = trim($accepted_content_type); if (strpos($accepted_content_type, '*/*') === 0) { return $this->getPluginByName($default_formatter_name); } foreach ($this->plugins as $formatter_name => $formatter) { /* @var FormatterInterface $formatter */ if (static::matchContentType($formatter->getContentTypeHeader(), $accepted_content_type)) { $formatter->setConfiguration(array( 'resource' => $this->resource, )); return $formatter; } } } } catch (PluginNotFoundException $e) { // Catch the exception and throw one of our own. $message = $e->getMessage(); } throw new ServiceUnavailableException($message); } /** * Helper function to get a formatter and apply a method. * * @param string $method * A valid method to call on the FormatterInterface object. * @param array $data * The array of data to process. * @param string $formatter_name * The name of the formatter for the current resource. Leave it NULL to use * the Accept headers. * * @return string * The processed output. */ protected function processData($method, array $data, $formatter_name = NULL) { if ($resource = $this->resource) { $request = $resource->getRequest(); } else { $request = restful()->getRequest(); } $accept = $request ->getHeaders() ->get('accept') ->getValueString(); $formatter = $this->negotiateFormatter($accept, $formatter_name); $output = ResourceManager::executeCallback(array($formatter, $method), array($data, $formatter_name)); // The content type header is modified after the massaging if there is // an error code. Therefore we need to set the content type header after // formatting the output. $content_type = $formatter->getContentTypeHeader(); $response_headers = restful() ->getResponse() ->getHeaders(); $response_headers->add(HttpHeader::create('Content-Type', $content_type)); return $output; } /** * Matches a string with path style wildcards. * * @param string $content_type * The string to check. * @param string $pattern * The pattern to check against. * * @return bool * TRUE if the input matches the pattern. * * @see drupal_match_path(). */ protected static function matchContentType($content_type, $pattern) { $regexps = &drupal_static(__METHOD__); if (!isset($regexps[$pattern])) { // Convert path settings to a regular expression. $to_replace = array( '/\\\\\*/', // asterisks ); $replacements = array( '.*', ); $patterns_quoted = preg_quote($pattern, '/'); // This will turn 'application/*' into '/^(application\/.*)(;.*)$/' // allowing us to match 'application/json; charset: utf8' $regexps[$pattern] = '/^(' . preg_replace($to_replace, $replacements, $patterns_quoted) . ')(;.*)?$/i'; } return (bool) preg_match($regexps[$pattern], $content_type); } /** * {@inheritdocs} */ public function getPlugins() { return $this->plugins; } /** * {@inheritdocs} */ public function getPlugin($instance_id) { return $this->plugins->get($instance_id); } /** * Gets a plugin by name initializing the resource. * * @param string $name * The formatter name. * * @return FormatterInterface * The plugin. */ protected function getPluginByName($name) { /* @var FormatterInterface $formatter */ $formatter = $this->plugins->get($name); if ($this->resource) { $formatter->setResource($this->resource); } return $formatter; } } ================================================ FILE: src/Formatter/FormatterManagerInterface.php ================================================ name = $name; $this->id = static::generateId($name); $this->values = $values; $this->extras = $extras; } /** * {@inheritdoc} */ public static function create($key, $value) { list($extras, $values) = self::parseHeaderValue($value); return new static($key, $values, $extras); } /** * {@inheritdoc} */ public function get() { return $this->values; } /** * {@inheritdoc} */ public function getValueString() { $parts = array(); $parts[] = implode(', ', $this->values); $parts[] = $this->extras; return implode('; ', array_filter($parts)); } /** * {@inheritdoc} */ public function getName() { return $this->name; } /** * Returns the string version of the header. * * @return string */ public function __toString() { return $this->name . ': ' . $this->getValueString(); } /** * {@inheritdoc} */ public function set($values) { $this->values = $values; } /** * {@inheritdoc} */ public function append($value) { // Ignore the extras. list(, $values) = static::parseHeaderValue($value); foreach ($values as $value) { $this->values[] = $value; } } /** * {@inheritdoc} */ public function getId() { return $this->id; } /** * {@inheritdoc} */ public static function generateId($name) { return strtolower($name); } /** * Parses the values and extras from a header value string. * * @param string $value * * @return array * The $extras and $values. */ protected static function parseHeaderValue($value) { $extras = NULL; $parts = explode(';', $value); if (count($parts) > 1) { // Only consider the last element. $extras = array_pop($parts); $extras = trim($extras); // In case there were more than one ';' then put everything back. $value = implode(';', $parts); } $values = array_map('trim', explode(',', $value)); return array($extras, $values); } } ================================================ FILE: src/Http/HttpHeaderBag.php ================================================ $value) { $header = HttpHeader::create($key, $value); $this->values[$header->getId()] = $header; } } /** * Returns the header bag as a string. * * @return string * The string representation. */ public function __toString() { $headers = array(); foreach ($this->values as $key => $header) { /* @var HttpHeader $header */ $headers[] = $header->__toString(); } return implode("\r\n", $headers); } /** * {@inheritdoc} */ public function get($key) { // Assume that $key is an ID. if (array_key_exists($key, $this->values)) { return $this->values[$key]; } // Test if key was a header name. $key = HttpHeader::generateId($key); if (array_key_exists($key, $this->values)) { return $this->values[$key]; } // Return a NULL object on which you can still call the HttpHeaderInterface // methods. return HttpHeaderNull::create(NULL, NULL); } /** * {@inheritdoc} */ public function has($key) { return !empty($this->values[$key]); } /** * {@inheritdoc} */ public function getValues() { return $this->values; } /** * {@inheritdoc} */ public function add(HttpHeaderInterface $header) { $this->values[$header->getId()] = $header; } /** * {@inheritdoc} */ public function append(HttpHeaderInterface $header) { if (!$this->has($header->getId())) { $this->add($header); return; } $existing_header = $this->get($header->getId()); // Append all the values in the passed in header to the existing header // values. foreach ($header->get() as $value) { $existing_header->append($value); } } /** * {@inheritdoc} */ public function remove($key) { // Assume that $key is an ID. if (!array_key_exists($key, $this->values)) { // Test if key was a header name. $key = HttpHeader::generateId($key); if (!array_key_exists($key, $this->values)) { return; } } unset($this->values[$key]); } /** * {@inheritdoc} */ public function current() { return current($this->values); } /** * {@inheritdoc} */ public function next() { return next($this->values); } /** * {@inheritdoc} */ public function key() { return key($this->values); } /** * {@inheritdoc} */ public function valid() { $key = key($this->values); return $key !== NULL && $key !== FALSE; } /** * {@inheritdoc} */ public function rewind() { return reset($this->values); } } ================================================ FILE: src/Http/HttpHeaderBagInterface.php ================================================ values; } /** * {@inheritdoc} */ public function getValueString() { return NULL; } /** * {@inheritdoc} */ public function getName() { return NULL; } /** * Returns the string version of the header. * * @return string */ public function __toString() { return NULL; } /** * {@inheritdoc} */ public function set($values) {} /** * {@inheritdoc} */ public function append($value) {} /** * {@inheritdoc} */ public function getId() { return NULL; } /** * {@inheritdoc} */ public static function generateId($name) { return NULL; } } ================================================ FILE: src/Http/Request.php ================================================ 'X_FORWARDED_FOR', self::HEADER_CLIENT_HOST => 'X_FORWARDED_HOST', self::HEADER_CLIENT_PROTO => 'X_FORWARDED_PROTO', self::HEADER_CLIENT_PORT => 'X_FORWARDED_PORT', ); protected static $trustedProxies = array(); /** * HTTP Method. * * @var string */ protected $method; /** * URI (path and query string). * * @var string */ protected $uri; /** * Path * * @var string */ protected $path; /** * Query parameters. * * @var array */ protected $query; /** * The input HTTP headers. * * @var HttpHeaderBag */ protected $headers; /** * The unprocessed body of the request. * * This should be a PHP stream, but let's keep it simple. * * @var string */ protected $body; /** * Indicates if the request was routed by the menu system. * * @var bool */ protected $viaRouter = FALSE; /** * The passed in CSRF token in the corresponding header. * * @var string */ protected $csrfToken = NULL; /** * Cookies for the request. * * @var array */ protected $cookies = array(); /** * Files attached to the request. * * @var array */ protected $files = array(); /** * Server information. * * @var array */ protected $server = array(); /** * Holds the parsed body. * * @var array */ private $parsedBody; /** * Holds the parsed input via URL. * * @internal * @var \ArrayObject */ private $parsedInput; /** * Store application data as part of the request. * * @var array */ protected $applicationData = array(); /** * Constructor. * * Parses the URL and the query params. It also uses input:// to get the body. */ public function __construct($path, array $query, $method = 'GET', HttpHeaderBag $headers, $via_router = FALSE, $csrf_token = NULL, array $cookies = array(), array $files = array(), array $server = array(), $parsed_body = NULL) { $this->path = $path; $this->query = !isset($query) ? static::parseInput() : $query; $this->query = $this->fixQueryFields($this->query); // If the method is empty, fall back to GET. $this->method = $method ?: static::METHOD_GET; $this->headers = $headers; $this->viaRouter = $via_router; $this->csrfToken = $csrf_token; $this->cookies = $cookies; $this->files = $files; $this->server = $server; $this->parsedBody = $parsed_body; // Allow implementing modules to alter the request. drupal_alter('restful_parse_request', $this); } /** * {@inheritdoc} */ public static function create($path, array $query = array(), $method = 'GET', HttpHeaderBag $headers = NULL, $via_router = FALSE, $csrf_token = NULL, array $cookies = array(), array $files = array(), array $server = array(), $parsed_body = NULL) { if (!$headers) { $headers = new HttpHeaderBag(); } if (($overridden_method = strtoupper($headers->get('x-http-method-override')->getValueString())) && ($method == static::METHOD_POST)) { if (!static::isValidMethod($overridden_method)) { throw new BadRequestException(sprintf('Invalid overridden method: %s.', $overridden_method)); } $method = $overridden_method; } return new static($path, $query, $method, $headers, $via_router, $csrf_token, $cookies, $files, $server, $parsed_body); } /** * {@inheritdoc} */ public static function createFromGlobals() { $path = implode('/', arg()); $query = drupal_get_query_parameters(); $method = strtoupper($_SERVER['REQUEST_METHOD']); // This flag is used to identify if the request is done "via Drupal" or "via // CURL"; $via_router = TRUE; $headers = static::parseHeadersFromGlobals(); $csrf_token = $headers->get('x-csrf-token')->getValueString(); return static::create($path, $query, $method, $headers, $via_router, $csrf_token, $_COOKIE, $_FILES, $_SERVER); } /** * {@inheritdoc} */ public static function isWriteMethod($method) { $method = strtoupper($method); return in_array($method, array( static::METHOD_PUT, static::METHOD_POST, static::METHOD_PATCH, static::METHOD_DELETE, )); } /** * {@inheritdoc} */ public static function isReadMethod($method) { $method = strtoupper($method); return in_array($method, array( static::METHOD_GET, static::METHOD_HEAD, static::METHOD_OPTIONS, static::METHOD_TRACE, static::METHOD_CONNECT, )); } /** * {@inheritdoc} */ public static function isValidMethod($method) { $method = strtolower($method); return static::isReadMethod($method) || static::isWriteMethod($method); } /** * {@inheritdoc} */ public function isListRequest($resource_path) { if ($this->method != static::METHOD_GET) { return FALSE; } return empty($resource_path) || strpos($resource_path, ',') !== FALSE; } /** * {@inheritdoc} */ public function getParsedBody() { if ($this->parsedBody) { return $this->parsedBody; } // Find out the body format and parse it into the \ArrayObject. $this->parsedBody = $this->parseBody($this->method); return $this->parsedBody; } /** * {@inheritdoc} */ public function getParsedInput() { if (isset($this->parsedInput)) { return $this->parsedInput; } // Get the input data provided via URL. $this->parsedInput = $this->query; unset($this->parsedInput['q']); return $this->parsedInput; } /** * {@inheritdoc} */ public function getPagerInput() { $input = $this->getParsedInput(); if (!isset($input['page'])) { $page = array('number' => 1); } else { $page = $input['page']; if (!is_array($page)) { $page = array('number' => $page); } } if (isset($input['range'])) { $page['size'] = $input['range']; } return $page + array('number' => 1); } /** * {@inheritdoc} */ public function setParsedInput(array $input) { $this->parsedInput = $input; } /** * Parses the body. * * @param string $method * The HTTP method. * * @return array * The parsed body. */ protected function parseBody($method) { if (!static::isWriteMethod($method)) { return NULL; } $content_type = $this ->getHeaders() ->get('Content-Type') ->get(); $content_type = reset($content_type); $content_type = $content_type ?: 'application/x-www-form-urlencoded'; return static::parseBodyContentType($content_type); } /** * Parses the provided payload according to a content type. * * @param string $content_type * The contents of the Content-Type header. * * @return array * The parsed body. * * @throws \Drupal\restful\Exception\BadRequestException */ protected static function parseBodyContentType($content_type) { if (!$input_string = file_get_contents('php://input')) { return NULL; } if ($content_type == 'application/x-www-form-urlencoded') { $body = NULL; parse_str($input_string, $body); return $body; } // Use the Content Type header to negotiate a formatter to parse the body. $formatter = restful() ->getFormatterManager() ->negotiateFormatter($content_type); return $formatter->parseBody($input_string); } /** * Parses the input data. * * @return array * The parsed input. */ protected static function parseInput() { return $_GET; } /** * Helps fixing the fields to ensure that dot-notation makes sense. * * Make sure to add all of the parents for the dot-notation sparse * fieldsets. fields=active,image.category.name,image.description becomes * fields=active,image,image.category,image.category.name,image.description * * @param array $input * The parsed input to fix. * * @return array * The parsed input array. */ protected function fixQueryFields(array $input) { // Make sure that we include all the parents for full linkage. foreach (array('fields', 'include') as $key_name) { if (empty($input[$key_name])) { continue; } $added_keys = array(); foreach (explode(',', $input[$key_name]) as $key) { $parts = explode('.', $key); for ($index = 0; $index < count($parts); $index++) { $path = implode('.', array_slice($parts, 0, $index + 1)); $added_keys[$path] = TRUE; } } $input[$key_name] = implode(',', array_keys(array_filter($added_keys))) ?: NULL; } return $input; } /** * Parses the header names and values from globals. * * @return HttpHeaderBag * The headers. */ protected static function parseHeadersFromGlobals() { $bag = new HttpHeaderBag(); $headers = array(); if (function_exists('apache_request_headers')) { $headers = apache_request_headers(); } else { $content_header_keys = array('CONTENT_TYPE', 'CONTENT_LENGTH'); foreach ($_SERVER as $key => $value) { if (strpos($key, 'HTTP_') === 0 || in_array($key, $content_header_keys)) { // Generate the plausible header name based on the $name. // Converts 'HTTP_X_FORWARDED_FOR' to 'X-Forwarded-For' $name = preg_replace('/^HTTP_/', '', $key); $parts = explode('_', $name); $parts = array_map('strtolower', $parts); $parts = array_map('ucfirst', $parts); $name = implode('-', $parts); $headers[$name] = $value; } } } // Iterate over the headers and bag them. foreach ($headers as $name => $value) { $bag->add(HttpHeader::create($name, $value)); } return $bag; } /** * {@inheritdoc} */ public function getPath($strip = TRUE) { // Remove the restful prefix from the beginning of the path. if ($strip && strpos($this->path, variable_get('restful_hook_menu_base_path', 'api')) !== FALSE) { return substr($this->path, strlen(variable_get('restful_hook_menu_base_path', 'api')) + 1); } return $this->path; } /** * {@inheritdoc} */ public function href() { return url($this->path, array( 'absolute' => TRUE, 'query' => $this->query, )); } /** * {@inheritdoc} */ public function getHeaders() { return $this->headers; } /** * Get the credentials based on the $_SERVER variables. * * @return array * A numeric array with the username and password. */ protected static function getCredentials() { $username = empty($_SERVER['PHP_AUTH_USER']) ? NULL : $_SERVER['PHP_AUTH_USER']; $password = empty($_SERVER['PHP_AUTH_PW']) ? NULL : $_SERVER['PHP_AUTH_PW']; // Try to fill PHP_AUTH_USER & PHP_AUTH_PW with REDIRECT_HTTP_AUTHORIZATION // for compatibility with Apache PHP CGI/FastCGI. // This requires the following line in your ".htaccess"-File: // RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] $authorization_header = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] : NULL; $authorization_header = $authorization_header ?: (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) ? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] : NULL); if (!empty($authorization_header) && !isset($username) && !isset($password)) { if (!$token = StringHelper::removePrefix('Basic ', $authorization_header)) { return NULL; } $authentication = base64_decode($token); list($username, $password) = explode(':', $authentication); $_SERVER['PHP_AUTH_USER'] = $username; $_SERVER['PHP_AUTH_PW'] = $password; } return array($username, $password); } /** * {@inheritdoc} */ public function getUser() { list($account,) = static::getCredentials(); return $account; } /** * {@inheritdoc} */ public function getPassword() { list(, $password) = static::getCredentials(); return $password; } /** * {@inheritdoc} */ public function getMethod() { return $this->method; } /** * {@inheritdoc} */ public function setMethod($method) { $this->method = strtoupper($method); } /** * {@inheritdoc} */ public function getServer() { return $this->server; } /** * {@inheritdoc} */ public function setApplicationData($key, $value) { $this->applicationData[$key] = $value; } /** * {@inheritdoc} */ public function clearApplicationData() { $this->applicationData = array(); } /** * {@inheritdoc} */ public function getApplicationData($key) { if (!isset($this->applicationData[$key])) { return NULL; } return $this->applicationData[$key]; } /** * {@inheritdoc} */ public function isSecure() { if (self::$trustedProxies && self::$trustedHeaders[self::HEADER_CLIENT_PROTO] && $proto = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PROTO])->getValueString()) { return in_array(strtolower(current(explode(',', $proto))), array('https', 'on', 'ssl', '1')); } $https = $this->server['HTTPS']; return !empty($https) && strtolower($https) !== 'off'; } /** * {@inheritdoc} */ public function getCookies() { return $this->cookies; } /** * {@inheritdoc} */ public function getFiles() { return $this->files; } /** * {@inheritdoc} */ public function getCsrfToken() { return $this->csrfToken; } /** * {@inheritdoc} */ public function isViaRouter() { return $this->viaRouter; } /** * {@inheritdoc} */ public function setViaRouter($via_router) { $this->viaRouter = $via_router; } } ================================================ FILE: src/Http/RequestInterface.php ================================================ 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', // RFC2518 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-Status', // RFC4918 208 => 'Already Reported', // RFC5842 226 => 'IM Used', // RFC3229 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Reserved', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect', // RFC7238 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Requested Range Not Satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', // RFC2324 422 => 'Unprocessable Entity', // RFC4918 423 => 'Locked', // RFC4918 424 => 'Failed Dependency', // RFC4918 425 => 'Reserved for WebDAV advanced collections expired proposal', // RFC2817 426 => 'Upgrade Required', // RFC2817 428 => 'Precondition Required', // RFC6585 429 => 'Too Many Requests', // RFC6585 431 => 'Request Header Fields Too Large', // RFC6585 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported', 506 => 'Variant Also Negotiates (Experimental)', // RFC2295 507 => 'Insufficient Storage', // RFC4918 508 => 'Loop Detected', // RFC5842 510 => 'Not Extended', // RFC2774 511 => 'Network Authentication Required', // RFC6585 ); /** * Constructor. * * @param mixed $content * The response content, see setContent() * @param int $status * The response status code * @param array $headers * An array of response headers * * @throws UnprocessableEntityException * When the HTTP status code is not valid */ public function __construct($content = '', $status = 200, $headers = array()) { $this->headers = new HttpHeaderBag($headers); $this->setContent($content); $this->setStatusCode($status); $this->setProtocolVersion('1.0'); if (!$this->headers->has('Date')) { $this->setDate(new \DateTime(NULL, new \DateTimeZone('UTC'))); } } /** * {@inheritdoc} */ public static function create($content = '', $status = 200, $headers = array()) { return new static($content, $status, $headers); } /** * Returns the Response as an HTTP string. * * The string representation of the Response is the same as the * one that will be sent to the client only if the prepare() method * has been called before. * * @return string * The Response as an HTTP string * * @see prepare() */ public function __toString() { return sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText) . "\r\n" . $this->headers . "\r\n" . $this->getContent(); } /** * Is the response empty? * * @return bool */ protected function isEmpty() { return in_array($this->statusCode, array(204, 304)); } /** * Is response informative? * * @return bool */ protected function isInformational() { return $this->statusCode >= 100 && $this->statusCode < 200; } /** * Is response successful? * * @return bool */ protected function isSuccessful() { return $this->statusCode >= 200 && $this->statusCode < 300; } /** * Is response invalid? * * @return bool */ protected function isInvalid() { return $this->statusCode < 100 || $this->statusCode >= 600; } /** * {@inheritdoc} */ public function prepare(RequestInterface $request) { $headers = $this->headers; if ($this->isInformational() || $this->isEmpty()) { $this->setContent(NULL); $headers->remove('Content-Type'); $headers->remove('Content-Length'); } else { // Content-type based on the Request. The content type should have been // set in the RestfulFormatter. // Fix Content-Type $charset = $this->charset ?: 'UTF-8'; $content_type = $headers->get('Content-Type')->getValueString(); if (stripos($content_type, 'text/') === 0 && stripos($content_type, 'charset') === FALSE) { // add the charset $headers->add(HttpHeader::create('Content-Type', $content_type . '; charset=' . $charset)); } // Fix Content-Length if ($headers->has('Transfer-Encoding')) { $headers->remove('Content-Length'); } if ($request->getMethod() == RequestInterface::METHOD_HEAD) { // cf. RFC2616 14.13 $length = $headers->get('Content-Length')->getValueString(); $this->setContent(NULL); if ($length) { $headers->add(HttpHeader::create('Content-Length', $length)); } } } // Fix protocol $server_info = $request->getServer(); if ($server_info['SERVER_PROTOCOL'] != 'HTTP/1.0') { $this->setProtocolVersion('1.1'); } // Check if we need to send extra expire info headers if ($this->getProtocolVersion() == '1.0' && $this->headers->get('Cache-Control')->getValueString() == 'no-cache') { $this->headers->add(HttpHeader::create('pragma', 'no-cache')); $this->headers->add(HttpHeader::create('expires', -1)); } $this->ensureIEOverSSLCompatibility($request); } /** * {@inheritdoc} */ public function setContent($content) { if ($content !== NULL && !is_string($content) && !is_numeric($content) && !is_callable(array($content, '__toString'))) { throw new InternalServerErrorException(sprintf('The Response content must be a string or object implementing __toString(), "%s" given.', gettype($content))); } $this->content = (string) $content; } /** * {@inheritdoc} */ public function getContent() { return $this->content; } /** * {@inheritdoc} */ public function setProtocolVersion($version) { $this->version = $version; } /** * {@inheritdoc} */ public function getProtocolVersion() { return $this->version; } /** * {@inheritdoc} */ public function send() { $this->sendHeaders(); $this->sendContent(); static::pageFooter(); } /** * Sends HTTP headers. */ protected function sendHeaders() { foreach ($this->headers as $key => $header) { /* @var HttpHeader $header */ drupal_add_http_header($header->getName(), $header->getValueString()); } drupal_add_http_header('Status', $this->getStatusCode()); } /** * Sends content for the current web response. */ protected function sendContent() { echo $this->content; } /** * {@inheritdoc} */ public function setStatusCode($code, $text = NULL) { $this->statusCode = $code = (int) $code; if ($this->isInvalid()) { throw new UnprocessableEntityException(sprintf('The HTTP status code "%s" is not valid.', $code)); } if ($text === NULL) { $this->statusText = isset(self::$statusTexts[$code]) ? self::$statusTexts[$code] : ''; return; } if ($text === FALSE) { $this->statusText = ''; return; } $this->statusText = $text; } /** * {@inheritdoc} */ public function getStatusCode() { return $this->statusCode; } /** * {@inheritdoc} */ public function setCharset($charset) { $this->charset = $charset; } /** * {@inheritdoc} */ public function getCharset() { return $this->charset; } /** * {@inheritdoc} */ public function getHeaders() { return $this->headers; } /** * Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9. * * @link http://support.microsoft.com/kb/323308 */ protected function ensureIEOverSSLCompatibility(Request $request) { $server_info = $request->getServer(); if (stripos($this->headers->get('Content-Disposition')->getValueString(), 'attachment') !== FALSE && preg_match('/MSIE (.*?);/i', $server_info['HTTP_USER_AGENT'], $match) == 1 && $request->isSecure() === TRUE) { if (intval(preg_replace("/(MSIE )(.*?);/", "$2", $match[0])) < 9) { $this->headers->remove('Cache-Control'); } } } /** * {@inheritdoc} */ public function setDate(\DateTime $date) { $date->setTimezone(new \DateTimeZone('UTC')); $this->headers->add(HttpHeader::create('Date', $date->format('D, d M Y H:i:s') . ' GMT')); } /** * Performs end-of-request tasks. * * This function sets the page cache if appropriate, and allows modules to * react to the closing of the page by calling hook_exit(). * * This is just a wrapper around drupal_page_footer() so extending classes can * override this method if necessary. * * @see drupal_page_footer(). */ protected static function pageFooter() { drupal_page_footer(); } } ================================================ FILE: src/Http/ResponseInterface.php ================================================ '', // A description of the plugin. 'description' => '', 'settings' => array(), 'id' => '', ); /** * Constructs AuthenticationPluginManager. * * @param \Traversable $namespaces * An object that implements \Traversable which contains the root paths * keyed by the corresponding namespace to look for plugin implementations. * @param \DrupalCacheInterface $cache_backend * Cache backend instance to use. */ public function __construct(\Traversable $namespaces, \DrupalCacheInterface $cache_backend) { parent::__construct('Plugin/authentication', $namespaces, 'Drupal\restful\Plugin\authentication\AuthenticationInterface', '\Drupal\restful\Annotation\Authentication'); $this->setCacheBackend($cache_backend, 'authentication_plugins'); $this->alterInfo('authentication_plugin'); } /** * AuthenticationPluginManager factory method. * * @param string $bin * The cache bin for the plugin manager. * @param bool $avoid_singleton * Do not use the stored singleton. * * @return AuthenticationPluginManager * The created manager. */ public static function create($bin = 'cache', $avoid_singleton = FALSE) { $factory = function ($bin) { return new static(Module::getNamespaces(), _cache_get_object($bin)); }; if ($avoid_singleton) { $factory($bin); } return static::semiSingletonInstance($factory, array($bin)); } } ================================================ FILE: src/Plugin/ConfigurablePluginTrait.php ================================================ instanceConfiguration)) { $this->instanceConfiguration = $this->defaultConfiguration(); } return $this->instanceConfiguration; } /** * {@inheritdoc} */ public function setConfiguration(array $configuration) { $this->instanceConfiguration = $configuration; } /** * {@inheritdoc} */ public function defaultConfiguration() { return array(); } /** * {@inheritdoc} */ public function calculateDependencies() { return array(); } } ================================================ FILE: src/Plugin/FormatterPluginManager.php ================================================ setCacheBackend($cache_backend, 'formatter_plugins'); $this->alterInfo('formatter_plugin'); } /** * FormatterPluginManager factory method. * * @param string $bin * The cache bin for the plugin manager. * * @return FormatterPluginManager * The created manager. */ public static function create($bin = 'cache') { return new static(Module::getNamespaces(), _cache_get_object($bin)); } } ================================================ FILE: src/Plugin/RateLimitPluginManager.php ================================================ setCacheBackend($cache_backend, 'rate_limit_plugins'); $this->alterInfo('rate_limit_plugin'); } /** * RateLimitPluginManager factory method. * * @param string $bin * The cache bin for the plugin manager. * * @return RateLimitPluginManager * The created manager. */ public static function create($bin = 'cache') { return new static(Module::getNamespaces(), _cache_get_object($bin)); } } ================================================ FILE: src/Plugin/ResourcePluginManager.php ================================================ setCacheBackend($cache_backend, 'resource_plugins'); $this->alterInfo('resource_plugin'); $this->request = $request; } /** * ResourcePluginManager factory method. * * @param string $bin * The cache bin for the plugin manager. * @param RequestInterface $request * The request object. * * @return ResourcePluginManager * The created manager. */ public static function create($bin = 'cache', RequestInterface $request = NULL) { return new static(Module::getNamespaces(), _cache_get_object($bin), $request); } /** * Overrides PluginManagerBase::createInstance(). * * This method is overridden to set the request object when the resource * object is instantiated. */ public function createInstance($plugin_id, array $configuration = array()) { /* @var ResourceInterface $resource */ $resource = parent::createInstance($plugin_id, $configuration); $resource->setRequest($this->request); return $resource; } } ================================================ FILE: src/Plugin/SemiSingletonTrait.php ================================================ getPluginId(); } } ================================================ FILE: src/Plugin/authentication/AuthenticationInterface.php ================================================ getUser(); $password = $request->getPassword(); return isset($username) && isset($password); } /** * {@inheritdoc} * * @see user_login_authenticate_validate(). */ public function authenticate(RequestInterface $request) { $username = $request->getUser(); $password = $request->getPassword(); // Do not allow any login from the current user's IP if the limit has been // reached. Default is 50 failed attempts allowed in one hour. This is // independent of the per-user limit to catch attempts from one IP to log // in to many different user accounts. We have a reasonably high limit // since there may be only one apparent IP for all users at an institution. if (!flood_is_allowed('failed_login_attempt_ip', variable_get('user_failed_login_ip_limit', 50), variable_get('user_failed_login_ip_window', 3600))) { throw new FloodException(format_string('Rejected by ip flood control.')); } if (!$uid = db_query_range("SELECT uid FROM {users} WHERE name = :name AND status = 1", 0, 1, array(':name' => $username))->fetchField()) { // Always register an IP-based failed login event. flood_register_event('failed_login_attempt_ip', variable_get('user_failed_login_ip_window', 3600), ip_address()); return NULL; } if (variable_get('user_failed_login_identifier_uid_only', FALSE)) { // Register flood events based on the uid only, so they apply for any // IP address. This is the most secure option. $identifier = $uid; } else { // The default identifier is a combination of uid and IP address. This // is less secure but more resistant to denial-of-service attacks that // could lock out all users with public user names. $identifier = $uid . '-' . ip_address(); } // Don't allow login if the limit for this user has been reached. // Default is to allow 5 failed attempts every 6 hours. if (flood_is_allowed('failed_login_attempt_user', variable_get('user_failed_login_user_limit', 5), variable_get('user_failed_login_user_window', 21600), $identifier)) { // We are not limited by flood control, so try to authenticate. if ($uid = user_authenticate($username, $password)) { // Clear the user based flood control. flood_clear_event('failed_login_attempt_user', $identifier); return user_load($uid); } flood_register_event('failed_login_attempt_user', variable_get('user_failed_login_user_window', 3600), $identifier); } else { flood_register_event('failed_login_attempt_user', variable_get('user_failed_login_user_window', 3600), $identifier); throw new FloodException(format_string('Rejected by user flood control.')); } } } ================================================ FILE: src/Plugin/authentication/CookieAuthentication.php ================================================ isCli($request)) { return NULL; } global $user; $account = user_load($user->uid); if (!$request::isWriteMethod($request->getMethod()) || $request->getApplicationData('rest_call')) { // Request is done via API not CURL, or not a write operation, so we don't // need to check for a CSRF token. return $account; } if (!RestfulManager::isRestfulPath($request)) { return $account; } if (!$request->getCsrfToken()) { throw new BadRequestException('No CSRF token passed in the HTTP header.'); } if (!drupal_valid_token($request->getCsrfToken(), Authentication::TOKEN_VALUE)) { throw new ForbiddenException('CSRF token validation failed.'); } // CSRF validation passed. return $account; } /** * Detects whether the script is running from a command line environment. * * @param RequestInterface $request. * The request. * * @return bool * TRUE if a command line environment is detected. FALSE otherwise. */ protected function isCli(RequestInterface $request) { // Needed to detect if run-tests.sh is running the tests. $cli = $request->getHeaders()->get('User-Agent')->getValueString() == 'Drupal command line'; return $cli || drupal_is_cli(); } } ================================================ FILE: src/Plugin/authentication/OAuth2ServerAuthentication.php ================================================ resourceManager = restful()->getResourceManager(); } /** * {@inheritdoc} */ public function applies(RequestInterface $request) { return module_exists('oauth2_server') && $this->getOAuth2Info($request); } /** * {@inheritdoc} */ public function authenticate(RequestInterface $request) { $oauth2_info = $this->getOAuth2Info($request); if (!$oauth2_info) { throw new ServerConfigurationException('The resource uses OAuth2 authentication but does not specify the OAuth2 server.'); } $result = oauth2_server_check_access($oauth2_info['server'], $oauth2_info['scope']); if ($result instanceof \OAuth2\Response) { throw new UnauthorizedException($result->getResponseBody(), $result->getStatusCode()); } elseif (empty($result['user_id'])) { return NULL; } return user_load($result['user_id']); } /** * Get OAuth2 information from the request. * * @param \Drupal\restful\Http\RequestInterface $request * The request. * * @return array|null * Simple associative array with the following keys: * - server: The OAuth2 server to authenticate against. * - scope: The scope required for the resource. */ protected function getOAuth2Info(RequestInterface $request) { $plugin_id = $this->getResourcePluginIdFromRequest(); if (!$plugin_id) { // If the plugin can't be determined, it is probably not a request to the // resource but something else that is just loading all the plugins. return NULL; } $plugin_definition = ResourcePluginManager::create('cache', $request)->getDefinition($plugin_id); if (empty($plugin_definition['oauth2Server'])) { return NULL; } $server = $plugin_definition['oauth2Server']; $scope = !empty($plugin_definition['oauth2Scope']) ? $plugin_definition['oauth2Scope'] : ''; return ['server' => $server, 'scope' => $scope]; } /** * Get the resource plugin id requested. * * @return null|string * The plugin id of the resource that was requested. */ protected function getResourcePluginIdFromRequest() { $resource_name = $this->resourceManager->getResourceIdFromRequest(); $version = $this->resourceManager->getVersionFromRequest(); if (!$resource_name || !$version) { return NULL; } return $resource_name . PluginBase::DERIVATIVE_SEPARATOR . $version[0] . '.' . $version[1]; } } ================================================ FILE: src/Plugin/formatter/Formatter.php ================================================ render($this->prepare($data)); } /** * {@inheritdoc} */ public function getContentTypeHeader() { // Default to the most generic content type. return 'application/hal+json; charset=utf-8'; } /** * {@inheritdoc} */ public function getResource() { if (isset($this->resource)) { return $this->resource; } // Get the resource from the instance configuration. $instance_configuration = $this->getConfiguration(); if (empty($instance_configuration['resource'])) { return NULL; } $this->resource = $instance_configuration['resource'] instanceof ResourceInterface ? $instance_configuration['resource'] : NULL; return $this->resource; } /** * {@inheritdoc} */ public function setResource(ResourceInterface $resource) { $this->resource = $resource; $this->setConfiguration(array( 'resource' => $resource, )); } /** * {@inheritdoc} */ public function parseBody($body) { throw new ServerConfigurationException(sprintf('Invalid body parser for: %s.', $body)); } /** * Helper function to know if a variable is iterable or not. * * @param mixed $input * The variable to test. * * @return bool * TRUE if the variable is iterable. */ protected static function isIterable($input) { return is_array($input) || $input instanceof \Traversable || $input instanceof \stdClass; } /** * Checks if the passed in data to be rendered can be cached. * * @param mixed $data * The data to be prepared and rendered. * * @return bool * TRUE if the data can be cached. */ protected function isCacheEnabled($data) { // We are only caching field collections, but you could cache at different // layers too. if (!$data instanceof ResourceFieldCollectionInterface) { return FALSE; } if (!$context = $data->getContext()) { return FALSE; } return !empty($context['cache_fragments']); } /** * Gets the cached computed value for the fields to be rendered. * * @param mixed $data * The data to be rendered. * * @return mixed * The cached data. */ protected function getCachedData($data) { if (!$render_cache = $this->createCacheController($data)) { return NULL; } return $render_cache->get(); } /** * Gets the cached computed value for the fields to be rendered. * * @param mixed $data * The data to be rendered. * * @return string * The cache hash. */ protected function getCacheHash($data) { if (!$render_cache = $this->createCacheController($data)) { return NULL; } return $render_cache->getCid(); } /** * Gets the cached computed value for the fields to be rendered. * * @param mixed $data * The data to be rendered. * @param mixed $output * The rendered data to output. * @param string[] $parent_hashes * An array that holds the name of the parent cache hashes that lead to the * current data structure. */ protected function setCachedData($data, $output, array $parent_hashes = array()) { if (!$render_cache = $this->createCacheController($data)) { return; } $render_cache->set($output); // After setting the cache for the current object, mark all parent hashes // with the current cache fragments. That will have the effect of allowing // to clear the parent caches based on the children fragments. $fragments = $this->cacheFragments($data); foreach ($parent_hashes as $parent_hash) { foreach ($fragments as $tag_type => $tag_value) { // Check if the fragment already exists. $query = new \EntityFieldQuery(); $duplicate = (bool) $query ->entityCondition('entity_type', 'cache_fragment') ->propertyCondition('value', $tag_value) ->propertyCondition('type', $tag_type) ->propertyCondition('hash', $parent_hash) ->count() ->execute(); if ($duplicate) { continue; } $cache_fragment = new CacheFragment(array( 'value' => $tag_value, 'type' => $tag_type, 'hash' => $parent_hash, ), 'cache_fragment'); try { $cache_fragment->save(); } catch (\Exception $e) { watchdog_exception('restful', $e); } } } } /** * Gets a cache controller based on the data to be rendered. * * @param mixed $data * The data to be rendered. * * @return \Drupal\restful\RenderCache\RenderCacheInterface; * The cache controller. */ protected function createCacheController($data) { if (!$cache_fragments = $this->cacheFragments($data)) { return NULL; } // Add the formatter fragment because every formatter may prepare the data // differently. /* @var \Doctrine\Common\Collections\ArrayCollection $cache_fragments */ $cache_fragments->set('formatter', $this->getPluginId()); /* @var \Drupal\restful\Plugin\resource\Decorators\CacheDecoratedResource $cached_resource */ if (!$cached_resource = $this->getResource()) { return NULL; } if (!$cached_resource instanceof CacheDecoratedResourceInterface) { return NULL; } return RenderCache::create($cache_fragments, $cached_resource->getCacheController()); } /** * Gets a cache fragments based on the data to be rendered. * * @param mixed $data * The data to be rendered. * * @return \Doctrine\Common\Collections\ArrayCollection; * The cache controller. */ protected static function cacheFragments($data) { $context = $data->getContext(); if (!$cache_fragments = $context['cache_fragments']) { return NULL; } return $cache_fragments; } /** * Returns only the allowed fields by filtering out the other ones. * * @param mixed $output * The data structure to filter. * @param bool|string[] $allowed_fields * FALSE to allow all fields. An array of allowed values otherwise. * * @return mixed * The filtered output. */ protected function limitFields($output, $allowed_fields = NULL) { if (!isset($allowed_fields)) { $request = ($resource = $this->getResource()) ? $resource->getRequest() : restful()->getRequest(); $input = $request->getParsedInput(); // Set the field limits to false if there are no limits. $allowed_fields = empty($input['fields']) ? FALSE : explode(',', $input['fields']); } if (!is_array($output)) { // $output is a simple value. return $output; } $result = array(); if (ResourceFieldBase::isArrayNumeric($output)) { foreach ($output as $item) { $result[] = $this->limitFields($item, $allowed_fields); } return $result; } foreach ($output as $field_name => $field_contents) { if ($allowed_fields !== FALSE && !in_array($field_name, $allowed_fields)) { continue; } $result[$field_name] = $this->limitFields($field_contents, $this->unprefixInputOptions($allowed_fields, $field_name)); } return $result; } /** * Given a prefix, return the allowed fields that apply removing the prefix. * * @param bool|string[] $allowed_fields * The list of allowed fields in dot notation. * @param string $prefix * The prefix used to select the fields and to remove from the front. * * @return bool|string[] * The new allowed fields for the nested sub-request. */ protected static function unprefixInputOptions($allowed_fields, $prefix) { if ($allowed_fields === FALSE) { return FALSE; } $closure_unprefix = function ($field_limit) use ($prefix) { if ($field_limit == $prefix) { return NULL; } $pos = strpos($field_limit, $prefix . '.'); // Remove the prefix from the $field_limit. return $pos === 0 ? substr($field_limit, strlen($prefix . '.')) : NULL; }; return array_filter(array_map($closure_unprefix, $allowed_fields)); } /** * Helper function that calculates the number of items per page. * * @param ResourceInterface $resource * The associated resource. * * @return int * The items per page. */ protected function calculateItemsPerPage(ResourceInterface $resource) { $data_provider = $resource->getDataProvider(); $max_range = $data_provider->getRange(); $original_input = $resource->getRequest()->getPagerInput(); $items_per_page = empty($original_input['size']) ? $max_range : $original_input['size']; return $items_per_page > $max_range ? $max_range : $items_per_page; } } ================================================ FILE: src/Plugin/formatter/FormatterHalJson.php ================================================ contentType = 'application/problem+json; charset=utf-8'; return $data; } // Here we get the data after calling the backend storage for the resources. if (!$resource = $this->getResource()) { throw new ServerConfigurationException('Resource unavailable for HAL formatter.'); } $is_list_request = $resource->getRequest()->isListRequest($resource->getPath()); $values = $this->extractFieldValues($data); $values = $this->limitFields($values); if ($is_list_request) { // If this is a listing, move everything into the _embedded. $curies_resource = $this->withCurie($resource->getResourceMachineName()); $output = array( '_embedded' => array($curies_resource => $values), ); } else { $output = reset($values) ?: array(); } $data_provider = $resource->getDataProvider(); if ( $data_provider && method_exists($data_provider, 'count') && $is_list_request ) { // Get the total number of items for the current request without // pagination. $output['count'] = $data_provider->count(); } if (method_exists($resource, 'additionalHateoas')) { $output = array_merge($output, $resource->additionalHateoas()); } // Add HATEOAS to the output. $this->addHateoas($output); // Cosmetic sorting to send the hateoas properties to the end of the output. uksort($output, function ($a, $b) { if ( ($a[0] == '_' && $b[0] == '_') || ($a[0] != '_' && $b[0] != '_') ) { return strcmp($a, $b); } return $a[0] == '_' ? 1 : -1; }); return $output; } /** * Add HATEOAS links to list of item. * * @param array $data * The data array after initial massaging. */ protected function addHateoas(array &$data) { if (!$resource = $this->getResource()) { return; } $request = $resource->getRequest(); if (!isset($data['_links'])) { $data['_links'] = array(); } // Get self link. $data['_links']['self'] = array( 'title' => 'Self', 'href' => $resource->versionedUrl($resource->getPath()), ); $input = $request->getPagerInput(); $page = $input['number']; if ($page > 1) { $input['number'] = $page - 1; $data['_links']['previous'] = array( 'title' => 'Previous', 'href' => $resource->getUrl(), ); } $curies_resource = $this->withCurie($resource->getResourceMachineName()); $listed_items = empty($data['_embedded'][$curies_resource]) ? 1 : count($data['_embedded'][$curies_resource]); // We know that there are more pages if the total count is bigger than the // number of items of the current request plus the number of items in // previous pages. $items_per_page = $this->calculateItemsPerPage($resource); $previous_items = ($page - 1) * $items_per_page; if (isset($data['count']) && $data['count'] > $listed_items + $previous_items) { $input['number'] = $page + 1; $data['_links']['next'] = array( 'title' => 'Next', 'href' => $resource->getUrl(), ); } if (!$curie = $this->getCurie()) { return; } $curie += array( 'path' => 'doc/rels', 'template' => '/{rel}', ); $data['_links']['curies'] = array( 'name' => $curie['name'], 'href' => url($curie['path'], array('absolute' => TRUE)) . $curie['template'], 'templated' => TRUE, ); } /** * Extracts the actual values from the resource fields. * * @param array|\Traversable|\stdClass $data * The array of rows. * * @return array[] * The array of prepared data. * * @throws \Drupal\restful\Exception\InternalServerErrorException */ protected function extractFieldValues($data, array $parents = array(), array &$parent_hashes = array()) { $output = array(); if ($this->isCacheEnabled($data)) { $parent_hashes[] = $this->getCacheHash($data); if ($cache = $this->getCachedData($data)) { return $cache->data; } } foreach ($data as $public_field_name => $resource_field) { if (!$resource_field instanceof ResourceFieldInterface) { // If $resource_field is not a ResourceFieldInterface it means that we // are dealing with a nested structure of some sort. If it is an array // we process it as a set of rows, if not then use the value directly. $parents[] = $public_field_name; $output[$public_field_name] = static::isIterable($resource_field) ? $this->extractFieldValues($resource_field, $parents, $parent_hashes) : $resource_field; continue; } if (!$data instanceof ResourceFieldCollectionInterface) { throw new InternalServerErrorException('Inconsistent output.'); } // This feels a bit awkward, but if the result is going to be cached, it // pays off the extra effort of generating the whole resource entity. That // way we can get a different field set with the previously cached entity. // If the entity is not going to be cached, then avoid generating the // field data altogether. $limit_fields = $data->getLimitFields(); if ( $this->isCacheEnabled($data) && $limit_fields && !in_array($resource_field->getPublicName(), $limit_fields) ) { // We are not going to cache this and this field is not in the output. continue; } $value = $resource_field->render($data->getInterpreter()); // If the field points to a resource that can be included, include it // right away. if ( static::isIterable($value) && $resource_field instanceof ResourceFieldResourceInterface ) { $output += array('_embedded' => array()); $output['_embedded'][$this->withCurie($public_field_name)] = $this->extractFieldValues($value, $parents, $parent_hashes); continue; } $output[$public_field_name] = $value; } if ($this->isCacheEnabled($data)) { $this->setCachedData($data, $output, $parent_hashes); } return $output; } /** * {@inheritdoc} */ public function render(array $structured_data) { return drupal_json_encode($structured_data); } /** * {@inheritdoc} */ public function getContentTypeHeader() { return $this->contentType; } /** * Prefix a property name with the curie, if present. * * @param string $property_name * The input string. * * @return string * The property name prefixed with the curie. */ protected function withCurie($property_name) { if ($curie = $this->getCurie()) { return $property_name ? $curie['name'] . static::CURIE_SEPARATOR . $property_name : $curie['name']; } return $property_name; } /** * Checks if the current plugin has a defined curie. * * @return array * Associative array with the curie information. */ protected function getCurie() { return $this->configuration['curie']; } } ================================================ FILE: src/Plugin/formatter/FormatterInterface.php ================================================ contentType = 'application/problem+json; charset=utf-8'; return $data; } $extracted = $this->extractFieldValues($data); $output = array('data' => $this->limitFields($extracted)); if ($resource = $this->getResource()) { $request = $resource->getRequest(); $data_provider = $resource->getDataProvider(); if ($request->isListRequest($resource->getPath())) { // Get the total number of items for the current request without // pagination. $output['count'] = $data_provider->count(); // If there are items that were taken out during access checks, // report them as denied in the metadata. if (variable_get('restful_show_access_denied', FALSE) && ($inaccessible_records = $data_provider->getMetadata()->get('inaccessible_records'))) { $output['denied'] = empty($output['meta']['denied']) ? $inaccessible_records : $output['meta']['denied'] + $inaccessible_records; } } if (method_exists($resource, 'additionalHateoas')) { $output = array_merge($output, $resource->additionalHateoas($output)); } // Add HATEOAS to the output. $this->addHateoas($output); } return $output; } /** * Extracts the actual values from the resource fields. * * @param array[]|ResourceFieldCollectionInterface $data * The array of rows or a ResourceFieldCollection. * @param string[] $parents * An array that holds the name of the parent fields that lead to the * current data structure. * @param string[] $parent_hashes * An array that holds the name of the parent cache hashes that lead to the * current data structure. * * @return array[] * The array of prepared data. * * @throws InternalServerErrorException */ protected function extractFieldValues($data, array $parents = array(), array &$parent_hashes = array()) { $output = array(); if ($this->isCacheEnabled($data)) { $parent_hashes[] = $this->getCacheHash($data); if ($cache = $this->getCachedData($data)) { return $cache->data; } } foreach ($data as $public_field_name => $resource_field) { if (!$resource_field instanceof ResourceFieldInterface) { // If $resource_field is not a ResourceFieldInterface it means that we // are dealing with a nested structure of some sort. If it is an array // we process it as a set of rows, if not then use the value directly. $parents[] = $public_field_name; $output[$public_field_name] = static::isIterable($resource_field) ? $this->extractFieldValues($resource_field, $parents, $parent_hashes) : $resource_field; continue; } if (!$data instanceof ResourceFieldCollectionInterface) { throw new InternalServerErrorException('Inconsistent output.'); } // This feels a bit awkward, but if the result is going to be cached, it // pays off the extra effort of generating the whole resource entity. That // way we can get a different field set with the previously cached entity. // If the entity is not going to be cached, then avoid generating the // field data altogether. $limit_fields = $data->getLimitFields(); if ( !$this->isCacheEnabled($data) && $limit_fields && !in_array($resource_field->getPublicName(), $limit_fields) ) { // We are not going to cache this and this field is not in the output. continue; } $value = $resource_field->render($data->getInterpreter()); // If the field points to a resource that can be included, include it // right away. if ( static::isIterable($value) && $resource_field instanceof ResourceFieldResourceInterface ) { $value = $this->extractFieldValues($value, $parents, $parent_hashes); } $output[$public_field_name] = $value; } if ($this->isCacheEnabled($data)) { $this->setCachedData($data, $output, $parent_hashes); } return $output; } /** * Add HATEOAS links to list of item. * * @param array $data * The data array after initial massaging. */ protected function addHateoas(array &$data) { if (!$resource = $this->getResource()) { return; } $request = $resource->getRequest(); // Get self link. $data['self'] = array( 'title' => 'Self', 'href' => $resource->versionedUrl($resource->getPath()), ); $input = $request->getParsedInput(); unset($input['page']); unset($input['range']); $input['page'] = $request->getPagerInput(); $page = $input['page']['number']; if ($page > 1) { $query = $input; $query['page']['number'] = $page - 1; $data['previous'] = array( 'title' => 'Previous', 'href' => $resource->versionedUrl('', array('query' => $query), TRUE), ); } // We know that there are more pages if the total count is bigger than the // number of items of the current request plus the number of items in // previous pages. $items_per_page = $this->calculateItemsPerPage($resource); $previous_items = ($page - 1) * $items_per_page; if (isset($data['count']) && $data['count'] > count($data['data']) + $previous_items) { $query = $input; $query['page']['number'] = $page + 1; $data['next'] = array( 'title' => 'Next', 'href' => $resource->versionedUrl('', array('query' => $query), TRUE), ); } } /** * {@inheritdoc} */ public function render(array $structured_data) { return drupal_json_encode($structured_data); } /** * {@inheritdoc} */ public function getContentTypeHeader() { return $this->contentType; } /** * {@inheritdoc} */ public function parseBody($body) { if (!$decoded_json = drupal_json_decode($body)) { throw new BadRequestException(sprintf('Invalid JSON provided: %s.', $body)); } return $decoded_json; } } ================================================ FILE: src/Plugin/formatter/FormatterJsonApi.php ================================================ contentType = 'application/problem+json; charset=utf-8'; return $data; } $extracted = $this->extractFieldValues($data); $included = array(); $output = array('data' => $this->renormalize($extracted, $included)); $output = $this->populateIncludes($output, $included); if ($resource = $this->getResource()) { $request = $resource->getRequest(); $data_provider = $resource->getDataProvider(); $is_list_request = $request->isListRequest($resource->getPath()); if ($is_list_request) { // Get the total number of items for the current request without // pagination. $output['meta']['count'] = $data_provider->count(); // If there are items that were taken out during access checks, // report them as denied in the metadata. if (variable_get('restful_show_access_denied', FALSE) && ($inaccessible_records = $data_provider->getMetadata()->get('inaccessible_records'))) { $output['meta']['denied'] = empty($output['meta']['denied']) ? $inaccessible_records : $output['meta']['denied'] + $inaccessible_records; } } else { // For non-list requests do not return an array of one item. $output['data'] = reset($output['data']); } if (method_exists($resource, 'additionalHateoas')) { $output = array_merge($output, $resource->additionalHateoas($output)); } // Add HATEOAS to the output. $this->addHateoas($output); } return $output; } /** * Extracts the actual values from the resource fields. * * @param array[]|ResourceFieldCollectionInterface $data * The array of rows or a ResourceFieldCollection. * @param string[] $parents * An array that holds the name of the parent fields that lead to the * current data structure. * @param string[] $parent_hashes * An array that holds the name of the parent cache hashes that lead to the * current data structure. * * @return array[] * The array of prepared data. * * @throws InternalServerErrorException * @throws \Drupal\restful\Exception\ServerConfigurationException */ protected function extractFieldValues($data, array $parents = array(), array $parent_hashes = array()) { $output = array(); if ($this->isCacheEnabled($data)) { $parent_hashes[] = $this->getCacheHash($data); if ($cache = $this->getCachedData($data)) { return $cache->data; } } foreach ($data as $public_field_name => $resource_field) { if (!$resource_field instanceof ResourceFieldInterface) { // If $resource_field is not a ResourceFieldInterface it means that we // are dealing with a nested structure of some sort. If it is an array // we process it as a set of rows, if not then use the value directly. $parents[] = $public_field_name; $output[$public_field_name] = static::isIterable($resource_field) ? $this->extractFieldValues($resource_field, $parents, $parent_hashes) : $resource_field; continue; } if (!$data instanceof ResourceFieldCollectionInterface) { throw new InternalServerErrorException('Inconsistent output.'); } // This feels a bit awkward, but if the result is going to be cached, it // pays off the extra effort of generating the whole resource entity. That // way we can get a different field set with the previously cached entity. // If the entity is not going to be cached, then avoid generating the // field data altogether. $limit_fields = $data->getLimitFields(); $output['#fields'] = empty($output['#fields']) ? array() : $output['#fields']; if ( !$this->isCacheEnabled($data) && $limit_fields && !in_array($resource_field->getPublicName(), $limit_fields) ) { // We are not going to cache this and this field is not in the output. continue; } $interpreter = $data->getInterpreter(); if (!$id_field = $data->getIdField()) { throw new ServerConfigurationException('Invalid required ID field for JSON API formatter.'); } $output['#fields'][$public_field_name] = $this->embedField($resource_field, $id_field->render($interpreter), $interpreter, $parents, $parent_hashes); } if ($data instanceof ResourceFieldCollectionInterface) { $output['#resource_name'] = $data->getResourceName(); $output['#resource_plugin'] = $data->getResourceId(); $resource_id = $data->getIdField()->render($data->getInterpreter()); if (!is_array($resource_id)) { // In some situations when making an OPTIONS call the $resource_id // returns the array of discovery information instead of a real value. $output['#resource_id'] = (string) $resource_id; try { $output['#links']['self'] = restful() ->getResourceManager() ->getPlugin($output['#resource_plugin']) ->versionedUrl($output['#resource_id']); } catch(PluginNotFoundException $e) {} } } if ($this->isCacheEnabled($data)) { $this->setCachedData($data, $output, $parent_hashes); } return $output; } /** * Add HATEOAS links to list of item. * * @param array $data * The data array after initial massaging. * @param ResourceInterface $resource * The resource to use. * @param string $path * The resource path. */ protected function addHateoas(array &$data, ResourceInterface $resource = NULL, $path = NULL) { $top_level = empty($resource); $resource = $resource ?: $this->getResource(); $path = isset($path) ? $path : $resource->getPath(); if (!$resource) { return; } $request = $resource->getRequest(); if (!isset($data['links'])) { $data['links'] = array(); } $input = $original_input = $request->getParsedInput(); unset($input['page']); unset($input['range']); unset($original_input['page']); unset($original_input['range']); $input['page'] = $request->getPagerInput(); $original_input['page'] = $request->getPagerInput(); // Get self link. $options = $top_level ? array('query' => $input) : array(); $data['links']['self'] = $resource->versionedUrl($path, $options); $page = $input['page']['number']; // We know that there are more pages if the total count is bigger than the // number of items of the current request plus the number of items in // previous pages. $items_per_page = $this->calculateItemsPerPage($resource); if (isset($data['meta']['count']) && $data['meta']['count'] > $items_per_page) { $num_pages = ceil($data['meta']['count'] / $items_per_page); unset($input['page']['number']); $data['links']['first'] = $resource->getUrl(array('query' => $input), FALSE); if ($page > 1) { $input = $original_input; $input['page']['number'] = $page - 1; $data['links']['previous'] = $resource->getUrl(array('query' => $input), FALSE); } if ($num_pages > 1) { $input = $original_input; $input['page']['number'] = $num_pages; $data['links']['last'] = $resource->getUrl(array('query' => $input), FALSE); if ($page != $num_pages) { $input = $original_input; $input['page']['number'] = $page + 1; $data['links']['next'] = $resource->getUrl(array('query' => $input), FALSE); } } } } /** * {@inheritdoc} */ public function render(array $structured_data) { return drupal_json_encode($structured_data); } /** * {@inheritdoc} */ public function getContentTypeHeader() { return $this->contentType; } /** * Gets the request object for this formatter. * * @return RequestInterface * The request object. */ protected function getRequest() { if ($resource = $this->getResource()) { return $resource->getRequest(); } return restful()->getRequest(); } /** * Move the embedded resources to the included key. * * Change the data structure from an auto-contained hierarchical tree to the * final JSON API structure. The auto-contained tree has redundant information * because every branch contains all the information that is embedded in there * and can be used as stand alone. * * @param array $output * The output array to modify to include the compounded documents. * @param array $included * Pool of documents to compound. * @param bool|string[] $allowed_fields * The sparse fieldset information. FALSE to select all fields. * @param array $includes_parents * An array containing the included path until the current field being * processed. * * @return array * The processed data. */ protected function renormalize(array $output, array &$included, $allowed_fields = NULL, $includes_parents = array()) { static $depth = -1; $depth++; if (!isset($allowed_fields)) { $request = ($resource = $this->getResource()) ? $resource->getRequest() : restful()->getRequest(); $input = $request->getParsedInput(); // Set the field limits to false if there are no limits. $allowed_fields = empty($input['fields']) ? FALSE : explode(',', $input['fields']); } if (!is_array($output)) { // $output is a simple value. $depth--; return $output; } $result = array(); if (ResourceFieldBase::isArrayNumeric($output)) { foreach ($output as $item) { $result[] = $this->renormalize($item, $included, $allowed_fields, $includes_parents); } $depth--; return $result; } if (!empty($output['#resource_name'])) { $result['type'] = $output['#resource_name']; } if (!empty($output['#resource_id'])) { $result['id'] = $output['#resource_id']; } if (!isset($output['#fields'])) { $depth--; return $this->renormalize($output, $included, $allowed_fields, $includes_parents); } foreach ($output['#fields'] as $field_name => $field_contents) { if ($allowed_fields !== FALSE && !in_array($field_name, $allowed_fields)) { continue; } if (empty($field_contents['#embedded'])) { $result['attributes'][$field_name] = $field_contents; } else { // Handle single and multiple relationships. $rel = array(); $single_item = $field_contents['#cardinality'] == 1; $relationship_links = empty($field_contents['#relationship_links']) ? NULL : $field_contents['#relationship_links']; unset($field_contents['#embedded']); unset($field_contents['#cardinality']); unset($field_contents['#relationship_links']); foreach ($field_contents as $field_item) { $include_links = empty($field_item['#include_links']) ? NULL : $field_item['#include_links']; unset($field_contents['#include_links']); $field_path = $this->buildIncludePath($includes_parents, $field_name); $field_item = $this->populateCachePlaceholder($field_item, $field_path); unset($field_item['#cache_placeholder']); $element = $field_item['#relationship_info']; unset($field_item['#relationship_info']); $include_key = $field_item['#resource_plugin'] . '--' . $field_item['#resource_id']; $nested_allowed_fields = $this->unprefixInputOptions($allowed_fields, $field_name); // If the list of the child allowed fields is empty, but the parent is // part of the includes, it means that the consumer meant to include // all the fields in the children. if (is_array($allowed_fields) && empty($nested_allowed_fields) && in_array($field_name, $allowed_fields)) { $nested_allowed_fields = FALSE; } // If we get here is because the relationship is included in the // sparse fieldset. That means that in this context, empty field // limits mean all the fields. $new_includes_parents = $includes_parents; $new_includes_parents[] = $field_name; $included[$field_path][$include_key] = $this->renormalize($field_item, $included, $nested_allowed_fields, $new_includes_parents); $included[$field_path][$include_key] += $include_links ? array('links' => $include_links) : array(); $rel[$include_key] = $element; } // Only place the relationship info. $result['relationships'][$field_name] = array( 'data' => $single_item ? reset($rel) : array_values($rel), ); if (!empty($relationship_links)) { $result['relationships'][$field_name]['links'] = $relationship_links; } } } // Set the links for every item. if (!empty($output['#links'])) { $result['links'] = $output['#links']; } // Decrease the depth level. $depth--; return $result; } /** * Gather all of the includes. */ protected function populateIncludes($output, $included) { // Loop through the included resource entities and add them to the output if // they are included from the request. $input = $this->getRequest()->getParsedInput(); $requested_includes = empty($input['include']) ? array() : explode(',', $input['include']); // Keep track of everything that has been included. $included_keys = array(); foreach ($requested_includes as $requested_include) { if (empty($included[$requested_include])) { continue; } foreach ($included[$requested_include] as $include_key => $included_item) { if (in_array($include_key, $included_keys)) { continue; } $output['included'][] = $included_item; $included_keys[] = $include_key; } } return $output; } /** * Embeds the final contents of a field. * * If the field is a relationship to another resource, it embeds the resource. * * @param ResourceFieldInterface $resource_field * The resource field being processed. If it is a related resource, this is * used to extract the contents of the resource. If not, it's used to * extract the simple value. * @param string $parent_id * ID in the parent resource where this is being embedded. * @param DataInterpreterInterface $interpreter * The context for the $resource_field. * @param array $parents * Tracks the parents of the field to construct the dot notation for the * field name. * @param string[] $parent_hashes * An array that holds the name of the parent cache hashes that lead to the * current data structure. * * @return array * The contents for the JSON API attribute or relationship. */ protected function embedField(ResourceFieldInterface $resource_field, $parent_id, DataInterpreterInterface $interpreter, array &$parents, array &$parent_hashes) { static $embedded_resources = array(); // If the field points to a resource that can be included, include it // right away. if (!$resource_field instanceof ResourceFieldResourceInterface) { return $resource_field->render($interpreter); } // Check if the resource needs to be included. If not then set 'full_view' // to false. $cardinality = $resource_field->getCardinality(); $output = array(); $public_field_name = $resource_field->getPublicName(); if (!$ids = $resource_field->compoundDocumentId($interpreter)) { return NULL; } $ids = $cardinality == 1 ? $ids = array($ids) : $ids; $resource_info = $resource_field->getResource(); $empty_value = array( '#fields' => array(), '#embedded' => TRUE, '#resource_plugin' => sprintf('%s:%d.%d', $resource_info['name'], $resource_info['majorVersion'], $resource_info['minorVersion']), '#cache_placeholder' => array( 'parents' => array_merge($parents, array($public_field_name)), 'parent_hashes' => $parent_hashes, ), ); $value = array_map(function ($id) use ($empty_value) { return $empty_value + array('#resource_id' => $id); }, $ids); if ($this->needsIncluding($resource_field, $parents)) { $cid = sprintf('%s:%d.%d--%s', $resource_info['name'], $resource_info['majorVersion'], $resource_info['minorVersion'], implode(',', $ids)); if (!isset($embedded_resources[$cid])) { $result = $resource_field->render($interpreter); if (empty($result) || !static::isIterable($result)) { $embedded_resources[$cid] = $result; return $result; } $new_parents = $parents; $new_parents[] = $public_field_name; $result = $this->extractFieldValues($result, $new_parents, $parent_hashes); $embedded_resources[$cid] = $cardinality == 1 ? array($result) : $result; } $value = $embedded_resources[$cid]; } // At this point we are dealing with an embed. $value = array_filter($value); // Set the resource for the reference. $resource_plugin = $resource_field->getResourcePlugin(); foreach ($value as $value_item) { $id = $value_item['#resource_id']; $basic_info = array( 'type' => $resource_field->getResourceMachineName(), 'id' => (string) $id, ); // We want to be able to include only the images in articles.images, // but not articles.related.images. That's why we need the path // including the parents. $item = array( '#resource_name' => $basic_info['type'], '#resource_plugin' => $resource_plugin->getPluginId(), '#resource_id' => $basic_info['id'], '#include_links' => array( 'self' => $resource_plugin->versionedUrl($basic_info['id']), ), '#relationship_info' => array( 'type' => $basic_info['type'], 'id' => $basic_info['id'], ), ) + $value_item; $output[] = $item; } // If there is a resource plugin for the parent, set the related // links. $links = array(); if ($resource = $this->getResource()) { $links['related'] = $resource->versionedUrl('', array( 'absolute' => TRUE, 'query' => array( 'filter' => array($public_field_name => reset($ids)), ), )); $links['self'] = $resource_plugin->versionedUrl($parent_id . '/relationships/' . $public_field_name); } return $output + array( '#embedded' => TRUE, '#cardinality' => $cardinality, '#relationship_links' => $links, ); } /** * Checks if a resource field needs to be embedded in the response. * * @param ResourceFieldResourceInterface $resource_field * The embedded resource field. * @param array $parents * The parents of this embedded resource. * * @return bool * TRUE if the field needs including. FALSE otherwise. */ protected function needsIncluding(ResourceFieldResourceInterface $resource_field, $parents) { $input = $this->getRequest()->getParsedInput(); $includes = empty($input['include']) ? array() : explode(',', $input['include']); return in_array($this->buildIncludePath($parents, $resource_field->getPublicName()), $includes); } /** * Build the dot notation path for an array of parents. * * Remove numeric parents since those only indicate that the field was * multivalue, not a parent: articles[related][1][tags][2][name] turns into * 'articles.related.tags.name'. * * @param array $parents * The nested parents. * @param string $public_field_name * The field name. * * @return string * The path. */ protected function buildIncludePath(array $parents, $public_field_name = NULL) { $array_path = $parents; if ($public_field_name) { array_push($array_path, $public_field_name); } $include_path = implode('.', array_filter($array_path, function ($item) { return !is_numeric($item); })); return $include_path; } /** * Given a field item that contains a cache placeholder render and cache it. * * @param array $field_item * The output to render. * * @param string $includes_path * The includes path encoded in dot notation. * * @return array * The rendered embedded field item. * * @throws \Drupal\restful\Exception\BadRequestException * @throws \Drupal\restful\Exception\InternalServerErrorException */ protected function populateCachePlaceholder(array $field_item, $includes_path) { if ( empty($field_item['#cache_placeholder']) || empty($field_item['#resource_id']) || empty($field_item['#resource_plugin']) ) { return $field_item; } $embedded_resource = restful() ->getResourceManager() ->getPluginCopy($field_item['#resource_plugin']); $input = $this->getRequest()->getParsedInput(); $new_input = $input + array('include' => '', 'fields' => ''); // If the field is not supposed to be included, then bail. $old_includes = array_filter(explode(',', $new_input['include'])); if (!in_array($includes_path, $old_includes)) { return $field_item; } $new_input['fields'] = implode(',', $this->unprefixInputOptions(explode(',', $new_input['fields']), $includes_path)); $new_input['include'] = implode(',', $this->unprefixInputOptions($old_includes, $includes_path)); // Create a new request from scratch copying most of the values but the // $query. $embedded_resource->setRequest(Request::create( $this->getRequest()->getPath(), array_filter($new_input), $this->getRequest()->getMethod(), $this->getRequest()->getHeaders(), $this->getRequest()->isViaRouter(), $this->getRequest()->getCsrfToken(), $this->getRequest()->getCookies(), $this->getRequest()->getFiles(), $this->getRequest()->getServer(), $this->getRequest()->getParsedBody() )); try { $data = $embedded_resource->getDataProvider() ->view($field_item['#resource_id']); } catch (InaccessibleRecordException $e) { // Populate it with an empty element. $data = array(); } return array_merge( $field_item, $this->extractFieldValues($data, $field_item['#cache_placeholder']['parents'], $field_item['#cache_placeholder']['parent_hashes']) ); } /** * {@inheritdoc} */ public function parseBody($body) { if (!$decoded_json = drupal_json_decode($body)) { throw new BadRequestException(sprintf('Invalid JSON provided: %s.', $body)); } if (empty($decoded_json['data'])) { throw new BadRequestException(sprintf('Invalid JSON provided: %s.', $body)); } $data = $decoded_json['data']; $includes = empty($decoded_json['included']) ? array() : $decoded_json['included']; // It's always weird to deal with lists of items vs a single item. $single_item = !ResourceFieldBase::isArrayNumeric($data); // Make sure we're always dealing with a list of items. $data = $single_item ? array($data) : $data; $output = array(); foreach ($data as $item) { $output[] = $this::restructureItem($item, $includes); } return $single_item ? reset($output) : $output; } /** * Take a JSON API item and makes it hierarchical object, like simple JSON. * * @param array $item * The JSON API item. * @param array $included * The included pool of elements. * * @return array * The hierarchical object. * * @throws \Drupal\restful\Exception\BadRequestException */ protected static function restructureItem(array $item, array $included) { if (empty($item['meta']['subrequest']) && empty($item['attributes']) && empty($item['relationship'])) { throw new BadRequestException('Invalid JSON provided: both attributes and relationship are empty.'); } // Make sure that the attributes and relationships are accessible. $element = empty($item['attributes']) ? array() : $item['attributes']; $relationships = empty($item['relationships']) ? array() : $item['relationships']; // For every relationship we need to see if it was included. foreach ($relationships as $field_name => $relationship) { if (empty($relationship['data'])) { throw new BadRequestException('Invalid JSON provided: relationship without data.'); } $data = $relationship['data']; // It's always weird to deal with lists of items vs a single item. $single_item = !ResourceFieldBase::isArrayNumeric($data); // Make sure we're always dealing with a list of items. $data = $single_item ? array($data) : $data; $element[$field_name] = array(); foreach ($data as $info_pair) { // Validate the JSON API structure for a relationship. if (empty($info_pair['type'])) { throw new BadRequestException('Invalid JSON provided: relationship item without type.'); } if (empty($info_pair['id'])) { throw new BadRequestException('Invalid JSON provided: relationship item without id.'); } // Initialize the object if empty. if ( !empty($info_pair['meta']['subrequest']) && $included_item = static::retrieveIncludedItem($info_pair['type'], $info_pair['id'], $included) ) { // If the relationship was included, restructure it and embed it. $value = array( 'body' => static::restructureItem($included_item, $included), 'id' => $info_pair['id'], 'request' => $info_pair['meta']['subrequest'], ); if (!empty($value['request']['method']) && $value['request']['method'] == RequestInterface::METHOD_POST) { // If the value is a POST remove the ID, since we already // retrieved the included item. unset($value['id']); } $element[$field_name][] = $value; } else { // If the include could not be retrieved, use the ID instead. $element[$field_name][] = array('id' => $info_pair['id']); } } // Make the single relationships to be a single item or a single ID. $element[$field_name] = $single_item ? reset($element[$field_name]) : $element[$field_name]; } return $element; } /** * Retrieves an item from the included pool of items. * * @param string $type * The resource type. * @param string $id * The resource identifier. * @param array $included * All the available included elements. * * @return array * The JSON API element. */ protected static function retrieveIncludedItem($type, $id, array $included) { foreach ($included as $item) { if ( !empty($item['type']) && $item['type'] == $type && !empty($item['id']) && $item['id'] == $id ) { return $item; } } return NULL; } } ================================================ FILE: src/Plugin/formatter/FormatterSingleJson.php ================================================ contentType = 'application/problem+json; charset=utf-8'; return $data; } $extracted = $this->extractFieldValues($data); $output = $this->limitFields($extracted); // Force returning a single item. $output = ResourceFieldBase::isArrayNumeric($output) ? reset($output) : $output; return $output ?: array(); } } ================================================ FILE: src/Plugin/rate_limit/RateLimit.php ================================================ period = new \DateInterval($configuration['period']); } catch (\Exception $e) { throw new ServerConfigurationException(sprintf('Invalid rate limit period: %s. Should be a valid format of \DateInterval.', $configuration['period'])); } $this->limits = $configuration['limits']; $this->resource = $configuration['resource']; } /** * {@inheritdoc} */ public function setLimit($limits) { $this->limits = $limits; } /** * {@inheritdoc} */ public function getLimit($account = NULL) { // If the user is anonymous. if (empty($account->roles)) { return $this->limits['anonymous user']; } // If the user is logged then return the best limit for all the roles the // user has. $max_limit = 0; foreach ($account->roles as $rid => $role) { if (!isset($this->limits[$role])) { // No limit configured for this role. continue; } if ($this->limits[$role] < $max_limit && $this->limits[$role] != RateLimitManager::UNLIMITED_RATE_LIMIT) { // The limit is smaller than one previously found. continue; } // This is the highest limit for the current user given all their roles. $max_limit = $this->limits[$role]; } return $max_limit; } /** * {@inheritdoc} */ public function setPeriod(\DateInterval $period) { $this->period = $period; } /** * {@inheritdoc} */ public function getPeriod() { return $this->period; } /** * {@inheritdoc} */ public function generateIdentifier($account = NULL) { $identifier = $this->resource->getResourceName() . PluginBase::DERIVATIVE_SEPARATOR; if ($this->getPluginId() == 'global') { // Don't split the id by resource if the event is global. $identifier = ''; } $identifier .= $this->getPluginId() . PluginBase::DERIVATIVE_SEPARATOR; $identifier .= empty($account->uid) ? ip_address() : $account->uid; return $identifier; } /** * {@inheritdoc} */ public function loadRateLimitEntity($account = NULL) { $query = new \EntityFieldQuery(); $results = $query ->entityCondition('entity_type', 'rate_limit') ->entityCondition('bundle', $this->getPluginId()) ->propertyCondition('identifier', $this->generateIdentifier($account)) ->execute(); if (empty($results['rate_limit'])) { return NULL; } $rlid = key($results['rate_limit']); return entity_load_single('rate_limit', $rlid); } } ================================================ FILE: src/Plugin/rate_limit/RateLimitGlobal.php ================================================ $role_info) { $this->limits[$rid] = $limit; } $this->period = new \DateInterval(variable_get('restful_global_rate_period', 'P1D')); } /** * {@inheritdoc} */ public function generateIdentifier($account = NULL) { $identifier = ''; $identifier .= $this->getPluginId() . PluginBase::DERIVATIVE_SEPARATOR; $identifier .= empty($account->uid) ? ip_address() : $account->uid; return $identifier; } /** * {@inheritdoc} * * All limits are the same for the global limit. Return the first one. */ public function getLimit($account = NULL) { return reset($this->limits); } /** * {@inheritdoc} * * Only track the global limit for the current user if the variable is on. */ public function isRequestedEvent(RequestInterface $request) { return $this->getLimit() > 0; } } ================================================ FILE: src/Plugin/rate_limit/RateLimitInterface.php ================================================ authenticationManager = $authentication_manager; } /** * {@inheritdoc} */ public function getAuthenticationManager() { return $this->authenticationManager; } /** * Data provider factory. * * @return DataProviderInterface * The data provider for this resource. * * @throws NotImplementedException */ public function dataProviderFactory() { return $this->subject->dataProviderFactory(); } /** * Proxy method to get the account from the authenticationManager. * * {@inheritdoc} */ public function getAccount($cache = TRUE) { // The request. $request = $this->subject->getRequest(); $account = $this->getAuthenticationManager()->getAccount($request, $cache); return $account; } /** * {@inheritdoc} */ public function getRequest() { return $this->subject->getRequest(); } /** * {@inheritdoc} */ public function getPath() { return $this->subject->getPath(); } /** * {@inheritdoc} */ public function getFieldDefinitions() { return $this->subject->getFieldDefinitions(); } /** * {@inheritdoc} */ public function getDataProvider() { return $this->subject->getDataProvider(); } /** * Constructs a Drupal\Component\Plugin\PluginBase object. * * @param ResourceInterface $subject * The decorated object. * @param AuthenticationManager $auth_manager * (optional) Injected authentication manager. */ public function __construct(ResourceInterface $subject, AuthenticationManager $auth_manager = NULL) { $this->subject = $subject; $this->authenticationManager = $auth_manager ? $auth_manager : new AuthenticationManager(); } /** * {@inheritdoc} */ public function process() { return $this->subject->process(); } /** * {@inheritdoc} */ public function controllersInfo() { return $this->subject->controllersInfo(); } /** * {@inheritdoc} */ public function getControllers() { return $this->subject->getControllers(); } /** * {@inheritdoc} */ public function getResourceName() { return $this->subject->getResourceName(); } /** * {@inheritdoc} */ public function index($path) { return $this->subject->index($path); } /** * {@inheritdoc} */ public function view($path) { return $this->subject->view($path); } /** * {@inheritdoc} */ public function create($path) { return $this->subject->create($path); } /** * {@inheritdoc} */ public function update($path) { return $this->subject->update($path); } /** * {@inheritdoc} */ public function replace($path) { return $this->subject->replace($path); } /** * {@inheritdoc} */ public function remove($path) { $this->subject->remove($path); } /** * {@inheritdoc} */ public function getVersion() { return $this->subject->getVersion(); } } ================================================ FILE: src/Plugin/resource/AuthenticatedResourceInterface.php ================================================ array( 'callback' => '\Drupal\restful\Plugin\resource\CsrfToken::getCsrfToken', ), ); } /** * Value callback; Return the CSRF token. * * @return string * The token. */ public static function getCsrfToken() { return drupal_get_token(\Drupal\restful\Plugin\authentication\Authentication::TOKEN_VALUE); } /** * {@inheritdoc} */ public function index($path) { $values = array(); foreach ($this->publicFields() as $public_property => $info) { $value = NULL; if ($info['callback']) { $value = ResourceManager::executeCallback($info['callback']); } if ($value && !empty($info['process_callbacks'])) { foreach ($info['process_callbacks'] as $process_callback) { $value = ResourceManager::executeCallback($process_callback, array($value)); } } $values[$public_property] = $value; } return $values; } } ================================================ FILE: src/Plugin/resource/DataInterpreter/ArrayWrapper.php ================================================ data = $data; } /** * {@inheritdoc} */ public function get($key) { return isset($this->data[$key]) ? $this->data[$key] : NULL; } } ================================================ FILE: src/Plugin/resource/DataInterpreter/ArrayWrapperInterface.php ================================================ account = $account; $this->wrapper = $wrapper; } /** * {@inheritdoc} */ public function getAccount() { return $this->account; } /** * {@inheritdoc} */ public function getWrapper() { return $this->wrapper; } } ================================================ FILE: src/Plugin/resource/DataInterpreter/DataInterpreterEMW.php ================================================ plugin = $plugin; $this->pluginDefinition = $plugin->getPluginDefinition(); // For configurable plugins, expose those properties as well. if ($plugin instanceof ConfigurablePluginInterface) { $this->pluginConfiguration = $plugin->getConfiguration(); } } /** * {@inheritdoc} */ public function get($key) { // If there is a key by that name in the plugin configuration return it, if // not then check the plugin definition. If it cannot be found, return NULL. $value = isset($this->pluginConfiguration[$key]) ? $this->pluginConfiguration[$key] : NULL; return $value ? $value : (isset($this->pluginDefinition[$key]) ? $this->pluginDefinition[$key] : NULL); } } ================================================ FILE: src/Plugin/resource/DataInterpreter/PluginWrapperInterface.php ================================================ subject = $subject; $this->cacheController = $cache_controller; } /** * {@inheritdoc} */ public static function isNestedField($field_name) { return DataProvider::isNestedField($field_name); } /** * {@inheritdoc} */ public static function processFilterInput($filter, $public_field) { return DataProvider::processFilterInput($filter, $public_field); } /** * {@inheritdoc} */ public function discover($path = NULL) { return $this->subject->discover($path); } /** * {@inheritdoc} */ public function getRange() { return $this->subject->getRange(); } /** * {@inheritdoc} */ public function setRange($range) { return $this->subject->setRange($range); } /** * {@inheritdoc} */ public function getAccount() { return $this->subject->getAccount(); } /** * {@inheritdoc} */ public function setAccount($account) { $this->subject->setAccount($account); } /** * {@inheritdoc} */ public function getRequest() { return $this->subject->getRequest(); } /** * {@inheritdoc} */ public function setRequest(RequestInterface $request) { $this->subject->setRequest($request); } /** * {@inheritdoc} */ public function getLangCode() { return $this->subject->getLangCode(); } /** * {@inheritdoc} */ public function setLangCode($langcode) { $this->subject->setLangCode($langcode); } /** * {@inheritdoc} */ public function getOptions() { return $this->subject->getOptions(); } /** * {@inheritdoc} */ public function addOptions(array $options) { $this->subject->addOptions($options); } /** * {@inheritdoc} */ public function getCacheFragments($identifier) { return $this->subject->getCacheFragments($identifier); } /** * {@inheritdoc} */ public function index() { // TODO: This is duplicating the code from DataProvider::index $ids = $this->getIndexIds(); return $this->viewMultiple($ids); } /** * {@inheritdoc} */ public function getIndexIds() { return $this->subject->getIndexIds(); } /** * {@inheritdoc} */ public function count() { return $this->subject->count(); } /** * {@inheritdoc} */ public function create($object) { return $this->subject->create($object); } /** * {@inheritdoc} */ public function view($identifier) { $resource_field_collection = $this->subject->view($identifier); if (!$resource_field_collection instanceof ResourceFieldCollectionInterface) { return NULL; } $resource_field_collection->setContext('cache_fragments', $this->getCacheFragments($identifier)); return $resource_field_collection; } /** * {@inheritdoc} */ public function viewMultiple(array $identifiers) { $return = array(); // If no IDs were requested, we should not throw an exception in case an // entity is un-accessible by the user. foreach ($identifiers as $identifier) { try { $row = $this->view($identifier); } catch (InaccessibleRecordException $e) { $row = NULL; } $return[] = $row; } return array_values(array_filter($return)); } /** * {@inheritdoc} */ public function update($identifier, $object, $replace = TRUE) { $this->clearRenderedCache($this->getCacheFragments($identifier)); return $this->subject->update($identifier, $object, $replace); } /** * {@inheritdoc} */ public function remove($identifier) { $this->clearRenderedCache($this->getCacheFragments($identifier)); $this->subject->remove($identifier); } /** * {@inheritdoc} */ public function methodAccess(ResourceFieldInterface $resource_field) { $this->subject->methodAccess($resource_field); } /** * {@inheritdoc} */ public function canonicalPath($path) { return $this->subject->canonicalPath($path); } /** * {@inheritdoc} */ public function setOptions(array $options) { $this->subject->setOptions($options); } /** * {@inheritdoc} */ public function setResourcePath($resource_path) { $this->subject->setResourcePath($resource_path); } /** * {@inheritdoc} */ public function getResourcePath() { return $this->subject->getResourcePath(); } /** * {@inheritdoc} */ public function getMetadata() { return $this->subject->getMetadata(); } /** * Clears the cache entries related to the given cache fragments. * * @param \Doctrine\Common\Collections\ArrayCollection $cache_fragments * The cache fragments to clear. */ protected function clearRenderedCache(ArrayCollection $cache_fragments) { $cache_object = new RenderCache($cache_fragments, NULL, $this->cacheController); $cache_object->clear(); } /** * Checks if the decorated object is an instance of something. * * @param string $class * Class or interface to check the instance. * * @return bool * TRUE if the decorated object is an instace of the $class. FALSE * otherwise. */ public function isInstanceOf($class) { if ($this instanceof $class || $this->subject instanceof $class) { return TRUE; } // Check if the decorated resource is also a decorator. if ($this->subject instanceof ExplorableDecoratorInterface) { return $this->subject->isInstanceOf($class); } return FALSE; } } ================================================ FILE: src/Plugin/resource/DataProvider/CacheDecoratedDataProviderInterface.php ================================================ $filter); } if (!isset($filter['value'])) { throw new BadRequestException(sprintf('Value not present for the "%s" filter. Please check the URL format.', $public_field)); } if (!is_array($filter['value'])) { $filter['value'] = array($filter['value']); } // Add the property. $filter['public_field'] = $public_field; // Set default operator. $filter += array('operator' => array_fill(0, count($filter['value']), '=')); if (!is_array($filter['operator'])) { $filter['operator'] = array($filter['operator']); } // Make sure that we have the same amount of operators than values. $first_operator = strtoupper($filter['operator'][0]); if (!in_array($first_operator, array( 'IN', 'NOT IN', 'BETWEEN', )) && count($filter['value']) != count($filter['operator']) ) { throw new BadRequestException('The number of operators and values has to be the same.'); } // Make sure that the BETWEEN operator gets only 2 values. if ($first_operator == 'BETWEEN' && count($filter['value']) != 2) { throw new BadRequestException('The BETWEEN operator takes exactly 2 values.'); } $filter += array('conjunction' => 'AND'); // Clean the operator in case it came from the URL. // e.g. filter[minor_version][operator][0]=">=" // str_replace will process all the elements in the array. $filter['operator'] = str_replace(array('"', "'"), '', $filter['operator']); static::isValidOperatorsForFilter($filter['operator']); static::isValidConjunctionForFilter($filter['conjunction']); return $filter; } /** * Constructor. * * @param RequestInterface $request * The request. * @param ResourceFieldCollectionInterface $field_definitions * The field definitions. * @param object $account * The authenticated account. * @param string $plugin_id * The resource ID. * @param string $resource_path * The resource path. * @param array $options * The plugin options for the data provider. * @param string $langcode * (Optional) The entity language code. */ public function __construct(RequestInterface $request, ResourceFieldCollectionInterface $field_definitions, $account, $plugin_id, $resource_path = NULL, array $options = array(), $langcode = NULL) { $this->request = $request; $this->fieldDefinitions = $field_definitions; $this->account = $account; $this->pluginId = $plugin_id; $this->options = $options; $this->resourcePath = $resource_path; if (!empty($options['range'])) { // TODO: Document that the range is now overridable in the annotation. $this->range = $options['range']; } $this->langcode = $langcode ?: static::getLanguage(); $this->metadata = new ArrayCollection(); } /** * {@inheritdoc} */ public function getRange() { return $this->range; } /** * {@inheritdoc} */ public function setRange($range) { $this->range = $range; } /** * {@inheritdoc} */ public function getAccount() { return $this->account; } /** * {@inheritdoc} */ public function setAccount($account) { $this->account = $account; } /** * {@inheritdoc} */ public function getRequest() { return $this->request; } /** * {@inheritdoc} */ public function setRequest(RequestInterface $request) { $this->request = $request; } /** * {@inheritdoc} */ public function getLangCode() { return $this->langcode; } /** * {@inheritdoc} */ public function setLangCode($langcode) { $this->langcode = $langcode; } /** * {@inheritdoc} */ public function getOptions() { return $this->options; } /** * {@inheritdoc} */ public function setOptions(array $options) { $this->options = $options; } /** * {@inheritdoc} */ public function addOptions(array $options) { $this->options = array_merge($this->options, $options); } /** * {@inheritdoc} */ public function getCacheFragments($identifier) { // If we are trying to get the context for multiple ids, join them. if (is_array($identifier)) { $identifier = implode(',', $identifier); } $fragments = new ArrayCollection(array( 'resource' => CacheDecoratedResource::serializeKeyValue($this->pluginId, $this->canonicalPath($identifier)), )); $options = $this->getOptions(); switch ($options['renderCache']['granularity']) { case DRUPAL_CACHE_PER_USER: if ($uid = $this->getAccount()->uid) { $fragments->set('user_id', (int) $uid); } break; case DRUPAL_CACHE_PER_ROLE: $fragments->set('user_role', implode(',', $this->getAccount()->roles)); break; } return $fragments; } /** * {@inheritdoc} */ public function index() { if (!$ids = $this->getIndexIds()) { return array(); } return $this->viewMultiple($ids); } /** * {@inheritdoc} */ public function discover($path = NULL) { // Alter the field definition by adding a callback to get the auto // discover information in render time. foreach ($this->fieldDefinitions as $public_field_name => $resource_field) { /* @var ResourceFieldInterface $resource_field */ if (method_exists($resource_field, 'autoDiscovery')) { // Adding the autoDiscover method to the resource field class will allow // you to be smarter about the auto discovery information. $callable = array($resource_field, 'autoDiscovery'); } else { // If the given field does not have discovery information, provide the // empty one instead of an error. $callable = array('\Drupal\restful\Plugin\resource\Field\ResourceFieldBase::emptyDiscoveryInfo', array($public_field_name)); } $resource_field->setCallback($callable); // Remove the process callbacks, those don't make sense during discovery. $resource_field->setProcessCallbacks(array()); $definition = $resource_field->getDefinition(); $discovery_info = empty($definition['discovery']) ? array() : $definition['discovery']; $resource_field->setPublicFieldInfo(new PublicFieldInfoBase($resource_field->getPublicName(), $discovery_info)); } return $path ? $this->viewMultiple(array($path)) : $this->index(); } /** * {@inheritdoc} */ public function canonicalPath($path) { // Assume that there is no alias. return $path; } /** * {@inheritdoc} */ public function methodAccess(ResourceFieldInterface $resource_field) { return in_array($this->getRequest()->getMethod(), $resource_field->getMethods()); } /** * Parses the request to get the sorting options. * * @return array * With the different sorting options. * * @throws \Drupal\restful\Exception\BadRequestException * @throws \Drupal\restful\Exception\UnprocessableEntityException */ protected function parseRequestForListSort() { $input = $this->getRequest()->getParsedInput(); if (empty($input['sort'])) { return array(); } $url_params = $this->options['urlParams']; if (!$url_params['sort']) { throw new UnprocessableEntityException('Sort parameters have been disabled in server configuration.'); } $sorts = array(); foreach (explode(',', $input['sort']) as $sort) { $direction = $sort[0] == '-' ? 'DESC' : 'ASC'; $sort = str_replace('-', '', $sort); // Check the sort is on a legal key. if (!$this->fieldDefinitions->get($sort)) { throw new BadRequestException(format_string('The sort @sort is not allowed for this path.', array('@sort' => $sort))); } $sorts[$sort] = $direction; } return $sorts; } /** * Filter the query for list. * * @returns array * An array of filters to apply. * * @throws \Drupal\restful\Exception\BadRequestException * @throws \Drupal\restful\Exception\UnprocessableEntityException * * @see \RestfulEntityBase::getQueryForList */ protected function parseRequestForListFilter() { if (!$this->request->isListRequest($this->getResourcePath())) { // Not a list request, so we don't need to filter. // We explicitly check this, as this function might be called from a // formatter plugin, after RESTful's error handling has finished, and an // invalid key might be passed. return array(); } $input = $this->getRequest()->getParsedInput(); if (empty($input['filter'])) { // No filtering is needed. return array(); } $url_params = empty($this->options['urlParams']) ? array() : $this->options['urlParams']; if (isset($url_params['filter']) && !$url_params['filter']) { throw new UnprocessableEntityException('Filter parameters have been disabled in server configuration.'); } $filters = array(); foreach ($input['filter'] as $public_field => $value) { if (!static::isNestedField($public_field) && !$this->fieldDefinitions->get($public_field)) { throw new BadRequestException(format_string('The filter @filter is not allowed for this path.', array('@filter' => $public_field))); } $filter = static::processFilterInput($value, $public_field); $filters[] = $filter + array('resource_id' => $this->pluginId); } return $filters; } /** * Parses the request object to get the pagination options. * * @return array * A numeric array with the offset and length options. * * @throws BadRequestException * @throws UnprocessableEntityException */ protected function parseRequestForListPagination() { $pager_input = $this->getRequest()->getPagerInput(); $page = isset($pager_input['number']) ? $pager_input['number'] : 1; if (!ctype_digit((string) $page) || $page < 1) { throw new BadRequestException('"Page" property should be numeric and equal or higher than 1.'); } $range = isset($pager_input['size']) ? (int) $pager_input['size'] : $this->getRange(); $range = $range > $this->getRange() ? $this->getRange() : $range; if (!ctype_digit((string) $range) || $range < 1) { throw new BadRequestException('"Range" property should be numeric and equal or higher than 1.'); } $url_params = empty($this->options['urlParams']) ? array() : $this->options['urlParams']; if (isset($url_params['range']) && !$url_params['range']) { throw new UnprocessableEntityException('Range parameters have been disabled in server configuration.'); } $offset = ($page - 1) * $range; return array($offset, $range); } /** * Adds query tags and metadata to the EntityFieldQuery. * * @param \EntityFieldQuery|\SelectQuery $query * The query to enhance. */ protected function addExtraInfoToQuery($query) { // Add a generic tags to the query. $query->addTag('restful'); $query->addMetaData('account', $this->getAccount()); } /** * Check if an operator is valid for filtering. * * @param array $operators * The array of operators. * * @throws BadRequestException */ protected static function isValidOperatorsForFilter(array $operators) { $allowed_operators = array( '=', '>', '<', '>=', '<=', '<>', '!=', 'NOT IN', 'BETWEEN', 'CONTAINS', 'IN', 'NOT IN', 'STARTS_WITH', ); foreach ($operators as $operator) { if (!in_array($operator, $allowed_operators)) { throw new BadRequestException(sprintf('Operator "%s" is not allowed for filtering on this resource. Allowed operators are: %s', $operator, implode(', ', $allowed_operators))); } } } /** * Check if a conjunction is valid for filtering. * * @param string $conjunction * The operator. * * @throws \Drupal\restful\Exception\BadRequestException */ protected static function isValidConjunctionForFilter($conjunction) { $allowed_conjunctions = array( 'AND', 'OR', 'XOR', ); if (!in_array(strtoupper($conjunction), $allowed_conjunctions)) { throw new BadRequestException(format_string('Conjunction "@conjunction" is not allowed for filtering on this resource. Allowed conjunctions are: !allowed', array( '@conjunction' => $conjunction, '!allowed' => implode(', ', $allowed_conjunctions), ))); } } /** * Gets the global language. * * @return string * The language code. */ protected static function getLanguage() { // Move to its own method to allow unit testing. return $GLOBALS['language']->language; } /** * Sets an HTTP header. * * @param string $name * The header name. * @param string $value * The header value. */ protected function setHttpHeader($name, $value) { $this ->getRequest() ->getHeaders() ->add(HttpHeader::create($name, $value)); } /** * {@inheritdoc} */ public function setResourcePath($resource_path) { $this->resourcePath = $resource_path; } /** * {@inheritdoc} */ public function getResourcePath() { return $this->resourcePath; } /** * {@inheritdoc} */ public static function isNestedField($field_name) { return strpos($field_name, '.') !== FALSE; } /** * {@inheritdoc} */ public function getMetadata() { return $this->metadata; } /** * Initialize the empty resource field collection to bundle the output. * * @param mixed $identifier * The ID of thing being viewed. * * @return ResourceFieldCollectionInterface * The collection of fields. * * @throws \Drupal\restful\Exception\NotFoundException */ protected function initResourceFieldCollection($identifier) { $resource_field_collection = new ResourceFieldCollection(array(), $this->getRequest()); $interpreter = $this->initDataInterpreter($identifier); $resource_field_collection->setInterpreter($interpreter); $id_field_name = empty($this->options['idField']) ? 'id' : $this->options['idField']; $resource_field_collection->setIdField($this->fieldDefinitions->get($id_field_name)); $resource_field_collection->setResourceId($this->pluginId); return $resource_field_collection; } /** * Get the data interpreter. * * @param mixed $identifier * The ID of thing being viewed. * * @return \Drupal\restful\Plugin\resource\DataInterpreter\DataInterpreterInterface * The data interpreter. */ abstract protected function initDataInterpreter($identifier); } ================================================ FILE: src/Plugin/resource/DataProvider/DataProviderDbQuery.php ================================================ options[$required_key]) { throw new ServiceUnavailableException(sprintf('%s is missing "%s" property in the "dataProvider" key of the $plugin', get_class($this), $required_key)); } }; array_walk($required_keys, $required_callback); $this->tableName = $this->options['tableName']; $this->idColumn = $this->options['idColumn']; $this->primary = empty($this->options['primary']) ? NULL : $this->primary = $this->options['primary']; } /** * {@inheritdoc} */ public function getTableName() { return $this->tableName; } /** * {@inheritdoc} */ public function setTableName($table_name) { $this->tableName = $table_name; } /** * {@inheritdoc} */ public function getPrimary() { return $this->primary; } /** * {@inheritdoc} */ public function setPrimary($primary) { $this->primary = $primary; } /** * {@inheritdoc} */ public function getCacheFragments($identifier) { if (is_array($identifier)) { // Like in https://example.org/api/articles/1,2,3. $identifier = implode(ResourceInterface::IDS_SEPARATOR, $identifier); } $fragments = new ArrayCollection(array( 'resource' => CacheDecoratedResource::serializeKeyValue($this->pluginId, $this->canonicalPath($identifier)), 'table_name' => $this->getTableName(), 'column' => implode(',', $this->getIdColumn()), )); $options = $this->getOptions(); switch ($options['renderCache']['granularity']) { case DRUPAL_CACHE_PER_USER: if ($uid = $this->getAccount()->uid) { $fragments->set('user_id', (int) $uid); } break; case DRUPAL_CACHE_PER_ROLE: $fragments->set('user_role', implode(',', $this->getAccount()->roles)); break; } return $fragments; } /** * {@inheritdoc} */ public function count() { return intval($this ->getQueryForList() ->countQuery() ->execute() ->fetchField()); } /** * {@inheritdoc} */ public function isPrimaryField($field_name) { return $this->primary == $field_name; } /** * Get ID column. * * @return array * An array with the name of the column(s) in the table to be used as the * unique key. */ protected function getIdColumn() { return is_array($this->idColumn) ? $this->idColumn : array($this->idColumn); } /** * {@inheritdoc} */ public function create($object) { $save = FALSE; $original_object = $object; $id_columns = $this->getIdColumn(); $record = array(); foreach ($this->fieldDefinitions as $public_field_name => $resource_field) { /* @var ResourceFieldDbColumnInterface $resource_field */ if (!$this->methodAccess($resource_field)) { // Allow passing the value in the request. unset($original_object[$public_field_name]); continue; } $property_name = $resource_field->getProperty(); // If this is the primary field, skip. if ($this->isPrimaryField($property_name)) { unset($original_object[$public_field_name]); continue; } if (isset($object[$public_field_name])) { $record[$property_name] = $object[$public_field_name]; } unset($original_object[$public_field_name]); $save = TRUE; } // No request was sent. if (!$save) { throw new BadRequestException('No values were sent with the request.'); } // If the original request is not empty, then illegal values are present. if (!empty($original_object)) { $error_message = format_plural(count($original_object), 'Property @names is invalid.', 'Property @names are invalid.', array('@names' => implode(', ', array_keys($original_object)))); throw new BadRequestException($error_message); } // Once the record is built, write it and view it. if (drupal_write_record($this->getTableName(), $record)) { // Handle multiple id columns. $id_values = array(); foreach ($id_columns as $id_column) { $id_values[$id_column] = $record[$id_column]; } $new_id = implode(self::COLUMN_IDS_SEPARATOR, $id_values); return array($this->view($new_id)); } return NULL; } /** * {@inheritdoc} */ public function view($identifier) { $query = $this->getQuery(); foreach ($this->getIdColumn() as $index => $column) { $identifier = is_array($identifier) ? $identifier : array($identifier); $query->condition($this->getTableName() . '.' . $column, current($this->getColumnFromIds($identifier, $index))); } $this->addExtraInfoToQuery($query); $result = $query ->range(0, 1) ->execute() ->fetch(\PDO::FETCH_OBJ); return $this->mapDbRowToPublicFields($result); } /** * {@inheritdoc} */ protected function mapDbRowToPublicFields($row) { $resource_field_collection = $this->initResourceFieldCollection($row); // Loop over all the defined public fields. foreach ($this->fieldDefinitions as $public_field_name => $resource_field) { $value = NULL; /* @var ResourceFieldDbColumnInterface $resource_field */ if (!$this->methodAccess($resource_field)) { // Allow passing the value in the request. continue; } $resource_field_collection->set($resource_field->id(), $resource_field); } return $resource_field_collection; } /** * Adds query tags and metadata to the EntityFieldQuery. * * @param \SelectQuery $query * The query to enhance. */ protected function addExtraInfoToQuery($query) { $query->addTag('restful'); $query->addMetaData('account', $this->getAccount()); $query->addMetaData('restful_handler', $this); } /** * {@inheritdoc} */ public function viewMultiple(array $identifiers) { // Get a list query with all the sorting and pagination in place. $query = $this->getQueryForList(); if (empty($identifiers)) { return array(); } foreach ($this->getIdColumn() as $index => $column) { $query->condition($this->getTableName() . '.' . $column, $this->getColumnFromIds($identifiers, $index), 'IN'); } $results = $query->execute(); $return = array(); foreach ($results as $result) { $return[] = $this->mapDbRowToPublicFields($result); } return $return; } /** * {@inheritdoc} */ protected function getQueryForList() { $query = $this->getQuery(); $this->queryForListSort($query); $this->queryForListFilter($query); $this->queryForListPagination($query); $this->addExtraInfoToQuery($query); return $query; } /** * {@inheritdoc} */ public function update($identifier, $object, $replace = FALSE) { // Build the update array. $save = FALSE; $original_object = $object; $id_columns = $this->getIdColumn(); $record = array(); foreach ($this->fieldDefinitions as $public_field_name => $resource_field) { /* @var ResourceFieldDbColumnInterface $resource_field */ if (!$this->methodAccess($resource_field)) { // Allow passing the value in the request. unset($original_object[$public_field_name]); continue; } $property = $resource_field->getProperty(); // If this is the primary field, skip. if ($this->isPrimaryField($property)) { continue; } if (isset($object[$public_field_name])) { $record[$property] = $object[$public_field_name]; } // For unset fields on full updates, pass NULL to drupal_write_record(). elseif ($replace) { $record[$property] = NULL; } unset($original_object[$public_field_name]); $save = TRUE; } // No request was sent. if (!$save) { throw new BadRequestException('No values were sent with the request.'); } // If the original request is not empty, then illegal values are present. if (!empty($original_object)) { $error_message = format_plural(count($original_object), 'Property @names is invalid.', 'Property @names are invalid.', array('@names' => implode(', ', array_keys($original_object)))); throw new BadRequestException($error_message); } // Add the id column values into the record. foreach ($this->getIdColumn() as $index => $column) { $record[$column] = current($this->getColumnFromIds(array($identifier), $index)); } // Once the record is built, write it. if (!drupal_write_record($this->getTableName(), $record, $id_columns)) { throw new ServiceUnavailableException('Record could not be updated to the database.'); } return array($this->view($identifier)); } /** * {@inheritdoc} */ public function remove($identifier) { // If it's a delete method we will want a 204 response code. // Set the HTTP headers. $this->setHttpHeader('Status', 204); $query = db_delete($this->getTableName()); foreach ($this->getIdColumn() as $index => $column) { $query->condition($column, current($this->getColumnFromIds(array($identifier), $index))); } $query->execute(); } /** * {@inheritdoc} */ public function getIndexIds() { $results = $this ->getQueryForList() ->execute(); $ids = array(); foreach ($results as $result) { $ids[] = array_map(function ($id_column) use ($result) { return $result->{$id_column}; }, $this->getIdColumn()); } return $ids; } /** * {@inheritdoc} */ public function index() { $results = $this ->getQueryForList() ->execute(); $return = array(); foreach ($results as $result) { $return[] = $this->mapDbRowToPublicFields($result); } return $return; } /** * Defines default sort columns if none are provided via the request URL. * * @return array * Array keyed by the database column name, and the order ('ASC' or 'DESC') * as value. */ protected function defaultSortInfo() { $sorts = array(); foreach ($this->getIdColumn() as $column) { if (!$this->fieldDefinitions->get($column)) { // Sort by the first ID column that is a public field. $sorts[$column] = 'ASC'; break; } } return $sorts; } /** * Sort the query for list. * * @param \SelectQuery $query * The query object. * * @throws BadRequestException * * @see \RestfulEntityBase::getQueryForList */ protected function queryForListSort(\SelectQuery $query) { // Get the sorting options from the request object. $sorts = $this->parseRequestForListSort(); $sorts = $sorts ? $sorts : $this->defaultSortInfo(); foreach ($sorts as $sort => $direction) { /* @var ResourceFieldDbColumnInterface $sort_field */ if ($sort_field = $this->fieldDefinitions->get($sort)) { $query->orderBy($sort_field->getColumnForQuery(), $direction); } } } /** * Filter the query for list. * * @param \SelectQuery $query * The query object. * * @throws BadRequestException * * @see \RestfulEntityBase::getQueryForList */ protected function queryForListFilter(\SelectQuery $query) { foreach ($this->parseRequestForListFilter() as $filter) { /* @var ResourceFieldDbColumnInterface $filter_field */ if (!$filter_field = $this->fieldDefinitions->get($filter['public_field'])) { continue; } $column_name = $filter_field->getColumnForQuery(); if (in_array(strtoupper($filter['operator'][0]), array('IN', 'NOT IN', 'BETWEEN'))) { $query->condition($column_name, $filter['value'], $filter['operator'][0]); continue; } $condition = db_condition($filter['conjunction']); for ($index = 0; $index < count($filter['value']); $index++) { $condition->condition($column_name, $filter['value'][$index], $filter['operator'][$index]); } $query->condition($condition); } } /** * Set correct page (i.e. range) for the query for list. * * Determine the page that should be seen. Page 1, is actually offset 0 in the * query range. * * @param \SelectQuery $query * The query object. * * @throws BadRequestException * * @see \RestfulEntityBase::getQueryForList */ protected function queryForListPagination(\SelectQuery $query) { list($range, $offset) = $this->parseRequestForListPagination(); $query->range($range, $offset); } /** * Get a basic query object. * * @return \SelectQuery * A new SelectQuery object for this connection. */ protected function getQuery() { $table = $this->getTableName(); return db_select($table)->fields($table); } /** * Given an array of string ID's return a single column. * * Strings are divided by the delimiter self::COLUMN_IDS_SEPARATOR. * * @param array $identifiers * An array of object IDs. * @param int $column * 0-N Zero indexed * * @return array * Returns an array at index $column */ protected function getColumnFromIds(array $identifiers, $column = 0) { // Get a single column. $get_part = function($identifier) use ($column) { $parts = explode(static::COLUMN_IDS_SEPARATOR, $identifier); if (!isset($parts[$column])) { throw new ServerConfigurationException('Invalid ID provided.'); } return $parts[$column]; }; return array_map($get_part, $identifiers); } /** * {@inheritdoc} */ protected function initDataInterpreter($identifier) { return new DataInterpreterArray($this->getAccount(), new ArrayWrapper((array) $identifier)); } } ================================================ FILE: src/Plugin/resource/DataProvider/DataProviderDbQueryInterface.php ================================================ decorated = $decorated; } /** * {@inheritdoc} */ public function getRange() { return $this->decorated->getRange(); } /** * {@inheritdoc} */ public function setRange($range) { $this->decorated->setRange($range); } /** * {@inheritdoc} */ public function getAccount() { return $this->decorated->getAccount(); } /** * {@inheritdoc} */ public function setAccount($account) { $this->decorated->setAccount($account); } /** * {@inheritdoc} */ public function getRequest() { return $this->decorated->getRequest(); } /** * {@inheritdoc} */ public function setRequest(RequestInterface $request) { $this->decorated->setRequest($request); } /** * {@inheritdoc} */ public function getLangCode() { return $this->decorated->getLangCode(); } /** * {@inheritdoc} */ public function setLangCode($langcode) { $this->decorated->setLangCode($langcode); } /** * {@inheritdoc} */ public function getOptions() { return $this->decorated->getOptions(); } /** * {@inheritdoc} */ public function addOptions(array $options) { $this->decorated->addOptions($options); } /** * {@inheritdoc} */ public function getCacheFragments($identifier) { $this->decorated->getCacheFragments($identifier); } /** * {@inheritdoc} */ public function canonicalPath($path) { return $this->decorated->canonicalPath($path); } /** * {@inheritdoc} */ public function methodAccess(ResourceFieldInterface $resource_field) { return $this->decorated->methodAccess($resource_field); } /** * {@inheritdoc} */ public function setOptions(array $options) { $this->decorated->setOptions($options); } /** * {@inheritdoc} */ public function getIndexIds() { return $this->decorated->getIndexIds(); } /** * {@inheritdoc} */ public function index() { return $this->decorated->index(); } /** * {@inheritdoc} */ public function count() { return $this->decorated->count(); } /** * {@inheritdoc} */ public function create($object) { return $this->decorated->create($object); } /** * {@inheritdoc} */ public function view($identifier) { return $this->decorated->view($identifier); } /** * {@inheritdoc} */ public function viewMultiple(array $identifiers) { return $this->decorated->viewMultiple($identifiers); } /** * {@inheritdoc} */ public function update($identifier, $object, $replace = FALSE) { return $this->decorated->update($identifier, $object, $replace); } /** * {@inheritdoc} */ public function remove($identifier) { $this->decorated->remove($identifier); } /** * {@inheritdoc} */ public function discover($path = NULL) { return $this->decorated->discover($path); } /** * {@inheritdoc} */ public static function isNestedField($field_name) { return DataProvider::isNestedField($field_name); } /** * {@inheritdoc} */ public static function processFilterInput($filter, $public_field) { return DataProvider::processFilterInput($filter, $public_field); } /** * {@inheritdoc} */ public function setResourcePath($resource_path) { $this->decorated->setResourcePath($resource_path); } /** * {@inheritdoc} */ public function getResourcePath() { return $this->decorated->getResourcePath(); } /** * {@inheritdoc} */ public function getMetadata() { return $this->decorated->getMetadata(); } } ================================================ FILE: src/Plugin/resource/DataProvider/DataProviderEntity.php ================================================ entityType = $options['entityType']; $options += array('bundles' => array()); if ($options['bundles']) { $this->bundles = $options['bundles']; } elseif ($options['bundles'] !== FALSE) { // If no bundles are passed, then assume all the bundles of the entity // type. $entity_info = entity_get_info($this->entityType); $this->bundles = !empty($entity_info['bundles']) ? array_keys($entity_info['bundles']) : $entity_info['type']; } if (isset($options['EFQClass'])) { $this->EFQClass = $options['EFQClass']; } $this->setResourcePath($resource_path); if (empty($this->options['urlParams'])) { $this->options['urlParams'] = array( 'filter' => TRUE, 'sort' => TRUE, 'fields' => TRUE, 'loadByFieldName' => TRUE, ); } } /** * {@inheritdoc} */ public function getCacheFragments($identifier) { if (is_array($identifier)) { // Like in https://example.org/api/articles/1,2,3. $identifier = implode(ResourceInterface::IDS_SEPARATOR, $identifier); } $fragments = new ArrayCollection(array( 'resource' => CacheDecoratedResource::serializeKeyValue($this->pluginId, $this->canonicalPath($identifier)), 'entity' => CacheDecoratedResource::serializeKeyValue($this->entityType, $this->getEntityIdByFieldId($identifier)), )); $options = $this->getOptions(); switch ($options['renderCache']['granularity']) { case DRUPAL_CACHE_PER_USER: if ($uid = $this->getAccount()->uid) { $fragments->set('user_id', (int) $uid); } break; case DRUPAL_CACHE_PER_ROLE: $fragments->set('user_role', implode(',', $this->getAccount()->roles)); break; } return $fragments; } /** * Defines default sort fields if none are provided via the request URL. * * @return array * Array keyed by the public field name, and the order ('ASC' or 'DESC') as * value. */ protected function defaultSortInfo() { return empty($this->options['sort']) ? array('id' => 'ASC') : $this->options['sort']; } /** * {@inheritdoc} */ public function getIndexIds() { $result = $this ->getQueryForList() ->execute(); if (empty($result[$this->entityType])) { return array(); } $entity_ids = array_keys($result[$this->entityType]); if (empty($this->options['idField'])) { return $entity_ids; } // Get the list of IDs. $resource_field = $this->fieldDefinitions->get($this->options['idField']); $ids = array(); foreach ($entity_ids as $entity_id) { $interpreter = new DataInterpreterEMW($this->getAccount(), new \EntityDrupalWrapper($this->entityType, $entity_id)); $ids[] = $resource_field->value($interpreter); } return $ids; } /** * {@inheritdoc} */ public function count() { $query = $this->getQueryCount(); return intval($query->execute()); } /** * {@inheritdoc} */ public function create($object) { $this->validateBody($object); $entity_info = $this->getEntityInfo(); $bundle_key = $entity_info['entity keys']['bundle']; // TODO: figure out how to derive the bundle when posting to a resource with // multiple bundles. $bundle = reset($this->bundles); $values = $bundle_key ? array($bundle_key => $bundle) : array(); $entity = entity_create($this->entityType, $values); if ($this->checkEntityAccess('create', $this->entityType, $entity) === FALSE) { // User does not have access to create entity. throw new ForbiddenException('You do not have access to create a new resource.'); } /* @var \EntityDrupalWrapper $wrapper */ $wrapper = entity_metadata_wrapper($this->entityType, $entity); $this->setPropertyValues($wrapper, $object, TRUE); // The access calls use the request method. Fake the view to be a GET. $old_request = $this->getRequest(); $this->getRequest()->setMethod(RequestInterface::METHOD_GET); $identifier = empty($this->options['idField']) ? $wrapper->getIdentifier() : $wrapper->get($this->options['idField'])->value(); $output = array($this->view($identifier)); // Put the original request back to a POST. $this->request = $old_request; return $output; } /** * {@inheritdoc} */ public function view($identifier) { $entity_id = $this->getEntityIdByFieldId($identifier); if (!$this->isValidEntity('view', $entity_id)) { throw new InaccessibleRecordException(sprintf('The current user cannot access entity "%s".', $entity_id)); } $field_collection = $this->initResourceFieldCollection($identifier); // Defer sparse fieldsets to the formatter. That way we can minimize cache // fragmentation because we have a unique cache record for all the sparse // fieldsets combinations. // When caching is enabled and we get a cache MISS we want to generate // output for the cache entry for the whole entity. That way we can use that // cache record independently of the sparse fieldset. // On the other hand, if cache is not enabled we don't want to output for // the whole entity, only the bits that we are going to need. For // performance reasons. $input = $this->getRequest()->getParsedInput(); $limit_fields = !empty($input['fields']) ? explode(',', $input['fields']) : array(); $field_collection->setLimitFields($limit_fields); foreach ($this->fieldDefinitions as $resource_field) { // Create an empty field collection and populate it with the appropriate // resource fields. /* @var \Drupal\restful\Plugin\resource\Field\ResourceFieldEntityInterface $resource_field */ if (!$this->methodAccess($resource_field) || !$resource_field->access('view', $field_collection->getInterpreter())) { // The field does not apply to the current method or has denied access. continue; } $field_collection->set($resource_field->id(), $resource_field); } return $field_collection; } /** * {@inheritdoc} */ public function viewMultiple(array $identifiers) { $return = array(); // If no IDs were requested, we should not throw an exception in case an // entity is un-accessible by the user. foreach ($identifiers as $identifier) { try { $row = $this->view($identifier); } catch (InaccessibleRecordException $e) { $row = NULL; } $return[] = $row; } return array_values(array_filter($return)); } /** * {@inheritdoc} */ public function update($identifier, $object, $replace = FALSE) { $this->validateBody($object); $entity_id = $this->getEntityIdByFieldId($identifier); $this->isValidEntity('update', $entity_id); /* @var \EntityDrupalWrapper $wrapper */ $wrapper = entity_metadata_wrapper($this->entityType, $entity_id); $this->setPropertyValues($wrapper, $object, $replace); // Set the HTTP headers. $this->setHttpHeader('Status', 201); if (!empty($wrapper->url) && $url = $wrapper->url->value()) { $this->setHttpHeader('Location', $url); } // The access calls use the request method. Fake the view to be a GET. $old_request = $this->getRequest(); $this->getRequest()->setMethod(RequestInterface::METHOD_GET); $output = array($this->view($identifier)); // Put the original request back to a PUT/PATCH. $this->request = $old_request; return $output; } /** * {@inheritdoc} */ public function remove($identifier) { $identifier = $this->getEntityIdByFieldId($identifier); $this->isValidEntity('delete', $identifier); /* @var \EntityDrupalWrapper $wrapper */ $wrapper = entity_metadata_wrapper($this->entityType, $identifier); $wrapper->delete(); // Set the HTTP headers. $this->setHttpHeader('Status', 204); } /** * {@inheritdoc} */ public function canonicalPath($path) { $ids = explode(Resource::IDS_SEPARATOR, $path); $canonical_ids = array_map(array($this, 'getEntityIdByFieldId'), $ids); return implode(Resource::IDS_SEPARATOR, $canonical_ids); } /** * {@inheritdoc} */ public function entityPreSave(\EntityDrupalWrapper $wrapper) {} /** * {@inheritdoc} */ public function entityValidate(\EntityDrupalWrapper $wrapper) { if (!module_exists('entity_validator')) { // Entity validator doesn't exist. return; } try { $validator_handler = ValidatorPluginManager::EntityValidator($wrapper->type(), $wrapper->getBundle()); } catch (PluginNotFoundException $e) { // Entity validator handler doesn't exist for the entity. return; } if ($validator_handler->validate($wrapper->value(), TRUE)) { // Entity is valid. return; } $errors = $validator_handler->getErrors(FALSE); $map = array(); foreach ($this->fieldDefinitions as $resource_field_name => $resource_field) { if (!$property = $resource_field->getProperty()) { continue; } $public_name = $resource_field->getPublicName(); if (empty($errors[$public_name])) { // Field validated. continue; } $map[$public_name] = $resource_field_name; $params['@fields'][] = $resource_field_name; } if (empty($params['@fields'])) { // There was a validation error, but on non-public fields, so we need to // throw an exception, but can't say on which fields it occurred. throw new BadRequestException('Invalid value(s) sent with the request.'); } $params['@fields'] = implode(',', $params['@fields']); $exception = new BadRequestException(format_plural(count($map), 'Invalid value in field @fields.', 'Invalid values in fields @fields.', $params)); foreach ($errors as $property_name => $messages) { if (empty($map[$property_name])) { // Entity is not valid, but on a field not public. continue; } $resource_field_name = $map[$property_name]; foreach ($messages as $message) { $message['params']['@field'] = $resource_field_name; $output = format_string($message['message'], $message['params']); $exception->addFieldError($resource_field_name, $output); } } // Throw the exception. throw $exception; } /** * Get the entity ID based on the ID provided in the request. * * As any field may be used as the ID, we convert it to the numeric internal * ID of the entity * * @param mixed $id * The provided ID. * * @throws BadRequestException * @throws UnprocessableEntityException * * @return int * The entity ID. */ protected function getEntityIdByFieldId($id) { $request = $this->getRequest(); $input = $request->getParsedInput(); $public_property_name = empty($input['loadByFieldName']) ? NULL : $input['loadByFieldName']; $public_property_name = $public_property_name ?: (empty($this->options['idField']) ? NULL : $this->options['idField']); if (!$public_property_name) { // The regular entity ID was provided. return $id; } // We need to get the internal field/property from the public name. if ((!$public_field_info = $this->fieldDefinitions->get($public_property_name)) || !$public_field_info->getProperty()) { throw new BadRequestException(format_string('Cannot load an entity using the field "@name"', array( '@name' => $public_property_name, ))); } $query = $this->getEntityFieldQuery(); $query->range(0, 1); // Find out if the provided ID is a Drupal field or an entity property. $property = $public_field_info->getProperty(); /* @var ResourceFieldEntity $public_field_info */ if (ResourceFieldEntity::propertyIsField($property)) { $query->fieldCondition($property, $public_field_info->getColumn(), $id); } else { $query->propertyCondition($property, $id); } // Execute the query and gather the results. $result = $query->execute(); if (empty($result[$this->entityType])) { throw new UnprocessableEntityException(format_string('The entity ID @id by @name cannot be loaded.', array( '@id' => $id, '@name' => $public_property_name, ))); } // There is nothing that guarantees that there is only one result, since // this is user input data. Return the first ID. $entity_id = key($result[$this->entityType]); return $entity_id; } /** * Initialize an EntityFieldQuery (or extending class). * * @return \EntityFieldQuery * The initialized query with the basics filled in. */ protected function getEntityFieldQuery() { $query = $this->EFQObject(); $entity_type = $this->entityType; $query->entityCondition('entity_type', $entity_type); $entity_info = $this->getEntityInfo(); if (!empty($this->bundles) && $entity_info['entity keys']['bundle']) { $query->entityCondition('bundle', $this->bundles, 'IN'); } return $query; } /** * {@inheritdoc} */ public function EFQObject() { $efq_class = $this->EFQClass; return new $efq_class(); } /** * Get the entity info for the current entity the endpoint handling. * * @param string $type * Optional. The entity type. * * @return array * The entity info. * * @see entity_get_info(). */ protected function getEntityInfo($type = NULL) { return entity_get_info($type ? $type : $this->entityType); } /** * Prepare a query for RestfulEntityBase::getList(). * * @return \Drupal\restful\Util\EntityFieldQuery * The EntityFieldQuery object. */ protected function getQueryForList() { $query = $this->getEntityFieldQuery(); // If we are trying to filter or sort on a computed field, just ignore it // and log an exception. try { $this->queryForListSort($query); } catch (BadRequestException $e) { watchdog_exception('restful', $e); } try { $this->queryForListFilter($query); } catch (BadRequestException $e) { watchdog_exception('restful', $e); } $this->queryForListPagination($query); $this->addExtraInfoToQuery($query); return $query; } /** * Prepare a query for RestfulEntityBase::count(). * * @return \EntityFieldQuery * The EntityFieldQuery object. */ protected function getQueryCount() { $query = $this->getEntityFieldQuery(); // If we are trying to filter or sort on a computed field, just ignore it // and log an exception. try { $this->queryForListSort($query); } catch (BadRequestException $e) { watchdog_exception('restful', $e); } try { $this->queryForListFilter($query); } catch (BadRequestException $e) { watchdog_exception('restful', $e); } $this->addExtraInfoToQuery($query); return $query->count(); } /** * Adds query tags and metadata to the EntityFieldQuery. * * @param \EntityFieldQuery $query * The query to enhance. */ protected function addExtraInfoToQuery($query) { parent::addExtraInfoToQuery($query); // The only time you need to add the access tags to a EFQ is when you don't // have fieldConditions. if (empty($query->fieldConditions) && empty($query->order)) { // Add a generic entity access tag to the query. $query->addTag($this->entityType . '_access'); } $query->addMetaData('restful_data_provider', $this); } /** * Sort the query for list. * * @param \EntityFieldQuery $query * The query object. * * @throws \Drupal\restful\Exception\BadRequestException * @throws \EntityFieldQueryException * * @see \RestfulEntityBase::getQueryForList */ protected function queryForListSort(\EntityFieldQuery $query) { $resource_fields = $this->fieldDefinitions; // Get the sorting options from the request object. $sorts = $this->parseRequestForListSort(); $sorts = $sorts ? $sorts : $this->defaultSortInfo(); foreach ($sorts as $public_field_name => $direction) { // Determine if sorting is by field or property. /* @var \Drupal\restful\Plugin\resource\Field\ResourceFieldEntityInterface $resource_field */ if (!$resource_field = $resource_fields->get($public_field_name)) { return; } $sort = array( 'public_field' => $public_field_name, 'direction' => $direction, 'resource_id' => $this->pluginId, ); $sort = $this->alterSortQuery($sort, $query); if (!empty($sort['processed'])) { // If the sort was already processed by the alter filters, continue. continue; } if (!$property_name = $resource_field->getProperty()) { if (!$resource_field instanceof ResourceFieldEntityAlterableInterface) { throw new BadRequestException('The current sort selection does not map to any entity property or Field API field.'); } // If there was no property but the resource field was sortable, do // not add the default field filtering. // TODO: This is a workaround. The filtering logic should live in the resource field class. return; } if (ResourceFieldEntity::propertyIsField($property_name)) { $query->fieldOrderBy($property_name, $resource_field->getColumn(), $sort['direction']); } else { $column = $this->getColumnFromProperty($property_name); $query->propertyOrderBy($column, $sort['direction']); } } } /** * Filter the query for list. * * @param \EntityFieldQuery $query * The query object. * * @throws \Drupal\restful\Exception\BadRequestException * * @see \RestfulEntityBase::getQueryForList */ protected function queryForListFilter(\EntityFieldQuery $query) { $resource_fields = $this->fieldDefinitions; $filters = $this->parseRequestForListFilter(); $this->validateFilters($filters); foreach ($filters as $filter) { // Determine if filtering is by field or property. /* @var \Drupal\restful\Plugin\resource\Field\ResourceFieldEntityInterface $resource_field */ if (!$resource_field = $resource_fields->get($filter['public_field'])) { if (!static::isNestedField($filter['public_field'])) { // This is not a nested filter. continue; } if (!empty($filter['target'])) { // If we cannot find the field, it may be a nested filter. Check if // the target of that is the current resource. continue; } $this->addNestedFilter($filter, $query); continue; } // Give the chance for other data providers to have a special handling for // a given field. $filter = $this->alterFilterQuery($filter, $query); if (!empty($filter['processed'])) { // If the filter was already processed by the alter filters, continue. continue; } if (!$property_name = $resource_field->getProperty()) { if (!$resource_field instanceof ResourceFieldEntityAlterableInterface) { throw new BadRequestException(sprintf('The current filter "%s" selection does not map to any entity property or Field API field and has no custom filtering.', $filter['public_field'])); } // If there was no property but the resource field was filterable, do // not add the default field filtering. // TODO: This is a workaround. The filtering logic should live in the resource field class. return; } if (field_info_field($property_name)) { if ($this::isMultipleValuOperator($filter['operator'][0])) { $query->fieldCondition($property_name, $resource_field->getColumn(), $this->getReferencedIds($filter['value'], $resource_field), $filter['operator'][0]); continue; } for ($index = 0; $index < count($filter['value']); $index++) { // If referencing an entity by an alternate ID, retrieve the actual // Drupal's entity ID using getReferencedId. $query->fieldCondition($property_name, $resource_field->getColumn(), $this->getReferencedId($filter['value'][$index], $resource_field), $filter['operator'][$index]); } } else { $column = $this->getColumnFromProperty($property_name); if ($this::isMultipleValuOperator($filter['operator'][0])) { $query->propertyCondition($column, $this->getReferencedIds($filter['value'], $resource_field), $filter['operator'][0]); continue; } for ($index = 0; $index < count($filter['value']); $index++) { $query->propertyCondition($column, $this->getReferencedId($filter['value'][$index], $resource_field), $filter['operator'][$index]); } } } } /** * Placeholder method to alter the filters. * * If no further processing for the filter is needed (i.e. alterFilterQuery * already added the query filters to $query), then set the 'processed' flag * in $filter to TRUE. Otherwise normal filtering will be added on top, * leading to unexpected results. * * @param array $filter * The parsed filter information. * @param \EntityFieldQuery $query * The EFQ to add the filter to. * * @return array * The modified $filter array. */ protected function alterFilterQuery(array $filter, \EntityFieldQuery $query) { if (!$resource_field = $this->fieldDefinitions->get($filter['public_field'])) { return $filter; } if (!$resource_field instanceof ResourceFieldEntityAlterableInterface) { // Check if the resource can check on decorated instances. if (!$resource_field instanceof ExplorableDecoratorInterface || !$resource_field->isInstanceOf(ResourceFieldEntityAlterableInterface::class)) { return $filter; } } return $resource_field->alterFilterEntityFieldQuery($filter, $query); } /** * Placeholder method to alter the filters. * * If no further processing for the filter is needed (i.e. alterFilterQuery * already added the query filters to $query), then set the 'processed' flag * in $filter to TRUE. Otherwise normal filtering will be added on top, * leading to unexpected results. * * @param array $sort * The sort array containing the keys: * - public_field: Contains the public property. * - direction: The sorting direction, either ASC or DESC. * - resource_id: The resource machine name. * @param \EntityFieldQuery $query * The EFQ to add the filter to. * * @return array * The modified $sort array. */ protected function alterSortQuery(array $sort, \EntityFieldQuery $query) { if (!$resource_field = $this->fieldDefinitions->get($sort['public_field'])) { return $sort; } if (!$resource_field instanceof ResourceFieldEntityAlterableInterface) { // Check if the resource can check on decorated instances. if (!$resource_field instanceof ExplorableDecoratorInterface || !$resource_field->isInstanceOf(ResourceFieldEntityAlterableInterface::class)) { return $sort; } } return $resource_field->alterSortEntityFieldQuery($sort, $query); } /** * Checks if the operator accepts multiple values. * * @param $operator_name * The name of the operator. * * @return bool * TRUE if the operator can interpret multiple values. FALSE otherwise. */ protected static function isMultipleValuOperator($operator_name) { return in_array(strtoupper($operator_name), array('IN', 'NOT IN', 'BETWEEN')); } /** * Validates the query parameters. * * @param array $filters * The parsed filters. * * @throws BadRequestException * When there is an invalid target for relational filters. */ protected function validateFilters(array $filters) { foreach ($filters as $filter) { if (empty($filter['target'])) { continue; } // If the target is not a part of the field, then raise an error. $field_name_parts = explode('.', $filter['public_field']); $target_parts = explode('.', $filter['target']); foreach ($target_parts as $delta => $target_part) { if ($target_part != $field_name_parts[$delta]) { // There is a discrepancy between target and field name. throw new BadRequestException(sprintf('The target "%s" should be a part of the field name "%s".', $filter['target'], $filter['public_field'])); } } } } /** * Set correct page (i.e. range) for the query for list. * * Determine the page that should be seen. Page 1, is actually offset 0 in the * query range. * * @param \EntityFieldQuery $query * The query object. * * @throws BadRequestException * * @see \RestfulEntityBase::getQueryForList */ protected function queryForListPagination(\EntityFieldQuery $query) { list($offset, $range) = $this->parseRequestForListPagination(); $query->range($offset, $range); } /** * Overrides DataProvider::isValidOperatorsForFilter(). */ protected static function isValidOperatorsForFilter(array $operators) { $allowed_operators = array( '=', '>', '<', '>=', '<=', '<>', '!=', 'BETWEEN', 'CONTAINS', 'IN', 'LIKE', 'NOT IN', 'STARTS_WITH', ); foreach ($operators as $operator) { if (!in_array($operator, $allowed_operators)) { throw new BadRequestException(sprintf('Operator "%s" is not allowed for filtering on this resource. Allowed operators are: %s', $operator, implode(', ', $allowed_operators))); } } } /** * Overrides DataProvider::isValidConjunctionForFilter(). */ protected static function isValidConjunctionForFilter($conjunction) { $allowed_conjunctions = array( 'AND', ); if (!in_array(strtoupper($conjunction), $allowed_conjunctions)) { throw new BadRequestException(format_string('Conjunction "@conjunction" is not allowed for filtering on this resource. Allowed conjunctions are: !allowed', array( '@conjunction' => $conjunction, '!allowed' => implode(', ', $allowed_conjunctions), ))); } } /** * Get the DB column name from a property. * * The "property" defined in the public field is actually the property * of the entity metadata wrapper. Sometimes that property can be a * different name than the column in the DB. For example, for nodes the * "uid" property is mapped in entity metadata wrapper as "author", so * we make sure to get the real column name. * * @param string $property_name * The property name. * * @return string * The column name. */ protected function getColumnFromProperty($property_name) { $property_info = entity_get_property_info($this->entityType); return $property_info['properties'][$property_name]['schema field']; } /** * Determine if an entity is valid, and accessible. * * @param string $op * The operation to perform on the entity (view, update, delete). * @param int $entity_id * The entity ID. * * @return bool * TRUE if entity is valid, and user can access it. * * @throws UnprocessableEntityException * @throws InaccessibleRecordException */ protected function isValidEntity($op, $entity_id) { $entity_type = $this->entityType; if (!ctype_digit((string) $entity_id) || !$entity = entity_load_single($entity_type, $entity_id)) { // We need to check if the entity ID is numeric since if this is a uuid // that starts by the number 4, and there is an entity with ID 4 that // entity will be loaded incorrectly. throw new UnprocessableEntityException(sprintf('The entity ID %s does not exist.', $entity_id)); } list(,, $bundle) = entity_extract_ids($entity_type, $entity); if (!empty($this->bundles) && !in_array($bundle, $this->bundles)) { return FALSE; } if ($this->checkEntityAccess($op, $entity_type, $entity) === FALSE) { if ($op == 'view' && !$this->getResourcePath()) { // Just return FALSE, without an exception, for example when a list of // entities is requested, and we don't want to fail all the list because // of a single item without access. // Add the inaccessible item to the metadata to fix the record count in // the formatter. $inaccessible_records = $this->getMetadata()->get('inaccessible_records'); $inaccessible_records[] = array( 'resource' => $this->pluginId, 'id' => $entity_id, ); $this->getMetadata()->set('inaccessible_records', $inaccessible_records); return FALSE; } // Entity was explicitly requested so we need to throw an exception. throw new InaccessibleRecordException(sprintf('You do not have access to entity ID %s.', $entity_id)); } return TRUE; } /** * Check access to CRUD an entity. * * @param string $op * The operation. Allowed values are "create", "update" and "delete". * @param string $entity_type * The entity type. * @param object $entity * The entity object. * * @return bool * TRUE or FALSE based on the access. If no access is known about the entity * return NULL. */ protected function checkEntityAccess($op, $entity_type, $entity) { $account = $this->getAccount(); return entity_access($op, $entity_type, $entity, $account); } /** * Set properties of the entity based on the request, and save the entity. * * @param \EntityDrupalWrapper $wrapper * The wrapped entity object, passed by reference. * @param array $object * The keyed array of properties sent in the payload. * @param bool $replace * Determine if properties that are missing from the request array should * be treated as NULL, or should be skipped. Defaults to FALSE, which will * set the fields to NULL. * * @throws BadRequestException * If the provided object is not valid. */ protected function setPropertyValues(\EntityDrupalWrapper $wrapper, $object, $replace = FALSE) { if (!is_array($object)) { throw new BadRequestException('Bad input data provided. Please, check your input and your Content-Type header.'); } $save = FALSE; $original_object = $object; $interpreter = new DataInterpreterEMW($this->getAccount(), $wrapper); // Keeps a list of the fields that have been set. $processed_fields = array(); $field_definitions = clone $this->fieldDefinitions; foreach ($field_definitions as $public_field_name => $resource_field) { /* @var \Drupal\restful\Plugin\resource\Field\ResourceFieldEntityInterface $resource_field */ if (!$this->methodAccess($resource_field)) { // Allow passing the value in the request. unset($original_object[$public_field_name]); continue; } $property_name = $resource_field->getProperty(); if ($resource_field->isComputed()) { // We may have for example an entity with no label property, but with a // label callback. In that case the $info['property'] won't exist, so // we skip this field. unset($original_object[$public_field_name]); continue; } $entity_property_access = $this::checkPropertyAccess($resource_field, 'edit', $interpreter); if (!array_key_exists($public_field_name, $object)) { // No property to set in the request. // Only set this to NULL if this property has not been set to a specific // value by another public field (since 2 public fields can reference // the same property). if ($replace && $entity_property_access && !in_array($property_name, $processed_fields)) { // We need to set the value to NULL. $field_value = NULL; } else { // Either we shouldn't set missing fields as NULL or access is denied // for the current property, hence we skip. continue; } } else { // Property is set in the request. // Delegate modifications on the value of the field. $field_value = $resource_field->preprocess($object[$public_field_name]); } $resource_field->set($field_value, $interpreter); // We check the property access only after setting the values, as the // access callback's response might change according to the field value. $entity_property_access = $this::checkPropertyAccess($resource_field, 'edit', $interpreter); if (!$entity_property_access) { throw new BadRequestException(format_string('Property @name cannot be set.', array('@name' => $public_field_name))); } $processed_fields[] = $property_name; unset($original_object[$public_field_name]); $save = TRUE; } if (!$save) { // No request was sent. throw new BadRequestException('No values were sent with the request'); } if ($original_object) { // Request had illegal values. $error_message = format_plural(count($original_object), 'Property @names is invalid.', 'Properties @names are invalid.', array('@names' => implode(', ', array_keys($original_object)))); throw new BadRequestException($error_message); } // Allow changing the entity just before it's saved. For example, setting // the author of the node entity. $this->entityPreSave($interpreter->getWrapper()); $this->entityValidate($interpreter->getWrapper()); $wrapper->save(); } /** * Validates the body payload object for entities. * * @param mixed $body * The parsed body. * * @throws \Drupal\restful\Exception\BadRequestException * For the empty body. */ protected function validateBody($body) { if (isset($body) && !is_array($body)) { $message = sprintf('Incorrect object parsed: %s', print_r($body, TRUE)); throw new BadRequestException($message); } } /** * Checks if the data provider user has access to the property. * * @param \Drupal\restful\Plugin\resource\Field\ResourceFieldInterface $resource_field * The field to check access on. * @param string $op * The operation to be performed on the field. * @param \Drupal\restful\Plugin\resource\DataInterpreter\DataInterpreterInterface $interpreter * The data interpreter. * * @return bool * TRUE if the user has access to the property. */ protected static function checkPropertyAccess(ResourceFieldInterface $resource_field, $op, DataInterpreterInterface $interpreter) { return $resource_field->access($op, $interpreter); } /** * Get referenced ID. * * @param string $value * The provided value, it can be an ID or not. * @param ResourceFieldInterface $resource_field * The resource field that points to another entity. * * @return string * If the field uses an alternate ID property, then the ID gets translated * to the original entity ID. If not, then the same provided ID is returned. * * @todo: Add testing to this functionality. */ protected function getReferencedId($value, ResourceFieldInterface $resource_field) { $field_definition = $resource_field->getDefinition(); if (empty($field_definition['referencedIdProperty'])) { return $value; } // Get information about the the Drupal field to see what entity type we are // dealing with. $field_info = field_info_field($resource_field->getProperty()); // We support: // - Entity Reference. // - Taxonomy Term. // - File & Image field. // - uid property. // - vid property. // If you need to support other types, you can create a custom data provider // that overrides this method. $target_entity_type = NULL; $bundles = array(); if (!$field_info) { if ($resource_field->getProperty() == 'uid') { // We make a special case for the user id. $target_entity_type = 'user'; } elseif ($resource_field->getProperty() == 'vid') { // We make a special case for the vocabulary id. $target_entity_type = 'taxonomy_vocabulary'; } } elseif (!empty($field_info['type']) && $field_info['type'] == 'entityreference') { $target_entity_type = $field_info['settings']['target_type']; $bundles = empty($field_info['settings']['handler_settings']['target_bundles']) ? array() : $field_info['settings']['handler_settings']['target_bundles']; } elseif (!empty($field_info['type']) && $field_info['type'] == 'file') { $target_entity_type = 'file'; } elseif (!empty($field_info['type']) && $field_info['type'] == 'taxonomy_term_reference') { $target_entity_type = 'taxonomy_term'; // Narrow down with the vocabulary information. Very useful if there are // multiple terms with the same name in different vocabularies. foreach ($field_info['settings']['allowed_values'] as $allowed_value) { $bundles[] = $allowed_value['vocabulary']; } } if (empty($target_entity_type) && $resource_field instanceof ResourceFieldResourceInterface && ($resource_info = $resource_field->getResource())) { $instance_id = sprintf('%s:%d.%d', $resource_info['name'], $resource_info['majorVersion'], $resource_info['minorVersion']); try { $handler = restful()->getResourceManager()->getPlugin($instance_id); if ($handler instanceof ResourceEntity) { $target_entity_type = $handler->getEntityType(); $bundles = $handler->getBundles(); } } catch (PluginNotFoundException $e) { // Do nothing. } } if (empty($target_entity_type)) { return $value; } // Now we have the entity type and bundles to look for the entity based on // the contents of the field or the entity property. $query = $this->EFQObject(); $query->entityCondition('entity_type', $target_entity_type); if (!empty($bundles)) { // Narrow down for bundles. $query->entityCondition('bundle', $bundles, 'IN'); } // Check if the referencedIdProperty is a field or a property. $id_property = $field_definition['referencedIdProperty']; if (field_info_field($id_property)) { $query->fieldCondition($id_property, $resource_field->getColumn(), $value); } else { $query->propertyCondition($id_property, $value); } // Only one result is returned. This assumes the reference fields are unique // for every entity. $results = $query->range(0, 1)->execute(); if ($results[$target_entity_type]) { return key($results[$target_entity_type]); } // If no entity could be found, fall back to the original value. return $value; } /** * Get reference IDs for multiple values. * * @param array $values * The provided values, they can be an IDs or not. * @param ResourceFieldInterface $resource_field * The resource field that points to another entity. * * @return string * If the field uses an alternate ID property, then the ID gets translated * to the original entity ID. If not, then the same provided ID is returned. * * @see getReferencedId() */ protected function getReferencedIds(array $values, ResourceFieldInterface $resource_field) { $output = array(); foreach ($values as $value) { $output[] = $this->getReferencedId($value, $resource_field); } return $output; } /** * Add relational filters to EFQ. * * This is for situation like when you only want articles that have taxonomies * that contain the word Drupal in their body field. This cannot be resolved * via EFQ alone. * * @param array $filter * The filter. * @param \EntityFieldQuery $query * The query to alter. */ protected function addNestedFilter(array $filter, \EntityFieldQuery $query) { $relational_filters = array(); foreach ($this->getFieldsInfoFromPublicName($filter['public_field']) as $field_info) { $relational_filters[] = new RelationalFilter($field_info['name'], $field_info['type'], $field_info['column'], $field_info['entity_type'], $field_info['bundles'], $field_info['target_column']); } $query->addRelationship($filter + array('relational_filters' => $relational_filters)); } /** * Transform the nested public name into an array of Drupal field information. * * @param string $name * The dot separated public name. * * @throws ServerConfigurationException * When the required resource information is not available. * @throws BadRequestException * When the nested field is invalid. * * @return array * An array of fields with name and type. */ protected function getFieldsInfoFromPublicName($name) { $public_field_names = explode('.', $name); $last_public_field_name = array_pop($public_field_names); $fields = array(); // The first field is in the current resource, but not the other ones. $definitions = $this->fieldDefinitions; foreach ($public_field_names as $index => $public_field_name) { /* @var ResourceFieldEntity $resource_field */ $resource_field = $definitions->get($public_field_name); // Get the resource for the field, so we can get information for the next // iteration. if (!$resource_field || !($resource = $resource_field->getResource())) { throw new ServerConfigurationException(sprintf('The nested field %s cannot be accessed because %s has no resource associated to it.', $name, $public_field_name)); } list($item, $definitions) = $this->getFieldsFromPublicNameItem($resource_field); $fields[] = $item; } if (!$resource_field = $definitions->get($last_public_field_name)) { throw new BadRequestException(sprintf('Invalid nested field provided %s', $last_public_field_name)); } $property = $resource_field->getProperty(); $item = array( 'name' => $property, 'type' => ResourceFieldEntity::propertyIsField($property) ? RelationalFilterInterface::TYPE_FIELD : RelationalFilterInterface::TYPE_PROPERTY, 'entity_type' => NULL, 'bundles' => array(), 'target_column' => NULL, ); $item['column'] = $item['type'] == RelationalFilterInterface::TYPE_FIELD ? $resource_field->getColumn() : NULL; $fields[] = $item; return $fields; } /** * Get the (reference) field information for a single item. * * @param ResourceFieldInterface $resource_field * The resource field. * * @throws \Drupal\restful\Exception\BadRequestException * * @return array * An array containing the following keys: * - 'name': Drupal's internal field name. Ex: field_article_related * - 'type': Either a field or a property. * - 'entity_type': The entity type this field points to. Not populated if * the field is not a reference (for instance the destination field used * in the where clause). * - 'bundles': The allowed bundles for this field. Not populated if the * field is not a reference (for instance the destination field used in * the where clause). */ protected function getFieldsFromPublicNameItem(ResourceFieldResourceInterface $resource_field) { $property = $resource_field->getProperty(); $item = array( 'name' => $property, 'type' => ResourceFieldEntity::propertyIsField($property) ? RelationalFilterInterface::TYPE_FIELD : RelationalFilterInterface::TYPE_PROPERTY, 'entity_type' => NULL, 'bundles' => array(), 'target_column' => $resource_field->getTargetColumn(), ); $item['column'] = $item['type'] == RelationalFilterInterface::TYPE_FIELD ? $resource_field->getColumn() : NULL; /* @var ResourceEntity $resource */ $resource = $resource_field->getResourcePlugin(); // Variables for the next iteration. $definitions = $resource->getFieldDefinitions(); $item['entity_type'] = $resource->getEntityType(); $item['bundles'] = $resource->getBundles(); return array($item, $definitions); } /** * {@inheritdoc} */ protected function initDataInterpreter($identifier) { $id = $identifier; $entity_id = $this->getEntityIdByFieldId($id); /* @var \EntityDrupalWrapper $wrapper */ $wrapper = entity_metadata_wrapper($this->entityType, $entity_id); $wrapper->language($this->getLangCode()); return new DataInterpreterEMW($this->getAccount(), $wrapper); } } ================================================ FILE: src/Plugin/resource/DataProvider/DataProviderEntityDecorator.php ================================================ decorated->entityPreSave($wrapper); } /** * Validate an entity before it is saved. * * @param \EntityDrupalWrapper $wrapper * The wrapped entity. * * @throws BadRequestException */ public function entityValidate(\EntityDrupalWrapper $wrapper) { $this->decorated->entityValidate($wrapper); } /** * Gets a EFQ object. * * @return \EntityFieldQuery * The object that inherits from \EntityFieldQuery. */ public function EFQObject() { return $this->decorated->EFQObject(); } } ================================================ FILE: src/Plugin/resource/DataProvider/DataProviderEntityInterface.php ================================================ options['options']) ? array() : $this->options['options']; $default_values = array( 'validators' => array( 'file_validate_extensions' => array(), 'file_validate_size' => array(), ), 'scheme' => file_default_scheme(), 'replace' => FILE_EXISTS_RENAME, ); $this->options['options'] = drupal_array_merge_deep($default_values, $file_options); } /** * {@inheritdoc} */ public function create($object) { $files = $this->getRequest()->getFiles(); if (!$files) { throw new BadRequestException('No files sent with the request.'); } $ids = array(); foreach ($files as $file_info) { // Populate the $_FILES the way file_save_upload() expects. $name = $file_info['name']; foreach ($file_info as $key => $value) { $files['files'][$key][$name] = $value; } if (!$file = $this->fileSaveUpload($name, $files)) { throw new BadRequestException('Unacceptable file sent with the request.'); } // Required to be able to reference this file. file_usage_add($file, 'restful', 'files', $file->fid); $ids[] = $file->fid; } $return = array(); foreach ($ids as $id) { // The access calls use the request method. Fake the view to be a GET. $old_request = $this->getRequest(); $this->getRequest()->setMethod(RequestInterface::METHOD_GET); try { $return[] = array($this->view($id)); } catch (ForbiddenException $e) { // A forbidden element should not forbid access to the whole list. } // Put the original request back to a POST. $this->request = $old_request; } return $return; } /** * An adaptation of file_save_upload() that includes more verbose errors. * * @param string $source * A string specifying the filepath or URI of the uploaded file to save. * @param array $files * Array containing information about the files. * * @return object * The saved file object. * * @throws BadRequestException * @throws ServiceUnavailableException * * @see file_save_upload() */ protected function fileSaveUpload($source, array $files) { static $upload_cache; $provider_options = $this->getOptions(); $options = $provider_options['options']; $validators = empty($options['validators']) ? NULL : $options['validators']; $destination = $options['scheme'] . "://"; $replace = empty($options['replace']) ? NULL : $options['replace']; // Return cached objects without processing since the file will have // already been processed and the paths in _FILES will be invalid. if (isset($upload_cache[$source])) { return $upload_cache[$source]; } // Make sure there's an upload to process. if (empty($files['files']['name'][$source])) { return NULL; } $this->checkUploadErrors($source, $files); // Begin building file object. $file_array = array( 'uid' => $this->getAccount()->uid, 'status' => 0, 'filename' => trim(drupal_basename($files['files']['name'][$source]), '.'), 'uri' => $files['files']['tmp_name'][$source], 'filesize' => $files['files']['size'][$source], ); $file_array['filemime'] = file_get_mimetype($file_array['filename']); $file = (object) $file_array; $extensions = ''; if (isset($validators['file_validate_extensions'])) { if (isset($validators['file_validate_extensions'][0])) { // Build the list of non-munged extensions if the caller provided them. $extensions = $validators['file_validate_extensions'][0]; } else { // If 'file_validate_extensions' is set and the list is empty then the // caller wants to allow any extension. In this case we have to remove // the validator or else it will reject all extensions. unset($validators['file_validate_extensions']); } } else { // No validator was provided, so add one using the default list. // Build a default non-munged safe list for file_munge_filename(). $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp'; $validators['file_validate_extensions'] = array(); $validators['file_validate_extensions'][0] = $extensions; } if (!empty($extensions)) { // Munge the filename to protect against possible malicious extension // hiding within an unknown file type (ie: filename.html.foo). $file->filename = file_munge_filename($file->filename, $extensions); } // Rename potentially executable files, to help prevent exploits (i.e. will // rename filename.php.foo and filename.php to filename.php.foo.txt and // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads' // evaluates to TRUE. if (!variable_get('allow_insecure_uploads', 0) && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $file->filename) && (substr($file->filename, -4) != '.txt')) { $file->filemime = 'text/plain'; $file->uri .= '.txt'; $file->filename .= '.txt'; // The .txt extension may not be in the allowed list of extensions. We // have to add it here or else the file upload will fail. if (!empty($extensions)) { $validators['file_validate_extensions'][0] .= ' txt'; // Unlike file_save_upload() we don't need to let the user know that // for security reasons, your upload has been renamed, since RESTful // will return the file name in the response. } } // If the destination is not provided, use the temporary directory. if (empty($destination)) { $destination = 'temporary://'; } // Assert that the destination contains a valid stream. $destination_scheme = file_uri_scheme($destination); if (!$destination_scheme || !file_stream_wrapper_valid_scheme($destination_scheme)) { $message = format_string('The file could not be uploaded, because the destination %destination is invalid.', array('%destination' => $destination)); throw new ServiceUnavailableException($message); } $file->source = $source; // A URI may already have a trailing slash or look like "public://". if (substr($destination, -1) != '/') { $destination .= '/'; } $file->destination = file_destination($destination . $file->filename, $replace); // If file_destination() returns FALSE then $replace == FILE_EXISTS_ERROR // and there's an existing file so we need to bail. if ($file->destination === FALSE) { $message = format_string('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', array('%source' => $source, '%directory' => $destination)); throw new ServiceUnavailableException($message); } // Add in our check of the the file name length. $validators['file_validate_name_length'] = array(); // Call the validation functions specified by this function's caller. $errors = file_validate($file, $validators); // Check for errors. if (!empty($errors)) { $message = format_string('The specified file %name could not be uploaded.', array('%name' => $file->filename)); if (count($errors) > 1) { $message .= theme('item_list', array('items' => $errors)); } else { $message .= ' ' . array_pop($errors); } throw new ServiceUnavailableException($message); } // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary // directory. This overcomes open_basedir restrictions for future file // operations. $file->uri = $file->destination; if (!$this::moveUploadedFile($files['files']['tmp_name'][$source], $file->uri)) { watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->uri)); $message = 'File upload error. Could not move uploaded file.'; throw new ServiceUnavailableException($message); } // Set the permissions on the new file. drupal_chmod($file->uri); // If we are replacing an existing file re-use its database record. if ($replace == FILE_EXISTS_REPLACE) { $existing_files = file_load_multiple(array(), array('uri' => $file->uri)); if (count($existing_files)) { $existing = reset($existing_files); $file->fid = $existing->fid; } } // Change the file status from temporary to permanent. $file->status = FILE_STATUS_PERMANENT; // If we made it this far it's safe to record this file in the database. if ($file = file_save($file)) { // Add file to the cache. $upload_cache[$source] = $file; return $file; } // Something went wrong, so throw a general exception. throw new ServiceUnavailableException('Unknown error has occurred.'); } /** * Checks of there is an error with the file upload and throws an exception. * * @param string $source * The name of the uploaded file * @param array $files * Array containing information about the files. * * @throws \Drupal\restful\Exception\BadRequestException * @throws \Drupal\restful\Exception\ServiceUnavailableException */ protected function checkUploadErrors($source, array $files) { // Check for file upload errors and return FALSE if a lower level system // error occurred. For a complete list of errors: // See http://php.net/manual/features.file-upload.errors.php. switch ($files['files']['error'][$source]) { case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: $message = format_string('The file %file could not be saved, because it exceeds %maxsize, the maximum allowed size for uploads.', array( '%file' => $files['files']['name'][$source], '%maxsize' => format_size(file_upload_max_size()), )); throw new BadRequestException($message); case UPLOAD_ERR_PARTIAL: case UPLOAD_ERR_NO_FILE: $message = format_string('The file %file could not be saved, because the upload did not complete.', array('%file' => $files['files']['name'][$source])); throw new BadRequestException($message); case UPLOAD_ERR_OK: // Final check that this is a valid upload, if it isn't, use the // default error handler. if ($this::isUploadedFile($files['files']['tmp_name'][$source])) { break; } default: // Unknown error. $message = format_string('The file %file could not be saved. An unknown error has occurred.', array('%file' => $files['files']['name'][$source])); throw new ServiceUnavailableException($message); } } /** * Helper function that checks if a file was uploaded via a POST request. * * @param string $filename * The name of the file. * * @return bool * TRUE if the file is uploaded. FALSE otherwise. */ protected static function isUploadedFile($filename) { return is_uploaded_file($filename); } /** * Helper function that moves an uploaded file. * * @param string $filename * The path of the file to move. * @param string $uri * The path where to move the file. * * @return bool * TRUE if the file was moved. FALSE otherwise. */ protected static function moveUploadedFile($filename, $uri) { return (bool) drupal_move_uploaded_file($filename, $uri); } } ================================================ FILE: src/Plugin/resource/DataProvider/DataProviderInterface.php ================================================ propertyCondition('status', NODE_PUBLISHED); return $query; } /** * Overrides DataProviderEntity::getQueryCount(). * * Only count published nodes. */ public function getQueryCount() { $query = parent::getQueryCount(); $query->propertyCondition('status', NODE_PUBLISHED); return $query; } /** * {@inheritdoc} */ public function entityPreSave(\EntityDrupalWrapper $wrapper) { $node = $wrapper->value(); if (!empty($node->nid)) { // Node is already saved. return; } node_object_prepare($node); $node->uid = $this->getAccount()->uid; } } ================================================ FILE: src/Plugin/resource/DataProvider/DataProviderNull.php ================================================ options['urlParams'])) { $this->options['urlParams'] = array( 'filter' => TRUE, 'sort' => TRUE, 'fields' => TRUE, ); } } /** * {@inheritdoc} */ public function count() { return count($this->getIndexIds()); } /** * {@inheritdoc} */ public function create($object) { throw new NotImplementedException('You cannot create plugins through the API.'); } /** * {@inheritdoc} */ public function view($identifier) { $resource_field_collection = $this->initResourceFieldCollection($identifier); $input = $this->getRequest()->getParsedInput(); $limit_fields = !empty($input['fields']) ? explode(',', $input['fields']) : array(); foreach ($this->fieldDefinitions as $resource_field_name => $resource_field) { /* @var \Drupal\restful\Plugin\resource\Field\ResourceFieldInterface $resource_field */ if ($limit_fields && !in_array($resource_field_name, $limit_fields)) { // Limit fields doesn't include this property. continue; } if (!$this->methodAccess($resource_field) || !$resource_field->access('view', $resource_field_collection->getInterpreter())) { // The field does not apply to the current method or has denied // access. continue; } $resource_field_collection->set($resource_field->id(), $resource_field); } return $resource_field_collection; } /** * {@inheritdoc} */ public function viewMultiple(array $identifiers) { $return = array(); foreach ($identifiers as $identifier) { try { $row = $this->view($identifier); } catch (InaccessibleRecordException $e) { $row = NULL; } $return[] = $row; } return array_values(array_filter($return)); } /** * {@inheritdoc} */ public function update($identifier, $object, $replace = FALSE) { // TODO: Document how to enable/disable resources using the API. $disabled_plugins = variable_get('restful_disabled_plugins', array()); if ($object['enable']) { $disabled_plugins[$identifier] = FALSE; } variable_set('restful_disabled_plugins', $disabled_plugins); } /** * {@inheritdoc} */ public function remove($identifier) { // TODO: Document how to enable/disable resources using the API. $disabled_plugins = variable_get('restful_disabled_plugins', array()); $disabled_plugins[$identifier] = TRUE; variable_set('restful_disabled_plugins', $disabled_plugins); } /** * {@inheritdoc} */ public function getIndexIds() { // Return all of the instance IDs. $plugins = restful() ->getResourceManager() ->getPlugins(); $output = $plugins->getIterator()->getArrayCopy(); // Apply filters. $output = $this->applyFilters($output); $output = $this->applySort($output); return array_keys($output); } /** * Removes plugins from the list based on the request options. * * @param \Drupal\restful\Plugin\resource\ResourceInterface[] $plugins * The array of resource plugins keyed by instance ID. * * @return \Drupal\restful\Plugin\resource\ResourceInterface[] * The same array minus the filtered plugins. */ protected function applyFilters(array $plugins) { $resource_manager = restful()->getResourceManager(); $filters = $this->parseRequestForListFilter(); // If the 'all' option is not present, then add a filters to retrieve only // the last resource. $input = $this->getRequest()->getParsedInput(); $all = !empty($input['all']); // Apply the filter to the list of plugins. foreach ($plugins as $instance_id => $plugin) { if (!$all) { // Remove the plugin if it's not the latest version. $version = $plugin->getVersion(); list($last_major, $last_minor) = $resource_manager->getResourceLastVersion($plugin->getResourceMachineName()); if ($version['major'] != $last_major || $version['minor'] != $last_minor) { // We don't add the major and minor versions to filters because we // cannot depend on the presence of the versions as public fields. unset($plugins[$instance_id]); continue; } } // If the discovery is turned off for the resource, unset it. $definition = $plugin->getPluginDefinition(); if (!$definition['discoverable']) { unset($plugins[$instance_id]); continue; } // A filter on a result needs the ResourceFieldCollection representing the // result to return. $interpreter = new DataInterpreterPlug($this->getAccount(), new PluginWrapper($plugin)); $this->fieldDefinitions->setInterpreter($interpreter); foreach ($filters as $filter) { if (!$this->fieldDefinitions->evalFilter($filter)) { unset($plugins[$instance_id]); } } } $this->fieldDefinitions->setInterpreter(NULL); return $plugins; } /** * Sorts plugins on the list based on the request options. * * @param \Drupal\restful\Plugin\resource\ResourceInterface[] $plugins * The array of resource plugins keyed by instance ID. * * @return \Drupal\restful\Plugin\resource\ResourceInterface[] * The sorted array. */ protected function applySort(array $plugins) { if ($sorts = $this->parseRequestForListSort()) { uasort($plugins, function ($plugin1, $plugin2) use ($sorts) { $interpreter1 = new DataInterpreterPlug($this->getAccount(), new PluginWrapper($plugin1)); $interpreter2 = new DataInterpreterPlug($this->getAccount(), new PluginWrapper($plugin2)); foreach ($sorts as $key => $order) { $property = $this->fieldDefinitions->get($key)->getProperty(); $value1 = $interpreter1->getWrapper()->get($property); $value2 = $interpreter2->getWrapper()->get($property); if ($value1 == $value2) { continue; } return ($order == 'DESC' ? -1 : 1) * strcmp($value1, $value2); } return 0; }); } return $plugins; } /** * {@inheritdoc} */ protected function initDataInterpreter($identifier) { $resource_manager = restful()->getResourceManager(); try { $plugin = $resource_manager->getPlugin($identifier); } catch (UnauthorizedException $e) { return NULL; } catch (PluginNotFoundException $e) { throw new NotFoundException('Invalid URL path.'); } // If the plugin is not discoverable throw an access denied exception. $definition = $plugin->getPluginDefinition(); if (empty($definition['discoverable'])) { throw new InaccessibleRecordException(sprintf('The plugin %s is not discoverable.', $plugin->getResourceName())); } return new DataInterpreterPlug($this->getAccount(), new PluginWrapper($plugin)); } /** * {@inheritdoc} */ public function getCacheFragments($identifier) { // If we are trying to get the context for multiple ids, join them. if (is_array($identifier)) { $identifier = implode(',', $identifier); } $fragments = new ArrayCollection(array( 'resource' => $identifier, )); $options = $this->getOptions(); switch ($options['renderCache']['granularity']) { case DRUPAL_CACHE_PER_USER: if ($uid = $this->getAccount()->uid) { $fragments->set('user_id', (int) $uid); } break; case DRUPAL_CACHE_PER_ROLE: $fragments->set('user_role', implode(',', $this->getAccount()->roles)); break; } return $fragments; } } ================================================ FILE: src/Plugin/resource/DataProvider/DataProviderResource.php ================================================ setRequest($request); $this->resource = $resource; $this->referencedDataProvider = $resource->getDataProvider(); parent::__construct($request, $field_definitions, $account, $plugin_id, $resource_path, $options, $langcode); } /** * {@inheritdoc} */ public static function init(RequestInterface $request, $resource_name, array $version, $resource_path = NULL) { /* @var ResourceInterface $resource */ $instance_id = $resource_name . PluginBase::DERIVATIVE_SEPARATOR . $version[0] . '.' . $version[1]; $resource = restful() ->getResourceManager() ->getPluginCopy($instance_id, Request::create('', array(), RequestInterface::METHOD_GET)); $plugin_definition = $resource->getPluginDefinition(); $resource->setPath($resource_path); return new static($request, $resource->getFieldDefinitions(), $resource->getAccount(), $resource->getPluginId(), $resource->getPath(), $plugin_definition['dataProvider'], static::getLanguage(), $resource); } /** * {@inheritdoc} */ public function index() { return $this->referencedDataProvider->index(); } /** * {@inheritdoc} */ public function getIndexIds() { return $this->referencedDataProvider->getIndexIds(); } /** * {@inheritdoc} */ public function create($object) { return $this->referencedDataProvider->create($object); } /** * {@inheritdoc} */ public function view($identifier) { return $this->referencedDataProvider->view($identifier); } /** * {@inheritdoc} */ public function viewMultiple(array $identifiers) { return $this->referencedDataProvider->viewMultiple($identifiers); } /** * {@inheritdoc} */ public function update($identifier, $object, $replace = FALSE) { return $this->referencedDataProvider->update($identifier, $object, $replace); } /** * {@inheritdoc} */ public function remove($identifier) { $this->referencedDataProvider->remove($identifier); } /** * {@inheritdoc} */ public function merge($identifier, $object) { if (!$identifier) { return $this->referencedDataProvider->create($object); } $replace = ($method = $this->getRequest()->getMethod()) ? $method == RequestInterface::METHOD_PUT : FALSE; return $this->referencedDataProvider->update($identifier, $object, $replace); } /** * {@inheritdoc} */ public function count() { return $this->referencedDataProvider->count(); } /** * {@inheritdoc} */ protected function initDataInterpreter($identifier) { return NULL; } } ================================================ FILE: src/Plugin/resource/DataProvider/DataProviderResourceInterface.php ================================================ value(); if (empty($term->vid)) { $vocabulary = taxonomy_vocabulary_machine_name_load($term->vocabulary_machine_name); $term->vid = $vocabulary->vid; } parent::setPropertyValues($wrapper, $object, $replace); } } ================================================ FILE: src/Plugin/resource/Decorators/CacheDecoratedResource.php ================================================ subject = $subject; $this->cacheController = $cache_controller ? $cache_controller : $this->newCacheObject(); $cache_info = $this->defaultCacheInfo(); $this->pluginDefinition['renderCache'] = $cache_info; } /** * {@inheritdoc} */ public function getCacheController() { return $this->cacheController; } /** * Get the default cache object based on the plugin configuration. * * By default, this returns an instance of the DrupalDatabaseCache class. * Classes implementing DrupalCacheInterface can register themselves both as a * default implementation and for specific bins. * * @return \DrupalCacheInterface * The cache object associated with the specified bin. * * @see \DrupalCacheInterface * @see _cache_get_object() */ protected function newCacheObject() { // We do not use drupal_static() here because we do not want to change the // storage of a cache bin mid-request. static $cache_object; if (isset($cache_object)) { // Return cached object. return $cache_object; } $cache_info = $this->defaultCacheInfo(); $class_name = empty($cache_info['class']) ? NULL : $cache_info['class']; // If there is no class name in the plugin definition, try to get it from // the variables. if (empty($class_name)) { $class_name = variable_get('cache_class_' . $cache_info['bin']); } // If it is still empty, then default to drupal's default cache class. if (empty($class_name)) { $class_name = variable_get('cache_default_class', 'DrupalDatabaseCache'); } $cache_object = new $class_name($cache_info['bin']); return $cache_object; } /** * {@inheritdoc} */ public function dataProviderFactory() { if ($this->dataProvider && $this->dataProvider instanceof CacheDecoratedDataProvider) { return $this->dataProvider; } // Get the data provider from the subject of the decorator. $decorated_provider = $this->subject->dataProviderFactory(); $this->dataProvider = new CacheDecoratedDataProvider($decorated_provider, $this->getCacheController()); $plugin_definition = $this->getPluginDefinition(); $this->dataProvider->addOptions(array( 'renderCache' => $this->defaultCacheInfo(), 'resource' => array( 'version' => array( 'major' => $plugin_definition['majorVersion'], 'minor' => $plugin_definition['minorVersion'], ), 'name' => $plugin_definition['resource'], ), )); return $this->dataProvider; } /** * {@inheritdoc} */ public function getPath() { return $this->subject->getPath(); } /** * {@inheritdoc} */ public function setPath($path) { $this->subject->setPath($path); } /** * {@inheritdoc} */ public function getFieldDefinitions() { return $this->subject->getFieldDefinitions(); } /** * {@inheritdoc} */ public function getDataProvider() { if (isset($this->dataProvider)) { return $this->dataProvider; } $this->dataProvider = $this->dataProviderFactory(); return $this->dataProvider; } /** * {@inheritdoc} */ public function setDataProvider(DataProviderInterface $data_provider = NULL) { $this->dataProvider = $data_provider; } /** * {@inheritdoc} */ public function process() { $path = $this->getPath(); return ResourceManager::executeCallback($this->getControllerFromPath($path), array($path)); } /** * {@inheritdoc} */ public function view($path) { // TODO: This is duplicating the code from Resource::view $ids = explode(static::IDS_SEPARATOR, $path); // REST requires a canonical URL for every resource. $canonical_path = $this->getDataProvider()->canonicalPath($path); $this ->getRequest() ->getHeaders() ->add(HttpHeader::create('Link', $this->versionedUrl($canonical_path, array(), FALSE) . '; rel="canonical"')); // If there is only one ID then use 'view'. Else, use 'viewMultiple'. The // difference between the two is that 'view' allows access denied // exceptions. if (count($ids) == 1) { return array($this->getDataProvider()->view($ids[0])); } else { return $this->getDataProvider()->viewMultiple($ids); } } /** * {@inheritdoc} */ public function index($path) { // TODO: This is duplicating the code from Resource::index return $this->getDataProvider()->index(); } /** * {@inheritdoc} */ public function update($path) { $this->invalidateResourceCache($path); // Update according to the decorated. return $this->subject->update($path); } /** * {@inheritdoc} */ public function replace($path) { $this->invalidateResourceCache($path); // Update according to the decorated. return $this->subject->replace($path); } /** * {@inheritdoc} */ public function remove($path) { $this->invalidateResourceCache($path); $this->subject->remove($path); } /** * Gets the default cache info. * * @return array * The cache info. */ protected function defaultCacheInfo() { $plugin_definition = $this->getPluginDefinition(); $cache_info = empty($plugin_definition['renderCache']) ? array() : $plugin_definition['renderCache']; $cache_info += array( 'render' => variable_get('restful_render_cache', FALSE), 'class' => NULL, 'bin' => RenderCache::CACHE_BIN, 'expire' => CACHE_PERMANENT, 'simpleInvalidate' => TRUE, 'granularity' => DRUPAL_CACHE_PER_USER, ); return $cache_info; } /** * {@inheritdoc} */ public function getResourceMachineName() { return $this->subject->getResourceMachineName(); } /** * {@inheritdoc} * * This is a decorated resource, get proxy the call until you reach the * annotated resource. */ public function getPluginDefinition() { return $this->subject->getPluginDefinition(); } /** * {@inheritdoc} */ public function enable() { $this->subject->enable(); } /** * {@inheritdoc} */ public function disable() { $this->subject->disable(); } /** * {@inheritdoc} */ public function isEnabled() { return $this->subject->isEnabled(); } /** * {@inheritdoc} */ public function hasSimpleInvalidation() { $data_provider = $this->getDataProvider(); $options = $data_provider->getOptions(); $cache_info = $options['renderCache']; return !empty($cache_info['simpleInvalidate']); } /** * Invalidates the resource cache for the given resource on the provided id. * * @param string $id * The id. */ protected function invalidateResourceCache($id) { // Invalidate the render cache for this resource. $query = new \EntityFieldQuery(); $canonical_id = $this->getDataProvider()->canonicalPath($id); $query ->entityCondition('entity_type', 'cache_fragment') ->propertyCondition('type', 'resource') ->propertyCondition('value', $this::serializeKeyValue($this->getResourceName(), $canonical_id)); foreach (CacheFragmentController::lookUpHashes($query) as $hash) { $this->getCacheController()->clear($hash); } } /** * {@inheritdoc} */ public static function serializeKeyValue($key, $value) { return sprintf('%s%s%s', $key, static::CACHE_PAIR_SEPARATOR, $value); } } ================================================ FILE: src/Plugin/resource/Decorators/CacheDecoratedResourceInterface.php ================================================ subject = $subject; $plugin_definition = $subject->getPluginDefinition(); $rate_limit_info = empty($plugin_definition['rateLimit']) ? array() : $plugin_definition['rateLimit']; if ($limit = variable_get('restful_global_rate_limit', 0)) { $rate_limit_info['global'] = array( 'period' => variable_get('restful_global_rate_period', 'P1D'), 'limits' => array(), ); foreach (user_roles() as $role_name) { $rate_limit_info['global']['limits'][$role_name] = $limit; } } $this->rateLimitManager = $rate_limit_manager ? $rate_limit_manager : new RateLimitManager($this, $rate_limit_info); } /** * Setter for $rateLimitManager. * * @param RateLimitManager $rate_limit_manager * The rate limit manager. */ protected function setRateLimitManager(RateLimitManager $rate_limit_manager) { $this->rateLimitManager = $rate_limit_manager; } /** * Getter for $rate_limit_manager. * * @return RateLimitManager * The rate limit manager. */ protected function getRateLimitManager() { return $this->rateLimitManager; } /** * {@inheritdoc} */ public function process() { // This will throw the appropriate exception if needed. $this->getRateLimitManager()->checkRateLimit($this->getRequest()); return $this->subject->process(); } /** * {@inheritdoc} */ public function setAccount($account) { $this->subject->setAccount($account); $this->rateLimitManager->setAccount($account); } } ================================================ FILE: src/Plugin/resource/Decorators/ResourceDecoratorBase.php ================================================ subject; } /** * {@inheritdoc} */ public function getPrimaryResource() { $resource = $this->getDecoratedResource(); while ($resource instanceof ResourceDecoratorInterface) { $resource = $resource->getDecoratedResource(); } return $resource; } /** * {@inheritdoc} */ public function dataProviderFactory() { return $this->subject->dataProviderFactory(); } /** * {@inheritdoc} */ public function getAccount($cache = TRUE) { return $this->subject->getAccount($cache); } /** * {@inheritdoc} */ public function setAccount($account) { $this->subject->setAccount($account); $this->getDataProvider()->setAccount($account); } /** * {@inheritdoc} */ public function switchUserBack() { $this->subject->switchUserBack(); } /** * {@inheritdoc} */ public function discover($path = NULL) { return $this->subject->discover($path); } /** * {@inheritdoc} */ public function getRequest() { return $this->subject->getRequest(); } /** * {@inheritdoc} */ public function setRequest(RequestInterface $request) { $this->subject->setRequest($request); // Make sure that the request is updated in the data provider. $this->getDataProvider()->setRequest($request); } /** * {@inheritdoc} */ public function getPath() { return $this->subject->getPath(); } /** * {@inheritdoc} */ public function setPath($path) { $this->subject->setPath($path); } /** * {@inheritdoc} */ public function getFieldDefinitions() { return $this->subject->getFieldDefinitions(); } /** * {@inheritdoc} */ public function getDataProvider() { return $this->subject->getDataProvider(); } /** * {@inheritdoc} */ public function setDataProvider(DataProviderInterface $data_provider = NULL) { $this->subject->setDataProvider($data_provider); } /** * {@inheritdoc} */ public function getResourceName() { return $this->subject->getResourceName(); } /** * {@inheritdoc} */ public function process() { return $this->subject->process(); } /** * {@inheritdoc} */ public function controllersInfo() { return $this->subject->controllersInfo(); } /** * {@inheritdoc} */ public function getControllers() { return $this->subject->getControllers(); } /** * {@inheritdoc} */ public function index($path) { return $this->subject->index($path); } /** * {@inheritdoc} */ public function view($path) { return $this->subject->view($path); } /** * {@inheritdoc} */ public function create($path) { return $this->subject->create($path); } /** * {@inheritdoc} */ public function update($path) { return $this->subject->update($path); } /** * {@inheritdoc} */ public function replace($path) { return $this->subject->replace($path); } /** * {@inheritdoc} */ public function remove($path) { $this->subject->remove($path); } /** * {@inheritdoc} */ public function getVersion() { return $this->subject->getVersion(); } /** * {@inheritdoc} */ public function versionedUrl($path = '', $options = array(), $version_string = TRUE) { return $this->subject->versionedUrl($path, $options, $version_string); } /** * {@inheritdoc} */ public function getConfiguration() { return $this->subject->getConfiguration(); } /** * {@inheritdoc} */ public function setConfiguration(array $configuration) { $this->subject->setConfiguration($configuration); } /** * {@inheritdoc} */ public function defaultConfiguration() { return $this->subject->defaultConfiguration(); } /** * {@inheritdoc} */ public function calculateDependencies() { return $this->subject->calculateDependencies(); } /** * {@inheritdoc} */ public function access() { return $this->subject->access(); } /** * {@inheritdoc} */ public function getControllerFromPath($path = NULL, ResourceInterface $resource = NULL) { return $this->subject->getControllerFromPath($path, $resource ?: $this); } /** * {@inheritdoc} */ public function getResourceMachineName() { return $this->subject->getResourceMachineName(); } /** * {@inheritdoc} * * This is a decorated resource, get proxy the request until you reach the * annotated resource. */ public function getPluginDefinition() { return $this->subject->getPluginDefinition(); } /** * {@inheritdoc} * * This is a decorated resource, set proxy the request until you reach the * annotated resource. */ public function setPluginDefinition(array $plugin_definition) { $this->subject->setPluginDefinition($plugin_definition); if (!empty($plugin_definition['dataProvider'])) { $this->getDataProvider()->addOptions($plugin_definition['dataProvider']); } } /** * {@inheritdoc} */ public function enable() { $this->subject->enable(); } /** * {@inheritdoc} */ public function disable() { $this->subject->disable(); } /** * {@inheritdoc} */ public function isEnabled() { return $this->subject->isEnabled(); } /** * {@inheritdoc} */ public function setFieldDefinitions(ResourceFieldCollectionInterface $field_definitions) { return $this->subject->setFieldDefinitions($field_definitions); } /** * {@inheritdoc} */ public function getUrl(array $options = array(), $keep_query = TRUE, RequestInterface $request = NULL) { return $this->subject->getUrl($options, $keep_query, $request); } /** * {@inheritdoc} */ public function doGet($path = '', array $query = array()) { $this->setPath($path); $this->setRequest(Request::create($this->versionedUrl($path, array('absolute' => FALSE)), $query, RequestInterface::METHOD_GET)); return $this->process(); } /** * {@inheritdoc} */ public function doPost(array $parsed_body) { return $this->doWrite(RequestInterface::METHOD_POST, '', $parsed_body); } /** * {@inheritdoc} */ public function doPatch($path, array $parsed_body) { if (!$path) { throw new BadRequestException('PATCH requires a path. None given.'); } return $this->doWrite(RequestInterface::METHOD_PATCH, $path, $parsed_body); } /** * {@inheritdoc} */ public function doPut($path, array $parsed_body) { if (!$path) { throw new BadRequestException('PUT requires a path. None given.'); } return $this->doWrite(RequestInterface::METHOD_PUT, $path, $parsed_body); } /** * {@inheritdoc} */ private function doWrite($method, $path, array $parsed_body) { $this->setPath($path); $this->setRequest(Request::create($this->versionedUrl($path, array('absolute' => FALSE)), array(), $method, NULL, FALSE, NULL, array(), array(), array(), $parsed_body)); return $this->process(); } /** * {@inheritdoc} */ public function doDelete($path) { if (!$path) { throw new BadRequestException('DELETE requires a path. None given.'); } $this->setPath($path); $this->setRequest(Request::create($this->versionedUrl($path, array('absolute' => FALSE)), array(), RequestInterface::METHOD_DELETE)); return $this->process(); } /** * {@inheritdoc} */ public function getPluginId() { return $this->subject->getPluginId(); } /** * Checks if the decorated object is an instance of something. * * @param string $class * Class or interface to check the instance. * * @return bool * TRUE if the decorated object is an instace of the $class. FALSE * otherwise. */ public function isInstanceOf($class) { if ($this instanceof $class || $this->subject instanceof $class) { return TRUE; } // Check if the decorated resource is also a decorator. if ($this->subject instanceof ExplorableDecoratorInterface) { return $this->subject->isInstanceOf($class); } return FALSE; } /** * If any method not declared, then defer it to the decorated field. * * This decorator class is proxying all the calls declared in the * ResourceInterface to the underlying decorated resource. But it is not * doing it for any of the methods of the parents of ResourceInterface. * * With this code, any method that is not declared in the class will try to * make that method call it in the decorated resource. * * @param string $name * The name of the method that could not be found. * @param array $arguments * The arguments passed to the method, collected in an array. * * @return mixed * The result of the call. */ public function __call($name, $arguments) { return call_user_func_array(array($this->subject, $name), $arguments); } } ================================================ FILE: src/Plugin/resource/Decorators/ResourceDecoratorInterface.php ================================================ array( 'property' => 'label', ), 'description' => array( 'property' => 'description', ), 'name' => array( 'property' => 'name', ), 'resource' => array( 'property' => 'resource', ), 'majorVersion' => array( 'property' => 'majorVersion', ), 'minorVersion' => array( 'property' => 'minorVersion', ), 'self' => array( 'callback' => array($this, 'getSelf'), ), ); } /** * {@inheritdoc} */ protected function dataProviderClassName() { return '\Drupal\restful\Plugin\resource\DataProvider\DataProviderPlug'; } /** * Returns the URL to the endpoint result. * * @param DataInterpreterInterface $interpreter * The plugin's data interpreter. * * @return string * The RESTful endpoint URL. */ public function getSelf(DataInterpreterInterface $interpreter) { if ($menu_item = $interpreter->getWrapper()->get('menuItem')) { $url = variable_get('restful_hook_menu_base_path', 'api') . '/' . $menu_item; return url($url, array('absolute' => TRUE)); } $base_path = variable_get('restful_hook_menu_base_path', 'api'); return url($base_path . '/v' . $interpreter->getWrapper()->get('majorVersion') . '.' . $interpreter->getWrapper()->get('minorVersion') . '/' . $interpreter->getWrapper()->get('resource'), array('absolute' => TRUE)); } /** * @inheritDoc */ public function controllersInfo() { return array( '' => array( // GET returns a list of entities. RequestInterface::METHOD_GET => 'index', RequestInterface::METHOD_HEAD => 'index', ), // We don't know what the ID looks like, assume that everything is the ID. '^.*$' => array( RequestInterface::METHOD_GET => 'view', RequestInterface::METHOD_HEAD => 'view', RequestInterface::METHOD_PUT => array( 'callback' => 'replace', 'access callback' => 'resourceManipulationAccess', ), RequestInterface::METHOD_DELETE => array( 'callback' => 'remove', 'access callback' => 'resourceManipulationAccess', ), ), ); } /** * Helper callback to check authorization for write operations. * * @param string $path * The resource path. * * @return bool * TRUE to grant access. FALSE otherwise. */ public function resourceManipulationAccess($path) { return user_access('administer restful resources', $this->getAccount()); } } ================================================ FILE: src/Plugin/resource/Field/PublicFieldInfo/PublicFieldInfoBase.php ================================================ array( 'label' => '', 'description' => '', ), // Describe the data. 'data' => array( 'type' => NULL, 'read_only' => FALSE, 'cardinality' => 1, 'required' => FALSE, ), // Information about the form element. 'form_element' => array( 'type' => NULL, 'default_value' => '', 'placeholder' => '', 'size' => NULL, 'allowed_values' => NULL, ), ); /** * Sections for the field information. * * @var array[] */ protected $categories = array(); /** * The name of the public field. * * @var string */ protected $fieldName = ''; /** * PublicFieldInfoBase constructor. * * @param string $field_name * The name of the field. * @param array[] $sections * The array of categories information. */ public function __construct($field_name, array $sections = array()) { $this->fieldName = $field_name; $sections = drupal_array_merge_deep($this::$defaultSections, $sections); foreach ($sections as $section_name => $section_info) { $this->addCategory($section_name, $section_info); } } /** * {@inheritdoc} */ public function prepare() { return $this->categories; } /** * {@inheritdoc} */ public function addCategory($category_name, array $section_info) { try { $this->validate($category_name, $section_info); // Process the section info adding defaults if needed. $this->categories[$category_name] = $this->process($category_name, $section_info); } catch (ServerConfigurationException $e) { // If there are validation errors do not add the section. } } /** * {@inheritdoc} */ public function getSection($section_name) { return empty($this->categories[$section_name]) ? array() : $this->categories[$section_name]; } /** * {@inheritdoc} */ public function addSectionDefaults($section_name, array $section_info) { $this->addCategory($section_name, array_merge( $section_info, $this->getSection($section_name) )); } /** * Validates the provided data for the section. * * @param string $section_name * The name of the categories. By default RESTful supports 'info', * 'form_element' and 'data'. * @param array $section_info * The structured array with the section information. * * @throws ServerConfigurationException * If the field does not pass validation. */ protected function validate($section_name, array $section_info) { if ($section_name == 'info') { $this->validateInfo($section_info); } elseif ($section_name == 'data') { $this->validateData($section_info); } elseif ($section_name == 'form_element') { $this->validateFormElement($section_info); } } /** * Processes the provided data for the section. * * @param string $section_name * The name of the categories. By default RESTful supports 'info', * 'form_element' and 'data'. * @param array $section_info * The structured array with the section information. * * @returns array * The processed section info. */ protected function process($section_name, array $section_info) { if ($section_name == 'data') { if ($section_info['type'] == 'string') { $section_info['size'] = isset($section_info['size']) ? $section_info['size'] : 255; } } elseif ($section_name == 'form_element') { // Default title and description to the ones in the 'info' section. if (empty($section_info['title'])) { $section_info['title'] = empty($this->categories['info']['title']) ? $this->fieldName : $this->categories['info']['title']; if (!empty($this->categories['info']['description'])) { $section_info['description'] = $this->categories['info']['description']; } } } return $section_info; } /** * Validates the info section. * * @param array $section_info * The structured array with the section information. * * @throws ServerConfigurationException * If the field does not pass validation. */ protected function validateInfo(array $section_info) { if (empty($section_info['label'])) { throw new ServerConfigurationException(sprintf('The label information is missing for this field: %s.', $this->fieldName)); } } /** * Validates the info section. * * @param array $section_info * The structured array with the section information. * * @throws ServerConfigurationException * If the field does not pass validation. */ protected function validateData(array $section_info) { if (empty($section_info['type'])) { throw new ServerConfigurationException(sprintf('The schema information is not valid for this field: %s.', $this->fieldName)); } } /** * Validates the info section. * * @param array $section_info * The structured array with the section information. * * @throws ServerConfigurationException * If the field does not pass validation. */ protected function validateFormElement(array $section_info) { if (empty($section_info['type'])) { throw new ServerConfigurationException(sprintf('The form element information is not valid for this field: %s.', $this->fieldName)); } } } ================================================ FILE: src/Plugin/resource/Field/PublicFieldInfo/PublicFieldInfoEntity.php ================================================ property = $property; $this->entityType = $entity_type; $this->bundle = $bundle; } /** * {@inheritdoc} */ public function getFormSchemaAllowedValues() { if (!module_exists('options')) { return NULL; } $field_name = $this->property; if (!$field_info = field_info_field($field_name)) { return NULL; } if (!$field_instance = field_info_instance($this->entityType, $field_name, $this->bundle)) { return NULL; } if (!$this::formSchemaHasAllowedValues($field_info, $field_instance)) { // Field doesn't have allowed values. return NULL; } // Use Field API's widget to get the allowed values. $type = str_replace('options_', '', $field_instance['widget']['type']); $multiple = $field_info['cardinality'] > 1 || $field_info['cardinality'] == FIELD_CARDINALITY_UNLIMITED; // Always pass TRUE for "required" and "has_value", as we don't want to get // the "none" option. $required = TRUE; $has_value = TRUE; $properties = _options_properties($type, $multiple, $required, $has_value); // Mock an entity. $values = array(); $entity_info = $this->getEntityInfo(); if (!empty($entity_info['entity keys']['bundle'])) { // Set the bundle of the entity. $values[$entity_info['entity keys']['bundle']] = $this->bundle; } $entity = entity_create($this->entityType, $values); return _options_get_options($field_info, $field_instance, $properties, $this->entityType, $entity); } /** * {@inheritdoc} */ public function getFormSchemaAllowedType() { if (!module_exists('options')) { return NULL; } $field_name = $this->property; if (!$field_info = field_info_field($field_name)) { return NULL; } if (!$field_instance = field_info_instance($this->entityType, $field_name, $this->bundle)) { return NULL; } return $field_instance['widget']['type']; } /** * Get the entity info for the current entity the endpoint handling. * * @param string $type * Optional. The entity type. * * @return array * The entity info. * * @see entity_get_info(). */ protected function getEntityInfo($type = NULL) { return entity_get_info($type ? $type : $this->entityType); } /** * Determines if a field has allowed values. * * If Field is reference, and widget is autocomplete, so for performance * reasons we do not try to grab all the referenced entities. * * @param array $field * The field info array. * @param array $field_instance * The instance info array. * * @return bool * TRUE if a field should be populated with the allowed values. */ protected static function formSchemaHasAllowedValues($field, $field_instance) { $field_types = array( 'entityreference', 'taxonomy_term_reference', 'field_collection', 'commerce_product_reference', ); $widget_types = array( 'taxonomy_autocomplete', 'entityreference_autocomplete', 'entityreference_autocomplete_tags', 'commerce_product_reference_autocomplete', ); return !in_array($field['type'], $field_types) || !in_array($field_instance['widget']['type'], $widget_types); } } ================================================ FILE: src/Plugin/resource/Field/PublicFieldInfo/PublicFieldInfoEntityInterface.php ================================================ fieldName = $field_name; } /** * {@inheritdoc} */ public function prepare() { return array(); } /** * {@inheritdoc} */ public function addCategory($category_name, array $section_info) {} /** * {@inheritdoc} */ public function getSection($section_name) { return array(); } /** * {@inheritdoc} */ public function addSectionDefaults($section_name, array $section_info) {} } ================================================ FILE: src/Plugin/resource/Field/ResourceField.php ================================================ setRequest($request); if (empty($field['public_name'])) { throw new ServerConfigurationException('No public name provided in the field mappings.'); } $this->publicName = $field['public_name']; $this->accessCallbacks = isset($field['access_callbacks']) ? $field['access_callbacks'] : $this->accessCallbacks; $this->property = isset($field['property']) ? $field['property'] : $this->property; // $this->column = isset($field['column']) ? $field['column'] : $this->column; $this->callback = isset($field['callback']) ? $field['callback'] : $this->callback; $this->processCallbacks = isset($field['process_callbacks']) ? $field['process_callbacks'] : $this->processCallbacks; $this->resource = isset($field['resource']) ? $field['resource'] : $this->resource; $this->methods = isset($field['methods']) ? $field['methods'] : $this->methods; // Store the definition, useful to access custom keys on custom resource // fields. $this->definition = $field; } /** * {@inheritdoc} */ public static function create(array $field, RequestInterface $request = NULL) { $request = $request ?: restful()->getRequest(); if ($class_name = static::fieldClassName($field)) { if ($class_name != get_called_class() && $class_name != '\\' . get_called_class()) { // Call the create factory in the derived class. return call_user_func_array(array($class_name, 'create'), array( $field, $request, new static($field, $request), )); } } // If no other class was found, then use the current one. $resource_field = new static($field, $request); $resource_field->addDefaults(); return $resource_field; } /** * {@inheritdoc} */ public function value(DataInterpreterInterface $interpreter) { if ($callback = $this->getCallback()) { return ResourceManager::executeCallback($callback, array($interpreter)); } return NULL; } /** * {@inheritdoc} */ public function set($value, DataInterpreterInterface $interpreter) { // ResourceField only supports callbacks, so no set is possible. } /** * {@inheritdoc} */ public function access($op, DataInterpreterInterface $interpreter) { foreach ($this->getAccessCallbacks() as $callback) { $result = ResourceManager::executeCallback($callback, array( $op, $this, $interpreter, )); if ($result == ResourceFieldBase::ACCESS_DENY) { return FALSE; } } return TRUE; } /** * {@inheritdoc} */ public function addDefaults() { // Almost all the defaults come are applied by the object's property // defaults. if (!$resource = $this->getResource()) { return; } // Expand array to be verbose. if (!is_array($resource)) { $resource = array('name' => $resource); } // Set default value. $resource += array( 'fullView' => TRUE, ); // Set the default value for the version of the referenced resource. if (!isset($resource['majorVersion']) || !isset($resource['minorVersion'])) { list($major_version, $minor_version) = restful() ->getResourceManager() ->getResourceLastVersion($resource['name']); $resource['majorVersion'] = $major_version; $resource['minorVersion'] = $minor_version; } $this->setResource($resource); } /** * Get the class name to use based on the field definition. * * @param array $field_definition * The processed field definition with the user values. * * @return string * The class name to use. If the class name is empty or does not implement * ResourceFieldInterface then ResourceField will be used. NULL if nothing * was found. */ public static function fieldClassName(array $field_definition) { if (!empty($field_definition['class'])) { $class_name = $field_definition['class']; } // Search for indicators that this is a ResourceFieldEntityInterface. elseif ( !empty($field_definition['sub_property']) || !empty($field_definition['formatter']) || !empty($field_definition['wrapper_method']) || !empty($field_definition['wrapper_method_on_entity']) || !empty($field_definition['column']) || !empty($field_definition['image_styles']) || (!empty($field_definition['property']) ? field_info_field($field_definition['property']) : NULL) ) { $class_name = '\Drupal\restful\Plugin\resource\Field\ResourceFieldEntity'; } elseif (!empty($field_definition['property'])) { $class_name = '\Drupal\restful\Plugin\resource\Field\ResourceFieldKeyValue'; } if ( !empty($class_name) && class_exists($class_name) && in_array( 'Drupal\restful\Plugin\resource\Field\ResourceFieldInterface', class_implements($class_name) ) ) { return $class_name; } return NULL; } /** * {@inheritdoc} */ public function compoundDocumentId(DataInterpreterInterface $interpreter) { // Since this kind of field can be anything, just return the value. return $this->value($interpreter); } /** * {@inheritdoc} */ public function render(DataInterpreterInterface $interpreter) { return $this->executeProcessCallbacks($this->value($interpreter)); } /** * {@inheritdoc} */ public function getCardinality() { // Default to cardinality of 1. return 1; } /** * {@inheritdoc} */ public function setCardinality($cardinality) { $this->cardinality = $cardinality; } } ================================================ FILE: src/Plugin/resource/Field/ResourceFieldBase.php ================================================ 'articles', * // Verbose * 'page' => array( * 'name' => 'pages', * 'fullView' => FALSE, * ), * ); * * @var array */ protected $resource = array(); /** * A generic array storage. * * @var array */ protected $metadata = array(); /** * The HTTP methods where this field applies. * * This replaces the create_or_update_passthrough feature. Defaults to all. * * @var array */ protected $methods = array( RequestInterface::METHOD_GET, RequestInterface::METHOD_HEAD, RequestInterface::METHOD_POST, RequestInterface::METHOD_PUT, RequestInterface::METHOD_PATCH, RequestInterface::METHOD_OPTIONS, ); /** * The request object to be used. * * @var RequestInterface */ protected $request; /** * The field definition array. * * Use with caution. * * @var array */ protected $definition = array(); /** * Information about the field. * * @var PublicFieldInfoInterface */ protected $publicFieldInfo; /** * Holds the field cardinality. * * @var int */ protected $cardinality; /** * Get the request in the data provider. * * @return RequestInterface * The request. */ public function getRequest() { return $this->request; } /** * Set the request. * * @param RequestInterface $request * The request. */ public function setRequest(RequestInterface $request) { $this->request = $request; } /** * {@inheritdoc} */ public function getPublicName() { return $this->publicName; } /** * {@inheritdoc} */ public function setPublicName($public_name) { $this->publicName = $public_name; } /** * {@inheritdoc} */ public function getAccessCallbacks() { return $this->accessCallbacks; } /** * {@inheritdoc} */ public function setAccessCallbacks($access_callbacks) { $this->accessCallbacks = $access_callbacks; } /** * {@inheritdoc} */ public function getProperty() { return $this->property; } /** * {@inheritdoc} */ public function setProperty($property) { $this->property = $property; } /** * {@inheritdoc} */ public function getCallback() { return $this->callback; } /** * {@inheritdoc} */ public function setCallback($callback) { $this->callback = $callback; } /** * {@inheritdoc} */ public function getProcessCallbacks() { return $this->processCallbacks; } /** * {@inheritdoc} */ public function setProcessCallbacks($process_callbacks) { $this->processCallbacks = $process_callbacks; } /** * {@inheritdoc} */ public function getResource() { return $this->resource; } /** * {@inheritdoc} */ public function setResource($resource) { $this->resource = $resource; } /** * {@inheritdoc} */ public function getMethods() { return $this->methods; } /** * {@inheritdoc} */ public function setMethods($methods) { foreach ($methods as $method) { if (Request::isValidMethod($method)) { throw new ServerConfigurationException(sprintf('The method %s in the field resource mapping is not valid.', $method)); } } $this->methods = $methods; } /** * {@inheritdoc} */ public function id() { return $this->publicName; } /** * {@inheritdoc} */ public function isComputed() { return !$this->getProperty(); } /** * {@inheritdoc} */ public final static function isArrayNumeric(array $input) { $keys = array_keys($input); foreach ($keys as $key) { if (!ctype_digit((string) $key)) { return FALSE; } } return isset($keys[0]) ? $keys[0] == 0 : TRUE; } /** * {@inheritdoc} */ public function addMetadata($key, $value) { $path = explode(':', $key); $leave = array_pop($path); $element = &$this->internalMetadataElement($key); $element[$leave] = $value; } /** * {@inheritdoc} */ public function getMetadata($key) { $path = explode(':', $key); $leave = array_pop($path); $element = $this->internalMetadataElement($key); return isset($element[$leave]) ? $element[$leave] : NULL; } /** * {@inheritdoc} */ public function getDefinition() { return $this->definition; } /** * {@inheritdoc} */ public function executeProcessCallbacks($value) { $process_callbacks = $this->getProcessCallbacks(); if (!isset($value) || empty($process_callbacks)) { return $value; } foreach ($process_callbacks as $process_callback) { $value = ResourceManager::executeCallback($process_callback, array($value)); } return $value; } /** * Returns the last array element from the nested namespace array. * * Searches in the metadata nested array the element in the data tree pointed * by the colon separated key. If the key goes through a non-existing path, it * initalize an empty array. The reference to that element is returned for * reading and writing purposes. * * @param string $key * The namespaced key. * * @return array * The reference to the array element. */ protected function &internalMetadataElement($key) { // If there is a namespace, then use it to do nested arrays. $path = explode(':', $key); array_pop($path); $element = &$this->metadata; foreach ($path as $path_item) { if (!isset($element[$path_item])) { // Initialize an empty namespace. $element[$path_item] = array(); } $element = $element[$path_item]; } return $element; } /** * {@inheritdoc} */ public function getPublicFieldInfo() { return $this->publicFieldInfo; } /** * {@inheritdoc} */ public function setPublicFieldInfo(PublicFieldInfoInterface $public_field_info) { $this->publicFieldInfo = $public_field_info; } /** * Basic auto discovery information. * * @return array * The array of information ready to be encoded. */ public function autoDiscovery() { return $this ->getPublicFieldInfo() ->prepare(); } /** * Returns the basic discovery information for a given field. * * @param string $name * The name of the public field. * * @return array * The array of information ready to be encoded. */ public static function emptyDiscoveryInfo($name) { $info = new PublicFieldInfoNull($name); return $info->prepare(); } } ================================================ FILE: src/Plugin/resource/Field/ResourceFieldCollection.php ================================================ body->value->value(). * Defaults to FALSE. * - "formatter": Used for rendering the value of a configurable field using * Drupal field API's formatter. The value is the $display value that is * passed to field_view_field(). * - "wrapper_method": The wrapper's method name to perform on the field. * This can be used for example to get the entity label, by setting the * value to "label". Defaults to "value". * - "wrapper_method_on_entity": A Boolean to indicate on what to perform * the wrapper method. If TRUE the method will perform on the entity (e.g. * $wrapper->label()) and FALSE on the property or sub property * (e.g. $wrapper->field_reference->label()). Defaults to FALSE. * - "column": If the property is a field, set the column that would be used * in queries. For example, the default column for a text field would be * "value". Defaults to the first column returned by field_info_field(), * otherwise FALSE. * - "callback": A callable callback to get a computed value. The wrapped * entity is passed as argument. Defaults To FALSE. * The callback function receive as first argument the entity * EntityMetadataWrapper object. * - "process_callbacks": An array of callbacks to perform on the returned * value, or an array with the object and method. Defaults To empty array. * - "resource": This property can be assigned only to an entity reference * field. Array of restful resources keyed by the target bundle. For * example, if the field is referencing a node entity, with "Article" and * "Page" bundles, we are able to map those bundles to their related * resource. Items with bundles that were not explicitly set would be * ignored. * It is also possible to pass an array as the value, with: * - "name": The resource name. * - "fullView": Determines if the referenced resource should be rendered * or just the referenced ID(s) to appear. Defaults to TRUE. * array( * // Shorthand. * 'article' => 'articles', * // Verbose * 'page' => array( * 'name' => 'pages', * 'fullView' => FALSE, * ), * ); * - "create_or_update_passthrough": Determines if a public field that isn't * mapped to any property or field, may be passed upon create or update * of an entity. Defaults to FALSE. * @param RequestInterface $request * The request. */ public function __construct(array $fields = array(), RequestInterface $request) { foreach ($fields as $public_name => $field_info) { $field_info['public_name'] = $public_name; // The default values are added. if (empty($field_info['resource'])) { $resource_field = ResourceField::create($field_info, $request); } else { $resource_field = ResourceFieldResource::create($field_info, $request); } $this->fields[$resource_field->id()] = $resource_field; } $this->idField = empty($fields['id']) ? NULL : $this->get('id'); } /** * {@inheritdoc} */ public static function factory(array $fields = array(), RequestInterface $request = NULL) { // TODO: Explore the possibility to change factory methods by using FactoryInterface. return new static($fields, $request ?: restful()->getRequest()); } /** * {@inheritdoc} */ public static function create() { static::factory(static::getInfo()); } /** * {@inheritdoc} */ public static function getInfo() { return array(); } /** * {@inheritdoc} */ public function get($key) { return isset($this->fields[$key]) ? $this->fields[$key] : NULL; } /** * {@inheritdoc} */ public function set($key, ResourceFieldInterface $field) { $this->fields[$key] = $field; } /** * {@inheritdoc} */ public function current() { return current($this->fields); } /** * {@inheritdoc} */ public function next() { return next($this->fields); } /** * {@inheritdoc} */ public function key() { return key($this->fields); } /** * {@inheritdoc} */ public function valid() { $key = key($this->fields); return $key !== NULL && $key !== FALSE; } /** * {@inheritdoc} */ public function rewind() { return reset($this->fields); } /** * {@inheritdoc} */ public function count() { return count($this->fields); } /** * {@inheritdoc} */ public function getInterpreter() { return $this->interpreter; } /** * {@inheritdoc} */ public function setInterpreter($interpreter) { $this->interpreter = $interpreter; } /** * {@inheritdoc} */ public function getIdField() { return $this->idField; } /** * {@inheritdoc} */ public function setIdField($id_field) { $this->idField = $id_field; } /** * {@inheritdoc} */ public function getResourceName() { $resource_id = $this->getResourceId(); $pos = strpos($resource_id, Resource::DERIVATIVE_SEPARATOR); return $pos === FALSE ? $resource_id : substr($resource_id, 0, $pos); } /** * {@inheritdoc} */ public function getResourceId() { return $this->resourceId; } /** * {@inheritdoc} */ public function setResourceId($resource_id) { $this->resourceId = $resource_id; } /** * {@inheritdoc} */ public function evalFilter(array $filter) { // Initialize to TRUE for AND and FALSE for OR (neutral value). $match = $filter['conjunction'] == 'AND'; for ($index = 0; $index < count($filter['value']); $index++) { if (!$resource_field = $this->get($filter['public_field'])) { // If the field is unknown don't use se filter. return TRUE; } $filter_value = $resource_field->value($this->getInterpreter()); if (is_null($filter_value)) { // Property doesn't exist on the plugin, so filter it out. return FALSE; } if ($filter['conjunction'] == 'OR') { $match = $match || $this::evaluateExpression($filter_value, $filter['value'][$index], $filter['operator'][$index]); if ($match) { break; } } else { $match = $match && $this::evaluateExpression($filter_value, $filter['value'][$index], $filter['operator'][$index]); if (!$match) { break; } } } return $match; } /** * {@inheritdoc} */ public function setContext($context_id, ArrayCollection $context) { $this->context[$context_id] = $context; } /** * {@inheritdoc} */ public function getContext() { return $this->context; } /** * {@inheritdoc} */ public function getLimitFields() { return $this->limitFields; } /** * {@inheritdoc} */ public function setLimitFields($limit_fields) { $this->limitFields = $limit_fields; // Make sure that the nested fields are added appropriately. foreach ($limit_fields as $limit_field) { $parts = explode('.', $limit_field); $this->limitFields[] = $parts[0]; } $this->limitFields = array_unique($this->limitFields); } /** * Evaluate a simple expression. * * @param mixed $value1 * The first value. * @param mixed $value2 * The second value. * @param string $operator * The operator. * * @return bool * TRUE or FALSE based on the evaluated expression. * * @throws BadRequestException */ protected static function evaluateExpression($value1, $value2, $operator) { switch($operator) { case '=': return $value1 == $value2; case '<': return $value1 < $value2; case '>': return $value1 > $value2; case '>=': return $value1 >= $value2; case '<=': return $value1 <= $value2; case '<>': case '!=': return $value1 != $value2; case 'IN': return in_array($value1, $value2); case 'BETWEEN': return $value1 >= $value2[0] && $value1 >= $value2[1]; } return FALSE; } } ================================================ FILE: src/Plugin/resource/Field/ResourceFieldCollectionInterface.php ================================================ columnForQuery = empty($field['columnForQuery']) ? $this->getProperty() : $field['columnForQuery']; } /** * {@inheritdoc} */ public static function create(array $field, RequestInterface $request = NULL) { $resource_field = new static($field, $request ?: restful()->getRequest()); $resource_field->addDefaults(); return $resource_field; } /** * {@inheritdoc} */ public function getColumnForQuery() { return $this->columnForQuery; } /** * {@inheritdoc} */ public function value(DataInterpreterInterface $interpreter) { $value = parent::value($interpreter); if (isset($value)) { return $value; } return $interpreter->getWrapper()->get($this->getProperty()); } } ================================================ FILE: src/Plugin/resource/Field/ResourceFieldDbColumnInterface.php ================================================ body->value->value(). * * @var string */ protected $subProperty; /** * Used for rendering the value of a configurable field using Drupal field * API's formatter. The value is the $display value that is passed to * field_view_field(). * * @var array */ protected $formatter; /** * The wrapper's method name to perform on the field. This can be used for * example to get the entity label, by setting the value to "label". Defaults * to "value". * * @var string */ protected $wrapperMethod = 'value'; /** * A Boolean to indicate on what to perform the wrapper method. If TRUE the * method will perform on the entity (e.g. $wrapper->label()) and FALSE on the * property or sub property (e.g. $wrapper->field_reference->label()). * * @var bool */ protected $wrapperMethodOnEntity = FALSE; /** * If the property is a field, set the column that would be used in queries. * For example, the default column for a text field would be "value". Defaults * to the first column returned by field_info_field(), otherwise FALSE. * * @var string */ protected $column; /** * Array of image styles to apply to this resource field maps to an image * field. * * @var array */ protected $imageStyles = array(); /** * The entity type. * * @var string */ protected $entityType; /** * The bundle name. * * @var string */ protected $bundle; /** * Constructor. * * @param array $field * Contains the field values. * @param RequestInterface $request * The request. * * @throws ServerConfigurationException * If the entity type is empty. */ public function __construct(array $field, RequestInterface $request) { if ($this->decorated) { $this->setRequest($request); } if (empty($field['entityType'])) { throw new ServerConfigurationException(sprintf('Unknown entity type for %s resource field.', __CLASS__)); } $this->setEntityType($field['entityType']); $this->wrapperMethod = isset($field['wrapper_method']) ? $field['wrapper_method'] : $this->wrapperMethod; $this->subProperty = isset($field['sub_property']) ? $field['sub_property'] : $this->subProperty; $this->formatter = isset($field['formatter']) ? $field['formatter'] : $this->formatter; $this->wrapperMethodOnEntity = isset($field['wrapper_method_on_entity']) ? $field['wrapper_method_on_entity'] : $this->wrapperMethodOnEntity; $this->column = isset($field['column']) ? $field['column'] : $this->column; $this->imageStyles = isset($field['image_styles']) ? $field['image_styles'] : $this->imageStyles; if (!empty($field['bundle'])) { // TODO: Document this usage. $this->setBundle($field['bundle']); } } /** * {@inheritdoc} */ public static function create(array $field, RequestInterface $request = NULL, ResourceFieldInterface $decorated = NULL) { $request = $request ?: restful()->getRequest(); $resource_field = NULL; $class_name = static::fieldClassName($field); // If the class exists and is a ResourceFieldEntityInterface use that one. if ( $class_name && class_exists($class_name) && in_array( 'Drupal\restful\Plugin\resource\Field\ResourceFieldEntityInterface', class_implements($class_name) ) ) { $resource_field = new $class_name($field, $request); } // If no specific class was found then use the current one. if (!$resource_field) { // Create the current object. $resource_field = new static($field, $request); } if (!$resource_field) { throw new ServerConfigurationException('Unable to create resource field'); } // Set the basic object to the decorated property. $resource_field->decorate($decorated ? $decorated : new ResourceField($field, $request)); $resource_field->decorated->addDefaults(); // Add the default specifics for the current object. $resource_field->addDefaults(); return $resource_field; } /** * {@inheritdoc} */ public function value(DataInterpreterInterface $interpreter) { $value = $this->decorated->value($interpreter); if (isset($value)) { // Let the decorated resolve callbacks. return $value; } // Check user has access to the property. if (!$this->access('view', $interpreter)) { return NULL; } $property_wrapper = $this->propertyWrapper($interpreter); $wrapper = $interpreter->getWrapper(); if ($property_wrapper instanceof \EntityListWrapper) { $values = array(); // Multiple values. foreach ($property_wrapper->getIterator() as $item_wrapper) { $values[] = $this->singleValue($item_wrapper, $wrapper, $interpreter->getAccount()); } return $values; } return $this->singleValue($property_wrapper, $wrapper, $interpreter->getAccount()); } /** * {@inheritdoc} */ public function compoundDocumentId(DataInterpreterInterface $interpreter) { $collections = $this->render($interpreter); // Extract the document ID from the field resource collection. $process = function ($collection) { if (!$collection instanceof ResourceFieldCollectionInterface) { return $collection; } $id_field = $collection->getIdField(); return $id_field->render($collection->getInterpreter()); }; // If cardinality is 1, then we don't have an array. return $this->getCardinality() == 1 ? $process($collections) : array_map($process, array_filter($collections)); } /** * Helper function to get the identifier from a property wrapper. * * @param \EntityMetadataWrapper $property_wrapper * The property wrapper to get the ID from. * * @return string * An identifier. */ protected function propertyIdentifier(\EntityMetadataWrapper $property_wrapper) { if ($property_wrapper instanceof \EntityDrupalWrapper) { // The property wrapper is a reference to another entity get the entity // ID. $identifier = $this->referencedId($property_wrapper); $resource = $this->getResource(); // TODO: Make sure we still want to support fullView. if (!$resource || !$identifier || (isset($resource['fullView']) && $resource['fullView'] === FALSE)) { return $identifier; } // If there is a resource that we are pointing to, we need to use the id // field that that particular resource has in its configuration. Trying to // load by the entity id in that scenario will lead to a 404. // We'll load the plugin to get the idField configuration. $instance_id = sprintf('%s:%d.%d', $resource['name'], $resource['majorVersion'], $resource['minorVersion']); /* @var ResourceInterface $resource */ $resource = restful() ->getResourceManager() ->getPluginCopy($instance_id, Request::create('', array(), RequestInterface::METHOD_GET)); $plugin_definition = $resource->getPluginDefinition(); if (empty($plugin_definition['dataProvider']['idField'])) { return $identifier; } try { return $property_wrapper->{$plugin_definition['dataProvider']['idField']}->value(); } catch (\EntityMetadataWrapperException $e) { return $identifier; } } // The property is a regular one, get the value out of it and use it as // the embedded identifier. return $this->fieldValue($property_wrapper); } /** * {@inheritdoc} */ public function set($value, DataInterpreterInterface $interpreter) { try { $property_wrapper = $interpreter->getWrapper()->{$this->getProperty()}; $property_wrapper->set($value); } catch (\Exception $e) { $this->decorated->set($value, $interpreter); } } /** * Returns the value for the current single field. * * This implementation will also add some metadata to the resource field * object about the entity it is referencing. * * @param \EntityMetadataWrapper $property_wrapper * The property wrapper. Either \EntityDrupalWrapper or \EntityListWrapper. * @param \EntityDrupalWrapper $wrapper * The entity wrapper. * @param object $account * The user account. * * @return mixed * A single value for the field. * * @throws \Drupal\restful\Exception\BadRequestException * @throws \Drupal\restful\Exception\ServerConfigurationException */ protected function singleValue(\EntityMetadataWrapper $property_wrapper, \EntityDrupalWrapper $wrapper, $account) { if ($resource = $this->getResource()) { // TODO: The resource input data in the field definition has changed. // Now it does not need to be keyed by bundle since you don't even need // an entity to use the resource based field. $embedded_identifier = $this->propertyIdentifier($property_wrapper); // Allow embedding entities with ID 0, like the anon user. if (empty($embedded_identifier) && $embedded_identifier !== 0) { return NULL; } if (isset($resource['fullView']) && $resource['fullView'] === FALSE) { return $embedded_identifier; } // We support dot notation for the sparse fieldsets. That means that // clients can specify the fields to show based on the "fields" query // string parameter. $parsed_input = array( 'fields' => implode(',', $this->nestedDottedChildren('fields')), 'include' => implode(',', $this->nestedDottedChildren('include')), 'filter' => $this->nestedDottedChildren('filter'), ); $request = Request::create('', array_filter($parsed_input), RequestInterface::METHOD_GET); // Get a plugin (that can be altered with decorators. $embedded_resource = restful()->getResourceManager()->getPluginCopy(sprintf('%s:%d.%d', $resource['name'], $resource['majorVersion'], $resource['minorVersion'])); // Configure the plugin copy with the sub-request and sub-path. $embedded_resource->setPath($embedded_identifier); $embedded_resource->setRequest($request); $embedded_resource->setAccount($account); $metadata = $this->getMetadata($wrapper->getIdentifier()); $metadata = $metadata ?: array(); $metadata[] = $this->buildResourceMetadataItem($property_wrapper); $this->addMetadata($wrapper->getIdentifier(), $metadata); try { // Get the contents to embed in place of the reference ID. /* @var ResourceFieldCollection $embedded_entity */ $embedded_entity = $embedded_resource ->getDataProvider() ->view($embedded_identifier); } catch (InaccessibleRecordException $e) { // If you don't have access to the embedded entity is like not having // access to the property. watchdog_exception('restful', $e); return NULL; } catch (UnprocessableEntityException $e) { // If you access a nonexistent embedded entity. watchdog_exception('restful', $e); return NULL; } // Test if the $embedded_entity meets the filter or not. if (empty($parsed_input['filter'])) { return $embedded_entity; } foreach ($parsed_input['filter'] as $filter) { // Filters only apply if the target is the current field. if (!empty($filter['target']) && $filter['target'] == $this->getPublicName() && !$embedded_entity->evalFilter($filter)) { // This filter is not met. return NULL; } } return $embedded_entity; } if ($this->getFormatter()) { // Get value from field formatter. $value = $this->formatterValue($property_wrapper, $wrapper); } else { // Single value. $value = $this->fieldValue($property_wrapper); } return $value; } /** * {@inheritdoc} * * @throws \EntityMetadataWrapperException */ public function access($op, DataInterpreterInterface $interpreter) { // Perform basic access checks. if (!$this->decorated->access($op, $interpreter)) { return FALSE; } if (!$this->getProperty()) { // If there is no property we cannot check for property access. return TRUE; } // Perform field API access checks. if (!$property_wrapper = $this->propertyWrapper($interpreter)) { return FALSE; } if ($this->isWrapperMethodOnEntity() && $this->getWrapperMethod() && $this->getProperty()) { // Sometimes we define fields as $wrapper->getIdentifier. We need to // resolve that to $wrapper->nid to call $wrapper->nid->info(). $property_wrapper = $property_wrapper->{$this->getProperty()}; } $account = $interpreter->getAccount(); // Check format access for text fields. if ( $op == 'edit' && $property_wrapper->type() == 'text_formatted' && $property_wrapper->value() && $property_wrapper->format->value() ) { $format = (object) array('format' => $property_wrapper->format->value()); // Only check filter access on write contexts. if (!filter_access($format, $account)) { return FALSE; } } $info = $property_wrapper->info(); if ($op == 'edit' && empty($info['setter callback'])) { // Property does not allow setting. return FALSE; } // If $interpreter->getWrapper()->value() === FALSE it means that the entity // could not be loaded, thus checking properties on it will result in // errors. // Ex: this happens when the embedded author is the anonymous user. Doing // user_load(0) returns FALSE. $access = $interpreter->getWrapper() ->value() !== FALSE && $property_wrapper->access($op, $account); return $access !== FALSE; } /** * Get the wrapper for the property associated to the current field. * * @param DataInterpreterInterface $interpreter * The data source. * * @return \EntityMetadataWrapper * Either a \EntityStructureWrapper or a \EntityListWrapper. * * @throws ServerConfigurationException */ protected function propertyWrapper(DataInterpreterInterface $interpreter) { // This is the first method that gets called for all fields after loading // the entity. We'll use that opportunity to set the actual bundle of the // field. $this->setBundle($interpreter->getWrapper()->getBundle()); // Exposing an entity field. $wrapper = $interpreter->getWrapper(); // For entity fields the DataInterpreter needs to contain an EMW. if (!$wrapper instanceof \EntityDrupalWrapper) { throw new ServerConfigurationException('Cannot get a value without an entity metadata wrapper data source.'); } $property = $this->getProperty(); try { return ($property && !$this->isWrapperMethodOnEntity()) ? $wrapper->{$property} : $wrapper; } catch (\EntityMetadataWrapperException $e) { throw new UnprocessableEntityException(sprintf('The property %s could not be found in %s:%s.', $property, $wrapper->type(), $wrapper->getBundle())); } } /** * Get value from a property. * * @param \EntityMetadataWrapper $property_wrapper * The property wrapper. Either \EntityDrupalWrapper or \EntityListWrapper. * * @return mixed * A single or multiple values. */ protected function fieldValue(\EntityMetadataWrapper $property_wrapper) { if ($this->getSubProperty() && $property_wrapper->value()) { $property_wrapper = $property_wrapper->{$this->getSubProperty()}; } // Wrapper method. return $property_wrapper->{$this->getWrapperMethod()}(); } /** * Get value from a field rendered by Drupal field API's formatter. * * @param \EntityMetadataWrapper $property_wrapper * The property wrapper. Either \EntityDrupalWrapper or \EntityListWrapper. * @param \EntityDrupalWrapper $wrapper * The entity wrapper. * * @return mixed * A single or multiple values. * * @throws \Drupal\restful\Exception\ServerConfigurationException */ protected function formatterValue(\EntityMetadataWrapper $property_wrapper, \EntityDrupalWrapper $wrapper) { $value = NULL; if (!ResourceFieldEntity::propertyIsField($this->getProperty())) { // Property is not a field. throw new ServerConfigurationException(format_string('@property is not a configurable field, so it cannot be processed using field API formatter', array('@property' => $this->getProperty()))); } // Get values from the formatter. $output = field_view_field($this->getEntityType(), $wrapper->value(), $this->getProperty(), $this->getFormatter()); // Unset the theme, as we just want to get the value from the formatter, // without the wrapping HTML. unset($output['#theme']); if ($property_wrapper instanceof \EntityListWrapper) { // Multiple values. foreach (element_children($output) as $delta) { $value[] = drupal_render($output[$delta]); } } else { // Single value. $value = drupal_render($output); } return $value; } /** * Get the children of a query string parameter that apply to the field. * * For instance: if the field is 'relatedArticles' and the query string is * '?relatedArticles.one.two,articles' it returns array('one.two'). * * @param string $key * The name of the key: include|fields * * @return string[] * The list of fields. */ protected function nestedDottedChildren($key) { // Filters are dealt with differently. if ($key == 'filter') { return $this->nestedDottedFilters(); } $allowed_values = array('include', 'fields'); if (!in_array($key, $allowed_values)) { return array(); } $input = $this ->getRequest() ->getParsedInput(); $limit_values = !empty($input[$key]) ? explode(',', $input[$key]) : array(); $limit_values = array_filter($limit_values, function ($value) { $parts = explode('.', $value); return $parts[0] == $this->getPublicName() && $value != $this->getPublicName(); }); return array_map(function ($value) { return substr($value, strlen($this->getPublicName()) + 1); }, $limit_values); } /** * Process the filter query string for the relevant sub-query. * * Selects the filters that start with the field name. * * @return array * The processed filters. */ protected function nestedDottedFilters() { $input = $this ->getRequest() ->getParsedInput(); if (empty($input['filter'])) { return array(); } $output_filters = array(); $filters = $input['filter']; foreach ($filters as $filter_public_name => $filter) { $filter = DataProvider::processFilterInput($filter, $filter_public_name); if (strpos($filter_public_name, $this->getPublicName() . '.') === 0) { // Remove the prefix and add it to the filters for the next request. $new_name = substr($filter_public_name, strlen($this->getPublicName()) + 1); $filter['public_field'] = $new_name; $output_filters[$new_name] = $filter; } } return $output_filters; } /** * {@inheritdoc} */ public function addMetadata($key, $value) { $this->decorated->addMetadata($key, $value); } /** * {@inheritdoc} */ public function getMetadata($key) { return $this->decorated->getMetadata($key); } /** * {@inheritdoc} */ public function getRequest() { return $this->decorated->getRequest(); } /** * {@inheritdoc} */ public function setRequest(RequestInterface $request) { $this->decorated->setRequest($request); } /** * {@inheritdoc} */ public function executeProcessCallbacks($value) { return $this->decorated->executeProcessCallbacks($value); } /** * {@inheritdoc} */ public function render(DataInterpreterInterface $interpreter) { return $this->executeProcessCallbacks($this->value($interpreter)); } /** * {@inheritdoc} */ public function getDefinition() { return $this->decorated->getDefinition(); } /** * {@inheritdoc} */ public function getPublicFieldInfo() { return $this->decorated->getPublicFieldInfo(); } /** * {@inheritdoc} */ public function setPublicFieldInfo(PublicFieldInfoInterface $public_field_info) { $this->decorated->setPublicFieldInfo($public_field_info); } /** * Get value for a field based on another resource. * * @param DataInterpreterInterface $source * The data source. * * @return mixed * A single or multiple values. */ protected function resourceValue(DataInterpreterInterface $source) {} /** * {@inheritdoc} */ public function decorate(ResourceFieldInterface $decorated) { $this->decorated = $decorated; } /** * {@inheritdoc} */ public function getSubProperty() { return $this->subProperty; } /** * {@inheritdoc} */ public function setSubProperty($sub_property) { $this->subProperty = $sub_property; } /** * {@inheritdoc} */ public function getFormatter() { return $this->formatter; } /** * {@inheritdoc} */ public function setFormatter($formatter) { $this->formatter = $formatter; } /** * {@inheritdoc} */ public function getWrapperMethod() { return $this->wrapperMethod; } /** * {@inheritdoc} */ public function setWrapperMethod($wrapper_method) { $this->wrapperMethod = $wrapper_method; } /** * {@inheritdoc} */ public function isWrapperMethodOnEntity() { return $this->wrapperMethodOnEntity; } /** * {@inheritdoc} */ public function setWrapperMethodOnEntity($wrapper_method_on_entity) { $this->wrapperMethodOnEntity = $wrapper_method_on_entity; } /** * {@inheritdoc} */ public function getColumn() { if (isset($this->column)) { return $this->column; } if ($this->getProperty() && $field = $this::fieldInfoField($this->getProperty())) { if ($field['type'] == 'text_long') { // Do not default to format. $this->setColumn('value'); } else { // Set the column name. $this->setColumn(key($field['columns'])); } } return $this->column; } /** * {@inheritdoc} */ public function setColumn($column) { $this->column = $column; } /** * {@inheritdoc} */ public function getImageStyles() { return $this->imageStyles; } /** * {@inheritdoc} */ public function setImageStyles($image_styles) { $this->imageStyles = $image_styles; } /** * {@inheritdoc} */ public function getEntityType() { return $this->entityType; } /** * {@inheritdoc} */ public function setEntityType($entity_type) { $this->entityType = $entity_type; } /** * Gets the \EntityStructureWrapper for the entity type. * * @return mixed * The \EntityStructureWrapper if the entity type exists. */ protected function entityTypeWrapper() { static $entity_wrappers = array(); $key = sprintf('%s:%s', $this->getEntityType(), $this->getBundle()); if (isset($entity_wrappers[$key])) { return $entity_wrappers[$key]; } $entity_wrappers[$key] = entity_metadata_wrapper($this->getEntityType(), NULL, array( 'bundle' => $this->getBundle(), )); return $entity_wrappers[$key]; } /** * {@inheritdoc} */ public function getBundle() { return $this->bundle; } /** * {@inheritdoc} */ public function setBundle($bundle) { // Do not do pointless work if not needed. if (!empty($this->bundle) && $this->bundle == $bundle) { return; } $this->bundle = $bundle; // If this is an options call, then introspect Entity API to add more data // to the public field information. if ($this->getRequest()->getMethod() == RequestInterface::METHOD_OPTIONS) { $this->populatePublicInfoField(); } } /** * {@inheritdoc} * * Almost all the defaults come are applied by the object's property defaults. */ public function addDefaults() { // Set the defaults from the decorated. $this->setResource($this->decorated->getResource()); // If entity metadata wrapper methods were used, then return the appropriate // entity property. if ($this->isWrapperMethodOnEntity() && $this->getWrapperMethod()) { $this->propertyOnEntity(); } // Set the Entity related defaults. if ( ($this->property = $this->decorated->getProperty()) && ($field = $this::fieldInfoField($this->property)) && $field['type'] == 'image' && ($image_styles = $this->getImageStyles()) ) { // If it's an image check if we need to add image style processing. $process_callbacks = $this->getProcessCallbacks(); array_unshift($process_callbacks, array( array($this, 'getImageUris'), array($image_styles), )); $this->setProcessCallbacks($process_callbacks); } } /** * {@inheritdoc} */ public static function getImageUris(array $file_array, $image_styles) { // Return early if there are no image styles. if (empty($image_styles)) { return $file_array; } // If $file_array is an array of file arrays. Then call recursively for each // item and return the result. if (static::isArrayNumeric($file_array)) { $output = array(); foreach ($file_array as $item) { $output[] = static::getImageUris($item, $image_styles); } return $output; } $file_array['image_styles'] = array(); foreach ($image_styles as $style) { $file_array['image_styles'][$style] = image_style_url($style, $file_array['uri']); } return $file_array; } /** * {@inheritdoc} */ public static function propertyIsField($name) { return (bool) static::fieldInfoField($name); } /** * {@inheritdoc} */ public function preprocess($value) { // By default assume that there is no preprocess and allow extending classes // to implement this. return $value; } /** * Get the class name to use based on the field definition. * * @param array $field_definition * The processed field definition with the user values. * * @return string * The class name to use. If the class name is empty or does not implement * ResourceFieldInterface then ResourceField will be used. NULL if nothing * was found. */ public static function fieldClassName(array $field_definition) { if (!empty($field_definition['class']) && $field_definition['class'] != '\Drupal\restful\Plugin\resource\Field\ResourceFieldEntity') { // If there is a class that is not the current, return it. return $field_definition['class']; } // If there is an extending class for the particular field use that class // instead. if (empty($field_definition['property']) || !$field_info = static::fieldInfoField($field_definition['property'])) { return NULL; } switch ($field_info['type']) { case 'entityreference': case 'taxonomy_term_reference': return '\Drupal\restful\Plugin\resource\Field\ResourceFieldEntityReference'; case 'text': case 'text_long': case 'text_with_summary': return '\Drupal\restful\Plugin\resource\Field\ResourceFieldEntityText'; case 'file': case 'image': // If the field is treated as a resource, then default to the reference. if (!empty($field_definition['resource'])) { return '\Drupal\restful\Plugin\resource\Field\ResourceFieldFileEntityReference'; } return '\Drupal\restful\Plugin\resource\Field\ResourceFieldEntityFile'; default: return NULL; } } /** * {@inheritdoc} */ public function getPublicName() { return $this->decorated->getPublicName(); } /** * {@inheritdoc} */ public function setPublicName($public_name) { $this->decorated->setPublicName($public_name); } /** * {@inheritdoc} */ public function getAccessCallbacks() { return $this->decorated->getAccessCallbacks(); } /** * {@inheritdoc} */ public function setAccessCallbacks($access_callbacks) { $this->decorated->setAccessCallbacks($access_callbacks); } /** * {@inheritdoc} */ public function getProperty() { return $this->property; } /** * {@inheritdoc} */ public function setProperty($property) { $this->property = $property; $this->decorated->setProperty($property); } /** * {@inheritdoc} */ public function getCallback() { return $this->decorated->getCallback(); } /** * {@inheritdoc} */ public function setCallback($callback) { $this->decorated->setCallback($callback); } /** * {@inheritdoc} */ public function getProcessCallbacks() { return $this->decorated->getProcessCallbacks(); } /** * {@inheritdoc} */ public function setProcessCallbacks($process_callbacks) { $this->decorated->setProcessCallbacks($process_callbacks); } /** * {@inheritdoc} */ public function getResource() { return $this->decorated->getResource(); } /** * {@inheritdoc} */ public function setResource($resource) { $this->decorated->setResource($resource); } /** * {@inheritdoc} */ public function getMethods() { return $this->decorated->getMethods(); } /** * {@inheritdoc} */ public function setMethods($methods) { $this->decorated->setMethods($methods); } /** * {@inheritdoc} */ public function id() { return $this->decorated->id(); } /** * {@inheritdoc} */ public function isComputed() { return $this->decorated->isComputed(); } /** * {@inheritdoc} */ public function autoDiscovery() { if (method_exists($this->decorated, 'autoDiscovery')) { return $this->decorated->autoDiscovery(); } return ResourceFieldBase::emptyDiscoveryInfo($this->getPublicName()); } /** * {@inheritdoc} */ public function getCardinality() { if (isset($this->cardinality)) { return $this->cardinality; } // Default to single cardinality. $this->cardinality = 1; if ($field_info = $this::fieldInfoField($this->getProperty())) { $this->cardinality = empty($field_info['cardinality']) ? $this->cardinality : $field_info['cardinality']; } return $this->cardinality; } /** * {@inheritdoc} */ public function setCardinality($cardinality) { $this->cardinality = $cardinality; } /** * Helper method to determine if an array is numeric. * * @param array $input * The input array. * * @return bool * TRUE if the array is numeric, false otherwise. */ public static function isArrayNumeric(array $input) { return ResourceFieldBase::isArrayNumeric($input); } /** * Builds a metadata item for a field value. * * It will add information about the referenced entity. NOTE: Do not type hint * the $wrapper argument to avoid PHP errors for the file entities. Those are * no true entity references, but file arrays (although they reference file * entities) * * @param \EntityDrupalWrapper $wrapper * The wrapper to the referenced entity. * * @return array * The metadata array item. */ protected function buildResourceMetadataItem($wrapper) { if ($wrapper instanceof \EntityValueWrapper) { $wrapper = entity_metadata_wrapper($this->getEntityType(), $wrapper->value()); } $id = $wrapper->getIdentifier(); $bundle = $wrapper->getBundle(); $resource = $this->getResource(); return array( 'id' => $id, 'entity_type' => $wrapper->type(), 'bundle' => $bundle, 'resource_name' => $resource['name'], ); } /** * Helper function to get the referenced entity ID. * * @param \EntityDrupalWrapper $property_wrapper * The wrapper for the referenced file array. * * @return mixed * The ID. */ protected function referencedId($property_wrapper) { return $property_wrapper->getIdentifier() ?: NULL; } /** * Sets the resource field property to the schema field in the entity. * * @throws \EntityMetadataWrapperException */ protected function propertyOnEntity() { // If there is no property try to get it based on the wrapper method and // store the value in the decorated object. $property = NULL; $wrapper_method = $this->getWrapperMethod(); $wrapper = $this->entityTypeWrapper(); if ($wrapper_method == 'label') { // Store the label key. $property = $wrapper->entityKey('label'); } elseif ($wrapper_method == 'getBundle') { // Store the bundle key. $property = $wrapper->entityKey('bundle'); } elseif ($wrapper_method == 'getIdentifier') { // Store the ID key. $property = $wrapper->entityKey('id'); } // There are occasions when the wrapper property is not the schema // database field. if (!is_a($wrapper, '\EntityStructureWrapper')) { // The entity type does not exist. return; } /* @var $wrapper \EntityStructureWrapper */ foreach ($wrapper->getPropertyInfo() as $wrapper_property => $property_info) { if (!empty($property_info['schema field']) && $property_info['schema field'] == $property) { $property = $wrapper_property; break; } } $this->setProperty($property); } /** * Populate public info field with Property API information. */ protected function populatePublicInfoField() { $field_definition = $this->getDefinition(); $discovery_info = empty($field_definition['discovery']) ? array() : $field_definition['discovery']; $public_field_info = new PublicFieldInfoEntity( $this->getPublicName(), $this->getProperty(), $this->getEntityType(), $this->getBundle(), $discovery_info ); $this->setPublicFieldInfo($public_field_info); if ($field_instance = field_info_instance($this->getEntityType(), $this->getProperty(), $this->getBundle())) { $public_field_info->addSectionDefaults('info', array( 'label' => $field_instance['label'], 'description' => $field_instance['description'], )); $field_info = $this::fieldInfoField($this->getProperty()); $section_info = array(); $section_info['label'] = empty($field_info['label']) ? NULL : $field_info['label']; $section_info['description'] = empty($field_info['description']) ? NULL : $field_info['description']; $public_field_info->addSectionDefaults('info', $section_info); $type = $public_field_info instanceof PublicFieldInfoEntityInterface ? $public_field_info->getFormSchemaAllowedType() : NULL; $public_field_info->addSectionDefaults('form_element', array( 'default_value' => isset($field_instance['default_value']) ? $field_instance['default_value'] : NULL, 'type' => $type, )); // Loading allowed values can be a performance issue, load them only if // they are not provided in the field definition. $form_element_info = $public_field_info->getSection('form_element'); if (!isset($form_element_info['allowed_values'])) { $allowed_values = $public_field_info instanceof PublicFieldInfoEntityInterface ? $public_field_info->getFormSchemaAllowedValues() : NULL; $public_field_info->addSectionDefaults('form_element', array( 'allowed_values' => $allowed_values, )); } } else { // Extract the discovery information from the property info. try { $property_info = $this ->entityTypeWrapper() ->getPropertyInfo($this->getProperty()); } catch(\EntityMetadataWrapperException $e) { return; } if (empty($property_info)) { return; } $public_field_info->addSectionDefaults('data', array( 'type' => $property_info['type'], 'required' => empty($property_info['required']) ? FALSE : $property_info['required'], )); $public_field_info->addSectionDefaults('info', array( 'label' => $property_info['label'], 'description' => $property_info['description'], )); } } /** * Gets statically cached information about a field. * * @param string $field_name * The name of the field to retrieve. $field_name can only refer to a * non-deleted, active field. For deleted fields, use * field_info_field_by_id(). To retrieve information about inactive fields, * use field_read_fields(). * * @return array * The field info. * * @see field_info_field() */ protected static function fieldInfoField($field_name) { return field_info_field($field_name); } } ================================================ FILE: src/Plugin/resource/Field/ResourceFieldEntityAlterableInterface.php ================================================ getProperty()); if ($field_info['cardinality'] == 1) { // Single value. return array( 'fid' => $value, 'display' => TRUE, ); } $value = is_array($value) ? $value : explode(',', $value); $return = array(); foreach ($value as $delta => $single_value) { $return[$delta] = array( 'fid' => $single_value, 'display' => TRUE, ); } return $return; } /** * {@inheritdoc} */ public function executeProcessCallbacks($value) { return $this->decorated->executeProcessCallbacks($value); } /** * {@inheritdoc} */ public function getRequest() { return $this->decorated->getRequest(); } /** * {@inheritdoc} */ public function setRequest(RequestInterface $request) { $this->decorated->setRequest($request); } /** * {@inheritdoc} */ public function getDefinition() { return $this->decorated->getDefinition(); } } ================================================ FILE: src/Plugin/resource/Field/ResourceFieldEntityInterface.php ================================================ referencedIdProperty = $field['referencedIdProperty']; } // TODO: Document referencedIdProperty. } /** * {@inheritdoc} */ public function preprocess($value) { if (!$value) { // If value is empty, return NULL, so no new entity will be created. return NULL; } $cardinality = $this->getCardinality(); if ($cardinality != 1 && !is_array($value)) { // If the field is entity reference type and its cardinality larger than // 1 set value to an array. $value = explode(',', $value); } if ($cardinality != 1 && ResourceFieldBase::isArrayNumeric($value)) { // Set the cardinality to 1 to process each value as a single value item. $this->setCardinality(1); // For multiple value items, pre-process them separately. $values = array(); foreach ($value as $item) { $values[] = $this->preprocess($item); } $this->setCardinality($cardinality); return $values; } // If the provided value is the ID to the referenced entity, then do not do // a sub-request. if (!is_array($value) || empty($value['body'])) { // Allow to pass an array with the ID instead of the ID directly. return (!empty($value['id']) && array_keys($value) == array('id')) ? $value['id'] : $value; } /* @var ResourceFieldCollectionInterface $merged_value */ $merged_value = $this->mergeEntityFromReference($value); return $merged_value->getInterpreter()->getWrapper()->getIdentifier(); } /** * Helper function; Create an entity from a a sub-resource. * * @param mixed $value * The single value for the sub-request. * * @return mixed * The value to set using the wrapped property. */ protected function mergeEntityFromReference($value) { $resource = $this->getResource(); if (empty($resource) || empty($value['body'])) { // Field is not defined as "resource", which means it only accepts an // integer as a valid value. // Or, we are passing an integer and cardinality is 1. That means that we // are passing the ID of the referenced entity. Hence setting the new // value to the reference field. return $value; } // Get the resource data provider and make the appropriate operations. // We need to create a RequestInterface object for the sub-request. $resource_data_provider = DataProviderResource::init(static::subRequest($value), $resource['name'], array( $resource['majorVersion'], $resource['minorVersion'], )); // We are always dealing with the single value. $merged = $resource_data_provider->merge(static::subRequestId($value), $value['body']); return reset($merged); } /** * {@inheritdoc} */ public static function subRequest(array $value) { if (empty($value['request'])) { throw new BadRequestException('Malformed body payload. Missing "request" key for the sub-request.'); } if (empty($value['request']['method'])) { throw new BadRequestException('Malformed body payload. Missing "method" int the "request" key for the sub-request.'); } $request_user_info = $value['request'] + array( 'path' => NULL, 'query' => array(), 'csrf_token' => NULL, ); $headers = empty($request_user_info['headers']) ? array() : $request_user_info['headers']; $request_user_info['headers'] = new HttpHeaderBag($headers); $request_user_info['via_router'] = FALSE; $request_user_info['cookies'] = $_COOKIE; $request_user_info['files'] = $_FILES; $request_user_info['server'] = $_SERVER; return Request::create( $request_user_info['path'], $request_user_info['query'], $request_user_info['method'], $request_user_info['headers'], $request_user_info['via_router'], $request_user_info['csrf_token'], $request_user_info['cookies'], $request_user_info['files'], $request_user_info['server'] ); } /** * Get the ID of the resource this write sub-request is for. * * @param array $value * The value provided for this sub-request item. * * @return string * The ID. */ protected static function subRequestId($value) { if ($value['request']['method'] == RequestInterface::METHOD_POST) { // If the request is for post, then disregard any possible ID. return NULL; } return empty($value['id']) ? NULL : $value['id']; } /** * {@inheritdoc} */ public function value(DataInterpreterInterface $interpreter) { $value = $this->decorated->value($interpreter); if (isset($value)) { // Let the decorated resolve callbacks. return $value; } // Check user has access to the property. if (!$this->access('view', $interpreter)) { return NULL; } $resource = $this->getResource(); // If the field definition does not contain a resource, or it is set // explicitly to fullView FALSE, then return only the entity ID. if ( $resource || (!empty($resource) && $resource['fullView'] !== FALSE) || $this->getFormatter() ) { // Let the resource embedding to the parent class. return parent::value($interpreter); } // Since this is a reference field (a field that points to other entities, // we can know for sure that the property wrappers are instances of // \EntityDrupalWrapper or lists of them. $property_wrapper = $this->propertyWrapper($interpreter); if (!$property_wrapper->value()) { // If there is no referenced entity, return. return NULL; } // If this is a multivalue field, then call recursively on the items. if ($property_wrapper instanceof \EntityListWrapper) { $values = array(); foreach ($property_wrapper->getIterator() as $item_wrapper) { $values[] = $this->referencedId($item_wrapper); } return $values; } /* @var $property_wrapper \EntityDrupalWrapper */ return $this->referencedId($property_wrapper); } /** * Helper function to get the referenced entity ID. * * @param \EntityDrupalWrapper $property_wrapper * The wrapper for the referenced entity. * * @return mixed * The ID. */ protected function referencedId($property_wrapper) { $identifier = $property_wrapper->getIdentifier(); if (!$this->referencedIdProperty) { return $identifier; } try { return $identifier ? $property_wrapper->{$this->referencedIdProperty}->value() : NULL; } catch (\EntityMetadataWrapperException $e) { // An exception will be raised for broken entity reference fields. return NULL; } } /** * {@inheritdoc} */ public function getRequest() { return $this->decorated->getRequest(); } /** * {@inheritdoc} */ public function setRequest(RequestInterface $request) { $this->decorated->setRequest($request); } /** * {@inheritdoc} */ public function getDefinition() { return $this->decorated->getDefinition(); } } ================================================ FILE: src/Plugin/resource/Field/ResourceFieldEntityReferenceInterface.php ================================================ getProperty()); // If there was no bundle that had the field instance, then return NULL. if (!$instance = field_info_instance($this->getEntityType(), $this->getProperty(), $this->getBundle())) { return NULL; } $return = NULL; if ($field_info['cardinality'] == 1) { // Single value. if (!$instance['settings']['text_processing']) { return $value; } return array( 'value' => $value, // TODO: This is hardcoded! Fix it. 'format' => 'filtered_html', ); } // Multiple values. foreach ($value as $delta => $single_value) { if (!$instance['settings']['text_processing']) { $return[$delta] = $single_value; } else { $return[$delta] = array( 'value' => $single_value, 'format' => 'filtered_html', ); } } return $return; } /** * {@inheritdoc} */ public function executeProcessCallbacks($value) { return $this->decorated->executeProcessCallbacks($value); } /** * {@inheritdoc} */ public function getRequest() { return $this->decorated->getRequest(); } /** * {@inheritdoc} */ public function setRequest(RequestInterface $request) { $this->decorated->setRequest($request); } /** * {@inheritdoc} */ public function getDefinition() { return $this->decorated->getDefinition(); } } ================================================ FILE: src/Plugin/resource/Field/ResourceFieldFileEntityReference.php ================================================ value(); $identifier = $file_array['fid']; $resource = $this->getResource(); // TODO: Make sure we still want to support fullView. if (!$resource || !$identifier || (isset($resource['fullView']) && $resource['fullView'] === FALSE)) { return $identifier; } // If there is a resource that we are pointing to, we need to use the id // field that that particular resource has in its configuration. Trying to // load by the entity id in that scenario will lead to a 404. // We'll load the plugin to get the idField configuration. $instance_id = sprintf('%s:%d.%d', $resource['name'], $resource['majorVersion'], $resource['minorVersion']); /* @var ResourceInterface $resource */ $resource = restful() ->getResourceManager() ->getPluginCopy($instance_id, Request::create('', array(), RequestInterface::METHOD_GET)); $plugin_definition = $resource->getPluginDefinition(); if (empty($plugin_definition['dataProvider']['idField'])) { return $identifier; } try { $file_wrapper = entity_metadata_wrapper('file', $file_array['fid']); return $file_wrapper->{$plugin_definition['dataProvider']['idField']}->value(); } catch (\EntityMetadataWrapperException $e) { return $identifier; } } /** * Builds a metadata item for a field value. * * It will add information about the referenced entity. * * @param \EntityMetadataWrapper $wrapper * The wrapper for the referenced file array. * * @return array * The metadata array item. */ protected function buildResourceMetadataItem($wrapper) { $file_array = $wrapper->value(); /* @var \EntityDrupalWrapper $wrapper */ $wrapper = entity_metadata_wrapper('file', $file_array['fid']); return parent::buildResourceMetadataItem($wrapper); } /** * Helper function to get the referenced entity ID. * * @param \EntityStructureWrapper $property_wrapper * The wrapper for the referenced file array. * * @return mixed * The ID. */ protected function referencedId($property_wrapper) { $file_array = $property_wrapper->value(); if (!$this->referencedIdProperty) { return $file_array['fid']; } /* @var \EntityDrupalWrapper $wrapper */ $wrapper = entity_metadata_wrapper('file', $file_array['fid']); return $wrapper->{$this->referencedIdProperty}->value(); } /** * {@inheritdoc} */ public function getRequest() { return $this->decorated->getRequest(); } /** * {@inheritdoc} */ public function setRequest(RequestInterface $request) { $this->decorated->setRequest($request); } } ================================================ FILE: src/Plugin/resource/Field/ResourceFieldInterface.php ================================================ 1. NULL if * there is no identifier to be found. */ public function compoundDocumentId(DataInterpreterInterface $interpreter); /** * Gets the value of a field and applies all process callbacks to it. * * @param DataInterpreterInterface $interpreter * The data interpreter. * * @return mixed * The value to render. */ public function render(DataInterpreterInterface $interpreter); /** * Gets the cardinality of the wrapped field. * * @return int * The number of potentially returned fields. Reuses field cardinality * constants. */ public function getCardinality(); /** * Set the cardinality. * * @param int $cardinality * The new cardinality. */ public function setCardinality($cardinality); /** * Get the request in the data provider. * * @return RequestInterface * The request. */ public function getRequest(); /** * Set the request. * * @param RequestInterface $request * The request. */ public function setRequest(RequestInterface $request); /** * Gets the original field definition as declared in Resource::publicFields(). * * @return array * The field definition. */ public function getDefinition(); /** * Gets the public field info object. * * @return PublicFieldInfoInterface * The public field info object. */ public function getPublicFieldInfo(); /** * Gets the public field info object. * * @param PublicFieldInfoInterface $public_field_info * The public field info object. */ public function setPublicFieldInfo(PublicFieldInfoInterface $public_field_info); } ================================================ FILE: src/Plugin/resource/Field/ResourceFieldKeyValue.php ================================================ getRequest(); $resource_field = new static($field, $request); $resource_field->addDefaults(); return $resource_field; } /** * {@inheritdoc} */ public function value(DataInterpreterInterface $interpreter) { if ($value = parent::value($interpreter)) { return $value; } return $interpreter->getWrapper()->get($this->getProperty()); } } ================================================ FILE: src/Plugin/resource/Field/ResourceFieldReference.php ================================================ uid, use * \Drupal\restful\Plugin\resource\Field\ResourceFieldEntityReference instead. * * @package Drupal\restful\Plugin\resource\Field */ class ResourceFieldReference extends ResourceField { /** * Overrides ResourceField::compoundDocumentId(). */ public function compoundDocumentId(DataInterpreterInterface $interpreter) { $collection = parent::compoundDocumentId($interpreter); if (!$collection instanceof ResourceFieldCollectionInterface) { return NULL; } $id_field = $collection->getIdField(); if (!$id_field instanceof ResourceFieldInterface) { return NULL; } return $id_field->render($collection->getInterpreter()); } } ================================================ FILE: src/Plugin/resource/Field/ResourceFieldResource.php ================================================ decorated) { $this->setRequest($request); } $this->resourceMachineName = $field['resource']['name']; // Compute the target column if empty. if (!empty($field['targetColumn'])) { $this->targetColumn = $field['targetColumn']; } } /** * {@inheritdoc} */ public function getResourceId(DataInterpreterInterface $interpreter) { if (isset($this->resourceId)) { return $this->resourceId; } $this->resourceId = $this->compoundDocumentId($interpreter); return $this->resourceId; } /** * {@inheritdoc} */ public function getResourceMachineName() { return $this->resourceMachineName; } /** * {@inheritdoc} */ public function getResourcePlugin() { if (isset($this->resourcePlugin)) { return $this->resourcePlugin; } $resource_info = $this->getResource(); $this->resourcePlugin = restful() ->getResourceManager() ->getPlugin(sprintf('%s:%d.%d', $resource_info['name'], $resource_info['majorVersion'], $resource_info['minorVersion'])); return $this->resourcePlugin; } /** * Gets the cardinality of the field. * * @return int * The number of potentially returned fields. Reuses field cardinality * constants. */ public function getCardinality() { if ($this->decorated instanceof ResourceFieldEntityInterface) { return $this->decorated->getCardinality(); } // Default to single cardinality. return 1; } /** * {@inheritdoc} */ public function setCardinality($cardinality) { $this->decorated->setCardinality($cardinality); } /** * {@inheritdoc} */ public static function create(array $field, RequestInterface $request = NULL) { $request = $request ?: restful()->getRequest(); $resource_field = ResourceField::create($field, $request); $output = new static($field, $request); $output->decorate($resource_field); return $output; } /** * {@inheritdoc} */ public static function isArrayNumeric(array $input) { return ResourceFieldBase::isArrayNumeric($input); } /** * {@inheritdoc} */ public function value(DataInterpreterInterface $interpreter) { return $this->decorated->value($interpreter); } /** * {@inheritdoc} */ public function access($op, DataInterpreterInterface $interpreter) { return $this->decorated->access($op, $interpreter); } /** * {@inheritdoc} */ public function addDefaults() { $this->decorated->addDefaults(); } /** * {@inheritdoc} */ public function set($value, DataInterpreterInterface $interpreter) { $this->decorated->set($value, $interpreter); } /** * {@inheritdoc} */ public function decorate(ResourceFieldInterface $decorated) { $this->decorated = $decorated; } /** * {@inheritdoc} */ public function addMetadata($key, $value) { $this->decorated->addMetadata($key, $value); } /** * {@inheritdoc} */ public function getMetadata($key) { return $this->decorated->getMetadata($key); } /** * {@inheritdoc} */ public function executeProcessCallbacks($value) { return $this->decorated->executeProcessCallbacks($value); } /** * {@inheritdoc} */ public function getPublicName() { return $this->decorated->getPublicName(); } /** * {@inheritdoc} */ public function setPublicName($public_name) { $this->decorated->setPublicName($public_name); } /** * {@inheritdoc} */ public function getAccessCallbacks() { return $this->decorated->getAccessCallbacks(); } /** * {@inheritdoc} */ public function setAccessCallbacks($access_callbacks) { $this->decorated->setAccessCallbacks($access_callbacks); } /** * {@inheritdoc} */ public function getProperty() { return $this->decorated->getProperty(); } /** * {@inheritdoc} */ public function setProperty($property) { $this->decorated->setProperty($property); } /** * {@inheritdoc} */ public function getCallback() { return $this->decorated->getCallback(); } /** * {@inheritdoc} */ public function setCallback($callback) { $this->decorated->setCallback($callback); } /** * {@inheritdoc} */ public function getProcessCallbacks() { return $this->decorated->getProcessCallbacks(); } /** * {@inheritdoc} */ public function setProcessCallbacks($process_callbacks) { $this->decorated->setProcessCallbacks($process_callbacks); } /** * {@inheritdoc} */ public function getResource() { return $this->decorated->getResource(); } /** * {@inheritdoc} */ public function setResource($resource) { $this->decorated->setResource($resource); } /** * {@inheritdoc} */ public function getMethods() { return $this->decorated->getMethods(); } /** * {@inheritdoc} */ public function setMethods($methods) { $this->decorated->setMethods($methods); } /** * {@inheritdoc} */ public function id() { return $this->decorated->id(); } /** * {@inheritdoc} */ public function isComputed() { return $this->decorated->isComputed(); } /** * {@inheritdoc} */ public function compoundDocumentId(DataInterpreterInterface $interpreter) { return $this->decorated->compoundDocumentId($interpreter); } /** * {@inheritdoc} */ public function render(DataInterpreterInterface $interpreter) { return $this->executeProcessCallbacks($this->value($interpreter)); } /** * If any method not declared, then defer it to the decorated field. */ public function __call($name, $arguments) { return call_user_func_array(array($this->decorated, $name), $arguments); } /** * {@inheritdoc} */ public function getRequest() { return $this->decorated->getRequest(); } /** * {@inheritdoc} */ public function setRequest(RequestInterface $request) { $this->decorated->setRequest($request); } /** * {@inheritdoc} */ public function getDefinition() { return $this->decorated->getDefinition(); } /** * {@inheritdoc} */ public function getPublicFieldInfo() { return $this->decorated->getPublicFieldInfo(); } /** * {@inheritdoc} */ public function setPublicFieldInfo(PublicFieldInfoInterface $public_field_info) { $this->decorated->setPublicFieldInfo($public_field_info); } /** * {@inheritdoc} */ public function autoDiscovery() { if (method_exists($this->decorated, 'autoDiscovery')) { return $this->decorated->autoDiscovery(); } return ResourceFieldBase::emptyDiscoveryInfo($this->getPublicName()); } /** * {@inheritdoc} */ public function getTargetColumn() { if (empty($this->targetColumn)) { // Check the definition of the decorated field. $definition = $this->decorated->getDefinition(); if (!empty($definition['targetColumn'])) { $this->targetColumn = $definition['targetColumn']; } elseif ($this->isInstanceOf(ResourceFieldEntityReferenceInterface::class)) { $entity_info = entity_get_info($this->getResourcePlugin()->getEntityType()); // Assume that the relationship is through the entity key id. $this->targetColumn = $entity_info['entity keys']['id']; } else { throw new ServerConfigurationException(sprintf('Target column could not be found for field "%s".', $this->getPublicName())); } } return $this->targetColumn; } /** * Checks if the decorated object is an instance of something. * * @param string $class * Class or interface to check the instance. * * @return bool * TRUE if the decorated object is an instace of the $class. FALSE * otherwise. */ public function isInstanceOf($class) { if ($this instanceof $class || $this->decorated instanceof $class) { return TRUE; } // Check if the decorated resource is also a decorator. if ($this->decorated instanceof ExplorableDecoratorInterface) { return $this->decorated->isInstanceOf($class); } return FALSE; } } ================================================ FILE: src/Plugin/resource/Field/ResourceFieldResourceInterface.php ================================================ getPluginDefinition(); $plugin_definition['authenticationOptional'] = (bool) variable_get('restful_file_upload_allow_anonymous_user', FALSE); // Store the plugin definition. $this->pluginDefinition = $plugin_definition; } /** * {@inheritdoc} * * If "File entity" module exists, determine access by its provided * permissions otherwise, check if variable is set to allow anonymous users to * upload. Defaults to authenticated user. */ public function access() { // The getAccount method may return an UnauthorizedException when an // authenticated user cannot be found. Since this is called from the access // callback, not from the page callback we need to catch the exception. try { $account = $this->getAccount(); } catch (UnauthorizedException $e) { // If a user is not found then load the anonymous user to check // permissions. $account = drupal_anonymous_user(); } if (module_exists('file_entity')) { return user_access('bypass file access', $account) || user_access('create files', $account); } return (variable_get('restful_file_upload_allow_anonymous_user', FALSE) || $account->uid) && parent::access(); } } ================================================ FILE: src/Plugin/resource/LoginCookie__1_0.php ================================================ $public_fields['id']); } /** * Overrides \RestfulBase::controllersInfo(). */ public function controllersInfo() { return array( '' => array( RequestInterface::METHOD_GET => 'loginAndRespondWithCookie', ), ); } /** * Login a user and return a JSON along with the authentication cookie. * * @return array * Array with the public fields populated. */ public function loginAndRespondWithCookie() { // Login the user. $account = $this->getAccount(); $this->loginUser($account); $user_resource = restful() ->getResourceManager() ->getPlugin('users:1.0'); // User resource may be disabled. $output = $user_resource ? $user_resource->view($account->uid) : array(); if ($resource_field_collection = reset($output)) { /* @var $resource_field_collection \Drupal\restful\Plugin\resource\Field\ResourceFieldCollectionInterface */ $resource_field_collection->set('X-CSRF-Token', ResourceField::create(array( 'public_name' => 'X-CSRF-Token', 'callback' => '\Drupal\restful\Plugin\resource\LoginCookie__1_0::getCSRFTokenValue', ))); } return $output; } /** * Log the user in. * * @param object $account * The user object that was retrieved by the AuthenticationManager. */ public function loginUser($account) { global $user; $this->authenticationManager->switchUserBack(); // Explicitly allow a session to be saved, as it was disabled in // UserSessionState::switchUser. However this resource is a special one, in // the sense that we want to keep the user authenticated after login. drupal_save_session(TRUE); // Override the global user. $user = user_load($account->uid); $login_array = array('name' => $account->name); user_login_finalize($login_array); } /** * Get the CSRF token string. * * @return string * The token. */ public static function getCSRFTokenValue() { $token = array_values(restful_csrf_session_token()); return reset($token); } /** * {@inheritdoc} */ public function switchUserBack() { // We don't want to switch back in this case! drupal_save_session(TRUE); } } ================================================ FILE: src/Plugin/resource/Resource.php ================================================ fieldDefinitions = ResourceFieldCollection::factory($this->processPublicFields($this->publicFields()), $this->getRequest()); $this->initAuthenticationManager(); } /** * {@inheritdoc} */ public function dataProviderFactory() { $plugin_definition = $this->getPluginDefinition(); $field_definitions = $this->getFieldDefinitions(); $class_name = $this->dataProviderClassName(); if (!class_exists($class_name)) { throw new ServerConfigurationException(sprintf('The DataProvider could not be found for this resource: %s.', $this->getResourceMachineName())); } return new $class_name($this->getRequest(), $field_definitions, $this->getAccount(), $this->getPluginId(), $this->getPath(), $plugin_definition['dataProvider']); } /** * Data provider class. * * @return string * The name of the class of the provider factory. */ protected function dataProviderClassName() { // Fallback to the null data provider, this means that we can only get data // from basic callbacks. return '\Drupal\restful\Plugin\resource\DataProvider\DataProviderNull'; } /** * {@inheritdoc} */ public function getAccount($cache = TRUE) { return $this->authenticationManager->getAccount($this->getRequest(), $cache); } /** * {@inheritdoc} */ public function switchUserBack() { return $this->authenticationManager->switchUserBack(); } /** * {@inheritdoc} */ public function setAccount($account) { $this->authenticationManager->setAccount($account); $this->getDataProvider()->setAccount($account); } /** * {@inheritdoc} */ public function getRequest() { if (isset($this->request)) { return $this->request; } $instance_configuration = $this->getConfiguration(); if (!$this->request = $instance_configuration['request']) { throw new ServerConfigurationException('Request object is not available for the Resource plugin.'); } return $this->request; } /** * {@inheritdoc} */ public function setRequest(RequestInterface $request) { $this->request = $request; // Make sure that the request is updated in the data provider. $this->getDataProvider()->setRequest($request); foreach ($this->fieldDefinitions as $resource_field) { /* @var \Drupal\restful\Plugin\resource\Field\ResourceFieldInterface $resource_field */ $resource_field->setRequest($request); } } /** * {@inheritdoc} */ public function getPath() { return $this->path; } /** * {@inheritdoc} */ public function setPath($path) { $this->path = $path; $this->getDataProvider()->setResourcePath($path); } /** * {@inheritdoc} */ public function getFieldDefinitions() { return $this->fieldDefinitions; } /** * {@inheritdoc} */ public function setFieldDefinitions(ResourceFieldCollectionInterface $field_definitions) { $this->fieldDefinitions = $field_definitions; } /** * {@inheritdoc} */ public function getDataProvider() { if (isset($this->dataProvider)) { return $this->dataProvider; } $this->dataProvider = $this->dataProviderFactory(); return $this->dataProvider; } /** * {@inheritdoc} */ public function setDataProvider(DataProviderInterface $data_provider = NULL) { $this->dataProvider = $data_provider; } /** * {@inheritdoc} */ public function getResourceName() { $definition = $this->getPluginDefinition(); return $definition['name']; } /** * {@inheritdoc} */ public function getResourceMachineName() { $definition = $this->getPluginDefinition(); return $definition['resource']; } /** * {@inheritdoc} */ public function defaultConfiguration() { return array( 'request' => restful()->getRequest(), ); } /** * {@inheritdoc} */ public function process() { $path = $this->getPath(); return ResourceManager::executeCallback($this->getControllerFromPath($path), array($path)); } /** * {@inheritdoc} */ public function doGet($path = '', array $query = array()) { $this->setPath($path); $this->setRequest(Request::create($this->versionedUrl($path, array('absolute' => FALSE)), $query, RequestInterface::METHOD_GET)); return $this->process(); } /** * {@inheritdoc} */ public function doPost(array $parsed_body) { return $this->doWrite(RequestInterface::METHOD_POST, '', $parsed_body); } /** * {@inheritdoc} */ public function doPatch($path, array $parsed_body) { if (!$path) { throw new BadRequestException('PATCH requires a path. None given.'); } return $this->doWrite(RequestInterface::METHOD_PATCH, $path, $parsed_body); } /** * {@inheritdoc} */ public function doPut($path, array $parsed_body) { if (!$path) { throw new BadRequestException('PUT requires a path. None given.'); } return $this->doWrite(RequestInterface::METHOD_PUT, $path, $parsed_body); } /** * {@inheritdoc} */ private function doWrite($method, $path, array $parsed_body) { $this->setPath($path); $this->setRequest(Request::create($this->versionedUrl($path, array('absolute' => FALSE)), array(), $method, NULL, FALSE, NULL, array(), array(), array(), $parsed_body)); return $this->process(); } /** * {@inheritdoc} */ public function doDelete($path) { if (!$path) { throw new BadRequestException('DELETE requires a path. None given.'); } $this->setPath($path); $this->setRequest(Request::create($this->versionedUrl($path, array('absolute' => FALSE)), array(), RequestInterface::METHOD_DELETE)); return $this->process(); } /** * {@inheritdoc} */ public function controllersInfo() { return array( '' => array( // GET returns a list of entities. RequestInterface::METHOD_GET => 'index', RequestInterface::METHOD_HEAD => 'index', // POST. RequestInterface::METHOD_POST => 'create', RequestInterface::METHOD_OPTIONS => 'discover', ), // We don't know what the ID looks like, assume that everything is the ID. '^.*$' => array( RequestInterface::METHOD_GET => 'view', RequestInterface::METHOD_HEAD => 'view', RequestInterface::METHOD_PUT => 'replace', RequestInterface::METHOD_PATCH => 'update', RequestInterface::METHOD_DELETE => 'remove', RequestInterface::METHOD_OPTIONS => 'discover', ), ); } /** * {@inheritdoc} */ public function getControllers() { $controllers = array(); foreach ($this->controllersInfo() as $path => $method_info) { $controllers[$path] = array(); foreach ($method_info as $http_method => $controller_info) { $controllers[$path][$http_method] = $controller_info; if (!is_array($controller_info)) { $controllers[$path][$http_method] = array('callback' => $controller_info); } } } return $controllers; } /** * {@inheritdoc} */ public function index($path) { return $this->getDataProvider()->index(); } /** * {@inheritdoc} */ public function view($path) { // TODO: Compare this with 1.x logic. $ids = explode(static::IDS_SEPARATOR, $path); // REST requires a canonical URL for every resource. $canonical_path = $this->getDataProvider()->canonicalPath($path); restful() ->getResponse() ->getHeaders() ->add(HttpHeader::create('Link', $this->versionedUrl($canonical_path, array(), FALSE) . '; rel="canonical"')); // If there is only one ID then use 'view'. Else, use 'viewMultiple'. The // difference between the two is that 'view' allows access denied // exceptions. if (count($ids) == 1) { return array($this->getDataProvider()->view($ids[0])); } else { return $this->getDataProvider()->viewMultiple($ids); } } /** * {@inheritdoc} */ public function create($path) { // TODO: Compare this with 1.x logic. $object = $this->getRequest()->getParsedBody(); return $this->getDataProvider()->create($object); } /** * {@inheritdoc} */ public function update($path) { // TODO: Compare this with 1.x logic. $object = $this->getRequest()->getParsedBody(); return $this->getDataProvider()->update($path, $object, FALSE); } /** * {@inheritdoc} */ public function replace($path) { // TODO: Compare this with 1.x logic. $object = $this->getRequest()->getParsedBody(); return $this->getDataProvider()->update($path, $object, TRUE); } /** * {@inheritdoc} */ public function remove($path) { // TODO: Compare this with 1.x logic. $this->getDataProvider()->remove($path); } /** * {@inheritdoc} */ public function discover($path = NULL) { $this->preflight($path); return $this->getDataProvider()->discover($path); } /** * {@inheritdoc} */ public function getControllerFromPath($path = NULL, ResourceInterface $resource = NULL) { if (empty($resource)) { $resource = $this; } $path = $path ?: $resource->getPath(); $method = $resource->getRequest()->getMethod(); $selected_controller = NULL; foreach ($resource->getControllers() as $pattern => $controllers) { // Find the controllers for the provided path. if ($pattern != $path && !($pattern && preg_match('/' . $pattern . '/', $path))) { continue; } if ($controllers === FALSE) { // Method isn't valid anymore, due to a deprecated API endpoint. $params = array('@path' => $path); throw new GoneException(format_string('The path @path endpoint is not valid.', $params)); } if (!isset($controllers[$method])) { $params = array('@method' => strtoupper($method)); throw new BadRequestException(format_string('The http method @method is not allowed for this path.', $params)); } // We found the controller, so we can break. $selected_controller = $controllers[$method]; if (is_array($selected_controller)) { // If there is a custom access method for this endpoint check it. if (!empty($selected_controller['access callback']) && !ResourceManager::executeCallback(array($resource, $selected_controller['access callback']), array($path))) { throw new ForbiddenException(sprintf('You do not have access to this endpoint: %s - %s', $method, $path)); } $selected_controller = $selected_controller['callback']; } // Create the callable from the method string. if (!ResourceManager::isValidCallback($selected_controller)) { // This means that the provided value means to be a public method on the // current class. $selected_controller = array($resource, $selected_controller); } break; } if (empty($selected_controller)) { throw new NotImplementedException(sprintf('There is no handler for "%s" on the path: %s', $resource->getRequest()->getMethod(), $path)); } return $selected_controller; } /** * {@inheritdoc} */ public function getVersion() { $plugin_definition = $this->getPluginDefinition(); $version = array( 'major' => $plugin_definition['majorVersion'], 'minor' => $plugin_definition['minorVersion'], ); return $version; } /** * {@inheritdoc} */ public function versionedUrl($path = '', $options = array(), $version_string = TRUE) { // Make the URL absolute by default. $options += array('absolute' => TRUE); $plugin_definition = $this->getPluginDefinition(); if (!empty($plugin_definition['menuItem'])) { $url = variable_get('restful_hook_menu_base_path', 'api') . '/'; $url .= $plugin_definition['menuItem'] . '/' . $path; return url(rtrim($url, '/'), $options); } $base_path = variable_get('restful_hook_menu_base_path', 'api'); $url = $base_path; if ($version_string) { $url .= '/v' . $plugin_definition['majorVersion'] . '.' . $plugin_definition['minorVersion']; } $url .= '/' . $plugin_definition['resource'] . '/' . $path; return url(rtrim($url, '/'), $options); } /** * {@inheritdoc} */ public function getUrl(array $options = array(), $keep_query = TRUE, RequestInterface $request = NULL) { // By default set URL to be absolute. $options += array( 'absolute' => TRUE, 'query' => array(), ); if ($keep_query) { $request = $request ?: $this->getRequest(); $input = $request->getParsedInput(); unset($input['page']); unset($input['range']); $input['page'] = $request->getPagerInput(); // Remove special params. unset($input['q']); // Add the request as query strings. $options['query'] += $input; } return $this->versionedUrl($this->getPath(), $options); } /** * {@inheritdoc} */ public function access() { return $this->accessByAllowOrigin(); } /** * {@inheritdoc} */ public function enable() { $this->enabled = TRUE; } /** * {@inheritdoc} */ public function disable() { $this->enabled = FALSE; } /** * {@inheritdoc} */ public function isEnabled() { return $this->enabled; } /** * {@inheritdoc} */ public function setPluginDefinition(array $plugin_definition) { $this->pluginDefinition = $plugin_definition; if (!empty($plugin_definition['dataProvider'])) { $this->getDataProvider()->addOptions($plugin_definition['dataProvider']); } } /** * Checks access based on the referer header and the allowOrigin setting. * * @return bool * TRUE if the access is granted. FALSE otherwise. */ protected function accessByAllowOrigin() { // Check the referrer header and return false if it does not match the // Access-Control-Allow-Origin $referer = $this->getRequest()->getHeaders()->get('Referer')->getValueString(); // If there is no allow_origin assume that it is allowed. Also, if there is // no referer then grant access since the request probably was not // originated from a browser. $plugin_definition = $this->getPluginDefinition(); $origin = isset($plugin_definition['allowOrigin']) ? $plugin_definition['allowOrigin'] : NULL; if (empty($origin) || $origin == '*' || !$referer) { return TRUE; } return strpos($referer, $origin) === 0; } /** * Public fields. * * @return array * The field definition array. */ abstract protected function publicFields(); /** * Get the public fields with the default values applied to them. * * @param array $field_definitions * The field definitions to process. * * @return array * The field definition array. */ protected function processPublicFields(array $field_definitions) { // By default do not do any special processing. return $field_definitions; } /** * Initializes the authentication manager and adds the appropriate providers. * * This will return an AuthenticationManagerInterface if the current resource * needs to be authenticated. To skip authentication completely do not set * authenticationTypes and set authenticationOptional to TRUE. */ protected function initAuthenticationManager() { $this->authenticationManager = new AuthenticationManager(); $plugin_definition = $this->getPluginDefinition(); $authentication_types = $plugin_definition['authenticationTypes']; $authentication_optional = $plugin_definition['authenticationOptional']; $this->authenticationManager->setIsOptional($authentication_optional); if (empty($authentication_types)) { if (empty($authentication_optional)) { // Fail early, fail good. throw new UnauthorizedException('There are no authentication providers and authentication is not optional.'); } return; } if ($authentication_types === TRUE) { // Add all the available authentication providers to the manager. $this->authenticationManager->addAllAuthenticationProviders(); return; } foreach ($authentication_types as $authentication_type) { // Read the authentication providers and add them to the manager. $this->authenticationManager->addAuthenticationProvider($authentication_type); } } /** * Adds the Allowed-Origin headers. * * @param string $path * The requested path. */ protected function preflight($path) { $plugin_definition = $this->getPluginDefinition(); $header_bag = restful() ->getResponse() ->getHeaders(); // Populate the Accept header. $accepted_formats = array(); $formatter_manager = restful()->getFormatterManager(); if (empty($plugin_definition['formatter'])) { foreach ($formatter_manager->getPlugins() as $formatter) { /** @var $formatter \Drupal\restful\Plugin\formatter\FormatterInterface */ $header_bag->append(HttpHeader::create('Accept', $formatter->getContentTypeHeader())); } } else { try { $accepted_format = $formatter_manager ->getPlugin($plugin_definition['formatter']) ->getContentTypeHeader(); $header_bag->add(HttpHeader::create('Accept', $accepted_format)); } catch(PluginNotFoundException $e) { throw new NotImplementedException($e->getMessage()); } } $allowed_origin = empty($plugin_definition['allowOrigin']) ? variable_get('restful_allowed_origin', NULL) : $plugin_definition['allowOrigin']; // Always add the allow origin if configured. if ($allowed_origin) { $header_bag->add(HttpHeader::create('Access-Control-Allow-Origin', check_plain($allowed_origin))); // @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials $accepts_credentials = $allowed_origin == '*' ? 'false' : 'true'; $header_bag->add(HttpHeader::create('Access-Control-Allow-Credentials', $accepts_credentials)); } // Make sure the Access-Control-Allow-Methods is populated. $allowed_methods = array(); foreach ($this->getControllers() as $pattern => $controllers) { // Find the controllers for the provided path. if ($pattern == $path || ($pattern && preg_match('/' . $pattern . '/', $path))) { foreach ($controllers as $method => $controller) { if (is_array($controller)) { // If there is a custom access method for this endpoint check it. if (!empty($selected_controller['access callback']) && !ResourceManager::executeCallback(array($this, $selected_controller['access callback']), array($path))) { // There is no access for this method. continue; } } $allowed_methods[] = $method; } $header_bag->add(HttpHeader::create( 'Access-Control-Allow-Methods', implode(',', $allowed_methods) )); break; } } } } ================================================ FILE: src/Plugin/resource/ResourceDbQuery.php ================================================ '\Drupal\restful\Plugin\resource\Field\ResourceFieldDbColumn'); }, $field_definitions); } /** * Data provider class. * * @return string * The name of the class of the provider factory. */ protected function dataProviderClassName() { return '\Drupal\restful\Plugin\resource\DataProvider\DataProviderDbQuery'; } } ================================================ FILE: src/Plugin/resource/ResourceEntity.php ================================================ entityType = $plugin_definition['dataProvider']['entityType']; if (isset($plugin_definition['dataProvider']['bundles'])) { $this->bundles = $plugin_definition['dataProvider']['bundles']; } parent::__construct($configuration, $plugin_id, $plugin_definition); } /** * Data provider factory. * * @return DataProviderEntityInterface * The data provider for this resource. * * @throws ServerConfigurationException */ public function dataProviderFactory() { $plugin_definition = $this->getPluginDefinition(); $field_definitions = $this->getFieldDefinitions(); if (!empty($plugin_definition['dataProvider']['viewMode'])) { $field_definitions_array = $this->viewModeFields($plugin_definition['dataProvider']['viewMode']); $field_definitions = ResourceFieldCollection::factory($field_definitions_array, $this->getRequest()); } $class_name = $this->dataProviderClassName(); if (!class_exists($class_name)) { throw new ServerConfigurationException(sprintf('The DataProvider could not be found for this resource: %s.', $this->getResourceMachineName())); } return new $class_name($this->getRequest(), $field_definitions, $this->getAccount(), $this->getPluginId(), $this->getPath(), $plugin_definition['dataProvider']); } /** * Data provider class. * * @return string * The name of the class of the provider factory. */ protected function dataProviderClassName() { // This helper function allows to map a resource to a different data // provider class. if ($this->getEntityType() == 'node') { return '\Drupal\restful\Plugin\resource\DataProvider\DataProviderNode'; } elseif ($this->getEntityType() == 'taxonomy_term') { return '\Drupal\restful\Plugin\resource\DataProvider\DataProviderTaxonomyTerm'; } elseif ($this->getEntityType() == 'file') { return '\Drupal\restful\Plugin\resource\DataProvider\DataProviderFile'; } return '\Drupal\restful\Plugin\resource\DataProvider\DataProviderEntity'; } /** * {@inheritdoc} */ protected function publicFields() { $public_fields = array(); $public_fields['id'] = array( 'wrapper_method' => 'getIdentifier', 'wrapper_method_on_entity' => TRUE, 'methods' => array(RequestInterface::METHOD_GET, RequestInterface::METHOD_OPTIONS), 'discovery' => array( // Information about the field for human consumption. 'info' => array( 'label' => t('ID'), 'description' => t('Base ID for the entity.'), ), // Describe the data. 'data' => array( 'cardinality' => 1, 'read_only' => TRUE, 'type' => 'integer', 'required' => TRUE, ), ), ); $public_fields['label'] = array( 'wrapper_method' => 'label', 'wrapper_method_on_entity' => TRUE, 'discovery' => array( // Information about the field for human consumption. 'info' => array( 'label' => t('Label'), 'description' => t('The label of the resource.'), ), // Describe the data. 'data' => array( 'type' => 'string', ), // Information about the form element. 'form_element' => array( 'type' => 'textfield', 'size' => 255, ), ), ); $public_fields['self'] = array( 'callback' => array($this, 'getEntitySelf'), ); return $public_fields; } /** * Gets the entity type. * * @return string * The entity type. */ public function getEntityType() { return $this->entityType; } /** * Gets the entity bundle. * * @return array * The bundles. */ public function getBundles() { return $this->bundles; } /** * Get the "self" url. * * @param DataInterpreterInterface $interpreter * The wrapped entity. * * @return string * The self URL. */ public function getEntitySelf(DataInterpreterInterface $interpreter) { return $this->versionedUrl($interpreter->getWrapper()->getIdentifier()); } /** * Get the public fields with the default values applied to them. * * @param array $field_definitions * The field definitions to process. * * @throws \Drupal\restful\Exception\ServerConfigurationException * For resources without ID field. * * @return array * The field definition array. */ protected function processPublicFields(array $field_definitions) { // The fields that only contain a property need to be set to be // ResourceFieldEntity. Otherwise they will be considered regular // ResourceField. return array_map(function ($field_definition) { $field_entity_class = '\Drupal\restful\Plugin\resource\Field\ResourceFieldEntity'; $class_name = ResourceFieldEntity::fieldClassName($field_definition); if (!$class_name || is_subclass_of($class_name, $field_entity_class)) { $class_name = $field_entity_class; } return $field_definition + array('class' => $class_name, 'entityType' => $this->getEntityType()); }, $field_definitions); } /** * Get the public fields with default values based on view mode information. * * @param array $view_mode_info * View mode configuration array. * * @return array * The public fields. * * @throws ServerConfigurationException */ protected function viewModeFields(array $view_mode_info) { $field_definitions = array(); $entity_type = $this->getEntityType(); $bundles = $this->getBundles(); $view_mode = $view_mode_info['name']; if (count($bundles) != 1) { throw new ServerConfigurationException('View modes can only be used in resources with a single bundle.'); } $bundle = reset($bundles); foreach ($view_mode_info['fieldMap'] as $field_name => $public_field_name) { $field_instance = field_info_instance($entity_type, $field_name, $bundle); $formatter_info = $field_instance['display'][$view_mode]; unset($formatter_info['module']); unset($formatter_info['weight']); unset($formatter_info['label']); $field_definitions[$public_field_name] = array( 'property' => $field_name, 'formatter' => $formatter_info, 'entityType' => $this->getEntityType(), ); } return $field_definitions; } } ================================================ FILE: src/Plugin/resource/ResourceInterface.php ================================================ array(RequestInterface::METHOD_GET => array( * 'callback' => 'view', * 'access callback' => array($this, 'viewAccess'), * )), * ); * } * * @todo: Populate the entity controllers so that they have the entity access checks in here. */ public function getControllers(); /** * Basic implementation for listing. * * @param string $path * The resource path. * * @return array * An array of structured data for the things being viewed. */ public function index($path); /** * Basic implementation for view. * * @param string $path * The resource path. * * @return array * An array of structured data for the things being viewed. */ public function view($path); /** * Basic implementation for create. * * @param string $path * The resource path. * * @return array * An array of structured data for the thing that was created. */ public function create($path); /** * Basic implementation for update. * * @param string $path * The resource path. * * @return array * An array of structured data for the thing that was updated. */ public function update($path); /** * Basic implementation for update. * * @param string $path * The resource path. * * @return array * An array of structured data for the thing that was replaced. */ public function replace($path); /** * Basic implementation for update. * * @param string $path * The resource path. */ public function remove($path); /** * Return array keyed with the major and minor version of the resource. * * @return array * Keyed array with the major and minor version as provided in the plugin * definition. */ public function getVersion(); /** * Gets a resource URL based on the current version. * * @param string $path * The path for the resource * @param array $options * Array of options as in url(). * @param bool $version_string * TRUE to add the version string to the URL. FALSE otherwise. * * @return string * The fully qualified URL. * * @see url() */ public function versionedUrl($path = '', $options = array(), $version_string = TRUE); /** * Determine if user can access the handler. * * @return bool * TRUE if the current request has access to the requested resource. FALSE * otherwise. */ public function access(); /** * Return the controller for a given path. * * @param string $path * (optional) The path to use. If none is provided the path from the * resource will be used. * @param ResourceInterface $resource * (optional) Use the passed in resource instead of $this. This is mainly * used by decorator resources. * * @return callable * A callable as expected by ResourceManager::executeCallback. * * @throws BadRequestException * @throws ForbiddenException * @throws GoneException * @throws NotImplementedException * @throws ServerConfigurationException * * @see ResourceManager::executeCallback() */ public function getControllerFromPath($path = NULL, ResourceInterface $resource = NULL); /** * Enable the resource. */ public function enable(); /** * Disable the resource. */ public function disable(); /** * Checks if the resource is enabled. * * @return bool * TRUE if the resource plugin is enabled. */ public function isEnabled(); /** * Sets the data provider. * * @param DataProviderInterface $data_provider * The data provider to set. */ public function setDataProvider(DataProviderInterface $data_provider = NULL); /** * Sets the plugin definition to the provided array. * * @param array $plugin_definition * Definition array to set manually. */ public function setPluginDefinition(array $plugin_definition); /** * Helper method; Get the URL of the resource and query strings. * * By default the URL is absolute. * * @param array $options * Array with options passed to url(). * @param bool $keep_query * If TRUE the $request will be appended to the $options['query']. This is * the typical behavior for $_GET method, however it is not for $_POST. * Defaults to TRUE. * @param RequestInterface $request * The request object. * * @return string * The URL address. */ public function getUrl(array $options = array(), $keep_query = TRUE, RequestInterface $request = NULL); /** * Discovery controller callback. * * @param string $path * The requested path. * * @return array * The resource field collection with the discovery information. */ public function discover($path = NULL); /** * Shorthand method to perform a quick GET request. * * @param string $path * The resource path. * @param array $query * The parsed query string. * * @return array * The array ready for the formatter. */ public function doGet($path = '', array $query = array()); /** * Shorthand method to perform a quick POST request. * * @param array $parsed_body * The parsed body. * * @return array * The array ready for the formatter. */ public function doPost(array $parsed_body); /** * Shorthand method to perform a quick PATCH request. * * @param string $path * The resource path. * @param array $parsed_body * The parsed body. * * @throws \Drupal\restful\Exception\BadRequestException * When the path is not present. * * @return array * The array ready for the formatter. */ public function doPatch($path, array $parsed_body); /** * Shorthand method to perform a quick PUT request. * * @param string $path * The resource path. * @param array $parsed_body * The parsed body. * * @throws \Drupal\restful\Exception\BadRequestException * When the path is not present. * * @return array * The array ready for the formatter. */ public function doPut($path, array $parsed_body); /** * Shorthand method to perform a quick DELETE request. * * @param string $path * The resource path. * * @throws \Drupal\restful\Exception\BadRequestException * When the path is not present. */ public function doDelete($path); } ================================================ FILE: src/Plugin/resource/ResourceNode.php ================================================ value(); if (!empty($node->nid)) { // Node is already saved. return; } node_object_prepare($node); $node->uid = $this->getAccount()->uid; } } ================================================ FILE: src/Plugin/resource/Users__1_0.php ================================================ 'mail', ); return $public_fields; } } ================================================ FILE: src/RateLimit/Entity/RateLimit.php ================================================ hits++; $this->save(); } /** * Checks if the entity is expired. */ public function isExpired() { return REQUEST_TIME > $this->expiration; } } ================================================ FILE: src/RateLimit/Entity/RateLimitController.php ================================================ account = $account; } /** * Get the account. * * @return \stdClass * The account object, */ public function getAccount() { return $this->account; } /** * Constructor for RateLimitManager. * * @param ResourceInterface $resource * Resource being checked. * @param array $plugin_options * Array of options keyed by plugin id. * @param object $account * The identified user account for the request. * @param RateLimitPluginManager $manager * The plugin manager. */ public function __construct(ResourceInterface $resource, array $plugin_options, $account = NULL, RateLimitPluginManager $manager = NULL) { $this->resource = $resource; $account = $account ? $account : $resource->getAccount(); $this->account = $account ? $account : drupal_anonymous_user(); $manager = $manager ?: RateLimitPluginManager::create(); $options = array(); foreach ($plugin_options as $plugin_id => $rate_options) { // Set the instance id to articles::request and specify the plugin id. $instance_id = $resource->getResourceName() . PluginBase::DERIVATIVE_SEPARATOR . $plugin_id; $options[$instance_id] = array( 'id' => $plugin_id, 'resource' => $resource, ); $options[$instance_id] += $rate_options; } $this->plugins = new RateLimitPluginCollection($manager, $options); } /** * Checks if the current request has reached the rate limit. * * If the user has reached the limit this method will throw an exception. If * not, the hits counter will be updated for subsequent calls. Since the * request can match multiple events, the access is only granted if all events * are cleared. * * @param RequestInterface $request * The request array. * * @throws FloodException if the rate limit has been reached for the * current request. */ public function checkRateLimit(RequestInterface $request) { $now = new \DateTime(); $now->setTimestamp(REQUEST_TIME); // Check all rate limits configured for this handler. foreach ($this->plugins as $instance_id => $plugin) { // If the limit is unlimited then skip everything. /* @var RateLimit $plugin */ $limit = $plugin->getLimit($this->account); $period = $plugin->getPeriod(); if ($limit == static::UNLIMITED_RATE_LIMIT) { // User has unlimited access to the resources. continue; } // If the current request matches the configured event then check if the // limit has been reached. if (!$plugin->isRequestedEvent($request)) { continue; } if (!$rate_limit_entity = $plugin->loadRateLimitEntity($this->account)) { // If there is no entity, then create one. // We don't need to save it since it will be saved upon hit. $rate_limit_entity = entity_create('rate_limit', array( 'timestamp' => REQUEST_TIME, 'expiration' => $now->add($period)->format('U'), 'hits' => 0, 'event' => $plugin->getPluginId(), 'identifier' => $plugin->generateIdentifier($this->account), )); } // When the new rate limit period starts. $new_period = new \DateTime(); $new_period->setTimestamp($rate_limit_entity->expiration); if ($rate_limit_entity->isExpired()) { // If the rate limit has expired renew the timestamps and assume 0 // hits. $rate_limit_entity->timestamp = REQUEST_TIME; $rate_limit_entity->expiration = $now->add($period)->format('U'); $rate_limit_entity->hits = 0; if ($limit == 0) { $exception = new FloodException('Rate limit reached'); $exception->setHeader('Retry-After', $new_period->format(\DateTime::RFC822)); throw $exception; } } else { if ($rate_limit_entity->hits >= $limit) { $exception = new FloodException('Rate limit reached'); $exception->setHeader('Retry-After', $new_period->format(\DateTime::RFC822)); throw $exception; } } // Save a new hit after generating the exception to mitigate DoS attacks. $rate_limit_entity->hit(); // Add the limit headers to the response. $remaining = $limit == static::UNLIMITED_RATE_LIMIT ? 'unlimited' : $limit - ($rate_limit_entity->hits + 1); $response = restful()->getResponse(); $headers = $response->getHeaders(); $headers->append(HttpHeader::create('X-Rate-Limit-Limit', $limit)); $headers->append(HttpHeader::create('X-Rate-Limit-Remaining', $remaining)); $time_remaining = $rate_limit_entity->expiration - REQUEST_TIME; $headers->append(HttpHeader::create('X-Rate-Limit-Reset', $time_remaining)); } } /** * Delete all expired rate limit entities. */ public static function deleteExpired() { // Clear the expired restful_rate_limit entries. $query = new \EntityFieldQuery(); $results = $query ->entityCondition('entity_type', 'rate_limit') ->propertyCondition('expiration', REQUEST_TIME, '>') ->execute(); if (!empty($results['rate_limit'])) { $rlids = array_keys($results['rate_limit']); entity_delete_multiple('rate_limit', $rlids); } } } ================================================ FILE: src/RateLimit/RateLimitManagerInterface.php ================================================ hash; } /** * The hash to be used as the cache ID. * * @param string $hash * The hash. */ public function setHash($hash) { $this->hash = $hash; } /** * Get the type. * * @return string * The type. */ public function getType() { return $this->type; } /** * Set the type. * * @param string $type * The type. */ public function setType($type) { $this->type = $type; } /** * Get the value. * * @return string * The value. */ public function getValue() { return $this->value; } /** * Set the value. * * @param string $value * The value. */ public function setValue($value) { $this->value = $value; } } ================================================ FILE: src/RenderCache/Entity/CacheFragmentController.php ================================================ generateCacheHash($cache_fragments); if ($fragments = $this->existingFragments($hash)) { return $fragments; } foreach ($cache_fragments as $tag_type => $tag_value) { $cache_fragment = new CacheFragment(array( 'value' => $tag_value, 'type' => $tag_type, 'hash' => $hash, ), static::ENTITY_TYPE); try { if ($id = $this->save($cache_fragment)) { $fragments[] = $cache_fragment; } } catch (\Exception $e) { // Log the exception. It's probably a duplicate fragment. watchdog_exception('restful', $e); } } return $fragments; } /** * Gets the existing fragments for a given hash. * * @param string $hash * The hash. * * @return CacheFragment[] * An array of fragments. */ protected function existingFragments($hash) { $query = new \EntityFieldQuery(); $results = $query ->entityCondition('entity_type', static::ENTITY_TYPE) ->propertyCondition('hash', $hash) ->execute(); return empty($results[static::ENTITY_TYPE]) ? array() : $this->load(array_keys($results[static::ENTITY_TYPE])); } /** * Generated the cache hash based on the cache fragments collection. * * @param ArrayCollection $cache_fragments * The collection of tags. * * @return string * The generated hash. */ public function generateCacheHash(ArrayCollection $cache_fragments) { return substr(sha1(serialize($cache_fragments->toArray())), 0, 40); } /** * Gets the hashes for an EFQ. * * @param \EntityFieldQuery $query * The EFQ. * * @return string[] * The hashes that meet the conditions. */ public static function lookUpHashes(\EntityFieldQuery $query) { $results = $query->execute(); if (empty($results[static::ENTITY_TYPE])) { return array(); } $fragment_ids = array_keys($results[static::ENTITY_TYPE]); $hashes = db_query('SELECT hash FROM {' . static::getTableName() . '} WHERE ' . static::getTableIdkey() . ' IN (:ids)', array( ':ids' => $fragment_ids, ))->fetchCol(); return $hashes; } /** * Removes all the cache fragments. */ public function wipe() { // We are not truncating the entity table so hooks are fired. $query = new \EntityFieldQuery(); $results = $query ->entityCondition('entity_type', static::ENTITY_TYPE) ->execute(); if (empty($results[static::ENTITY_TYPE])) { return; } if ($this->isFastDeleteEnabled()) { db_truncate($this::getTableName())->execute(); return; } $this->delete(array_keys($results[static::ENTITY_TYPE])); } /** * {@inheritdoc} */ public function delete($ids, \DatabaseTransaction $transaction = NULL) { if ($this->isFastDeleteEnabled()) { $this->fastDelete($ids, $transaction); return; } parent::delete($ids, $transaction); } /** * Do a fast delete without loading entities of firing delete hooks. * * @param array $ids * An array of entity IDs. * @param \DatabaseTransaction $transaction * Optionally a DatabaseTransaction object to use. Allows overrides to pass * in their transaction object. * * @throws \Exception * When there is a database error. */ protected function fastDelete($ids, \DatabaseTransaction $transaction = NULL) { $transaction = isset($transaction) ? $transaction : db_transaction(); try { db_delete($this::getTableName()) ->condition($this::getTableIdkey(), $ids, 'IN') ->execute(); // Reset the cache as soon as the changes have been applied. $this->resetCache($ids); // Ignore slave server temporarily. db_ignore_slave(); } catch (\Exception $e) { $transaction->rollback(); watchdog_exception($this->entityType, $e); throw $e; } } /** * Helper function that checks if this controller uses a fast delete. * * @return bool * TRUE if fast delete is enabled. FALSE otherwise. */ protected function isFastDeleteEnabled() { return (bool) variable_get('restful_fast_cache_clear', TRUE); } /** * Get the resource ID for the selected hash. * * @param string $hash * The unique hash for the cache fragments. * * @return string * The resource ID. */ public static function resourceIdFromHash($hash) { $query = new \EntityFieldQuery(); $results = $query ->entityCondition('entity_type', static::ENTITY_TYPE) ->propertyCondition('type', 'resource') ->propertyCondition('hash', $hash) ->range(0, 1) ->execute(); if (empty($results[static::ENTITY_TYPE])) { return NULL; } $cache_fragment = entity_load_single(static::ENTITY_TYPE, key($results[static::ENTITY_TYPE])); $pos = strpos($cache_fragment->value, CacheDecoratedResource::CACHE_PAIR_SEPARATOR); return $pos === FALSE ? $cache_fragment->value : substr($cache_fragment->value, 0, $pos); } /** * Gets the name of the table for the cache fragment entity. * * @return string * The name. */ protected static function getTableName() { if (static::$tableName) { return static::$tableName; } // Get the hashes from the base table. $info = entity_get_info(static::ENTITY_TYPE); static::$tableName = $info['base table']; return static::$tableName; } /** * Gets the name of the table for the cache fragment entity. * * @return string * The name. */ protected static function getTableIdkey() { if (static::$tableIdKey) { return static::$tableIdKey; } // Get the hashes from the base table. $info = entity_get_info(static::ENTITY_TYPE); static::$tableIdKey = $info['entity keys']['id']; return static::$tableIdKey; } } ================================================ FILE: src/RenderCache/RenderCache.php ================================================ cacheFragments = $cache_fragments; /* @var CacheFragmentController $controller */ $controller = entity_get_controller('cache_fragment'); $this->hash = $hash ?: $controller->generateCacheHash($cache_fragments); $this->cacheObject = $cache_object; } /** * {@inheritdoc} */ public static function create(ArrayCollection $cache_fragments, \DrupalCacheInterface $cache_object) { /* @var CacheFragmentController $controller */ $controller = entity_get_controller('cache_fragment'); return new static($cache_fragments, $controller->generateCacheHash($cache_fragments), $cache_object); } /** * {@inheritdoc} */ public function get() { $cid = $this->generateCacheId(); $query = new \EntityFieldQuery(); $count = $query ->entityCondition('entity_type', 'cache_fragment') ->propertyCondition('hash', $cid) ->count() ->execute(); if ($count) { return $this->cacheObject->get($cid); } // If there are no cache fragments for the given hash then clear the cache // and return NULL. $this->cacheObject->clear($cid); return NULL; } /** * {@inheritdoc} */ public function set($value) { /* @var CacheFragmentController $controller */ $controller = entity_get_controller('cache_fragment'); if (!$controller->createCacheFragments($this->cacheFragments)) { return; } $this->cacheObject->set($this->generateCacheId(), $value); } /** * {@inheritdoc} */ public function clear() { // Remove the cache. $this->cacheObject->clear($this->generateCacheId()); // Delete all cache fragments for that hash. $query = new \EntityFieldQuery(); $results = $query ->entityCondition('entity_type', 'cache_fragment') ->propertyCondition('hash', $this->generateCacheId()) ->execute(); if (empty($results['cache_fragment'])) { return; } // Delete the actual entities. entity_delete_multiple('cache_fragment', array_keys($results['cache_fragment'])); } /** * {@inheritdoc} */ public function getCid() { return $this->hash; } /** * Generates the cache id based on the hash and the fragment IDs. * * @return string * The cid. */ protected function generateCacheId() { return $this->hash; } } ================================================ FILE: src/RenderCache/RenderCacheInterface.php ================================================ current()) { return FALSE; } return $resource->isEnabled(); } } ================================================ FILE: src/Resource/ResourceManager.php ================================================ request = $request; $this->pluginManager = $manager ?: ResourcePluginManager::create('cache', $request); $options = array(); foreach ($this->pluginManager->getDefinitions() as $plugin_id => $plugin_definition) { // Set the instance id to articles::1.5 (for example). $options[$plugin_id] = $plugin_definition; } $this->plugins = new ResourcePluginCollection($this->pluginManager, $options); } /** * {@inheritdoc} */ public function getPlugins($only_enabled = TRUE) { if (!$only_enabled) { return $this->plugins; } $cloned_plugins = clone $this->plugins; $instance_ids = $cloned_plugins->getInstanceIds(); foreach ($instance_ids as $instance_id) { $plugin = NULL; try { $plugin = $cloned_plugins->get($instance_id); } catch (UnauthorizedException $e) {} if (!$plugin instanceof ResourceInterface) { $cloned_plugins->remove($instance_id); $cloned_plugins->removeInstanceId($instance_id); } } return $cloned_plugins; } /** * {@inheritdoc} */ public function getPlugin($instance_id, RequestInterface $request = NULL) { /* @var ResourceInterface $plugin */ if (!$plugin = $this->plugins->get($instance_id)) { return NULL; } if ($request) { $plugin->setRequest($request); } return $plugin; } /** * {@inheritdoc} */ public function getPluginCopy($instance_id, RequestInterface $request = NULL) { if (!$plugin = $this->pluginManager->createInstance($instance_id)) { return NULL; } if ($request) { $plugin->setRequest($request); } // Allow altering the resource, this way we can read the resource's // definition to return a different class that is using composition. drupal_alter('restful_resource', $plugin); $plugin = $plugin->isEnabled() ? $plugin : NULL; return $plugin; } /** * {@inheritdoc} */ public function clearPluginCache($instance_id) { $this->plugins->remove($instance_id); } /** * {@inheritdoc} */ public function getResourceIdFromRequest() { $resource_name = &drupal_static(__METHOD__); if (isset($resource_name)) { return $resource_name; } $path = $this->request->getPath(FALSE); list($resource_name,) = static::getPageArguments($path); return $resource_name; } /** * {@inheritdoc} */ public function getVersionFromRequest() { $version = &drupal_static(__METHOD__); if (isset($version)) { return $version; } $version = $this->getVersionFromProvidedRequest($this->request); return $version; } /** * {@inheritdoc} */ public function getVersionFromProvidedRequest(RequestInterface $request = NULL) { $path = $request->getPath(FALSE); list($resource_name, $version) = static::getPageArguments($path); if (preg_match('/^v\d+(\.\d+)?$/', $version)) { $version = $this->parseVersionString($version, $resource_name); return $version; } // If there is no version in the URL check the header. if ($version_string = $this->request->getHeaders()->get('x-api-version')->getValueString()) { $version = $this->parseVersionString($version_string, $resource_name); return $version; } // If there is no version negotiation information return the latest version. $version = $this->getResourceLastVersion($resource_name); return $version; } /** * {@inheritdoc} */ public function negotiate() { return $this->negotiateFromRequest($this->request); } /** * {@inheritdoc} */ public function negotiateFromRequest(RequestInterface $request) { $version = $this->getVersionFromProvidedRequest($request); list($resource_name,) = static::getPageArguments($request->getPath(FALSE)); try { $resource = $this->getPlugin($resource_name . PluginBase::DERIVATIVE_SEPARATOR . $version[0] . '.' . $version[1]); return $resource->isEnabled() ? $resource : NULL; } catch (PluginNotFoundException $e) { throw new ServerConfigurationException($e->getMessage()); } } /** * {@inheritdoc} */ public static function executeCallback($callback, array $params = array()) { if (!is_callable($callback)) { if (is_array($callback) && count($callback) == 2 && is_array($callback[1])) { // This code deals with the third scenario in the docblock. Get the // callback and the parameters from the array, merge the parameters with // the existing ones and call recursively to reuse the logic for the // other cases. return static::executeCallback($callback[0], array_merge($params, $callback[1])); } $callback_name = is_array($callback) ? $callback[1] : $callback; throw new ServerConfigurationException("Callback function: $callback_name does not exist."); } return call_user_func_array($callback, $params); } /** * {@inheritdoc} */ public static function isValidCallback($callback) { // Valid callbacks are: // - 'function_name' // - 'SomeClass::someStaticMethod' // - array('function_name', array('param1', 2)) // - array($this, 'methodName') // - array(array($this, 'methodName'), array('param1', 2)) if (!is_callable($callback)) { if (is_array($callback) && count($callback) == 2 && is_array($callback[1])) { return static::isValidCallback($callback[0]); } return FALSE; } return TRUE; } /** * Get the resource name and version from the page arguments in the router. * * @param string $path * The path to match the router item. Leave it empty to use the current one. * * @return array * An array of 2 elements with the page arguments. */ protected static function getPageArguments($path = NULL) { $router_item = static::getMenuItem($path); $output = array(NULL, NULL); if (empty($router_item['page_arguments'])) { return $output; } $page_arguments = $router_item['page_arguments']; $index = 0; foreach ($page_arguments as $page_argument) { $output[$index] = $page_argument; $index++; if ($index >= 2) { break; } } return $output; } /** * {@inheritdoc} */ public static function getPageCallback($path = NULL) { $router_item = static::getMenuItem($path); return isset($router_item['page_callback']) ? $router_item['page_callback'] : NULL; } /** * Parses the version string. * * @param string $version * The string containing the version information. * @param string $resource_name * (optional) Name of the resource to get the latest minor version. * * @return array * Numeric array with major and minor version. */ protected function parseVersionString($version, $resource_name = NULL) { if (preg_match('/^v\d+(\.\d+)?$/', $version)) { // Remove the leading 'v'. $version = substr($version, 1); } $output = explode('.', $version); if (count($output) == 1) { $major_version = $output[0]; // Abort if the version is not numeric. if (!$resource_name || !ctype_digit((string) $major_version)) { return NULL; } // Get the latest version for the resource. return $this->getResourceLastVersion($resource_name, $major_version); } // Abort if any of the versions is not numeric. if (!ctype_digit((string) $output[0]) || !ctype_digit((string) $output[1])) { return NULL; } return $output; } /** * Get the non translated menu item. * * @param string $path * The path to match the router item. Leave it empty to use the current one. * * @return array * The page arguments. * * @see menu_get_item() */ protected static function getMenuItem($path = NULL) { $router_items = &drupal_static(__METHOD__); if (!isset($path)) { $path = $_GET['q']; } if (!isset($router_items[$path])) { $original_map = arg(NULL, $path); $parts = array_slice($original_map, 0, MENU_MAX_PARTS); $ancestors = menu_get_ancestors($parts); $router_item = db_query_range('SELECT * FROM {menu_router} WHERE path IN (:ancestors) ORDER BY fit DESC', 0, 1, array(':ancestors' => $ancestors))->fetchAssoc(); if ($router_item) { // Allow modules to alter the router item before it is translated and // checked for access. drupal_alter('menu_get_item', $router_item, $path, $original_map); $router_item['original_map'] = $original_map; if ($original_map === FALSE) { $router_items[$path] = FALSE; return FALSE; } $router_item['map'] = $original_map; $router_item['page_arguments'] = array_merge(menu_unserialize($router_item['page_arguments'], $original_map), array_slice($original_map, $router_item['number_parts'])); $router_item['theme_arguments'] = array_merge(menu_unserialize($router_item['theme_arguments'], $original_map), array_slice($original_map, $router_item['number_parts'])); } $router_items[$path] = $router_item; } return $router_items[$path]; } /** * {@inheritdoc} */ public function getResourceLastVersion($resource_name, $major_version = NULL) { $resources = array(); // Get all the resources corresponding to the resource name. foreach ($this->pluginManager->getDefinitions() as $plugin_id => $plugin_definition) { if ($plugin_definition['resource'] != $resource_name || (isset($major_version) && $plugin_definition['majorVersion'] != $major_version)) { continue; } $resources[$plugin_definition['majorVersion']][$plugin_definition['minorVersion']] = $plugin_definition; } // Sort based on the major version. ksort($resources, SORT_NUMERIC); // Get a list of resources for the latest major version. $resources = end($resources); if (empty($resources)) { return NULL; } // Sort based on the minor version. ksort($resources, SORT_NUMERIC); // Get the latest resource for the minor version. $resource = end($resources); return array($resource['majorVersion'], $resource['minorVersion']); } } ================================================ FILE: src/Resource/ResourceManagerInterface.php ================================================ isEnabled() ? $resource : NULL; return $resource; } } ================================================ FILE: src/RestfulManager.php ================================================ request; } /** * Mutator for the request. * * @param RequestInterface $request * The request object. */ public function setRequest(RequestInterface $request) { $this->request = $request; } /** * Accessor for the response. * * @return ResponseInterface * The response object. */ public function getResponse() { return clone $this->response; } /** * Mutator for the response. * * @param ResponseInterface $response * The response object. */ public function setResponse(ResponseInterface $response) { $this->response = $response; } /** * Accessor for the resource manager. * * @return ResourceManagerInterface */ public function getResourceManager() { return clone $this->resourceManager; } /** * Mutator for the resource manager. * * @param ResourceManagerInterface $resource_manager */ public function setResourceManager(ResourceManagerInterface $resource_manager) { $this->resourceManager = $resource_manager; } /** * Accessor for the formatter manager. * * @return FormatterManagerInterface */ public function getFormatterManager() { return clone $this->formatterManager; } /** * Mutator for the formatter manager. * * @param FormatterManagerInterface $formatter_manager */ public function setFormatterManager(FormatterManagerInterface $formatter_manager) { $this->formatterManager = $formatter_manager; } /** * @return PersistableCacheInterface */ public function getPersistableCache() { return $this->persistableCache; } /** * @param PersistableCacheInterface $persistable_cache */ public function setPersistableCache($persistable_cache) { $this->persistableCache = $persistable_cache; } /** * Constructor. */ public function __construct(RequestInterface $request, ResponseInterface $response, ResourceManagerInterface $resource_manager, FormatterManagerInterface $formatter_manager, PersistableCacheInterface $persistable_cache) { // Init the properties. $this->request = $request; $this->response = $response; $this->resourceManager = $resource_manager; $this->formatterManager = $formatter_manager; $this->persistableCache = $persistable_cache; } /** * Factory method. * * @return RestfulManager * The newly created manager. */ public static function createFromGlobals() { $request = Request::createFromGlobals(); $response = Response::create(); $resource_manager = new ResourceManager($request); $formatter_manager = new FormatterManager(); $persistable_cache = new PersistableCache(RenderCache::CACHE_BIN); return new static($request, $response, $resource_manager, $formatter_manager, $persistable_cache); } /** * Processes the request to produce a response. * * @return Response * The response being sent back to the consumer via the menu callback. */ public function process() { // Gets the appropriate resource plugin (based on the request object). That // plugin processes the request according to the REST principles. $data = $this->resourceManager->process(); // Formats the data based on the negotiatied output format. It accesses to // the request information via the service container. $body = $this->formatterManager->format($data); // Prepares the body for the response. At this point all headers have been // added to the response. $this->response->setContent($body); // The menu callback function is in charge of adding all the headers and // returning the body. return $this->response; } /** * Checks if the passed in request belongs to RESTful. * * @param RequestInterface $request * The path to check. * * @return bool * TRUE if the path belongs to RESTful. */ public static function isRestfulPath(RequestInterface $request) { return ResourceManager::getPageCallback($request->getPath(FALSE)) == static::FRONT_CONTROLLER_CALLBACK; } /** * Helper function to echo static strings. * * @param DataInterpreterInterface $value * The resource value. * @param mixed $message * The string to relay. * * @return mixed * Returns $message */ public static function echoMessage(DataInterpreterInterface $value, $message) { return $message; } } ================================================ FILE: src/Util/EntityFieldQuery.php ================================================ ', 'NOT BETWEEN', 'NOT LIKE', ); /** * {@inheritdoc} */ public function getRelationships() { return $this->relationships; } /** * {@inheritdoc} */ public function addRelationship(array $relational_filter) { $this->relationships[] = $relational_filter; } /** * {@inheritdoc} */ public function queryCallback() { // Scan all the field conditions to see if there is any operator that needs // a left join. If there is none, use the default behavior. $left_field_conditions = array_filter($this->fieldConditions, function ($field_condition) { return !empty($field_condition['operator']) && in_array($field_condition['operator'], static::$leftJoinOperators); }); return empty($left_field_conditions) ? parent::queryCallback() : array($this, 'buildQuery'); } /** * Builds the SelectQuery and executes finishQuery(). */ protected function buildQuery() { // Make the query be based on the entity table so we can get all the // entities. $select_query = $this->prePropertyQuery(); list($select_query, $id_key) = $this->fieldStorageQuery($select_query); return $this->finishQuery($select_query, $id_key); } /** * {@inheritdoc} */ public function finishQuery($select_query, $id_key = 'entity_id') { $entity_type = $this->entityConditions['entity_type']['value']; foreach ($this->getRelationships() as $delta => $relationship) { // A relational filter consists of a chain of relationships and a value // for a condition at the end. // Relationships start with the entity base table. $entity_info = entity_get_info($entity_type); $entity_table = $entity_table_alias = $entity_info['base table']; // Add the table if the base entity table was not added because: // 1. There was a fieldCondition or fieldOrderBy, AND // 2. There was no property condition or order. if ($delta == 0) { $is_entity_table_present = FALSE; $field_base_table_alias = NULL; foreach ($select_query->getTables() as $table_info) { // Search for the base table and check if the entity table is present // for the resource's entity type. if (!$field_base_table_alias && empty($table_info['join type'])) { $field_base_table_alias = $table_info['alias']; } if ($table_info['table'] == $entity_table) { $is_entity_table_present = TRUE; break; } } if (!$is_entity_table_present && $field_base_table_alias) { // We have the base table and we need to join it to the entity table. _field_sql_storage_query_join_entity($select_query, $entity_type, $field_base_table_alias); } } // Pop the last item, since it is the one that has to match the filter and // will have the WHERE associated. $condition = array_pop($relationship['relational_filters']); foreach ($relationship['relational_filters'] as $relational_filter) { /* @var RelationalFilterInterface $relational_filter */ if ($relational_filter->getType() == RelationalFilterInterface::TYPE_FIELD) { $field_table_name = _field_sql_storage_tablename(field_info_field($relational_filter->getName())); $field_table_alias = $this::aliasJoinTable($field_table_name, $select_query); $select_query->addJoin('INNER', $field_table_name, $field_table_alias, sprintf('%s.%s = %s.%s', $entity_table_alias, $entity_info['entity keys']['id'], $field_table_alias, $id_key )); // Get the entity type being referenced. $entity_info = entity_get_info($relational_filter->getEntityType()); $entity_table_alias = $this::aliasJoinTable($entity_info['base table'], $select_query); $select_query->addJoin('INNER', $entity_info['base table'], $entity_table_alias, sprintf('%s.%s = %s.%s', $field_table_name, _field_sql_storage_columnname($relational_filter->getName(), $relational_filter->getColumn()), $entity_table_alias, $relational_filter->getTargetColumn() )); } elseif ($relational_filter->getType() == RelationalFilterInterface::TYPE_PROPERTY) { // In this scenario we want to join with the new table entity. This // will only work if the property contains the referenced entity ID // (which is not unreasonable). $host_entity_table = $entity_table_alias; $entity_info = entity_get_info($relational_filter->getEntityType()); $entity_table_alias = $this::aliasJoinTable($entity_info['base table'], $select_query); $select_query->addJoin('INNER', $entity_info['base table'], $entity_table_alias, sprintf('%s.%s = %s.%s', $host_entity_table, $relational_filter->getName(), $entity_table_alias, $relational_filter->getTargetColumn() )); } } /* @var RelationalFilterInterface $condition */ if ($condition->getType() == RelationalFilterInterface::TYPE_FIELD) { // Make the join to the filed table for the condition. $field_table_name = _field_sql_storage_tablename(field_info_field($condition->getName())); $field_column = _field_sql_storage_columnname($condition->getName(), $condition->getColumn()); $field_table_alias = $this::aliasJoinTable($field_table_name, $select_query); $select_query->addJoin('INNER', $field_table_name, $field_table_alias, sprintf('%s.%s = %s.%s', $entity_table_alias, $entity_info['entity keys']['id'], $field_table_alias, $id_key )); if (in_array($relationship['operator'], array('IN', 'BETWEEN'))) { $select_query->condition($field_table_name . '.' . $field_column, $relationship['value'], $relationship['operator'][0]); } else { for ($index = 0; $index < count($relationship['value']); $index++) { $select_query->condition($field_table_name . '.' . $field_column, $relationship['value'][$index], $relationship['operator'][$index]); } } } elseif ($condition->getType() == RelationalFilterInterface::TYPE_PROPERTY) { if (in_array($relationship['operator'], array('IN', 'BETWEEN'))) { $select_query->condition($entity_table_alias . '.' . $condition->getName(), $relationship['value'], $relationship['operator'][0]); } else { for ($index = 0; $index < count($relationship['value']); $index++) { $select_query->condition($entity_table_alias . '.' . $condition->getName(), $relationship['value'][$index], $relationship['operator'][$index]); } } } } return parent::finishQuery($select_query, $id_key); } /** * Helper function tha checks if the select query already has a join. * * @param string $table_name * The name of the table. * @param SelectQuery $query * The query. * * @return string * The table alias. */ protected static function aliasJoinTable($table_name, SelectQuery $query) { foreach ($query->getTables() as $table_info) { if ($table_info['alias'] == $table_name) { $matches = array(); preg_match('/.*_(\d+)$/', $table_name, $matches); $num = empty($matches[1]) ? -1 : $matches[1]; return static::aliasJoinTable($table_name . '_' . ($num + 1), $query); } } return $table_name; } /** * Copy of propertyQuery() without the finishQuery execution. * * @see \EntityFieldQuery::propertyQuery() */ protected function prePropertyQuery() { if (empty($this->entityConditions['entity_type'])) { throw new \EntityFieldQueryException(t('For this query an entity type must be specified.')); } $entity_type = $this->entityConditions['entity_type']['value']; $entity_info = entity_get_info($entity_type); if (empty($entity_info['base table'])) { throw new \EntityFieldQueryException(t('Entity %entity has no base table.', array('%entity' => $entity_type))); } $base_table = $entity_info['base table']; $base_table_schema = drupal_get_schema($base_table); $select_query = db_select($base_table); $select_query->addExpression(':entity_type', 'entity_type', array(':entity_type' => $entity_type)); // Process the property conditions. foreach ($this->propertyConditions as $property_condition) { $this->addCondition($select_query, $base_table . '.' . $property_condition['column'], $property_condition); } // Process the four possible entity condition. // The id field is always present in entity keys. $sql_field = $entity_info['entity keys']['id']; $this->addMetaData('base_table', $base_table); $this->addMetaData('entity_id_key', $sql_field); $id_map['entity_id'] = $sql_field; $select_query->addField($base_table, $sql_field, 'entity_id'); if (isset($this->entityConditions['entity_id'])) { $this->addCondition($select_query, $base_table . '.' . $sql_field, $this->entityConditions['entity_id']); } // If there is a revision key defined, use it. if (!empty($entity_info['entity keys']['revision'])) { $sql_field = $entity_info['entity keys']['revision']; $select_query->addField($base_table, $sql_field, 'revision_id'); if (isset($this->entityConditions['revision_id'])) { $this->addCondition($select_query, $base_table . '.' . $sql_field, $this->entityConditions['revision_id']); } } else { $sql_field = 'revision_id'; $select_query->addExpression('NULL', 'revision_id'); } $id_map['revision_id'] = $sql_field; // Handle bundles. if (!empty($entity_info['entity keys']['bundle'])) { $sql_field = $entity_info['entity keys']['bundle']; $having = FALSE; if (!empty($base_table_schema['fields'][$sql_field])) { $select_query->addField($base_table, $sql_field, 'bundle'); } } else { $sql_field = 'bundle'; $select_query->addExpression(':bundle', 'bundle', array(':bundle' => $entity_type)); $having = TRUE; } $id_map['bundle'] = $sql_field; if (isset($this->entityConditions['bundle'])) { if (!empty($entity_info['entity keys']['bundle'])) { $this->addCondition($select_query, $base_table . '.' . $sql_field, $this->entityConditions['bundle'], $having); } else { // This entity has no bundle, so invalidate the query. $select_query->where('1 = 0'); } } // Order the query. foreach ($this->order as $order) { if ($order['type'] == 'entity') { $key = $order['specifier']; if (!isset($id_map[$key])) { throw new \EntityFieldQueryException(t('Do not know how to order on @key for @entity_type', array('@key' => $key, '@entity_type' => $entity_type))); } $select_query->orderBy($id_map[$key], $order['direction']); } elseif ($order['type'] == 'property') { $select_query->orderBy($base_table . '.' . $order['specifier'], $order['direction']); } } return $select_query; } /** * Copies field_sql_storage_field_storage_query() using left joins some times. * * @see field_sql_storage_field_storage_query() */ protected function fieldStorageQuery(SelectQuery $select_query) { if ($this->age == FIELD_LOAD_CURRENT) { $tablename_function = '_field_sql_storage_tablename'; $id_key = 'entity_id'; } else { $tablename_function = '_field_sql_storage_revision_tablename'; $id_key = 'revision_id'; } $table_aliases = array(); $query_tables = NULL; $base_table = $this->metaData['base_table']; // Add tables for the fields used. $field_base_table = NULL; foreach ($this->fields as $key => $field) { $tablename = $tablename_function($field); $table_alias = _field_sql_storage_tablealias($tablename, $key, $this); $table_aliases[$key] = $table_alias; $select_query->addMetaData('base_table', $base_table); $entity_id_key = $this->metaData['entity_id_key']; if ($field_base_table) { if (!isset($query_tables[$table_alias])) { $this->addFieldJoin($select_query, $field['field_name'], $tablename, $table_alias, "$table_alias.entity_type = $field_base_table.entity_type AND $table_alias.$id_key = $field_base_table.$id_key"); } } else { // By executing prePropertyQuery() we made sure that the base table is // the entity table. $this->addFieldJoin($select_query, $field['field_name'], $tablename, $table_alias, "$base_table.$entity_id_key = $table_alias.$id_key"); // Store a reference to the list of joined tables. $query_tables =& $select_query->getTables(); // Allow queries internal to the Field API to opt out of the access // check, for situations where the query's results should not depend on // the access grants for the current user. if (!isset($this->tags['DANGEROUS_ACCESS_CHECK_OPT_OUT'])) { $select_query->addTag('entity_field_access'); } if (!$this->containsLeftJoinOperator($this->fields[$key]['field_name'])) { $field_base_table = $table_alias; } } if ($field['cardinality'] != 1 || $field['translatable']) { $select_query->distinct(); } } // Add field conditions. We need a fresh grouping cache. drupal_static_reset('_field_sql_storage_query_field_conditions'); _field_sql_storage_query_field_conditions($this, $select_query, $this->fieldConditions, $table_aliases, '_field_sql_storage_columnname'); // Add field meta conditions. _field_sql_storage_query_field_conditions($this, $select_query, $this->fieldMetaConditions, $table_aliases, '_field_sql_storage_query_columnname'); // If there was no field condition that created an INNER JOIN, that means // that additional JOINs need to carry the OR condition. For the base table // we'll use the table for the first field. $needs_or = FALSE; if (!isset($field_base_table)) { $needs_or = TRUE; // Get the table name for the first field. $field_table_name = key($this->fields[0]['storage']['details']['sql'][$this->age]); $field_base_table = _field_sql_storage_tablealias($field_table_name, 0, $this); } if (isset($this->deleted)) { $delete_condition = array( 'value' => (int) $this->deleted, 'operator' => '=', 'or' => $needs_or, ); $this->addCondition($select_query, "$field_base_table.deleted", $delete_condition); } foreach ($this->entityConditions as $key => $condition) { $condition['or'] = $needs_or; $this->addCondition($select_query, "$field_base_table.$key", $condition); } // Order the query. foreach ($this->order as $order) { if ($order['type'] == 'entity') { $key = $order['specifier']; $select_query->orderBy("$field_base_table.$key", $order['direction']); } elseif ($order['type'] == 'field') { $specifier = $order['specifier']; $field = $specifier['field']; $table_alias = $table_aliases[$specifier['index']]; $sql_field = "$table_alias." . _field_sql_storage_columnname($field['field_name'], $specifier['column']); $select_query->orderBy($sql_field, $order['direction']); } elseif ($order['type'] == 'property') { $select_query->orderBy("$base_table." . $order['specifier'], $order['direction']); } } return array($select_query, $id_key); } /** * Adds a join to the field table with the appropriate join type. * * @param SelectQuery $select_query * The select query to modify. * @param string $field_name * The name of the field to join. * @param string $table * The table against which to join. * @param string $alias * The alias for the table. In most cases this should be the first letter * of the table, or the first letter of each "word" in the table. * @param string $condition * The condition on which to join this table. If the join requires values, * this clause should use a named placeholder and the value or values to * insert should be passed in the 4th parameter. For the first table joined * on a query, this value is ignored as the first table is taken as the base * table. The token %alias can be used in this string to be replaced with * the actual alias. This is useful when $alias is modified by the database * system, for example, when joining the same table more than once. * @param array $arguments * An array of arguments to replace into the $condition of this join. * * @return string * The unique alias that was assigned for this table. */ protected function addFieldJoin(SelectQuery $select_query, $field_name, $table, $alias = NULL, $condition = NULL, $arguments = array()) { // Find if we need a left or inner join by inspecting the field conditions. $type = 'INNER'; foreach ($this->fieldConditions as $field_condition) { if ($field_condition['field']['field_name'] == $field_name) { $type = in_array($field_condition['operator'], static::$leftJoinOperators) ? 'LEFT' : 'INNER'; break; } } return $select_query->addJoin($type, $table, $alias, $condition, $arguments); } /** * Adds a condition to an already built SelectQuery (internal function). * * This is a helper for hook_entity_query() and hook_field_storage_query(). * * @param SelectQuery $select_query * A SelectQuery object. * @param string $sql_field * The name of the field. * @param array $condition * A condition as described in EntityFieldQuery::fieldCondition() and * EntityFieldQuery::entityCondition(). * @param bool $having * HAVING or WHERE. This is necessary because SQL can't handle WHERE * conditions on aliased columns. */ public function addCondition(SelectQuery $select_query, $sql_field, $condition, $having = FALSE) { $needs_or = !empty($condition['or']) || in_array($condition['operator'], static::$leftJoinOperators); if ( in_array($condition['operator'], array('CONTAINS', 'STARTS_WITH')) || !$needs_or ) { parent::addCondition($select_query, $sql_field, $condition, $having); return; } $method = $having ? 'havingCondition' : 'condition'; $db_or = db_or()->condition($sql_field, $condition['value'], $condition['operator']); if (strtoupper($condition['operator']) != 'IS NULL' && strtoupper($condition['operator']) != 'IS NOT NULL') { $db_or->condition($sql_field, NULL, 'IS NULL'); } $select_query->$method($db_or); } /** * Checks if any of the conditions contains a LEFT JOIN operation. * * @param string $field_name * If provided only this field will be checked. * * @return bool * TRUE if any of the conditions contain a left join operator. */ protected function containsLeftJoinOperator($field_name = NULL) { foreach ($this->fieldConditions as $field_condition) { if ($field_name && $field_condition['field']['field_name'] != $field_name) { continue; } if (in_array($field_condition['operator'], static::$leftJoinOperators)) { return TRUE; } } return FALSE; } } ================================================ FILE: src/Util/EntityFieldQueryRelationalConditionsInterface.php ================================================ cacheBin = $cache_bin ?: 'cache'; } /** * {@inheritdoc} */ public function contains($key) { return isset($this->data[$key]); } /** * {@inheritdoc} */ public function &get($key) { if (!$this->contains($key)) { // Load from the real cache if it's not loaded yet. $this->load($key); } if (!$this->contains($key)) { $this->data[$key] = NULL; } return $this->data[$key]; } /** * {@inheritdoc} */ public function set($key, $value) { $this->data[$key] = $value; } /** * {@inheritdoc} */ public function delete($key) { unset($this->data[$key]); } /** * {@inheritdoc} */ public function persist() { foreach ($this->data as $key => $value) { cache_set($key, $value, $this->cacheBin); } } /** * Persist the data in the cache backend during shutdown. */ public function __destruct() { $this->persist(); } /** * Checks if a key was already loaded before. * * @param string $key * The key to check. * * @return bool * TRUE if it was loaded before. FALSE otherwise. */ protected function isLoaded($key) { return isset($this->loaded[$key]); } /** * Tries to load an item from the real cache. * * @param string $key * The key of the item. */ protected function load($key) { if ($this->isLoaded($key)) { return; } // Mark the key as loaded. $this->loaded[$key] = TRUE; if ($cache = cache_get($key, $this->cacheBin)) { $this->data[$key] = $cache->data; } } } ================================================ FILE: src/Util/PersistableCacheInterface.php ================================================ name = $name; $this->type = $type; $this->column = $column; $this->entityType = $entity_type; $this->bundles = $bundles; $this->targetColumn = $target_column; } /** * {@inheritdoc} */ public function getName() { return $this->name; } /** * {@inheritdoc} */ public function getType() { return $this->type; } /** * {@inheritdoc} */ public function getEntityType() { return $this->entityType; } /** * {@inheritdoc} */ public function getBundles() { return $this->bundles; } /** * {@inheritdoc} */ public function getColumn() { return $this->column; } /** * {@inheritdoc} */ public function getTargetColumn() { return $this->targetColumn; } } ================================================ FILE: src/Util/RelationalFilterInterface.php ================================================ 'Authentication', 'description' => 'Test the request authentication.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_test'); // Create a custom user that we'll use to identify. $this->account = $this->drupalCreateUser(); $this->originalServer = $_SERVER; $this->originalSessions = $_SESSION; $this->pluginManager = AuthenticationPluginManager::create(); } public function tearDown() { // Put back the $_SERVER array. $_SERVER = $this->originalServer; $_SESSION = $this->originalSession; parent::tearDown(); } /** * Test authenticating a user. */ function testAuthentication() { // Start a session just in case we're executing the code from CLI. drupal_session_start(); global $user; $resource_manager = restful()->getResourceManager(); $request = Request::create(''); $handler = $resource_manager->getPlugin('main:1.5'); // Case 1. Check that the handler has the expected authentication providers. $providers = array_keys($this->pluginManager->getDefinitions()); $plugin_definition = $handler->getPluginDefinition(); foreach ($plugin_definition['authenticationTypes'] as $provider_name) { $this->assertTrue(in_array($provider_name, $providers), format_string('The %name authorization type was found.', array( '%name' => $provider_name, ))); } // Case 2. Test that the account from the authentication manager is the // logged in user. // We need to hijack the global user object in order to force it to be our // test account and make the cookie authentication provider to resolve it. $user = $this->account; $handler->setRequest($request); $handler->setAccount(NULL); $this->assertEqual($this->account->uid, $handler->getAccount(FALSE)->uid, 'The authentication manager resolved the currently logged in user.'); $cookie_provider = $this->pluginManager->createInstance('cookie'); // Case 3. Test the 'cookie_auth' authentication provider. $this->assertEqual($this->account->uid, $cookie_provider->authenticate($request)->uid, 'The cookie provider resolved the currently logged in user.'); $user = drupal_anonymous_user(); // Case 4. Test that the 'cookie_auth' resolves the anonymous user. $this->assertEqual(0, $cookie_provider->authenticate($request)->uid, 'The cookie provider resolved the anonymous user.'); $basic_auth_provider = $this->pluginManager->createInstance('basic_auth'); // Case 5. Valid login using basic auth. $_SERVER['PHP_AUTH_USER'] = $this->account->name; $_SERVER['PHP_AUTH_PW'] = $this->account->pass_raw; $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] = NULL; $this->assertEqual($this->account->uid, $basic_auth_provider->authenticate($request)->uid, 'The basic auth provider resolved the currently logged in user.'); // Case 6. Valid login using REDIRECT_HTTP_AUTHORIZATION. $_SERVER['PHP_AUTH_USER'] = NULL; $_SERVER['PHP_AUTH_PW'] = NULL; $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] = 'Basic ' . base64_encode($this->account->name . ':' . $this->account->pass_raw); $this->assertEqual($this->account->uid, $basic_auth_provider->authenticate($request)->uid, 'The basic auth provider resolved the currently logged in user. Using REDIRECT_HTTP_AUTHORIZATION.'); // Case 7. Invalid pass for basic auth. $_SERVER['PHP_AUTH_USER'] = $this->account->name; $_SERVER['PHP_AUTH_PW'] = $this->randomName(); $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] = NULL; $this->assertNull($basic_auth_provider->authenticate($request), 'The basic auth provider could not resolve a user with invalid password.'); // Case 8. Invalid username for basic auth. $_SERVER['PHP_AUTH_USER'] = $this->randomName(); $_SERVER['PHP_AUTH_PW'] = $this->account->pass_raw; $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] = NULL; $this->assertNull($basic_auth_provider->authenticate($request), 'The basic auth provider could not resolve a user with invalid username.'); // Case 9. Valid login using REDIRECT_HTTP_AUTHORIZATION. $_SERVER['PHP_AUTH_USER'] = NULL; $_SERVER['PHP_AUTH_PW'] = NULL; $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] = 'Basic ' . base64_encode($this->randomName() . ':' . $this->randomName()); $this->assertNull($basic_auth_provider->authenticate($request), 'The basic auth provider could not resolve a user with invalid encoded username & password. Using REDIRECT_HTTP_AUTHORIZATION.'); // Case 11. Accessing a resource with optional authentication. // We are getting a 403 instead of 401, as the access is now based on the // permissions. user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array('access user profiles' => FALSE)); $handler = $resource_manager->getPlugin('users:1.0'); $handler->setRequest(Request::create('api/v1.0/users')); $handler->setPath(''); $handler->setAccount(NULL); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual($result[0]['self'], url('api/v1.0/users/0', array('absolute' => TRUE))); $this->assertEqual(count($result), 1, 'The anonymous users can only see themselves.'); // To assert permissions control access to the resource, we change the // permission for anonymous to access other user's profile. user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array('access user profiles' => TRUE)); // If the process function does not throw an exception, the test passes. restful()->getFormatterManager()->format($handler->process(), 'json'); } /** * Test recording of access time. */ public function testAccessTime() { global $user; $user1 = $this->drupalCreateUser(); $user2 = $this->drupalCreateUser(); $handler = restful()->getResourceManager()->getPlugin('main:1.5'); // Case 1. Ensure that access time is recorded for cookie auth. $user = $user1; $user1_access_time_before = db_query('SELECT access FROM {users} WHERE uid = :d', array(':d' => $user1->uid))->fetchObject(); // Perform request authentication. $handler->getAccount(); $user1_access_time = db_query('SELECT access FROM {users} WHERE uid = :d', array(':d' => $user1->uid))->fetchObject(); $this->assertEqual($user1_access_time->access, REQUEST_TIME, 'Cookie authenticated user access time is updated.'); $this->assertNotEqual($user1_access_time_before->access, $user1_access_time->access, 'Access time before and after request are equal.'); // Case 2. Ensure that access time is recorded for basic auth. $user = $user2; $_SERVER['PHP_AUTH_USER'] = $user2->name; $_SERVER['PHP_AUTH_PW'] = $user2->pass_raw; $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] = NULL; $handler = restful()->getResourceManager()->getPlugin('articles:1.0'); // Perform request authentication. $handler->getAccount(); $user2_access_time = db_query('SELECT access FROM {users} WHERE uid = :d', array(':d' => $user2->uid))->fetchObject(); $this->assertEqual($user2_access_time->access, REQUEST_TIME, 'Basic authenticated user access time is updated.'); // Case 3. Ensure that the timestamp gets updated. $user = $user1; // Get a timestamp that is in the past. $the_past = REQUEST_TIME - variable_get('session_write_interval'); // To begin, we'll set the timestamp for user1 back a little bit. db_update('users') ->fields(array('access' => $the_past)) ->condition('uid', $user1->uid) ->execute(); $user1_pre_access_time = db_query('SELECT access FROM {users} WHERE uid = :d', array(':d' => $user1->uid))->fetchObject(); $this->assertEqual($user1_pre_access_time->access, $the_past, 'Set user1 access time to a time in the past.'); // Perform an authenticated request. $this->drupalGet('/api/v1.5/main', array(), array( 'Authorization' => 'Basic ' . base64_encode($user1->name . ':' . $user1->pass_raw)) ); $user1_post_access_time = db_query('SELECT access FROM {users} WHERE uid = :d', array(':d' => $user1->uid))->fetchObject(); $this->assertEqual($user1_post_access_time->access, REQUEST_TIME, 'Basic authenticated user access time is updated.'); } } ================================================ FILE: tests/RestfulAutoCompleteTestCase.test ================================================ 'Autocomplete', 'description' => 'Test the autocomplete functionality.', 'group' => 'RESTful', ); } /** * @var \stdClass. * * The user object. */ protected $user; function setUp() { parent::setUp('restful_test'); $this->user = $this->drupalCreateUser(); } /** * Test the autocomplete functionality. */ function testAutocomplete() { // Create terms. restful_test_create_vocabulary_and_terms('tags', FALSE); $handler = restful_get_restful_handler('test_tags'); // "CONTAINS" operator. $request = array( 'autocomplete' => array( 'string' => 'ter', ), ); $result = $handler->get('', $request); $expected_result = array ( 1 => 'term1', 2 => 'term2', 3 => 'term3', ); $this->assertEqual($result, $expected_result, 'Autocomplete with "CONTAINS" operator returned expected results.'); // "STARTS_WITH" operator. $request = array( 'autocomplete' => array( 'string' => 'term1', 'operator' => 'STARTS_WITH' ), ); $result = $handler->get('', $request); $expected_result = array ( 1 => 'term1', ); $this->assertEqual($result, $expected_result, 'Autocomplete with "CONTAINS" operator returned expected results.'); // Empty query. $request = array( 'autocomplete' => array( 'string' => 'non-existing', ), ); $result = $handler->get('', $request); $this->assertFalse($result, 'No values returned from empty query.'); // Test autocomplete for users. $handler = restful_get_restful_handler('users'); $request = array( 'autocomplete' => array( 'string' => substr($this->user->name, 0, 3), ), ); $result = $handler->get('', $request); $this->assertTrue($result, 'Results has returned for entity without a bundle key.'); } } ================================================ FILE: tests/RestfulCommentTestCase.test ================================================ 'Comment integration', 'description' => 'Test the CRUD of a comment.', 'group' => 'RESTful', ); } public function setUp() { parent::setUp('restful_example', 'comment'); $this->account = $this->drupalCreateUser(array( 'create article content', 'edit own comments', )); $settings = array( 'type' => 'article', 'title' => $this->randomName(), 'uid' => $this->account->uid, ); $this->node = $this->drupalCreateNode($settings); // Add a comment_text field instead of comment_body field as the // comment_body property is required, see entity_metadata_comment_entity_property_info() // for more information. $this->addTextField('comment', 'comment_node_article', 'comment_text'); } /** * Test creating a comment (POST method). */ public function testCreateComment() { $resource_manager = restful()->getResourceManager(); $this->drupalLogin($this->account); $handler = $resource_manager->getPlugin('comments:1.0'); // @todo Remove "administer comments" when https://www.drupal.org/node/2236229 is fixed. $permissions = array( 'post comments', 'administer comments', ); // Set a different user from the logged in user, to assert the comment's // author is set correctly. $user = $this->drupalCreateUser($permissions); $handler->setAccount($user); $label = $this->randomName(); $request = array( 'label' => $label, 'nid' => $this->node->nid, ); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doPost($request), 'json')); $result = $result['data']; $comment = comment_load($result[0]['id']); $this->assertEqual($comment->uid, $user->uid, 'Correct user was set to be the author of the comment.'); $this->assertEqual($comment->nid, $this->node->nid, 'Correct nid set.'); $this->assertEqual($comment->subject, $label, 'Correct subject set.'); } /** * Test creating a comment (GET method). */ public function testRetrieve() { $resource_manager = restful()->getResourceManager(); $label = $this->randomName(); $settings = array( 'node_type' => 'comment_node_article', 'nid' => $this->node->nid, 'uid' => $this->account->uid, 'subject' => $label, ); $comment = $this->createComment($settings); $id = $comment->cid; $handler = $resource_manager->getPlugin('comments:1.0'); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doGet($id), 'json')); $result = $result['data']; $comment = $result[0]; $this->assertEqual($comment['nid'], $this->node->nid, 'Correct nid get.'); $this->assertEqual($comment['label'], $label, 'Correct subject get.'); } /** * Test updating a comment (PUT method). */ public function testUpdateCommentAsPut() { $resource_manager = restful()->getResourceManager(); $label = $this->randomName(); $new_label = $this->randomName(); $text = $this->randomName(); $settings = array( 'node_type' => 'comment_node_article', 'nid' => $this->node->nid, 'uid' => $this->account->uid, 'subject' => $label, ); $settings['comment_text'][LANGUAGE_NONE][0]['value'] = $text; $comment = $this->createComment($settings); $id = $comment->cid; $handler = $resource_manager->getPlugin('comments:1.0'); $handler->setAccount($this->account); $request = array('label' => $new_label); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doPut($id, $request), 'json')); $result = $result['data']; $expected_result = array( array( 'id' => $id, 'label' => $new_label, 'self' => $handler->versionedUrl($id), 'nid' => $this->node->nid, 'comment_text' => NULL, ), ); $result[0]['comment_text'] = trim(strip_tags($result[0]['comment_text'])); $this->assertEqual($result, $expected_result); } /** * Test updating a comment (PATCH method). */ public function testUpdateCommentAsPatch() { $resource_manager = restful()->getResourceManager(); $label = $this->randomName(); $new_label = $this->randomName(); $text = $this->randomName(); $settings = array( 'node_type' => 'comment_node_article', 'nid' => $this->node->nid, 'uid' => $this->account->uid, 'subject' => $label, ); $settings['comment_text'][LANGUAGE_NONE][0]['value'] = $text; $comment = $this->createComment($settings); $id = $comment->cid; $handler = $resource_manager->getPlugin('comments:1.0'); $handler->setAccount($this->account); $request = array('label' => $new_label); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doPatch($id, $request), 'json')); $result = $result['data']; $expected_result = array( array( 'id' => $id, 'label' => $new_label, 'self' => $handler->versionedUrl($id), 'nid' => $this->node->nid, 'comment_text' => $text, ), ); $result[0]['comment_text'] = trim(strip_tags($result[0]['comment_text'])); $this->assertEqual($result, $expected_result); } /** * Test deleting a comment (DELETE method). */ public function testDeleteComment() { $resource_manager = restful()->getResourceManager(); $label = $this->randomName(); $text = $this->randomName(); $settings = array( 'node_type' => 'comment_node_article', 'nid' => $this->node->nid, 'uid' => $this->account->uid, 'subject' => $label, ); $settings['comment_text'][LANGUAGE_NONE][0]['value'] = $text; $comment = $this->createComment($settings); $id = $comment->cid; $handler = $resource_manager->getPlugin('comments:1.0'); // Set a different user from the comment user, as only the administer // can delete comments. $user = $this->drupalCreateUser(array('administer comments')); $handler->setAccount($user); $handler->doDelete($id); $result = !comment_load($id); $this->assertTrue($result, 'Comment deleted.'); } /** * Adds a text field. * * @param string $entity_type * The entity type. * @param string $bundle. * The bundle name. * @param string $field_name. * The field name. */ protected function addTextField($entity_type, $bundle, $field_name) { // Text - single, with text processing. $field = array( 'field_name' => $field_name, 'type' => 'text_long', 'entity_types' => array($entity_type), 'cardinality' => 1, ); field_create_field($field); $instance = array( 'field_name' => $field_name, 'bundle' => $bundle, 'entity_type' => $entity_type, 'label' => t('Text single with text processing'), 'settings' => array( 'text_processing' => 1, ), ); field_create_instance($instance); } /** * Creates a comment based on default settings. * * @param array $settings * An associative array of settings to change from the defaults, keys are * comment properties, for example 'subject' => 'Hello, world!'. * @return object * Created comment object. */ protected function createComment($settings = array()) { // Populate defaults array. $settings += array( 'cid' => FALSE, 'pid' => 0, 'subject' => $this->randomName(8), 'status' => COMMENT_PUBLISHED, 'node_type' => 'comment_node_article', 'language' => LANGUAGE_NONE, 'comment_text' => array(LANGUAGE_NONE => array(array())), ); // If the comment's user uid is not specified manually, use the currently // logged in user if available, or else the user running the test. if (!isset($settings['uid'])) { if ($this->loggedInUser) { $settings['uid'] = $this->loggedInUser->uid; } else { global $user; $settings['uid'] = $user->uid; } } // Merge comment text field value and format separately. $body = array( 'value' => $this->randomName(32), 'format' => filter_default_format(), ); $settings['comment_text'][$settings['language']][0] += $body; $comment = (object) $settings; comment_save($comment); return $comment; } } ================================================ FILE: tests/RestfulCreateEntityTestCase.test ================================================ 'Create entity', 'description' => 'Test the creation of an entity.', 'group' => 'RESTful', ); } /** * @var string * * Holds the path to a random generated image for upload purposes. */ private $imagePath; /** * @var \stdClass * * Holds the created account. */ protected $account; /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_test', 'entityreference'); // Add common fields, vocabulary and terms. restful_test_add_fields(); $images = $this->drupalGetTestFiles('image'); $image = reset($images); $this->imagePath = drupal_realpath($image->uri); $this->account = $this->drupalCreateUser(); } /** * Test creating an entity (POST method). */ public function testCreateEntity() { $resource_manager = restful()->getResourceManager(); // Create test entities to be referenced. $ids = array(); for ($i = 0; $i < 2; $i++) { /* @var \Entity $entity */ $entity = entity_create('entity_test', array('name' => 'main')); $entity->save(); $ids[] = $entity->pid; } $images = array(); foreach ($this->drupalGetTestFiles('image') as $file) { $file = file_save($file); $images[] = $file->fid; } $resource_manager->clearPluginCache('main:1.1'); $handler = $resource_manager->getPlugin('main:1.1'); $query = new EntityFieldQuery(); $result = $query ->entityCondition('entity_type', 'taxonomy_term') ->entityCondition('bundle', 'test_vocab') ->execute(); $tids = array_keys($result['taxonomy_term']); $text1 = $this->randomName(); $text2 = $this->randomName(); $request = array( 'text_single' => $text1, 'text_multiple' => array($text1, $text2), 'text_single_processing' => $text1, 'text_multiple_processing' => array($text1, $text2), 'entity_reference_single' => $ids[0], 'entity_reference_multiple' => $ids, 'term_single' => $tids[0], 'term_multiple' => array($tids[0], $tids[1]), 'file_single' => $images[0], 'file_multiple' => array($images[0], $images[1]), 'image_single' => $images[0], 'image_multiple' => array($images[0], $images[1]), ); $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doPost($request))); $response = $response['data']; $result = $response[0]; $text_single = trim(strip_tags($result['text_single'])); $text_multiple = array( trim(strip_tags($result['text_multiple'][0])), trim(strip_tags($result['text_multiple'][1])), ); $expected_result = $request; // Strip some elements, and the text, for easier assertion. $striped_result = $result; unset($striped_result['id']); unset($striped_result['label']); unset($striped_result['self']); unset($striped_result['entity_reference_single_resource']); unset($striped_result['entity_reference_multiple_resource']); $striped_result['text_single'] = $text_single; $striped_result['text_multiple'] = $text_multiple; $striped_result['text_single_processing'] = $text_single; $striped_result['text_multiple_processing'] = $text_multiple; ksort($striped_result); ksort($expected_result); $this->assertEqual($expected_result, $striped_result, 'Entity was created with correct values.'); $this->assertEqual($result['entity_reference_single_resource']['id'], $ids[0], ' Entity reference single resource was created correctly'); $this->assertEqual($result['entity_reference_multiple_resource'][0]['id'], $ids[0], ' Entity reference multiple resource was created correctly'); // Create an entity with empty request. try { $handler->setRequest(Request::create('', array(), RequestInterface::METHOD_POST)); $handler->setPath(''); restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('User can create an entity with empty request.'); } catch (BadRequestException $e) { $this->pass('User cannot create an entity with empty request.'); } // Create an entity with invalid property name. $request['invalid'] = 'wrong'; try { $handler->setRequest(Request::create('', array(), RequestInterface::METHOD_POST, NULL, FALSE, NULL, array(), array(), array(), $request)); $handler->setPath(''); restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('User can create an entity with invalid property name.'); } catch (BadRequestException $e) { $this->pass('User cannot create an entity with invalid property name.'); } // Create entity with comma separated multiple entity reference. $request = array('entity_reference_multiple' => implode(',', $ids)); $handler->setRequest(Request::create('', array(), RequestInterface::METHOD_POST, NULL, FALSE, NULL, array(), array(), array(), $request)); $handler->setPath(''); $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $response = $response['data']; $result = $response[0]; $this->assertEqual($result['entity_reference_multiple'], $ids, 'Created entity with comma separated multiple entity reference.'); // Create entity with comma separated multiple taxonomy term reference. $ids = array($tids[0], $tids[1]); $request = array('term_multiple' => implode(',', $ids)); $handler->setRequest(Request::create('', array(), RequestInterface::METHOD_POST, NULL, FALSE, NULL, array(), array(), array(), $request)); $handler->setPath(''); $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $response = $response['data']; $result = $response[0]; $this->assertEqual($result['term_multiple'], $ids, 'Created entity with comma separated multiple taxonomy term reference.'); // Create entity with comma separated multiple file reference. $ids = array($images[0], $images[1]); $request = array('file_multiple' => implode(',', $ids)); $handler->setRequest(Request::create('', array(), RequestInterface::METHOD_POST, NULL, FALSE, NULL, array(), array(), array(), $request)); $handler->setPath(''); $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $response = $response['data']; $result = $response[0]; $this->assertEqual($result['file_multiple'], $ids, 'Created entity with comma separated multiple file reference.'); // Create entity with comma separated multiple image reference. $ids = array($images[0], $images[1]); $request = array('image_multiple' => implode(',', $ids)); $handler->setRequest(Request::create('', array(), RequestInterface::METHOD_POST, NULL, FALSE, NULL, array(), array(), array(), $request)); $handler->setPath(''); $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $response = $response['data']; $result = $response[0]; $this->assertEqual($result['image_multiple'], $ids, 'Created entity with comma separated multiple image reference.'); } /** * Test access for file upload. */ public function testFileUploadAccess() { variable_set('restful_file_upload', TRUE); variable_set('restful_file_upload_allow_anonymous_user', TRUE); // Make sure the user is logged out even when using the UI tests. $this->drupalLogout(); // Test access for anonymous user (allowed). $handler = $this->fileResource(); $this->assertTrue($handler->access(), 'File upload is allowed to anonymous users.'); variable_set('restful_file_upload_allow_anonymous_user', FALSE); // Now that we have a successfully uploaded file, make sure it's the same // file that was uploaded. $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process())); $original = hash_file('md5', $this->imagePath); $file = file_load($response['data'][0][0]['id']); $uploaded = hash_file('md5', file_create_url($file->uri)); $this->assertEqual($original, $uploaded, 'Original and uploaded file are identical.'); // Test access for anonymous user (denied). $handler = $this->fileResource(); $this->assertFalse($handler->access(), 'File upload is denied to anonymous users.'); $this->drupalLogin($this->account); // Test access for authenticated users (allowed). $handler = $this->fileResource(); $this->assertTrue($handler->access(), 'File upload is allowed to authenticated users.'); // Test access for authenticated users (denied). variable_set('restful_file_upload', FALSE); try { $this->fileResource(); $this->fail('The file upload endpoint is not disalbed.'); } catch (\Drupal\restful\Exception\ServiceUnavailableException $e) { $this->pass('The file upload endpoint is disalbed.'); } } /** * Uploads a file issuing a POST HTTP request. * * @return \Drupal\restful\Plugin\resource\ResourceInterface * The file resource. * * @throws \Drupal\restful\Exception\BadRequestException * @throws \Drupal\restful\Exception\ServiceUnavailableException */ protected function fileResource() { // We are setting the $_FILES array directly on tests, so we need to // override the is_uploaded_file PHP method by setting a custom variable. // Otherwise the manually set file will not be detected. variable_set('restful_insecure_uploaded_flag', TRUE); // Clear the plugin definition cache to regenerate authenticationOptional // based on the variable value. $resource_manager = restful()->getResourceManager(); $resource_manager->clearPluginCache('files_upload_test:1.0'); if (!$handler = $resource_manager->getPlugin('files_upload_test:1.0')) { throw new \Drupal\restful\Exception\ServiceUnavailableException(); } $plugin_definition = $handler->getPluginDefinition(); $plugin_definition['authenticationOptional'] = variable_get('restful_file_upload_allow_anonymous_user', FALSE); $handler->setPluginDefinition($plugin_definition); $account = $this->account; if (!$this->loggedInUser) { $account = drupal_anonymous_user(); } $handler->setAccount($account); // Due to entity_metadata_file_access we need to set the global user to the // user for the test. $GLOBALS['user'] = $account; $value = '@' . $this->imagePath; // PHP 5.5 introduced a CurlFile object that deprecates the old @filename // syntax. See: https://wiki.php.net/rfc/curl-file-upload if (function_exists('curl_file_create')) { $value = curl_file_create($this->imagePath); } $tmp_name = drupal_tempnam('/tmp', 'restful_test_'); file_put_contents($tmp_name, file_get_contents($this->imagePath)); $headers = new \Drupal\restful\Http\HttpHeaderBag(array('Content-Type' => 'multipart/form-data')); // Mock the $_FILES global variable. $files = array( 'my-filename' => array( 'error' => 0, 'name' => basename($this->imagePath), 'size' => filesize($this->imagePath), 'tmp_name' => $tmp_name, 'type' => 'image/png', ), ); $handler->setRequest(Request::create('api/file-upload', array(), RequestInterface::METHOD_POST, $headers, FALSE, NULL, array(), $files, array(), array('filename' => $value))); $handler->setPath(''); return $handler; } } ================================================ FILE: tests/RestfulCreateNodeTestCase.test ================================================ 'Node integration', 'description' => 'Test the creation of a node entity type.', 'group' => 'RESTful', ); } public function setUp() { parent::setUp('restful_example'); } /** * Test creating a node (POST method). */ public function testCreateNode() { $resource_manager = restful()->getResourceManager(); $user1 = $this->drupalCreateUser(); $this->drupalLogin($user1); $handler = $resource_manager->getPlugin('articles:1.1'); // Set a different user from the logged in user, to assert the node's author // is set correctly. $user2 = $this->drupalCreateUser(array('create article content')); $handler->setAccount($user2); $text1 = $this->randomName(); $request = array('label' => $text1); $handler->setRequest(Request::create('', array(), RequestInterface::METHOD_POST, NULL, FALSE, NULL, array(), array(), array(), $request)); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $node = node_load($result[0]['id']); $this->assertEqual($node->uid, $user2->uid, 'Correct user was set to be the author of the node.'); $this->assertEqual($node->title, $text1, 'Correct title set.'); } } ================================================ FILE: tests/RestfulCreatePrivateNodeTestCase.test ================================================ 'Node access integration', 'description' => 'Test the creation of a node entity type with private access.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_example', 'restful_node_access_test'); // Add entity reference fields. restful_test_add_fields('node', 'article'); // Rebuild node access. node_access_rebuild(); } /** * Test creating a node (POST method) with reference to existing private node. * * In this test we make sure that entity_metadata_no_hook_node_access() * returns TRUE, thus allows access to set the entity reference property. * * @see restful_node_access_test_node_access_records() */ public function testCreateNodeWithReference() { $user1 = $this->drupalCreateUser(array('create article content')); $user2 = $this->drupalCreateUser(array('create article content')); $this->drupalLogin($user1); $settings = array( 'type' => 'article', 'uid' => $user1->uid, ); // Create a node that will be set to private. $node1 = $this->drupalCreateNode($settings); // Assert user has access to the node. $this->assertTrue(node_access('view', $node1, $user1), 'Author has access to view node.'); // Assert another user doesn't have access to the node. $this->assertFalse(node_access('view', $node1, $user2), 'Authenticated user, but not author does not have access to view the node.'); $handler = restful()->getResourceManager()->getPlugin('test_articles:1.2'); $formatter = restful()->getFormatterManager()->getPlugin('json'); $formatter->setResource($handler); $handler->setAccount($user1); $parsed_body = array( 'label' => $this->randomName(), 'entity_reference_single' => $node1->nid, ); $result = $formatter->prepare($handler->doPost($parsed_body)); $this->assertTrue($result, 'Private node with reference to another private node was created.'); } } ================================================ FILE: tests/RestfulCreateTaxonomyTermTestCase.test ================================================ 'Taxonomy term integration', 'description' => 'Test the creation of a taxonomy term entity type.', 'group' => 'RESTful', ); } public function setUp() { parent::setUp('restful_test'); } /** * Test the creation of a taxonomy term entity type. */ public function testCreate() { $resource_manager = restful()->getResourceManager(); $user1 = $this->drupalCreateUser(array('create article content')); $this->drupalLogin($user1); $handler = $resource_manager->getPlugin('test_tags:1.0'); $handler->setAccount($user1); $text1 = $this->randomName(); $request = array('label' => $text1); $handler->setRequest(Request::create('', array(), RequestInterface::METHOD_POST, NULL, FALSE, NULL, array(), array(), array(), $request)); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $id = $result[0]['id']; $this->assertTrue($id, 'Term was created by a non-admin user.'); } } ================================================ FILE: tests/RestfulCsrfTokenTestCase.test ================================================ 'CSRF token', 'description' => 'Test the validation of a CSRF token for write operations.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_example'); } /** * Test the validation of a CSRF token for authenticated users. */ public function testCsrfToken() { global $user; $permissions = array( 'create article content', 'edit any article content', 'delete any article content', ); $account = $this->drupalCreateUser($permissions); $this->drupalLogin($account); $user = $account; // Check CSRF is not checked for read operations. $this->checkCsrfRequest(array(RequestInterface::METHOD_GET), FALSE); $this->checkCsrfRequest($this->writeOperations, TRUE); } /** * Test the validation of a CSRF token for anonymous users. */ public function testCsrfTokenAnon() { global $user; $user = drupal_anonymous_user(); // Allow anonymous user to CRUD article content. $permissions = array( 'create article content' => TRUE, 'edit any article content' => TRUE, 'delete any article content' => TRUE, ); user_role_change_permissions(DRUPAL_ANONYMOUS_RID, $permissions); $this->checkCsrfRequest($this->writeOperations, FALSE, FALSE); } /** * Perform requests without, with invalid and with valid CSRF tokens. * * @param array $methods * Array with HTTP method names. * @param bool $csrf_required * Indicate if CSRF is required for the request, thus errors would be set if * no CSRF or invalid one is sent with the request. * @param bool $auth_user * Determine if a user should be created and logged in. Defaults to TRUE. */ protected function checkCsrfRequest($methods = array(), $csrf_required, $auth_user = TRUE) { $params['@role'] = $auth_user ? 'authenticated' : 'anonymous'; foreach ($methods as $method) { $request = Request::isReadMethod($method) ? array() : array('label' => $this->randomName()); $params['@method'] = $method; // No CSRF token. $result = $this->httpRequest($this->getPath($method), $method, $request, array( 'Content-Type' => 'application/x-www-form-urlencoded', ), FALSE); if ($csrf_required) { $params['@code'] = 400; $this->assertEqual($result['code'], $params['@code'], format_string('@code on @method without CSRF token for @role user.', $params)); } else { $this->assertTrue($result['code'] >= 200 && $result['code'] <= 204, format_string('@method without CSRF token for @role user is allowed.', $params)); } // Invalid CSRF token. $result = $this->httpRequest($this->getPath($method), $method, $request, array( 'Content-Type' => 'application/x-www-form-urlencoded', 'X-CSRF-Token' => 'invalidToken', ), FALSE); if ($csrf_required) { $params['@code'] = 403; $this->assertEqual($result['code'], $params['@code'], format_string('@code on @method with invalid CSRF token for @role user.', $params)); } else { $this->assertTrue($result['code'] >= 200 && $result['code'] <= 204, format_string('@method with invalid CSRF token for @role user is allowed.', $params)); } // Valid CSRF token. $result = $this->httpRequest($this->getPath($method), $method, $request, array( 'Content-Type' => 'application/x-www-form-urlencoded', )); $this->assertTrue($result['code'] >= 200 && $result['code'] <= 204, format_string('@method allowed with CSRF token for @role user.', $params)); } } /** * Get the path for the request. * * For non "POST" methods, a new node is created. * * @param $method * The HTTP method. * * @return string * The path for the request. */ protected function getPath($method) { if ($method == RequestInterface::METHOD_POST) { return 'api/v1.0/articles'; } $settings = array( 'type' => 'article', 'title' => $this->randomString(), ); $node = $this->drupalCreateNode($settings); return 'api/v1.0/articles/' . $node->nid; } } ================================================ FILE: tests/RestfulCurlBaseTestCase.test ================================================ TRUE, 'query' => $body) : array('absolute' => TRUE); $curl_options = array( CURLOPT_HTTPGET => TRUE, CURLOPT_URL => url($url, $options), CURLOPT_NOBODY => FALSE, ); break; case RequestInterface::METHOD_HEAD: case RequestInterface::METHOD_OPTIONS: // Set query if there are addition GET parameters. $options = isset($body) ? array('absolute' => TRUE, 'query' => $body) : array('absolute' => TRUE); $curl_options = array( CURLOPT_HTTPGET => FALSE, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_URL => url($url, $options), CURLOPT_NOBODY => FALSE, ); break; case RequestInterface::METHOD_POST: $curl_options = array( CURLOPT_HTTPGET => FALSE, CURLOPT_POST => TRUE, CURLOPT_POSTFIELDS => is_array($body) ? http_build_query($body) : $body, CURLOPT_URL => url($url, array('absolute' => TRUE)), CURLOPT_NOBODY => FALSE, ); if (empty($headers['Content-Type'])) { $headers['Content-Type'] = 'application/' . $format; } break; case RequestInterface::METHOD_PUT: case RequestInterface::METHOD_PATCH: $curl_options = array( CURLOPT_HTTPGET => FALSE, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_POSTFIELDS => is_array($body) ? http_build_query($body) : $body, CURLOPT_URL => url($url, array('absolute' => TRUE)), CURLOPT_NOBODY => FALSE, ); if (empty($headers['Content-Type'])) { $headers['Content-Type'] = 'application/' . $format; } break; case RequestInterface::METHOD_DELETE: $curl_options = array( CURLOPT_HTTPGET => FALSE, CURLOPT_CUSTOMREQUEST => RequestInterface::METHOD_DELETE, CURLOPT_URL => url($url, array('absolute' => TRUE)), CURLOPT_NOBODY => FALSE, ); break; } $curl_options += array(CURLOPT_HTTPHEADER => array()); if (Request::isWriteMethod($method) && $use_token) { // Add CSRF token for write operations. if (empty($this->csrfToken)) { $response = $this->drupalGet('api/session/token'); $result = drupal_json_decode($response); if (!empty($result['X-CSRF-Token'])) { $this->csrfToken = $result['X-CSRF-Token']; $headers['X-CSRF-Token'] = $result['X-CSRF-Token']; } } else { $headers['X-CSRF-Token'] = $this->csrfToken; } } foreach ($headers as $key => $value) { $curl_options[CURLOPT_HTTPHEADER][] = $key . ': ' . $value; } $response = $this->curlExec($curl_options); $response_headers = array(); foreach ($this->drupalGetHeaders() as $header_name => $header_value) { $response_headers[] = $header_name . ': ' . $header_value; } $response_headers = implode("\n", $response_headers); $code = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE); $this->verbose($method . ' request to: ' . $url . '
Code: ' . $code . '
Response headers: ' . $response_headers . '
Response body: ' . $response); return array( 'code' => $code, 'headers' => $response_headers, 'body' => $response, ); } } ================================================ FILE: tests/RestfulDataProviderPlugPluginsTestCase.test ================================================ 'Plug plugins', 'description' => 'Test the Plug plugins data provider.', 'group' => 'RESTful', ); } public function setUp() { parent::setUp('restful_example'); } /** * Test the data provider. */ public function testDataProvider() { $resource_manager = restful()->getResourceManager(); $handler = $resource_manager->getPlugin('discovery:1.0'); // Assert sorting and filtering works as expected. $request = array( // Get all resources. 'all' => TRUE, 'sort' => '-name', 'filter' => array( 'minorVersion' => array( 'value' => '4', 'operator' => '>=', ), ), ); $handler->setRequest(Request::create('', $request)); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual(count($result), 4, 'Discovery filtered resources correctly.'); $this->assertTrue($result[0]['name'] = 'articles_1_7' && $result[1]['name'] = 'articles_1_6' && $result[2]['name'] = 'articles_1_5' && $result[2]['name'] = 'articles_1_4', 'Discovery sorted resources correctly.'); // Assert sorting and filtering works as expected. $request = array( // Get all resources. 'filter' => array( 'resource' => array( 'value' => 'articles', 'operator' => '=', ), ), ); $handler->setRequest(Request::create('', $request)); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual(count($result), 1, 'Latest resources shown by default.'); $request = array( // Get all resources. 'filter' => array( 'resource' => array( 'value' => 'articles', 'operator' => '=', ), ), 'all' => 1, ); $handler->setRequest(Request::create('', $request)); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertTrue(count($result) > 1, 'All resources shown by passing the "all" query string.'); // Assert sorting and filtering works as expected. $request = array( // Get all resources. 'filter' => array( 'resource' => array( 'value' => 'discovery', 'operator' => '=', ), ), ); $handler->setRequest(Request::create('', $request)); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual(count($result), 0, 'Resources without discovery are ignored.'); // Make sure that unauthorized users cannot enable/disable resources via the // API. $handler->setAccount(drupal_anonymous_user()); $handler->setRequest(Request::create('api/v1.0/discovery/articles:1.0', array(), RequestInterface::METHOD_DELETE)); $handler->setPath('articles:1.0'); try { restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('Un-privileged user can disable endpoints.'); } catch (ForbiddenException $e) { $this->pass('Un-privileged user cannot disable endpoints.'); } $handler->setRequest(Request::create('api/v1.0/discovery/articles:1.0', array(), RequestInterface::METHOD_PUT, NULL, FALSE, NULL, array(), array(), array(), array('enable' => 1))); $handler->setPath('articles:1.0'); try { restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('Un-privileged user can enable endpoints.'); } catch (ForbiddenException $e) { $this->pass('Un-privileged user cannot enable endpoints.'); } // Make sure that authorized users can enable/disable resources via the API. $account = $this->drupalCreateUser(array('administer restful resources')); $handler->setAccount($account); $handler->setRequest(Request::create('api/v1.0/discovery/articles:1.0', array(), RequestInterface::METHOD_DELETE)); $handler->setPath('articles:1.0'); $handler->process(); $resource = $resource_manager->getPlugin('articles:1.0'); $this->assertNull($resource, 'Plugin disabled via the API.'); $handler->setRequest(Request::create('api/v1.0/discovery/articles:1.0', array(), RequestInterface::METHOD_PUT, NULL, FALSE, NULL, array(), array(), array(), array('enable' => 1))); $handler->setPath('articles:1.0'); $handler->process(); $resource = $resource_manager->getPlugin('articles:1.0'); $this->assertTrue($resource->isEnabled(), 'Plugin enabled via the API.'); } } ================================================ FILE: tests/RestfulDbQueryTestCase.test ================================================ 'DB Query', 'description' => 'Test the DB Query data provider.', 'group' => 'RESTful', ); } /** * Operations before the testing begins. */ public function setUp() { parent::setUp('restful_test'); } /** * Test authenticating a user. */ public function testCrudOperations() { $resource_manager = restful()->getResourceManager(); $random_int = intval(mt_rand(1, 100)); $random_string = $this->randomName(); $random_serialized = serialize(array( 'key1' => $random_int, 'key2' => $random_string, )); // Populate the table with some values. $mock_data = array( 'str_field' => $random_string, 'int_field' => $random_int, 'serialized_field' => $random_serialized, ); $id = db_insert($this->tableName) ->fields($mock_data) ->execute(); $id = intval($id); $this->assertTrue(!empty($id), 'The manual record could be inserted'); // Get the handler. $handler = $resource_manager->getPlugin('db_query_test:1.0'); // Testing read context. $handler->setRequest(Request::create('api/db_query_test/v1.0/' . $id)); $handler->setPath($id); $result = $handler->process(); /* @var \Drupal\restful\Plugin\resource\Field\ResourceFieldCollection $collection */ $collection = $result[0]; $this->assertEqual($collection->get('string') ->value($collection->getInterpreter()), $mock_data['str_field'], 'The record was retrieved successfully.'); $this->assertEqual($collection->get('integer') ->value($collection->getInterpreter()), $mock_data['int_field'], 'The record was retrieved successfully.'); $this->assertEqual($collection->get('serialized') ->value($collection->getInterpreter()), $mock_data['serialized_field'], 'The record was retrieved successfully.'); // Testing JSON API formatter. $formatter_manager = restful()->getFormatterManager(); $formatter_manager->setResource($handler); $result = drupal_json_decode($formatter_manager->format($handler->process(), 'json_api')); $this->assertEqual($result['data']['attributes']['string'], $mock_data['str_field'], 'The record was retrieved successfully.'); $this->assertEqual($result['data']['attributes']['integer'], $mock_data['int_field'], 'The record was retrieved successfully.'); $this->assertEqual($result['data']['attributes']['serialized'], $mock_data['serialized_field'], 'The record was retrieved successfully.'); // Testing update context. $mock_data2 = array( 'string' => $this->randomName(), ); $handler->setRequest(Request::create('api/db_query_test/v1.0/' . $id, array(), RequestInterface::METHOD_PATCH, NULL, FALSE, NULL, array(), array(), array(), $mock_data2)); $handler->setPath($id); $handler->process(); // Now get the updated object. $handler->setRequest(Request::create('api/db_query_test/v1.0/' . $id)); $handler->setPath($id); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $expected = array( // ID should be unchanged. 'id' => $id, // String should be the string that we updated. 'string' => $mock_data2['string'], // Serialized value should be unchanged. 'serialized' => $random_serialized, // Integer value should be unchanged. 'integer' => $random_int, ); // We expect that only the string field has changed. $this->assertEqual($result['data'][0], $expected, 'The record was updated with PUT successfully.'); // Testing replace context. $mock_data3 = array( 'string' => $this->randomName(), ); $handler->setRequest(Request::create('api/db_query_test/v1.0/' . $id, array(), RequestInterface::METHOD_PUT, NULL, FALSE, NULL, array(), array(), array(), $mock_data3)); $handler->setPath($id); $handler->process(); // Now get the updated object. $handler->setRequest(Request::create('api/db_query_test/v1.0/' . $id)); $handler->setPath($id); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $expected = array( // ID should be unchanged. 'id' => $id, // String should be the string that we PUT. 'string' => $mock_data3['string'], // Serialized field should be null. 'serialized' => 'N;', // Integer field should be default value from schema. 'integer' => 0, ); // We expect that only the supplied fields are present. $this->assertEqual($result['data'][0], $expected, 'The record was updated with PATCH successfully.'); // Testing delete context. $handler->setRequest(Request::create('api/db_query_test/v1.0/' . $id, array(), RequestInterface::METHOD_DELETE)); $handler->setPath($id); $handler->process(); $count = db_select($this->tableName) ->countQuery() ->execute() ->fetchField(); $this->assertEqual($count, 0, 'The record was deleted successfully.'); // Testing create context. $mock_data4 = array( 'string' => $random_string, 'integer' => $random_int, 'serialized' => array( 'key1' => $random_int, 'key2' => $random_string, ), ); $handler->setRequest(Request::create('api/db_query_test/v1.0', array(), RequestInterface::METHOD_POST, NULL, FALSE, NULL, array(), array(), array(), $mock_data4)); $handler->setPath(''); $handler->process(); $count = db_select($this->tableName) ->countQuery() ->execute() ->fetchField(); $this->assertEqual($count, 1, 'The record was created.'); // Testing listing for read context. $handler->setRequest(Request::create('api/db_query_test/v1.0')); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); // The created record should match our input. $expected = $mock_data4; // Account for serialization. $expected['serialized'] = $random_serialized; // Account for not knowing the ID of the new entity beforehand. unset($result['data'][0]['id']); $this->assertEqual($result['data'][0], $expected, 'All the content listed successfully.'); // Testing filters. $mock_data5 = array( 'str_field' => $this->randomName(), 'int_field' => 101, ); $mock_data5['serialized_field'] = serialize($mock_data5); db_insert($this->tableName) ->fields($mock_data5) ->execute(); $mock_data6 = array( 'str_field' => $this->randomName(), 'int_field' => 102, ); $mock_data6['serialized_field'] = serialize($mock_data6); db_insert($this->tableName) ->fields($mock_data6) ->execute(); $parsed_input = array( 'filter' => array( 'integer' => array( 'value' => array(101, 102), 'conjunction' => 'OR', ), ), ); $handler->setRequest(Request::create('api/db_query_test/v1.0', $parsed_input)); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $this->assertEqual(count($result['data']), 2); $parsed_input = array( 'filter' => array( 'integer' => array( 'value' => array(101, 102), 'operator' => array('=', '>='), 'conjunction' => 'OR', ), ), ); $handler->setRequest(Request::create('api/db_query_test/v1.0', $parsed_input)); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $this->assertEqual(count($result['data']), 2); } /** * Test the render cache. */ public function __testRenderCache() { $resource_manager = restful()->getResourceManager(); $account = $this->drupalCreateUser(); // Populate the table with some values. $mock_data = array( 'str_field' => $this->randomName(), 'int_field' => intval(mt_rand(1, 100)), ); $mock_data['serialized_field'] = serialize($mock_data); $id = db_insert($this->tableName) ->fields($mock_data) ->execute(); $id = intval($id); // Get the handler. /* @var \Drupal\restful\Plugin\resource\Decorators\CacheDecoratedResourceInterface $handler */ $handler = $resource_manager->getPlugin('db_query_test:1.0'); $handler->setAccount($account); $cache = $handler->getCacheController(); // Populate the cache entry. $handler->setRequest(Request::create('api/db_query_test/v1.0/' . $id)); $handler->setPath($id); $handler->process(); $version = $handler->getVersion(); $cid = 'v' . $version['major'] . '.' . $version['minor'] . '::db_query_test::uu' . $account->uid . '::patb:restful_test_db_query::cl:id::id:' . $id; $cache_data = $cache->get($cid); $this->assertNotNull($cache_data->data, 'Cache data is present.'); $this->assertEqual($cache_data->data[0]['string'], $mock_data['str_field'], 'The record was retrieved successfully.'); $this->assertEqual($cache_data->data[0]['integer'], $mock_data['int_field'], 'The record was retrieved successfully.'); $this->assertEqual($cache_data->data[0]['serialized'], $mock_data['serialized_field'], 'The record was retrieved successfully.'); } } ================================================ FILE: tests/RestfulDiscoveryTestCase.test ================================================ 'Discovery', 'description' => 'Test the discovery features.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_test'); } /** * Test the discovery page. */ public function testDiscoveryPage() { $request = $this->httpRequest('api'); $body = drupal_json_decode($request['body']); $this->assertTrue(!empty($body['self']), 'The discovery plugin exposed the plugins.'); // Disable the plugin and test a 404. variable_set('restful_enable_discovery_resource', FALSE); $request = $this->httpRequest('api'); $body = drupal_json_decode($request['body']); $this->assertEqual($body['status'], 404, 'The discovery plugin is disabled.'); variable_set('restful_enable_discovery_resource', TRUE); } /** * Test the headers populated in an OPTIONS request. */ public function testOptionsMethod() { // 1. Assert Access-Control-Allow-Methods. $response = $this->httpRequest('api/v1.4/test_articles', RequestInterface::METHOD_OPTIONS); $this->assertTrue(strpos($response['headers'], sprintf('access-control-allow-methods: %s', implode(', ', array( RequestInterface::METHOD_HEAD, RequestInterface::METHOD_OPTIONS, )))) !== FALSE, 'Access-Control-Allow-Methods header is populated correctly.'); // Make sure it returns the appropriate headers for every path. $response = $this->httpRequest('api/v1.4/test_articles/1', RequestInterface::METHOD_OPTIONS); $this->assertTrue(strpos($response['headers'], sprintf('access-control-allow-methods: %s', implode(', ', array( RequestInterface::METHOD_PATCH, RequestInterface::METHOD_DELETE, RequestInterface::METHOD_OPTIONS, )))) !== FALSE, 'Access-Control-Allow-Methods header is populated correctly for different paths.'); // 2. Assert Accept. // List the content types the route accepts. $this->assertTrue(strpos($response['headers'], 'accept: application/xml; charset=utf-8') !== FALSE, 'Accept header is populated correctly for configured formatter.'); $response = $this->httpRequest('api/v1.2/test_articles', RequestInterface::METHOD_OPTIONS); $this->assertTrue(strpos($response['headers'], 'application/hal+json') !== FALSE, 'Accept header is populated correctly for non configured formatters.'); $this->assertTrue(strpos($response['headers'], 'application/json') !== FALSE, 'Accept header is populated correctly for non configured formatters.'); $this->assertTrue(strpos($response['headers'], 'application/vnd.api+json') !== FALSE, 'Accept header is populated correctly for non configured formatters.'); $this->assertTrue(strpos($response['headers'], 'application/drupal.single+json') !== FALSE, 'Accept header is populated correctly for non configured formatters.'); $this->assertTrue(strpos($response['headers'], 'application/xml') !== FALSE, 'Accept header is populated correctly for non configured formatters.'); // 3. Assert Access-Control-Allow-Origin. $response = $this->httpRequest('api/v1.4/test_articles', RequestInterface::METHOD_HEAD); $this->assertTrue(strpos($response['headers'], 'access-control-allow-origin: *') !== FALSE, 'Accept header is populated correctly for non configured formatters.'); // 4. Assert Access. $response = $this->httpRequest('api/v1.4/test_articles/1', RequestInterface::METHOD_HEAD); $this->assertTrue($response['code'], 400, 'Access is denied for unsupported HTTP methods.'); } /** * Field discovery. */ public function testFieldDiscovery() { // Add common fields, vocabulary and terms. restful_test_add_fields(); // Create an entity. $entity = entity_create('entity_test', array('name' => 'main', 'label' => $this->randomName())); $pid = $entity->save(); $handler = restful()->getResourceManager()->getPlugin('main:1.1'); $handler->setRequest(Request::create('api/v1.1/main/' . $pid, array(), RequestInterface::METHOD_OPTIONS)); $handler->setPath($pid); $formatter = restful() ->getFormatterManager() ->negotiateFormatter(NULL); $formatter->setResource($handler); $result = $formatter->prepare($handler->process()); $expected = array( 'id' => array( 'data' => array( 'cardinality' => 1, 'read_only' => TRUE, 'type' => 'integer', 'required' => TRUE, ), 'info' => array( 'description' => t('Base ID for the entity.'), 'label' => t('ID'), ), ), 'label' => array( 'data' => array( 'cardinality' => 1, 'read_only' => FALSE, 'type' => 'string', 'required' => FALSE, 'size' => 255, ), 'form_element' => array( 'allowed_values' => NULL, 'type' => 'textfield', 'default_value' => '', 'placeholder' => '', 'size' => 255, 'description' => 'The label of the resource.', 'title' => 'label', ), 'info' => array( 'description' => t('The label of the resource.'), 'label' => t('Label'), ), ), 'text_multiple' => array( 'data' => array( 'cardinality' => FIELD_CARDINALITY_UNLIMITED, 'read_only' => FALSE, 'type' => 'string', 'size' => 255, 'required' => FALSE, ), 'form_element' => array( 'allowed_values' => NULL, 'default_value' => '', 'placeholder' => t('This is helpful.'), 'size' => 255, 'type' => 'textfield', 'description' => 'This field holds different text inputs.', 'title' => 'text_multiple', ), 'info' => array( 'description' => t('This field holds different text inputs.'), 'label' => t('Text multiple'), ), ), ); foreach ($expected as $public_field => $discovery_info) { foreach (array('data', 'form_element', 'info') as $section_name) { if (empty($result['data'][0][$public_field][$section_name]) && empty($expected[$public_field][$section_name])) { continue; } $this->assertEqual($result['data'][0][$public_field][$section_name], $expected[$public_field][$section_name], format_string('The "@section" information is properly described for @field.', array( '@field' => $public_field, '@section' => $section_name, ))); } } } /** * Field discovery allowed values. */ public function testFieldDiscoveryAllowedValues() { // Add entity reference fields. restful_test_add_fields('node', 'article'); $handler = restful()->getResourceManager()->getPlugin('test_articles:1.2'); // Create 3 nodes. $expected_result = array(); foreach (array(1, 2, 3) as $id) { $title = 'article/' . $id; $settings = array( 'title' => $title, 'type' => 'article', ); $node = $this->drupalCreateNode($settings); $expected_result[$node->nid] = $title; } // Set widget to select list. $instance = field_info_instance('node', 'entity_reference_single', 'article'); $instance['widget']['type'] = 'options_select'; field_update_instance($instance); $handler->setRequest(Request::create('api/v1.2/test_articles', array(), RequestInterface::METHOD_OPTIONS)); $handler->setPath(''); $formatter = restful() ->getFormatterManager() ->negotiateFormatter(NULL); $formatter->setResource($handler); $result = $formatter->prepare($handler->process()); $this->assertEqual($result['data'][0]['entity_reference_single']['form_element']['allowed_values'], $expected_result); // Set widget to autocomplete. $instance['widget']['type'] = 'entityreference_autocomplete'; field_update_instance($instance); // Invalidate public fields cache. $handler = restful()->getResourceManager()->getPluginCopy('test_articles:1.2'); $handler->setRequest(Request::create('api/v1.2/test_articles', array(), RequestInterface::METHOD_OPTIONS)); $handler->setPath(''); $formatter = restful() ->getFormatterManager() ->negotiateFormatter(NULL); $formatter->setResource($handler); $result = $formatter->prepare($handler->process()); $this->assertNull($result['data'][0]['entity_reference_single']['form_element']['allowed_values']); } } ================================================ FILE: tests/RestfulEntityAndPropertyAccessTestCase.test ================================================ 'Entity and property access', 'description' => 'Test access for the entity and the properties.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_test'); } /** * Test access control for creating an entity. */ public function testCreateAccess() { $handler = restful()->getResourceManager()->getPlugin('test_articles:1.0'); $parsed_body = array('label' => $this->randomName()); // Non-privileged user. $user1 = $this->drupalCreateUser(); try { $handler->setAccount($user1); $this->doRequest(\Drupal\restful\Http\RequestInterface::METHOD_POST, $handler, $parsed_body); $this->fail('Non-privileged user can create entity.'); } catch (Exception $e) { $this->pass('Non-privileged user cannot create entity.'); } // Privileged user. $user2 = $this->drupalCreateUser(array('create article content')); $handler->setAccount($user2); $result = $this->doRequest(\Drupal\restful\Http\RequestInterface::METHOD_POST, $handler, $parsed_body); $this->assertTrue($result['data'][0], 'Privileged user can create entity.'); // Privileged user, with limited access to property. restful_test_deny_access_field(); $handler->setAccount($user2); $result = $this->doRequest(\Drupal\restful\Http\RequestInterface::METHOD_POST, $handler, $parsed_body); $this->assertTrue($result['data'][0], 'Privileged user can create entity, with limited access to property.'); // Privileged user, with limited access to property, and that property // passed in the request. $text1 = $this->randomName(); $parsed_body['body'] = $text1; try { $handler->setAccount($user1); $this->doRequest(\Drupal\restful\Http\RequestInterface::METHOD_POST, $handler, $parsed_body); $this->fail('Non-privileged user can create entity with inaccessible property that was passed in the request.'); } catch (Exception $e) { $this->pass('Non-privileged user cannot create entity with inaccessible property that was passed in the request.'); } restful_test_clear_access_field(); } /** * Test access control for updating an entity. */ public function testUpdateAccess() { $label = $this->randomName(); $new_label = $this->randomName(); $settings = array( 'type' => 'article', 'title' => $label, ); $node = $this->drupalCreateNode($settings); $id = $node->nid; $handler = restful()->getResourceManager()->getPlugin('test_articles:1.0'); $parsed_body = array('label' => $new_label); // Non-privileged user. $user1 = $this->drupalCreateUser(); try { $handler->setAccount($user1); $this->doRequest(\Drupal\restful\Http\RequestInterface::METHOD_PUT, $handler, $parsed_body, $id); $this->fail('Non-privileged user can update entity.'); } catch (Exception $e) { $this->pass('Non-privileged user cannot update entity.'); } // Privileged user. $user2 = $this->drupalCreateUser(array('edit any article content')); $handler->setAccount($user2); $result = $this->doRequest(\Drupal\restful\Http\RequestInterface::METHOD_PUT, $handler, $parsed_body, $id); $this->assertTrue($result['data'][0], 'Privileged user can update entity.'); $this->assertEqual($result['data'][0]['id'], $id, 'Updated entity has the same entity ID.'); $this->assertEqual($result['data'][0]['label'], $new_label, 'Entity label was updated.'); // Privileged user, with limited access to property. restful_test_deny_access_field(); $handler->setAccount($user2); $result = $this->doRequest(\Drupal\restful\Http\RequestInterface::METHOD_PUT, $handler, $parsed_body, $id); $this->assertTrue($result['data'][0], 'Privileged user can update entity, with limited access to property.'); // Privileged user, with limited access to property, and that property // passed in the request. $text1 = $this->randomName(); $parsed_body['body'] = $text1; try { $handler->setAccount($user1); $this->doRequest(\Drupal\restful\Http\RequestInterface::METHOD_PUT, $handler, $parsed_body, $id); $this->fail('Non-privileged user can update entity with inaccessible property that was passed in the request.'); } catch (Exception $e) { $this->pass('Non-privileged user cannot update entity with inaccessible property that was passed in the request.'); } restful_test_clear_access_field(); } /** * Test access control for viewing an entity. */ public function testViewAccess() { $user1 = $this->drupalCreateUser(); $label = $this->randomName(); $settings = array( 'type' => 'article', 'title' => $label, 'uid' => $user1->uid, ); $node1 = $this->drupalCreateNode($settings); $wrapper = entity_metadata_wrapper('node' ,$node1); $wrapper->body->set(array('value' => $this->randomName())); $wrapper->save(); $handler = restful()->getResourceManager()->getPlugin('test_articles:1.2'); // Privileged user. $handler->setAccount($user1); $response = $this->doRequest(\Drupal\restful\Http\RequestInterface::METHOD_GET, $handler, array(), $node1->nid); $result = $response['data'][0]; $this->assertTrue($result['body'], 'Privileged user can view entity.'); // Privileged user, with limited access to property. restful_test_deny_access_field(); $handler->setAccount($user1); $result = $this->doRequest(\Drupal\restful\Http\RequestInterface::METHOD_GET, $handler, array(), $node1->nid); $this->assertTrue(!isset($result['data'][0]['body']), 'Privileged user can view entity but without inaccessible properties.'); restful_test_clear_access_field(); // Non-privileged user (Revoke "access content" permission). user_role_revoke_permissions(DRUPAL_ANONYMOUS_RID, array('access content')); $user2 = drupal_anonymous_user(); try { $handler->setAccount($user2); $this->doRequest(\Drupal\restful\Http\RequestInterface::METHOD_GET, $handler, array(), $node1->nid); $this->fail('Non-privileged user can view entity.'); } catch (Exception $e) { $this->pass('Non-privileged user cannot view entity.'); } } /** * Tests custom access callbacks at the resource method level. */ public function testEndPointAccessCallback() { $settings = array( 'type' => 'article', ); $node = $this->drupalCreateNode($settings); $handler = restful()->getResourceManager()->getPlugin('test_articles:1.3'); try { $handler->doGet($node->nid); $this->fail('Custom access callback per resource\'s method not executed correctly.'); } catch (ForbiddenException $e) { $this->pass('Custom access callback per endpoint executed correctly.'); } $handler->setPath($node->nid); $handler->setRequest(\Drupal\restful\Http\Request::create($handler->versionedUrl($node->nid, array('absolute' => FALSE)), array(), \Drupal\restful\Http\RequestInterface::METHOD_HEAD)); $handler->process(); $this->pass('Custom access callback per endpoint executed correctly.'); } /** * Test access callback per public field. */ public function testPublicFieldAccess() { $settings = array( 'title' => 'no access', 'type' => 'article', ); $node = $this->drupalCreateNode($settings); $handler = restful()->getResourceManager()->getPlugin('test_articles:1.0'); $result = $this->doRequest(\Drupal\restful\Http\RequestInterface::METHOD_GET, $handler, array(), $node->nid); $this->assertNotNull($result['data'][0]['label'], 'Label access is allowed without access callback.'); variable_set('restful_test_revoke_public_field_access', 'label'); $handler = restful()->getResourceManager()->getPluginCopy('test_articles:1.0'); $result = $this->doRequest(\Drupal\restful\Http\RequestInterface::METHOD_GET, $handler, array(), $node->nid); $this->assertTrue(empty($result['data'][0]['label']), 'Label access is denied with access callback.'); } /** * Helper method to format a request. * * @param string $method * The HTTP verb. * @param \Drupal\restful\Plugin\resource\ResourceInterface $handler * The handler. * @param array $input * The parsed body array. * @param string $path * The path where to make the request. * * @return array * The formatted output. */ protected function doRequest($method, \Drupal\restful\Plugin\resource\ResourceInterface $handler, array $input = array(), $path = '') { $output = NULL; if ($method == \Drupal\restful\Http\RequestInterface::METHOD_POST) { $output = $handler->doPost($input); } elseif ($method == \Drupal\restful\Http\RequestInterface::METHOD_PUT) { $output = $handler->doPut($path, $input); } elseif ($method == \Drupal\restful\Http\RequestInterface::METHOD_PATCH) { $output = $handler->doPatch($path, $input); } elseif ($method == \Drupal\restful\Http\RequestInterface::METHOD_GET) { $output = $handler->doGet($path, $input); } if (!isset($output)) { return NULL; } $formatter = restful()->getFormatterManager()->negotiateFormatter(NULL, 'json'); $formatter->setResource($handler); return $formatter->prepare($output); } } ================================================ FILE: tests/RestfulEntityUserAccessTestCase.test ================================================ 'User resource access', 'description' => 'Test access to the base "users" resource.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful'); } /** * Test access control for viewing the "users" resource. */ public function testViewAccess() { $resource_manager = restful()->getResourceManager(); $user1 = $this->drupalCreateUser(); $user2 = $this->drupalCreateUser(array('access user profiles')); // Non-privileged user. $handler = $resource_manager->getPlugin('users:1.0'); $handler->setAccount($user1); try { $handler->setRequest(Request::create('api/v1.0/users/' . $user2->uid)); $handler->setPath($user2->uid); $handler->process(); $this->fail('Non-privileged user can view another user.'); } catch (InaccessibleRecordException $e) { $this->pass('Non-privileged user cannot view another user.'); } catch (\Exception $e) { $this->fail('Incorrect exception thrown for non-privileged user accessing another user.'); } // Listing of users. $handler->setRequest(Request::create('api/v1.0/users')); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual($result[0]['self'], url('api/v1.0/users/0', array('absolute' => TRUE))); $this->assertEqual($result[1]['self'], url('api/v1.0/users/2', array('absolute' => TRUE))); $this->assertEqual(count($result), 2, 'Unprivileged users can only see themselves and the anonymous user.'); // View own user account. $handler->setRequest(Request::create('api/v1.0/users/' . $user1->uid)); $handler->setPath($user1->uid); $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $response = $response['data']; $result = $response[0]; $this->assertEqual($result['mail'], $user1->mail, 'User can see own mail.'); // Privileged user, watching another user's profile. $handler->setAccount($user2); $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $response = $response['data']; $result = $response[0]; $expected_result = array( 'id' => $user1->uid, 'label' => $user1->name, 'self' => $handler->versionedUrl($user1->uid), ); $this->assertEqual($result, $expected_result, "Privileged user can access another user's resource."); // Listing of users. $handler->setRequest(Request::create('api/v1.0/users')); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; // Check we have all the expected users count (user ID 1 and our declared // users plus the anon user). $this->assertTrue(count($result) == 4, 'Privileged user can access listing of users.'); } } ================================================ FILE: tests/RestfulEntityValidatorTestCase.test ================================================ 'Entity validator', 'description' => 'Test integration with entity validator module.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_test', 'entity_validator_example'); } /** * Test entity validator. */ public function testEntityValidator() { $user1 = $this->drupalCreateUser(array('create article content')); $this->drupalLogin($user1); $handler = restful() ->getResourceManager() ->getPlugin('test_articles:1.0'); $handler->setAccount($user1); $parsed_body = array('title' => 'no', 'body' => 'Text with Drupal'); try { $request = Request::create('', $parsed_body, RequestInterface::METHOD_POST, NULL, FALSE, NULL, array(), array(), array(), $parsed_body); $handler->setRequest($request); $handler->process(); $this->fail('Too short title did not cause a "Bad request" error.'); } catch (BadRequestException $e) { $field_errors = $e->getFieldErrors(); $expected_result = array( 'title' => array( 'The title should be at least 3 characters long.', ), ); $this->assertEqual($field_errors, $expected_result, 'Correct error message passed in the JSON'); } $parsed_body['title'] = 'yes'; $request = Request::create('', $parsed_body, RequestInterface::METHOD_POST, NULL, FALSE, NULL, array(), array(), array(), $parsed_body); $handler->setRequest($request); $result = restful() ->getFormatterManager() ->negotiateFormatter(NULL, 'json') ->prepare($handler->process()); $this->assertTrue($result['data'][0]['id'], 'Entity with proper title length passed validation and was created.'); } } ================================================ FILE: tests/RestfulExceptionHandleTestCase.test ================================================ 'Exception handling', 'description' => 'Test converting exceptions into JSON with code, message and description.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_example'); } /** * Test converting exceptions into JSON with code, message and description. * * When calling the API via hook_menu(), exceptions should be converted to a * valid JSON. */ public function testExceptionHandle() { $options = array('sort' => 'wrong_key'); $result = $this->httpRequest('api/v1.0/articles', \Drupal\restful\Http\RequestInterface::METHOD_GET, $options); $expected_result = array( 'type' => 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1', 'title' => 'The sort wrong_key is not allowed for this path.', 'status' => 400, 'detail' => 'Bad Request', ); $this->assertEqual(drupal_json_decode($result['body']), $expected_result); $this->assertEqual($result['code'], 400, 'Correct HTTP code.'); } /** * Test when an entity is not found that a 4XX is returned instead of 500. */ public function testEntityNotFoundDelivery() { $url = 'api/v1.0/articles/1'; $result = $this->httpRequest($url); $body = drupal_json_decode($result['body']); $this->assertEqual($result['code'], '422', format_string('422 status code sent for @url url.', array('@url' => $url))); $this->assertTrue(strpos($result['headers'], 'application/problem+json;') !== FALSE, '"application/problem+json" found in invalid request.'); $this->assertEqual($body['title'], 'The entity ID 1 does not exist.', 'Correct error message.'); } } ================================================ FILE: tests/RestfulForbiddenItemsTestCase.test ================================================ 'Forbidden Items', 'description' => 'Tests handling access denied items.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_test'); restful_test_add_fields('node', 'article'); } /** * Tests access denied in lists and single elements. */ public function testAccessDenied() { $account = $this->drupalCreateUser(); $nids = $this->createEntityWithReferences($account->uid); $resource_manager = restful()->getResourceManager(); $handler = $resource_manager->getPluginCopy('test_articles:1.2'); $handler->setAccount($account); $this->assertTrue((bool) $handler->doGet($nids[2])); restful_test_deny_access_node($nids[1]); try { $handler->doGet($nids[1]); $this->fail('There should be a Forbidden exception.'); } catch (InaccessibleRecordException $e) { $this->assertEqual($e->getCode(), 404); $this->assertEqual($e->getMessage(), InaccessibleRecordException::ERROR_404_MESSAGE); } variable_set('restful_show_access_denied', TRUE); $handler = $resource_manager->getPluginCopy('test_articles:1.2'); $handler->setAccount($account); $this->assertTrue((bool) $handler->doGet($nids[2])); restful_test_deny_access_node($nids[1]); try { $handler->doGet($nids[1]); $this->fail('There should be a Forbidden exception.'); } catch (InaccessibleRecordException $e) { $this->assertEqual($e->getCode(), 403); $this->assertNotEqual($e->getMessage(), InaccessibleRecordException::ERROR_404_MESSAGE); } // When we include the related entities we are loading the referenced // entity, that's when we check for the entity access. If we are only // getting the list of IDs we don't know which entities will be accessible // or not. $handler = $resource_manager->getPluginCopy('test_articles:1.2'); $handler->setPath(''); $handler->setRequest(Request::create(NULL, array('include' => 'entity_reference_single,entity_reference_multiple'))); $handler->setAccount($account); $response = $handler->process(); $returned_nids = array_map(function (ResourceFieldCollectionInterface $item) { return $item->getIdField()->render($item->getInterpreter()); }, $response); $this->assertTrue(count($response) == 2 && !in_array($nids[1], $returned_nids), 'Listing a denied node removes it from the listing.'); $formatter = restful()->getFormatterManager()->getPlugin('json_api'); $formatter->setResource($handler); $results = $formatter->prepare($response); $this->assertEqual(count($results['data'][0]['relationships']['entity_reference_multiple']['data']), 1, 'The inaccessible node is not present in the relationship.'); // Avoid count or pagination problems due to denied items. $this->assertTrue(empty($results['links']['next'])); // Make sure that denied items in the related elements do not alter the top // level count incorrectly. $handler = $resource_manager->getPluginCopy('test_articles:1.2'); $handler->setPath(''); $handler->setRequest(Request::create(NULL, array( 'include' => 'entity_reference_single,entity_reference_multiple', ))); $handler->setAccount($account); $response = $handler->process(); $formatter->setResource($handler); $results = $formatter->prepare($response); $this->assertEqual(count($results['data'][0]['relationships']['entity_reference_multiple']['data']), 1, 'The count is not altered incorrectly.'); $this->assertEqual(count($results['meta']['denied']), 1, 'Denied elements are reported.'); // Same test without the includes should yield the same results. $handler = $resource_manager->getPluginCopy('test_articles:1.2'); $handler->setPath(''); $handler->setRequest(Request::create(NULL, array( 'range' => 1, ))); $handler->setAccount($account); $response = $handler->process(); $formatter->setResource($handler); $results = $formatter->prepare($response); $this->assertEqual(count($results['data'][0]['relationships']['entity_reference_multiple']['data']), 1, 'Access checks are applied when the entity is not included.'); $this->assertTrue(empty($results['meta']['denied']), 'No denied item was detected.'); } /** * Adds some content to be retrieved. * * @param int $uid * The owner ID. * * @return int[] * The entity IDs. */ protected function createEntityWithReferences($uid) { $node1 = (object) array( 'title' => t('Node 1'), 'type' => 'article', 'uid' => $uid, ); node_object_prepare($node1); node_save($node1); $node2 = (object) array( 'title' => t('Node 2'), 'type' => 'article', 'uid' => $uid, ); node_object_prepare($node2); node_save($node2); $node3 = (object) array( 'title' => t('Node 3'), 'type' => 'article', 'uid' => $uid, ); node_object_prepare($node3); node_save($node3); // Set some references to node1. $wrapper = entity_metadata_wrapper('node', $node1); $wrapper->entity_reference_single->set($node3); $wrapper->entity_reference_multiple[] = $node2; $wrapper->entity_reference_multiple[] = $node3; $wrapper->save(); return array( $node1->nid, $node2->nid, $node3->nid, ); } } ================================================ FILE: tests/RestfulGetHandlersTestCase.test ================================================ 'Get handlers', 'description' => 'Test getting handlers by version (major and minor).', 'group' => 'RESTful', ); } public function setUp() { parent::setUp('restful_example'); } /** * Test getting handlers via API. */ public function testGetHandlers() { $resource_manager = restful()->getResourceManager(); $title = $this->randomName(); $settings = array( 'type' => 'article', 'title' => $title, ); $node1 = $this->drupalCreateNode($settings); $handler = $resource_manager->getPlugin('articles:1.1'); $this->assertTrue($handler instanceof \Drupal\restful_example\Plugin\resource\node\article\v1\Articles__1_0); $handler->setRequest(\Drupal\restful\Http\Request::create('articles/v1.1/' . $node1->nid)); $handler->setPath($node1->nid); $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $response = $response['data']; $result = $response[0]; $this->assertTrue(empty($result['self']), '"self" property does not appear in minor version 1.'); } } ================================================ FILE: tests/RestfulHalJsonTestCase.test ================================================ 'View HAL+JSON', 'description' => 'Test the viewing of an entity in HAL+JSON format.', 'group' => 'RESTful', ); } public function setUp() { parent::setUp('restful_example', 'restful_test', 'entityreference'); restful_test_add_fields(); } /** * Test embedded resources. */ public function testHalEmbeddedResources() { $user1 = $this->drupalCreateUser(); /* @var \Entity $entity1 */ $entity1 = entity_create('entity_test', array( 'name' => 'main', 'uid' => $user1->uid, )); $entity1->save(); /* @var \Entity $entity2 */ $entity2 = entity_create('entity_test', array( 'name' => 'main', 'uid' => $user1->uid, )); $entity2->save(); $entity3 = entity_create('entity_test', array( 'name' => 'main', 'uid' => $user1->uid, )); /* @var \EntityDrupalWrapper $wrapper */ $wrapper = entity_metadata_wrapper('entity_test', $entity3); $text1 = $this->randomName(); $text2 = $this->randomName(); $wrapper->text_single->set($text1); $wrapper->text_multiple->set(array($text1, $text2)); $wrapper->entity_reference_single->set($entity1); $wrapper->entity_reference_multiple[] = $entity1; $wrapper->entity_reference_multiple[] = $entity2; $wrapper->save(); $handler = restful()->getResourceManager()->getPlugin('main:1.1'); $headers = new HttpHeaderBag(array( 'X-API-Version' => 'v1.1', 'Accept' => 'application/hal+json', )); $handler->setRequest(Request::create('api/main/' . $wrapper->getIdentifier(), array(), RequestInterface::METHOD_GET, $headers)); $handler->setPath($wrapper->getIdentifier()); $response = $handler->process(); $formatter_manager = restful()->getFormatterManager(); $formatter_manager->setResource($handler); $formatter_result = $formatter_manager->format($response); $results = drupal_json_decode($formatter_result); $this->assertEqual(2, count($results['_embedded'])); $this->assertEqual($wrapper->entity_reference_multiple->count(), count($results['_embedded']['hal:entity_reference_multiple_resource'])); $this->assertEqual($entity1->pid, count($results['_embedded']['hal:entity_reference_single_resource']['id'])); } } ================================================ FILE: tests/RestfulHookMenuTestCase.test ================================================ 'Menu API', 'description' => 'Test the hook_menu() and delivery callback implementations.', 'group' => 'RESTful', ); } /** * Overrides DrupalWebTestCase::setUp(). */ public function setUp() { parent::setUp('restful_example'); // Allow anonymous users to edit articles. user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array('edit any article content' => TRUE)); } /** * Test viewing an entity (GET method). */ public function testViewEntity() { $user1 = $this->drupalCreateUser(array('edit own article content')); $title = $this->randomName(); $settings = array( 'type' => 'article', 'title' => $title, 'uid' => $user1->uid, ); $node1 = $this->drupalCreateNode($settings); // Test version 1.0 $result = $this->httpRequest('api/v1.0/articles/' . $node1->nid); $expected_result = array( 'data' => array(array( 'id' => $node1->nid, 'label' => $node1->title, 'self' => url('api/v1.0/articles/' . $node1->nid, array('absolute' => TRUE)), )), 'self' => array( 'title' => 'Self', 'href' => url('api/v1.0/articles/' . $node1->nid, array('absolute' => TRUE)), ), ); $this->assertEqual($result['body'], json_encode($expected_result)); // Test version 1.1 $result = $this->httpRequest('api/v1.1/articles/' . $node1->nid, RequestInterface::METHOD_GET); $expected_result['self']['href'] = url('api/v1.1/articles/' . $node1->nid, array('absolute' => TRUE)); unset($expected_result['data'][0]['self']); $this->assertEqual($result['body'], json_encode($expected_result)); // Test method override. $headers = array( 'X-HTTP-Method-Override' => RequestInterface::METHOD_PATCH, 'X-CSRF-Token' => drupal_get_token(\Drupal\restful\Plugin\authentication\Authentication::TOKEN_VALUE), 'Content-Type' => 'application/json', 'Authorization' => 'Basic ' . base64_encode($user1->name . ':' . $user1->pass_raw), ); $body = array( 'label' => 'new title', ); $handler = restful()->getResourceManager()->getPlugin('articles:1.1'); $header_bag = new \Drupal\restful\Http\HttpHeaderBag($headers); $handler->setRequest(Request::create('api/v1.0/articles/' . $node1->nid, array(), RequestInterface::METHOD_POST, $header_bag, FALSE, NULL, array(), array(), array(), $body)); $handler->setPath($node1->nid); restful()->getFormatterManager()->format($handler->process(), 'json'); $node1 = node_load($node1->nid); $this->assertEqual($node1->title, 'new title', 'HTTP method was overridden.'); } /** * Test HTTP headers change according to the response. */ public function testHttpHeadersAndStatus() { // Valid request (even though it's empty). $result = $this->httpRequest('api/v1.0/articles/', RequestInterface::METHOD_GET); $this->assertTrue(strpos($result['headers'], 'application/json;'), '"application/json" found in valid request.'); // Invalid request. $result = $this->httpRequest('api/v1.0/articles/invalid_id', RequestInterface::METHOD_GET); $this->assertTrue(strpos($result['headers'], 'application/problem+json;') !== FALSE, '"application/problem+json" found in invalid request.'); // Switch site to offline mode. variable_set('maintenance_mode', TRUE); $this->httpauth_credentials = NULL; $result = $this->httpRequest('api/login'); $this->assertEqual($result['code'], '503', '503 status code sent for site in offline mode.'); } /** * Test hijacking of api/* pages and showing proper error messages. */ public function testNotFoundDelivery() { // Invalid URLs. $urls = array( 'api/invalid', ); foreach ($urls as $url) { $result = $this->httpRequest($url); $body = drupal_json_decode($result['body']); $this->assertEqual($result['code'], '404', format_string('404 status code sent for @url url.', array('@url' => $url))); $this->assertTrue(strpos($result['headers'], 'application/problem+json;') !== FALSE, '"application/problem+json" found in invalid request.'); $this->assertEqual($body['title'], 'Invalid URL path.', 'Correct error message.'); } // Non-related url. $result = $this->httpRequest('api-api'); $this->assertEqual($result['code'], '404', format_string('404 status code sent for @url url.', array('@url' => $url))); $this->assertFalse(strpos($result['headers'], 'application/problem+json;'), 'Only correct URL is hijacked.'); } /** * Test the version negotiation. */ public function testVersionNegotiation() { // Fake the HTTP header. $test_harness = array( array( 'path' => 'api/v1.1/articles', 'version_header' => NULL, 'expected_version' => array(1, 1), 'expected_resource' => 'articles', ), array( 'path' => 'api/v1/articles', 'version_header' => NULL, 'expected_version' => array(1, 7), 'expected_resource' => 'articles', ), array( 'path' => 'api/articles', 'version_header' => 'v1', 'expected_version' => array(1, 7), 'expected_resource' => 'articles', ), array( 'path' => 'api/articles', 'version_header' => 'v1.0', 'expected_version' => array(1, 0), 'expected_resource' => 'articles', ), array( 'path' => 'api/articles', 'version_header' => NULL, 'expected_version' => array(2, 1), 'expected_resource' => 'articles', ), ); foreach ($test_harness as $test_item) { $headers = NULL; if (!empty($test_item['version_header'])) { $headers = new \Drupal\restful\Http\HttpHeaderBag(array( 'X-API-Version' => $test_item['version_header'], )); } $request = \Drupal\restful\Http\Request::create($test_item['path'], array(), RequestInterface::METHOD_GET, $headers); $resource_manager = new \Drupal\restful\Resource\ResourceManager($request); drupal_static_reset('Drupal\restful\Resource\ResourceManager::getVersionFromRequest'); $this->assertEqual($resource_manager->getVersionFromRequest(), $test_item['expected_version'], sprintf('%s resolves correctly.', $test_item['path'])); $this->assertEqual($resource_manager->getResourceIdFromRequest(), $test_item['expected_resource'], sprintf('Resource name obtained correctly from %s.', $test_item['path'])); } } } ================================================ FILE: tests/RestfulJsonApiTestCase.test ================================================ 'JSON API', 'description' => 'Test the JSON API formatter.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_example', 'restful_test', 'entityreference', 'uuid'); restful_test_add_fields(); $this->account = $this->drupalCreateUser(); $this->entities = $this->createEntities(); $this->handler = restful()->getResourceManager()->getPlugin('main:1.1'); $this->formatter = restful() ->getFormatterManager() ->negotiateFormatter(NULL, 'json_api'); $configuration = array( 'resource' => $this->handler, ); $this->formatter->setConfiguration($configuration); } /** * Create test entities. * * @return Entity[] * An array of entities. */ protected function createEntities() { $entities = array(); $entities[] = entity_create('entity_test', array( 'name' => 'main', 'uid' => $this->account->uid, )); $entities[0]->save(); $entities[] = entity_create('entity_test', array( 'name' => 'main', 'uid' => $this->account->uid, )); $entities[1]->save(); $entities[] = entity_create('entity_test', array( 'name' => 'main', 'uid' => $this->account->uid, )); $entities[0]->entity_reference_single[LANGUAGE_NONE][] = array('target_id' => $entities[1]->pid); $entities[0]->save(); $entities[2]->entity_reference_single[LANGUAGE_NONE][] = array('target_id' => $entities[0]->pid); $entities[2]->entity_reference_multiple[LANGUAGE_NONE][] = array('target_id' => $entities[0]->pid); $entities[2]->entity_reference_multiple[LANGUAGE_NONE][] = array('target_id' => $entities[1]->pid); $entities[2]->save(); return $entities; } /** * Test requesting resources. */ public function testReading() { /* @var \EntityDrupalWrapper $wrapper */ $wrapper = entity_metadata_wrapper('entity_test', $this->entities[2]); $this->handler->setRequest(Request::create('api/v1.1/main/' . $wrapper->getIdentifier())); $this->handler->setPath($wrapper->getIdentifier()); $resource_field_collections = $this->handler->process(); $result = drupal_json_decode($this->formatter->format($resource_field_collections)); // Assert the basic properties of the resource. $this->assertEqual('main', $result['data']['type']); $this->assertEqual($wrapper->getIdentifier(), $result['data']['id']); // 1. Assert the "attributes" key. // Remove the NULL keys since we're not adding them to the $expected array. $attributes = array_filter($result['data']['attributes']); $expected = array( // The following fields don't have a resource in their definition, // therefore they are treated as regular fields and not relationships. 'entity_reference_multiple' => array( $this->entities[0]->pid, $this->entities[1]->pid, ), 'entity_reference_single' => $this->entities[0]->pid, 'id' => $wrapper->getIdentifier(), 'label' => $wrapper->label(), 'self' => $this->handler->versionedUrl($wrapper->getIdentifier()), // Create it empty and fill it later. 'text_multiple' => array(), 'text_single' => $wrapper->text_single->value(), ); foreach ($wrapper->text_multiple as $text_multiple_wrapper) { /* @var \EntityStructureWrapper $text_multiple_wrapper */ $expected['text_multiple'][] = $text_multiple_wrapper->value(); } $this->assertEqual(array_filter($expected), array_filter($attributes)); $this->assertEqual($this->handler->versionedUrl($wrapper->getIdentifier(), array( 'query' => array('page' => array('number' => 1)), )), $result['links']['self']); // 2. Assert the relationships. $relationships = $result['data']['relationships']; // Make sure that the entity_reference_*_resource are included as // relationships. $this->assertEqual($relationships['entity_reference_multiple_resource']['links'], array( 'self' => $this->handler->versionedUrl($wrapper->getIdentifier() . '/relationships/entity_reference_multiple_resource'), 'related' => $this->handler->versionedUrl('', array('query' => array('filter' => array('entity_reference_multiple_resource' => $wrapper->entity_reference_multiple[0]->getIdentifier())))), )); $this->assertEqual($relationships['entity_reference_single_resource']['data']['type'], 'main'); $this->assertEqual($relationships['entity_reference_single_resource']['data']['id'], $wrapper->entity_reference_single->getIdentifier()); $this->assertEqual($relationships['entity_reference_multiple_resource']['data'][0]['type'], 'main'); $this->assertEqual($relationships['entity_reference_multiple_resource']['data'][0]['id'], $wrapper->entity_reference_multiple[0]->getIdentifier()); $this->assertEqual($relationships['entity_reference_multiple_resource']['data'][1]['type'], 'main'); $this->assertEqual($relationships['entity_reference_multiple_resource']['data'][1]['id'], $wrapper->entity_reference_multiple[1]->getIdentifier()); // Make sure that using fields + includes lists all the fields in the // embedded entity if there is no subfield added. $this->handler->setRequest(Request::create('api/v1.1/main/' . $wrapper->getIdentifier(), array( 'include' => 'entity_reference_multiple_resource,entity_reference_single_resource', 'fields' => 'entity_reference_multiple_resource,entity_reference_single_resource', ))); $this->handler->setPath($wrapper->getIdentifier()); $this->formatter->setResource($this->handler); $resource_field_collections = $this->handler->process(); $result = drupal_json_decode($this->formatter->format($resource_field_collections)); $this->assertEqual(count($result['included'][0]['attributes']), 16); // Now make sure that including fields only lists those fields. $this->handler->setRequest(Request::create('api/v1.1/main/' . $wrapper->getIdentifier(), array( 'include' => 'entity_reference_single_resource', 'fields' => 'entity_reference_single_resource.label', ))); $this->handler->setPath($wrapper->getIdentifier()); $this->formatter->setResource($this->handler); $resource_field_collections = $this->handler->process(); $result = drupal_json_decode($this->formatter->format($resource_field_collections)); $this->assertEqual(count($result['included'][0]['attributes']), 1); // Make a request with the include query string. $this->handler->setRequest(Request::create('api/v1.1/main/' . $wrapper->getIdentifier(), array('include' => 'entity_reference_multiple_resource,entity_reference_single_resource'))); $this->handler->setPath($wrapper->getIdentifier()); $this->formatter->setResource($this->handler); $resource_field_collections = $this->handler->process(); $result = drupal_json_decode($this->formatter->format($resource_field_collections)); $included = $result['included']; // Make sure there are no repeated includes. $this->assertEqual(2, count($included)); $expected_includes = array( array( 'attributes' => array( 'entity_reference_single' => $wrapper->entity_reference_single->entity_reference_single->getIdentifier(), 'id' => $wrapper->entity_reference_multiple[0]->getIdentifier(), 'label' => $wrapper->entity_reference_multiple[0]->label(), 'self' => $this->handler->versionedUrl($wrapper->entity_reference_multiple[0]->getIdentifier()), ), 'id' => $wrapper->entity_reference_multiple[0]->getIdentifier(), 'links' => array( 'self' => $this->handler->versionedUrl($wrapper->entity_reference_multiple[0]->getIdentifier()), ), 'type' => 'main', 'relationships' => array( 'entity_reference_single_resource' => array( 'data' => array( 'id' => $wrapper->entity_reference_single->entity_reference_single->getIdentifier(), 'type' => 'main', ), 'links' => array( 'self' => $this->handler->versionedUrl($wrapper->entity_reference_multiple[0]->getIdentifier() . '/relationships/entity_reference_single_resource'), 'related' => $this->handler->versionedUrl('', array('query' => array('filter' => array('entity_reference_single_resource' => $wrapper->entity_reference_single->entity_reference_single->getIdentifier())))), ), ), ), ), array( 'attributes' => array( 'id' => $wrapper->entity_reference_multiple[1]->getIdentifier(), 'label' => $wrapper->entity_reference_multiple[1]->label(), 'self' => $this->handler->versionedUrl($wrapper->entity_reference_multiple[1]->getIdentifier()), ), 'id' => $wrapper->entity_reference_multiple[1]->getIdentifier(), 'links' => array( 'self' => $this->handler->versionedUrl($wrapper->entity_reference_multiple[1]->getIdentifier()), ), 'type' => 'main', ), ); // Remove the empty fields from the actual response for easier comparison. $included[0]['attributes'] = array_filter($included[0]['attributes']); $included[1]['attributes'] = array_filter($included[1]['attributes']); $this->assertEqual($expected_includes, $included); // 3. Assert the nested relationships. $relationships = $result['data']['relationships']; // Make sure that the entity_reference_*_resource are included as // relationships. $this->assertEqual($relationships['entity_reference_single_resource']['data']['type'], 'main'); $this->assertEqual($relationships['entity_reference_single_resource']['data']['id'], $wrapper->entity_reference_single->getIdentifier()); $this->assertEqual($relationships['entity_reference_multiple_resource']['data'][0]['type'], 'main'); $this->assertEqual($relationships['entity_reference_multiple_resource']['data'][0]['id'], $wrapper->entity_reference_multiple[0]->getIdentifier()); $this->assertEqual($relationships['entity_reference_multiple_resource']['data'][1]['type'], 'main'); $this->assertEqual($relationships['entity_reference_multiple_resource']['data'][1]['id'], $wrapper->entity_reference_multiple[1]->getIdentifier()); // Make a request with the include query string. $this->handler->setRequest(Request::create('api/v1.1/main/' . $wrapper->getIdentifier(), array('include' => 'entity_reference_single_resource.entity_reference_single_resource'))); $this->handler->setPath($wrapper->getIdentifier()); $this->formatter->setResource($this->handler); $resource_field_collections = $this->handler->process(); $result = drupal_json_decode($this->formatter->format($resource_field_collections)); $included = $result['included']; // Make sure there are no repeated includes. $this->assertEqual(2, count($included)); $expected_includes = array( array( 'attributes' => array( 'entity_reference_single' => $wrapper->entity_reference_single->entity_reference_single->getIdentifier(), 'id' => $wrapper->entity_reference_multiple[0]->getIdentifier(), 'label' => $wrapper->entity_reference_multiple[0]->label(), 'self' => $this->handler->versionedUrl($wrapper->entity_reference_multiple[0]->getIdentifier()), ), 'id' => $wrapper->entity_reference_multiple[0]->getIdentifier(), 'links' => array( 'self' => $this->handler->versionedUrl($wrapper->entity_reference_multiple[0]->getIdentifier()), ), 'type' => 'main', 'relationships' => array( 'entity_reference_single_resource' => array( 'data' => array( 'id' => $wrapper->entity_reference_single->entity_reference_single->getIdentifier(), 'type' => 'main', ), 'links' => array( 'self' => $this->handler->versionedUrl($wrapper->entity_reference_multiple[0]->getIdentifier() . '/relationships/entity_reference_single_resource'), 'related' => $this->handler->versionedUrl('', array('query' => array('filter' => array('entity_reference_single_resource' => $wrapper->entity_reference_single->entity_reference_single->getIdentifier())))), ), ), ), ), array( 'attributes' => array( 'id' => $wrapper->entity_reference_multiple[1]->getIdentifier(), 'label' => $wrapper->entity_reference_multiple[1]->label(), 'self' => $this->handler->versionedUrl($wrapper->entity_reference_multiple[1]->getIdentifier()), ), 'id' => $wrapper->entity_reference_multiple[1]->getIdentifier(), 'links' => array( 'self' => $this->handler->versionedUrl($wrapper->entity_reference_multiple[1]->getIdentifier()), ), 'type' => 'main', ), ); // Remove the empty fields from the actual response for easier comparison. $included[0]['attributes'] = array_filter($included[0]['attributes']); $included[1]['attributes'] = array_filter($included[1]['attributes']); sort($expected_includes); sort($included); $this->assertEqual($expected_includes, $included); // 4. Assert the nested sparse fieldsets. // Make a request with the include query string. $this->handler->setRequest(Request::create('api/v1.1/main/' . $wrapper->getIdentifier(), array( 'include' => 'entity_reference_single_resource.entity_reference_single_resource', 'fields' => implode(',', array( // Request a field in the first include and another one in the second. 'entity_reference_single_resource.text_single', 'entity_reference_single_resource.entity_reference_single_resource.text_multiple', )), ))); $this->handler->setPath($wrapper->getIdentifier()); $this->formatter->setResource($this->handler); $result = $this->formatter->prepare($this->handler->process()); $this->assertEqual(2, count($result['included'])); // Make sure there is only one field in the nested includes. $this->assertTrue(empty($result['data']['attributes'])); $this->assertFalse(empty($result['data']['relationships'])); $this->assertEqual(array('text_single'), array_keys($result['included'][0]['attributes'])); $this->assertFalse(empty($result['included'][0]['relationships'])); $this->assertEqual(array('text_multiple'), array_keys($result['included'][1]['attributes'])); $this->assertTrue(empty($result['included'][1]['relationships'])); // Make a request with the include query string. $this->handler->setRequest(Request::create('api/v1.1/main/' . $wrapper->getIdentifier(), array( 'include' => 'entity_reference_single_resource.entity_reference_single_resource', 'fields' => implode(',', array( // Request a field in the first include and another one in the second. 'entity_reference_single_resource.entity_reference_single_resource.text_multiple', )), ))); $this->handler->setPath($wrapper->getIdentifier()); $this->formatter->setResource($this->handler); $result = $this->formatter->prepare($this->handler->process()); $this->assertEqual(2, count($result['included'])); // Make sure there is only one field in the nested includes. $this->assertTrue(empty($result['data']['attributes'])); $this->assertFalse(empty($result['data']['relationships'])); $this->assertTrue(empty($result['included'][0]['attributes'])); $this->assertFalse(empty($result['included'][0]['relationships'])); $this->assertTrue(empty($result['included'][1]['relationships'])); $this->assertEqual(array('text_multiple'), array_keys($result['included'][1]['attributes'])); // 6. Assert the attributes for a list. // Make a list request and check the attributes. $this->handler->setRequest(Request::create('')); $this->handler->setPath(''); $this->formatter->setResource($this->handler); $resource_field_collections = $this->handler->process(); $result = drupal_json_decode($this->formatter->format($resource_field_collections)); $this->assertTrue(\Drupal\restful\Plugin\resource\Field\ResourceFieldBase::isArrayNumeric($result['data'])); $this->assertTrue(count($result['data']), count($this->entities)); foreach ($result['data'] as $delta => $row) { // Assert the basic properties of the resource. $this->assertEqual('main', $row['type']); $this->assertEqual($this->entities[$delta]->pid, $row['id']); } $expected = array_filter($expected); $attributes = array_filter($result['data'][2]['attributes']); $this->assertEqual($expected, $attributes); // 5. Assert the relationships for a list. $this->handler->setRequest(Request::create('', array('include' => 'entity_reference_single_resource'))); $this->handler->setPath(''); $this->formatter->setResource($this->handler); $resource_field_collections = $this->handler->process(); $result = drupal_json_decode($this->formatter->format($resource_field_collections)); $included = $result['included']; $this->assertEqual(2, count($included)); $expected_includes = array( array( 'type' => 'main', 'id' => (string) $wrapper->entity_reference_single->entity_reference_single->getIdentifier(), 'links' => array( 'self' => $this->handler->versionedUrl($wrapper->entity_reference_single->entity_reference_single->getIdentifier()), ), 'attributes' => array( 'id' => $wrapper->entity_reference_single->entity_reference_single->getIdentifier(), 'label' => $wrapper->entity_reference_single->entity_reference_single->label(), 'self' => $this->handler->versionedUrl($wrapper->entity_reference_single->entity_reference_single->getIdentifier()), ), ), array( 'type' => 'main', 'id' => (string) $wrapper->entity_reference_single->getIdentifier(), 'links' => array( 'self' => $this->handler->versionedUrl($wrapper->entity_reference_single->getIdentifier()), ), 'attributes' => array( 'entity_reference_single' => $wrapper->entity_reference_single->entity_reference_single->getIdentifier(), 'id' => $wrapper->entity_reference_single->getIdentifier(), 'label' => $wrapper->entity_reference_single->label(), 'self' => $this->handler->versionedUrl($wrapper->entity_reference_single->getIdentifier()), ), 'relationships' => array( 'entity_reference_single_resource' => array( 'data' => array( 'id' => $wrapper->entity_reference_single->entity_reference_single->getIdentifier(), 'type' => 'main', ), 'links' => array( 'self' => $this->handler->versionedUrl($wrapper->entity_reference_single->getIdentifier() . '/relationships/entity_reference_single_resource'), 'related' => $this->handler->versionedUrl('', array('query' => array('filter' => array('entity_reference_single_resource' => $wrapper->entity_reference_single->entity_reference_single->getIdentifier())))), ), ), ), ), ); // Remove the empty fields from the actual response for easier comparison. $included[0]['attributes'] = array_filter($included[0]['attributes']); $included[1]['attributes'] = array_filter($included[1]['attributes']); $this->assertEqual($expected_includes, $included); // 7. Assert request processing. $request = Request::create('', array( 'fields' => 'first.second.third.fourth,first.third.fourth', 'include' => 'fifth.sixth.seventh', )); $input = $request->getParsedInput(); $this->assertEqual($input['fields'], 'first,first.second,first.second.third,first.second.third.fourth,first.third,first.third.fourth'); $this->assertEqual($input['include'], 'fifth,fifth.sixth,fifth.sixth.seventh'); // 8. Assert generic reference resource field. // Add some data to the DB table. $record = array( 'str_field' => $this->randomName(), 'int_field' => mt_rand(1, 10), 'serialized_field' => serialize(array()), ); drupal_write_record('restful_test_db_query', $record); $handler = restful()->getResourceManager()->getPlugin('main:1.8'); $handler->setRequest(Request::create('', array( 'include' => 'random_rel', 'range' => 1, ))); $handler->setPath(1); $formatter = restful()->getFormatterManager()->getPlugin('json_api'); $formatter->setResource($handler); $result = $formatter->prepare($handler->process()); $this->assertEqual($result['included'][0]['attributes']['id'], 1); } /** * Test pagination. */ public function testPagination() { $entities = $this->createEntities(); $count = count($entities); for ($index = 0; $index < 3; $index++) { $entities[] = entity_create('entity_test', array( 'name' => 'main', 'uid' => $this->account->uid, )); $entities[$count + $index]->save(); } // 1. Test pagination links in the first page. $range = 2; $this->handler->setRequest(Request::create('', array('range' => $range))); $this->handler->setPath(''); $this->formatter->setResource($this->handler); $resource_field_collections = $this->handler->process(); $result = drupal_json_decode($this->formatter->format($resource_field_collections)); $links = $result['links']; $expected_links = array( 'self' => $this->handler->versionedUrl('', array( 'query' => array( 'page' => array( 'number' => 1, 'size' => $range, ), ), )), 'first' => $this->handler->versionedUrl('', array('query' => array('page' => array('size' => $range)))), 'last' => $this->handler->versionedUrl('', array( 'query' => array( 'page' => array( 'number' => 5, 'size' => $range, ), ), )), 'next' => $this->handler->versionedUrl('', array( 'query' => array( 'page' => array( 'number' => 2, 'size' => $range, ), ), )), ); $this->assertEqual($links, $expected_links); // 2. Test pagination links in the middle page. $range = 2; $this->handler->setRequest(Request::create('', array( 'range' => $range, 'page' => 2, ))); $this->handler->setPath(''); $this->formatter->setResource($this->handler); $resource_field_collections = $this->handler->process(); $result = drupal_json_decode($this->formatter->format($resource_field_collections)); $links = $result['links']; $expected_links = array( 'self' => $this->handler->versionedUrl('', array( 'query' => array( 'page' => array( 'number' => 2, 'size' => $range, ), ), )), 'first' => $this->handler->versionedUrl('', array('query' => array('page' => array('size' => $range)))), 'previous' => $this->handler->versionedUrl('', array( 'query' => array( 'page' => array( 'number' => 1, 'size' => $range, ), ), )), 'last' => $this->handler->versionedUrl('', array( 'query' => array( 'page' => array( 'number' => 5, 'size' => $range, ), ), )), 'next' => $this->handler->versionedUrl('', array( 'query' => array( 'page' => array( 'number' => 3, 'size' => $range, ), ), )), ); $this->assertEqual($links, $expected_links); // 3. Test pagination links in the last page. $range = 2; $this->handler->setRequest(Request::create('', array( 'page' => array( 'size' => $range, 'number' => 5, ), ))); $this->handler->setPath(''); $this->formatter->setResource($this->handler); $resource_field_collections = $this->handler->process(); $result = drupal_json_decode($this->formatter->format($resource_field_collections)); $links = $result['links']; $expected_links = array( 'self' => $this->handler->versionedUrl('', array( 'query' => array( 'page' => array( 'size' => $range, 'number' => 5, ), ), )), 'first' => $this->handler->versionedUrl('', array('query' => array('page' => array('size' => $range)))), 'previous' => $this->handler->versionedUrl('', array( 'query' => array( 'page' => array( 'size' => $range, 'number' => 4, ), ), )), 'last' => $this->handler->versionedUrl('', array( 'query' => array( 'page' => array( 'size' => $range, 'number' => 5, ), ), )), ); $this->assertEqual($links, $expected_links); } /** * Test alternative ID field. */ public function testAlternativeIdField() { $entities = $this->createEntities(); $resource_manager = restful()->getResourceManager(); $format_manager = restful()->getFormatterManager(); $formatter = $format_manager->negotiateFormatter(NULL, 'json_api'); $resource_manager->clearPluginCache('main:1.7'); $handler = $resource_manager->getPlugin('main:1.7'); // Add files and taxonomy term references. $query = new EntityFieldQuery(); $result = $query->entityCondition('entity_type', 'taxonomy_term') ->entityCondition('bundle', 'test_vocab') ->execute(); $tids = array_keys($result['taxonomy_term']); $images = array(); foreach ($this->drupalGetTestFiles('image') as $file) { $file = file_save($file); $images[] = $file->fid; } $entities[2]->term_single[LANGUAGE_NONE][] = array('tid' => $tids[0]); $entities[2]->term_multiple[LANGUAGE_NONE][] = array('tid' => $tids[1]); $entities[2]->term_multiple[LANGUAGE_NONE][] = array('tid' => $tids[2]); $entities[2]->file_single[LANGUAGE_NONE][] = array( 'fid' => $images[0], 'display' => TRUE, ); $entities[2]->file_multiple[LANGUAGE_NONE][] = array( 'fid' => $images[1], 'display' => TRUE, ); $entities[2]->file_multiple[LANGUAGE_NONE][] = array( 'fid' => $images[2], 'display' => TRUE, ); entity_save('entity_test', $entities[2]); $handler->setRequest(Request::create('api/main/v1.7/' . $entities[2]->uuid)); $handler->setPath($entities[2]->uuid); $formatter->setResource($handler); $response = $formatter->prepare($handler->process()); $result = $response['data']['attributes']; // Make sure that IDs are UUID. $this->assertEqual(count(entity_uuid_load('file', array($result['file_single']))), 1, 'UUID correctly loaded for: file_single'); $this->assertEqual(count(entity_uuid_load('file', $result['file_multiple'])), 2, 'UUID correctly loaded for: file_multiple'); $this->assertEqual(count(entity_uuid_load('taxonomy_term', array($result['term_single']))), 1, 'UUID correctly loaded for: term_single'); $this->assertEqual(count(entity_uuid_load('taxonomy_term', $result['term_multiple'])), 2, 'UUID correctly loaded for: term_multiple'); $this->assertEqual(count(entity_uuid_load('entity_test', array($result['entity_reference_single']))), 1, 'UUID correctly loaded for: entity_reference_single'); $this->assertEqual(count(entity_uuid_load('entity_test', $result['entity_reference_multiple'])), 2, 'UUID correctly loaded for: entity_reference_multiple'); // Assert relationship. $relationships = $response['data']['relationships']; $this->assertEqual($relationships['entity_reference_resource']['data']['id'], $entities[0]->uuid); $this->assertEqual($relationships['entity_reference_resource']['data']['type'], 'main'); // Assert that the self link contains the UUID. $this->assertTrue(strpos($response['links']['self'], $entities[2]->uuid) !== FALSE, 'UUID found in self link.'); // Test that if referencedIdProperty and idField don't match, embedding does // not happen. variable_set('restful_test_alternative_id_error', TRUE); $resource_manager->clearPluginCache('main:1.7'); $handler = $resource_manager->getPlugin('main:1.7'); $handler->setRequest(Request::create('api/main/v1.7/' . $entities[2]->uuid, array( 'include' => 'entity_reference_resource_error', ))); $handler->setPath($entities[2]->uuid); $format_manager->setResource($handler); $response = $format_manager->negotiateFormatter(NULL, 'json_api') ->prepare($handler->process()); $result = $response['data']['attributes']; // Make sure the entity_reference_resource_error is NULL. $this->assertNull($result['entity_reference_resource_error'], 'Entity cannot be loaded with uuid when there is no idField.'); variable_del('restful_test_alternative_id_error'); // Clear caches to remove error field. $resource_manager->clearPluginCache('main:1.7'); $handler = $resource_manager->getPlugin('main:1.7'); // Filter the entities by the reference. $handler->setRequest(Request::create('api/main/v1.7', array( 'filter' => array('entity_reference_single' => $entities[0]->uuid), ))); $handler->setPath(''); $formatter->setResource($handler); $response = $formatter->prepare($handler->process()); // Assert that $entities[2] points to $entities[0] $this->assertEqual($response['data'][0]['id'], $entities[2]->uuid); // Filter the entities by the reference. $handler->setRequest(Request::create('api/main/v1.7', array( 'filter' => array('entity_reference_multiple' => $entities[1]->uuid), ))); $handler->setPath(''); $formatter->setResource($handler); $response = $formatter->prepare($handler->process()); // Assert that $entities[2] points to $entities[0] $this->assertEqual($response['data'][0]['id'], $entities[2]->uuid); } /** * Sub-request parsing. */ public function testSubRequestParsing() { $relationships = array( 'rel1' => array('data' => array('type' => 'tags', 'id' => 7)), 'rel2' => array( 'data' => array( array( 'type' => 'articles', 'id' => 1, ), array( 'type' => 'articles', 'id' => 2, 'meta' => array('subrequest' => array('method' => 'PUT')), ), array( 'type' => 'articles', 'id' => 3, 'meta' => array('subrequest' => array('method' => 'PATCH')), ), array( 'type' => 'articles', 'id' => 'newRel2--1', 'meta' => array( 'subrequest' => array( 'method' => 'POST', 'headers' => array('X-CSRF-Token' => 'my-token'), ), ), ), ), ), ); // Generate 3 random strings. $rnd_text = array_map(array($this, 'randomName'), range(0, 4)); $included = array( array( 'type' => 'articles', 'id' => 1, 'attributes' => array('label' => $rnd_text[0]), 'relationships' => array( 'articles' => array( 'data' => array( 'type' => 'articles', 'id' => 2, ), ), ), ), array( 'type' => 'articles', 'id' => 2, 'attributes' => array('label' => $rnd_text[1]), ), array( 'type' => 'articles', 'id' => 3, 'attributes' => array('label' => $rnd_text[2]), ), array( 'type' => 'articles', 'id' => 'newRel2--1', 'attributes' => array('label' => $rnd_text[3]), 'relationships' => array( 'articles' => array( 'data' => array( 'type' => 'articles', 'id' => 1, ), ), ), ), ); $data = array( array( 'type' => 'articles', 'id' => 5, 'attributes' => array( 'title' => $rnd_text[4], ), 'relationships' => $relationships, ), ); $json_api = array( 'data' => $data, 'included' => $included, ); $parsed_body = restful() ->getFormatterManager() ->getPlugin('json_api') ->parseBody(drupal_json_encode($json_api)); $expected = array( array( 'title' => $rnd_text[4], 'rel1' => array('id' => 7), 'rel2' => array( array('id' => 1), array( 'id' => 2, 'body' => array( 'label' => $rnd_text[1], ), 'request' => array('method' => 'PUT'), ), array( 'id' => 3, 'body' => array( 'label' => $rnd_text[2], ), 'request' => array('method' => 'PATCH'), ), array( 'body' => array( 'label' => $rnd_text[3], 'articles' => array('id' => 1), ), 'request' => array( 'method' => 'POST', 'headers' => array('X-CSRF-Token' => 'my-token'), ), ), ), ), ); $this->assertEqual($expected, $parsed_body); } } ================================================ FILE: tests/RestfulListEntityMultipleBundlesTestCase.test ================================================ 'List entity with multiple bundles', 'description' => 'Test listing an entity with multiple bundles.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_test'); } /** * Test listing an entity with multiple bundles. */ public function testList() { $user1 = $this->drupalCreateUser(); /* @var \Entity $entity1 */ $entity1 = entity_create('entity_test', array('name' => 'main', 'uid' => $user1->uid)); $entity1->save(); $entity2 = entity_create('entity_test', array('name' => 'main', 'uid' => $user1->uid)); /* @var \Entity $entity2 */ $entity2->save(); $entity3 = entity_create('entity_test', array('name' => 'test', 'uid' => $user1->uid)); /* @var \Entity $entity3 */ $entity3->save(); $handler = restful()->getResourceManager()->getPlugin('entity_tests:1.0'); $formatter = restful()->getFormatterManager()->getPlugin('json'); $formatter->setResource($handler); $expected_result = array( array( 'id' => $entity1->pid, 'label' => 'Main test type', 'self' => url('api/v1.0/entity_tests/' . $entity1->pid, array('absolute' => TRUE)), 'main_bundle' => array( 'id' => $entity1->pid, 'label' => 'Main test type', 'self' => url('api/v1.0/main/' . $entity1->pid, array('absolute' => TRUE)), ), 'tests_bundle' => NULL, ), array( 'id' => $entity2->pid, 'label' => 'Main test type', 'self' => url('api/v1.0/entity_tests/' . $entity2->pid, array('absolute' => TRUE)), 'main_bundle' => array( 'id' => $entity2->pid, 'label' => 'Main test type', 'self' => url('api/v1.0/main/' . $entity2->pid, array('absolute' => TRUE)), ), 'tests_bundle' => NULL, ), array( 'id' => $entity3->pid, 'label' => 'label', 'self' => url('api/v1.0/entity_tests/' . $entity3->pid, array('absolute' => TRUE)), 'main_bundle' => NULL, 'tests_bundle' => array( 'type' => 'test', 'id' => $entity3->pid, 'label' => 'label', 'self' => url('api/v1.0/tests/' . $entity3->pid, array('absolute' => TRUE)), ), ), ); $result = $formatter->prepare($handler->doGet()); $this->assertEqual($result['data'], $expected_result); } } ================================================ FILE: tests/RestfulListTestCase.test ================================================ 'List entities', 'description' => 'Test the listing of entities.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_example', 'restful_test'); } /** * Test the sorting of entities. */ public function testSort() { // Allow the anonymous users access to the author field. user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array('access user profiles' => TRUE)); $user1 = $this->drupalCreateUser(); $user2 = $this->drupalCreateUser(); $settings = array('type' => 'article'); $info = array( 'abc' => $user1->uid, 'xyz' => $user1->uid, 'efg' => $user2->uid, ); $nodes = array(); foreach ($info as $title => $uid) { $settings['title'] = $title; $settings['uid'] = $uid; $node = $this->drupalCreateNode($settings); $nodes[$title] = $node->nid; } // Add unpublished node, to confirm it is not listed. $settings['status'] = NODE_NOT_PUBLISHED; $this->drupalCreateNode($settings); $resource_manager = restful()->getResourceManager(); $handler = $resource_manager->getPlugin('articles:1.0'); $query['fields'] = 'id,label'; $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); // No sorting (default sorting). $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $expected_result = array( array( 'id' => $nodes['abc'], 'label' => 'abc', ), array( 'id' => $nodes['xyz'], 'label' => 'xyz', ), array( 'id' => $nodes['efg'], 'label' => 'efg', ), ); $this->assertEqual($result, $expected_result, 'No sorting (default sorting).'); // Sort by ID descending. $query['sort'] = '-id'; $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $expected_result = array( array( 'id' => $nodes['efg'], 'label' => 'efg', ), array( 'id' => $nodes['xyz'], 'label' => 'xyz', ), array( 'id' => $nodes['abc'], 'label' => 'abc', ), ); $this->assertEqual($result, $expected_result, 'Sort by ID descending.'); // Sort by label ascending. $query['sort'] = 'label'; $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $expected_result = array( array( 'id' => $nodes['abc'], 'label' => 'abc', ), array( 'id' => $nodes['efg'], 'label' => 'efg', ), array( 'id' => $nodes['xyz'], 'label' => 'xyz', ), ); $this->assertEqual($result, $expected_result, 'Sort by label ascending.'); // Sort by label and by ID. For that we add another node titled "abc". $settings = array( 'type' => 'article', 'title' => 'abc', ); $node = $this->drupalCreateNode($settings); $query['sort'] = 'label,id'; $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $expected_result = array( array( 'id' => $nodes['abc'], 'label' => 'abc', ), array( 'id' => $node->nid, 'label' => 'abc', ), array( 'id' => $nodes['efg'], 'label' => 'efg', ), array( 'id' => $nodes['xyz'], 'label' => 'xyz', ), ); $this->assertEqual($result, $expected_result, 'Sort by ID and by label.'); // Test default sorting from plugin definition; by label, then by reverse // id. $handler = $resource_manager->getPlugin('test_articles:1.1'); unset($query['sort']); $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $expected_result = array( array( 'id' => $node->nid, 'label' => 'abc', 'self' => $handler->versionedUrl($node->nid), ), array( 'id' => $nodes['abc'], 'label' => 'abc', 'self' => $handler->versionedUrl($nodes['abc']), ), array( 'id' => $nodes['efg'], 'label' => 'efg', 'self' => $handler->versionedUrl($nodes['efg']), ), array( 'id' => $nodes['xyz'], 'label' => 'xyz', 'self' => $handler->versionedUrl($nodes['xyz']), ), ); $this->assertEqual($result, $expected_result, 'Default sort by ID and by label.'); // Test that the default sort can be overridden. $query['sort'] = 'id'; $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $expected_result = array( array( 'id' => $nodes['abc'], 'label' => 'abc', 'self' => $handler->versionedUrl($nodes['abc']), ), array( 'id' => $nodes['xyz'], 'label' => 'xyz', 'self' => $handler->versionedUrl($nodes['xyz']), ), array( 'id' => $nodes['efg'], 'label' => 'efg', 'self' => $handler->versionedUrl($nodes['efg']), ), array( 'id' => $node->nid, 'label' => 'abc', 'self' => $handler->versionedUrl($node->nid), ), ); $this->assertEqual($result, $expected_result, 'Sort by ID, overriding default sort.'); // Illegal sort property. $query['sort'] = 'wrong_key'; $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); try { restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('Illegal sort property used.'); } catch (BadRequestException $e) { $this->pass('Exception thrown on illegal sort property.'); } // Illegal sort property, descending. $query['sort'] = '-wrong_key'; $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); try { restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('Illegal sort property, descending, used.'); } catch (BadRequestException $e) { $this->pass('Exception thrown on illegal sort property, descending.'); } // Test valid sort with sort params disabled. $resource_manager->clearPluginCache($handler->getPluginId()); $plugin_definition = $handler->getPluginDefinition(); $plugin_definition['dataProvider']['urlParams'] = array( 'filter' => TRUE, 'sort' => FALSE, 'range' => TRUE, ); $handler->setPluginDefinition($plugin_definition); // Re-instantiate the data provider. $handler->setDataProvider(NULL); $query['sort'] = 'label,id'; $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); try { restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('Exception not raised for disabled sort parameter.'); } catch (UnprocessableEntityException $e) { $this->pass('Exception raised for disabled sort parameter.'); } // Test the range overrides. unset($query['sort']); $query['range'] = 2; $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual(count($result), $query['range'], 'Range parameter overridden correctly'); // Test the max cap in the the annotation configuration. $resource_manager->clearPluginCache($handler->getPluginId()); $plugin_definition = $handler->getPluginDefinition(); $plugin_definition['range'] = 1; $handler->setPluginDefinition($plugin_definition); $query['range'] = 2; $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doGet('', $query), 'json')); $this->assertEqual(count($result['data']), $query['range'], 'Range is limited to the configured maximum.'); // Test invalid range. $query['range'] = $this->randomName(); $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); try { restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('Exception not raised on invalid range parameter.'); } catch (BadRequestException $e) { $this->pass('Exception raised on invalid range parameter.'); } // Test valid range with range params disabled. $resource_manager->clearPluginCache($handler->getPluginId()); $plugin_definition = $handler->getPluginDefinition(); $plugin_definition['dataProvider']['urlParams'] = array( 'filter' => TRUE, 'sort' => TRUE, 'range' => FALSE, ); // Unset the previously added range. unset($plugin_definition['range']); $handler->setPluginDefinition($plugin_definition); // Re-instantiate the data provider. $handler->setDataProvider(NULL); $query['range'] = 2; $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); try { restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('Exception not raised for disabled range parameter.'); } catch (UnprocessableEntityException $e) { $this->pass('Exception raised for disabled range parameter.'); } // Sort by an entity metadata wrapper property that is different from the // DB column name. $handler = $resource_manager->getPlugin('articles:1.5'); $query = array('sort' => '-user'); $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual($result[0]['user']['id'], 3, 'List sorted by the "author" entity metadata wrapper property, which maps to the "uid" DB column name.'); $query = array('sort' => 'static'); $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); try { restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('Exception not thrown for invalid sort.'); } catch (BadRequestException $e) { $this->pass('Exception thrown for invalid sort.'); } } /** * Test filtering. */ public function testFilter() { $resource_manager = restful()->getResourceManager(); $user1 = $this->drupalCreateUser(); $user2 = $this->drupalCreateUser(); $this->addIntegerFields(); $info = array( array( 'title' => 'abc', 'integer_single' => 1, 'integer_multiple' => array(1, 2, 3), 'uid' => $user1->uid, ), array( 'title' => 'another abc', 'integer_single' => 5, 'integer_multiple' => array(3, 4, 5), 'uid' => $user1->uid, ), array( 'title' => 'last', 'integer_single' => NULL, 'integer_multiple' => array(), 'uid' => $user2->uid, ), ); $nodes = array(); foreach ($info as $row) { $title = $row['title']; $settings = array( 'type' => 'article', 'title' => $title, ); $settings['integer_single'][LANGUAGE_NONE][0]['value'] = $row['integer_single']; foreach ($row['integer_multiple'] as $key => $value) { $settings['integer_multiple'][LANGUAGE_NONE][$key]['value'] = $value; } $settings['uid'] = $row['uid']; $node = $this->drupalCreateNode($settings); $nodes[$title] = $node->nid; } $handler = $resource_manager->getPlugin('test_articles:1.2'); $fields = array( 'id', 'label', 'integer_single', 'intger_multiple', ); $query['fields'] = implode(',', $fields); // Single value property. $query['filter'] = array('label' => 'abc'); $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual($result[0]['id'], $nodes['abc'], 'Filter list by single value property.'); // Assert count is correct. $formatter_handler = restful() ->getFormatterManager() ->getPlugin('hal_json'); $instance_configuration = $formatter_handler->getConfiguration(); $formatter_handler->setConfiguration(array_merge($instance_configuration ?: array(), array( 'resource' => $handler, ))); $output = $formatter_handler->prepare($result); $this->assertEqual($output['count'], 1, '"count" property is correct.'); // Single value field. $resource_manager->clearPluginCache($handler->getPluginId()); $handler = $resource_manager->getPlugin('test_articles:1.2'); $query['filter'] = array('integer_single' => '1'); $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual($result[0]['id'], $nodes['abc'], 'Filter list by Single value field.'); // LIKE operator. $input['filter'] = array( 'label' => array( 'value' => '%nothe%', 'operator' => 'LIKE', ), ); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doGet('', $input), 'json')); $result = $result['data']; $this->assertEqual($result[0]['id'], $nodes['another abc'], 'Filter list using LIKE operator.'); // STARTS_WITH operator. $input['filter'] = array( 'label' => array( 'value' => 'las', 'operator' => 'STARTS_WITH', ), ); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doGet('', $input), 'json')); $result = $result['data']; $this->assertEqual($result[0]['id'], $nodes['last'], 'Filter list using STARTS_WITH operator.'); // CONTAINS operator. $input['filter'] = array( 'label' => array( 'value' => 'bc', 'operator' => 'CONTAINS', ), ); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doGet('', $input), 'json')); $result = $result['data']; // Sort the results before checking it. usort($result, function ($a, $b) { return strcmp($a['label'], $b['label']); }); $this->assertEqual($result[0]['id'], $nodes['abc'], 'Filter list using CONTAINS operator.'); $this->assertEqual($result[1]['id'], $nodes['another abc'], 'Filter list using CONTAINS operator.'); // Multiple value field. $query['filter'] = array( 'integer_multiple' => array( 'value' => array(4, 5), 'operator' => 'BETWEEN', ), ); $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual($result[0]['id'], $nodes['another abc'], 'Filter list by multiple value field.'); // Invalid key. $query['filter'] = array('invalid_key' => '3'); $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); try { restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('No exception was thrown on invalid key for filter.'); } catch (BadRequestException $e) { $this->pass('Correct exception was thrown on invalid key for filter.'); } catch (\Exception $e) { $this->fail('Incorrect exception was thrown on invalid key for filter.'); } // Assert filtering doesn't apply for non-list request // (e.g. /api/v1.0/articles/1?filter[label]=foo), as this might be called // from a formatter plugin, after RESTful's error handling has finished. $query_string = array('filter' => array('invalid-key' => 'foo')); $result = $this->httpRequest('api/v1.0/articles/1', RequestInterface::METHOD_GET, $query_string); $this->assertEqual($result['code'], '200', 'Invalid filter key was ignored on non-list query.'); // Test multiple filters on the same field. $query = array( 'filter' => array( 'integer_single' => array( 'value' => array(1, 4), 'operator' => array('>', '<>'), ), ), ); $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual($nodes['another abc'], $result[0]['id']); $query = array( 'filter' => array( 'integer_single' => array( 'value' => array(1, 5), 'operator' => array('>', '<>'), ), ), ); $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual($result, array()); $query = array( 'filter' => array( 'integer_multiple' => array( 'value' => array(3, 4), ), ), ); $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual($nodes['another abc'], $result[0]['id']); // Test valid filter with filter params disabled. $resource_manager->clearPluginCache($handler->getPluginId()); $plugin_definition = $handler->getPluginDefinition(); $plugin_definition['dataProvider']['urlParams'] = array( 'filter' => FALSE, 'sort' => TRUE, 'range' => TRUE, ); $handler->setPluginDefinition($plugin_definition); $handler->setDataProvider(NULL); $query['filter'] = array('label' => 'abc'); $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); try { restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('Exception not raised for disabled filter parameter.'); } catch (UnprocessableEntityException $e) { $this->pass('Exception raised for disabled filter parameter.'); } // Filter by an entity metadata wrapper property that is different from the // DB column name. $handler = $resource_manager->getPlugin('articles:1.5'); $query = array( 'filter' => array( 'user' => array( 'value' => $user1->uid, ), ), ); $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual(count($result), 2, 'List filtered by the "author" entity metadata wrapper property, which maps to the "uid" DB column name.'); $query = array( 'filter' => array( 'static' => array( 'value' => 0, ), ), ); $handler->setRequest(Request::create('', $query, RequestInterface::METHOD_GET)); $handler->setPath(''); try { restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('Illegal filter property used.'); } catch (BadRequestException $e) { $this->pass('Exception thrown on illegal filter property.'); } } /** * Test pagination. */ public function testPagination() { $resource_manager = restful()->getResourceManager(); foreach (range(1, 9) as $key) { $settings = array( 'type' => 'article', 'title' => $key, ); $this->drupalCreateNode($settings); } $handler = $resource_manager->getPlugin('articles:1.0'); $formatter_handler = restful() ->getFormatterManager() ->negotiateFormatter(''); $instance_configuration = $formatter_handler->getConfiguration(); $formatter_handler->setConfiguration(array_merge($instance_configuration ?: array(), array( 'resource' => $handler, ))); // Set a smaller range for the pagination. $data_provider = $handler->getDataProvider(); $data_provider->setRange(3); // Check pagination of first page. $handler->setRequest(Request::create('', array('page' => 1), RequestInterface::METHOD_GET)); $handler->setPath(''); $result = drupal_json_decode($formatter_handler->format($handler->process())); $this->assertEqual(count($result['data']), 3); $this->assertTrue($result['next'], '"Next" link exists on the first page.'); $this->assertTrue(empty($result['previous']), '"Previous" link does not exist on the first page.'); // Check pagination of middle pages. $handler->setRequest(Request::create('', array('page' => 2), RequestInterface::METHOD_GET)); $result = drupal_json_decode($formatter_handler->format($handler->process())); $this->assertTrue($result['next'], '"Next" link exists on the middle page.'); $this->assertEqual($result['next']['href'], $handler->versionedUrl('', array( 'query' => array('page' => array('number' => 3)), ))); $this->assertTrue($result['previous'], '"Previous" link exists on the middle page.'); $this->assertEqual($result['previous']['href'], $handler->versionedUrl('', array( 'query' => array('page' => array('number' => 1)), ))); // Check pagination of last page. $handler->setRequest(Request::create('', array('page' => 3), RequestInterface::METHOD_GET)); $result = drupal_json_decode($formatter_handler->format($handler->process())); $this->assertTrue(empty($result['next']), '"Next" link does not exist on the last page.'); $this->assertTrue($result['previous'], '"Previous" link exists on the last page.'); // Check other query strings are retained in the _links. $handler->setRequest(Request::create('', array( 'page' => 3, 'sort' => '-id', ), RequestInterface::METHOD_GET)); $result = drupal_json_decode($formatter_handler->format($handler->process())); $this->assertTrue(strpos($result['previous']['href'], 'sort=-id'), 'Query strings are retained in the _links.'); // Check pagination with non-numeric value. $handler->setRequest(Request::create('', array('page' => 'string'), RequestInterface::METHOD_GET)); try { restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('No exception thrown for pagination with non-numeric value.'); } catch (BadRequestException $e) { $this->pass('Correct exception thrown for pagination with non-numeric value.'); } catch (\Exception $e) { $this->fail('Incorrect exception thrown for pagination with non-numeric value.'); } // Check pagination with 0 value. $handler->setRequest(Request::create('', array('page' => 0), RequestInterface::METHOD_GET)); try { restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('No exception thrown for pagination with 0 value.'); } catch (BadRequestException $e) { $this->pass('Correct exception thrown for pagination with 0 value.'); } catch (\Exception $e) { $this->fail('Incorrect exception thrown for pagination with 0 value.'); } // Check pagination with high number, where there are not items, yielded no // results, but is a valid call. $handler->setRequest(Request::create('', array('page' => 100), RequestInterface::METHOD_GET)); $result = drupal_json_decode($formatter_handler->format($handler->process())); $this->assertEqual($result['data'], array(), 'pagination with high number, where there are not items yielded no results.'); // Check total number of results. $handler->setRequest(Request::create('', array('page' => 3), RequestInterface::METHOD_GET)); $result = drupal_json_decode($formatter_handler->format($handler->process())); $this->assertEqual($result['count'], 9, 'Total count exists and is correct.'); } /** * Helper function; Add single and multiple integer fields. */ private function addIntegerFields() { // Integer - single. $field = array( 'field_name' => 'integer_single', 'type' => 'number_integer', 'entity_types' => array('node'), 'cardinality' => 1, ); field_create_field($field); $instance = array( 'field_name' => 'integer_single', 'bundle' => 'article', 'entity_type' => 'node', 'label' => t('Integer single'), ); field_create_instance($instance); // Integer - multiple. $field = array( 'field_name' => 'integer_multiple', 'type' => 'number_integer', 'entity_types' => array('node'), 'cardinality' => FIELD_CARDINALITY_UNLIMITED, ); field_create_field($field); $instance = array( 'field_name' => 'integer_multiple', 'bundle' => 'article', 'entity_type' => 'node', 'label' => t('Integer multiple'), ); field_create_instance($instance); } /** * Test error handling when no access is granted to an entity in a list. */ public function testAccessHandling() { $resource_manager = restful()->getResourceManager(); $settings = array( 'type' => 'article', ); $node1 = $this->drupalCreateNode($settings); $node2 = $this->drupalCreateNode($settings); $node3 = $this->drupalCreateNode($settings); $user1 = $this->drupalCreateUser(); // Deny access via hook_node_access() to a specific node. restful_test_deny_access_node($node2->nid); variable_set('restful_show_access_denied', TRUE); $handler = $resource_manager->getPlugin('articles:1.0'); $handler->setAccount($user1); $handler->setRequest(Request::create('api/articles/v1.0')); $handler->setPath(''); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $this->assertEqual(count($result['data']), 2, 'List returned and ignored un-accessible entity.'); $this->assertEqual(count($result['denied']), 1, 'A denied entity was detected.'); // Get a list with specific IDs. $ids = array( $node1->nid, $node2->nid, $node3->nid, ); $handler->setRequest(Request::create('api/articles/v1.0' . implode(',', $ids))); $handler->setPath(implode(',', $ids)); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $this->assertEqual(count($result['data']), 2, 'List of specific items returned and ignored un-accessible entity.'); $this->assertEqual(count($result['denied']), 1, 'A denied entity was detected.'); } /** * Test node count and pager when there are unpublished nodes. */ public function testCountWithUnpublished() { foreach (range(1, 9) as $key) { $settings = array( 'type' => 'article', 'title' => $key, 'status' => NODE_PUBLISHED, ); $this->drupalCreateNode($settings); } foreach (range(1, 3) as $key) { $settings = array( 'type' => 'article', 'title' => $key, 'status' => NODE_NOT_PUBLISHED, ); $this->drupalCreateNode($settings); } $handler = restful()->getResourceManager()->getPlugin('articles:1.0'); $formatter = restful()->getFormatterManager()->getPlugin('json'); $formatter->setResource($handler); // Set a range for pagination to check only . $handler->getDataProvider()->setRange(9); // Check pagination of first page. $result = $handler->doGet('', array('page' => 1)); $output = drupal_json_decode($formatter->format($result)); $this->assertEqual(count($result), 9, 'Number of results displayed'); $this->assertEqual(count($result), $handler->getDataProvider() ->count(), 'Count of results takes into account unpublished nodes'); $this->assertTrue(empty($output['next']), '"Next" link does not exist on the first page.'); $this->assertTrue(empty($output['previous']), '"Previous" link does not exist on the first page.'); } } ================================================ FILE: tests/RestfulPassThroughTestCase.test ================================================ 'Pass through', 'description' => 'Test the pass-through configuration.', 'group' => 'RESTful', ); } /** * The name of the test table. * * @var string */ protected $tableName = 'restful_test_db_query'; function setUp() { parent::setUp('restful_test', 'restful_example'); } /** * Test the pass-through property for an entity. */ public function testEntity() { $handler = restful_get_restful_handler('articles', 1, 1); $base_request = array('label' => $this->randomName()); $this->validatePassThrough($handler, $base_request, TRUE); } /** * Test the pass-through property for an entity. */ public function testDbQuery() { $handler = restful_get_restful_handler('db_query_test'); $base_request = array( 'integer' => 1, 'string' => $this->randomName(), ); $this->validatePassThrough($handler, $base_request); } /** * Validate the pass-through for different handlers. * * @param RestfulInterface $handler * The handler object. * @param array $base_request * The base request to create or update. */ protected function validatePassThrough(\RestfulInterface $handler, array $base_request = array()) { $request = $base_request; $public_fields = $handler->getPublicFields(); $public_fields['foo'] = array( 'create_or_update_passthrough' => TRUE, ); $handler->setPublicFields($public_fields); $result = $handler->post('', $request); $this->assertTrue($result[0]['id'], 'Object was created without a pass-through public field.'); $request['foo'] = 'bar'; $result = $handler->post('', $request); $this->assertTrue($result[0]['id'], 'Object was created with a pass-through public field.'); // Assert update. $id = $result[0]['id']; $result = $handler->put($id, $request); $this->assertTrue($result[0]['id'], 'Object was updated with a pass-through public field.'); } } ================================================ FILE: tests/RestfulRateLimitTestCase.test ================================================ 'Rate limits', 'description' => 'Test the rate limit feature.', 'group' => 'RESTful', ); } public function setUp() { parent::setUp('restful_example'); $settings = array('type' => 'article'); $titles = array( 'abc', 'xyz', 'efg', ); foreach ($titles as $title) { $settings['title'] = $title; $node = $this->drupalCreateNode($settings); $nodes[$title] = $node->nid; } } /** * Tests global rate limits. */ public function testGlobalLimits() { // Test the global limit. variable_set('restful_global_rate_limit', 1); // P3D for 3 days period. See // http://php.net/manual/en/class.dateinterval.php for more information // about the interval format. variable_set('restful_global_rate_period', 'P3D'); $account = $this->drupalCreateUser(); $this->roleExecute($account, 1, array('articles', 1, 0)); } /** * Tests the rate limits and its expiration feature. */ public function testLimits() { variable_del('restful_global_rate_limit'); variable_del('restful_global_rate_period'); // This handler has a limit of 2 requests for the anonymous user. $account = drupal_anonymous_user(); $this->roleExecute($account, 2, array('articles', 1, 4)); // This handler has a limit of 3 requests for the authenticated user. $account = $this->drupalCreateUser(); $this->roleExecute($account, 3, array('articles', 1, 4)); // Now that the limit has been reached for $account. Fake expiration and see // that the limit has been renewed. $query = new \EntityFieldQuery(); $results = $query ->entityCondition('entity_type', 'rate_limit') ->entityCondition('bundle', 'request') ->propertyCondition('identifier', 'articles:1.4:request:' . $account->uid) ->execute(); $rl = entity_load_single('rate_limit', key($results['rate_limit'])); $rl->timestamp = REQUEST_TIME - 2; $rl->expiration = REQUEST_TIME - 1; $rl->save(); $this->roleExecute($account, 3, array('articles', 1, 4)); } /** * Tests the total amount of allowed calls and the following fail. * * @param object $account * The user account object. * @param int $limit * The number of calls allowed for a user with the same roles as $account. * @param array $resource_options * Array of options as received in restful_get_restful_handler. */ protected function roleExecute($account, $limit, array $resource_options) { $resource_manager = restful()->getResourceManager(); $handler = $resource_manager->getPlugin($resource_options[0] . ':' . $resource_options[1] . '.' . $resource_options[2]); $handler->setAccount($account); // Test rate limits. $handler->setRequest(Request::create('')); $handler->setPath(''); for ($count = 0; $count < $limit; $count++) { try { restful()->getFormatterManager()->format($handler->process(), 'json'); $this->pass('The rate limit authorized the request.'); } catch (FloodException $e) { $this->fail('The rate limit did not authorize the request.'); } } try { restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('The rate limit authorized the request.'); } catch (FloodException $e) { $this->pass('The rate limit did not authorize the request.'); $headers = $e->getHeaders(); $this->assertTrue(in_array('Retry-After', array_keys($headers)), 'Retry-After header found after rate limit exception.'); $this->assertTrue(new \DateTime($headers['Retry-After']) > new \DateTime(), 'Retry-After is set to a time in the future.'); } } } ================================================ FILE: tests/RestfulReferenceTestCase.test ================================================ 'Referenced resources', 'description' => 'Test defining a public field as a resource for base properties (e.g. the UID on the node entity) and fields.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_test'); } /** * Test property reference. */ public function testPropertyReference() { $resource_manager = restful()->getResourceManager(); $user1 = $this->drupalCreateUser(); $settings = array( 'type' => 'article', 'uid' => $user1->uid, ); $node1 = $this->drupalCreateNode($settings); $node2 = $this->drupalCreateNode($settings); variable_set('restful_test_reference_simple', TRUE); $resource_manager->clearPluginCache('test_articles:1.2'); $handler = $resource_manager->getPlugin('test_articles:1.2'); $handler->setRequest(Request::create('api/test_articles/v1.2/' . $node1->nid)); $handler->setPath($node1->nid); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual($result[0]['user']['uid'], $user1->uid, 'Property is not defined as resource, thus the referenced entity appears as the entity.'); variable_set('restful_test_reference_resource', TRUE); $resource_manager->clearPluginCache('test_articles:1.2'); $handler = $resource_manager->getPlugin('test_articles:1.2'); $handler->setRequest(Request::create('api/test_articles/v1.2/' . $node2->nid)); $handler->setPath($node2->nid); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual($result[0]['user']['id'], $user1->uid, 'Property is defined as resource, thus the referenced entity appears as the rendered resource.'); } /** * Test field reference. */ public function testEntityReference() { $resource_manager = restful()->getResourceManager(); restful_test_add_fields(); foreach (array('entity_reference_single', 'entity_reference_multiple') as $field_name) { $field = field_info_field($field_name); $field['settings']['target_type'] = 'node'; field_update_field($field); } $resource_manager->clearPluginCache('main:1.6'); $resource_manager->clearPluginCache('test_articles:1.1'); $handler = $resource_manager->getPlugin('main:1.6'); $node_handler = $resource_manager->getPlugin('test_articles:1.1'); $user1 = $this->drupalCreateUser(); $settings = array( 'type' => 'article', 'uid' => $user1->uid, ); $node1 = $this->drupalCreateNode($settings); $node2 = $this->drupalCreateNode($settings); $user1 = $this->drupalCreateUser(); $entity1 = entity_create('entity_test', array( 'name' => 'main', 'uid' => $user1->uid, )); $entity1->save(); $wrapper = entity_metadata_wrapper('entity_test', $entity1); $wrapper->entity_reference_single->set($node1); $wrapper->entity_reference_multiple->set(array($node1, $node2)); $wrapper->save(); $handler->setRequest(Request::create('api/v1.6/main/' . $entity1->pid)); $handler->setPath($entity1->pid); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $node_handler->setRequest(Request::create('api/v1.1/test_articles' . $node1->nid)); $node_handler->setPath($node1->nid); $response1 = drupal_json_decode(restful() ->getFormatterManager() ->format($node_handler->process(), 'json')); $response1 = $response1['data']; $node_handler->setRequest(Request::create('api/v1.1/test_articles' . $node2->nid)); $node_handler->setPath($node2->nid); $response2 = drupal_json_decode(restful() ->getFormatterManager() ->format($node_handler->process(), 'json')); $response2 = $response2['data']; $expected_result = $response1[0]; $this->assertEqual($result[0]['entity_reference_single_resource'], $expected_result, 'Single reference with resource of another entity has correct response.'); $expected_result = array( $response1[0], $response2[0], ); $this->assertEqual($result[0]['entity_reference_multiple_resource'], $expected_result, 'Multiple reference with resource of another entity has correct response.'); // Test the "fullView" property on a referenced entity. // We change the definition via the handler instead of creating another // plugin. $resource_field = $handler->getFieldDefinitions() ->get('entity_reference_single_resource'); $resource_info = $resource_field->getResource(); $resource_info['fullView'] = FALSE; $resource_field->setResource($resource_info); $handler->getFieldDefinitions() ->set('entity_reference_single_resource', $resource_field); // Clear cache. $handler->setRequest(Request::create('api/v1.6/main/' . $entity1->pid)); $handler->setPath($entity1->pid); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual($result[0]['entity_reference_single_resource'], $node1->nid, '"fullView" disabled is showing only the entity ID.'); } } ================================================ FILE: tests/RestfulRenderCacheTestCase.test ================================================ 'Render Cache', 'description' => 'Test the render cache capabilities.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_test'); } /** * Test Render Cache. */ public function testRenderCache() { $resource_manager = restful()->getResourceManager(); $settings = array('type' => 'article'); $account = $this->drupalCreateUser(); $num_articles = 3; for ($index = 0; $index < $num_articles; $index++) { $settings['title'] = 'Node title ' . $index; $node = $this->drupalCreateNode($settings); $nodes[$node->nid] = $node; } /* @var \Drupal\restful\Plugin\resource\Decorators\CacheDecoratedResourceInterface $handler */ $handler = $resource_manager->getPlugin('test_articles:1.1'); $handler->setAccount($account); $cache = $handler->getCacheController(); // Make sure the cache is activated. $data_provider = $handler->getDataProvider(); $options = $data_provider->getOptions(); $cache_info = $options['renderCache']; $this->assertTrue($cache_info['render'], 'Cache render is activated'); // Empty the cache. $cache->clear('*', TRUE); $this->assertTrue($cache->isEmpty(), 'Cache render is empty.'); // Test that cache is being generated correctly. // Get the articles. drupal_static_reset('Drupal\restful\Resource\ResourceManager::getVersionFromRequest'); $handler->setRequest(Request::create('')); $handler->setPath(''); $formatter = restful() ->getFormatterManager() ->negotiateFormatter(NULL, 'json'); $formatter->setResource($handler); $pre_cache_results = drupal_json_decode($formatter->format($handler->process())); $pre_cache_results = $pre_cache_results['data']; // Make sure some cache entries are generated. $this->assertFalse($cache->isEmpty(), 'Cache render is being populated.'); // Get the cached results. $handler->setRequest(Request::create('')); $handler->setPath(''); $formatter->setResource($handler); $post_cache_results = drupal_json_decode($formatter->format($handler->process())); $post_cache_results = $post_cache_results['data']; // Make sure the cached results are the same as the non-cached results. $this->assertEqual($pre_cache_results[0], $post_cache_results[0], 'Cached data is consistent.'); // Get a cached result directly from the cache backend. $node = reset($nodes); $fragments = new ArrayCollection(array( 'resource' => $handler->getResourceName() . '#' . $node->nid, 'entity' => 'node#' . $node->nid, 'user_id' => (int) $account->uid, 'formatter' => 'json', )); $cache_object = RenderCache::create($fragments, $handler->getCacheController()); $record = $cache_object->get(); $this->assertEqual($pre_cache_results[0], $record->data, 'Data in cache bins is correct.'); // Test that invalidation is clearing cache records. // 1. Update node and watch cache vanish. // Make sure the cache is activated. $input = array('fields' => 'id,label'); $handler->setRequest(Request::create('', $input)); $handler->setPath(''); restful()->getFormatterManager()->format($handler->process(), 'json'); $this->assertTrue($cache_info['simpleInvalidate'], 'Cache render simple invalidation is activated'); $node->title .= ' updated'; node_save($node); $this->assertFalse($cache_object->get(), 'Cache record cleared correctly.'); $this->assertFalse($cache->isEmpty(), 'Remaining cache items are intact after updating entity.'); // Regenerate cache for $node. $handler->setRequest(Request::create($node->nid)); $handler->setPath($node->nid); restful()->getFormatterManager()->format($handler->process(), 'json'); $handler->setRequest(Request::create('', $input)); $handler->setPath($node->nid); restful()->getFormatterManager()->format($handler->process(), 'json'); $this->assertNotNull($cache_object->get(), 'Cache is being generated for non-list requests.'); // 2. Update the user account. All cache records should be invalidated. $account->name .= ' updated'; user_save($account); $this->assertFalse($cache_object->get(), 'Cache object has been cleared after updating a user.'); // The cache fragment garbage collection happens on shutdown. For testing // purposes we'll call the function directly here. restful_entity_clear_render_cache(); // Make sure that the cache fragment entities have been deleted. $query = new \EntityFieldQuery(); /* @var \Drupal\restful\RenderCache\Entity\CacheFragmentController $controller */ $controller = entity_get_controller('cache_fragment'); $results = $query ->entityCondition('entity_type', 'cache_fragment') ->propertyCondition('hash', $controller->generateCacheHash($fragments)) ->count() ->execute(); $this->assertEqual($results, 0, 'The cache fragment entities were deleted correctly.'); // Test cache with request params. // Rebuild caches. $cache->clear('*', TRUE); $handler->setRequest(Request::create('', $input)); $handler->setPath(''); $formatter->setResource($handler); $results_w_request = drupal_json_decode($formatter->format($handler->process())); $results_w_request = $results_w_request['data']; $cached_w_request = $cache_object->get()->data; // Check that the results with sparse fieldsets only contain the selected // fields, but the cache contains all fields. $this->assertEqual(array_keys($results_w_request[0]), array( 'id', 'label', ), 'Response contains only the selected fields.'); $this->assertEqual(array_keys($cached_w_request), array( 'id', 'label', 'self', ), 'Cached object contains all the selected fields.'); // Test helper functions. foreach ($nodes as $nid => $entity) { // Populate the cache. $handler->setRequest(Request::create($nid)); $handler->setPath($nid); restful()->getFormatterManager()->format($handler->process(), 'json'); $handler->setRequest(Request::create($nid, $input)); $handler->setPath($nid); restful()->getFormatterManager()->format($handler->process(), 'json'); } // Test PER_ROLE granularity. $cache->clear('*', TRUE); $resource_manager->clearPluginCache($handler->getPluginId()); // Re-instantiate the data provider. $handler->setDataProvider(NULL); $cache_info['granularity'] = DRUPAL_CACHE_PER_ROLE; $plugin_definition = $handler->getPluginDefinition(); $plugin_definition['renderCache'] = $cache_info; $handler->setPluginDefinition($plugin_definition); $handler->setRequest(Request::create('')); $handler->setPath(''); restful()->getFormatterManager()->format($handler->process(), 'json'); $role_fragments = new ArrayCollection(array( 'resource' => $handler->getResourceName() . '#' . $node->nid, 'entity' => 'node#' . $node->nid, 'user_role' => 'anonymous user,authenticated user', 'formatter' => 'json', )); $this->assertTrue($cache->get($controller->generateCacheHash($role_fragments)), 'Cache key contains role information.'); } /** * Tests for SA 154563. */ public function testPageCache() { // Enable page cache. variable_set('cache', TRUE); variable_set('restful_page_cache', TRUE); // Make anonymous users not to be able to access content. user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array( 'access content' => FALSE, )); // Create a new article. $settings = array('type' => 'article'); $settings['title'] = 'Node title'; $node = $this->drupalCreateNode($settings); $path = 'api/v1.0/test_articles/' . $node->nid; $url = url($path, array('absolute' => TRUE)); $cache = _cache_get_object('cache_page'); // Create a user that can access content. $account = $this->drupalCreateUser(array('access content')); // 1. Test the cookie authentication. // Log in the user (creating the cookie). $this->drupalLogin($account); // Access the created article creating a page cache entry. $response = $this->httpRequest($path); $this->assertEqual($response['code'], 200, 'Access granted for logged in user.'); if (!drupal_is_cli()) { // Make sure that there is not a page cache entry. $this->assertFalse($cache->get($url), 'A page cache entry was not created for a authenticated user.'); } // Log out the user. $this->drupalLogout(); // Try to access the cached resource. $response = $this->httpRequest($path); // The user should get a 401. $this->assertEqual($response['code'], 401, 'Access denied for anonymous user.'); // Clear the cache, since anonymous requests get cached. Requests with basic // authentication will pick that cached version. This is a know issue and we // accept it as a lesser evil. $cache->clear($url); // 2. Test the basic authentication. $response = $this->httpRequest($path, RequestInterface::METHOD_GET, NULL, array( 'Authorization' => 'Basic ' . drupal_base64_encode($account->name . ':' . $account->pass_raw), )); $this->assertEqual($response['code'], 200, 'Access granted for logged in user.'); if (!drupal_is_cli()) { // Make sure that there is a page cache entry. $this->assertFalse($cache->get($url), 'A page cache entry was not created with basic auth.'); } // Try to access the cached resource. $response = $this->httpRequest($path); // The user should get a 401. $this->assertEqual($response['code'], 401, 'Access denied for anonymous user.'); if (!drupal_is_cli()) { // Make sure that there is not a page cache entry. $this->assertFalse($cache->get($url), 'A page cache entry was created for an anonymous user.'); } // Remove the cache entry. $cache->clear($url); // 3. Test that when restful_page_cache is off there is no page cache. variable_set('restful_page_cache', FALSE); // Try to access the cached resource as anonymous users. $this->httpRequest($path); if (!drupal_is_cli()) { $this->assertFalse($cache->get($url), 'A page cache entry was not created for an anonymous users when restful_page_cache is off.'); } } } ================================================ FILE: tests/RestfulSimpleJsonTestCase.test ================================================ 'View Simple JSON', 'description' => 'Test the viewing of an entity in HAL+JSON format.', 'group' => 'RESTful', ); } public function setUp() { parent::setUp('restful_example', 'restful_test', 'entityreference'); restful_test_add_fields(); } /** * Test the Simple JSON formatter. */ public function testSimpleJson() { $resource_manager = restful()->getResourceManager(); $node = restful_test_create_node_with_tags(); $handler = $resource_manager->getPlugin('articles:1.5'); $resource_manager->clearPluginCache($handler->getPluginId()); // Use the simple JSON formatter. $plugin_definition = $handler->getPluginDefinition(); $plugin_definition['formatter'] = 'json'; $handler->setPluginDefinition($plugin_definition); $handler->setRequest(Request::create('/api/v1.5/articles/' . $node->nid)); $handler->setPath($node->nid); $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $response = $response['data']; $formatter_manager = new FormatterManager($handler); $accept = empty($GLOBALS['_SERVER']['HTTP_ACCEPT']) ? NULL : $GLOBALS['_SERVER']['HTTP_ACCEPT']; $formatter = $formatter_manager->negotiateFormatter($accept, 'json'); $result = $formatter->format($response); if ($decoded_json = drupal_json_decode($result)) { $this->pass('Valid JSON output generated.'); } else { $this->fail('Invalid JSON output generated.'); } $this->assertNotNull($decoded_json['data'], 'The "data" wrapper was created successfully.'); // Assert the embedded tags. foreach ($decoded_json['data'][0]['tags'] as $index => $tag_info) { $this->assertNotNull($tag_info['self'], 'The "self" property was populated for the tags.'); $this->assertNotNull($tag_info['id'], 'The "id" property was populated.'); $this->assertEqual($tag_info['label'], $response[0]['tags'][$index]['label'], 'The "label" property was populated correctly.'); } // Assert the HATEOAS. // Create another node for pagination. restful_test_create_node_with_tags(); // Change the max range. $handler->getDataProvider()->setRange(1); $handler->setRequest(Request::create('')); $handler->setPath(''); $results = $handler->process(); $formatter = restful()->getFormatterManager()->getPlugin('json'); $formatter->setResource($handler); $data = $formatter->prepare($results); $this->assertNotNull($data['self'], '"Self" property added.'); $this->assertEqual($data['count'], 2, 'Count was populated correctly.'); $this->assertEqual(count($data['data']), 1, 'The correct number of items was listed.'); $this->assertNotNull($data['next'], '"Next" property added.'); // Test the pagination links for bigger ranges. // Create an extra node for more pagination options. restful_test_create_node_with_tags(); $handler->getDataProvider()->setRange(50); $handler->setRequest(Request::create('', array( 'range' => 1, 'page' => 2, ))); $handler->setPath(''); $results = $handler->process(); $formatter = restful()->getFormatterManager()->getPlugin('json'); $formatter->setResource($handler); $data = $formatter->prepare($results); $this->assertEqual($data['count'], 3, 'Count was populated correctly.'); $this->assertEqual(count($data['data']), 1, 'The correct number of items was listed.'); $this->assertNotNull($data['next'], '"Next" property added.'); $this->assertNotNull($data['previous'], '"Previous" property added.'); // Test the max cap for the range. $handler->getDataProvider()->setRange(1); $handler->setRequest(Request::create('/api/v1.5/articles', array( 'page' => array( 'size' => 1000, 'number' => 2, ), ))); $handler->setPath(''); $results = $handler->process(); $formatter = restful()->getFormatterManager()->getPlugin('json'); $formatter->setResource($handler); $data = $formatter->prepare($results); $this->assertNotNull($data['next'], '"Next" property added.'); } } ================================================ FILE: tests/RestfulSubResourcesCreateEntityTestCase.test ================================================ 'Sub requests', 'description' => 'Test the creation of sub-resources (referenced entities) via sub requests.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_test', 'entity_validator_example'); // Entity reference - single. $field = array( 'entity_types' => array('node'), 'settings' => array( 'handler' => 'base', 'target_type' => 'node', 'handler_settings' => array(), ), 'field_name' => 'entity_reference_single', 'type' => 'entityreference', 'cardinality' => 1, ); field_create_field($field); $instance = array( 'entity_type' => 'node', 'field_name' => 'entity_reference_single', 'bundle' => 'article', 'label' => t('Entity reference single'), ); field_create_instance($instance); // Entity reference - multiple. $field = array( 'entity_types' => array('node'), 'settings' => array( 'handler' => 'base', 'target_type' => 'node', 'handler_settings' => array(), ), 'field_name' => 'entity_reference_multiple', 'type' => 'entityreference', 'cardinality' => FIELD_CARDINALITY_UNLIMITED, ); field_create_field($field); $instance = array( 'entity_type' => 'node', 'field_name' => 'entity_reference_multiple', 'bundle' => 'article', 'label' => t('Entity reference multiple'), ); field_create_instance($instance); $resource_manager = restful()->getResourceManager(); $resource_manager->clearPluginCache('test_articles:1.2'); $this->handler = $resource_manager->getPlugin('test_articles:1.2'); user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array( 'create article content' => TRUE, )); } /** * Test creating and updating an entity with sub-resources. */ public function testCreateAndUpdateEntity() { $base_request = $request = array( 'label' => 'parent', 'body' => 'Drupal', 'entity_reference_single' => array( 'body' => array( 'label' => 'child1', 'body' => 'Drupal1', ), 'request' => array( 'method' => 'POST', ), ), 'entity_reference_multiple' => array( array( 'body' => array( 'label' => 'child2', 'body' => 'Drupal2', ), 'request' => array( 'method' => 'POST', ), ), array( 'body' => array( 'label' => 'child3', 'body' => 'Drupal3', ), 'request' => array( 'method' => 'POST', ), ), ), ); // POST parent. $this->processRequests('post', '', $request); // PUT/ PATCH parent. $settings = array( 'type' => 'article', 'title' => 'title1', ); $node1 = $this->drupalCreateNode($settings); $request = $base_request; foreach (array('put', 'patch') as $method) { $this->processRequests($method, $node1->nid, $request); } // POST parent, reference existing on single reference, multiple reference // empty. $node2 = $this->drupalCreateNode($settings); $node3 = $this->drupalCreateNode($settings); $node4 = $this->drupalCreateNode($settings); $node5 = $this->drupalCreateNode($settings); $request = $base_request; $request['entity_reference_single'] = $node1->nid; $request['entity_reference_multiple'] = array( $node2->nid, $node3->nid, ); $this->processRequests('post', '', $request); // POST parent, reference existing on multiple reference, POST a new one, // PATCH existing one, and PUT existing one. $request = $base_request; $request['entity_reference_single'] = $node2->nid; $request['entity_reference_multiple']['request'] = array( 'method' => RequestInterface::METHOD_PATCH, ); $request['entity_reference_multiple'] = array( array( // Set the $node3. 'id' => $node3->nid, ), array( 'body' => array( // Create a new node. 'label' => 'POST new one', 'body' => 'Drupal', ), 'request' => array('method' => 'POST'), ), array( // Set $node4 and update some of the fields. 'id' => $node4->nid, 'body' => array( 'label' => 'PATCH existing one', 'body' => 'Drupal', ), 'request' => array('method' => 'PATCH'), ), array( // Set $node5 and update some of the fields. 'id' => $node5->nid, 'body' => array( 'label' => 'PATCH existing one', 'body' => 'Drupal', ), 'request' => array('method' => 'POST'), ), ); $this->processRequests('post', '', $request); // Test the version of the sub-resource. $public_fields = $this->handler->getFieldDefinitions(); $erm_resource = $public_fields->get('entity_reference_multiple') ->getResource(); $this->assertEqual($erm_resource['majorVersion'], 1, 'Sub resource major version is correct.'); $this->assertEqual($erm_resource['minorVersion'], 2, 'Sub resource minor version is correct.'); } /** * Assert valid and invalid requests. * * @param string $method * The method name. * @param string $path * The path. * @param array $request * The request array. */ protected function processRequests($method = 'post', $path = '', array $request = array()) { $method = strtoupper($method); $query = $parsed_body = array(); if (Request::isWriteMethod($method)) { $parsed_body = $request; } else { $query = $request; } $this->handler->setRequest(Request::create($path, $query, $method, NULL, FALSE, NULL, array(), array(), array(), $parsed_body)); $this->handler->setPath($path); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($this->handler->process(), 'json')); $result = $result['data'][0]; $this->assertTrue($result['id'], 'Parent entity created.'); $this->assertTrue($result['entity_reference_single']['id'], 'Single sub-resource created or updated.'); $this->assertTrue($result['entity_reference_multiple'][0]['id'] && $result['entity_reference_multiple'][1]['id'], 'Multiple sub-resource created or updated.'); // Validation fail on the parent. $request['label'] = 'no'; $this->assertInvalidRequest($method, $path, $request); // Validation fail on the single resource. $request['label'] = 'parent'; if (is_array($request['entity_reference_single'])) { $request['entity_reference_single']['label'] = 'no'; $this->assertInvalidRequest($method, $path, $request); $request['entity_reference_single']['label'] = 'child1'; } // Validation fail on the multiple resource. if ( !empty($request['entity_reference_multiple'][0]['label']) && is_array($request['entity_reference_multiple'][0]['label']) ) { $request['entity_reference_multiple'][0]['label'] = 'no'; $this->assertInvalidRequest($method, $path, $request); } } /** * Assert an invalid request fails. * * @param string $method * The method name. * @param string $path * The path. * @param array $request * The request array. */ protected function assertInvalidRequest($method = 'post', $path = '', array $request = array()) { $method = strtoupper($method); $query = $parsed_body = array(); if (Request::isWriteMethod($method)) { $query = $request; } else { $parsed_body = $request; } try { $this->handler->setRequest(Request::create($path, $query, $method, NULL, FALSE, NULL, array(), array(), array(), $parsed_body)); $this->handler->setPath($path); $this->handler->process(); $this->fail('No exception thrown on validation fail on the parent.'); } catch (BadRequestException $e) { $this->pass('Correct exception thrown on validation fail on the parent.'); } catch (\Exception $e) { $this->fail('Wrong exception thrown on validation fail on the parent.'); } } } ================================================ FILE: tests/RestfulUpdateEntityCurlTestCase.test ================================================ 'Update entity with CURL', 'description' => 'Test the updating of an entity using PUT and PATCH methods using CURL.', 'group' => 'RESTful', ); } function setUp() { parent::setUp('restful_test'); $this->account = $this->drupalCreateUser(array( 'administer site configuration', 'administer nodes', 'edit own article content', 'edit any article content', )); $this->drupalLogin($this->account); $this->httpauth_credentials = $this->account->name . ':' . $this->account->pass_raw; } /** * Test update an entity (PUT & PATCH methods). */ function testUpdateEntityAsPutPatch() { // Test update an entity (PUT method). $label = $this->randomName(); $new_label = $this->randomName(); $text = $this->randomName(); $settings = array( 'type' => 'article', 'title' => $label, ); $settings['body'][LANGUAGE_NONE][0]['value'] = $text; $node = $this->drupalCreateNode($settings); $id = $node->nid; $request = array('label' => $new_label); $result = $this->httpRequest('api/v1.2/test_articles/' . $id, RequestInterface::METHOD_PUT, $request, array( 'Content-Type' => 'application/x-www-form-urlencoded', )); $expected_result = array( 'data' => array(array( 'id' => '1', 'label' => $new_label, 'self' => url('api/v1.2/test_articles/' . $id, array('absolute' => TRUE)), 'body' => NULL, )), 'self' => array( 'href' => url('api/v1.2/test_articles/' . $id, array('absolute' => TRUE)), 'title' => 'Self', ), ); $result = drupal_json_decode($result['body']); $this->assertEqual($expected_result, $result); // Update an entity with invalid property name. $request['invalid'] = 'wrong'; $result = $this->httpRequest('api/v1.2/test_articles/' . $id, RequestInterface::METHOD_PUT, $request); $this->assertEqual($result['code'], 400, 'User cannot update using PUT method an entity with invalid property name.'); // Test update an entity (PATCH method). $label = $this->randomName(); $new_label = $this->randomName(); $text = $this->randomName(); $settings = array( 'type' => 'article', 'title' => $label, ); $settings['body'][LANGUAGE_NONE][0]['value'] = $text; $node = $this->drupalCreateNode($settings); $id = $node->nid; $request = array('label' => $new_label); $result = $this->httpRequest("api/v1.2/test_articles/$id", RequestInterface::METHOD_PATCH, $request, array( 'Content-Type' => 'application/x-www-form-urlencoded', )); $expected_result = array( 'data' => array(array( 'id' => '2', 'label' => $new_label, 'self' => url('api/v1.2/test_articles/' . $id, array('absolute' => TRUE)), 'body' => $text, )), 'self' => array( 'title' => 'Self', 'href' => url('api/v1.2/test_articles/' . $id, array('absolute' => TRUE)), ), ); $result = drupal_json_decode($result['body']); $result['data'][0]['body'] = trim(strip_tags($result['data'][0]['body'])); ksort($result); ksort($expected_result); $this->assertEqual($result, $expected_result); // Update an entity with invalid property name. $request['invalid'] = 'wrong'; $result = $this->httpRequest("api/v1.2/test_articles/$id", RequestInterface::METHOD_PATCH, $request); $this->assertEqual($result['code'], 400, 'User cannot update using PATCH method an entity with invalid property name.'); } } ================================================ FILE: tests/RestfulUpdateEntityTestCase.test ================================================ 'Update entity', 'description' => 'Test the updating of an entity using PUT and PATCH methods.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful_test'); $this->account = $this->drupalCreateUser(array( 'create article content', 'edit own article content', )); } /** * Test update an entity (PUT method). */ public function testUpdateEntityAsPut() { $resource_manager = restful()->getResourceManager(); $label = $this->randomName(); $new_label = $this->randomName(); $text = $this->randomName(); $settings = array( 'type' => 'article', 'title' => $label, 'uid' => $this->account->uid, ); $settings['body'][LANGUAGE_NONE][0]['value'] = $text; $node = $this->drupalCreateNode($settings); $id = $node->nid; $handler = $resource_manager->getPlugin('test_articles:1.2'); $handler->setAccount($this->account); $request = array('label' => $new_label); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doPut($id, $request), 'json')); $result = $result['data']; $expected_result = array( array( 'id' => $id, 'label' => $new_label, 'self' => $handler->versionedUrl($id), 'body' => NULL, ), ); $this->assertEqual($result, $expected_result); // Update an entity with invalid property name. $request['invalid'] = 'wrong'; try { $handler->setRequest(Request::create('api/test_articles/v1.2/' . $id, array(), RequestInterface::METHOD_PUT, NULL, FALSE, NULL, array(), array(), array(), $request)); $handler->setPath($id); restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('User can update using PUT method an entity with invalid property name.'); } catch (Exception $e) { $this->pass('User cannot update using PUT method an entity with invalid property name.'); } $new_label = $this->randomName(); $new_body = $this->randomName(); $request = array( 'label' => $new_label, 'body' => $new_body, ); // Setting new value for 'body'. $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doPut($id, $request), 'json')); // Making sure the new body has been set. $this->assertEqual(trim(strip_tags($result['data'][0]['body'])), $new_body); // Removing the body value. $request = array( 'label' => $new_label, 'body' => NULL, ); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doPut($id, $request), 'json')); $this->assertEqual($result['data'][0]['body'], NULL); } /** * Test update an entity (PATCH method). */ function testUpdateEntityAsPatch() { $resource_manager = restful()->getResourceManager(); $label = $this->randomName(); $new_label = $this->randomName(); $text = $this->randomName(); $settings = array( 'type' => 'article', 'title' => $label, 'uid' => $this->account->uid, ); $settings['body'][LANGUAGE_NONE][0]['value'] = $text; $node = $this->drupalCreateNode($settings); $id = $node->nid; $handler = $resource_manager->getPlugin('test_articles:1.2'); $handler->setAccount($this->account); $request = array('label' => $new_label); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doPatch($id, $request), 'json')); $result = $result['data']; $expected_result = array( array( 'id' => $id, 'label' => $new_label, 'self' => $handler->versionedUrl($id), 'body' => $text, ), ); $result[0]['body'] = trim(strip_tags($result[0]['body'])); $this->assertEqual($result, $expected_result); // Update an entity with invalid property name. $request['invalid'] = 'wrong'; try { $handler->setRequest(Request::create('api/test_articles/v1.2/' . $id, array(), RequestInterface::METHOD_PATCH, NULL, FALSE, NULL, array(), array(), array(), $request)); $handler->setPath($id); restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('User can update using PATCH method an entity with invalid property name.'); } catch (Exception $e) { $this->pass('User cannot update using PATCH method an entity with invalid property name.'); } $new_body = $this->randomName(); $request = array('body' => $new_body); // Setting new value for 'body'. $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doPatch($id, $request), 'json')); // Making sure the new body has been set. $this->assertEqual(trim(strip_tags($result['data'][0]['body'])), $new_body); // Removing the body value. $request = array('body' => NULL); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doPatch($id, $request), 'json')); $this->assertEqual($result['data'][0]['body'], NULL); } } ================================================ FILE: tests/RestfulUserLoginCookieTestCase.test ================================================ 'Login endpoint', 'description' => 'Test the "/api/login "endpoint.', 'group' => 'RESTful', ); } /** * {@inheritdoc} */ public function setUp() { parent::setUp('restful'); $this->originalServer = $_SERVER; } /** * {@inheritdoc} */ public function tearDown() { global $user; // Put back the user object. $user = $this->originalUser; // Put back the $_SERVER array. $_SERVER = $this->originalServer; parent::tearDown(); } /** * Test login using curl via /api/login. */ public function testLogin() { global $user; // We need to hijack the global user object in order to force it to be an // anonymous user. $user = drupal_anonymous_user(); $user1 = $this->drupalCreateUser(); $this->httpauth_credentials = $user1->name . ':' . $user1->pass_raw; $result = $this->httpRequest('api/login'); $this->assertEqual($result['code'], '200', '200 status code sent for an anonymous user logging in.'); // Since we are already logged in, we should get a 403. $result = $this->httpRequest('api/login'); $this->assertEqual($result['code'], '403', '403 status code sent for an already logged in user.'); } } ================================================ FILE: tests/RestfulVariableTestCase.test ================================================ 'Variable', 'description' => 'Test the variable data provider.', 'group' => 'RESTful', ); } /** * Operations before the testing begins. */ public function setUp() { parent::setUp('restful_example'); } /** * Test authenticating a user. */ public function testCrudOperations() { // Set up random content and resource handler. $random_string = $this->randomName(); $handler = restful()->getResourceManager()->getPlugin('variables:1.0'); $formatter = restful()->getFormatterManager()->negotiateFormatter(NULL, 'json'); $formatter->setResource($handler); // Populate the test environment with variables. $random_numbers = array(); for ($i = 0; $i < 6; $i++) { $random_numbers[] = intval(mt_rand(1, 100)); variable_set('variable_' . $i, array('test_data' => $random_numbers[$i])); } $this->assertTrue(variable_get('variable_5'), 'The variables have been set.'); // Testing read. $results = $formatter->prepare($handler->doGet('variable_5')); $expected = array('test_data' => $random_numbers[5]); $this->assertEqual($results['data'][0]['variable_name'], 'variable_5', 'The variable name was successfully retrieved.'); $this->assertEqual($results['data'][0]['variable_value'], $expected, 'The variable value was successfully retrieved.'); // Testing read context listing. $results = $formatter->prepare($handler->doGet()); $in_results = FALSE; foreach ($results['data'] as $result) { if ($result['variable_name'] == 'variable_5') { $in_results = TRUE; } } $this->assertTrue($in_results, 'All the content listed successfully.'); // Testing sort for read context. // Set a variable that will probably sort last. variable_set('zzzzz', 'some value'); // Find the last variable name, which will probably be the one we just set. $query = array( 'sort' => '-variable_name', ); $results = $formatter->prepare($handler->doGet('', $query)); $last_variable_name = $results['data'][0]['variable_name']; // Generate a variable name that will always sort last. $new_variable_name = 'zzz'; while (strcmp($new_variable_name, $last_variable_name) <= 0) { $new_variable_name .= 'z'; } variable_set($new_variable_name, array('key' => $random_string)); $query = array( 'sort' => '-variable_name', ); $results = $formatter->prepare($handler->doGet('', $query)); $expected = array( 'variable_name' => $new_variable_name, 'variable_value' => array('key' => $random_string), ); $this->assertEqual($results['data'][0], $expected, 'List is sorted correctly.'); // Testing create. $parsed_body = array( 'variable_name' => 'created_variable', 'variable_value' => $random_string, ); $handler->doPost($parsed_body); $results = $formatter->prepare($handler->doGet('created_variable')); $this->assertEqual($results['data'][0]['variable_name'], 'created_variable', 'The variable was created.'); $this->assertEqual($results['data'][0]['variable_value'], $random_string, 'The created variable value is present.'); // Testing update. $parsed_body = array('variable_name' => 'created_variable'); $handler->doPatch('created_variable', $parsed_body); $results = $formatter->prepare($handler->doGet('created_variable')); // Fields that are not supplied should not be updated. $this->assertEqual($results['data'][0]['variable_value'], $random_string, 'The variable value was not updated.'); // Testing replace. $handler->doPut('created_variable', $parsed_body); $results = $formatter->prepare($handler->doGet('created_variable')); // Fields that are not supplied should be NULL. $this->assertFalse($results['data'][0]['variable_value'], 'The variable value was removed.'); // Testing delete. $handler->doDelete('created_variable'); $deleted = !variable_get('created_variable'); $this->assertTrue($deleted); } /** * Test the render cache. */ public function testRenderCache() { // Create a test variable. /* @var \Drupal\restful\Plugin\resource\Decorators\CacheDecoratedResource $handler */ $handler = restful()->getResourceManager()->getPlugin('variables:1.0'); $formatter = restful()->getFormatterManager()->negotiateFormatter(NULL, 'json'); $formatter->setResource($handler); $parsed_body = array( 'variable_name' => 'test_variable_cache', 'variable_value' => TRUE, ); $handler->doPost($parsed_body); $created = variable_get('test_variable_cache'); $this->assertNotNull($created, 'The cache variable has been created.'); // Populate the cache entries. $account = $this->drupalCreateUser(); $handler->setAccount($account); $formatter->prepare($handler->doGet('test_variable_cache')); // Get the cache value. $cache_fragments = $handler->getDataProvider()->getCacheFragments('test_variable_cache'); $cache_fragments->set('formatter', 'json'); $render_cache = RenderCache::create($cache_fragments, $handler->getCacheController()); $cache_data = $render_cache->get(); $this->assertNotNull($cache_data->data, 'Cache data is present.'); $this->assertEqual($cache_data->data['variable_name'], 'test_variable_cache', 'The variable name was retrieved from the cache.'); $this->assertEqual($cache_data->data['variable_value'], TRUE, 'The variable value was retrieved from the cache.'); } } ================================================ FILE: tests/RestfulViewEntityMultiLingualTestCase.test ================================================ 'View multilingual entity', 'description' => 'Test the viewing of a multilingual entity.', 'group' => 'RESTful', ); } function setUp() { parent::setUp( 'restful_example', 'restful_test', 'entityreference', 'locale' ); $this->testUser = $this->drupalCreateUser( array( 'administer languages', 'access administration pages', 'administer site configuration', ) ); require_once DRUPAL_ROOT . '/includes/locale.inc'; $this->drupalLogin($this->testUser); $languages = language_list(); if (!isset($languages['en'])) { locale_add_language('en', NULL, NULL, LANGUAGE_LTR, '', 'en'); } if (!isset($languages['fr'])) { locale_add_language('fr', NULL, NULL, LANGUAGE_LTR, '', 'fr'); } $this->addTestFields(); } /** * Add test fields. */ protected function addTestFields() { // Text - single. $field = array( 'field_name' => 'text_single', 'type' => 'text_long', 'entity_types' => array('restful_test_translatable_entity'), 'cardinality' => 1, 'translatable' => TRUE, ); field_create_field($field); $instance = array( 'field_name' => 'text_single', 'bundle' => 'restful_test_translatable_entity', 'entity_type' => 'restful_test_translatable_entity', 'label' => t('Text single'), 'settings' => array( 'text_processing' => 0, ), ); field_create_instance($instance); // Text - multiple. $field = array( 'field_name' => 'text_multiple', 'type' => 'text_long', 'entity_types' => array('restful_test_translatable_entity'), 'cardinality' => FIELD_CARDINALITY_UNLIMITED, 'translatable' => TRUE, ); field_create_field($field); $instance = array( 'field_name' => 'text_multiple', 'bundle' => 'restful_test_translatable_entity', 'entity_type' => 'restful_test_translatable_entity', 'label' => t('Text multiple'), 'settings' => array( 'text_processing' => 0, ), ); field_create_instance($instance); } /** * Test viewing an entity with translatable fields. */ function testViewMultiLangualEntity() { $user = $this->drupalCreateUser(); $values = array( 'name' => 'restful_test_translatable_entity', 'uid' => $user->uid, 'label' => 'Test translation', ); $entity = entity_create('restful_test_translatable_entity', $values); $wrapper = entity_metadata_wrapper('restful_test_translatable_entity', $entity); $text1 = array( 'en' => $this->randomName(), 'fr' => $this->randomName(), ); $text2 = array( 'en' => $this->randomName(), 'fr' => $this->randomName(), ); foreach (array('en', 'fr') as $langcode) { $wrapper->language($langcode); $wrapper->text_single->set($text1[$langcode]); $wrapper->text_multiple->set(array($text1[$langcode], $text2[$langcode])); } $wrapper->save(); $id = $entity->pid; foreach (array('en', 'fr') as $langcode) { $this->assertExpectedResult($langcode, $id, $text1[$langcode], $text2[$langcode]); } } /** * Helper to test viewing an entity (GET method) in a certain language. * * @param string $langcode * The language to view the entity in. * @param int $id * The ID of the entity to test. * @param string $text1 * The first expected string (text_single and text_multiple[0]). * @param string $text2 * The second expected string (text_multiple[2]). */ private function assertExpectedResult($langcode, $id, $text1, $text2) { $resource_manager = restful()->getResourceManager(); $handler = $resource_manager->getPlugin('restful_test_translatable_entity:1.0'); // Explicitly set the langcode. $handler->getDataProvider()->setLangCode($langcode); $handler->setRequest(Request::create('api/restful_test_translatable_entity/' . $id)); $handler->setPath($id); $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $response = $response['data']; $result = $response[0]; $this->assertEqual(trim(strip_tags($result['text_single'])), $text1, 'Entity view has correct result for "text_single" in language "' . $langcode . '".'); $this->assertEqual(trim(strip_tags($result['text_multiple'][0])), $text1, 'Entity view has correct result for the first value of "text_multiple" in language "' . $langcode . '".'); $this->assertEqual(trim(strip_tags($result['text_multiple'][1])), $text2, 'Entity view has correct result for the second value of "text_multiple" in language "' . $langcode . '".'); } } ================================================ FILE: tests/RestfulViewEntityTestCase.test ================================================ 'View entity', 'description' => 'Test the viewing of an entity.', 'group' => 'RESTful', ); } /** * Overrides DrupalWebTestCase::setUp(). */ public function setUp() { parent::setUp('restful_example', 'restful_test', 'entityreference'); restful_test_add_fields(); } /** * Test viewing an entity (GET method). * * v1.0 - Simple entity view (id, label, self). * v1.1 - Text and entity reference fields. * v1.2 - "callback" and "process callback". * v1.3 - Non-existing "callback" property. * v1.4 - Non-existing "process callback" property. * v1.6 - XML output format. */ public function testViewEntity() { $user1 = $this->drupalCreateUser(); $entity1 = entity_create('entity_test', array( 'name' => 'main', 'uid' => $user1->uid, )); $entity1->save(); $entity2 = entity_create('entity_test', array( 'name' => 'main', 'uid' => $user1->uid, )); $entity2->save(); $entity3 = entity_create('entity_test', array( 'name' => 'main', 'uid' => $user1->uid, )); $wrapper = entity_metadata_wrapper('entity_test', $entity3); $text1 = $this->randomName(); $text2 = $this->randomName(); $wrapper->text_single->set($text1); $wrapper->text_multiple->set(array($text1, $text2)); $wrapper->entity_reference_single->set($entity1); $wrapper->entity_reference_multiple[] = $entity1; $wrapper->entity_reference_multiple[] = $entity2; $wrapper->save(); $id = $entity3->pid; $base_expected_result = array( 'id' => $id, 'label' => 'Main test type', ); $resource_manager = restful()->getResourceManager(); // v1.0 - Simple entity view (id, label, self). $handler = $resource_manager->getPlugin('main:1.0'); $base_expected_result['self'] = $handler->versionedUrl($id); $expected_result = $base_expected_result; $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->doGet($id), 'json')); $response = $response['data']; $result = $response[0]; $this->assertEqual($result, $expected_result, 'Entity view has expected result for "main" resource v1'); // v1.1 - Text and entity reference field. $handler = $resource_manager->getPlugin('main:1.1'); $request = Request::create('api/main/v1.1/' . $id); $handler->setRequest($request); $base_expected_result['self'] = $handler->versionedUrl($id); $handler->setPath($id); $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $response = $response['data']; $result = $response[0]; $base_expected_result_v1 = $base_expected_result; // NULL fields. $base_expected_result_v1 += array( 'text_single_processing' => NULL, 'text_multiple_processing' => NULL, 'term_single' => NULL, 'term_multiple' => NULL, 'file_single' => NULL, 'file_multiple' => NULL, 'image_single' => NULL, 'image_multiple' => NULL, ); $expected_result = $base_expected_result_v1; $expected_result['text_single'] = $text1; $expected_result['text_multiple'] = array($text1, $text2); $expected_result['entity_reference_single'] = $entity1->pid; $expected_result['entity_reference_multiple'] = array( $entity1->pid, $entity2->pid, ); $request = Request::create('api/main/v1.1/' . $entity1->pid); $handler->setRequest($request); $handler->setPath($entity1->pid); $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $response = $response['data']; $expected_result['entity_reference_single_resource'] = $response[0]; $response1 = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process())); $response1 = $response1['data']; $request2 = Request::create('api/main/v1.1/' . $entity2->pid); $handler->setRequest($request2); $response2 = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process())); $response2 = $response2['data']; $expected_result['entity_reference_multiple_resource'] = array( $response1[0], $response2[0], ); $stripped_result = $result; $stripped_result['text_single'] = trim(strip_tags($result['text_single'])); $stripped_result['text_multiple'][0] = trim(strip_tags($result['text_multiple'][0])); $stripped_result['text_multiple'][1] = trim(strip_tags($result['text_multiple'][1])); ksort($stripped_result); ksort($expected_result); $this->assertFalse(drupal_array_diff_assoc_recursive($result, $stripped_result), 'Entity view has correct result for "main" resource v1.1'); // Test the "fullView" property on a referenced entity. // We change the definition via the handler instead of creating another // plugin. $field_definitions = $handler->getFieldDefinitions(); // Single entity reference field with "resource". $reference_field = $field_definitions->get('entity_reference_single_resource'); $reference_field->setResource(array( 'name' => 'main', 'fullView' => FALSE, )); $field_definitions->set('entity_reference_single_resource', $reference_field); $handler->setFieldDefinitions($field_definitions); $request = Request::create('api/main/v1.1/' . $id); $handler->setRequest($request); $handler->setPath($id); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual($result[0]['entity_reference_single_resource'], $entity1->pid, '"fullView" property is working properly.'); // Empty the text and entity reference fields. $wrapper->text_single->set(NULL); $wrapper->text_multiple->set(NULL); $wrapper->entity_reference_single->set(NULL); $wrapper->entity_reference_multiple->set(NULL); $wrapper->save(); $handler->setRequest($request); $handler->setPath($id); $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $response = $response['data']; $result = $response[0]; $expected_result = $base_expected_result_v1; $expected_result['text_single'] = NULL; $expected_result['text_multiple'] = NULL; $expected_result['text_single'] = NULL; $expected_result['text_multiple'] = NULL; $expected_result['entity_reference_single'] = NULL; $expected_result['entity_reference_multiple'] = NULL; $expected_result['entity_reference_single_resource'] = NULL; $expected_result['entity_reference_multiple_resource'] = NULL; ksort($result); ksort($expected_result); $this->assertEqual($result, $expected_result, 'Entity view has correct result for "main" resource v1.1 with empty entity reference.'); // Load an entity by an alternate field. $entity4 = entity_create('entity_test', array( 'name' => 'main', 'uid' => $user1->uid, )); $wrapper = entity_metadata_wrapper('entity_test', $entity4); $text = $this->randomName(); $wrapper->text_single->set($text); $wrapper->save(); $request = Request::create('api/main/v1.1/' . $text, array('loadByFieldName' => 'text_single')); $handler->setRequest($request); $handler->setPath($text); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertNotNull($result[0]); // Make sure canonical header is added. $result = $this->httpRequest('api/main/v1.1/' . $text, RequestInterface::METHOD_GET, array('loadByFieldName' => 'text_single')); $canonical_link = $handler->versionedUrl($wrapper->getIdentifier(), array(), FALSE) . '; rel="canonical"'; $this->assertTrue(strpos($result['headers'], $canonical_link)); // v1.2 - "callback" and "process callback". $handler = $resource_manager->getPlugin('main:1.2'); $request = Request::create('api/main/v1.2/' . $id); $handler->setRequest($request); $base_expected_result['self'] = $handler->versionedUrl($id); $handler->setPath($id); $response = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $response = $response['data']; $result = $response[0]; $expected_result = $base_expected_result; $expected_result['callback'] = 'callback'; $expected_result['process_callback_from_callback'] = 'callback processed from callback'; $expected_result['process_callback_from_value'] = $id . ' processed from value'; $this->assertEqual($result, $expected_result, 'Entity view has correct result for "main" resource v1.2'); // v1.3 - Non-existing "callback" property. $handler = $resource_manager->getPlugin('main:1.3'); $request = Request::create('api/main/v1.3/' . $id); $handler->setRequest($request); try { $handler->setPath($id); restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('Non-existing "callback" property did not trigger an exception.'); } catch (Exception $e) { $this->pass('Non-existing "callback" property triggered an exception.'); } // v1.4 - Non-existing "process callback" property. $handler = $resource_manager->getPlugin('main:1.4'); $request = Request::create('api/main/v1.4/' . $id); $handler->setRequest($request); try { $handler->setPath($id); restful()->getFormatterManager()->format($handler->process(), 'json'); $this->fail('Non-existing "process callback" property did not trigger an exception.'); } catch (Exception $e) { $this->pass('Non-existing "process callback" property triggered an exception.'); } // v1.6 - XML output format. $settings = array( 'type' => 'article', 'uid' => $user1->uid, ); $node = $this->drupalCreateNode($settings); $response = $this->httpRequest('api/v1.6/articles', RequestInterface::METHOD_GET); // Returns something like: // // // // 1 // // http://example.com/node/1 // ... // // // 50 // <_links> // https://example.com/api/v1.0/articles // // $xml = new SimpleXMLElement($response['body']); $results = $xml->xpath('/api/_embedded/articles/item0/label'); $result = reset($results); $this->assertEqual($node->title, $result->__toString(), 'XML parsed correctly.'); // Test Image variations based on image styles. // Add the multiple images field to the article bundle. $field = array( 'field_name' => 'field_images', 'type' => 'image', 'settings' => array(), 'cardinality' => FIELD_CARDINALITY_UNLIMITED, ); field_create_field($field); $instance = array( 'field_name' => 'field_images', 'entity_type' => 'node', 'label' => 'Image multiple', 'bundle' => 'article', ); field_create_instance($instance); $images = $this->drupalGetTestFiles('image'); $image = array_shift($images); $image = file_save((object) $image); // Test multiple Image variations based on image styles. $article = array( 'type' => 'article', 'field_images' => array(LANGUAGE_NONE => array()), 'field_image' => array(LANGUAGE_NONE => array(array('fid' => $image->fid))), ); foreach ($images as $image) { $image = file_save((object) $image); $article['field_images'][LANGUAGE_NONE][] = array('fid' => $image->fid); } $article = $this->drupalCreateNode($article); $resource_manager->clearPluginCache('articles:1.5'); $handler = $resource_manager->getPlugin('articles:1.5'); $request = Request::create('api/v1.5/articles/' . $article->nid); $handler->setRequest($request); $handler->setPath($article->nid); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual(array_keys($result[0]['image']['styles']) == array( 'thumbnail', 'medium', 'large', ), 'The selected image styles are present.'); $this->assertEqual(count(array_filter(array_values($result[0]['image']['styles']))) == 3, 'The image styles are populated.'); $this->assertEqual(count($result[0]['images']), count($images), 'The number of images is correct.'); $this->assertEqual(array_keys($result[0]['images'][0]['styles']) == array( 'thumbnail', 'medium', 'large', ), 'The selected image styles are present.'); $this->assertEqual(count(array_filter(array_values($result[0]['images'][0]['styles']))) == 3, 'The image styles are populated.'); } /** * Test the generation of the Vary headers. */ public function testHeaders() { $node = $this->drupalCreateNode(array('type' => 'article')); // When there is no header version passed in there is no need of Vary. $response = $this->httpRequest('api/v1.1/articles'); $this->assertFalse(preg_match('/X-API-Version/', $response['headers']), 'The Vary header was not added.'); // Make sure that the version header is present in the response. $this->assertTrue(preg_match('/v1\.1/', $response['headers']), 'The Vary header was added.'); $response = $this->httpRequest('api/articles', RequestInterface::METHOD_GET, NULL, array('X-API-Version' => 'v1.1')); $this->assertTrue(preg_match('/X-API-Version/', $response['headers']), 'The Vary header was added.'); // Make sure that the version header is present in the response. $this->assertTrue(preg_match('/v1\.1/', $response['headers']), 'The Vary header was added.'); // Test that if there is no explicit formatter in the plugin definition then // it is selected based on the Accept header. variable_set('restful_default_output_formatter', 'invalid'); $response = $this->httpRequest('api/v1.0/articles/' . $node->nid, RequestInterface::METHOD_GET, NULL, array( 'Accept' => 'application/hal+json', )); $this->assertNotNull(drupal_json_decode($response['body']), 'JSON output detected.'); // Test XML selection. $response = $this->httpRequest('api/v1.0/articles/' . $node->nid, RequestInterface::METHOD_GET, NULL, array( 'Accept' => 'application/xml', )); $this->assertNotNull(new SimpleXMLElement($response['body']), 'XML output detected.'); // Test wildcard selection. $response = $this->httpRequest('api/v1.0/articles/' . $node->nid, RequestInterface::METHOD_GET, NULL, array( 'Accept' => 'application/*', )); $this->assertEqual($response['code'], 200, 'Some output format detected for wildcard Accept.'); // Test that plugin definition takes precedence. $response = $this->httpRequest('api/v1.6/articles/' . $node->nid, RequestInterface::METHOD_GET, NULL, array( 'Accept' => 'application/hal+json', )); $this->assertNotNull(new SimpleXMLElement($response['body']), 'Plugin definition takes precedence.'); // The following should resolve to the 'invalid' formatter. It should raise // an exception. $response = $this->httpRequest('api/v1.0/articles/' . $node->nid, RequestInterface::METHOD_GET, NULL, array( 'Accept' => 'non-existing', )); $this->assertEqual($response['code'], 503, 'Error shown for invalid formatter.'); } } ================================================ FILE: tests/RestfulViewModeAndFormatterTestCase.test ================================================ 'View mode and formatter', 'description' => 'Test the integration with entity view mode and field API formatters.', 'group' => 'RESTful', ); } function setUp() { parent::setUp('restful_example', 'restful_test'); } /** * Test the view mode integration. */ public function testViewModeIntegration() { $resource_manager = restful()->getResourceManager(); $handler = $resource_manager->getPlugin('articles:1.7'); $nodes[] = restful_test_create_node_with_tags(); $nodes[] = restful_test_create_node_with_tags(); // Make sure to get more than one node. $handler->setRequest(Request::create('api/articles/v1.7/' . $nodes[0]->nid . ',' . $nodes[1]->nid)); $handler->setPath($nodes[0]->nid . ',' . $nodes[1]->nid); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; // Make sure that all the fields were mapped. $this->assertNotNull($result[0]['body'], 'Body field is populated.'); $this->assertTrue($result[0]['tags'], 'Tags field is populated.'); $this->assertNotNull($result[0]['image'], 'Image field is not NULL.'); } /** * Test the field API formatter integration. */ public function testFormatterIntegration() { $resource_manager = restful()->getResourceManager(); $handler = $resource_manager->getPlugin('articles:1.5'); // Create node. $text = 'Some body with long text'; $settings = array( 'type' => 'article', 'body' => array( LANGUAGE_NONE => array( array('value' => $text), ), ), ); $node = $this->drupalCreateNode($settings); // Field with no formatter. $request = Request::create('api/articles/v1.5/' . $node->nid); $handler->setRequest($request); $handler->setPath($node->nid); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; $this->assertEqual(trim(strip_tags($result[0]['body'])), $text, 'Raw value passed without a formatter.'); // Add formatter settings. $field_definitions = $handler->getFieldDefinitions(); $display = array( 'type' => 'text_summary_or_trimmed', 'settings' => array( 'trim_length' => 10, ), ); /* @var \Drupal\restful\Plugin\resource\Field\ResourceFieldEntityText $body */ $body = $field_definitions->get('body'); $body->setFormatter($display); $field_definitions->set('body', $body); $handler->setFieldDefinitions($field_definitions); $resource_manager->clearPluginCache('articles:1.5'); $handler->setRequest($request); $handler->setPath($node->nid); $result = drupal_json_decode(restful() ->getFormatterManager() ->format($handler->process(), 'json')); $result = $result['data']; // Core's trim formatter also inclues the opening

tag in the calculation // of number of chars. $this->assertEqual($result[0]['body'], '

Some bo

', 'Value trimmed by formatter.'); } } ================================================ FILE: tests/modules/restful_node_access_test/restful_node_access_test.info ================================================ name = RESTful node access Test description = Helper module for testing the RESTful module with node access. core = 7.x dependencies[] = restful dependencies[] = restful_test hidden = TRUE ================================================ FILE: tests/modules/restful_node_access_test/restful_node_access_test.module ================================================ uid) { return NULL; } $grants = array(); $grants['restful_test'][] = $account->uid; return $grants; } /** * Implements hook_node_access_records(). */ function restful_node_access_test_node_access_records($node) { $grants = array(); $grants[] = array( 'realm' => 'restful_test', 'gid' => $node->uid, 'grant_view' => 1, 'grant_update' => 0, 'grant_delete' => 0, 'priority' => 0, ); return $grants; } ================================================ FILE: tests/modules/restful_test/restful_test.info ================================================ name = RESTful Test description = Helper module for testing the RESTful module. core = 7.x dependencies[] = restful dependencies[] = restful_example dependencies[] = entity_feature dependencies[] = entityreference hidden = TRUE registry_autoload[] = PSR-0 registry_autoload[] = PSR-4 ================================================ FILE: tests/modules/restful_test/restful_test.install ================================================ 'char', 'length' => 36, 'not null' => TRUE, 'default' => '', 'description' => 'The Universally Unique Identifier.', )); db_add_index('entity_test', 'uuid', array('uuid')); } } /** * Implements hook_schema_alter(). */ function restful_test_schema_alter(&$schema = array()) { $field = array( 'type' => 'char', 'length' => 36, 'not null' => TRUE, 'default' => '', 'description' => 'The Universally Unique Identifier.', ); foreach (array('entity_test', 'restful_test_translatable_entity') as $table) { $schema[$table]['fields']['uuid'] = $field; } } /** * Implements hook_uninstall(). */ function restful_test_uninstall() { variable_del('restful_test_alternative_id_error'); field_attach_delete_bundle('restful_test_translatable_entity', 'restful_test_translatable_entity'); } /** * Implements hook_schema(). */ function restful_test_schema() { $schema['restful_test_translatable_entity'] = array( 'description' => 'Stores restful_test_translatable_entity items.', 'fields' => array( 'pid' => array( 'type' => 'serial', 'not null' => TRUE, 'description' => 'Primary Key: Unique restful_test_translatable_entity item ID.', ), 'name' => array( 'description' => 'The name of the restful_test_translatable_entity.', 'type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => '', ), 'uid' => array( 'type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, 'default' => NULL, 'description' => "The {users}.uid of the associated user.", ), 'label' => array( 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '', ), 'uuid' => array( 'type' => 'char', 'length' => 36, 'not null' => TRUE, 'default' => '', 'description' => 'The Universally Unique Identifier.', ), ), 'indexes' => array( 'uid' => array('uid'), 'uuid' => array('uuid'), ), 'foreign keys' => array( 'uid' => array('users' => 'uid'), ), 'primary key' => array('pid'), ); // Defining table via hook_install() due to drupal_write_record(). $schema['restful_test_db_query'] = array( 'description' => 'Table for DbQuery testing.', 'fields' => array( 'id' => array( 'description' => 'The primary identifier for a record.', 'type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE, ), 'str_field' => array( 'description' => 'String piece of data.', 'type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => '', ), 'int_field' => array( 'description' => 'An int piece of data.', 'type' => 'int', 'not null' => TRUE, 'default' => 0, ), 'serialized_field' => array( 'description' => 'An serialized piece of data.', 'type' => 'blob', 'serialize' => TRUE, ), ), 'primary key' => array('id'), ); return $schema; } ================================================ FILE: tests/modules/restful_test/restful_test.module ================================================ nid != $nid) { return NULL; } // Deny access. return NODE_ACCESS_DENY; } /** * Flag a field to not be accessible. * * @param string $field_name * The field name. Defaults to "body". */ function restful_test_deny_access_field($field_name = 'body') { variable_set('restful_test_deny_access_field', $field_name); } /** * Clear un-accessible fields. */ function restful_test_clear_access_field() { variable_del('restful_test_deny_access_field'); } /** * Implements hook_field_access(). */ function restful_test_field_access($op, $field, $entity_type, $entity, $account) { if (!$field_name = variable_get('restful_test_deny_access_field')) { return NULL; } if ($field_name == $field['field_name']) { return FALSE; } return NULL; } /** * Helper function to add common fields. * * @param string $entity_type * The entity type. Defautls to "entity_test". * @param string $bundle. * The bundle name. Defaults to "main". * * @return int * The vocabulary ID created. */ function restful_test_add_fields($entity_type = 'entity_test', $bundle = 'main') { // Text - single. $field = array( 'field_name' => 'text_single', 'type' => 'text_long', 'entity_types' => array($entity_type), 'cardinality' => 1, ); field_create_field($field); $instance = array( 'field_name' => 'text_single', 'bundle' => $bundle, 'entity_type' => $entity_type, 'label' => t('Text single'), 'settings' => array( // No text processing 'text_processing' => 0, ), ); field_create_instance($instance); // Text - single, with text processing. $field = array( 'field_name' => 'text_single_processing', 'type' => 'text_long', 'entity_types' => array($entity_type), 'cardinality' => 1, ); field_create_field($field); $instance = array( 'field_name' => 'text_single_processing', 'bundle' => $bundle, 'entity_type' => $entity_type, 'label' => t('Text single with text processing'), 'settings' => array( 'text_processing' => 1, ), ); field_create_instance($instance); // Text - multiple. $field = array( 'field_name' => 'text_multiple', 'type' => 'text_long', 'entity_types' => array($entity_type), 'cardinality' => FIELD_CARDINALITY_UNLIMITED, ); field_create_field($field); $instance = array( 'field_name' => 'text_multiple', 'bundle' => $bundle, 'entity_type' => $entity_type, 'label' => t('Text multiple'), 'settings' => array( 'text_processing' => 0, ), ); field_create_instance($instance); // Text - multiple, with text processing. $field = array( 'field_name' => 'text_multiple_processing', 'type' => 'text_long', 'entity_types' => array($entity_type), 'cardinality' => FIELD_CARDINALITY_UNLIMITED, ); field_create_field($field); $instance = array( 'field_name' => 'text_multiple_processing', 'bundle' => $bundle, 'entity_type' => $entity_type, 'label' => t('Text multiple with text processing'), 'settings' => array( 'text_processing' => 1, ), ); field_create_instance($instance); // Entity reference - single. $field = array( 'entity_types' => array($entity_type), 'settings' => array( 'handler' => 'base', 'target_type' => $entity_type, 'handler_settings' => array( ), ), 'field_name' => 'entity_reference_single', 'type' => 'entityreference', 'cardinality' => 1, ); field_create_field($field); $instance = array( 'entity_type' => $entity_type, 'field_name' => 'entity_reference_single', 'bundle' => $bundle, 'label' => t('Entity reference single'), ); field_create_instance($instance); // Entity reference - multiple. $field = array( 'entity_types' => array($entity_type), 'settings' => array( 'handler' => 'base', 'target_type' => $entity_type, 'handler_settings' => array( ), ), 'field_name' => 'entity_reference_multiple', 'type' => 'entityreference', 'cardinality' => FIELD_CARDINALITY_UNLIMITED, ); field_create_field($field); $instance = array( 'entity_type' => $entity_type, 'field_name' => 'entity_reference_multiple', 'bundle' => $bundle, 'label' => t('Entity reference multiple'), ); field_create_instance($instance); $vocabulary_id = restful_test_create_vocabulary_and_terms(); // Taxonomy term - single. $field = array( 'field_name' => 'term_single', 'type' => 'taxonomy_term_reference', 'entity_types' => array($entity_type), 'cardinality' => 1, ); field_create_field($field); $instance = array( 'field_name' => 'term_single', 'bundle' => $bundle, 'entity_type' => $entity_type, 'label' => t('Term reference single'), 'settings' => array( 'settings' => array( 'allowed_values' => array( array( 'vocabulary' => $vocabulary_id, ), ), ), ), ); field_create_instance($instance); // Taxonomy term - multiple. $field = array( 'field_name' => 'term_multiple', 'type' => 'taxonomy_term_reference', 'entity_types' => array($entity_type), 'cardinality' => FIELD_CARDINALITY_UNLIMITED, ); field_create_field($field); $instance = array( 'field_name' => 'term_multiple', 'bundle' => $bundle, 'entity_type' => $entity_type, 'label' => t('Term reference multiple'), 'settings' => array( 'settings' => array( 'allowed_values' => array( array( 'vocabulary' => $vocabulary_id, ), ), ), ), ); field_create_instance($instance); // File field - single. $field = array( 'field_name' => 'file_single', 'type' => 'file', 'settings' => array(), 'cardinality' => 1, ); field_create_field($field); $instance = array( 'field_name' => 'file_single', 'entity_type' => $entity_type, 'label' => 'File single', 'bundle' => $bundle, ); field_create_instance($instance); // File field - multiple. $field = array( 'field_name' => 'file_multiple', 'type' => 'file', 'settings' => array(), 'cardinality' => FIELD_CARDINALITY_UNLIMITED, ); field_create_field($field); $instance = array( 'field_name' => 'file_multiple', 'entity_type' => $entity_type, 'label' => 'File multiple', 'bundle' => $bundle, ); field_create_instance($instance); // Image field - single. $field = array( 'field_name' => 'image_single', 'type' => 'image', 'settings' => array(), 'cardinality' => 1, ); field_create_field($field); $instance = array( 'field_name' => 'image_single', 'entity_type' => $entity_type, 'label' => 'Image single', 'bundle' => $bundle, ); field_create_instance($instance); // Image field - multiple. $field = array( 'field_name' => 'image_multiple', 'type' => 'image', 'settings' => array(), 'cardinality' => FIELD_CARDINALITY_UNLIMITED, ); field_create_field($field); $instance = array( 'field_name' => 'image_multiple', 'entity_type' => $entity_type, 'label' => 'Image multiple', 'bundle' => $bundle, ); field_create_instance($instance); return $vocabulary_id; } /** * Helper function; Create a vocabulary and terms. * * @param string $machine_name * The machine name of the vocabulary. Defaults to 'test_vocab'. * @param bool $create_vocab * Determines if to create a vocabulary, or use an existing one. * * @return int * The newly created vocabulary ID. */ function restful_test_create_vocabulary_and_terms($machine_name = 'test_vocab', $create_vocab = TRUE) { if ($create_vocab) { $vocabulary = (object) array( 'name' => 'Tags test', 'description' => '', 'machine_name' => $machine_name, ); taxonomy_vocabulary_save($vocabulary); } else { $vocabulary = taxonomy_vocabulary_machine_name_load($machine_name); } $vid = $vocabulary->vid; // Create three terms. foreach (array(1, 2, 3) as $id) { $values = array( 'name' => 'term' . $id, 'vid' => $vid, ); $term = entity_create('taxonomy_term', $values); taxonomy_term_save($term); } return $vid; } /** * Helper function; Create a node with taxonomy terms. * * @return object * The saved node. */ function restful_test_create_node_with_tags() { $values = array('type' => 'article'); $node = entity_create('node', $values); $vocabulary = taxonomy_vocabulary_machine_name_load('tags'); // Create a random number of tags for the created node. for ($index = 0; $index < mt_rand(1, 10); $index++) { $term = (object) array( 'vid' => $vocabulary->vid, 'name' => 'term ' . $vocabulary->vid . '::' . $index, ); taxonomy_term_save($term); $terms[] = $term; $node->field_tags[LANGUAGE_NONE][$index]['tid'] = $term->tid; } node_save($node); return $node; } /** * Implements hook_entity_info(). */ function restful_test_entity_info() { return array( 'restful_test_translatable_entity' => array( 'label' => t('Translatable Test Entity'), 'plural label' => t('Translatable Test Entities'), 'description' => t('An entity type used by the RESTful tests.'), 'entity class' => 'Entity', 'controller class' => 'EntityAPIController', 'base table' => 'restful_test_translatable_entity', 'fieldable' => TRUE, 'entity keys' => array( 'id' => 'pid', 'bundle' => 'name', 'label' => 'label', 'uuid' => 'uuid', ), // Make use the class' label() and uri() implementation by default. 'label callback' => 'entity_class_label', 'uri callback' => 'entity_class_uri', 'bundles' => array( 'restful_test_translatable_entity' => array( 'label' => 'Translatable Test Entity', ), ), 'bundle keys' => array( 'bundle' => 'name', ), 'module' => 'restful_test', 'translation' => array( 'locale' => TRUE, ), 'uuid' => TRUE, ), ); } /** * Implements hook_restful_resource_alter(). * * Decorate an existing resource with other services (e.g. rate limit and render * cache). */ function restful_test_restful_resource_alter(\Drupal\restful\Plugin\resource\ResourceInterface &$resource) { // Disable the Files Upload resource based on the settings variable. if ($resource->getResourceMachineName() == 'files_upload_test') { variable_get('restful_file_upload', FALSE) ? $resource->enable() : $resource->disable(); } } /** * Implements hook_entity_info_alter(). */ function restful_test_entity_info_alter(&$entity_info) { $entity_info['entity_test']['uuid'] = TRUE; $entity_info['entity_test']['entity keys']['uuid'] = 'uuid'; } /** * Implements hook_entity_property_info_alter(). */ function restful_test_entity_property_info_alter(&$info) { $entity_types = array('entity_test', 'restful_test_translatable_entity'); foreach ($entity_types as $entity_type) { $entity_info = entity_get_info($entity_type); if (isset($entity_info['uuid']) && $entity_info['uuid'] == TRUE && !empty($entity_info['entity keys']['uuid']) && empty($info[$entity_type]['properties'][$entity_info['entity keys']['uuid']])) { $info[$entity_type]['properties'][$entity_info['entity keys']['uuid']] = array( 'label' => t('UUID'), 'type' => 'text', 'description' => t('The universally unique ID.'), 'schema field' => $entity_info['entity keys']['uuid'], ); if (!empty($entity_info['entity keys']['revision uuid']) && empty($info[$entity_type]['properties'][$entity_info['entity keys']['revision uuid']])) { $info[$entity_type]['properties'][$entity_info['entity keys']['revision uuid']] = array( 'label' => t('Revision UUID'), 'type' => 'text', 'description' => t("The revision's universally unique ID."), 'schema field' => $entity_info['entity keys']['revision uuid'], ); } } } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/DataProvider/DataProviderFileTest.php ================================================ 'id'); $fields['string'] = array('property' => 'str_field'); $fields['integer'] = array('property' => 'int_field'); $fields['serialized'] = array('property' => 'serialized_field'); return $fields; } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/entity_test/EntityTests__1_0.php ================================================ 'pid', 'class' => '\Drupal\restful\Plugin\resource\Field\ResourceFieldEntityReference', 'resource' => array( 'name' => 'main', 'majorVersion' => 1, 'minorVersion' => 0, ), ); $public_fields['tests_bundle'] = array( 'property' => 'pid', 'class' => '\Drupal\restful\Plugin\resource\Field\ResourceFieldEntityReference', 'resource' => array( 'name' => 'tests', 'majorVersion' => 1, 'minorVersion' => 0, ), ); return $public_fields; } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/entity_test/main/v1/Main__1_0.php ================================================ 'text_single', ); $public_fields['text_multiple'] = array( 'property' => 'text_multiple', 'discovery' => array( 'info' => array( 'label' => t('Text multiple'), 'description' => t('This field holds different text inputs.'), ), 'data' => array( 'type' => 'string', 'cardinality' => FIELD_CARDINALITY_UNLIMITED, ), 'form_element' => array( 'type' => 'textfield', 'size' => 255, 'placeholder' => t('This is helpful.'), ), ), ); $public_fields['text_single_processing'] = array( 'property' => 'text_single_processing', 'sub_property' => 'value', ); $public_fields['text_multiple_processing'] = array( 'property' => 'text_multiple_processing', 'sub_property' => 'value', ); $public_fields['entity_reference_single'] = array( 'property' => 'entity_reference_single', 'wrapper_method' => 'getIdentifier', ); $public_fields['entity_reference_multiple'] = array( 'property' => 'entity_reference_multiple', 'wrapper_method' => 'getIdentifier', ); // Single entity reference field with "resource". $public_fields['entity_reference_single_resource'] = array( 'property' => 'entity_reference_single', 'resource' => array( 'name' => 'main', 'majorVersion' => 1, 'minorVersion' => 1, ), ); // Multiple entity reference field with "resource". $public_fields['entity_reference_multiple_resource'] = array( 'property' => 'entity_reference_multiple', 'resource' => array( 'name' => 'main', 'majorVersion' => 1, 'minorVersion' => 1, ), ); $public_fields['term_single'] = array( 'property' => 'term_single', 'sub_property' => 'tid', ); $public_fields['term_multiple'] = array( 'property' => 'term_multiple', 'sub_property' => 'tid', ); $public_fields['file_single'] = array( 'property' => 'file_single', 'process_callbacks' => array( array($this, 'getFilesId'), ), ); $public_fields['file_multiple'] = array( 'property' => 'file_multiple', 'process_callbacks' => array( array($this, 'getFilesId'), ), ); $public_fields['image_single'] = array( 'property' => 'image_single', 'process_callbacks' => array( array($this, 'getFilesId'), ), ); $public_fields['image_multiple'] = array( 'property' => 'image_multiple', 'process_callbacks' => array( array($this, 'getFilesId'), ), ); return $public_fields; } /** * Return the files ID from the multiple files array. * * Since by default Entity API does not allow to get the file ID, we extract * it ourself in this preprocess callback. * * @param array $value * Array of files array as retrieved by the wrapper. * * @return int * Array with file IDs. */ public function getFilesId(array $value) { if (ResourceFieldBase::isArrayNumeric($value)) { $return = array(); foreach ($value as $file_array) { $return[] = $this->getFilesId($file_array); } return $return; } return empty($value['fid']) ? NULL : $value['fid']; } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/entity_test/main/v1/Main__1_2.php ================================================ array($this, 'callback'), ); $public_fields['process_callback_from_callback'] = array( 'callback' => array($this, 'callback'), 'process_callbacks' => array( array($this, 'processCallbackFromCallback'), ), ); $public_fields['process_callback_from_value'] = array( 'wrapper_method' => 'getIdentifier', 'wrapper_method_on_entity' => TRUE, 'process_callbacks' => array( array($this, 'processCallbackFromValue'), ), ); return $public_fields; } /** * Return a computed value. * * @param DataInterpreterInterface $interpreter * The data interpreter. * * @return mixed * The output for the computed field. */ public function callback(DataInterpreterInterface $interpreter) { return 'callback'; } /** * Process a computed value. */ public function processCallbackFromCallback($value) { return $value . ' processed from callback'; } /** * Process a property value. */ public function processCallbackFromValue($value) { return $value . ' processed from value'; } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/entity_test/main/v1/Main__1_3.php ================================================ array($this, 'invalidCallback'), ); return $public_fields; } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/entity_test/main/v1/Main__1_4.php ================================================ 'label', 'wrapper_method_on_entity' => TRUE, 'process_callbacks' => array( array($this, 'invalidProcessCallback'), ), ); return $public_fields; } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/entity_test/main/v1/Main__1_5.php ================================================ 'label', 'wrapper_method_on_entity' => TRUE, 'process_callbacks' => array( array($this, 'invalidProcessCallback'), ), ); return $public_fields; } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/entity_test/main/v1/Main__1_6.php ================================================ 'uuid', 'methods' => array(), ); // Single entity reference field without "resource". $public_fields['entity_reference_single'] = array( 'property' => 'entity_reference_single', 'referencedIdProperty' => 'uuid', ); // Single entity reference field with "resource" that loads by uuid. $public_fields['entity_reference_resource'] = array( 'property' => 'entity_reference_single', 'referencedIdProperty' => 'uuid', 'resource' => array( 'name' => 'main', 'majorVersion' => 1, 'minorVersion' => 7, ), ); // Multiple entity reference field without "resource". $public_fields['entity_reference_multiple'] = array( 'property' => 'entity_reference_multiple', 'referencedIdProperty' => 'uuid', ); $public_fields['term_single'] = array( 'property' => 'term_single', 'referencedIdProperty' => 'uuid', ); $public_fields['term_multiple'] = array( 'property' => 'term_multiple', 'referencedIdProperty' => 'uuid', ); $public_fields['file_single'] = array( 'property' => 'file_single', 'class' => '\Drupal\restful\Plugin\resource\Field\ResourceFieldFileEntityReference', 'referencedIdProperty' => 'uuid', ); $public_fields['file_multiple'] = array( 'property' => 'file_multiple', 'class' => '\Drupal\restful\Plugin\resource\Field\ResourceFieldFileEntityReference', 'referencedIdProperty' => 'uuid', ); return $public_fields; } /** * {@inheritdoc} */ protected function processPublicFields(array $field_definitions) { if (variable_get('restful_test_alternative_id_error', FALSE)) { // Single entity reference field with "resource" that does not load by // uuid. $field_definitions['entity_reference_resource_error'] = array( 'property' => 'entity_reference_single', 'referencedIdProperty' => 'uuid', 'resource' => array( 'name' => 'main', 'majorVersion' => 1, 'minorVersion' => 6, ), ); } return parent::processPublicFields($field_definitions); } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/entity_test/main/v1/Main__1_8.php ================================================ $class . '::randomRelationship', 'class' => '\Drupal\restful\Plugin\resource\Field\ResourceFieldReference', 'resource' => array( 'name' => 'db_query_test', 'majorVersion' => 1, 'minorVersion' => 0, ), ); return $public_fields; } /** * Returns a random relationship. * * This serves as an example of a use case for the generic relationship. * * @param DataInterpreterInterface $interpreter * The data interpreter. * * @return mixed * The embeddable result. */ public static function randomRelationship(DataInterpreterInterface $interpreter) { /* @var \Drupal\restful\Plugin\resource\ResourceInterface $handler */ $handler = restful()->getResourceManager()->getPlugin('db_query_test:1.0'); // This simbolizes some complex logic that gets a rendered resource. $id = static::complexCalculation(); return $handler->getDataProvider()->view($id); } /** * Do a complex calculation. * * @return int * The ID of the db_query_test. */ protected static function complexCalculation() { return 1; } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/entity_test/tests/Tests__1_0.php ================================================ array( 'wrapper_method' => 'getBundle', 'wrapper_method_on_entity' => TRUE, ), ) + parent::publicFields(); } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/file/file_upload_test/v1/FilesUploadTest__1_0.php ================================================ 'body', 'sub_property' => 'value', ); return $public_fields; } /** * {@inheritdoc} */ protected function processPublicFields(array $field_definitions) { $field_definitions = parent::processPublicFields($field_definitions); if (!$altered_public_name = variable_get('restful_test_revoke_public_field_access')) { return $field_definitions; } foreach ($field_definitions as $public_name => &$field_definition) { if ($public_name != $altered_public_name) { continue; } $field_definition['access_callbacks'] = array(array($this, 'publicFieldAccessFalse')); } return $field_definitions; } /** * An access callback that returns TRUE if title is "access". Otherwise FALSE. * * @param string $op * The operation that access should be checked for. Can be "view" or "edit". * Defaults to "edit". * @param ResourceFieldInterface $resource_field * The resource field to check access upon. * @param DataInterpreterInterface $interpreter * The data interpreter. * * @return string * "Allow" or "Deny" if user has access to the property. */ public static function publicFieldAccessFalse($op, ResourceFieldInterface $resource_field, DataInterpreterInterface $interpreter) { return $interpreter->getWrapper()->label() == 'access' ? \Drupal\restful\Plugin\resource\Field\ResourceFieldBase::ACCESS_ALLOW : \Drupal\restful\Plugin\resource\Field\ResourceFieldBase::ACCESS_DENY; } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/node/test_article/v1/TestArticles__1_1.php ================================================ 'body', 'sub_property' => 'value', ); // By checking that the field exists, we allow re-using this class on // different tests, where different fields exist. if (field_info_field('entity_reference_single')) { $public_fields['entity_reference_single'] = array( 'property' => 'entity_reference_single', 'resource' => array( 'name' => 'test_articles', 'majorVersion' => 1, 'minorVersion' => 2, ), ); } if (field_info_field('entity_reference_multiple')) { $public_fields['entity_reference_multiple'] = array( 'property' => 'entity_reference_multiple', 'resource' => array( 'name' => 'test_articles', 'majorVersion' => 1, 'minorVersion' => 2, ), ); } if (field_info_field('integer_single')) { $public_fields['integer_single'] = array( 'property' => 'integer_single', ); } if (field_info_field('integer_multiple')) { $public_fields['integer_multiple'] = array( 'property' => 'integer_multiple', ); } if (variable_get('restful_test_reference_simple')) { $public_fields['user'] = array( 'property' => 'author', ); if (variable_get('restful_test_reference_resource')) { $public_fields['user']['resource'] = array( 'name' => 'users', 'majorVersion' => 1, 'minorVersion' => 0, ); } } return $public_fields; } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/node/test_article/v1/TestArticles__1_3.php ================================================ 'view', 'access callback' => 'accessViewEntityFalse', ); $info['^.*$'][RequestInterface::METHOD_HEAD] = array( 'callback' => 'view', 'access callback' => 'accessViewEntityTrue', ); return $info; } /** * Custom access callback for the GET method. * * @return bool * TRUE for access granted, FALSE otherwise. */ public function accessViewEntityFalse() { return FALSE; } /** * Custom access callback for the HEAD method. * * @return bool * TRUE for access granted, FALSE otherwise. */ public function accessViewEntityTrue() { return TRUE; } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/node/test_article/v1/TestArticles__1_4.php ================================================ array( RequestInterface::METHOD_HEAD => 'index', RequestInterface::METHOD_OPTIONS => 'discover', ), '^(\d+,)*\d+$' => array( RequestInterface::METHOD_PATCH => 'update', RequestInterface::METHOD_DELETE => 'remove', RequestInterface::METHOD_OPTIONS => 'discover', ), ); } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/restful_test_translatable_entity/v1/RestfulTestTranslatableEntityResource__1_0.php ================================================ 'text_single', ); $public_fields['text_multiple'] = array( 'property' => 'text_multiple', ); return $public_fields; } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/taxonomy_term/v1/DataProviderTaxonomyTerm.php ================================================ getAccount(); return user_access('create article content', $account); } /** * {@inheritdoc} */ protected static function checkPropertyAccess(ResourceFieldInterface $resource_field, $op, DataInterpreterInterface $interpreter) { $term = $interpreter->getWrapper()->value(); if ($resource_field->getProperty() == 'name' && empty($term->tid) && $op == 'edit') { return TRUE; } return parent::checkPropertyAccess($resource_field, $op, $interpreter); } } ================================================ FILE: tests/modules/restful_test/src/Plugin/resource/taxonomy_term/v1/TestTags__1_0.php ================================================