1<?php 2/** 3 * Matomo - free/libre analytics platform 4 * 5 * @link https://matomo.org 6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later 7 * 8 */ 9namespace Piwik\ViewDataTable; 10 11use Piwik\Cache; 12use Piwik\Common; 13use Piwik\Option; 14use Piwik\Piwik; 15use Piwik\Plugin\Report; 16use Piwik\Plugin\ViewDataTable; 17use Piwik\Plugins\CoreVisualizations\Visualizations\Cloud; 18use Piwik\Plugins\CoreVisualizations\Visualizations\HtmlTable; 19use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Bar; 20use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Pie; 21use Piwik\Plugins\Goals\Visualizations\Goals; 22use Piwik\Plugins\Insights\Visualizations\Insight; 23use Piwik\Plugin\Manager as PluginManager; 24 25/** 26 * ViewDataTable Manager. 27 * 28 */ 29class Manager 30{ 31 /** 32 * Returns the viewDataTable IDs of a visualization's class lineage. 33 * 34 * @see self::getVisualizationClassLineage 35 * 36 * @param string $klass The visualization class. 37 * 38 * @return array 39 */ 40 public static function getIdsWithInheritance($klass) 41 { 42 $klasses = Common::getClassLineage($klass); 43 44 $result = array(); 45 foreach ($klasses as $klass) { 46 try { 47 $result[] = $klass::getViewDataTableId(); 48 } catch (\Exception $e) { 49 // in case $klass did not define an id: eg Plugin\ViewDataTable 50 continue; 51 } 52 } 53 54 return $result; 55 } 56 57 /** 58 * Returns all registered visualization classes. Uses the 'Visualization.getAvailable' 59 * event to retrieve visualizations. 60 * 61 * @return array Array mapping visualization IDs with their associated visualization classes. 62 * @throws \Exception If a visualization class does not exist or if a duplicate visualization ID 63 * is found. 64 * @return array 65 */ 66 public static function getAvailableViewDataTables() 67 { 68 $cache = Cache::getTransientCache(); 69 $cacheId = 'ViewDataTable.getAvailableViewDataTables'; 70 $dataTables = $cache->fetch($cacheId); 71 72 if (!empty($dataTables)) { 73 return $dataTables; 74 } 75 76 $klassToExtend = '\\Piwik\\Plugin\\ViewDataTable'; 77 78 /** @var string[] $visualizations */ 79 $visualizations = PluginManager::getInstance()->findMultipleComponents('Visualizations', $klassToExtend); 80 81 $result = array(); 82 83 foreach ($visualizations as $viz) { 84 if (!class_exists($viz)) { 85 throw new \Exception("Invalid visualization class '$viz' found in Visualization.getAvailableVisualizations."); 86 } 87 88 if (!is_subclass_of($viz, $klassToExtend)) { 89 throw new \Exception("ViewDataTable class '$viz' does not extend Plugin/ViewDataTable"); 90 } 91 92 $vizId = $viz::getViewDataTableId(); 93 94 if (isset($result[$vizId])) { 95 throw new \Exception("ViewDataTable ID '$vizId' is already in use!"); 96 } 97 98 $result[$vizId] = $viz; 99 } 100 101 /** 102 * Triggered to filter available DataTable visualizations. 103 * 104 * Plugins that want to disable certain visualizations should subscribe to 105 * this event and remove visualizations from the incoming array. 106 * 107 * **Example** 108 * 109 * public function filterViewDataTable(&$visualizations) 110 * { 111 * unset($visualizations[HtmlTable::ID]); 112 * } 113 * 114 * @param array &$visualizations An array of all available visualizations indexed by visualization ID. 115 * @since Piwik 3.0.0 116 */ 117 Piwik::postEvent('ViewDataTable.filterViewDataTable', array(&$result)); 118 119 $cache->save($cacheId, $result); 120 121 return $result; 122 } 123 124 /** 125 * Returns all available visualizations that are not part of the CoreVisualizations plugin. 126 * 127 * @return array Array mapping visualization IDs with their associated visualization classes. 128 */ 129 public static function getNonCoreViewDataTables() 130 { 131 $result = array(); 132 133 foreach (static::getAvailableViewDataTables() as $vizId => $vizClass) { 134 if (false === strpos($vizClass, 'Piwik\\Plugins\\CoreVisualizations') 135 && false === strpos($vizClass, 'Piwik\\Plugins\\Goals\\Visualizations\\Goals')) { 136 $result[$vizId] = $vizClass; 137 } 138 } 139 140 return $result; 141 } 142 143 /** 144 * This method determines the default set of footer icons to display below a report. 145 * 146 * $result has the following format: 147 * 148 * ``` 149 * array( 150 * array( // footer icon group 1 151 * 'class' => 'footerIconGroup1CssClass', 152 * 'buttons' => array( 153 * 'id' => 'myid', 154 * 'title' => 'My Tooltip', 155 * 'icon' => 'path/to/my/icon.png' 156 * ) 157 * ), 158 * array( // footer icon group 2 159 * 'class' => 'footerIconGroup2CssClass', 160 * 'buttons' => array(...) 161 * ), 162 * ... 163 * ) 164 * ``` 165 */ 166 public static function configureFooterIcons(ViewDataTable $view) 167 { 168 $result = array(); 169 170 $normalViewIcons = self::getNormalViewIcons($view); 171 172 if (!empty($normalViewIcons['buttons'])) { 173 $result[] = $normalViewIcons; 174 } 175 176 // add insight views 177 $insightsViewIcons = array( 178 'class' => 'tableInsightViews', 179 'buttons' => array(), 180 ); 181 182 $graphViewIcons = self::getGraphViewIcons($view); 183 184 $nonCoreVisualizations = static::getNonCoreViewDataTables(); 185 186 foreach ($nonCoreVisualizations as $id => $klass) { 187 if ($klass::canDisplayViewDataTable($view) || $view::ID == $id) { 188 $footerIcon = static::getFooterIconFor($id); 189 if (Insight::ID == $footerIcon['id']) { 190 $insightsViewIcons['buttons'][] = static::getFooterIconFor($id); 191 } else { 192 $graphViewIcons['buttons'][] = static::getFooterIconFor($id); 193 } 194 } 195 } 196 197 $graphViewIcons['buttons'] = array_filter($graphViewIcons['buttons']); 198 199 if (!empty($insightsViewIcons['buttons']) 200 && $view->config->show_insights 201 ) { 202 $result[] = $insightsViewIcons; 203 } 204 205 if (!empty($graphViewIcons['buttons'])) { 206 $result[] = $graphViewIcons; 207 } 208 209 return $result; 210 } 211 212 /** 213 * Returns an array with information necessary for adding the viewDataTable to the footer. 214 * 215 * @param string $viewDataTableId 216 * 217 * @return array 218 */ 219 private static function getFooterIconFor($viewDataTableId) 220 { 221 $tables = static::getAvailableViewDataTables(); 222 223 if (!array_key_exists($viewDataTableId, $tables)) { 224 return; 225 } 226 227 $klass = $tables[$viewDataTableId]; 228 229 return array( 230 'id' => $klass::getViewDataTableId(), 231 'title' => Piwik::translate($klass::FOOTER_ICON_TITLE), 232 'icon' => $klass::FOOTER_ICON, 233 ); 234 } 235 236 public static function clearAllViewDataTableParameters() 237 { 238 Option::deleteLike('viewDataTableParameters_%'); 239 } 240 241 public static function clearUserViewDataTableParameters($userLogin) 242 { 243 Option::deleteLike('viewDataTableParameters_' . $userLogin . '_%'); 244 } 245 246 public static function getViewDataTableParameters($login, $controllerAction, $containerId = null) 247 { 248 $paramsKey = self::buildViewDataTableParametersOptionKey($login, $controllerAction, $containerId); 249 $params = Option::get($paramsKey); 250 251 if (empty($params)) { 252 return array(); 253 } 254 255 $params = json_decode($params); 256 $params = (array) $params; 257 258 // when setting an invalid parameter, we silently ignore the invalid parameter and proceed 259 $params = self::removeNonOverridableParameters($controllerAction, $params); 260 self::unsetComparisonParams($params); 261 262 return $params; 263 } 264 265 /** 266 * Any parameter set here will be set into one of the following objects: 267 * 268 * - ViewDataTable.requestConfig[paramName] 269 * - ViewDataTable.config.custom_parameters[paramName] 270 * - ViewDataTable.config.custom_parameters[paramName] 271 * 272 * (see ViewDataTable::overrideViewPropertiesWithParams) 273 274 * @param $login 275 * @param $controllerAction 276 * @param $parametersToOverride 277 * @param string|null $containerId 278 * @throws \Exception 279 */ 280 public static function saveViewDataTableParameters($login, $controllerAction, $parametersToOverride, $containerId = null) 281 { 282 $params = self::getViewDataTableParameters($login, $controllerAction); 283 284 self::unsetComparisonParams($params); 285 286 foreach ($parametersToOverride as $key => $value) { 287 if ($key === 'viewDataTable' 288 && !empty($params[$key]) 289 && $params[$key] !== $value) { 290 if (!empty($params['columns'])) { 291 unset($params['columns']); 292 } 293 if (!empty($params['columns_to_display'])) { 294 unset($params['columns_to_display']); 295 } 296 } 297 298 $params[$key] = $value; 299 } 300 301 $paramsKey = self::buildViewDataTableParametersOptionKey($login, $controllerAction, $containerId); 302 303 // when setting an invalid parameter, we fail and let user know 304 self::errorWhenSettingNonOverridableParameter($controllerAction, $params); 305 306 Option::set($paramsKey, json_encode($params)); 307 } 308 309 private static function buildViewDataTableParametersOptionKey($login, $controllerAction, $containerId) 310 { 311 $result = sprintf('viewDataTableParameters_%s_%s', $login, $controllerAction); 312 if (!empty($containerId)) { 313 $result .= '_' . $containerId; 314 } 315 return $result; 316 } 317 318 /** 319 * Display a meaningful error message when any invalid parameter is being set. 320 * 321 * @param $params 322 * @throws 323 */ 324 private static function errorWhenSettingNonOverridableParameter($controllerAction, $params) 325 { 326 $viewDataTable = self::makeTemporaryViewDataTableInstance($controllerAction, $params); 327 $viewDataTable->throwWhenSettingNonOverridableParameter($params); 328 } 329 330 private static function removeNonOverridableParameters($controllerAction, $params) 331 { 332 $viewDataTable = self::makeTemporaryViewDataTableInstance($controllerAction, $params); 333 $nonOverridableParams = $viewDataTable->getNonOverridableParams($params); 334 335 foreach($params as $key => $value) { 336 if(in_array($key, $nonOverridableParams)) { 337 unset($params[$key]); 338 } 339 } 340 return $params; 341 } 342 343 /** 344 * @param $controllerAction 345 * @param $params 346 * @return ViewDataTable 347 * @throws \Exception 348 */ 349 private static function makeTemporaryViewDataTableInstance($controllerAction, $params) 350 { 351 $report = new Report(); 352 $viewDataTableType = isset($params['viewDataTable']) ? $params['viewDataTable'] : $report->getDefaultTypeViewDataTable(); 353 354 $apiAction = $controllerAction; 355 $loadViewDataTableParametersForUser = false; 356 $viewDataTable = Factory::build($viewDataTableType, $apiAction, $controllerAction, $forceDefault = false, $loadViewDataTableParametersForUser); 357 return $viewDataTable; 358 } 359 360 private static function getNormalViewIcons(ViewDataTable $view) 361 { 362 // add normal view icons (eg, normal table, all columns, goals) 363 $normalViewIcons = array( 364 'class' => 'tableAllColumnsSwitch', 365 'buttons' => array(), 366 ); 367 368 if ($view->config->show_table) { 369 $normalViewIcons['buttons'][] = static::getFooterIconFor(HtmlTable::ID); 370 } 371 372 if ($view->config->show_table_all_columns) { 373 $normalViewIcons['buttons'][] = static::getFooterIconFor(HtmlTable\AllColumns::ID); 374 } 375 376 if ($view->config->show_goals) { 377 $goalButton = static::getFooterIconFor(Goals::ID); 378 if (Common::getRequestVar('idGoal', false) == 'ecommerceOrder') { 379 $goalButton['icon'] = 'icon-ecommerce-order'; 380 } 381 382 $normalViewIcons['buttons'][] = $goalButton; 383 } 384 385 if ($view->config->show_ecommerce) { 386 $normalViewIcons['buttons'][] = array( 387 'id' => 'ecommerceOrder', 388 'title' => Piwik::translate('General_EcommerceOrders'), 389 'icon' => 'icon-ecommerce-order', 390 'text' => Piwik::translate('General_EcommerceOrders') 391 ); 392 393 $normalViewIcons['buttons'][] = array( 394 'id' => 'ecommerceAbandonedCart', 395 'title' => Piwik::translate('General_AbandonedCarts'), 396 'icon' => 'icon-ecommerce-abandoned-cart', 397 'text' => Piwik::translate('General_AbandonedCarts') 398 ); 399 } 400 401 $normalViewIcons['buttons'] = array_filter($normalViewIcons['buttons']); 402 403 return $normalViewIcons; 404 } 405 406 private static function getGraphViewIcons(ViewDataTable $view) 407 { 408 // add graph views 409 $graphViewIcons = array( 410 'class' => 'tableGraphViews', 411 'buttons' => array(), 412 ); 413 414 if ($view->config->show_all_views_icons) { 415 if ($view->config->show_bar_chart) { 416 $graphViewIcons['buttons'][] = static::getFooterIconFor(Bar::ID); 417 } 418 419 if ($view->config->show_pie_chart) { 420 $graphViewIcons['buttons'][] = static::getFooterIconFor(Pie::ID); 421 } 422 423 if ($view->config->show_tag_cloud) { 424 $graphViewIcons['buttons'][] = static::getFooterIconFor(Cloud::ID); 425 } 426 } 427 428 return $graphViewIcons; 429 } 430 431 private static function unsetComparisonParams(&$params) 432 { 433 unset($params['compareDates']); 434 unset($params['comparePeriods']); 435 unset($params['compareSegments']); 436 unset($params['compare']); 437 } 438} 439