]> granicus.if.org Git - ejabberd/commitdiff
* src/web/ejabberd_http_poll.erl: Properly handle bad requests
authorAlexey Shchepin <alexey@process-one.net>
Tue, 5 Oct 2004 19:31:17 +0000 (19:31 +0000)
committerAlexey Shchepin <alexey@process-one.net>
Tue, 5 Oct 2004 19:31:17 +0000 (19:31 +0000)
* src/web/ejabberd_web_admin.erl: Ported features from J-EAI

SVN Revision: 272

ChangeLog
src/web/ejabberd_http_poll.erl
src/web/ejabberd_web_admin.erl

index 6cbef661bfeccd8918c57e051fb3aa0da4cc6d6a..4f5b3dc9385b9335e319548ea6c5f67ecd38f5a8 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,9 @@
+2004-10-05  Alexey Shchepin  <alexey@sevcom.net>
+
+       * src/web/ejabberd_http_poll.erl: Properly handle bad requests
+
+       * src/web/ejabberd_web_admin.erl: Ported features from J-EAI
+
 2004-09-30  Alexey Shchepin  <alexey@sevcom.net>
 
        * src/web/ejabberd_http.erl: Fixed processing of POST body for
index 58d918a736ce54803adf6bad2da121ec1148cd0c..d76173a30dad95e25597ed69c530ee7b3f504065 100644 (file)
@@ -119,8 +119,10 @@ process_request(#request{path = [],
            end;
        _ ->
            {200, [?CT, {"Set-Cookie", "ID=-2:0; expires=-1"}], ""}
-    end.
-
+    end;
+process_request(_Request) ->
+    {400, [], {xmlelement, "h1", [],
+              [{xmlcdata, "400 Bad Request"}]}}.
 
 %%%----------------------------------------------------------------------
 %%% Callback functions from gen_fsm
index 550d3ecd09cdc790859dc614a044422866804f0e..484ac8bcd0c0019a0a1e58b89dae390d13a61ce9 100644 (file)
@@ -5,6 +5,9 @@
 %%% Created :  9 Apr 2004 by Alexey Shchepin <alexey@sevcom.net>
 %%% Id      : $Id$
 %%%----------------------------------------------------------------------
+%%% Copyright (c) 2004 Alexey Shchepin
+%%% Copyright (c) 2004 Process One
+%%%----------------------------------------------------------------------
 
 -module(ejabberd_web_admin).
 -author('alexey@sevcom.net').
@@ -101,6 +104,7 @@ make_xhtml(Els, Lang) ->
                                                             [?LI([?ACT("/admin/acls/", "Access Control Lists")]),
                                                              ?LI([?ACT("/admin/access/", "Access Rules")]),
                                                              ?LI([?ACT("/admin/users/", "Users")]),
+                                                             ?LI([?ACT("/admin/online-users/", "Online Users")]),
                                                              ?LI([?ACT("/admin/nodes/", "Nodes")]),
                                                              ?LI([?ACT("/admin/stats/", "Statistics")])
                                                             ])]),
@@ -115,7 +119,7 @@ make_xhtml(Els, Lang) ->
                                      [?XE("tbody",
                                           [?XE("tr",
                                                [?XCT("td",
-                                                     "ejabberd (c) 2002-2004 Alexey Shchepin")
+                                                     "ejabberd (c) 2002-2004 Alexey Shchepin, 2004 Process One")
                                                ])])
                                      ])])])])])])
       ]}}.
@@ -494,6 +498,7 @@ process_admin(#request{user = User,
                     ?LI([?ACT("access/", "Access Rules"), ?C(" "),
                          ?ACT("access-raw/", "(raw)")]),
                     ?LI([?ACT("users/", "Users")]),
+                    ?LI([?ACT("online-users/", "Online Users")]),
                     ?LI([?ACT("nodes/", "Nodes")]),
                     ?LI([?ACT("stats/", "Statistics")])
                    ])
@@ -753,14 +758,21 @@ process_admin(#request{user = User,
                        path = ["users"],
                        q = Query,
                        lang = Lang} = Request) ->
-    Res = list_users(),
+    Res = list_users(Query, Lang),
     make_xhtml([?XCT("h1", "ejabberd users")] ++ Res, Lang);
 
 process_admin(#request{user = User,
                       path = ["users", Diap],
                       q = Query,
                       lang = Lang} = Request) ->
