// Ryzom - MMORPG Framework // Copyright (C) 2010-2021 Winch Gate Property Limited // // This source file has been modified by the following contributors: // Copyright (C) 2013 Laszlo KIS-ADAM (dfighter) // Copyright (C) 2019-2020 Jan BOON (Kaetemi) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . //#include #include "stdpch.h" #include "nel/gui/group_html.h" #include #include "nel/misc/types_nl.h" #include "nel/misc/rgba.h" #include "nel/misc/algo.h" #include "nel/misc/utf_string_view.h" #include "nel/gui/libwww.h" #include "nel/gui/group_html.h" #include "nel/gui/group_list.h" #include "nel/gui/group_menu.h" #include "nel/gui/group_container.h" #include "nel/gui/view_link.h" #include "nel/gui/ctrl_scroll.h" #include "nel/gui/ctrl_button.h" #include "nel/gui/ctrl_text_button.h" #include "nel/gui/action_handler.h" #include "nel/gui/group_paragraph.h" #include "nel/gui/group_editbox.h" #include "nel/gui/widget_manager.h" #include "nel/gui/lua_manager.h" #include "nel/gui/view_bitmap.h" #include "nel/gui/dbgroup_combo_box.h" #include "nel/gui/lua_ihm.h" #include "nel/misc/i18n.h" #include "nel/misc/md5.h" #include "nel/3d/texture_file.h" #include "nel/misc/big_file.h" #include "nel/gui/url_parser.h" #include "nel/gui/http_cache.h" #include "nel/gui/http_hsts.h" #include "nel/web/curl_certificates.h" #include "nel/gui/html_parser.h" #include "nel/gui/html_element.h" #include "nel/gui/css_style.h" #include "nel/gui/css_parser.h" #include "nel/gui/css_border_renderer.h" #include "nel/gui/css_background_renderer.h" #include using namespace std; using namespace NLMISC; #ifdef DEBUG_NEW #define new DEBUG_NEW #endif // Default maximum time the request is allowed to take #define DEFAULT_RYZOM_CONNECTION_TIMEOUT (300.0) // Allow up to 10 redirects, then give up #define DEFAULT_RYZOM_REDIRECT_LIMIT (10) // #define FONT_WEIGHT_NORMAL 400 #define FONT_WEIGHT_BOLD 700 namespace NLGUI { // Uncomment nlwarning() to see the log about curl downloads #define LOG_DL(fmt, ...) //nlwarning(fmt, ## __VA_ARGS__) // Uncomment to log curl progess //#define LOG_CURL_PROGRESS 1 CGroupHTML::SWebOptions CGroupHTML::options; // Return URL with https is host is in HSTS list static std::string upgradeInsecureUrl(const std::string &url) { if (toLowerAscii(url.substr(0, 7)) != "http://") { return url; } CUrlParser uri(url); if (!CStrictTransportSecurity::getInstance()->isSecureHost(uri.host)){ return url; } LOG_DL("HSTS url : '%s', using https", url.c_str()); uri.scheme = "https"; return uri.toString(); } // Active cURL www transfer class CCurlWWWData { public: CCurlWWWData(CURL *curl, const std::string &url) : Request(curl), Url(url), Content(""), HeadersSent(NULL) { } ~CCurlWWWData() { if (Request) curl_easy_cleanup(Request); if (HeadersSent) curl_slist_free_all(HeadersSent); } void sendHeaders(const std::vector headers) { for(uint i = 0; i < headers.size(); ++i) { HeadersSent = curl_slist_append(HeadersSent, headers[i].c_str()); } curl_easy_setopt(Request, CURLOPT_HTTPHEADER, HeadersSent); } void setRecvHeader(const std::string &header) { size_t pos = header.find(": "); if (pos == std::string::npos) return; std::string key = toLowerAscii(header.substr(0, pos)); if (pos != std::string::npos) { HeadersRecv[key] = header.substr(pos + 2); //nlinfo(">> received header '%s' = '%s'", key.c_str(), HeadersRecv[key].c_str()); } } // return last received "Location: " header or empty string if no header set const std::string getLocationHeader() { if (HeadersRecv.count("location") > 0) return HeadersRecv["location"]; return ""; } const uint32 getExpires() { time_t ret = 0; if (HeadersRecv.count("expires") > 0) ret = curl_getdate(HeadersRecv["expires"].c_str(), NULL); return ret > -1 ? ret : 0; } const std::string getLastModified() { if (HeadersRecv.count("last-modified") > 0) { return HeadersRecv["last-modified"]; } return ""; } const std::string getEtag() { if (HeadersRecv.count("etag") > 0) { return HeadersRecv["etag"]; } return ""; } bool hasHSTSHeader() { // ignore header if not secure connection if (toLowerAscii(Url.substr(0, 8)) != "https://") { return false; } return HeadersRecv.count("strict-transport-security") > 0; } const std::string getHSTSHeader() { if (hasHSTSHeader()) { return HeadersRecv["strict-transport-security"]; } return ""; } public: CURL *Request; std::string Url; std::string Content; private: // headers sent with curl request, must be released after transfer curl_slist * HeadersSent; // headers received from curl transfer std::map HeadersRecv; }; // cURL transfer callbacks // *************************************************************************** static size_t curlHeaderCallback(char *buffer, size_t size, size_t nmemb, void *pCCurlWWWData) { CCurlWWWData * me = static_cast(pCCurlWWWData); if (me) { std::string header; header.append(buffer, size * nmemb); me->setRecvHeader(header.substr(0, header.find_first_of("\n\r"))); } return size * nmemb; } // *************************************************************************** static size_t curlDataCallback(char *buffer, size_t size, size_t nmemb, void *pCCurlWWWData) { CCurlWWWData * me = static_cast(pCCurlWWWData); if (me) me->Content.append(buffer, size * nmemb); return size * nmemb; } // *************************************************************************** static size_t curlProgressCallback(void *pCCurlWWWData, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { CCurlWWWData * me = static_cast(pCCurlWWWData); if (me) { if (dltotal > 0 || dlnow > 0 || ultotal > 0 || ulnow > 0) { #ifdef LOG_CURL_PROGRESS nlwarning("> dltotal %ld, dlnow %ld, ultotal %ld, ulnow %ld, url '%s'", dltotal, dlnow, ultotal, ulnow, me->Url.c_str()); #endif } } // return 1 to cancel download return 0; } CGroupHTML::CDataDownload::~CDataDownload() { delete data; data = NULL; } void CGroupHTML::StylesheetDownloadCB::finish() { if (CFile::fileExists(tmpdest)) { if (CFile::fileExists(dest)) { CFile::deleteFile(dest); } CFile::moveFile(dest, tmpdest); } Parent->cssDownloadFinished(url, dest); } void CGroupHTML::ImageDownloadCB::addImage(CViewBase *img, const CStyleParams &style, TImageType type) { Images.push_back(SImageInfo(img, style, type)); } void CGroupHTML::ImageDownloadCB::removeImage(CViewBase *img) { for(std::vector::iterator it = Images.begin(); it != Images.end(); ++it) { if (it->Image == img) { Images.erase(it); break; } } } void CGroupHTML::ImageDownloadCB::finish() { // Image setTexture will remove itself from Images while iterating over it. // Do the swap to keep iterator safe. std::vector vec; vec.swap(Images); // tmpdest file does not exist if download skipped (ie cache was used) if (CFile::fileExists(tmpdest) || CFile::getFileSize(tmpdest) == 0) { try { // verify that image is not corrupted uint32 w, h; CBitmap::loadSize(tmpdest, w, h); if (w != 0 && h != 0) { if (CFile::fileExists(dest)) CFile::deleteFile(dest); } } catch(const NLMISC::Exception &e) { // exception message has .tmp file name, so keep it for further analysis nlwarning("Invalid image (%s) from url (%s): %s", tmpdest.c_str(), url.c_str(), e.what()); } // to reload image on page, the easiest seems to be changing texture // to temp file temporarily. that forces driver to reload texture from disk // ITexture::touch() seem not to do this. // cache was updated, first set texture as temp file for(std::vector::iterator it = vec.begin(); it != vec.end(); ++it) { SImageInfo &img = *it; Parent->setImage(img.Image, tmpdest, img.Type); Parent->setImageSize(img.Image, img.Style); } CFile::moveFile(dest, tmpdest); } if (!CFile::fileExists(dest) || CFile::getFileSize(dest) == 0) { // placeholder if cached image failed dest = "web_del.tga"; } // even if image was cached, incase there was 'http://' image set to CViewBitmap for(std::vector::iterator it = vec.begin(); it != vec.end(); ++it) { SImageInfo &img = *it; Parent->setImage(img.Image, dest, img.Type); Parent->setImageSize(img.Image, img.Style); } } void CGroupHTML::TextureDownloadCB::finish() { // tmpdest file does not exist if download skipped (ie cache was used) if (CFile::fileExists(tmpdest) && CFile::getFileSize(tmpdest) > 0) { if (CFile::fileExists(dest)) CFile::deleteFile(dest); CFile::moveFile(dest, tmpdest); } CViewRenderer &rVR = *CViewRenderer::getInstance(); for(uint i = 0; i < TextureIds.size(); i++) { rVR.reloadTexture(TextureIds[i].first, dest); TextureIds[i].second->invalidateCoords(); } } void CGroupHTML::BnpDownloadCB::finish() { bool verified = false; // no tmpfile if file was already in cache if (CFile::fileExists(tmpdest)) { verified = m_md5sum.empty() || (m_md5sum != getMD5(tmpdest).toString()); if (verified) { if (CFile::fileExists(dest)) { CFile::deleteFile(dest); } CFile::moveFile(dest, tmpdest); } else { CFile::deleteFile(tmpdest); } } else if (CFile::fileExists(dest)) { verified = m_md5sum.empty() || (m_md5sum != getMD5(dest).toString()); } if (!m_lua.empty()) { std::string script = "\nlocal __CURRENT_WINDOW__ = \""+Parent->getId()+"\""; script += toString("\nlocal __DOWNLOAD_STATUS__ = %s\n", verified ? "true" : "false"); script += m_lua; CLuaManager::getInstance().executeLuaScript(script, true ); } } // Check if domain is on TrustedDomain bool CGroupHTML::isTrustedDomain(const string &domain) { if (domain == options.webServerDomain) return true; vector::iterator it; it = find(options.trustedDomains.begin(), options.trustedDomains.end(), domain); return it != options.trustedDomains.end(); } // Update view after download has finished void CGroupHTML::setImage(CViewBase * view, const string &file, const TImageType type) { CCtrlButton *btn = dynamic_cast(view); if(btn) { if (type == NormalImage) { btn->setTexture (file); btn->setTexturePushed(file); btn->invalidateCoords(); btn->invalidateContent(); paragraphChange(); } else { btn->setTextureOver(file); } return; } CViewBitmap *btm = dynamic_cast(view); if(btm) { btm->setTexture (file); btm->invalidateCoords(); btm->invalidateContent(); paragraphChange(); return; } CGroupCell *btgc = dynamic_cast(view); if(btgc) { btgc->setTexture (file); btgc->invalidateCoords(); btgc->invalidateContent(); paragraphChange(); return; } CGroupTable *table = dynamic_cast(view); if (table) { table->setTexture(file); return; } } // Force image width, height void CGroupHTML::setImageSize(CViewBase *view, const CStyleParams &style) { sint32 width = style.Width; sint32 height = style.Height; sint32 maxw = style.MaxWidth; sint32 maxh = style.MaxHeight; sint32 imageWidth, imageHeight; bool changed = true; // get image texture size // if image is being downloaded, then correct size is set after thats done CCtrlButton *btn = dynamic_cast(view); if(btn) { btn->fitTexture(); imageWidth = btn->getW(false); imageHeight = btn->getH(false); } else { CViewBitmap *btm = dynamic_cast(view); if(btm) { btm->fitTexture(); imageWidth = btm->getW(false); imageHeight = btm->getH(false); } else { // not supported return; } } // if width/height is not requested, then use image size // else recalculate missing value, keep image ratio if (width == -1 && height == -1) { width = imageWidth; height = imageHeight; changed = false; } else if (width == -1 || height == -1) { float ratio = (float) imageWidth / std::max(1, imageHeight); if (width == -1) width = height * ratio; else height = width / ratio; } // apply max-width, max-height rules if asked if (maxw > -1 || maxh > -1) { _Style.applyCssMinMax(width, height, 0, 0, maxw, maxh); changed = true; } if (changed) { CCtrlButton *btn = dynamic_cast(view); if(btn) { btn->setScale(true); btn->setW(width); btn->setH(height); } else { CViewBitmap *image = dynamic_cast(view); if(image) { image->setScale(true); image->setW(width); image->setH(height); } } } } void CGroupHTML::setTextButtonStyle(CCtrlTextButton *ctrlButton, const CStyleParams &style) { // this will also set size for treating it like "display: inline-block;" if (style.Width > 0) ctrlButton->setWMin(style.Width); if (style.Height > 0) ctrlButton->setHMin(style.Height); CViewText *pVT = ctrlButton->getViewText(); if (pVT) { setTextStyle(pVT, style); } if (style.hasStyle("background-color")) { ctrlButton->setColor(style.Background.color); if (style.hasStyle("-ryzom-background-color-over")) { ctrlButton->setColorOver(style.BackgroundColorOver); } else { ctrlButton->setColorOver(style.Background.color); } ctrlButton->setTexture("", "blank.tga", "", false); ctrlButton->setTextureOver("", "blank.tga", ""); ctrlButton->setProperty("force_text_over", "true"); } else if (style.hasStyle("-ryzom-background-color-over")) { ctrlButton->setColorOver(style.BackgroundColorOver); ctrlButton->setProperty("force_text_over", "true"); ctrlButton->setTextureOver("blank.tga", "blank.tga", "blank.tga"); } } void CGroupHTML::setTextStyle(CViewText *pVT, const CStyleParams &style) { if (pVT) { pVT->setColor(style.TextColor); pVT->setFontName(style.FontFamily); pVT->setFontSize(style.FontSize, false); pVT->setEmbolden(style.FontWeight >= FONT_WEIGHT_BOLD); pVT->setOblique(style.FontOblique); pVT->setUnderlined(style.Underlined); pVT->setStrikeThrough(style.StrikeThrough); if (style.TextShadow.Enabled) { pVT->setShadow(true); pVT->setShadowColor(style.TextShadow.Color); pVT->setShadowOutline(style.TextShadow.Outline); pVT->setShadowOffset(style.TextShadow.X, style.TextShadow.Y); } } } // Get an url and return the local filename with the path where the url image should be string CGroupHTML::localImageName(const string &url) { string dest = "cache/"; dest += getMD5((uint8 *)url.c_str(), (uint32)url.size()).toString(); dest += ".cache"; return dest; } void CGroupHTML::pumpCurlQueue() { if (RunningCurls < options.curlMaxConnections) { std::list::iterator it=Curls.begin(); while(it != Curls.end() && RunningCurls < options.curlMaxConnections) { if ((*it)->data == NULL) { LOG_DL("(%s) starting new download '%s'", _Id.c_str(), it->url.c_str()); if (!startCurlDownload(*it)) { LOG_DL("(%s) failed to start '%s)'", _Id.c_str(), it->url.c_str()); finishCurlDownload(*it); it = Curls.erase(it); continue; } } ++it; } } if (RunningCurls > 0 || !Curls.empty()) LOG_DL("(%s) RunningCurls %d, _Curls %d", _Id.c_str(), RunningCurls, Curls.size()); } // Add url to MultiCurl queue and return cURL handle bool CGroupHTML::startCurlDownload(CDataDownload *download) { if (!MultiCurl) { nlwarning("Invalid MultiCurl handle, unable to download '%s'", download->url.c_str()); return false; } time_t currentTime; time(¤tTime); CHttpCacheObject cache; if (CFile::fileExists(download->dest)) cache = CHttpCache::getInstance()->lookup(download->dest); if (cache.Expires > currentTime) { LOG_DL("Cache for (%s) is not expired (%s, expires:%d)", download->url.c_str(), download->dest.c_str(), cache.Expires - currentTime); return false; } // use browser Id so that two browsers would not use same temp file download->tmpdest = localImageName(_Id + download->dest) + ".tmp"; // erase the tmp file if exists if (CFile::fileExists(download->tmpdest)) { CFile::deleteFile(download->tmpdest); } FILE *fp = nlfopen (download->tmpdest, "wb"); if (fp == NULL) { nlwarning("Can't open file '%s' for writing: code=%d '%s'", download->tmpdest.c_str (), errno, strerror(errno)); return false; } CURL *curl = curl_easy_init(); if (!curl) { fclose(fp); CFile::deleteFile(download->tmpdest); nlwarning("Creating cURL handle failed, unable to download '%s'", download->url.c_str()); return false; } LOG_DL("curl easy handle %p created for '%s'", curl, download->url.c_str()); // https:// if (toLowerAscii(download->url.substr(0, 8)) == "https://") { // if supported, use custom SSL context function to load certificates NLWEB::CCurlCertificates::useCertificates(curl); } download->data = new CCurlWWWData(curl, download->url); download->fp = fp; // initial connection timeout, curl default is 300sec curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, download->ConnectionTimeout); curl_easy_setopt(curl, CURLOPT_NOPROGRESS, true); curl_easy_setopt(curl, CURLOPT_URL, download->url.c_str()); // limit curl to HTTP and HTTPS protocols only curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); std::vector headers; if (!cache.Etag.empty()) headers.push_back("If-None-Match: " + cache.Etag); if (!cache.LastModified.empty()) headers.push_back("If-Modified-Since: " + cache.LastModified); if (headers.size() > 0) download->data->sendHeaders(headers); // catch headers curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, NLGUI::curlHeaderCallback); curl_easy_setopt(curl, CURLOPT_WRITEHEADER, download->data); std::string userAgent = options.appName + "/" + options.appVersion; curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str()); CUrlParser uri(download->url); if (!uri.host.empty()) sendCookies(curl, uri.host, isTrustedDomain(uri.host)); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite); CURLMcode ret = curl_multi_add_handle(MultiCurl, curl); if (ret != CURLM_OK) { nlwarning("cURL multi handle %p error %d on '%s'", curl, ret, download->url.c_str()); return false; } RunningCurls++; return true; } void CGroupHTML::finishCurlDownload(CDataDownload *download) { if (download) { download->finish(); delete download; } else { nlwarning("Unknown CURL download (nullptr)"); } } // Add a image download request in the multi_curl // return new textureId and download callback ICurlDownloadCB *CGroupHTML::addTextureDownload(const string &url, sint32 &texId, CViewBase *view) { CViewRenderer &rVR = *CViewRenderer::getInstance(); // data:image/png;base64,AA...== if (startsWith(url, "data:image/")) { texId = rVR.createTextureFromDataURL(url); return NULL; } std::string finalUrl; // load the image from local files/bnp if (lookupLocalFile(finalUrl, std::string(CFile::getPath(url) + CFile::getFilenameWithoutExtension(url) + ".tga").c_str(), false)) { texId = rVR.createTexture(finalUrl); return NULL; } finalUrl = upgradeInsecureUrl(getAbsoluteUrl(url)); // use requested url for local name (cache) string dest = localImageName(url); LOG_DL("add to download '%s' dest '%s'", finalUrl.c_str(), dest.c_str()); if (CFile::fileExists(dest) && CFile::getFileSize(dest) > 0) texId = rVR.createTexture(dest); else texId = rVR.newTextureId(dest); // Search if we are not already downloading this url. for(std::list::iterator it = Curls.begin(); it != Curls.end(); ++it) { if((*it)->url == finalUrl) { LOG_DL("already downloading '%s' img %p", finalUrl.c_str(), img); TextureDownloadCB *cb = dynamic_cast(*it); if (cb) { cb->addTexture(texId, view); // return pointer to shared ImageDownloadCB return cb; } else { nlwarning("Found texture download '%s', but casting to TextureDownloadCB failed", finalUrl.c_str()); } } } Curls.push_back(new TextureDownloadCB(finalUrl, dest, texId, this)); // as we return pointer to callback, skip starting downloads just now //pumpCurlQueue(); return Curls.back(); } // Add a image download request in the multi_curl ICurlDownloadCB *CGroupHTML::addImageDownload(const string &url, CViewBase *img, const CStyleParams &style, TImageType type, const std::string &placeholder) { std::string finalUrl; img->setModulateGlobalColor(style.GlobalColor); // data:image/png;base64,AA...== if (startsWith(url, "data:image/")) { setImage(img, decodeURIComponent(url), type); setImageSize(img, style); return NULL; } // load the image from local files/bnp std::string image = CFile::getPath(url) + CFile::getFilenameWithoutExtension(url) + ".tga"; if (lookupLocalFile(finalUrl, image.c_str(), false)) { setImage(img, image, type); setImageSize(img, style); return NULL; } finalUrl = upgradeInsecureUrl(getAbsoluteUrl(url)); // use requested url for local name (cache) string dest = localImageName(url); LOG_DL("add to download '%s' dest '%s' img %p", finalUrl.c_str(), dest.c_str(), img); // Display cached image while downloading new if (type != OverImage) { std::string temp = dest; if (!CFile::fileExists(temp) || CFile::getFileSize(temp) == 0) { temp = placeholder; } setImage(img, temp, type); setImageSize(img, style); } // Search if we are not already downloading this url. for(std::list::iterator it = Curls.begin(); it != Curls.end(); ++it) { if((*it)->url == finalUrl) { LOG_DL("already downloading '%s' img %p", finalUrl.c_str(), img); ImageDownloadCB *cb = dynamic_cast(*it); if (cb) { cb->addImage(img, style, type); // return pointer to shared ImageDownloadCB return cb; } else { nlwarning("Found image download '%s', but casting to ImageDownloadCB failed", finalUrl.c_str()); } } } Curls.push_back(new ImageDownloadCB(finalUrl, dest, img, style, type, this)); // as we return pointer to callback, skip starting downloads just now //pumpCurlQueue(); return Curls.back(); } void CGroupHTML::removeImageDownload(ICurlDownloadCB *handle, CViewBase *img) { ImageDownloadCB *cb = dynamic_cast(handle); if (!cb) { nlwarning("Trying to remove image from downloads, but ICurlDownloadCB pointer did not cast to ImageDownloadCB"); return; } // image will be removed from handle, but handle is kept and image will be downloaded cb->removeImage(img); } void CGroupHTML::initImageDownload() { LOG_DL("Init Image Download"); string pathName = "cache"; if ( ! CFile::isExists( pathName ) ) CFile::createDirectory( pathName ); } // Get an url and return the local filename with the path where the bnp should be string CGroupHTML::localBnpName(const string &url) { size_t lastIndex = url.find_last_of("/"); string dest = "user/"+url.substr(lastIndex+1); return dest; } // Add a bnp download request in the multi_curl, return true if already downloaded bool CGroupHTML::addBnpDownload(string url, const string &action, const string &script, const string &md5sum) { url = upgradeInsecureUrl(getAbsoluteUrl(url)); // Search if we are not already downloading this url. for(std::list::const_iterator it = Curls.begin(); it != Curls.end(); ++it) { if((*it)->url == url) { LOG_DL("already downloading '%s'", url.c_str()); return false; } } string dest = localBnpName(url); LOG_DL("add to download '%s' dest '%s'", url.c_str(), dest.c_str()); // create/delete the local file if (NLMISC::CFile::fileExists(dest)) { if (action == "override" || action == "delete") { CFile::setRWAccess(dest); NLMISC::CFile::deleteFile(dest); } else { return true; } } if (action != "delete") { Curls.push_back(new BnpDownloadCB(url, dest, md5sum, script, this)); pumpCurlQueue(); } else return true; return false; } void CGroupHTML::initBnpDownload() { if (!_TrustedDomain) return; LOG_DL("Init Bnp Download"); string pathName = "user"; if ( ! CFile::isExists( pathName ) ) CFile::createDirectory( pathName ); } void CGroupHTML::addStylesheetDownload(const std::vector links) { for(uint i = 0; i < links.size(); ++i) { _StylesheetQueue.push_back(links[i]); std::string url = getAbsoluteUrl(links[i].Url); _StylesheetQueue.back().Url = url; // push to the front of the queue Curls.push_front(new StylesheetDownloadCB(url, localImageName(url), this)); } pumpCurlQueue(); } // Call this evenly to check if an element is downloaded and then manage it void CGroupHTML::checkDownloads() { //nlassert(_CrtCheckMemory()); if(Curls.empty() && _CurlWWW == NULL) { return; } int NewRunningCurls = 0; while(CURLM_CALL_MULTI_PERFORM == curl_multi_perform(MultiCurl, &NewRunningCurls)) { LOG_DL("more to do now %d - %d curls", NewRunningCurls, Curls.size()); } LOG_DL("NewRunningCurls:%d, RunningCurls:%d", NewRunningCurls, RunningCurls); // check which downloads are done CURLMsg *msg; int msgs_left; while ((msg = curl_multi_info_read(MultiCurl, &msgs_left))) { LOG_DL("> (%s) msgs_left %d", _Id.c_str(), msgs_left); if (msg->msg == CURLMSG_DONE) { if (_CurlWWW && _CurlWWW->Request && _CurlWWW->Request == msg->easy_handle) { std::string error; bool success = msg->data.result == CURLE_OK; if (!success) { error = curl_easy_strerror(msg->data.result); } LOG_DL("html download finished with curl code %d (%s)", (sint)msg->msg, error.c_str()); htmlDownloadFinished(success, error); } else { for(std::list::iterator it = Curls.begin(); it != Curls.end(); ++it) { if((*it)->data && (*it)->data->Request == msg->easy_handle) { std::string error; bool success = msg->data.result == CURLE_OK; if (!success) { error = curl_easy_strerror(msg->data.result); } LOG_DL("data download finished with curl code %d (%s)", (sint)msg->msg, error.c_str()); dataDownloadFinished(success, error, *it); Curls.erase(it); break; } } } } } RunningCurls = NewRunningCurls; pumpCurlQueue(); } void CGroupHTML::releaseDownloads() { LOG_DL("Release Downloads"); if (_CurlWWW) { LOG_DL("(%s) stop html url '%s'", _Id.c_str(), _CurlWWW->Url.c_str()); if (MultiCurl) curl_multi_remove_handle(MultiCurl, _CurlWWW->Request); delete _CurlWWW; _CurlWWW = NULL; } releaseDataDownloads(); } void CGroupHTML::releaseDataDownloads() { LOG_DL("Clear pointers to %d curls", Curls.size()); // remove all queued and already started downloads for(std::list::iterator it = Curls.begin(); it != Curls.end(); ++it) { CDataDownload &dl = *(*it); if (dl.data) { LOG_DL("(%s) stop data url '%s'", _Id.c_str(), dl.url.c_str()); if (MultiCurl) { curl_multi_remove_handle(MultiCurl, dl.data->Request); } // close and remove temp file if (dl.fp) { fclose(dl.fp); if (CFile::fileExists(dl.tmpdest)) { CFile::deleteFile(dl.tmpdest); } } } // release CDataDownload delete *it; } Curls.clear(); // also clear css queue as it depends on Curls _StylesheetQueue.clear(); } class CGroupListAdaptor : public CInterfaceGroup { public: CGroupListAdaptor(const TCtorParam ¶m) : CInterfaceGroup(param) {} private: void updateCoords() { if (_Parent) { // Get the W max from the parent _W = std::min(_Parent->getMaxWReal(), _Parent->getWReal()); _WReal = _W; } CInterfaceGroup::updateCoords(); } }; // *************************************************************************** template void popIfNotEmpty(A &vect) { if(!vect.empty()) vect.pop_back(); } // *************************************************************************** TStyle CGroupHTML::parseStyle (const string &str_styles) { TStyle styles; vector elements; NLMISC::splitString(str_styles, ";", elements); for(uint i = 0; i < elements.size(); ++i) { vector style; NLMISC::splitString(elements[i], ":", style); if (style.size() >= 2) { string fullstyle = style[1]; for (uint j=2; j < style.size(); j++) fullstyle += ":"+style[j]; styles[trim(style[0])] = trimSeparators(fullstyle); } } return styles; } // *************************************************************************** void CGroupHTML::addText (const char *buf, int len) { if (_Browsing) { if (_IgnoreText) return; // Build a UTF8 string if (_ParsingLua && _TrustedDomain) { // we are parsing a lua script _LuaScript += string(buf, buf + len); // no more to do return; } // Build a unicode string CUtfStringView inputStringView(buf, len); // Build the final unicode string string tmp; tmp.reserve(len); u32char lastChar = 0; u32char inputStringView0 = *inputStringView.begin(); for (CUtfStringView::iterator it(inputStringView.begin()), end(inputStringView.end()); it != end; ++it) { u32char output; bool keep; // special treatment for 'nbsp' (which is returned as a discreet space) if (len == 1 && inputStringView0 == 32) { // this is a nbsp entity output = *it; keep = true; } else { // not nbsp, use normal white space removal routine keep = translateChar (output, *it, lastChar); } if (keep) { CUtfStringView::append(tmp, output); lastChar = output; } } if (!tmp.empty()) addString(tmp); } } // *************************************************************************** #define registerAnchorName(prefix) \ {\ if (present[prefix##_ID] && value[prefix##_ID]) \ _AnchorName.push_back(value[prefix##_ID]); \ } // *************************************************************************** void CGroupHTML::beginElement (CHtmlElement &elm) { _Style.pushStyle(); _CurrentHTMLElement = &elm; _CurrentHTMLNextSibling = elm.nextSibling; // set element style from css and style attribute _Style.getStyleFor(elm); if (!elm.Style.empty()) { _Style.applyStyle(elm.Style); } if (elm.hasNonEmptyAttribute("name")) { _AnchorName.push_back(elm.getAttribute("name")); } if (elm.hasNonEmptyAttribute("id")) { _AnchorName.push_back(elm.getAttribute("id")); } if (_Style.Current.DisplayBlock) { endParagraph(); } switch(elm.ID) { case HTML_A: htmlA(elm); break; case HTML_BASE: htmlBASE(elm); break; case HTML_BODY: htmlBODY(elm); break; case HTML_BR: htmlBR(elm); break; case HTML_BUTTON: htmlBUTTON(elm); break; case HTML_DD: htmlDD(elm); break; case HTML_DEL: renderPseudoElement(":before", elm); break; case HTML_DIV: htmlDIV(elm); break; case HTML_DL: htmlDL(elm); break; case HTML_DT: htmlDT(elm); break; case HTML_EM: renderPseudoElement(":before", elm); break; case HTML_FONT: htmlFONT(elm); break; case HTML_FORM: htmlFORM(elm); break; case HTML_H1://no-break case HTML_H2://no-break case HTML_H3://no-break case HTML_H4://no-break case HTML_H5://no-break case HTML_H6: htmlH(elm); break; case HTML_HEAD: htmlHEAD(elm); break; case HTML_HR: htmlHR(elm); break; case HTML_HTML: htmlHTML(elm); break; case HTML_I: htmlI(elm); break; case HTML_IMG: htmlIMG(elm); break; case HTML_INPUT: htmlINPUT(elm); break; case HTML_LI: htmlLI(elm); break; case HTML_LUA: htmlLUA(elm); break; case HTML_META: htmlMETA(elm); break; case HTML_METER: htmlMETER(elm); break; case HTML_OBJECT: htmlOBJECT(elm); break; case HTML_OL: htmlOL(elm); break; case HTML_OPTION: htmlOPTION(elm); break; case HTML_P: htmlP(elm); break; case HTML_PRE: htmlPRE(elm); break; case HTML_PROGRESS: htmlPROGRESS(elm); break; case HTML_SCRIPT: htmlSCRIPT(elm); break; case HTML_SELECT: htmlSELECT(elm); break; case HTML_SMALL: renderPseudoElement(":before", elm); break; case HTML_SPAN: renderPseudoElement(":before", elm); break; case HTML_STRONG: renderPseudoElement(":before", elm); break; case HTML_STYLE: htmlSTYLE(elm); break; case HTML_TABLE: htmlTABLE(elm); break; case HTML_TBODY: renderPseudoElement(":before", elm); break; case HTML_TD: htmlTD(elm); break; case HTML_TEXTAREA: htmlTEXTAREA(elm); break; case HTML_TFOOT: renderPseudoElement(":before", elm); break; case HTML_TH: htmlTH(elm); break; case HTML_TITLE: htmlTITLE(elm); break; case HTML_TR: htmlTR(elm); break; case HTML_U: renderPseudoElement(":before", elm); break; case HTML_UL: htmlUL(elm); break; default: renderPseudoElement(":before", elm); break; } } // *************************************************************************** void CGroupHTML::endElement(CHtmlElement &elm) { _CurrentHTMLElement = &elm; switch(elm.ID) { case HTML_A: htmlAend(elm); break; case HTML_BASE: break; case HTML_BODY: renderPseudoElement(":after", elm); break; case HTML_BR: break; case HTML_BUTTON: htmlBUTTONend(elm); break; case HTML_DD: htmlDDend(elm); break; case HTML_DEL: renderPseudoElement(":after", elm); break; case HTML_DIV: htmlDIVend(elm); break; case HTML_DL: htmlDLend(elm); break; case HTML_DT: htmlDTend(elm); break; case HTML_EM: renderPseudoElement(":after", elm);break; case HTML_FONT: break; case HTML_FORM: htmlFORMend(elm); break; case HTML_H1://no-break case HTML_H2://no-break case HTML_H3://no-break case HTML_H4://no-break case HTML_H5://no-break case HTML_H6: htmlHend(elm); break; case HTML_HEAD: htmlHEADend(elm); break; case HTML_HR: break; case HTML_HTML: break; case HTML_I: htmlIend(elm); break; case HTML_IMG: break; case HTML_INPUT: break; case HTML_LI: htmlLIend(elm); break; case HTML_LUA: htmlLUAend(elm); break; case HTML_META: break; case HTML_METER: break; case HTML_OBJECT: htmlOBJECTend(elm); break; case HTML_OL: htmlOLend(elm); break; case HTML_OPTION: htmlOPTIONend(elm); break; case HTML_P: htmlPend(elm); break; case HTML_PRE: htmlPREend(elm); break; case HTML_SCRIPT: htmlSCRIPTend(elm); break; case HTML_SELECT: htmlSELECTend(elm); break; case HTML_SMALL: renderPseudoElement(":after", elm);break; case HTML_SPAN: renderPseudoElement(":after", elm);break; case HTML_STRONG: renderPseudoElement(":after", elm);break; case HTML_STYLE: htmlSTYLEend(elm); break; case HTML_TABLE: htmlTABLEend(elm); break; case HTML_TD: htmlTDend(elm); break; case HTML_TBODY: renderPseudoElement(":after", elm); break; case HTML_TEXTAREA: break; case HTML_TFOOT: renderPseudoElement(":after", elm); break; case HTML_TH: htmlTHend(elm); break; case HTML_TITLE: break; case HTML_TR: htmlTRend(elm); break; case HTML_U: renderPseudoElement(":after", elm); break; case HTML_UL: htmlULend(elm); break; default: renderPseudoElement(":after", elm); break; } if (_Style.Current.DisplayBlock) { endParagraph(); } _Style.popStyle(); } // *************************************************************************** void CGroupHTML::renderPseudoElement(const std::string &pseudo, const CHtmlElement &elm) { if (pseudo != ":before" && pseudo != ":after") return; if (!elm.hasPseudo(pseudo)) return; _Style.pushStyle(); _Style.applyStyle(elm.getPseudo(pseudo)); // TODO: 'content' should already be tokenized in css parser as it has all the functions for that std::string content = trim(_Style.getStyle("content")); if (toLowerAscii(content) == "none" || toLowerAscii(content) == "normal") { _Style.popStyle(); return; } std::string::size_type pos = 0; // TODO: tokenize by whitespace while(pos < content.size()) { std::string::size_type start; std::string token; // not supported // counter, open-quote, close-quote, no-open-quote, no-close-quote if (content[pos] == '"' || content[pos] == '\'') { char quote = content[pos]; pos++; start = pos; while(pos < content.size() && content[pos] != quote) { if (content[pos] == '\\') pos++; pos++; } token = content.substr(start, pos - start); addString(token); // skip closing quote pos++; } else if (content[pos] == 'u' && pos < content.size() - 6 && toLowerAscii(content.substr(pos, 4)) == "url(") { // url(/path-to/image.jpg) / "Alt!" // url("/path to/image.jpg") / "Alt!" std::string tooltip; start = pos + 4; // fails if url contains ')' pos = content.find(")", start); if (pos == std::string::npos) break; token = trim(content.substr(start, pos - start)); // skip ')' pos++; // scan for tooltip start = pos; while(pos < content.size() && content[pos] == ' ' && content[pos] != '/') { pos++; } if (pos < content.size() && content[pos] == '/') { // skip '/' pos++; // skip whitespace while(pos < content.size() && content[pos] == ' ') { pos++; } if (pos < content.size() && (content[pos] == '\'' || content[pos] == '"')) { char openQuote = content[pos]; pos++; start = pos; while(pos < content.size() && content[pos] != openQuote) { if (content[pos] == '\\') pos++; pos++; } tooltip = content.substr(start, pos - start); // skip closing quote pos++; } else { // tooltip should be quoted pos = start; tooltip.clear(); } } else { // no tooltip pos = start; } if (tooltip.empty()) { addImage(getId() + pseudo, token, false, _Style.Current); } else { tooltip = trimQuotes(tooltip); addButton(CCtrlButton::PushButton, getId() + pseudo, token, token, "", "", "", tooltip.c_str(), _Style.Current); } } else if (content[pos] == 'a' && pos < content.size() - 7) { // attr(title) start = pos + 5; std::string::size_type end = 0; end = content.find(")", start); if (end != std::string::npos) { token = content.substr(start, end - start); // skip ')' pos = end + 1; if (elm.hasAttribute(token)) { addString(elm.getAttribute(token)); } } else { // skip over 'a' pos++; } } else { pos++; } } _Style.popStyle(); } // *************************************************************************** void CGroupHTML::renderDOM(CHtmlElement &elm) { if (elm.Type == CHtmlElement::TEXT_NODE) { addText(elm.Value.c_str(), elm.Value.size()); } else { beginElement(elm); if (!_IgnoreChildElements) { std::list::iterator it = elm.Children.begin(); while(it != elm.Children.end()) { renderDOM(*it); ++it; } } _IgnoreChildElements = false; endElement(elm); } } // *************************************************************************** NLMISC_REGISTER_OBJECT(CViewBase, CGroupHTML, std::string, "html"); // *************************************************************************** uint32 CGroupHTML::_GroupHtmlUIDPool= 0; CGroupHTML::TGroupHtmlByUIDMap CGroupHTML::_GroupHtmlByUID; // *************************************************************************** CGroupHTML::CGroupHTML(const TCtorParam ¶m) : CGroupScrollText(param), _TimeoutValue(DEFAULT_RYZOM_CONNECTION_TIMEOUT), _RedirectsRemaining(DEFAULT_RYZOM_REDIRECT_LIMIT), _CurrentHTMLElement(NULL) { // add it to map of group html created _GroupHtmlUID= ++_GroupHtmlUIDPool; // valid assigned Id begin to 1! _GroupHtmlByUID[_GroupHtmlUID]= this; // init _TrustedDomain = false; _ParsingLua = false; _LuaHrefHack = false; _IgnoreText = false; _IgnoreChildElements = false; _BrowseNextTime = false; _PostNextTime = false; _Browsing = false; _CurrentViewLink = NULL; _CurrentViewImage = NULL; _Indent.clear(); _LI = false; _SelectOption = false; _GroupListAdaptor = NULL; _UrlFragment.clear(); _RefreshUrl.clear(); _NextRefreshTime = 0.0; _LastRefreshTime = 0.0; _RenderNextTime = false; _WaitingForStylesheet = false; _AutoIdSeq = 0; _FormOpen = false; // Register CWidgetManager::getInstance()->registerClockMsgTarget(this); // HTML parameters ErrorColor = CRGBA(255, 0, 0); LinkColor = CRGBA(0, 0, 255); ErrorColorGlobalColor = false; LinkColorGlobalColor = false; TextColorGlobalColor = false; LIBeginSpace = 4; ULBeginSpace = 12; PBeginSpace = 12; TDBeginSpace = 0; ULIndent = 30; LineSpaceFontFactor = 0.5f; DefaultButtonGroup = "html_text_button"; DefaultFormTextGroup = "edit_box_widget"; DefaultFormTextAreaGroup = "edit_box_widget_multiline"; DefaultFormSelectGroup = "html_form_select_widget"; DefaultFormSelectBoxMenuGroup = "html_form_select_box_menu_widget"; DefaultCheckBoxBitmapNormal = "checkbox_normal.tga"; DefaultCheckBoxBitmapPushed = "checkbox_pushed.tga"; DefaultCheckBoxBitmapOver = "checkbox_over.tga"; DefaultRadioButtonBitmapNormal = "w_radiobutton.png"; DefaultRadioButtonBitmapPushed = "w_radiobutton_pushed.png"; DefaultBackgroundBitmapView = "bg"; clearContext(); MultiCurl = curl_multi_init(); #ifdef CURLMOPT_MAX_HOST_CONNECTIONS if (MultiCurl) { // added in libcurl 7.30.0 curl_multi_setopt(MultiCurl, CURLMOPT_MAX_HOST_CONNECTIONS, options.curlMaxConnections); curl_multi_setopt(MultiCurl, CURLMOPT_PIPELINING, 1); } #endif RunningCurls = 0; _CurlWWW = NULL; initImageDownload(); initBnpDownload(); // setup default browser style setProperty("browser_css_file", "browser.css"); } // *************************************************************************** CGroupHTML::~CGroupHTML() { //releaseImageDownload(); // TestYoyo //nlinfo("** CGroupHTML Destroy: %x, %s, uid%d", this, _Id.c_str(), _GroupHtmlUID); /* Erase from map of Group HTML (thus requestTerminated() callback won't be called) Do it first, just because don't want requestTerminated() to be called while I'm destroying (useless and may be dangerous) */ _GroupHtmlByUID.erase(_GroupHtmlUID); clearContext(); releaseDownloads(); if (_CurlWWW) delete _CurlWWW; if(MultiCurl) curl_multi_cleanup(MultiCurl); } std::string CGroupHTML::getProperty( const std::string &name ) const { if( name == "url" ) { return _URL; } else if( name == "title_prefix" ) { return _TitlePrefix; } else if( name == "error_color" ) { return toString( ErrorColor ); } else if( name == "link_color" ) { return toString( LinkColor ); } else if( name == "error_color_global_color" ) { return toString( ErrorColorGlobalColor ); } else if( name == "link_color_global_color" ) { return toString( LinkColorGlobalColor ); } else if( name == "text_color_global_color" ) { return toString( TextColorGlobalColor ); } else if( name == "td_begin_space" ) { return toString( TDBeginSpace ); } else if( name == "paragraph_begin_space" ) { return toString( PBeginSpace ); } else if( name == "li_begin_space" ) { return toString( LIBeginSpace ); } else if( name == "ul_begin_space" ) { return toString( ULBeginSpace ); } else if( name == "ul_indent" ) { return toString( ULIndent ); } else if( name == "multi_line_space_factor" ) { return toString( LineSpaceFontFactor ); } else if( name == "form_text_area_group" ) { return DefaultFormTextGroup; } else if( name == "form_select_group" ) { return DefaultFormSelectGroup; } else if( name == "checkbox_bitmap_normal" ) { return DefaultCheckBoxBitmapNormal; } else if( name == "checkbox_bitmap_pushed" ) { return DefaultCheckBoxBitmapPushed; } else if( name == "checkbox_bitmap_over" ) { return DefaultCheckBoxBitmapOver; } else if( name == "radiobutton_bitmap_normal" ) { return DefaultRadioButtonBitmapNormal; } else if( name == "radiobutton_bitmap_pushed" ) { return DefaultRadioButtonBitmapPushed; } else if( name == "radiobutton_bitmap_over" ) { return DefaultRadioButtonBitmapOver; } else if( name == "background_bitmap_view" ) { return DefaultBackgroundBitmapView; } else if( name == "home" ) { return Home; } else if( name == "browse_next_time" ) { return toString( _BrowseNextTime ); } else if( name == "browse_tree" ) { return _BrowseTree; } else if( name == "browse_undo" ) { return _BrowseUndoButton; } else if( name == "browse_redo" ) { return _BrowseRedoButton; } else if( name == "browse_refresh" ) { return _BrowseRefreshButton; } else if( name == "timeout" ) { return toString( _TimeoutValue ); } else if( name == "browser_css_file" ) { return _BrowserCssFile; } else return CGroupScrollText::getProperty( name ); } void CGroupHTML::setProperty( const std::string &name, const std::string &value ) { if( name == "url" ) { _URL = value; return; } else if( name == "title_prefix" ) { _TitlePrefix = value; return; } else if( name == "error_color" ) { CRGBA c; if( fromString( value, c ) ) ErrorColor = c; return; } else if( name == "link_color" ) { CRGBA c; if( fromString( value, c ) ) LinkColor = c; return; } else if( name == "error_color_global_color" ) { bool b; if( fromString( value, b ) ) ErrorColorGlobalColor = b; return; } else if( name == "link_color_global_color" ) { bool b; if( fromString( value, b ) ) LinkColorGlobalColor = b; return; } else if( name == "text_color_global_color" ) { bool b; if( fromString( value, b ) ) TextColorGlobalColor = b; return; } else if( name == "td_begin_space" ) { uint i; if( fromString( value, i ) ) TDBeginSpace = i; return; } else if( name == "paragraph_begin_space" ) { uint i; if( fromString( value, i ) ) PBeginSpace = i; return; } else if( name == "li_begin_space" ) { uint i; if( fromString( value, i ) ) LIBeginSpace = i; return; } else if( name == "ul_begin_space" ) { uint i; if( fromString( value, i ) ) ULBeginSpace = i; return; } else if( name == "ul_indent" ) { uint i; if( fromString( value, i ) ) ULIndent = i; return; } else if( name == "multi_line_space_factor" ) { float f; if( fromString( value, f ) ) LineSpaceFontFactor = f; return; } else if( name == "form_text_area_group" ) { DefaultFormTextGroup = value; return; } else if( name == "form_select_group" ) { DefaultFormSelectGroup = value; return; } else if( name == "checkbox_bitmap_normal" ) { DefaultCheckBoxBitmapNormal = value; return; } else if( name == "checkbox_bitmap_pushed" ) { DefaultCheckBoxBitmapPushed = value; return; } else if( name == "checkbox_bitmap_over" ) { DefaultCheckBoxBitmapOver = value; return; } else if( name == "radiobutton_bitmap_normal" ) { DefaultRadioButtonBitmapNormal = value; return; } else if( name == "radiobutton_bitmap_pushed" ) { DefaultRadioButtonBitmapPushed = value; return; } else if( name == "radiobutton_bitmap_over" ) { DefaultRadioButtonBitmapOver = value; return; } else if( name == "background_bitmap_view" ) { DefaultBackgroundBitmapView = value; return; } else if( name == "home" ) { Home = value; return; } else if( name == "browse_next_time" ) { bool b; if( fromString( value, b ) ) _BrowseNextTime = b; return; } else if( name == "browse_tree" ) { _BrowseTree = value; return; } else if( name == "browse_undo" ) { _BrowseUndoButton = value; return; } else if( name == "browse_redo" ) { _BrowseRedoButton = value; return; } else if( name == "browse_refresh" ) { _BrowseRefreshButton = value; return; } else if( name == "timeout" ) { double d; if( fromString( value, d ) ) _TimeoutValue = d; return; } else if( name == "browser_css_file") { _BrowserStyle.reset(); _BrowserCssFile = value; if (!_BrowserCssFile.empty()) { std::string filename = CPath::lookup(_BrowserCssFile, false, true, true); if (!filename.empty()) { CIFile in; if (in.open(filename)) { std::string css; if (in.readAll(css)) _BrowserStyle.parseStylesheet(css); else nlwarning("Failed to read browser css from '%s'", filename.c_str()); } else { nlwarning("Failed to open browser css file '%s'", filename.c_str()); } } else { nlwarning("Browser css file '%s' not found", _BrowserCssFile.c_str()); } } } else CGroupScrollText::setProperty( name, value ); } xmlNodePtr CGroupHTML::serialize( xmlNodePtr parentNode, const char *type ) const { xmlNodePtr node = CGroupScrollText::serialize( parentNode, type ); if( node == NULL ) return NULL; xmlSetProp( node, BAD_CAST "type", BAD_CAST "html" ); xmlSetProp( node, BAD_CAST "url", BAD_CAST _URL.c_str() ); xmlSetProp( node, BAD_CAST "title_prefix", BAD_CAST _TitlePrefix.c_str() ); xmlSetProp( node, BAD_CAST "error_color", BAD_CAST toString( ErrorColor ).c_str() ); xmlSetProp( node, BAD_CAST "link_color", BAD_CAST toString( LinkColor ).c_str() ); xmlSetProp( node, BAD_CAST "error_color_global_color", BAD_CAST toString( ErrorColorGlobalColor ).c_str() ); xmlSetProp( node, BAD_CAST "link_color_global_color", BAD_CAST toString( LinkColorGlobalColor ).c_str() ); xmlSetProp( node, BAD_CAST "text_color_global_color", BAD_CAST toString( TextColorGlobalColor ).c_str() ); xmlSetProp( node, BAD_CAST "td_begin_space", BAD_CAST toString( TDBeginSpace ).c_str() ); xmlSetProp( node, BAD_CAST "paragraph_begin_space", BAD_CAST toString( PBeginSpace ).c_str() ); xmlSetProp( node, BAD_CAST "li_begin_space", BAD_CAST toString( LIBeginSpace ).c_str() ); xmlSetProp( node, BAD_CAST "ul_begin_space", BAD_CAST toString( ULBeginSpace ).c_str() ); xmlSetProp( node, BAD_CAST "ul_indent", BAD_CAST toString( ULIndent ).c_str() ); xmlSetProp( node, BAD_CAST "multi_line_space_factor", BAD_CAST toString( LineSpaceFontFactor ).c_str() ); xmlSetProp( node, BAD_CAST "form_text_area_group", BAD_CAST DefaultFormTextGroup.c_str() ); xmlSetProp( node, BAD_CAST "form_select_group", BAD_CAST DefaultFormSelectGroup.c_str() ); xmlSetProp( node, BAD_CAST "checkbox_bitmap_normal", BAD_CAST DefaultCheckBoxBitmapNormal.c_str() ); xmlSetProp( node, BAD_CAST "checkbox_bitmap_pushed", BAD_CAST DefaultCheckBoxBitmapPushed.c_str() ); xmlSetProp( node, BAD_CAST "checkbox_bitmap_over", BAD_CAST DefaultCheckBoxBitmapOver.c_str() ); xmlSetProp( node, BAD_CAST "radiobutton_bitmap_normal", BAD_CAST DefaultRadioButtonBitmapNormal.c_str() ); xmlSetProp( node, BAD_CAST "radiobutton_bitmap_pushed", BAD_CAST DefaultRadioButtonBitmapPushed.c_str() ); xmlSetProp( node, BAD_CAST "radiobutton_bitmap_over", BAD_CAST DefaultRadioButtonBitmapOver.c_str() ); xmlSetProp( node, BAD_CAST "background_bitmap_view", BAD_CAST DefaultBackgroundBitmapView.c_str() ); xmlSetProp( node, BAD_CAST "home", BAD_CAST Home.c_str() ); xmlSetProp( node, BAD_CAST "browse_next_time", BAD_CAST toString( _BrowseNextTime ).c_str() ); xmlSetProp( node, BAD_CAST "browse_tree", BAD_CAST _BrowseTree.c_str() ); xmlSetProp( node, BAD_CAST "browse_undo", BAD_CAST _BrowseUndoButton.c_str() ); xmlSetProp( node, BAD_CAST "browse_redo", BAD_CAST _BrowseRedoButton.c_str() ); xmlSetProp( node, BAD_CAST "browse_refresh", BAD_CAST _BrowseRefreshButton.c_str() ); xmlSetProp( node, BAD_CAST "timeout", BAD_CAST toString( _TimeoutValue ).c_str() ); xmlSetProp( node, BAD_CAST "browser_css_file", BAD_CAST _BrowserCssFile.c_str() ); return node; } // *************************************************************************** bool CGroupHTML::parse(xmlNodePtr cur,CInterfaceGroup *parentGroup) { nlassert( CWidgetManager::getInstance()->isClockMsgTarget(this)); if(!CGroupScrollText::parse(cur, parentGroup)) return false; // TestYoyo //nlinfo("** CGroupHTML parsed Ok: %x, %s, %s, uid%d", this, _Id.c_str(), typeid(this).name(), _GroupHtmlUID); CXMLAutoPtr ptr; // Get the url ptr = xmlGetProp (cur, (xmlChar*)"url"); if (ptr) _URL = (const char*)ptr; // Bkup default for undo/redo _AskedUrl= _URL; ptr = xmlGetProp (cur, (xmlChar*)"title_prefix"); if (ptr) _TitlePrefix = CI18N::get((const char*)ptr); // Parameters ptr = xmlGetProp (cur, (xmlChar*)"error_color"); if (ptr) ErrorColor = convertColor(ptr); ptr = xmlGetProp (cur, (xmlChar*)"link_color"); if (ptr) LinkColor = convertColor(ptr); ptr = xmlGetProp (cur, (xmlChar*)"error_color_global_color"); if (ptr) ErrorColorGlobalColor = convertBool(ptr); ptr = xmlGetProp (cur, (xmlChar*)"link_color_global_color"); if (ptr) LinkColorGlobalColor = convertBool(ptr); ptr = xmlGetProp (cur, (xmlChar*)"text_color_global_color"); if (ptr) TextColorGlobalColor = convertBool(ptr); ptr = xmlGetProp (cur, (xmlChar*)"td_begin_space"); if (ptr) fromString((const char*)ptr, TDBeginSpace); ptr = xmlGetProp (cur, (xmlChar*)"paragraph_begin_space"); if (ptr) fromString((const char*)ptr, PBeginSpace); ptr = xmlGetProp (cur, (xmlChar*)"li_begin_space"); if (ptr) fromString((const char*)ptr, LIBeginSpace); ptr = xmlGetProp (cur, (xmlChar*)"ul_begin_space"); if (ptr) fromString((const char*)ptr, ULBeginSpace); ptr = xmlGetProp (cur, (xmlChar*)"ul_indent"); if (ptr) fromString((const char*)ptr, ULIndent); ptr = xmlGetProp (cur, (xmlChar*)"multi_line_space_factor"); if (ptr) fromString((const char*)ptr, LineSpaceFontFactor); ptr = xmlGetProp (cur, (xmlChar*)"form_text_group"); if (ptr) DefaultFormTextGroup = (const char*)(ptr); ptr = xmlGetProp (cur, (xmlChar*)"form_text_area_group"); if (ptr) DefaultFormTextAreaGroup = (const char*)(ptr); ptr = xmlGetProp (cur, (xmlChar*)"form_select_group"); if (ptr) DefaultFormSelectGroup = (const char*)(ptr); ptr = xmlGetProp (cur, (xmlChar*)"checkbox_bitmap_normal"); if (ptr) DefaultCheckBoxBitmapNormal = (const char*)(ptr); ptr = xmlGetProp (cur, (xmlChar*)"checkbox_bitmap_pushed"); if (ptr) DefaultCheckBoxBitmapPushed = (const char*)(ptr); ptr = xmlGetProp (cur, (xmlChar*)"checkbox_bitmap_over"); if (ptr) DefaultCheckBoxBitmapOver = (const char*)(ptr); ptr = xmlGetProp (cur, (xmlChar*)"radiobutton_bitmap_normal"); if (ptr) DefaultRadioButtonBitmapNormal = (const char*)(ptr); ptr = xmlGetProp (cur, (xmlChar*)"radiobutton_bitmap_pushed"); if (ptr) DefaultRadioButtonBitmapPushed = (const char*)(ptr); ptr = xmlGetProp (cur, (xmlChar*)"radiobutton_bitmap_over"); if (ptr) DefaultRadioButtonBitmapOver = (const char*)(ptr); ptr = xmlGetProp (cur, (xmlChar*)"background_bitmap_view"); if (ptr) DefaultBackgroundBitmapView = (const char*)(ptr); ptr = xmlGetProp (cur, (xmlChar*)"home"); if (ptr) Home = (const char*)(ptr); ptr = xmlGetProp (cur, (xmlChar*)"browse_next_time"); if (ptr) _BrowseNextTime = convertBool(ptr); ptr = xmlGetProp (cur, (xmlChar*)"browse_tree"); if(ptr) _BrowseTree = (const char*)ptr; ptr = xmlGetProp (cur, (xmlChar*)"browse_undo"); if(ptr) _BrowseUndoButton= (const char*)ptr; ptr = xmlGetProp (cur, (xmlChar*)"browse_redo"); if(ptr) _BrowseRedoButton = (const char*)ptr; ptr = xmlGetProp (cur, (xmlChar*)"browse_refresh"); if(ptr) _BrowseRefreshButton = (const char*)ptr; ptr = xmlGetProp (cur, (xmlChar*)"timeout"); if(ptr) fromString((const char*)ptr, _TimeoutValue); ptr = xmlGetProp (cur, (xmlChar*)"browser_css_file"); if (ptr) { setProperty("browser_css_file", (const char *)ptr); } return true; } // *************************************************************************** bool CGroupHTML::handleEvent (const NLGUI::CEventDescriptor& eventDesc) { bool traited = false; if (eventDesc.getType() == NLGUI::CEventDescriptor::mouse) { const NLGUI::CEventDescriptorMouse &mouseEvent = (const NLGUI::CEventDescriptorMouse &)eventDesc; if (mouseEvent.getEventTypeExtended() == NLGUI::CEventDescriptorMouse::mousewheel) { // Check if mouse wheel event was on any of multiline select box widgets // Must do this before CGroupScrollText for (uint i=0; i<_Forms.size() && !traited; i++) { for (uint j=0; j<_Forms[i].Entries.size() && !traited; j++) { if (_Forms[i].Entries[j].SelectBox) { if (_Forms[i].Entries[j].SelectBox->handleEvent(eventDesc)) { traited = true; break; } } } } } } if (!traited) traited = CGroupScrollText::handleEvent (eventDesc); if (eventDesc.getType() == NLGUI::CEventDescriptor::system) { const NLGUI::CEventDescriptorSystem &systemEvent = (const NLGUI::CEventDescriptorSystem &) eventDesc; if (systemEvent.getEventTypeExtended() == NLGUI::CEventDescriptorSystem::clocktick) { // Handle now handle (); } if (systemEvent.getEventTypeExtended() == NLGUI::CEventDescriptorSystem::activecalledonparent) { if (!((NLGUI::CEventDescriptorActiveCalledOnParent &) systemEvent).getActive()) { // stop refresh when window gets hidden _NextRefreshTime = 0; } } } return traited; } // *************************************************************************** void CGroupHTML::endParagraph() { _Paragraph = NULL; paragraphChange (); } // *************************************************************************** void CGroupHTML::newParagraph(uint beginSpace) { // Add a new paragraph CGroupParagraph *newParagraph = new CGroupParagraph(CViewBase::TCtorParam()); newParagraph->setId(getCurrentGroup()->getId() + ":PARAGRAPH" + toString(getNextAutoIdSeq())); newParagraph->setResizeFromChildH(true); newParagraph->setMarginLeft(getIndent()); if (!_Style.Current.TextAlign.empty()) { if (_Style.Current.TextAlign == "left") newParagraph->setTextAlign(CGroupParagraph::AlignLeft); else if (_Style.Current.TextAlign == "center") newParagraph->setTextAlign(CGroupParagraph::AlignCenter); else if (_Style.Current.TextAlign == "right") newParagraph->setTextAlign(CGroupParagraph::AlignRight); else if (_Style.Current.TextAlign == "justify") newParagraph->setTextAlign(CGroupParagraph::AlignJustify); } // Add to the group addHtmlGroup (newParagraph, beginSpace); _Paragraph = newParagraph; paragraphChange (); } // *************************************************************************** void CGroupHTML::browse(const char *url) { // modify undo/redo pushUrlUndoRedo(url); // do the browse, with no undo/redo doBrowse(url); } // *************************************************************************** void CGroupHTML::refresh() { if (!_URL.empty()) doBrowse(_URL.c_str(), true); } // *************************************************************************** void CGroupHTML::doBrowse(const char *url, bool force) { LOG_DL("(%s) Browsing URL : '%s'", _Id.c_str(), url); CUrlParser uri(url); if (!uri.hash.empty()) { // Anchor to scroll after page has loaded _UrlFragment = uri.hash; uri.inherit(_DocumentUrl); uri.hash.clear(); // compare urls and see if we only navigating to new anchor if (!force && _DocumentUrl == uri.toString()) { // scroll happens in updateCoords() invalidateCoords(); return; } } else _UrlFragment.clear(); // go _URL = uri.toString(); _BrowseNextTime = true; _WaitingForStylesheet = false; // if a BrowseTree is bound to us, try to select the node that opens this URL (auto-locate) if(!_BrowseTree.empty()) { CGroupTree *groupTree=dynamic_cast(CWidgetManager::getInstance()->getElementFromId(_BrowseTree)); if(groupTree) { string nodeId= selectTreeNodeRecurs(groupTree->getRootNode(), url); // select the node if(!nodeId.empty()) { groupTree->selectNodeById(nodeId); } } } } // *************************************************************************** void CGroupHTML::browseError (const char *msg) { releaseDownloads(); // Get the list group from CGroupScrollText removeContent(); newParagraph(0); CViewText *viewText = new CViewText ("", (string("Error : ")+msg).c_str()); viewText->setColor (ErrorColor); viewText->setModulateGlobalColor(ErrorColorGlobalColor); viewText->setMultiLine (true); getParagraph()->addChild (viewText); if(!_TitlePrefix.empty()) setTitle (_TitlePrefix); updateRefreshButton(); invalidateCoords(); } void CGroupHTML::browseErrorHtml(const std::string &html) { releaseDownloads(); removeContent(); renderHtmlString(html); updateRefreshButton(); invalidateCoords(); } // *************************************************************************** bool CGroupHTML::isBrowsing() { // do not show spinning cursor for image downloads (!Curls.empty()) return _BrowseNextTime || _PostNextTime || _RenderNextTime || _Browsing || _WaitingForStylesheet || _CurlWWW; } // *************************************************************************** void CGroupHTML::updateCoords() { CGroupScrollText::updateCoords(); // all elements are in their correct place, tell scrollbar to scroll to anchor if (!_Browsing && !_UrlFragment.empty()) { doBrowseAnchor(_UrlFragment); _UrlFragment.clear(); } if (!m_HtmlBackground.isEmpty() || !m_BodyBackground.isEmpty()) { // get scroll offset from list CGroupList *list = getList(); if (list) { CInterfaceElement* vp = list->getParentPos() ? list->getParentPos() : this; sint htmlW = std::max(vp->getWReal(), list->getWReal()); sint htmlH = list->getHReal(); sint htmlX = list->getXReal() + list->getOfsX(); sint htmlY = list->getYReal() + list->getOfsY(); if (!m_HtmlBackground.isEmpty()) { m_HtmlBackground.setFillViewport(true); m_HtmlBackground.setBorderArea(htmlX, htmlY, htmlW, htmlH); m_HtmlBackground.setPaddingArea(htmlX, htmlY, htmlW, htmlH); m_HtmlBackground.setContentArea(htmlX, htmlY, htmlW, htmlH); } if (!m_BodyBackground.isEmpty()) { // TODO: html padding + html border m_BodyBackground.setBorderArea(htmlX, htmlY, htmlW, htmlH); // TODO: html padding + html border + body border m_BodyBackground.setPaddingArea(htmlX, htmlY, htmlW, htmlH); // TODO: html padding + html_border + body padding m_BodyBackground.setContentArea(htmlX, htmlY, htmlW, htmlH); } } } } // *************************************************************************** bool CGroupHTML::translateChar(u32char &output, u32char input, u32char lastCharParam) const { // Keep this char ? bool keep = true; // char is between table elements // TODO: only whitespace is handled, text is added to either TD, or after TABLE (should be before) bool tableWhitespace = getTable() && (_Cells.empty() || _Cells.back() == NULL); switch (input) { // Return / tab only in
 mode
		case '\t':
		case '\n':
			{
				if (tableWhitespace)
				{
					keep = false;
				}
				else
				{
					// Get the last char
					u32char lastChar = lastCharParam;
					if (lastChar == 0)
						lastChar = getLastChar();
					keep = ((lastChar != (u32char)' ') &&
							(lastChar != 0)) || getPRE() || (_CurrentViewImage && (lastChar == 0));
					if(!getPRE())
						input = (u32char)' ';
				}
			}
			break;
		case ' ':
			{
				if (tableWhitespace)
				{
					keep = false;
				}
				else
				{
					// Get the last char
					u32char lastChar = lastCharParam;
					if (lastChar == 0)
						lastChar = getLastChar();
					keep = ((lastChar != (u32char)' ') &&
							(lastChar != (u32char)'\n') &&
							(lastChar != 0)) || getPRE() || (_CurrentViewImage && (lastChar == 0));
				}
			}
			break;
		case 0xd:
			keep = false;
			break;
		}

		if (keep)
		{
			output = input;
		}

		return keep;
	}

	// ***************************************************************************

	void CGroupHTML::registerAnchor(CInterfaceElement* elm)
	{
		if (!_AnchorName.empty())
		{
			for(uint32 i=0; i <  _AnchorName.size(); ++i)
			{
				// filter out duplicates and register only first
				if (!_AnchorName[i].empty() && _Anchors.count(_AnchorName[i]) == 0)
				{
					_Anchors[_AnchorName[i]] = elm;
				}
			}

			_AnchorName.clear();
		}
	}

	// ***************************************************************************
	bool CGroupHTML::isSameStyle(CViewLink *text, const CStyleParams &style) const
	{
		if (!text) return false;

		bool embolden = style.FontWeight >= FONT_WEIGHT_BOLD;
		bool sameShadow = style.TextShadow.Enabled && text->getShadow();
		if (sameShadow && style.TextShadow.Enabled)
		{
			sint sx, sy;
			text->getShadowOffset(sx, sy);
			sameShadow = (style.TextShadow.Color == text->getShadowColor());
			sameShadow = sameShadow && (style.TextShadow.Outline == text->getShadowOutline());
			sameShadow = sameShadow && (style.TextShadow.X == sx) && (style.TextShadow.Y == sy);
		}
		// Compatible with current parameters ?
		return sameShadow &&
			(style.TextColor == text->getColor()) &&
			(style.FontFamily == text->getFontName()) &&
			(style.FontSize == (uint)text->getFontSize()) &&
			(style.Underlined == text->getUnderlined()) &&
			(style.StrikeThrough == text->getStrikeThrough()) &&
			(embolden == text->getEmbolden()) &&
			(style.FontOblique == text->getOblique()) &&
			(getLink() == text->Link) &&
			(style.GlobalColorText == text->getModulateGlobalColor());
	}

	// ***************************************************************************
	void CGroupHTML::newTextButton(const std::string &text, const std::string &tpl)
	{
		_CurrentViewLink = NULL;
		_CurrentViewImage = NULL;

		// Action handler parameters : "name=group_html_id|form=id_of_the_form|submit_button=button_name"
		string param = "name=" + this->_Id + "|url=" + getLink();
		string name;
		if (!_AnchorName.empty())
			name = _AnchorName.back();

		typedef pair TTmplParam;
		vector tmplParams;
		tmplParams.push_back(TTmplParam("id", ""));
		tmplParams.push_back(TTmplParam("onclick", "browse"));
		tmplParams.push_back(TTmplParam("onclick_param", param));
		tmplParams.push_back(TTmplParam("active", "true"));
		CInterfaceGroup *buttonGroup = CWidgetManager::getInstance()->getParser()->createGroupInstance(tpl, getId()+":"+name, tmplParams);
		if (!buttonGroup)
		{
			nlinfo("Text button template '%s' not found", tpl.c_str());
			return;
		}
		buttonGroup->setId(getId()+":"+name);

		// Add the ctrl button
		CCtrlTextButton *ctrlButton = dynamic_cast(buttonGroup->getCtrl("button"));
		if (!ctrlButton) ctrlButton = dynamic_cast(buttonGroup->getCtrl("b"));
		if (!ctrlButton)
		{
			nlinfo("Text button template '%s' is missing :button or :b text element", tpl.c_str());
			return;
		}
		ctrlButton->setModulateGlobalColorAll(_Style.Current.GlobalColor);
		ctrlButton->setTextModulateGlobalColorNormal(_Style.Current.GlobalColorText);
		ctrlButton->setTextModulateGlobalColorOver(_Style.Current.GlobalColorText);
		ctrlButton->setTextModulateGlobalColorPushed(_Style.Current.GlobalColorText);

		// Translate the tooltip
		ctrlButton->setText(text);
		ctrlButton->setDefaultContextHelp(std::string(getLinkTitle()));
		// empty url / button disabled
		ctrlButton->setFrozen(*getLink() == '\0');

		setTextButtonStyle(ctrlButton, _Style.Current);

		_Paragraph->addChild(buttonGroup);
	}

	// ***************************************************************************
	void CGroupHTML::newTextLink(const std::string &text)
	{
		CViewLink *newLink = new CViewLink(CViewBase::TCtorParam());
		if (getA())
		{
			newLink->Link = getLink();
			newLink->LinkTitle = getLinkTitle();
			if (!newLink->Link.empty())
			{
				newLink->setHTMLView (this);
				newLink->setActionOnLeftClick("browse");
				newLink->setParamsOnLeftClick("name=" + getId() + "|url=" + newLink->Link);
			}
		}
		newLink->setText(text);
		newLink->setMultiLineSpace((uint)((float)(_Style.Current.FontSize)*LineSpaceFontFactor));
		newLink->setMultiLine(true);
		newLink->setModulateGlobalColor(_Style.Current.GlobalColorText);
		setTextStyle(newLink, _Style.Current);

		registerAnchor(newLink);

		if (getA() && !newLink->Link.empty())
			getParagraph()->addChildLink(newLink);
		else
			getParagraph()->addChild(newLink);

		_CurrentViewLink = newLink;
		_CurrentViewImage = NULL;
	}

	// ***************************************************************************

	void CGroupHTML::addString(const std::string &str)
	{
		string tmpStr = str;

		if (_Localize)
		{
			string	_str = tmpStr;
			string::size_type	p = _str.find('#');
			if (p == string::npos)
			{
				tmpStr = CI18N::get(_str);
			}
			else
			{
				string	cmd = _str.substr(0, p);
				string	arg = _str.substr(p+1);

				if (cmd == "date")
				{
					uint	year, month, day;
					sscanf(arg.c_str(), "%d/%d/%d", &year, &month, &day);
					tmpStr = CI18N::get( "uiMFIDate");

					year += (year > 70 ? 1900 : 2000);

					strFindReplace(tmpStr, "%year", toString("%d", year) );
					strFindReplace(tmpStr, "%month", CI18N::get(toString("uiMonth%02d", month)) );
					strFindReplace(tmpStr, "%day", toString("%d", day) );
				}
				else
				{
					tmpStr = arg;
				}
			}
		}

		// In title ?
		if (_Object)
		{
			_ObjectScript += tmpStr;
		}
		else if (_SelectOption)
		{
			if (!(_Forms.empty()))
			{
				if (!_Forms.back().Entries.empty())
				{
					_SelectOptionStr += tmpStr;
				}
			}
		}
		else
		{
			// In a paragraph ?
			if (!_Paragraph)
			{
				newParagraph (0);
				paragraphChange ();
			}

			CStyleParams &style = _Style.Current;

			// Text added ?
			bool added = false;

			if (_CurrentViewLink)
			{
				bool skipLine = !_CurrentViewLink->getText().empty() && *(_CurrentViewLink->getText().rbegin()) == '\n';
				if (!skipLine && isSameStyle(_CurrentViewLink, style))
				{
					// Concat the text
					_CurrentViewLink->setText(_CurrentViewLink->getText()+tmpStr);
					_CurrentViewLink->invalidateContent();
					added = true;
				}
			}

			// Not added ?
			if (!added)
			{
				if (getA() && string(getLinkClass()) == "ryzom-ui-button")
					newTextButton(tmpStr, DefaultButtonGroup);
				else
					newTextLink(tmpStr);
			}
		}
	}

	// ***************************************************************************

	void CGroupHTML::addImage(const std::string &id, const std::string &img, bool reloadImg, const CStyleParams &style)
	{
		// In a paragraph ?
		if (!_Paragraph)
		{
			newParagraph (0);
			paragraphChange ();
		}

		// No more text in this text view
		_CurrentViewLink = NULL;

		// Not added ?
		CViewBitmap *newImage = new CViewBitmap (TCtorParam());
		newImage->setId(id);

		addImageDownload(img, newImage, style, NormalImage);
		newImage->setRenderLayer(getRenderLayer()+1);

		getParagraph()->addChild(newImage);
		paragraphChange ();

		setImageSize(newImage, style);
	}

	// ***************************************************************************

	CInterfaceGroup *CGroupHTML::addTextArea(const std::string &templateName, const char *name, uint rows, uint cols, bool multiLine, const std::string &content, uint maxlength)
	{
		// In a paragraph ?
		if (!_Paragraph)
		{
			newParagraph (0);
			paragraphChange ();
		}

		// No more text in this text view
		_CurrentViewLink = NULL;

		CStyleParams &style = _Style.Current;
		{
			// override cols/rows values from style
			if (style.Width > 0) cols = style.Width / style.FontSize;
			if (style.Height > 0) rows = style.Height / style.FontSize;

			// Not added ?
			std::vector > templateParams;
			templateParams.push_back (std::pair ("w", toString (cols*style.FontSize)));
			templateParams.push_back (std::pair ("id", name));
			templateParams.push_back (std::pair ("prompt", ""));
			templateParams.push_back (std::pair ("multiline", multiLine?"true":"false"));
			templateParams.push_back (std::pair ("fontsize", toString (style.FontSize)));
			templateParams.push_back (std::pair ("color", style.TextColor.toString()));
			if (style.FontWeight >= FONT_WEIGHT_BOLD)
				templateParams.push_back (std::pair ("fontweight", "bold"));
			if (style.FontOblique)
				templateParams.push_back (std::pair ("fontstyle", "oblique"));
			if (multiLine)
				templateParams.push_back (std::pair ("multi_min_line", toString(rows)));
			templateParams.push_back (std::pair ("want_return", multiLine?"true":"false"));
			templateParams.push_back (std::pair ("onenter", ""));
			templateParams.push_back (std::pair ("enter_recover_focus", "false"));
			if (maxlength > 0)
				templateParams.push_back (std::pair ("max_num_chars", toString(maxlength)));
			templateParams.push_back (std::pair ("shadow", toString(style.TextShadow.Enabled)));
			if (style.TextShadow.Enabled)
			{
				templateParams.push_back (std::pair ("shadow_x", toString(style.TextShadow.X)));
				templateParams.push_back (std::pair ("shadow_y", toString(style.TextShadow.Y)));
				templateParams.push_back (std::pair ("shadow_color", style.TextShadow.Color.toString()));
				templateParams.push_back (std::pair ("shadow_outline", toString(style.TextShadow.Outline)));
			}

			CInterfaceGroup *textArea = CWidgetManager::getInstance()->getParser()->createGroupInstance (templateName.c_str(),
				getParagraph()->getId(), templateParams.empty()?NULL:&(templateParams[0]), (uint)templateParams.size());

			// Group created ?
			if (textArea)
			{
				// Set the content
				CGroupEditBox *eb = dynamic_cast(textArea->getGroup("eb"));
				if (eb)
				{
					eb->setInputString(decodeHTMLEntities(content));
					if (style.hasStyle("background-color"))
					{
						CViewBitmap *bg = dynamic_cast(eb->getView("bg"));
						if (bg)
						{
							bg->setTexture("blank.tga");
							bg->setColor(style.Background.color);
						}
					}
				}

				textArea->invalidateCoords();
				getParagraph()->addChild (textArea);
				paragraphChange ();

				return textArea;
			}
		}

		// Not group created
		return NULL;
	}

	// ***************************************************************************
	CDBGroupComboBox *CGroupHTML::addComboBox(const std::string &templateName, const char *name)
	{
		// In a paragraph ?
		if (!_Paragraph)
		{
			newParagraph (0);
			paragraphChange ();
		}


		{
			// Not added ?
			std::vector > templateParams;
			templateParams.push_back (std::pair ("id", name));
			CInterfaceGroup *group = CWidgetManager::getInstance()->getParser()->createGroupInstance (templateName.c_str(),
				getParagraph()->getId(), templateParams.empty()?NULL:&(templateParams[0]), (uint)templateParams.size());

			// Group created ?
			if (group)
			{
				// Set the content
				CDBGroupComboBox *cb = dynamic_cast(group);
				if (!cb)
				{
					nlwarning("'%s' template has bad type, combo box expected", templateName.c_str());
					delete cb;
					return NULL;
				}
				else
				{
					getParagraph()->addChild (cb);
					paragraphChange ();
					return cb;
				}
			}
		}

		// Not group created
		return NULL;
	}

	// ***************************************************************************
	CGroupMenu *CGroupHTML::addSelectBox(const std::string &templateName, const char *name)
	{
		// In a paragraph ?
		if (!_Paragraph)
		{
			newParagraph (0);
			paragraphChange ();
		}

		// Not added ?
		std::vector > templateParams;
		templateParams.push_back(std::pair ("id", name));
		CInterfaceGroup *group = CWidgetManager::getInstance()->getParser()->createGroupInstance(templateName.c_str(),
			getParagraph()->getId(), &(templateParams[0]), (uint)templateParams.size());

		// Group created ?
		if (group)
		{
			// Set the content
			CGroupMenu *sb = dynamic_cast(group);
			if (!sb)
			{
				nlwarning("'%s' template has bad type, CGroupMenu expected", templateName.c_str());
				delete sb;
				return NULL;
			}
			else
			{
				getParagraph()->addChild (sb);
				paragraphChange ();
				return sb;
			}
		}

		// No group created
		return NULL;
	}

	// ***************************************************************************

	CCtrlButton *CGroupHTML::addButton(CCtrlButton::EType type, const std::string &name, const std::string &normalBitmap, const std::string &pushedBitmap,
									  const std::string &overBitmap, const char *actionHandler, const char *actionHandlerParams,
									  const std::string &tooltip, const CStyleParams &style)
	{
		// In a paragraph ?
		if (!_Paragraph)
		{
			newParagraph (0);
			paragraphChange ();
		}

		// Add the ctrl button
		CCtrlButton *ctrlButton = new CCtrlButton(TCtorParam());
		if (!name.empty())
		{
			ctrlButton->setId(name);
		}

		std::string normal;
		if (startsWith(normalBitmap, "data:image/"))
		{
			normal = decodeURIComponent(normalBitmap);
		}
		else
		{
			// Load only tga files.. (conversion in dds filename is done in the lookup procedure)
			normal = normalBitmap.empty()?"":CFile::getPath(normalBitmap) + CFile::getFilenameWithoutExtension(normalBitmap) + ".tga";

			// if the image doesn't exist on local, we check in the cache
			if(!CPath::exists(normal))
			{
				// search in the compressed texture
				CViewRenderer &rVR = *CViewRenderer::getInstance();
				sint32 id = rVR.getTextureIdFromName(normal);
				if(id == -1)
				{
					normal = localImageName(normalBitmap);
					addImageDownload(normalBitmap, ctrlButton, style);
				}
			}
		}

		std::string pushed;
		if (startsWith(pushedBitmap, "data:image/"))
		{
			pushed = decodeURIComponent(pushedBitmap);
		}
		else
		{
			pushed = pushedBitmap.empty()?"":CFile::getPath(pushedBitmap) + CFile::getFilenameWithoutExtension(pushedBitmap) + ".tga";
			// if the image doesn't exist on local, we check in the cache, don't download it because the "normal" will already setuped it
			if(!CPath::exists(pushed))
			{
				// search in the compressed texture
				CViewRenderer &rVR = *CViewRenderer::getInstance();
				sint32 id = rVR.getTextureIdFromName(pushed);
				if(id == -1)
				{
					pushed = localImageName(pushedBitmap);
				}
			}
		}

		std::string over;
		if (startsWith(overBitmap, "data:image/"))
		{
			over = decodeURIComponent(overBitmap);
		}
		else
		{
			over = overBitmap.empty()?"":CFile::getPath(overBitmap) + CFile::getFilenameWithoutExtension(overBitmap) + ".tga";
			// schedule mouseover bitmap for download if its different from normal
			if (!over.empty() && !CPath::exists(over))
			{
				if (overBitmap != normalBitmap)
				{
					over = localImageName(overBitmap);
					addImageDownload(overBitmap, ctrlButton, style, OverImage);
				}
			}
		}

		ctrlButton->setType (type);
		if (!normal.empty())
			ctrlButton->setTexture (normal);
		if (!pushed.empty())
			ctrlButton->setTexturePushed (pushed);
		if (!over.empty())
			ctrlButton->setTextureOver (over);
		ctrlButton->setModulateGlobalColorAll (style.GlobalColor);
		ctrlButton->setActionOnLeftClick (actionHandler);
		ctrlButton->setParamsOnLeftClick (actionHandlerParams);

		// Translate the tooltip or display raw text (tooltip from webig)
		if (!tooltip.empty())
		{
			if (CI18N::hasTranslation(tooltip))
			{
				ctrlButton->setDefaultContextHelp(CI18N::get(tooltip));
				//ctrlButton->setOnContextHelp(CI18N::get(tooltip).toString());
			}
			else
			{
				ctrlButton->setDefaultContextHelp(tooltip);
				//ctrlButton->setOnContextHelp(string(tooltip));
			}

			ctrlButton->setInstantContextHelp(true);
			ctrlButton->setToolTipParent(TTMouse);
			ctrlButton->setToolTipParentPosRef(Hotspot_TTAuto);
			ctrlButton->setToolTipPosRef(Hotspot_TTAuto);
		}

		getParagraph()->addChild (ctrlButton);
		paragraphChange ();

		setImageSize(ctrlButton, style);

		return ctrlButton;
	}

	// ***************************************************************************

	void CGroupHTML::flushString()
	{
		_CurrentViewLink = NULL;
	}

	// ***************************************************************************

	void CGroupHTML::clearContext()
	{
		_Paragraph = NULL;
		_PRE.clear();
		_Indent.clear();
		_LI = false;
		_UL.clear();
		_DL.clear();
		_A.clear();
		_Link.clear();
		_LinkTitle.clear();
		_Tables.clear();
		_Cells.clear();
		_TR.clear();
		_Forms.clear();
		_FormOpen = false;
		_FormSubmit.clear();
		_Groups.clear();
		_Divs.clear();
		_Anchors.clear();
		_AnchorName.clear();
		_CellParams.clear();
		_Object = false;
		_Localize = false;
		_ReadingHeadTag = false;
		_IgnoreHeadTag = false;
		_IgnoreBaseUrlTag = false;
		_AutoIdSeq = 0;
		m_TableRowBackgroundColor.clear();

		paragraphChange ();

		releaseDataDownloads();
	}

	// ***************************************************************************

	u32char CGroupHTML::getLastChar() const
	{
		if (_CurrentViewLink)
		{
			::u32string str = CUtfStringView(_CurrentViewLink->getText()).toUtf32(); // FIXME: Optimize reverse UTF iteration
			if (!str.empty())
				return str[str.length()-1];
		}
		return 0;
	}

	// ***************************************************************************

	void CGroupHTML::paragraphChange ()
	{
		_CurrentViewLink = NULL;
		_CurrentViewImage = NULL;
		CGroupParagraph *paragraph = getParagraph();
		if (paragraph)
		{
			// Number of child in this paragraph
			uint numChild = paragraph->getNumChildren();
			if (numChild)
			{
				// Get the last child
				CViewBase *child = paragraph->getChild(numChild-1);

				// Is this a string view ?
				_CurrentViewLink = dynamic_cast(child);
				_CurrentViewImage = dynamic_cast(child);
			}
		}
	}

	// ***************************************************************************

	CInterfaceGroup *CGroupHTML::getCurrentGroup()
	{
		if (!_Cells.empty() && _Cells.back())
			return _Cells.back()->Group;
		else
			return _GroupListAdaptor;
	}

	// ***************************************************************************

	void CGroupHTML::addHtmlGroup (CInterfaceGroup *group, uint beginSpace)
	{
		if (!group)
			return;

		registerAnchor(group);

		if (!_DivName.empty())
		{
			group->setName(_DivName);
			_Groups.push_back(group);
		}

		group->setSizeRef(CInterfaceElement::width);

		// Compute begin space between paragraph and tables
		// * If first in group, no begin space

		// Pointer on the current paragraph (can be a table too)
		CGroupParagraph *p = dynamic_cast(group);

		CInterfaceGroup *parentGroup = CGroupHTML::getCurrentGroup();
		const std::vector &groups = parentGroup->getGroups ();
		group->setParent(parentGroup);
		group->setParentSize(parentGroup);
		if (groups.empty())
		{
			group->setParentPos(parentGroup);
			group->setPosRef(Hotspot_TL);
			group->setParentPosRef(Hotspot_TL);
			beginSpace = 0;
		}
		else
		{
			// Last is a paragraph ?
			group->setParentPos(groups.back());
			group->setPosRef(Hotspot_TL);
			group->setParentPosRef(Hotspot_BL);
		}

		// Set the begin space
		if (p)
			p->setTopSpace(beginSpace);
		else
			group->setY(-(sint32)beginSpace);
		parentGroup->addGroup (group);
	}

	// ***************************************************************************

	void CGroupHTML::setContainerTitle (const std::string &title)
	{
		CInterfaceElement *parent = getParent();
		if (parent)
		{
			if ((parent = parent->getParent()))
			{
				CGroupContainer *container = dynamic_cast(parent);
				if (container)
				{
					container->setTitle(title);
				}
			}
		}
	}

	void CGroupHTML::setTitle(const std::string &title)
	{
		if(_TitlePrefix.empty())
			_TitleString = title;
		else
			_TitleString = _TitlePrefix + " - " + title;

		setContainerTitle(_TitleString);
	}

	std::string CGroupHTML::getTitle() const {
		return _TitleString;
	};

	// ***************************************************************************

	bool CGroupHTML::lookupLocalFile (string &result, const char *url, bool isUrl)
	{
		result = url;
		string tmp;

		if (toLowerAscii(result).find("file:") == 0 && result.size() > 5)
		{
			result = result.substr(5, result.size()-5);
		}
		else if (result.find("://") != string::npos || result.find("//") == 0)
		{
			// http://, https://, etc or protocol-less url "//domain.com/image.png"
			return false;
		}

		tmp = CPath::lookup (CFile::getFilename(result), false, false, false);
		if (tmp.empty())
		{
			// try to find in local directory
			tmp = CPath::lookup (result, false, false, true);
		}

		if (!tmp.empty())
		{
			// Normalize the path
			if (isUrl)
				//result = "file:"+toLowerAscii(CPath::standardizePath (CPath::getFullPath (CFile::getPath(result)))+CFile::getFilename(result));*/
				result = "file:/"+tmp;
			else
				result = tmp;
			return true;
		}
		else
		{
			// Is it a texture in the big texture ?
			if (CViewRenderer::getInstance()->getTextureIdFromName (result) >= 0)
			{
				return true;
			}
			else
			{
				// This is not a file in the CPath, let libwww open this URL
				result = url;
				return false;
			}
		}
	}

	// ***************************************************************************

	void CGroupHTML::submitForm(uint button, sint32 x, sint32 y)
	{
		if (button >= _FormSubmit.size())
			return;

		for(uint formId = 0; formId < _Forms.size(); formId++)
		{
			// case sensitive search (user id is lowecase, auto id is uppercase)
			if (_Forms[formId].id == _FormSubmit[button].form)
			{
				_PostNextTime = true;
				_PostFormId = formId;
				_PostFormAction = _FormSubmit[button].formAction;
				_PostFormSubmitType = _FormSubmit[button].type;
				_PostFormSubmitButton = _FormSubmit[button].name;
				_PostFormSubmitValue = _FormSubmit[button].value;
				_PostFormSubmitX = x;
				_PostFormSubmitY = y;

				return;
			}
		}

		nlwarning("Unable to find form '%s' to submit (button '%s')", _FormSubmit[button].form.c_str(), _FormSubmit[button].name.c_str());
	}

	// ***************************************************************************

	void CGroupHTML::setupBackground(CSSBackgroundRenderer *bg)
	{
		if (!bg) return;

		bg->setModulateGlobalColor(_Style.Current.GlobalColor);
		bg->setBackground(_Style.Current.Background);
		bg->setFontSize(_Style.Root.FontSize, _Style.Current.FontSize);

		bg->setViewport(getList()->getParentPos() ? getList()->getParentPos() : this);

		if (!_Style.Current.Background.image.empty())
			addTextureDownload(_Style.Current.Background.image, bg->TextureId, this);
	}

	// ***************************************************************************

	void CGroupHTML::setBackgroundColor (const CRGBA &bgcolor)
	{
		// TODO: DefaultBackgroundBitmapView should be removed from interface xml
		CViewBase *view = getView (DefaultBackgroundBitmapView);
		if (view)
			view->setActive(false);

		m_HtmlBackground.setColor(bgcolor);
	}

	// ***************************************************************************

	void CGroupHTML::setBackground (const string &bgtex, bool scale, bool tile)
	{
		// TODO: DefaultBackgroundBitmapView should be removed from interface xml
		CViewBase *view = getView (DefaultBackgroundBitmapView);
		if (view)
			view->setActive(false);

		m_HtmlBackground.setImage(bgtex);
		m_HtmlBackground.setImageRepeat(tile);
		m_HtmlBackground.setImageCover(scale);

		if (!bgtex.empty())
			addTextureDownload(bgtex, m_HtmlBackground.TextureId, this);
	}


	struct CButtonFreezer : public CInterfaceElementVisitor
	{
		virtual void visitCtrl(CCtrlBase *ctrl)
		{
			CCtrlBaseButton		*textButt = dynamic_cast(ctrl);
			if (textButt)
			{
				textButt->setFrozen(true);
			}
		}
	};

	// ***************************************************************************

	void CGroupHTML::handle ()
	{
		H_AUTO(RZ_Interface_Html_handle)

		const CWidgetManager::SInterfaceTimes × = CWidgetManager::getInstance()->getInterfaceTimes();

		// handle curl downloads
		checkDownloads();

		// handle refresh timer
		if (_NextRefreshTime > 0 && _NextRefreshTime <= (times.thisFrameMs / 1000.0f) )
		{
			// there might be valid uses for 0sec refresh, but two in a row is probably a mistake
			if (_NextRefreshTime - _LastRefreshTime >= 1.0)
			{
				_LastRefreshTime = _NextRefreshTime;
				doBrowse(_RefreshUrl.c_str());
			}
			else
				nlwarning("Ignore second 0sec http-equiv refresh in a row (url '%s')", _URL.c_str());

			_NextRefreshTime = 0;
		}

		if (_CurlWWW)
		{
			// still transfering html page
			if (_TimeoutValue != 0 && _ConnectingTimeout <= ( times.thisFrameMs / 1000.0f ) )
			{
				browseError(("Connection timeout : "+_URL).c_str());
			}
		}
		else
		if (_RenderNextTime)
		{
			_RenderNextTime = false;
			renderHtmlString(_DocumentHtml);
		}
		else
		if (_WaitingForStylesheet)
		{
			renderDocument();
		}
		else
		if (_BrowseNextTime || _PostNextTime)
		{
			// Set timeout
			_ConnectingTimeout = ( times.thisFrameMs / 1000.0f ) + _TimeoutValue;

			// freeze form buttons
			CButtonFreezer freezer;
			this->visit(&freezer);

			// Home ?
			if (_URL == "home")
				_URL = home();

			string finalUrl;
			bool isLocal = lookupLocalFile (finalUrl, _URL.c_str(), true);

			if (!isLocal && _URL.c_str()[0] == '/')
			{
				if (options.webServer.empty())
				{
					// Try again later
					return;
				}
				finalUrl = options.webServer + finalUrl;
			}

			_URL = finalUrl;

			CUrlParser uri (_URL);
			_TrustedDomain = isTrustedDomain(uri.host);
			_DocumentDomain = uri.host;

			// file is probably from bnp (ingame help)
			if (isLocal)
			{
				doBrowseLocalFile(finalUrl);
			}
			else
			{
				SFormFields formfields;
				if (_PostNextTime)
				{
					buildHTTPPostParams(formfields);
					// _URL is set from form.Action
					finalUrl = _URL;
				}
				else
				{
					// Add custom get params from child classes
					addHTTPGetParams (finalUrl, _TrustedDomain);
				}

				doBrowseRemoteUrl(finalUrl, "", _PostNextTime, formfields);
			}

			_BrowseNextTime = false;
			_PostNextTime = false;
		}
	}

	// ***************************************************************************
	void CGroupHTML::buildHTTPPostParams (SFormFields &formfields)
	{
		// Add text area text
		uint i;

		if (_PostFormId >= _Forms.size())
		{
			nlwarning("(%s) invalid form index %d, _Forms %d", _Id.c_str(), _PostFormId, _Forms.size());
			return;
		}
		// Ref the form
		CForm &form = _Forms[_PostFormId];

		// button can override form action url (and methor, but we only do POST)
		_URL = _PostFormAction.empty() ? form.Action : _PostFormAction;

		CUrlParser uri(_URL);
		_TrustedDomain = isTrustedDomain(uri.host);
		_DocumentDomain = uri.host;

		for (i=0; igetGroup ("eb");
				if (group)
				{
					// Should be a CGroupEditBox
					CGroupEditBox *editBox = dynamic_cast(group);
					if (editBox)
					{
						entryData = editBox->getViewText()->getText();
						addEntry = true;
					}
				}
			}
			else if (form.Entries[i].Checkbox)
			{
				// todo handle unicode POST here
				if (form.Entries[i].Checkbox->getPushed ())
				{
					entryData = form.Entries[i].Value;
					addEntry = true;
				}
			}
			else if (form.Entries[i].ComboBox)
			{
				CDBGroupComboBox *cb = form.Entries[i].ComboBox;
				entryData = form.Entries[i].SelectValues[cb->getSelection()];
				addEntry = true;
			}
			else if (form.Entries[i].SelectBox)
			{
				CGroupMenu *sb = form.Entries[i].SelectBox;
				CGroupSubMenu *rootMenu = sb->getRootMenu();
				if (rootMenu)
				{
					for(uint j=0; jgetNumLine(); ++j)
					{
						CInterfaceGroup *ig = rootMenu->getUserGroupLeft(j);
						if (ig)
						{
							CCtrlBaseButton *cb = dynamic_cast(ig->getCtrl("b"));
							if (cb && cb->getPushed())
								formfields.add(form.Entries[i].Name, form.Entries[i].SelectValues[j]);
						}
					}
				}
			}
			// This is a hidden value
			else
			{
				entryData = form.Entries[i].Value;
				addEntry = true;
			}

			// Add this entry
			if (addEntry)
			{
				formfields.add(form.Entries[i].Name, entryData);
			}
		}

		if (_PostFormSubmitType == "image")
		{
			// Add the button coordinates
			if (_PostFormSubmitButton.find_first_of("[") == string::npos)
			{
				formfields.add(_PostFormSubmitButton + "_x", NLMISC::toString(_PostFormSubmitX));
				formfields.add(_PostFormSubmitButton + "_y", NLMISC::toString(_PostFormSubmitY));
			}
			else
			{
				formfields.add(_PostFormSubmitButton, NLMISC::toString(_PostFormSubmitX));
				formfields.add(_PostFormSubmitButton, NLMISC::toString(_PostFormSubmitY));
			}
		}
		else
			formfields.add(_PostFormSubmitButton, _PostFormSubmitValue);

		// Add custom params from child classes
		addHTTPPostParams(formfields, _TrustedDomain);
	}

	// ***************************************************************************
	void CGroupHTML::doBrowseLocalFile(const std::string &uri)
	{
		releaseDownloads();
		updateRefreshButton();

		std::string filename;
		if (toLowerAscii(uri).find("file:/") == 0)
		{
			filename = uri.substr(6, uri.size() - 6);
		}
		else
		{
			filename = uri;
		}

		LOG_DL("browse local file '%s'", filename.c_str());

		_TrustedDomain = true;
		_DocumentDomain = "localhost";

		CIFile in;
		if (in.open(filename))
		{
			std::string html;
			while(!in.eof())
			{
				char buf[1024];
				in.getline(buf, 1024);
				html += std::string(buf) + "\n";
			}
			in.close();

			if (!renderHtmlString(html))
			{
				browseError((string("Failed to parse html from file : ")+filename).c_str());
			}
		}
		else
		{
			browseError((string("The page address is malformed : ")+filename).c_str());
		}
	}

	// ***************************************************************************
	void CGroupHTML::doBrowseRemoteUrl(std::string url, const std::string &referer, bool doPost, const SFormFields &formfields)
	{
		// stop all downloads from previous page
		releaseDownloads();
		updateRefreshButton();

		// Reset the title
		if(_TitlePrefix.empty())
			setTitle (CI18N::get("uiPleaseWait"));
		else
			setTitle (_TitlePrefix + " - " + CI18N::get("uiPleaseWait"));

		url = upgradeInsecureUrl(url);

		LOG_DL("(%s) browse url (trusted=%s) '%s', referer='%s', post='%s', nb form values %d",
				_Id.c_str(), (_TrustedDomain ? "true" :"false"), url.c_str(), referer.c_str(), (doPost ? "true" : "false"), formfields.Values.size());

		if (!MultiCurl)
		{
			browseError(string("Invalid MultCurl handle, loading url failed : "+url).c_str());
			return;
		}

		CURL *curl = curl_easy_init();
		if (!curl)
		{
			nlwarning("(%s) failed to create curl handle", _Id.c_str());
			browseError(string("Failed to create cURL handle : " + url).c_str());
			return;
		}

		// https://
		if (toLowerAscii(url.substr(0, 8)) == "https://")
		{
			// if supported, use custom SSL context function to load certificates
			NLWEB::CCurlCertificates::useCertificates(curl);
		}

		// do not follow redirects, we have own handler
		curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0);
		// after redirect
		curl_easy_setopt(curl, CURLOPT_FRESH_CONNECT, 1);

		// tell curl to use compression if possible (gzip, deflate)
		// leaving this empty allows all encodings that curl supports
		//curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");

		// limit curl to HTTP and HTTPS protocols only
		curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
		curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);

		// Destination
		curl_easy_setopt(curl, CURLOPT_URL, url.c_str());

		// User-Agent:
		std::string userAgent = options.appName + "/" + options.appVersion;
		curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str());

		// Cookies
		sendCookies(curl, _DocumentDomain, _TrustedDomain);

		// Referer
		if (!referer.empty())
		{
			curl_easy_setopt(curl, CURLOPT_REFERER, referer.c_str());
			LOG_DL("(%s) set referer '%s'", _Id.c_str(), referer.c_str());
		}

		if (doPost)
		{
			// serialize form data and add it to curl
			std::string data;
			for(uint i=0; i0)
					data += "&";

				data += std::string(escapedName) + "=" + escapedValue;

				curl_free(escapedName);
				curl_free(escapedValue);
			}
			curl_easy_setopt(curl, CURLOPT_POST, 1);
			curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, data.size());
			curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, data.c_str());
		}
		else
		{
			curl_easy_setopt(curl, CURLOPT_HTTPGET, 1);
		}

		// transfer handle
		_CurlWWW = new CCurlWWWData(curl, url);

		// set the language code used by the client
		std::vector headers;
		headers.push_back("Accept-Language: "+options.languageCode);
		headers.push_back("Accept-Charset: utf-8");
		_CurlWWW->sendHeaders(headers);

		// catch headers for redirect
		curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, NLGUI::curlHeaderCallback);
		curl_easy_setopt(curl, CURLOPT_WRITEHEADER, _CurlWWW);

		// catch body
		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NLGUI::curlDataCallback);
		curl_easy_setopt(curl, CURLOPT_WRITEDATA, _CurlWWW);

	#ifdef LOG_CURL_PROGRESS
		// progress callback
		curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
		curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, NLGUI::curlProgressCallback);
		curl_easy_setopt(curl, CURLOPT_XFERINFODATA, _CurlWWW);
	#else
		// progress off
		curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1);
	#endif

		//
		curl_multi_add_handle(MultiCurl, curl);

		// start the transfer
		int NewRunningCurls = 0;
		curl_multi_perform(MultiCurl, &NewRunningCurls);
		RunningCurls++;

		_RedirectsRemaining = DEFAULT_RYZOM_REDIRECT_LIMIT;
	}

	// ***************************************************************************
	void CGroupHTML::htmlDownloadFinished(bool success, const std::string &error)
	{
		if (!success)
		{
			CUrlParser uri(_CurlWWW->Url);

			// potentially unwanted chars
			std::string url = _CurlWWW->Url;
			url = strFindReplaceAll(url, string("<"), string("%3C"));
			url = strFindReplaceAll(url, string(">"), string("%3E"));
			url = strFindReplaceAll(url, string("\""), string("%22"));
			url = strFindReplaceAll(url, string("'"), string("%27"));

			std::string err;
			err = "cURL error";
			err += "

Connection failed with cURL error

"; err += error; err += "
(" + uri.scheme + "://" + uri.host + ")
reload"; err += ""; browseErrorHtml(err); return; } // received content from remote std::string content = trim(_CurlWWW->Content); // save HSTS header from all requests regardless of HTTP code if (_CurlWWW->hasHSTSHeader()) { CUrlParser uri(_CurlWWW->Url); CStrictTransportSecurity::getInstance()->setFromHeader(uri.host, _CurlWWW->getHSTSHeader()); } receiveCookies(_CurlWWW->Request, _DocumentDomain, _TrustedDomain); long code; curl_easy_getinfo(_CurlWWW->Request, CURLINFO_RESPONSE_CODE, &code); LOG_DL("(%s) web transfer '%p' completed with http code %d, url (len %d) '%s'", _Id.c_str(), _CurlWWW->Request, code, _CurlWWW->Url.size(), _CurlWWW->Url.c_str()); if ((code >= 301 && code <= 303) || code == 307 || code == 308) { if (_RedirectsRemaining < 0) { browseError(string("Redirect limit reached : " + _URL).c_str()); return; } // redirect, get the location and try browse again // we cant use curl redirection because 'addHTTPGetParams()' must be called on new destination std::string location(_CurlWWW->getLocationHeader()); if (location.empty()) { browseError(string("Request was redirected, but location was not set : "+_URL).c_str()); return; } LOG_DL("(%s) request (%d) redirected to (len %d) '%s'", _Id.c_str(), _RedirectsRemaining, location.size(), location.c_str()); location = getAbsoluteUrl(location); _PostNextTime = false; _RedirectsRemaining--; doBrowse(location.c_str()); } else if ( (code < 200 || code >= 300) ) { // catches 304 not modified, but html is not in cache anyway // if server did not send any error back if (content.empty()) { content = string("ERROR

Connection failed

HTTP code '" + toString((sint32)code) + "'

URL '" + _CurlWWW->Url + "'

"); } } char *ch; std::string contentType; CURLcode res = curl_easy_getinfo(_CurlWWW->Request, CURLINFO_CONTENT_TYPE, &ch); if (res == CURLE_OK && ch != NULL) { contentType = ch; } htmlDownloadFinished(content, contentType, code); // clear curl handler if (MultiCurl) { curl_multi_remove_handle(MultiCurl, _CurlWWW->Request); } delete _CurlWWW; _CurlWWW = NULL; // refresh button uses _CurlWWW. refresh button may stay disabled if // there is no css files to download and page is rendered before _CurlWWW is freed updateRefreshButton(); } void CGroupHTML::dataDownloadFinished(bool success, const std::string &error, CDataDownload *data) { fclose(data->fp); CUrlParser uri(data->url); if (!uri.host.empty()) { receiveCookies(data->data->Request, uri.host, isTrustedDomain(uri.host)); } long code = -1; curl_easy_getinfo(data->data->Request, CURLINFO_RESPONSE_CODE, &code); LOG_DL("(%s) transfer '%p' completed with http code %d, url (len %d) '%s'", _Id.c_str(), data->data->Request, code, data->url.size(), data->url.c_str()); curl_multi_remove_handle(MultiCurl, data->data->Request); // save HSTS header from all requests regardless of HTTP code if (success) { if (data->data->hasHSTSHeader()) { CStrictTransportSecurity::getInstance()->setFromHeader(uri.host, data->data->getHSTSHeader()); } // 2XX success, 304 Not Modified if ((code >= 200 && code <= 204) || code == 304) { CHttpCacheObject obj; obj.Expires = data->data->getExpires(); obj.Etag = data->data->getEtag(); obj.LastModified = data->data->getLastModified(); CHttpCache::getInstance()->store(data->dest, obj); if (code == 304 && CFile::fileExists(data->tmpdest)) { CFile::deleteFile(data->tmpdest); } } else if ((code >= 301 && code <= 303) || code == 307 || code == 308) { if (data->redirects < DEFAULT_RYZOM_REDIRECT_LIMIT) { std::string location(data->data->getLocationHeader()); if (!location.empty()) { CUrlParser uri(location); if (!uri.isAbsolute()) { uri.inherit(data->url); location = uri.toString(); } // clear old request state, and curl easy handle delete data->data; data->data = NULL; data->fp = NULL; data->url = location; data->redirects++; // push same request in the front of the queue // cache filename is based of original url Curls.push_front(data); LOG_DL("Redirect '%s'", location.c_str()); // no finished callback called, so cleanup old temp if (CFile::fileExists(data->tmpdest)) { CFile::deleteFile(data->tmpdest); } return; } nlwarning("Redirected to empty url '%s'", data->url.c_str()); } else { nlwarning("Redirect limit reached for '%s'", data->url.c_str()); } } else { nlwarning("HTTP request failed with code [%d] for '%s'\n",code, data->url.c_str()); // 404, 500, etc if (CFile::fileExists(data->dest)) { CFile::deleteFile(data->dest); } } } else { nlwarning("DATA download failed '%s', error '%s'", data->url.c_str(), error.c_str()); } finishCurlDownload(data); } void CGroupHTML::htmlDownloadFinished(const std::string &content, const std::string &type, long code) { LOG_DL("(%s) HTML download finished, content length %d, type '%s', code %d", _Id.c_str(), content.size(), type.c_str(), code); // create markup for image downloads if (type.find("image/") == 0 && !content.empty()) { try { std::string dest = localImageName(_URL); COFile out; out.open(dest); out.serialBuffer((uint8 *)(content.c_str()), content.size()); out.close(); LOG_DL("(%s) image saved to '%s', url '%s'", _Id.c_str(), dest.c_str(), _URL.c_str()); } catch(...) { } // create html code with image url inside and do the request again renderHtmlString(""+_URL+""); } else if (_TrustedDomain && type.find("text/lua") == 0) { setTitle(_TitleString); _LuaScript = "\nlocal __CURRENT_WINDOW__=\""+this->_Id+"\" \n"+content; CLuaManager::getInstance().executeLuaScript(_LuaScript, true); _LuaScript.clear(); // disable refresh button clearRefresh(); // disable redo into this url _AskedUrl.clear(); } else { // Sanitize downloaded HTML UTF-8 encoding, and render renderHtmlString(CUtfStringView(content).toUtf8(true)); } } // *************************************************************************** void CGroupHTML::cssDownloadFinished(const std::string &url, const std::string &local) { for(std::vector::iterator it = _StylesheetQueue.begin(); it != _StylesheetQueue.end(); ++it) { if (it->Url == url) { // read downloaded file into HtmlStyles if (CFile::fileExists(local) && it->Index < _HtmlStyles.size()) { CIFile in; if (in.open(local)) { if (!in.readAll(_HtmlStyles[it->Index])) { nlwarning("Failed to read downloaded css file(%s), url(%s)", local.c_str(), url.c_str()); } } } _StylesheetQueue.erase(it); break; } } } void CGroupHTML::renderDocument() { if (!Curls.empty() && !_StylesheetQueue.empty()) { // waiting for stylesheets to finish downloading return; } _WaitingForStylesheet = false; //TGameTime renderStart = CTime::getLocalTime(); // clear previous state and page beginBuild(); removeContent(); // process all