/*  $Id$

    Part of SWI-Prolog

    Author:        Jan Wielemaker
    E-mail:        J.Wielemaker@uva.nl
    WWW:           http://www.swi-prolog.org
    Copyright (C): 1985-2009, University of Amsterdam

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License
    as published by the Free Software Foundation; either version 2
    of the License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

    As a special exception, if you link this library with other files,
    compiled with a Free Software compiler, to produce an executable, this
    library does not by itself cause the resulting executable to be covered
    by the GNU General Public License. This exception does not however
    invalidate any other reasons why the executable file might be covered by
    the GNU General Public License.
*/

:- module(url,
	  [ parse_url/2,		% +URL, -Parts | -URL +Parts
	    parse_url/3,		% +URL|URI, +BaseURL, -Parts
	    				% -URL, +BaseURL, +Parts
	    is_absolute_url/1,		% +URL
	    global_url/3,		% +Local, +Base, -Global
	    http_location/2,		% ?Parts, ?Location
	    www_form_encode/2,		% Value <-> Encoded
	    parse_url_search/2,		% Form-data <-> Form fields

	    url_iri/2,			% ?URL, ?IRI

	    file_name_to_url/2,		% ?FileName, ?URL

	    set_url_encoding/2		% ?Old, +New
	  ]).
:- use_module(library(lists)).
:- use_module(library(error)).
:- use_module(library(utf8)).

/** <module> Analysing and constructing URL

This library deals with the analysis and construction of a URL,
Universal Resource Locator. URL is the basis for communicating locations
of resources (data) on the web. A URL consists of a protocol identifier
(e.g. HTTP, FTP, and a protocol-specific syntax further defining the
location. URLs are standardized in RFC-1738.

The implementation in this library covers only a small portion of the
defined protocols.  Though the initial implementation followed RFC-1738
strictly, the current is more relaxed to deal with frequent violations
of the standard encountered in practical use.

@author	Jan Wielemaker
@author Lukas Faulstich
@deprecated New code should use library(uri), provided by the =clib=
 	    package.
*/

		 /*******************************
		 *	      GLOBALISE		*
		 *******************************/

%%	global_url(+URL, +Base, -Global) is det.
%
%	Translate a possibly relative URL  into   an  absolute  one.
%
%	@error syntax_error(illegal_url) if URL is not legal.

