diff --git a/lib/kolab_wopi/api.ex b/lib/kolab_wopi/api.ex index 902adc0..7db5112 100644 --- a/lib/kolab_wopi/api.ex +++ b/lib/kolab_wopi/api.ex @@ -1,31 +1,55 @@ defmodule KolabWopi.API do use Plug.Router - use Plug.ErrorHandler + #use Plug.ErrorHandler + use Plug.Debugger require Logger plug Plug.Logger plug :match plug :dispatch def init(options) do options end def start_link do {:ok, _} = Plug.Adapters.Cowboy.http __MODULE__, [] end forward "/wopi/files", to: __MODULE__.Files forward "/wopi/containers", to: __MODULE__.Containers forward "/wopi/ecosystem", to: __MODULE__.Ecosystem forward "/wopibootstrapper", to: __MODULE__.Bootstrap match _ do conn |> send_resp(404, "Not found") end - defp handle_errors(conn, %{kind: _kind, reason: _reason, stack: _stack}) do - conn |> send_resp(500, "Internal Server Error") + def get_access_token(conn) do + conn = Plug.Conn.fetch_query_params(conn) + access_token = conn.query_params["access_token"] + + # validate access_token + if not is_binary(access_token) or byte_size(access_token) == 0 do + # FIXME: why it does not return "401 Unauthorized" if I remove Plug.Debugger? + raise __MODULE__.ExceptionUnauthorized + end + + access_token end + + #defp handle_errors(conn, %{kind: _kind, reason: reason, stack: _stack}) do + # status = 500 + # message = "Internal Server Error" + + # if reason.plug_status do + # status = reason.plug_status + # end + # if reason.message do + # message = reason.message + # end + + # conn |> send_resp(status, message) + #end end diff --git a/lib/kolab_wopi/api/exceptions.ex b/lib/kolab_wopi/api/exceptions.ex new file mode 100644 index 0000000..4052f97 --- /dev/null +++ b/lib/kolab_wopi/api/exceptions.ex @@ -0,0 +1,7 @@ +defmodule KolabWopi.API.ExceptionUnauthorized do + defexception [message: "Unauthorized", plug_status: 401] +end + +defimpl Plug.Exception, for: KolabWopi.API.ExceptionUnauthorized do + def status(_), do: 401 +end diff --git a/lib/kolab_wopi/api/files.ex b/lib/kolab_wopi/api/files.ex index f241516..8b9017d 100644 --- a/lib/kolab_wopi/api/files.ex +++ b/lib/kolab_wopi/api/files.ex @@ -1,412 +1,429 @@ defmodule KolabWopi.API.Files do use Plug.Router + alias KolabWopi.API + alias KolabWopi.Chwala, as: Kolab + plug :match plug :dispatch ### ### Routes handling ### get "/:file_id" do check_file_info(conn, file_id) end get "/:file_id/contents" do get_file(conn, file_id) end get "/:file_id/ancestry" do enumerate_ancestors(conn, file_id) end get "/:file_id/ecosystem_pointer" do get_ecosystem(conn, file_id) end post "/:file_id/contents" do action = get_req_header(conn, "x-wopi-override") case action do "PUT" -> put_file(conn, file_id) _ -> send_resp(conn, 501, "Not implemented") end end post "/:file_id" do action = get_req_header(conn, "x-wopi-override") case action do "UNLOCK" -> un_lock(conn, file_id) "LOCK" -> lock(conn, file_id) "GET_LOCK" -> get_lock(conn, file_id) "REFRESH_LOCK" -> refresh_lock(conn, file_id) "PUT_RELATIVE" -> put_relative_file(conn, file_id) "RENAME_FILE" -> rename_file(conn, file_id) "DELETE" -> delete_file(conn, file_id) "PUT_USER_INFO" -> put_user_info(conn, file_id) "GET_SHARE_URL" -> get_share_url(conn, file_id) _ -> send_resp(conn, 501, "Not implemented") end end match _ do send_resp(conn, 404, "Not found") end ### ### Action handlers ### @doc """ [GET] CheckFileInfo: Returns information about a file. Parameters: - file_id: A string that specifies a file ID Query parameters: - access_token: Authorization token Request Headers: - X-WOPI-SessionContext: The value of the Session context parameter. Status Codes: - 200 Success JSON Response (only required attributes listed): - BaseFileName: The string name of the file without a path. Used for display in user interface (UI), and determining the extension of the file. - OwnerId: A string that uniquely identifies the owner of the file. - Size: The size of the file in bytes, expressed as a long, a 64-bit signed integer. - UserId: A string value uniquely identifying the user currently accessing the file. - Version: The current version of the file based on the server’s file version schema, as a string. This value must change when the file changes. """ def check_file_info(conn, file_id) do - conn |> send_resp(501, "Not implemented") + # read and validate access token + token = API.get_access_token(conn) + + # get file metadata from Chwala + file_data = Kolab.document_info(token, file_id) + + IO.inspect file_data + + # convert file metadata to Wopi format + response_body = "some json" + + # send response + conn + |> put_resp_content_type("application/json") + |> send_resp(200, response_body) end @doc """ [GET] GetFile: Retrieves a file from a host. Parameters: - file_id: A string that specifies a file ID Query parameters: - access_token: Authorization token Request Headers: - X-WOPI-MaxExpectedSize: An integer specifying the upper bound of the expected size of the file being requested. Optional. The host should use the maximum value of a 4-byte integer if this value is not set in the request. Status Codes: - 200 Success - 412 File is larger than X-WOPI-MaxExpectedSize """ def get_file(conn, file_id) do conn |> send_resp(501, "Not implemented") end @doc """ [POST] PutFile: Updates a file’s binary contents. Parameters: - file_id: A string that specifies a file ID Query parameters: - access_token: Authorization token Request Headers: - X-WOPI-Override: The string "PUT". - X-WOPI-Lock: A string provided by the WOPI client in a previous Lock request. Note that this header will not be included during document creation. Response Headers: - X-WOPI-Lock: A string value identifying the current lock on the file. This header must always be included when responding to the request with 409. It should not be included when responding to the request with 200 OK. - X-WOPI-LockFailureReason: An optional string value indicating the cause of a lock failure. - X-WOPI-ItemVersion: An optional string value indicating the version of the file. Its value should be the same as Version value in CheckFileInfo. Status Codes: - 200 Success - 409 Conflict: Lock mismatch/locked by another interface - 413 Request Entity Too Large: File is too large; Host limit exceeded. """ def put_file(conn, file_id) do conn |> send_resp(501, "Not implemented") end @doc """ [POST] PutRelativeFile: Creates a new file on the host based on the current file. The host must use the content in the POST body. Parameters: - file_id: A string that specifies a file ID Query parameters: - access_token: Authorization token Request Headers: - X-WOPI-Override: The string "PUT_RELATIVE". - X-WOPI-SuggestedTarget: A UTF-7 encoded string specifying either a file extension or a full file name, including the file extension. - X-WOPI-RelativeTarget: A UTF-7 encoded string that specifies a full file name including the file extension. - X-WOPI-OverwriteRelativeTarget: A Boolean value that specifies whether the host must overwrite the file name if it exists. - X-WOPI-Size: An integer that specifies the size of the file in bytes. - X-WOPI-FileConversion: A header whose presence indicates that the request is being made in the context of a binary document conversion. Body: The request body must be the full binary contents of the file. Response Headers: - X-WOPI-ValidRelativeTarget: A UTF-7 encoded string that specifies a full file name including the file extension. - X-WOPI-Lock: A string value identifying the current lock on the file. This header must always be included when responding to the request with 409. It should not be included when responding to the request with 200 OK. - X-WOPI-LockFailureReason: An optional string value indicating the cause of a lock failure. Status Codes: - 200 Success - 400 Bad Request: Specified name is illegal - 409 Conflict: Target file already exists or the file is locked. - 413 Request Entity Too Large: File is too large; Host limit exceeded. JSON Response (only required attributes listed): - Name: The string name of the newly created file without a path. - Url: A string URI of the form http://server/<...>/wopi/files/(file_id)?access_token=(access token), of the newly created file on the host. """ def put_relative_file(conn, file_id) do conn |> send_resp(501, "Not implemented") end @doc """ [POST] RenameFile: Renames a file. Parameters: - file_id: A string that specifies a file ID Query parameters: - access_token: Authorization token Request Headers: - X-WOPI-Override: The string "RENAME_FILE". - X-WOPI-Lock: A string identifying the lock on the file. - X-WOPI-RequestedName: A UTF-7 encoded string that is a file name, not including the file extension. Response Headers: - X-WOPI-InvalidFileNameError: A string describing the reason the rename operation could not be completed. - X-WOPI-Lock: A string value identifying the current lock on the file. This header must always be included when responding to the request with 409. It should not be included when responding to the request with 200 OK. - X-WOPI-LockFailureReason: An optional string value indicating the cause of a lock failure. Status Codes: - 200 Success - 400 Bad Request: Specified name is illegal - 409 Conflict: Target file already exists or the file is locked. JSON Response (only required attributes listed): - Name: The string name of the renamed file without a path or file extension. """ def rename_file(conn, file_id) do conn |> send_resp(501, "Not implemented") end @doc """ [POST] DeleteFile: Deletes a file. Parameters: - file_id: A string that specifies a file ID Query parameters: - access_token: Authorization token Request Headers: - X-WOPI-Override: The string "DELETE". Response Headers: - X-WOPI-Lock: A string value identifying the current lock on the file. This header must always be included when responding to the request with 409. It should not be included when responding to the request with 200 OK. - X-WOPI-LockFailureReason: An optional string value indicating the cause of a lock failure. Status Codes: - 200 Success - 409 Conflict: Target file already exists or the file is locked. """ def delete_file(conn, file_id) do conn |> send_resp(501, "Not implemented") end @doc """ [POST] PutUserInfo: Stores some basic user information on the host. Parameters: - file_id: A string that specifies a file ID Query parameters: - access_token: Authorization token Request Headers: - X-WOPI-Override: The string "PUT_USER_INFO". Body: The request body must be the full UserInfo string. Status Codes: - 200 Success """ def put_user_info(conn, file_id) do conn |> send_resp(501, "Not implemented") end @doc """ [POST] Lock: Locks a file for editing. [POST] UnLockAndRelock: Releases a lock on a file, and then immediately takes a new lock on the file Parameters: - file_id: A string that specifies a file ID Query parameters: - access_token: Authorization token Request Headers: - X-WOPI-Override: The string "LOCK". - X-WOPI-Lock: A string provided by the WOPI client that the host must use to identify the lock on the file. - X-WOPI-OldLock: A string provided by the WOPI client that is the existing lock on the file. Required for UnLockAndRelock. Note that if X-WOPI-OldLock is not provided, the request is identical to a Lock request. Response Headers: - X-WOPI-Lock: A string value identifying the current lock on the file. This header must always be included when responding to the request with 409. It should not be included when responding to the request with 200 OK. - X-WOPI-LockFailureReason: An optional string value indicating the cause of a lock failure. - X-WOPI-ItemVersion: An optional string value indicating the version of the file. Its value should be the same as Version value in CheckFileInfo. Status Codes: - 200 Success - 400 Bad Request: X-WOPI-Lock was not provided or was empty - 409 Conflict: Lock mismatch/locked by another interface """ def lock(conn, file_id) do conn |> send_resp(501, "Not implemented") end @doc """ [POST] GetLock: Retrieves a lock on a file. Parameters: - file_id: A string that specifies a file ID Query parameters: - access_token: Authorization token Request Headers: - X-WOPI-Override: The string "GET_LOCK". Response Headers: - X-WOPI-Lock: A string value identifying the current lock on the file. This header must always be included when responding to the request with 409. It should not be included when responding to the request with 200 OK. - X-WOPI-LockFailureReason: An optional string value indicating the cause of a lock failure. Status Codes: - 200 Success - 409 Conflict: Lock mismatch/locked by another interface """ def get_lock(conn, file_id) do conn |> send_resp(501, "Not implemented") end @doc """ [POST] RefreshLock: Refreshes the lock on a file. Parameters: - file_id: A string that specifies a file ID Query parameters: - access_token: Authorization token Request Headers: - X-WOPI-Override: The string "REFRESH_LOCK". - X-WOPI-Lock: A string provided by the WOPI client that the host must use to identify the lock on the file. Response Headers: - X-WOPI-Lock: A string value identifying the current lock on the file. This header must always be included when responding to the request with 409. It should not be included when responding to the request with 200 OK. - X-WOPI-LockFailureReason: An optional string value indicating the cause of a lock failure. Status Codes: - 200 Success - 400 Bad Request: X-WOPI-Lock was not provided or was empty - 409 Conflict: Lock mismatch/locked by another interface """ def refresh_lock(conn, file_id) do conn |> send_resp(501, "Not implemented") end @doc """ [POST] UnLock: Releases the lock on a file. Parameters: - file_id: A string that specifies a file ID Query parameters: - access_token: Authorization token Request Headers: - X-WOPI-Override: The string "UNLOCK". - X-WOPI-Lock: A string provided by the WOPI client that the host must use to identify the lock on the file. Response Headers: - X-WOPI-Lock: A string value identifying the current lock on the file. This header must always be included when responding to the request with 409. It should not be included when responding to the request with 200 OK. - X-WOPI-LockFailureReason: An optional string value indicating the cause of a lock failure. Status Codes: - 200 Success - 400 Bad Request: X-WOPI-Lock was not provided or was empty - 409 Conflict: Lock mismatch/locked by another interface """ def un_lock(conn, file_id) do conn |> send_resp(501, "Not implemented") end @doc """ [POST] GetShareUrl: Returns a Share URL that is suitable for viewing a shared file. Parameters: - file_id: A string that specifies a file ID Query parameters: - access_token: Authorization token Request Headers: - X-WOPI-Override: The string "GET_SHARE_URL". - X-WOPI-UrlType: A string indicating what Share URL type to return. Status Codes: - 200 Success JSON Response: - ShareUrl: A URI that points to a webpage that allows the user to access the file. """ def get_share_url(conn, file_id) do conn |> send_resp(501, "Not implemented") end @doc """ [GET] EnumerateAncestors Parameters: - file_id: A string that specifies a file ID Query parameters: - access_token: Authorization token Response Headers: - X-WOPI-EnumerationIncomplete: An optional header indicating that the enumeration of the container’s ancestry is incomplete. Status Codes: - 200 Success JSON Response (only required attributes listed): - AncestorsWithRootFirst: An array of JSON-formatted objects (Name,Url) """ def enumerate_ancestors(conn, file_id) do conn |> send_resp(501, "Not implemented") end @doc """ [GET] GetEcosystem Parameters: - file_id: A string that specifies a file ID Query parameters: - access_token: Authorization token Status Codes: - 200 Success JSON Response (only required attributes listed): - Url: A string URI for the WOPI server’s ecosystem endpoint. """ def get_ecosystem(conn, file_id) do conn |> send_resp(501, "Not implemented") end end diff --git a/lib/kolab_wopi/chwala.ex b/lib/kolab_wopi/chwala.ex index b5f2c87..d3540e1 100644 --- a/lib/kolab_wopi/chwala.ex +++ b/lib/kolab_wopi/chwala.ex @@ -1,10 +1,141 @@ defmodule KolabWopi.Chwala do use GenServer + use HTTPoison.Base require Logger + @url "http://localhost/chwala/api/" + def start_link do GenServer.start_link(__MODULE__, []) end + @doc """ + Fetches document metadata from Chwala + + Args: + * `token` - Chwala access token + * `file_id` - File identifier + + Returns List of tuples + """ + def document_info(token, file_id) do + # Method: GET + # Params: + # method: "document_info" + # id: file_id + # token: token + # + # In case of an error response body will be: + # [code: 403, reason: "Invalid session", status: "ERROR"] + # on success it will be: + # [status: "OK", result: []] + + # TODO: this request is not yet implemented in Chwala + + params = %{ + token: token, + id: file_id, + } + + # TODO: error handling + + response = get!(@url, [], params: params) + response.body + end + + @doc """ + Fetches document content from Chwala + + Args: + * `token` - Chwala access token + * `file_id` - File identifier + + Returns binary + """ + def document_get(token, file_id) do + # Method: GET + # Params: + # method: "document" + # id: file_id + # token: token + # + # In case of an error http code will be 500 + # On success response body will contain file content + + params = %{ + token: token, + id: file_id, + } + + response = get!(@url, [], params: params) + response.body + end + + @doc """ + Updates document content in Chwala + + Args: + * `token` - Chwala access token + * `file_id` - File identifier + * `body` - File content + + Returns ??? + """ + def document_put(token, file_id, body) do + # Method: PUT + # Params: + # method: "document" + # id: file_id + # token: token + # + # In case of an error response body will be e.g.: + # [code: 403, reason: "Invalid session", status: "ERROR"] + # On success it will be: + # [status: "OK", result: []] + + params = %{ + token: token, + id: file_id, + } + + # TODO: use streams to pass the file body? + + response = put!(@url, body, [], params: params) + end + + @doc """ + Renames the document + + Args: + * `token` - Chwala access token + * `file_id` - File identifier + * `name` - New file name + + Returns ??? + """ + def document_rename(token, file_id, name) do + # Method: POST + # Params: + # method: "document_rename" + # id: file_id + # token: token + # name: name + # + # In case of an error response body will be e.g.: + # [code: 403, reason: "Invalid session", status: "ERROR"] + # On success it will be: + # [status: "OK", result: []] + + # TODO: not implemented in Chwala yet + end + + # Decodes JSON response + # TODO: What if the response body is expected to be not JSON + # but binary data, e.g. in document_get + defp process_response_body(body) do + body + |> Poison.decode! + |> Enum.map(fn({k, v}) -> {String.to_atom(k), v} end) + end end diff --git a/mix.exs b/mix.exs index da704f5..912215f 100644 --- a/mix.exs +++ b/mix.exs @@ -1,36 +1,37 @@ defmodule KolabWopi.Mixfile do use Mix.Project def project do [ app: :kolab_wopi, + description: "Kolab REST API implementing Web Application Open Interface", version: "0.0.1", elixir: "~> 1.2", build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod, deps: deps ] end # Configuration for the OTP application # # Type "mix help compile.app" for more information def application do [ applications: [:logger, :cowboy, :plug, :httpoison], mod: {KolabWopi, []} ] end # Dependencies can be Hex packages: # # Type "mix help deps" for more examples and options defp deps do [ {:cowboy, "~> 1.0"}, {:plug, "~> 1.2"}, {:httpoison, "~> 0.9"}, {:poison, "~> 2.2"} ] end end