-    Res = list_users_in_diapason(Diap),
+    Res = list_users_in_diapason(Diap, Lang),
+    make_xhtml([?XCT("h1", "ejabberd users")] ++ Res, Lang);
+
+process_admin(#request{user = User,
+                       path = ["online-users"],
+                       q = Query,
+                       lang = Lang} = Request) ->
+    Res = list_online_users(Lang),
     make_xhtml([?XCT("h1", "ejabberd users")] ++ Res, Lang);
 
 process_admin(#request{user = User,
@@ -777,11 +789,18 @@ process_admin(#request{user = User,
     Res = user_info(U, Query, Lang),
     make_xhtml(Res, Lang);
 
+process_admin(#request{user = User,
+                      path = ["user", U, "queue"],
+                      q = Query,
+                      lang = Lang} = Request) ->
+    Res = user_queue(U, Query, Lang),
+    make_xhtml(Res, Lang);
+
 process_admin(#request{user = User,
                       path = ["user", U, "roster"],
                       q = Query,
                       lang = Lang} = Request) ->
-    Res = user_roster(U, Query, Lang),
+    Res = user_roster(U, Query, Lang, true),
     make_xhtml(Res, Lang);
 
 process_admin(#request{user = User,
@@ -1036,44 +1055,123 @@ parse_access_rule(Text) ->
 
 
 
-list_users() ->
+list_users(Query, Lang) ->
+    Res = list_users_parse_query(Query),
     Users = ejabberd_auth:dirty_get_registered_users(),
     SUsers = lists:sort(Users),
-    case length(SUsers) of
-       N when N =< 100 ->
-           lists:flatmap(
-             fun(U) ->
-                     [?AC("../user/" ++ U ++ "/", U), ?BR]
-             end, SUsers);
-       N ->
-           NParts = trunc(math:sqrt(N * 0.618)) + 1,
-           M = trunc(N / NParts) + 1,
-           lists:flatmap(
-             fun(K) ->
-                     L = K + M - 1,
-                     Node = integer_to_list(K) ++ "-" ++ integer_to_list(L),
-                     Last = if L < N -> lists:nth(L, SUsers);
-                               true -> lists:last(SUsers)
-                            end,
-                     Name = 
-                         lists:nth(K, SUsers) ++ [$\s, 226, 128, 148, $\s] ++
-                         Last,
-                     [?AC(Node ++ "/", Name), ?BR]
-             end, lists:seq(1, N, M))
+    FUsers =
+       case length(SUsers) of
+           N when N =< 100 ->
+               [list_given_users(SUsers, Lang)];
+           N ->
+               NParts = trunc(math:sqrt(N * 0.618)) + 1,
+               M = trunc(N / NParts) + 1,
+               lists:flatmap(
+                 fun(K) ->
+                         L = K + M - 1,
+                         Node = integer_to_list(K) ++ "-" ++ integer_to_list(L),
+                         Last = if L < N -> lists:nth(L, SUsers);
+                                   true -> lists:last(SUsers)
+                                end,
+                         Name = 
+                             lists:nth(K, SUsers) ++ [$\s, 226, 128, 148, $\s] ++
+                             Last,
+                         [?AC(Node ++ "/", Name), ?BR]
+                 end, lists:seq(1, N, M))
+       end,
+    case Res of
+       ok -> [?CT("submitted"), ?P];
+       error -> [?CT("bad format"), ?P];
+       nothing -> []
+    end ++
+       [?XAE("form", [{"method", "post"}],
+             [?XE("table",
+                  [?XE("tr",
+                       [?XC("td", ?T("User") ++ ":"),
+                        ?XE("td", [?INPUT("text", "newusername", "")])
+                       ]),
+                   ?XE("tr",
+                       [?XC("td", ?T("Password") ++ ":"),
+                        ?XE("td", [?INPUT("password", "newuserpassword", "")])
+                       ]),
+                   ?XE("tr",
+                       [?X("td"),
+                        ?XAE("td", [{"class", "alignright"}],
+                             [?INPUTT("submit", "addnewuser", "Add User")])
+                       ])]),
+              ?P] ++
+             FUsers)].
+
+list_users_parse_query(Query) ->
+    case lists:keysearch("addnewuser", 1, Query) of
+       {value, _} ->
+           {value, {_, User}} =
+               lists:keysearch("newusername", 1, Query),
+           {value, {_, Password}} =
+               lists:keysearch("newuserpassword", 1, Query),
+           case jlib:nodeprep(User) of
+               error ->
+                   error;
+               "" ->
+                   error;
+               _ ->
+                   ejabberd_auth:try_register(User, Password),
+                   ok
+           end;
+       false ->
+           nothing
     end.
 
-list_users_in_diapason(Diap) ->
+list_users_in_diapason(Diap, Lang) ->
     Users = ejabberd_auth:dirty_get_registered_users(),
     SUsers = lists:sort(Users),
     {ok, [S1, S2]} = regexp:split(Diap, "-"),
     N1 = list_to_integer(S1),
     N2 = list_to_integer(S2),
     Sub = lists:sublist(SUsers, N1, N2 - N1 + 1),
-    lists:flatmap(
-      fun(U) ->
-             [?AC("../../user/" ++ U ++ "/", U), ?BR]
-      end, Sub).
-
+    [list_given_users(Sub, Lang)].
+
+list_given_users(Users, Lang) ->
+    ?XE("table",
+       [?XE("thead",
+            [?XE("tr",
+                 [?XCT("td", "User"),
+                  ?XCT("td", "Offline messages"),
+                  ?XCT("td", "Last Activity")])]),
+        ?XE("tbody",
+            lists:map(
+              fun(User) ->
+                      QueueLen = length(mnesia:dirty_read({offline_msg, User})),
+                      FQueueLen = [?AC("../../user/" ++ User ++ "/queue/",
+                                       integer_to_list(QueueLen))],
+                      FLast =
+                          case ejabberd_sm:get_user_resources(User) of
+                              [] ->
+                                  case mnesia:dirty_read({last_activity, User}) of
+                                      [] ->
+                                          ?T("Never");
+                                      [E] ->
+                                          Shift = element(3, E),
+                                          TimeStamp = {Shift div 1000000,
+                                                       Shift rem 1000000,
+                                                       0},
+                                          {{Year, Month, Day}, {Hour, Minute, Second}} =
+                                              calendar:now_to_local_time(TimeStamp),
+                                          lists:flatten(
+                                            io_lib:format(
+                                              "~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w",
+                                              [Year, Month, Day, Hour, Minute, Second]))
+                                  end;
+                              _ ->
+                                  ?T("Online")
+                          end,
+                      ?XE("tr",
+                          [?XE("td", [?AC("../../user/" ++ User ++ "/",
+                                          User)]),
+                           ?XE("td", FQueueLen),
+                           ?XC("td", FLast)])
+              end, Users)
+           )]).
 
 
 get_stats(Lang) ->
@@ -1100,6 +1198,15 @@ get_stats(Lang) ->
          ])].
 
 
