BxBaseStudioDashboard.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  1. <?php defined('BX_DOL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * @defgroup UnaView UNA Studio Representation classes
  7. * @ingroup UnaStudio
  8. * @{
  9. */
  10. class BxBaseStudioDashboard extends BxDolStudioDashboard
  11. {
  12. public function __construct()
  13. {
  14. parent::__construct();
  15. }
  16. public function getPageCss()
  17. {
  18. return array_merge(parent::getPageCss(), array(
  19. 'page_layouts.css',
  20. 'dashboard.css',
  21. ));
  22. }
  23. public function getPageJs()
  24. {
  25. return array_merge(parent::getPageJs(), array(
  26. 'jquery.anim.js',
  27. 'dashboard.js'
  28. ));
  29. }
  30. public function getPageJsClass()
  31. {
  32. return 'BxDolStudioDashboard';
  33. }
  34. public function getPageJsObject()
  35. {
  36. return 'oBxDolStudioDashboard';
  37. }
  38. public function getPageCaption()
  39. {
  40. return parent::getPageCaption() . $this->getPageJsCode();
  41. }
  42. public function getPageJsCode($aOptions = array(), $bWrap = true)
  43. {
  44. $aStatusTitles = [];
  45. foreach(['ok', 'warn', 'fail', 'undef'] as $sStatus)
  46. $aStatusTitles[$sStatus] = _t('_sys_audit_title_' . $sStatus);
  47. $aOptions = array_merge($aOptions, array(
  48. 'sActionUrl' => BX_DOL_URL_STUDIO . 'dashboard.php',
  49. 'aStatusTitles' => $aStatusTitles // the status words the Updates rows get once the check is in
  50. ));
  51. return parent::getPageJsCode($aOptions, $bWrap);
  52. }
  53. public function getPageCode($sPage = '', $bWrap = true)
  54. {
  55. $sResult = parent::getPageCode($sPage, $bWrap);
  56. if($sResult === false)
  57. return false;
  58. $oPage = BxDolPage::getObjectInstance('sys_std_dashboard', BxDolStudioTemplate::getInstance());
  59. return $sResult . $oPage->getCode();
  60. }
  61. /**
  62. * The Version block's asynchronous part: the system update state and the count of apps with updates, as status rows.
  63. */
  64. public function getPageCodeVersionAvailable()
  65. {
  66. // Row statuses are the status-row modifiers (ok / warn / undef); the audit class that names them isn't loaded here.
  67. // Latest Stable and Latest Beta each show the newest version on that channel: green when the site has it, orange when it is newer.
  68. $bUpgradeAvailable = false;
  69. $aRows = [];
  70. foreach(['stable', 'beta'] as $sChannel) {
  71. list($sVersion, $bNewer, $bUpgrade) = $this->getVersionChannelInfo($sChannel);
  72. if($sVersion)
  73. $aRows[$sChannel] = ['status' => $bNewer ? 'warn' : 'ok', 'value' => $sVersion];
  74. else
  75. $aRows[$sChannel] = ['status' => 'undef', 'value' => _t('_adm_dbd_txt_ht_na')];
  76. // the Update button in the block caption follows the channel the site is set to
  77. if($bUpgrade && $sChannel == (getParam('sys_upgrade_channel') == 'beta' ? 'beta' : 'stable'))
  78. $bUpgradeAvailable = true;
  79. }
  80. // The Apps row is named after the number of installed apps; its value is the number waiting for an update.
  81. $sApps = $this->getAppsCountText();
  82. $mixedUpdates = BxDolStudioInstallerUtils::getInstance()->checkUpdates();
  83. if(!is_array($mixedUpdates))
  84. $aApps = ['status' => 'undef', 'name' => $sApps, 'value' => _t('_adm_dbd_txt_ht_na')];
  85. else if(($iUpdates = count($mixedUpdates)) > 0)
  86. $aApps = ['status' => 'warn', 'name' => $sApps, 'value' => '<a href="' . BX_DOL_URL_STUDIO . 'store.php?page=updates">' . _t('_adm_dbd_txt_ver_apps_updates', $iUpdates) . '</a>'];
  87. else
  88. $aApps = ['status' => 'ok', 'name' => $sApps, 'value' => _t('_adm_dbd_txt_ver_up_to_date')];
  89. return [
  90. 'upgrade' => (int)$bUpgradeAvailable,
  91. 'stable' => $aRows['stable'],
  92. 'beta' => $aRows['beta'],
  93. 'apps' => $aApps
  94. ];
  95. }
  96. public function serviceGetWidgetNotices() {
  97. $iResult = 0;
  98. //--- Check Version: a newer release on the channel the site follows
  99. list(, $bNewer) = $this->getVersionChannelInfo(getParam('sys_upgrade_channel') == 'beta' ? 'beta' : 'stable');
  100. if($bNewer)
  101. $iResult += 1;
  102. //--- Check Host Requirements
  103. $oAudit = new BxDolStudioToolsAudit();
  104. foreach($this->aItemsHTools as $sTitle => $sFunc) {
  105. $aResult = $oAudit->checkRequirements(BX_DOL_AUDIT_FAIL, $sFunc);
  106. if(!empty($aResult)) {
  107. $iResult += 1;
  108. break;
  109. }
  110. }
  111. return $iResult;
  112. }
  113. /**
  114. * "23 Apps": everything installed besides the system itself (apps, templates and languages).
  115. */
  116. protected function getAppsCountText()
  117. {
  118. $iApps = 0;
  119. foreach(BxDolModuleQuery::getInstance()->getModules() as $aModule)
  120. if($aModule['type'] != 'system')
  121. $iApps++;
  122. return _t('_adm_dbd_txt_ver_apps_count', $iApps);
  123. }
  124. /**
  125. * Whether the site can receive automatic updates. They come with the UNA account's Pro and Max plans (https://unacms.com/start), which
  126. * the site joins with the marketplace key pair from Settings > System; swap the check for the account API's answer once it reports the plan.
  127. */
  128. public function isAutoUpdateAccess()
  129. {
  130. return getParam('sys_oauth_key') != '' && getParam('sys_oauth_secret') != '';
  131. }
  132. public function serviceGetBlockVersion()
  133. {
  134. $sJsObject = $this->getPageJsObject();
  135. $aSysInfo = BxDolModuleQuery::getInstance()->getModuleByName('system');
  136. // The update switches mirror the Settings > General options.
  137. $aToggles = [
  138. ['name' => 'sys_autoupdate', 'title' => _t('_adm_dbd_txt_ver_auto'), 'icon' => 'refresh-cw', 'on' => getParam('sys_autoupdate') == 'on'],
  139. ['name' => 'sys_upgrade_channel', 'title' => _t('_adm_dbd_txt_ver_beta'), 'icon' => 'flask-conical', 'on' => getParam('sys_upgrade_channel') == 'beta'],
  140. ['name' => 'sys_autoupdate_force_modified_files', 'title' => _t('_adm_dbd_txt_ver_force'), 'icon' => 'zap', 'on' => getParam('sys_autoupdate_force_modified_files') == 'on'],
  141. ];
  142. $aTmplVarsToggles = [];
  143. foreach($aToggles as $aToggle)
  144. $aTmplVarsToggles[] = [
  145. 'js_object' => $sJsObject,
  146. 'name' => $aToggle['name'],
  147. 'title' => $aToggle['title'],
  148. 'icon' => $this->getChartIcon($aToggle['icon'])['svg'],
  149. 'state' => $aToggle['on'] ? 'on' : 'off',
  150. 'checked' => $aToggle['on'] ? 'true' : 'false',
  151. 'checked_attr' => $aToggle['on'] ? 'checked' : ''
  152. ];
  153. // when the running version was applied: the last update, or the installation itself when it was never updated
  154. $iApplied = $aSysInfo['updated'] > 0 ? $aSysInfo['updated'] : $aSysInfo['date'];
  155. // the last status row: whether the site can receive automatic updates (see isAutoUpdateAccess)
  156. $bAccess = $this->isAutoUpdateAccess();
  157. $sContent = BxDolStudioTemplate::getInstance()->parseHtmlByName('dbd_versions.html', array(
  158. 'js_object' => $sJsObject,
  159. 'domain' => getParam('site_title'),
  160. 'version' => bx_get_ver(),
  161. 'version_time' => _t('_adm_dbd_txt_ver_applied', bx_time_js($iApplied, BX_FORMAT_DATE_TIME, true)),
  162. 'bx_repeat:toggles' => $aTmplVarsToggles,
  163. 'installed' => bx_time_js($aSysInfo['date'], BX_FORMAT_DATE, true),
  164. 'updated' => bx_time_js($iApplied, BX_FORMAT_DATE, true),
  165. 'icon_installed' => $this->getChartIcon('package-check')['svg'],
  166. 'icon_updated' => $this->getChartIcon('history')['svg'],
  167. 'icon_stable' => $this->getChartIcon('cloud-download')['svg'],
  168. 'icon_beta' => $this->getChartIcon('flask-conical')['svg'],
  169. 'icon_apps' => $this->getChartIcon('layout-grid')['svg'],
  170. 'apps' => $this->getAppsCountText(),
  171. 'checking' => _t('_adm_dbd_txt_ver_checking'),
  172. 'icon_access' => $this->getChartIcon('badge-check')['svg'],
  173. 'access_class' => $bAccess ? ' bx-dbd-htools-item-ok' : '', // the same bx_if twice in one row would be cut as one span by the template parser
  174. 'bx_if:access_active' => ['condition' => $bAccess, 'content' => []],
  175. 'bx_if:access_inactive' => ['condition' => !$bAccess, 'content' => [
  176. 'access_url' => 'https://unacms.com/start' // a bx_if block only sees its own content
  177. ]]
  178. ));
  179. // The Update button sits in the block caption; dashboard.js shows it once the check finds an upgrade for the site's channel.
  180. return array(
  181. 'content' => $sContent,
  182. 'menu' => '<button type="button" class="bx-btn bx-btn-primary bx-dbd-version-update" onclick="' . $sJsObject . '.performUpgrade()" style="display:none">' . _t('_adm_dbd_btn_update') . '</button>'
  183. );
  184. }
  185. /**
  186. * The system module's storage as Space rows per engine scope: the Storage app's files and images (an image's resized copies count
  187. * towards its size, not its file count), each linking to its tab of the Storage app, then everything else the system keeps (account
  188. * pictures, comment and editor images, wiki files, queues) as "System Media", linking to the Storage app itself. The system module has no icon of its own, and
  189. * resolving one would register a template location named 'system' and break page rendering, so these rows use Lucide glyphs.
  190. * @return array ['local' => rows, 'remote' => rows with objects on a remote engine]
  191. */
  192. protected function getSystemStorageItems()
  193. {
  194. $aGroups = [
  195. 'files' => ['objects' => ['sys_files'], 'counted' => ['sys_files'], 'label' => _t('_adm_dbd_txt_su_files'), 'icon' => 'file', 'url' => BX_DOL_URL_STUDIO . 'storages.php?page=files'],
  196. 'images' => ['objects' => ['sys_images', 'sys_images_resized'], 'counted' => ['sys_images'], 'label' => _t('_adm_dbd_txt_su_images'), 'icon' => 'image', 'url' => BX_DOL_URL_STUDIO . 'storages.php?page=images'],
  197. 'media' => ['objects' => [], 'counted' => [], 'label' => _t('_adm_dbd_txt_su_system_media'), 'icon' => 'hard-drive', 'url' => BX_DOL_URL_STUDIO . 'storages.php'],
  198. ];
  199. $aTotals = [];
  200. foreach($aGroups as $sGroup => $aGroup)
  201. $aTotals[$sGroup] = ['local' => ['size' => 0, 'files' => 0, 'objects' => 0], 'remote' => ['size' => 0, 'files' => 0, 'objects' => 0]];
  202. foreach($this->oDb->getStorageObjects('sys') as $aObject) {
  203. $sGroup = 'media';
  204. foreach($aGroups as $sName => $aGroup)
  205. if(in_array($aObject['object'], $aGroup['objects'])) {
  206. $sGroup = $sName;
  207. break;
  208. }
  209. $sScope = $aObject['engine'] == 'Local' ? 'local' : 'remote';
  210. $aTotals[$sGroup][$sScope]['size'] += (int)$aObject['current_size'];
  211. $aTotals[$sGroup][$sScope]['objects']++;
  212. if(in_array($aObject['object'], $aGroups[$sGroup]['counted']))
  213. $aTotals[$sGroup][$sScope]['files'] += (int)$aObject['current_number'];
  214. }
  215. $aResult = ['local' => [], 'remote' => []];
  216. foreach($aGroups as $sGroup => $aGroup)
  217. foreach(['local', 'remote'] as $sScope) {
  218. $aTotal = $aTotals[$sGroup][$sScope];
  219. if($sScope == 'remote' && $aTotal['objects'] == 0)
  220. continue;
  221. $aResult[$sScope][] = [
  222. 'label' => $aGroup['label'],
  223. 'value' => $aTotal['size'],
  224. 'icon' => $this->getChartIcon($aGroup['icon']),
  225. 'url' => $aGroup['url'],
  226. 'note' => !empty($aGroup['counted']) ? _t($aTotal['files'] == 1 ? '_adm_dbd_txt_su_file_count_1' : '_adm_dbd_txt_su_files_count', $aTotal['files']) : ''
  227. ];
  228. }
  229. return $aResult;
  230. }
  231. /**
  232. * Users: a chart in the hero and the rows under it as its legend. The period tabs in the block caption (dbd_users_tabs.html, Total first
  233. * and selected) pick the time axis: the last 7 days, 30 days or year as a bar per day or month, or Total as the running total over all
  234. * time (a point per month, or per year once that spans more than two years). The rows pick the series: Accounts (added; first and selected),
  235. * Active users (last seen), a row per profile type (its profiles added, from the app's content table) and a row per permission level a
  236. * profile can hold (its current members by when they joined it; the pseudo levels are not rows), each showing its figure for the
  237. * chosen period; the hero shows the charted one's figure and title on the left and, on the right, how many of it are online now (a session
  238. * within the online window: every online account is an active user, a profile type's are those whose account is online). dashboard.js
  239. * (initUsersChart) draws from the data of every series over every axis, which all come with the block.
  240. */
  241. public function serviceGetBlockUsers()
  242. {
  243. $sJsObject = $this->getPageJsObject();
  244. $iNow = time();
  245. // the axes: all time first (the default tab), a point per month since the first account, per year once that spans more than two years
  246. $iFirst = $this->oDb->getAccountsFirstAdded();
  247. $iYears = $iFirst > 0 ? (int)date('Y', $iNow) - (int)date('Y', $iFirst) + 1 : 1;
  248. $iMonths = $iFirst > 0 ? ($iYears - 1) * 12 + (int)date('n', $iNow) - (int)date('n', $iFirst) + 1 : 1;
  249. $aAxes = [
  250. 'all' => $iMonths > 24 ? ['year', $iYears, 'Y', 'Y'] : ['month', max($iMonths, 1), 'M', 'M Y'],
  251. '7d' => ['day', 7, 'D', 'j M Y'],
  252. '30d' => ['day', 30, 'j', 'j M Y'],
  253. '1y' => ['month', 12, 'M', 'M Y'],
  254. ];
  255. $aPeriods = $aTmplVarsPeriods = [];
  256. foreach($aAxes as $sPeriod => $aAxis) {
  257. $aPeriods[$sPeriod] = [
  258. 'title' => _t('_adm_dbd_txt_us_' . $sPeriod),
  259. 'label' => _t('_adm_dbd_txt_us_' . $sPeriod . '_label'),
  260. 'type' => $sPeriod == 'all' ? 'line' : 'bars' // the Total tab charts the running total of the buckets instead of the buckets
  261. ];
  262. $aTmplVarsPeriods[] = [
  263. 'js_object' => $sJsObject,
  264. 'name' => $sPeriod,
  265. 'title' => $aPeriods[$sPeriod]['title'],
  266. 'selected' => $sPeriod == 'all' ? 'true' : 'false',
  267. 'tabindex' => $sPeriod == 'all' ? '0' : '-1'
  268. ];
  269. }
  270. // the series and where each is counted from
  271. $oDb = $this->oDb;
  272. $iOnline = $this->oDb->getAccountsOnlineCount();
  273. $aSeries = [
  274. 'accounts' => ['title' => _t('_adm_dbd_txt_us_accounts'), 'online' => $iOnline],
  275. 'active' => ['title' => _t('_adm_dbd_txt_us_active'), 'online' => $iOnline],
  276. ];
  277. $aSources = [
  278. 'accounts' => function($sFormat, $iSince) use($oDb) { return $oDb->getAccountsAddedBy($sFormat, $iSince); },
  279. 'active' => function($sFormat, $iSince) use($oDb) { return $oDb->getAccountsSeenBy($sFormat, $iSince); },
  280. ];
  281. // one row per enabled profile app (the modules flagged with the profile subtype), at zero too, with its pending profiles as a note;
  282. // its series counts the app's content table by its added field, so an app without one gets a row that charts nothing
  283. $aCounts = [];
  284. foreach($this->oDb->getProfilesByType() as $aRow)
  285. $aCounts[$aRow['type']] = $aRow;
  286. $aTmplVarsProfiles = [];
  287. foreach(BxDolModuleQuery::getInstance()->getModulesBy(['type' => 'modules_subtypes', 'value' => BX_DOL_MODULE_SUBTYPE_PROFILE]) as $aModule) {
  288. if(empty($aModule['enabled']))
  289. continue;
  290. $sSeries = '';
  291. $oModule = BxDolModule::getInstance($aModule['name']);
  292. $aCnf = $oModule && !empty($oModule->_oConfig->CNF) ? $oModule->_oConfig->CNF : [];
  293. if(!empty($aCnf['TABLE_ENTRIES']) && !empty($aCnf['FIELD_ADDED'])) {
  294. $sSeries = 'profiles_' . $aModule['name'];
  295. $sTable = $aCnf['TABLE_ENTRIES'];
  296. $sField = $aCnf['FIELD_ADDED'];
  297. $aSeries[$sSeries] = ['title' => $aModule['title'], 'online' => $this->oDb->getProfilesOnlineCount($aModule['name'])];
  298. $aSources[$sSeries] = function($sFormat, $iSince) use($oDb, $sTable, $sField) { return $oDb->getEntriesAddedBy($sTable, $sField, $sFormat, $iSince); };
  299. }
  300. $aRow = $aCounts[$aModule['name']] ?? ['total' => 0, 'active' => 0];
  301. $iPending = (int)$aRow['total'] - (int)$aRow['active'];
  302. $aTmplVarsProfiles[] = [
  303. 'js_object' => $sJsObject,
  304. 'series' => $sSeries,
  305. 'icon_url' => BxDolStudioUtils::getModuleIcon($aModule),
  306. 'name' => $aModule['title'],
  307. 'bx_if:show_note' => ['condition' => $iPending > 0, 'content' => ['note' => _t('_adm_dbd_txt_us_pending', $iPending)]],
  308. 'value' => number_format((int)$aRow['total'])
  309. ];
  310. }
  311. // one row per permission level a profile can hold (BxDolStudioDashboardQuery::getLevels), with its current members; its series is when
  312. // they joined it, and its icon the level's own (a Lucide name), the shield when that is missing
  313. $aLevelCounts = $this->oDb->getLevelsMembersCount();
  314. $aTmplVarsLevels = [];
  315. foreach($this->oDb->getLevels() as $aLevel) {
  316. $iLevel = (int)$aLevel['ID'];
  317. $sSeries = 'level_' . $iLevel;
  318. $sTitle = _t($aLevel['Name']);
  319. $aSeries[$sSeries] = ['title' => $sTitle, 'online' => $this->oDb->getLevelOnlineCount($iLevel)];
  320. $aSources[$sSeries] = function($sFormat, $iSince) use($oDb, $iLevel) { return $oDb->getLevelMembersSinceBy($iLevel, $sFormat, $iSince); };
  321. $sIcon = !empty($aLevel['Icon']) ? $this->getChartIcon($aLevel['Icon'])['svg'] : '';
  322. $aTmplVarsLevels[] = [
  323. 'js_object' => $sJsObject,
  324. 'series' => $sSeries,
  325. 'icon' => $sIcon ?: $this->getChartIcon('shield')['svg'],
  326. 'name' => $sTitle,
  327. 'value' => number_format((int)($aLevelCounts[$iLevel] ?? 0))
  328. ];
  329. }
  330. // every series over every axis; the Total axis carries the running total
  331. foreach($aSeries as $sSeries => &$aOne) {
  332. $aOne['periods'] = [];
  333. foreach($aAxes as $sPeriod => $aAxis) {
  334. $aItems = $this->getUsersChartBuckets($aAxis[0], $aAxis[1], $aAxis[2], $aAxis[3], $aSources[$sSeries]);
  335. if($sPeriod == 'all') {
  336. $iRunning = 0;
  337. foreach($aItems as &$aItem) {
  338. $iRunning += $aItem['count'];
  339. $aItem['total'] = $iRunning;
  340. }
  341. unset($aItem);
  342. }
  343. $aOne['periods'][$sPeriod] = $aItems;
  344. }
  345. }
  346. unset($aOne);
  347. $sContent = BxDolStudioTemplate::getInstance()->parseHtmlByName('dbd_users.html', [
  348. 'js_object' => $sJsObject,
  349. 'icon_accounts' => $this->getChartIcon('at-sign')['svg'],
  350. 'icon_active' => $this->getChartIcon('users')['svg'],
  351. 'accounts' => number_format($this->oDb->getAccountsCount()),
  352. 'active' => number_format(array_sum(array_column($aSeries['active']['periods']['all'], 'count'))), // dashboard.js keeps every row's figure on the chosen period
  353. 'bx_if:show_profiles' => ['condition' => !empty($aTmplVarsProfiles), 'content' => ['bx_repeat:profiles' => $aTmplVarsProfiles]],
  354. 'bx_if:show_levels' => ['condition' => !empty($aTmplVarsLevels), 'content' => ['bx_repeat:levels' => $aTmplVarsLevels]],
  355. 'chart_data' => json_encode([
  356. 'period' => 'all',
  357. 'series_default' => 'accounts',
  358. 'periods' => $aPeriods,
  359. 'series' => $aSeries,
  360. 'txt_peak' => _t('_adm_dbd_txt_us_peak', '{0}') // the chart's accessible name names the peak bucket's count
  361. ])
  362. ]);
  363. // the period switcher sits in the block caption, like the Server block's tabs
  364. return [
  365. 'content' => $sContent,
  366. 'menu' => BxDolStudioTemplate::getInstance()->parseHtmlByName('dbd_users_tabs.html', ['js_object' => $sJsObject, 'bx_repeat:periods' => $aTmplVarsPeriods])
  367. ];
  368. }
  369. /**
  370. * A series' buckets over an axis: the last $iCount days, months or years up to now, each with the short label under its bar, its full
  371. * name for the tooltip and what $fCounts (a MySQL date format and the axis start, giving bucket => count) counted in it.
  372. */
  373. protected function getUsersChartBuckets($sUnit, $iCount, $sLabel, $sTitle, $fCounts)
  374. {
  375. $aFormats = ['day' => ['%Y-%m-%d', 'Y-m-d'], 'month' => ['%Y-%m', 'Y-m'], 'year' => ['%Y', 'Y']];
  376. list($sSqlFormat, $sPhpFormat) = $aFormats[$sUnit];
  377. $iNow = time();
  378. $aBuckets = [];
  379. for($i = $iCount - 1; $i >= 0; $i--) {
  380. switch($sUnit) {
  381. case 'month':
  382. $iTime = strtotime(date('Y-m-01', $iNow) . ' -' . $i . ' months');
  383. break;
  384. case 'year':
  385. $iTime = mktime(0, 0, 0, 1, 1, (int)date('Y', $iNow) - $i);
  386. break;
  387. default:
  388. $iTime = strtotime(date('Y-m-d', $iNow) . ' -' . $i . ' days');
  389. }
  390. $aBuckets[date($sPhpFormat, $iTime)] = ['label' => date($sLabel, $iTime), 'title' => date($sTitle, $iTime), 'count' => 0];
  391. }
  392. $sFirst = array_key_first($aBuckets);
  393. $iSince = $sUnit == 'day' ? strtotime($sFirst . ' 00:00:00') : ($sUnit == 'month' ? strtotime($sFirst . '-01 00:00:00') : mktime(0, 0, 0, 1, 1, (int)$sFirst));
  394. foreach($fCounts($sSqlFormat, $iSince) as $sBucket => $iAdded)
  395. if(isset($aBuckets[$sBucket]))
  396. $aBuckets[$sBucket]['count'] = (int)$iAdded;
  397. return array_values($aBuckets);
  398. }
  399. public function serviceGetBlockSpace($bDynamic = false)
  400. {
  401. $bInclideScriptSpace = false; //Use dynamic loading by default if this setting is enabled.
  402. $sJsObject = $this->getPageJsObject();
  403. $aChartData = array();
  404. if(!$bDynamic) {
  405. // Two charts and two lists: what lives on this server (the database, the Storage app's files and images, the rest of the system's
  406. // media, then every installed module's local storage, at zero too) and what lives on remote storage (whichever of those have
  407. // objects on a remote engine); when no remote storage is in use the second chart and list are a "not connected" placeholder.
  408. // Rows link to the page that manages them; the Storage app's rows also count their files.
  409. $aLocal = array(
  410. array('label' => _t('_adm_dbd_txt_su_database'), 'value' => $this->getDbSize(), 'icon' => $this->getChartIcon('database')),
  411. );
  412. $aRemote = array();
  413. if($bInclideScriptSpace) {
  414. $iSizeDiskTotal = $this->getFolderSize(BX_DIRECTORY_PATH_ROOT);
  415. $iSizeDiskMedia = $this->getFolderSize(BX_DIRECTORY_STORAGE);
  416. $aLocal[] = array('label' => _t('_adm_dbd_txt_su_system'), 'value' => $iSizeDiskTotal - $iSizeDiskMedia, 'icon' => $this->getChartIcon('hard-drive'));
  417. }
  418. $aSystem = $this->getSystemStorageItems();
  419. $aLocal = array_merge($aLocal, $aSystem['local']);
  420. $aRemote = array_merge($aRemote, $aSystem['remote']);
  421. $aModules = BxDolModuleQuery::getInstance()->getModulesBy(array('type' => 'all'));
  422. foreach($aModules as $aModule) {
  423. if($aModule['name'] == 'system')
  424. continue;
  425. $aIcon = $this->getChartIcon(BxDolStudioUtils::getModuleIcon($aModule), true);
  426. $sUrl = BX_DOL_URL_STUDIO . 'module.php?name=' . $aModule['name'];
  427. $aSizes = $this->oDb->getModuleStorageSizes($aModule['name']);
  428. $aLocal[] = array('label' => $aModule['title'], 'value' => $aSizes['local']['size'], 'icon' => $aIcon, 'url' => $sUrl);
  429. if($aSizes['remote']['objects'] > 0)
  430. $aRemote[] = array('label' => $aModule['title'], 'value' => $aSizes['remote']['size'], 'icon' => $aIcon, 'url' => $sUrl);
  431. }
  432. $fDiskTotal = @disk_total_space(BX_DIRECTORY_PATH_ROOT);
  433. $aChartLocal = $this->getChartData($this->getChartItems($aLocal), array_sum(array_column($aLocal, 'value')), $fDiskTotal !== false ? (float)$fDiskTotal : 0);
  434. $aChartLocal['label'] = bx_process_output(_t('_adm_dbd_txt_su_local'));
  435. $sRemote = bx_process_output(_t('_adm_dbd_txt_su_remote'));
  436. if($this->oDb->isRemoteStorageUsed()) {
  437. $aChartRemote = $this->getChartData($this->getChartItems($aRemote), array_sum(array_column($aRemote, 'value')));
  438. $aChartRemote['label'] = $sRemote;
  439. $aRowsRemote = $aChartRemote['items'];
  440. }
  441. else {
  442. $sOff = bx_process_output(_t('_adm_dbd_txt_su_remote_off'));
  443. $aChartRemote = array('label' => $sRemote, 'items' => array(), 'used' => $sOff, 'total' => '', 'used_bytes' => 0, 'total_bytes' => 0);
  444. $aRowsRemote = array(array('name' => $sRemote, 'bytes' => 0, 'size' => $sOff, 'icon' => $this->getChartIcon('cloud-off'), 'placeholder' => true));
  445. }
  446. $aChartData = array(
  447. 'charts' => array($aChartLocal, $aChartRemote),
  448. 'groups' => array(array('items' => $aChartLocal['items']), array('items' => $aRowsRemote)),
  449. );
  450. }
  451. $sContent = BxDolStudioTemplate::getInstance()->parseHtmlByName('dbd_space.html', array(
  452. 'bx_if:show_content' => array(
  453. 'condition' => !$bDynamic,
  454. 'content' => array(
  455. 'js_object' => $sJsObject,
  456. 'chart_data' => $this->getChartJson($aChartData)
  457. )
  458. ),
  459. 'bx_if:show_loader' => array(
  460. 'condition' => $bDynamic,
  461. 'content' => array(
  462. 'js_object' => $sJsObject,
  463. )
  464. )
  465. ));
  466. return array('content' => $sContent);
  467. }
  468. public function serviceGetBlockHostTools($bDynamic = false)
  469. {
  470. $sJsObject = $this->getPageJsObject();
  471. $oTemplate = BxDolStudioTemplate::getInstance();
  472. // the links in the audit report (?action=audit_send_test_email / phpinfo) come back to this page: answered before anything is rendered
  473. (new BxDolStudioToolsAudit())->processRequest();
  474. $aTmplVarsGauges = $aTmplVarsItems = [];
  475. if($bDynamic) {
  476. // Overview gauges: a donut per metric, its arc a share of the ring's circumference (2 * pi * 40); the ring shows the
  477. // metric's figure with its unit, when it has one, small underneath.
  478. $fRing = 251.327;
  479. foreach($this->getServerMetrics() as $sMetric => $aMetric) {
  480. $sNa = _t('_adm_dbd_txt_ht_na');
  481. $aTmplVarsGauges[] = [
  482. 'name' => $sMetric,
  483. 'class_add' => $aMetric !== null && $aMetric['percent'] >= 90 ? ' bx-dbd-gauge-high' : '',
  484. 'title' => _t('_adm_dbd_txt_ht_' . $sMetric),
  485. 'value' => $aMetric !== null ? $aMetric['value'] : $sNa,
  486. 'label' => $aMetric !== null ? trim($aMetric['value'] . ' ' . $aMetric['unit']) . ($aMetric['percent'] >= 90 ? ', ' . _t('_adm_dbd_txt_ht_high') : '') : $sNa, // the red arc alone would not tell a screen reader
  487. 'bx_if:show_unit' => [
  488. 'condition' => $aMetric !== null && $aMetric['unit'] !== '',
  489. 'content' => ['unit' => $aMetric !== null ? $aMetric['unit'] : '']
  490. ],
  491. 'dash' => $aMetric !== null ? round($aMetric['percent'] / 100 * $fRing, 2) : 0,
  492. 'ring' => $fRing
  493. ];
  494. }
  495. // Status rows: a row per section of the last audit with its headline (a version) or its outcome, and the issue count when it has issues.
  496. foreach($this->getAuditReport()['sections'] as $aSection) {
  497. $aSummary = $this->getAuditSectionSummary($aSection);
  498. $aTmplVarsItems[] = [
  499. 'status' => $aSummary['status'],
  500. 'status_title' => _t('_sys_audit_title_' . $aSummary['status']), // read out in place of the dot's colour
  501. 'icon' => $this->getChartIcon($aSection['icon'])['svg'],
  502. 'title' => $aSection['title'],
  503. 'value' => $aSummary['value'],
  504. 'bx_if:show_desc' => [
  505. 'condition' => $aSummary['desc'] !== '',
  506. 'content' => ['desc' => $aSummary['desc']]
  507. ]
  508. ];
  509. }
  510. }
  511. $sContent = $oTemplate->parseHtmlByName('dbd_htools.html', [
  512. 'js_object' => $sJsObject,
  513. 'bx_if:show_content' => [
  514. 'condition' => $bDynamic,
  515. 'content' => [
  516. 'bx_repeat:gauges' => $aTmplVarsGauges,
  517. 'bx_repeat:items' => $aTmplVarsItems,
  518. ]
  519. ],
  520. 'bx_if:show_loader' => [
  521. 'condition' => !$bDynamic,
  522. 'content' => [
  523. 'js_object' => $sJsObject,
  524. ]
  525. ]
  526. ]);
  527. if($bDynamic)
  528. return ['content' => $sContent];
  529. // The tab switcher sits in the block caption; the panels it drives arrive with the Overview.
  530. return [
  531. 'content' => $sContent,
  532. 'menu' => $oTemplate->parseHtmlByName('dbd_htools_tabs.html', ['js_object' => $sJsObject])
  533. ];
  534. }
  535. /**
  536. * Server block, Audit tab: a hero with when the last audit ran, a bar of its outcomes and the button to run a new one,
  537. * then the sections as an accordion of status rows, and the note on what needs a person.
  538. */
  539. public function getBlockHostToolsAudit()
  540. {
  541. $oAudit = new BxDolStudioToolsAudit();
  542. $aReport = $this->getAuditReport();
  543. $sJsObject = $this->getPageJsObject();
  544. $sChevron = $this->getChartIcon('chevron-down')['svg'];
  545. $aTotals = ['ok' => 0, 'warn' => 0, 'fail' => 0, 'undef' => 0];
  546. $aTmplVarsSections = [];
  547. foreach($aReport['sections'] as $aSection) {
  548. $aSummary = $this->getAuditSectionSummary($aSection);
  549. foreach($aSummary['counts'] as $sType => $iCount)
  550. $aTotals[$sType] += $iCount;
  551. $aTmplVarsRows = [];
  552. foreach($aSection['rows'] as $aRow)
  553. $aTmplVarsRows[] = $this->getHostToolsRowVars($aRow, $oAudit);
  554. $aTmplVarsSections[] = [
  555. 'js_object' => $sJsObject,
  556. 'name' => $aSection['name'],
  557. 'status' => $aSummary['status'],
  558. 'icon' => $this->getChartIcon($aSection['icon'])['svg'],
  559. 'icon_chevron' => $sChevron,
  560. 'title' => $aSection['title'],
  561. 'value' => $aSummary['value'],
  562. 'bx_repeat:rows' => $aTmplVarsRows
  563. ];
  564. }
  565. // The bar: a segment per outcome, passed first; the same counts are written out under the button.
  566. $iTotal = max(1, array_sum($aTotals));
  567. $aTmplVarsSegments = $aSummary = [];
  568. foreach(['ok' => '_adm_dbd_txt_ht_passed', 'warn' => '_adm_dbd_txt_ht_warnings', 'fail' => '_adm_dbd_txt_ht_failed', 'undef' => '_adm_dbd_txt_ht_unknown'] as $sType => $sKey) {
  569. if($aTotals[$sType] == 0)
  570. continue;
  571. $sLabel = _t($sType == 'warn' && $aTotals[$sType] == 1 ? '_adm_dbd_txt_ht_warning' : $sKey, $aTotals[$sType]);
  572. $aTmplVarsSegments[] = ['type' => $sType, 'share' => round($aTotals[$sType] / $iTotal * 100, 2), 'label' => bx_html_attribute($sLabel)];
  573. $aSummary[] = $sLabel;
  574. }
  575. ob_start();
  576. $oAudit->generateJs();
  577. $sJs = ob_get_clean();
  578. return $sJs . BxDolStudioTemplate::getInstance()->parseHtmlByName('dbd_htools_audit.html', [
  579. 'js_object' => $sJsObject,
  580. 'time' => bx_time_js($aReport['time'], BX_FORMAT_DATE_TIME),
  581. 'bar_label' => bx_html_attribute(implode(', ', $aSummary)),
  582. 'bx_repeat:segments' => $aTmplVarsSegments,
  583. 'summary' => implode(' &middot; ', $aSummary),
  584. 'bx_repeat:sections' => $aTmplVarsSections
  585. ]);
  586. }
  587. /**
  588. * A section's outcome for the Status tab and the audit accordion: its worst row status (unknown rows only count when
  589. * there is nothing else), its headline (a version) or its outcome as the value, and, under a headline that stands for
  590. * several rows, the issue count as the note.
  591. * @return array status ('ok', 'warn', 'fail', 'undef'), value, desc, counts (rows per status)
  592. */
  593. protected function getAuditSectionSummary($aSection)
  594. {
  595. $aCounts = ['ok' => 0, 'warn' => 0, 'fail' => 0, 'undef' => 0];
  596. foreach($aSection['rows'] as $aRow)
  597. if(isset($aCounts[$aRow['type']]))
  598. $aCounts[$aRow['type']]++;
  599. $sStatus = 'undef';
  600. foreach(['fail', 'warn', 'ok'] as $sType)
  601. if($aCounts[$sType] > 0) {
  602. $sStatus = $sType;
  603. break;
  604. }
  605. $aIssues = [];
  606. if($aCounts['fail'] > 0)
  607. $aIssues[] = _t('_adm_dbd_txt_ht_failed', $aCounts['fail']);
  608. if($aCounts['warn'] > 0)
  609. $aIssues[] = _t($aCounts['warn'] == 1 ? '_adm_dbd_txt_ht_warning' : '_adm_dbd_txt_ht_warnings', $aCounts['warn']);
  610. $sIssues = implode(', ', $aIssues);
  611. $bHeadline = $aSection['value'] !== '';
  612. return [
  613. 'status' => $sStatus,
  614. 'value' => $bHeadline ? $aSection['value'] : ($sIssues !== '' ? $sIssues : _t($sStatus == 'ok' ? '_sys_audit_title_ok' : '_sys_audit_title_undef')),
  615. 'desc' => $bHeadline && count($aSection['rows']) > 1 ? $sIssues : '',
  616. 'counts' => $aCounts
  617. ];
  618. }
  619. /**
  620. * One audit row as status-row template variables; a row without a value shows its status word instead.
  621. */
  622. protected function getHostToolsRowVars($aRow, $oAudit)
  623. {
  624. return [
  625. 'status' => $aRow['type'],
  626. 'title' => $aRow['name'],
  627. 'value' => $aRow['value'] !== '' ? $aRow['value'] : $oAudit->typeToTitle($aRow['type']),
  628. 'bx_if:show_desc' => [
  629. 'condition' => $aRow['msg'] !== '',
  630. 'content' => ['desc' => $aRow['msg']]
  631. ]
  632. ];
  633. }
  634. public function serviceGetBlockCache()
  635. {
  636. $sJsObject = $this->getPageJsObject();
  637. $sChartData = $this->getCacheChartData();
  638. $bChartData = $sChartData !== false;
  639. $sContent = BxDolStudioTemplate::getInstance()->parseHtmlByName('dbd_cache.html', [
  640. 'bx_if:show_chart' => [
  641. 'condition' => $bChartData,
  642. 'content' => [
  643. 'js_object' => $sJsObject,
  644. 'chart_data' => $sChartData,
  645. ]
  646. ],
  647. 'bx_if:show_empty' => [
  648. 'condition' => !$bChartData,
  649. 'content' => [
  650. 'message' => MsgBox(_t('_adm_dbd_msg_c_all_disabled'))
  651. ]
  652. ],
  653. ]);
  654. return ['content' => $sContent];
  655. }
  656. public function serviceGetBlockQueues()
  657. {
  658. $sChartData = $this->getQueuesChartData();
  659. if($sChartData === false)
  660. return ['content' => ''];
  661. // "Clear all queues" is the hero's button (chart data), each row that can be emptied carries its own clear button.
  662. return ['content' => BxDolStudioTemplate::getInstance()->parseHtmlByName('dbd_queues.html', [
  663. 'js_object' => $this->getPageJsObject(),
  664. 'chart_data' => $sChartData,
  665. ])];
  666. }
  667. /**
  668. * @return array [latest version on the channel or '' when the check failed, whether it is newer than the installed one, whether an upgrade patch can be run]
  669. */
  670. private function getVersionChannelInfo($sChannel)
  671. {
  672. $oUpgrader = bx_instance('BxDolUpgrader');
  673. $aUpdateInfo = $oUpgrader->getVersionUpdateInfo($sChannel);
  674. if(!is_array($aUpdateInfo) || empty($aUpdateInfo['latest_version']))
  675. return ['', false, false];
  676. return [$aUpdateInfo['latest_version'], $oUpgrader->isNewVersionAvailable($aUpdateInfo), $oUpgrader->isUpgradeAvailable($aUpdateInfo)];
  677. }
  678. }
  679. /** @} */