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 	      + "&amp;request=resource&amp;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, '&', "&amp;");
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