Example #1
0
 /**
  * Singleton instance getter
  *
  * @return	vB5_User
  */
 public static function instance()
 {
     if (self::$instance === null) {
         $class = __CLASS__;
         self::$instance = new $class();
     }
     return self::$instance;
 }
Example #2
0
 /**
  * Handles an [img] tag.
  *
  * @param	string	The text to search for an image in.
  * @param	string	Whether to parse matching images into pictures or just links.
  *
  * @return	string	HTML representation of the tag.
  */
 function handle_bbcode_img($bbcode, $do_imgcode, $has_img_code = false, $fulltext = '', $forceShowImages = false)
 {
     $sessionurl = self::$sessionUrl;
     $showImages = (self::getUserValue('userid') == 0 or self::getUserValue('showimages') or $forceShowImages);
     /* Do search on $fulltext, which would be the entire article, not just a page of the article which would be in $page */
     if (!$fulltext) {
         $fulltext = $bbcode;
     }
     if ($has_img_code & self::BBCODE_HAS_ATTACH and preg_match_all('#\\[attach(?:=(right|left|config))?\\]([[:alnum:]]+)\\[/attach\\]#i', $fulltext, $matches)) {
         $legacyIds = $attachmentIds = array();
         foreach ($matches[2] as $key => $attachmentid) {
             $align = $matches[1]["{$key}"];
             $search[] = '#\\[attach(' . (!empty($align) ? '=' . $align : '') . ')\\](' . $attachmentid . ')\\[/attach\\]#i';
             // We need to decide whether it's a legacy attachment or a vB5 one
             if (preg_match('#^n(\\d+)$#', $attachmentid, $matches2)) {
                 // if the id has 'n' as prefix, it's a nodeid
                 $attachmentIds[] = intval($matches2[1]);
             } else {
                 // it's a legacy attachmentid
                 $legacyIds[] = intval($attachmentid);
             }
         }
         if (!empty($legacyIds)) {
             $this->oldAttachments = vB_Api::instanceInternal('filedata')->fetchLegacyAttachments($legacyIds);
             $attachmentIds += array_values($this->oldAttachments);
         }
         // we may already have the attachments (see vB5_Template_NodeText)
         if (!empty($this->attachments)) {
             $attachmentIds = array_diff(array_unique($attachmentIds), array_keys($this->attachments));
         }
         if (!empty($attachmentIds)) {
             $attachments = vB_Api::instanceInternal('filedata')->fetchFiledataByid($attachmentIds);
             $this->setAttachments($attachments);
         }
         $bbcode = preg_replace_callback($search, array($this, 'attachReplaceCallback'), $bbcode);
     }
     /*
     			VBV-12051 - Check for legacy [IMG] code using filedataid
     			Since we know that the filedata/fetch?filedataid URL only works for publicview
     			filedata (generally channel icons, forum logo, sigpics, theme icons and the like),
     			let's assume that any [IMG] tag with a filedataid that's used by any *remaining*
     			attachments (after above attachReplaceCallback) is an incorrect inline-inserted
     			attachment image, and replace them with the proper image tags.
     			This can cause some weirdness, like multiple [attach] bbcodes with the same nodeid,
     			but I think this is better than the current broken behavior (where filedataid img
     			will only show for the poster, and the attachment associated with the filedataid
     			is listed in the  bbcode_attachment_list)
     */
     $baseURL = vB5_Template_Options::instance()->get('options.frontendurl');
     /*
     $expectedPrefix = preg_quote($baseURL . '/filedata/fetch?filedataid=', '#');
     $regex = '#\[img\]\s*' . $expectedPrefix . '(?<filedataid>[0-9]+?)(?<extra>[&\#][^*\r\n]*|[a-z0-9/\\._\- !]*)\[/img\]#iU';
     */
     $expectedPrefix = preg_quote($baseURL . '/filedata/fetch?', '#');
     $regex = '#\\[img\\]\\s*' . $expectedPrefix . '(?<querystring>[^\\s]*filedataid=(?<filedataid>[0-9]+?)[^\\s]*)\\s*\\[/img\\]#iU';
     if ($has_img_code & self::BBCODE_HAS_IMG and preg_match_all($regex, $bbcode, $matches)) {
         if (!empty($matches['filedataid'])) {
             $searchForCallback = array();
             $searchForStrReplace = array();
             $replaceForStrReplace = array();
             $this->delayedUnsetAttachments = array();
             $oldUnsetattach = $this->unsetattach;
             $this->unsetattach = false;
             foreach ($matches['filedataid'] as $key => $filedataid) {
                 if (!empty($this->filedataidsToAttachmentids[$filedataid])) {
                     // There might be multiple attachments using the filedataid, but we just have
                     // no way of knowing which this one is supposed to be. Let's just walk through them.
                     $nodeid = current($this->filedataidsToAttachmentids[$filedataid]);
                     if (false === next($this->filedataidsToAttachmentids[$filedataid])) {
                         reset($this->filedataidsToAttachmentids[$filedataid]);
                     }
                     $searchForStrReplace[$key] = $matches[0][$key];
                     // 1 is align/config, 2 is "n{nodeid}"
                     $matchesForCallback = array(1 => '', 2 => "n{$nodeid}");
                     // grab size if provided in query string.
                     $querydata = array();
                     $querystring = vB_String::unHtmlSpecialChars($matches['querystring'][$key]);
                     parse_str($querystring, $querydata);
                     if (!empty($querydata['type'])) {
                         $matchesForCallback['settings'] = array('size' => $querydata['type']);
                     } elseif (!empty($querydata['thumb'])) {
                         $matchesForCallback['settings'] = array('size' => 'thumb');
                     }
                     $replaceForStrReplace[$key] = $this->attachReplaceCallback($matchesForCallback);
                 }
             }
             $this->unsetattach = $oldUnsetattach;
             /*
             					Temporarily turning off unsetattach & then doing the unset at the very end is required to
             					handle the edge case where multiple [img] bbcodes with the SAME filedataid exists.
             					If we don't turn off unsetattach, only the first instance will be replaced with an image tag.
             					If we turn off unsetattach and don't do this delayed unsetting, the attachment that was "used"
             					by the [img] matches will still remain to be listed in the attachments box.
             */
             if (!empty($this->delayedUnsetAttachments) and $this->unsetattach) {
                 foreach ($this->delayedUnsetAttachments as $arr) {
                     $attachmentid = $arr['attachmentid'];
                     $filedataid = $arr['filedataid'];
                     unset($this->attachments["{$attachmentid}"], $this->filedataidsToAttachmentids[$filedataid][$attachmentid]);
                 }
             }
             if (!empty($replaceForStrReplace)) {
                 $bbcode = str_replace($searchForStrReplace, $replaceForStrReplace, $bbcode);
             }
         }
     }
     if ($has_img_code & self::BBCODE_HAS_IMG) {
         if ($do_imgcode and $showImages) {
             // do [img]xxx[/img]
             $bbcode = preg_replace_callback('#\\[img\\]\\s*(https?://([^*\\r\\n]+|[a-z0-9/\\._\\- !]+))\\[/img\\]#iU', array($this, 'handleBbcodeImgMatchCallback'), $bbcode);
         } else {
             $bbcode = preg_replace_callback('#\\[img\\]\\s*(https?://([^*\\r\\n]+|[a-z0-9/\\._\\- !]+))\\[/img\\]#iU', array($this, 'handleBbcodeUrlCallback'), $bbcode);
         }
     }
     if ($has_img_code & self::BBCODE_HAS_SIGPIC) {
         $bbcode = preg_replace_callback('#\\[sigpic\\](.*)\\[/sigpic\\]#siU', array($this, 'handleBBCodeSicpicPregReplace'), $bbcode);
     }
     if ($has_img_code & self::BBCODE_HAS_RELPATH) {
         $bbcode = str_replace('[relpath][/relpath]', vB_String::htmlSpecialCharsUni(vB5_Request::get('vBUrlClean')), $bbcode);
     }
     return $bbcode;
 }
 public function actionLoadNewPosts()
 {
     /*
     			Bug notes
     
     			There seems to be a "race condition" where pageload_servertime is set to something *before* the creation date of the last reply
     			that is actually included in that page load. This can result in a reply that's already on the page to be loaded (duped)
     			when the user makes a reply. (Probably not a "true" race condition. More likely the pageload time is set, then the rest of the
     			page load including the api call to fetch posts on the page takes several seconds, and someone happened to make a post
     			right between those two events. This is more likely for a busy forum, so we should handle this.)
     			Probably will need to add something to the thread display to handle this somehow, or pass in a list of all nodes on the page
     			and exclude them.
     			**UPDATE:**
     			Added data-node-publishdate to the <li > tags for the posts, and added JS to go through them
     			and pass in the max(publishdate[], timestamp). So this *shouldn't* happen anymore.
     */
     $input = array('parentid' => isset($_POST['parentid']) ? intval($_POST['parentid']) : 0, 'channelid' => isset($_POST['channelid']) ? intval($_POST['channelid']) : 0, 'loadnodeid' => isset($_POST['loadnodeid']) ? intval($_POST['loadnodeid']) : 0, 'timestamp' => isset($_POST['timestamp']) ? intval($_POST['timestamp']) : 0, 'pageload_servertime' => isset($_POST['pageload_servertime']) ? intval($_POST['pageload_servertime']) : 0, 'view' => isset($_POST['view']) ? trim($_POST['view']) : 'stream', 'currentpage' => isset($_POST['currentpage']) ? intval($_POST['currentpage']) : 1, 'pagetotal' => isset($_POST['pagetotal']) ? intval($_POST['pagetotal']) : 0, 'postcount' => isset($_POST['postcount']) ? intval($_POST['postcount']) : 0, 'postsperpage' => isset($_POST['postsperpage']) ? intval($_POST['postsperpage']) : 0, 'commentsperpage' => isset($_POST['commentsperpage']) ? intval($_POST['commentsperpage']) : 0, 'past_page_limit_aware' => isset($_POST['past_page_limit_aware']) ? filter_var($_POST['past_page_limit_aware'], FILTER_VALIDATE_BOOLEAN) : false);
     $api = Api_InterfaceAbstract::instance();
     /*
     			BEGIN >>> Redirect to new page <<<
     			If we're trying to load a nodeid, and currentpage is < pagetotal, this indicates a scenario where
     			a reply was posted on a page that's not the last page. vB4 behavior for this was to redirect browser
     			to the page that the reply is on, so we should do the same.
     */
     if (!empty($input['loadnodeid'])) {
         $usersNewReply = $api->callApi('node', 'getFullContentforNodes', array($input['loadnodeid']));
         $usersNewReply = empty($usersNewReply) ? null : reset($usersNewReply);
     } else {
         $usersNewReply = null;
     }
     if (!empty($usersNewReply) and $input['currentpage'] < $input['pagetotal']) {
         // redirect to loadnode
         $url = $api->callApi('route', 'getUrl', array('route' => $usersNewReply['routeid'], 'data' => $usersNewReply, 'extra' => array('p' => $usersNewReply['nodeid'])));
         if (is_string($url)) {
             $url = vB5_Template_Options::instance()->get('options.frontendurl') . $url;
             // TODO, return a template saying "redirecting... or something. The wait before reload is noticeable."
             return $this->sendAsJson(array('redirect' => $url));
         } else {
             // UNTESTED.
             // todo, send user to same topic, but with ?goto=newpost
             $url = $api->callApi('route', 'getUrl', array('route' => $usersNewReply['routeid'], 'data' => array('nodeid' => $usersNewReply['starter']), 'extra' => array('goto' => 'newpost')));
             $url = vB5_Template_Options::instance()->get('options.frontendurl') . $url;
             return $this->sendAsJson(array('redirect' => $url));
         }
     }
     // END >>> Redirect to new page <<<
     /*
     			BEGIN >>> Fetch new replies under topic <<<
     */
     // based on widget_conversationdisplay search options
     $search_json = array('date' => array('from' => $input['timestamp']), 'channel' => $input['parentid']);
     if ($input['view'] == 'stream') {
         // UNTESTED &  UNSUPPORTED
         // based on vB5_Frontend_Controller_Activity::actionGet()
         $search_json['depth'] = 2;
         $search_json['view'] = 'conversation_stream';
         $search_json['sort']['created'] = 'DESC';
     } else {
         $input['view'] = 'thread';
         $search_json['view'] = 'thread';
         // thread
         $search_json['depth'] = 1;
         $search_json['view'] = 'conversation_thread';
         $search_json['sort']['created'] = 'ASC';
         $search_json['nolimit'] = 1;
         // TODO: remove this?
     }
     $search_json['ignore_protected'] = 1;
     $numAllowed = max($input['postsperpage'] - $input['postcount'], 0);
     if (!empty($usersNewReply)) {
         // Grab 2 extra *just* in case the one immediately after $numAllowed is the new reply
         $perpage = $numAllowed + 2;
     } else {
         $perpage = $numAllowed + 1;
     }
     $functionParams = array($search_json, $perpage, 1);
     $searchResult = Api_InterfaceAbstract::instance()->callApi('search', 'getInitialResults', $functionParams);
     $newReplies = $searchResult['results'];
     // END >>> Fetch new replies under topic <<<
     /*
     			BEGIN >>> Get next page URL <<<
     */
     $routeid = false;
     $firstnode = reset($newReplies);
     if (isset($firstnode['routeid'])) {
         $routeid = $firstnode['routeid'];
     } else {
         // UNTESTED
         $parentnode = $api->callApi('node', 'getNodeFullContent', array('nodeid' => $input['parentid'], 'contenttypeid' => false, 'options' => array('showVM' => 1, 'withParent' => 1)));
         $parentnode = $parentnode[$input['parentid']];
         $routeid = $parentnode['routeid'];
     }
     $nextPageUrl = $api->callApi('route', 'getUrl', array('route' => $routeid, 'data' => array('nodeid' => $input['parentid'], 'pagenum' => $input['currentpage'] + 1), 'extra' => array()));
     $nextPageUrl = vB5_Template_Options::instance()->get('options.frontendurl') . $nextPageUrl;
     // END >>> Get next page URL <<<
     /*
     			BEGIN >>> GENERATE TEMPLATE <<<
     */
     $channelBbcodes = $api->callApi('content_channel', 'getBbcodeOptions', array($input['channelid']));
     // Used for display_contenttype_threadview_header template, post index (ex. #123 link)
     $pagingInfo = array('currentpage' => $input['currentpage'], 'perpage' => $input['postsperpage']);
     // the template automatically calculates what the postIndex should be given the $postIndex *offset* (# of posts already on the page)
     $postIndex = $input['postcount'];
     /*
     			This is handy for debugging. Can remove once this code is stabilized.
     */
     $templateInfo = array();
     $newRepliesSinceTime = false;
     // "New replies since ##:##"
     $moreUnreadReplies = false;
     // "There are more unread replies after the current page. Please click here to..."
     if (!empty($usersNewReply)) {
         // Only show the "new replies since {time}" phrase if
         // other replies can be shown on the page AND there are other posts than the user's new reply.
         if ($numAllowed > 0 and $searchResult['totalRecords'] > 1) {
             $newRepliesSinceTime = true;
         }
     } else {
         $newRepliesSinceTime = true;
     }
     if ($newRepliesSinceTime) {
         $templateInfo['new_replies_since_x'] = true;
         $topHTML = $this->renderPostNoticeTemplate('new_replies_since_x', array('timestamp' => $input['timestamp']));
     } else {
         $topHTML = '';
     }
     /*
      * Go through the replies, render the display template, check if user's new reply is in here.
      */
     $bottomHTML = '';
     $counter = 1;
     $prependFetchTime = false;
     $past_page_limit = false;
     foreach ($newReplies as $node) {
         if ($counter <= $numAllowed) {
             $templateInfo['reply'][$node['nodeid']] = true;
             $extra = array('pagingInfo' => $pagingInfo, 'postIndex' => $postIndex++);
             $topHTML .= $this->renderSinglePostTemplate($node, $input['view'], $channelBbcodes, $extra) . "\n";
             if ($input['loadnodeid'] and $node['nodeid'] == $input['loadnodeid']) {
                 // We don't want to accidentally duplicate the user's reply if it's included here.
                 unset($usersNewReply);
             } else {
                 // Only prepend the "New post(s) since {time}" if there are posts other than the user's post that triggered
                 // this.
                 $prependFetchTime = true;
             }
             $counter++;
             // We only care about this while we're still within limit.
         } else {
             // Let's not show a warning more than once.
             $past_page_limit = true;
             // TODO: Add something for stream view (reverse order)?
             if (empty($input['past_page_limit_aware']) and $input['view'] == 'thread') {
                 $templateInfo['replies_below_on_next_page'] = true;
                 // Put up a warning saying below do not fit on the current page
                 $bottomHTML = $this->renderPostNoticeTemplate('replies_below_on_next_page', array('nextpageurl' => $nextPageUrl));
             }
             if (!empty($usersNewReply)) {
                 // If we've yet to render the user's new reply, there's a possibility that this node is
                 // the user's. Only show the "there are more unread replies" message when there are new
                 // posts OTHER than the user's new reply since the last time they checked ($input['timestamp'])
                 if ($usersNewReply['nodeid'] != $node['nodeid']) {
                     $moreUnreadReplies = true;
                 }
             } else {
                 // If we're not also fetching the user's reply, or we already rendered it within $numAllowed (above),
                 // this reply will always be on the 'second page'.
                 $moreUnreadReplies = true;
             }
         }
     }
     if (!empty($usersNewReply)) {
         $templateInfo['user_own_reply'][$usersNewReply['nodeid']] = true;
         $extra = array('pagingInfo' => $pagingInfo, 'postIndex' => $postIndex++);
         $bottomHTML .= $this->renderSinglePostTemplate($usersNewReply, $input['view'], $channelBbcodes, $extra) . "\n";
     }
     if ($moreUnreadReplies) {
         $templateInfo['more_replies_after_current_page'] = true;
         $bottomHTML .= $this->renderPostNoticeTemplate('more_replies_after_current_page', array('nextpageurl' => $nextPageUrl));
     }
     // END >>> GENERATE TEMPLATE <<<
     /*
     			BEGIN	>>> Return results array <<<
     */
     $results = array();
     $results['success'] = true;
     $results['past_page_limit'] = $past_page_limit;
     $results['timenow'] = vB5_Request::get('timeNow');
     $results['template'] = $topHTML . "\n" . $bottomHTML;
     $results['css_links'] = vB5_Template_Stylesheet::instance()->getAjaxCssLinks();
     $this->sendAsJsonAndCloseConnection($results);
     // END	>>> Return results array <<<
     /*
     			The reason I decided not to just do markread via AJAX + apidetach is that the timenow would be different, since
     			the current session's time and the that apidetach/node/markread call would have a bit of lag. So it's more
     			correct to do it here, and saves a request to do so.
     			We should decouple the "close request" logic from applicationlight's handleAjaxApiDetached() into a separate
     			function, and call it from here.
     */
     // The library markRead() function handles the case when user is a guest. JS needs to handle the case when
     // it's cookie based threadmarking.
     $api->callApi('node', 'markRead', array($input['parentid']));
     return;
 }
