1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 1997-2016. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20%%
21-module(mod_dir).
22
23-include("httpd.hrl").
24-include("httpd_internal.hrl").
25
26-export([do/1]).
27
28%% do
29
30do(Info) ->
31    case Info#mod.method of
32	"GET" ->
33	    case proplists:get_value(status, Info#mod.data) of
34		%% A status code has been generated!
35		{_StatusCode, _PhraseArgs, _Reason} ->
36		    {proceed,Info#mod.data};
37		%% No status code has been generated!
38		undefined ->
39		    case proplists:get_value(response, Info#mod.data) of
40			%% No response has been generated!
41			undefined ->
42			    do_dir(Info);
43			%% A response has been generated or sent!
44			_Response ->
45			    {proceed,Info#mod.data}
46		    end
47	    end;
48	%% Not a GET method!
49	_ ->
50	    {proceed,Info#mod.data}
51    end.
52
53do_dir(Info) ->
54    Path = mod_alias:path(Info#mod.data,Info#mod.config_db,
55			  Info#mod.request_uri),
56    DefaultPath = mod_alias:default_index(Info#mod.config_db,Path),
57    %% Is it a directory?
58    case file:read_file_info(DefaultPath) of
59	{ok,FileInfo} when FileInfo#file_info.type == directory ->
60	    case dir(DefaultPath,string:strip( Info#mod.request_uri,right,$/),
61		     Info#mod.config_db) of
62		{ok, DirUnicode} ->
63		    Dir = unicode:characters_to_binary(DirUnicode),
64		    LastModified =
65			case (catch httpd_util:rfc1123_date(
66				      FileInfo#file_info.mtime)) of
67			    Date when is_list(Date) ->
68				[{"date", Date}];
69			    _ -> %% This will rarly happen, but could happen
70				%% if a computer is wrongly configured.
71				[]
72			end,
73		    Head=[{content_type,"text/html; charset=UTF-8"},
74			  {content_length,
75			   integer_to_list(erlang:iolist_size(Dir))},
76			  {code,200} | LastModified],
77		    {proceed,[{response,{response, Head, Dir}},
78			      {mime_type,"text/html"} | Info#mod.data]};
79		{error, Reason} ->
80		    {proceed,
81		     [{status,{404,Info#mod.request_uri,Reason}}|
82		      Info#mod.data]}
83	    end;
84	{ok, _FileInfo} ->
85	    {proceed,Info#mod.data};
86	{error,Reason} ->
87	    Status = httpd_file:handle_error(Reason, "access", Info,
88					     DefaultPath),
89	    {proceed, [{status, Status}| Info#mod.data]}
90    end.
91
92dir(Path,RequestURI,ConfigDB) ->
93    case file:list_dir(Path) of
94	{ok,FileList} ->
95	    SortedFileList=lists:sort(FileList),
96	    {ok,[header(Path,RequestURI),
97		 body(Path,RequestURI,ConfigDB,SortedFileList),
98		 footer(Path,SortedFileList)]};
99	{error,Reason} ->
100	    {error,?NICE("Can't open directory "++Path++": "++
101			 file:format_error(Reason))}
102    end.
103
104encode_html_entity(FileName) ->
105	Enc = fun($&) -> "&";
106	         ($<) -> "&lt;";
107			 ($>) -> "&gt;";
108			 ($") -> "&quot;";
109			 ($') -> "&#x27;";
110			 ($/) -> "&#x2F;";
111			 (C)  -> C
112		  end,
113	unicode:characters_to_list([Enc(C) || C <- FileName]).
114
115%% header
116
117header(Path,RequestURI) ->
118    DisplayURI = case RequestURI of
119                     "" -> "/";
120                     _  -> RequestURI
121                 end,
122    Header = "<!DOCTYPE html>\n"
123	"<HTML>\n<HEAD>\n"
124	"<meta charset=\"UTF-8\">"
125	"<TITLE>Index of " ++ DisplayURI ++ "</TITLE>\n"
126	"</HEAD>\n<BODY>\n<H1>Index of "++
127	DisplayURI ++ "</H1>\n<PRE><span>" ++ icon(blank) ++
128	"</span> Name                   Last modified         "
129	"Size  Description <HR>\n",
130    case RequestURI of
131	"" ->
132	    Header;
133	_ ->
134            ParentRequestURI = re:replace(RequestURI,"[^/]*\$", "",
135                                          [{return,list}]),
136	    ParentPath =
137		re:replace(string:strip(Path,right,$/),"[^/]*\$","",
138			   [{return,list}]),
139	    Header++format(ParentPath,ParentRequestURI)
140    end.
141
142format(Path,RequestURI) ->
143    {ok,FileInfo}=file:read_file_info(Path),
144    {{Year, Month, Day},{Hour, Minute, _Second}} = FileInfo#file_info.mtime,
145    io_lib:format("<span title=\"~s\">~s</span>"
146		  " <A HREF=\"~ts\">Parent directory</A>      "
147		  " ~2.2.0w-~s-~w ~2.2.0w:~2.2.0w        -\n",
148		  ["DIR",icon(back),RequestURI,Day,
149		   httpd_util:month(Month),Year,Hour,Minute]).
150
151%% body
152
153body(_Path, _RequestURI, _ConfigDB, []) ->
154    [];
155body(Path, RequestURI, ConfigDB, [Entry | Rest]) ->
156    [format(Path, RequestURI, ConfigDB, Entry)|
157     body(Path, RequestURI, ConfigDB, Rest)].
158
159format(Path,RequestURI,ConfigDB,InitEntry) ->
160	Entry = encode_html_entity(InitEntry),
161    case file:read_file_info(Path++"/"++InitEntry) of
162	{ok,FileInfo} when FileInfo#file_info.type == directory ->
163	    {{Year, Month, Day},{Hour, Minute, _Second}} =
164		FileInfo#file_info.mtime,
165	    EntryLength = string:length(InitEntry),
166	    if
167		EntryLength > 21 ->
168		    TruncatedEntry = encode_html_entity(
169                                       string:slice(InitEntry, 0, 19)),
170		    io_lib:format("<span title=\"[~s]\">~s</span> "
171				  "<A HREF=\"~ts\">~ts..</A>"
172				  "~*.*c~2.2.0w-~s-~w ~2.2.0w:~2.2.0w"
173				  "        -\n",
174                                  ["DIR", icon(folder),
175                                   RequestURI ++ "/" ++ percent_encode(InitEntry) ++ "/",
176                                   TruncatedEntry, 23-21, 23-21, $ ,
177                                   Day, httpd_util:month(Month),
178                                   Year,Hour,Minute]);
179		true ->
180		    io_lib:format("<span title=\"[~s]\">~s</span>"
181				  " <A HREF=\"~ts\">~ts</A>~*.*c~2.2.0"
182				  "w-~s-~w ~2.2.0w:~2.2.0w        -\n",
183				  ["DIR", icon(folder),
184                                   RequestURI ++ "/" ++ percent_encode(InitEntry) ++ "/",Entry,
185				   23-EntryLength,23-EntryLength,$ ,Day,
186				   httpd_util:month(Month),Year,Hour,Minute])
187	    end;
188	{ok,FileInfo} ->
189	    {{Year, Month, Day},{Hour, Minute,_Second}} =
190		FileInfo#file_info.mtime,
191	    Suffix=httpd_util:strip_extension_dot(Entry),
192	    MimeType=httpd_util:lookup_mime(ConfigDB,Suffix,""),
193	    EntryLength = string:length(InitEntry),
194	    if
195		EntryLength > 21 ->
196		    TruncatedEntry = encode_html_entity(
197                                       string:slice(InitEntry, 0, 19)),
198		    io_lib:format("<span title=\"[~s]\">~s</span>"
199				  " <A HREF=\"~ts\">~ts..</A>~*.*c~2.2.0"
200				  "w-~s-~w ~2.2.0w:~2.2.0w~8wk  ~s\n",
201				  [Suffix, icon(Suffix, MimeType),
202                                   RequestURI ++ "/" ++ percent_encode(InitEntry),
203                                   TruncatedEntry, 23-21, 23-21, $ , Day,
204				   httpd_util:month(Month),Year,Hour,Minute,
205				   trunc(FileInfo#file_info.size/1024+1),
206				   MimeType]);
207		true ->
208		    io_lib:format("<span title=\"[~s]\">~s</span> "
209				  "<A HREF=\"~ts\">~ts</A>~*.*c~2.2.0w-~s-~w"
210				  " ~2.2.0w:~2.2.0w~8wk  ~s\n",
211				  [Suffix, icon(Suffix, MimeType),
212                                   RequestURI ++ "/" ++ percent_encode(InitEntry), Entry, 23-EntryLength,
213				   23-EntryLength, $ ,Day,
214				   httpd_util:month(Month),Year,Hour,Minute,
215				   trunc(FileInfo#file_info.size/1024+1),
216				   MimeType])
217	    end;
218	{error, _Reason} ->
219	    ""
220    end.
221
222percent_encode(URI) when is_list(URI) ->
223    Reserved = reserved(),
224    lists:append([uri_encode(Char, Reserved) || Char <- URI]).
225
226reserved() ->
227    sets:from_list([$;, $:, $@, $&, $=, $+, $,, $?, $/,
228            $#, $[, $], $<, $>, $\", ${, $}, $|, %"
229			       $\\, $', $^, $%, $ ]).
230
231uri_encode(Char, Reserved) ->
232    case sets:is_element(Char, Reserved) of
233	true ->
234	    [ $% | http_util:integer_to_hexlist(Char)];
235	false ->
236	    [Char]
237    end.
238
239%% footer
240
241footer(Path,FileList) ->
242    case lists:member("README",FileList) of
243	true ->
244	    {ok,Body}=file:read_file(Path++"/README"),
245	    "</PRE>\n<HR>\n<PRE>\n"++binary_to_list(Body)++
246		"\n</PRE>\n</BODY>\n</HTML>\n";
247	false ->
248	    "</PRE>\n</BODY>\n</HTML>\n"
249    end.
250
251%%
252%% Icon mappings (Emoji)
253%%
254
255-define(package, "&#x1F4E6;").                  % package
256-define(image, "&#x1F4F7;").                    % camera
257-define(audio, "&#x1F4E2;").                    % loudspeaker
258-define(video, "&#x1F3A5;").                    % movie camera
259
260-define(page, "&#x1F4C4;").                     % page
261-define(page2, "&#x1F4C3;").                    % page, curled
262-define(world, "&#x1F30D;").                    % globe
263-define(unknown, ?page).
264-define(text, "&#x1F4DD;").                     % page with pencil
265-define(sourcecode, "&#x1F4DC;").               % scroll
266
267icon(Suffix,MimeType) ->
268    case icon(Suffix) of
269	undefined ->
270	    case MimeType of
271		[$t,$e,$x,$t,$/|_] ->
272		    ?text;
273		[$i,$m,$a,$g,$e,$/|_] ->
274		    ?image;
275		[$a,$u,$d,$i,$o,$/|_] ->
276		    ?audio;
277		[$v,$i,$d,$e,$o,$/|_] ->
278		    ?video;
279		_ ->
280		    ?unknown
281	    end;
282	Icon ->
283	    Icon
284    end.
285
286icon(blank) -> "&#x1F4C2;"; % open folder
287icon(back) -> "&#x1F519;"; % back arrow
288icon(folder) -> "&#x1F4C1;"; % closed folder
289icon("bin") -> ?page2;
290icon("exe") -> ?page2;
291icon("hqx") -> ?page2;
292icon("tar") -> ?package;
293icon("wrl") -> ?world;
294icon("wrl.gz") -> ?world;
295icon("vrml") -> ?world;
296icon("vrm") -> ?world;
297icon("iv") -> ?world;
298icon("Z") -> ?package;
299icon("z") -> ?package;
300icon("tgz") -> ?package;
301icon("gz") -> ?package;
302icon("zip") -> ?package;
303icon("bz2") -> ?package;
304icon("ps") -> ?page;
305icon("ai") -> ?image;
306icon("eps") -> ?image;
307icon("html") -> ?text;
308icon("shtml") -> ?text;
309icon("htm") -> ?text;
310icon("pdf") -> ?text;
311icon("txt") -> ?text;
312icon("erl") -> ?sourcecode;
313icon("c") -> ?sourcecode;
314icon("pl") -> ?sourcecode;
315icon("py") -> ?sourcecode;
316icon("for") -> ?sourcecode;
317icon("dvi") -> ?text;
318icon("conf") -> ?sourcecode;
319icon("sh") -> ?sourcecode;
320icon("shar") -> ?sourcecode;
321icon("csh") -> ?sourcecode;
322icon("ksh") -> ?sourcecode;
323icon("tcl") -> ?sourcecode;
324icon("tex") -> ?sourcecode;
325icon("core") -> ?sourcecode;
326icon("xml") -> ?sourcecode;
327icon("jpg") -> ?image;
328icon("JPG") -> ?image;
329icon("jpeg") -> ?image;
330icon("png") -> ?image;
331icon("gif") -> ?image;
332icon("avi") -> ?video;
333icon("mp4") -> ?video;
334icon("m4a") -> ?audio;
335icon("mp3") -> ?audio;
336icon("aac") -> ?audio;
337icon("flac") -> ?audio;
338icon(_) -> undefined.
339