canvas-lms/app/controllers/custom_data_controller.rb

417 lines
14 KiB
Ruby

#
# Copyright (C) 2014 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
# @API Users
# @subtopic Custom Data
class CustomDataController < ApplicationController
before_action :require_namespace, :get_scope, :get_context
before_action :require_custom_data, :except => :set_data
# @API Store custom data
# @beta
# Store arbitrary user data as JSON.
#
# Arbitrary JSON data can be stored for a User.
# A typical scenario would be an external site/service that registers users in Canvas
# and wants to capture additional info about them. The part of the URL that follows
# +/custom_data/+ defines the scope of the request, and it reflects the structure of
# the JSON data to be stored or retrieved.
#
# The value +self+ may be used for +user_id+ to store data associated with the calling user.
# In order to access another user's custom data, you must be an account administrator with
# permission to manage users.
#
# A namespace parameter, +ns+, is used to prevent custom_data collisions between
# different apps. This parameter is required for all custom_data requests.
#
# A request with Content-Type multipart/form-data or Content-Type
# application/x-www-form-urlencoded can only be used to store strings.
#
# Example PUT with multipart/form-data data:
# curl 'https://<canvas>/api/v1/users/<user_id>/custom_data/telephone' \
# -X PUT \
# -F 'ns=com.my-organization.canvas-app' \
# -F 'data=555-1234' \
# -H 'Authorization: Bearer <token>'
#
# Response:
# !!!javascript
# {
# "data": "555-1234"
# }
#
# Subscopes (or, generated scopes) can also be specified by passing values to
# +data+[+subscope+].
#
# Example PUT specifying subscopes:
# curl 'https://<canvas>/api/v1/users/<user_id>/custom_data/body/measurements' \
# -X PUT \
# -F 'ns=com.my-organization.canvas-app' \
# -F 'data[waist]=32in' \
# -F 'data[inseam]=34in' \
# -F 'data[chest]=40in' \
# -H 'Authorization: Bearer <token>'
#
# Response:
# !!!javascript
# {
# "data": {
# "chest": "40in",
# "waist": "32in",
# "inseam": "34in"
# }
# }
#
# Following such a request, subsets of the stored data to be retrieved directly from a subscope.
#
# Example {api:UsersController#get_custom_data GET} from a generated scope
# curl 'https://<canvas>/api/v1/users/<user_id>/custom_data/body/measurements/chest' \
# -X GET \
# -F 'ns=com.my-organization.canvas-app' \
# -H 'Authorization: Bearer <token>'
#
# Response:
# !!!javascript
# {
# "data": "40in"
# }
#
# If you want to store more than just strings (i.e. numbers, arrays, hashes, true, false,
# and/or null), you must make a request with Content-Type application/json as in the following
# example.
#
# Example PUT with JSON data:
# curl 'https://<canvas>/api/v1/users/<user_id>/custom_data' \
# -H 'Content-Type: application/json' \
# -X PUT \
# -d '{
# "ns": "com.my-organization.canvas-app",
# "data": {
# "a-number": 6.02e23,
# "a-bool": true,
# "a-string": "true",
# "a-hash": {"a": {"b": "ohai"}},
# "an-array": [1, "two", null, false]
# }
# }' \
# -H 'Authorization: Bearer <token>'
#
# Response:
# !!!javascript
# {
# "data": {
# "a-number": 6.02e+23,
# "a-bool": true,
# "a-string": "true",
# "a-hash": {
# "a": {
# "b": "ohai"
# }
# },
# "an-array": [1, "two", null, false]
# }
# }
#
# If the data is an Object (as it is in the above example), then subsets of the data can
# be accessed by including the object's (possibly nested) keys in the scope of a GET request.
#
# Example {api:UsersController#get_custom_data GET} with a generated scope:
# curl 'https://<canvas>/api/v1/users/<user_id>/custom_data/a-hash/a/b' \
# -X GET \
# -F 'ns=com.my-organization.canvas-app' \
# -H 'Authorization: Bearer <token>'
#
# Response:
# !!!javascript
# {
# "data": "ohai"
# }
#
#
# On success, this endpoint returns an object containing the data that was stored.
#
# Responds with status code 200 if the scope already contained data, and it was overwritten
# by the data specified in the request.
#
# Responds with status code 201 if the scope was previously empty, and the data specified
# in the request was successfully stored there.
#
# Responds with status code 400 if the namespace parameter, +ns+, is missing or invalid, or if
# the +data+ parameter is missing.
#
# Responds with status code 409 if the requested scope caused a conflict and data was not stored.
# This happens when storing data at the requested scope would cause data at an outer scope
# to be lost. e.g., if +/custom_data+ was +{"fashion_app": {"hair": "blonde"}}+, but
# you tried to +`PUT /custom_data/fashion_app/hair/style -F data=buzz`+, then for the request
# to succeed,the value of +/custom_data/fashion_app/hair+ would have to become a hash, and its
# old string value would be lost. In this situation, an error object is returned with the
# following format:
#
# !!!javascript
# {
# "message": "write conflict for custom_data hash",
# "conflict_scope": "fashion_app/hair",
# "type_at_conflict": "String",
# "value_at_conflict": "blonde"
# }
#
# @argument ns [Required, String]
# The namespace under which to store the data. This should be something other
# Canvas API apps aren't likely to use, such as a reverse DNS for your organization.
#
# @argument data [Required, JSON]
# The data you want to store for the user, at the specified scope. If the data is
# composed of (possibly nested) JSON objects, scopes will be generated for the (nested)
# keys (see examples).
#
# @example_request
# curl 'https://<canvas>/api/v1/users/<user_id>/custom_data/food_app' \
# -X PUT \
# -F 'ns=com.my-organization.canvas-app' \
# -F 'data[weight]=81kg' \
# -F 'data[favorites][meat]=pork belly' \
# -F 'data[favorites][dessert]=pistachio ice cream' \
# -H 'Authorization: Bearer <token>'
#
# @example_response
# {
# "data": {
# "weight": "81kg",
# "favorites": {
# "meat": "pork belly",
# "dessert": "pistachio ice cream"
# }
# }
# }
def set_data
return unless authorized_action(@context, @current_user, [:manage, :manage_user_details])
cd = CustomData.where(user_id: @context.id, namespace: @namespace).first
cd = CustomData.new(user_id: @context.id, namespace: @namespace) unless cd
data = params[:data]
render(json: {message: 'no data specified'}, status: :bad_request) and return if data.nil?
data = data.to_unsafe_h if data.is_a?(ActionController::Parameters)
begin
overwrite = cd.set_data(@scope, data)
rescue CustomData::WriteConflict => wc
render(json: wc.as_json.merge(message: wc.message), status: :conflict) and return
end
if cd.save
render(json: {data: cd.get_data(@scope)},
status: (overwrite ? :ok : :created))
else
render(json: cd.errors, status: :bad_request)
end
end
# @API Load custom data
# @beta
# Load custom user data.
#
# Arbitrary JSON data can be stored for a User. This API call
# retrieves that data for a (optional) given scope.
# See {api:UsersController#set_custom_data Store Custom Data} for details and
# examples.
#
# On success, this endpoint returns an object containing the data that was requested.
#
# Responds with status code 400 if the namespace parameter, +ns+, is missing or invalid,
# or if the specified scope does not contain any data.
#
# @argument ns [Required, String]
# The namespace from which to retrieve the data. This should be something other
# Canvas API apps aren't likely to use, such as a reverse DNS for your organization.
#
# @example_request
# curl 'https://<canvas>/api/v1/users/<user_id>/custom_data/food_app/favorites/dessert' \
# -X GET \
# -F 'ns=com.my-organization.canvas-app' \
# -H 'Authorization: Bearer <token>'
#
# @example_response
# {
# "data": "pistachio ice cream"
# }
def get_data
return unless authorized_action(@context, @current_user, :read)
begin
data = @cd.get_data @scope
rescue ArgumentError => e
render(json: {message: e.message}, status: :bad_request) and return
end
render(json: {data: data})
end
# @API Delete custom data
# @beta
# Delete custom user data.
#
# Arbitrary JSON data can be stored for a User. This API call
# deletes that data for a given scope. Without a scope, all custom_data is deleted.
# See {api:UsersController#set_custom_data Store Custom Data} for details and
# examples of storage and retrieval.
#
# As an example, we'll store some data, then delete a subset of it.
#
# Example {api:UsersController#set_custom_data PUT} with valid JSON data:
# curl 'https://<canvas>/api/v1/users/<user_id>/custom_data' \
# -X PUT \
# -F 'ns=com.my-organization.canvas-app' \
# -F 'data[fruit][apple]=so tasty' \
# -F 'data[fruit][kiwi]=a bit sour' \
# -F 'data[veggies][root][onion]=tear-jerking' \
# -H 'Authorization: Bearer <token>'
#
# Response:
# !!!javascript
# {
# "data": {
# "fruit": {
# "apple": "so tasty",
# "kiwi": "a bit sour"
# },
# "veggies": {
# "root": {
# "onion": "tear-jerking"
# }
# }
# }
# }
#
# Example DELETE:
# curl 'https://<canvas>/api/v1/users/<user_id>/custom_data/fruit/kiwi' \
# -X DELETE \
# -F 'ns=com.my-organization.canvas-app' \
# -H 'Authorization: Bearer <token>'
#
# Response:
# !!!javascript
# {
# "data": "a bit sour"
# }
#
# Example {api:UsersController#get_custom_data GET} following the above DELETE:
# curl 'https://<canvas>/api/v1/users/<user_id>/custom_data' \
# -X GET \
# -F 'ns=com.my-organization.canvas-app' \
# -H 'Authorization: Bearer <token>'
#
# Response:
# !!!javascript
# {
# "data": {
# "fruit": {
# "apple": "so tasty"
# },
# "veggies": {
# "root": {
# "onion": "tear-jerking"
# }
# }
# }
# }
#
# Note that hashes left empty after a DELETE will get removed from the custom_data store.
# For example, following the previous commands, if we delete /custom_data/veggies/root/onion,
# then the entire /custom_data/veggies scope will be removed.
#
# Example DELETE that empties a parent scope:
# curl 'https://<canvas>/api/v1/users/<user_id>/custom_data/veggies/root/onion' \
# -X DELETE \
# -F 'ns=com.my-organization.canvas-app' \
# -H 'Authorization: Bearer <token>'
#
# Response:
# !!!javascript
# {
# "data": "tear-jerking"
# }
#
# Example {api:UsersController#get_custom_data GET} following the above DELETE:
# curl 'https://<canvas>/api/v1/users/<user_id>/custom_data' \
# -X GET \
# -F 'ns=com.my-organization.canvas-app' \
# -H 'Authorization: Bearer <token>'
#
# Response:
# !!!javascript
# {
# "data": {
# "fruit": {
# "apple": "so tasty"
# }
# }
# }
#
# On success, this endpoint returns an object containing the data that was deleted.
#
# Responds with status code 400 if the namespace parameter, +ns+, is missing or invalid,
# or if the specified scope does not contain any data.
#
# @argument ns [Required, String]
# The namespace from which to delete the data. This should be something other
# Canvas API apps aren't likely to use, such as a reverse DNS for your organization.
#
# @example_request
# curl 'https://<canvas>/api/v1/users/<user_id>/custom_data/fruit/kiwi' \
# -X DELETE \
# -F 'ns=com.my-organization.canvas-app' \
# -H 'Authorization: Bearer <token>'
#
# @example_response
# !!!javascript
# {
# "data": "a bit sour"
# }
def delete_data
return unless authorized_action(@context, @current_user, [:manage, :manage_user_details])
begin
ret = @cd.delete_data(@scope)
rescue ArgumentError => e
render(json: {message: e.message}, status: :bad_request) and return
end
if @cd.destroyed? || @cd.save
render(json: {data: ret})
else
render(json: @cd.errors, status: :bad_request)
end
end
private
def require_namespace
@namespace = params[:ns]
render(json: { message: "invalid namespace" }, status: :bad_request) and return if @namespace.blank?
end
def get_scope
@scope = params[:scope]
end
def require_custom_data
@cd = CustomData.where(user_id: @context.id, namespace: @namespace).first or
render(json: { message: "no data for scope" }, status: :bad_request)
end
end