diff --git a/modules/radar/radar_og/radar_og.module b/modules/radar/radar_og/radar_og.module index 3d22591f1b8564f49465eff05882f03f8fec06c5..2f20c5935066e77db09a4f17f838bbec5559e7f9 100644 --- a/modules/radar/radar_og/radar_og.module +++ b/modules/radar/radar_og/radar_og.module @@ -1177,4 +1177,19 @@ function radar_og_entityreference_request_prepopulate($entity_type, $entity, $fi return $items; } +function radar_og_can_post_request($gid, $type, $account = NULL) { + // Only checking non-member at the moment. There should be possibility that + // groups that have members who can propose, and admins who accept. Seems no + // one noticed this isn't an option at the moment? + static $non_member_gids = NULL; + + if (is_null($non_member_gids)) { + $query = db_query( + 'SELECT gid FROM {og_role_permission} p INNER JOIN {og_role} r ON r.rid = p.rid where (p.permission = :permission_1 OR p.permission = :permission_2) AND r.name = :role_name', + array(':permission_1' => 'subscribe', ':permission_2' => 'subscribe without approval', 'role_name' => 'non-member')); + $non_member_gids = $query->fetchCol(); + } + + return in_array($gid, $non_member_gids); +} diff --git a/modules/radar/radar_services/plugins/services_entity/resource_controller.inc b/modules/radar/radar_services/plugins/services_entity/resource_controller.inc index 80c60359e9d8f73d895de3a706fab0848aad73d2..3abb01b38e2b9e4982f817a98b3ad5005eb4ba1a 100644 --- a/modules/radar/radar_services/plugins/services_entity/resource_controller.inc +++ b/modules/radar/radar_services/plugins/services_entity/resource_controller.inc @@ -1,14 +1,8 @@ <?php + /** - * This class is designed to create a very clean API that integrates with - * the services and entity modules. We want to strip all "drupalisms" out - * of the API. For example, there should be no [LANGUAGE_NONE][0][value] or - * field_ in the API. - * - * It should be possible to create an API that is easily replicated on another - * system. - * - * Much of this code is borrowed from restws module. + * Ugly mash-up of Services Entity Resource Clean, Services UUID Resource, and + * custom submission rules. */ class RadarServicesEntityResourceController extends ServicesEntityResourceControllerClean { @@ -38,6 +32,42 @@ class RadarServicesEntityResourceController extends ServicesEntityResourceContro // Access is handled per-entity by index(). return TRUE; } + // Further restrict Create, Delete and Update operations + // even if on the website ui it could be otherwise. + if ($op != 'view' && !user_access('services create update delete operations')) { + return FALSE; + } + if ($op != 'create') { + // Retrieve, Delete, Update. + list($entity_type, $entity_uuid) = $args; + if ($entity_type == 'location' && $op != 'view') { + services_error(t('If you need to edit locations via the API please contact radar@squat.net'), 403); + } + $info = entity_get_info($entity_type); + $uuid_key = $info['entity keys']['uuid']; + $ids = entity_get_id_by_uuid($entity_type, [$entity_uuid]); + $entity_id = reset($ids); + + if (empty($entity_id)) { + if ($op == 'update') { + // Create operation with specified UUID. Check $op == 'create'. + // Sorry this reads like rubbish, but this is the only option here + // that falls through to the next part of the function the rest return + // something. + $op = 'create'; + $args = [$entity_type, $args[2]]; + } + else { + services_error(t('Entity not found'), 404); + } + } + else { + $entity = entity_load_single($entity_type, $entity_id); + + // Pass the entity to the access control. + return entity_access($op, $entity_type, $entity ? $entity : NULL); + } + } // For create operations, we need to pass a new entity to entity_access() // in order to check per-bundle creation rights. For all other operations // we load the existing entity instead. @@ -50,19 +80,106 @@ class RadarServicesEntityResourceController extends ServicesEntityResourceContro } // Create a wrapper from the entity so we can call its access() method. $wrapper = $this->createWrapperFromValues($entity_type, $data); - return $wrapper->entityAccess('create'); list($entity_type, $data) = $args; + return $wrapper->entityAccess('create'); } - else { - // Retrieve, Delete, Update. - list($entity_type, $entity_id) = $args; - $entity = entity_uuid_load($entity_type, [$entity_id]); - $entity = reset($entity); + } - // Pass the entity to the access control. - return entity_access($op, $entity_type, $entity ? $entity : NULL); + public function create($entity_type, array $values) { + $this->formSubmissionChecks('create', $entity_type, $values); + $info = entity_get_info($entity_type); + $uuid_key = $info['entity keys']['uuid']; + // Mixing UUID and EntityMetadataWrapper. Need the author already with UID. + if (!empty($values['author']['id'])) { + $uids = entity_get_id_by_uuid('user', [$values['author']['id']]); + $values['author']['id'] = reset($uids); } + $wrapper = $this->createWrapperFromValues($entity_type, $values); + // Check write access on each property. + foreach (array_keys($values) as $name) { + if (!$this->propertyAccess($wrapper, $name, 'create')) { + services_error(t("Not authorized to set property '@p'", array('@p' => $name)), 403); + } + } + + // Make sure that bundle information is present on entities that have + // bundles. We have to do this after creating the wrapper, because the + // name of the bundle key may differ from that of the corresponding + // metadata property (e.g. for taxonomy terms, the bundle key is + // 'vocabulary_machine_name', while the property is 'vocabulary'). + if ($bundle_key = $wrapper->entityKey('bundle')) { + $entity = $wrapper->value(); + if (empty($entity->{$bundle_key})) { + $entity_info = $wrapper->entityInfo(); + if (isset($entity_info['bundles']) && count($entity_info['bundles']) === 1) { + // If the entity supports only a single bundle, then use that as a + // default. This allows creation of such entities if (as with ECK) + // they still use a bundle key. + $entity->{$bundle_key} = reset($entity_info['bundles']); + } + else { + services_error('Missing bundle: ' . $bundle_key, 406); + } + } + } + + $properties = $wrapper->getPropertyInfo(); + $diff = array_diff_key($values, $properties); + if (!empty($diff)) { + services_error('Unknown data properties: ' . implode(' ', array_keys($diff)) . '.', 406); + } + $entity = $wrapper->value(); + // Manually adds UUID if required. + uuid_entity_presave($entity, $entity_type); + entity_uuid_save($entity_type, $entity); + return $this->retrieve($entity_type, $entity->{$uuid_key}, '*', NULL); + } + + public function update($entity_type, $entity_uuid, array $values) { + $info = entity_get_info($entity_type); + $uuid_key = $info['entity keys']['uuid']; + $values[$uuid_key] = $entity_uuid; + $this->formSubmissionChecks('update', $entity_type, $values); + $ids = entity_get_id_by_uuid($entity_type, [$entity_uuid]); + $entity_id = reset($ids); + if (empty($entity_id)) { + // UUID is not existing, a PUT to create with specified UUID. + $values[$uuid_key] = $entity_uuid; + return $this->create($entity_type, $values); + } + if (!empty($values['author']['id'])) { + $uids = entity_get_id_by_uuid('user', [$values['author']['id']]); + $values['author']['id'] = reset($uids); + } + $property_info = entity_get_all_property_info($entity_type); + $values = $this->transform_values($entity_type, $property_info, $values); + try { + $wrapper = entity_metadata_wrapper($entity_type, $entity_id); + foreach ($values as $name => $value) { + // Only attempt to set properties when the new value differs from that + // on the existing entity; otherwise, requests will fail for read-only + // and unauthorized properties, even if they are not being changed. This + // allows us to UPDATE a previously retrieved entity without removing + // such properties from the payload, as long as they are unchanged. + if (!$this->propertyHasValue($wrapper, $name, $value)) { + // We set the property before checking access so the new value + // will be passed to the access callback. This is necesssary in + // some cases (e.g. text-format fields) where access permissions + // depend on the value that is being set. + $wrapper->{$name}->set($value); + if (!$this->propertyAccess($wrapper, $name, 'update')) { + services_error(t("Not authorized to set property '@property-name'.", array('@property-name' => $name)), 403); + } + } + } + } + catch (EntityMetadataWrapperException $e) { + services_error($e->getMessage(), 406); + } + $entity = $wrapper->value(); + entity_uuid_save($entity_type, $entity); + return $this->retrieve($entity_type, $entity->{$uuid_key}, '*', NULL); } /** @@ -246,7 +363,7 @@ class RadarServicesEntityResourceController extends ServicesEntityResourceContro $data[$name] = $property->value(); } elseif ($property instanceof EntityListWrapper) { - if (count($fields_array[$name])) { + if (isset($field_array[$name]) && count($fields_array[$name])) { $property_fields = implode(',', $fields_array[$name]); } else { @@ -255,7 +372,7 @@ class RadarServicesEntityResourceController extends ServicesEntityResourceContro $data[$name] = $this->get_data($property, $property_fields); } elseif ($property instanceof EntityStructureWrapper) { - if (count($fields_array[$name])) { + if (isset($field_array[$name]) && count($fields_array[$name])) { $property_fields = implode(',', $fields_array[$name]); } else { @@ -335,12 +452,52 @@ class RadarServicesEntityResourceController extends ServicesEntityResourceContro * @return type */ protected function transform_values($entity_type, $property_info, $values) { - foreach($values as $key => $value) { + foreach ($values as $key => $value) { + // Stop gap while we still have a field_type. + if ($key == 'type') { + continue; + } // Handle Resource references so we can pass pack the object. if (is_array($value) && isset($value['id'])) { $values[$key] = $value['id']; } - // Check if this is actually a field_ value + // Also if it's a multiple field. + if (is_array($value)) { + foreach ($value as $delta => $item) { + // Handle Resource references so we can pass pack the object. + if (is_array($item) && isset($item['id'])) { + $values[$key][$delta] = $item['id']; + } + // Reformat incoming date. + if (is_array($item) && isset($item['time_start'])) { + if (empty($item['timezone'])) { + services_error(t('Timezone required: @key', ['@key' => $key]), 400); + } + $date = $item['time_start']; + $date_match = []; + if (preg_match('/^(\d{4}-\d{2}-\d{2}).(\d{2}:\d{2}:\d{2})/', $date, $date_match)) { + $values[$key][$delta]['value'] = $date_match[1] . ' ' . $date_match[2]; + } + else { + services_error(t('Date @key time_start required format YYYY-MM-DD HH:MM:SS', ['@key' => $key]), 400); + } + if (!empty($item['time_end'])) { + $date = $item['time_end']; + $date_match = []; + if (preg_match('/^(\d{4}-\d{2}-\d{2}).(\d{2}:\d{2}:\d{2})/', $date, $date_match)) { + $values[$key][$delta]['value2'] = $date_match[1] . ' ' . $date_match[2]; + } + else { + services_error(t('Date @key time_end required format YYYY-MM-DD HH:MM:SS', ['@key' => $key]), 400); + } + } + else { + $values[$key][$delta]['value2'] = $date_match[1] . ' ' . $date_match[2]; + } + } + } + } + // Check if this is actually a field_ value. if (isset($property_info['field_' . $key])) { $values['field_' . $key] = $values[$key]; unset($values[$key]); @@ -400,4 +557,157 @@ class RadarServicesEntityResourceController extends ServicesEntityResourceContro return $value == $property->value(); } + // Validation. + // + // Various permissions assume access via the form. These need to recreate + // this: + // * groups are unpublished. + // * users without moderation permissions can't change owner, status, date on + // node. + // * og permissions are checked for groups and proposed groups. + private function formSubmissionChecks($op, $entity_type, &$values) { + global $user; + + if ($op == 'update') { + if (!user_access('administer nodes')) { + unset($values['uid'], $values['author'], $values['name'], $values['created'], $values['revision']); + } + if (isset($values['log'])) { + $values['log'] .= "\nVia API.\n"; + } + else { + $values['log'] = 'Via API'; + } + + if (isset($values['og_group_ref']) || isset($values['og_group_requst'])) { + $info = entity_get_info($entity_type); + $uuid_key = $info['entity keys']['uuid']; + $entity_ids = entity_get_id_by_uuid($entity_type, [$values[$uuid_key]]); + $entity_id = reset($entity_ids); + $original = entity_metadata_wrapper($entity_type, $entity_id); + $this->filterUpdatePostingGroups($values['og_group_ref'], $values['og_group_request'], $entity_type, $values['type'], $original); + if (empty($values['og_group_ref'])) { + $values['status'] = 0; + } + } + } + if ($op == 'create') { + $values['author'] = ['id' => $user->uuid]; + $values['created'] = time(); + if (isset($values['log'])) { + $values['log'] .= "\nVia API.\n"; + } + else { + $values['log'] = 'Via API'; + } + if (!user_access('administer nodes')) { + if ($entity_type == 'node') { + if ($values['type'] == 'event') { + $this->filterCreatePostingGroups($values['og_group_ref'], $values['og_group_request'], $values['type']); + if (empty($values['og_group_ref'])) { + $values['status'] = 0; + } + } + else { + $values['status'] = 0; + } + } + } + } + } + + private function filterCreatePostingGroups(&$og_group_ref, &$og_group_request, $type) { + $post = []; + $request = []; + + foreach ((array) $og_group_ref as $group) { + $gids = entity_get_id_by_uuid('node', [$group['id']]); + $gid = reset($gids); + if (og_user_access('node', $gid, "create $type content")) { + $post[] = $group; + } + else { + // Push to see if it can be a request. + $og_group_request[] = $group; + } + } + + foreach ((array) $og_group_request as $group) { + $gids = entity_get_id_by_uuid('node', [$group['id']]); + $gid = reset($gids); + if (radar_og_can_post_request($gid, $type)) { + $request[] = $group; + } + } + + $og_group_ref = $post; + $og_group_request = $request; + } + + private function filterUpdatePostingGroups(&$og_group_ref, &$og_group_request, $entity_type, $type, $original) { + global $user; + $uid = $user->uid; + + $post = []; + foreach ((array) $og_group_ref as $group) { + $nids = entity_get_id_by_uuid($entity_type, [$group['id']]); + $post[] = reset($nids); + } + print_r($post); + $request = []; + foreach ((array) $og_group_request as $group) { + $nids = entity_get_id_by_uuid($entity_type, [$group['id']]); + $request[] = reset($nids); + } + + $new_groups = array_diff($post, $original->og_group_ref->raw()); + $del_groups = array_diff($original->og_group_ref->raw(), $post); + $new_request = array_diff($request, $original->og_group_request->raw()); + $del_request = array_diff($original->og_group_request->raw(), $request); + + foreach ($new_groups as $gid) { + if (!og_user_access('node', $gid, "create $type content")) { + $uuids = entity_get_uuid_by_id('node', [$gid]); + $uuid = reset($uuids); + foreach ($og_group_ref as $key => $ref) { + if ($ref['id'] == $uuid) { + unset($og_group_ref[$key]); + } + } + } + } + + if ($original->author->uid != $uid) { + foreach ($del_groups as $gid) { + if (!og_user_access('node', $gid, "create $type content")) { + $uuids = entity_get_uuid_by_id('node', [$gid]); + $uuid = reset($uuids); + $og_group_ref[] = ['id' => $uuid]; + } + } + } + + foreach ($new_request as $gid) { + if (!radar_og_can_post_request($gid, $type)) { + $uuids = entity_get_uuid_by_id('node', [$gid]); + $uuid = reset($uuids); + foreach ($og_group_request as $key => $ref) { + if ($ref['id'] == $uuid) { + unset($og_group_request[$key]); + } + } + } + } + + if ($original->author->uid != $uid) { + foreach ($del_request as $gid) { + if (!og_user_access('node', $gid, "create $type content")) { + $uuids = entity_get_uuid_by_id('node', [$gid]); + $uuid = reset($uuids); + $og_group_ref[] = ['id' => $uuid]; + } + } + } + } + } diff --git a/modules/radar/radar_services/radar_services.features.user_permission.inc b/modules/radar/radar_services/radar_services.features.user_permission.inc index cc3dd56fb9abcf6ab74907b485181919f0ae6297..7174b9f8063b274cfab2c1c2fc1080fe9a996955 100644 --- a/modules/radar/radar_services/radar_services.features.user_permission.inc +++ b/modules/radar/radar_services/radar_services.features.user_permission.inc @@ -10,6 +10,15 @@ function radar_services_user_default_permissions() { $permissions = array(); + // Exported permission: 'services create update delete operations'. + $permissions['services create update delete operations'] = array( + 'name' => 'services create update delete operations', + 'roles' => array( + 'authenticated user' => 'authenticated user', + ), + 'module' => 'radar_services', + ); + // Exported permission: 'services_search_api search from any index'. $permissions['services_search_api search from any index'] = array( 'name' => 'services_search_api search from any index', diff --git a/modules/radar/radar_services/radar_services.info b/modules/radar/radar_services/radar_services.info index 79fb36a5be0804244c810cd477c80337f04d60ce..fffbadfdad98f45d31d62ca604c3a0cdea7b77e4 100644 --- a/modules/radar/radar_services/radar_services.info +++ b/modules/radar/radar_services/radar_services.info @@ -7,6 +7,7 @@ project = radar_services dependencies[] = cors dependencies[] = ctools dependencies[] = features +dependencies[] = radar_services dependencies[] = rest_server dependencies[] = services dependencies[] = services_search_api @@ -17,6 +18,7 @@ features[features_api][] = api:2 features[services_endpoint][] = api_1_0 features[services_endpoint][] = api_1_1 features[services_endpoint][] = api_1_2 +features[user_permission][] = services create update delete operations features[user_permission][] = services_search_api search from any index features[variable][] = cors_domains features[variable][] = services_entity_resource_class diff --git a/modules/radar/radar_services/radar_services.module b/modules/radar/radar_services/radar_services.module index 31e48310ce0c91433c87caa3cb61ae2c35aa4acd..9c3a239fb3e0904fa215ca4acf5b71c37e07bf63 100644 --- a/modules/radar/radar_services/radar_services.module +++ b/modules/radar/radar_services/radar_services.module @@ -7,6 +7,18 @@ include_once 'radar_services.features.inc'; +/** + * Implements hook_permission(). + */ +function radar_services_permission() { + return [ + 'services create update delete operations' => [ + 'title' => t('Perform create, update and delete operations with services'), + 'description' => t('Additional permission to perform operations that could be done by the user via the website ui.'), + ], + ]; +} + /** * Implements hook_services_resources(). */