Completed
Push — 3.0 ( 9dd29c...237018 )
by Jeroen
53:05
created

engine/lib/comments.php (2 issues)

1
<?php
2
/**
3
 * Elgg comments library
4
 *
5
 * @package    Elgg.Core
6
 * @subpackage Comments
7
 * @since 1.9
8
 */
9
10
use Elgg\Database\QueryBuilder;
11
12
/**
13
 * Comments initialization function
14
 *
15
 * @return void
16
 * @access private
17
 */
18
function _elgg_comments_init() {
19 75
	elgg_register_entity_type('object', 'comment');
20
21 75
	elgg_register_plugin_hook_handler('container_permissions_check', 'object', '_elgg_comments_container_permissions_override');
22 75
	elgg_register_plugin_hook_handler('permissions_check', 'object', '_elgg_comments_permissions_override');
23 75
	elgg_register_plugin_hook_handler('email', 'system', '_elgg_comments_notification_email_subject');
24
	
25 75
	elgg_register_plugin_hook_handler('register', 'menu:social', '_elgg_comments_social_menu_setup');
26
	
27 75
	elgg_register_event_handler('update:after', 'all', '_elgg_comments_access_sync', 600);
28
29 75
	elgg_register_ajax_view('core/ajax/edit_comment');
30 75
	elgg_register_ajax_view('page/elements/comments');
31 75
	elgg_register_ajax_view('river/elements/responses');
32
33 75
	elgg_register_plugin_hook_handler('likes:is_likable', 'object:comment', 'Elgg\Values::getTrue');
34
	
35 75
	elgg_register_notification_event('object', 'comment', ['create']);
36 75
	elgg_register_plugin_hook_handler('get', 'subscriptions', '_elgg_comments_add_content_owner_to_subscriptions');
37 75
	elgg_register_plugin_hook_handler('prepare', 'notification:create:object:comment', '_elgg_comments_prepare_content_owner_notification');
38 75
	elgg_register_plugin_hook_handler('prepare', 'notification:create:object:comment', '_elgg_comments_prepare_notification');
39 75
}
40
41
/**
42
 * Are comments displayed with latest first?
43
 *
44
 * @param ElggEntity $container Entity containing comments
45
 * @return bool False means oldest first.
46
 * @since 3.0
47
 */
48
function elgg_comments_are_latest_first(ElggEntity $container = null) {
49
	$params = [
50
		'entity' => $container,
51
	];
52
	return (bool) elgg_trigger_plugin_hook('config', 'comments_latest_first', $params, true);
53
}
54
55
/**
56
 * How many comments appear per page.
57
 *
58
 * @param ElggEntity $container Entity containing comments
59
 * @return int
60
 * @since 3.0
61
 */
62
function elgg_comments_per_page(ElggEntity $container = null) {
63
	$params = [
64
		'entity' => $container,
65
	];
66
	return (int) elgg_trigger_plugin_hook('config', 'comments_per_page', $params, 25);
67
}
68
69
/**
70
 * Redirect to the comment in context of the containing page
71
 *
72
 * @param int $comment_guid  GUID of the comment
73
 * @param int $fallback_guid GUID of the containing entity
74
 *
75
 * @return void
76
 * @access private
77
 */
