%% % This module enables access to the PEP node "urn:xmpp:microblog" via % an Atompub compliant interface. % % -module(atom_pubsub). -author('eric@ohmforce.com'). -include("ejabberd.hrl"). -include("jlib.hrl"). -include("pubsub.hrl"). -include("ejabberd_http.hrl"). -include("logger.hrl"). -export([process/2]). process([Domain,User|_]=LocalPath, #request{auth = Auth} = Request)-> case get_auth(Auth) of %%make sure user belongs to pubsub domain {User, Domain} -> out(Request, Request#request.method, LocalPath,User); _ -> out(Request, Request#request.method, LocalPath,undefined) end; process(_LocalPath, _Request)-> error(404). get_host([Domain,_User|_Rest])-> "pubsub."++Domain. get_root([Domain,User|_Rest]) -> ["home", Domain, User]. get_collection([Domain,User, Node|_Rest])->["home", Domain, User, Node]. get_item_name([_Domain,_User, _Node, Member]) -> Member. collection_uri(R, Domain, User, Node) -> case Node of ["home", Domain, User|_Rest]-> base_uri(R, Domain, User)++"/"++lists:last(Node); _ -> base_uri(R, Domain, User)++"/"++Node end. entry_uri(R, Domain, User, Node, Id)-> collection_uri(R, Domain, User, Node)++"/"++Id. get_member([_Domain,_User, _Node, Member]=Uri)-> [get_host(Uri), get_collection(Uri), Member]. base_uri(#request{host=Host, port=Port}, Domain, User)-> "http://"++Host++":"++i2l(Port)++"/pubsub/"++Domain++"/"++ User. generate_etag(#pubsub_item{modification={_JID, {_, D2, D3}}})->integer_to_list(D3+D2). out(_Args, 'POST', [_,_, _], undefined) ->error(401); out(_Args, 'PUT', [_,_, _], undefined) ->error(401); out(_Args, 'DELETE', [_,_, _], undefined) ->error(401); %% Service document out(Args, 'GET', [Domain, UserNode]=Uri, _User) -> %%Collections = mnesia:dirty_match_object(#pubsub_node{nodeid={get_host(Uri), '_'},_ = '_'}), case mod_pubsub:tree_action(get_host(Uri), get_subnodes, [get_host(Uri),get_root(Uri), "" ]) of [] -> error(404); Collections -> {200, [{"Content-Type", "application/atomsvc+xml"}], "" ++ xml:element_to_string(service(Args,Domain, UserNode, Collections))} end; %% Collection out(Args, 'GET', [Domain, User, Node]=Uri, _User) -> case mod_pubsub:tree_action(get_host(Uri), get_node, [get_host(Uri),get_collection(Uri)]) of {error, _} -> error(404); _ -> Items = lists:sort(fun(X,Y)-> {_,DateX} = X#pubsub_item.modification, {_,DateY} = Y#pubsub_item.modification, DateX > DateY end, mod_pubsub:get_items( get_host(Uri), get_collection(Uri), "")), case Items of [] -> ?DEBUG("Items : ~p ~n", [collection(get_collection(Uri), collection_uri(Args,Domain,User,Node), calendar:now_to_universal_time(erlang:now()), User, "", [])]), {200, [{"Content-Type", "application/atom+xml"}], collection(get_collection(Uri), collection_uri(Args,Domain,User,Node), calendar:now_to_universal_time(erlang:now()), User, "", [])}; _ -> #pubsub_item{modification = {_JID,LastDate}} = LastItem = hd(Items), Etag =generate_etag(LastItem), IfNoneMatch=proplists:get_value('If-None-Match', Args#request.headers), if IfNoneMatch==Etag -> success(304); true -> XMLEntries= [item_to_entry(Args,Node,Entry)||Entry <- Items], {200, [{"Content-Type", "application/atom+xml"},{"Etag", Etag}], "" ++ xml:element_to_string( collection(get_collection(Uri), collection_uri(Args,Domain,User,Node), calendar:now_to_universal_time(LastDate), User, "", XMLEntries))} end end end; %% Add new collection out(_Args, 'POST', [_Domain, _User], _User)-> error(403); out(Args, 'POST', [Domain,User, Node]=Uri, User) -> %%FIXME Slug Slug = case lists:keysearch("Slug",3,Args#request.headers) of false -> uniqid(false) ; {value, {_,_,_,_,Value}} -> Value end, Payload = xml_stream:parse_element(Args#request.data), [FilteredPayload]=xml:remove_cdata([Payload]), %FilteredPayload2 = case xml:get_subtag(FilteredPayload, "app:edited") -> % {xmlelement, Name, Attrs, [{cdata, }]} case mod_pubsub:publish_item(get_host(Uri), Domain, get_collection(Uri), jlib:make_jid(User,Domain, ""), Slug, [FilteredPayload]) of {result, []} -> ?DEBUG("Publishing to ~p~n",[entry_uri(Args, Domain,User, Node,Slug)]), {201, [{"location", entry_uri(Args, Domain,User,Node,Slug)}], Payload}; {error, Error} -> error(400, Error) end; out(_Args, 'POST', [_, _, _], _) -> {status, 403}; %% Atom doc out(Args, 'GET', [_Domain,_U, Node, _Member]=URI, _User) -> Failure = fun(_Error)->error(404)end, Success = fun(Item)-> Etag =generate_etag(Item), IfNoneMatch=proplists:get_value('If-None-Match', Args#request.headers), if IfNoneMatch==Etag -> success(304); true -> {200, [{"Content-Type", "application/atom+xml"},{"Etag", Etag}], "" ++ xml:element_to_string(item_to_entry(Args, Node, Item))} end end, get_item(URI, Failure, Success); %% Update doc out(Args, 'PUT', [Domain,User, _Node, Member]=Uri, User) -> Payload = xml_stream:parse_element(Args#request.data), Failure = fun(_Error)->error(404)end, Success = fun(Item)-> Etag =generate_etag(Item), IfMatch=proplists:get_value('If-Match', Args#request.headers), if IfMatch==Etag -> case mod_pubsub:publish_item(get_host(Uri), Domain, get_collection(Uri), jlib:make_jid(User,Domain, ""), Member, [Payload]) of {result, _Result} -> {200, [{"Content-Type", "application/atom+xml"}],""}; {error, {xmlelement, "error", [{"code","404"},_],_}} -> error(404); {error, _Error} -> error(500) end; true -> error(412) %% ressource has been modified since last get for this client. end end, get_item(Uri, Failure, Success); %% out(_Args, 'PUT',_Url, _User) -> error(401); out(_Args, 'DELETE', [Domain,User, _Node, _Member]=Uri, User) -> case mod_pubsub:delete_item(get_host(Uri), get_collection(Uri), jlib:make_jid(User,Domain, ""), get_item_name(Uri)) of {result, _Result} -> success(200); {error, {xmlelement, "error", [{"code","404"},_],_}} -> error(404); {error, _Code1} -> error(500) end; out(_Args, 'DELETE',_Url, _User) -> error(401); out(_, _, _, _) -> error(403). get_item(Uri, Failure, Success)-> case catch mod_pubsub:node_action(get_host(Uri), get_collection(Uri), get_item, get_member(Uri)) of {error, Reason} -> Failure(Reason); {result, Item} -> Success(Item) end. item_to_entry(Args,Node,#pubsub_item{itemid={Id,_}, payload=Entry}=Item)-> [R]=xml:remove_cdata(Entry), item_to_entry(Args, Node, Id, R, Item). item_to_entry(Args,Node, Id,{xmlelement, "entry", Attrs, SubEl}, #pubsub_item{modification={JID, Secs} }) -> Date = calendar:now_to_local_time(Secs), {User, Domain, _}=jlib:jid_tolower(JID), SubEl2=[{xmlelement, "app:edited", [], [{xmlcdata, w3cdtf(Date)}]}, {xmlelement, "link",[{"rel", "edit"}, {"href", entry_uri(Args,Domain,User, Node, Id)}],[] }, {xmlelement, "id", [],[{xmlcdata, Id}]} | SubEl], {xmlelement, "entry", [{"xmlns:app","http://www.w3.org/2007/app"}|Attrs], SubEl2}; % Don't do anything except adding xmlns item_to_entry(_Args,Node, _Id, {xmlelement, Name, Attrs, Subels}=Element, _Item)-> case proplists:is_defined("xmlns",Attrs) of true -> Element; false -> {xmlelement, Name, [{"xmlns", Node}|Attrs], Subels} end. collection(Title, Link, Updated, Author, _Id, Entries)-> {xmlelement, "feed", [{"xmlns", "http://www.w3.org/2005/Atom"}, {"xmlns:app", "http://www.w3.org/2007/app"}], [ {xmlelement, "title", [],[{xmlcdata, Title}]}, {xmlelement, "updated", [],[{xmlcdata, w3cdtf(Updated)}]}, {xmlelement, "link", [{"href", Link}], []}, {xmlelement, "author", [], [ {xmlelement, "name", [], [{xmlcdata,Author}]} ]}, {xmlelement, "title", [],[{xmlcdata, Title}]} | Entries ]}. service(Args, Domain, User, Collections)-> {xmlelement, "service", [{"xmlns", "http://www.w3.org/2007/app"}, {"xmlns:atom", "http://www.w3.org/2005/Atom"}, {"xmlns:app", "http://www.w3.org/2007/app"}],[ {xmlelement, "workspace", [],[ {xmlelement, "atom:title", [],[{xmlcdata,"Feed for "++User++"@"++Domain}]} | lists:map(fun(#pubsub_node{nodeid={_Server, Id}, type=_Type})-> {xmlelement, "collection", [{"href", collection_uri(Args,Domain,User, Id)}], [ {xmlelement, "atom:title", [], [{xmlcdata, lists:last(Id)}]} ]} end, Collections) ]} ]}. %%% lifted from ejabberd_web_admin get_auth(Auth) -> case Auth of {SJID, P} -> case jlib:string_to_jid(SJID) of error -> unauthorized; #jid{user = U, server = S} -> case ejabberd_auth:check_password(U, S, P) of true -> {U, S}; false -> unauthorized end end; _ -> unauthorized end. %% simple output functions error(404)-> {404, [], "Not Found"}; error(403)-> {403, [], "Forbidden"}; error(500)-> {500, [], "Internal server error"}; error(401)-> {401, [{"WWW-Authenticate", "basic realm=\"ejabberd\""}],"Unauthorized"}; error(Code)-> {Code, [], ""}. success(200)-> {200, [], ""}; success(Code)-> {Code, [], ""}. error(Code, Error) when is_list(Error) -> {Code, [], Error}; error(Code, {xmlelement, "error",_,_}=Error) -> {Code, [], xml:element_to_string(Error)}; error(Code, _Error) -> {Code, [], "Bad request"}. % Code below is taken (with some modifications) from the yaws webserver, which % is distributed under the folowing license: % % This software (the yaws webserver) is free software. % Parts of this software is Copyright (c) Claes Wikstrom % Any use or misuse of the source code is hereby freely allowed. % % 1. Redistributions of source code must retain the above copyright % notice as well as this list of conditions. % % 2. Redistributions in binary form must reproduce the above copyright % notice as well as this list of conditions. %%% Create W3CDTF (http://www.w3.org/TR/NOTE-datetime) formatted date %%% w3cdtf(GregSecs) -> "YYYY-MM-DDThh:mm:ssTZD" %%% uniqid(false)-> {T1, T2, T3} = now(), lists:flatten(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])); uniqid(Slug) -> Slut = string:to_lower(Slug), S = string:substr(Slut, 1, 9), {_T1, T2, T3} = now(), lists:flatten(io_lib:fwrite("~s-~.16B~.16B", [S, T2, T3])). w3cdtf(Date) -> %1 Date = calendar:gregorian_seconds_to_datetime(GregSecs), {{Y, Mo, D},{H, Mi, S}} = Date, [UDate|_] = calendar:local_time_to_universal_time_dst(Date), {DiffD,{DiffH,DiffMi,_}}=calendar:time_difference(UDate,Date), w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi). %%% w3cdtf's helper function w3cdtf_diff(Y, Mo, D, H, Mi, S, _DiffD, DiffH, DiffMi) when DiffH < 12, DiffH /= 0 -> i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++ add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++ add_zero(S) ++ "+" ++ add_zero(DiffH) ++ ":" ++ add_zero(DiffMi); w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi) when DiffH > 12, DiffD == 0 -> i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++ add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++ add_zero(S) ++ "+" ++ add_zero(DiffH) ++ ":" ++ add_zero(DiffMi); w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi) when DiffH > 12, DiffD /= 0, DiffMi /= 0 -> i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++ add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++ add_zero(S) ++ "-" ++ add_zero(23-DiffH) ++ ":" ++ add_zero(60-DiffMi); w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi) when DiffH > 12, DiffD /= 0, DiffMi == 0 -> i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++ add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++ add_zero(S) ++ "-" ++ add_zero(24-DiffH) ++ ":" ++ add_zero(DiffMi); w3cdtf_diff(Y, Mo, D, H, Mi, S, _DiffD, DiffH, _DiffMi) when DiffH == 0 -> i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++ add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++ add_zero(S) ++ "Z". add_zero(I) when is_integer(I) -> add_zero(i2l(I)); add_zero([A]) -> [$0,A]; add_zero(L) when is_list(L) -> L. i2l(I) when is_integer(I) -> integer_to_list(I); i2l(L) when is_list(L) -> L.