View source with formatted comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        J.Wielemaker@cs.vu.nl
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (C): 2009-2024, VU University Amsterdam
    7			      CWI, Amsterdam
    8			      SWI-Prolog Solutions b.v.
    9
   10    This program is free software; you can redistribute it and/or
   11    modify it under the terms of the GNU General Public License
   12    as published by the Free Software Foundation; either version 2
   13    of the License, or (at your option) any later version.
   14
   15    This program is distributed in the hope that it will be useful,
   16    but WITHOUT ANY WARRANTY; without even the implied warranty of
   17    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   18    GNU General Public License for more details.
   19
   20    You should have received a copy of the GNU General Public
   21    License along with this library; if not, write to the Free Software
   22    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   23
   24    As a special exception, if you link this library with other files,
   25    compiled with a Free Software compiler, to produce an executable, this
   26    library does not by itself cause the resulting executable to be covered
   27    by the GNU General Public License. This exception does not however
   28    invalidate any other reasons why the executable file might be covered by
   29    the GNU General Public License.
   30*/
   31
   32:- module(plweb_download, []).   33:- use_module(library(http/html_write)).   34:- use_module(library(http/js_write)).   35:- use_module(library(http/http_dispatch)).   36:- use_module(library(http/http_path)).   37:- use_module(library(http/http_parameters)).   38:- use_module(library(http/http_dirindex)).   39:- use_module(library(http/http_wrapper)).   40:- use_module(library(http/http_cors)).   41:- use_module(library(dcg/basics)).   42:- use_module(library(broadcast)).   43:- use_module(library(pairs)).   44:- use_module(library(lists)).   45:- use_module(library(apply)).   46:- use_module(library(error)).   47:- use_module(library(filesex)).   48:- use_module(library(persistency)).   49:- use_module(library(crypto)).   50:- use_module(library(random)).   51:- use_module(library(debug)).   52:- use_module(wiki).   53
   54%%	download(+Request) is det.
   55%
   56%	HTTP handler for SWI-Prolog download pages.
   57
   58:- http_handler(download(devel),        download_table, []).   59:- http_handler(download(stable),       download_table, []).   60:- http_handler(download(old),          download_table, []).   61:- http_handler(download('daily/bin/'), download_daily, []).   62:- http_handler(download(.),	        download,
   63		[prefix, spawn(download), priority(10)]).   64:- http_handler(root(download),	        http_redirect(moved, download(.)),
   65		[priority(10)]).   66
   67%%	download_table(+Request)
   68%
   69%	Provide a table with possible download targets.
   70%	test edit
   71
   72download_table(Request) :-
   73	http_parameters(Request,
   74			[ show(Show, [oneof([all,latest]), default(latest)])
   75			]),
   76	memberchk(path(Path), Request),
   77	http_absolute_location(root(download), DownLoadRoot, []),
   78	atom_concat(DownLoadRoot, DownLoadDir, Path),
   79	absolute_file_name(download(DownLoadDir),
   80			   Dir,
   81			   [ file_type(directory),
   82			     access(read)
   83			   ]),
   84	list_downloads(Dir, [show(Show), request(Request)]).
   85
   86%%	list_downloads(+Directory)
   87
   88list_downloads(Dir, Options) :-
   89	(   wiki_file_to_dom(Dir, 'header.txt', Header0)
   90	->  (   Header0 = [h1(_, Title)|Header]
   91	    ->	true
   92	    ;	Header = Header0
   93	    )
   94	;   Header = []
   95	),
   96	(   var(Title)
   97	->  Title = 'SWI-Prolog downloads'
   98	;   true
   99	),
  100	reply_html_page(
  101	    download(Dir, Title),
  102	    title('SWI-Prolog downloads'),
  103	    [ \html(Header),
  104	      br(clear(all)),
  105	      table(class(downloads),
  106		    \download_table(Dir, Options)),
  107	      \machine_download_text,
  108	      \wiki(Dir, 'footer.txt')
  109	    ]).
  110
  111wiki_file_to_dom(Dir, File, DOM) :-
  112	directory_file_path(Dir, File, WikiFile),
  113	access_file(WikiFile, read), !,
  114	wiki_file_to_dom(WikiFile, DOM).
  115
  116wiki(Dir, File) -->
  117	{ wiki_file_to_dom(Dir, File, DOM) }, !,
  118	html(DOM).
  119wiki(_, _) -->
  120	[].
  121
  122machine_download_text -->
  123	html({|html||
  124<div class="machine-download">
  125Install scripts may download the SHA256 checksum by appending
  126<code>.sha256</code> to the file name.  Scripts can download
  127the latest version by replacing the version of the file with
  128<code>latest</code>.  This causes the server to reply with the
  129location of the latest version using an
  130<code>HTTP 303 See Other</code> message.
  131</div>
  132	     |}).
  133
  134
  135download_table(Dir, Options) -->
  136	list_files(Dir, bin, bin,    'Binaries',         Options),
  137	list_files(Dir, src, src,    'Sources',          Options),
  138	list_files(Dir, doc, doc,    'Documentation',    Options),
  139	toggle_show(Options).
  140
  141%%	toggle_show(+Options) is det.
  142%
  143%	Add a toggle to switch between   showing only the latest version
  144%	and all versions.
  145
  146toggle_show(Options) -->
  147	{ option(request(Request), Options),
  148	  memberchk(path(Path), Request), !,
  149	  file_base_name(Path, MySelf),
  150	  (   option(show(all), Options)
  151	  ->  NewShow = latest
  152	  ;   NewShow = all
  153	  )
  154	},
  155	html(tr(td([class(toggle), colspan(3)],
  156		   a(href(MySelf+'?show='+NewShow),
  157		     [ 'Show ', NewShow, ' files' ])))).
  158toggle_show(_) -->
  159	[].
  160
  161%%	list_files(+Dir, +SubDir, +Class, +Label, +Options) is det.
  162%
  163%	Create table rows for all  files   in  Dir/SubDir.  If files are
  164%	present, emit a =tr= with Label  and   a  =tr= row for each each
  165%	matching file.  Options are:
  166%
  167%	    * show(Show)
  168%	    One of =all= or =latest= (default).
  169
  170list_files(Dir, SubDir, Class, Label, Options) -->
  171	{ directory_file_path(Dir, SubDir, Directory),
  172	  download_files(Directory, Class, Files, Options),
  173	  Files \== []
  174	},
  175	html(tr(th(colspan(3), Label))),
  176	list_files(Files).
  177list_files(_, _, _, _, _) -->
  178	[].
  179
  180list_files([]) --> [].
  181list_files([H|T]) -->
  182	list_file(H),
  183	list_files(T).
  184
  185list_file(File) -->
  186	html(tr(class(download),
  187		[ td(class(dl_icon), \file_icon(File)),
  188		  td(class(dl_size), \file_size(File)),
  189		  td(class(dl_file), \file_description(File))
  190		])).
  191
  192file_icon(file(Type, PlatForm, _, _, _)) -->
  193	{ icon_for_file(Type, PlatForm, Icon, Alt), !,
  194	  http_absolute_location(icons(Icon), HREF, [])
  195	},
  196	html(img([src(HREF), alt(Alt)])).
  197file_icon(_) -->
  198	html(?).			% no defined icon
  199
  200icon_for_file(bin, linux(universal),
  201	      'linux.png', 'Linux 32/64 intel').
  202icon_for_file(bin, linux(_,_),
  203	      'linux32.gif', 'Linux RPM').
  204icon_for_file(bin, macos(lion,_),
  205	      'lion.png', 'Lion').
  206icon_for_file(bin, macos(snow_leopard,_),
  207	      'snowleopard.gif', 'Snow Leopard').
  208icon_for_file(bin, macos(snow_leopard_and_later,_),
  209	      'macapp.png', 'Snow Leopard and later').
  210icon_for_file(bin, macos(bundle,_),
  211	      'macapp.png', 'MacOS bundle').
  212icon_for_file(bin, macos(_,_),
  213	      'mac.gif', 'MacOSX version').
  214icon_for_file(_, windows(win32),
  215	      'win32.gif', 'Windows version (32-bits)').
  216icon_for_file(_, windows(win64),
  217	      'win64.gif', 'Windows version (64-bits)').
  218icon_for_file(src, _,
  219	      'src.gif', 'Source archive').
  220icon_for_file(_, pdf,
  221	      'pdf.gif', 'PDF file').
  222
  223
  224file_size(file(_, _, _, _, Path)) -->
  225	{ size_file(Path, Bytes)
  226	},
  227	html('~D bytes'-[Bytes]).
  228
  229file_description(file(bin, PlatForm, Version, _, Path)) -->
  230	{ down_file_href(Path, HREF)
  231	},
  232	html([ a(href(HREF),
  233		 [ 'SWI-Prolog ', \version(Version), ' for ',
  234		   \platform(PlatForm)
  235		 ]),
  236	       \platform_notes(PlatForm, Path),
  237	       \checksum(Path)
  238	     ]).
  239file_description(file(src, Format, Version, _, Path)) -->
  240	{ down_file_href(Path, HREF)
  241	},
  242	html([ a(href(HREF),
  243		 [ 'SWI-Prolog source for ', \version(Version)
  244		 ]),
  245	       \platform_notes(Format, Path),
  246	       \checksum(Path)
  247	     ]).
  248file_description(file(doc, Format, Version, _, Path)) -->
  249	{ down_file_href(Path, HREF)
  250	},
  251	html([ a(href(HREF),
  252		 [ 'SWI-Prolog ', \version(Version),
  253		   ' reference manual in PDF'
  254		 ]),
  255	       \platform_notes(Format, Path)
  256	     ]).
  257file_description(file(pkg(Pkg), PlatForm, Version, _, Path)) -->
  258	{ down_file_href(Path, HREF)
  259	},
  260	html([ a(href(HREF),
  261		 [ \package(Pkg), ' (version ', \version(Version), ') for ',
  262		   \platform(PlatForm)
  263		 ]),
  264	       \platform_notes(pkg(Pkg), Path)
  265	     ]).
  266
  267package(Name) -->
  268	html([ 'Package ', Name ]).
  269
  270version(version(Major, Minor, Patch, '')) --> !,
  271	html(b('~w.~w.~w'-[Major, Minor, Patch])).
  272version(version(Major, Minor, Patch, Tag)) -->
  273	html(b('~w.~w.~w-~w'-[Major, Minor, Patch, Tag])).
  274
  275checksum(Path) -->
  276	{ file_checksum(Path, SHA256) },
  277	html(div([ class(checksum),
  278		   title('You can use the checksum to verify the integrity \c
  279		          of the downloaded file.  It provides some protection \c
  280			  against deliberate tamporing with the file.')
  281		 ],
  282		 [ span(class('checkum-header'), 'SHA256'), :,
  283		   span(class([checksum,sha256]), SHA256)
  284		 ])).
  285
  286down_file_href(Path, HREF) :-
  287	absolute_file_name(download(.),
  288			   Dir,
  289			   [ file_type(directory),
  290			     access(read)
  291			   ]),
  292	atom_concat(Dir, SlashLocal, Path),
  293	delete_leading_slash(SlashLocal, Local),
  294	add_envelope(Local, SafeLocal),
  295	http_absolute_location(download(SafeLocal), HREF, []).
  296
  297delete_leading_slash(SlashPath, Path) :-
  298	atom_concat(/, Path, SlashPath), !.
  299delete_leading_slash(Path, Path).
  300
  301platform(linux(universal)) -->
  302	html(['Linux 32/64 bits (TAR)']).
  303platform(linux(rpm, _)) -->
  304	html(['i586/Linux (RPM)']).
  305platform(macos(Name, CPU)) -->
  306	html(['MacOSX ', \html_macos_version(Name, CPU), ' on ', \cpu(CPU)]).
  307platform(windows(win32)) -->
  308	html(['Microsoft Windows (32 bit)']).
  309platform(windows(win64)) -->
  310	html(['Microsoft Windows (64 bit)']).
  311
  312html_macos_version(tiger, _)        --> html('10.4 (Tiger)').
  313html_macos_version(leopard, _)      --> html('10.5 (Leopard)').
  314html_macos_version(snow_leopard, _) --> html('10.6 (Snow Leopard)').
  315html_macos_version(lion, _)	    --> html('10.7 (Lion)').
  316html_macos_version(snow_leopard_and_later, _)
  317				    --> html('10.6 (Snow Leopard) and later').
  318html_macos_version(bundle, x86_64)  --> html('10.12 (Sierra) and later').
  319html_macos_version(bundle, fat)     --> html('10.14 (Mojave) and later').
  320html_macos_version(OS, _CPU)	    --> html(OS).
  321
  322cpu(fat) --> !, html("x86_64 and arm64").
  323cpu(CPU) --> html(CPU).
  324
  325
  326%%	platform_notes(+Platform, +Path) is det.
  327%
  328%	Include notes on the platform. These notes  are stored in a wiki
  329%	file in the same directory as the download file.
  330
  331platform_notes(Platform, Path) -->
  332	{ file_directory_name(Path, Dir),
  333	  platform_note_file(Platform, File),
  334	  atomic_list_concat([Dir, /, File], NoteFile),
  335	  debug(download, 'Trying note-file ~q', [NoteFile]),
  336	  access_file(NoteFile, read), !,
  337	  debug(download, 'Found note-file ~q', [NoteFile]),
  338	  wiki_file_to_dom(NoteFile, DOM)
  339	},
  340	html(DOM).
  341platform_notes(_, _) -->
  342	[].
  343
  344platform_note_file(linux(rpm,_),     'linux-rpm.txt').
  345platform_note_file(linux(universal), 'linux.txt').
  346platform_note_file(windows(win32),   'win32.txt').
  347platform_note_file(windows(win64),   'win64.txt').
  348platform_note_file(pkg(Pkg),         File) :-
  349	file_name_extension(Pkg, txt, File).
  350platform_note_file(macos(Version,fat), File) :-
  351	atomic_list_concat([macosx, -, fat, -, Version, '.txt'], File).
  352platform_note_file(macos(Version,_), File) :-
  353	atomic_list_concat([macosx, -, Version, '.txt'], File).
  354platform_note_file(macos(_,_),	     'macosx.txt').
  355platform_note_file(tgz,		     'src-tgz.txt').
  356platform_note_file(pdf,		     'doc-pdf.txt').
  357
  358
  359		 /*******************************
  360		 *	   CLASSIFY FILES	*
  361		 *******************************/
  362
  363%%	download_files(+Dir, +Class, -Files, +Options)
  364%
  365%	Files is a list of  files  that   satisfy  Class  and Options in
  366%	Dir/Subdir.
  367
  368:- dynamic
  369	download_cache/6.  % Hash, Dir, Class, Opts, Time, Files
  370
  371download_files(Dir, Class, Files, Options0) :-
  372	exists_directory(Dir), !,
  373	include(download_option, Options0, Options),
  374	term_hash(ci(Dir,Class,Options), Hash),
  375	time_file(Dir, DirTime),
  376	(   download_cache(Hash, Dir, Class, Options, Time, Files0),
  377	    (	DirTime == Time
  378	    ->	true
  379	    ;	retractall(download_cache(Hash, Dir, Class, Options, _, _)),
  380		fail
  381	    )
  382	->  true
  383	;   download_files_nc(Dir, Class, Files0, Options),
  384	    asserta(download_cache(Hash, Dir, Class, Options, DirTime, Files0))
  385	),
  386	Files = Files0.
  387download_files(_, _, [], _).
  388
  389clear_download_cache :-
  390	retractall(download_cache(_Hash, _Dir, _Class, _Options, _Time, _Files0)).
  391
  392download_option(show(_)).
  393
  394
  395download_files_nc(Directory, Class, Sorted, Options) :-
  396	atom_concat(Directory, '/*', Pattern),
  397	expand_file_name(Pattern, Files),
  398	classify_files(Files, Class, Classified, Options),
  399	sort_files(Classified, Sorted, Options).
  400
  401classify_files([], _, [], _).
  402classify_files([H0|T0], Class, [H|T], Options) :-
  403	classify_file(H0, H, Options),
  404	arg(1, H, Classification),
  405	subsumes_term(Class, Classification), !,
  406	classify_files(T0, Class, T, Options).
  407classify_files([_|T0], Class, T, Options) :-
  408	classify_files(T0, Class, T, Options).
  409
  410%%	classify_file(+Path, -Term, +Options) is semidet.
  411
  412classify_file(Path, file(Type, Platform, Version, Name, Path), Options) :-
  413	file_base_name(Path, Name),
  414	atom_codes(Name, Codes),
  415	phrase(file(Type, Platform, Version, Options), Codes).
  416
  417file(bin, macos(OSVersion, CPU), Version, Options) -->
  418	{ option(show(all), Options) },
  419	"swi-prolog-", opt_devel, long_version(Version), "-",
  420	macos_version(OSVersion),
  421	(   "-",
  422	    macos_cpu(CPU)
  423	->  ""
  424	;   { macos_def_cpu(OSVersion, CPU) }
  425	),
  426	".mpkg.zip", !.
  427% Cmake version
  428file(bin, macos(bundle, intel), Version, _) -->
  429	"swipl-", long_version(Version), opt_release(_),
  430	opt_cpu(_),
  431	".dmg", !.
  432file(bin, macos(bundle, fat), Version, _) -->
  433	"swipl-", long_version(Version), opt_release(_),
  434	".fat.dmg", !.
  435file(bin, macos(snow_leopard_and_later, intel), Version, _) -->
  436	"SWI-Prolog-", long_version(Version),
  437	".dmg", !.
  438file(bin, windows(WinType), Version, _) -->
  439	"swipl-", long_version(Version), opt_release(_),
  440	cmake_win_type(WinType),
  441	".exe", !.
  442file(bin, windows(WinType), Version, _) -->
  443	win_type(WinType), "pl",
  444	short_version(Version),
  445	".exe", !.
  446file(bin, windows(WinType), Version, _) -->
  447	swipl, win_type(WinType), "-",
  448	short_version(Version),
  449	".exe", !.
  450file(bin, linux(rpm, suse), Version, _) -->
  451	swipl, long_version(Version), "-", digits(_Build), ".i586.rpm", !.
  452file(bin, linux(universal), Version, _) -->
  453	"swipl-",
  454	long_version(Version), "-", "linux",
  455	".tar.gz", !.
  456file(src, tgz, Version, _) -->
  457	swipl, long_version(Version), ".tar.gz", !.
  458file(doc, pdf, Version, _) -->
  459	"SWI-Prolog-", long_version(Version), ".pdf", !.
  460
  461swipl --> "swipl-", !.
  462swipl --> "pl-".
  463
  464opt_release(Rel) --> "-", int(Rel, 4), !.
  465opt_release(-)   --> "".
  466
  467opt_devel --> "devel-", !.
  468opt_devel --> "".
  469
  470opt_cpu(x86_64) --> ".", "x86_64", !.
  471opt_cpu(unknown) --> "".
  472
  473macos_version(tiger)        --> "tiger".
  474macos_version(leopard)      --> "leopard".
  475macos_version(snow_leopard) --> "snow-leopard".
  476macos_version(lion)         --> "lion".
  477
  478macos_cpu(ppc)   --> "powerpc".
  479macos_cpu(intel) --> "intel".
  480macos_cpu(x86)   --> "32bit".
  481
  482macos_def_cpu(snow_leopard, intel) :- !.
  483macos_def_cpu(lion, intel) :- !.
  484macos_def_cpu(_, ppc).
  485
  486win_type(win32) --> "w32".
  487win_type(win64) --> "w64".
  488
  489cmake_win_type(win64) --> ".", "x64".
  490cmake_win_type(win32) --> ".", "x86".
  491
  492long_version(version(Major, Minor, Patch, Tag)) -->
  493	int(Major, 1), ".", int(Minor, 2), ".", int(Patch, 2), !,
  494        tag(Tag), !.
  495long_version(latest) -->
  496	"latest".
  497
  498tag(Tag) -->
  499	"-", alnums(Codes), !,
  500        { atom_codes(Tag, Codes) }.
  501tag('') --> "".
  502
  503int(Value, MaxDigits) -->
  504	digits(Digits),
  505	{ length(Digits, Len),
  506	  Len =< MaxDigits,
  507	  Len > 0,
  508	  number_codes(Value, Digits)
  509	}.
  510
  511alnums([H|T]) -->
  512	[H], { code_type(H, alnum) }, !,
  513        alnums(T).
  514alnums([]) --> "".
  515
  516short_version(version(Major, Minor, Patch, Tag)) -->
  517	digits(Digits),
  518	{   Digits = [D1,D2,D3]
  519	->  number_codes(Major, [D1]),
  520	    number_codes(Minor, [D2]),
  521	    number_codes(Patch, [D3])
  522	;   Digits = [D1,D2,D3,D4]
  523	->  (   number_codes(51, [D1,D2])		% 5.1X.Y
  524	    ->  number_codes(Major, [D1]),
  525	        number_codes(Minor, [D2,D3]),
  526		number_codes(Patch, [D4])
  527	    ;   number_codes(Major, [D1]),
  528	        number_codes(Minor, [D2]),
  529		number_codes(Patch, [D3,D4])
  530	    )
  531	;   Digits = [D1,D2,D3,D4,D5]
  532	->  number_codes(Major, [D1]),
  533	    number_codes(Minor, [D2,D3]),
  534	    number_codes(Patch, [D4,D5])
  535	},
  536        tag(Tag), !.
  537short_version(latest) -->
  538	"latest".
  539
  540%%	sort_files(+In, -Out, +Options)
  541%
  542%	Sort files by type and version. Type: linux, windows, mac, src,
  543%	doc.  Versions: latest first.
  544%
  545%	Options:
  546%
  547%	    * show(Show)
  548%	    One of =all= or =latest=.
  549
  550sort_files(In, Out, Options) :-
  551	map_list_to_pairs(map_type, In, Typed0),
  552	(   option(show(all), Options)
  553	->  Typed = Typed0
  554	;   exclude(old_tagged_file, Typed0, Typed)
  555	),
  556	keysort(Typed, TSorted),
  557	group_pairs_by_key(TSorted, TGrouped),
  558	maplist(sort_group_by_version, TGrouped, TGroupSorted),
  559	(   option(show(all), Options)
  560	->  pairs_values(TGroupSorted, TValues),
  561	    flatten(TValues, Out)
  562	;   take_latest(TGroupSorted, Out)
  563	).
  564
  565map_type(File, Tag) :-
  566	File = file(Type, Platform, _Version, _Name, _Path),
  567	type_tag(Type, Platform, Tag).
  568
  569type_tag(bin, linux(A),   tag(10, linux(A))) :- !.
  570type_tag(bin, linux(A,B), tag(11, linux(A,B))) :- !.
  571type_tag(bin, windows(A), tag(Tg, windows(A))) :- !,
  572	win_tag(A, Tg2),
  573        Tg is 20+Tg2.
  574type_tag(bin, macos(A,B), tag(Tg, macos(A,B))) :- !,
  575	mac_tag(A, Tg2),
  576	Tg is 30+Tg2.
  577type_tag(src, Format,     tag(40, Format)) :- !.
  578type_tag(doc, Format,     tag(50, Format)) :- !.
  579type_tag(X,   Y,	  tag(60, X-Y)).
  580
  581mac_tag(bundle,			4).
  582mac_tag(snow_leopard_and_later,	5).
  583mac_tag(lion,			6).
  584mac_tag(snow_leopard,		7).
  585mac_tag(leopard,		8).
  586mac_tag(tiger,			9).
  587
  588win_tag(win64, 1).
  589win_tag(win32, 2).
  590
  591sort_group_by_version(Tag-Files, Tag-Sorted) :-
  592	map_list_to_pairs(tag_version, Files, TFiles),
  593	keysort(TFiles, TRevSorted),
  594	pairs_values(TRevSorted, RevSorted),
  595	reverse(RevSorted, Sorted).
  596
  597tag_version(File, Tag) :-
  598	File = file(_,_,Version,_,_),
  599	version_tag(Version, Tag).
  600
  601version_tag(version(Major, Minor, Patch, Tag),
  602	    version(Major, Minor, Patch, Order)) :-
  603	(   pre_version(Tag, Order)
  604	->  true
  605	;   print_message(error,
  606			  error(domain_error(pre_release_version, Tag),_)),
  607	    Order = pre(-100, 0)
  608	).
  609
  610pre_version('', pre(0, 0)) :- !.
  611pre_version(NrA, pre(0, 0)) :-
  612	atom_number(NrA, _Nr), !.
  613pre_version(Tag, pre(TagOrder, N)) :-
  614	tag(TagPrefix, TagOrder),
  615	atom_concat(TagPrefix, NA, Tag),
  616	atom_number(NA, N).
  617
  618tag(rc,    -1).
  619tag(beta,  -2).
  620tag(alpha, -3).
  621
  622take_latest([], []).
  623take_latest([_-[H|_]|T0], [H|T]) :- !,
  624	take_latest(T0, T).
  625take_latest([_-[]|T0], T) :- !,		% emty set
  626	take_latest(T0, T).
  627
  628%%	old_tagged_file(+TypeFile) is semidet.
  629
  630old_tagged_file(tag(_,Type)-_File) :-
  631	old_file_type(Type).
  632
  633old_file_type(linux(_)).
  634old_file_type(linux(_,_)).
  635old_file_type(macos(_,ppc)).
  636old_file_type(macos(tiger,_)).
  637old_file_type(macos(snow_leopard_and_later,_)).
  638
  639
  640		 /*******************************
  641		 *	     DOWNLOAD		*
  642		 *******************************/
  643
  644%%	download(+Request) is det.
  645%
  646%	Actually download a file.  Two special requests are supported:
  647%
  648%	  - By postfixing the file with `.sha256` you get the SHA256
  649%	    checksum rather than the file.
  650%	  - If you replace the version with `latest` you get an HTTP
  651%	    303 (See Other) reply pointing at the latest version.
  652
  653download(Request) :-
  654	memberchk(path_info(Download), Request),
  655	file_name_extension(File, envelope, Download), !,
  656	envelope(File).
  657download(Request) :-
  658	memberchk(path_info(Download), Request),
  659	(   file_name_extension(File, sha256, Download)
  660	->  true
  661	;   File = Download
  662	),
  663	download_file(File, AbsFile),
  664	cors_enable,
  665	format('Cross-origin-resource-policy: cross-origin\n'),
  666	(   File == Download
  667	->  http_peer(Request, Remote),
  668	    broadcast(download(File, Remote)),
  669	    http_reply_file(AbsFile, [unsafe(true)], Request)
  670	;   file_checksum(AbsFile, SHA256),
  671	    format('Content-type: text/plain~n~n'),
  672	    format('~w~n', [SHA256])
  673	).
  674download(Request) :-
  675	memberchk(path_info(Download), Request),
  676	classify_file(Download, file(Class,Platform,latest,_,_), [show(last)]),
  677	file_directory_name(Download, Dir),
  678	absolute_file_name(download(Dir),
  679			   AbsDir,
  680			   [ access(read),
  681			     file_type(directory),
  682			     file_errors(fail)
  683			   ]),
  684	download_files(AbsDir, Class, Files, [show(last)]),
  685	memberchk(file(Class, Platform, _, File, _), Files), !,
  686	directory_file_path(Dir, File, Redirect),
  687	http_link_to_id(download, path_postfix(Redirect), URI),
  688	http_redirect(see_other, URI, Request).
  689download(Request) :-
  690	(   memberchk(path_info(Download), Request)
  691	->  http_safe_file(download(Download), [])
  692	;   Download = '.'
  693	),
  694	absolute_file_name(download(Download),
  695			   AbsFile,
  696			   [ access(read),
  697			     file_errors(fail),
  698			     file_type(directory)
  699			   ]), !,
  700	http_reply_dirindex(AbsFile,
  701			    [ unsafe(true),
  702			      name(name_cell)
  703			    ], Request).
  704download(Request) :-
  705	memberchk(path(Path), Request),
  706	existence_error(http_location, Path).
  707
  708download_file(File, AbsFile) :-
  709	http_safe_file(download(File), []),
  710	absolute_file_name(download(File),
  711			   AbsFile,
  712			   [ access(read),
  713			     file_errors(fail)
  714			   ]).
  715
  716:- public
  717	name_cell//1.  718
  719name_cell(File) -->
  720	{ needs_envelope(File),
  721	  file_base_name(File, Name),
  722	  uri_encoded(path, Name, Ref0),
  723	  file_name_extension(Ref0, envelope, Ref)
  724	},
  725	html(a(href(Ref), Name)).
  726name_cell(File) -->
  727	{ file_base_name(File, Name),
  728	  uri_encoded(path, Name, Ref)
  729	},
  730	html(a(href(Ref), Name)).
  731
  732%%	download_daily(+Request)
  733%
  734%	Provide the download page for the windows binaries.
  735
  736download_daily(_Request) :-
  737	absolute_file_name(download('daily/bin'), Dir,
  738			   [ file_type(directory),
  739			     access(read)
  740			   ]),
  741	reply_html_page(
  742	    download(Dir, 'Download daily builds for Windows'),
  743	    title('Download daily builds for Windows'),
  744	    [ \explain_win_daily,
  745	      \directory_index(Dir,
  746			       [ order_by(time),
  747				 order(descending),
  748				 name(name_cell)
  749			       ])
  750	    ]).
  751
  752
  753explain_win_daily -->
  754	html({|html||
  755	      <p>The table below provides access to the most recent 7
  756	      daily builds of SWI-Prolog for Windows, both the 32- and
  757	      64-bit versions.  The build is done automatically from the
  758	      <a href="/git/">GIT sources</a>.  The files use the following
  759	      naming convention:
  760	      </p>
  761	      <ul>
  762	        <li><code>swipl-w</code><var>bits</var><code>-</code><var>date</var><code>.exe</code>
  763	      </ul>
  764	      <p>
  765	      Please note that these versions <b>may be unstable!</b>  It is
  766	      adviced to follow current discussions on the
  767	      <a href="/Mailinglist.html">mailing
  768	      list</a> and/or the git commit messages at
  769	      <a href="https://github.com/SWI-Prolog/swipl-devel">GitHub</a>.
  770	      The primary purpose of the daily builds is to quickly provide
  771	      binaries after a bug report.
  772	      </p>
  773	     |}).
  774
  775
  776		 /*******************************
  777		 *	      ENVELOPE		*
  778		 *******************************/
  779
  780needs_envelope(File) :-
  781	file_name_extension(_, exe, File).
  782
  783add_envelope(File, Envelope) :-
  784	needs_envelope(File),
  785	!,
  786	file_name_extension(File, envelope, Envelope).
  787add_envelope(File, File).
  788
  789envelope(File) :-
  790	maybe(0.1),
  791	download_file(File, AbsFile),
  792	file_checksum(AbsFile, OkHash),
  793	compute_file_checksum(AbsFile, NewHash),
  794	NewHash \== OkHash,
  795	!,
  796	reply_html_page(
  797	    download(File, 'Possibly tampered binary'),
  798	    title('Possibly tampered binary'),
  799	    \tampered(File, OkHash, NewHash)).
  800envelope(File) :-
  801	file_base_name(File, Base),
  802	reply_html_page(
  803	    download(Base, 'Download binary'),
  804	    title('Download a binary file'),
  805	    \envelope(File)).
  806
  807envelope(File) -->
  808	{ http_absolute_location(icons('alert.gif'), Alert, []),
  809	  http_absolute_location(icons('vt_logo.png'), VTLogo, []),
  810	  download_file(File, AbsFile),
  811	  file_checksum(AbsFile, Hash),
  812	  file_base_name(File, Base),
  813	  format(atom(VTHREF), 'https://www.virustotal.com/file/~w/analysis/', Hash)
  814	},
  815	html({|html(Base, Hash, VTHREF, VTLogo, Alert)||
  816<p><img src=Alert style="float:left">
  817Windows antivirus software works using <i>signatures</i> and <i>heuristics</i>.
  818Using the huge amount of virusses and malware known today, arbitrary executables
  819are often <a href="https://en.wikipedia.org/wiki/Antivirus_software#Problems_caused_by_false_positives">falsily classified as malicious</a>.
  820<a href="https://safebrowsing.google.com/">Google Safe Browsing</a>, used by
  821most modern browsers, therefore often classifies our Windows binaries as
  822malware. You can use e.g., <a href="https://www.virustotal.com/gui/home/url">virustotal</a> to verify files with a large number of antivirus programs.
  823</p>
  824
  825<p>
  826Our Windows binaries are cross-compiled on an isolated Linux container.  The
  827integrity of the binaries on the server is regularly verified by validating its
  828SHA256 fingerprint.
  829</p>
  830
  831<p>
  832Please select the checkbox below to enable the actual download link.
  833</p>
  834
  835<table>
  836<tr><td><input type="checkbox" id="understand"><td>I understand</tr>
  837<tr><td><td><a id="download">Download <code>Base</code></a>
  838<span style="color:#888; font-size:small;">(SHA256: <code>Hash</code>)</span></tr>
  839<tr><td style="text-align:right"><img src=VTLogo style="width:1.5ex"><td><a href=VTHREF>VIRUSTOTAL Scan Result</a></tr>
  840</table>
  841	     |}),
  842	js_script({|javascript(Base)||
  843$(function() {
  844  $("#understand").prop("checked", false)
  845                  .on("click", function() {
  846    $("#download").attr("href", Base);
  847  });
  848});
  849
  850		  |}).
  851
  852tampered(File, OkHash, NewHash) -->
  853	{ http_absolute_location(icons('alert.gif'), Alert, [])
  854	},
  855	html({|html(File, Alert, OkHash, NewHash)||
  856<p><img src=Alert style="float:left">
  857The file <code>File</code> SHA256 signature has changed.  Please
  858report this at <a href="mailto:bugs@swi-prolog.org">bugs@swi-prolog.org</a>
  859	     |}).
  860
  861
  862		 /*******************************
  863		 *	     CHECKSUMS		*
  864		 *******************************/
  865
  866:- persistent
  867	sha256(path:atom,
  868	       sha256:atom).  869
  870checksum_file(File) :-
  871	absolute_file_name(data('checksum.db'), File,
  872			   [ access(write) ]).
  873
  874attach_db :-
  875	checksum_file(File),
  876	(   db_attached(File)
  877	->  true
  878	;   db_attach(File, [])
  879	).
  880
  881%!	file_checksum(+Path:atom, -Sum:atom) is det.
  882%
  883%	True when Sum is the SHA256 checksum   of  file. We keep this in
  884%	the Prolog database because  this   simplifies  uploading files.
  885%	Although the data under control  of   the  server  and thus more
  886%	vulnerable than the download area on   disk  because that is not
  887%	writeable by the server, I think  this   is  also  better from a
  888%	security point of view because it  requires the attacker to both
  889%	modify the filesystem and the   server,  something that requires
  890%	different rights and expertise.
  891
  892file_checksum(Path, Sum) :-
  893	attach_db,
  894	sha256(Path, Sum0), !,
  895	Sum = Sum0.
  896file_checksum(Path, Sum) :-
  897	compute_file_checksum(Path, Sum).
  898
  899compute_file_checksum(Path, Sum) :-
  900	crypto_file_hash(Path, Sum,
  901			 [ encoding(octet),
  902			   algorithm(sha256)
  903			 ]),
  904	assert_sha256(Path, Sum)