78
function _elgg_comment_redirect($comment_guid, $fallback_guid) {
79
	$fail = function () {
80
		register_error(elgg_echo('generic_comment:notfound'));
81
		forward(REFERER);
82
	};
83
84
	$comment = get_entity($comment_guid);
85
	if (!$comment) {
86
		// try fallback if given
87
		$fallback = get_entity($fallback_guid);
88
		if (!$fallback) {
89
			$fail();
90
		}
91
92
		register_error(elgg_echo('generic_comment:notfound_fallback'));
93
		forward($fallback->getURL());
94
	}
95
96
	if (!$comment instanceof ElggComment) {
97
		$fail();
98
	}
99
100
	$container = $comment->getContainerEntity();
101
	if (!$container) {
102
		$fail();
103
	}
104
105
	$operator = elgg_comments_are_latest_first($container) ? '>' : '<';
106
107
	// this won't work with threaded comments, but core doesn't support that yet
108
	$condition = function(QueryBuilder $qb, $main_alias) use ($comment, $operator) {
109
		return $qb->compare("{$main_alias}.guid", $operator, $comment->guid, ELGG_VALUE_GUID);
110
	};
111
	$count = elgg_get_entities([
112
		'type' => 'object',
113
		'subtype' => 'comment',
114
		'container_guid' => $container->guid,
115
		'count' => true,
116
		'wheres' => [$condition],
117
	]);
118
	$limit = (int) get_input('limit');
119
	if (!$limit) {
120
		$limit = elgg_comments_per_page($container);
121
	}
122
	$offset = floor($count / $limit) * $limit;
123
	if (!$offset) {
124
		$offset = null;
125
	}
126
127
	$url = elgg_http_add_url_query_elements($container->getURL(), [
128
		'offset' => $offset,
129
	]);
130
	
131
	// make sure there's only one fragment (#)
132
	$parts = parse_url($url);
133
	$parts['fragment'] = "elgg-object-{$comment->guid}";
134
	$url = elgg_http_build_url($parts, false);
135
	
136
	forward($url);
137
}
138
139
/**
140
 * Allow users to comment on entities not owned by them.
141
 *
142
 * Object being commented on is used as the container of the comment so
143
 * permission check must be overridden if user isn't the owner of the object.
144
 *
145
 * @param string  $hook   'container_permissions_check'
146
 * @param string  $type   'object'
147
 * @param boolean $return Can the current user write to this container?
148
 * @param array   $params Array of parameters (container, user, subtype)
149
 *
150
 * @return array
151
 * @access private
152
 * @todo this doesn't seem to make a difference if a user can comment or not
153
 */
154
function _elgg_comments_container_permissions_override($hook, $type, $return, $params) {
155
156
	// is someone trying to comment, if so override permissions check
157 8
	if ($params['subtype'] === 'comment') {
158
		return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the documented return type array.
Loading history...
159
	}
160
161 8
	return $return;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $return returns the type boolean which is incompatible with the documented return type array.
Loading history...
162
}
163
164
/**
165
 * By default, only authors can edit their comments.
166
 *
167
 * @param string  $hook   'permissions_check'
168
 * @param string  $type   'object'
169
 * @param boolean $return Can the given user edit the given entity?
170
 * @param array   $params Array of parameters (entity, user)
171
 *
172
 * @return boolean Whether the given user is allowed to edit the given comment.
173
 * @access private
174
 */
175
function _elgg_comments_permissions_override($hook, $type, $return, $params) {
176 126
	$entity = $params['entity'];
177 126
	$user = $params['user'];
178
	
179 126
	if ($entity instanceof ElggComment && $user) {
180
		return $entity->getOwnerGUID() == $user->getGUID();
181
	}
182
	
183 126
	return $return;
184
}
185
186
/**
187
 * Set subject for email notifications about new ElggComment objects
188
 *
189
 * The "Re: " part is required by some email clients in order to properly
190
 * group the notifications in threads.
191
 *
192
 * Group discussion replies extend ElggComment objects so this takes care
193
 * of their notifications also.
194
 *
195
 * @param string $hook        'email'
196
 * @param string $type        'system'
197
 * @param array  $returnvalue Current mail parameters
198
 * @param array  $params      Original mail parameters
199
 * @return array $returnvalue Modified mail parameters
200
 * @access private
201
 */
202
function _elgg_comments_notification_email_subject($hook, $type, $returnvalue, $params) {
203
	if (!is_array($returnvalue) || !is_array($returnvalue['params'])) {
204
		// another hook handler returned a non-array, let's not override it
205
		return;
206
	}
207
208
	if (empty($returnvalue['params']['notification'])) {
209
		return;
210
	}
211
	
212
	/** @var Elgg\Notifications\Notification */
213
	$notification = $returnvalue['params']['notification'];
214
215
	if ($notification instanceof Elgg\Notifications\Notification) {
216
		$object = elgg_extract('object', $notification->params);
217
218
		if ($object instanceof ElggComment) {
219
			$container = $object->getContainerEntity();
220
221
			$returnvalue['subject'] = 'Re: ' . $container->getDisplayName();
222
		}
223
	}
224
225
	return $returnvalue;
226
}
227
228
/**
229
 * Update comment access to match that of the container
230
 *
231
 * @param string     $event  'update:after'
232
 * @param string     $type   'all'
233
 * @param ElggEntity $entity The updated entity
234
 * @return bool
235
 *
236
 * @access private
237
 */