+list_online_users(_Lang) ->
+    Users = lists:map(fun({U, R}) -> U end,
+                     ejabberd_sm:dirty_get_sessions_list()),
+    SUsers = lists:usort(Users),
+    lists:flatmap(
+      fun(U) ->
+             [?AC("../user/" ++ U ++ "/", U), ?BR]
+      end, SUsers).
+
 user_info(User, Query, Lang) ->
     Res = user_parse_query(User, Query),
     Resources = ejabberd_sm:get_user_resources(User),
@@ -1116,6 +1223,9 @@ user_info(User, Query, Lang) ->
     Password = ejabberd_auth:get_password_s(User),
     FPassword = [?INPUT("text", "password", Password), ?C(" "),
                 ?INPUTT("submit", "chpassword", "Change Password")],
+    QueueLen = length(mnesia:dirty_read({offline_msg, User})),
+    FQueueLen = [?AC("queue/",
+                    integer_to_list(QueueLen))],
     [?XC("h1", "User: " ++ User)] ++
        case Res of
            ok -> [?CT("submitted"), ?P];
@@ -1124,13 +1234,18 @@ user_info(User, Query, Lang) ->
        end ++
        [?XAE("form", [{"method", "post"}],
              [?XCT("h3", "Connected Resources:")] ++ FResources ++
-             [?XCT("h3", "Password:")] ++ FPassword)].
+             [?XCT("h3", "Password:")] ++ FPassword ++
+             [?XCT("h3", "Offline messages:")] ++ FQueueLen ++
+             [?XE("h3", [?ACT("roster/", "Roster")])] ++
+             [?BR, ?INPUTT("submit", "removeuser", "Remove User")])].
 
 
 user_parse_query(User, Query) ->
     case lists:keysearch("chpassword", 1, Query) of
        {value, _} ->
            case lists:keysearch("password", 1, Query) of
