blob: d501159174cc6c96e722fd360df19fd987482af5 [file] [log] [blame]
% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.
-module(chttpd_httpd_handlers).
-export([url_handler/1, db_handler/1, design_handler/1, handler_info/3]).
-export([
not_supported/2,
not_supported/3,
not_implemented/2
]).
-include_lib("couch/include/couch_db.hrl").
url_handler(<<>>) -> fun chttpd_misc:handle_welcome_req/1;
url_handler(<<"favicon.ico">>) -> fun chttpd_misc:handle_favicon_req/1;
url_handler(<<"_utils">>) -> fun chttpd_misc:handle_utils_dir_req/1;
url_handler(<<"_all_dbs">>) -> fun chttpd_misc:handle_all_dbs_req/1;
url_handler(<<"_deleted_dbs">>) -> fun chttpd_misc:handle_deleted_dbs_req/1;
url_handler(<<"_dbs_info">>) -> fun chttpd_misc:handle_dbs_info_req/1;
url_handler(<<"_active_tasks">>) -> fun chttpd_misc:handle_task_status_req/1;
url_handler(<<"_scheduler">>) -> fun couch_replicator_httpd:handle_scheduler_req/1;
url_handler(<<"_node">>) -> fun chttpd_node:handle_node_req/1;
url_handler(<<"_reload_query_servers">>) -> fun chttpd_misc:handle_reload_query_servers_req/1;
url_handler(<<"_replicate">>) -> fun chttpd_misc:handle_replicate_req/1;
url_handler(<<"_uuids">>) -> fun chttpd_misc:handle_uuids_req/1;
url_handler(<<"_session">>) -> fun chttpd_auth:handle_session_req/1;
url_handler(<<"_up">>) -> fun chttpd_misc:handle_up_req/1;
url_handler(_) -> no_match.
db_handler(<<"_view_cleanup">>) -> fun chttpd_db:handle_view_cleanup_req/2;
db_handler(<<"_compact">>) -> fun chttpd_db:handle_compact_req/2;
db_handler(<<"_design">>) -> fun chttpd_db:handle_design_req/2;
db_handler(<<"_partition">>) -> fun chttpd_db:handle_partition_req/2;
db_handler(<<"_temp_view">>) -> fun ?MODULE:not_supported/2;
db_handler(<<"_changes">>) -> fun chttpd_db:handle_changes_req/2;
db_handler(<<"_purge">>) -> fun ?MODULE:not_implemented/2;
db_handler(<<"_purged_infos_limit">>) -> fun ?MODULE:not_implemented/2;
db_handler(_) -> no_match.
design_handler(<<"_view">>) -> fun chttpd_view:handle_view_req/3;
design_handler(<<"_show">>) -> fun ?MODULE:not_supported/3;
design_handler(<<"_list">>) -> fun ?MODULE:not_supported/3;
design_handler(<<"_update">>) -> fun chttpd_show:handle_doc_update_req/3;
design_handler(<<"_info">>) -> fun chttpd_db:handle_design_info_req/3;
design_handler(<<"_rewrite">>) -> fun ?MODULE:not_supported/3;
design_handler(_) -> no_match.
handler_info('GET', [], _) ->
{'welcome_message.read', #{}};
handler_info('GET', [<<"_active_tasks">>], _) ->
{'active_tasks.read', #{}};
handler_info('GET', [<<"_all_dbs">>], _) ->
{'all_dbs.read', #{}};
handler_info('GET', [<<"_deleted_dbs">>], _) ->
{'account-deleted-dbs.read', #{}};
handler_info('POST', [<<"_deleted_dbs">>], _) ->
{'account-deleted-dbs.undelete', #{}};
handler_info('DELETE', [<<"_deleted_dbs">>, Db], _) ->
{'account-deleted-dbs.delete', #{'db.name' => Db}};
handler_info('POST', [<<"_dbs_info">>], _) ->
{'dbs_info.read', #{}};
handler_info('GET', [<<"_node">>, <<"_local">>], _) ->
{'node.name.read', #{}};
handler_info(Method, [<<"_node">>, <<"_local">> | Rest], HttpReq) ->
handler_info(Method, [<<"_node">>, node() | Rest], HttpReq);
handler_info('GET', [<<"_node">>, Node, <<"_config">>], _) ->
{'node.config.all.read', #{node => Node}};
handler_info('GET', [<<"_node">>, Node, <<"_config">>, Section], _) ->
{'node.config.section.read', #{node => Node, 'config.section' => Section}};
handler_info('GET', [<<"_node">>, Node, <<"_config">>, Section, Key], _) ->
{'node.config.key.read', #{
node => Node,
'config.section' => Section,
'config.key' => Key
}};
handler_info('PUT', [<<"_node">>, Node, <<"_config">>, Section, Key], _) ->
{'node.config.key.write', #{
node => Node,
'config.section' => Section,
'config.key' => Key
}};
handler_info('DELETE', [<<"_node">>, Node, <<"_config">>, Section, Key], _) ->
{'node.config.key.delete', #{
node => Node,
'config.section' => Section,
'config.key' => Key
}};
handler_info('GET', [<<"_node">>, Node, <<"_stats">> | Path], _) ->
{'node.stats.read', #{node => Node, 'stat.path' => Path}};
handler_info('GET', [<<"_node">>, Node, <<"_system">>], _) ->
{'node.system.read', #{node => Node}};
handler_info('POST', [<<"_node">>, Node, <<"_restart">>], _) ->
{'node.restart.execute', #{node => Node}};
handler_info('POST', [<<"_reload_query_servers">>], _) ->
{'query_servers.reload', #{}};
handler_info('POST', [<<"_replicate">>], _) ->
{'replication.create', #{}};
handler_info('GET', [<<"_scheduler">>, <<"jobs">>], _) ->
{'replication.jobs.read', #{}};
handler_info('GET', [<<"_scheduler">>, <<"jobs">>, JobId], _) ->
{'replication.job.read', #{'job.id' => JobId}};
handler_info('GET', [<<"_scheduler">>, <<"docs">>], _) ->
{'replication.docs.read', #{'db.name' => <<"_replicator">>}};
handler_info('GET', [<<"_scheduler">>, <<"docs">>, Db], _) ->
{'replication.docs.read', #{'db.name' => Db}};
handler_info('GET', [<<"_scheduler">>, <<"docs">>, Db, DocId], _) ->
{'replication.doc.read', #{'db.name' => Db, 'doc.id' => DocId}};
handler_info('GET', [<<"_scheduler">>, <<"docs">> | Path], _) ->
case lists:splitwith(fun(Elem) -> Elem /= <<"_replicator">> end, Path) of
{_, [<<"_replicator">>]} ->
{'replication.docs.read', #{
'db.name' => filename:join(Path)
}};
{DbParts, [<<"_replicator">>, DocId]} ->
{'replication.doc.read', #{
'db.name' => filename:join(DbParts ++ [<<"_replicator">>]),
'doc.id' => DocId
}};
_ ->
no_match
end;
handler_info('GET', [<<"_session">>], _) ->
{'session.read', #{}};
handler_info('POST', [<<"_session">>], _) ->
{'session.create', #{}};
handler_info('DELETE', [<<"_session">>], _) ->
{'session.delete', #{}};
handler_info('GET', [<<"_up">>], _) ->
{'health.read', #{}};
handler_info('GET', [<<"_utils">> | Path], _) ->
{'utils.read', #{'file.path' => filename:join(Path)}};
handler_info('GET', [<<"_uuids">>], _) ->
{'uuids.read', #{}};
handler_info('GET', [<<"favicon.ico">>], _) ->
{'favicon.ico.read', #{}};
handler_info(Method, [<<"_", _/binary>> = Part| Rest], Req) ->
% Maybe bail here so that we don't trample over a
% different url_handler plugin. However, we continue
% on for known system databases.
DbName = case Part of
<<"_dbs">> -> '_dbs';
<<"_global_changes">> -> '_global_changes';
<<"_metadata">> -> '_metadata';
<<"_nodes">> -> '_nodes';
<<"_replicator">> -> '_replicator';
<<"_users">> -> '_users';
_ -> no_match
end,
if DbName == no_match -> no_match; true ->
handler_info(Method, [DbName | Rest], Req)
end;
handler_info('GET', [Db], _) ->
{'db.info.read', #{'db.name' => Db}};
handler_info('PUT', [Db], _) ->
{'db.create', #{'db.name' => Db}};
handler_info('POST', [Db], _) ->
{'db.doc.write', #{'db.name' => Db}};
handler_info('DELETE', [Db], _) ->
{'db.delete', #{'db.name' => Db}};
handler_info(M, [Db, <<"_all_docs">>], _) when M == 'GET'; M == 'POST' ->
{'db.all_docs.read', #{'db.name' => Db}};
handler_info('POST', [Db, <<"_all_docs">>, <<"queries">>], _) ->
{'db.all_docs.read', #{'db.name' => Db, multi => true}};
handler_info('POST', [Db, <<"_bulk_docs">>], _) ->
{'db.docs.write', #{'db.name' => Db, bulk => true}};
handler_info('POST', [Db, <<"_bulk_get">>], _) ->
{'db.docs.read', #{'db.name' => Db, bulk => true}};
handler_info('GET', [Db, <<"_changes">>], _) ->
{'db.changes.read', #{'db.name' => Db}};
handler_info('POST', [Db, <<"_changes">>], _) ->
{'db.changes.read', #{'db.name' => Db}};
handler_info('POST', [Db, <<"_compact">>], _) ->
{'db.compact.execute', #{'db.name' => Db}};
handler_info('GET', [Db, <<"_design">>, Name], _) ->
{'db.design.doc.read', #{'db.name' => Db, 'design.id' => Name}};
handler_info('POST', [Db, <<"_design">>, Name], _) ->
{'db.design.doc.write', #{'db.name' => Db, 'design.id' => Name}};
handler_info('PUT', [Db, <<"_design">>, Name], _) ->
{'db.design.doc.write', #{'db.name' => Db, 'design.id' => Name}};
handler_info('COPY', [Db, <<"_design">>, Name], Req) ->
{'db.design.doc.write', #{
'db.name' => Db,
'design.id' => get_copy_destination(Req),
'copy.source.doc.id' => <<"_design/", Name/binary>>
}};
handler_info('DELETE', [Db, <<"_design">>, Name], _) ->
{'db.design.doc.delete', #{'db.name' => Db, 'design.id' => Name}};
handler_info('GET', [Db, <<"_design">>, Name, <<"_info">>], _) ->
{'db.design.info.read', #{'db.name' => Db, 'design.id' => Name}};
handler_info(M, [Db, <<"_design">>, Name, <<"_list">>, List, View], _)
when M == 'GET'; M == 'POST', M == 'OPTIONS' ->
{'db.design.list.read', #{
'db.name' => Db,
'design.id' => Name,
'design.list.name' => List,
'design.view.name' => View
}};
handler_info(M, [Db, <<"_design">>, Name, <<"_list">>, List, Design, View], _)
when M == 'GET'; M == 'POST', M == 'OPTIONS' ->
{'db.design.list.read', #{
'db.name' => Db,
'design.id' => Name,
'design.list.name' => List,
'design.view.source.id' => Design,
'design.view.name' => View
}};
handler_info(_, [Db, <<"_design">>, Name, <<"_rewrite">> | Path], _) ->
{'db.design.rewrite.execute', #{
'db.name' => Db,
'design.id' => Name,
'rewrite.path' => filename:join(Path)
}};
handler_info(_, [Db, <<"_design">>, Name, <<"_show">>, Show, DocId], _) ->
{'db.design.show.execute', #{
'db.name' => Db,
'design.id' => Name,
'design.show.name' => Show,
'design.show.doc.id' => DocId
}};
handler_info(_, [Db, <<"_design">>, Name, <<"_update">>, Update | Rest], _) ->
BaseTags = #{
'db.name' => Db,
'design.id' => Name,
'design.update.name' => Update
},
Tags = case Rest of
[] ->
BaseTags;
_ ->
DocId = filename:join(Rest),
maps:put('design.update.doc.id', DocId, BaseTags)
end,
{'db.design.update.execute', Tags};
handler_info('POST', [Db, <<"_design">>, Name, <<"_view">>, View, <<"queries">>], _) ->
{'db.design.view.multi.read', #{
'db.name' => Db,
'design.id' => Name,
'design.view.name' => View
}};
handler_info(M, [Db, <<"_design">>, Name, <<"_view">>, View], _)
when M == 'GET'; M == 'POST' ->
{'db.design.view.read', #{
'db.name' => Db,
'design.id' => Name,
'design.view.name' => View
}};
handler_info(_, [_Db, <<"_design">>, _Name, <<"_", _/binary>> | _], _) ->
% Bail here so that we don't treat a plugin
% design handler in place of a design attachment
no_match;
handler_info('GET', [Db, <<"_design">>, Name | Path], _) ->
{'db.design.doc.attachment.read', #{
'db.name' => Db,
'design.id' => Name,
'attachment.name' => filename:join(Path)
}};
handler_info('PUT', [Db, <<"_design">>, Name | Path], _) ->
{'db.design.doc.attachment.write', #{
'db.name' => Db,
'design.id' => Name,
'attachment.name' => filename:join(Path)
}};
handler_info('DELETE', [Db, <<"_design">>, Name | Path], _) ->
{'db.design.doc.attachment.delete', #{
'db.name' => Db,
'design.id' => Name,
'attachment.name' => filename:join(Path)
}};
handler_info(_, [Db, <<"_design/", Name/binary>> | Rest], Req) ->
% Recurse if someone sent us `_design%2Fname`
chttpd_handlers:handler_info(Req#httpd{
path_parts = [Db, <<"_design">>, Name | Rest]
});
handler_info(M, [Db, <<"_design_docs">>], _) when M == 'GET'; M == 'POST' ->
{'db.design_docs.read', #{'db.name' => Db}};
handler_info('POST', [Db, <<"_design_docs">>, <<"queries">>], _) ->
{'db.design_docs.read', #{'db.name' => Db, multi => true}};
handler_info('POST', [Db, <<"_ensure_full_commit">>], _) ->
{'db.ensure_full_commit.execute', #{'db.name' => Db}};
handler_info('GET', [Db, <<"_local">>, Name], _) ->
{'db.local.doc.read', #{'db.name' => Db, 'local.id' => Name}};
handler_info('POST', [Db, <<"_local">>, Name], _) ->
{'db.local.doc.write', #{'db.name' => Db, 'local.id' => Name}};
handler_info('PUT', [Db, <<"_local">>, Name], _) ->
{'db.local.doc.write', #{'db.name' => Db, 'local.id' => Name}};
handler_info('COPY', [Db, <<"_local">>, Name], Req) ->
{'db.local.doc.write', #{
'db.name' => Db,
'local.id' => get_copy_destination(Req),
'copy.source.doc.id' => <<"_local/", Name/binary>>
}};
handler_info('DELETE', [Db, <<"_local">>, Name], _) ->
{'db.local.doc.delete', #{'db.name' => Db, 'local.id' => Name}};
handler_info(_, [Db, <<"_local">>, Name | _Path], _) ->
{'db.local.doc.invalid_attachment_req', #{
'db.name' => Db,
'local.id' => Name
}};
handler_info(M, [Db, <<"_local_docs">>], _) when M == 'GET'; M == 'POST' ->
{'db.local_docs.read', #{'db.name' => Db}};
handler_info('POST', [Db, <<"_local_docs">>, <<"queries">>], _) ->
{'db.local_docs.read', #{'db.name' => Db, multi => true}};
handler_info('POST', [Db, <<"_missing_revs">>], _) ->
{'db.docs.missing_revs.execute', #{'db.name' => Db}};
handler_info('GET', [Db, <<"_partition">>, Partition], _) ->
{'db.partition.info.read', #{'db.name' => Db, partition => Partition}};
handler_info(_, [Db, <<"_partition">>, Partition | Rest], Req) ->
NewPath = case Rest of
[<<"_all_docs">> | _] ->
[Db | Rest];
[<<"_index">> | _] ->
[Db | Rest];
[<<"_find">> | _] ->
[Db | Rest];
[<<"_explain">> | _] ->
[Db | Rest];
[<<"_design">>, _Name, <<"_", _/binary>> | _] ->
[Db | Rest];
_ ->
no_match
end,
if NewPath == no_match -> no_match; true ->
{OpName, Tags} = chttpd_handlers:handler_info(Req#httpd{
path_parts = NewPath
}),
NewOpName = case atom_to_list(OpName) of
"db." ++ Name -> list_to_atom("db.partition." ++ Name);
Else -> list_to_atom(Else ++ ".partition")
end,
{NewOpName, maps:put(partition, Partition, Tags)}
end;
handler_info('POST', [Db, <<"_purge">>], _) ->
{'db.docs.purge', #{'db.name' => Db}};
handler_info('GET', [Db, <<"_purged_infos_limit">>], _) ->
{'db.purged_infos_limit.read', #{'db.name' => Db}};
handler_info('PUT', [Db, <<"_purged_infos_limit">>], _) ->
{'db.purged_infos_limit.write', #{'db.name' => Db}};
handler_info('POST', [Db, <<"_revs_diff">>], _) ->
{'db.docs.revs_diff.execute', #{'db.name' => Db}};
handler_info('GET', [Db, <<"_revs_limit">>], _) ->
{'db.revs_limit.read', #{'db.name' => Db}};
handler_info('PUT', [Db, <<"_revs_limit">>], _) ->
{'db.revs_limit.write', #{'db.name' => Db}};
handler_info('GET', [Db, <<"_security">>], _) ->
{'db.security.read', #{'db.name' => Db}};
handler_info('PUT', [Db, <<"_security">>], _) ->
{'db.security.write', #{'db.name' => Db}};
handler_info(_, [Db, <<"_view_cleanup">>], _) ->
{'views.cleanup.execute', #{'db.name' => Db}};
handler_info(_, [_Db, <<"_", _/binary>> | _], _) ->
% Bail here for other possible db_handleres
no_match;
handler_info('GET', [Db, DocId], _) ->
{'db.doc.read', #{'db.name' => Db, 'doc.id' => DocId}};
handler_info('POST', [Db, DocId], _) ->
{'db.doc.write', #{'db.name' => Db, 'design.id' => DocId}};
handler_info('PUT', [Db, DocId], _) ->
{'db.doc.write', #{'db.name' => Db, 'design.id' => DocId}};
handler_info('COPY', [Db, DocId], Req) ->
{'db.doc.write', #{
'db.name' => Db,
'doc.id' => get_copy_destination(Req),
'copy.source.doc.id' => DocId
}};
handler_info('DELETE', [Db, DocId], _) ->
{'db.doc.delete', #{'db.name' => Db, 'doc.id' => DocId}};
handler_info('GET', [Db, DocId | Path], _) ->
{'db.doc.attachment.read', #{
'db.name' => Db,
'doc.id' => DocId,
'attachment.name' => filename:join(Path)
}};
handler_info('PUT', [Db, DocId | Path], _) ->
{'db.doc.attachment.write', #{
'db.name' => Db,
'doc.id' => DocId,
'attachment.name' => filename:join(Path)
}};
handler_info('DELETE', [Db, DocId | Path], _) ->
{'db.doc.attachment.delete', #{
'db.name' => Db,
'doc.id' => DocId,
'attachment.name' => filename:join(Path)
}};
handler_info(_, _, _) ->
no_match.
get_copy_destination(Req) ->
try
{DocIdStr, _} = couch_httpd_db:parse_copy_destination_header(Req),
list_to_binary(mochiweb_util:unquote(DocIdStr))
catch _:_ ->
unknown
end.
not_supported(#httpd{} = Req, Db, _DDoc) ->
not_supported(Req, Db).
not_supported(#httpd{} = Req, _Db) ->
Msg = <<"resource is not supported in CouchDB >= 4.x">>,
chttpd:send_error(Req, 410, gone, Msg).
not_implemented(#httpd{} = Req, _Db) ->
Msg = <<"resource is not implemented">>,
chttpd:send_error(Req, 501, not_implemented, Msg).