1 /*
2 * Copyright (C) 2008 Emweb bv, Herent, Belgium.
3 *
4 * See the LICENSE file for terms of use.
5 */
6
7 #include <boost/algorithm/string.hpp>
8 #include <regex>
9 #include <map>
10
11 #include "Wt/WApplication.h"
12 #include "Wt/WLinkedCssStyleSheet.h"
13 #include "Wt/WLoadingIndicator.h"
14 #include "Wt/WContainerWidget.h"
15 #include "Wt/WRandom.h"
16 #include "Wt/WWebWidget.h"
17 #include "Wt/WStringStream.h"
18 #include "Wt/WTheme.h"
19 #include "Wt/Utils.h"
20
21 #include "Configuration.h"
22 #include "DomElement.h"
23 #include "EscapeOStream.h"
24 #include "FileServe.h"
25 #include "WebController.h"
26 #include "WebRenderer.h"
27 #include "WebRequest.h"
28 #include "WebSession.h"
29 #include "WebUtils.h"
30 #include "StringUtils.h"
31
32 #ifdef WT_WIN32
33 #include <process.h> // for getpid()
34 #ifdef min
35 #undef min
36 #endif
37 #endif
38
39 #ifndef WT_TARGET_JAVA
40 #define DESCRIBE(w) typeid(*(w)).name()
41 #else
42 #define DESCRIBE(w) "(fixme)"
43 #endif
44
45 #ifdef WT_TARGET_JAVA
46 #define RETHROW(e) throw e
47 #else
48 #define RETHROW(e) throw
49 #endif
50
51 namespace {
52
isAbsoluteUrl(const std::string & url)53 bool isAbsoluteUrl(const std::string& url) {
54 return url.find("://") != std::string::npos;
55 }
56
appendAttribute(Wt::EscapeOStream & eos,const std::string & name,const std::string & value)57 void appendAttribute(Wt::EscapeOStream& eos,
58 const std::string& name,
59 const std::string& value) {
60 eos << ' ' << name << "=\"";
61 eos.pushEscape(Wt::EscapeOStream::HtmlAttribute);
62 eos << value;
63 eos.popEscape();
64 eos << '"';
65 }
66
closeSpecial(Wt::WStringStream & s)67 void closeSpecial(Wt::WStringStream& s) {
68 s << ">\n";
69 }
70
closeSpecial(Wt::EscapeOStream & s)71 void closeSpecial(Wt::EscapeOStream& s) {
72 s << ">\n";
73 }
74 }
75
76 namespace skeletons {
77 extern const char *Boot_html1;
78 extern const char *Plain_html1;
79 extern const char *Hybrid_html1;
80 extern const char *Wt_js1;
81 extern const char *Boot_js1;
82 extern const char *JQuery_js1;
83
84 extern std::vector<const char *> JQuery_js();
85 extern std::vector<const char *> Wt_js();
86 }
87
88 namespace Wt {
89
90 LOGGER("WebRenderer");
91
CookieValue()92 WebRenderer::CookieValue::CookieValue()
93 : secure(false)
94 { }
95
CookieValue(const std::string & v,const std::string & p,const std::string & d,const WDateTime & e,bool s)96 WebRenderer::CookieValue::CookieValue(const std::string& v,
97 const std::string& p,
98 const std::string& d,
99 const WDateTime& e,
100 bool s)
101 : value(v),
102 path(p),
103 domain(d),
104 expires(e),
105 secure(s)
106 { }
107
WebRenderer(WebSession & session)108 WebRenderer::WebRenderer(WebSession& session)
109 : session_(session),
110 visibleOnly_(true),
111 rendered_(false),
112 initialStyleRendered_(false),
113 twoPhaseThreshold_(5000),
114 pageId_(0),
115 ackErrs_(0),
116 expectedAckId_(0),
117 scriptId_(0),
118 linkedCssCount_(-1),
119 currentStatelessSlotIsActuallyStateless_(true),
120 formObjectsChanged_(true),
121 updateLayout_(false),
122 cookieUpdateNeeded_(false),
123 learning_(false)
124 { }
125
setRendered(bool how)126 void WebRenderer::setRendered(bool how)
127 {
128 if (rendered_ != how) {
129 LOG_DEBUG("setRendered: " << how);
130 rendered_ = how;
131 }
132 }
133
setTwoPhaseThreshold(int bytes)134 void WebRenderer::setTwoPhaseThreshold(int bytes)
135 {
136 twoPhaseThreshold_ = bytes;
137 }
138
needUpdate(WWidget * w,bool laterOnly)139 void WebRenderer::needUpdate(WWidget *w, bool laterOnly)
140 {
141 LOG_DEBUG("needUpdate: " << w->id() << " (" << DESCRIBE(w) << ")");
142
143 updateMap_.insert(w);
144
145 if (!laterOnly)
146 moreUpdates_ = true;
147 }
148
doneUpdate(WWidget * w)149 void WebRenderer::doneUpdate(WWidget *w)
150 {
151 LOG_DEBUG("doneUpdate: " << w->id() << " (" << DESCRIBE(w) << ")");
152
153 updateMap_.erase(w);
154 }
155
isDirty()156 bool WebRenderer::isDirty() const
157 {
158 return !updateMap_.empty()
159 || formObjectsChanged_
160 || session_.app()->hasQuit()
161 || !session_.app()->afterLoadJavaScript_.empty()
162 || session_.app()->serverPushChanged_
163 || session_.app()->styleSheetsAdded_
164 || !session_.app()->styleSheetsToRemove_.empty()
165 || session_.app()->styleSheet().isDirty()
166 || session_.app()->internalPathIsChanged_
167 || !collectedJS1_.empty()
168 || !collectedJS2_.empty()
169 || !invisibleJS_.empty()
170 || !wsRequestsToHandle_.empty()
171 || cookieUpdateNeeded_;
172 }
173
formObjects()174 const WebRenderer::FormObjectsMap& WebRenderer::formObjects() const
175 {
176 return currentFormObjects_;
177 }
178
bodyClassRtl()179 std::string WebRenderer::bodyClassRtl() const
180 {
181 if (session_.app()) {
182 std::string s = session_.app()->bodyClass_;
183 if (!s.empty())
184 s += ' ';
185
186 s += session_.app()->layoutDirection() == LayoutDirection::LeftToRight
187 ? "Wt-ltr" : "Wt-rtl";
188
189 session_.app()->bodyHtmlClassChanged_ = false;
190
191 return s;
192 } else
193 return std::string();
194 }
195
saveChanges()196 void WebRenderer::saveChanges()
197 {
198 collectedJS1_ << invisibleJS_.str();
199 invisibleJS_.clear();
200 collectJS(&collectedJS1_);
201 }
202
discardChanges()203 void WebRenderer::discardChanges()
204 {
205 collectJS(nullptr);
206 }
207
ackUpdate(unsigned int updateId)208 WebRenderer::AckState WebRenderer::ackUpdate(unsigned int updateId)
209 {
210 /*
211 * If we are using an unreliable transport, then we remember
212 * JavaScript updates until they are ack'ed. This works because
213 * requests are not pipelined.
214 *
215 * WebSocket requests are pipelined so this simple mechanism will
216 * not work. When switching from web sockets to AJAX or vice-versa ?
217 *
218 * If normal AJAX request -> web socket closes. We assume everything
219 * got delivered and start doing ack updates again.
220 *
221 * If web socket request -> we assume last AJAX request got
222 * delivered ?
223 */
224 LOG_DEBUG("ackUpdate: expecting " << expectedAckId_ << ", received " << updateId);
225 if (updateId == expectedAckId_) {
226 LOG_DEBUG("jsSynced(false) after ackUpdate okay");
227 setJSSynced(false);
228 ackErrs_ = 0;
229 return CorrectAck;
230 } else if (expectedAckId_ - updateId < 5) {
231 ++ackErrs_;
232 return ackErrs_ < 3 ? ReasonableAck : BadAck; // That's still acceptible but no longer plausible
233 } else
234 return BadAck;
235 }
236
letReloadJS(WebResponse & response,bool newSession,bool embedded)237 void WebRenderer::letReloadJS(WebResponse& response, bool newSession,
238 bool embedded)
239 {
240 if (!embedded) {
241 setCaching(response, false);
242 setHeaders(response, "text/javascript; charset=UTF-8");
243 }
244
245 // FIXME: we should foresee something independent of app->javaScriptClass()
246 response.out() <<
247 "if (window.Wt) window.Wt._p_.quit(null); window.location.reload(true);";
248 }
249
letReloadHTML(WebResponse & response,bool newSession)250 void WebRenderer::letReloadHTML(WebResponse& response, bool newSession)
251 {
252 setCaching(response, false);
253 setHeaders(response, "text/html; charset=UTF-8");
254
255 response.out() << "<html><script type=\"text/javascript\">";
256 letReloadJS(response, newSession, true);
257 response.out() << "</script><body></body></html>";
258 }
259
streamRedirectJS(WStringStream & out,const std::string & redirect)260 void WebRenderer::streamRedirectJS(WStringStream& out,
261 const std::string& redirect)
262 {
263 if (session_.app() && session_.app()->internalPathIsChanged_)
264 out << "if (window." << session_.app()->javaScriptClass() << ") "
265 << session_.app()->javaScriptClass()
266 << "._p_.setHash("
267 << WWebWidget::jsStringLiteral(session_.app()->newInternalPath_)
268 << ", false);\n";
269 out <<
270 "if (window.location.replace)"
271 " window.location.replace(" << WWebWidget::jsStringLiteral(redirect) << ");"
272 "else"
273 " window.location.href=" << WWebWidget::jsStringLiteral(redirect) << ";\n";
274 }
275
serveResponse(WebResponse & response)276 void WebRenderer::serveResponse(WebResponse& response)
277 {
278 session_.setTriggerUpdate(false);
279
280 switch (response.responseType()) {
281 case WebResponse::ResponseType::Update:
282 serveJavaScriptUpdate(response);
283 break;
284 case WebResponse::ResponseType::Page:
285 initialStyleRendered_ = false;
286 ++pageId_;
287 if (session_.app())
288 serveMainpage(response);
289 else
290 serveBootstrap(response);
291 break;
292 case WebResponse::ResponseType::Script:
293 bool hybridPage = session_.progressiveBoot() || session_.env().ajax();
294 if (!hybridPage)
295 setRendered(false);
296 serveMainscript(response);
297 break;
298 }
299 }
300
setPageVars(FileServe & page)301 void WebRenderer::setPageVars(FileServe& page)
302 {
303 WApplication *app = session_.app();
304
305 page.setVar("DOCTYPE", session_.docType());
306
307 std::string htmlAttr;
308 if (app && !app->htmlClass_.empty()) {
309 htmlAttr = " class=\"" + app->htmlClass_ + "\"";
310 }
311
312 if (session_.env().agentIsIE())
313 page.setVar("HTMLATTRIBUTES",
314 "xmlns:v=\"urn:schemas-microsoft-com:vml\""
315 " lang=\"en\" dir=\"ltr\"" + htmlAttr);
316 else
317 page.setVar("HTMLATTRIBUTES", "lang=\"en\" dir=\"ltr\"" + htmlAttr);
318 page.setVar("METACLOSE", ">");
319
320 std::string attr = bodyClassRtl();
321
322 if (!attr.empty())
323 attr = " class=\"" + attr + "\"";
324
325 if (app && app->layoutDirection() == LayoutDirection::RightToLeft)
326 attr += " dir=\"RTL\"";
327
328 page.setVar("BODYATTRIBUTES", attr);
329
330 page.setVar("HEADDECLARATIONS", headDeclarations());
331
332 page.setCondition("FORM", !session_.env().agentIsSpiderBot()
333 && !session_.env().ajax());
334 page.setCondition("BOOT_STYLE", true);
335 }
336
streamBootContent(WebResponse & response,FileServe & boot,bool hybrid)337 void WebRenderer::streamBootContent(WebResponse& response,
338 FileServe& boot, bool hybrid)
339 {
340 Configuration& conf = session_.controller()->configuration();
341
342 WStringStream out(response.out());
343
344 boot.setVar("BLANK_HTML",
345 session_.bootstrapUrl(response,
346 WebSession::BootstrapOption::ClearInternalPath)
347 + "&request=resource&resource=blank");
348 boot.setVar("SESSION_ID", session_.sessionId());
349 //TODO remove APP_CLASS, will later only be used in the javascript
350 boot.setVar("APP_CLASS", "Wt");
351
352 boot.streamUntil(out, "BOOT_JS");
353
354 if (!(hybrid && session_.app()->hasQuit())) {
355 FileServe bootJs(skeletons::Boot_js1);
356
357 bootJs.setVar("SELF_URL",
358 safeJsStringLiteral
359 (session_.bootstrapUrl
360 (response, WebSession::BootstrapOption::ClearInternalPath)));
361 bootJs.setVar("SESSION_ID", session_.sessionId());
362
363 expectedAckId_ = scriptId_ = WRandom::get();
364 ackErrs_ = 0;
365
366 bootJs.setVar("SCRIPT_ID", scriptId_);
367 bootJs.setVar("RANDOMSEED", WRandom::get());
368 bootJs.setVar("RELOAD_IS_NEWSESSION", conf.reloadIsNewSession());
369 bootJs.setVar("USE_COOKIES",
370 conf.sessionTracking() == Configuration::CookiesURL);
371 bootJs.setVar("AJAX_CANONICAL_URL",
372 safeJsStringLiteral(session_.ajaxCanonicalUrl(response)));
373 bootJs.setVar("APP_CLASS", "Wt");
374 bootJs.setVar("PATH_INFO", safeJsStringLiteral
375 (session_.pagePathInfo_));
376
377 bootJs.setCondition("COOKIE_CHECKS", conf.cookieChecks());
378 bootJs.setCondition("SPLIT_SCRIPT", conf.splitScript());
379 bootJs.setCondition("HYBRID", hybrid);
380 bootJs.setCondition("PROGRESS", hybrid && !session_.env().ajax());
381 bootJs.setCondition("DEFER_SCRIPT", true);
382 bootJs.setCondition("WEBGL_DETECT", conf.webglDetect());
383
384 std::string internalPath
385 = hybrid ? session_.app()->internalPath() : session_.env().internalPath();
386 bootJs.setVar("INTERNAL_PATH", safeJsStringLiteral(internalPath));
387
388 bootJs.stream(out);
389 }
390
391 out.spool(response.out());
392 }
393
serveLinkedCss(WebResponse & response)394 void WebRenderer::serveLinkedCss(WebResponse& response)
395 {
396 response.setContentType("text/css");
397
398 if (!initialStyleRendered_) {
399 WApplication *app = session_.app();
400
401 WStringStream out(response.out());
402
403 if (app->theme())
404 app->theme()->serveCss(out);
405
406 for (unsigned i = 0; i < app->styleSheets_.size(); ++i)
407 app->styleSheets_[i].cssText(out);
408
409 app->styleSheetsAdded_ = 0;
410
411 initialStyleRendered_ = true;
412 linkedCssCount_ = app->styleSheets_.size();
413
414 out.spool(response.out());
415 } else if (linkedCssCount_ > -1) {
416 /*
417 * Make sure we serve the same response again, since a 'GET' must be
418 *idempotent. This is used by e.g. browser-side tools like 'usersnap'
419 */
420 WApplication *app = session_.app();
421
422 WStringStream out(response.out());
423
424 if (app->theme())
425 app->theme()->serveCss(out);
426
427 unsigned count
428 = std::min((std::size_t)linkedCssCount_, app->styleSheets_.size());
429
430 for (unsigned i = 0; i < count; ++i)
431 app->styleSheets_[i].cssText(out);
432
433 out.spool(response.out());
434 }
435 }
436
serveBootstrap(WebResponse & response)437 void WebRenderer::serveBootstrap(WebResponse& response)
438 {
439 Configuration& conf = session_.controller()->configuration();
440
441 FileServe boot(skeletons::Boot_html1);
442 setPageVars(boot);
443
444 WStringStream noJsRedirectUrl;
445 DomElement::htmlAttributeValue
446 (noJsRedirectUrl,
447 session_.bootstrapUrl(response,
448 WebSession::BootstrapOption::KeepInternalPath) + "&js=no");
449
450 boot.setVar("REDIRECT_URL", noJsRedirectUrl.str());
451 boot.setVar("AUTO_REDIRECT",
452 "<noscript><meta http-equiv=\"refresh\" content=\"0; url="
453 + noJsRedirectUrl.str() + "\"></noscript>");
454 boot.setVar("NOSCRIPT_TEXT", conf.redirectMessage());
455
456 WStringStream bootStyleUrl;
457 DomElement::htmlAttributeValue
458 (bootStyleUrl,
459 session_.bootstrapUrl(response,
460 WebSession::BootstrapOption::ClearInternalPath)
461 + "&request=style&page=" + std::to_string(pageId_));
462
463 boot.setVar("BOOT_STYLE_URL", bootStyleUrl.str());
464
465 setCaching(response, false);
466 response.addHeader("X-Frame-Options", "SAMEORIGIN");
467
468 std::string contentType = "text/html; charset=UTF-8";
469
470 setHeaders(response, contentType);
471
472 WStringStream out(response.out());
473 streamBootContent(response, boot, false);
474 boot.stream(out);
475
476 setRendered(false);
477
478 out.spool(response.out());
479 }
480
serveError(int status,WebResponse & response,const std::string & message)481 void WebRenderer::serveError(int status, WebResponse& response,
482 const std::string& message)
483 {
484 bool js = response.responseType() != WebResponse::ResponseType::Page;
485
486 WApplication *app = session_.app();
487 if (!js || !app) {
488 response.setStatus(status);
489 response.setContentType("text/html");
490 response.out()
491 << "<title>Error occurred.</title>"
492 << "<h2>Error occurred.</h2>"
493 << WWebWidget::escapeText(WString(message), true).toUTF8()
494 << '\n';
495 } else {
496 response.out() << app->javaScriptClass()
497 << "._p_.quit(null);"
498 << "document.title = 'Error occurred.';"
499 << "document.body.innerHtml='<h2>Error occurred.</h2>' +"
500 << WWebWidget::jsStringLiteral(message)
501 << ';';
502 }
503 }
504
setCookie(const std::string name,const std::string value,const WDateTime & expires,const std::string domain,const std::string path,bool secure)505 void WebRenderer::setCookie(const std::string name, const std::string value,
506 const WDateTime& expires,
507 const std::string domain, const std::string path,
508 bool secure)
509 {
510 cookiesToSet_[name] = CookieValue(value, path, domain, expires, secure);
511 cookieUpdateNeeded_ = true;
512 }
513
setCaching(WebResponse & response,bool allowCache)514 void WebRenderer::setCaching(WebResponse& response, bool allowCache)
515 {
516 if (allowCache)
517 response.addHeader("Cache-Control", "max-age=2592000,private");
518 else {
519 response.addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
520 response.addHeader("Pragma", "no-cache");
521 response.addHeader("Expires", "0");
522 }
523 }
524
setHeaders(WebResponse & response,const std::string mimeType)525 void WebRenderer::setHeaders(WebResponse& response, const std::string mimeType)
526 {
527 for (std::map<std::string, CookieValue>::const_iterator
528 i = cookiesToSet_.begin(); i != cookiesToSet_.end(); ++i) {
529 const CookieValue& cookie = i->second;
530
531 WStringStream header;
532
533 std::string value = cookie.value;
534 if (value.empty())
535 value = "deleted";
536
537 header << Utils::urlEncode(i->first) << '=' << Utils::urlEncode(value)
538 << "; Version=1;";
539
540 if (!cookie.expires.isNull()) {
541 #ifndef WT_TARGET_JAVA
542 std::string formatString = "ddd, dd-MMM-yyyy hh:mm:ss 'GMT'";
543 #else
544 std::string formatString = "EEE, dd-MMM-yyyy HH:mm:ss 'GMT'";
545 #endif
546
547 std::string d
548 = cookie.expires.toString
549 (WString::fromUTF8(formatString), false).toUTF8();
550
551 header << "Expires=" << d << ';';
552 }
553
554 if (!cookie.domain.empty())
555 header << " Domain=" << cookie.domain << ';';
556
557 if (cookie.path.empty())
558 if (!session_.env().publicDeploymentPath_.empty())
559 header << " Path=" << session_.env().publicDeploymentPath_ << ';';
560 else
561 header << " Path=" << session_.env().deploymentPath() << ';';
562 else
563 header << " Path=" << cookie.path << ';';
564
565 header << " httponly;";
566
567 if (cookie.secure)
568 header << " secure;";
569
570 response.addHeader("Set-Cookie", header.str());
571 }
572 cookiesToSet_.clear();
573 cookieUpdateNeeded_ = false;
574
575 response.setContentType(mimeType);
576 }
577
renderSetServerPush(WStringStream & out)578 void WebRenderer::renderSetServerPush(WStringStream& out)
579 {
580 if (session_.app()->serverPushChanged_) {
581 out << session_.app()->javaScriptClass()
582 << "._p_.setServerPush("
583 << session_.app()->updatesEnabled()
584 << ");";
585
586 session_.app()->serverPushChanged_ = false;
587 }
588 }
589
sessionUrl()590 std::string WebRenderer::sessionUrl() const
591 {
592 std::string result = session_.applicationUrl();
593 if (isAbsoluteUrl(result))
594 return session_.appendSessionQuery(result);
595 else {
596 // Wt.js will prepend the correct deployment path
597 return session_.appendSessionQuery(".").substr(1);
598 }
599 }
600
serveJavaScriptUpdate(WebResponse & response)601 void WebRenderer::serveJavaScriptUpdate(WebResponse& response)
602 {
603 if (!response.isWebSocketMessage()) {
604 setCaching(response, false);
605 setHeaders(response, "text/javascript; charset=UTF-8");
606 }
607
608 if (session_.sessionIdChanged_) {
609 collectedJS1_ << session_.app()->javaScriptClass()
610 << "._p_.setSessionUrl("
611 << WWebWidget::jsStringLiteral(sessionUrl())
612 << ");";
613 }
614
615 WStringStream out(response.out());
616
617 if (!rendered_) {
618 serveMainAjax(out);
619 } else {
620 collectJavaScript();
621
622 addResponseAckPuzzle(out);
623 renderSetServerPush(out);
624
625 LOG_DEBUG("js: " << collectedJS1_.str() << collectedJS2_.str());
626
627 out << collectedJS1_.str() << collectedJS2_.str();
628
629 if (response.isWebSocketMessage()) {
630 renderCookieUpdate(out);
631 renderWsRequestsDone(out);
632
633 LOG_DEBUG("jsSynced(false) after rendering websocket message");
634 setJSSynced(false);
635 }
636 }
637
638 out.spool(response.out());
639 }
640
renderWsRequestsDone(WStringStream & out)641 void WebRenderer::renderWsRequestsDone(WStringStream &out)
642 {
643 if (!wsRequestsToHandle_.empty()) {
644 out << session_.app()->javaScriptClass()
645 << "._p_.wsRqsDone(";
646 for (std::size_t i = 0; i < wsRequestsToHandle_.size(); ++i) {
647 if (i != 0)
648 out << ',';
649 out << wsRequestsToHandle_[i];
650 }
651 out << ");";
652 wsRequestsToHandle_.clear();
653 }
654
655 }
656
updateMultiSessionCookie(const WebRequest & request)657 void WebRenderer::updateMultiSessionCookie(const WebRequest &request)
658 {
659 Configuration &conf = session_.controller()->configuration();
660 setCookie("ms" + request.scriptName(),
661 session_.multiSessionId(),
662 WDateTime::currentDateTime().addSecs(conf.multiSessionCookieTimeout()),
663 "", "",
664 session_.env().urlScheme() == "https");
665 }
666
renderCookieUpdate(WStringStream & out)667 void WebRenderer::renderCookieUpdate(WStringStream &out)
668 {
669 if (cookieUpdateNeeded_) {
670 out << session_.app()->javaScriptClass()
671 << "._p_.refreshCookie();";
672 cookieUpdateNeeded_ = false;
673 }
674 }
675
addContainerWidgets(WWebWidget * w,std::vector<WContainerWidget * > & result)676 void WebRenderer::addContainerWidgets(WWebWidget *w,
677 std::vector<WContainerWidget *>& result)
678 {
679 for (unsigned i = 0; i < w->children().size(); ++i) {
680 WWidget *c = w->children()[i];
681
682 if (!c->isRendered())
683 return;
684
685 if (!c->isHidden())
686 addContainerWidgets(c->webWidget(), result);
687
688 WContainerWidget *wc = dynamic_cast<WContainerWidget *>(c);
689 if (wc)
690 result.push_back(wc);
691 }
692 }
693
addResponseAckPuzzle(WStringStream & out)694 void WebRenderer::addResponseAckPuzzle(WStringStream& out)
695 {
696 std::string puzzle;
697
698 Configuration& conf = session_.controller()->configuration();
699 if (conf.ajaxPuzzle() && expectedAckId_ == scriptId_) {
700 /*
701 * We need to pick a random WContainerWidget. Let's be dumb for now.
702 */
703 std::vector<WContainerWidget *> widgets;
704
705 WApplication *app = session_.app();
706 addContainerWidgets(app->domRoot_.get(), widgets);
707 if (app->domRoot2_)
708 addContainerWidgets(app->domRoot2_.get(), widgets);
709
710 unsigned r = WRandom::get() % widgets.size();
711
712 WContainerWidget *wc = widgets[r];
713
714 puzzle = '"' + wc->id() + '"';
715
716 std::string l;
717 for (WWidget *w = wc->parent(); w; w = w->parent()) {
718 if (w->id().empty())
719 continue;
720 if (w->id() == l)
721 continue;
722
723 l = w->id();
724
725 if (!solution_.empty())
726 solution_ += ',';
727
728 solution_ += l;
729 }
730 }
731
732 /*
733 * Passing the expectedAckId_ within the collectedJS1_ +
734 * collectedJS2_ risks of inflating responses when a script loading
735 * is blocked. The purpose of the ackId is to detect what has been
736 * succesfully transmitted (mostly in the presence of server push
737 * which can cancel ajax requests. Therefore we chose here to use
738 * the ackIds_ only to signal proper ajax transfers, and thus at the
739 * end of the request transfer.
740 *
741 * It does present us with another probem: what if e.g. an ExtJS
742 * library is still loading and we already update one of its widgets
743 * assuming it has been rendered ? This should be handled
744 * client-side: only when libraries have been loaded, the application can
745 * continue. TO BE DONE.
746 */
747
748 ++expectedAckId_;
749 LOG_DEBUG("addResponseAckPuzzle: incremented expectedAckId to " << expectedAckId_);
750
751 out << session_.app()->javaScriptClass()
752 << "._p_.response(" << expectedAckId_;
753 if (!puzzle.empty())
754 out << "," << puzzle;
755 out << ");";
756 }
757
checkResponsePuzzle(const WebRequest & request)758 bool WebRenderer::checkResponsePuzzle(const WebRequest& request)
759 {
760 if (!solution_.empty()) {
761 const std::string *ackPuzzleE = request.getParameter("ackPuzzle");
762
763 if (!ackPuzzleE) {
764 LOG_SECURE("Ajax puzzle fail: solution missing");
765 return false;
766 }
767
768 std::string ackPuzzle = *ackPuzzleE;
769
770 Utils::SplitVector answer, solution;
771
772 boost::split(solution, solution_, boost::is_any_of(","));
773 boost::split(answer, ackPuzzle, boost::is_any_of(","));
774
775 unsigned j = 0;
776 bool fail = false;
777
778 for (unsigned i = 0; i < solution.size(); ++i) {
779 for (; j < answer.size(); ++j) {
780 if (solution[i] == answer[j])
781
782 break;
783 else {
784 /* Verify that answer[j] is not a valid widget id */
785 }
786 }
787
788 if (j == answer.size()) {
789 fail = true;
790 break;
791 }
792 }
793
794 if (j < answer.size() - 1)
795 fail = true;
796
797 if (fail) {
798 LOG_SECURE("Ajax puzzle fail: '" << ackPuzzle << "' vs '"
799 << solution_ << '\'');
800
801 solution_.clear();
802
803 return false;
804 } else {
805 solution_.clear();
806 return true;
807 }
808 } else
809 return true;
810 }
811
collectJavaScript()812 void WebRenderer::collectJavaScript()
813 {
814 WApplication *app = session_.app();
815 Configuration& conf = session_.controller()->configuration();
816
817 /*
818 * Pending invisible changes are also collected into JS1. This is
819 * also done in ackUpdate(), but just in case an update was not
820 * acknowledged.
821 *
822 * This is also used to render JavaScript that was rendered in asHtml()
823 * in a hybrid page.
824 */
825 LOG_DEBUG("Rendering invisible: " << invisibleJS_.str());
826
827 collectedJS1_ << invisibleJS_.str();
828 invisibleJS_.clear();
829
830 /*
831 * This opens scopes, waiting for new libraries to be loaded.
832 */
833 int librariesLoaded = loadScriptLibraries(collectedJS1_, app);
834
835 /*
836 * This closes the same scopes.
837 */
838 loadScriptLibraries(collectedJS2_, app, librariesLoaded);
839
840 /*
841 * Everything else happens inside JS1: after libraries have been loaded.
842 */
843 app->streamBeforeLoadJavaScript(collectedJS1_, false);
844
845 if (app->domRoot2_)
846 app->domRoot2_->rootAsJavaScript(app, collectedJS1_, false);
847
848 collectJavaScriptUpdate(collectedJS1_);
849
850 if (app->bodyHtmlClassChanged_) {
851 bool widgetset = session_.type() == EntryPointType::WidgetSet;
852 std::string op = widgetset ? "+=" : "=";
853 collectedJS1_ << "document.body.parentNode.className" << op << '\''
854 << app->htmlClass_ << "';"
855 << "document.body.className" << op << '\'' << bodyClassRtl() << "';"
856 << "document.body.setAttribute('dir', '";
857 if (app->layoutDirection() == LayoutDirection::LeftToRight)
858 collectedJS1_ << "LTR";
859 else
860 collectedJS1_ << "RTL";
861 collectedJS1_ << "');";
862 }
863
864 if (visibleOnly_) {
865 bool needFetchInvisible = false;
866
867 if (!updateMap_.empty()) {
868 needFetchInvisible = true;
869
870 if (twoPhaseThreshold_ > 0) {
871 /*
872 * See how large the invisible changes are, perhaps we can
873 * send them along
874 */
875 visibleOnly_ = false;
876
877 collectJavaScriptUpdate(invisibleJS_);
878
879 if (invisibleJS_.length() < (unsigned)twoPhaseThreshold_) {
880 collectedJS1_ << invisibleJS_.str();
881 invisibleJS_.clear();
882 needFetchInvisible = false;
883 }
884
885 visibleOnly_ = true;
886 }
887 }
888
889 if (needFetchInvisible)
890 collectedJS1_ << app->javaScriptClass()
891 << "._p_.update(null, 'none', null, false);";
892 }
893
894 if (conf.inlineCss())
895 app->styleSheet().javaScriptUpdate(app, collectedJS1_, false);
896
897 loadStyleSheets(collectedJS1_, app);
898
899 if (app->autoJavaScriptChanged_) {
900 collectedJS1_ << app->javaScriptClass()
901 << "._p_.autoJavaScript=function(){"
902 << app->autoJavaScript_ << "};";
903 app->autoJavaScriptChanged_ = false;
904 }
905
906 visibleOnly_ = true;
907
908 app->domRoot_->doneRerender();
909 if (app->domRoot2_)
910 app->domRoot2_->doneRerender();
911
912 std::string redirect = session_.getRedirect();
913 if (!redirect.empty())
914 streamRedirectJS(collectedJS1_, redirect);
915 }
916
serveMainscript(WebResponse & response)917 void WebRenderer::serveMainscript(WebResponse& response)
918 {
919 /*
920 * Serving a script is using a GET request, which can be replayed.
921 * Therefore we need to either be able to reconstruct the response
922 * (possible if !rendered_), or we need to save the response in
923 * collectedJS variables.
924 */
925 Configuration& conf = session_.controller()->configuration();
926 bool widgetset = session_.type() == EntryPointType::WidgetSet;
927
928 bool serveSkeletons = !conf.splitScript()
929 || response.getParameter("skeleton");
930 bool serveRest = !conf.splitScript() || !serveSkeletons;
931
932 session_.sessionIdChanged_ = false;
933
934 setCaching(response, conf.splitScript() && serveSkeletons);
935 setHeaders(response, "text/javascript; charset=UTF-8");
936
937 WStringStream out(response.out());
938
939 if (!widgetset) {
940 // FIXME: this cannot be replayed
941 std::string redirect = session_.getRedirect();
942
943 if (!redirect.empty()) {
944 streamRedirectJS(out, redirect);
945 out.spool(response.out());
946 return;
947 }
948 } else {
949 expectedAckId_ = scriptId_ = WRandom::get();
950 ackErrs_ = 0;
951 }
952
953 WApplication *app = session_.app();
954
955 const bool innerHtml = true;
956
957 if (serveSkeletons) {
958 bool haveJQuery = app->customJQuery();
959
960 if (!haveJQuery) {
961 out << "if (typeof window.$ === 'undefined') {";
962 #ifndef WT_TARGET_JAVA
963 std::vector<const char *> parts = skeletons::JQuery_js();
964 for (std::size_t i = 0; i < parts.size(); ++i)
965 out << const_cast<char *>(parts[i]);
966 #else
967 out << const_cast<char *>(skeletons::JQuery_js1);
968 #endif
969 out << '}';
970 }
971
972 #ifndef WT_TARGET_JAVA
973 std::vector<const char *> parts = skeletons::Wt_js();
974 #else
975 std::vector<const char *> parts = std::vector<const char *>();
976 #endif
977 std::string Wt_js_combined;
978 if (parts.size() > 1)
979 for (std::size_t i = 0; i < parts.size(); ++i)
980 Wt_js_combined += parts[i];
981
982 FileServe script(parts.size() > 1
983 ? Wt_js_combined.c_str() : skeletons::Wt_js1);
984
985 script.setCondition
986 ("CATCH_ERROR", conf.errorReporting() != Configuration::NoErrors);
987 script.setCondition
988 ("SHOW_ERROR", conf.errorReporting() == Configuration::ErrorMessage);
989 script.setCondition
990 ("UGLY_INTERNAL_PATHS", session_.useUglyInternalPaths());
991
992 #ifdef WT_DEBUG_JS
993 script.setCondition("DYNAMIC_JS", true);
994 #else
995 script.setCondition("DYNAMIC_JS", false);
996 #endif // WT_DEBUG_JS
997
998 script.setVar("WT_CLASS", WT_CLASS);
999 script.setVar("APP_CLASS", app->javaScriptClass());
1000 script.setCondition("STRICTLY_SERIALIZED_EVENTS", conf.serializedEvents());
1001 script.setCondition("WEB_SOCKETS", conf.webSockets());
1002 script.setVar("INNER_HTML", innerHtml);
1003 script.setVar("ACK_UPDATE_ID", expectedAckId_);
1004 script.setVar("SESSION_URL", WWebWidget::jsStringLiteral(sessionUrl()));
1005 script.setVar("QUITTED_STR",
1006 WString::tr("Wt.QuittedMessage").jsStringLiteral());
1007 script.setVar("MAX_FORMDATA_SIZE", conf.maxFormDataSize());
1008 script.setVar("MAX_PENDING_EVENTS", conf.maxPendingEvents());
1009
1010 std::string deployPath = session_.env().publicDeploymentPath_;
1011 if (deployPath.empty())
1012 deployPath = session_.deploymentPath();
1013
1014 script.setVar("DEPLOY_PATH", WWebWidget::jsStringLiteral(deployPath));
1015
1016 // WS_PATH = DEPLOY_PATH for C++, = CONTEXT_PATH for Java = request.contextPath()
1017 // WS_ID = empty for C++, servlet ID for Java
1018 #ifdef WT_TARGET_JAVA
1019 script.setVar("WS_PATH", WWebWidget::jsStringLiteral(session_.controller()->getContextPath() + "/ws"));
1020 script.setVar("WS_ID", WWebWidget::jsStringLiteral(std::to_string(session_.controller()->getIdForWebSocket())));
1021 #else
1022 script.setVar("WS_PATH", WWebWidget::jsStringLiteral(deployPath));
1023 script.setVar("WS_ID", WWebWidget::jsStringLiteral(std::string("")));
1024 #endif
1025
1026 script.setVar("KEEP_ALIVE", std::to_string(conf.keepAlive()));
1027
1028 script.setVar("IDLE_TIMEOUT", conf.idleTimeout() != -1 ?
1029 std::to_string(conf.idleTimeout()) : std::string("null"));
1030
1031 script.setVar("INDICATOR_TIMEOUT", conf.indicatorTimeout());
1032 script.setVar("SERVER_PUSH_TIMEOUT", conf.serverPushTimeout() * 1000);
1033
1034 /*
1035 * Was in honor of Mozilla Bugzilla #246651
1036 */
1037 script.setVar("CLOSE_CONNECTION", false);
1038
1039 /*
1040 * Set the original script params for a widgetset session, so that any
1041 * Ajax update request has all the information to reload the session.
1042 */
1043 std::string params;
1044 if (session_.type() == EntryPointType::WidgetSet) {
1045 const Http::ParameterMap *m = &session_.env().getParameterMap();
1046 Http::ParameterMap::const_iterator it = m->find("Wt-params");
1047 Http::ParameterMap wtParams;
1048 if (it != m->end()) {
1049 // Parse and reencode Wt-params, so it's definitely safe
1050 Http::Request::parseFormUrlEncoded(it->second[0], wtParams);
1051 m = &wtParams;
1052 }
1053 for (Http::ParameterMap::const_iterator i = m->begin();
1054 i != m->end(); ++i) {
1055 if (!params.empty())
1056 params += '&';
1057 params
1058 += Utils::urlEncode(i->first) + '=' + Utils::urlEncode(i->second[0]);
1059 }
1060 }
1061 script.setVar("PARAMS", params);
1062
1063 script.stream(out);
1064 }
1065
1066 if (!serveRest) {
1067 out.spool(response.out());
1068 return;
1069 }
1070
1071 out << app->javaScriptClass() << "._p_.setPage(" << pageId_ << ");";
1072
1073 formObjectsChanged_ = true;
1074 app->autoJavaScriptChanged_ = true;
1075
1076 if (session_.type() == EntryPointType::WidgetSet) {
1077 out << app->javaScriptClass() << "._p_.update(null, 'load', null, false);";
1078 } else if (!rendered_) {
1079 serveMainAjax(out);
1080 } else {
1081 bool enabledAjax = app->enableAjax_;
1082
1083 if (app->enableAjax_) {
1084 // Before-load JavaScript of libraries that were loaded directly
1085 // in HTML
1086 collectedJS1_ << "var form = " WT_CLASS ".getElement('Wt-form'); "
1087 "if (form) {" << beforeLoadJS_.str();
1088
1089 beforeLoadJS_.clear();
1090
1091 collectedJS1_
1092 << "var domRoot=" << app->domRoot_->jsRef() << ';'
1093 << WT_CLASS ".progressed(domRoot);";
1094
1095 // Load JavaScript libraries that were added during enableAjax()
1096 int librariesLoaded = loadScriptLibraries(collectedJS1_, app);
1097
1098 app->streamBeforeLoadJavaScript(collectedJS1_, false);
1099
1100 collectedJS2_
1101 << WT_CLASS ".resolveRelativeAnchors();"
1102 << "domRoot.style.visibility = 'visible';"
1103 << app->javaScriptClass() << "._p_.doAutoJavaScript();";
1104
1105 loadScriptLibraries(collectedJS2_, app, librariesLoaded);
1106
1107 collectedJS2_ << '}';
1108
1109 app->enableAjax_ = false;
1110 } else
1111 app->streamBeforeLoadJavaScript(out, true);
1112
1113 out << "window." << app->javaScriptClass()
1114 << "LoadWidgetTree = function(){\n";
1115
1116 if (app->internalPathsEnabled_)
1117 out << app->javaScriptClass() << "._p_.enableInternalPaths("
1118 << WWebWidget::jsStringLiteral(app->renderedInternalPath_)
1119 << ");\n";
1120
1121 visibleOnly_ = false;
1122
1123 formObjectsChanged_ = true;
1124 currentFormObjectsList_.clear();
1125 collectJavaScript();
1126 updateLoadIndicator(collectedJS1_, app, true);
1127
1128 LOG_DEBUG("js: " << collectedJS1_.str() << collectedJS2_.str());
1129
1130 out << collectedJS1_.str();
1131
1132 addResponseAckPuzzle(out);
1133
1134 out << app->javaScriptClass()
1135 << "._p_.setHash("
1136 << WWebWidget::jsStringLiteral(app->newInternalPath_)
1137 << ", false);\n";
1138
1139 if (!app->environment().internalPathUsingFragments())
1140 session_.setPagePathInfo(app->newInternalPath_);
1141
1142 out << app->javaScriptClass()
1143 << "._p_.update(null, 'load', null, false);"
1144 << collectedJS2_.str()
1145 << "};"; // LoadWidgetTree = function() { ... }
1146
1147 session_.app()->serverPushChanged_ = true;
1148 renderSetServerPush(out);
1149
1150 if (enabledAjax)
1151 out
1152 /*
1153 * Firefox < 3.5 doesn't have this and in that case it could be
1154 * that we are already ready and jqeury doesn't fire the callback.
1155 */
1156 << "\nif (typeof document.readyState === 'undefined')"
1157 << " setTimeout(function() { "
1158 << app->javaScriptClass() << "._p_.load(true);"
1159 << "}, 400);"
1160 << "else ";
1161
1162 out << "$(document).ready(function() { "
1163 << app->javaScriptClass() << "._p_.load(true);});\n";
1164 }
1165
1166 out.spool(response.out());
1167 }
1168
serveMainAjax(WStringStream & out)1169 void WebRenderer::serveMainAjax(WStringStream& out)
1170 {
1171 Configuration& conf = session_.controller()->configuration();
1172 bool widgetset = session_.type() == EntryPointType::WidgetSet;
1173 WApplication *app = session_.app();
1174
1175 WWebWidget *mainWebWidget = app->domRoot_.get();
1176
1177 visibleOnly_ = true;
1178
1179 /*
1180 * Render root widgets (domRoot_, and for widget set, also children of
1181 * domRoot2_). This automatically creates loading stubs for
1182 * invisible widgets.
1183 */
1184 app->loadingIndicator_->show();
1185 DomElement *mainElement = mainWebWidget->createSDomElement(app);
1186 app->loadingIndicator_->hide();
1187
1188 app->scriptLibrariesAdded_ = app->scriptLibraries_.size();
1189 int librariesLoaded = loadScriptLibraries(out, app);
1190
1191 out << app->javaScriptClass() << "._p_.autoJavaScript=function(){"
1192 << app->autoJavaScript_ << "};\n";
1193 app->autoJavaScriptChanged_ = false;
1194
1195 app->streamBeforeLoadJavaScript(out, true);
1196
1197 if (!widgetset)
1198 out << "window." << app->javaScriptClass()
1199 << "LoadWidgetTree = function(){\n";
1200
1201 if (!initialStyleRendered_) {
1202 /*
1203 * In case we have not yet served the bootstyle for this page:
1204 */
1205 if (app->theme()) {
1206 auto styleSheets = app->theme()->styleSheets();
1207 for (unsigned i = 0; i < styleSheets.size(); ++i)
1208 loadStyleSheet(out, app, styleSheets[i]);
1209 }
1210
1211 app->styleSheetsAdded_ = app->styleSheets_.size();
1212 loadStyleSheets(out, app);
1213
1214 initialStyleRendered_ = true;
1215 }
1216
1217 /*
1218 * Need to do this after createSDomElement, since additional CSS/JS
1219 * may be made during rendering, e.g. from WViewWidget::render()
1220 */
1221 if (conf.inlineCss())
1222 app->styleSheet().javaScriptUpdate(app, out, true);
1223
1224 if (app->bodyHtmlClassChanged_) {
1225 std::string op = widgetset ? "+=" : "=";
1226 out << "document.body.parentNode.className" << op << '\'' << app->htmlClass_ << "';"
1227 << "document.body.className" << op << '\'' << bodyClassRtl() << "';"
1228 << "document.body.setAttribute('dir', '";
1229 if (app->layoutDirection() == LayoutDirection::LeftToRight)
1230 out << "LTR";
1231 else
1232 out << "RTL";
1233 out << "');";
1234 }
1235
1236 #ifdef WT_DEBUG_ENABLED
1237 WStringStream s;
1238 #else
1239 WStringStream& s = out;
1240 #endif // WT_DEBUG_ENABLED
1241
1242 mainElement->addToParent(s, "document.body", widgetset ? 0 : -1, app);
1243 delete mainElement;
1244
1245 addResponseAckPuzzle(s);
1246
1247 if (app->hasQuit())
1248 s << app->javaScriptClass() << "._p_.quit("
1249 << (app->quittedMessage_.empty() ? "null" :
1250 app->quittedMessage_.jsStringLiteral()) + ");";
1251
1252 if (widgetset)
1253 app->domRoot2_->rootAsJavaScript(app, s, true);
1254
1255 #ifdef WT_DEBUG_ENABLED
1256 LOG_DEBUG("js: " << s.str());
1257 out << s.str();
1258 #endif // WT_DEBUG_ENABLED
1259
1260 currentFormObjectsList_ = createFormObjectsList(app);
1261 out << app->javaScriptClass()
1262 << "._p_.setFormObjects([" << currentFormObjectsList_ << "]);\n";
1263 formObjectsChanged_ = false;
1264
1265 setRendered(true);
1266 setJSSynced(true);
1267
1268 preLearnStateless(app, collectedJS1_);
1269
1270 LOG_DEBUG("js: " << collectedJS1_.str());
1271
1272 out << collectedJS1_.str();
1273 collectedJS1_.clear();
1274
1275 updateLoadIndicator(out, app, true);
1276
1277 if (widgetset) {
1278 const std::string *historyE = app->environment().getParameter("Wt-history");
1279 if (historyE) {
1280 out << WT_CLASS << ".history.initialize('"
1281 << (*historyE)[0] << "-field', '"
1282 << (*historyE)[0] << "-iframe', '');\n";
1283 }
1284 }
1285
1286 app->streamAfterLoadJavaScript(out);
1287 out << "{var o=null,e=null;"
1288 << app->hideLoadingIndicator_.javaScript()
1289 << '}';
1290
1291 if (!widgetset) {
1292 if (!app->hasQuit())
1293 out << session_.app()->javaScriptClass()
1294 << "._p_.update(null, 'load', null, false);\n";
1295 out << "};\n";
1296 }
1297
1298 renderSetServerPush(out);
1299
1300 out << "$(document).ready(function() { "
1301 << app->javaScriptClass() << "._p_.load(" << !widgetset << ");});\n";
1302
1303 loadScriptLibraries(out, app, librariesLoaded);
1304 }
1305
jsSynced()1306 bool WebRenderer::jsSynced() const
1307 {
1308 return collectedJS1_.empty() &&
1309 collectedJS2_.empty();
1310 }
1311
setJSSynced(bool invisibleToo)1312 void WebRenderer::setJSSynced(bool invisibleToo)
1313 {
1314 LOG_DEBUG("setJSSynced: " << invisibleToo);
1315
1316 collectedJS1_.clear();
1317 collectedJS2_.clear();
1318
1319 if (!invisibleToo)
1320 collectedJS1_ << invisibleJS_.str();
1321
1322 invisibleJS_.clear();
1323 }
1324
safeJsStringLiteral(const std::string & value)1325 std::string WebRenderer::safeJsStringLiteral(const std::string& value)
1326 {
1327 std::string s = WWebWidget::jsStringLiteral(value);
1328 return Wt::Utils::replace(s, "<", "<'+'");
1329 }
1330
updateLoadIndicator(WStringStream & out,WApplication * app,bool all)1331 void WebRenderer::updateLoadIndicator(WStringStream& out, WApplication *app,
1332 bool all)
1333 {
1334 if (app->showLoadingIndicator_.needsUpdate(all)) {
1335 out << "showLoadingIndicator = function() {var o=null,e=null;\n"
1336 << app->showLoadingIndicator_.javaScript() << "};\n";
1337 app->showLoadingIndicator_.updateOk();
1338 }
1339
1340 if (app->hideLoadingIndicator_.needsUpdate(all)) {
1341 out << "hideLoadingIndicator = function() {var o=null,e=null;\n"
1342 << app->hideLoadingIndicator_.javaScript() << "};\n";
1343 app->hideLoadingIndicator_.updateOk();
1344 }
1345 }
1346
renderStyleSheet(WStringStream & out,const WLinkedCssStyleSheet & sheet,WApplication * app)1347 void WebRenderer::renderStyleSheet(WStringStream& out,
1348 const WLinkedCssStyleSheet& sheet,
1349 WApplication *app)
1350 {
1351 out << "<link href=\"";
1352 DomElement::htmlAttributeValue(out, sheet.link().resolveUrl(app));
1353 out << "\" rel=\"stylesheet\" type=\"text/css\"";
1354
1355 if (!sheet.media().empty() && sheet.media() != "all")
1356 out << " media=\"" << sheet.media() << '"';
1357
1358 closeSpecial(out);
1359 }
1360
1361 /*
1362 * Serves the Plain or Hybrid HTML page.
1363 *
1364 * Requires that the application has been started.
1365 *
1366 * The Hybrid page is served when a progressive bootstrap is indicated
1367 * or when we are in an ajax session. We need to remember that in the next
1368 * serveMainscript() we only need to serve an update, not render the whole
1369 * interface.
1370 */
serveMainpage(WebResponse & response)1371 void WebRenderer::serveMainpage(WebResponse& response)
1372 {
1373 ++expectedAckId_;
1374 session_.sessionIdChanged_ = false;
1375
1376 Configuration& conf = session_.controller()->configuration();
1377
1378 WApplication *app = session_.app();
1379
1380 /*
1381 * This implements the redirect for Post-Redirect-Get, or when the
1382 * internal path changed.
1383 *
1384 * Post-Redirect-Get does not work properly though: refresh() may misbehave
1385 * and have unintended side effects ?
1386 */
1387 if (!app->environment().ajax()
1388 && (/*response.requestMethod() == "POST"
1389 || */(app->internalPathIsChanged_
1390 && app->renderedInternalPath_ != app->newInternalPath_))) {
1391 app->renderedInternalPath_ = app->newInternalPath_;
1392
1393 if (session_.state() == WebSession::State::JustCreated &&
1394 conf.progressiveBoot(app->environment().internalPath())) {
1395 session_.redirect
1396 (session_.fixRelativeUrl
1397 (session_.bookmarkUrl(app->newInternalPath_)));
1398 session_.kill();
1399 } else {
1400 session_.redirect
1401 (session_.fixRelativeUrl
1402 (session_.mostRelativeUrl(app->newInternalPath_)));
1403 }
1404 }
1405
1406 std::string redirect = session_.getRedirect();
1407
1408 if (!redirect.empty()) {
1409 response.setStatus(302); // Should be 303 in fact ?
1410 response.setRedirect(redirect);
1411 setHeaders(response, "text/html; charset=UTF-8");
1412 return;
1413 }
1414
1415 WWebWidget *mainWebWidget = app->domRoot_.get();
1416
1417 visibleOnly_ = true;
1418
1419 /*
1420 * The element to render. This automatically creates loading stubs
1421 * for invisible widgets, which is also what we want for
1422 * non-JavaScript versions.
1423 */
1424 DomElement *mainElement = mainWebWidget->createSDomElement(app);
1425 setRendered(true);
1426 setJSSynced(true);
1427
1428 WStringStream styleSheets;
1429
1430 if (app->theme()) {
1431 std::vector<WLinkedCssStyleSheet> sheets = app->theme()->styleSheets();
1432
1433 for (unsigned i = 0; i < sheets.size(); ++i)
1434 renderStyleSheet(styleSheets, sheets[i], app);
1435 }
1436
1437 for (unsigned i = 0; i < app->styleSheets_.size(); ++i)
1438 renderStyleSheet(styleSheets, app->styleSheets_[i], app);
1439
1440 app->styleSheetsAdded_ = 0;
1441 initialStyleRendered_ = true;
1442
1443 beforeLoadJS_.clear();
1444 for (unsigned i = 0; i < app->scriptLibraries_.size(); ++i) {
1445 std::string url = app->scriptLibraries_[i].uri;
1446 styleSheets << "<script src=";
1447 DomElement::htmlAttributeValue(styleSheets, session_.fixRelativeUrl(url));
1448 styleSheets << "></script>\n";
1449
1450 beforeLoadJS_ << app->scriptLibraries_[i].beforeLoadJS;
1451 }
1452 app->scriptLibrariesAdded_ = 0;
1453
1454 app->newBeforeLoadJavaScript_ = app->beforeLoadJavaScript_.length();
1455
1456 bool hybridPage = session_.progressiveBoot() || session_.env().ajax();
1457 FileServe page(hybridPage ? skeletons::Hybrid_html1 : skeletons::Plain_html1);
1458
1459 setPageVars(page);
1460 page.setVar("SESSION_ID", session_.sessionId());
1461
1462 std::string url
1463 = (app->environment().agentIsSpiderBot() || !session_.useUrlRewriting())
1464 ? session_.bookmarkUrl(app->newInternalPath_)
1465 : session_.mostRelativeUrl(app->newInternalPath_);
1466
1467 url = session_.fixRelativeUrl(url);
1468
1469 url = Wt::Utils::replace(url, '&', "&");
1470 page.setVar("RELATIVE_URL", url);
1471
1472 if (conf.inlineCss()) {
1473 WStringStream css;
1474 app->styleSheet().cssText(css, true);
1475 page.setVar("STYLESHEET", css.str());
1476 } else
1477 page.setVar("STYLESHEET", "");
1478
1479 page.setVar("STYLESHEETS", styleSheets.str());
1480
1481 page.setVar("TITLE", WWebWidget::escapeText(app->title()).toUTF8());
1482
1483 app->titleChanged_ = false;
1484
1485 std::string contentType = "text/html; charset=UTF-8";
1486
1487 setCaching(response, false);
1488 response.addHeader("X-Frame-Options", "SAMEORIGIN");
1489 setHeaders(response, contentType);
1490
1491 currentFormObjectsList_ = createFormObjectsList(app);
1492
1493 if (hybridPage)
1494 streamBootContent(response, page, true);
1495
1496 WStringStream out(response.out());
1497 page.streamUntil(out, "HTML");
1498
1499 DomElement::TimeoutList timeouts;
1500 {
1501 EscapeOStream js;
1502 EscapeOStream eout(out);
1503 mainElement->asHTML(eout, js, timeouts);
1504
1505 /*
1506 * invisibleJS_ is being streamed as the first JavaScript inside
1507 * collectJavaScript(). This is where this belongs since between
1508 * the HTML and the script there may already be changes (because
1509 * of server push) that delete elements that were rendered.
1510 */
1511 invisibleJS_ << js.str();
1512 delete mainElement;
1513
1514 app->domRoot_->doneRerender();
1515 }
1516
1517 int refresh;
1518 if (app->environment().ajax()) {
1519 WStringStream str;
1520 DomElement::createTimeoutJs(str, timeouts, app);
1521 app->doJavaScript(str.str());
1522
1523 refresh = 1000000;
1524 } else {
1525 if (app->hasQuit() || conf.sessionTimeout() == -1)
1526 refresh = 1000000;
1527 else {
1528 refresh = conf.sessionTimeout() / 3;
1529 for (unsigned i = 0; i < timeouts.size(); ++i)
1530 refresh = std::min(refresh, 1 + timeouts[i].msec/1000);
1531 }
1532 }
1533 page.setVar("REFRESH", std::to_string(refresh));
1534
1535 page.stream(out);
1536
1537 app->internalPathIsChanged_ = false;
1538
1539 out.spool(response.out());
1540 }
1541
loadScriptLibraries(WStringStream & out,WApplication * app,int count)1542 int WebRenderer::loadScriptLibraries(WStringStream& out,
1543 WApplication *app, int count)
1544 {
1545 if (count == -1) {
1546 int first = app->scriptLibraries_.size() - app->scriptLibrariesAdded_;
1547
1548 for (unsigned i = first; i < app->scriptLibraries_.size(); ++i) {
1549 std::string uri = session_.fixRelativeUrl(app->scriptLibraries_[i].uri);
1550
1551 out << app->scriptLibraries_[i].beforeLoadJS
1552 << app->javaScriptClass() << "._p_.loadScript('" << uri << "',";
1553 DomElement::jsStringLiteral(out, app->scriptLibraries_[i].symbol, '\'');
1554 out << ");\n";
1555 out << app->javaScriptClass() << "._p_.onJsLoad(\""
1556 << uri << "\",function() {\n";
1557 }
1558
1559 count = app->scriptLibrariesAdded_;
1560 app->scriptLibrariesAdded_ = 0;
1561
1562 return count;
1563 } else {
1564 if (count) {
1565 out << app->javaScriptClass() << "._p_.doAutoJavaScript();";
1566 for (int i = 0; i < count; ++i)
1567 out << "});";
1568 }
1569
1570 return 0;
1571 }
1572 }
1573
loadStyleSheet(WStringStream & out,WApplication * app,const WLinkedCssStyleSheet & sheet)1574 void WebRenderer::loadStyleSheet(WStringStream& out, WApplication *app,
1575 const WLinkedCssStyleSheet& sheet)
1576 {
1577 out << WT_CLASS << ".addStyleSheet('"
1578 << sheet.link().resolveUrl(app) << "', '"
1579 << sheet.media() << "');\n ";
1580 }
1581
removeStyleSheets(WStringStream & out,WApplication * app)1582 void WebRenderer::removeStyleSheets(WStringStream& out, WApplication *app)
1583 {
1584 for (int i = (int)app->styleSheetsToRemove_.size() - 1; i > -1; --i){
1585 out << WT_CLASS << ".removeStyleSheet('"
1586 << app->styleSheetsToRemove_[i].link().resolveUrl(app) << "');\n ";
1587 app->styleSheetsToRemove_.erase(app->styleSheetsToRemove_.begin() + i);
1588 }
1589 }
1590
loadStyleSheets(WStringStream & out,WApplication * app)1591 void WebRenderer::loadStyleSheets(WStringStream& out, WApplication *app)
1592 {
1593 int first = app->styleSheets_.size() - app->styleSheetsAdded_;
1594
1595 for (unsigned i = first; i < app->styleSheets_.size(); ++i)
1596 loadStyleSheet(out, app, app->styleSheets_[i]);
1597
1598 removeStyleSheets(out, app);
1599
1600 app->styleSheetsAdded_ = 0;
1601 }
1602
collectChanges(std::vector<DomElement * > & changes)1603 void WebRenderer::collectChanges(std::vector<DomElement *>& changes)
1604 {
1605 WApplication *app = session_.app();
1606
1607 do {
1608 moreUpdates_ = false;
1609
1610 std::multimap<int, WWidget *> depthOrder;
1611
1612 for (UpdateMap::const_iterator i = updateMap_.begin();
1613 i != updateMap_.end(); ++i) {
1614 int depth = 1;
1615
1616 WWidget *ww = *i;
1617 WWidget *w = ww;
1618 for (; w->parent(); ++depth)
1619 w = w->parent();
1620
1621 if (w != app->domRoot_.get() && w != app->domRoot2_.get()) {
1622 LOG_DEBUG("ignoring: " << ww->id() << " (" << DESCRIBE(ww) << ") " <<
1623 w->id() << " (" << DESCRIBE(w) << ")");
1624
1625 // not in displayed widgets: will be removed from the update list
1626 depth = 0;
1627 }
1628
1629 #ifndef WT_TARGET_JAVA
1630 depthOrder.insert(std::make_pair(depth, ww));
1631 #else
1632 depthOrder.insert(depth, ww);
1633 #endif // WT_TARGET_JAVA
1634 }
1635
1636 for (std::multimap<int, WWidget *>::const_iterator i = depthOrder.begin();
1637 i != depthOrder.end(); ++i) {
1638 UpdateMap::iterator j = updateMap_.find(i->second);
1639 if (j != updateMap_.end()) {
1640 WWidget *w = i->second;
1641
1642 // depth == 0: remove it from the update list
1643 if (i->first == 0) {
1644 w->webWidget()->propagateRenderOk();
1645 continue;
1646 }
1647
1648 LOG_DEBUG("updating: " << w->id() << " (" << DESCRIBE(w) << ")");
1649
1650 if (!learning_ && visibleOnly_) {
1651 if (w->isRendered()) {
1652 w->getSDomChanges(changes, app);
1653
1654 /* if (!w->isVisible()) {
1655 // We should postpone rendering the changes -- but
1656 // at the same time need to propageRenderOk() now for stateless
1657 // slot learning to work properly.
1658 w->getSDomChanges(changes, app);
1659 } else
1660 w->getSDomChanges(changes, app); */
1661 } else {
1662 LOG_DEBUG("Ignoring: " << w->id());
1663 }
1664 } else {
1665 w->getSDomChanges(changes, app);
1666 }
1667 }
1668 }
1669 } while (!learning_ && moreUpdates_);
1670 }
1671
collectJavaScriptUpdate(WStringStream & out)1672 void WebRenderer::collectJavaScriptUpdate(WStringStream& out)
1673 {
1674 WApplication *app = session_.app();
1675
1676 out << '{';
1677
1678 try {
1679 if (session_.sessionIdChanged_) {
1680 if (session_.hasSessionIdInUrl()) {
1681 if (app->environment().ajax() &&
1682 !app->environment().internalPathUsingFragments()) {
1683 streamRedirectJS(out, app->url(app->internalPath()));
1684 // better would be to use HTML5 history in this case but that would
1685 // need some minor JavaScript reorganizations
1686 } else {
1687 streamRedirectJS(out, app->url(app->internalPath()));
1688 }
1689 out << '}';
1690 return;
1691 }
1692
1693 out << session_.app()->javaScriptClass()
1694 << "._p_.setSessionUrl("
1695 << WWebWidget::jsStringLiteral(sessionUrl())
1696 << ");";
1697 session_.sessionIdChanged_ = false;
1698 }
1699
1700 collectJS(&out);
1701
1702 /*
1703 * Now, as we have cleared and recorded all JavaScript changes that were
1704 * caused by the actual code, we can learn stateless code and collect
1705 * changes that result.
1706 */
1707
1708 preLearnStateless(app, out);
1709
1710 if (formObjectsChanged_) {
1711 std::string formObjectsList = createFormObjectsList(app);
1712 if (formObjectsList != currentFormObjectsList_) {
1713 currentFormObjectsList_ = formObjectsList;
1714 out << app->javaScriptClass()
1715 << "._p_.setFormObjects([" << currentFormObjectsList_ << "]);";
1716 }
1717 }
1718
1719 app->streamAfterLoadJavaScript(out);
1720
1721 if (app->hasQuit())
1722 out << app->javaScriptClass() << "._p_.quit("
1723 << (app->quittedMessage_.empty() ? "null" :
1724 app->quittedMessage_.jsStringLiteral()) + ");";
1725
1726 if (updateLayout_) {
1727 out << "window.onresize();";
1728 updateLayout_ = false;
1729 }
1730
1731 app->renderedInternalPath_ = app->newInternalPath_;
1732
1733 updateLoadIndicator(out, app, false);
1734 } catch (const std::exception &e) {
1735 out << '}';
1736 RETHROW(e);
1737 } catch (...) {
1738 out << '}';
1739 throw;
1740 }
1741 out << '}';
1742 }
1743
updateFormObjects(WWebWidget * source,bool checkDescendants)1744 void WebRenderer::updateFormObjects(WWebWidget *source, bool checkDescendants)
1745 {
1746 formObjectsChanged_ = true;
1747 }
1748
updateFormObjectsList(WApplication * app)1749 void WebRenderer::updateFormObjectsList(WApplication *app)
1750 {
1751 if (formObjectsChanged_) {
1752 currentFormObjects_.clear();
1753
1754 app->domRoot_->getFormObjects(currentFormObjects_);
1755 if (app->domRoot2_)
1756 app->domRoot2_->getFormObjects(currentFormObjects_);
1757 }
1758 }
1759
createFormObjectsList(WApplication * app)1760 std::string WebRenderer::createFormObjectsList(WApplication *app)
1761 {
1762 updateFormObjectsList(app);
1763
1764 std::string result;
1765
1766 for (FormObjectsMap::const_iterator i = currentFormObjects_.begin();
1767 i != currentFormObjects_.end(); ++i) {
1768 if (!result.empty())
1769 result += ',';
1770
1771 result += "'" + i->first + "'";
1772 }
1773
1774 formObjectsChanged_ = false;
1775
1776 return result;
1777 }
1778
collectJS(WStringStream * js)1779 void WebRenderer::collectJS(WStringStream* js)
1780 {
1781 std::vector<DomElement *> changes;
1782
1783 collectChanges(changes);
1784
1785 WApplication *app = session_.app();
1786
1787 if (js) {
1788 if (!preLearning())
1789 app->streamBeforeLoadJavaScript(*js, false);
1790
1791 Configuration& conf = session_.controller()->configuration();
1792 if (conf.inlineCss())
1793 app->styleSheet().javaScriptUpdate(app, *js, false);
1794
1795 EscapeOStream sout(*js);
1796
1797 for (unsigned i = 0; i < changes.size(); ++i)
1798 changes[i]->asJavaScript(sout, DomElement::Priority::Delete);
1799
1800 for (unsigned i = 0; i < changes.size(); ++i) {
1801 changes[i]->asJavaScript(sout, DomElement::Priority::Update);
1802 delete changes[i];
1803 }
1804 } else {
1805 for (unsigned i = 0; i < changes.size(); ++i)
1806 delete changes[i];
1807 }
1808
1809 if (js) {
1810 if (app->titleChanged_) {
1811 *js << app->javaScriptClass()
1812 << "._p_.setTitle(" << app->title().jsStringLiteral() << ");\n";
1813 }
1814
1815 if (app->closeMessageChanged_) {
1816 *js << app->javaScriptClass()
1817 << "._p_.setCloseMessage(" << app->closeMessage().jsStringLiteral()
1818 << ");\n";
1819 }
1820
1821 if (app->localeChanged_) {
1822 *js << app->javaScriptClass()
1823 << "._p_.setLocale(" << WString(app->locale().name()).jsStringLiteral()
1824 << ");\n";
1825 }
1826 }
1827
1828 app->titleChanged_ = false;
1829 app->closeMessageChanged_ = false;
1830 app->localeChanged_ = false;
1831
1832 if (js) {
1833 int librariesLoaded = loadScriptLibraries(*js, app);
1834
1835 app->streamAfterLoadJavaScript(*js);
1836
1837 if (app->internalPathIsChanged_) {
1838 *js << app->javaScriptClass()
1839 << "._p_.setHash("
1840 << WWebWidget::jsStringLiteral(app->newInternalPath_)
1841 << ", false);\n";
1842 if (!preLearning() && !app->environment().internalPathUsingFragments())
1843 session_.setPagePathInfo(app->newInternalPath_);
1844 }
1845
1846 loadScriptLibraries(*js, app, librariesLoaded);
1847 } else
1848 app->afterLoadJavaScript_.clear();
1849
1850 app->internalPathIsChanged_ = false;
1851 app->renderedInternalPath_ = app->newInternalPath_;
1852 }
1853
preLearnStateless(WApplication * app,WStringStream & out)1854 void WebRenderer::preLearnStateless(WApplication *app, WStringStream& out)
1855 {
1856 if (!session_.env().ajax())
1857 return;
1858
1859 collectJS(&out);
1860
1861 // TODO optimize this so that only signals which require an update
1862 // are processed instead of looping through all signals.
1863
1864 WApplication::SignalMap& ss = session_.app()->exposedSignals();
1865
1866 for (WApplication::SignalMap::iterator i = ss.begin();
1867 i != ss.end(); ) {
1868 Wt::EventSignalBase* s = i->second;
1869
1870 if (s->owner() == app)
1871 s->processPreLearnStateless(this);
1872 else if (s->canAutoLearn()) {
1873 WWidget *ww = static_cast<WWidget *>(s->owner());
1874 if (ww && ww->isRendered())
1875 s->processPreLearnStateless(this);
1876 }
1877
1878 ++i;
1879 }
1880
1881 out << statelessJS_.str();
1882 statelessJS_.clear();
1883 }
1884
learn(WStatelessSlot * slot)1885 std::string WebRenderer::learn(WStatelessSlot* slot)
1886 {
1887 if (slot->invalidated())
1888 return std::string();
1889
1890 if (slot->type() == WStatelessSlot::SlotType::PreLearnStateless)
1891 learning_ = true;
1892
1893 learningIncomplete_ = false;
1894
1895 currentStatelessSlotIsActuallyStateless_ = true;
1896
1897 slot->trigger();
1898
1899 WStringStream js;
1900
1901 collectJS(&js);
1902
1903 std::string result = js.str();
1904
1905 LOG_DEBUG("learned: " << result);
1906
1907 if (slot->type() == WStatelessSlot::SlotType::PreLearnStateless) {
1908 slot->undoTrigger();
1909 collectJS(nullptr);
1910
1911 learning_ = false;
1912 } else { // AutoLearnStateless
1913 statelessJS_ << result;
1914 }
1915
1916 if (currentStatelessSlotIsActuallyStateless_ && !learningIncomplete_) {
1917 slot->setJavaScript(result);
1918 } else if (!currentStatelessSlotIsActuallyStateless_) {
1919 slot->invalidate();
1920 }
1921
1922 collectJS(&statelessJS_);
1923
1924 return result;
1925 }
1926
learningIncomplete()1927 void WebRenderer::learningIncomplete()
1928 {
1929 learningIncomplete_ = true;
1930 }
1931
headDeclarations()1932 std::string WebRenderer::headDeclarations() const
1933 {
1934 EscapeOStream result;
1935
1936 const Configuration& conf = session_.env().server()->configuration();
1937
1938 const std::vector<HeadMatter>& headMatters = conf.headMatter();
1939 for (unsigned i = 0; i < headMatters.size(); ++i) {
1940 const HeadMatter& m = headMatters[i];
1941
1942 bool add = true;
1943 if (!m.userAgent().empty()) {
1944 std::string s = session_.env().userAgent();
1945 std::regex expr(m.userAgent());
1946 if (!std::regex_search(s, expr))
1947 add = false;
1948 }
1949
1950 if (add)
1951 result << m.contents();
1952 }
1953
1954 const std::vector<MetaHeader>& confMetaHeaders = conf.metaHeaders();
1955 std::vector<MetaHeader> metaHeaders;
1956
1957 for (unsigned i = 0; i < confMetaHeaders.size(); ++i) {
1958 const MetaHeader& m = confMetaHeaders[i];
1959
1960 bool add = true;
1961 if (!m.userAgent.empty()) {
1962 std::string s = session_.env().userAgent();
1963 std::regex expr(m.userAgent);
1964 if (!std::regex_search(s, expr))
1965 add = false;
1966 }
1967
1968 if (add)
1969 metaHeaders.push_back(confMetaHeaders[i]);
1970 }
1971
1972 if (session_.app()) {
1973 const std::vector<MetaHeader>& appMetaHeaders
1974 = session_.app()->metaHeaders_;
1975
1976 for (unsigned i = 0; i < appMetaHeaders.size(); ++i) {
1977 const MetaHeader& m = appMetaHeaders[i];
1978
1979 bool add = true;
1980 for (unsigned j = 0; j < metaHeaders.size(); ++j) {
1981 MetaHeader& m2 = metaHeaders[j];
1982
1983 if (m.type == m2.type && m.name == m2.name) {
1984 m2.content = m.content;
1985 add = false;
1986 break;
1987 }
1988 }
1989
1990 if (add)
1991 metaHeaders.push_back(m);
1992 }
1993 }
1994
1995 for (unsigned i = 0; i < metaHeaders.size(); ++i) {
1996 const MetaHeader& m = metaHeaders[i];
1997
1998 result << "<meta";
1999
2000 if (!m.name.empty()) {
2001 std::string attribute;
2002 switch (m.type) {
2003 case MetaHeaderType::Meta: attribute = "name"; break;
2004 case MetaHeaderType::Property: attribute = "property"; break;
2005 case MetaHeaderType::HttpHeader: attribute = "http-equiv"; break;
2006 }
2007
2008 appendAttribute(result, attribute, m.name);
2009 }
2010
2011 if (!m.lang.empty())
2012 appendAttribute(result, "lang", m.lang);
2013
2014 appendAttribute(result, "content", m.content.toUTF8());
2015
2016 closeSpecial(result);
2017 }
2018
2019 if (session_.app()) {
2020 for (unsigned i = 0; i < session_.app()->metaLinks_.size(); ++i) {
2021 const WApplication::MetaLink& ml = session_.app()->metaLinks_[i];
2022
2023 result << "<link";
2024
2025 appendAttribute(result, "href", ml.href);
2026 appendAttribute(result, "rel", ml.rel);
2027 if (!ml.media.empty())
2028 appendAttribute(result, "media", ml.media);
2029 if (!ml.hreflang.empty())
2030 appendAttribute(result, "hreflang", ml.hreflang);
2031 if (!ml.type.empty())
2032 appendAttribute(result, "type", ml.type);
2033 if (!ml.sizes.empty())
2034 appendAttribute(result, "sizes", ml.sizes);
2035 if (ml.disabled)
2036 appendAttribute(result, "disabled", "");
2037
2038 closeSpecial(result);
2039 }
2040 } else
2041 if (session_.env().agentIsIE()) {
2042 /*
2043 * WARNING: Similar code in WApplication.C must be kept in sync for
2044 * progressive boot.
2045 */
2046 if (session_.env().agentIsIElt(9)) {
2047 bool selectIE7 = conf.uaCompatible().find("IE8=IE7")
2048 != std::string::npos;
2049
2050 if (selectIE7) {
2051 result << "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=7\"";
2052 closeSpecial(result);
2053 }
2054 } else if (session_.env().agent() == UserAgent::IE9) {
2055 result << "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=9\"";
2056 closeSpecial(result);
2057 } else if (session_.env().agent() == UserAgent::IE10) {
2058 result << "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=10\"";
2059 closeSpecial(result);
2060 } else {
2061 result << "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=11\"";
2062 closeSpecial(result);
2063 }
2064 }
2065
2066 if (!session_.favicon().empty()) {
2067 result <<
2068 "<link rel=\"shortcut icon\" href=\"" << session_.favicon() << '"';
2069 closeSpecial(result);
2070 }
2071
2072 std::string baseUrl;
2073 WApplication::readConfigurationProperty("baseURL", baseUrl);
2074
2075 if (!baseUrl.empty()) {
2076 result << "<base href=\"" << baseUrl << '"';
2077 closeSpecial(result);
2078 }
2079
2080 return result.str();
2081 }
2082
addWsRequestId(int wsRqId)2083 void WebRenderer::addWsRequestId(int wsRqId)
2084 {
2085 wsRequestsToHandle_.push_back(wsRqId);
2086 }
2087
2088 }
2089