238
function _elgg_comments_access_sync($event, $type, $entity) {
239 196
	if (!($entity instanceof \ElggEntity)) {
240 189
		return true;
241
	}
242
	
243
	// need to override access in case comments ended up with ACCESS_PRIVATE
244
	// and to ensure write permissions
245 136
	$ia = elgg_set_ignore_access(true);
246
	$options = [
247 136
		'type' => 'object',
248 136
		'subtype' => 'comment',
249 136
		'container_guid' => $entity->getGUID(),
250
		'wheres' => [function(\Elgg\Database\QueryBuilder $qb, $main_alias) use ($entity) {
251 136
			return $qb->compare("{$main_alias}.access_id", '!=', $entity->access_id, ELGG_VALUE_INTEGER);
252 136
		}],
253 136
		'limit' => 0,
254
	];
255
256 136
	$batch = new \ElggBatch('elgg_get_entities', $options, null, 25, false);
257 136
	foreach ($batch as $comment) {
258
		// Update comment access_id
259 1
		$comment->access_id = $entity->access_id;
260 1
		$comment->save();
261
	}
262
		
263 136
	elgg_set_ignore_access($ia);
264
	
265 136
	return true;
266
}
267
268
/**
269
 * Add the owner of the content being commented on to the subscribers
270
 *
271
 * @param string $hook        'get'
272
 * @param string $type        'subscribers'
273
 * @param array  $returnvalue current subscribers
274
 * @param array  $params      supplied params
275
 *
276
 * @return void|array
277
 *
278
 * @access private
279
 */
280
function _elgg_comments_add_content_owner_to_subscriptions($hook, $type, $returnvalue, $params) {
281
	
282 2
	$event = elgg_extract('event', $params);
283 2
	if (!$event instanceof \Elgg\Notifications\SubscriptionNotificationEvent) {
284 2
		return;
285
	}
286
	
287
	if ($event->getAction() !== 'create') {
288
		return;
289
	}
290
	
291
	$object = $event->getObject();
292
	if (!$object instanceof ElggComment) {
293
		return;
294
	}
295
	
296
	$content_owner = $object->getContainerEntity()->getOwnerEntity();
297
	if (!$content_owner instanceof ElggUser) {
298
		return;
299
	}
300
	
301
	$notification_settings = $content_owner->getNotificationSettings();
302
	if (empty($notification_settings)) {
303
		return;
304
	}
305
	
306
	$returnvalue[$content_owner->guid] = [];
307
	foreach ($notification_settings as $method => $enabled) {
308
		if (empty($enabled)) {
309
			continue;
310
		}
311
		
312
		$returnvalue[$content_owner->guid][] = $method;
313
	}
314
	
315
	return $returnvalue;
316
}
317
318
/**
319
 * Set the notification message for the owner of the content being commented on
320
 *
321
 * @param string                           $hook        'prepare'
322
 * @param string                           $type        'notification:create:object:comment'
323
 * @param \Elgg\Notifications\Notification $returnvalue current notification message
324
 * @param array                            $params      supplied params
325
 *
326
 * @return void|\Elgg\Notifications\Notification
327
 *
328
 * @access private
329
 */
330
function _elgg_comments_prepare_content_owner_notification($hook, $type, $returnvalue, $params) {
331
	
332
	$comment = elgg_extract('object', $params);
333
	if (!$comment instanceof ElggComment) {
334
		return;
335
	}
336
	
337
	/* @var $content \ElggEntity */
338
	$content = $comment->getContainerEntity();
339
	$recipient = elgg_extract('recipient', $params);
340
	if ($content->owner_guid !== $recipient->guid) {
341
		// not the content owner
342
		return;
343
	}
344
	
345
	$language = elgg_extract('language', $params);
346
	/* @var $commenter \ElggUser */
347
	$commenter = $comment->getOwnerEntity();
348
	
349
	$returnvalue->subject = elgg_echo('generic_comment:notification:owner:subject', [], $language);
350
	$returnvalue->summary = elgg_echo('generic_comment:notification:owner:summary', [], $language);
351
	$returnvalue->body = elgg_echo('generic_comment:notification:owner:body', [
352
		$content->getDisplayName(),
353
		$commenter->getDisplayName(),
354
		$comment->description,
355
		$comment->getURL(),
356
		$commenter->getDisplayName(),
357
		$commenter->getURL(),
358
	], $language);
359
	
360
	return $returnvalue;
361
}
362
363
/**
364
 * Set the notification message for interested users
365
 *
366
 * @param string                           $hook        'prepare'
367
 * @param string                           $type        'notification:create:object:comment'
368
 * @param \Elgg\Notifications\Notification $returnvalue current notification message
369
 * @param array                            $params      supplied params
370
 *
371
 * @return void|\Elgg\Notifications\Notification
372
 *
373
 * @access private
374
 */
