Skip to content

Commit

Permalink
Add permissions and settings to disable & prevent image uploads + bri…
Browse files Browse the repository at this point in the history
…ng back image URL inputs
  • Loading branch information
dsevillamartin committed May 20, 2024
1 parent 54753a8 commit 99c6d0d
Show file tree
Hide file tree
Showing 14 changed files with 184 additions and 19 deletions.
1 change: 1 addition & 0 deletions extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
->default('fof-polls.enableGlobalPolls', false)
->default('fof-polls.image_height', 250)
->default('fof-polls.image_width', 250)
->default('fof-polls.allowImageUploads', false)
->serializeToForum('globalPollsEnabled', 'fof-polls.enableGlobalPolls', 'boolval')
->serializeToForum('allowPollOptionImage', 'fof-polls.allowOptionImage', 'boolval')
->serializeToForum('pollMaxOptions', 'fof-polls.maxOptions', 'intval')
Expand Down
14 changes: 14 additions & 0 deletions js/src/admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ app.initializers.add('fof/polls', () => {
label: app.translator.trans('fof-polls.admin.settings.max_options'),
min: 2,
})
.registerSetting({
setting: 'fof-polls.allowImageUploads',
type: 'switch',
label: app.translator.trans('fof-polls.admin.settings.allow_image_uploads'),
help: app.translator.trans('fof-polls.admin.settings.allow_image_uploads_help'),
})
.registerSetting({
setting: 'fof-polls.enableGlobalPolls',
type: 'boolean',
Expand Down Expand Up @@ -77,6 +83,14 @@ app.initializers.add('fof/polls', () => {
},
'start'
)
.registerPermission(
{
icon: 'fas fa-image',
label: app.translator.trans('fof-polls.admin.permissions.upload_images'),
permission: 'uploadPollImages',
},
'start'
)
.registerPermission(
{
icon: 'fas fa-poll',
Expand Down
68 changes: 60 additions & 8 deletions js/src/forum/components/PollForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,32 @@ export default class PollForm extends Component<PollFormAttrs, PollFormState> {
95
);

const hasImage = this.image();

items.add(
'poll_image',
<div className="Form-group">
<label className="label">{app.translator.trans('fof-polls.forum.modal.poll_image.label')}</label>
<p className="helpText">{app.translator.trans('fof-polls.forum.modal.poll_image.help')}</p>
<UploadPollImageButton name="pollImage" poll={this.state.poll} onUpload={this.pollImageUploadSuccess.bind(this)} />
<input type="hidden" name="pollImage" value={this.image()} />
{this.uploadConditional(
this.state.poll?.isImageUpload(),
<>
<p className="helpText">{app.translator.trans('fof-polls.forum.modal.poll_image.help')}</p>
<UploadPollImageButton name="pollImage" poll={this.state.poll} onUpload={this.pollImageUploadSuccess.bind(this)} />
<input type="hidden" name="pollImage" bidi={this.image} />
</>,
<input
type="url"
name="pollImage"
className="FormControl"
bidi={this.image}
placeholder={app.translator.trans('fof-polls.forum.modal.image_option_placeholder')}
/>
)}
</div>,
90
);

if (this.image()) {
if (hasImage) {
items.add(
'poll_image_alt',
<div className="Form-group">
Expand Down Expand Up @@ -262,8 +276,11 @@ export default class PollForm extends Component<PollFormAttrs, PollFormState> {

displayOptions(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
const canUpload = app.forum.attribute<boolean>('canUploadPollImages');

this.options.forEach((option, i) => {
const imgFunc = this.optionImageUrls[i];

items.add(
'option-' + i,
<div className="Form-group">
Expand All @@ -277,10 +294,23 @@ export default class PollForm extends Component<PollFormAttrs, PollFormState> {
/>
{app.forum.attribute<boolean>('allowPollOptionImage') && (
<div className="Poll-answer-image">
<label className="label">{app.translator.trans('fof-polls.forum.modal.poll_option_image.label')}</label>
<p className="helpText">{app.translator.trans('fof-polls.forum.modal.poll_option_image.help')}</p>
<UploadPollImageButton name="pollOptionImage" option={option} onUpload={this.pollOptionImageUploadSuccess.bind(this, i)} />
<input type="hidden" name={'answerImage' + (i + 1)} value={this.optionImageUrls[i]()} />
{this.uploadConditional(
option?.isImageUpload(),
<>
<label className="label">{app.translator.trans('fof-polls.forum.modal.poll_option_image.label')}</label>
<p className="helpText">{app.translator.trans('fof-polls.forum.modal.poll_option_image.help')}</p>
<UploadPollImageButton name="pollOptionImage" option={option} onUpload={this.pollOptionImageUploadSuccess.bind(this, i)} />
<input type="hidden" name={'answerImage' + (i + 1)} value={imgFunc()} />
</>,

<input
type="url"
name={'answerImage' + (i + 1)}
className="FormControl"
bidi={imgFunc[i]}
placeholder={app.translator.trans('fof-polls.forum.modal.image_option_placeholder')}
/>
)}
</div>
)}
</fieldset>
Expand Down Expand Up @@ -400,4 +430,26 @@ export default class PollForm extends Component<PollFormAttrs, PollFormState> {
pollOptionImageUploadSuccess(index: number, fileName: string | null | undefined): void {
this.optionImageUrls[index] = Stream(fileName);
}

uploadConditional(isUpload: boolean, ifCanUpload: JSX.Element, ifCannotUpload: JSX.Element) {
const canUpload = app.forum.attribute<boolean>('canUploadPollImages');
const canUploadNow = this.state.poll?.exists || (app.forum.attribute('canStartPolls') && app.forum.attribute('canStartGlobalPolls'));

// if can upload OR image is already uploaded
if (canUpload || isUpload) {
// may not have enough permissions to upload before creating poll
if (!canUploadNow && !isUpload) {
return (
<>
{ifCannotUpload}
<p class="helpText">{app.translator.trans('fof-polls.forum.modal.poll_image.later_help')}</p>
</>
);
}

return ifCanUpload;
}

return ifCannotUpload;
}
}
23 changes: 20 additions & 3 deletions js/src/forum/components/UploadPollImageButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import app from 'flarum/forum/app';
import Button, { IButtonAttrs } from 'flarum/common/components/Button';
import classList from 'flarum/common/utils/classList';
import type Mithril from 'mithril';
import Mithril from 'mithril';
import Poll from '../models/Poll';
import PollOption from '../models/PollOption';

Expand Down Expand Up @@ -31,6 +31,7 @@ export default class UploadPollImageButton extends Button<UploadPollImageButtonA
this.attrs.className = classList(this.attrs.className, 'Button');

const imageUrl = this.getImageUrl();
const canUpload = app.forum.attribute<boolean>('canUploadPollImages');

if (imageUrl) {
this.attrs.onclick = this.remove.bind(this);
Expand All @@ -40,14 +41,19 @@ export default class UploadPollImageButton extends Button<UploadPollImageButtonA
<p>
<img src={imageUrl} alt="" />
</p>
<p>{super.view({ ...vnode, children: app.translator.trans('fof-polls.forum.upload_image.remove_button') })}</p>
<p>
{super.view({
...vnode,
children: app.translator.trans('fof-polls.forum.upload_image.remove_button'),
})}
</p>
</div>
);
} else {
this.attrs.onclick = this.upload.bind(this);
}

return super.view({ ...vnode, children: app.translator.trans('fof-polls.forum.upload_image.upload_button') });
return canUpload && super.view({ ...vnode, children: app.translator.trans('fof-polls.forum.upload_image.upload_button') });
}

/**
Expand Down Expand Up @@ -98,6 +104,17 @@ export default class UploadPollImageButton extends Button<UploadPollImageButtonA
method: 'DELETE',
url: this.resourceUrl(fileName),
})
.then((upload) => {
if (this.attrs.poll?.exists) {
this.attrs.poll.pushAttributes({ image: null, imageUrl: null, isImageUpload: false });
}

if (this.attrs.option?.exists) {
this.attrs.option.pushAttributes({ imageUrl: false });
}

return upload;
})
.then(this.success.bind(this), this.failure.bind(this));
}

Expand Down
4 changes: 4 additions & 0 deletions js/src/forum/models/Poll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export default class Poll extends Model {
return Model.attribute<string | null>('imageAlt').call(this);
}

isImageUpload() {
return Model.attribute<boolean>('isImageUpload').call(this);
}

hasEnded() {
return Model.attribute<boolean>('hasEnded').call(this);
}
Expand Down
4 changes: 4 additions & 0 deletions js/src/forum/models/PollOption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export default class PollOption extends Model {
return Model.attribute<string | null>('imageUrl').call(this);
}

isImageUpload() {
return Model.attribute<boolean>('isImageUpload').call(this);
}

voteCount() {
return Model.attribute<number>('voteCount').call(this);
}
Expand Down
6 changes: 6 additions & 0 deletions resources/less/forum.less
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@
fieldset {
flex-grow: 1;
}

.Poll-answer-image {
img {
max-width: 100%;
}
}
}

.PollModal--button {
Expand Down
6 changes: 5 additions & 1 deletion resources/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ fof-polls:
admin:
settings:
allow_option_image: Allow an image URL to be provided for each poll option
allow_image_uploads: Allow image uploads for polls and their options
allow_image_uploads_help: If enabled, users will be able to upload images alongside also providing an image URL (if enabled for options, always enabled for the poll itself).
enable_global_polls: Enable global polls
enable_global_polls_help: Global polls are not tied to a specific post, and may be accessed from a dedicated polls page.
max_options: Maximum number of options per poll
Expand All @@ -15,6 +17,7 @@ fof-polls:
start_global: Start a global poll
self_edit: Edit created polls (requires post edit permission)
self_post_edit: Edit *all* polls on own posts (requires post edit permission)
upload_images: Allow users to upload images for polls
vote: Vote on polls
change_vote: Change vote
moderate: Edit & remove polls
Expand Down Expand Up @@ -86,7 +89,7 @@ fof-polls:
hide_votes_label: Hide votes until poll ends
allow_change_vote_label: Allow users to change their vote
question_placeholder: Question
subtitle_placeholder: Subtitle/Description (Optional)
subtitle_placeholder: Subtitle/Description (optional)
submit: Submit
delete: => core.ref.delete
error: => core.lib.error.generic_message
Expand All @@ -95,6 +98,7 @@ fof-polls:
help: Upload an image to be displayed alongside the poll (optional).
alt_label: Image Alt Text
alt_help_text: This text is required when an image is set, it will be displayed if the image fails to load.
later_help: You will be able to upload an image after creating the poll.
poll_option_image:
label: Poll Answer Image
help: Upload an image to be displayed alongside the poll answer (optional).
Expand Down
14 changes: 14 additions & 0 deletions src/Api/AddForumAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,28 @@
namespace FoF\Polls\Api;

use Flarum\Api\Serializer\ForumSerializer;
use Flarum\Settings\SettingsRepositoryInterface;

class AddForumAttributes
{
/**
* @var SettingsRepositoryInterface
*/
protected $settings;

public function __construct(SettingsRepositoryInterface $settings)
{
$this->settings = $settings;
}

public function __invoke(ForumSerializer $serializer, array $model, array $attributes): array
{
$attributes['canStartPolls'] = $serializer->getActor()->can('discussion.polls.start');
$attributes['canStartGlobalPolls'] = $serializer->getActor()->can('startGlobalPoll');

$areUploadsEnabled = (bool) $this->settings->get('fof-polls.allowImageUploads');
$attributes['canUploadPollImages'] = $areUploadsEnabled && $serializer->getActor()->can('uploadPollImages');

return $attributes;
}
}
23 changes: 20 additions & 3 deletions src/Api/Controllers/UploadPollImageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,25 @@ public function handle(ServerRequestInterface $request): ResponseInterface
$actor = RequestUtil::getActor($request);
$pollId = Arr::get($request->getQueryParams(), 'pollId');

if ($actor->cannot('startPoll') || $actor->cannot('startGlobalPoll')) {
throw new PermissionDeniedException('You do not have permission to upload poll images');
$areUploadsAllowed = (bool) $this->settings->get('fof-polls.allowImageUploads');

if (!$areUploadsAllowed) {
throw new PermissionDeniedException();
}

$actor->assertCan('uploadPollImages');

// if a poll ID is given, check that the user can edit that poll (and thus upload images!)
if ($pollId) {
$poll = Poll::findOrFail($pollId);

$actor->assertCan('edit', $poll);
} else {
$poll = null;

// we don't know whether this image is for a global or a regular poll -- image upload can be done before poll creation
$actor->assertCan('startPoll');
$actor->assertCan('startGlobalPoll');
}

$file = Arr::get($request->getUploadedFiles(), $this->filenamePrefix);
Expand All @@ -83,7 +100,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface

$this->uploadDir->put($uploadName, $encodedImage);

if ($pollId && $poll = Poll::find($pollId)) {
if ($pollId && $poll) {
$poll->image = $uploadName;
$poll->save();
}
Expand Down
26 changes: 22 additions & 4 deletions src/Api/Controllers/UploadPollOptionImageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,37 @@ public function handle(ServerRequestInterface $request): ResponseInterface
$actor = RequestUtil::getActor($request);
$optionId = Arr::get($request->getQueryParams(), 'optionId');

if ($actor->cannot('startPoll') || $actor->cannot('startGlobalPoll')) {
throw new PermissionDeniedException('You do not have permission to upload poll option images');
$areUploadsAllowed = (bool) $this->settings->get('fof-polls.allowImageUploads');

if (!$areUploadsAllowed) {
throw new PermissionDeniedException();
}

$actor->assertCan('uploadPollImages');

// if an option ID is given, check that the user can edit that poll (and thus upload images!)
if ($optionId) {
$option = PollOption::findOrFail($optionId);
$poll = $option->poll;

$actor->assertCan('edit', $poll);
} else {
$option = null;

// we don't know whether this image is for a global or a regular poll -- image upload can be done before poll creation
$actor->assertCan('startPoll');
$actor->assertCan('startGlobalPoll');
}

$file = Arr::get($request->getUploadedFiles(), $this->filenamePrefix);

$uploadName = $uploadName = $this->uploadName();
$uploadName = $this->uploadName();

$encodedImage = $this->makeImage($file, $uploadName);

$this->uploadDir->put($uploadName, $encodedImage);

if ($optionId && $option = PollOption::find($optionId)) {
if ($option) {
$option->image_url = $uploadName;
$option->save();
}
Expand Down
4 changes: 4 additions & 0 deletions src/Api/Serializers/PollOptionSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ protected function getDefaultAttributes($option)
'voteCount' => $this->actor->can('seeVoteCount', $option->poll) ? (int) $option->vote_count : null,
];

if ($attributes['imageUrl']) {
$attributes['isImageUpload'] = !filter_var($option->image_url, FILTER_VALIDATE_URL);
}

return $attributes;
}

Expand Down
Loading

0 comments on commit 99c6d0d

Please sign in to comment.