+               {value, {_, undefined}} ->
+                   error;
                {value, {_, Password}} ->
                    ejabberd_auth:set_password(User, Password),
                    ok;
@@ -1138,10 +1253,90 @@ user_parse_query(User, Query) ->
                    error
            end;
        _ ->
+           case lists:keysearch("removeuser", 1, Query) of
+               {value, _} ->
+                   ejabberd_auth:remove_user(User),
+                   ok;
+               false ->
+                   nothing
+           end
+    end.
+
+
+user_queue(User, Query, Lang) ->
+    Res = user_queue_parse_query(User, Query),
+    Msgs = lists:keysort(3, mnesia:dirty_read({offline_msg, User})),
+    FMsgs =
+       lists:map(
+         fun({offline_msg, _User, TimeStamp, _Expire, From, To,
+              {xmlelement, Name, Attrs, Els}} = Msg) ->
+                 ID = jlib:encode_base64(binary_to_list(term_to_binary(Msg))),
+                 {{Year, Month, Day}, {Hour, Minute, Second}} =
+                     calendar:now_to_local_time(TimeStamp),
+                 Time = lists:flatten(
+                          io_lib:format(
+                            "~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w",
+                            [Year, Month, Day, Hour, Minute, Second])),
+                 SFrom = jlib:jid_to_string(From),
+                 STo = jlib:jid_to_string(To),
+                 Attrs2 = jlib:replace_from_to_attrs(SFrom, STo, Attrs),
+                 Packet = jlib:remove_attr(
+                            "jeai-id", {xmlelement, Name, Attrs2, Els}),
+                 FPacket = pretty_print(Packet),
+                 ?XE("tr",
+                     [?XAE("td", [{"class", "valign"}], [?INPUT("checkbox", "selected", ID)]),
+                      ?XAC("td", [{"class", "valign"}], Time),
+                      ?XAC("td", [{"class", "valign"}], SFrom),
+                      ?XAC("td", [{"class", "valign"}], STo),
+                      ?XAE("td", [{"class", "valign"}], [?XC("pre", FPacket)])]
+                    )
+         end, Msgs),
+    [?XC("h1", User ++ " offline messages queue")] ++
+       case Res of
+           ok -> [?CT("submitted"), ?P];
+           error -> [?CT("bad format"), ?P];
+           nothing -> []
+       end ++
+       [?XAE("form", [{"method", "post"}],
+             [?XE("table",
+                  [?XE("thead",
+                       [?XE("tr",
+                            [?X("td"),
+                             ?XCT("td", "Time"),
+                             ?XCT("td", "From"),
+                             ?XCT("td", "To"),
+                             ?XCT("td", "Packet")
+                            ])]),
+                   ?XE("tbody", FMsgs)]),
+              ?BR,
+              ?INPUTT("submit", "delete", "Delete Selected")
+             ])].
+
+user_queue_parse_query(User, Query) ->
+    case lists:keysearch("delete", 1, Query) of
+       {value, _} ->
+           Msgs = lists:keysort(3, mnesia:dirty_read({offline_msg, User})),
+           F = fun() ->
+                       lists:foreach(
+                         fun(Msg) ->
+                                 ID = jlib:encode_base64(
+                                        binary_to_list(term_to_binary(Msg))),
+                                 case lists:member({"selected", ID}, Query) of
+                                     true ->
+                                         mnesia:delete_object(Msg);
+                                     false ->
+                                         ok
+                                 end
+                         end, Msgs)
+               end,
+           mnesia:transaction(F),
+           ok;
+       false ->
            nothing
     end.
 
 
