array( 'title' => t('Access the farmOS API info endpoint'), ), 'administer farm api oauth clients' => array( 'title' => t('Administer farmOS OAuth Clients.'), ), ); return $perms; } /** * Implements hook_menu(). */ function farm_api_menu() { $items = array(); // General farm information JSON endpoint. $items['farm.json'] = array( 'page callback' => 'farm_api_info', 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); // OAuth client configuration form. $items['admin/config/farm/oauth'] = array( 'title' => 'farmOS OAuth', 'description' => 'farmOS OAuth Client settings.', 'page callback' => 'drupal_get_form', 'page arguments' => array('farm_api_oauth_settings_form'), 'access arguments' => array('administer farm api oauth clients'), 'file' => 'farm_api.oauth.inc', ); return $items; } /* * Implements hook_farm_api_oauth2_client() */ function farm_api_farm_api_oauth2_client() { $clients = array(); // Provide default farmOS OAuth Client for general use. $clients['farm'] = array( 'label' => 'farmOS (Default)', 'client_key' => 'farm', 'redirect_uri' => '', 'settings' => array( 'override_grant_types' => TRUE, 'allow_implicit' => FALSE, 'grant_types' => array( 'password' => 'password', 'refresh_token' => 'refresh_token', ), 'always_issue_new_refresh_token' => TRUE, 'unset_refresh_token_after_use' => TRUE, ) ); return $clients; } /** * Farm info API callback. */ function farm_api_info() { // Start with an empty info array. $info = array(); // Check for an authenticated user with access to farmOS API info. $access = user_access('access farm api info'); // Iterate through all the modules that implement hook_farm_info. $hook = 'farm_info'; $modules = module_implements($hook); foreach($modules as $module) { // Invoke the hook to get info. $module_info = module_invoke($module, $hook); // If the info is empty, skip it. if (!is_array($module_info)) { continue; } // Iterate through the info items. foreach ($module_info as $key => $item) { // If the item is an array with an 'info' key, that is what we will // include. if (is_array($item) && !empty($item['info'])) { // If the user is authenticated with permission OR if an OAuth2 scope is // authorized for this request, add the item to the info array. if ($access || (!empty($item['scope']) && farm_api_check_scope($item['scope']))) { // Add the key to an array before merging. $item = array( $key => $item['info'], ); // Include in info. $info = array_merge($info, $item); } } // If item does not have an 'info' include if the user has access. // This is simple info that does not require a check for OAuth scope. elseif ($access) { // Add the key to an array before merging. $item = array( $key => $item, ); // Include in info. $info = array_merge($info, $item); } } } // Output as JSON. drupal_json_output($info); } /** * Implements hook_farm_api_farm_info(). */ function farm_api_farm_info() { global $base_url, $conf, $user, $language; // Include info that requires the farm_info scope. $info = array( 'name' => array( 'info' => $conf['site_name'], 'scope' => 'farm_info', ), 'url' => array( 'info' => $base_url, 'scope' => 'farm_info', ), 'api_version' => array( 'info' => FARM_API_VERSION, 'scope' => 'farm_info', ), ); // Include user info if logged in. if (!empty($user->uid)) { $info['user'] = array( 'uid' => $user->uid, 'name' => $user->name, 'mail' => $user->mail, 'language' => $language->language, ); } // Include list of installed languages. $languages = language_list(); foreach ($languages as $langcode => $language) { if (!empty($language->enabled)) { $info['languages'][$langcode] = array( 'language' => $language->language, 'name' => $language->name, 'native' => $language->native, 'direction' => $language->direction, ); } } return $info; } /** * Helper function to allow modules to check for an authorized scope in the * current request before providing info in an API endpoint. * * @param string $scope * A single OAuth Scope to check. * * @return bool * Return TRUE if request is authorized with specified scope. FALSE otherwise. */ function farm_api_check_scope($scope) { // Load the OAuth2 Server name $server_name = variable_get('restws_oauth2_server_name', FALSE); if (!$server_name) { return FALSE; } // Check OAuth scope. $result = oauth2_server_check_access($server_name, $scope); // Check if a Token was returned, or an error Response. if ($result instanceof \OAuth2\Response) { return FALSE; } // Return True if request is authorized with specified scope. return TRUE; } /** * Implements hook_modules_enabled(). */ function farm_api_modules_enabled($modules) { // If the modules provide OAuth2 clients, enable them. $hook = 'farm_api_oauth2_client'; foreach ($modules as $module) { $function = $module . '_' . $hook; if (function_exists($function)) { $clients = $function(); foreach ($clients as $client) { $label = !empty($client['label']) ? $client['label'] : ''; $client_key = !empty($client['client_key']) ? $client['client_key'] : ''; $client_secret = !empty($client['client_secret']) ? $client['client_secret'] : ''; $redirect_uri = !empty($client['redirect_uri']) ? $client['redirect_uri'] : ''; $settings = !empty($client['settings']) ? $client['settings'] : array(); if (!empty($label) && !empty($client_key)) { farm_api_enable_oauth_client($label, $client_key, $client_secret, $redirect_uri, $settings); } } } } } /** * Implements hook_modules_disabled(). */ function farm_api_modules_disabled($modules) { // If the modules provided OAuth2 clients, disable them. $hook = 'farm_api_oauth2_client'; foreach ($modules as $module) { $function = $module . '_' . $hook; if (function_exists($function)) { $clients = $function(); foreach ($clients as $client) { $client_id = db_query('SELECT client_id FROM {oauth2_server_client} WHERE client_key = :client_key', array(':client_key' => $client['client_key']))->fetchField(); if (!empty($client_id)) { entity_delete('oauth2_server_client', $client_id); } } } } } /** * Helper function for enabling a farmOS OAuth2 Client. * * @param string $label * The human-readable label for the client. * @param string $client_key * The machine name of the client to enable. * @param string $client_secret * Optional client secret. * @param string $redirect_uri * Optional redirect URIs (separated by newlines). * @param array $settings * Optional array of client settings to override server-level defaults. */ function farm_api_enable_oauth_client($label, $client_key, $client_secret = '', $redirect_uri = '', $settings = array()) { $server_name = variable_get('restws_oauth2_server_name', 'farmos_oauth'); // Create OAuth2 Server Client Entity $new_client = entity_create('oauth2_server_client', array()); $new_client->server = $server_name; $new_client->client_key = $client_key; $new_client->label = $label; // Add an optional client secret. if (!empty($client_secret)) { $new_client->client_secret = $client_secret; } // Add optional OAuth Client settings used to override OAuth2 // server-level settings. Do not set this value as an empty array. if (!empty($settings)) { $new_client->settings = $settings; } // The module supports entering multiple redirect uris separated by a // newline. Both a dummy and the real uri are specified to confirm that // validation passes. $new_client->redirect_uri = $redirect_uri; $new_client->automatic_authorization = FALSE; $new_client->save(); } /** * Implements hook_module_implements_alter(). */ function farm_api_module_implements_alter(&$implementations, $hook) { // We only want to alter hook_restws_request_alter() implementations. if ($hook != 'restws_request_alter') { return; } // If either restws_file or farm_api don't implement the hook, bail. $modules = array( 'restws_file', 'farm_api', ); foreach ($modules as $module) { if (!array_key_exists($module, $implementations)) { return; } } // Put farm_api's hook above restws_file's hook, so that our field aliasing // happens first. $implementations = array('farm_api' => $implementations['farm_api']) + $implementations; } /** * Implements hook_restws_format_info_alter(). * * Overrides the default JSON handler from restws with our own. */ function farm_api_restws_format_info_alter(&$format_info) { $format_info['json']['class'] = '\Drupal\farm_api\RestWS\Format\FarmFormatJSON'; } /** * Implements hook_restws_request_alter(). */ function farm_api_restws_request_alter(array &$request) { // If the format is not JSON, bail. if ($request['format']->getName() != 'json') { return; } // Build a field alias map to remove the 'field_farm_' prefix. $prefix = 'field_farm_'; $alias_map = farm_api_field_alias_map($prefix); // Get the entity type. $entity_type = NULL; if (!empty($request['resource']->resource())) { $entity_type = $request['resource']->resource(); } // If we are dealing with a taxonomy term, do not alias description or parent. if ($entity_type == 'taxonomy_term') { unset($alias_map['description']); unset($alias_map['parent']); } // In order to handle URL query string filters, we need to perform the alias // translation on all GET parameters. The restws module filters based on the // output of drupal_get_query_parameters(), which uses the $_GET global. foreach ($_GET as $name => &$value) { if (array_key_exists($name, $alias_map)) { $_GET[$alias_map[$name]] = $_GET[$name]; unset($_GET[$name]); } } // Allow filtering by term name in taxonomy term reference fields. // eg: /log.json?log_category=Tillage foreach ($_GET as $field_name => &$filter_value) { $field_info = field_info_field($field_name); if ($field_info['type'] == 'taxonomy_term_reference') { if ($vocabulary = drupal_array_get_nested_value($field_info, array('settings', 'allowed_values', '0', 'vocabulary'))) { if ($term = farm_term($filter_value, $vocabulary, FALSE)) { $_GET[$field_name] = $term->tid; } } } } // If the payload is empty, bail. if (empty($request['payload'])) { return; } // Decode the payload JSON. $payload = drupal_json_decode($request['payload']); // If the payload could not be decoded, bail. if (empty($payload)) { return; } // Keep track of whether or not any changes were made to the payload. $changed = FALSE; // Iterate through the fields in the payload. If any match a mapped alias, // translate it to use the real field name. foreach ($payload as $key => $value) { if (array_key_exists($key, $alias_map)) { $payload[$alias_map[$key]] = $payload[$key]; unset($payload[$key]); $changed = TRUE; } } // If a taxonomy term name is provided, look up its term ID. If it does not // exist, create it. foreach ($payload as $field_name => $field_values) { // Add special logic for the "unit" field in "Quantity" field collections. // Field collections are handled by the restws_field_collection module, and // they are skipped inside farm_api_field_alias_map(), so we just look for a // $field_name of "quantity", process any provided "name" through // farm_term(), set the term ID, and then let restws_field_collection do the // rest. if ($field_name == 'quantity') { // Set the vocabulary machine name. $vocabulary = 'farm_quantity_units'; // Iterate through field collection values and convert unit names to tids. if (!empty($field_values)) { foreach ($field_values as $delta => $field_value) { if (!empty($field_value['unit']['name'])) { $term = farm_term($field_value['unit']['name'], $vocabulary); if (!empty($term->tid)) { $payload[$field_name][$delta]['unit']['id'] = $term->tid; unset($payload[$field_name][$delta]['unit']['name']); } $changed = TRUE; } } } // We don't need to go any farther than this, so end this iteration. continue; } // Look up the field information and process taxonomy term names. $field_info = field_info_field($field_name); if (empty($field_info)) { continue; } if ($vocabulary = drupal_array_get_nested_value($field_info, array('settings', 'allowed_values', '0', 'vocabulary'))) { // If the field values contains a "name" property, we assume that it is a // single value field, so we convert it to an array and remember to // convert it back at the end. $single = FALSE; if (isset($field_values['name'])) { $field_values = array($field_values); $single = TRUE; } // Iterate through the field values and process term names. foreach ($field_values as $delta => $field_value) { if (!empty($field_value['name'])) { $term = farm_term($field_value['name'], $vocabulary); if ($single) { unset($payload[$field_name]['name']); $payload[$field_name]['id'] = $term->tid; } else { unset($payload[$field_name][$delta]['name']); $payload[$field_name][$delta]['id'] = $term->tid; } $changed = TRUE; } } } } // If we changed the payload, re-encode it as JSON. if ($changed) { $request['payload'] = drupal_json_encode($payload); } } /** * Implements hook_restws_response_alter(). */ function farm_api_restws_response_alter(&$response, $function, $formatName, $resourceController) { // If the format is not JSON, bail. if ($formatName != 'json') { return; } // If the response contains a list of entities, iterate through them and // pass each to farm_api_restws_response_alter_item(). if (!empty($response['list'])) { foreach ($response['list'] as &$item) { farm_api_restws_response_alter_item($item); } } // Otherwise, process the response directly. else { farm_api_restws_response_alter_item($response); } } /** * Helper function for altering a restws response item. * * @param $item * The restws response item, passed by reference. */ function farm_api_restws_response_alter_item(&$item) { // Build a field alias map to remove the 'field_farm_' prefix. $prefix = 'field_farm_'; $alias_map = farm_api_field_alias_map($prefix); // Flip the alias map so that it is keyed by actual field name. $field_aliases = array_flip($alias_map); // Iterate through the item properties. foreach (array_keys($item) as $key) { // If the field name exists in the alias map, replace it with the alias. if (array_key_exists($key, $field_aliases)) { $item[$field_aliases[$key]] = $item[$key]; unset($item[$key]); } // Remove Feeds properties. $feeds_prefixes = array( 'feed_', 'feeds_', ); foreach ($feeds_prefixes as $prefix) { if (strpos($key, $prefix) === 0) { unset($item[$key]); } } } } /** * Build a field alias map for restws requests and responses. * * @param string $prefix * The field name prefix to remove from fields. * * @return array * Returns an array of field names with the alias as the key, and the actual * field name as the value. */ function farm_api_field_alias_map($prefix) { // Start an empty map array. $alias_map = array(); // Load a list of all fields. $fields = field_info_field_map(); // Iterate through the fields to build an alias map. foreach ($fields as $field_name => $field_info) { // If the field is a field_collection, skip it. Field collection alias are a // special case that are currently handled by the restws_field_collection // module in farmOS. if ($field_info['type'] == 'field_collection') { continue; } // If the field name starts with the prefix, add it to the map. if (strpos($field_name, $prefix) === 0) { $alias = str_replace($prefix, '', $field_name); $alias_map[$alias] = $field_name; } } // Return the alias map. return $alias_map; }