Example #4
0
 public function actionLoginForm(array $errors = array(), array $formData = array())
 {
     $disableLoginForm = false;
     //@TODO: Validate URL to check against whitelisted URLs
     // VBV-8394 Remove URLPATH querystring from Login form URL
     // use referer URL instead of querystring
     //  however, if the query string is provided, use that instead to handle older URLs
     if (empty($_REQUEST['url'])) {
         // use referrer
         $url = filter_var(isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : vB5_Template_Options::instance()->get('options.frontendurl'), FILTER_SANITIZE_STRING);
     } else {
         // it's an old url. Use the query string
         $url = filter_var(isset($_REQUEST['url']) ? $_REQUEST['url'] : vB5_Template_Options::instance()->get('options.frontendurl'), FILTER_SANITIZE_STRING);
     }
     // if it's encoded, we need to decode it to check if it's gonna try to redirect to the login or registration form.
     $url_decoded = base64_decode($url, true);
     $url_decoded = $url_decoded ? $url_decoded : $url;
     if (!empty($url_decoded) and (strpos($url_decoded, '/auth/') !== false or strpos($url_decoded, '/register') !== false)) {
         $url = '';
     }
     // Try to resolve some XSS attack. See VBV-1124
     // Make sure the URL hasn't been base64 encoded already
     if (!base64_decode($url, true)) {
         $url = base64_encode($url);
     }
     // VBV-7835 Stop search engine index this page
     header("X-Robots-Tag: noindex, nofollow");
     // START: Enforce using https for login if frontendurl_login is set to https (VBV-8474)
     // get the current URL and the base login URL for comparison
     $requestBaseUrl = vB5_Request::instance()->get('vBUrlWebroot');
     $loginBaseUrl = vB5_Template_Options::instance()->get('options.frontendurl_login');
     $matchA = preg_match('#^(https?)://#', $requestBaseUrl, $matchResultA);
     $matchB = preg_match('#^(https?)://#', $loginBaseUrl, $matchResultB);
     // if the URL scheme (http or https) doesn't match, redirect to the right one
     if (!($matchA and $matchB and $matchResultA[1] === $matchResultB[1])) {
         // avoid infinite redirects
         if (isset($_REQUEST['vb_login_redirected']) and $_REQUEST['vb_login_redirected'] == 1) {
             // Something exteral to vB is redirecting back from https to http.
             // Since we can't allow logging in over http if configured for https,
             // we can't show the login form here
             if (!isset($errors['errors'])) {
                 $errors['errors'] = array();
             }
             $errors['errors'][] = 'unable_to_redirect_to_the_correct_login_url';
             $disableLoginForm = true;
         } else {
             header('Location: ' . $loginBaseUrl . '/auth/login-form?vb_login_redirected=1&url=' . urlencode($url));
             exit;
         }
     }
     // END: Enforce using https for login if frontendurl_login is set to https
     $user = vB5_User::instance();
     $templater = new vB5_Template('login_form');
     $templater->register('charset', $user['lang_charset']);
     $templater->register('errors', $errors);
     $templater->register('formData', $formData);
     $templater->register('url', $url);
     $templater->register('urlpath', $url);
     $templater->register('disableLoginForm', $disableLoginForm);
     $this->outputPage($templater->render());
 }
