Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php
12256 views
1
<?php
2
3
final class HarbormasterBuildLogRenderController
4
extends HarbormasterController {
5
6
public function shouldAllowPublic() {
7
return true;
8
}
9
10
public function handleRequest(AphrontRequest $request) {
11
$viewer = $this->getViewer();
12
13
$id = $request->getURIData('id');
14
15
$log = id(new HarbormasterBuildLogQuery())
16
->setViewer($viewer)
17
->withIDs(array($id))
18
->executeOne();
19
if (!$log) {
20
return new Aphront404Response();
21
}
22
23
$highlight_range = $request->getURILineRange('lines', 1000);
24
25
$log_size = $this->getTotalByteLength($log);
26
27
$head_lines = $request->getInt('head');
28
if ($head_lines === null) {
29
$head_lines = 8;
30
}
31
$head_lines = min($head_lines, 1024);
32
$head_lines = max($head_lines, 0);
33
34
$tail_lines = $request->getInt('tail');
35
if ($tail_lines === null) {
36
$tail_lines = 16;
37
}
38
$tail_lines = min($tail_lines, 1024);
39
$tail_lines = max($tail_lines, 0);
40
41
$head_offset = $request->getInt('headOffset');
42
if ($head_offset === null) {
43
$head_offset = 0;
44
}
45
46
$tail_offset = $request->getInt('tailOffset');
47
if ($tail_offset === null) {
48
$tail_offset = $log_size;
49
}
50
51
// Figure out which ranges we're actually going to read. We'll read either
52
// one range (either just at the head, or just at the tail) or two ranges
53
// (one at the head and one at the tail).
54
55
// This gets a little bit tricky because: the ranges may overlap; we just
56
// want to do one big read if there is only a little bit of text left
57
// between the ranges; we may not know where the tail range ends; and we
58
// can only read forward from line map markers, not from any arbitrary
59
// position in the file.
60
61
$bytes_per_line = 140;
62
$body_lines = 8;
63
64
$views = array();
65
if ($head_lines > 0) {
66
$views[] = array(
67
'offset' => $head_offset,
68
'lines' => $head_lines,
69
'direction' => 1,
70
'limit' => $tail_offset,
71
);
72
}
73
74
if ($highlight_range) {
75
$highlight_views = $this->getHighlightViews(
76
$log,
77
$highlight_range,
78
$log_size);
79
foreach ($highlight_views as $highlight_view) {
80
$views[] = $highlight_view;
81
}
82
}
83
84
if ($tail_lines > 0) {
85
$views[] = array(
86
'offset' => $tail_offset,
87
'lines' => $tail_lines,
88
'direction' => -1,
89
'limit' => $head_offset,
90
);
91
}
92
93
$reads = $views;
94
foreach ($reads as $key => $read) {
95
$offset = $read['offset'];
96
97
$lines = $read['lines'];
98
99
$read_length = 0;
100
$read_length += ($lines * $bytes_per_line);
101
$read_length += ($body_lines * $bytes_per_line);
102
103
$direction = $read['direction'];
104
if ($direction < 0) {
105
if ($offset > $read_length) {
106
$offset -= $read_length;
107
} else {
108
$read_length = $offset;
109
$offset = 0;
110
}
111
}
112
113
$position = $log->getReadPosition($offset);
114
list($position_offset, $position_line) = $position;
115
$read_length += ($offset - $position_offset);
116
117
$reads[$key]['fetchOffset'] = $position_offset;
118
$reads[$key]['fetchLength'] = $read_length;
119
$reads[$key]['fetchLine'] = $position_line;
120
}
121
122
$reads = $this->mergeOverlappingReads($reads);
123
124
foreach ($reads as $key => $read) {
125
$fetch_offset = $read['fetchOffset'];
126
$fetch_length = $read['fetchLength'];
127
if ($fetch_offset + $fetch_length > $log_size) {
128
$fetch_length = $log_size - $fetch_offset;
129
}
130
131
$data = $log->loadData($fetch_offset, $fetch_length);
132
133
$offset = $read['fetchOffset'];
134
$line = $read['fetchLine'];
135
$lines = $this->getLines($data);
136
$line_data = array();
137
foreach ($lines as $line_text) {
138
$length = strlen($line_text);
139
$line_data[] = array(
140
'offset' => $offset,
141
'length' => $length,
142
'line' => $line,
143
'data' => $line_text,
144
);
145
$line += 1;
146
$offset += $length;
147
}
148
149
$reads[$key]['data'] = $data;
150
$reads[$key]['lines'] = $line_data;
151
}
152
153
foreach ($views as $view_key => $view) {
154
$anchor_byte = $view['offset'];
155
156
if ($view['direction'] < 0) {
157
$anchor_byte = $anchor_byte - 1;
158
}
159
160
$data_key = null;
161
foreach ($reads as $read_key => $read) {
162
$s = $read['fetchOffset'];
163
$e = $s + $read['fetchLength'];
164
165
if (($s <= $anchor_byte) && ($e >= $anchor_byte)) {
166
$data_key = $read_key;
167
break;
168
}
169
}
170
171
if ($data_key === null) {
172
throw new Exception(
173
pht('Unable to find fetch!'));
174
}
175
176
$anchor_key = null;
177
foreach ($reads[$data_key]['lines'] as $line_key => $line) {
178
$s = $line['offset'];
179
$e = $s + $line['length'];
180
181
if (($s <= $anchor_byte) && ($e > $anchor_byte)) {
182
$anchor_key = $line_key;
183
break;
184
}
185
}
186
187
if ($anchor_key === null) {
188
throw new Exception(
189
pht(
190
'Unable to find lines.'));
191
}
192
193
if ($view['direction'] > 0) {
194
$slice_offset = $anchor_key;
195
} else {
196
$slice_offset = max(0, $anchor_key - ($view['lines'] - 1));
197
}
198
$slice_length = $view['lines'];
199
200
$views[$view_key] += array(
201
'sliceKey' => $data_key,
202
'sliceOffset' => $slice_offset,
203
'sliceLength' => $slice_length,
204
);
205
}
206
207
foreach ($views as $view_key => $view) {
208
$slice_key = $view['sliceKey'];
209
$lines = array_slice(
210
$reads[$slice_key]['lines'],
211
$view['sliceOffset'],
212
$view['sliceLength']);
213
214
$data_offset = null;
215
$data_length = null;
216
foreach ($lines as $line) {
217
if ($data_offset === null) {
218
$data_offset = $line['offset'];
219
}
220
$data_length += $line['length'];
221
}
222
223
// If the view cursor starts in the middle of a line, we're going to
224
// strip part of the line.
225
$direction = $view['direction'];
226
if ($direction > 0) {
227
$view_offset = $view['offset'];
228
$view_length = $data_length;
229
if ($data_offset < $view_offset) {
230
$trim = ($view_offset - $data_offset);
231
$view_length -= $trim;
232
}
233
234
$limit = $view['limit'];
235
if ($limit !== null) {
236
if ($limit < ($view_offset + $view_length)) {
237
$view_length = ($limit - $view_offset);
238
}
239
}
240
} else {
241
$view_offset = $data_offset;
242
$view_length = $data_length;
243
if ($data_offset + $data_length > $view['offset']) {
244
$view_length -= (($data_offset + $data_length) - $view['offset']);
245
}
246
247
$limit = $view['limit'];
248
if ($limit !== null) {
249
if ($limit > $view_offset) {
250
$view_length -= ($limit - $view_offset);
251
$view_offset = $limit;
252
}
253
}
254
}
255
256
$views[$view_key] += array(
257
'viewOffset' => $view_offset,
258
'viewLength' => $view_length,
259
);
260
}
261
262
$views = $this->mergeOverlappingViews($views);
263
264
foreach ($views as $view_key => $view) {
265
$slice_key = $view['sliceKey'];
266
$lines = array_slice(
267
$reads[$slice_key]['lines'],
268
$view['sliceOffset'],
269
$view['sliceLength']);
270
271
$view_offset = $view['viewOffset'];
272
foreach ($lines as $line_key => $line) {
273
$line_offset = $line['offset'];
274
275
if ($line_offset >= $view_offset) {
276
break;
277
}
278
279
$trim = ($view_offset - $line_offset);
280
if ($trim && ($trim >= strlen($line['data']))) {
281
unset($lines[$line_key]);
282
continue;
283
}
284
285
$line_data = substr($line['data'], $trim);
286
$lines[$line_key]['data'] = $line_data;
287
$lines[$line_key]['length'] = strlen($line_data);
288
$lines[$line_key]['offset'] += $trim;
289
break;
290
}
291
292
$view_end = $view['viewOffset'] + $view['viewLength'];
293
foreach ($lines as $line_key => $line) {
294
$line_end = $line['offset'] + $line['length'];
295
if ($line_end <= $view_end) {
296
continue;
297
}
298
299
$trim = ($line_end - $view_end);
300
if ($trim && ($trim >= strlen($line['data']))) {
301
unset($lines[$line_key]);
302
continue;
303
}
304
305
$line_data = substr($line['data'], -$trim);
306
$lines[$line_key]['data'] = $line_data;
307
$lines[$line_key]['length'] = strlen($line_data);
308
}
309
310
$views[$view_key]['viewData'] = $lines;
311
}
312
313
$spacer = null;
314
$render = array();
315
316
$head_view = head($views);
317
if ($head_view['viewOffset'] > $head_offset) {
318
$render[] = array(
319
'spacer' => true,
320
'head' => $head_offset,
321
'tail' => $head_view['viewOffset'],
322
);
323
}
324
325
foreach ($views as $view) {
326
if ($spacer) {
327
$spacer['tail'] = $view['viewOffset'];
328
$render[] = $spacer;
329
}
330
331
$render[] = $view;
332
333
$spacer = array(
334
'spacer' => true,
335
'head' => ($view['viewOffset'] + $view['viewLength']),
336
);
337
}
338
339
$tail_view = last($views);
340
if ($tail_view['viewOffset'] + $tail_view['viewLength'] < $tail_offset) {
341
$render[] = array(
342
'spacer' => true,
343
'head' => $tail_view['viewOffset'] + $tail_view['viewLength'],
344
'tail' => $tail_offset,
345
);
346
}
347
348
$uri = $log->getURI();
349
350
$rows = array();
351
foreach ($render as $range) {
352
if (isset($range['spacer'])) {
353
$rows[] = $this->renderExpandRow($range);
354
continue;
355
}
356
357
$lines = $range['viewData'];
358
foreach ($lines as $line) {
359
$display_line = ($line['line'] + 1);
360
$display_text = ($line['data']);
361
362
$row_attr = array();
363
if ($highlight_range) {
364
if (($display_line >= $highlight_range[0]) &&
365
($display_line <= $highlight_range[1])) {
366
$row_attr = array(
367
'class' => 'phabricator-source-highlight',
368
);
369
}
370
}
371
372
$display_line = phutil_tag(
373
'a',
374
array(
375
'href' => $uri.'$'.$display_line,
376
'data-n' => $display_line,
377
),
378
'');
379
380
$line_cell = phutil_tag('th', array(), $display_line);
381
$text_cell = phutil_tag('td', array(), $display_text);
382
383
$rows[] = phutil_tag(
384
'tr',
385
$row_attr,
386
array(
387
$line_cell,
388
$text_cell,
389
));
390
}
391
}
392
393
if ($log->getLive()) {
394
$last_view = last($views);
395
$last_line = last($last_view['viewData']);
396
if ($last_line) {
397
$last_offset = $last_line['offset'];
398
} else {
399
$last_offset = 0;
400
}
401
402
$last_tail = $last_view['viewOffset'] + $last_view['viewLength'];
403
$show_live = ($last_tail === $log_size);
404
if ($show_live) {
405
$rows[] = $this->renderLiveRow($last_offset);
406
}
407
}
408
409
$table = javelin_tag(
410
'table',
411
array(
412
'class' => 'harbormaster-log-table PhabricatorMonospaced',
413
'sigil' => 'phabricator-source',
414
'meta' => array(
415
'uri' => $log->getURI(),
416
),
417
),
418
$rows);
419
420
// When this is a normal AJAX request, return the rendered log fragment
421
// in an AJAX payload.
422
if ($request->isAjax()) {
423
return id(new AphrontAjaxResponse())
424
->setContent(
425
array(
426
'markup' => hsprintf('%s', $table),
427
));
428
}
429
430
// If the page is being accessed as a standalone page, present a
431
// readable version of the fragment for debugging.
432
433
require_celerity_resource('harbormaster-css');
434
435
$header = pht('Standalone Log Fragment');
436
437
$render_view = id(new PHUIObjectBoxView())
438
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
439
->setHeaderText($header)
440
->appendChild($table);
441
442
$page_view = id(new PHUITwoColumnView())
443
->setFooter($render_view);
444
445
$crumbs = $this->buildApplicationCrumbs()
446
->addTextCrumb(pht('Build Log %d', $log->getID()), $log->getURI())
447
->addTextCrumb(pht('Fragment'))
448
->setBorder(true);
449
450
return $this->newPage()
451
->setTitle(
452
array(
453
pht('Build Log %d', $log->getID()),
454
pht('Standalone Fragment'),
455
))
456
->setCrumbs($crumbs)
457
->appendChild($page_view);
458
}
459
460
private function getTotalByteLength(HarbormasterBuildLog $log) {
461
$total_bytes = $log->getByteLength();
462
if ($total_bytes) {
463
return (int)$total_bytes;
464
}
465
466
// TODO: Remove this after enough time has passed for installs to run
467
// log rebuilds or decide they don't care about older logs.
468
469
// Older logs don't have this data denormalized onto the log record unless
470
// an administrator has run `bin/harbormaster rebuild-log --all` or
471
// similar. Try to figure it out by summing up the size of each chunk.
472
473
// Note that the log may also be legitimately empty and have actual size
474
// zero.
475
$chunk = new HarbormasterBuildLogChunk();
476
$conn = $chunk->establishConnection('r');
477
478
$row = queryfx_one(
479
$conn,
480
'SELECT SUM(size) total FROM %T WHERE logID = %d',
481
$chunk->getTableName(),
482
$log->getID());
483
484
return (int)$row['total'];
485
}
486
487
private function getLines($data) {
488
$parts = preg_split("/(\r\n|\r|\n)/", $data, 0, PREG_SPLIT_DELIM_CAPTURE);
489
490
if (last($parts) === '') {
491
array_pop($parts);
492
}
493
494
$lines = array();
495
for ($ii = 0; $ii < count($parts); $ii += 2) {
496
$line = $parts[$ii];
497
if (isset($parts[$ii + 1])) {
498
$line .= $parts[$ii + 1];
499
}
500
$lines[] = $line;
501
}
502
503
return $lines;
504
}
505
506
507
private function mergeOverlappingReads(array $reads) {
508
// Find planned reads which will overlap and merge them into a single
509
// larger read.
510
511
$uk = array_keys($reads);
512
$vk = array_keys($reads);
513
514
foreach ($uk as $ukey) {
515
foreach ($vk as $vkey) {
516
// Don't merge a range into itself, even though they do technically
517
// overlap.
518
if ($ukey === $vkey) {
519
continue;
520
}
521
522
$uread = idx($reads, $ukey);
523
if ($uread === null) {
524
continue;
525
}
526
527
$vread = idx($reads, $vkey);
528
if ($vread === null) {
529
continue;
530
}
531
532
$us = $uread['fetchOffset'];
533
$ue = $us + $uread['fetchLength'];
534
535
$vs = $vread['fetchOffset'];
536
$ve = $vs + $vread['fetchLength'];
537
538
if (($vs > $ue) || ($ve < $us)) {
539
continue;
540
}
541
542
$min = min($us, $vs);
543
$max = max($ue, $ve);
544
545
$reads[$ukey]['fetchOffset'] = $min;
546
$reads[$ukey]['fetchLength'] = ($max - $min);
547
$reads[$ukey]['fetchLine'] = min(
548
$uread['fetchLine'],
549
$vread['fetchLine']);
550
551
unset($reads[$vkey]);
552
}
553
}
554
555
return $reads;
556
}
557
558
private function mergeOverlappingViews(array $views) {
559
$uk = array_keys($views);
560
$vk = array_keys($views);
561
562
$body_lines = 8;
563
$body_bytes = ($body_lines * 140);
564
565
foreach ($uk as $ukey) {
566
foreach ($vk as $vkey) {
567
if ($ukey === $vkey) {
568
continue;
569
}
570
571
$uview = idx($views, $ukey);
572
if ($uview === null) {
573
continue;
574
}
575
576
$vview = idx($views, $vkey);
577
if ($vview === null) {
578
continue;
579
}
580
581
// If these views don't use the same line data, don't try to
582
// merge them.
583
if ($uview['sliceKey'] != $vview['sliceKey']) {
584
continue;
585
}
586
587
// If these views are overlapping or separated by only a few bytes,
588
// merge them into a single view.
589
$us = $uview['viewOffset'];
590
$ue = $us + $uview['viewLength'];
591
592
$vs = $vview['viewOffset'];
593
$ve = $vs + $vview['viewLength'];
594
595
// Don't merge if one of the slices starts at a byte offset
596
// significantly after the other ends.
597
if (($vs > $ue + $body_bytes) || ($us > $ve + $body_bytes)) {
598
continue;
599
}
600
601
$uss = $uview['sliceOffset'];
602
$use = $uss + $uview['sliceLength'];
603
604
$vss = $vview['sliceOffset'];
605
$vse = $vss + $vview['sliceLength'];
606
607
// Don't merge if one of the slices starts at a line offset
608
// significantly after the other ends.
609
if ($uss > ($vse + $body_lines) || $vss > ($use + $body_lines)) {
610
continue;
611
}
612
613
// These views are overlapping or nearly overlapping, so we merge
614
// them. We merge views even if they aren't exactly adjacent since
615
// it's silly to render an "expand more" which only expands a couple
616
// of lines.
617
618
$offset = min($us, $vs);
619
$length = max($ue, $ve) - $offset;
620
621
$slice_offset = min($uss, $vss);
622
$slice_length = max($use, $vse) - $slice_offset;
623
624
$views[$ukey] = array(
625
'viewOffset' => $offset,
626
'viewLength' => $length,
627
'sliceOffset' => $slice_offset,
628
'sliceLength' => $slice_length,
629
) + $views[$ukey];
630
631
unset($views[$vkey]);
632
}
633
}
634
635
return $views;
636
}
637
638
private function renderExpandRow($range) {
639
640
$icon_up = id(new PHUIIconView())
641
->setIcon('fa-chevron-up');
642
643
$icon_down = id(new PHUIIconView())
644
->setIcon('fa-chevron-down');
645
646
$up_text = array(
647
pht('Show More Above'),
648
' ',
649
$icon_up,
650
);
651
652
$expand_up = javelin_tag(
653
'a',
654
array(
655
'sigil' => 'harbormaster-log-expand',
656
'meta' => array(
657
'headOffset' => $range['head'],
658
'tailOffset' => $range['tail'],
659
'head' => 128,
660
'tail' => 0,
661
),
662
),
663
$up_text);
664
665
$mid_text = pht(
666
'Show More (%s Bytes)',
667
new PhutilNumber($range['tail'] - $range['head']));
668
669
$expand_mid = javelin_tag(
670
'a',
671
array(
672
'sigil' => 'harbormaster-log-expand',
673
'meta' => array(
674
'headOffset' => $range['head'],
675
'tailOffset' => $range['tail'],
676
'head' => 128,
677
'tail' => 128,
678
),
679
),
680
$mid_text);
681
682
$down_text = array(
683
$icon_down,
684
' ',
685
pht('Show More Below'),
686
);
687
688
$expand_down = javelin_tag(
689
'a',
690
array(
691
'sigil' => 'harbormaster-log-expand',
692
'meta' => array(
693
'headOffset' => $range['head'],
694
'tailOffset' => $range['tail'],
695
'head' => 0,
696
'tail' => 128,
697
),
698
),
699
$down_text);
700
701
$expand_cells = array(
702
phutil_tag(
703
'td',
704
array(
705
'class' => 'harbormaster-log-expand-up',
706
),
707
$expand_up),
708
phutil_tag(
709
'td',
710
array(
711
'class' => 'harbormaster-log-expand-mid',
712
),
713
$expand_mid),
714
phutil_tag(
715
'td',
716
array(
717
'class' => 'harbormaster-log-expand-down',
718
),
719
$expand_down),
720
);
721
722
return $this->renderActionTable($expand_cells);
723
}
724
725
private function renderLiveRow($log_size) {
726
$icon_down = id(new PHUIIconView())
727
->setIcon('fa-angle-double-down');
728
729
$icon_pause = id(new PHUIIconView())
730
->setIcon('fa-pause');
731
732
$follow = javelin_tag(
733
'a',
734
array(
735
'sigil' => 'harbormaster-log-expand harbormaster-log-live',
736
'class' => 'harbormaster-log-follow-start',
737
'meta' => array(
738
'headOffset' => $log_size,
739
'head' => 0,
740
'tail' => 1024,
741
'live' => true,
742
),
743
),
744
array(
745
$icon_down,
746
' ',
747
pht('Follow Log'),
748
));
749
750
$stop_following = javelin_tag(
751
'a',
752
array(
753
'sigil' => 'harbormaster-log-expand',
754
'class' => 'harbormaster-log-follow-stop',
755
'meta' => array(
756
'stop' => true,
757
),
758
),
759
array(
760
$icon_pause,
761
' ',
762
pht('Stop Following Log'),
763
));
764
765
$expand_cells = array(
766
phutil_tag(
767
'td',
768
array(
769
'class' => 'harbormaster-log-follow',
770
),
771
array(
772
$follow,
773
$stop_following,
774
)),
775
);
776
777
return $this->renderActionTable($expand_cells);
778
}
779
780
private function renderActionTable(array $action_cells) {
781
$action_row = phutil_tag('tr', array(), $action_cells);
782
783
$action_table = phutil_tag(
784
'table',
785
array(
786
'class' => 'harbormaster-log-expand-table',
787
),
788
$action_row);
789
790
$format_cells = array(
791
phutil_tag('th', array()),
792
phutil_tag(
793
'td',
794
array(
795
'class' => 'harbormaster-log-expand-cell',
796
),
797
$action_table),
798
);
799
800
return phutil_tag('tr', array(), $format_cells);
801
}
802
803
private function getHighlightViews(
804
HarbormasterBuildLog $log,
805
array $range,
806
$log_size) {
807
// If we're highlighting a line range in the file, we first need to figure
808
// out the offsets for the lines we care about.
809
list($range_min, $range_max) = $range;
810
811
// Read the markers to find a range we can load which includes both lines.
812
$read_range = $log->getLineSpanningRange($range_min, $range_max);
813
list($min_pos, $max_pos, $min_line) = $read_range;
814
815
$length = ($max_pos - $min_pos);
816
817
// Reject to do the read if it requires us to examine a huge amount of
818
// data. For example, the user may request lines "$1-1000" of a file where
819
// each line has 100MB of text.
820
$limit = (1024 * 1024 * 16);
821
if ($length > $limit) {
822
return array();
823
}
824
825
$data = $log->loadData($min_pos, $length);
826
827
$offset = $min_pos;
828
$min_offset = null;
829
$max_offset = null;
830
831
$lines = $this->getLines($data);
832
$number = ($min_line + 1);
833
834
foreach ($lines as $line) {
835
if ($min_offset === null) {
836
if ($number === $range_min) {
837
$min_offset = $offset;
838
}
839
}
840
841
$offset += strlen($line);
842
843
if ($max_offset === null) {
844
if ($number === $range_max) {
845
$max_offset = $offset;
846
break;
847
}
848
}
849
850
$number += 1;
851
}
852
853
$context_lines = 8;
854
855
// Build views around the beginning and ends of the respective lines. We
856
// expect these views to overlap significantly in normal circumstances
857
// and be merged later.
858
$views = array();
859
860
if ($min_offset !== null) {
861
$views[] = array(
862
'offset' => $min_offset,
863
'lines' => $context_lines + ($range_max - $range_min) - 1,
864
'direction' => 1,
865
'limit' => null,
866
);
867
if ($min_offset > 0) {
868
$views[] = array(
869
'offset' => $min_offset,
870
'lines' => $context_lines,
871
'direction' => -1,
872
'limit' => null,
873
);
874
}
875
}
876
877
if ($max_offset !== null) {
878
$views[] = array(
879
'offset' => $max_offset,
880
'lines' => $context_lines + ($range_max - $range_min),
881
'direction' => -1,
882
'limit' => null,
883
);
884
if ($max_offset < $log_size) {
885
$views[] = array(
886
'offset' => $max_offset,
887
'lines' => $context_lines,
888
'direction' => 1,
889
'limit' => null,
890
);
891
}
892
}
893
894
return $views;
895
}
896
897
}
898
899