375
function _elgg_comments_prepare_notification($hook, $type, $returnvalue, $params) {
376
	
377
	$comment = elgg_extract('object', $params);
378
	if (!$comment instanceof ElggComment) {
379
		return;
380
	}
381
	
382
	/* @var $content \ElggEntity */
383
	$content = $comment->getContainerEntity();
384
	$recipient = elgg_extract('recipient', $params);
385
	if ($content->getOwnerGUID() === $recipient->getGUID()) {
386
		// the content owner, this is handled in other hook
387
		return;
388
	}
389
	
390
	$language = elgg_extract('language', $params);
391
	/* @var $commenter \ElggUser */
392
	$commenter = $comment->getOwnerEntity();
393
	
394
	$returnvalue->subject = elgg_echo('generic_comment:notification:user:subject', [$content->getDisplayName()], $language);
395
	$returnvalue->summary = elgg_echo('generic_comment:notification:user:summary', [$content->getDisplayName()], $language);
396
	$returnvalue->body = elgg_echo('generic_comment:notification:user:body', [
397
		$content->getDisplayName(),
398
		$commenter->getDisplayName(),
399
		$comment->description,
400
		$comment->getURL(),
401
		$commenter->getDisplayName(),
402
		$commenter->getURL(),
403
	], $language);
404
405
	$returnvalue->url = $comment->getURL();
406
	
407
	return $returnvalue;
408
}
409
410
/**
411
 * Adds comment menu items to entity menu
412
 *
413
 * @param \Elgg\Hook $hook Hook information
414
 *
415
 * @return void|\ElggMenuItem[]
416
 *
417
 * @access private
418
 * @since 3.0
419
 */
420
function _elgg_comments_social_menu_setup(\Elgg\Hook $hook) {
421 1
	$entity = $hook->getEntityParam();
422 1
	if (!$entity) {
423
		return;
424
	}
425
	
426 1
	$return = $hook->getValue();
427
	
428 1
	$comment_count = $entity->countComments();
429 1
	$can_comment = $entity->canComment();
430 1
	if ($can_comment || $comment_count) {
431
		$text = $can_comment ? elgg_echo('comment:this') : elgg_echo('comments');
432
		
433
		$options = [
434
			'name' => 'comment',
435
			'icon' => 'speech-bubble',
436
			'badge' => $comment_count ?: null,
437
			'text' => $text,
438
			'title' => $text,
439
			'href' => $entity->getURL() . '#comments',
440
		];
441
				
442
		$item = $hook->getParam('item');
443
		if ($item && $can_comment) {
444
			$options['href'] = "#comments-add-{$entity->guid}-{$item->id}";
445
			$options['rel'] = 'toggle';
446
		}
447
		
448
		$return[] = \ElggMenuItem::factory($options);
449
	}
450
	
451 1
	return $return;
452
}
453
454
/**
455
 * Runs unit tests for \ElggComment
456
 *
457
 * @param string $hook   unit_test
458
 * @param string $type   system
459
 * @param mixed  $value  Array of tests
460
 * @param mixed  $params Params
461
 *
462
 * @return array
463
 * @access private
464
 * @codeCoverageIgnore
465
 */
466
function _elgg_comments_test($hook, $type, $value, $params) {
467
	$value[] = ElggCoreCommentTest::class;
468
	return $value;
469
}
470
471
/**
472
 * @see \Elgg\Application::loadCore Do not do work here. Just register for events.
473
 */
474
return function(\Elgg\EventsService $events, \Elgg\HooksRegistrationService $hooks) {
475 64
	$events->registerHandler('init', 'system', '_elgg_comments_init');
476 64
	$hooks->registerHandler('unit_test', 'system', '_elgg_comments_test');
477
};
478