/**
  * Token-based, per-request authentication
  *
  * This method takes the entire request string and turns the query into an
  * array of data. It then uses all the data to generate a signature the same
  * way it got generated on the client. If the server signature and client
  * token match, the client is considered legimate and the request is served.
  *
  * Based on initial work by Diego Zanella
  * @link http://careers.stackoverflow.com/diegozanella
  *
  * @since  0.1.0
  * @access public
  * @throws Exception
  * @return void
  * @static
  */
 public static function authenticateRequest()
 {
     $username = getIncomingValue("username");
     $email = getIncomingValue("email");
     if (!$username && !$email) {
         throw new Exception(t("API.Error.User.Missing"), 401);
     }
     if (!($userID = static::getUserID($username, $email))) {
         throw new Exception(t("API.Error.User.Invalid"), 401);
     }
     if (!($timestamp = getIncomingValue("timestamp"))) {
         throw new Exception(t("API.Error.Timestamp.Missing"), 401);
     }
     // Make sure that request is still valid
     if (abs($timestamp - time()) > c("API.Expiration")) {
         throw new Exception(t("API.Error.Timestamp.Invalid"), 401);
     }
     if (!($token = getIncomingValue("token"))) {
         throw new Exception(t("API.Error.Token.Missing"), 401);
     }
     $parsedUrl = parse_url(Gdn::request()->pathAndQuery());
     // Turn the request query data into an array to be used in the token
     // generation
     parse_str(val("query", $parsedUrl, []), $data);
     // Unset the values we don't want to include in the token generation
     unset($data["token"], $data["DeliveryType"], $data["DeliveryMethod"]);
     if ($token != ($signature = static::generateSignature($data))) {
         throw new Exception(t("API.Error.Token.Invalid"), 401);
     }
     // Now that the client has been thoroughly verified, start a session for
     // the duration of the request using the User ID specified earlier
     if ($token == $signature) {
         Gdn::session()->start(intval($userID), false);
     }
 }
 /**
  * Map the API request to the corrosponding controller
  *
  * @since  0.1.0
  * @access public
  * @throws Exception
  * @return void
  * @static
  */
 public static function dispatchRequest()
 {
     $request = Gdn::request();
     $requestUri = static::getRequestUri();
     $requestMethod = static::getRequestMethod();
     if (!in_array($requestMethod, static::$supportedMethods)) {
         throw new Exception(t("API.Error.MethodNotAllowed"), 405);
     }
     if (!Gdn::session()->isValid()) {
         $username = getIncomingValue("username");
         $email = getIncomingValue("email");
         if ($username || $email) {
             APIAuth::authenticateRequest();
         }
     }
     $resource = val(1, $requestUri);
     $apiClass = ucfirst($resource) . "API";
     if (!class_exists($apiClass)) {
         throw new Exception(sprintf(t("API.Error.Class.Invalid"), $apiClass), 404);
     }
     if (!is_subclass_of($apiClass, "APIMapper")) {
         throw new Exception(t("API.Error.Mapper"), 500);
     }
     $apiClass = new $apiClass();
     $isWriteMethod = in_array($requestMethod, ["post", "put", "delete"]);
     $requestArguments = $isWriteMethod ? static::getRequestArguments() : [];
     $dispatch = static::map($resource, $apiClass, $requestUri, $requestMethod, $requestArguments);
     $controller = $dispatch["controller"];
     if (!$controller) {
         throw new Exception(t("API.Error.Controller.Missing"), 500);
     }
     $inputData = array_merge($requestArguments, $dispatch["arguments"]);
     if ($isWriteMethod) {
         // Set the transient key since we no longer have a front-end that
         // takes care of doing it for us
         $inputData["TransientKey"] = Gdn::session()->transientKey();
         // Authentication is always required for write-methods
         $dispatch["authenticate"] = true;
         // As Garden doesn"t take PUT and DELETE requests into account when
         // verifying requests using IsPostBack() and IsAuthencatedPostBack(),
         // we need to mask PUTs and DELETEs as POSTs.
         $request->requestMethod("post");
         // Add any API-specific arguments to the requests arguments
         $request->setRequestArguments(Gdn_Request::INPUT_POST, $inputData);
         // Set the PHP $_POST global as the result of any form data picked
         // up by Garden.
         $_POST = $request->post();
     }
     if ($dispatch["authenticate"] && !Gdn::session()->isValid()) {
         throw new Exception(t("API.Error.AuthRequired"), 401);
     }
     $application = $dispatch["application"];
     if ($application) {
         Gdn_Autoloader::attachApplication($application);
     }
     $method = $dispatch["method"];
     $arguments = $dispatch["arguments"];
     Gdn::request()->withControllerMethod($controller, $method, $arguments);
 }
 /**
  * Dismiss a message (per user).
  *
  * @since 2.0.0
  * @access public
  */
 public function dismiss($MessageID = '', $TransientKey = false)
 {
     $Session = Gdn::session();
     if ($TransientKey !== false && $Session->validateTransientKey($TransientKey)) {
         $Prefs = $Session->getPreference('DismissedMessages', array());
         $Prefs[] = $MessageID;
         $Session->setPreference('DismissedMessages', $Prefs);
     }
     if ($this->_DeliveryType === DELIVERY_TYPE_ALL) {
         redirect(getIncomingValue('Target', '/discussions'));
     }
     $this->render();
 }
 /**
  * Autocomplete a username.
  *
  * @since 2.0.0
  * @access public
  */
 public function autoComplete()
 {
     $this->deliveryType(DELIVERY_TYPE_NONE);
     $Q = getIncomingValue('q');
     $UserModel = new UserModel();
     $Data = $UserModel->getLike(array('u.Name' => $Q), 'u.Name', 'asc', 10, 0);
     foreach ($Data->result() as $User) {
         echo htmlspecialchars($User->Name) . '|' . Gdn_Format::text($User->UserID) . "\n";
     }
     $this->render();
 }
    /**
     * Alternate version of Index that uses the embed master view.
     *
     * @param int $DiscussionID Unique identifier, if discussion has been created.
     * @param string $DiscussionStub Deprecated.
     * @param int $Offset
     * @param int $Limit
     */
    public function embed($DiscussionID = '', $DiscussionStub = '', $Offset = '', $Limit = '')
    {
        $this->title(t('Comments'));
        // Add theme data
        $this->Theme = c('Garden.CommentsTheme', $this->Theme);
        Gdn_Theme::section('Comments');
        // Force view options
        $this->MasterView = 'empty';
        $this->CanEditComments = false;
        // Don't show the comment checkboxes on the embed comments page
        // Add some css to help with the transparent bg on embedded comments
        if ($this->Head) {
            $this->Head->addString('<style type="text/css">
body { background: transparent !important; }
</style>');
        }
        // Javascript files & options
        $this->addJsFile('jquery.gardenmorepager.js');
        $this->addJsFile('jquery.autosize.min.js');
        $this->addJsFile('discussion.js');
        $this->removeJsFile('autosave.js');
        $this->addDefinition('DoInform', '0');
        // Suppress inform messages on embedded page.
        $this->addDefinition('SelfUrl', Gdn::request()->PathAndQuery());
        $this->addDefinition('Embedded', true);
        // Define incoming variables (prefer querystring parameters over method parameters)
        $DiscussionID = is_numeric($DiscussionID) && $DiscussionID > 0 ? $DiscussionID : 0;
        $DiscussionID = getIncomingValue('vanilla_discussion_id', $DiscussionID);
        $Offset = getIncomingValue('Offset', $Offset);
        $Limit = getIncomingValue('Limit', $Limit);
        $vanilla_identifier = getIncomingValue('vanilla_identifier', '');
        // Only allow vanilla identifiers of 32 chars or less - md5 if larger
        if (strlen($vanilla_identifier) > 32) {
            $vanilla_identifier = md5($vanilla_identifier);
        }
        $vanilla_type = getIncomingValue('vanilla_type', 'page');
        $vanilla_url = getIncomingValue('vanilla_url', '');
        $vanilla_category_id = getIncomingValue('vanilla_category_id', '');
        $ForeignSource = array('vanilla_identifier' => $vanilla_identifier, 'vanilla_type' => $vanilla_type, 'vanilla_url' => $vanilla_url, 'vanilla_category_id' => $vanilla_category_id);
        $this->setData('ForeignSource', $ForeignSource);
        // Set comment sorting
        $SortComments = c('Garden.Embed.SortComments') == 'desc' ? 'desc' : 'asc';
        $this->setData('SortComments', $SortComments);
        // Retrieve the discussion record
        $Discussion = false;
        if ($DiscussionID > 0) {
            $Discussion = $this->DiscussionModel->getID($DiscussionID);
        } elseif ($vanilla_identifier != '' && $vanilla_type != '') {
            $Discussion = $this->DiscussionModel->GetForeignID($vanilla_identifier, $vanilla_type);
        }
        // Set discussion data if we have one for this page
        if ($Discussion) {
            // Allow Vanilla.Comments.View to be defined to limit access to embedded comments only.
            // Otherwise, go with normal discussion view permissions. Either will do.
            $this->permission(array('Vanilla.Discussions.View', 'Vanilla.Comments.View'), false, 'Category', $Discussion->PermissionCategoryID);
            $this->setData('Discussion', $Discussion, true);
            $this->setData('DiscussionID', $Discussion->DiscussionID, true);
            $this->title($Discussion->Name);
            // Actual number of comments, excluding the discussion itself
            $ActualResponses = $Discussion->CountComments;
            // Define the query offset & limit
            if (!is_numeric($Limit) || $Limit < 0) {
                $Limit = c('Garden.Embed.CommentsPerPage', 30);
            }
            $OffsetProvided = $Offset != '';
            list($Offset, $Limit) = offsetLimit($Offset, $Limit);
            $this->Offset = $Offset;
            if (c('Vanilla.Comments.AutoOffset')) {
                if ($ActualResponses <= $Limit) {
                    $this->Offset = 0;
                }
                if ($this->Offset == $ActualResponses) {
                    $this->Offset -= $Limit;
                }
            } elseif ($this->Offset == '') {
                $this->Offset = 0;
            }
            if ($this->Offset < 0) {
                $this->Offset = 0;
            }
            // Set the canonical url to have the proper page title.
            $this->canonicalUrl(discussionUrl($Discussion, pageNumber($this->Offset, $Limit)));
            // Load the comments.
            $CurrentOrderBy = $this->CommentModel->orderBy();
            if (stringBeginsWith(GetValueR('0.0', $CurrentOrderBy), 'c.DateInserted')) {
                $this->CommentModel->orderBy('c.DateInserted ' . $SortComments);
                // allow custom sort
            }
            $this->setData('Comments', $this->CommentModel->get($Discussion->DiscussionID, $Limit, $this->Offset), true);
            if (count($this->CommentModel->where()) > 0) {
                $ActualResponses = false;
            }
            $this->setData('_Count', $ActualResponses);
            // Build a pager
            $PagerFactory = new Gdn_PagerFactory();
            $this->EventArguments['PagerType'] = 'MorePager';
            $this->fireEvent('BeforeBuildPager');
            $this->Pager = $PagerFactory->getPager($this->EventArguments['PagerType'], $this);
            $this->Pager->ClientID = 'Pager';
            $this->Pager->MoreCode = 'More Comments';
            $this->Pager->configure($this->Offset, $Limit, $ActualResponses, 'discussion/embed/' . $Discussion->DiscussionID . '/' . Gdn_Format::url($Discussion->Name) . '/%1$s');
            $this->Pager->CurrentRecords = $this->Comments->numRows();
            $this->fireEvent('AfterBuildPager');
        }
        // Define the form for the comment input
        $this->Form = Gdn::Factory('Form', 'Comment');
        $this->Form->Action = url('/post/comment/');
        $this->Form->addHidden('CommentID', '');
        $this->Form->addHidden('Embedded', 'true');
        // Tell the post controller that this is an embedded page (in case there are custom views it needs to pick up from a theme).
        $this->Form->addHidden('DisplayNewCommentOnly', 'true');
        // Only load/display the new comment after posting (don't load all new comments since the page last loaded).
        // Grab the page title
        if ($this->Request->get('title')) {
            $this->Form->setValue('Name', $this->Request->get('title'));
        }
        // Set existing DiscussionID for comment form
        if ($Discussion) {
            $this->Form->addHidden('DiscussionID', $Discussion->DiscussionID);
        }
        foreach ($ForeignSource as $Key => $Val) {
            // Drop the foreign source information into the form so it can be used if creating a discussion
            $this->Form->addHidden($Key, $Val);
            // Also drop it into the definitions so it can be picked up for stashing comments
            $this->addDefinition($Key, $Val);
        }
        // Retrieve & apply the draft if there is one:
        $Draft = false;
        if (Gdn::session()->UserID && $Discussion) {
            $DraftModel = new DraftModel();
            $Draft = $DraftModel->get(Gdn::session()->UserID, 0, 1, $Discussion->DiscussionID)->firstRow();
            $this->Form->addHidden('DraftID', $Draft ? $Draft->DraftID : '');
        }
        if ($Draft) {
            $this->Form->setFormValue('Body', $Draft->Body);
        } else {
            // Look in the session stash for a comment
            $StashComment = Gdn::session()->getPublicStash('CommentForForeignID_' . $ForeignSource['vanilla_identifier']);
            if ($StashComment) {
                $this->Form->setValue('Body', $StashComment);
                $this->Form->setFormValue('Body', $StashComment);
            }
        }
        // Deliver JSON data if necessary
        if ($this->_DeliveryType != DELIVERY_TYPE_ALL) {
            if ($this->Discussion) {
                $this->setJson('LessRow', $this->Pager->toString('less'));
                $this->setJson('MoreRow', $this->Pager->toString('more'));
            }
            $this->View = 'comments';
        }
        // Ordering note for JS
        if ($SortComments == 'desc') {
            $this->addDefinition('PrependNewComments', '1');
        }
        // Report the discussion id so js can use it.
        if ($Discussion) {
            $this->addDefinition('DiscussionID', $Discussion->DiscussionID);
        }
        $this->fireEvent('BeforeDiscussionRender');
        $this->render();
    }
 /**
  * Set the date range.
  *
  * @param $Sender
  * @throws Exception
  */
 private function configureRange($Sender)
 {
     // Grab the range resolution from the url or form. Default to "day" range.
     $Sender->Range = getIncomingValue('Range');
     if (!in_array($Sender->Range, array(VanillaStatsPlugin::RESOLUTION_DAY, VanillaStatsPlugin::RESOLUTION_MONTH))) {
         $Sender->Range = VanillaStatsPlugin::RESOLUTION_DAY;
     }
     // Define default values for start & end dates
     $Sender->DayStampStart = strtotime('1 month ago');
     // Default to 1 month ago
     $Sender->MonthStampStart = strtotime('12 months ago');
     // Default to 24 months ago
     $Sender->DayDateStart = Gdn_Format::toDate($Sender->DayStampStart);
     $Sender->MonthDateStart = Gdn_Format::toDate($Sender->MonthStampStart);
     // Validate that any values coming from the url or form are valid
     $Sender->DateRange = getIncomingValue('DateRange');
     $DateRangeParts = explode('-', $Sender->DateRange);
     $Sender->StampStart = strtotime(val(0, $DateRangeParts));
     $Sender->StampEnd = strtotime(val(1, $DateRangeParts));
     if (!$Sender->StampEnd) {
         $Sender->StampEnd = strtotime('yesterday');
     }
     // If no date was provided, or the provided values were invalid, use defaults
     if (!$Sender->StampStart) {
         $Sender->StampEnd = time();
         if ($Sender->Range == 'day') {
             $Sender->StampStart = $Sender->DayStampStart;
         }
         if ($Sender->Range == 'month') {
             $Sender->StampStart = $Sender->MonthStampStart;
         }
     }
     // Assign the variables used in the page with the validated values.
     $Sender->DateStart = Gdn_Format::toDate($Sender->StampStart);
     $Sender->DateEnd = Gdn_Format::toDate($Sender->StampEnd);
     $Sender->DateRange = $Sender->DateStart . ' - ' . $Sender->DateEnd;
     // Define the range boundaries.
     $Database = Gdn::database();
     // We use the User table as the boundary start b/c users are always inserted before discussions or comments.
     // We have to put a little kludge in here b/c an older version of Vanilla hard-inserted the admin user with an insert date of Sept 16, 1975.
     $Data = $Database->sql()->select('DateInserted')->from('User')->where('DateInserted >', '1975-09-17')->orderBy('DateInserted', 'asc')->limit(1)->get()->firstRow();
     $Sender->BoundaryStart = Gdn_Format::date($Data ? $Data->DateInserted : $Sender->DateStart, '%Y-%m-%d');
     $Sender->BoundaryEnd = Gdn_Format::date($Sender->DateEnd, '%Y-%m-%d');
 }