Fossil SCM
Added the /srchsetup page for configuring the /search screen.
Commit
ca833ff86f0a6a7e00477cf88fb88176f75c6fdb
Parent
32f8da0ce785b63…
2 files changed
+67
-57
+55
-1
+67
-57
| --- src/search.c | ||
| +++ src/search.c | ||
| @@ -424,13 +424,13 @@ | ||
| 424 | 424 | int argc, |
| 425 | 425 | sqlite3_value **argv |
| 426 | 426 | ){ |
| 427 | 427 | Blob txt; |
| 428 | 428 | const char *zType = (const char*)sqlite3_value_text(argv[0]); |
| 429 | - const char *zArg1 = (const char*)sqlite3_value_text(argv[1]); | |
| 430 | - const char *zArg2 = (const char*)sqlite3_value_text(argv[2]); | |
| 431 | - search_stext(zType[0], zArg1, zArg2, &txt); | |
| 429 | + int rid = sqlite3_value_int(argv[1]); | |
| 430 | + const char *zName = (const char*)sqlite3_value_text(argv[2]); | |
| 431 | + search_stext(zType[0], rid, zName, &txt); | |
| 432 | 432 | sqlite3_result_text(context, blob_materialize(&txt), -1, fossil_free); |
| 433 | 433 | } |
| 434 | 434 | |
| 435 | 435 | /* |
| 436 | 436 | ** Encode a string for use as a query parameter in a URL |
| @@ -556,18 +556,32 @@ | ||
| 556 | 556 | ** * Search wiki |
| 557 | 557 | */ |
| 558 | 558 | void search_page(void){ |
| 559 | 559 | const char *zPattern = PD("s",""); |
| 560 | 560 | Stmt q; |
| 561 | - const char *zSrchEnable = "dwc"; | |
| 561 | + int okCheckin; | |
| 562 | + int okDoc; | |
| 563 | + int okTicket; | |
| 564 | + int okWiki; | |
| 565 | + int allOff; | |
| 566 | + const char *zDisable; | |
| 562 | 567 | |
| 563 | 568 | login_check_credentials(); |
| 564 | - if( !g.perm.Read ){ login_needed(); return; } | |
| 569 | + okCheckin = g.perm.Read && db_get_boolean("search-ci",0); | |
| 570 | + okDoc = g.perm.Read && db_get_boolean("search-doc",0); | |
| 571 | + okTicket = g.perm.RdTkt && db_get_boolean("search-tkt",0); | |
| 572 | + okWiki = g.perm.RdWiki && db_get_boolean("search-wiki",0); | |
| 573 | + allOff = (okCheckin + okDoc + okTicket + okWiki == 0); | |
| 574 | + zDisable = allOff ? " disabled" : ""; | |
| 575 | + zPattern = allOff ? "" : PD("s",""); | |
| 565 | 576 | style_header("Search"); |
| 566 | 577 | @ <form method="GET" action="search"><center> |
| 567 | - @ <input type="text" name="s" size="40" value="%h(zPattern)"> | |
| 568 | - @ <input type="submit" value="Search"> | |
| 578 | + @ <input type="text" name="s" size="40" value="%h(zPattern)"%s(zDisable)> | |
| 579 | + @ <input type="submit" value="Search"%s(zDisable)> | |
| 580 | + if( allOff ){ | |
| 581 | + @ <p class="generalError">Search is disabled</p> | |
| 582 | + } | |
| 569 | 583 | @ </center></form> |
| 570 | 584 | while( fossil_isspace(zPattern[0]) ) zPattern++; |
| 571 | 585 | if( zPattern[0] ){ |
| 572 | 586 | search_sql_setup(g.db); |
| 573 | 587 | add_content_sql_commands(g.db); |
| @@ -575,28 +589,30 @@ | ||
| 575 | 589 | SRCHFLG_STATIC|SRCHFLG_HTML|SRCHFLG_SCORE); |
| 576 | 590 | db_multi_exec( |
| 577 | 591 | "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;" |
| 578 | 592 | "CREATE TEMP TABLE x(label TEXT,url TEXT,date TEXT,snip TEXT);" |
| 579 | 593 | ); |
| 580 | - if( strchr(zSrchEnable, 'd') ){ | |
| 581 | - db_multi_exec( | |
| 582 | - "INSERT INTO x(label,url,date,snip)" | |
| 583 | - " SELECT printf('Document: %%s',foci.filename)," | |
| 584 | - " printf('%R/doc/trunk/%%s',foci.filename)," | |
| 585 | - " (SELECT datetime(event.mtime) FROM event" | |
| 586 | - " WHERE objid=symbolic_name_to_rid('trunk'))," | |
| 587 | - " snippet(stext('d',blob.rid,foci.filename))" | |
| 588 | - " FROM foci CROSS JOIN blob" | |
| 589 | - " WHERE checkinID=symbolic_name_to_rid('trunk')" | |
| 590 | - " AND blob.uuid=foci.uuid" | |
| 591 | - " AND (filename GLOB '*.wiki' OR" | |
| 592 | - " filename GLOB '*.md' OR" | |
| 593 | - " filename GLOB '*.txt' OR" | |
| 594 | - " filename GLOB '*.html');" | |
| 595 | - ); | |
| 596 | - } | |
| 597 | - if( strchr(zSrchEnable, 'w') ){ | |
| 594 | + if( okDoc ){ | |
| 595 | + char *zDocGlob = db_get("doc-glob",""); | |
| 596 | + char *zDocBr = db_get("doc-branch","trunk"); | |
| 597 | + if( zDocGlob && zDocGlob[0] && zDocBr && zDocBr[0] ){ | |
| 598 | + db_multi_exec( | |
| 599 | + "INSERT INTO x(label,url,date,snip)" | |
| 600 | + " SELECT printf('Document: %%s',foci.filename)," | |
| 601 | + " printf('%R/doc/%T/%%s',foci.filename)," | |
| 602 | + " (SELECT datetime(event.mtime) FROM event" | |
| 603 | + " WHERE objid=symbolic_name_to_rid('trunk'))," | |
| 604 | + " snippet(stext('d',blob.rid,foci.filename))" | |
| 605 | + " FROM foci CROSS JOIN blob" | |
| 606 | + " WHERE checkinID=symbolic_name_to_rid('trunk')" | |
| 607 | + " AND blob.uuid=foci.uuid" | |
| 608 | + " AND %z", | |
| 609 | + zDocBr, glob_expr("foci.filename", zDocGlob) | |
| 610 | + ); | |
| 611 | + } | |
| 612 | + } | |
| 613 | + if( okWiki ){ | |
| 598 | 614 | db_multi_exec( |
| 599 | 615 | "WITH wiki(name,rid,mtime) AS (" |
| 600 | 616 | " SELECT substr(tagname,6), tagxref.rid, max(tagxref.mtime)" |
| 601 | 617 | " FROM tag, tagxref" |
| 602 | 618 | " WHERE tag.tagname GLOB 'wiki-*'" |
| @@ -609,11 +625,11 @@ | ||
| 609 | 625 | " datetime(mtime)," |
| 610 | 626 | " snippet(stext('w',rid,name))" |
| 611 | 627 | " FROM wiki;" |
| 612 | 628 | ); |
| 613 | 629 | } |
| 614 | - if( strchr(zSrchEnable, 'c') ){ | |
| 630 | + if( okCheckin ){ | |
| 615 | 631 | db_multi_exec( |
| 616 | 632 | "WITH ckin(uuid,rid,mtime) AS (" |
| 617 | 633 | " SELECT blob.uuid, event.objid, event.mtime" |
| 618 | 634 | " FROM event, blob" |
| 619 | 635 | " WHERE event.type='ci'" |
| @@ -627,11 +643,11 @@ | ||
| 627 | 643 | " FROM ckin;" |
| 628 | 644 | ); |
| 629 | 645 | } |
| 630 | 646 | db_prepare(&q, "SELECT url, substr(snip,9), label" |
| 631 | 647 | " FROM x WHERE snip IS NOT NULL" |
| 632 | - " ORDER BY substr(snip,1,9) DESC, date DESC;"); | |
| 648 | + " ORDER BY substr(snip,1,8) DESC, date DESC;"); | |
| 633 | 649 | @ <ol> |
| 634 | 650 | while( db_step(&q)==SQLITE_ROW ){ |
| 635 | 651 | const char *zUrl = db_column_text(&q, 0); |
| 636 | 652 | const char *zSnippet = db_column_text(&q, 1); |
| 637 | 653 | const char *zLabel = db_column_text(&q, 2); |
| @@ -676,40 +692,36 @@ | ||
| 676 | 692 | /* |
| 677 | 693 | ** Return "search text" - a reduced version of a document appropriate for |
| 678 | 694 | ** full text search and/or for constructing a search result snippet. |
| 679 | 695 | ** |
| 680 | 696 | ** cType: d Embedded documentation |
| 681 | -** s Source code listing | |
| 682 | 697 | ** w Wiki page |
| 683 | 698 | ** c Check-in comment |
| 684 | 699 | ** t Ticket text |
| 685 | -** e Event/Blog text | |
| 686 | -** k Diff of a wiki | |
| 687 | -** f Diff of a checkin | |
| 700 | +** | |
| 701 | +** rid The RID of an artifact that defines the object | |
| 702 | +** being searched. | |
| 688 | 703 | ** |
| 689 | -** zArg1, zArg2: Description of the document, depending on cType. | |
| 704 | +** zName Name of the object being searched. | |
| 690 | 705 | */ |
| 691 | 706 | void search_stext( |
| 692 | 707 | char cType, /* Type of document */ |
| 693 | - const char *zArg1, /* First parameter */ | |
| 694 | - const char *zArg2, /* Second parameter */ | |
| 708 | + int rid, /* BLOB.RID or TAG.TAGID value for document */ | |
| 709 | + const char *zName, /* Name of the document */ | |
| 695 | 710 | Blob *pOut /* OUT: Initialize to the search text */ |
| 696 | 711 | ){ |
| 697 | 712 | blob_init(pOut, 0, 0); |
| 698 | 713 | switch( cType ){ |
| 699 | - case 'd': /* Doc. zArg1: RID of the file. zArg2: Filename */ | |
| 700 | - case 's': { /* Source. zArg1: RID of the file. zArg2: Filename */ | |
| 701 | - int rid = atoi(zArg1); | |
| 714 | + case 'd': { /* Documents */ | |
| 702 | 715 | Blob doc; |
| 703 | 716 | content_get(rid, &doc); |
| 704 | 717 | blob_to_utf8_no_bom(&doc, 0); |
| 705 | - get_stext_by_mimetype(&doc, mimetype_from_name(zArg2), pOut); | |
| 718 | + get_stext_by_mimetype(&doc, mimetype_from_name(zName), pOut); | |
| 706 | 719 | blob_reset(&doc); |
| 707 | 720 | break; |
| 708 | 721 | } |
| 709 | - case 'w': { /* Wiki. zArg1: RID of the page. zArg2: Page name */ | |
| 710 | - int rid = atoi(zArg1); | |
| 722 | + case 'w': { /* Wiki */ | |
| 711 | 723 | Manifest *pWiki = manifest_get(rid, CFTYPE_WIKI,0); |
| 712 | 724 | Blob wiki; |
| 713 | 725 | if( pWiki==0 ) break; |
| 714 | 726 | blob_init(&wiki, pWiki->zWiki, -1); |
| 715 | 727 | get_stext_by_mimetype(&wiki, wiki_filter_mimetypes(pWiki->zMimetype), |
| @@ -716,12 +728,11 @@ | ||
| 716 | 728 | pOut); |
| 717 | 729 | blob_reset(&wiki); |
| 718 | 730 | manifest_destroy(pWiki); |
| 719 | 731 | break; |
| 720 | 732 | } |
| 721 | - case 'c': { /* Ckeckin: zArg1: RID of the checkin. zArg2: Not used */ | |
| 722 | - int rid = atoi(zArg1); | |
| 733 | + case 'c': { /* Ckeck-in Comments */ | |
| 723 | 734 | static Stmt q; |
| 724 | 735 | db_static_prepare(&q, |
| 725 | 736 | "SELECT coalesce(ecomment,comment)" |
| 726 | 737 | " ||' (user: '||coalesce(euser,user,'?')" |
| 727 | 738 | " ||', tags: '||" |
| @@ -741,46 +752,45 @@ | ||
| 741 | 752 | } |
| 742 | 753 | } |
| 743 | 754 | } |
| 744 | 755 | |
| 745 | 756 | /* |
| 746 | -** The arguments cType,zArg1,zArg2 define an object that can be searched | |
| 757 | +** The arguments cType,rid,zName define an object that can be searched | |
| 747 | 758 | ** for. Return a URL (relative to the root of the Fossil project) that |
| 748 | 759 | ** will jump to that document. |
| 749 | 760 | ** |
| 750 | 761 | ** Space to hold the returned string is obtained from mprintf() and should |
| 751 | 762 | ** be freed by the caller using fossil_free() or the equivalent. |
| 752 | 763 | */ |
| 753 | 764 | char *search_url( |
| 754 | 765 | char cType, /* Type of document */ |
| 755 | - const char *zArg1, /* First parameter */ | |
| 756 | - const char *zArg2 /* Second parameter */ | |
| 766 | + int rid, /* BLOB.RID or TAG.TAGID for the object */ | |
| 767 | + const char *zName /* Name of the object */ | |
| 757 | 768 | ){ |
| 758 | 769 | char *zUrl = 0; |
| 759 | 770 | switch( cType ){ |
| 760 | - case 'd': { /* Doc. zArg1: RID of the file. zArg2: Filename */ | |
| 761 | - case 's': /* Source. zArg1: RID of the file. zArg2: Filename */ | |
| 771 | + case 'd': { /* Documents */ | |
| 762 | 772 | zUrl = db_text(0, |
| 763 | 773 | "SELECT printf('/doc/%%s%%s', substr(blob.uuid,20), %Q)" |
| 764 | 774 | " FROM mlink, blob" |
| 765 | 775 | " WHERE mlink.fid=%d AND mlink.mid=blob.rid", |
| 766 | - zArg2, atoi(zArg1)); | |
| 776 | + zName, rid); | |
| 767 | 777 | break; |
| 768 | 778 | } |
| 769 | - case 'w': { /* Wiki. zArg1: RID of the page. zArg2: Page name */ | |
| 770 | - char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d",atoi(zArg1)); | |
| 771 | - zUrl = mprintf("/wiki?id=%z&name=%t", zId, zArg2); | |
| 779 | + case 'w': { /* Wiki */ | |
| 780 | + char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); | |
| 781 | + zUrl = mprintf("/wiki?id=%z&name=%t", zId, zName); | |
| 772 | 782 | break; |
| 773 | 783 | } |
| 774 | - case 'c': { /* Ckeckin: zArg1: RID of the checkin. zArg2: Not used */ | |
| 775 | - char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d",atoi(zArg1)); | |
| 784 | + case 'c': { /* Ckeck-in Comment */ | |
| 785 | + char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); | |
| 776 | 786 | zUrl = mprintf("/info/%z", zId); |
| 777 | 787 | break; |
| 778 | 788 | } |
| 779 | 789 | } |
| 780 | 790 | return zUrl; |
| 781 | -} | |
| 791 | +} | |
| 782 | 792 | |
| 783 | 793 | /* |
| 784 | 794 | ** COMMAND: test-search-stext |
| 785 | 795 | ** |
| 786 | 796 | ** Usage: fossil test-search-stext TYPE ARG1 ARG2 |
| @@ -787,11 +797,11 @@ | ||
| 787 | 797 | */ |
| 788 | 798 | void test_search_stext(void){ |
| 789 | 799 | Blob out; |
| 790 | 800 | char *zUrl; |
| 791 | 801 | db_find_and_open_repository(0,0); |
| 792 | - if( g.argc!=5 ) usage("TYPE ARG1 ARG2"); | |
| 793 | - search_stext(g.argv[2][0], g.argv[3], g.argv[4], &out); | |
| 794 | - zUrl = search_url(g.argv[2][0], g.argv[3], g.argv[4]); | |
| 802 | + if( g.argc!=5 ) usage("TYPE RID NAME"); | |
| 803 | + search_stext(g.argv[2][0], atoi(g.argv[3]), g.argv[4], &out); | |
| 804 | + zUrl = search_url(g.argv[2][0], atoi(g.argv[3]), g.argv[4]); | |
| 795 | 805 | fossil_print("%s\n%z\n",blob_str(&out),zUrl); |
| 796 | 806 | blob_reset(&out); |
| 797 | 807 | } |
| 798 | 808 |
| --- src/search.c | |
| +++ src/search.c | |
| @@ -424,13 +424,13 @@ | |
| 424 | int argc, |
| 425 | sqlite3_value **argv |
| 426 | ){ |
| 427 | Blob txt; |
| 428 | const char *zType = (const char*)sqlite3_value_text(argv[0]); |
| 429 | const char *zArg1 = (const char*)sqlite3_value_text(argv[1]); |
| 430 | const char *zArg2 = (const char*)sqlite3_value_text(argv[2]); |
| 431 | search_stext(zType[0], zArg1, zArg2, &txt); |
| 432 | sqlite3_result_text(context, blob_materialize(&txt), -1, fossil_free); |
| 433 | } |
| 434 | |
| 435 | /* |
| 436 | ** Encode a string for use as a query parameter in a URL |
| @@ -556,18 +556,32 @@ | |
| 556 | ** * Search wiki |
| 557 | */ |
| 558 | void search_page(void){ |
| 559 | const char *zPattern = PD("s",""); |
| 560 | Stmt q; |
| 561 | const char *zSrchEnable = "dwc"; |
| 562 | |
| 563 | login_check_credentials(); |
| 564 | if( !g.perm.Read ){ login_needed(); return; } |
| 565 | style_header("Search"); |
| 566 | @ <form method="GET" action="search"><center> |
| 567 | @ <input type="text" name="s" size="40" value="%h(zPattern)"> |
| 568 | @ <input type="submit" value="Search"> |
| 569 | @ </center></form> |
| 570 | while( fossil_isspace(zPattern[0]) ) zPattern++; |
| 571 | if( zPattern[0] ){ |
| 572 | search_sql_setup(g.db); |
| 573 | add_content_sql_commands(g.db); |
| @@ -575,28 +589,30 @@ | |
| 575 | SRCHFLG_STATIC|SRCHFLG_HTML|SRCHFLG_SCORE); |
| 576 | db_multi_exec( |
| 577 | "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;" |
| 578 | "CREATE TEMP TABLE x(label TEXT,url TEXT,date TEXT,snip TEXT);" |
| 579 | ); |
| 580 | if( strchr(zSrchEnable, 'd') ){ |
| 581 | db_multi_exec( |
| 582 | "INSERT INTO x(label,url,date,snip)" |
| 583 | " SELECT printf('Document: %%s',foci.filename)," |
| 584 | " printf('%R/doc/trunk/%%s',foci.filename)," |
| 585 | " (SELECT datetime(event.mtime) FROM event" |
| 586 | " WHERE objid=symbolic_name_to_rid('trunk'))," |
| 587 | " snippet(stext('d',blob.rid,foci.filename))" |
| 588 | " FROM foci CROSS JOIN blob" |
| 589 | " WHERE checkinID=symbolic_name_to_rid('trunk')" |
| 590 | " AND blob.uuid=foci.uuid" |
| 591 | " AND (filename GLOB '*.wiki' OR" |
| 592 | " filename GLOB '*.md' OR" |
| 593 | " filename GLOB '*.txt' OR" |
| 594 | " filename GLOB '*.html');" |
| 595 | ); |
| 596 | } |
| 597 | if( strchr(zSrchEnable, 'w') ){ |
| 598 | db_multi_exec( |
| 599 | "WITH wiki(name,rid,mtime) AS (" |
| 600 | " SELECT substr(tagname,6), tagxref.rid, max(tagxref.mtime)" |
| 601 | " FROM tag, tagxref" |
| 602 | " WHERE tag.tagname GLOB 'wiki-*'" |
| @@ -609,11 +625,11 @@ | |
| 609 | " datetime(mtime)," |
| 610 | " snippet(stext('w',rid,name))" |
| 611 | " FROM wiki;" |
| 612 | ); |
| 613 | } |
| 614 | if( strchr(zSrchEnable, 'c') ){ |
| 615 | db_multi_exec( |
| 616 | "WITH ckin(uuid,rid,mtime) AS (" |
| 617 | " SELECT blob.uuid, event.objid, event.mtime" |
| 618 | " FROM event, blob" |
| 619 | " WHERE event.type='ci'" |
| @@ -627,11 +643,11 @@ | |
| 627 | " FROM ckin;" |
| 628 | ); |
| 629 | } |
| 630 | db_prepare(&q, "SELECT url, substr(snip,9), label" |
| 631 | " FROM x WHERE snip IS NOT NULL" |
| 632 | " ORDER BY substr(snip,1,9) DESC, date DESC;"); |
| 633 | @ <ol> |
| 634 | while( db_step(&q)==SQLITE_ROW ){ |
| 635 | const char *zUrl = db_column_text(&q, 0); |
| 636 | const char *zSnippet = db_column_text(&q, 1); |
| 637 | const char *zLabel = db_column_text(&q, 2); |
| @@ -676,40 +692,36 @@ | |
| 676 | /* |
| 677 | ** Return "search text" - a reduced version of a document appropriate for |
| 678 | ** full text search and/or for constructing a search result snippet. |
| 679 | ** |
| 680 | ** cType: d Embedded documentation |
| 681 | ** s Source code listing |
| 682 | ** w Wiki page |
| 683 | ** c Check-in comment |
| 684 | ** t Ticket text |
| 685 | ** e Event/Blog text |
| 686 | ** k Diff of a wiki |
| 687 | ** f Diff of a checkin |
| 688 | ** |
| 689 | ** zArg1, zArg2: Description of the document, depending on cType. |
| 690 | */ |
| 691 | void search_stext( |
| 692 | char cType, /* Type of document */ |
| 693 | const char *zArg1, /* First parameter */ |
| 694 | const char *zArg2, /* Second parameter */ |
| 695 | Blob *pOut /* OUT: Initialize to the search text */ |
| 696 | ){ |
| 697 | blob_init(pOut, 0, 0); |
| 698 | switch( cType ){ |
| 699 | case 'd': /* Doc. zArg1: RID of the file. zArg2: Filename */ |
| 700 | case 's': { /* Source. zArg1: RID of the file. zArg2: Filename */ |
| 701 | int rid = atoi(zArg1); |
| 702 | Blob doc; |
| 703 | content_get(rid, &doc); |
| 704 | blob_to_utf8_no_bom(&doc, 0); |
| 705 | get_stext_by_mimetype(&doc, mimetype_from_name(zArg2), pOut); |
| 706 | blob_reset(&doc); |
| 707 | break; |
| 708 | } |
| 709 | case 'w': { /* Wiki. zArg1: RID of the page. zArg2: Page name */ |
| 710 | int rid = atoi(zArg1); |
| 711 | Manifest *pWiki = manifest_get(rid, CFTYPE_WIKI,0); |
| 712 | Blob wiki; |
| 713 | if( pWiki==0 ) break; |
| 714 | blob_init(&wiki, pWiki->zWiki, -1); |
| 715 | get_stext_by_mimetype(&wiki, wiki_filter_mimetypes(pWiki->zMimetype), |
| @@ -716,12 +728,11 @@ | |
| 716 | pOut); |
| 717 | blob_reset(&wiki); |
| 718 | manifest_destroy(pWiki); |
| 719 | break; |
| 720 | } |
| 721 | case 'c': { /* Ckeckin: zArg1: RID of the checkin. zArg2: Not used */ |
| 722 | int rid = atoi(zArg1); |
| 723 | static Stmt q; |
| 724 | db_static_prepare(&q, |
| 725 | "SELECT coalesce(ecomment,comment)" |
| 726 | " ||' (user: '||coalesce(euser,user,'?')" |
| 727 | " ||', tags: '||" |
| @@ -741,46 +752,45 @@ | |
| 741 | } |
| 742 | } |
| 743 | } |
| 744 | |
| 745 | /* |
| 746 | ** The arguments cType,zArg1,zArg2 define an object that can be searched |
| 747 | ** for. Return a URL (relative to the root of the Fossil project) that |
| 748 | ** will jump to that document. |
| 749 | ** |
| 750 | ** Space to hold the returned string is obtained from mprintf() and should |
| 751 | ** be freed by the caller using fossil_free() or the equivalent. |
| 752 | */ |
| 753 | char *search_url( |
| 754 | char cType, /* Type of document */ |
| 755 | const char *zArg1, /* First parameter */ |
| 756 | const char *zArg2 /* Second parameter */ |
| 757 | ){ |
| 758 | char *zUrl = 0; |
| 759 | switch( cType ){ |
| 760 | case 'd': { /* Doc. zArg1: RID of the file. zArg2: Filename */ |
| 761 | case 's': /* Source. zArg1: RID of the file. zArg2: Filename */ |
| 762 | zUrl = db_text(0, |
| 763 | "SELECT printf('/doc/%%s%%s', substr(blob.uuid,20), %Q)" |
| 764 | " FROM mlink, blob" |
| 765 | " WHERE mlink.fid=%d AND mlink.mid=blob.rid", |
| 766 | zArg2, atoi(zArg1)); |
| 767 | break; |
| 768 | } |
| 769 | case 'w': { /* Wiki. zArg1: RID of the page. zArg2: Page name */ |
| 770 | char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d",atoi(zArg1)); |
| 771 | zUrl = mprintf("/wiki?id=%z&name=%t", zId, zArg2); |
| 772 | break; |
| 773 | } |
| 774 | case 'c': { /* Ckeckin: zArg1: RID of the checkin. zArg2: Not used */ |
| 775 | char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d",atoi(zArg1)); |
| 776 | zUrl = mprintf("/info/%z", zId); |
| 777 | break; |
| 778 | } |
| 779 | } |
| 780 | return zUrl; |
| 781 | } |
| 782 | |
| 783 | /* |
| 784 | ** COMMAND: test-search-stext |
| 785 | ** |
| 786 | ** Usage: fossil test-search-stext TYPE ARG1 ARG2 |
| @@ -787,11 +797,11 @@ | |
| 787 | */ |
| 788 | void test_search_stext(void){ |
| 789 | Blob out; |
| 790 | char *zUrl; |
| 791 | db_find_and_open_repository(0,0); |
| 792 | if( g.argc!=5 ) usage("TYPE ARG1 ARG2"); |
| 793 | search_stext(g.argv[2][0], g.argv[3], g.argv[4], &out); |
| 794 | zUrl = search_url(g.argv[2][0], g.argv[3], g.argv[4]); |
| 795 | fossil_print("%s\n%z\n",blob_str(&out),zUrl); |
| 796 | blob_reset(&out); |
| 797 | } |
| 798 |
| --- src/search.c | |
| +++ src/search.c | |
| @@ -424,13 +424,13 @@ | |
| 424 | int argc, |
| 425 | sqlite3_value **argv |
| 426 | ){ |
| 427 | Blob txt; |
| 428 | const char *zType = (const char*)sqlite3_value_text(argv[0]); |
| 429 | int rid = sqlite3_value_int(argv[1]); |
| 430 | const char *zName = (const char*)sqlite3_value_text(argv[2]); |
| 431 | search_stext(zType[0], rid, zName, &txt); |
| 432 | sqlite3_result_text(context, blob_materialize(&txt), -1, fossil_free); |
| 433 | } |
| 434 | |
| 435 | /* |
| 436 | ** Encode a string for use as a query parameter in a URL |
| @@ -556,18 +556,32 @@ | |
| 556 | ** * Search wiki |
| 557 | */ |
| 558 | void search_page(void){ |
| 559 | const char *zPattern = PD("s",""); |
| 560 | Stmt q; |
| 561 | int okCheckin; |
| 562 | int okDoc; |
| 563 | int okTicket; |
| 564 | int okWiki; |
| 565 | int allOff; |
| 566 | const char *zDisable; |
| 567 | |
| 568 | login_check_credentials(); |
| 569 | okCheckin = g.perm.Read && db_get_boolean("search-ci",0); |
| 570 | okDoc = g.perm.Read && db_get_boolean("search-doc",0); |
| 571 | okTicket = g.perm.RdTkt && db_get_boolean("search-tkt",0); |
| 572 | okWiki = g.perm.RdWiki && db_get_boolean("search-wiki",0); |
| 573 | allOff = (okCheckin + okDoc + okTicket + okWiki == 0); |
| 574 | zDisable = allOff ? " disabled" : ""; |
| 575 | zPattern = allOff ? "" : PD("s",""); |
| 576 | style_header("Search"); |
| 577 | @ <form method="GET" action="search"><center> |
| 578 | @ <input type="text" name="s" size="40" value="%h(zPattern)"%s(zDisable)> |
| 579 | @ <input type="submit" value="Search"%s(zDisable)> |
| 580 | if( allOff ){ |
| 581 | @ <p class="generalError">Search is disabled</p> |
| 582 | } |
| 583 | @ </center></form> |
| 584 | while( fossil_isspace(zPattern[0]) ) zPattern++; |
| 585 | if( zPattern[0] ){ |
| 586 | search_sql_setup(g.db); |
| 587 | add_content_sql_commands(g.db); |
| @@ -575,28 +589,30 @@ | |
| 589 | SRCHFLG_STATIC|SRCHFLG_HTML|SRCHFLG_SCORE); |
| 590 | db_multi_exec( |
| 591 | "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;" |
| 592 | "CREATE TEMP TABLE x(label TEXT,url TEXT,date TEXT,snip TEXT);" |
| 593 | ); |
| 594 | if( okDoc ){ |
| 595 | char *zDocGlob = db_get("doc-glob",""); |
| 596 | char *zDocBr = db_get("doc-branch","trunk"); |
| 597 | if( zDocGlob && zDocGlob[0] && zDocBr && zDocBr[0] ){ |
| 598 | db_multi_exec( |
| 599 | "INSERT INTO x(label,url,date,snip)" |
| 600 | " SELECT printf('Document: %%s',foci.filename)," |
| 601 | " printf('%R/doc/%T/%%s',foci.filename)," |
| 602 | " (SELECT datetime(event.mtime) FROM event" |
| 603 | " WHERE objid=symbolic_name_to_rid('trunk'))," |
| 604 | " snippet(stext('d',blob.rid,foci.filename))" |
| 605 | " FROM foci CROSS JOIN blob" |
| 606 | " WHERE checkinID=symbolic_name_to_rid('trunk')" |
| 607 | " AND blob.uuid=foci.uuid" |
| 608 | " AND %z", |
| 609 | zDocBr, glob_expr("foci.filename", zDocGlob) |
| 610 | ); |
| 611 | } |
| 612 | } |
| 613 | if( okWiki ){ |
| 614 | db_multi_exec( |
| 615 | "WITH wiki(name,rid,mtime) AS (" |
| 616 | " SELECT substr(tagname,6), tagxref.rid, max(tagxref.mtime)" |
| 617 | " FROM tag, tagxref" |
| 618 | " WHERE tag.tagname GLOB 'wiki-*'" |
| @@ -609,11 +625,11 @@ | |
| 625 | " datetime(mtime)," |
| 626 | " snippet(stext('w',rid,name))" |
| 627 | " FROM wiki;" |
| 628 | ); |
| 629 | } |
| 630 | if( okCheckin ){ |
| 631 | db_multi_exec( |
| 632 | "WITH ckin(uuid,rid,mtime) AS (" |
| 633 | " SELECT blob.uuid, event.objid, event.mtime" |
| 634 | " FROM event, blob" |
| 635 | " WHERE event.type='ci'" |
| @@ -627,11 +643,11 @@ | |
| 643 | " FROM ckin;" |
| 644 | ); |
| 645 | } |
| 646 | db_prepare(&q, "SELECT url, substr(snip,9), label" |
| 647 | " FROM x WHERE snip IS NOT NULL" |
| 648 | " ORDER BY substr(snip,1,8) DESC, date DESC;"); |
| 649 | @ <ol> |
| 650 | while( db_step(&q)==SQLITE_ROW ){ |
| 651 | const char *zUrl = db_column_text(&q, 0); |
| 652 | const char *zSnippet = db_column_text(&q, 1); |
| 653 | const char *zLabel = db_column_text(&q, 2); |
| @@ -676,40 +692,36 @@ | |
| 692 | /* |
| 693 | ** Return "search text" - a reduced version of a document appropriate for |
| 694 | ** full text search and/or for constructing a search result snippet. |
| 695 | ** |
| 696 | ** cType: d Embedded documentation |
| 697 | ** w Wiki page |
| 698 | ** c Check-in comment |
| 699 | ** t Ticket text |
| 700 | ** |
| 701 | ** rid The RID of an artifact that defines the object |
| 702 | ** being searched. |
| 703 | ** |
| 704 | ** zName Name of the object being searched. |
| 705 | */ |
| 706 | void search_stext( |
| 707 | char cType, /* Type of document */ |
| 708 | int rid, /* BLOB.RID or TAG.TAGID value for document */ |
| 709 | const char *zName, /* Name of the document */ |
| 710 | Blob *pOut /* OUT: Initialize to the search text */ |
| 711 | ){ |
| 712 | blob_init(pOut, 0, 0); |
| 713 | switch( cType ){ |
| 714 | case 'd': { /* Documents */ |
| 715 | Blob doc; |
| 716 | content_get(rid, &doc); |
| 717 | blob_to_utf8_no_bom(&doc, 0); |
| 718 | get_stext_by_mimetype(&doc, mimetype_from_name(zName), pOut); |
| 719 | blob_reset(&doc); |
| 720 | break; |
| 721 | } |
| 722 | case 'w': { /* Wiki */ |
| 723 | Manifest *pWiki = manifest_get(rid, CFTYPE_WIKI,0); |
| 724 | Blob wiki; |
| 725 | if( pWiki==0 ) break; |
| 726 | blob_init(&wiki, pWiki->zWiki, -1); |
| 727 | get_stext_by_mimetype(&wiki, wiki_filter_mimetypes(pWiki->zMimetype), |
| @@ -716,12 +728,11 @@ | |
| 728 | pOut); |
| 729 | blob_reset(&wiki); |
| 730 | manifest_destroy(pWiki); |
| 731 | break; |
| 732 | } |
| 733 | case 'c': { /* Ckeck-in Comments */ |
| 734 | static Stmt q; |
| 735 | db_static_prepare(&q, |
| 736 | "SELECT coalesce(ecomment,comment)" |
| 737 | " ||' (user: '||coalesce(euser,user,'?')" |
| 738 | " ||', tags: '||" |
| @@ -741,46 +752,45 @@ | |
| 752 | } |
| 753 | } |
| 754 | } |
| 755 | |
| 756 | /* |
| 757 | ** The arguments cType,rid,zName define an object that can be searched |
| 758 | ** for. Return a URL (relative to the root of the Fossil project) that |
| 759 | ** will jump to that document. |
| 760 | ** |
| 761 | ** Space to hold the returned string is obtained from mprintf() and should |
| 762 | ** be freed by the caller using fossil_free() or the equivalent. |
| 763 | */ |
| 764 | char *search_url( |
| 765 | char cType, /* Type of document */ |
| 766 | int rid, /* BLOB.RID or TAG.TAGID for the object */ |
| 767 | const char *zName /* Name of the object */ |
| 768 | ){ |
| 769 | char *zUrl = 0; |
| 770 | switch( cType ){ |
| 771 | case 'd': { /* Documents */ |
| 772 | zUrl = db_text(0, |
| 773 | "SELECT printf('/doc/%%s%%s', substr(blob.uuid,20), %Q)" |
| 774 | " FROM mlink, blob" |
| 775 | " WHERE mlink.fid=%d AND mlink.mid=blob.rid", |
| 776 | zName, rid); |
| 777 | break; |
| 778 | } |
| 779 | case 'w': { /* Wiki */ |
| 780 | char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 781 | zUrl = mprintf("/wiki?id=%z&name=%t", zId, zName); |
| 782 | break; |
| 783 | } |
| 784 | case 'c': { /* Ckeck-in Comment */ |
| 785 | char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 786 | zUrl = mprintf("/info/%z", zId); |
| 787 | break; |
| 788 | } |
| 789 | } |
| 790 | return zUrl; |
| 791 | } |
| 792 | |
| 793 | /* |
| 794 | ** COMMAND: test-search-stext |
| 795 | ** |
| 796 | ** Usage: fossil test-search-stext TYPE ARG1 ARG2 |
| @@ -787,11 +797,11 @@ | |
| 797 | */ |
| 798 | void test_search_stext(void){ |
| 799 | Blob out; |
| 800 | char *zUrl; |
| 801 | db_find_and_open_repository(0,0); |
| 802 | if( g.argc!=5 ) usage("TYPE RID NAME"); |
| 803 | search_stext(g.argv[2][0], atoi(g.argv[3]), g.argv[4], &out); |
| 804 | zUrl = search_url(g.argv[2][0], atoi(g.argv[3]), g.argv[4]); |
| 805 | fossil_print("%s\n%z\n",blob_str(&out),zUrl); |
| 806 | blob_reset(&out); |
| 807 | } |
| 808 |
+55
-1
| --- src/setup.c | ||
| +++ src/setup.c | ||
| @@ -70,11 +70,11 @@ | ||
| 70 | 70 | @ <p class="generalError"><b>Configuration Error:</b> Please add |
| 71 | 71 | @ <tt><base href="$secureurl/$current_page"></tt> after |
| 72 | 72 | @ <tt><head></tt> in the <a href="setup_header">HTML header</a>!</p> |
| 73 | 73 | } |
| 74 | 74 | |
| 75 | - @ <table border="0" cellspacing="7"> | |
| 75 | + @ <table border="0" cellspacing="3"> | |
| 76 | 76 | setup_menu_entry("Users", "setup_ulist", |
| 77 | 77 | "Grant privileges to individual users."); |
| 78 | 78 | setup_menu_entry("Access", "setup_access", |
| 79 | 79 | "Control access settings."); |
| 80 | 80 | setup_menu_entry("Configuration", "setup_config", |
| @@ -86,10 +86,12 @@ | ||
| 86 | 86 | setup_menu_entry("Login-Group", "setup_login_group", |
| 87 | 87 | "Manage single sign-on between this repository and others" |
| 88 | 88 | " on the same server"); |
| 89 | 89 | setup_menu_entry("Tickets", "tktsetup", |
| 90 | 90 | "Configure the trouble-ticketing system for this repository"); |
| 91 | + setup_menu_entry("Search","srchsetup", | |
| 92 | + "Configure the built-in search engine"); | |
| 91 | 93 | setup_menu_entry("Transfers", "xfersetup", |
| 92 | 94 | "Configure the transfer system for this repository"); |
| 93 | 95 | setup_menu_entry("Skins", "setup_skin", |
| 94 | 96 | "Select from a menu of prepackaged \"skins\" for the web interface"); |
| 95 | 97 | setup_menu_entry("CSS", "setup_editcss", |
| @@ -2155,5 +2157,57 @@ | ||
| 2155 | 2157 | if(limit>0 && counter<limit){ |
| 2156 | 2158 | @ <div>%d(counter) entries shown.</div> |
| 2157 | 2159 | } |
| 2158 | 2160 | style_footer(); |
| 2159 | 2161 | } |
| 2162 | + | |
| 2163 | +/* | |
| 2164 | +** WEBPAGE: srchsetup | |
| 2165 | +** | |
| 2166 | +** Configure the search engine. | |
| 2167 | +*/ | |
| 2168 | +void page_srchsetup(){ | |
| 2169 | + login_check_credentials(); | |
| 2170 | + if( !g.perm.Setup && !g.perm.Admin ){ | |
| 2171 | + login_needed(); | |
| 2172 | + } | |
| 2173 | + style_header("Search Configuration"); | |
| 2174 | + @ <form action="%s(g.zTop)/srchsetup" method="post"><div> | |
| 2175 | + login_insert_csrf_secret(); | |
| 2176 | + @ <div style="text-align:center;font-weight:bold;"> | |
| 2177 | + @ Server-specific settings that affect the | |
| 2178 | + @ <a href="%R/help?cmd=/search">/search</a> webpage. | |
| 2179 | + @ </div> | |
| 2180 | + @ <hr /> | |
| 2181 | + textarea_attribute("Document Glob List", 3, 35, "doc-glob", "dg", "", 0); | |
| 2182 | + @ <p>The "Document Glob List" is a comma- or newline-separated list | |
| 2183 | + @ of GLOB expressions that identify all documents within the source | |
| 2184 | + @ tree that are to be searched when "Document Search" is enabled. | |
| 2185 | + @ Some examples: | |
| 2186 | + @ <table border=0 cellpadding=2 align=center> | |
| 2187 | + @ <tr><td>*.wiki,*.html,*.md,*.txt<td style="width: 4x;"> | |
| 2188 | + @ <td>Search all wiki, HTML, Markdown, and Text files</tr> | |
| 2189 | + @ <tr><td>doc/*.md,*/README.txt,README.txt<td> | |
| 2190 | + @ <td>Search all Markdown files in the doc/ subfolder and all README.txt | |
| 2191 | + @ files.</tr> | |
| 2192 | + @ <tr><td>*<td><td>Search all checked-in files</tr> | |
| 2193 | + @ <tr><td><i>(blank)</i><td> | |
| 2194 | + @ <td>Search nothing. (Disables document search).</tr> | |
| 2195 | + @ </table> | |
| 2196 | + @ <hr /> | |
| 2197 | + entry_attribute("Document Branch", 20, "doc-branch", "db", "trunk", 0); | |
| 2198 | + @ <p>When searching documents, use the versions of the files found at the | |
| 2199 | + @ type of the "Document Branch" branch. Recommended value: "trunk". | |
| 2200 | + @ Document search is disabled if blank. | |
| 2201 | + @ <hr/> | |
| 2202 | + onoff_attribute("Search Check-in Comments", "search-ci", "sc", 0, 0); | |
| 2203 | + @ <br> | |
| 2204 | + onoff_attribute("Search Documents", "search-doc", "sd", 0, 0); | |
| 2205 | + @ <br> | |
| 2206 | + onoff_attribute("Search Tickets", "search-tkt", "st", 0, 0); | |
| 2207 | + @ <br> | |
| 2208 | + onoff_attribute("Search Wiki","search-wiki", "sw", 0, 0); | |
| 2209 | + @ <hr/> | |
| 2210 | + @ <p><input type="submit" name="submit" value="Apply Changes" /></p> | |
| 2211 | + @ </div></form> | |
| 2212 | + style_footer(); | |
| 2213 | +} | |
| 2160 | 2214 |
| --- src/setup.c | |
| +++ src/setup.c | |
| @@ -70,11 +70,11 @@ | |
| 70 | @ <p class="generalError"><b>Configuration Error:</b> Please add |
| 71 | @ <tt><base href="$secureurl/$current_page"></tt> after |
| 72 | @ <tt><head></tt> in the <a href="setup_header">HTML header</a>!</p> |
| 73 | } |
| 74 | |
| 75 | @ <table border="0" cellspacing="7"> |
| 76 | setup_menu_entry("Users", "setup_ulist", |
| 77 | "Grant privileges to individual users."); |
| 78 | setup_menu_entry("Access", "setup_access", |
| 79 | "Control access settings."); |
| 80 | setup_menu_entry("Configuration", "setup_config", |
| @@ -86,10 +86,12 @@ | |
| 86 | setup_menu_entry("Login-Group", "setup_login_group", |
| 87 | "Manage single sign-on between this repository and others" |
| 88 | " on the same server"); |
| 89 | setup_menu_entry("Tickets", "tktsetup", |
| 90 | "Configure the trouble-ticketing system for this repository"); |
| 91 | setup_menu_entry("Transfers", "xfersetup", |
| 92 | "Configure the transfer system for this repository"); |
| 93 | setup_menu_entry("Skins", "setup_skin", |
| 94 | "Select from a menu of prepackaged \"skins\" for the web interface"); |
| 95 | setup_menu_entry("CSS", "setup_editcss", |
| @@ -2155,5 +2157,57 @@ | |
| 2155 | if(limit>0 && counter<limit){ |
| 2156 | @ <div>%d(counter) entries shown.</div> |
| 2157 | } |
| 2158 | style_footer(); |
| 2159 | } |
| 2160 |
| --- src/setup.c | |
| +++ src/setup.c | |
| @@ -70,11 +70,11 @@ | |
| 70 | @ <p class="generalError"><b>Configuration Error:</b> Please add |
| 71 | @ <tt><base href="$secureurl/$current_page"></tt> after |
| 72 | @ <tt><head></tt> in the <a href="setup_header">HTML header</a>!</p> |
| 73 | } |
| 74 | |
| 75 | @ <table border="0" cellspacing="3"> |
| 76 | setup_menu_entry("Users", "setup_ulist", |
| 77 | "Grant privileges to individual users."); |
| 78 | setup_menu_entry("Access", "setup_access", |
| 79 | "Control access settings."); |
| 80 | setup_menu_entry("Configuration", "setup_config", |
| @@ -86,10 +86,12 @@ | |
| 86 | setup_menu_entry("Login-Group", "setup_login_group", |
| 87 | "Manage single sign-on between this repository and others" |
| 88 | " on the same server"); |
| 89 | setup_menu_entry("Tickets", "tktsetup", |
| 90 | "Configure the trouble-ticketing system for this repository"); |
| 91 | setup_menu_entry("Search","srchsetup", |
| 92 | "Configure the built-in search engine"); |
| 93 | setup_menu_entry("Transfers", "xfersetup", |
| 94 | "Configure the transfer system for this repository"); |
| 95 | setup_menu_entry("Skins", "setup_skin", |
| 96 | "Select from a menu of prepackaged \"skins\" for the web interface"); |
| 97 | setup_menu_entry("CSS", "setup_editcss", |
| @@ -2155,5 +2157,57 @@ | |
| 2157 | if(limit>0 && counter<limit){ |
| 2158 | @ <div>%d(counter) entries shown.</div> |
| 2159 | } |
| 2160 | style_footer(); |
| 2161 | } |
| 2162 | |
| 2163 | /* |
| 2164 | ** WEBPAGE: srchsetup |
| 2165 | ** |
| 2166 | ** Configure the search engine. |
| 2167 | */ |
| 2168 | void page_srchsetup(){ |
| 2169 | login_check_credentials(); |
| 2170 | if( !g.perm.Setup && !g.perm.Admin ){ |
| 2171 | login_needed(); |
| 2172 | } |
| 2173 | style_header("Search Configuration"); |
| 2174 | @ <form action="%s(g.zTop)/srchsetup" method="post"><div> |
| 2175 | login_insert_csrf_secret(); |
| 2176 | @ <div style="text-align:center;font-weight:bold;"> |
| 2177 | @ Server-specific settings that affect the |
| 2178 | @ <a href="%R/help?cmd=/search">/search</a> webpage. |
| 2179 | @ </div> |
| 2180 | @ <hr /> |
| 2181 | textarea_attribute("Document Glob List", 3, 35, "doc-glob", "dg", "", 0); |
| 2182 | @ <p>The "Document Glob List" is a comma- or newline-separated list |
| 2183 | @ of GLOB expressions that identify all documents within the source |
| 2184 | @ tree that are to be searched when "Document Search" is enabled. |
| 2185 | @ Some examples: |
| 2186 | @ <table border=0 cellpadding=2 align=center> |
| 2187 | @ <tr><td>*.wiki,*.html,*.md,*.txt<td style="width: 4x;"> |
| 2188 | @ <td>Search all wiki, HTML, Markdown, and Text files</tr> |
| 2189 | @ <tr><td>doc/*.md,*/README.txt,README.txt<td> |
| 2190 | @ <td>Search all Markdown files in the doc/ subfolder and all README.txt |
| 2191 | @ files.</tr> |
| 2192 | @ <tr><td>*<td><td>Search all checked-in files</tr> |
| 2193 | @ <tr><td><i>(blank)</i><td> |
| 2194 | @ <td>Search nothing. (Disables document search).</tr> |
| 2195 | @ </table> |
| 2196 | @ <hr /> |
| 2197 | entry_attribute("Document Branch", 20, "doc-branch", "db", "trunk", 0); |
| 2198 | @ <p>When searching documents, use the versions of the files found at the |
| 2199 | @ type of the "Document Branch" branch. Recommended value: "trunk". |
| 2200 | @ Document search is disabled if blank. |
| 2201 | @ <hr/> |
| 2202 | onoff_attribute("Search Check-in Comments", "search-ci", "sc", 0, 0); |
| 2203 | @ <br> |
| 2204 | onoff_attribute("Search Documents", "search-doc", "sd", 0, 0); |
| 2205 | @ <br> |
| 2206 | onoff_attribute("Search Tickets", "search-tkt", "st", 0, 0); |
| 2207 | @ <br> |
| 2208 | onoff_attribute("Search Wiki","search-wiki", "sw", 0, 0); |
| 2209 | @ <hr/> |
| 2210 | @ <p><input type="submit" name="submit" value="Apply Changes" /></p> |
| 2211 | @ </div></form> |
| 2212 | style_footer(); |
| 2213 | } |
| 2214 |