1
0
mirror of https://github.com/gnosygnu/xowa.git synced 2024-10-27 20:34:16 +00:00

HTTP Server: Support 'action=edit' / 'action=html' [#264]

Also:
* Change XOWA GUI to use 'action' query args, instead of xowa-cmd
* Remove nginx http_variables due to compiler warnings
This commit is contained in:
gnosygnu 2018-11-03 11:04:03 -04:00
parent 6e361414f5
commit dca2357067
9 changed files with 212 additions and 36 deletions

View File

@ -20,11 +20,10 @@ public class Http_request_parser {
private int type, content_length;
private byte[] url, protocol, host, user_agent, accept, accept_language,
accept_encoding, x_requested_with, cookie, referer, content_type,
content_type_boundary, connection, pragma, cache_control, origin,
upgrade_request, x_host, x_real_ip;
content_type_boundary, connection, pragma, cache_control, origin;
private Http_post_data_hash post_data_hash;
private final Bry_bfr tmp_bfr = Bry_bfr_.New_w_size(255); private final Btrie_rv trv = new Btrie_rv();
private final Http_server_wtr server_wtr; private final boolean log;
private final Bry_bfr tmp_bfr = Bry_bfr_.New_w_size(255); private final Btrie_rv trv = new Btrie_rv();
private final Http_server_wtr server_wtr; private final boolean log;
public Http_request_parser(Http_server_wtr server_wtr, boolean log) {this.server_wtr = server_wtr; this.log = log;}
public void Clear() {
this.dnt = false;
@ -33,7 +32,6 @@ public class Http_request_parser {
= this.accept_language = this.accept_encoding = this.x_requested_with = this.cookie
= this.referer = this.content_type = this.content_type_boundary
= this.connection = this.pragma = this.cache_control = this.origin
= this.upgrade_request = this.x_host = this.x_real_ip
= null;
this.post_data_hash = null;
}
@ -89,9 +87,9 @@ public class Http_request_parser {
case Tid_pragma: this.pragma = Bry_.Mid(line, val_bgn, line_len); break;
case Tid_cache_control: this.cache_control = Bry_.Mid(line, val_bgn, line_len); break;
case Tid_origin: this.origin = Bry_.Mid(line, val_bgn, line_len); break;
case Tid_upgrade_request: this.upgrade_request = Bry_.Mid(line, val_bgn, line_len); break;
case Tid_x_host: this.x_host = Bry_.Mid(line, val_bgn, line_len); break;
case Tid_x_real_ip: this.x_real_ip = Bry_.Mid(line, val_bgn, line_len); break;
case Tid_upgrade_request: break;
case Tid_x_host: break;
case Tid_x_real_ip: break;
case Tid_accept_charset: break;
default: throw Err_.new_unhandled(tid);
}
@ -157,7 +155,7 @@ public class Http_request_parser {
private static final int Tid_get = 1, Tid_post = 2, Tid_host = 3, Tid_user_agent = 4, Tid_accept = 5, Tid_accept_language = 6, Tid_accept_encoding = 7, Tid_dnt = 8
, Tid_x_requested_with = 9, Tid_cookie = 10, Tid_referer = 11, Tid_content_length = 12, Tid_content_type = 13, Tid_connection = 14, Tid_pragma = 15, Tid_cache_control = 16
, Tid_origin = 17, Tid_accept_charset = 188, Tid_upgrade_request = 19, Tid_x_host = 20, Tid_x_real_ip = 21;
private static final Btrie_slim_mgr trie = Btrie_slim_mgr.ci_a7()
private static final Btrie_slim_mgr trie = Btrie_slim_mgr.ci_a7()
.Add_str_int("GET" , Tid_get)
.Add_str_int("POST" , Tid_post)
.Add_str_int("Host:" , Tid_host)
@ -180,7 +178,7 @@ public class Http_request_parser {
.Add_str_int("X-Host:" , Tid_x_host)
.Add_str_int("X-Real-IP:" , Tid_x_real_ip)
;
private static final byte[] Tkn_boundary = Bry_.new_a7("boundary="), Tkn_content_type_boundary_end = Bry_.new_a7("--")
private static final byte[] Tkn_boundary = Bry_.new_a7("boundary="), Tkn_content_type_boundary_end = Bry_.new_a7("--")
, Tkn_content_disposition = Bry_.new_a7("Content-Disposition:"), Tkn_form_data = Bry_.new_a7("form-data;")
, Tkn_name = Bry_.new_a7("name=")
;

View File

@ -28,7 +28,9 @@ public class Xoa_url_ {
Qarg__redirect = Bry_.new_a7("redirect")
, Qarg__redirect__no = Bry_.new_a7("no")
, Qarg__action = Bry_.new_a7("action")
, Qarg__action__read = Bry_.new_a7("read")
, Qarg__action__edit = Bry_.new_a7("edit")
, Qarg__action__html = Bry_.new_a7("html")
, Qarg__curid = Bry_.new_a7("curid")
;
}

View File

@ -32,6 +32,7 @@ package gplx.xowa.apps.servers.http; import gplx.*; import gplx.xowa.*; import g
import gplx.core.threads.*; import gplx.core.net.*; import gplx.core.primitives.*; import gplx.core.envs.*;
import gplx.langs.jsons.*; import gplx.langs.htmls.encoders.*;
import gplx.xowa.wikis.pages.*;
import gplx.xowa.addons.wikis.searchs.gui.htmlbars.*;
public class Http_server_mgr implements Gfo_invk {
private final Object thread_lock = new Object();
private final Gfo_usr_dlg usr_dlg;
@ -76,7 +77,7 @@ public class Http_server_mgr implements Gfo_invk {
}
else
Note("HTTP Server not started");
}
}
running = val;
}
public void Run() {
@ -90,7 +91,7 @@ public class Http_server_mgr implements Gfo_invk {
String cmd = url_converter.Decode_str(url_encoded_str);
app.Gfs_mgr().Run_str(cmd);
}
public String Parse_page_to_html(Http_data__client data__client, byte[] wiki_domain, byte[] ttl_bry) {
public String Parse_page_to_html(Http_data__client data__client, byte[] wiki_domain, byte[] ttl_bry, byte mode) {
synchronized (thread_lock) {
// create a shim gui to automatically handle default XOWA gui JS calls
if (init_gui_needed) {
@ -101,7 +102,7 @@ public class Http_server_mgr implements Gfo_invk {
// get the wiki
Xowe_wiki wiki = (Xowe_wiki)app.Wiki_mgr().Get_by_or_make_init_y(wiki_domain); // assert init for Main_Page; EX:click zh.w on wiki sidebar; DATE:2015-07-19
if (Runtime_.Memory_total() > Io_mgr.Len_gb) Xowe_wiki_.Rls_mem(wiki, true); // release memory at 1 GB; DATE:2015-09-11
// get the url / ttl
if (Bry_.Len_eq_0(ttl_bry)) ttl_bry = wiki.Props().Main_page();
Xoa_url url = wiki.Utl__url_parser().Parse(ttl_bry);
@ -118,7 +119,7 @@ public class Http_server_mgr implements Gfo_invk {
page.Html_data().Head_mgr().Itm__server().Init_by_http(data__client).Enabled_y_();
// generate html
String rv = String_.new_u8(wiki.Html_mgr().Page_wtr_mgr().Gen(page, Xopg_page_.Tid_read)); // NOTE: must generate HTML now in order for "wait" and "async_server" to work with text_dbs; DATE:2016-07-10
String rv = String_.new_u8(wiki.Html_mgr().Page_wtr_mgr().Gen(page, mode)); // NOTE: must generate HTML now in order for "wait" and "async_server" to work with text_dbs; DATE:2016-07-10
boolean rebuild_html = false;
switch (retrieve_mode) {
case File_retrieve_mode.Mode_skip: // noop
@ -133,7 +134,7 @@ public class Http_server_mgr implements Gfo_invk {
page = wiki.Page_mgr().Load_page(url, ttl, tab); // HACK: fetch page again so that HTML will now include img data
break;
}
if (rebuild_html) rv = String_.new_u8(wiki.Html_mgr().Page_wtr_mgr().Gen(page, Xopg_page_.Tid_read));
if (rebuild_html) rv = String_.new_u8(wiki.Html_mgr().Page_wtr_mgr().Gen(page, mode));
return rv;
}
}

View File

@ -18,6 +18,7 @@ import gplx.core.ios.*; import gplx.core.ios.streams.*;
import gplx.core.primitives.*; import gplx.core.net.*; import gplx.langs.htmls.encoders.*;
import gplx.xowa.apps.*;
import gplx.xowa.htmls.js.*;
import gplx.xowa.wikis.pages.*;
class Http_server_wkr implements Gfo_invk {
private final int uid;
private final Http_server_mgr server_mgr;
@ -98,20 +99,15 @@ class Http_server_wkr implements Gfo_invk {
app.Http_server().Run_xowa_cmd(app, String_.new_u8(cmd));
}
private void Write_wiki(byte[] req) {
String wiki_domain = ""; String page_name = "";
String[] req_split = String_.Split(String_.new_u8(req), "/");
if(req_split.length >= 1){
wiki_domain = req_split[1];
Http_url_parser url_parser = new Http_url_parser(url_encoder);
String page_html = "";
if (!url_parser.Parse(req)) {
page_html = url_parser.Err_msg();
}
if(req_split.length >= 4){
page_name = req_split[3];
for(int i = 4; i <= req_split.length-1; i++){
page_name += "/" + req_split[i];
}
page_name = url_encoder.Decode_str(page_name);
else {
page_html = app.Http_server().Parse_page_to_html(data__client, url_parser.Wiki(), url_parser.Page(), url_parser.Action());
page_html = Convert_page(page_html, root_dir_http, String_.new_u8(url_parser.Wiki()));
}
String page_html = app.Http_server().Parse_page_to_html(data__client, Bry_.new_u8(wiki_domain), Bry_.new_u8(page_name));
page_html = Convert_page(page_html, root_dir_http, wiki_domain);
Xosrv_http_wkr_.Write_response_as_html(client_wtr, Bool_.N, page_html);
}
private void Process_post(Http_request_itm request) {

View File

@ -0,0 +1,99 @@
/*
XOWA: the XOWA Offline Wiki Application
Copyright (C) 2012-2017 gnosygnu@gmail.com
XOWA is licensed under the terms of the General Public License (GPL) Version 3,
or alternatively under the terms of the Apache License Version 2.0.
You may use XOWA according to either of these licenses as is most appropriate
for your project on a case-by-case basis.
The terms of each license can be found in the source code repository:
GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
*/
package gplx.xowa.apps.servers.http; import gplx.*; import gplx.xowa.*; import gplx.xowa.apps.*; import gplx.xowa.apps.servers.*;
import gplx.langs.htmls.encoders.*;
import gplx.xowa.htmls.hrefs.*;
import gplx.xowa.wikis.pages.*;
class Http_url_parser {
private final Gfo_url_encoder url_encoder;
public Http_url_parser(Gfo_url_encoder url_encoder) {
this.url_encoder = url_encoder;
}
public byte[] Wiki() {return wiki;} public Http_url_parser Wiki_(String v) {this.wiki = Bry_.new_u8(v); return this;} private byte[] wiki;
public byte[] Page() {return page;} public Http_url_parser Page_(String v) {this.page = Bry_.new_u8(v); return this;} private byte[] page;
public byte Action() {return action;} public Http_url_parser Action_(byte v) {this.action = v; return this;} private byte action;
public String Err_msg() {return err_msg;} public Http_url_parser Err_msg_(String v) {this.err_msg = v; return this;} private String err_msg;
public String To_str() {
Bry_bfr bfr = Bry_bfr_.New();
bfr.Add_str_a7("wiki=").Add_safe(wiki).Add_byte_nl();
bfr.Add_str_a7("page=").Add_safe(page).Add_byte_nl();
bfr.Add_str_a7("action=").Add_byte(action).Add_byte_nl();
bfr.Add_str_a7("err_msg=").Add_str_u8_null(err_msg).Add_byte_nl();
return bfr.To_str_and_clear();
}
// Parse urls of form "/wiki_name/wiki/Page_name?action=val"
public boolean Parse(byte[] url) {
try {
// initial validations
if (url == null) return Fail(url, "invalid url; url is null");
int url_len = url.length;
if (url_len == 0) return Fail(url, "invalid url; url is empty");
if (url[0] != Byte_ascii.Slash) return Fail(url, "invalid url; must start with '/'");
// get wiki
int wiki_bgn = 1; // skip initial "/"
int wiki_end = Bry_find_.Find_fwd_or(url, Byte_ascii.Slash, wiki_bgn, url_len, url_len);
this.wiki = Bry_.Mid(url, wiki_bgn, wiki_end);
if (wiki_end == url_len) {// no slash found; url is wiki-only; EX: "/en.wikipedia.org"
return true;
}
// get page after "/wiki/"
byte[] wiki_separator = Xoh_href_.Bry__wiki;
int page_bgn = wiki_end + wiki_separator.length;
if (!Bry_.Eq(url, wiki_end, wiki_end + wiki_separator.length, Xoh_href_.Bry__wiki))
return Fail(url, "invalid url; must have '/wiki/' after wiki_domain");
int page_end = url_len;
// search for action arg
this.action = Xopg_page_.Tid_read;
int action_key_bgn = Bry_find_.Find_bwd(url, Qarg__action__frag, url_len);
if (action_key_bgn != Bry_find_.Not_found) {
int action_val_bgn = action_key_bgn + Qarg__action__frag.length;
int action_val_end = url_len;
boolean trim_page = true;
if (Bry_.Eq(url, action_val_bgn, action_val_end, Xoa_url_.Qarg__action__read))
this.action = Xopg_page_.Tid_read;
else if (Bry_.Eq(url, action_val_bgn, action_val_end, Xoa_url_.Qarg__action__edit))
this.action = Xopg_page_.Tid_edit;
else if (Bry_.Eq(url, action_val_bgn, action_val_end, Xoa_url_.Qarg__action__html))
this.action = Xopg_page_.Tid_html;
else
trim_page = false;
if (trim_page)
page_end = action_key_bgn;
}
this.page = url_encoder.Decode(Bry_.Mid(url, page_bgn, page_end));
return true;
}
catch (Exception e) {
this.err_msg = Err_.Message_gplx_log(e);
return false;
}
}
private boolean Fail(byte[] url, String err_msg) {
this.wiki = null;
this.page = null;
this.err_msg = err_msg + "; url=" + String_.new_u8(url);
return false;
}
private static final byte[]
Qarg__action__frag = Bry_.Add(Byte_ascii.Question_bry, Xoa_url_.Qarg__action, Byte_ascii.Eq_bry) // "?action="
;
}

View File

@ -0,0 +1,65 @@
/*
XOWA: the XOWA Offline Wiki Application
Copyright (C) 2012-2017 gnosygnu@gmail.com
XOWA is licensed under the terms of the General Public License (GPL) Version 3,
or alternatively under the terms of the Apache License Version 2.0.
You may use XOWA according to either of these licenses as is most appropriate
for your project on a case-by-case basis.
The terms of each license can be found in the source code repository:
GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
*/
package gplx.xowa.apps.servers.http; import gplx.*; import gplx.xowa.*; import gplx.xowa.apps.*; import gplx.xowa.apps.servers.*;
import org.junit.*; import gplx.core.tests.*;
import gplx.langs.htmls.encoders.*;
import gplx.xowa.wikis.pages.*;
public class Http_url_parser_tst {
private final Http_url_parser_fxt fxt = new Http_url_parser_fxt();
@Test public void Parse() {
// wiki-only
fxt.Test__parse("/en.wikipedia.org", fxt.Make().Wiki_("en.wikipedia.org"));
// wiki + page
fxt.Test__parse("/en.wikipedia.org/wiki/Page_1", fxt.Make().Wiki_("en.wikipedia.org").Page_("Page_1"));
// url-decoded; "%20" -> " "
fxt.Test__parse("/en.wikipedia.org/wiki/Page%201", fxt.Make().Wiki_("en.wikipedia.org").Page_("Page 1"));
// page with multiple slashes
fxt.Test__parse("/en.wikipedia.org/wiki/Page_1/A/B/C", fxt.Make().Wiki_("en.wikipedia.org").Page_("Page_1/A/B/C"));
// action=edit
fxt.Test__parse("/en.wikipedia.org/wiki/Page_1?action=edit", fxt.Make().Wiki_("en.wikipedia.org").Page_("Page_1").Action_(Xopg_page_.Tid_edit));
// action=html
fxt.Test__parse("/en.wikipedia.org/wiki/Page_1?action=html", fxt.Make().Wiki_("en.wikipedia.org").Page_("Page_1").Action_(Xopg_page_.Tid_html));
// action=N/A
fxt.Test__parse("/en.wikipedia.org/wiki/Page_1?action=a", fxt.Make().Wiki_("en.wikipedia.org").Page_("Page_1?action=a"));
// fail: null
fxt.Test__parse(null, fxt.Make().Err_msg_("invalid url; url is null; url="));
// fail: empty
fxt.Test__parse("", fxt.Make().Err_msg_("invalid url; url is empty; url="));
// fail: missing '/' at start
fxt.Test__parse("en.wikipedia.org", fxt.Make().Err_msg_("invalid url; must start with '/'; url=en.wikipedia.org"));
// fail: missing '/wiki/'
fxt.Test__parse("/en.wikipedia.org/Page_1", fxt.Make().Err_msg_("invalid url; must have '/wiki/' after wiki_domain; url=/en.wikipedia.org/Page_1"));
}
}
class Http_url_parser_fxt {
private final Gfo_url_encoder url_encoder = Gfo_url_encoder_.New__http_url().Make();
public Http_url_parser Make() {return new Http_url_parser(url_encoder);}
public void Test__parse(String url, Http_url_parser expd) {
Http_url_parser actl = new Http_url_parser(url_encoder);
actl.Parse(url == null ? null : Bry_.new_u8(url));
Gftest.Eq__ary__lines(expd.To_str(), actl.To_str());
}
}

View File

@ -23,8 +23,17 @@ public class Xog_tab_itm_read_mgr {
public static void Show_page(Xog_tab_itm tab, Xoae_page new_page, boolean reset_to_read, boolean new_page_is_same, boolean show_is_err, byte history_nav_type) {
if (reset_to_read)
tab.View_mode_(Xopg_page_.Tid_read);
if (new_page.Url().Qargs_mgr().Match(Xoa_url_.Qarg__action, Xoa_url_.Qarg__action__edit))
tab.View_mode_(Xopg_page_.Tid_edit);
// set View_mode based on "action="; DATE:2018-11-03
byte[] action_val = new_page.Url().Qargs_mgr().Get_val_bry_or(Xoa_url_.Qarg__action, Xoa_url_.Qarg__action__read);
byte view_mode = Xopg_page_.Tid_read;
if (Bry_.Eq(action_val, Xoa_url_.Qarg__action__read))
view_mode = Xopg_page_.Tid_read;
else if (Bry_.Eq(action_val, Xoa_url_.Qarg__action__edit))
view_mode = Xopg_page_.Tid_edit;
else if (Bry_.Eq(action_val, Xoa_url_.Qarg__action__html))
view_mode = Xopg_page_.Tid_html;
tab.View_mode_(view_mode);
Xoae_page cur_page = tab.Page(); Xog_html_itm html_itm = tab.Html_itm(); Gfui_html html_box = html_itm.Html_box();
Xog_win_itm win = tab.Tab_mgr().Win();

View File

@ -129,7 +129,7 @@ public class Xow_portal_mgr implements Gfo_invk {
Xow_ns ns = ns_mgr.Ids_get_or_null(ns_id);
return ns == null || ns.Exists() ? Bry_.Empty : missing_ns_cls;
}
public byte[] Div_view_bry(Bry_bfr_mkr bfr_mkr, byte output_tid, byte[] search_text) {
public byte[] Div_view_bry(Bry_bfr_mkr bfr_mkr, byte output_tid, byte[] search_text, Xoa_ttl ttl) {
Bry_bfr tmp_bfr = bfr_mkr.Get_k004();
byte[] read_cls = Bry_.Empty, edit_cls = Bry_.Empty, html_cls = Bry_.Empty;
switch (output_tid) {
@ -137,7 +137,14 @@ public class Xow_portal_mgr implements Gfo_invk {
case Xopg_page_.Tid_edit: edit_cls = Cls_selected_y; break;
case Xopg_page_.Tid_html: html_cls = Cls_selected_y; break;
}
div_view_fmtr.Bld_bfr_many(tmp_bfr, read_cls, edit_cls, html_cls, search_text);
// build url_fragment with action query argument; EX: "/wiki/Page_name?action="
byte[] url_frag_w_action_qarg = Bry_.Add(Xoh_href_.Bry__wiki, ttl.Page_db(), Byte_ascii.Question_bry, Xoa_url_.Qarg__action, Byte_ascii.Eq_bry);
div_view_fmtr.Bld_bfr_many(tmp_bfr, read_cls, edit_cls, html_cls, search_text
, ttl.Page_db()
, Bry_.Add(url_frag_w_action_qarg, Xoa_url_.Qarg__action__edit)
, Bry_.Add(url_frag_w_action_qarg, Xoa_url_.Qarg__action__html));
return tmp_bfr.To_bry_and_rls();
} public static final byte[] Cls_selected_y = Bry_.new_a7("selected"), Cls_new = Bry_.new_a7("new"), Cls_display_none = Bry_.new_a7("xowa_display_none");
public byte[] Div_logo_bry(boolean nightmode) {return nightmode ? div_logo_night : div_logo_day;} private byte[] div_logo_day = Bry_.Empty, div_logo_night = Bry_.Empty;
@ -161,7 +168,7 @@ public class Xow_portal_mgr implements Gfo_invk {
private final Bry_fmtr
div_personal_fmtr = Bry_fmtr.new_("~{portal_personal_subj_href};~{portal_personal_subj_text};~{portal_personal_talk_cls};~{portal_personal_talk_href};~{portal_personal_talk_cls};", "portal_personal_subj_href", "portal_personal_subj_text", "portal_personal_subj_cls", "portal_personal_talk_href", "portal_personal_talk_cls")
, div_ns_fmtr = Bry_fmtr.new_("~{portal_ns_subj_href};~{portal_ns_subj_cls};~{portal_ns_talk_href};~{portal_ns_talk_cls};~{portal_div_vnts}", "portal_ns_subj_href", "portal_ns_subj_cls", "portal_ns_talk_href", "portal_ns_talk_cls", "portal_div_vnts")
, div_view_fmtr = Bry_fmtr.new_("", "portal_view_read_cls", "portal_view_edit_cls", "portal_view_html_cls", "search_text")
, div_view_fmtr = Bry_fmtr.new_("", "portal_view_read_cls", "portal_view_edit_cls", "portal_view_html_cls", "search_text", "portal_view_read_href", "portal_view_edit_href", "portal_view_html_href")
, div_logo_fmtr = Bry_fmtr.new_("", "portal_nav_main_href", "portal_logo_url")
, div_sync_fmtr = Bry_fmtr.new_("", "page_url")
, div_wikis_fmtr = Bry_fmtr.new_("", "toggle_btn", "toggle_hdr")

View File

@ -164,10 +164,9 @@ app.wikis.get('~{wiki_key}').html.portal {
<div id="p-views" class="vectorTabs">
<h3>~{<>msgs.get('views');<>}</h3>
<ul>
<li id="ca-view" class="~{portal_view_read_cls}"><span><a href="xowa-cmd:app.gui.main_win.page_view_read;">~{<>msgs.get('vector-view-view');<>}</a></span></li>
<li id="ca-edit" class="~{portal_view_edit_cls}"><span><a href="xowa-cmd:app.gui.main_win.page_view_edit;"~{<>msgs.get_html_accesskey_and_title('ca-edit');<>}>~{<>msgs.get('vector-view-edit');<>}</a></span></li>
<li id="ca-history" class="~{portal_view_html_cls}"><span><a href="xowa-cmd:app.gui.main_win.page_view_html;"~{<>msgs.get_html_accesskey_and_title('xowa-portal-view_html');<>}>~{<>msgs.get('xowa-portal-view_html');<>}</a></span>
</li>
<li id="ca-view" class="~{portal_view_read_cls}"><span><a href="~{portal_view_read_href}" class="xowa-hover-off">~{<>msgs.get('vector-view-view');<>}</a></span></li>
<li id="ca-edit" class="~{portal_view_edit_cls}"><span><a href="~{portal_view_edit_href}" class="xowa-hover-off"~{<>msgs.get_html_accesskey_and_title('ca-edit');<>}>~{<>msgs.get('vector-view-edit');<>}</a></span></li>
<li id="ca-history" class="~{portal_view_html_cls}"><span><a href="~{portal_view_html_href}" class="xowa-hover-off"~{<>msgs.get_html_accesskey_and_title('xowa-portal-view_html');<>}>~{<>msgs.get('xowa-portal-view_html');<>}</a></span></li>
<li id="p-search" role="search">
<form id="searchform" action="/wiki/~{<>app.gui.win_opts.search_url;<>}">
<div id="simpleSearch">