1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8function wikiplugin_pivottable_info()
9{
10	return [
11		'name' => tr('Pivot table'),
12		'description' => tr('Create and display data in pivot table for reporting'),
13		'prefs' => ['wikiplugin_pivottable'],
14		'body' => tra('Leave one space in the box below to allow easier editing of current values with the plugin popup helper later on'),
15		'validate' => 'all',
16		'format' => 'html',
17		'iconname' => 'table',
18		'introduced' => '16.1',
19		'params' => [
20			'data' => [
21				'name' => tr('Data source'),
22				'description' => tr("For example 'tracker:1' or 'activitystream'"),
23				'required' => true,
24				'default' => 0,
25				'filter' => 'text',
26				'profile_reference' => 'tracker',
27				'separator' => ':',
28				'profile_reference_extra_values' => ['activitystream' => 'Activity Stream'],
29			],
30			'overridePermissions' => [
31				'name' => tra('Override item permissions'),
32				'description' => tra('Return all tracker items ignoring permissions to view the corresponding items.'),
33				'since' => '18.1',
34				'required' => false,
35				'filter' => 'alpha',
36				'default' => 'n',
37				'options' => [
38					['text' => '', 'value' => ''],
39					['text' => tra('Yes'), 'value' => 'y'],
40					['text' => tra('No'), 'value' => 'n']
41				]
42			],
43			'width' => [
44				'required' => false,
45				'name' => tra('Width'),
46				'description' => tr('Width of pivot table. Units: % or px.'),
47				'since' => '',
48				'filter' => 'word',
49				'default' => '100%',
50			],
51			'height' => [
52				'required' => false,
53				'name' => tra('Height'),
54				'description' => tr('Height of pivot table. Units: px'),
55				'since' => '',
56				'filter' => 'word',
57				'default' => '400px',
58			],
59			'rows' => [
60				'required' => false,
61				'name' => tra('Pivot table Rows'),
62				'description' => tr('Which field or fields to use as table rows. Leaving blank will remove grouping by table rows. ') . ' ' . tr('Use permanentNames in case of tracker fields.') . ' ' . tr('Separated by colon (:) if more than one.'),
63				'since' => '',
64				'filter' => 'text',
65				'default' => '',
66				'profile_reference' => 'tracker_field',
67				'separator' => ':',
68			],
69			'cols' => [
70				'required' => false,
71				'name' => tra('Pivot table Columns'),
72				'description' => tr('Which field or fields to use as table columns. Leaving blank will use the first available field.') . ' ' . tr('Use permanentNames in case of tracker fields.') . ' ' . tr('Separated by colon (:) if more than one.'),
73				'since' => '',
74				'filter' => 'text',
75				'default' => '',
76				'profile_reference' => 'tracker_field',
77				'separator' => ':',
78			],
79			'heatmapDomain' => [
80				'required' => false,
81				'name' => tra('Values used to decide what heatmapColors to use.'),
82				'description' => tr(''),
83				'since' => '17',
84				'filter' => 'text',
85				'default' => '',
86				'separator' => ':',
87			],
88			'heatmapColors' => [
89				'required' => false,
90				'name' => tra('Color for each heatmapDomain value.'),
91				'description' => tr(''),
92				'since' => '17',
93				'filter' => 'text',
94				'default' => '',
95				'separator' => ':',
96			],
97			'rendererName' => [
98				'name' => tr('Renderer Name'),
99				'description' => tr('Display format of data'),
100				'since' => '',
101				'required' => false,
102				'filter' => 'text',
103				'default' => 'Table',
104				'options' => [
105					['text' => 'Table', 'value' => 'Table'],
106					['text' => tra('Table Barchart'), 'value' => 'Table Barchart'],
107					['text' => tra('Heatmap'), 'value' => 'Heatmap'],
108					['text' => tra('Row Heatmap'), 'value' => 'Row Heatmap'],
109					['text' => tra('Col Heatmap'), 'value' => 'Col Heatmap'],
110					['text' => tra('Line Chart'), 'value' => 'Line Chart'],
111					['text' => tra('Bar Chart'), 'value' => 'Bar Chart'],
112					['text' => tra('Overlay Bar Chart'), 'value' => 'Overlay Bar Chart'],
113					['text' => tra('Stacked Bar Chart'), 'value' => 'Stacked Bar Chart'],
114					['text' => tra('Relative Bar Chart'), 'value' => 'Relative Bar Chart'],
115					['text' => tra('Boxplot Chart'), 'value' => 'Boxplot Chart'],
116					['text' => tra('Horizontal Boxplot Chart'), 'value' => 'Horizontal Boxplot Chart'],
117					['text' => tra('Area Chart'), 'value' => 'Area Chart'],
118					['text' => tra('Histogram'), 'value' => 'Histogram'],
119					['text' => tra('Density Histogram'), 'value' => 'Density Histogram'],
120					['text' => tra('Percent Histogram'), 'value' => 'Percent Histogram'],
121					['text' => tra('Probability Histogram'), 'value' => 'Probability Histogram'],
122					['text' => tra('Density Histogram Horizontal'), 'value' => 'Density Histogram Horizontal'],
123					['text' => tra('Percent Histogram Horizontal'), 'value' => 'Percent Histogram Horizontal'],
124					['text' => tra('Probability Histogram Horizontal'), 'value' => 'Probability Histogram Horizontal'],
125					['text' => tra('Horizontal Histogram'), 'value' => 'Horizontal Histogram'],
126					['text' => tra('Histogram2D'), 'value' => 'Histogram2D'],
127					['text' => tra('Density Histogram2D'), 'value' => 'Density Histogram2D'],
128					['text' => tra('Percent Histogram2D'), 'value' => 'Percent Histogram2D'],
129					['text' => tra('Probability Histogram2D'), 'value' => 'Probability Histogram2D'],
130					['text' => tra('Density Histogram2D Horizontal'), 'value' => 'Density Histogram2D Horizontal'],
131					['text' => tra('Percent Histogram2D Horizontal'), 'value' => 'Percent Histogram2D Horizontal'],
132					['text' => tra('Probability Histogram2D Horizontal'), 'value' => 'Probability Histogram2D Horizontal'],
133					['text' => tra('Horizontal Histogram2D'), 'value' => 'Horizontal Histogram2D'],
134					['text' => tra('Scatter Chart'), 'value' => 'Scatter Chart'],
135					['text' => tra('Treemap'), 'value' => 'Treemap']
136				]
137			],
138			'aggregatorName' => [
139				'name' => tr('Aggregator Name'),
140				'description' => tr('Function to apply on the numeric values from the variables selected.'),
141				'since' => '',
142				'required' => false,
143				'filter' => 'text',
144				'default' => 'Count',
145				'options' => [
146					['text' => 'Count', 'value' => 'Count'],
147					['text' => tra('Count Unique Values'), 'value' => 'Count Unique Values'],
148					['text' => tra('List Unique Values'), 'value' => 'List Unique Values'],
149					['text' => tra('Sum'), 'value' => 'Sum'],
150					['text' => tra('Integer Sum'), 'value' => 'Integer Sum'],
151					['text' => tra('Average'), 'value' => 'Average'],
152					['text' => tra('Minimum'), 'value' => 'Minimum'],
153					['text' => tra('Maximum'), 'value' => 'Maximum'],
154					['text' => tra('Sum over Sum'), 'value' => 'Sum over Sum'],
155					['text' => tra('80% Upper Bound'), 'value' => '80% Upper Bound'],
156					['text' => tra('80% Lower Bound'), 'value' => '80% Lower Bound'],
157					['text' => tra('Sum as Fraction of Total'), 'value' => 'Sum as Fraction of Total'],
158					['text' => tra('Sum as Fraction of Rows'), 'value' => 'Sum as Fraction of Rows'],
159					['text' => tra('Sum as Fraction of Columns'), 'value' => 'Sum as Fraction of Columns'],
160					['text' => tra('Count as Fraction of Total'), 'value' => 'Count as Fraction of Total'],
161					['text' => tra('Count as Fraction of Rows'), 'value' => 'Count as Fraction of Rows'],
162					['text' => tra('Count as Fraction of Columns'), 'value' => 'Count as Fraction of Columns']
163				]
164			],
165			'vals' => [
166				'name' => tr('Values'),
167				'description' => tr('Variable with numeric values or tracker field permNames, on which the formula from the aggregator is applied. It can be left empty if aggregator is related to Counts.') . ' ' . tr('Use permanentNames in case of tracker fields, separated by : in case of multiple fields function.'),
168				'since' => '',
169				'required' => false,
170				'filter' => 'text',
171				'profile_reference' => 'tracker_field',
172				'separator' => ':',
173			],
174			'inclusions' => [
175				'name' => tr('Inclusions'),
176				'description' => tr('Filter values for fields in rows or columns. Contains JSON encoded object of arrays of strings.'),
177				'since' => '',
178				'required' => false,
179				'filter' => 'text',
180			],
181			'menuLimit' => [
182				'name' => tr('Filter list limit'),
183				'description' => tr('Pivottable menuLimit option override - number of entries to consider the menu list too big when filtering on a particular column or row.'),
184				'since' => '16.2',
185				'required' => false,
186				'filter' => 'digits',
187			],
188			'aggregateDetails' => [
189				'name' => tr('Aggregate details'),
190				'description' => tr('When enabled, clicking a table cell will popup all items that were aggregated into that cell. Specify the name of the field or fields to use to display the details separated by colon. Enabled by default. To disable, set contents to an empty string.'),
191				'since' => '16.2',
192				'required' => false,
193				'filter' => 'text',
194				'profile_reference' => 'tracker_field',
195				'separator' => ':',
196			],
197			'highlightMine' => [
198				'name' => tra('Highlight my items'),
199				'description' => tra('Highlight owned items\' values in Charts.'),
200				'since' => '16.3',
201				'required' => false,
202				'filter' => 'alpha',
203				'default' => 'n',
204				'options' => [
205					['text' => '', 'value' => ''],
206					['text' => tra('Yes'), 'value' => 'y'],
207					['text' => tra('No'), 'value' => 'n']
208				]
209			],
210			'highlightGroup' => [
211				'name' => tra('Highlight my group items'),
212				'description' => tra('Highlight items\' values belonging to one of my groups in Charts.'),
213				'since' => '16.3',
214				'required' => false,
215				'filter' => 'alpha',
216				'default' => 'n',
217				'options' => [
218					['text' => '', 'value' => ''],
219					['text' => tra('Yes'), 'value' => 'y'],
220					['text' => tra('No'), 'value' => 'n']
221				]
222			],
223			'highlightGroupColors' => [
224				'required' => false,
225				'name' => tra('Color for each highlighted group items.'),
226				'description' => tr(''),
227				'since' => '18.1',
228				'filter' => 'text',
229				'default' => '',
230				'separator' => ':',
231			],
232			'xAxisLabel' => [
233				'name' => tr('xAxis label'),
234				'description' => tr('Override label of horizontal axis when using Chart renderers.'),
235				'since' => '16.3',
236				'required' => false,
237				'filter' => 'text',
238			],
239			'yAxisLabel' => [
240				'name' => tr('yAxis label'),
241				'description' => tr('Override label of vertical axis when using Chart renderers.'),
242				'since' => '16.3',
243				'required' => false,
244				'filter' => 'text',
245			],
246			'chartTitle' => [
247				'name' => tr('Chart title'),
248				'description' => tr('Override title when using Chart renderers.'),
249				'since' => '16.3',
250				'required' => false,
251				'filter' => 'text',
252			],
253			'chartHoverBar' => [
254				'name' => tr('Chart hover bar'),
255				'description' => tr('Display the Chart hover bar or not.'),
256				'since' => '16.3',
257				'required' => false,
258				'filter' => 'alpha',
259				'default' => 'y',
260				'options' => [
261					['text' => tra('Yes'), 'value' => 'y'],
262					['text' => tra('No'), 'value' => 'n']
263				]
264			],
265			'translate' => [
266				'name' => tr('Translate displayed data'),
267				'description' => tr('Use translated data values for calculations and display.') . ' ' . tr('Default value: No'),
268				'since' => '18.3',
269				'required' => false,
270				'filter' => 'alpha',
271				'default' => 'n',
272				'options' => [
273					['text' => '', 'value' => ''],
274					['text' => tra('No'), 'value' => 'n'],
275					['text' => tra('Yes'), 'value' => 'y']
276				]
277			]
278		],
279	];
280}
281
282function wikiplugin_pivottable($data, $params)
283{
284
285	//included globals for permission check
286	global $prefs, $page, $wikiplugin_included_page, $user;
287
288	//checking if vendor files are present
289	if (! file_exists('vendor_bundled/vendor/nicolaskruchten/pivottable/')) {
290		return WikiParser_PluginOutput::internalError(tr('Missing required files, please make sure plugin files are installed at vendor_bundled/vendor/nicolaskruchten/pivottable. <br/><br /> To install, please run composer or download from following url:<a href="https://github.com/nicolaskruchten/pivottable/archive/master.zip" target="_blank">https://github.com/nicolaskruchten/pivottable/archive/master.zip</a>'));
291	}
292
293	static $id = 0;
294	$id++;
295
296	$headerlib = TikiLib::lib('header');
297	$headerlib->add_cssfile('vendor_bundled/vendor/nicolaskruchten/pivottable/dist/pivot.css');
298	$headerlib->add_jsfile('vendor_bundled/vendor/nicolaskruchten/pivottable/dist/pivot.js', true);
299	$headerlib->add_jsfile('vendor_bundled/vendor/plotly/plotly.js/dist/plotly-cartesian.min.js', true);
300	$headerlib->add_jsfile('vendor_bundled/vendor/nagarajanchinnasamy/subtotal/dist/subtotal.min.js', true);
301	$headerlib->add_jsfile('lib/jquery_tiki/wikiplugin-pivottable.js', true);
302
303	$lang = substr($prefs['site_language'], 0, 2);
304	if (file_exists('vendor_bundled/vendor/nicolaskruchten/pivottable/dist/pivot.' . $lang . '.js')) {
305		$headerlib->add_jsfile('vendor_bundled/vendor/nicolaskruchten/pivottable/dist/pivot.' . $lang . '.js', true);
306	}
307
308	$translate = (! empty($params['translate']) && $params['translate'] == 'y') ? true : false;
309
310	$smarty = TikiLib::lib('smarty');
311	$smarty->assign('lang', $lang);
312
313
314	//checking data type
315	if (empty($params['data']) || ! is_array($params['data'])) {
316		return WikiParser_PluginOutput::internalError(tr('Missing data parameter with format: source:ID, e.g. tracker:1'));
317	}
318	$dataType = $params['data'][0];
319	if ($dataType !== 'activitystream' && $dataType !== 'tracker') {
320		return WikiParser_PluginOutput::internalError(tr('Error data parameter'));
321	}
322
323	if (! empty($params['rendererName'])) {
324		$rendererName = $params['rendererName'];
325	} else {
326		$rendererName = "Table";
327	}
328
329	if (! empty($params['aggregatorName'])) {
330		$aggregatorName = $params['aggregatorName'];
331	} else {
332		$aggregatorName = "Count";
333	}
334
335	if (! empty($params['width'])) {
336		$width = $params['width'];
337	} else {
338		$width = "100%";
339	}
340
341	if (! empty($params['height'])) {
342		$height = $params['height'];
343	} else {
344		$height = "1000px";
345	}
346
347	if ($dataType === "tracker") {
348		$trackerId = $params['data'][1];
349		$definition = Tracker_Definition::get($trackerId);
350		if (! $definition) {
351			return WikiParser_PluginOutput::userError(tr('Tracker data source not found.'));
352		}
353
354		$perms = Perms::get(['type' => 'tracker', 'object' => $trackerId]);
355
356		$fields = $definition->getFields();
357
358		if (! $perms->admin_trackers && $params['overridePermissions'] !== 'y') {
359			$hasFieldPermissions = false;
360			foreach ($fields as $key => $field) {
361				$isHidden = $field['isHidden'];
362				$visibleBy = $field['visibleBy'];
363
364				if ($isHidden != 'n' || ! empty($visibleBy)) {
365					$hasFieldPermissions = true;
366				}
367
368				if ($isHidden == 'c') {
369					// creators can see their own items coming from the search index
370				} elseif ($isHidden == 'y') {
371					// Visible by administrator only
372					unset($fields[$key]);
373				} elseif (! empty($visibleBy)) {
374					// Permission based on visibleBy apply
375					$commonGroups = array_intersect($visibleBy, $perms->getGroups());
376					if (count($commonGroups) == 0) {
377						unset($fields[$key]);
378					}
379				}
380			}
381			if (! $hasFieldPermissions && ! $perms->view_trackers && ! $definition->isEnabled('userCanSeeOwn') && ! $definition->isEnabled('groupCanSeeOwn') && ! $definition->isEnabled('writerCanModify')) {
382				return WikiParser_PluginOutput::userError(tr('You do not have rights to view tracker data.'));
383			}
384		}
385
386		$fields[] = [
387			'name' => 'object_id',
388			'permName' => 'object_id',
389			'type' => 't'
390		];
391
392		$fields[] = [
393			'name' => 'object_type',
394			'permName' => 'object_type',
395			'type' => 't'
396		];
397
398		$fields[] = [
399			'name' => 'creation_date',
400			'permName' => 'creation_date',
401			'type' => 'f'
402		];
403		$fields[] = [
404			'name' => 'modification_date',
405			'permName' => 'modification_date',
406			'type' => 'f'
407		];
408		$fields[] = [
409			'name' => 'tracker_status',
410			'permName' => 'tracker_status',
411			'type' => 't'
412		];
413
414		$heatmapParams = [];
415		if ($rendererName === 'Heatmap') {
416			$validConfig = ! (empty($params['heatmapDomain']) && empty($params['heatmapColors']))
417				&& is_array($params['heatmapDomain'])
418				&& is_array($params['heatmapColors'])
419				&& count($params['heatmapDomain']) === count($params['heatmapColors']);
420
421			if ($validConfig) {
422				$heatmapParams = [
423					'domain' => array_map(floatval, $params['heatmapDomain']),
424					'colors' => $params['heatmapColors']
425				];
426			}
427
428			unset($validConfig);
429		}
430
431		$query = new Search_Query;
432		$query->filterType('trackeritem');
433		$query->filterContent($trackerId, 'tracker_id');
434
435		$unifiedsearchlib = TikiLib::lib('unifiedsearch');
436		if (! empty($params['overridePermissions']) && $params['overridePermissions'] === 'y') {
437			$unifiedsearchlib->initQueryBase($query);
438			$unifiedsearchlib->initQueryPresentation($query);
439		} else {
440			$unifiedsearchlib->initQuery($query);
441		}
442
443		$matches = WikiParser_PluginMatcher::match($data);
444
445		$builder = new Search_Query_WikiBuilder($query);
446		$builder->apply($matches);
447
448		if (! $index = $unifiedsearchlib->getIndex()) {
449			return WikiParser_PluginOutput::userError(tr('Unified search index not found.'));
450		}
451
452		$result = [];
453		foreach ($query->scroll($index) as $row) {
454			$result[] = $row;
455		}
456		$result = Search_ResultSet::create($result);
457		$result->setId('wppivottable-' . $id);
458
459		$resultBuilder = new Search_ResultSet_WikiBuilder($result);
460		$resultBuilder->apply($matches);
461
462		$columnsListed = false;
463		$derivedAttributes = [];
464		$splittedAttributes = [];
465
466		foreach ($matches as $match) {
467			if ($match->getName() == 'display' || $match->getName() == 'column') {
468				$columnsListed = true;
469			} elseif ($match->getName() == 'derivedattribute') {
470				if (preg_match('/name="([^"]+)"/', $match->getArguments(), $match_name)
471					&& preg_match('/function="([^"]+)"/', $match->getArguments(), $match_function)
472					&& preg_match('/parameters="([^"]*)"/', $match->getArguments(), $match_parameters)) {
473					$derivedattr_name = $match_name[1];
474					$function_name = $match_function[1];
475					$function_params = explode(':', $match_parameters[1]);
476
477					if (empty($function_params)) {
478						$function_params = '';
479					} else {
480						$function_params = '"' . implode('","', $function_params) . '"';
481					}
482
483					$derivedAttributes[] = sprintf('"%s": %s(%s)', $derivedattr_name, $function_name, $function_params);
484				}
485			} elseif ($match->getName() == 'split') {
486				$parser = new WikiParser_PluginArgumentParser;
487				$arguments = $parser->parse($match->getArguments());
488				if (! isset($arguments['field'])) {
489					return WikiParser_PluginOutput::userError(tr('Split wiki modifier should specify a field.'));
490				}
491				if (! isset($arguments['separator'])) {
492					$arguments['separator'] = ',';
493				}
494				$arguments['field'] = str_replace('tracker_field_', '', $arguments['field']);
495				$splittedAttributes[] = $arguments;
496			}
497		}
498
499		if ($columnsListed) {
500			$data .= '{display name="object_id"}{display name="object_type"}';
501			$plugin = new Search_Formatter_Plugin_ArrayTemplate($data);
502			$usedFields = array_keys($plugin->getFields());
503			foreach ($fields as $key => $field) {
504				if (! in_array('tracker_field_' . $field['permName'], $usedFields)
505					&& ! in_array($field['permName'], $usedFields) ) {
506					unset($fields[$key]);
507				}
508			}
509			$fields = array_values($fields);
510			$plugin->setFieldPermNames($fields);
511		} else {
512			$plugin = new Search_Formatter_Plugin_ArrayTemplate(implode("", array_map(
513				function ($f) {
514					if (in_array($f['permName'], ['object_id', 'object_type', 'creation_date', 'modification_date', 'tracker_status'])) {
515						return '{display name="' . $f['permName'] . '" default=" "}';
516					} elseif ($f['type'] == 'e') {
517						return '{display name="tracker_field_' . $f['permName'] . '" format="categorylist" singleList="y" separator=" "}';
518					} else {
519						return '{display name="tracker_field_' . $f['permName'] . '" default=" "}';
520					}
521				},
522				$fields
523			)));
524			$plugin->setFieldPermNames($fields);
525		}
526	}
527
528	if ($dataType === "activitystream") {
529		$unifiedsearchlib = TikiLib::lib('unifiedsearch');
530		$query = new Search_Query;
531		$unifiedsearchlib->initQuery($query);
532		$query->filterType('activity');
533
534		if ($params['overridePermissions'] === 'y') {
535			$unifiedsearchlib->initQueryBase($query);
536			$unifiedsearchlib->initQueryPresentation($query);
537		} else {
538			$unifiedsearchlib->initQuery($query);
539		}
540
541		$matches = WikiParser_PluginMatcher::match($data);
542
543		$builder = new Search_Query_WikiBuilder($query);
544		$builder->enableAggregate();
545		$builder->apply($matches);
546
547		$query->setOrder('modification_date_desc');
548
549		if (! $index = $unifiedsearchlib->getIndex()) {
550			throw new Services_Exception_NotAvailable(tr('Activity stream currently unavailable.'));
551		}
552
553		$result = [];
554		foreach ($query->scroll($index) as $row) {
555			$result[] = $row;
556		}
557		$result = Search_ResultSet::create($result);
558		$result->setId('wppivottable-' . $id);
559
560		$paginationArguments = $builder->getPaginationArguments();
561
562		$resultBuilder = new Search_ResultSet_WikiBuilder($result);
563		$resultBuilder->setPaginationArguments($paginationArguments);
564		$resultBuilder->apply($matches);
565
566		$fields = [];
567		$fields[] = [
568			'name' => 'object',
569			'permName' => 'object',
570			'type' => 't'
571		];
572
573		$fields[] = [
574			'name' => 'object_id',
575			'permName' => 'object_id',
576			'type' => 't'
577		];
578
579		$fields[] = [
580			'name' => 'object_type',
581			'permName' => 'object_type',
582			'type' => 't'
583		];
584
585		$fields[] = [
586			'name' => 'modification_date',
587			'permName' => 'modification_date',
588			'type' => 'f'
589		];
590
591		$fields[] = [
592			'name' => 'user',
593			'permName' => 'user',
594			'type' => 't'
595		];
596
597		$fields[] = [
598			'name' => 'event_type',
599			'permName' => 'event_type',
600			'type' => 't'
601		];
602
603		$fields[] = [
604			'name' => 'type',
605			'permName' => 'type',
606			'type' => 't'
607		];
608
609		try {
610			$plugin = new Search_Formatter_Plugin_ArrayTemplate(implode("", array_map(
611				function ($f) {
612					$activityArray = [
613						'object',
614						'object_id',
615						'object_type',
616						'modification_date',
617						'user',
618						'event_type',
619						'type'
620					];
621					if (in_array($f['permName'], $activityArray)) {
622						return '{display name="' . $f['permName'] . '" default=" "}';
623					}
624				},
625				$fields
626			)));
627			$plugin->setFieldPermNames($fields);
628		} catch (SmartyException $e) {
629			throw new Services_Exception_NotAvailable($e->getMessage());
630		}
631	}
632
633	$builder = new Search_Formatter_Builder;
634	$builder->setId('wppivottable-' . $id);
635	$builder->setCount($result->count());
636	$builder->apply($matches);
637	$builder->setFormatterPlugin($plugin);
638
639	$formatter = $builder->getFormatter();
640	$entries = $formatter->getPopulatedList($result, false);
641	$entries = $plugin->renderEntries($entries);
642
643	$pivotData = [];
644	foreach ($entries as $entry) {
645		$row = [];
646		foreach ($entry as $fieldName => $value) {
647			if ($entry['object_type'] != 'activity' && $field = $definition->getFieldFromPermName($fieldName)) {
648				// Actual data values
649				if ($translate) {
650					$row[$field['name']] = tra($value);
651				} else {
652					$row[$field['name']] = $value;
653				}
654			} else {
655				// predefined fields (created date, lastmod, etc.)
656				$row[$fieldName] = $value;
657			}
658		}
659		$pivotData[] = $row;
660	}
661
662	foreach ($splittedAttributes as $arguments) {
663		$field = $definition->getFieldFromPermName($arguments['field']);
664		if (empty($field)) {
665			continue;
666		} else {
667			$field = $field['name'];
668		}
669		$separator = $arguments['separator'];
670		$key = 0;
671		while($key < count($pivotData)) {
672			$row = $pivotData[$key];
673			if (! isset($row[$field])) {
674				$key++;
675				continue;
676			}
677			$splitted = explode($separator, $row[$field]);
678			if (count($splitted) == 1) {
679				$key++;
680				continue;
681			}
682			$replacement = array_map(function($value) use ($row, $field) {
683				return array_merge($row, [$field => ltrim($value)]);
684			}, $splitted);
685			array_splice($pivotData, $key, 1, $replacement);
686			$key += count($replacement);
687		}
688	}
689
690	//translating permName to field name for columns and rows
691	$cols = [];
692	if (! empty($params['cols'])) {
693		foreach ($params['cols'] as $colName) {
694			if ($params['data'][0] !== 'activitystream' && $field = $definition->getFieldFromPermName(trim($colName))) {
695				$cols[] = $field['name'];
696			} else {
697				$cols[] = $colName;
698			}
699		}
700	} elseif (! empty($fields) && empty($params['rows'])) {
701		$cols[] = $fields[0]['name'];
702	}
703
704	$rows = [];
705	if (! empty($params['rows'])) {
706		foreach ($params['rows'] as $rowName) {
707			if ($params['data'][0] !== 'activitystream' && $field = $definition->getFieldFromPermName(trim($rowName))) {
708				$rows[] = $field['name'];
709			} else {
710				$rows[] = $rowName;
711			}
712		}
713	}
714
715	$vals = [];
716	if (! empty($params['vals'])) {
717		foreach ($params['vals'] as $valName) {
718			if ($params['data'][0] !== 'activitystream' && $field = $definition->getFieldFromPermName(trim($valName))) {
719				$vals[] = $field['name'];
720			} else {
721				$vals[] = $valName;
722			}
723		}
724	}
725
726	$inclusions = ! empty($params['inclusions']) ? $params['inclusions'] : '{}';
727
728	// parsing array to hold permNames mapped with field names for save button
729	// and list of date fields for custom sorting
730	$fieldsArr = [];
731	$dateFields = [];
732	foreach ($fields as $field) {
733		$fieldsArr[$field['name']] = $field['permName'];
734		if ($field['type'] == 'f') {
735			$dateFields[] = $field['name'];
736		}
737	}
738
739	$smarty->loadPlugin('smarty_function_object_link');
740
741	if (! isset($params['aggregateDetails'])) {
742		if (isset($fields[2])) {
743			$params['aggregateDetails'][] = $fields[2]['permName'];
744		} elseif (isset($fields[0])) {
745			$params['aggregateDetails'][] = $fields[0]['permName'];
746		}
747	}
748
749	if (! empty($params['aggregateDetails']) && ! empty($params['aggregateDetails'][0])) {
750		$aggregateDetails = [];
751		foreach ($params['aggregateDetails'] as $fieldName) {
752			if ($params['data'][0] != 'activitystream' && $field = $definition->getFieldFromPermName(trim($fieldName))) {
753				$aggregateDetails[] = $field['name'];
754			} else {
755				$aggregateDetails[] = trim($fieldName);
756			}
757		}
758		foreach ($pivotData as &$row) {
759			$title = implode(' ', array_map(function ($field) use ($row) {
760				return $row[$field];
761			}, $aggregateDetails));
762			$pivotLinkParams = [
763				'type' => $row['object_type'],
764				'id' => $row['object_id'],
765				'title' => $title,
766			];
767			if ($row['object_type'] === 'activity') {
768				$pivotLinkParams = [
769					'type' => $row['type'],
770					'id' => $row['object'],
771					'title' => $row['type'],
772				];
773			}
774			$row['pivotLink'] = smarty_function_object_link($pivotLinkParams, $smarty->getEmptyInternalTemplate());
775		}
776	} else {
777		$params['aggregateDetails'] = [];
778	}
779
780	$highlight = [];
781	if (! empty($params['highlightMine']) && $params['highlightMine'] === 'y' && $params['data'][0] !== 'activitystream') {
782		$ownerFields = array_map(function ($fieldId) use ($definition) {
783			return $definition->getField($fieldId);
784		}, $definition->getItemOwnerFields());
785		foreach ($pivotData as $item) {
786			foreach ($ownerFields as $ownerField) {
787				$itemUsers = TikiLib::lib('trk')->parse_user_field(@$item[$ownerField['name']]);
788				if (in_array($user, $itemUsers)) {
789					$highlight[] = ['item' => $item];
790					break;
791				}
792			}
793		}
794	}
795	if (! empty($params['highlightGroup']) && $params['highlightGroup'] === 'y') {
796		$groupField = null;
797		foreach ($fields as $field) {
798			if ($field['type'] == 'g') {
799				$groupField = $field;
800				break;
801			}
802		}
803		if ($groupField) {
804			$myGroups = TikiLib::lib('tiki')->get_user_groups($user);
805			foreach ($pivotData as $item) {
806				$group = @$item[$groupField['name']];
807				if (in_array($group, $myGroups)) {
808					$highlight[] = ['item' => $item, 'group' => $group];
809				}
810			}
811			$groupColors = [];
812			if ($prefs['feature_conditional_formatting'] === 'y') {
813				$groupsInfo = TikiLib::lib('user')->get_group_info($myGroups);
814				foreach ($groupsInfo as $groupInfo) {
815					$groupColors[$groupInfo['groupName']] = $groupInfo['groupColor'];
816				}
817			}
818			if (! empty($params['highlightGroupColors'])) {
819				$index = 0;
820				foreach ($myGroups as $group) {
821					if (empty($params['highlightGroupColors'][$index])) {
822						break;
823					}
824					$groupColors[$group] = $params['highlightGroupColors'][$index];
825					$index++;
826				}
827			}
828			if ($groupColors) {
829				foreach ($highlight as &$row) {
830					if (! empty($row['group'])) {
831						$group = $row['group'];
832						if ($group && ! empty($groupColors[$group])) {
833							$row['color'] = $groupColors[$group];
834						}
835					}
836				}
837			}
838		}
839	}
840
841	//checking if user can see edit button
842	if (! empty($wikiplugin_included_page)) {
843		$sourcepage = $wikiplugin_included_page;
844	} else {
845		$sourcepage = $page;
846	}
847	//checking if user has edit permissions on the wiki page using the current permission library to obey global/categ/object perms
848	$objectperms = Perms::get([ 'type' => 'wiki page', 'object' => $sourcepage ]);
849	if ($objectperms->edit) {
850		$showControls = true;
851	} else {
852		$showControls = false;
853	}
854
855	$out = str_replace(['~np~', '~/np~'], '', $formatter->renderFilters());
856
857	$smarty->assign('pivottable', [
858		'id' => 'pivottable' . $id,
859		'trows' => $rows,
860		'tcolumns' => $cols,
861		'dataSource' => $dataType == 'activitystream' ? $dataType : $dataType . ':' . $trackerId,
862		'data' => $pivotData,
863		'derivedAttributes' => $derivedAttributes,
864		'rendererName' => $rendererName,
865		'aggregatorName' => $aggregatorName,
866		'vals' => $vals,
867		'width' => $width,
868		'height' => $height,
869		'heatmapParams' => $heatmapParams,
870		'showControls' => $showControls,
871		'page' => $sourcepage,
872		'fieldsArr' => $fieldsArr,
873		'dateFields' => $dateFields,
874		'inclusions' => $inclusions,
875		'menuLimit' => empty($params['menuLimit']) ? null : $params['menuLimit'],
876		'aggregateDetails' => implode(':', $params['aggregateDetails']),
877		'highlight' => $highlight,
878		'highlightMine' => empty($params['highlightMine']) ? null : $params['highlightMine'],
879		'highlightGroup' => empty($params['highlightGroup']) ? null : $params['highlightGroup'],
880		'xAxisLabel' => empty($params['xAxisLabel']) ? null : $params['xAxisLabel'],
881		'yAxisLabel' => empty($params['yAxisLabel']) ? null : $params['yAxisLabel'],
882		'chartTitle' => empty($params['chartTitle']) ? null : $params['chartTitle'],
883		'chartHoverBar' => empty($params['chartHoverBar']) ? null : $params['chartHoverBar'],
884		'translate' => empty($params['translate']) ? null : $params['translate'],
885		'index' => $id
886	]);
887
888	$out .= $smarty->fetch('wiki-plugins/wikiplugin_pivottable.tpl');
889
890	return $out;
891}
892