Example #5
0
 /**
  * Handles an [img] tag.
  *
  * @param	string	The text to search for an image in.
  * @param	string	Whether to parse matching images into pictures or just links.
  *
  * @return	string	HTML representation of the tag.
  */
 function handle_bbcode_img($bbcode, $do_imgcode, $has_img_code = false, $fulltext = '', $forceShowImages = false)
 {
     $sessionurl = self::$sessionUrl;
     $showImages = (vB5_User::get('userid') == 0 or vB5_User::get('showimages') or $forceShowImages);
     /* Do search on $fulltext, which would be the entire article, not just a page of the article which would be in $page */
     if (!$fulltext) {
         $fulltext = $bbcode;
     }
     // Skip checking if inside a [QUOTE] or [NOPARSE] tag.
     $find = array('#\\[QUOTE[^\\]]*\\].*\\[/QUOTE\\]#siU', '#\\[NOPARSE\\].*\\[/NOPARSE\\]#siU');
     $replace = '';
     $fulltext_omit_noparse = preg_replace($find, $replace, $fulltext);
     // Prevent inline attachments from showing up in the attachments list
     $this->skipAttachmentList = array();
     // keep the regex roughly in sync with the one in text library's fixAttachBBCode()
     $attachRegex = '#\\[attach(?:=(?<align>[a-z]+))?\\](?<type>n|temp_)?(?<id>[0-9]+)(?<tempsuffix>[0-9_]+)?\\[/attach\\]#i';
     if ($has_img_code & self::BBCODE_HAS_ATTACH and preg_match_all($attachRegex, $fulltext_omit_noparse, $matches)) {
         if (!empty($matches['id'])) {
             $legacyIds = array();
             $searchForStrReplace = array();
             $replaceForStrReplace = array();
             $matchesForCallback = array();
             foreach ($matches['id'] as $key => $id) {
                 // full original, replace this with $replaceForStrReplace
                 $searchForStrReplace[$key] = $matches[0][$key];
                 // 0 is the full match, 1 is align/config, 2 is "n{nodeid}"
                 $matchesForCallback[$key] = array(0 => $matches[0][$key], 1 => !empty($matches['align'][$key]) ? $matches['align'][$key] : '', 2 => $matches['type'][$key] . $id . $matches['tempsuffix'][$key]);
                 switch ($matches['type'][$key]) {
                     case 'n':
                         $nodeid = 0;
                         // either a nodeid or a bugged filedataid.
                         if (!empty($this->attachments[$id])) {
                             // This is a normal one with an attachmentid
                             $nodeid = $id;
                             $filedataid = $this->attachments[$id]['filedataid'];
                         } else {
                             if (!empty($this->filedataidsToAttachmentids[$id])) {
                                 /*
                                  * VBV-10556  -  In addition to [IMG] bbcodes using filedataid URLs, there might be some old [ATTACH] bbcodes with
                                  * n{filedataid} instead of n{attachid}.
                                  * $id is a probably a filedataid, or just bogus. Since we can't tell the difference at this point, assume it's bugged not bogus.
                                  * There might be multiple attachments using the filedataid, but we just have
                                  * no way of knowing which this one is supposed to be. Let's just walk through them.
                                  */
                                 $filedataid = $id;
                                 $nodeid = current($this->filedataidsToAttachmentids[$id]);
                                 if (false === next($this->filedataidsToAttachmentids[$id])) {
                                     reset($this->filedataidsToAttachmentids[$id]);
                                 }
                             }
                         }
                         if (!empty($nodeid)) {
                             $matchesForCallback[$key][2] = "n{$nodeid}";
                             // Flag attachment for removal from array
                             if ($this->unsetattach) {
                                 $this->skipAttachmentList[$nodeid] = array('attachmentid' => $nodeid, 'filedataid' => $filedataid);
                             }
                         }
                         break;
                         // end case 'n'
                     // end case 'n'
                     case 'temp_':
                         // temporary id.
                         break;
                         // end case 'temp_'
                     // end case 'temp_'
                     default:
                         // most likely a legacy id.
                         $legacyIds[$key] = intval($id);
                         // The legacy attachments are set later, meaning it will fail the "filedataid" interpolation fixes
                         // but AFAIK that's okay, because those are vB5 bugs and shouldn't be using legacy ids.
                         break;
                         // end default case
                 }
             }
             // grab and set any legacy attachments.
             if (!empty($legacyIds)) {
                 $this->oldAttachments = Api_InterfaceAbstract::instance()->callApi('filedata', 'fetchLegacyAttachments', array($legacyIds));
                 if (!empty($this->oldAttachments)) {
                     $this->setAttachments($this->oldAttachments, true);
                 }
             }
             // Use attachReplaceCallback() to construct the replacements.
             foreach ($matchesForCallback as $key => $matchesArr) {
                 $replaceForStrReplace[$key] = $this->attachReplaceCallback($matchesArr);
                 if ($searchForStrReplace[$key] == $replaceForStrReplace[$key]) {
                     // No idea if PHP already optimizes this, but don't bother str_replace with the same value.
                     unset($searchForStrReplace[$key], $replaceForStrReplace[$key]);
                 }
             }
             if (!empty($replaceForStrReplace)) {
                 $bbcode = str_replace($searchForStrReplace, $replaceForStrReplace, $bbcode);
             }
         }
     }
     /*
     			VBV-12051 - Check for legacy [IMG] code using filedataid
     			Since we know that the filedata/fetch?filedataid URL only works for publicview
     			filedata (generally channel icons, forum logo, sigpics, theme icons and the like),
     			let's assume that any [IMG] tag with a filedataid that's used by any *remaining*
     			attachments (after above attachReplaceCallback) is an incorrect inline-inserted
     			attachment image, and replace them with the proper image tags.
     			This can cause some weirdness, like multiple [attach] bbcodes with the same nodeid,
     			but I think this is better than the current broken behavior (where filedataid img
     			will only show for the poster, and the attachment associated with the filedataid
     			is listed in the  bbcode_attachment_list)
     */
     $baseURL = vB5_Template_Options::instance()->get('options.frontendurl');
     /*
     $expectedPrefix = preg_quote($baseURL . '/filedata/fetch?filedataid=', '#');
     $regex = '#\[img\]\s*' . $expectedPrefix . '(?<filedataid>[0-9]+?)(?<extra>[&\#][^*\r\n]*|[a-z0-9/\\._\- !]*)\[/img\]#iU';
     */
     $expectedPrefix = preg_quote($baseURL . '/filedata/fetch?', '#');
     $regex = '#\\[img\\]\\s*' . $expectedPrefix . '(?<querystring>[^\\s]*filedataid=(?<filedataid>[0-9]+?)[^\\s]*)\\s*\\[/img\\]#iU';
     if ($has_img_code & self::BBCODE_HAS_IMG and preg_match_all($regex, $fulltext_omit_noparse, $matches)) {
         if (!empty($matches['filedataid'])) {
             $searchForStrReplace = array();
             $replaceForStrReplace = array();
             $matchesForCallback = array();
             foreach ($matches['filedataid'] as $key => $filedataid) {
                 if (!empty($this->filedataidsToAttachmentids[$filedataid])) {
                     // There might be multiple attachments using the filedataid, but we just have
                     // no way of knowing which this one is supposed to be. Let's just walk through them.
                     $nodeid = current($this->filedataidsToAttachmentids[$filedataid]);
                     if (false === next($this->filedataidsToAttachmentids[$filedataid])) {
                         reset($this->filedataidsToAttachmentids[$filedataid]);
                     }
                     $searchForStrReplace[$key] = $matches[0][$key];
                     // 0 is the full match, 1 is align/config, 2 is "n{nodeid}"
                     $matchesForCallback = array(0 => $matches[0][$key], 1 => '', 2 => "n{$nodeid}");
                     // grab size if provided in query string.
                     $querydata = array();
                     $querystring = vB_String::unHtmlSpecialChars($matches['querystring'][$key]);
                     parse_str($querystring, $querydata);
                     if (!empty($querydata['type'])) {
                         $matchesForCallback['settings'] = array('size' => $querydata['type']);
                     } elseif (!empty($querydata['thumb'])) {
                         $matchesForCallback['settings'] = array('size' => 'thumb');
                     }
                     $replaceForStrReplace[$key] = $this->attachReplaceCallback($matchesForCallback);
                     if ($searchForStrReplace[$key] == $replaceForStrReplace[$key]) {
                         // No idea if PHP already optimizes this, but don't bother str_replace with the same value.
                         unset($searchForStrReplace[$key], $replaceForStrReplace[$key]);
                     }
                     if ($this->unsetattach) {
                         $this->skipAttachmentList[$nodeid] = array('attachmentid' => $nodeid, 'filedataid' => $filedataid);
                     }
                 }
             }
             if (!empty($replaceForStrReplace)) {
                 $bbcode = str_replace($searchForStrReplace, $replaceForStrReplace, $bbcode);
             }
         }
     }
     // Now handle everything that was left behind. These are ones we just don't know how to "fix" because we
     // couldn't find an existing, remaining attachment that we can interpolate to.
     if ($has_img_code & self::BBCODE_HAS_ATTACH and !empty($search)) {
         $bbcode = preg_replace_callback($search, array($this, 'attachReplaceCallbackFinal'), $bbcode);
     }
     if ($has_img_code & self::BBCODE_HAS_IMG) {
         if ($do_imgcode and $showImages) {
             // do [img]xxx[/img]
             $bbcode = preg_replace_callback('#\\[img\\]\\s*(https?://([^*\\r\\n]+|[a-z0-9/\\._\\- !]+))\\[/img\\]#iU', array($this, 'handleBbcodeImgMatchCallback'), $bbcode);
         } else {
             $bbcode = preg_replace_callback('#\\[img\\]\\s*(https?://([^*\\r\\n]+|[a-z0-9/\\._\\- !]+))\\[/img\\]#iU', array($this, 'handleBbcodeUrlCallback'), $bbcode);
         }
     }
     if ($has_img_code & self::BBCODE_HAS_SIGPIC) {
         $bbcode = preg_replace_callback('#\\[sigpic\\](.*)\\[/sigpic\\]#siU', array($this, 'handleBBCodeSigPicCallback'), $bbcode);
     }
     if ($has_img_code & self::BBCODE_HAS_RELPATH) {
         $bbcode = str_replace('[relpath][/relpath]', vB5_String::htmlSpecialCharsUni(vB5_Request::get('vBUrlClean')), $bbcode);
     }
     return $bbcode;
 }
