/** * 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'); }