Directory.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. <?php
  2. declare(strict_types = 1);
  3. // {{{ License
  4. // This file is part of GNU social - https://www.gnu.org/software/social
  5. //
  6. // GNU social is free software: you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // GNU social is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  18. // }}}
  19. namespace Plugin\Directory\Controller;
  20. use App\Core\DB\DB;
  21. use function App\Core\I18n\_m;
  22. use App\Entity\Actor;
  23. use App\Util\Common;
  24. use App\Util\Exception\BugFoundException;
  25. use App\Util\Exception\ClientException;
  26. use Component\Collection\Util\Controller\CircleController;
  27. use Symfony\Component\HttpFoundation\Request;
  28. class Directory extends CircleController
  29. {
  30. public const ALLOWED_FIELDS = ['nickname', 'created', 'modified', 'activity', 'subscribers'];
  31. /**
  32. * Function responsible for displaying a list of actors of a given
  33. * $actor_type, sorted by the `order_by` GET parameter, if given
  34. */
  35. private function impl(Request $request, int $actor_type, string $title, string $empty_message): array
  36. {
  37. if ($actor_type !== Actor::PERSON && $actor_type !== Actor::GROUP) {
  38. throw new BugFoundException("Unimplemented for actor type: {$actor_type}");
  39. }
  40. $page = $this->int('page') ?? 1;
  41. $limit = Common::config('feeds', 'entries_per_page');
  42. $offset = $limit * ($page - 1);
  43. // -------- Figure out the order by field and operator --------
  44. $order_by_qs = $this->string('order_by');
  45. if (!\is_null($order_by_qs) && mb_detect_encoding($order_by_qs, 'ASCII', strict: true) !== false) {
  46. $order_by_op = mb_substr($order_by_qs, -1);
  47. if (\in_array($order_by_op, ['^', '<'])) {
  48. $order_by_field = mb_substr($order_by_qs, 0, -1);
  49. $order_by_op = 'DESC';
  50. } elseif (\in_array($order_by_op, ['v', '>'])) {
  51. $order_by_field = mb_substr($order_by_qs, 0, -1);
  52. $order_by_op = 'ASC';
  53. } else {
  54. $order_by_field = $order_by_qs;
  55. $order_by_op = match ($this->string('order_op')) {
  56. 'ASC' => 'ASC',
  57. 'DESC' => 'DESC',
  58. default => 'ASC',
  59. };
  60. }
  61. if (!\in_array($order_by_field, self::ALLOWED_FIELDS)) {
  62. throw new ClientException(_m('Invalid order by given: {order_by}', ['{order_by}' => $order_by_field]));
  63. }
  64. } else {
  65. $order_by_field = 'nickname';
  66. $order_by_op = 'ASC';
  67. }
  68. $order_by = [$order_by_field => $order_by_op];
  69. // -------- *** --------
  70. // -------- Query builder for selecting actors joined with another table, namely activity and group_inbox --------
  71. $general_query_fn_fn = function (string $func, string $order) use ($limit, $offset) {
  72. return fn (string $table, string $join_field, string $aggregate_field) => fn (int $actor_type) => DB::sql(
  73. <<<EOQ
  74. select {select}
  75. from actor actr
  76. join (
  77. select tbl.{$join_field}, {$func}(tbl.{$aggregate_field}) as aggr
  78. from {$table} tbl
  79. group by tbl.{$join_field}
  80. ) actor_activity on actr.id = actor_activity.{$join_field}
  81. where actr.type = :type
  82. order by actor_activity.aggr {$order}
  83. limit :limit offset :offset
  84. EOQ,
  85. [
  86. 'type' => $actor_type,
  87. 'limit' => $limit,
  88. 'offset' => $offset,
  89. ],
  90. ['actr' => Actor::class],
  91. );
  92. };
  93. // -------- *** --------
  94. // -------- Start setting up the queries --------
  95. $actor_query_fn = fn (int $actor_type) => DB::findBy(Actor::class, ['type' => $actor_type], order_by: $order_by, limit: $limit, offset: $offset);
  96. $minmax_query_fn = $general_query_fn_fn(func: $order_by_op === 'ASC' ? 'MAX' : 'MIN', order: $order_by_op);
  97. $count_query_fn = $general_query_fn_fn(func: 'COUNT', order: $order_by_op);
  98. // -------- *** --------
  99. // -------- Figure out the final query --------
  100. $query_fn = match ($order_by_field) {
  101. 'nickname', 'created' => $actor_query_fn, // select only from actors
  102. 'modified' => match ($actor_type) { // select by most/least recent activity
  103. Actor::PERSON => $minmax_query_fn(table: 'activity', join_field: 'actor_id', aggregate_field: 'created'),
  104. Actor::GROUP => $minmax_query_fn(table: 'group_inbox', join_field: 'group_id', aggregate_field: 'created'),
  105. },
  106. 'activity' => match ($actor_type) { // select by most/least activity amount
  107. Actor::PERSON => $count_query_fn(table: 'activity', join_field: 'actor_id', aggregate_field: 'created'),
  108. Actor::GROUP => $count_query_fn(table: 'group_inbox', join_field: 'group_id', aggregate_field: 'created'),
  109. },
  110. 'subscribers' => match ($actor_type) { // select by actors with most/least subscribers/members
  111. Actor::PERSON => $count_query_fn(table: 'subscription', join_field: 'subscribed_id', aggregate_field: 'subscriber_id'),
  112. Actor::GROUP => $count_query_fn(table: 'group_member', join_field: 'group_id', aggregate_field: 'actor_id'),
  113. },
  114. default => throw new BugFoundException("Unkown order by found, but should have been validated: {$order_by_field}"),
  115. };
  116. // -------- *** --------
  117. $sort_form_fields = [];
  118. foreach (self::ALLOWED_FIELDS as $al) {
  119. $sort_form_fields[] = [
  120. 'checked' => $order_by_field === $al,
  121. 'value' => $al,
  122. 'label' => _m(ucfirst($al)),
  123. ];
  124. }
  125. return [
  126. '_template' => 'collection/actors.html.twig',
  127. 'actors' => $query_fn($actor_type),
  128. 'title' => $title,
  129. 'empty_message' => $empty_message,
  130. 'sort_form_fields' => $sort_form_fields,
  131. 'page' => $page,
  132. ];
  133. }
  134. public function people(Request $request): array
  135. {
  136. return $this->impl($request, Actor::PERSON, title: _m('People'), empty_message: _m('No people here'));
  137. }
  138. public function groups(Request $request): array
  139. {
  140. return $this->impl($request, Actor::GROUP, title: _m('Groups'), empty_message: _m('No groups here'));
  141. }
  142. }