610 lines
17 KiB
Plaintext
610 lines
17 KiB
Plaintext
<?php
|
|
|
|
/**
|
|
* @file
|
|
* Farm API module.
|
|
*/
|
|
|
|
define('FARM_API_VERSION', '1.4');
|
|
|
|
include_once 'farm_api.features.inc';
|
|
|
|
/**
|
|
* Implements hook_permission().
|
|
*/
|
|
function farm_api_permission() {
|
|
$perms = array(
|
|
'access farm api info' => 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_menu_alter().
|
|
*/
|
|
function farm_api_menu_alter(&$items) {
|
|
|
|
// Make OAuth2 token endpoints available at both oauth2/* and oauth/*.
|
|
// See https://www.drupal.org/project/farm/issues/3172818
|
|
$oauth_aliases = array(
|
|
'oauth2/authorize' => 'oauth/authorize',
|
|
'oauth2/revoke' => 'oauth/revoke',
|
|
'oauth2/token' => 'oauth/token',
|
|
);
|
|
foreach ($oauth_aliases as $source => $alias) {
|
|
if (!empty($items[$source])) {
|
|
$items[$alias] = $items[$source];
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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 (!empty($field_info['type']) && $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;
|
|
}
|