35
   36:- module(pldoc_http,
   37          [ doc_enable/1,                  38            doc_server/1,                  39            doc_server/2,                  40            doc_browser/0,
   41            doc_browser/1                  42          ]).   43:- use_module(library(pldoc)).   44:- use_module(library(http/thread_httpd)).   45:- use_module(library(http/http_parameters)).   46:- use_module(library(http/html_write)).   47:- use_module(library(http/mimetype)).   48:- use_module(library(dcg/basics)).   49:- use_module(library(http/http_dispatch)).   50:- use_module(library(http/http_hook)).   51:- use_module(library(http/http_path)).   52:- use_module(library(http/http_wrapper)).   53:- use_module(library(uri)).   54:- use_module(library(debug)).   55:- use_module(library(lists)).   56:- use_module(library(url)).   57:- use_module(library(socket)).   58:- use_module(library(option)).   59:- use_module(library(error)).   60:- use_module(library(www_browser)).   61:- use_module(pldoc(doc_process)).   62:- use_module(pldoc(doc_htmlsrc)).   63:- use_module(pldoc(doc_html)).   64:- use_module(pldoc(doc_index)).   65:- use_module(pldoc(doc_search)).   66:- use_module(pldoc(doc_man)).   67:- use_module(pldoc(doc_wiki)).   68:- use_module(pldoc(doc_util)).   69:- use_module(pldoc(doc_access)).   70:- use_module(pldoc(doc_pack)).   71
   78
   79:- dynamic
   80    doc_server_port/1,
   81    doc_enabled/0.   82
   83http:location(pldoc, root(pldoc), []).
   84http:location(pldoc_man, pldoc(refman), []).
   85http:location(pldoc_pkg, pldoc(package), []).
   86http:location(pldoc_resource, Path, []) :-
   87    http_location_by_id(pldoc_resource, Path).
   88
   94
   95doc_enable(true) :-
   96    (   doc_enabled
   97    ->  true
   98    ;   assertz(doc_enabled)
   99    ).
  100doc_enable(false) :-
  101    retractall(doc_enabled).
  102
  136
  137doc_server(Port) :-
  138    doc_server(Port,
  139               [ allow(localhost),
  140                 allow(ip(127,0,0,1))   141               ]).
  142
  143doc_server(Port, _) :-
  144    doc_enable(true),
  145    catch(doc_current_server(Port), _, fail),
  146    !.
  147doc_server(Port, Options) :-
  148    doc_enable(true),
  149    prepare_editor,
  150    host_access_options(Options, ServerOptions),
  151    http_absolute_location(pldoc('.'), Entry, []),
  152    merge_options(ServerOptions,
  153                  [ port(Port),
  154                    entry_page(Entry)
  155                  ], HTTPOptions),
  156    http_server(http_dispatch, HTTPOptions),
  157    assertz(doc_server_port(Port)).
  158
  169
  170doc_current_server(Port) :-
  171    (   doc_server_port(P)
  172    ->  Port = P
  173    ;   http_current_server(_:_, P)
  174    ->  Port = P
  175    ;   existence_error(http_server, pldoc)
  176    ).
  177
  182
  183doc_browser :-
  184    doc_browser([]).
  185doc_browser(Spec) :-
  186    catch(doc_current_server(Port),
  187          error(existence_error(http_server, pldoc), _),
  188          doc_server(Port)),
  189    browser_url(Spec, Request),
  190    format(string(URL), 'http://localhost:~w~w', [Port, Request]),
  191    www_open_url(URL).
  192
  193browser_url([], Root) :-
  194    !,
  195    http_location_by_id(pldoc_root, Root).
  196browser_url(Name, URL) :-
  197    atom(Name),
  198    !,
  199    browser_url(Name/_, URL).
  200browser_url(Name//Arity, URL) :-
  201    must_be(atom, Name),
  202    integer(Arity),
  203    !,
  204    PredArity is Arity+2,
  205    browser_url(Name/PredArity, URL).
  206browser_url(Name/Arity, URL) :-
  207    !,
  208    must_be(atom, Name),
  209    (   predicate(Name, Arity, _, _, _)
  210    ->  format(string(S), '~q/~w', [Name, Arity]),
  211        http_link_to_id(pldoc_man, [predicate=S], URL)
  212    ;   browser_url(_:Name/Arity, URL)
  213    ).
  214browser_url(Spec, URL) :-
  215    !,
  216    Spec = M:Name/Arity,
  217    doc_comment(Spec, _Pos, _Summary, _Comment),
  218    !,
  219    (   var(M)
  220    ->  format(string(S), '~q/~w', [Name, Arity])
  221    ;   format(string(S), '~q:~q/~w', [M, Name, Arity])
  222    ),
  223    http_link_to_id(pldoc_object, [object=S], URL).
  224
  229
  230prepare_editor :-
  231    current_prolog_flag(editor, pce_emacs),
  232    !,
  233    start_emacs.
  234prepare_editor.
  235
  236
  237                   240
  241:- http_handler(pldoc(.),          pldoc_root,
  242                [ prefix,
  243                  authentication(pldoc(read)),
  244                  condition(doc_enabled)
  245                ]).  246:- http_handler(pldoc('index.html'), pldoc_index,   []).  247:- http_handler(pldoc(file),       pldoc_file,     []).  248:- http_handler(pldoc(place),      go_place,       []).  249:- http_handler(pldoc(edit),       pldoc_edit,
  250                [authentication(pldoc(edit))]).  251:- http_handler(pldoc(doc),        pldoc_doc,      [prefix]).  252:- http_handler(pldoc(man),        pldoc_man,      []).  253:- http_handler(pldoc(doc_for),    pldoc_object,   [id(pldoc_doc_for)]).  254:- http_handler(pldoc(search),     pldoc_search,   []).  255:- http_handler(pldoc('res/'),     pldoc_resource, [prefix]).  256
  257
  264
  265pldoc_root(Request) :-
  266    http_parameters(Request,
  267                    [ empty(Empty, [ oneof([true,false]),
  268                                     default(false)
  269                                   ])
  270                    ]),
  271    pldoc_root(Request, Empty).
  272
  273pldoc_root(Request, false) :-
  274    http_location_by_id(pldoc_root, Root),
  275    memberchk(path(Path), Request),
  276    Root \== Path,
  277    !,
  278    existence_error(http_location, Path).
  279pldoc_root(_Request, false) :-
  280    working_directory(Dir0, Dir0),
  281    allowed_directory(Dir0),
  282    !,
  283    ensure_slash_end(Dir0, Dir1),
  284    doc_file_href(Dir1, Ref0),
  285    atom_concat(Ref0, 'index.html', Index),
  286    throw(http_reply(see_other(Index))).
  287pldoc_root(Request, _) :-
  288    pldoc_index(Request).
  289
  290
  295
  296pldoc_index(_Request) :-
  297    reply_html_page(pldoc(index),
  298                    title('SWI-Prolog documentation'),
  299                    [ \doc_links('', []),
  300                       h1('SWI-Prolog documentation'),
  301                      \man_overview([])
  302                    ]).
  303
  304
  308
  309pldoc_file(Request) :-
  310    http_parameters(Request,
  311                    [ file(File, [])
  312                    ]),
  313    (   source_file(File)
  314    ->  true
  315    ;   throw(http_reply(forbidden(File)))
  316    ),
  317    doc_for_file(File, []).
  318
  326
  327pldoc_edit(Request) :-
  328    http:authenticate(pldoc(edit), Request, _),
  329    http_parameters(Request,
  330                    [ file(File,
  331                           [ optional(true),
  332                             description('Name of the file to edit')
  333                           ]),
  334                      line(Line,
  335                           [ optional(true),
  336                             integer,
  337                             description('Line in the file')
  338                           ]),
  339                      name(Name,
  340                           [ optional(true),
  341                             description('Name of a Prolog predicate to edit')
  342                           ]),
  343                      arity(Arity,
  344                            [ integer,
  345                              optional(true),
  346                              description('Arity of a Prolog predicate to edit')
  347                            ]),
  348                      module(Module,
  349                             [ optional(true),
  350                               description('Name of a Prolog module to search for predicate')
  351                             ])
  352                    ]),
  353    (   atom(File)
  354    ->  allowed_file(File)
  355    ;   true
  356    ),
  357    (   atom(File), integer(Line)
  358    ->  Edit = file(File, line(Line))
  359    ;   atom(File)
  360    ->  Edit = file(File)
  361    ;   atom(Name), integer(Arity)
  362    ->  (   atom(Module)
  363        ->  Edit = (Module:Name/Arity)
  364        ;   Edit = (Name/Arity)
  365        )
  366    ),
  367    edit(Edit),
  368    format('Content-type: text/plain~n~n'),
  369    format('Started ~q~n', [edit(Edit)]).
  370pldoc_edit(_Request) :-
  371    http_location_by_id(pldoc_edit, Location),
  372    throw(http_reply(forbidden(Location))).
  373
  374
  378
  379go_place(Request) :-
  380    http_parameters(Request,
  381                    [ place(Place, [])
  382                    ]),
  383    places(Place).
  384
  385places(':packs:') :-
  386    !,
  387    http_link_to_id(pldoc_pack, [], HREF),
  388    throw(http_reply(moved(HREF))).
  389places(Dir0) :-
  390    expand_alias(Dir0, Dir),
  391    (   allowed_directory(Dir)
  392    ->  format(string(IndexFile), '~w/index.html', [Dir]),
  393        doc_file_href(IndexFile, HREF),
  394        throw(http_reply(moved(HREF)))
  395    ;   throw(http_reply(forbidden(Dir)))
  396    ).
  397
  398
  402
  403allowed_directory(Dir) :-
  404    source_directory(Dir),
  405    !.
  406allowed_directory(Dir) :-
  407    working_directory(CWD, CWD),
  408    same_file(CWD, Dir).
  409allowed_directory(Dir) :-
  410    prolog:doc_directory(Dir).
  411
  412
  417
  418allowed_file(File) :-
  419    source_file(_, File),
  420    !.
  421allowed_file(File) :-
  422    absolute_file_name(File, Canonical),
  423    file_directory_name(Canonical, Dir),
  424    allowed_directory(Dir).
  425
  426
  430
  431pldoc_resource(Request) :-
  432    http_location_by_id(pldoc_resource, ResRoot),
  433    memberchk(path(Path), Request),
  434    atom_concat(ResRoot, File, Path),
  435    file(File, Local),
  436    http_reply_file(pldoc(Local), [], Request).
  437
  438file('pldoc.css',     'pldoc.css').
  439file('pllisting.css', 'pllisting.css').
  440file('pldoc.js',      'pldoc.js').
  441file('edit.png',      'edit.png').
  442file('editpred.png',  'editpred.png').
  443file('up.gif',        'up.gif').
  444file('source.png',    'source.png').
  445file('public.png',    'public.png').
  446file('private.png',   'private.png').
  447file('reload.png',    'reload.png').
  448file('favicon.ico',   'favicon.ico').
  449file('h1-bg.png',     'h1-bg.png').
  450file('h2-bg.png',     'h2-bg.png').
  451file('pub-bg.png',    'pub-bg.png').
  452file('priv-bg.png',   'priv-bg.png').
  453file('multi-bg.png',  'multi-bg.png').
  454
  455
  466
  467pldoc_doc(Request) :-
  468    memberchk(path(ReqPath), Request),
  469    http_location_by_id(pldoc_doc, Me),
  470    atom_concat(Me, AbsFile0, ReqPath),
  471    (   sub_atom(ReqPath, _, _, 0, /)
  472    ->  atom_concat(ReqPath, 'index.html', File),
  473        throw(http_reply(moved(File)))
  474    ;   clean_path(AbsFile0, AbsFile1),
  475        expand_alias(AbsFile1, AbsFile),
  476        is_absolute_file_name(AbsFile)
  477    ->  documentation(AbsFile, Request)
  478    ).
  479
  480documentation(Path, Request) :-
  481    file_base_name(Path, Base),
  482    file(_, Base),                           483    !,
  484    http_reply_file(pldoc(Base), [], Request).
  485documentation(Path, Request) :-
  486    file_name_extension(_, Ext, Path),
  487    autolink_extension(Ext, image),
  488    http_reply_file(Path, [unsafe(true)], Request).
  489documentation(Path, Request) :-
  490    Index = '/index.html',
  491    sub_atom(Path, _, _, 0, Index),
  492    atom_concat(Dir, Index, Path),
  493    exists_directory(Dir),                   494    !,
  495    (   allowed_directory(Dir)
  496    ->  edit_options(Request, EditOptions),
  497        doc_for_dir(Dir, EditOptions)
  498    ;   throw(http_reply(forbidden(Dir)))
  499    ).
  500documentation(File, Request) :-
  501    wiki_file(File, WikiFile),
  502    !,
  503    (   allowed_file(WikiFile)
  504    ->  true
  505    ;   throw(http_reply(forbidden(File)))
  506    ),
  507    edit_options(Request, Options),
  508    doc_for_wiki_file(WikiFile, Options).
  509documentation(Path, Request) :-
  510    pl_file(Path, File),
  511    !,
  512    (   allowed_file(File)
  513    ->  true
  514    ;   throw(http_reply(forbidden(File)))
  515    ),
  516    doc_reply_file(File, Request).
  517documentation(Path, _) :-
  518    throw(http_reply(not_found(Path))).
  519
  520:- public
  521    doc_reply_file/2.  522
  523doc_reply_file(File, Request) :-
  524    http_parameters(Request,
  525                    [ public_only(Public),
  526                      reload(Reload),
  527                      show(Show),
  528                      format_comments(FormatComments)
  529                    ],
  530                    [ attribute_declarations(param)
  531                    ]),
  532    (   exists_file(File)
  533    ->  true
  534    ;   throw(http_reply(not_found(File)))
  535    ),
  536    (   Reload == true,
  537        source_file(File)
  538    ->  load_files(File, [if(changed), imports([])])
  539    ;   true
  540    ),
  541    edit_options(Request, EditOptions),
  542    (   Show == src
  543    ->  format('Content-type: text/html~n~n', []),
  544        source_to_html(File, stream(current_output),
  545                       [ skin(src_skin(Request, Show, FormatComments)),
  546                         format_comments(FormatComments)
  547                       ])
  548    ;   Show == raw
  549    ->  http_reply_file(File,
  550                        [ unsafe(true),   551                          mime_type(text/plain)
  552                        ], Request)
  553    ;   doc_for_file(File,
  554                     [ public_only(Public),
  555                       source_link(true)
  556                     | EditOptions
  557                     ])
  558    ).
  559
  560
  561:- public src_skin/5.                     562
  563src_skin(Request, _Show, FormatComments, header, Out) :-
  564    memberchk(request_uri(ReqURI), Request),
  565    negate(FormatComments, AltFormatComments),
  566    replace_parameters(ReqURI, [show(raw)], RawLink),
  567    replace_parameters(ReqURI, [format_comments(AltFormatComments)], CmtLink),
  568    phrase(html(div(class(src_formats),
  569                    [ 'View source with ',
  570                      a(href(CmtLink), \alt_view(AltFormatComments)),
  571                      ' or as ',
  572                      a(href(RawLink), raw)
  573                    ])), Tokens),
  574    print_html(Out, Tokens).
  575
  576alt_view(true) -->
  577    html('formatted comments').
  578alt_view(false) -->
  579    html('raw comments').
  580
  581negate(true, false).
  582negate(false, true).
  583
  584replace_parameters(ReqURI, Extra, URI) :-
  585    uri_components(ReqURI, C0),
  586    uri_data(search, C0, Search0),
  587    (   var(Search0)
  588    ->  uri_query_components(Search, Extra)
  589    ;   uri_query_components(Search0, Form0),
  590        merge_options(Extra, Form0, Form),
  591        uri_query_components(Search, Form)
  592    ),
  593    uri_data(search, C0, Search, C),
  594    uri_components(URI, C).
  595
  596
  601
  602edit_options(Request, [edit(true)]) :-
  603    catch(http:authenticate(pldoc(edit), Request, _), _, fail),
  604    !.
  605edit_options(_, []).
  606
  607
  609
  610pl_file(File, PlFile) :-
  611    file_name_extension(Base, html, File),
  612    !,
  613    absolute_file_name(Base,
  614                       PlFile,
  615                       [ file_errors(fail),
  616                         file_type(prolog),
  617                         access(read)
  618                       ]).
  619pl_file(File, File).
  620
  625
  626wiki_file(File, TxtFile) :-
  627    file_name_extension(_, Ext, File),
  628    wiki_file_extension(Ext),
  629    !,
  630    TxtFile = File.
  631wiki_file(File, TxtFile) :-
  632    file_base_name(File, Base),
  633    autolink_file(Base, wiki),
  634    !,
  635    TxtFile = File.
  636wiki_file(File, TxtFile) :-
  637    file_name_extension(Base, html, File),
  638    wiki_file_extension(Ext),
  639    file_name_extension(Base, Ext, TxtFile),
  640    access_file(TxtFile, read).
  641
  642wiki_file_extension(md).
  643wiki_file_extension(txt).
  644
  645
  649
  650clean_path(Path0, Path) :-
  651    current_prolog_flag(windows, true),
  652    sub_atom(Path0, 2, _, _, :),
  653    !,
  654    sub_atom(Path0, 1, _, 0, Path).
  655clean_path(Path, Path).
  656
  657
  668
  669pldoc_man(Request) :-
  670    http_parameters(Request,
  671                    [ predicate(PI, [optional(true)]),
  672                      function(Fun, [optional(true)]),
  673                      'CAPI'(F,     [optional(true)]),
  674                      section(Sec,  [optional(true)])
  675                    ]),
  676    (   ground(PI)
  677    ->  atom_pi(PI, Obj)
  678    ;   ground(Fun)
  679    ->  atomic_list_concat([Name,ArityAtom], /, Fun),
  680        atom_number(ArityAtom, Arity),
  681        Obj = f(Name/Arity)
  682    ;   ground(F)
  683    ->  Obj = c(F)
  684    ;   ground(Sec)
  685    ->  atom_concat('sec:', Sec, SecID),
  686        Obj = section(SecID)
  687    ),
  688    man_title(Obj, Title),
  689    reply_html_page(
  690        pldoc(object(Obj)),
  691        title(Title),
  692        \man_page(Obj, [])).
  693
  694man_title(f(Obj), Title) :-
  695    !,
  696    format(atom(Title), 'SWI-Prolog -- function ~w', [Obj]).
  697man_title(c(Obj), Title) :-
  698    !,
  699    format(atom(Title), 'SWI-Prolog -- API-function ~w', [Obj]).
  700man_title(section(_Id), Title) :-
  701    !,
  702    format(atom(Title), 'SWI-Prolog -- Manual', []).
  703man_title(Obj, Title) :-
  704    format(atom(Title), 'SWI-Prolog -- ~w', [Obj]).
  705
  710
  711pldoc_object(Request) :-
  712    http_parameters(Request,
  713                    [ object(Atom, []),
  714                      header(Header, [default(true)])
  715                    ]),
  716    atom_to_term(Atom, Obj, _),
  717    (   prolog:doc_object_title(Obj, Title)
  718    ->  true
  719    ;   Title = Atom
  720    ),
  721    edit_options(Request, EditOptions),
  722    reply_html_page(
  723        pldoc(object(Obj)),
  724        title(Title),
  725        \object_page(Obj, [header(Header)|EditOptions])).
  726
  727
  731
  732pldoc_search(Request) :-
  733    http_parameters(Request,
  734                    [ for(For,
  735                          [ optional(true),
  736                            description('String to search for')
  737                          ]),
  738                      page(Page,
  739                           [ integer,
  740                             default(1),
  741                             description('Page of search results to view')
  742                           ]),
  743                      in(In,
  744                         [ oneof([all,app,noapp,man,lib,pack,wiki]),
  745                           default(all),
  746                           description('Search everying, application only or manual only')
  747                         ]),
  748                      match(Match,
  749                            [ oneof([name,summary]),
  750                              default(summary),
  751                              description('Match only the name or also the summary')
  752                            ]),
  753                      resultFormat(Format,
  754                                   [ oneof(long,summary),
  755                                     default(summary),
  756                                     description('Return full documentation or summary-lines')
  757                                   ])
  758                    ]),
  759    edit_options(Request, EditOptions),
  760    format(string(Title), 'Prolog search -- ~w', [For]),
  761    reply_html_page(pldoc(search(For)),
  762                    title(Title),
  763                    \search_reply(For,
  764                                  [ resultFormat(Format),
  765                                    search_in(In),
  766                                    search_match(Match),
  767                                    page(Page)
  768                                  | EditOptions
  769                                  ])).
  770
  771
  772                   775
  776:- public
  777    param/2.                          778
  779param(public_only,
  780      [ boolean,
  781        default(true),
  782        description('If true, hide private predicates')
  783      ]).
  784param(reload,
  785      [ boolean,
  786        default(false),
  787        description('Reload the file and its documentation')
  788      ]).
  789param(show,
  790      [ oneof([doc,src,raw]),
  791        default(doc),
  792        description('How to show the file')
  793      ]).
  794param(format_comments,
  795      [ boolean,
  796        default(true),
  797        description('If true, use PlDoc for rendering structured comments')
  798      ])