| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434 |
- <?php defined('BX_DOL') or die('hack attempt');
- /**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup Analytics Analytics
- * @ingroup UnaModules
- *
- * @{
- */
- /**
- * Analytics module
- */
- define('BX_ANALYTICS_SYSTEM', 'system');
- define('BX_ANALYTICS_TOP_BY_LIKES', 'top_by_likes');
- define('BX_ANALYTICS_TOP_BY_VIEWS', 'top_by_views');
- define('BX_ANALYTICS_CONTENT_TOTAL', 'content_total');
- define('BX_ANALYTICS_CONTENT_SPEED', 'content_speed');
- define('BX_ANALYTICS_MOST_FOLLOWED_PROFILES', 'top_by_followers');
- define('BX_ANALYTICS_MOST_ACTIVE_PROFILES', 'top_by_activity');
- class BxAnalyticsModule extends BxDolModule
- {
- protected $aColors = array('#3366CC', '#DC3912', '#FF9900', '#109618', '#990099');
- protected $_oReports = null;
- /**
- * Reports that blocks, Studio and the API may ask for; each is a BxAnalyticsReports method.
- */
- protected $_aReports = ['filters', 'overview', 'top', 'table', 'realtime', 'map', 'bots', 'sessions', 'users', 'events', 'journeys', 'retention', 'goals', 'funnels', 'revenue', 'subscriptions', 'sales', 'credits', 'errors', 'performance', 'query'];
- /**
- * Reports that show members by name.
- */
- protected $_aPeopleReports = ['sessions', 'users'];
- /**
- * Reports that show money and buyers.
- */
- protected $_aRevenueReports = ['revenue', 'subscriptions', 'sales', 'credits'];
- /**
- * Reports with a CSV download. None of them names members: one that does must stay
- * behind the same checks as getReportData (checkAllowedPeople).
- */
- protected $_aExportReports = ['query', 'table', 'top'];
- /**
- * Payment alerts (unit `system`) that keep the revenue ledger.
- */
- protected $_aPaymentAlerts = ['register_payment', 'refund_payment', 'create_subscription', 'prolong_subscription', 'overdue_subscription', 'cancel_subscription', 'cancel_order', 'finalize_checkout'];
- /**
- * Studio sections: page => list of [report, block params, spans all columns].
- */
- protected $_aStudioSections = [
- 'overview' => [['filters', [], true], ['overview', [], true], ['top', ['group' => 'pages']], ['top', ['group' => 'sources']], ['top', ['group' => 'locations']], ['top', ['group' => 'devices']], ['top', ['group' => 'platforms']]],
- 'realtime' => [['realtime', [], true]],
- 'pages' => [['filters', [], true], ['table', ['table' => 'pages'], true]],
- 'sources' => [['filters', [], true], ['table', ['table' => 'sources'], true]],
- 'audience' => [['filters', [], true], ['top', ['group' => 'locations']], ['top', ['group' => 'devices']], ['top', ['group' => 'platforms']]],
- 'map' => [['filters', [], true], ['map', [], true]],
- 'bots' => [['filters', [], true], ['bots', [], true]],
- 'sessions' => [['filters', [], true], ['sessions', [], true]],
- 'users' => [['filters', [], true], ['users', [], true]],
- 'events' => [['filters', [], true], ['events', [], true]],
- 'journeys' => [['filters', [], true], ['journeys', [], true]],
- 'retention' => [['filters', [], true], ['retention', [], true]],
- 'goals' => [['filters', [], true], ['goals', [], true]],
- 'funnels' => [['filters', [], true], ['funnels', [], true]],
- 'revenue' => [['filters', [], true], ['revenue', [], true]],
- 'subscriptions' => [['filters', [], true], ['subscriptions', [], true]],
- 'sales' => [['filters', [], true], ['sales', [], true]],
- 'credits' => [['filters', [], true], ['credits', [], true]],
- 'errors' => [['filters', [], true], ['errors', [], true]],
- 'performance' => [['filters', [], true], ['performance', [], true]],
- 'query' => [['filters', [], true], ['query', [], true]],
- ];
- /**
- * Studio sections that are admin tools, not reports: the API playground, the pages built
- * from analytics blocks, and the tracker setup. They show page objects, addresses and
- * settings, so they stay out of $_aReports, which get_report (any API client) can ask for.
- */
- protected $_aStudioTools = ['api', 'dashboards', 'setup'];
- /**
- * The page whose blocks a new analytics page starts with, and the cells copied: the
- * center and the right bar of layout 12, the cells NEO's dashboard tabs show.
- */
- protected $_sDashboardTemplate = 'bx_analytics_page';
- protected $_aDashboardCells = [2, 3];
- protected $_iDashboardLayout = 12;
- /**
- * The event the Setup section's test sends.
- */
- const SETUP_TEST_EVENT = 'analytics_setup_test';
- /**
- * The start of a cached report: the cache directory holds .php files, and one requested
- * over HTTP shows nothing past it.
- */
- const REPORT_CACHE_GUARD = "<?php exit; ?>\n";
- /**
- * Cache name of the reports' generation (not under REPORT_CACHE_PREFIX, so pruning the
- * expired reports keeps it) and how long it is kept: long past any report.
- */
- const REPORT_CACHE_GENERATION = 'bx_analytics_reports_generation';
- const REPORT_CACHE_GENERATION_TTL = 2592000;
- protected $_sReportCacheGeneration = null;
-
- function __construct(&$aModule)
- {
- parent::__construct($aModule);
- }
-
- /**
- * Service methods
- */
-
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-other Other
- * @subsubsection bx_analytics get_canvas
- *
- * @code bx_srv('bx_analytics', 'get_canvas', [...]); @endcode
- *
- * Get canvas
- *
- * @return an html.
- *
- * @see BxAnalyticsModule::serviceGetCanvas
- */
- /**
- * @ref bx_analytics-get_canvas "get_canvas"
- */
- public function serviceGetCanvas()
- {
- if(bx_is_api() || $this->checkAllowed() !== CHECK_ACTION_RESULT_ALLOWED)
- return bx_is_api() ? [] : '';
- return $this->_oTemplate->getCanvas();
- }
-
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-other Other
- * @subsubsection bx_analytics is_avaliable
- *
- * @code bx_srv('bx_analytics', 'is_avaliable', [...]); @endcode
- *
- * Check avalability for current profile
- *
- * @return boolean.
- *
- * @see BxAnalyticsModule::serviceIsAvaliable
- */
- /**
- * @ref bx_analytics-is_avaliable "is_avaliable"
- */
- public function serviceIsAvaliable()
- {
- return $this->checkAllowed() === CHECK_ACTION_RESULT_ALLOWED ? true : false;
- }
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-other Other
- * @subsubsection bx_analytics-is_revenue_avaliable is_revenue_avaliable
- *
- * @code bx_srv('bx_analytics', 'is_revenue_avaliable'); @endcode
- *
- * Whether the current profile may see the revenue reports ("view analytics revenue",
- * administrators by default). Shows the Revenue item of the account dashboard menu.
- *
- * @return boolean
- *
- * @see BxAnalyticsModule::serviceIsRevenueAvaliable
- */
- /**
- * @ref bx_analytics-is_revenue_avaliable "is_revenue_avaliable"
- */
- public function serviceIsRevenueAvaliable()
- {
- return $this->checkAllowedRevenue() === CHECK_ACTION_RESULT_ALLOWED;
- }
-
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-other Other
- * @subsubsection bx_analytics get_modules
- *
- * @code bx_srv('bx_analytics', 'get_modules', [...]); @endcode
- *
- * Get modules list
- *
- * @return an html.
- *
- * @see BxAnalyticsModule::serviceGetModules
- */
- /**
- * @ref bx_analytics-get_modules "get_modules"
- */
- public function serviceGetModules()
- {
- $aResult = array();
- $aResult[BX_ANALYTICS_SYSTEM] = _t('_bx_analytics_system_text');
- $BxDolModuleQuery = BxDolModuleQuery::getInstance();
- $aModules = $BxDolModuleQuery->getModulesBy(array('type' => 'modules', 'active' => 1));
- foreach($aModules as $aModule){
- $oModule = BxDolModule::getInstance($aModule['name']);
- if($oModule instanceof iBxDolContentInfoService){
- $aResult[$aModule['name']] = $aModule['title'];
- }
- }
-
- /**
- * @hooks
- * @hookdef hook-bx_analytics-get_modules 'bx_analytics', 'get_modules' - hook on get modules list with avalaible analytics
- * - $unit_name - equals `get_modules`
- * - $action - equals `get_modules`
- * - $object_id - not used
- * - $sender_id -not used
- * - $extra_params - array of additional params with the following array keys:
- * - `list` - [array] by ref, array of modules for analytics as [module_name] = module_title, can be overridden in hook processing
- * @hook @ref hook-bx_analytics-get_modules
- */
- bx_alert($this->_aModule['name'], 'get_modules', 0, 0, array(
- 'list' => &$aResult,
- 'list_ref' => &$aResult,
- ));
-
- return $aResult;
- }
-
- public function serviceGetSafeServices()
- {
- return array_merge(parent::serviceGetSafeServices(), [
- 'GetReport' => '',
- 'GetTrackerConfig' => '',
- ]);
- }
- public function serviceGetPublicServices()
- {
- return array_merge(parent::serviceGetPublicServices(), [
- 'Collect' => '',
- ]);
- }
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-tracking Tracking
- * @subsubsection bx_analytics-collect collect
- *
- * @code bx_srv('bx_analytics', 'collect', [$aPayload]); @endcode
- * @code api.php?r=bx_analytics/collect with the JSON payload as the request body @endcode
- *
- * Store pageviews and events sent by NEO, the apps or another API client.
- * The payload is described in BxAnalyticsCollector. Over the API the payload is read from
- * the request body when no parameter is given.
- *
- * One address may send so many hits a minute and start so many visits an hour (Settings >
- * Tracking). Past a limit the hits are dropped, and the answer is still a success with
- * `stored: 0` and `limited: true`, not an error: a tracker goes on, and its hits are
- * stored again from the next minute. Send fewer, batched requests when you see it.
- *
- * @param $mixedPayload payload array or JSON string
- * @return array with `stored` - the number of hits stored; `limited` - true when they
- * were dropped for a rate limit
- *
- * @see BxAnalyticsModule::serviceCollect
- */
- /**
- * @ref bx_analytics-collect "collect"
- */
- public function serviceCollect($mixedPayload = '')
- {
- if(empty($mixedPayload))
- $mixedPayload = $this->_getCollectBody();
- if(is_string($mixedPayload) && !$this->isCollectBodyAllowed($mixedPayload))
- return ['stored' => 0];
- $aPayload = is_array($mixedPayload) ? $mixedPayload : json_decode((string)$mixedPayload, true);
- if(!is_array($aPayload))
- return ['stored' => 0];
- $oCollector = $this->getCollector();
- $iStored = $oCollector->collect($aPayload, [
- 'platform' => BX_ANALYTICS_PLATFORM_API,
- 'trusted' => $this->_isApiKeyRequest(),
- 'profile_id' => bx_get_logged_profile_id(),
- ]);
- return $oCollector->isLimited() ? ['stored' => 0, 'limited' => true] : ['stored' => $iStored];
- }
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-tracking Tracking
- * @subsubsection bx_analytics-track track
- *
- * @code bx_srv('bx_analytics', 'track', ['export_done', ['rows' => 120]]); @endcode
- *
- * Record a custom event from server code, such as a finished import or a webhook. It is
- * stored like a site action: on the platform of the request (core for cron jobs), joined
- * to the member's running visit on that platform when there is one. For PHP only: not
- * available over the API. Don't call `collect` from server code instead, which records
- * the server's own address and user agent as an API client.
- *
- * @param $sName event name: up to 64 letters, digits, spaces and _ . : / -
- * @param $aProps properties: up to 30 keys of letters, digits, _ . -, with scalar values
- * @param $iProfileId who did it; 0 for the logged in member, if any
- * @param $fValue a number to sum in reports, such as an amount
- * @return bool whether it was stored (false when tracking is off, the name is invalid, or
- * the platform or the member's level is excluded)
- *
- * @see BxAnalyticsModule::serviceTrack
- */
- /**
- * @ref bx_analytics-track "track"
- */
- public function serviceTrack($sName, $aProps = [], $iProfileId = 0, $fValue = 0)
- {
- if(!is_string($sName) || !preg_match('/^[\p{L}\p{N} _.:\/-]{1,64}$/u', $sName))
- return false;
- $iProfileId = (int)$iProfileId;
- if(!$iProfileId)
- $iProfileId = (int)bx_get_logged_profile_id();
- return $this->getCollector()->collectAction($sName, '', 0, $iProfileId, [
- 'type' => BX_ANALYTICS_HIT_EVENT,
- 'api_key' => $this->_isApiKeyRequest(),
- 'props' => is_array($aProps) ? $aProps : [],
- 'value' => is_numeric($fValue) ? (float)$fValue : 0,
- ]);
- }
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-tracking Tracking
- * @subsubsection bx_analytics-get_tracker_config get_tracker_config
- *
- * @code bx_srv('bx_analytics', 'get_tracker_config', ['rn-ios']); @endcode
- * @code api.php?r=bx_analytics/get_tracker_config¶ms[]=next-web @endcode
- *
- * Tracker settings for NEO and the apps, read once when the client starts:
- * whether to send hits at all, whether to keep a visitor id (persistent identity mode),
- * whether to honour Do Not Track, the session timeout, and what to report about the
- * app's health. The hits are described in BxAnalyticsCollector.
- *
- * @param $sPlatform client platform code (next-web, rn-ios, rn-android, api); when empty,
- * the platform of the request
- * @return array with
- * - `enabled`, `persistent`, `respect_dnt` (booleans), `session_timeout` (seconds)
- * - `errors` (boolean): send `error` hits
- * - `errors_ignore` (array of strings): messages not to send, patterns with * for any
- * characters, matched against the whole message, case-insensitively; the server
- * drops them too, so filtering on the client only saves requests
- * - `vitals` (boolean): send `vitals` hits
- * - `vitals_sample` (0 to 100): percent of page loads (app launches on native) that
- * measure vitals, decided once per load with a random roll from 0 to 100 below vitals_sample
- *
- * @see BxAnalyticsModule::serviceGetTrackerConfig
- */
- /**
- * @ref bx_analytics-get_tracker_config "get_tracker_config"
- */
- public function serviceGetTrackerConfig($sPlatform = '')
- {
- if(!is_string($sPlatform) || $sPlatform === '') {
- bx_import('Request', $this->_aModule);
- $oRequest = new BxAnalyticsRequest();
- $sPlatform = $oRequest->getPlatform(defined('BX_API'), $this->_isApiKeyRequest());
- }
- // `core` is the server's own platform, never a client's
- $bEnabled = $this->_oConfig->isTrackingEnabled() && $sPlatform != BX_ANALYTICS_PLATFORM_CORE && $this->_oConfig->isPlatformEnabled($sPlatform);
- return [
- 'enabled' => $bEnabled,
- 'persistent' => $bEnabled && $this->_oConfig->getIdentityMode() == BX_ANALYTICS_IDENTITY_PERSISTENT,
- 'respect_dnt' => $this->_oConfig->isRespectDnt(),
- 'session_timeout' => $this->_oConfig->getSessionTimeout(),
- 'errors' => $bEnabled && $this->_oConfig->isTrackErrors(),
- 'errors_ignore' => $this->_oConfig->getErrorsIgnore(),
- 'vitals' => $bEnabled && $this->_oConfig->isTrackVitals() && $this->_oConfig->getVitalsSample() > 0,
- 'vitals_sample' => $this->_oConfig->getVitalsSample(),
- ];
- }
- /**
- * Hits from the classic tracker: same origin, a JSON body sent with sendBeacon. Always
- * 204, hits dropped for a rate limit too (see BxAnalyticsCollector::_isWithinHitLimit).
- */
- public function actionCollect()
- {
- $sBody = $this->_getCollectBody();
- $aPayload = $sBody !== false ? json_decode($sBody, true) : null;
- if(is_array($aPayload))
- $this->getCollector()->collect($aPayload, [
- 'platform' => BX_ANALYTICS_PLATFORM_CORE_WEB,
- 'same_origin' => true,
- 'profile_id' => bx_get_logged_profile_id(),
- ]);
- header('HTTP/1.1 204 No Content');
- header('Cache-Control: no-store');
- exit;
- }
- /**
- * Whether a collect request's body is small enough to decode: no larger than a tracker
- * ever sends (MAX_COLLECT_BYTES). The endpoint is open to anyone, and decoding takes
- * memory and time before any rate limit applies.
- */
- public function isCollectBodyAllowed($sBody)
- {
- return is_string($sBody) && mb_strlen($sBody, '8bit') <= $this->_oConfig->CNF['MAX_COLLECT_BYTES'];
- }
- /**
- * The body of a collect request, or false when it is too large (see isCollectBodyAllowed),
- * told by its length header before it is read when there is one.
- */
- protected function _getCollectBody()
- {
- $iMax = $this->_oConfig->CNF['MAX_COLLECT_BYTES'];
- if(isset($_SERVER['CONTENT_LENGTH']) && (int)$_SERVER['CONTENT_LENGTH'] > $iMax)
- return false;
- $sBody = file_get_contents('php://input', false, null, 0, $iMax + 1);
- return $this->isCollectBodyAllowed($sBody) ? $sBody : false;
- }
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-tracking Tracking
- * @subsubsection bx_analytics-get_tracker_code get_tracker_code
- *
- * @code bx_srv('bx_analytics', 'get_tracker_code', [...]); @endcode
- *
- * The classic UI tracker, injected at the end of every page.
- *
- * @return HTML string, empty when tracking is off for the classic UI or for the viewer
- *
- * @see BxAnalyticsModule::serviceGetTrackerCode
- */
- /**
- * @ref bx_analytics-get_tracker_code "get_tracker_code"
- */
- public function serviceGetTrackerCode()
- {
- if(!$this->_oConfig->isTrackingEnabled() || !$this->_oConfig->isPlatformEnabled(BX_ANALYTICS_PLATFORM_CORE_WEB))
- return '';
- // bots don't run the tracker: the page counts them as it renders
- bx_import('UserAgent', $this->_aModule);
- $sUserAgent = isset($_SERVER['HTTP_USER_AGENT']) ? (string)$_SERVER['HTTP_USER_AGENT'] : '';
- if(BxAnalyticsUserAgent::isBot($sUserAgent)) {
- $this->getCollector()->collectBot($sUserAgent, isset($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : '');
- return '';
- }
- $aLevels = $this->_oConfig->getExcludedLevels();
- if(!empty($aLevels)) {
- $iProfileId = bx_get_logged_profile_id();
- $aMembership = BxDolAcl::getInstance()->getMemberMembershipInfo($iProfileId ? $iProfileId : 0);
- if(!empty($aMembership['id']) && in_array((int)$aMembership['id'], $aLevels, true))
- return '';
- }
- return $this->_oTemplate->getTrackerCode();
- }
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-reports Reports
- * @subsubsection bx_analytics-get_report get_report
- *
- * @code bx_srv('bx_analytics', 'get_report', ['overview', ['range' => '30d', 'platform' => 'rn-ios']]); @endcode
- * @code api.php?r=bx_analytics/get_report¶ms[]=top¶ms[]={"group":"pages","tab":"entry","range":"7d"} @endcode
- *
- * Report data for NEO and other clients. Reports: filters, overview, top, table, realtime
- * and the other sections listed in BxAnalyticsModule::$_aReports. Sessions and users need
- * the "view analytics people" permission; revenue, subscriptions, sales and credits need
- * "view analytics revenue". Errors and performance need only "use analytics", but an
- * error's stack trace (visitor text) is left out without "view analytics people".
- *
- * @param $sReport report name
- * @param $mixedParams filter parameters (see BxAnalyticsFilter) and the report's own
- * (group, table, tab, page, limit...); an array or a JSON string
- * @return array report data, or an error array when the viewer may not use analytics
- *
- * @see BxAnalyticsModule::serviceGetReport
- */
- /**
- * @ref bx_analytics-get_report "get_report"
- */
- public function serviceGetReport($sReport, $mixedParams = [])
- {
- if(!$this->serviceIsAvaliable())
- return ['error' => _t('_sys_txt_access_denied'), 'code' => 403];
- if((in_array($sReport, $this->_aRevenueReports, true) && $this->checkAllowedRevenue() !== CHECK_ACTION_RESULT_ALLOWED) || (in_array($sReport, $this->_aPeopleReports, true) && $this->checkAllowedPeople() !== CHECK_ACTION_RESULT_ALLOWED))
- return ['error' => _t('_sys_txt_access_denied'), 'code' => 403];
- $aParams = is_array($mixedParams) ? $mixedParams : json_decode((string)$mixedParams, true);
- if(!is_array($aParams))
- $aParams = [];
- $aData = $this->getReportData($sReport, $aParams);
- if($aData === false)
- return ['error' => _t('_sys_request_page_not_found_cpt'), 'code' => 404];
- return $aData;
- }
- /**
- * A report block reloaded by the dashboard script: data plus rendered HTML. The block's
- * own parameters (group, table...) come as JSON in `bp`, the filters as the page's URL
- * parameters.
- */
- public function actionReport($sReport = '')
- {
- header('Content-Type: application/json; charset=utf-8');
- header('Cache-Control: no-store');
- if(!$this->serviceIsAvaliable() || (in_array($sReport, $this->_aRevenueReports, true) && $this->checkAllowedRevenue() !== CHECK_ACTION_RESULT_ALLOWED)) {
- http_response_code(403);
- echo json_encode(['error' => _t('_sys_txt_access_denied')]);
- exit;
- }
- $aBlockParams = json_decode((string)bx_get('bp'), true);
- if(!is_array($aBlockParams))
- $aBlockParams = [];
- $aParams = array_merge($_GET, $aBlockParams);
- unset($aParams['bp']);
- $aData = $this->getReportData($sReport, $aParams);
- if($aData === false) {
- http_response_code(404);
- echo json_encode(['error' => _t('_sys_request_page_not_found_cpt')]);
- exit;
- }
- echo json_encode([
- 'data' => $aData,
- 'html' => $this->_oTemplate->getReportHtml($sReport, $aData, $this->getFilter($aParams), $aBlockParams),
- ]);
- exit;
- }
- /**
- * A report as a CSV download: GET export/<report>/ with the page's filters and the
- * report's own parameters (q_dim, q_metric, q_dir for the query; table or group and tab
- * otherwise). Every value of the dimension, up to MAX_EXPORT_ROWS rows. Reading, so the
- * analytics permission is checked without counting a use of it.
- */
- public function actionExport($sReport = '')
- {
- $aExport = $this->getReportExport((string)$sReport, $_GET);
- if($aExport === false)
- $this->_outputJson(['error' => _t('_sys_txt_access_denied'), 'code' => 403]);
- header('Content-Type: text/csv; charset=utf-8');
- header('Content-Disposition: attachment; filename="' . $aExport['filename'] . '"');
- header('Cache-Control: no-store');
- header('X-Content-Type-Options: nosniff');
- $rOutput = fopen('php://output', 'w');
- BxAnalyticsCsv::write($rOutput, $aExport['header'], $aExport['rows']);
- fclose($rOutput);
- exit;
- }
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-blocks Blocks
- * @subsubsection bx_analytics-get_block_filters get_block_filters
- *
- * @code bx_srv('bx_analytics', 'get_block_filters'); @endcode
- *
- * The filter bar: period, platform, active filters and visitors online. It keeps the
- * filters in the page URL, and every analytics block on the page follows them.
- *
- * @return HTML string, or a block description for the API; empty for viewers without access
- *
- * @see BxAnalyticsModule::serviceGetBlockFilters
- */
- /**
- * @ref bx_analytics-get_block_filters "get_block_filters"
- */
- public function serviceGetBlockFilters()
- {
- return $this->_getBlock('filters');
- }
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-blocks Blocks
- * @subsubsection bx_analytics-get_block_overview get_block_overview
- *
- * @code bx_srv('bx_analytics', 'get_block_overview'); @endcode
- *
- * Key figures with the change against the previous period, and the main chart.
- *
- * @return HTML string, or a block description for the API; empty for viewers without access
- *
- * @see BxAnalyticsModule::serviceGetBlockOverview
- */
- /**
- * @ref bx_analytics-get_block_overview "get_block_overview"
- */
- public function serviceGetBlockOverview()
- {
- return $this->_getBlock('overview');
- }
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-blocks Blocks
- * @subsubsection bx_analytics-get_block_top get_block_top
- *
- * @code bx_srv('bx_analytics', 'get_block_top', ['sources']); @endcode
- *
- * A tabbed top list: pages, sources, locations, devices or platforms.
- *
- * @param $sGroup list group
- * @return HTML string, or a block description for the API; empty for viewers without access
- *
- * @see BxAnalyticsModule::serviceGetBlockTop
- */
- /**
- * @ref bx_analytics-get_block_top "get_block_top"
- */
- public function serviceGetBlockTop($sGroup = 'pages')
- {
- return $this->_getBlock('top', ['group' => $sGroup]);
- }
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-blocks Blocks
- * @subsubsection bx_analytics-get_block_table get_block_table
- *
- * @code bx_srv('bx_analytics', 'get_block_table', ['pages']); @endcode
- *
- * A full table with every metric, a page at a time: pages or sources.
- *
- * @param $sTable table name
- * @return HTML string, or a block description for the API; empty for viewers without access
- *
- * @see BxAnalyticsModule::serviceGetBlockTable
- */
- /**
- * @ref bx_analytics-get_block_table "get_block_table"
- */
- public function serviceGetBlockTable($sTable = 'pages')
- {
- return $this->_getBlock('table', ['table' => $sTable]);
- }
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-blocks Blocks
- * @subsubsection bx_analytics-get_block_report get_block_report
- *
- * @code bx_srv('bx_analytics', 'get_block_report', ['realtime']); @endcode
- *
- * Any other report as a block: realtime, map, bots, sessions, users, events, goals,
- * funnels, journeys, retention, revenue, subscriptions, sales, credits, errors,
- * performance, query.
- *
- * @param $sReport report name
- * @param $aParams the report's own parameters
- * @return HTML string, or a block description for the API; empty for viewers without access
- *
- * @see BxAnalyticsModule::serviceGetBlockReport
- */
- /**
- * @ref bx_analytics-get_block_report "get_block_report"
- */
- public function serviceGetBlockReport($sReport, $aParams = [])
- {
- return $this->_getBlock($sReport, is_array($aParams) ? $aParams : []);
- }
- /**
- * A Studio section: the filter bar (where the section uses it) and the section's blocks.
- */
- public function getStudioSection($sSection)
- {
- // admin tools: not reports, so get_report and the blocks never reach them
- if(in_array($sSection, $this->_aStudioTools, true)) {
- if(!isAdmin())
- return '';
- $sMethod = '_getStudio' . bx_gen_method_name($sSection);
- return $this->_oTemplate->getStudioSection([['title' => '', 'content' => $this->$sMethod(), 'wide' => true]]);
- }
- if(!isset($this->_aStudioSections[$sSection]))
- return '';
- // one report under the filter bar needs no heading: the section title names it
- $iReports = 0;
- foreach($this->_aStudioSections[$sSection] as $aBlock)
- if($aBlock[0] != 'filters')
- $iReports++;
- $aBlocks = [];
- foreach($this->_aStudioSections[$sSection] as $aBlock) {
- list($sReport, $aParams, $bWide) = array_pad($aBlock, 3, false);
- $sTitle = '';
- if($iReports > 1 && $sReport == 'top')
- $sTitle = _t('_bx_analytics_group_' . $aParams['group']);
- elseif($iReports > 1 && !in_array($sReport, ['filters', 'overview', 'table'], true))
- $sTitle = _t('_bx_analytics_report_' . (isset($aParams['title']) ? $aParams['title'] : $sReport));
- $aBlocks[] = [
- 'title' => $sTitle,
- 'content' => $this->_getBlock($sReport, $aParams ? $aParams : []),
- 'wide' => (bool)$bWide,
- ];
- }
- return $this->_oTemplate->getStudioSection($aBlocks);
- }
- /**
- * A report as a page block: a description NEO renders from for the API, HTML otherwise.
- */
- protected function _getBlock($sReport, $aBlockParams = [])
- {
- if(!$this->serviceIsAvaliable() || !in_array($sReport, $this->_aReports, true))
- return bx_is_api() ? [] : '';
- if(in_array($sReport, $this->_aPeopleReports, true) && $this->checkAllowedPeople() !== CHECK_ACTION_RESULT_ALLOWED)
- return bx_is_api() ? [] : '';
- if(in_array($sReport, $this->_aRevenueReports, true) && $this->checkAllowedRevenue() !== CHECK_ACTION_RESULT_ALLOWED)
- return bx_is_api() ? [] : '';
- if(bx_is_api())
- return [bx_api_get_block('analytics_' . $sReport, $this->_getApiBlockData($sReport, $aBlockParams))];
- $aParams = array_merge($_GET, $aBlockParams);
- $aData = $this->getReportData($sReport, $aParams);
- if($aData === false)
- return '';
- return $this->_oTemplate->getBlock($sReport, $aData, $this->getFilter($aParams), $aBlockParams);
- }
- /*
- * Studio tools: API playground, dashboards, setup. Site administrators only.
- */
- /**
- * Reports the viewer may ask get_report for, with their parameters: common (the filter)
- * and reports => [report => parameters]. Reports with members or money are left out
- * without the permission to see them.
- */
- public function getApiCatalog()
- {
- $aSchema = $this->getReports()->getParamsSchema();
- $aReports = [];
- foreach($this->_aReports as $sReport)
- if($this->_isReportAllowed($sReport) && isset($aSchema['reports'][$sReport]))
- $aReports[$sReport] = $aSchema['reports'][$sReport];
- return ['common' => $aSchema['common'], 'reports' => $aReports];
- }
- /**
- * Run get_report as api.php would for the current viewer and answer with api.php's
- * envelope: status, module, method, params and data, or status and error. Only a report
- * of the catalog is run: the playground never takes a service or a method name.
- *
- * @param string $sReport report name
- * @param mixed $mixedParams the report's parameters, an array or a JSON object string
- */
- public function getApiResponse($sReport, $mixedParams)
- {
- if(is_array($mixedParams))
- $aParams = $mixedParams;
- else
- $aParams = trim((string)$mixedParams) === '' ? [] : json_decode((string)$mixedParams, true);
- if(!is_array($aParams) || (!empty($aParams) && array_values($aParams) === $aParams))
- return ['status' => 400, 'error' => _t('_bx_analytics_txt_api_bad_params')];
- $aCatalog = $this->getApiCatalog();
- if(!is_string($sReport) || !isset($aCatalog['reports'][$sReport]))
- return ['status' => 404, 'error' => _t('_sys_request_page_not_found_cpt')];
- $mixedRet = $this->serviceGetReport($sReport, $aParams);
- if(is_array($mixedRet) && isset($mixedRet['error']))
- return [
- 'status' => isset($mixedRet['code']) ? (int)$mixedRet['code'] : 500,
- 'error' => $mixedRet['error'],
- 'data' => '',
- ];
- return [
- 'status' => 200,
- 'module' => $this->getName(),
- 'method' => 'get_report',
- 'params' => [$sReport, (object)$aParams],
- 'data' => $mixedRet,
- ];
- }
- /**
- * Run a report from the API playground: POST report, params (JSON), csrf_token.
- */
- public function actionApi()
- {
- $this->_checkAdminRequest();
- $iStart = microtime(true);
- $aResponse = $this->getApiResponse((string)bx_get('report', 'post'), (string)bx_get('params', 'post'));
- $this->_outputJson([
- 'ok' => true,
- 'ms' => (int)round(1000 * (microtime(true) - $iStart)),
- 'response' => $aResponse,
- ]);
- }
- /**
- * Site pages with analytics blocks: title, blocks, blocks NEO renders, view and builder
- * links, and whether the page goes away with the module (created by it).
- */
- public function getDashboards()
- {
- $oPermalinks = BxDolPermalinks::getInstance();
- $aResult = [];
- foreach($this->_oDb->getPagesWithBlocks($this->getName()) as $aPage)
- $aResult[] = [
- 'object' => $aPage['object'],
- 'title' => _t($aPage['title']),
- 'blocks' => (int)$aPage['blocks'],
- 'api_blocks' => (int)$aPage['api_blocks'],
- 'view_url' => bx_absolute_url($oPermalinks->permalink($aPage['url'] !== '' ? $aPage['url'] : 'page.php?i=' . $aPage['uri'])),
- 'edit_url' => BX_DOL_URL_STUDIO . 'builder_page.php?type=' . rawurlencode($aPage['module']) . '&page=' . rawurlencode($aPage['object']),
- 'module_page' => $aPage['module'] == $this->getName(),
- ];
- return $aResult;
- }
- /**
- * A new site page of the module with the analytics page's blocks in the center and the
- * right bar, all shown in NEO. Its title is a language string in every language. Pages
- * of the module are removed when the module is disabled.
- *
- * @param string $sTitle page title, 1 to 100 characters
- * @return array|false object, uri, edit_url and view_url, or false
- */
- public function createDashboard($sTitle)
- {
- $sTitle = trim((string)$sTitle);
- if($sTitle === '' || get_mb_len($sTitle) > 100)
- return false;
- $sUri = uriGenerate(get_mb_substr($sTitle, 0, 50), 'sys_objects_page', 'uri', ['empty' => 'analytics']);
- // the object names language keys: Latin letters, digits and _ only, whatever the title
- $sName = trim(preg_replace('/[^a-z0-9_]+/', '_', get_mb_substr($sUri, 0, 40)), '_');
- $sObject = uriGenerate($this->getName() . '_' . ($sName !== '' ? $sName : 'dashboard'), 'sys_objects_page', 'object', ['divider' => '_']);
- if($this->_oDb->isPageObject($sObject) || $this->_oDb->isPageUri($sUri))
- return false;
- $sKey = '_' . $sObject . '_page_title';
- $sKeySystem = '_' . $sObject . '_page_title_sys';
- $oLanguages = BxDolStudioLanguagesUtils::getInstance();
- // disabling the module removes its pages but not their titles: keys of a page that is
- // gone are stale, and adding a string to an existing key fails
- $oLanguages->deleteLanguageString($sKey);
- $oLanguages->deleteLanguageString($sKeySystem);
- if(!$oLanguages->addLanguageString($sKey, $sTitle) || !$oLanguages->addLanguageString($sKeySystem, $sTitle)) {
- $oLanguages->deleteLanguageString($sKey);
- $oLanguages->deleteLanguageString($sKeySystem);
- return false;
- }
- $mixedBlocks = $this->_oDb->insertPageWithBlocks([
- 'object' => $sObject,
- 'uri' => $sUri,
- 'title_system' => $sKeySystem,
- 'title' => $sKey,
- 'module' => $this->getName(),
- 'layout_id' => $this->_iDashboardLayout,
- 'author' => bx_get_logged_profile_id(),
- ], $this->_sDashboardTemplate, $this->_aDashboardCells);
- // page objects and their URIs are cached
- foreach(['sys_pages_objects_data', 'sys_pages_uri_object_map', 'sys_pages_urirewrite_object_map'] as $sCache)
- $this->_oDb->cleanCache($sCache);
- if($mixedBlocks === false) {
- $oLanguages->deleteLanguageString($sKey);
- $oLanguages->deleteLanguageString($sKeySystem);
- return false;
- }
- return [
- 'object' => $sObject,
- 'uri' => $sUri,
- 'blocks' => $mixedBlocks,
- 'edit_url' => BX_DOL_URL_STUDIO . 'builder_page.php?type=' . rawurlencode($this->getName()) . '&page=' . rawurlencode($sObject),
- 'view_url' => bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $sUri)),
- ];
- }
- /**
- * Create an analytics page: POST title, csrf_token. Answers with its builder link.
- */
- public function actionDashboardCreate()
- {
- $this->_checkAdminRequest();
- $aPage = $this->createDashboard((string)bx_get('title', 'post'));
- if($aPage === false)
- $this->_outputJson(['error' => _t('_bx_analytics_txt_dash_create_failed')]);
- $this->_outputJson(['ok' => true, 'url' => $aPage['edit_url']]);
- }
- /**
- * Activity per platform: last (latest hit), hits (last 24 hours), visitors (in visits
- * active in the last 24 hours), sessions (started in the last 7 days), app_version (the
- * highest seen in those visits, apps only). Kept for a minute.
- *
- * @param int $iNow the time to count back from
- * @param bool $bCache use and fill the minute's copy
- */
- public function getPlatformActivity($iNow, $bCache = true)
- {
- $sCache = 'bx_analytics_platform_activity';
- if($bCache) {
- $aCached = $this->_oDb->getCache($sCache, '', 60);
- if(is_array($aCached))
- return $aCached;
- }
- $iDay = $iNow - 86400;
- $iWeek = $iNow - 7 * 86400;
- $aHits = $this->_oDb->getRecentHits($iDay, $iNow);
- $aLast = $this->_oDb->getLastHits($iNow);
- $aVisitors = $this->_oDb->getActiveVisitors($iDay, $iNow);
- $aSessions = $this->_oDb->getStartedSessions($iWeek, $iNow);
- $aApps = array_values(array_filter(array_keys($this->_oConfig->getPlatforms()), [$this->_oConfig, 'isAppPlatform']));
- $aVersions = [];
- foreach($this->_oDb->getAppVersions($iWeek, $iNow, $aApps) as $aRow)
- if(!isset($aVersions[$aRow['platform']]) || version_compare($aRow['app_version'], $aVersions[$aRow['platform']], '>'))
- $aVersions[$aRow['platform']] = $aRow['app_version'];
- $aResult = [];
- foreach(array_keys($this->_oConfig->getPlatforms()) as $sPlatform) {
- $iLast = max(isset($aLast[$sPlatform]) ? (int)$aLast[$sPlatform] : 0, isset($aHits[$sPlatform]) ? (int)$aHits[$sPlatform]['last'] : 0);
- // hits outside a visit (the server's own, actions without a running visit) are
- // only in events: without a hit in the last 24 hours, look up the latest one
- if(!isset($aHits[$sPlatform]))
- $iLast = max($iLast, $this->_oDb->getLastEvent($sPlatform, $iNow));
- $aResult[$sPlatform] = [
- 'last' => $iLast,
- 'hits' => isset($aHits[$sPlatform]) ? (int)$aHits[$sPlatform]['hits'] : 0,
- 'visitors' => isset($aVisitors[$sPlatform]) ? (int)$aVisitors[$sPlatform] : 0,
- 'sessions' => isset($aSessions[$sPlatform]) ? (int)$aSessions[$sPlatform] : 0,
- 'app_version' => isset($aVersions[$sPlatform]) ? $aVersions[$sPlatform] : '',
- ];
- }
- if($bCache)
- $this->_oDb->setCache($sCache, $aResult, 60);
- return $aResult;
- }
- /**
- * Tracker status per platform: enabled, status (receiving: a hit in the last hour,
- * quiet: in the last 7 days, nodata, off) and the activity.
- *
- * @param int $iNow the time to count back from
- * @param bool $bCache use the minute's copy of the activity
- */
- public function getSetupStatus($iNow, $bCache = true)
- {
- $bTracking = $this->_oConfig->isTrackingEnabled();
- $aActivity = $this->getPlatformActivity($iNow, $bCache);
- $aResult = [];
- foreach($aActivity as $sPlatform => $aRow) {
- $bEnabled = $bTracking && $this->_oConfig->isPlatformEnabled($sPlatform);
- $sStatus = 'nodata';
- if(!$bEnabled)
- $sStatus = 'off';
- elseif($aRow['last'] >= $iNow - 3600)
- $sStatus = 'receiving';
- elseif($aRow['last'] >= $iNow - 7 * 86400)
- $sStatus = 'quiet';
- $aResult[$sPlatform] = ['platform' => $sPlatform, 'enabled' => $bEnabled, 'status' => $sStatus] + $aRow;
- }
- return $aResult;
- }
- /**
- * What the classic tracker's collector would do with a pageview of the site's home page
- * from the current request: a dry run that stores nothing.
- */
- public function checkRequest()
- {
- return $this->getCollector()->check($this->_getSetupPayload(BX_ANALYTICS_HIT_PAGEVIEW), $this->_getSetupContext());
- }
- /**
- * Dry run of the tracker for the admin's browser: POST csrf_token. Answers with the
- * result rendered.
- */
- public function actionCheck()
- {
- $this->_checkAdminRequest();
- $this->_outputJson(['ok' => true, 'html' => $this->_oTemplate->getSetupCheckHtml($this->checkRequest())]);
- }
- /**
- * Send a test event from the admin's browser through the real collector: POST
- * csrf_token. It is stored like any hit, so it adds a visit. Answers with whether it
- * was stored and the status table again.
- */
- public function actionTestEvent()
- {
- $this->_checkAdminRequest();
- $iStored = $this->getCollector()->collect($this->_getSetupPayload(BX_ANALYTICS_HIT_EVENT), $this->_getSetupContext());
- $this->_oDb->cleanCache('bx_analytics_platform_activity');
- $this->_outputJson([
- 'ok' => true,
- 'stored' => $iStored,
- 'message' => _t($iStored ? '_bx_analytics_txt_setup_test_stored' : '_bx_analytics_txt_setup_test_dropped'),
- 'html' => $this->_oTemplate->getSetupStatusHtml($this->getSetupStatus(time(), false)),
- ]);
- }
- /**
- * A classic tracker hit on the site's home page, for the Setup checks.
- */
- protected function _getSetupPayload($sType)
- {
- $aHit = ['type' => $sType, 'url' => BX_DOL_URL_ROOT, 'title' => _t('_bx_analytics_txt_setup_test_title')];
- if($sType == BX_ANALYTICS_HIT_EVENT)
- $aHit['name'] = self::SETUP_TEST_EVENT;
- return ['platform' => BX_ANALYTICS_PLATFORM_CORE_WEB, 'hits' => [$aHit]];
- }
- protected function _getSetupContext()
- {
- return [
- 'platform' => BX_ANALYTICS_PLATFORM_CORE_WEB,
- 'same_origin' => true,
- 'profile_id' => bx_get_logged_profile_id(),
- ];
- }
- /**
- * Studio > API: the playground.
- */
- protected function _getStudioApi()
- {
- return $this->_oTemplate->getStudioApi([
- 'catalog' => $this->getApiCatalog(),
- 'api_enabled' => (bool)getParam('sys_api_enable'),
- 'api_access' => getParam('sys_api_access_by_origin') || getParam('sys_api_access_by_key'),
- ]);
- }
- /**
- * Studio > Dashboards: pages with analytics blocks, and a new one.
- */
- protected function _getStudioDashboards()
- {
- return $this->_oTemplate->getStudioDashboards([
- 'pages' => $this->getDashboards(),
- ]);
- }
- /**
- * Studio > Setup: tracker status per platform, what each client needs, custom events,
- * and the checks of the admin's own browser.
- */
- protected function _getStudioSetup()
- {
- $aAlerts = [];
- foreach($this->_oDb->getHandlerAlerts($this->getName()) as $aAlert) {
- // the alerts that record site actions, not the ones that keep data in order; new
- // content of each module (sync_actions) is said in words, not listed
- if(in_array($aAlert['unit'] . ':' . $aAlert['action'], ['profile:delete', 'system:enable', 'system:disable'], true) || ($aAlert['unit'] == 'system' && in_array($aAlert['action'], $this->_aPaymentAlerts, true)) || ($aAlert['action'] == 'added' && $aAlert['unit'] != 'comment'))
- continue;
- $aAlerts[] = $aAlert['unit'] . ':' . $aAlert['action'];
- }
- return $this->_oTemplate->getStudioSetup([
- 'status' => $this->getSetupStatus(time()),
- 'tracking' => $this->_oConfig->isTrackingEnabled(),
- 'injection' => $this->_oDb->isInjectionActive('bx_analytics_tracker'),
- 'track_actions' => $this->_oConfig->isTrackActions(),
- 'alerts' => $aAlerts,
- 'api_enabled' => (bool)getParam('sys_api_enable'),
- 'api_key' => (bool)getParam('sys_api_access_by_key'),
- 'api_origin' => (bool)getParam('sys_api_access_by_origin'),
- ]);
- }
- /**
- * Whether the request is a POST from a site administrator with a valid form token: what
- * every Studio tool action needs.
- */
- public function isAdminRequest()
- {
- return isAdmin() && isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] == 'POST' && BxDolForm::isCsrfTokenValid((string)bx_get('csrf_token', 'post'), false);
- }
- /**
- * Answer 403 and stop, unless the request is an administrator's POST with a valid token.
- */
- protected function _checkAdminRequest()
- {
- if(!$this->isAdminRequest())
- $this->_outputJson(['error' => _t('_sys_txt_access_denied'), 'code' => 403]);
- }
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-blocks Blocks
- * @subsubsection bx_analytics-get_studio_dashboard_traffic get_studio_dashboard_traffic
- *
- * @code bx_srv('bx_analytics', 'get_studio_dashboard_traffic'); @endcode
- *
- * Traffic block of the Studio dashboard: visitors online on every platform and the last 7 days.
- *
- * @return array with `content` for the Studio dashboard
- *
- * @see BxAnalyticsModule::serviceGetStudioDashboardTraffic
- */
- /**
- * @ref bx_analytics-get_studio_dashboard_traffic "get_studio_dashboard_traffic"
- */
- public function serviceGetStudioDashboardTraffic()
- {
- if(!isAdmin())
- return '';
- // the week's figures come from the report cache; only the visitors online are live
- $aOverview = $this->getReportData('overview', ['range' => '7d']);
- $aPlatforms = $this->getReportData('top', ['range' => '7d', 'group' => 'platforms', 'limit' => 5]);
- if($aOverview === false || $aPlatforms === false)
- return '';
- return [
- 'content' => $this->_oTemplate->getStudioDashboardTraffic($aOverview, $this->getReports()->getRealtime($this->getFilter(['range' => '7d']), ['limit' => 0]), $aPlatforms),
- ];
- }
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-other Other
- * @subsubsection bx_analytics-pruning pruning
- *
- * @code bx_srv('bx_analytics', 'pruning'); @endcode
- *
- * Hourly cron job: deletes old daily salts, then data past the retention period in
- * small batches, within a time limit so other cron jobs aren't held up. The revenue
- * ledger is never pruned. While past payments are being imported, the cron imports
- * another step of them.
- *
- * @return int rows deleted
- *
- * @see BxAnalyticsModule::servicePruning
- */
- /**
- * @ref bx_analytics-pruning "pruning"
- */
- public function servicePruning()
- {
- $CNF = &$this->_oConfig->CNF;
- // yesterday's salt is still needed to continue visits that crossed midnight
- $this->_oDb->deleteSaltsBefore(gmdate('Y-m-d', time() - 86400));
- $iBefore = time() - 86400 * $this->_oConfig->getRetentionDays();
- $this->_oDb->deleteBotsBefore(gmdate('Y-m-d', $iBefore));
- $iDeadline = time() + $CNF['PRUNING_SECONDS'];
- $iDeleted = 0;
- foreach($this->getPruningCutoffs(time()) as $sMethod => $iTime)
- do {
- $iBatch = $this->_oDb->$sMethod($iTime, $CNF['PRUNING_BATCH']);
- $iDeleted += $iBatch;
- } while($iBatch >= $CNF['PRUNING_BATCH'] && time() < $iDeadline);
- $this->pruneReportCache(time());
- try {
- $oRevenue = $this->getRevenue();
- if($oRevenue->getBackfillStatus()['state'] == 'running') {
- $oRevenue->backfill(time() + $CNF['BACKFILL_SECONDS']);
- $this->cleanReportCache();
- }
- }
- catch(Throwable $e) {
- bx_log('sys_payments', 'bx_analytics: import of past payments failed: ' . $e->getMessage(), BX_LOG_ERR);
- }
- return $iDeleted;
- }
- /**
- * What pruning deletes and from when: a BxAnalyticsDb delete method => the time before
- * which its rows go. Vitals have a shorter retention of their own (a row per sampled
- * page view), never longer than the rest. Rate limit counters are kept for 2 hours.
- * Error groups that go on happening keep their sample stack as long as the rest.
- *
- * The run's time limit is shared, in this order: the small tables that grow fastest
- * come first, so a backlog of events (after the retention was lowered) can't leave the
- * rate limit counters growing for hours.
- *
- * @param int $iNow the time pruning runs at
- */
- public function getPruningCutoffs($iNow)
- {
- $iBefore = (int)$iNow - 86400 * $this->_oConfig->getRetentionDays();
- return [
- // the collector's rate limit counters: the last full hour and the running one
- 'deleteLimitsBefore' => (int)$iNow - 7200,
- 'deleteVitalsBefore' => (int)$iNow - 86400 * $this->_oConfig->getVitalsRetentionDays(),
- 'deleteErrorHitsBefore' => $iBefore,
- 'deleteErrorGroupsBefore' => $iBefore,
- 'clearErrorStacksBefore' => $iBefore,
- 'deleteEventsBefore' => $iBefore,
- 'deleteSessionsBefore' => $iBefore,
- ];
- }
- /**
- * @page service Service Calls
- * @section bx_analytics Analytics
- * @subsection bx_analytics-tracking Tracking
- * @subsubsection bx_analytics-sync_actions sync_actions
- *
- * @code bx_srv('bx_analytics', 'sync_actions'); @endcode
- *
- * Subscribe to "added" of every content module on the site, so new content becomes an
- * event (comments and votes have site-wide alerts). Runs when the module is enabled and
- * whenever another module is enabled or disabled.
- *
- * @return int subscriptions written
- *
- * @see BxAnalyticsModule::serviceSyncActions
- */
- /**
- * @ref bx_analytics-sync_actions "sync_actions"
- */
- public function serviceSyncActions()
- {
- $aUnits = [];
- foreach(BxDolModuleQuery::getInstance()->getModulesBy(['type' => 'modules', 'active' => 1]) as $aModule) {
- $oModule = BxDolModule::getInstance($aModule['name']);
- if($oModule instanceof iBxDolContentInfoService)
- $aUnits[$aModule['name']][] = 'added';
- }
- $iWritten = $this->_oDb->setActionAlerts($this->getName(), ['added'], $aUnits, ['comment']);
- BxDolAlerts::cacheInvalidate();
- return $iWritten;
- }
- /**
- * Alerts the module listens to: deleted profiles, modules switched on and off, and the
- * site actions it records as events.
- */
- public function onAlert($oAlert)
- {
- $sUnit = $oAlert->sUnit;
- $sAction = $oAlert->sAction;
- // money: whatever goes wrong here must never break a checkout
- if($sUnit == 'system' && !empty($oAlert->aExtras['module']) && in_array($sAction, $this->_aPaymentAlerts, true)) {
- try {
- $this->getRevenue()->onPaymentAlert($oAlert);
- }
- catch(Throwable $e) {
- bx_log('sys_payments', 'bx_analytics: payment alert ' . $sAction . ' failed: ' . $e->getMessage(), BX_LOG_ERR);
- }
- return;
- }
- if($sUnit == 'profile' && $sAction == 'delete') {
- $this->onProfileDelete($oAlert->iObject);
- return;
- }
- if($sUnit == 'system' && in_array($sAction, ['enable', 'disable'], true)) {
- if(empty($oAlert->aExtras['config']['name']) || $oAlert->aExtras['config']['name'] != $this->getName())
- $this->serviceSyncActions();
- return;
- }
- $aContext = ['api_key' => $this->_isApiKeyRequest()];
- $iSender = (int)$oAlert->iSender;
- switch($sAction) {
- case 'add':
- if($sUnit == 'account') {
- $this->getCollector()->collectAction('signup', 'account', $oAlert->iObject, 0, $aContext);
- return;
- }
- if($sUnit == 'profile') {
- $sType = isset($oAlert->aExtras['module']) ? $oAlert->aExtras['module'] : '';
- $this->getCollector()->collectAction('profile_add', $sType, $oAlert->iObject, $oAlert->iObject, $aContext);
- }
- return;
- case 'login':
- if($sUnit == 'account') {
- $oProfile = BxDolProfile::getInstanceAccountProfile($oAlert->iObject);
- $this->getCollector()->collectAction('login', 'account', $oAlert->iObject, $oProfile ? $oProfile->id() : 0, $aContext);
- }
- return;
- case 'connection_added':
- if($sUnit == 'system')
- $this->getCollector()->collectAction('connection', isset($oAlert->aExtras['object_name']) ? $oAlert->aExtras['object_name'] : '', isset($oAlert->aExtras['content']) ? (int)$oAlert->aExtras['content'] : 0, $iSender, $aContext);
- return;
- case 'added':
- // comments of any system, or new content of a module
- if($sUnit == 'comment')
- $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);
- else
- $this->getCollector()->collectAction('content_add', $sUnit, $oAlert->iObject, $iSender, $aContext);
- return;
- case 'do':
- if($sUnit == 'vote') {
- $aProps = [];
- if(!empty($oAlert->aExtras['reaction']) && is_string($oAlert->aExtras['reaction']))
- $aProps['reaction'] = $oAlert->aExtras['reaction'];
- $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]);
- }
- return;
- }
- }
- /**
- * Forget a deleted profile: its visits stay in reports as an anonymous visitor's. Its
- * payments stay in the revenue ledger, as they stay in the payment module, but lose the
- * visit and the traffic sources they were tied to.
- */
- public function onProfileDelete($iProfileId)
- {
- $iProfileId = (int)$iProfileId;
- if(!$iProfileId)
- return 0;
- $iChanged = $this->_oDb->forgetProfile($iProfileId, random_bytes(16)) + $this->_oDb->forgetRevenueProfile($iProfileId);
- // cached reports may still show the member's name and link
- if($iChanged)
- $this->cleanReportCache();
- return $iChanged;
- }
- public function serviceGetOptionsPlatforms()
- {
- $aResult = [];
- foreach($this->_oConfig->getPlatforms() as $sCode => $sTitle)
- $aResult[$sCode] = _t($sTitle);
- return $aResult;
- }
- public function serviceGetOptionsIdentity()
- {
- return [
- BX_ANALYTICS_IDENTITY_COOKIELESS => _t('_bx_analytics_identity_cookieless'),
- BX_ANALYTICS_IDENTITY_PERSISTENT => _t('_bx_analytics_identity_persistent'),
- ];
- }
- public function serviceGetOptionsGeoSource()
- {
- return [
- BX_ANALYTICS_GEO_NONE => _t('_bx_analytics_geo_none'),
- BX_ANALYTICS_GEO_HEADERS => _t('_bx_analytics_geo_headers'),
- BX_ANALYTICS_GEO_CLOUDFLARE => _t('_bx_analytics_geo_cloudflare'),
- BX_ANALYTICS_GEO_VERCEL => _t('_bx_analytics_geo_vercel'),
- BX_ANALYTICS_GEO_CLOUDFRONT => _t('_bx_analytics_geo_cloudfront'),
- ];
- }
- public function getCollector()
- {
- bx_import('UserAgent', $this->_aModule);
- bx_import('Source', $this->_aModule);
- bx_import('Request', $this->_aModule);
- bx_import('Errors', $this->_aModule);
- bx_import('Vitals', $this->_aModule);
- bx_import('Collector', $this->_aModule);
- return new BxAnalyticsCollector($this);
- }
- public function getFilter($aParams)
- {
- bx_import('Filter', $this->_aModule);
- return new BxAnalyticsFilter($this, $aParams);
- }
- /**
- * The revenue ledger, for the current request.
- */
- public function getRevenue()
- {
- $this->getCollector(); // loads the request, user agent and collector classes
- bx_import('Revenue', $this->_aModule);
- return new BxAnalyticsRevenue($this);
- }
- public function getReports()
- {
- if($this->_oReports === null) {
- bx_import('Errors', $this->_aModule);
- bx_import('Vitals', $this->_aModule);
- bx_import('Reports', $this->_aModule);
- $this->_oReports = new BxAnalyticsReports($this);
- }
- return $this->_oReports;
- }
- /**
- * Report data by name, or false for an unknown report or parameters it doesn't know.
- */
- public function getReportData($sReport, $aParams)
- {
- if(!$this->_isReportAllowed($sReport))
- return false;
- $oReports = $this->getReports();
- $sMethod = 'get' . bx_gen_method_name($sReport);
- if(!method_exists($oReports, $sMethod))
- return false;
- $oFilter = $this->getFilter($aParams);
- // the same report with the same filters and parameters, for the same kind of viewer,
- // comes from the cache for a few minutes; live figures never do
- $sCache = $oReports->isLive($sReport, $aParams) ? '' : $this->_getReportCacheName($sReport, $oFilter, $aParams);
- if($sCache !== '') {
- $aCached = $this->_getReportCache($sCache);
- if(is_array($aCached))
- return $aCached;
- }
- $aData = $oReports->$sMethod($oFilter, $aParams);
- if($aData === false)
- return false;
- $aData['filter'] = $oFilter->getParams();
- if($sCache !== '')
- $this->_setReportCache($sCache, $aData);
- return $aData;
- }
- /**
- * Drop every cached report, after a change that reports show (goals, funnels, the status
- * of an error, a deleted member). Report names carry a generation, and a new one leaves
- * the old copies to expire: Memcache and Memcached delete by prefix by emptying the whole
- * server, and Redis by a KEYS scan of every key, so nothing is deleted by prefix there.
- * Files are deleted too (keys of the database cache are `db_<name>_<site hash>`, see
- * BxDolDb::genDbCacheKey, so the prefix is the name's start after `db_`).
- */
- public function cleanReportCache()
- {
- $oCache = $this->_getReportCacheEngine();
- if(!$oCache)
- return;
- if($oCache instanceof BxDolCacheFileHtml)
- $oCache->removeAllByPrefix('db_' . $this->_oConfig->CNF['REPORT_CACHE_PREFIX']);
- $this->_sReportCacheGeneration = sprintf('%.6F', microtime(true));
- $oCache->setData($this->_oDb->genDbCacheKey(self::REPORT_CACHE_GENERATION), $this->_sReportCacheGeneration, self::REPORT_CACHE_GENERATION_TTL);
- }
- /**
- * The generation of cached reports: changed by cleanReportCache(), '' before the first.
- */
- protected function _getReportCacheGeneration()
- {
- if($this->_sReportCacheGeneration !== null)
- return $this->_sReportCacheGeneration;
- $oCache = $this->_getReportCacheEngine();
- $sGeneration = $oCache ? $oCache->getData($this->_oDb->genDbCacheKey(self::REPORT_CACHE_GENERATION)) : '';
- return $this->_sReportCacheGeneration = is_string($sGeneration) ? $sGeneration : '';
- }
- /**
- * Delete cached reports past their time to live, in files: a file is deleted when its own
- * report is read again, and most never are (yesterday's filters, a tab opened once).
- * Other cache engines expire entries themselves.
- *
- * @param int $iNow the current time
- * @return int files deleted
- */
- public function pruneReportCache($iNow)
- {
- $oCache = $this->_getReportCacheEngine();
- if(!$oCache)
- $oCache = $this->_oDb->getDbCacheObject();
- if(!($oCache instanceof BxDolCacheFile) || !($rDir = opendir(BX_DIRECTORY_PATH_CACHE)))
- return 0;
- $sPrefix = 'db_' . $this->_oConfig->CNF['REPORT_CACHE_PREFIX'];
- $iBefore = (int)$iNow - $this->_oConfig->CNF['REPORT_CACHE_TTL'];
- $iDeleted = 0;
- while(($sFile = readdir($rDir)) !== false)
- if(bx_mb_strpos($sFile, $sPrefix) === 0 && @filemtime(BX_DIRECTORY_PATH_CACHE . $sFile) < $iBefore && $oCache->delData($sFile))
- $iDeleted++;
- closedir($rDir);
- return $iDeleted;
- }
- /**
- * The cache of reports: the database cache's engine, but with files as plain text. The
- * file engine writes PHP scripts read with include(), and opcache would keep a compiled
- * copy of every report (a new one for every filter, page and day) until it ran out of
- * memory. Null when the database cache is off.
- */
- protected function _getReportCacheEngine()
- {
- if(!getParam('sys_db_cache_enable'))
- return null;
- $oCache = $this->_oDb->getDbCacheObject();
- return $oCache instanceof BxDolCacheFile ? bx_instance('BxDolCacheFileHtml') : $oCache;
- }
- /**
- * A cached report, as JSON after REPORT_CACHE_GUARD, or null.
- */
- protected function _getReportCache($sName)
- {
- $oCache = $this->_getReportCacheEngine();
- if(!$oCache)
- return null;
- $sData = $oCache->getData($this->_oDb->genDbCacheKey($sName), $this->_oConfig->CNF['REPORT_CACHE_TTL']);
- if(!is_string($sData) || bx_mb_strpos($sData, self::REPORT_CACHE_GUARD) !== 0)
- return null;
- $aData = json_decode(get_mb_substr($sData, get_mb_len(self::REPORT_CACHE_GUARD), get_mb_len($sData)), true);
- return is_array($aData) ? $aData : null;
- }
- protected function _setReportCache($sName, $aData)
- {
- $oCache = $this->_getReportCacheEngine();
- $sData = json_encode($aData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION);
- if(!$oCache || $sData === false)
- return false;
- return $oCache->setData($this->_oDb->genDbCacheKey($sName), self::REPORT_CACHE_GUARD . $sData, $this->_oConfig->CNF['REPORT_CACHE_TTL']);
- }
- /**
- * The cache name of a report: the report, the resolved filter (not the current second,
- * so a cached copy serves the whole period), the parameters the report reads, the
- * language, what the viewer may see (money, people, managing, administration), since
- * reports differ by those, and the generation (see cleanReportCache).
- */
- protected function _getReportCacheName($sReport, $oFilter, $aParams)
- {
- $aFilter = $oFilter->get();
- unset($aFilter['now'], $aFilter['buckets']);
- $aOwn = [];
- foreach($this->getReports()->getCacheParams($sReport, $aParams) as $sKey => $mixedValue)
- if(is_string($sKey) && $sKey !== '' && $sKey[0] != '_')
- $aOwn[$sKey] = $mixedValue;
- ksort($aOwn);
- $aViewer = [
- isAdmin(),
- $this->checkAllowedRevenue() === CHECK_ACTION_RESULT_ALLOWED,
- $this->checkAllowedPeople() === CHECK_ACTION_RESULT_ALLOWED,
- $this->checkAllowedManage() === CHECK_ACTION_RESULT_ALLOWED,
- ];
- return $this->_oConfig->CNF['REPORT_CACHE_PREFIX'] . hash('sha256', json_encode([$sReport, $aFilter, $aOwn, bx_lang_name(), $aViewer, $this->_getReportCacheGeneration()]));
- }
- /**
- * A report's rows for a CSV download, behind the same checks as getReportData: header,
- * rows, total, truncated, and filename (made of the report, the dimension and the
- * period's dates only). False when the viewer may not use analytics or see the report,
- * and for a report without a download. The use isn't counted: the action is countable,
- * and reading a report again is not another use.
- */
- public function getReportExport($sReport, $aParams)
- {
- if(!$this->serviceIsAvaliable() || !in_array($sReport, $this->_aExportReports, true) || !$this->_isReportAllowed($sReport))
- return false;
- $oFilter = $this->getFilter($aParams);
- $aExport = $this->getReports()->getExport($sReport, $oFilter, $aParams, $this->_oConfig->CNF['MAX_EXPORT_ROWS']);
- if($aExport === false)
- return false;
- $aFilter = $oFilter->get();
- bx_import('Csv', $this->_aModule);
- $aExport['filename'] = BxAnalyticsCsv::getFilename([$sReport, $aExport['dimension'], $aFilter['from_date'], $aFilter['to_date']]);
- return $aExport;
- }
- /**
- * Whether a report exists and the viewer may see it: reports with members need "view
- * analytics people", reports with money "view analytics revenue".
- */
- protected function _isReportAllowed($sReport)
- {
- if(!in_array($sReport, $this->_aReports, true))
- return false;
- if(in_array($sReport, $this->_aPeopleReports, true) && $this->checkAllowedPeople() !== CHECK_ACTION_RESULT_ALLOWED)
- return false;
- if(in_array($sReport, $this->_aRevenueReports, true) && $this->checkAllowedRevenue() !== CHECK_ACTION_RESULT_ALLOWED)
- return false;
- return true;
- }
- /**
- * What an API block carries: the report and the endpoint NEO calls for its data.
- */
- protected function _getApiBlockData($sReport, $aParams = [])
- {
- $aRanges = [];
- foreach(['today', 'yesterday', '24h', '7d', '30d', '90d', '12m'] as $sRange)
- $aRanges[] = ['value' => $sRange, 'title' => _t('_bx_analytics_range_' . $sRange)];
- $aPlatforms = [];
- foreach($this->_oConfig->getPlatforms() as $sCode => $sTitle)
- $aPlatforms[] = ['value' => $sCode, 'title' => _t($sTitle)];
- $aMetrics = [];
- foreach($this->getReports()->getMetrics() as $sMetric)
- $aMetrics[$sMetric] = _t('_bx_analytics_metric_' . $sMetric);
- return [
- 'report' => $sReport,
- 'params' => (object)$aParams,
- // the element asks for its data itself, with the page's filters, from get_report
- 'request_url' => 'bx_analytics/get_report',
- 'ranges' => $aRanges,
- 'platforms' => $aPlatforms,
- 'metrics' => $aMetrics,
- 'strings' => [
- 'range' => _t('_bx_analytics_txt_range'),
- 'platform' => _t('_bx_analytics_txt_platform'),
- 'all_platforms' => _t('_bx_analytics_txt_all_platforms'),
- 'online' => _t('_bx_analytics_txt_online_now', '{0}'),
- 'remove_filter' => _t('_bx_analytics_txt_remove_filter', '{0}'),
- 'filter_by' => _t('_bx_analytics_txt_filter_by', '{0}'),
- 'previous_period' => _t('_bx_analytics_txt_previous_period', '{0}'),
- 'key_figures' => _t('_bx_analytics_txt_key_figures'),
- 'visitors' => _t('_bx_analytics_metric_visitors'),
- 'no_data' => _t('_bx_analytics_txt_no_data'),
- // NEO shows a Retry button next to it, so no "reload the page"
- 'error' => _t('_bx_analytics_txt_error_report'),
- 'duration_h' => _t('_bx_analytics_txt_duration_h'),
- 'duration_m' => _t('_bx_analytics_txt_duration_m'),
- 'duration_s' => _t('_bx_analytics_txt_duration_s'),
- ],
- ];
- }
- public function isApiKeyRequest()
- {
- return $this->_isApiKeyRequest();
- }
- /**
- * The request came through api.php with an API key that api.php has already checked:
- * the NEO server, whose proxy headers carry the visitor's address.
- */
- protected function _isApiKeyRequest()
- {
- if(!defined('BX_API') || !getParam('sys_api_access_by_key'))
- return false;
- $sAuth = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] : '';
- if($sAuth === '' && function_exists('getallheaders')) {
- $aHeaders = array_change_key_case((array)getallheaders(), CASE_LOWER);
- $sAuth = isset($aHeaders['authorization']) ? $aHeaders['authorization'] : '';
- }
- return strpos($sAuth, 'Bearer ') === 0;
- }
- /**
- * Action methods
- */
-
- public function actionGetReports($sModuleName)
- {
- if(!$this->serviceIsAvaliable())
- return '';
-
- header('Content-Type: application/json');
-
- $aSystemsVote = BxDolVote::getSystems();
- $aRv = array();
- if ($sModuleName == BX_ANALYTICS_SYSTEM){
- $aRv[BX_ANALYTICS_TOP_BY_LIKES] = _t('_bx_analytics_type_top_contents_by_likes');
- $aRv[BX_ANALYTICS_TOP_BY_VIEWS] = _t('_bx_analytics_type_top_by_views');
- $aRv[BX_ANALYTICS_CONTENT_TOTAL] = _t('_bx_analytics_type_accounts_growth');
- $aRv[BX_ANALYTICS_CONTENT_SPEED] = _t('_bx_analytics_type_accounts_speed');
- }
- else{
- $oModule = BxDolModule::getInstance($sModuleName);
-
- if(BxDolRequest::serviceExists($sModuleName, 'act_as_profile') && BxDolService::call($sModuleName, 'act_as_profile') == true){
- $aRv[BX_ANALYTICS_MOST_FOLLOWED_PROFILES] = _t('_bx_analytics_type_most_followed_profiles');
- $aRv[BX_ANALYTICS_MOST_ACTIVE_PROFILES] = _t('_bx_analytics_type_most_active_profiles');
- }
-
- if(isset($oModule->_oConfig->CNF['OBJECT_VOTES']) && isset($aSystemsVote[$oModule->_oConfig->CNF['OBJECT_VOTES']])){
- $aRv[BX_ANALYTICS_TOP_BY_LIKES] = _t('_bx_analytics_type_top_contents_by_likes');
- }
-
- if(isset($oModule->_oConfig->CNF['OBJECT_VIEWS'])){
- $aRv[BX_ANALYTICS_TOP_BY_VIEWS] = _t('_bx_analytics_type_top_by_views');
- }
-
- $aRv[BX_ANALYTICS_CONTENT_TOTAL] = _t('_bx_analytics_type_content_growth');
- $aRv[BX_ANALYTICS_CONTENT_SPEED] = _t('_bx_analytics_type_content_speed');
- }
- /**
- * @hooks
- * @hookdef hook-bx_analytics-get_reports 'bx_analytics', 'get_reports' - hook on get avalaible reports for specified module
- * - $unit_name - equals `bx_analytics`
- * - $action - equals `get_reports`
- * - $object_id - not used
- * - $sender_id -not used
- * - $extra_params - array of additional params with the following array keys:
- * - `module` - [string] module name
- * - `list` - [array] by ref, array of avalaible reports for analytics as [report_name] = report_title, can be overridden in hook processing
- * @hook @ref hook-bx_analytics-get_reports
- */
- bx_alert($this->_aModule['name'], 'get_reports', 0, 0, array(
- 'module' => $sModuleName,
- 'list' => &$aRv,
- 'list_ref' => &$aRv,
- ));
-
- echo json_encode($aRv);
- }
-
- public function actionGetReportsData($sModuleName, $sReportName, $sDateFrom, $sDateTo, $sType = '')
- {
- if(!$this->serviceIsAvaliable())
- return '';
-
- if ($sType == 'csv'){
- header('Content-type: text/csv');
- header('Content-disposition: attachment;filename=' . $sModuleName . '_' . $sReportName . '_' . $sDateFrom . '_' . $sDateTo . '.csv');
- echo $this->actionGetReportsDataCsv($sModuleName, $sReportName, $sDateFrom, $sDateTo);
-
- }
-
- if ($sType == ''){
- header('Content-Type: application/json');
- echo $this->actionGetReportsDataJson($sModuleName, $sReportName, $sDateFrom, $sDateTo);
- }
- }
-
- private function actionGetReportsDataCsv($sModuleName, $sReportName, $sDateFrom, $sDateTo)
- {
- $sRv = '';
- $sColumn = ";";
- $sRow = "\n";
- $sData = $this->actionGetReportsDataJson($sModuleName, $sReportName, $sDateFrom, $sDateTo);
- $oRv = json_decode($sData, true);
- $oData = $oRv['data'];
- $sCol1 = $oRv['strings'][0];
- $iColumnCount = count($oData['datasets']);
- $sRv .= $sCol1 . $sColumn;
- foreach($oData['datasets'] as $aItem){
- $sRv .= ($aItem['label'] != '' ? $aItem['label'] : $oRv['strings'][1]) . $sColumn;
- }
-
- $sRv .= $sRow;
- $iK = 0;
- foreach($oData['datasets'][0]['data'] as $sDx => $aItemData){
- $sTxt = "";
- if (is_array($oData['datasets'][0]['data'][$sDx]))
- $sTxt = $oData['datasets'][0]['data'][$sDx]['x'];
- else
- $sTxt = $oData['labels'][$sDx];
- if (isset($oRv['links']) && count($oRv['links'])>0) {
- $sTxt .= ' (' . $oRv['links'][$sDx] . ')';
- }
-
- $sRv .= $sTxt . $sColumn;
- for ($i = 0; $i < $iColumnCount; $i++){
- $sText = '';
- if (is_array($aItemData)) {
- $sText = $aItemData['y'];
- }
- else {
- $sText = $aItemData;
- }
- $sRv .= $sText . $sColumn;
- }
- $sRv .= $sRow;
- $iK++;
- }
- echo $sRv;
- }
-
- private function actionGetReportsDataJson($sModuleName, $sReportName, $sDateFrom, $sDateTo)
- {
- $iDateFrom = strtotime($sDateFrom);
- $iDateTo = strtotime($sDateTo) + 86400;
- $sType = "bar";
- $bIsTimeX = false;
- $iMinValueY = 0;
- $iMaxValueY = 0;
- $bDefaultFillData = true;
- $sReportType = $sReportName;
-
- $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')));
-
- if ($sModuleName != BX_ANALYTICS_SYSTEM){
- $oModule = BxDolModule::getInstance($sModuleName);
- $aValues['strings'][0] = $oModule->_aModule['title'];
- $aValues['strings'][1] = _t('_bx_analytics_type_' . $sReportType . '_label');
- }
- else{
- $aValues['strings'][0] = _t('_bx_analytics_txt_total_content');
- $aValues['strings'][1] = _t('_bx_analytics_type_' . $sReportType . '_label');
- }
-
- switch ($sReportType) {
- case BX_ANALYTICS_TOP_BY_LIKES:
- $bDefaultFillData = false;
- $this->getViewsOrVotes(BxDolVote::getSystems(), 'OBJECT_VOTES', $sModuleName, $iDateFrom, $iDateTo, $aValues, $iMaxValueY, $iMinValueY);
- break;
-
- case BX_ANALYTICS_TOP_BY_VIEWS:
- $bDefaultFillData = false;
- $this->getViewsOrVotes(BxDolView::getSystems(), 'OBJECT_VIEWS', $sModuleName, $iDateFrom, $iDateTo, $aValues, $iMaxValueY, $iMinValueY);
- break;
-
- case BX_ANALYTICS_MOST_FOLLOWED_PROFILES:
- $oModule = BxDolModule::getInstance($sModuleName);
- $aData = $this->_oDb->getMostFollowedProfiles($sModuleName, $iDateFrom, $iDateTo);
- break;
-
- case BX_ANALYTICS_MOST_ACTIVE_PROFILES:
- $aData = array();
- $oModule = BxDolModule::getInstance($sModuleName);
- $aTmp = $this->getSelectedModules();
- foreach($aTmp[0] as $sContentModule){
- $oContentModule = BxDolModule::getInstance($sContentModule);
- if ($oContentModule && isset($oContentModule->_oConfig->CNF['TABLE_ENTRIES']) && isset($oContentModule->_oConfig->CNF['FIELD_AUTHOR'])){
- $sContentTable = $oContentModule->_oConfig->CNF['TABLE_ENTRIES'];
- $sColumnAuthor = $oContentModule->_oConfig->CNF['FIELD_AUTHOR'];
- $sColumnAdded = $oContentModule->_oConfig->CNF['FIELD_ADDED'];
- if (!empty($sContentTable) && !empty($sColumnAuthor) && !empty($sColumnAdded) && (!BxDolRequest::serviceExists($sContentModule, 'act_as_profile') || BxDolService::call($sContentModule, 'act_as_profile') == false)){
- $aData1 = $this->_oDb->getMostActiveProfiles($sModuleName, $sContentModule, $sContentTable, $sColumnAuthor, $sColumnAdded, $iDateFrom, $iDateTo);
- foreach($aData1 as $aTmp3){
- if (array_key_exists($aTmp3['object_id'], $aData))
- $aData[$aTmp3['object_id']]['value'] += $aTmp3['value'];
- else
- $aData[$aTmp3['object_id']] = $aTmp3;
- }
- }
- }
- }
- $aData = $this->sortArrayOfArraysAndSlice($aData, 'value', intval(getParam('bx_analytics_items_count')));
- break;
-
- default:
- $sType = "line";
- $sTableName = '';
- $aValues['strings'][0] = _t('_bx_analytics_txt_date');
- $bDefaultFillData = false;
- if ($sModuleName != BX_ANALYTICS_SYSTEM){
- $oModule = BxDolModule::getInstance($sModuleName);
- if (isset($oModule->_oConfig->CNF['TABLE_ENTRIES']))
- $sTableName = $oModule->_oConfig->CNF['TABLE_ENTRIES'];
- $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']));
- }
- else{
- $sTableName = 'sys_accounts';
- $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')));
- }
- if (isset($oModule) && isset($oModule->_oConfig->CNF['FIELD_ADDED']))
- $sColumnAdded = $oModule->_oConfig->CNF['FIELD_ADDED'];
- else
- $sColumnAdded = 'added';
-
- $aData = array();
- $iValuePrev = 0;
-
- /**
- * @hooks
- * @hookdef hook-bx_analytics-get_chart_data_line 'bx_analytics', 'get_chart_data_line' - hook on get avalaible reports for specified module
- * - $unit_name - equals `bx_analytics`
- * - $action - equals `get_chart_data_line`
- * - $object_id - not used
- * - $sender_id -not used
- * - $extra_params - array of additional params with the following array keys:
- * - `module` - [string] module name
- * - `report_name` - [string] report name
- * - `report_type` - [string] by ref, report name, can be overridden in hook processing
- * - `date_from` - [int] report's start of interval in php time format
- * - `date_to` - [int] report's end of interval in php time format
- * - `data` - [array] by ref, array of data for chart, can be overridden in hook processing
- * - `prev_value` - [int] by ref, previous value for chart (started value), can be overridden in hook processing
- * @hook @ref hook-bx_analytics-get_chart_data_line
- */
- bx_alert($this->_aModule['name'], 'get_chart_data_line', 0, 0, array(
- 'module' => $sModuleName,
- 'report_name' => $sReportName,
- 'report_type' => &$sReportType,
- 'report_type_ref' => &$sReportType,
- 'date_from' => $iDateFrom,
- 'date_to' => $iDateTo,
- 'data' => &$aData,
- 'data_ref' => &$aData,
- 'prev_value' => &$iValuePrev,
- 'prev_value_ref' => &$iValuePrev,
- ));
-
-
-
- if ($sColumnAdded != '' && count($aData) == 0){
- $aData = $this->_oDb->getGrowth($sTableName, $sColumnAdded, $iDateFrom, $iDateTo);
- $iValuePrev = $this->_oDb->getGrowthInitValue($sTableName, $sColumnAdded, $iDateFrom);
- }
-
- if (count($aData) > 0){
- $aTmpDates = array();
- foreach ($aData as $aValue) {
- $sX = $aValue['period'];
- $aTmpDates[$sX] = $aValue['count'];
- }
- for ($i = $iDateFrom; $i < $iDateTo ; $i = $i + 86400 ){
- $sX = date('Y-m-d', $i);
- if (!array_key_exists($sX, $aTmpDates)){
- array_push($aValues['values'][0]['data'], array('x' => $sX, 'y' => $sReportType == BX_ANALYTICS_CONTENT_TOTAL ? $iValuePrev : 0));
- }
- else{
- array_push($aValues['values'][0]['data'], array('x' => $sX, 'y' => $sReportType == BX_ANALYTICS_CONTENT_TOTAL ? ($iValuePrev + $aTmpDates[$sX]) : $aTmpDates[$sX]));
- $iValuePrev += $aTmpDates[$sX];
- }
- //array_push($aValues['labels'], $sX);
- if ($sReportType == BX_ANALYTICS_CONTENT_TOTAL){
- if ($iValuePrev > $iMaxValueY)
- $iMaxValueY = $iValuePrev;
- }
- else{
- if (array_key_exists($sX, $aTmpDates) && $aTmpDates[$sX] > $iMaxValueY)
- $iMaxValueY = $aTmpDates[$sX];
- }
- }
- }
- break;
- }
-
- if ($bDefaultFillData){
- foreach ($aData as $aValue) {
- array_push($aValues['labels'], $this->getItemName($oModule->serviceGetTitle($aValue['object_id'])));
- array_push($aValues['values'][0]['data'], $aValue['value']);
- array_push($aValues['links'], $oModule->serviceGetLink($aValue['object_id']));
- if ($aValue['value'] > $iMaxValueY)
- $iMaxValueY = $aValue['value'];
- }
- }
- $iMaxValueY = ceil($iMaxValueY * 1.1);
-
- $aDataForChartXAxes = array();
- if ($bIsTimeX){
- $sUnit = 'day';
- $iInterval = ($iDateTo - $iDateFrom) / 86400;
- if ($iInterval > 50)
- $sUnit = 'week';
- if ($iInterval > 100)
- $sUnit = 'month';
-
- $aDataForChartXAxes = array(
- 'type' => 'time',
- 'time' => array(
- 'tooltipFormat' => 'DD.MM.YYYY',
- 'unit' => $sUnit,
- ),
- 'display' => true,
- 'distribution' => 'linear',
- 'ticks' => array(
- 'display' => true,
- 'autoSkip' => true,
- )
- );
- }
- else{
- $aDataForChartXAxes = array(
- 'ticks' => array(
- 'autoSkip' => true
- ),
- 'display' => true
- );
- }
-
- $aDataForChartDatasets = array();
- for($i = 0; $i < count($aValues['values']); $i++){
- $aDataForChartDatasets[] = array(
- 'label' => $aValues['values'][$i]['legend'],
- 'fill' => false,
- 'backgroundColor' => $this->aColors[$i],
- 'borderColor' => $this->aColors[$i],
- 'borderWidth' => 2,
- 'data' => $aValues['values'][$i]['data']
- );
- }
-
- $aDataForChart = array(
- 'type' => $sType,
- 'data' => array(
- 'labels' => $aValues['labels'],
- 'datasets' => $aDataForChartDatasets
- ),
- 'options' => array(
- 'legend' => array(
- 'position' => 'bottom',
- 'display' => count($aValues['values']) == 1 ? false :true
- ),
- 'elements' => array(
- 'point'=> array(
- 'radius' => 0
- )
- ),
- 'scales' => array(
- 'y' => array(
- 'ticks' => array(
- 'max' => $iMaxValueY,
- 'min' => $iMinValueY,
- 'stepSize' => $this->getStep($iMinValueY, $iMaxValueY),
- 'autoSkip' => true
- )
- ),
- 'x' => $aDataForChartXAxes
- )
- ),
- 'links' => $aValues['links'],
- 'strings' => $aValues['strings'],
- );
-
- return json_encode($aDataForChart);
- }
-
- /**
- * Who may define goals and funnels: site administrators, and levels with "manage analytics"
- * besides "use analytics".
- */
- public function checkAllowedManage()
- {
- if(isAdmin())
- return CHECK_ACTION_RESULT_ALLOWED;
- if($this->checkAllowed() !== CHECK_ACTION_RESULT_ALLOWED)
- return _t('_sys_txt_access_denied');
- $aCheck = checkActionModule(bx_get_logged_profile_id(), 'manage analytics', $this->getName(), false);
- return $aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED ? CHECK_ACTION_RESULT_ALLOWED : _t('_sys_txt_access_denied');
- }
- /**
- * Save or delete a goal: POST op (save, delete), id, name, type (page, event), pattern, csrf_token.
- */
- public function actionGoal()
- {
- $aResult = $this->_checkManageRequest();
- if($aResult === true) {
- $iId = (int)bx_get('id', 'post');
- if(bx_get('op', 'post') == 'delete')
- $aResult = ['ok' => $this->_oDb->deleteGoal($iId)];
- else {
- $aGoal = $this->_getMatchInput(bx_get('type', 'post'), bx_get('pattern', 'post'));
- $sName = trim((string)bx_get('name', 'post'));
- if($aGoal === false || $sName === '' || get_mb_len($sName) > 128)
- $aResult = ['error' => _t('_bx_analytics_txt_goal_invalid')];
- else if(!$iId && $this->isGoalLimitReached())
- $aResult = ['error' => _t('_bx_analytics_txt_goal_limit', $this->_oConfig->CNF['MAX_GOALS'])];
- else {
- $iId = $this->_oDb->saveGoal($iId, ['name' => $sName] + $aGoal);
- $aResult = ['ok' => (bool)$iId, 'id' => $iId];
- }
- }
- $this->cleanReportCache();
- }
- $this->_outputJson($aResult);
- }
- /**
- * Whether there are as many goals as there can be: the Goals report counts each one in
- * both periods, a query per goal and period.
- */
- public function isGoalLimitReached()
- {
- return $this->_oDb->getGoalsCount() >= $this->_oConfig->CNF['MAX_GOALS'];
- }
- /**
- * Save or delete a funnel: POST op, id, name, step_type[], step_pattern[], csrf_token.
- */
- public function actionFunnel()
- {
- $aResult = $this->_checkManageRequest();
- if($aResult === true) {
- $iId = (int)bx_get('id', 'post');
- if(bx_get('op', 'post') == 'delete')
- $aResult = ['ok' => $this->_oDb->deleteFunnel($iId), 'id' => 0];
- else {
- $aTypes = bx_get('step_type', 'post');
- $aPatterns = bx_get('step_pattern', 'post');
- $aSteps = [];
- if(is_array($aTypes) && is_array($aPatterns))
- foreach(array_values($aTypes) as $i => $sType) {
- if(!isset($aPatterns[$i]) || trim((string)$aPatterns[$i]) === '')
- continue;
- $aStep = $this->_getMatchInput($sType, $aPatterns[$i]);
- if($aStep === false) {
- $aSteps = false;
- break;
- }
- $aSteps[] = $aStep;
- }
- $sName = trim((string)bx_get('name', 'post'));
- if($aSteps === false || count($aSteps) < 2 || count($aSteps) > $this->_oConfig->CNF['MAX_FUNNEL_STEPS'] || $sName === '' || get_mb_len($sName) > 128)
- $aResult = ['error' => _t('_bx_analytics_txt_funnel_invalid')];
- else {
- $iId = $this->_oDb->saveFunnel($iId, $sName, $aSteps);
- $aResult = ['ok' => (bool)$iId, 'id' => $iId];
- }
- }
- $this->cleanReportCache();
- }
- $this->_outputJson($aResult);
- }
- /**
- * Mark an error group open, resolved or ignored: POST op (the status), id, csrf_token.
- * An ignored error is left out of the lists but keeps counting; a resolved one opens
- * again when it happens after that.
- */
- public function actionErrorStatus()
- {
- $aResult = $this->_checkManageRequest();
- if($aResult === true) {
- $sStatus = (string)bx_get('op', 'post');
- $iId = (int)bx_get('id', 'post');
- if(!in_array($sStatus, ['open', 'resolved', 'ignored'], true) || !$iId || !$this->_oDb->getErrorGroup($iId))
- $aResult = ['error' => _t('_sys_request_page_not_found_cpt'), 'code' => 404];
- else {
- $aResult = ['ok' => $this->_oDb->setErrorStatus($iId, $sStatus, time()), 'id' => $iId];
- $this->cleanReportCache();
- }
- }
- $this->_outputJson($aResult);
- }
- /**
- * A POST from a manager with a valid form token, or an error array.
- */
- protected function _checkManageRequest()
- {
- if($_SERVER['REQUEST_METHOD'] != 'POST' || $this->checkAllowedManage() !== CHECK_ACTION_RESULT_ALLOWED)
- return ['error' => _t('_sys_txt_access_denied'), 'code' => 403];
- if(!BxDolForm::isCsrfTokenValid((string)bx_get('csrf_token', 'post'), false))
- return ['error' => _t('_sys_txt_access_denied'), 'code' => 403];
- return true;
- }
- /**
- * A goal or funnel step from form input: a page path (starting with / or *, * for any
- * characters) or an event name (name, or name@module), or false.
- */
- protected function _getMatchInput($sType, $sPattern)
- {
- $sPattern = trim((string)$sPattern);
- if($sPattern === '' || get_mb_len($sPattern) > 255)
- return false;
- if($sType == 'page')
- return in_array(get_mb_substr($sPattern, 0, 1), ['/', '*'], true) ? ['type' => 'page', 'pattern' => $sPattern] : false;
- if($sType == 'event')
- return preg_match('/^[\p{L}\p{N} _.:\/-]{1,64}(@[a-z0-9_]{1,32})?$/u', $sPattern) ? ['type' => 'event', 'pattern' => $sPattern] : false;
- return false;
- }
- protected function _outputJson($aResult)
- {
- header('Content-Type: application/json; charset=utf-8');
- header('Cache-Control: no-store');
- if(isset($aResult['code']))
- http_response_code((int)$aResult['code']);
- echo json_encode($aResult);
- exit;
- }
- /**
- * Reports that show members by name: sessions and users. Site administrators always
- * see them; everyone else needs "view analytics people" besides "use analytics".
- */
- public function checkAllowedPeople()
- {
- if(isAdmin())
- return CHECK_ACTION_RESULT_ALLOWED;
- if($this->checkAllowed() !== CHECK_ACTION_RESULT_ALLOWED)
- return _t('_sys_txt_access_denied');
- $aCheck = checkActionModule(bx_get_logged_profile_id(), 'view analytics people', $this->getName(), false);
- return $aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED ? CHECK_ACTION_RESULT_ALLOWED : _t('_sys_txt_access_denied');
- }
- /**
- * Reports that show money and buyers: revenue, subscriptions, sales, credits, and members'
- * lifetime value. Site administrators always see them; everyone else needs "view analytics
- * revenue" besides "use analytics".
- */
- public function checkAllowedRevenue()
- {
- if(isAdmin())
- return CHECK_ACTION_RESULT_ALLOWED;
- if($this->checkAllowed() !== CHECK_ACTION_RESULT_ALLOWED)
- return _t('_sys_txt_access_denied');
- $aCheck = checkActionModule(bx_get_logged_profile_id(), 'view analytics revenue', $this->getName(), false);
- return $aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED ? CHECK_ACTION_RESULT_ALLOWED : _t('_sys_txt_access_denied');
- }
- /**
- * Import past payments from the payment module now: POST op (import, restart), csrf_token.
- * Site administrators only. One step runs within a time limit; the hourly cron continues.
- */
- public function actionBackfill()
- {
- if(!isAdmin() || $_SERVER['REQUEST_METHOD'] != 'POST' || !BxDolForm::isCsrfTokenValid((string)bx_get('csrf_token', 'post'), false))
- $this->_outputJson(['error' => _t('_sys_txt_access_denied'), 'code' => 403]);
- $oRevenue = $this->getRevenue();
- $aStatus = $oRevenue->getBackfillStatus();
- if($aStatus['state'] == 'unavailable')
- $this->_outputJson(['error' => _t('_bx_analytics_txt_revenue_no_payment')]);
- if(bx_get('op', 'post') == 'restart' || $aStatus['state'] == 'done')
- $oRevenue->restartBackfill();
- $aResult = $oRevenue->backfill(time() + $this->_oConfig->CNF['BACKFILL_SECONDS']);
- // cached revenue reports hold the old totals and the import's state
- $this->cleanReportCache();
- $this->_outputJson(['ok' => true] + $aResult);
- }
- public function checkAllowed($isPerformAction = false)
- {
- // site administrators always see analytics, whatever their profile's level
- if(isAdmin())
- return CHECK_ACTION_RESULT_ALLOWED;
- $aCheck = checkActionModule(bx_get_logged_profile_id(), 'use analytics', $this->getName(), $isPerformAction);
- if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
- return _t('_sys_txt_access_denied');
- return CHECK_ACTION_RESULT_ALLOWED;
- }
-
- public function getSelectedModules()
- {
- $aModulesList = $this->serviceGetModules();
- $aModules = array_keys($aModulesList);
- $sModulesDisabled = explode(',', getParam('bx_analytics_modules_disabled'));
- return array(array_diff($aModules, $sModulesDisabled), $aModulesList);
- }
- private function getViewsOrVotes($aSystems, $sSystem, $sModuleName, $iDateFrom, $iDateTo, &$aValues, &$iMaxValueY, &$iMinValueY)
- {
- $aData = array();
- if ($sModuleName != BX_ANALYTICS_SYSTEM){
- $oModule = BxDolModule::getInstance($sModuleName);
- $sSystem = $oModule->_oConfig->CNF['OBJECT_VIEWS'];
- if(isset($aSystems[$sSystem]))
- $aData = $this->_oDb->getTopContentByLikes($sModuleName, $aSystems[$sSystem]['table_track'], $iDateFrom, $iDateTo);
- }
- else{
- $aTmp = $this->getSelectedModules();
- foreach ($aTmp[0] as $sModule) {
- if ($sModule != BX_ANALYTICS_SYSTEM){
- $oModule = BxDolModule::getInstance($sModule);
- if (isset($oModule->_oConfig->CNF['OBJECT_VIEWS'])){
- $sSystem = $oModule->_oConfig->CNF['OBJECT_VIEWS'];
- if(isset($aSystems[$sSystem])){
- $aData1 = $this->_oDb->getTopContentByLikes($sModule, $aSystems[$sSystem]['table_track'], $iDateFrom, $iDateTo);
- $aData = array_merge($aData, $aData1);
- }
- }
- }
- }
- $aData = $this->sortArrayOfArraysAndSlice($aData, 'value', intval(getParam('bx_analytics_items_count')));
- }
- foreach ($aData as $aValue) {
- $oModule = BxDolModule::getInstance($aValue['module']);
- array_push($aValues['labels'], $this->getItemName($oModule->serviceGetTitle($aValue['object_id'])));
- array_push($aValues['values'][0]['data'], $aValue['value']);
- array_push($aValues['links'], $oModule->serviceGetLink($aValue['object_id']));
- if ($aValue['value'] > $iMaxValueY)
- $iMaxValueY = $aValue['value'];
- }
- }
-
- private function getStep($iMin, $iMax)
- {
- $iCount = 8;
- return ceil(($iMax - $iMin) / $iCount);
- }
-
- private function sortArrayOfArraysAndSlice($aArr, $sFld, $Count)
- {
- $aDataSort = array();
- foreach ($aArr as $sKey => $oRow)
- {
- $aDataSort[$sKey] = $oRow[$sFld];
- }
- array_multisort($aDataSort, SORT_DESC, $aArr);
- return array_slice($aArr, 0, $Count);
- }
-
- private function getItemName($sString)
- {
- $sString = strip_tags($sString);
- if ($sString == ''){
- $sString = _t('_bx_analytics_txt_empty_title_item');
- }
- return $sString;
- }
- }
- /** @} */
|