Example #6
0
 public function index($pageid)
 {
     //the api init can redirect.  We need to make sure that happens before we echo anything
     $api = Api_InterfaceAbstract::instance();
     $top = '';
     // We should not cache register page for guest. See VBV-7695.
     if (vB5_Request::get('cachePageForGuestTime') > 0 and !vB5_User::get('userid') and (empty($_REQUEST['routestring']) or $_REQUEST['routestring'] != 'register' and $_REQUEST['routestring'] != 'lostpw')) {
         // languageid should be in the pagekey to fix VBV-8095
         $fullPageKey = 'vBPage_' . md5(serialize($_REQUEST)) . '_' . vB::getCurrentSession()->get('languageid');
         $styleid = vB5_Cookie::get('userstyleid', vB5_Cookie::TYPE_UINT);
         if (!empty($styleid)) {
             $fullPageKey .= '_' . $styleid;
         }
         $fullPage = vB_Cache::instance(vB_Cache::CACHE_LARGE)->read($fullPageKey);
         if (!empty($fullPage)) {
             echo $fullPage;
             exit;
         }
     }
     $preheader = vB5_ApplicationAbstract::getPreheader();
     $top .= $preheader;
     if (vB5_Request::get('useEarlyFlush')) {
         echo $preheader;
         flush();
     }
     $router = vB5_ApplicationAbstract::instance()->getRouter();
     $arguments = $router->getArguments();
     $userAction = $router->getUserAction();
     $pageKey = $router->getPageKey();
     $api->callApi('page', 'preload', array($pageKey));
     if (!empty($userAction)) {
         $api->callApi('wol', 'register', array($userAction['action'], $userAction['params'], $pageKey, vB::getRequest()->getScriptPath(), !empty($arguments['nodeid']) ? $arguments['nodeid'] : 0));
     }
     if (isset($arguments['pagenum'])) {
         $arguments['pagenum'] = intval($arguments['pagenum']) > 0 ? intval($arguments['pagenum']) : 1;
     }
     $pageid = (int) (isset($arguments['pageid']) ? $arguments['pageid'] : (isset($arguments['contentid']) ? $arguments['contentid'] : 0));
     if ($pageid < 1) {
         // @todo This needs to output a user-friendly "page not found" page
         throw new Exception('Could not find page.');
     }
     $page = $api->callApi('page', 'fetchPageById', array($pageid, $arguments));
     if (!$page) {
         // @todo This needs to output a user-friendly "page not found" page
         throw new Exception('Could not find page.');
     }
     // Go to the first new / unread post for this user in this topic
     if (!empty($_REQUEST['goto']) and $_REQUEST['goto'] == 'newpost' and !empty($arguments['nodeid']) and !empty($arguments['channelid'])) {
         if ($this->vboptions['threadmarking'] and vB5_User::get('userid')) {
             // Database read marking
             $channelRead = $api->callApi('node', 'getNodeReadTime', array($arguments['channelid']));
             $topicRead = $api->callApi('node', 'getNodeReadTime', array($arguments['nodeid']));
             $topicView = max($topicRead, $channelRead, time() - $this->vboptions['markinglimit'] * 86400);
         } else {
             // Cookie read marking
             $topicView = intval(vB5_Cookie::fetchBbarrayCookie('discussion_view', $arguments['nodeid']));
             if (!$topicView) {
                 $topicView = vB5_User::get('lastvisit');
             }
         }
         $topicView = intval($topicView);
         // Get the first unread reply
         $goToNodeId = $api->callApi('node', 'getFirstChildAfterTime', array($arguments['nodeid'], $topicView));
         if (empty($goToNodeId)) {
             $thread = $api->callApi('node', 'getNodes', array(array($arguments['nodeid'])));
             if (!empty($thread) and isset($thread[$arguments['nodeid']])) {
                 $goToNodeId = $thread[$arguments['nodeid']]['lastcontentid'];
             }
         }
         if ($goToNodeId) {
             // Redirect to the new post
             $urlCache = vB5_Template_Url::instance();
             $urlKey = $urlCache->register($router->getRouteId(), array('nodeid' => $arguments['nodeid']), array('p' => $goToNodeId));
             $replacements = $urlCache->finalBuildUrls(array($urlKey));
             $url = $replacements[$urlKey];
             if ($url) {
                 $url .= '#post' . $goToNodeId;
                 if (headers_sent()) {
                     echo '<script type="text/javascript">window.location = "' . $url . '";</script>';
                 } else {
                     header('Location: ' . $url);
                 }
                 exit;
             }
         }
     }
     $page['routeInfo'] = array('routeId' => $router->getRouteId(), 'arguments' => $arguments, 'queryParameters' => $router->getQueryParameters());
     $page['crumbs'] = $router->getBreadcrumbs();
     $page['headlinks'] = $router->getHeadLinks();
     $page['pageKey'] = $pageKey;
     // default value for pageSchema
     $page['pageSchema'] = 'http://schema.org/WebPage';
     $queryParameters = $router->getQueryParameters();
     /*
      *	VBV-12506
      *	this is where we would add other things to clean up dangerous query params.
      *	For VBV-12486, I'll just unset anything here that can't use vb:var in the templates,
      *	but really we should just make a whitelist of expected page object parameters that
      *	come from the query string and unset EVERYTHING else. For the expected ones, we
      *	should also force the value into the expected (and hopefully safer) range
      */
     /*
      *	VBV-12506
      *	$doNotReplaceWithQueryParams is a list of parameters that the page object usually
      *	gets naturally/internally, and we NEVER want to replace with a user provided query
      *	parameter. (In fact, *when* exactly DO we want to do this???)
      *	If we don't do this, it's a potential XSS vulnerability for the items that we
      *	cannot send through vb:var for whatever reason (title for ex)
      * 	and even if they *are* sent through vb:var, the replacements can sometimes just
      *	break the page even when it's sent through vb:var (for example, ?pagetemplateid=%0D,
      *	the new line this inserts in var pageData = {...} in the header template tends to
      *	break things (tested on Chrome).
      *	Furthermore, any script that uses the pageData var would get the user injected data
      *	that might cause more problems down the line.
      *	Parameter Notes:
      *		'titleprefix'
      *			As these two should already be html escaped, we don't want to double escape
      *			them. So we can't us vb:var in the templates. As such, we must prevent a
      *			malicious querystring from being injected into the page object here.
      *		'title'
      *			Similar to above, but channels are allowed to have HTML in the title, so
      *			they are intentinoally not escaped in the DB, and the templates can't use
      *			vb:var.
      *		'pageid', 'channelid', 'nodeid'
      *			These are usually set in the arguments, so the array_merge below usually
      *			takes care of not passing a pageid query string through to the page object,
      *			but I'm leaving them in just in case.
      */
     $doNotReplaceWithQueryParams = array('titleprefix', 'title', 'pageid', 'channelid', 'nodeid', 'pagetemplateid', 'url', 'pagenum', 'tagCloudTitle');
     foreach ($doNotReplaceWithQueryParams as $key) {
         unset($queryParameters[$key]);
     }
     $arguments = array_merge($queryParameters, $arguments);
     foreach ($arguments as $key => $value) {
         $page[$key] = $value;
     }
     $options = vB5_Template_Options::instance();
     $page['phrasedate'] = $options->get('miscoptions.phrasedate');
     $page['optionsdate'] = $options->get('miscoptions.optionsdate');
     // if no meta description, use node data or global one instead, prefer node data
     if (empty($page['metadescription']) and !empty($page['nodedescription'])) {
         $page['metadescription'] = $page['nodedescription'];
     }
     if (empty($page['metadescription'])) {
         $page['metadescription'] = $options->get('options.description');
     }
     $config = vB5_Config::instance();
     // Non-persistent notices @todo - change this to use vB_Cookie
     $page['ignore_np_notices'] = vB5_ApplicationAbstract::getIgnoreNPNotices();
     $templateCache = vB5_Template_Cache::instance();
     $templater = new vB5_Template($page['screenlayouttemplate']);
     //IMPORTANT: If you add any variable to the page object here,
     // please make sure you add them to other controllers which create page objects.
     // That includes at a minimum the search controller (in two places currently)
     // and vB5_ApplicationAbstract::showErrorPage
     $templater->registerGlobal('page', $page);
     $page = $this->outputPage($templater->render(), false);
     $fullPage = $top . $page;
     if (!empty($fullPageKey) and is_string($fullPageKey)) {
         vB_Cache::instance(vB_Cache::CACHE_LARGE)->write($fullPageKey, $fullPage, vB5_Request::get('cachePageForGuestTime'), 'vbCachedFullPage');
     }
     // these are the templates rendered for this page
     $loadedTemplates = vB5_Template::getRenderedTemplates();
     $api->callApi('page', 'savePreCacheInfo', array($pageKey));
     if (!vB5_Request::get('useEarlyFlush')) {
         echo $fullPage;
     } else {
         echo $page;
     }
 }
