BxAnalyticsModule.php 59 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477
  1. <?php defined('BX_DOL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * @defgroup Analytics Analytics
  7. * @ingroup UnaModules
  8. *
  9. * @{
  10. */
  11. /**
  12. * Analytics module
  13. */
  14. define('BX_ANALYTICS_SYSTEM', 'system');
  15. define('BX_ANALYTICS_TOP_BY_LIKES', 'top_by_likes');
  16. define('BX_ANALYTICS_TOP_BY_VIEWS', 'top_by_views');
  17. define('BX_ANALYTICS_CONTENT_TOTAL', 'content_total');
  18. define('BX_ANALYTICS_CONTENT_SPEED', 'content_speed');
  19. define('BX_ANALYTICS_MOST_FOLLOWED_PROFILES', 'top_by_followers');
  20. define('BX_ANALYTICS_MOST_ACTIVE_PROFILES', 'top_by_activity');
  21. class BxAnalyticsModule extends BxDolModule
  22. {
  23. protected $aColors = array('#3366CC', '#DC3912', '#FF9900', '#109618', '#990099');
  24. protected $_oReports = null;
  25. /**
  26. * Reports that blocks, Studio and the API may ask for; each is a BxAnalyticsReports method.
  27. */
  28. protected $_aReports = ['filters', 'overview', 'top', 'table', 'realtime', 'map', 'bots', 'sessions', 'users', 'events', 'journeys', 'retention', 'goals', 'funnels'];
  29. /**
  30. * Reports that show members by name.
  31. */
  32. protected $_aPeopleReports = ['sessions', 'users'];
  33. /**
  34. * Studio sections: page => list of [report, block params, spans all columns].
  35. */
  36. protected $_aStudioSections = [
  37. 'overview' => [['filters', [], true], ['overview', [], true], ['top', ['group' => 'pages']], ['top', ['group' => 'sources']], ['top', ['group' => 'locations']], ['top', ['group' => 'devices']], ['top', ['group' => 'platforms']]],
  38. 'realtime' => [['realtime', [], true]],
  39. 'pages' => [['filters', [], true], ['table', ['table' => 'pages'], true]],
  40. 'sources' => [['filters', [], true], ['table', ['table' => 'sources'], true]],
  41. 'audience' => [['filters', [], true], ['top', ['group' => 'locations']], ['top', ['group' => 'devices']], ['top', ['group' => 'platforms']]],
  42. 'map' => [['filters', [], true], ['map', [], true]],
  43. 'bots' => [['filters', [], true], ['bots', [], true]],
  44. 'sessions' => [['filters', [], true], ['sessions', [], true]],
  45. 'users' => [['filters', [], true], ['users', [], true]],
  46. 'events' => [['filters', [], true], ['events', [], true]],
  47. 'journeys' => [['filters', [], true], ['journeys', [], true]],
  48. 'retention' => [['filters', [], true], ['retention', [], true]],
  49. 'goals' => [['filters', [], true], ['goals', [], true]],
  50. 'funnels' => [['filters', [], true], ['funnels', [], true]],
  51. ];
  52. function __construct(&$aModule)
  53. {
  54. parent::__construct($aModule);
  55. }
  56. /**
  57. * Service methods
  58. */
  59. /**
  60. * @page service Service Calls
  61. * @section bx_analytics Analytics
  62. * @subsection bx_analytics-other Other
  63. * @subsubsection bx_analytics get_canvas
  64. *
  65. * @code bx_srv('bx_analytics', 'get_canvas', [...]); @endcode
  66. *
  67. * Get canvas
  68. *
  69. * @return an html.
  70. *
  71. * @see BxAnalyticsModule::serviceGetCanvas
  72. */
  73. /**
  74. * @ref bx_analytics-get_canvas "get_canvas"
  75. */
  76. public function serviceGetCanvas()
  77. {
  78. if(bx_is_api() || $this->checkAllowed() !== CHECK_ACTION_RESULT_ALLOWED)
  79. return bx_is_api() ? [] : '';
  80. return $this->_oTemplate->getCanvas();
  81. }
  82. /**
  83. * @page service Service Calls
  84. * @section bx_analytics Analytics
  85. * @subsection bx_analytics-other Other
  86. * @subsubsection bx_analytics is_avaliable
  87. *
  88. * @code bx_srv('bx_analytics', 'is_avaliable', [...]); @endcode
  89. *
  90. * Check avalability for current profile
  91. *
  92. * @return boolean.
  93. *
  94. * @see BxAnalyticsModule::serviceIsAvaliable
  95. */
  96. /**
  97. * @ref bx_analytics-is_avaliable "is_avaliable"
  98. */
  99. public function serviceIsAvaliable()
  100. {
  101. return $this->checkAllowed() === CHECK_ACTION_RESULT_ALLOWED ? true : false;
  102. }
  103. /**
  104. * @page service Service Calls
  105. * @section bx_analytics Analytics
  106. * @subsection bx_analytics-other Other
  107. * @subsubsection bx_analytics get_modules
  108. *
  109. * @code bx_srv('bx_analytics', 'get_modules', [...]); @endcode
  110. *
  111. * Get modules list
  112. *
  113. * @return an html.
  114. *
  115. * @see BxAnalyticsModule::serviceGetModules
  116. */
  117. /**
  118. * @ref bx_analytics-get_modules "get_modules"
  119. */
  120. public function serviceGetModules()
  121. {
  122. $aResult = array();
  123. $aResult[BX_ANALYTICS_SYSTEM] = _t('_bx_analytics_system_text');
  124. $BxDolModuleQuery = BxDolModuleQuery::getInstance();
  125. $aModules = $BxDolModuleQuery->getModulesBy(array('type' => 'modules', 'active' => 1));
  126. foreach($aModules as $aModule){
  127. $oModule = BxDolModule::getInstance($aModule['name']);
  128. if($oModule instanceof iBxDolContentInfoService){
  129. $aResult[$aModule['name']] = $aModule['title'];
  130. }
  131. }
  132. /**
  133. * @hooks
  134. * @hookdef hook-bx_analytics-get_modules 'bx_analytics', 'get_modules' - hook on get modules list with avalaible analytics
  135. * - $unit_name - equals `get_modules`
  136. * - $action - equals `get_modules`
  137. * - $object_id - not used
  138. * - $sender_id -not used
  139. * - $extra_params - array of additional params with the following array keys:
  140. * - `list` - [array] by ref, array of modules for analytics as [module_name] = module_title, can be overridden in hook processing
  141. * @hook @ref hook-bx_analytics-get_modules
  142. */
  143. bx_alert($this->_aModule['name'], 'get_modules', 0, 0, array(
  144. 'list' => &$aResult,
  145. 'list_ref' => &$aResult,
  146. ));
  147. return $aResult;
  148. }
  149. public function serviceGetSafeServices()
  150. {
  151. return array_merge(parent::serviceGetSafeServices(), [
  152. 'GetReport' => '',
  153. 'GetTrackerConfig' => '',
  154. ]);
  155. }
  156. public function serviceGetPublicServices()
  157. {
  158. return array_merge(parent::serviceGetPublicServices(), [
  159. 'Collect' => '',
  160. ]);
  161. }
  162. /**
  163. * @page service Service Calls
  164. * @section bx_analytics Analytics
  165. * @subsection bx_analytics-tracking Tracking
  166. * @subsubsection bx_analytics-collect collect
  167. *
  168. * @code bx_srv('bx_analytics', 'collect', [$aPayload]); @endcode
  169. * @code api.php?r=bx_analytics/collect with the JSON payload as the request body @endcode
  170. *
  171. * Store pageviews and events sent by NEO, the apps or another API client.
  172. * The payload is described in BxAnalyticsCollector. Over the API the payload is read from
  173. * the request body when no parameter is given.
  174. *
  175. * @param $mixedPayload payload array or JSON string
  176. * @return array with `stored` - the number of hits stored
  177. *
  178. * @see BxAnalyticsModule::serviceCollect
  179. */
  180. /**
  181. * @ref bx_analytics-collect "collect"
  182. */
  183. public function serviceCollect($mixedPayload = '')
  184. {
  185. if(empty($mixedPayload))
  186. $mixedPayload = file_get_contents('php://input');
  187. $aPayload = is_array($mixedPayload) ? $mixedPayload : json_decode((string)$mixedPayload, true);
  188. if(!is_array($aPayload))
  189. return ['stored' => 0];
  190. $iStored = $this->getCollector()->collect($aPayload, [
  191. 'platform' => BX_ANALYTICS_PLATFORM_API,
  192. 'trusted' => $this->_isApiKeyRequest(),
  193. 'profile_id' => bx_get_logged_profile_id(),
  194. ]);
  195. return ['stored' => $iStored];
  196. }
  197. /**
  198. * @page service Service Calls
  199. * @section bx_analytics Analytics
  200. * @subsection bx_analytics-tracking Tracking
  201. * @subsubsection bx_analytics-get_tracker_config get_tracker_config
  202. *
  203. * @code bx_srv('bx_analytics', 'get_tracker_config', ['rn-ios']); @endcode
  204. * @code api.php?r=bx_analytics/get_tracker_config&params[]=next-web @endcode
  205. *
  206. * Tracker settings for NEO and the apps, read once when the client starts:
  207. * whether to send hits at all, whether to keep a visitor id (persistent identity mode),
  208. * whether to honour Do Not Track, and the session timeout.
  209. *
  210. * @param $sPlatform client platform code (next-web, rn-ios, rn-android, api); when empty,
  211. * the platform of the request
  212. * @return array with `enabled`, `persistent`, `respect_dnt` (booleans) and
  213. * `session_timeout` (seconds)
  214. *
  215. * @see BxAnalyticsModule::serviceGetTrackerConfig
  216. */
  217. /**
  218. * @ref bx_analytics-get_tracker_config "get_tracker_config"
  219. */
  220. public function serviceGetTrackerConfig($sPlatform = '')
  221. {
  222. if(!is_string($sPlatform) || $sPlatform === '') {
  223. bx_import('Request', $this->_aModule);
  224. $oRequest = new BxAnalyticsRequest();
  225. $sPlatform = $oRequest->getPlatform(defined('BX_API'), $this->_isApiKeyRequest());
  226. }
  227. // `core` is the server's own platform, never a client's
  228. $bEnabled = $this->_oConfig->isTrackingEnabled() && $sPlatform != BX_ANALYTICS_PLATFORM_CORE && $this->_oConfig->isPlatformEnabled($sPlatform);
  229. return [
  230. 'enabled' => $bEnabled,
  231. 'persistent' => $bEnabled && $this->_oConfig->getIdentityMode() == BX_ANALYTICS_IDENTITY_PERSISTENT,
  232. 'respect_dnt' => $this->_oConfig->isRespectDnt(),
  233. 'session_timeout' => $this->_oConfig->getSessionTimeout(),
  234. ];
  235. }
  236. /**
  237. * Hits from the classic tracker: same origin, a JSON body sent with sendBeacon.
  238. */
  239. public function actionCollect()
  240. {
  241. $aPayload = json_decode((string)file_get_contents('php://input'), true);
  242. if(is_array($aPayload))
  243. $this->getCollector()->collect($aPayload, [
  244. 'platform' => BX_ANALYTICS_PLATFORM_CORE_WEB,
  245. 'same_origin' => true,
  246. 'profile_id' => bx_get_logged_profile_id(),
  247. ]);
  248. header('HTTP/1.1 204 No Content');
  249. header('Cache-Control: no-store');
  250. exit;
  251. }
  252. /**
  253. * @page service Service Calls
  254. * @section bx_analytics Analytics
  255. * @subsection bx_analytics-tracking Tracking
  256. * @subsubsection bx_analytics-get_tracker_code get_tracker_code
  257. *
  258. * @code bx_srv('bx_analytics', 'get_tracker_code', [...]); @endcode
  259. *
  260. * The classic UI tracker, injected at the end of every page.
  261. *
  262. * @return HTML string, empty when tracking is off for the classic UI or for the viewer
  263. *
  264. * @see BxAnalyticsModule::serviceGetTrackerCode
  265. */
  266. /**
  267. * @ref bx_analytics-get_tracker_code "get_tracker_code"
  268. */
  269. public function serviceGetTrackerCode()
  270. {
  271. if(!$this->_oConfig->isTrackingEnabled() || !$this->_oConfig->isPlatformEnabled(BX_ANALYTICS_PLATFORM_CORE_WEB))
  272. return '';
  273. // bots don't run the tracker: the page counts them as it renders
  274. bx_import('UserAgent', $this->_aModule);
  275. $sUserAgent = isset($_SERVER['HTTP_USER_AGENT']) ? (string)$_SERVER['HTTP_USER_AGENT'] : '';
  276. if(BxAnalyticsUserAgent::isBot($sUserAgent)) {
  277. $this->getCollector()->collectBot($sUserAgent, isset($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : '');
  278. return '';
  279. }
  280. $aLevels = $this->_oConfig->getExcludedLevels();
  281. if(!empty($aLevels)) {
  282. $iProfileId = bx_get_logged_profile_id();
  283. $aMembership = BxDolAcl::getInstance()->getMemberMembershipInfo($iProfileId ? $iProfileId : 0);
  284. if(!empty($aMembership['id']) && in_array((int)$aMembership['id'], $aLevels, true))
  285. return '';
  286. }
  287. return $this->_oTemplate->getTrackerCode();
  288. }
  289. /**
  290. * @page service Service Calls
  291. * @section bx_analytics Analytics
  292. * @subsection bx_analytics-reports Reports
  293. * @subsubsection bx_analytics-get_report get_report
  294. *
  295. * @code bx_srv('bx_analytics', 'get_report', ['overview', ['range' => '30d', 'platform' => 'rn-ios']]); @endcode
  296. * @code api.php?r=bx_analytics/get_report&params[]=top&params[]={"group":"pages","tab":"entry","range":"7d"} @endcode
  297. *
  298. * Report data for NEO and other clients. Reports: filters, overview, top, table, realtime
  299. * and the other sections listed in BxAnalyticsModule::$_aReports.
  300. *
  301. * @param $sReport report name
  302. * @param $mixedParams filter parameters (see BxAnalyticsFilter) and the report's own
  303. * (group, table, tab, page, limit...); an array or a JSON string
  304. * @return array report data, or an error array when the viewer may not use analytics
  305. *
  306. * @see BxAnalyticsModule::serviceGetReport
  307. */
  308. /**
  309. * @ref bx_analytics-get_report "get_report"
  310. */
  311. public function serviceGetReport($sReport, $mixedParams = [])
  312. {
  313. if(!$this->serviceIsAvaliable())
  314. return ['error' => _t('_sys_txt_access_denied'), 'code' => 403];
  315. $aParams = is_array($mixedParams) ? $mixedParams : json_decode((string)$mixedParams, true);
  316. if(!is_array($aParams))
  317. $aParams = [];
  318. $aData = $this->getReportData($sReport, $aParams);
  319. if($aData === false)
  320. return ['error' => _t('_sys_request_page_not_found_cpt'), 'code' => 404];
  321. return $aData;
  322. }
  323. /**
  324. * A report block reloaded by the dashboard script: data plus rendered HTML. The block's
  325. * own parameters (group, table...) come as JSON in `bp`, the filters as the page's URL
  326. * parameters.
  327. */
  328. public function actionReport($sReport = '')
  329. {
  330. header('Content-Type: application/json; charset=utf-8');
  331. header('Cache-Control: no-store');
  332. if(!$this->serviceIsAvaliable()) {
  333. http_response_code(403);
  334. echo json_encode(['error' => _t('_sys_txt_access_denied')]);
  335. exit;
  336. }
  337. $aBlockParams = json_decode((string)bx_get('bp'), true);
  338. if(!is_array($aBlockParams))
  339. $aBlockParams = [];
  340. $aParams = array_merge($_GET, $aBlockParams);
  341. unset($aParams['bp']);
  342. $aData = $this->getReportData($sReport, $aParams);
  343. if($aData === false) {
  344. http_response_code(404);
  345. echo json_encode(['error' => _t('_sys_request_page_not_found_cpt')]);
  346. exit;
  347. }
  348. echo json_encode([
  349. 'data' => $aData,
  350. 'html' => $this->_oTemplate->getReportHtml($sReport, $aData, $this->getFilter($aParams), $aBlockParams),
  351. ]);
  352. exit;
  353. }
  354. /**
  355. * @page service Service Calls
  356. * @section bx_analytics Analytics
  357. * @subsection bx_analytics-blocks Blocks
  358. * @subsubsection bx_analytics-get_block_filters get_block_filters
  359. *
  360. * @code bx_srv('bx_analytics', 'get_block_filters'); @endcode
  361. *
  362. * The filter bar: period, platform, active filters and visitors online. It keeps the
  363. * filters in the page URL, and every analytics block on the page follows them.
  364. *
  365. * @return HTML string, or a block description for the API; empty for viewers without access
  366. *
  367. * @see BxAnalyticsModule::serviceGetBlockFilters
  368. */
  369. /**
  370. * @ref bx_analytics-get_block_filters "get_block_filters"
  371. */
  372. public function serviceGetBlockFilters()
  373. {
  374. return $this->_getBlock('filters');
  375. }
  376. /**
  377. * @page service Service Calls
  378. * @section bx_analytics Analytics
  379. * @subsection bx_analytics-blocks Blocks
  380. * @subsubsection bx_analytics-get_block_overview get_block_overview
  381. *
  382. * @code bx_srv('bx_analytics', 'get_block_overview'); @endcode
  383. *
  384. * Key figures with the change against the previous period, and the main chart.
  385. *
  386. * @return HTML string, or a block description for the API; empty for viewers without access
  387. *
  388. * @see BxAnalyticsModule::serviceGetBlockOverview
  389. */
  390. /**
  391. * @ref bx_analytics-get_block_overview "get_block_overview"
  392. */
  393. public function serviceGetBlockOverview()
  394. {
  395. return $this->_getBlock('overview');
  396. }
  397. /**
  398. * @page service Service Calls
  399. * @section bx_analytics Analytics
  400. * @subsection bx_analytics-blocks Blocks
  401. * @subsubsection bx_analytics-get_block_top get_block_top
  402. *
  403. * @code bx_srv('bx_analytics', 'get_block_top', ['sources']); @endcode
  404. *
  405. * A tabbed top list: pages, sources, locations, devices or platforms.
  406. *
  407. * @param $sGroup list group
  408. * @return HTML string, or a block description for the API; empty for viewers without access
  409. *
  410. * @see BxAnalyticsModule::serviceGetBlockTop
  411. */
  412. /**
  413. * @ref bx_analytics-get_block_top "get_block_top"
  414. */
  415. public function serviceGetBlockTop($sGroup = 'pages')
  416. {
  417. return $this->_getBlock('top', ['group' => $sGroup]);
  418. }
  419. /**
  420. * @page service Service Calls
  421. * @section bx_analytics Analytics
  422. * @subsection bx_analytics-blocks Blocks
  423. * @subsubsection bx_analytics-get_block_table get_block_table
  424. *
  425. * @code bx_srv('bx_analytics', 'get_block_table', ['pages']); @endcode
  426. *
  427. * A full table with every metric, a page at a time: pages or sources.
  428. *
  429. * @param $sTable table name
  430. * @return HTML string, or a block description for the API; empty for viewers without access
  431. *
  432. * @see BxAnalyticsModule::serviceGetBlockTable
  433. */
  434. /**
  435. * @ref bx_analytics-get_block_table "get_block_table"
  436. */
  437. public function serviceGetBlockTable($sTable = 'pages')
  438. {
  439. return $this->_getBlock('table', ['table' => $sTable]);
  440. }
  441. /**
  442. * @page service Service Calls
  443. * @section bx_analytics Analytics
  444. * @subsection bx_analytics-blocks Blocks
  445. * @subsubsection bx_analytics-get_block_report get_block_report
  446. *
  447. * @code bx_srv('bx_analytics', 'get_block_report', ['realtime']); @endcode
  448. *
  449. * Any other report as a block: realtime, map, bots, sessions, users, events, goals,
  450. * funnels, journeys, retention, revenue, subscriptions, sales, credits, errors,
  451. * performance, query.
  452. *
  453. * @param $sReport report name
  454. * @param $aParams the report's own parameters
  455. * @return HTML string, or a block description for the API; empty for viewers without access
  456. *
  457. * @see BxAnalyticsModule::serviceGetBlockReport
  458. */
  459. /**
  460. * @ref bx_analytics-get_block_report "get_block_report"
  461. */
  462. public function serviceGetBlockReport($sReport, $aParams = [])
  463. {
  464. return $this->_getBlock($sReport, is_array($aParams) ? $aParams : []);
  465. }
  466. /**
  467. * A Studio section: the filter bar (where the section uses it) and the section's blocks.
  468. */
  469. public function getStudioSection($sSection)
  470. {
  471. if(!isset($this->_aStudioSections[$sSection]))
  472. return '';
  473. // one report under the filter bar needs no heading: the section title names it
  474. $iReports = 0;
  475. foreach($this->_aStudioSections[$sSection] as $aBlock)
  476. if($aBlock[0] != 'filters')
  477. $iReports++;
  478. $aBlocks = [];
  479. foreach($this->_aStudioSections[$sSection] as $aBlock) {
  480. list($sReport, $aParams, $bWide) = array_pad($aBlock, 3, false);
  481. $sTitle = '';
  482. if($iReports > 1 && $sReport == 'top')
  483. $sTitle = _t('_bx_analytics_group_' . $aParams['group']);
  484. elseif($iReports > 1 && !in_array($sReport, ['filters', 'overview', 'table'], true))
  485. $sTitle = _t('_bx_analytics_report_' . (isset($aParams['title']) ? $aParams['title'] : $sReport));
  486. $aBlocks[] = [
  487. 'title' => $sTitle,
  488. 'content' => $this->_getBlock($sReport, $aParams ? $aParams : []),
  489. 'wide' => (bool)$bWide,
  490. ];
  491. }
  492. return $this->_oTemplate->getStudioSection($aBlocks);
  493. }
  494. /**
  495. * A report as a page block: a description NEO renders from for the API, HTML otherwise.
  496. */
  497. protected function _getBlock($sReport, $aBlockParams = [])
  498. {
  499. if(!$this->serviceIsAvaliable() || !in_array($sReport, $this->_aReports, true))
  500. return bx_is_api() ? [] : '';
  501. if(in_array($sReport, $this->_aPeopleReports, true) && $this->checkAllowedPeople() !== CHECK_ACTION_RESULT_ALLOWED)
  502. return bx_is_api() ? [] : '';
  503. if(bx_is_api())
  504. return [bx_api_get_block('analytics_' . $sReport, $this->_getApiBlockData($sReport, $aBlockParams))];
  505. $aParams = array_merge($_GET, $aBlockParams);
  506. $aData = $this->getReportData($sReport, $aParams);
  507. if($aData === false)
  508. return '';
  509. return $this->_oTemplate->getBlock($sReport, $aData, $this->getFilter($aParams), $aBlockParams);
  510. }
  511. /**
  512. * @page service Service Calls
  513. * @section bx_analytics Analytics
  514. * @subsection bx_analytics-blocks Blocks
  515. * @subsubsection bx_analytics-get_studio_dashboard_traffic get_studio_dashboard_traffic
  516. *
  517. * @code bx_srv('bx_analytics', 'get_studio_dashboard_traffic'); @endcode
  518. *
  519. * Traffic block of the Studio dashboard: visitors online on every platform and the last 7 days.
  520. *
  521. * @return array with `content` for the Studio dashboard
  522. *
  523. * @see BxAnalyticsModule::serviceGetStudioDashboardTraffic
  524. */
  525. /**
  526. * @ref bx_analytics-get_studio_dashboard_traffic "get_studio_dashboard_traffic"
  527. */
  528. public function serviceGetStudioDashboardTraffic()
  529. {
  530. if(!isAdmin())
  531. return '';
  532. $oFilter = $this->getFilter(['range' => '7d']);
  533. $oReports = $this->getReports();
  534. return [
  535. 'content' => $this->_oTemplate->getStudioDashboardTraffic($oReports->getOverview($oFilter), $oReports->getRealtime($oFilter, ['limit' => 0]), $oReports->getTop($oFilter, ['group' => 'platforms', 'limit' => 5])),
  536. ];
  537. }
  538. /**
  539. * @page service Service Calls
  540. * @section bx_analytics Analytics
  541. * @subsection bx_analytics-other Other
  542. * @subsubsection bx_analytics-pruning pruning
  543. *
  544. * @code bx_srv('bx_analytics', 'pruning'); @endcode
  545. *
  546. * Hourly cron job: deletes old daily salts, then data past the retention period in
  547. * small batches, within a time limit so other cron jobs aren't held up.
  548. *
  549. * @return int rows deleted
  550. *
  551. * @see BxAnalyticsModule::servicePruning
  552. */
  553. /**
  554. * @ref bx_analytics-pruning "pruning"
  555. */
  556. public function servicePruning()
  557. {
  558. $CNF = &$this->_oConfig->CNF;
  559. // yesterday's salt is still needed to continue visits that crossed midnight
  560. $this->_oDb->deleteSaltsBefore(gmdate('Y-m-d', time() - 86400));
  561. $iBefore = time() - 86400 * $this->_oConfig->getRetentionDays();
  562. $this->_oDb->deleteBotsBefore(gmdate('Y-m-d', $iBefore));
  563. $iDeadline = time() + $CNF['PRUNING_SECONDS'];
  564. $iDeleted = 0;
  565. foreach(['deleteEventsBefore', 'deleteSessionsBefore'] as $sMethod)
  566. do {
  567. $iBatch = $this->_oDb->$sMethod($iBefore, $CNF['PRUNING_BATCH']);
  568. $iDeleted += $iBatch;
  569. } while($iBatch >= $CNF['PRUNING_BATCH'] && time() < $iDeadline);
  570. return $iDeleted;
  571. }
  572. /**
  573. * @page service Service Calls
  574. * @section bx_analytics Analytics
  575. * @subsection bx_analytics-tracking Tracking
  576. * @subsubsection bx_analytics-sync_actions sync_actions
  577. *
  578. * @code bx_srv('bx_analytics', 'sync_actions'); @endcode
  579. *
  580. * Subscribe to "added" of every content module on the site, so new content becomes an
  581. * event (comments and votes have site-wide alerts). Runs when the module is enabled and
  582. * whenever another module is enabled or disabled.
  583. *
  584. * @return int subscriptions written
  585. *
  586. * @see BxAnalyticsModule::serviceSyncActions
  587. */
  588. /**
  589. * @ref bx_analytics-sync_actions "sync_actions"
  590. */
  591. public function serviceSyncActions()
  592. {
  593. $aUnits = [];
  594. foreach(BxDolModuleQuery::getInstance()->getModulesBy(['type' => 'modules', 'active' => 1]) as $aModule) {
  595. $oModule = BxDolModule::getInstance($aModule['name']);
  596. if($oModule instanceof iBxDolContentInfoService)
  597. $aUnits[$aModule['name']][] = 'added';
  598. }
  599. $iWritten = $this->_oDb->setActionAlerts($this->getName(), ['added'], $aUnits, ['comment']);
  600. BxDolAlerts::cacheInvalidate();
  601. return $iWritten;
  602. }
  603. /**
  604. * Alerts the module listens to: deleted profiles, modules switched on and off, and the
  605. * site actions it records as events.
  606. */
  607. public function onAlert($oAlert)
  608. {
  609. $sUnit = $oAlert->sUnit;
  610. $sAction = $oAlert->sAction;
  611. if($sUnit == 'profile' && $sAction == 'delete') {
  612. $this->onProfileDelete($oAlert->iObject);
  613. return;
  614. }
  615. if($sUnit == 'system' && in_array($sAction, ['enable', 'disable'], true)) {
  616. if(empty($oAlert->aExtras['config']['name']) || $oAlert->aExtras['config']['name'] != $this->getName())
  617. $this->serviceSyncActions();
  618. return;
  619. }
  620. $aContext = ['api_key' => $this->_isApiKeyRequest()];
  621. $iSender = (int)$oAlert->iSender;
  622. switch($sAction) {
  623. case 'add':
  624. if($sUnit == 'account') {
  625. $this->getCollector()->collectAction('signup', 'account', $oAlert->iObject, 0, $aContext);
  626. return;
  627. }
  628. if($sUnit == 'profile') {
  629. $sType = isset($oAlert->aExtras['module']) ? $oAlert->aExtras['module'] : '';
  630. $this->getCollector()->collectAction('profile_add', $sType, $oAlert->iObject, $oAlert->iObject, $aContext);
  631. }
  632. return;
  633. case 'login':
  634. if($sUnit == 'account') {
  635. $oProfile = BxDolProfile::getInstanceAccountProfile($oAlert->iObject);
  636. $this->getCollector()->collectAction('login', 'account', $oAlert->iObject, $oProfile ? $oProfile->id() : 0, $aContext);
  637. }
  638. return;
  639. case 'connection_added':
  640. if($sUnit == 'system')
  641. $this->getCollector()->collectAction('connection', isset($oAlert->aExtras['object_name']) ? $oAlert->aExtras['object_name'] : '', isset($oAlert->aExtras['content']) ? (int)$oAlert->aExtras['content'] : 0, $iSender, $aContext);
  642. return;
  643. case 'added':
  644. // comments of any system, or new content of a module
  645. if($sUnit == 'comment')
  646. $this->getCollector()->collectAction('comment', isset($oAlert->aExtras['object_system']) ? $oAlert->aExtras['object_system'] : '', isset($oAlert->aExtras['object_id']) ? (int)$oAlert->aExtras['object_id'] : 0, $iSender, $aContext);
  647. else
  648. $this->getCollector()->collectAction('content_add', $sUnit, $oAlert->iObject, $iSender, $aContext);
  649. return;
  650. case 'do':
  651. if($sUnit == 'vote') {
  652. $aProps = [];
  653. if(!empty($oAlert->aExtras['reaction']) && is_string($oAlert->aExtras['reaction']))
  654. $aProps['reaction'] = $oAlert->aExtras['reaction'];
  655. $this->getCollector()->collectAction('vote', isset($oAlert->aExtras['object_system']) ? $oAlert->aExtras['object_system'] : '', isset($oAlert->aExtras['object_id']) ? (int)$oAlert->aExtras['object_id'] : 0, $iSender, $aContext + ['props' => $aProps]);
  656. }
  657. return;
  658. }
  659. }
  660. /**
  661. * Forget a deleted profile: its visits stay in reports as an anonymous visitor's.
  662. */
  663. public function onProfileDelete($iProfileId)
  664. {
  665. $iProfileId = (int)$iProfileId;
  666. if(!$iProfileId)
  667. return 0;
  668. return $this->_oDb->forgetProfile($iProfileId, random_bytes(16));
  669. }
  670. public function serviceGetOptionsPlatforms()
  671. {
  672. $aResult = [];
  673. foreach($this->_oConfig->getPlatforms() as $sCode => $sTitle)
  674. $aResult[$sCode] = _t($sTitle);
  675. return $aResult;
  676. }
  677. public function serviceGetOptionsIdentity()
  678. {
  679. return [
  680. BX_ANALYTICS_IDENTITY_COOKIELESS => _t('_bx_analytics_identity_cookieless'),
  681. BX_ANALYTICS_IDENTITY_PERSISTENT => _t('_bx_analytics_identity_persistent'),
  682. ];
  683. }
  684. public function serviceGetOptionsGeoSource()
  685. {
  686. return [
  687. BX_ANALYTICS_GEO_NONE => _t('_bx_analytics_geo_none'),
  688. BX_ANALYTICS_GEO_HEADERS => _t('_bx_analytics_geo_headers'),
  689. ];
  690. }
  691. public function getCollector()
  692. {
  693. bx_import('UserAgent', $this->_aModule);
  694. bx_import('Source', $this->_aModule);
  695. bx_import('Request', $this->_aModule);
  696. bx_import('Collector', $this->_aModule);
  697. return new BxAnalyticsCollector($this);
  698. }
  699. public function getFilter($aParams)
  700. {
  701. bx_import('Filter', $this->_aModule);
  702. return new BxAnalyticsFilter($this, $aParams);
  703. }
  704. public function getReports()
  705. {
  706. if($this->_oReports === null) {
  707. bx_import('Reports', $this->_aModule);
  708. $this->_oReports = new BxAnalyticsReports($this);
  709. }
  710. return $this->_oReports;
  711. }
  712. /**
  713. * Report data by name, or false for an unknown report or parameters it doesn't know.
  714. */
  715. public function getReportData($sReport, $aParams)
  716. {
  717. if(!in_array($sReport, $this->_aReports, true))
  718. return false;
  719. if(in_array($sReport, $this->_aPeopleReports, true) && $this->checkAllowedPeople() !== CHECK_ACTION_RESULT_ALLOWED)
  720. return false;
  721. $oReports = $this->getReports();
  722. $sMethod = 'get' . bx_gen_method_name($sReport);
  723. if(!method_exists($oReports, $sMethod))
  724. return false;
  725. $oFilter = $this->getFilter($aParams);
  726. $aData = $oReports->$sMethod($oFilter, $aParams);
  727. if($aData === false)
  728. return false;
  729. $aData['filter'] = $oFilter->getParams();
  730. return $aData;
  731. }
  732. /**
  733. * What an API block carries: the report and the endpoint NEO calls for its data.
  734. */
  735. protected function _getApiBlockData($sReport, $aParams = [])
  736. {
  737. $aRanges = [];
  738. foreach(['today', 'yesterday', '24h', '7d', '30d', '90d', '12m'] as $sRange)
  739. $aRanges[] = ['value' => $sRange, 'title' => _t('_bx_analytics_range_' . $sRange)];
  740. $aPlatforms = [];
  741. foreach($this->_oConfig->getPlatforms() as $sCode => $sTitle)
  742. $aPlatforms[] = ['value' => $sCode, 'title' => _t($sTitle)];
  743. $aMetrics = [];
  744. foreach($this->getReports()->getMetrics() as $sMetric)
  745. $aMetrics[$sMetric] = _t('_bx_analytics_metric_' . $sMetric);
  746. return [
  747. 'report' => $sReport,
  748. 'params' => (object)$aParams,
  749. // the element asks for its data itself, with the page's filters, from get_report
  750. 'request_url' => 'bx_analytics/get_report',
  751. 'ranges' => $aRanges,
  752. 'platforms' => $aPlatforms,
  753. 'metrics' => $aMetrics,
  754. 'strings' => [
  755. 'range' => _t('_bx_analytics_txt_range'),
  756. 'platform' => _t('_bx_analytics_txt_platform'),
  757. 'all_platforms' => _t('_bx_analytics_txt_all_platforms'),
  758. 'online' => _t('_bx_analytics_txt_online_now', '{0}'),
  759. 'remove_filter' => _t('_bx_analytics_txt_remove_filter', '{0}'),
  760. 'filter_by' => _t('_bx_analytics_txt_filter_by', '{0}'),
  761. 'previous_period' => _t('_bx_analytics_txt_previous_period', '{0}'),
  762. 'key_figures' => _t('_bx_analytics_txt_key_figures'),
  763. 'visitors' => _t('_bx_analytics_metric_visitors'),
  764. 'no_data' => _t('_bx_analytics_txt_no_data'),
  765. // NEO shows a Retry button next to it, so no "reload the page"
  766. 'error' => _t('_bx_analytics_txt_error_report'),
  767. 'duration_h' => _t('_bx_analytics_txt_duration_h'),
  768. 'duration_m' => _t('_bx_analytics_txt_duration_m'),
  769. 'duration_s' => _t('_bx_analytics_txt_duration_s'),
  770. ],
  771. ];
  772. }
  773. /**
  774. * The request came through api.php with an API key that api.php has already checked:
  775. * the NEO server, whose proxy headers carry the visitor's address.
  776. */
  777. protected function _isApiKeyRequest()
  778. {
  779. if(!defined('BX_API') || !getParam('sys_api_access_by_key'))
  780. return false;
  781. $sAuth = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] : '';
  782. if($sAuth === '' && function_exists('getallheaders')) {
  783. $aHeaders = array_change_key_case((array)getallheaders(), CASE_LOWER);
  784. $sAuth = isset($aHeaders['authorization']) ? $aHeaders['authorization'] : '';
  785. }
  786. return strpos($sAuth, 'Bearer ') === 0;
  787. }
  788. /**
  789. * Action methods
  790. */
  791. public function actionGetReports($sModuleName)
  792. {
  793. if(!$this->serviceIsAvaliable())
  794. return '';
  795. header('Content-Type: application/json');
  796. $aSystemsVote = BxDolVote::getSystems();
  797. $aRv = array();
  798. if ($sModuleName == BX_ANALYTICS_SYSTEM){
  799. $aRv[BX_ANALYTICS_TOP_BY_LIKES] = _t('_bx_analytics_type_top_contents_by_likes');
  800. $aRv[BX_ANALYTICS_TOP_BY_VIEWS] = _t('_bx_analytics_type_top_by_views');
  801. $aRv[BX_ANALYTICS_CONTENT_TOTAL] = _t('_bx_analytics_type_accounts_growth');
  802. $aRv[BX_ANALYTICS_CONTENT_SPEED] = _t('_bx_analytics_type_accounts_speed');
  803. }
  804. else{
  805. $oModule = BxDolModule::getInstance($sModuleName);
  806. if(BxDolRequest::serviceExists($sModuleName, 'act_as_profile') && BxDolService::call($sModuleName, 'act_as_profile') == true){
  807. $aRv[BX_ANALYTICS_MOST_FOLLOWED_PROFILES] = _t('_bx_analytics_type_most_followed_profiles');
  808. $aRv[BX_ANALYTICS_MOST_ACTIVE_PROFILES] = _t('_bx_analytics_type_most_active_profiles');
  809. }
  810. if(isset($oModule->_oConfig->CNF['OBJECT_VOTES']) && isset($aSystemsVote[$oModule->_oConfig->CNF['OBJECT_VOTES']])){
  811. $aRv[BX_ANALYTICS_TOP_BY_LIKES] = _t('_bx_analytics_type_top_contents_by_likes');
  812. }
  813. if(isset($oModule->_oConfig->CNF['OBJECT_VIEWS'])){
  814. $aRv[BX_ANALYTICS_TOP_BY_VIEWS] = _t('_bx_analytics_type_top_by_views');
  815. }
  816. $aRv[BX_ANALYTICS_CONTENT_TOTAL] = _t('_bx_analytics_type_content_growth');
  817. $aRv[BX_ANALYTICS_CONTENT_SPEED] = _t('_bx_analytics_type_content_speed');
  818. }
  819. /**
  820. * @hooks
  821. * @hookdef hook-bx_analytics-get_reports 'bx_analytics', 'get_reports' - hook on get avalaible reports for specified module
  822. * - $unit_name - equals `bx_analytics`
  823. * - $action - equals `get_reports`
  824. * - $object_id - not used
  825. * - $sender_id -not used
  826. * - $extra_params - array of additional params with the following array keys:
  827. * - `module` - [string] module name
  828. * - `list` - [array] by ref, array of avalaible reports for analytics as [report_name] = report_title, can be overridden in hook processing
  829. * @hook @ref hook-bx_analytics-get_reports
  830. */
  831. bx_alert($this->_aModule['name'], 'get_reports', 0, 0, array(
  832. 'module' => $sModuleName,
  833. 'list' => &$aRv,
  834. 'list_ref' => &$aRv,
  835. ));
  836. echo json_encode($aRv);
  837. }
  838. public function actionGetReportsData($sModuleName, $sReportName, $sDateFrom, $sDateTo, $sType = '')
  839. {
  840. if(!$this->serviceIsAvaliable())
  841. return '';
  842. if ($sType == 'csv'){
  843. header('Content-type: text/csv');
  844. header('Content-disposition: attachment;filename=' . $sModuleName . '_' . $sReportName . '_' . $sDateFrom . '_' . $sDateTo . '.csv');
  845. echo $this->actionGetReportsDataCsv($sModuleName, $sReportName, $sDateFrom, $sDateTo);
  846. }
  847. if ($sType == ''){
  848. header('Content-Type: application/json');
  849. echo $this->actionGetReportsDataJson($sModuleName, $sReportName, $sDateFrom, $sDateTo);
  850. }
  851. }
  852. private function actionGetReportsDataCsv($sModuleName, $sReportName, $sDateFrom, $sDateTo)
  853. {
  854. $sRv = '';
  855. $sColumn = ";";
  856. $sRow = "\n";
  857. $sData = $this->actionGetReportsDataJson($sModuleName, $sReportName, $sDateFrom, $sDateTo);
  858. $oRv = json_decode($sData, true);
  859. $oData = $oRv['data'];
  860. $sCol1 = $oRv['strings'][0];
  861. $iColumnCount = count($oData['datasets']);
  862. $sRv .= $sCol1 . $sColumn;
  863. foreach($oData['datasets'] as $aItem){
  864. $sRv .= ($aItem['label'] != '' ? $aItem['label'] : $oRv['strings'][1]) . $sColumn;
  865. }
  866. $sRv .= $sRow;
  867. $iK = 0;
  868. foreach($oData['datasets'][0]['data'] as $sDx => $aItemData){
  869. $sTxt = "";
  870. if (is_array($oData['datasets'][0]['data'][$sDx]))
  871. $sTxt = $oData['datasets'][0]['data'][$sDx]['x'];
  872. else
  873. $sTxt = $oData['labels'][$sDx];
  874. if (isset($oRv['links']) && count($oRv['links'])>0) {
  875. $sTxt .= ' (' . $oRv['links'][$sDx] . ')';
  876. }
  877. $sRv .= $sTxt . $sColumn;
  878. for ($i = 0; $i < $iColumnCount; $i++){
  879. $sText = '';
  880. if (is_array($aItemData)) {
  881. $sText = $aItemData['y'];
  882. }
  883. else {
  884. $sText = $aItemData;
  885. }
  886. $sRv .= $sText . $sColumn;
  887. }
  888. $sRv .= $sRow;
  889. $iK++;
  890. }
  891. echo $sRv;
  892. }
  893. private function actionGetReportsDataJson($sModuleName, $sReportName, $sDateFrom, $sDateTo)
  894. {
  895. $iDateFrom = strtotime($sDateFrom);
  896. $iDateTo = strtotime($sDateTo) + 86400;
  897. $sType = "bar";
  898. $bIsTimeX = false;
  899. $iMinValueY = 0;
  900. $iMaxValueY = 0;
  901. $bDefaultFillData = true;
  902. $sReportType = $sReportName;
  903. $aValues = array('labels' => array(), 'values' => array(array('legend' => '', 'data' => array())), 'links' => array(), 'strings' => array(0 => _t('_bx_analytics_txt_item'), 1 => _t('_bx_analytics_txt_value')));
  904. if ($sModuleName != BX_ANALYTICS_SYSTEM){
  905. $oModule = BxDolModule::getInstance($sModuleName);
  906. $aValues['strings'][0] = $oModule->_aModule['title'];
  907. $aValues['strings'][1] = _t('_bx_analytics_type_' . $sReportType . '_label');
  908. }
  909. else{
  910. $aValues['strings'][0] = _t('_bx_analytics_txt_total_content');
  911. $aValues['strings'][1] = _t('_bx_analytics_type_' . $sReportType . '_label');
  912. }
  913. switch ($sReportType) {
  914. case BX_ANALYTICS_TOP_BY_LIKES:
  915. $bDefaultFillData = false;
  916. $this->getViewsOrVotes(BxDolVote::getSystems(), 'OBJECT_VOTES', $sModuleName, $iDateFrom, $iDateTo, $aValues, $iMaxValueY, $iMinValueY);
  917. break;
  918. case BX_ANALYTICS_TOP_BY_VIEWS:
  919. $bDefaultFillData = false;
  920. $this->getViewsOrVotes(BxDolView::getSystems(), 'OBJECT_VIEWS', $sModuleName, $iDateFrom, $iDateTo, $aValues, $iMaxValueY, $iMinValueY);
  921. break;
  922. case BX_ANALYTICS_MOST_FOLLOWED_PROFILES:
  923. $oModule = BxDolModule::getInstance($sModuleName);
  924. $aData = $this->_oDb->getMostFollowedProfiles($sModuleName, $iDateFrom, $iDateTo);
  925. break;
  926. case BX_ANALYTICS_MOST_ACTIVE_PROFILES:
  927. $aData = array();
  928. $oModule = BxDolModule::getInstance($sModuleName);
  929. $aTmp = $this->getSelectedModules();
  930. foreach($aTmp[0] as $sContentModule){
  931. $oContentModule = BxDolModule::getInstance($sContentModule);
  932. if ($oContentModule && isset($oContentModule->_oConfig->CNF['TABLE_ENTRIES']) && isset($oContentModule->_oConfig->CNF['FIELD_AUTHOR'])){
  933. $sContentTable = $oContentModule->_oConfig->CNF['TABLE_ENTRIES'];
  934. $sColumnAuthor = $oContentModule->_oConfig->CNF['FIELD_AUTHOR'];
  935. $sColumnAdded = $oContentModule->_oConfig->CNF['FIELD_ADDED'];
  936. if (!empty($sContentTable) && !empty($sColumnAuthor) && !empty($sColumnAdded) && (!BxDolRequest::serviceExists($sContentModule, 'act_as_profile') || BxDolService::call($sContentModule, 'act_as_profile') == false)){
  937. $aData1 = $this->_oDb->getMostActiveProfiles($sModuleName, $sContentModule, $sContentTable, $sColumnAuthor, $sColumnAdded, $iDateFrom, $iDateTo);
  938. foreach($aData1 as $aTmp3){
  939. if (array_key_exists($aTmp3['object_id'], $aData))
  940. $aData[$aTmp3['object_id']]['value'] += $aTmp3['value'];
  941. else
  942. $aData[$aTmp3['object_id']] = $aTmp3;
  943. }
  944. }
  945. }
  946. }
  947. $aData = $this->sortArrayOfArraysAndSlice($aData, 'value', intval(getParam('bx_analytics_items_count')));
  948. break;
  949. default:
  950. $sType = "line";
  951. $sTableName = '';
  952. $aValues['strings'][0] = _t('_bx_analytics_txt_date');
  953. $bDefaultFillData = false;
  954. if ($sModuleName != BX_ANALYTICS_SYSTEM){
  955. $oModule = BxDolModule::getInstance($sModuleName);
  956. if (isset($oModule->_oConfig->CNF['TABLE_ENTRIES']))
  957. $sTableName = $oModule->_oConfig->CNF['TABLE_ENTRIES'];
  958. $aValues['strings'][1] = ($sReportType == BX_ANALYTICS_CONTENT_TOTAL ? _t('_bx_analytics_txt_total_count', $oModule->_aModule['title']) : _t('_bx_analytics_txt_count', $oModule->_aModule['title']));
  959. }
  960. else{
  961. $sTableName = 'sys_accounts';
  962. $aValues['strings'][1] = ($sReportType == BX_ANALYTICS_CONTENT_TOTAL ? _t('_bx_analytics_txt_total_count', _t('_bx_analytics_txt_accounts')) : _t('_bx_analytics_txt_count', _t('_bx_analytics_txt_accounts')));
  963. }
  964. if (isset($oModule) && isset($oModule->_oConfig->CNF['FIELD_ADDED']))
  965. $sColumnAdded = $oModule->_oConfig->CNF['FIELD_ADDED'];
  966. else
  967. $sColumnAdded = 'added';
  968. $aData = array();
  969. $iValuePrev = 0;
  970. /**
  971. * @hooks
  972. * @hookdef hook-bx_analytics-get_chart_data_line 'bx_analytics', 'get_chart_data_line' - hook on get avalaible reports for specified module
  973. * - $unit_name - equals `bx_analytics`
  974. * - $action - equals `get_chart_data_line`
  975. * - $object_id - not used
  976. * - $sender_id -not used
  977. * - $extra_params - array of additional params with the following array keys:
  978. * - `module` - [string] module name
  979. * - `report_name` - [string] report name
  980. * - `report_type` - [string] by ref, report name, can be overridden in hook processing
  981. * - `date_from` - [int] report's start of interval in php time format
  982. * - `date_to` - [int] report's end of interval in php time format
  983. * - `data` - [array] by ref, array of data for chart, can be overridden in hook processing
  984. * - `prev_value` - [int] by ref, previous value for chart (started value), can be overridden in hook processing
  985. * @hook @ref hook-bx_analytics-get_chart_data_line
  986. */
  987. bx_alert($this->_aModule['name'], 'get_chart_data_line', 0, 0, array(
  988. 'module' => $sModuleName,
  989. 'report_name' => $sReportName,
  990. 'report_type' => &$sReportType,
  991. 'report_type_ref' => &$sReportType,
  992. 'date_from' => $iDateFrom,
  993. 'date_to' => $iDateTo,
  994. 'data' => &$aData,
  995. 'data_ref' => &$aData,
  996. 'prev_value' => &$iValuePrev,
  997. 'prev_value_ref' => &$iValuePrev,
  998. ));
  999. if ($sColumnAdded != '' && count($aData) == 0){
  1000. $aData = $this->_oDb->getGrowth($sTableName, $sColumnAdded, $iDateFrom, $iDateTo);
  1001. $iValuePrev = $this->_oDb->getGrowthInitValue($sTableName, $sColumnAdded, $iDateFrom);
  1002. }
  1003. if (count($aData) > 0){
  1004. $aTmpDates = array();
  1005. foreach ($aData as $aValue) {
  1006. $sX = $aValue['period'];
  1007. $aTmpDates[$sX] = $aValue['count'];
  1008. }
  1009. for ($i = $iDateFrom; $i < $iDateTo ; $i = $i + 86400 ){
  1010. $sX = date('Y-m-d', $i);
  1011. if (!array_key_exists($sX, $aTmpDates)){
  1012. array_push($aValues['values'][0]['data'], array('x' => $sX, 'y' => $sReportType == BX_ANALYTICS_CONTENT_TOTAL ? $iValuePrev : 0));
  1013. }
  1014. else{
  1015. array_push($aValues['values'][0]['data'], array('x' => $sX, 'y' => $sReportType == BX_ANALYTICS_CONTENT_TOTAL ? ($iValuePrev + $aTmpDates[$sX]) : $aTmpDates[$sX]));
  1016. $iValuePrev += $aTmpDates[$sX];
  1017. }
  1018. //array_push($aValues['labels'], $sX);
  1019. if ($sReportType == BX_ANALYTICS_CONTENT_TOTAL){
  1020. if ($iValuePrev > $iMaxValueY)
  1021. $iMaxValueY = $iValuePrev;
  1022. }
  1023. else{
  1024. if (array_key_exists($sX, $aTmpDates) && $aTmpDates[$sX] > $iMaxValueY)
  1025. $iMaxValueY = $aTmpDates[$sX];
  1026. }
  1027. }
  1028. }
  1029. break;
  1030. }
  1031. if ($bDefaultFillData){
  1032. foreach ($aData as $aValue) {
  1033. array_push($aValues['labels'], $this->getItemName($oModule->serviceGetTitle($aValue['object_id'])));
  1034. array_push($aValues['values'][0]['data'], $aValue['value']);
  1035. array_push($aValues['links'], $oModule->serviceGetLink($aValue['object_id']));
  1036. if ($aValue['value'] > $iMaxValueY)
  1037. $iMaxValueY = $aValue['value'];
  1038. }
  1039. }
  1040. $iMaxValueY = ceil($iMaxValueY * 1.1);
  1041. $aDataForChartXAxes = array();
  1042. if ($bIsTimeX){
  1043. $sUnit = 'day';
  1044. $iInterval = ($iDateTo - $iDateFrom) / 86400;
  1045. if ($iInterval > 50)
  1046. $sUnit = 'week';
  1047. if ($iInterval > 100)
  1048. $sUnit = 'month';
  1049. $aDataForChartXAxes = array(
  1050. 'type' => 'time',
  1051. 'time' => array(
  1052. 'tooltipFormat' => 'DD.MM.YYYY',
  1053. 'unit' => $sUnit,
  1054. ),
  1055. 'display' => true,
  1056. 'distribution' => 'linear',
  1057. 'ticks' => array(
  1058. 'display' => true,
  1059. 'autoSkip' => true,
  1060. )
  1061. );
  1062. }
  1063. else{
  1064. $aDataForChartXAxes = array(
  1065. 'ticks' => array(
  1066. 'autoSkip' => true
  1067. ),
  1068. 'display' => true
  1069. );
  1070. }
  1071. $aDataForChartDatasets = array();
  1072. for($i = 0; $i < count($aValues['values']); $i++){
  1073. $aDataForChartDatasets[] = array(
  1074. 'label' => $aValues['values'][$i]['legend'],
  1075. 'fill' => false,
  1076. 'backgroundColor' => $this->aColors[$i],
  1077. 'borderColor' => $this->aColors[$i],
  1078. 'borderWidth' => 2,
  1079. 'data' => $aValues['values'][$i]['data']
  1080. );
  1081. }
  1082. $aDataForChart = array(
  1083. 'type' => $sType,
  1084. 'data' => array(
  1085. 'labels' => $aValues['labels'],
  1086. 'datasets' => $aDataForChartDatasets
  1087. ),
  1088. 'options' => array(
  1089. 'legend' => array(
  1090. 'position' => 'bottom',
  1091. 'display' => count($aValues['values']) == 1 ? false :true
  1092. ),
  1093. 'elements' => array(
  1094. 'point'=> array(
  1095. 'radius' => 0
  1096. )
  1097. ),
  1098. 'scales' => array(
  1099. 'y' => array(
  1100. 'ticks' => array(
  1101. 'max' => $iMaxValueY,
  1102. 'min' => $iMinValueY,
  1103. 'stepSize' => $this->getStep($iMinValueY, $iMaxValueY),
  1104. 'autoSkip' => true
  1105. )
  1106. ),
  1107. 'x' => $aDataForChartXAxes
  1108. )
  1109. ),
  1110. 'links' => $aValues['links'],
  1111. 'strings' => $aValues['strings'],
  1112. );
  1113. return json_encode($aDataForChart);
  1114. }
  1115. /**
  1116. * Who may define goals and funnels: site administrators, and levels with "manage analytics"
  1117. * besides "use analytics".
  1118. */
  1119. public function checkAllowedManage()
  1120. {
  1121. if(isAdmin())
  1122. return CHECK_ACTION_RESULT_ALLOWED;
  1123. if($this->checkAllowed() !== CHECK_ACTION_RESULT_ALLOWED)
  1124. return _t('_sys_txt_access_denied');
  1125. $aCheck = checkActionModule(bx_get_logged_profile_id(), 'manage analytics', $this->getName(), false);
  1126. return $aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED ? CHECK_ACTION_RESULT_ALLOWED : _t('_sys_txt_access_denied');
  1127. }
  1128. /**
  1129. * Save or delete a goal: POST op (save, delete), id, name, type (page, event), pattern, csrf_token.
  1130. */
  1131. public function actionGoal()
  1132. {
  1133. $aResult = $this->_checkManageRequest();
  1134. if($aResult === true) {
  1135. $iId = (int)bx_get('id', 'post');
  1136. if(bx_get('op', 'post') == 'delete')
  1137. $aResult = ['ok' => $this->_oDb->deleteGoal($iId)];
  1138. else {
  1139. $aGoal = $this->_getMatchInput(bx_get('type', 'post'), bx_get('pattern', 'post'));
  1140. $sName = trim((string)bx_get('name', 'post'));
  1141. if($aGoal === false || $sName === '' || get_mb_len($sName) > 128)
  1142. $aResult = ['error' => _t('_bx_analytics_txt_goal_invalid')];
  1143. else {
  1144. $iId = $this->_oDb->saveGoal($iId, ['name' => $sName] + $aGoal);
  1145. $aResult = ['ok' => (bool)$iId, 'id' => $iId];
  1146. }
  1147. }
  1148. }
  1149. $this->_outputJson($aResult);
  1150. }
  1151. /**
  1152. * Save or delete a funnel: POST op, id, name, step_type[], step_pattern[], csrf_token.
  1153. */
  1154. public function actionFunnel()
  1155. {
  1156. $aResult = $this->_checkManageRequest();
  1157. if($aResult === true) {
  1158. $iId = (int)bx_get('id', 'post');
  1159. if(bx_get('op', 'post') == 'delete')
  1160. $aResult = ['ok' => $this->_oDb->deleteFunnel($iId), 'id' => 0];
  1161. else {
  1162. $aTypes = bx_get('step_type', 'post');
  1163. $aPatterns = bx_get('step_pattern', 'post');
  1164. $aSteps = [];
  1165. if(is_array($aTypes) && is_array($aPatterns))
  1166. foreach(array_values($aTypes) as $i => $sType) {
  1167. if(!isset($aPatterns[$i]) || trim((string)$aPatterns[$i]) === '')
  1168. continue;
  1169. $aStep = $this->_getMatchInput($sType, $aPatterns[$i]);
  1170. if($aStep === false) {
  1171. $aSteps = false;
  1172. break;
  1173. }
  1174. $aSteps[] = $aStep;
  1175. }
  1176. $sName = trim((string)bx_get('name', 'post'));
  1177. if($aSteps === false || count($aSteps) < 2 || count($aSteps) > $this->_oConfig->CNF['MAX_FUNNEL_STEPS'] || $sName === '' || get_mb_len($sName) > 128)
  1178. $aResult = ['error' => _t('_bx_analytics_txt_funnel_invalid')];
  1179. else {
  1180. $iId = $this->_oDb->saveFunnel($iId, $sName, $aSteps);
  1181. $aResult = ['ok' => (bool)$iId, 'id' => $iId];
  1182. }
  1183. }
  1184. }
  1185. $this->_outputJson($aResult);
  1186. }
  1187. /**
  1188. * A POST from a manager with a valid form token, or an error array.
  1189. */
  1190. protected function _checkManageRequest()
  1191. {
  1192. if($_SERVER['REQUEST_METHOD'] != 'POST' || $this->checkAllowedManage() !== CHECK_ACTION_RESULT_ALLOWED)
  1193. return ['error' => _t('_sys_txt_access_denied'), 'code' => 403];
  1194. if(!BxDolForm::isCsrfTokenValid((string)bx_get('csrf_token', 'post'), false))
  1195. return ['error' => _t('_sys_txt_access_denied'), 'code' => 403];
  1196. return true;
  1197. }
  1198. /**
  1199. * A goal or funnel step from form input: a page path (starting with / or *, * for any
  1200. * characters) or an event name (name, or name@module), or false.
  1201. */
  1202. protected function _getMatchInput($sType, $sPattern)
  1203. {
  1204. $sPattern = trim((string)$sPattern);
  1205. if($sPattern === '' || get_mb_len($sPattern) > 255)
  1206. return false;
  1207. if($sType == 'page')
  1208. return in_array(get_mb_substr($sPattern, 0, 1), ['/', '*'], true) ? ['type' => 'page', 'pattern' => $sPattern] : false;
  1209. if($sType == 'event')
  1210. return preg_match('/^[\p{L}\p{N} _.:\/-]{1,64}(@[a-z0-9_]{1,32})?$/u', $sPattern) ? ['type' => 'event', 'pattern' => $sPattern] : false;
  1211. return false;
  1212. }
  1213. protected function _outputJson($aResult)
  1214. {
  1215. header('Content-Type: application/json; charset=utf-8');
  1216. header('Cache-Control: no-store');
  1217. if(isset($aResult['code']))
  1218. http_response_code((int)$aResult['code']);
  1219. echo json_encode($aResult);
  1220. exit;
  1221. }
  1222. /**
  1223. * Reports that show members by name: sessions and users. Site administrators always
  1224. * see them; everyone else needs "view analytics people" besides "use analytics".
  1225. */
  1226. public function checkAllowedPeople()
  1227. {
  1228. if(isAdmin())
  1229. return CHECK_ACTION_RESULT_ALLOWED;
  1230. if($this->checkAllowed() !== CHECK_ACTION_RESULT_ALLOWED)
  1231. return _t('_sys_txt_access_denied');
  1232. $aCheck = checkActionModule(bx_get_logged_profile_id(), 'view analytics people', $this->getName(), false);
  1233. return $aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED ? CHECK_ACTION_RESULT_ALLOWED : _t('_sys_txt_access_denied');
  1234. }
  1235. public function checkAllowed($isPerformAction = false)
  1236. {
  1237. // site administrators always see analytics, whatever their profile's level
  1238. if(isAdmin())
  1239. return CHECK_ACTION_RESULT_ALLOWED;
  1240. $aCheck = checkActionModule(bx_get_logged_profile_id(), 'use analytics', $this->getName(), $isPerformAction);
  1241. if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
  1242. return _t('_sys_txt_access_denied');
  1243. return CHECK_ACTION_RESULT_ALLOWED;
  1244. }
  1245. public function getSelectedModules()
  1246. {
  1247. $aModulesList = $this->serviceGetModules();
  1248. $aModules = array_keys($aModulesList);
  1249. $sModulesDisabled = explode(',', getParam('bx_analytics_modules_disabled'));
  1250. return array(array_diff($aModules, $sModulesDisabled), $aModulesList);
  1251. }
  1252. private function getViewsOrVotes($aSystems, $sSystem, $sModuleName, $iDateFrom, $iDateTo, &$aValues, &$iMaxValueY, &$iMinValueY)
  1253. {
  1254. $aData = array();
  1255. if ($sModuleName != BX_ANALYTICS_SYSTEM){
  1256. $oModule = BxDolModule::getInstance($sModuleName);
  1257. $sSystem = $oModule->_oConfig->CNF['OBJECT_VIEWS'];
  1258. if(isset($aSystems[$sSystem]))
  1259. $aData = $this->_oDb->getTopContentByLikes($sModuleName, $aSystems[$sSystem]['table_track'], $iDateFrom, $iDateTo);
  1260. }
  1261. else{
  1262. $aTmp = $this->getSelectedModules();
  1263. foreach ($aTmp[0] as $sModule) {
  1264. if ($sModule != BX_ANALYTICS_SYSTEM){
  1265. $oModule = BxDolModule::getInstance($sModule);
  1266. if (isset($oModule->_oConfig->CNF['OBJECT_VIEWS'])){
  1267. $sSystem = $oModule->_oConfig->CNF['OBJECT_VIEWS'];
  1268. if(isset($aSystems[$sSystem])){
  1269. $aData1 = $this->_oDb->getTopContentByLikes($sModule, $aSystems[$sSystem]['table_track'], $iDateFrom, $iDateTo);
  1270. $aData = array_merge($aData, $aData1);
  1271. }
  1272. }
  1273. }
  1274. }
  1275. $aData = $this->sortArrayOfArraysAndSlice($aData, 'value', intval(getParam('bx_analytics_items_count')));
  1276. }
  1277. foreach ($aData as $aValue) {
  1278. $oModule = BxDolModule::getInstance($aValue['module']);
  1279. array_push($aValues['labels'], $this->getItemName($oModule->serviceGetTitle($aValue['object_id'])));
  1280. array_push($aValues['values'][0]['data'], $aValue['value']);
  1281. array_push($aValues['links'], $oModule->serviceGetLink($aValue['object_id']));
  1282. if ($aValue['value'] > $iMaxValueY)
  1283. $iMaxValueY = $aValue['value'];
  1284. }
  1285. }
  1286. private function getStep($iMin, $iMax)
  1287. {
  1288. $iCount = 8;
  1289. return ceil(($iMax - $iMin) / $iCount);
  1290. }
  1291. private function sortArrayOfArraysAndSlice($aArr, $sFld, $Count)
  1292. {
  1293. $aDataSort = array();
  1294. foreach ($aArr as $sKey => $oRow)
  1295. {
  1296. $aDataSort[$sKey] = $oRow[$sFld];
  1297. }
  1298. array_multisort($aDataSort, SORT_DESC, $aArr);
  1299. return array_slice($aArr, 0, $Count);
  1300. }
  1301. private function getItemName($sString)
  1302. {
  1303. $sString = strip_tags($sString);
  1304. if ($sString == ''){
  1305. $sString = _t('_bx_analytics_txt_empty_title_item');
  1306. }
  1307. return $sString;
  1308. }
  1309. }
  1310. /** @} */