@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * @extends PhabricatorCursorPagedPolicyAwareQuery<PhrictionDocument>
5 */
6final class PhrictionDocumentQuery
7 extends PhabricatorCursorPagedPolicyAwareQuery {
8
9 private $ids;
10 private $phids;
11 private $slugs;
12 private $depths;
13 private $slugPrefix;
14 private $statuses;
15
16 private $parentPaths;
17 private $ancestorPaths;
18
19 private $needContent;
20
21 const ORDER_HIERARCHY = 'hierarchy';
22
23 public function withIDs(array $ids) {
24 $this->ids = $ids;
25 return $this;
26 }
27
28 public function withPHIDs(array $phids) {
29 $this->phids = $phids;
30 return $this;
31 }
32
33 public function withSlugs(array $slugs) {
34 $this->slugs = $slugs;
35 return $this;
36 }
37
38 public function withDepths(array $depths) {
39 $this->depths = $depths;
40 return $this;
41 }
42
43 public function withSlugPrefix($slug_prefix) {
44 $this->slugPrefix = $slug_prefix;
45 return $this;
46 }
47
48 public function withStatuses(array $statuses) {
49 $this->statuses = $statuses;
50 return $this;
51 }
52
53 public function withParentPaths(array $paths) {
54 $this->parentPaths = $paths;
55 return $this;
56 }
57
58 public function withAncestorPaths(array $paths) {
59 $this->ancestorPaths = $paths;
60 return $this;
61 }
62
63 public function needContent($need_content) {
64 $this->needContent = $need_content;
65 return $this;
66 }
67
68 public function newResultObject() {
69 return new PhrictionDocument();
70 }
71
72 protected function willFilterPage(array $documents) {
73
74 if ($documents) {
75 $ancestor_slugs = array();
76 foreach ($documents as $key => $document) {
77 $document_slug = $document->getSlug();
78 foreach (PhabricatorSlug::getAncestry($document_slug) as $ancestor) {
79 $ancestor_slugs[$ancestor][] = $key;
80 }
81 }
82
83 if ($ancestor_slugs) {
84 $table = new PhrictionDocument();
85 $conn_r = $table->establishConnection('r');
86 $ancestors = queryfx_all(
87 $conn_r,
88 'SELECT * FROM %T WHERE slug IN (%Ls)',
89 $document->getTableName(),
90 array_keys($ancestor_slugs));
91 $ancestors = $table->loadAllFromArray($ancestors);
92 $ancestors = mpull($ancestors, null, 'getSlug');
93
94 foreach ($ancestor_slugs as $ancestor_slug => $document_keys) {
95 $ancestor = idx($ancestors, $ancestor_slug);
96 foreach ($document_keys as $document_key) {
97 $documents[$document_key]->attachAncestor(
98 $ancestor_slug,
99 $ancestor);
100 }
101 }
102 }
103 }
104 // To view a Phriction document, you must also be able to view all of the
105 // ancestor documents. Filter out documents which have ancestors that are
106 // not visible.
107
108 $document_map = array();
109 foreach ($documents as $document) {
110 $document_map[$document->getSlug()] = $document;
111 foreach ($document->getAncestors() as $key => $ancestor) {
112 if ($ancestor) {
113 $document_map[$key] = $ancestor;
114 }
115 }
116 }
117
118 $filtered_map = $this->applyPolicyFilter(
119 $document_map,
120 array(PhabricatorPolicyCapability::CAN_VIEW));
121
122 // Filter all of the documents where a parent is not visible.
123 foreach ($documents as $document_key => $document) {
124 // If the document itself is not visible, filter it.
125 if (!isset($filtered_map[$document->getSlug()])) {
126 $this->didRejectResult($documents[$document_key]);
127 unset($documents[$document_key]);
128 continue;
129 }
130
131 // If an ancestor exists but is not visible, filter the document.
132 foreach ($document->getAncestors() as $ancestor_key => $ancestor) {
133 if (!$ancestor) {
134 continue;
135 }
136
137 if (!isset($filtered_map[$ancestor_key])) {
138 $this->didRejectResult($documents[$document_key]);
139 unset($documents[$document_key]);
140 break;
141 }
142 }
143 }
144
145 if (!$documents) {
146 return $documents;
147 }
148
149 if ($this->needContent) {
150 $contents = id(new PhrictionContentQuery())
151 ->setViewer($this->getViewer())
152 ->setParentQuery($this)
153 ->withPHIDs(mpull($documents, 'getContentPHID'))
154 ->execute();
155 $contents = mpull($contents, null, 'getPHID');
156
157 foreach ($documents as $key => $document) {
158 $content_phid = $document->getContentPHID();
159 if (empty($contents[$content_phid])) {
160 unset($documents[$key]);
161 continue;
162 }
163 $document->attachContent($contents[$content_phid]);
164 }
165 }
166
167 return $documents;
168 }
169
170 protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
171 $select = parent::buildSelectClauseParts($conn);
172
173 if ($this->shouldJoinContentTable()) {
174 $select[] = qsprintf($conn, 'c.title');
175 }
176
177 return $select;
178 }
179
180 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
181 $joins = parent::buildJoinClauseParts($conn);
182
183 if ($this->shouldJoinContentTable()) {
184 $content_dao = new PhrictionContent();
185 $joins[] = qsprintf(
186 $conn,
187 'JOIN %T c ON d.contentPHID = c.phid',
188 $content_dao->getTableName());
189 }
190
191 return $joins;
192 }
193
194 private function shouldJoinContentTable() {
195 if ($this->getOrderVector()->containsKey('title')) {
196 return true;
197 }
198
199 return false;
200 }
201
202 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
203 $where = parent::buildWhereClauseParts($conn);
204
205 if ($this->ids !== null) {
206 $where[] = qsprintf(
207 $conn,
208 'd.id IN (%Ld)',
209 $this->ids);
210 }
211
212 if ($this->phids !== null) {
213 $where[] = qsprintf(
214 $conn,
215 'd.phid IN (%Ls)',
216 $this->phids);
217 }
218
219 if ($this->slugs !== null) {
220 $where[] = qsprintf(
221 $conn,
222 'd.slug IN (%Ls)',
223 $this->slugs);
224 }
225
226 if ($this->statuses !== null) {
227 $where[] = qsprintf(
228 $conn,
229 'd.status IN (%Ls)',
230 $this->statuses);
231 }
232
233 if ($this->slugPrefix !== null) {
234 $where[] = qsprintf(
235 $conn,
236 'd.slug LIKE %>',
237 $this->slugPrefix);
238 }
239
240 if ($this->depths !== null) {
241 $where[] = qsprintf(
242 $conn,
243 'd.depth IN (%Ld)',
244 $this->depths);
245 }
246
247 if ($this->parentPaths !== null || $this->ancestorPaths !== null) {
248 $sets = array(
249 array(
250 'paths' => $this->parentPaths,
251 'parents' => true,
252 ),
253 array(
254 'paths' => $this->ancestorPaths,
255 'parents' => false,
256 ),
257 );
258
259 $paths = array();
260 foreach ($sets as $set) {
261 $set_paths = $set['paths'];
262 if ($set_paths === null) {
263 continue;
264 }
265
266 if (!$set_paths) {
267 throw new PhabricatorEmptyQueryException(
268 pht('No parent/ancestor paths specified.'));
269 }
270
271 $is_parents = $set['parents'];
272 foreach ($set_paths as $path) {
273 $path_normal = PhabricatorSlug::normalize($path);
274 if ($path !== $path_normal) {
275 throw new Exception(
276 pht(
277 'Document path "%s" is not a valid path. The normalized '.
278 'form of this path is "%s".',
279 $path,
280 $path_normal));
281 }
282
283 $depth = PhabricatorSlug::getDepth($path_normal);
284 if ($is_parents) {
285 $min_depth = $depth + 1;
286 $max_depth = $depth + 1;
287 } else {
288 $min_depth = $depth + 1;
289 $max_depth = null;
290 }
291
292 $paths[] = array(
293 $path_normal,
294 $min_depth,
295 $max_depth,
296 );
297 }
298 }
299
300 $path_clauses = array();
301 foreach ($paths as $path) {
302 $parts = array();
303 list($prefix, $min, $max) = $path;
304
305 // If we're getting children or ancestors of the root document, they
306 // aren't actually stored with the leading "/" in the database, so
307 // just skip this part of the clause.
308 if ($prefix !== '/') {
309 $parts[] = qsprintf(
310 $conn,
311 'd.slug LIKE %>',
312 $prefix);
313 }
314
315 if ($min !== null) {
316 $parts[] = qsprintf(
317 $conn,
318 'd.depth >= %d',
319 $min);
320 }
321
322 if ($max !== null) {
323 $parts[] = qsprintf(
324 $conn,
325 'd.depth <= %d',
326 $max);
327 }
328
329 if ($parts) {
330 $path_clauses[] = qsprintf($conn, '%LA', $parts);
331 }
332 }
333
334 if ($path_clauses) {
335 $where[] = qsprintf($conn, '%LO', $path_clauses);
336 }
337 }
338
339 return $where;
340 }
341
342 public function getBuiltinOrders() {
343 return parent::getBuiltinOrders() + array(
344 self::ORDER_HIERARCHY => array(
345 'vector' => array('depth', 'title', 'updated', 'id'),
346 'name' => pht('Hierarchy'),
347 ),
348 );
349 }
350
351 public function getOrderableColumns() {
352 return parent::getOrderableColumns() + array(
353 'depth' => array(
354 'table' => 'd',
355 'column' => 'depth',
356 'reverse' => true,
357 'type' => 'int',
358 ),
359 'title' => array(
360 'table' => 'c',
361 'column' => 'title',
362 'reverse' => true,
363 'type' => 'string',
364 ),
365 'updated' => array(
366 'table' => 'd',
367 'column' => 'editedEpoch',
368 'type' => 'int',
369 'unique' => false,
370 ),
371 );
372 }
373
374 protected function newPagingMapFromCursorObject(
375 PhabricatorQueryCursor $cursor,
376 array $keys) {
377
378 $document = $cursor->getObject();
379
380 $map = array(
381 'id' => (int)$document->getID(),
382 'depth' => $document->getDepth(),
383 'updated' => (int)$document->getEditedEpoch(),
384 );
385
386 if (isset($keys['title'])) {
387 $map['title'] = $cursor->getRawRowProperty('title');
388 }
389
390 return $map;
391 }
392
393 protected function getPrimaryTableAlias() {
394 return 'd';
395 }
396
397 public function getQueryApplicationClass() {
398 return PhabricatorPhrictionApplication::class;
399 }
400
401}