Example #7
0
 public static function doRedirect($url, $bypasswhitelist = false)
 {
     if (!$bypasswhitelist and !self::allowRedirectToUrl($url)) {
         throw new vB5_Exception('invalid_redirect_url');
     }
     if (vB5_Request::get('useEarlyFlush')) {
         echo '<script type="text/javascript">window.location = "' . $url . '";</script>';
     } else {
         header('Location: ' . $url);
     }
     die;
 }
Example #8
0
 function actionResult()
 {
     //the api init can redirect.  We need to make sure that happens before we echo anything
     $api = Api_InterfaceAbstract::instance();
     $top = '';
     if (vB5_Request::get('cachePageForGuestTime') > 0 and !vB5_User::get('userid')) {
         $fullPageKey = md5(serialize($_REQUEST));
         $fullPage = vB_Cache::instance()->read($fullPageKey);
         if (!empty($fullPage)) {
             echo $fullPage;
             exit;
         }
     }
     $preheader = vB5_ApplicationAbstract::getPreheader();
     $top .= $preheader;
     if (vB5_Request::get('useEarlyFlush')) {
         echo $preheader;
         flush();
     }
     $serverData = array_merge($_GET, $_POST);
     $router = vB5_ApplicationAbstract::instance()->getRouter();
     $arguments = $router->getArguments();
     $userAction = $router->getUserAction();
     if (!empty($userAction)) {
         $api->callApi('wol', 'register', array($userAction['action'], $userAction['params']));
     }
     // if Human verification is required, and we don't have 'q' set in serverData (means the user is using
     // the quick search box), we redirect user to advanced search page with HV
     $requirehv = $api->callApi('hv', 'fetchRequireHvcheck', array('search'));
     if (!empty($serverData['AdvSearch']) or $requirehv and isset($serverData['q'])) {
         $adv_search = $api->callApi('route', 'getRoute', array('pathInfo' => 'advanced_search', 'queryString' => ''), true);
         $arguments = $adv_search['arguments'];
     } elseif ($requirehv) {
         // Advanced search form submitted
         if (empty($serverData['humanverify'])) {
             $serverData['humanverify'] = array();
         }
         $return = $api->callApi('hv', 'verifyToken', array($serverData['humanverify'], 'search'));
         if ($return !== true) {
             $adv_search = $api->callApi('route', 'getRoute', array('pathInfo' => 'advanced_search', 'queryString' => ''), true);
             $arguments = $adv_search['arguments'];
             $error = $return['errors'][0][0];
         }
     }
     $pageid = (int) (isset($arguments['pageid']) ? $arguments['pageid'] : $arguments['contentid']);
     $page = $api->callApi('page', 'fetchPageById', array($pageid, $arguments));
     if (!$page) {
         echo 'Could not find page.';
         exit;
     }
     $phrases = $api->callApi('phrase', 'fetch', array(array('advanced_search', 'search_results')));
     $page['crumbs'] = array(0 => array('title' => $phrases['advanced_search'], 'url' => vB5_Template_Runtime::buildUrl('advanced_search', array(), array(), array('noBaseUrl' => true))), 1 => array('title' => $phrases['search_results'], 'url' => ''));
     // avoid search page itself being indexed
     $page['noindex'] = 1;
     if (!empty($serverData['cookie'])) {
         $serverData['searchJSON'] = '{"specific":[' . $_COOKIE[$serverData['cookie']] . ']}';
     }
     if (!empty($serverData['searchJSON'])) {
         if (is_string($serverData['searchJSON'])) {
             if (preg_match('/[^\\x00-\\x7F]/', $serverData['searchJSON'])) {
                 $serverData['searchJSON'] = vB5_String::toUtf8($serverData['searchJSON'], vB5_String::getTempCharset());
             }
             $serverData['searchJSON'] = json_decode($serverData['searchJSON'], true);
         }
         if (!empty($serverData['searchJSON'])) {
             if (!empty($serverData['searchJSON']['keywords'])) {
                 $serverData['searchJSON']['keywords'] = str_replace(array('"', '\\'), '', $serverData['searchJSON']['keywords']);
                 $serverData['searchJSON']['keywords'] = filter_var($serverData['searchJSON']['keywords'], FILTER_SANITIZE_STRING);
             }
             $serverData['searchJSON'] = json_encode($serverData['searchJSON']);
         } else {
             $serverData['searchJSON'] = '';
         }
         $page['searchJSON'] = $serverData['searchJSON'];
         $extra = array('searchJSON' => !empty($serverData['searchJSON']) ? $serverData['searchJSON'] : '{}');
         if (!empty($serverData['AdvSearch'])) {
             $extra['AdvSearch'] = 1;
         }
         $page['url'] = str_replace('&amp;', '&', vB5_Route::buildUrl('search', array(), $extra));
         //$page['searchJSONStructure'] = json_decode($page['searchJSON'],true);
         $page['crumbs'][0]['url'] = vB5_Template_Runtime::buildUrl('advanced_search', array(), array('searchJSON' => $page['searchJSON']), array('noBaseUrl' => true));
     } elseif (!empty($serverData['q'])) {
         $serverData['q'] = str_replace(array('"', '\\'), '', $serverData['q']);
         $serverData['q'] = filter_var($serverData['q'], FILTER_SANITIZE_STRING);
         $searchType = '';
         if (!empty($serverData['type'])) {
             $serverData['type'] = str_replace(array('"', '\\'), '', $serverData['type']);
             $serverData['type'] = filter_var($serverData['type'], FILTER_SANITIZE_STRING);
             $searchType = ',"type":"' . $serverData['type'] . '"';
         }
         $page['searchJSON'] = '{"keywords":"' . $serverData['q'] . '","sort":"title"' . $searchType . '}';
         $extra = array('q' => $serverData['q']);
         if (!empty($serverData['AdvSearch'])) {
             $extra['AdvSearch'] = 1;
         }
         $page['url'] = str_replace('&amp;', '&', vB5_Route::buildUrl('search', array(), $extra));
         $page['searchStr'] = $serverData['q'];
         $page['crumbs'][0]['url'] = vB5_Template_Runtime::buildUrl('advanced_search', array(''), array('searchJSON' => $page['searchJSON']), array('noBaseUrl' => true));
     } elseif (!empty($serverData['r'])) {
         unset($page['crumbs'][0]);
         $page['url'] = str_replace('&amp;', '&', vB5_Route::buildUrl('search', array(), array('r' => $serverData['r'])));
         $page['resultId'] = $serverData['r'];
         if (!empty($serverData['p']) && is_numeric($serverData['p'])) {
             $page['currentPage'] = intval($serverData['p']);
         }
         $page['crumbs'][0]['url'] = vB5_Template_Runtime::buildUrl('advanced_search', array(), array('r' => $serverData['r']), array('noBaseUrl' => true));
     } else {
         return $this->actionIndex();
     }
     $page['ignore_np_notices'] = vB5_ApplicationAbstract::getIgnoreNPNotices();
     if (!empty($error)) {
         $page['error'] = $error;
     }
     $templater = new vB5_Template($page['screenlayouttemplate']);
     $templater->registerGlobal('page', $page);
     $page = $this->outputPage($templater->render(), false);
     $fullPage = $top . $page;
     if (vB5_Request::get('cachePageForGuestTime') > 0 and !vB5_User::get('userid')) {
         vB_Cache::instance()->write($fullPageKey, $fullPage, vB5_Request::get('cachePageForGuestTime'));
     }
     if (!vB5_Request::get('useEarlyFlush')) {
         echo $fullPage;
     } else {
         echo $page;
     }
 }