+
 -record(roster, {uj,
                 user,
                 jid,
@@ -1156,10 +1351,11 @@ ask_to_pending(subscribe) -> out;
 ask_to_pending(unsubscribe) -> none;
 ask_to_pending(Ask) -> Ask.
 
-user_roster(User, Query, Lang) ->
-    %Res = user_parse_query(User, Query),
-    Res = nothing, % TODO
+
+user_roster(User, Query, Lang, Admin) ->
     LUser = jlib:nameprep(User),
+    Items1 = mnesia:dirty_index_read(roster, LUser, #roster.user),
+    Res = user_roster_parse_query(User, Items1, Query, Admin),
     Items = mnesia:dirty_index_read(roster, LUser, #roster.user),
     SItems = lists:sort(Items),
     FItems =
@@ -1173,22 +1369,43 @@ user_roster(User, Query, Lang) ->
                               [?XCT("td", "JID"),
                                ?XCT("td", "Name"),
                                ?XCT("td", "Subscription"),
-                               ?XCT("td", "Pending")
+                               ?XCT("td", "Pending"),
+                               ?XCT("td", "Groups")
                               ])]),
                      ?XE("tbody",
                          lists:map(
                            fun(R) ->
+                                   Groups =
+                                       lists:flatmap(
+                                         fun(Group) ->
+                                                 [?C(Group), ?BR]
+                                         end, R#roster.groups),
+                                   Pending = ask_to_pending(R#roster.ask),
                                    ?XE("tr",
-                                       [?XE("td", [?C(jlib:jid_to_string(
-                                                        R#roster.jid))]),
-                                        ?XE("td", [?C(R#roster.name)]),
-                                        ?XE("td",
-                                            [?C(atom_to_list(
-                                                  R#roster.subscription))]),
-                                        ?XE("td",
-                                            [?C(atom_to_list(
-                                                  ask_to_pending(
-                                                    R#roster.ask)))])])
+                                       [?XAC("td", [{"class", "valign"}],
+                                             jlib:jid_to_string(R#roster.jid)),
+                                        ?XAC("td", [{"class", "valign"}],
+                                             R#roster.name),
+                                        ?XAC("td", [{"class", "valign"}],
+                                             atom_to_list(R#roster.subscription)),
+                                        ?XAC("td", [{"class", "valign"}],
+                                             atom_to_list(Pending)),
+                                        ?XAE("td", [{"class", "valign"}], Groups),
+                                        if
+                                            Pending == in ->
+                                                ?XAE("td", [{"class", "valign"}],
+                                                     [?INPUTT("submit",
+                                                              "validate" ++
+                                                              term_to_id(R#roster.jid),
+                                                              "Validate")]);
+                                            true ->
+                                                ?X("td")
+                                        end,
+                                        ?XAE("td", [{"class", "valign"}],
+                                             [?INPUTT("submit",
+                                                      "remove" ++
+                                                      term_to_id(R#roster.jid),
+                                                      "Remove")])])
                            end, SItems))])]
        end,
     [?XC("h1", "Roster of " ++ User)] ++
@@ -1198,7 +1415,129 @@ user_roster(User, Query, Lang) ->
            nothing -> []
        end ++
        [?XAE("form", [{"method", "post"}],
-             FItems)].
+             FItems ++
+             [?P,
+              ?INPUT("text", "newjid", ""), ?C(" "),
+              ?INPUTT("submit", "addjid", "Add JID")
+             ])].
+
+user_roster_parse_query(User, Items, Query, Admin) ->
+    case lists:keysearch("addjid", 1, Query) of
+       {value, _} ->
+           case lists:keysearch("newjid", 1, Query) of
+               {value, {_, undefined}} ->
+                   error;
+               {value, {_, SJID}} ->
+                   case jlib:string_to_jid(SJID) of
+                       JID when is_record(JID, jid) ->
+                           user_roster_subscribe_jid(User, JID),
+                           ok;
+                       error ->
+                           error
+                   end;
+               false ->
+                   error
+           end;
+       false ->
+           case lists:keysearch("adduser", 1, Query) of
+               {value, _} ->
+                   case lists:keysearch("newuser", 1, Query) of
+                       {value, {_, undefined}} ->
+                           error;
+                       {value, {_, U}} ->
+                           if
+                               Admin ->
+                                   user_roster_subscribe_users(User, U);
+                               true ->
+                                   case jlib:make_jid(U, ?MYNAME, "") of
+                                       JID when is_record(JID, jid) ->
+                                           user_roster_subscribe_jid(
+                                             User, JID),
+                                           ok;
+                                       false ->
+                                           error
+                                   end
+                           end;
+                       false ->
+                           error
+                   end;
+               false ->
+                   case catch user_roster_item_parse_query(
+                                User, Items, Query) of
+                       submitted ->
+                           ok;
+                       {'EXIT', _Reason} ->
+                           error;
+                       _ ->
+                           nothing
+                   end
+           end
+    end.
+
+user_roster_subscribe_users(User1, User2) ->
+    case jlib:make_jid(User1, ?MYNAME, "") of
+       JID1 when is_record(JID1, jid) ->
+           case jlib:make_jid(User2, ?MYNAME, "") of
+               JID2 when is_record(JID2, jid) ->
+                   mod_roster:out_subscription(User1, JID2, subscribe),
+                   mod_roster:in_subscription(User2, JID1, subscribe),
+                   mod_roster:out_subscription(User2, JID1, subscribe),
+                   mod_roster:in_subscription(User1, JID2, subscribe),
+                   mod_roster:out_subscription(User1, JID2, subscribed),
+                   mod_roster:in_subscription(User2, JID1, subscribed),
+                   mod_roster:out_subscription(User2, JID1, subscribed),
+                   mod_roster:in_subscription(User1, JID2, subscribed),
+                   ok;
+               false ->
+                   error
+           end;
+       false ->
+           error
+    end.
+
+user_roster_subscribe_jid(User, JID) ->
+    mod_roster:out_subscription(User, JID, subscribe),
+    UJID = jlib:make_jid(User, ?MYNAME, ""),
+    ejabberd_router:route(
+      UJID, JID, {xmlelement, "presence", [{"type", "subscribe"}], []}).
+
+user_roster_item_parse_query(User, Items, Query) ->
+    lists:foreach(
+      fun(R) ->
+             JID = R#roster.jid,
+             case lists:keysearch(
+                    "validate" ++ term_to_id(JID), 1, Query) of
+                 {value, _} ->
+                     JID1 = jlib:make_jid(JID),
+                     mod_roster:out_subscription(User, JID1, subscribed),
+                     UJID = jlib:make_jid(User, ?MYNAME, ""),
+                     ejabberd_router:route(
+                       UJID, JID1, {xmlelement, "presence",
+                                    [{"type", "subscribed"}], []}),
+                     throw(submitted);
+                 false ->
+                     case lists:keysearch(
+                            "remove" ++ term_to_id(JID), 1, Query) of
+                         {value, _} ->
+                             UJID = jlib:make_jid(User, ?MYNAME, ""),
+                             mod_roster:process_iq(
+                               UJID, UJID,
+                               #iq{type = set,
+                                   sub_el = {xmlelement, "query",
+                                             [{"xmlns", ?NS_ROSTER}],
+                                             [{xmlelement, "item",
+                                               [{"jid", jlib:jid_to_string(JID)},
+                                                {"subscription", "remove"}],
+                                               []}]}}),
+                             throw(submitted);
+                         false ->
+                             ok
+                     end
+
+             end
+      end, Items),
+    nothing.
+
 
 
 get_nodes(Lang) ->
@@ -1650,3 +1989,45 @@ node_ports_parse_query(Node, Ports, Query) ->
 
 
 
+pretty_print(El) ->
+    lists:flatten(pretty_print(El, "")).
+
+pretty_print({xmlcdata, CData}, Prefix) ->
+    [Prefix, CData, $\n];
+pretty_print({xmlelement, Name, Attrs, Els}, Prefix) ->
+    [Prefix, $<, Name,
+     case Attrs of
+        [] ->
+            [];
+        [{Attr, Val} | RestAttrs] ->
+            AttrPrefix = [Prefix,
+                          string:copies(" ", length(Name) + 2)],
+            [$\s, Attr, $=, $', xml:crypt(Val), $' |
+             lists:map(fun({Attr1, Val1}) ->
+                               [$\n, AttrPrefix,
+                                Attr1, $=, $', xml:crypt(Val1), $']
+                       end, RestAttrs)]
+     end,
+     if
+        Els == [] ->
+            "/>\n";
+        true ->
+            OnlyCData = lists:all(fun({xmlcdata, _}) -> true;
+                                     ({xmlelement, _, _, _}) -> false
+                                  end, Els),
+            if
+                OnlyCData ->
+                    [$>,
+                     xml:get_cdata(Els),
+                     $<, $/, Name, $>, $\n
+                    ];
+                true ->
+                    [$>, $\n,
+                     lists:map(fun(E) ->
+                                       pretty_print(E, [Prefix, "  "])
+                               end, Els),
+                     Prefix, $<, $/, Name, $>, $\n
+                    ]
+            end
+     end].
+