global_url(URL, BaseURL, Global) :-
	(   is_absolute_url(URL),
	    \+ sub_atom(URL, _, _, _, '%')	% may have escape, use general
	->  Global = URL
	;   sub_atom(URL, 0, _, _, '//')
	->  parse_url(BaseURL, [], Attributes),
	    memberchk(protocol(Proto), Attributes),
	    atomic_list_concat([Proto, :, URL], Global)
	;   sub_atom(URL, 0, _, _, #)
	->  (   sub_atom(BaseURL, _, _, 0, #)
	    ->	sub_atom(URL, 1, _, 0, NoHash),
		atom_concat(BaseURL, NoHash, Global)
	    ;	atom_concat(BaseURL, URL, Global)
	    )
	;   parse_url(URL, BaseURL, Attributes)
	->  phrase(curl(Attributes), Chars),
	    atom_codes(Global, Chars)
	;   throw(error(syntax_error(illegal_url), URL))
	).

%%	is_absolute_url(+URL)
%
%	True if URL is an absolute URL. That  is, a URL that starts with
%	a protocol identifier.

is_absolute_url(URL) :-
	sub_atom(URL, 0, _, _, 'http://'), !.
is_absolute_url(URL) :-
	sub_atom(URL, 0, _, _, 'https://'), !.
is_absolute_url(URL) :-
	sub_atom(URL, 0, _, _, 'ftp://'), !.
is_absolute_url(URL) :-
	sub_atom(URL, 0, _, _, 'file://'), !.
is_absolute_url(URL) :-
	atom_codes(URL, Codes),
	phrase(absolute_url, Codes, _), !.


		 /*******************************
		 *	  CREATE URL/URI	*
		 *******************************/

%%	http_location(?Parts, ?Location)
%
%	Construct or analyze an  HTTP  location.   This  is  similar  to
%	parse_url/2, but only deals with the   location  part of an HTTP
%	URL. That is, the path, search   and fragment specifiers. In the
%	HTTP protocol, the first line of a message is
%
%	    ==
%	    <Action> <Location> HTTP/<version>
%	    ==
%
%	@param Location	Atom or list of character codes.

http_location(Parts, Location) :-	% Parts --> Location
	nonvar(Parts), !,
	phrase(curi(Parts), String), !,
	atom_codes(Location, String).
http_location(Parts, Location) :-	% Location --> Parts
	atom(Location), !,
	atom_codes(Location, Codes),
	phrase(http_location(Parts), Codes).
http_location(Parts, Codes) :-		% LocationCodes --> Parts
	is_list(Codes),
	phrase(http_location(Parts), Codes).


curl(A) -->
	{ memberchk(protocol(Protocol), A)
	}, !,
	catomic(Protocol),
	":",
	curl(Protocol, A).
curl(A) -->
	curl(http, A).

curl(file, A) --> !,
	(   "//"
	->  cpath(A)
	;   cpath(A)
	).
curl(_, A) -->
	"//",
	cuser(A),
	chost(A),
	cport(A),
	cpath(A),
	csearch(A),
	cfragment(A).

curi(A) -->
	cpath(A),
	csearch(A).

cpath(A) -->
	(   { memberchk(path(Path), A) }
	->  { atom_codes(Path, Codes) },
	    www_encode(Codes, "/+:,")
	;   ""
	).

cuser(A) -->
	(   { memberchk(user(User), A) }
	->  { atom_codes(User, Codes) },
	    www_encode(Codes, ":"),
	    "@"
	;   ""
	).

chost(A) -->
	(   { memberchk(host(Host), A) }
	->  { atom_codes(Host, Codes) },
	    www_encode(Codes, "")
	;   ""
	).

cport(A) -->
	(   { memberchk(port(Port), A), Port \== 80 }
	->  { number_codes(Port, Codes) },
	    ":",
	    www_encode(Codes, "")
	;   ""
	).


catomic(A, In, Out) :-
	atom_codes(A, Codes),
	append(Codes, Out, In).

%%	csearch(+Attributes)//

csearch(A)-->
	(   { memberchk(search(Parameters), A) }
	->  csearch(Parameters, "?")
	;   []
	).

csearch([], _) -->
	[].
csearch([Parameter|Parameters], Sep) --> !,
	codes(Sep),
	cparam(Parameter),
	csearch(Parameters, "&").

cparam(Name=Value) --> !,
	cname(Name),
	"=",
	cvalue(Value).
cparam(NameValue) -->			% allow to feed Name(Value)
	{ compound(NameValue), !,
	  NameValue =.. [Name,Value]
	},
	cname(Name),
	"=",
	cvalue(Value).
cparam(Name)-->
	cname(Name).

codes([]) --> [].
codes([H|T]) --> [H], codes(T).

cname(Atom) -->
	{ atom_codes(Atom, Codes) },
	www_encode(Codes, "").

%%	cvalue(+Value)// is det.
%
%	Construct a string from  Value.  Value   is  either  atomic or a
%	code-list.

cvalue(Value) -->
	{ atomic(Value), !,
	  atom_codes(Value, Codes)
	},
	www_encode(Codes, "").
cvalue(Codes) -->
	{ must_be(codes, Codes)
	},
	www_encode(Codes, "").


%%	cfragment(+Attributes)//

cfragment(A) -->
	{ memberchk(fragment(Frag), A), !,
	  atom_codes(Frag, Codes)
	},
	"#",
	www_encode(Codes, "").
cfragment(_) -->
	"".


		 /*******************************
		 *	      PARSING		*
		 *******************************/

%%	parse_url(+URL, -Attributes) is det.
%
%	Construct or analyse a URL. URL is an   atom  holding a URL or a
%	variable. Parts is a list of   components.  Each component is of
%	the format Name(Value). Defined components are:
%
%	    * protocol(Protocol)
%	    The used protocol. This is, after  the optional =|url:|=, an
%	    identifier separated from the remainder of  the URL using :.
%	    parse_url/2 assumes the =http= protocol   if  no protocol is
%	    specified and the URL can be parsed  as a valid HTTP url. In
%	    addition to the RFC-1738  specified   protocols,  the =file=
%	    protocol is supported as well.
%
%	    * host(Host)
%	    Host-name or IP-address on which   the  resource is located.
%	    Supported by all network-based protocols.
%
%	    * port(Port)
%	    Integer port-number to access on   the \arg{Host}. This only
%	    appears if the port is  explicitly   specified  in  the URL.
%	    Implicit default ports (e.g.  80   for  HTTP)  do \emph{not}
%	    appear in the part-list.
%
%	    * path(Path)
%	    (File-) path addressed by the URL. This is supported for the
%	    =ftp=, =http= and =file= protocols. If  no path appears, the
%	    library generates the path =|/|=.
%
%	    * search(ListOfNameValue)
%	    Search-specification of HTTP URL. This is the part after the
%	    =|?|=, normally used to transfer data   from HTML forms that
%	    use the =GET=  protocol.  In  the   URL  it  consists  of  a
%	    www-form-encoded list of Name=Value pairs. This is mapped to
%	    a list of Prolog Name=Value  terms   with  decoded names and
%	    values.
%
%	    * fragment(Fragment)
%	    Fragment specification of HTTP URL. This   is the part after
%	    the =|#|= character.
%
%	The example below illustrates the all this for an HTTP URL.
%
%	    ==
%	    ?- parse_url('http://swi.psy.uva.nl/message.cgi?msg=Hello+World%21#x', P).
%
%	    P = [ protocol(http),
%		  host('swi.psy.uva.nl'),
%		  fragment(x),
%		  search([ msg = 'Hello World!'
%			 ]),
%		  path('/message.cgi')
%	        ]
%	    ==
%
%	By instantiating the parts-list this predicate   can  be used to
%	create a URL.

parse_url(URL, Attributes) :-
	nonvar(URL), !,
	atom_codes(URL, Codes),
	phrase(url(Attributes), Codes).
parse_url(URL, Attributes) :-
	phrase(curl(Attributes), Codes), !,
	atom_codes(URL, Codes).

%%	parse_url(+URL, +BaseURL, -Attributes) is det.
%
%	Similar to parse_url/2 for relative URLs.  If URL is relative,
%	it is resolved using the absolute URL BaseURL.

parse_url(URL, BaseURL, Attributes) :-
	nonvar(URL), !,
	atom_codes(URL, Codes),
	(   phrase(absolute_url, Codes, _)
	->  phrase(url(Attributes), Codes)
	;   (   atomic(BaseURL)
	    ->  parse_url(BaseURL, BaseA0)
	    ;	BaseA0 = BaseURL
	    ),
	    select(path(BasePath), BaseA0, BaseA1),
	    delete(BaseA1, search(_), BaseA2),
	    delete(BaseA2, fragment(_), BaseA3),
	    phrase(relative_uri(URIA0), Codes),
	    select(path(LocalPath), URIA0, URIA1), !,
	    globalise_path(LocalPath, BasePath, Path),
	    append(BaseA3, [path(Path)|URIA1], Attributes)
	).
parse_url(URL, BaseURL, Attributes) :-
	parse_url(BaseURL, BaseAttributes),
	memberchk(path(BasePath), BaseAttributes),
	(   memberchk(path(LocalPath), Attributes)
	->  globalise_path(LocalPath, BasePath, Path)
	;   Path = BasePath
	),
	append([path(Path)|Attributes], BaseAttributes, GlobalAttributes),
	phrase(curl(GlobalAttributes), Chars),
	atom_codes(URL, Chars).


%%	globalise_path(+LocalPath, +RelativeTo, -FullPath) is det.
%
%	The first clause deals with the  standard URL /... global paths.
%	The second with file://drive:path on MS-Windows.   This is a bit
%	of a cludge, but unfortunately common practice is -especially on
%	Windows- not always following the standard

globalise_path(LocalPath, _, LocalPath) :-
	sub_atom(LocalPath, 0, _, _, /), !.
globalise_path(LocalPath, _, LocalPath) :-
	is_absolute_file_name(LocalPath), !.
globalise_path(Local, Base, Path) :-
	base_dir(Base, BaseDir),
	make_path(BaseDir, Local, Path).

base_dir(BasePath, BaseDir) :-
	(   atom_concat(BaseDir, /, BasePath)
	->  true
	;   file_directory_name(BasePath, BaseDir)
	).

make_path(Dir, Local, Path) :-
	atom_concat('../', L2, Local),
	file_directory_name(Dir, Parent),
	Parent \== Dir, !,
	make_path(Parent, L2, Path).
make_path(/, Local, Path) :- !,
	atom_concat(/, Local, Path).
make_path(Dir, Local, Path) :-
	atomic_list_concat([Dir, /, Local], Path).


%%	absolute_url//
%
%	True if the input  describes  an   absolute  URL.  This means it
%	starts with a URL schema. We demand a   schema  of length > 1 to
%	avoid confusion with Windows drive letters.

absolute_url -->
	lwalpha(_First),
	schema_chars(Rest),
	{ Rest \== [] },
	":", !.


		 /*******************************
		 *	     SEQUENCES		*
		 *******************************/

digits(L) -->
	digits(L, []).

digits([C|T0], T) -->
	digit(C), !,
	digits(T0, T).
digits(T, T) -->
	[].


digit(C, [C|T], T) :- code_type(C, digit).

		 /*******************************
		 *	      RFC-3986		*
		 *******************************/

%%	uri(-Parts)//

url([protocol(Schema)|Parts]) -->
	schema(Schema),
	":", !,
	hier_part(Schema, Parts, P2),
	query(P2, P3),
	fragment(P3, []).
url([protocol(http)|Parts]) -->		% implicit HTTP
	authority(Parts, [path(Path)]),
	path_abempty(Path).

relative_uri(Parts) -->
	relative_part(Parts, P2),
	query(P2, P3),
	fragment(P3, []).

relative_part(Parts, Tail) -->
	"//", !,
	authority(Parts, [path(Path)|Tail]),
	path_abempty(Path).
relative_part([path(Path)|T], T) -->
	(   path_absolute(Path)
	;   path_noschema(Path)
	;   path_empty(Path)
	), !.

http_location([path(Path)|P2]) -->
	path_abempty(Path),
	query(P2, P3),
	fragment(P3, []).

%%	schema(-Atom)//
%
%	Schema  is  case-insensitive  and  the    canonical  version  is
%	lowercase.
%
%	==
%	Schema ::= ALPHA *(ALPHA|DIGIT|"+"|"-"|".")
%	==

schema(Schema) -->
	lwalpha(C0),
	schema_chars(Codes),
	{ atom_codes(Schema, [C0|Codes]) }.

schema_chars([H|T]) -->
	schema_char(H), !,
	schema_chars(T).
schema_chars([]) -->
	[].

schema_char(H) -->
	[C],
	{ C < 128,
	  (   code_type(C, alpha)
	  ->  code_type(H, to_lower(C))
	  ;   code_type(C, digit)
	  ->  H = C
	  ;   schema_extra(C)
	  ->  H = C
	  )
	}.

schema_extra(0'+).
schema_extra(0'-).
schema_extra(0'.).	% 0'


%%	hier_part(+Schema, -Parts, ?Tail)//

hier_part(file, [path(Path)|Tail], Tail) --> !,
	"//",
	(   win_drive_path(Path)
	;   path_absolute(Path)
	;   path_rootless(Path)
	;   path_empty(Path)
	), !.
hier_part(_, Parts, Tail) -->
	"//", !,
	authority(Parts, [path(Path)|Tail]),
	path_abempty(Path).
hier_part(_, [path(Path)|T], T) -->
	(   path_absolute(Path)
	;   path_rootless(Path)
	;   path_empty(Path)
	), !.

authority(Parts, Tail) -->
	user_info_chars(UserChars),
	"@", !,
	{ atom_codes(User, UserChars),
	  Parts = [user(User),host(Host)|T0]
	},
	host(Host),
	port(T0,Tail).
authority([host(Host)|T0], Tail) -->
	host(Host),
	port(T0, Tail).

user_info_chars([H|T]) -->
	user_info_char(H), !,
	user_info_chars(T).
user_info_chars([]) -->
	[].

user_info_char(_) --> "@", !, {fail}.
user_info_char(C) --> pchar(C).

%host(Host) --> ip_literal(Host), !.		% TBD: IP6 addresses
host(Host) --> ip4_address(Host), !.
host(Host) --> reg_name(Host).

ip4_address(Atom) -->
	i256_chars(Chars, [0'.|T0]),
	i256_chars(T0, [0'.|T1]),
	i256_chars(T1, [0'.|T2]),
	i256_chars(T2, []),
	{ atom_codes(Atom, Chars) }.

i256_chars(Chars, T) -->
	digits(Chars, T),
	\+ \+ { T = [],
		Chars \== [],
		number_codes(I, Chars),
		I < 256
	      }.

reg_name(Host) -->
	reg_name_chars(Chars),
	{ atom_codes(Host, Chars) }.

reg_name_chars([H|T]) -->
	reg_name_char(H), !,
	reg_name_chars(T).
reg_name_chars([]) -->
	[].

reg_name_char(C) -->
	pchar(C),
	{ C \== 0':,
	  C \== 0'@
	}.

port([port(Port)|T], T) -->
	":", !,
	digit(D0),
	digits(Ds),
	{ number_codes(Port, [D0|Ds]) }.
port(T, T) -->
	[].

path_abempty(Path) -->
	segments_chars(Chars, []),
	{   Chars == []
	->  Path = '/'
	;   atom_codes(Path, Chars)
	}.


win_drive_path(Path) -->
	drive_letter(C0),
	":",
	(   "/"
	->  {Codes = [C0, 0':, 0'/|Chars]}
	;   {Codes = [C0, 0':|Chars]}
	),
	segment_nz_chars(Chars, T0),
	segments_chars(T0, []),
	{ atom_codes(Path, Codes) }.


path_absolute(Path) -->
	"/",
	segment_nz_chars(Chars, T0),
	segments_chars(T0, []),
	{ atom_codes(Path, [0'/| Chars]) }.

path_noschema(Path) -->
	segment_nz_nc_chars(Chars, T0),
	segments_chars(T0, []),
	{ atom_codes(Path, Chars) }.

path_rootless(Path) -->
	segment_nz_chars(Chars, T0),
	segments_chars(T0, []),
	{ atom_codes(Path, Chars) }.

path_empty('/') -->
	"".

segments_chars([0'/|Chars], T) -->	% 0'
	"/", !,
	segment_chars(Chars, T0),
	segments_chars(T0, T).
segments_chars(T, T) -->
	[].

segment_chars([H|T0], T) -->
	pchar(H), !,
	segment_chars(T0, T).
segment_chars(T, T) -->
	[].

segment_nz_chars([H|T0], T) -->
	pchar(H),
	segment_chars(T0, T).

segment_nz_nc_chars([H|T0], T) -->
	segment_nz_nc_char(H), !,
	segment_nz_nc_chars(T0, T).
segment_nz_nc_chars(T, T) -->
	[].

segment_nz_nc_char(_) --> ":", !, {fail}.
segment_nz_nc_char(C) --> pchar(C).


%%	query(-Parts, ?Tail)// is det.
%
%	Extract &Name=Value, ...

query([search(Params)|T], T) -->
	"?", !,
	search(Params).
query(T,T) -->
	[].

search([Parameter|Parameters])-->
	parameter(Parameter), !,
	(   search_sep
        ->  search(Parameters)
        ;   { Parameters = [] }
        ).
search([]) -->
	[].

parameter(Param)--> !,
	search_chars(NameS),
	{ atom_codes(Name, NameS)
	},
	(   "="
        ->  search_value_chars(ValueS),
	    { atom_codes(Value, ValueS),
	      Param = (Name = Value)
	    }
        ;   { Param = Name
	    }
        ).

search_chars([C|T]) -->
	search_char(C), !,
	search_chars(T).
search_chars([]) -->
	[].

search_char(_) --> search_sep, !, { fail }.
search_char(_) --> "=", !, { fail }.
search_char(C) --> fragment_char(C).

search_value_chars([C|T]) -->
	search_value_char(C), !,
	search_value_chars(T).
search_value_chars([]) -->
	[].

search_value_char(_) --> search_sep, !, { fail }.
search_value_char(C) --> fragment_char(C).

%%	search_sep// is semidet.
%
%	Matches a search-parameter separator.  Traditonally, this is the
%	&-char, but these days there are `newstyle' ;-char separators.
%
%	@see http://perldoc.perl.org/CGI.html
%	@tbd This should be configurable

search_sep --> "&", !.
search_sep --> ";".


%%	fragment(-Fragment, ?Tail)//
%
%	Extract the fragment (after the =#=)

fragment([fragment(Fragment)|T], T) -->
	"#", !,
	fragment_chars(Codes),
	{ atom_codes(Fragment, Codes) }.
fragment(T, T) -->
	[].

fragment_chars([H|T]) -->
	fragment_char(H), !,
	fragment_chars(T).
fragment_chars([]) -->
	[].


%%	fragment_char(-Char)
%
%	Find a fragment character.

fragment_char(C)   --> pchar(C), !.
fragment_char(0'/) --> "/", !.
fragment_char(0'?) --> "?", !.
fragment_char(0'[) --> "[", !.		% Not according RDF3986!
fragment_char(0']) --> "]", !.


		 /*******************************
		 *	CHARACTER CLASSES	*
		 *******************************/

%%	pchar(-Code)//
%
%	unreserved|pct_encoded|sub_delim|":"|"@"
%
%	Performs UTF-8 decoding of percent encoded strings.

pchar(0' ) --> "+", !.			%' ?
pchar(C) -->
	[C],
	{   unreserved(C)
	;   sub_delim(C)
	;   C == 0':
        ;   C == 0'@
	}, !.
pchar(C) -->
	percent_coded(C).

%%	lwalpha(-C)//
%
%	Demand alpha, return as lowercase

lwalpha(H) -->
	[C],
	{ C < 128,
	  code_type(C, alpha),
	  code_type(H, to_lower(C))
	}.

drive_letter(C) -->
	[C],
	{ C < 128,
	  code_type(C, alpha)
	}.


		 /*******************************
		 *	RESERVED CHARACTERS	*
		 *******************************/

%%	sub_delim(?Code)
%
%	Sub-delimiters

sub_delim(0'!).
sub_delim(0'$).
sub_delim(0'&).
sub_delim(0'').
sub_delim(0'().
sub_delim(0')).
sub_delim(0'*).
sub_delim(0'+).
sub_delim(0',).
sub_delim(0';).
sub_delim(0'=).


%%	unreserved(+C)
%
%	Characters that can be represented without procent escaping
%	RFC 3986, section 2.3

term_expansion(unreserved(map), Clauses) :-
	findall(unreserved(C), unreserved_(C), Clauses).

unreserved_(C) :-
	between(1, 128, C),
	code_type(C, alnum).
unreserved_(0'-).
unreserved_(0'.).
unreserved_(0'_).
unreserved_(0'~).			% 0'

unreserved(map).			% Expanded


		 /*******************************
		 *	        FORMS		*
		 *******************************/

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Encoding/decoding of form-fields  using   the  popular  www-form-encoded
encoding used with the HTTP GET.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

%%	www_form_encode(+Value, -XWWWFormEncoded) is det.
%%	www_form_encode(-Value, +XWWWFormEncoded) is det.
%
%	En/Decode        between        native          value        and
%	application/x-www-form-encoded. Maps space to   +,  keeps alnum,
%	maps anything else to =|%XX|= and   newlines to =|%OD%OA|=. When
%	decoding, newlines appear as a single newline (10) character.

www_form_encode(Value, Encoded) :-
	atomic(Value), !,
	atom_codes(Value, Codes),
	phrase(www_encode(Codes, ""), EncCodes),
	atom_codes(Encoded, EncCodes).
www_form_encode(Value, Encoded) :-
	atom_codes(Encoded, EncCodes),
	phrase(www_decode(Codes), EncCodes),
	atom_codes(Value, Codes).

%%	www_encode(+Codes, +ExtraUnescaped)//

www_encode([0'\r, 0'\n|T], Extra) --> !,
	"%0D%0A",
	www_encode(T, Extra).
www_encode([0'\n|T], Extra) --> !,
	"%0D%0A",
	www_encode(T, Extra).
www_encode([H|T], Extra) -->
	percent_encode(H, Extra),
	www_encode(T, Extra).
www_encode([], _) -->
	"".

percent_encode(C, _Extra) -->
	{ unreserved(C) }, !,
	[C].
percent_encode(C, Extra) -->
	{ memberchk(C, Extra) }, !,
	[C].
%percent_encode(0' , _) --> !, "+".	% Deprecated: use %20
percent_encode(C, _) -->
	{ C =< 127 }, !,
	percent_byte(C).
percent_encode(C, _) -->		% Unicode characters
	{ current_prolog_flag(url_encoding, utf8), !,
	  phrase(utf8_codes([C]), Bytes)
	},
	percent_bytes(Bytes).
percent_encode(C, _) -->
	{ C =< 255 }, !,
	percent_byte(C).
percent_encode(_C, _) -->
	{ representation_error(url_character)
	}.

percent_bytes([]) -->
	"".
percent_bytes([H|T]) -->
	percent_byte(H),
	percent_bytes(T).

percent_byte(C) -->
	[0'%, D1, D2],
	{   nonvar(C)
	->  Dv1 is (C>>4 /\ 0xf),
	    Dv2 is (C /\ 0xf),
	    code_type(D1, xdigit(Dv1)),
	    code_type(D2, xdigit(Dv2))
	;   code_type(D1, xdigit(Dv1)),
	    code_type(D2, xdigit(Dv2)),
	    C is ((Dv1)<<4) + Dv2
	}.

percent_coded(C) -->
	percent_byte(C0), !,
	(   { C0 == 13			% %0D%0A --> \n
	    },
	    "%0",
	    ( "A" ; "a" )
	->  { C = 10
	    }
	;   { C0 >= 0xc0 },		% UTF-8 lead-in
	    utf8_cont(Cs),
	    { phrase(utf8_codes([C]), [C0|Cs]) }
	->  []
	;   { C = C0
	    }
	).

%%	www_decode(-Codes)//

www_decode([0' |T]) -->
	"+", !,
        www_decode(T).
www_decode([C|T]) -->
	percent_coded(C), !,
	www_decode(T).
www_decode([C|T]) -->
	[C], !,
	www_decode(T).
www_decode([]) -->
	[].

utf8_cont([H|T]) -->
	percent_byte(H),
	{ between(0x80, 0xbf, H) }, !,
	utf8_cont(T).
utf8_cont([]) -->
	[].


%%	set_url_encoding(?Old, +New) is semidet.
%
%	Query and set the encoding for URLs.  The default is =utf8=.
%	The only other defined value is =iso_latin_1=.
%
%	@tbd	Having a global flag is highly inconvenient, but a
%		work-around for old sites using ISO Latin 1 encoding.

:- create_prolog_flag(url_encoding, utf8, [type(atom)]).

set_url_encoding(Old, New) :-
	current_prolog_flag(url_encoding, Old),
	(   Old == New
	->  true
	;   must_be(oneof([utf8, iso_latin_1]), New),
	    set_prolog_flag(url_encoding, New)
	).


		 /*******************************
		 *	 IRI PROCESSING		*
		 *******************************/

%%	url_iri(+Encoded, -Decoded) is det.
%%	url_iri(-Encoded, +Decoded) is det.
%
%	Convert between a URL, encoding in US-ASCII   and an IRI. An IRI
%	is a fully expanded Unicode string.   Unicode  strings are first
%	encoded into UTF-8, after which %-encoding takes place.

url_iri(Encoded, Decoded) :-
	nonvar(Encoded), !,
	(   sub_atom(Encoded, _, _, _, '%')
	->  atom_codes(Encoded, Codes),
	    unescape_precent(Codes, UTF8),
	    phrase(utf8_codes(Unicodes), UTF8),
	    atom_codes(Decoded, Unicodes)
	;   Decoded = Encoded
	).
url_iri(URL, IRI) :-
	atom_codes(IRI, IRICodes),
	phrase(percent_encode(IRICodes, "/:?#&="), UrlCodes),
	atom_codes(URL, UrlCodes).


unescape_precent([], []).
unescape_precent([0'%,C1,C2|T0], [H|T]) :- !,	%'
	code_type(C1, xdigit(D1)),
	code_type(C2, xdigit(D2)),
	H is D1*16 + D2,
	unescape_precent(T0, T).
unescape_precent([H|T0], [H|T]) :-
	unescape_precent(T0, T).


		 /*******************************
		 *	     FORM DATA		*
		 *******************************/

%%	parse_url_search(?Spec, ?Fields:list(Name=Value)) is det.
%
%	Construct or analyze an HTTP   search  specification. This deals
%	with       form       data       using       the       MIME-type
%	=application/x-www-form-urlencoded=  as  used   in    HTTP   GET
%	requests.

parse_url_search(Spec, Fields) :-
	atomic(Spec), !,
	atom_codes(Spec, Codes),
	phrase(search(Fields), Codes).
parse_url_search(Codes, Fields) :-
	is_list(Codes), !,
	phrase(search(Fields), Codes).
parse_url_search(Codes, Fields) :-
	must_be(list, Fields),
	phrase(csearch(Fields, ""), Codes).


		 /*******************************
		 *	    FILE URLs		*
		 *******************************/

%%	file_name_to_url(+File, -URL) is det.
%%	file_name_to_url(-File, +URL) is semidet.
%
%	Translate between a filename and a file:// URL.
%
%	@tbd	Current implementation does not deal with paths that
%		need special encoding.

file_name_to_url(File, FileURL) :-
	nonvar(File), !,
	absolute_file_name(File, Path),
	atom_concat('file://', Path, FileURL), !.
file_name_to_url(File, FileURL) :-
	atom_concat('file://', File, FileURL), !.

