loadAlbumPhotoComment($albumUrlAlias, $photoFilename, $commentID, $album, $photo, $comment)) { return null; } if (!User::currentOrAnonymous()->can('moderate-comments', $photo)) { App::abort(403); return null; } if (!$comment->isModerated()) { if ($request->has('approve')) { $comment->approved_at = new \DateTime(); $comment->approved_user_id = $this->getUser()->id; $comment->save(); $this->notifyAlbumOwner($album, $photo, $comment); $request->getSession()->flash('success', trans('gallery.photo_comment_approved_successfully')); } else if ($request->has('reject')) { $comment->rejected_at = new \DateTime(); $comment->rejected_user_id = $this->getUser()->id; $comment->save(); $request->getSession()->flash('success', trans('gallery.photo_comment_rejected_successfully')); } } return redirect($photo->url()); } public function reply(Request $request, $albumUrlAlias, $photoFilename, $commentID) { $album = null; /** @var Photo $photo */ $photo = null; /** @var PhotoComment $comment */ $comment = null; if (!$this->loadAlbumPhotoComment($albumUrlAlias, $photoFilename, $commentID, $album, $photo, $comment)) { return null; } if (!User::currentOrAnonymous()->can('post-comment', $photo)) { App::abort(403); return null; } return Theme::render('partials.photo_comments_reply_form', [ 'photo' => $photo, 'reply_comment' => $comment ]); } public function store(Request $request, $albumUrlAlias, $photoFilename) { $album = null; /** @var Photo $photo */ $photo = null; /** @var PhotoComment $comment */ $comment = null; if (!$this->loadAlbumPhotoComment($albumUrlAlias, $photoFilename, 0, $album, $photo, $comment)) { return null; } if (!User::currentOrAnonymous()->can('post-comment', $photo)) { App::abort(403); return null; } // Validate and link the parent comment, if provided // We do this here so if the validation fails, we still have the parent comment available in the catch block $parentComment = null; if ($request->has('parent_comment_id')) { $parentComment = $photo->comments()->where('id', intval($request->get('parent_comment_id')))->first(); if (is_null($parentComment)) { return redirect($photo->url()); } } try { $this->validate($request, [ 'name' => 'required|max:255', 'email' => 'sometimes|max:255|email', 'comment' => 'required' ]); $commentText = $this->stripDisallowedHtmlTags($request->get('comment')); $comment = new PhotoComment(); $comment->photo_id = $photo->id; $comment->fill($request->only(['name', 'email'])); $comment->comment = $commentText; if (!is_null($parentComment)) { $comment->parent_comment_id = $parentComment->id; } // Set the created user ID if we're logged in $user = $this->getUser(); if (!is_null($user) && !$user->isAnonymous()) { $comment->created_user_id = $user->id; } // Auto-approve the comment if we're allowed to moderate comments $isAutoApproved = false; if (User::currentOrAnonymous()->can('moderate-comments', $photo)) { $comment->approved_at = new \DateTime(); $comment->approved_user_id = $user->id; $isAutoApproved = true; } // Auto-approve the comment if settings allow if ($user->isAnonymous() && !UserConfig::get('moderate_anonymous_users')) { $comment->approved_at = new \DateTime(); $comment->approved_user_id = null; // we don't have a user ID to set! $isAutoApproved = true; } else if (!$user->isAnonymous() && !UserConfig::get('moderate_known_users')) { $comment->approved_at = new \DateTime(); $comment->approved_user_id = $user->id; $isAutoApproved = true; } $comment->save(); // Send notification e-mails to moderators or album owner if (!$isAutoApproved) { $this->notifyAlbumModerators($album, $photo, $comment); $request->getSession()->flash('success', trans('gallery.photo_comment_posted_successfully_pending_moderation')); } else { $this->notifyAlbumOwner($album, $photo, $comment); $request->getSession()->flash('success', trans('gallery.photo_comment_posted_successfully')); } if ($request->isXmlHttpRequest()) { return response()->json(['redirect_url' => $photo->url()]); } else { return redirect($photo->url()); } } catch (ValidationException $e) { if (!is_null($parentComment)) { return redirect() ->to($photo->replyToCommentFormUrl($parentComment->id)) ->withErrors($e->errors()) ->withInput($request->all()); } else { return redirect() ->back() ->withErrors($e->errors()) ->withInput($request->all()); } } } private function loadAlbumPhotoComment($albumUrlAlias, $photoFilename, $commentID, &$album, &$photo, &$comment) { $album = DbHelper::getAlbumByPath($albumUrlAlias); if (is_null($album)) { App::abort(404); return false; } $this->authorizeForUser($this->getUser(), 'view', $album); $photo = PhotoController::loadPhotoByAlbumAndFilename($album, $photoFilename); if (!UserConfig::get('allow_photo_comments')) { // Not allowed to post comments App::abort(404); return false; } if (intval($commentID > 0)) { $comment = $photo->comments()->where('id', $commentID)->first(); if (is_null($comment)) { App::abort(404); return false; } } return true; } /** * Sends an e-mail notification to an album's moderators that a comment is available to moderate. * @param Album $album * @param Photo $photo * @param PhotoComment $comment */ private function notifyAlbumModerators(Album $album, Photo $photo, PhotoComment $comment) { // Get all users from the cache $helper = new PermissionsHelper(); $moderators = $helper->usersWhoCan_Album($album, 'moderate-comments'); /** @var User $moderator */ foreach ($moderators as $moderator) { Mail::to($moderator)->send(new ModeratePhotoComment($moderator, $album, $photo, $comment)); } } /** * Sends an e-mail notification to an album's owned that a comment has been posted/approved. * @param Album $album * @param Photo $photo * @param PhotoComment $comment */ private function notifyAlbumOwner(Album $album, Photo $photo, PhotoComment $comment) { $owner = $album->user; $poster = new User(); $poster->name = $comment->authorDisplayName(); $poster->email = $comment->authorEmail(); Mail::to($owner)->send(new PhotoCommentApproved($owner, $album, $photo, $comment)); Mail::to($poster)->send(new PhotoCommentApprovedUser($poster, $album, $photo, $comment)); } private function stripDisallowedHtmlTags($commentText) { $allowedHtmlTags = explode(',', UserConfig::get('photo_comments_allowed_html')); $allowedHtmlTagsCleaned = []; foreach ($allowedHtmlTags as $tag) { $allowedHtmlTagsCleaned[] = trim($tag); } // Match any starting HTML tags $regexMatchString = '/<(?!\/)([a-z]+)(?:\s.*)*>/Us'; $htmlTagMatches = []; preg_match_all($regexMatchString, $commentText, $htmlTagMatches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); for ($index = 0; $index < count($htmlTagMatches); $index++) { $htmlTagMatch = $htmlTagMatches[$index]; $htmlTag = $htmlTagMatch[1][0]; // e.g. "p" for

if (in_array($htmlTag, $allowedHtmlTagsCleaned)) { // This tag is allowed - carry on continue; } /* This tag is not allowed - remove it from the string */ // Find the closing tag $disallowedStringOffset = $htmlTagMatch[0][1]; $endingTagMatches = []; preg_match(sprintf('/(<%1$s.*>)(.+)<\/%1$s>/Us', $htmlTag), $commentText, $endingTagMatches, 0, $disallowedStringOffset); // Replace the matched string with the inner string $commentText = substr_replace($commentText, $endingTagMatches[2], $disallowedStringOffset, strlen($endingTagMatches[0])); // Adjust the offsets for strings after the one we're processing, so the offsets match up with the string correctly for ($index2 = $index + 1; $index2 < count($htmlTagMatches); $index2++) { // If this string appears entirely BEFORE the next one starts, we need to subtract the entire length. // Otherwise, we only need to substract the length of the start tag, as the next one starts within it. $differenceAfterReplacement = strlen($endingTagMatches[1]); if ($htmlTagMatch[0][1] + strlen($endingTagMatches[0]) < $htmlTagMatches[$index2][0][1]) { $differenceAfterReplacement = strlen($endingTagMatches[0]) - strlen($endingTagMatches[2]); } $htmlTagMatches[$index2][0][1] -= $differenceAfterReplacement; $htmlTagMatches[$index2][1][1] -= $differenceAfterReplacement; } } return $commentText; } }