Matomo Analytics – Ethical Stats. Powerful Insights. - Version 4.4.2

Version Description

Download this release

Release Info

Developer matomoteam
Plugin Icon 128x128 Matomo Analytics – Ethical Stats. Powerful Insights.
Version 4.4.2
Comparing to
See all releases

Code changes from version 4.4.1 to 4.4.2

Files changed (97) hide show
  1. LEGALNOTICE +6 -1
  2. assets/chart.js +52 -0
  3. assets/js/blocks/matomo_opt_out.js +19 -0
  4. classes/WpMatomo.php +48 -47
  5. classes/WpMatomo/API.php +34 -28
  6. classes/WpMatomo/Access.php +4 -5
  7. classes/WpMatomo/Admin/AccessSettings.php +12 -12
  8. classes/WpMatomo/Admin/Admin.php +3 -4
  9. classes/WpMatomo/Admin/AdminSettings.php +34 -36
  10. classes/WpMatomo/Admin/AdvancedSettings.php +38 -39
  11. classes/WpMatomo/Admin/Chart.php +20 -0
  12. classes/WpMatomo/Admin/CookieConsent.php +10 -10
  13. classes/WpMatomo/Admin/Dashboard.php +73 -61
  14. classes/WpMatomo/Admin/ExclusionSettings.php +28 -22
  15. classes/WpMatomo/Admin/GeolocationSettings.php +17 -17
  16. classes/WpMatomo/Admin/GetStarted.php +12 -14
  17. classes/WpMatomo/Admin/Info.php +23 -21
  18. classes/WpMatomo/Admin/Marketplace.php +0 -1
  19. classes/WpMatomo/Admin/Menu.php +63 -63
  20. classes/WpMatomo/Admin/PrivacySettings.php +8 -8
  21. classes/WpMatomo/Admin/SafeModeMenu.php +6 -6
  22. classes/WpMatomo/Admin/Summary.php +57 -24
  23. classes/WpMatomo/Admin/SystemReport.php +602 -525
  24. classes/WpMatomo/Admin/TrackingSettings.php +84 -62
  25. classes/WpMatomo/Admin/TrackingSettings/Forms.php +16 -12
  26. classes/WpMatomo/Admin/views/access.php +33 -25
  27. classes/WpMatomo/Admin/views/advanced_settings.php +52 -39
  28. classes/WpMatomo/Admin/views/exclusion_settings.php +119 -107
  29. classes/WpMatomo/Admin/views/geolocation_settings.php +34 -32
  30. classes/WpMatomo/Admin/views/get_started.php +20 -11
  31. classes/WpMatomo/Admin/views/info.php +25 -22
  32. classes/WpMatomo/Admin/views/info_bug_report.php +14 -14
  33. classes/WpMatomo/Admin/views/info_help.php +10 -6
  34. classes/WpMatomo/Admin/views/info_high_traffic.php +4 -4
  35. classes/WpMatomo/Admin/views/info_multisite.php +22 -19
  36. classes/WpMatomo/Admin/views/info_newsletter.php +15 -15
  37. classes/WpMatomo/Admin/views/info_shared.php +2 -2
  38. classes/WpMatomo/Admin/views/marketplace.php +207 -176
  39. classes/WpMatomo/Admin/views/privacy_gdpr.php +50 -42
  40. classes/WpMatomo/Admin/views/settings.php +20 -17
  41. classes/WpMatomo/Admin/views/settings_errors.php +7 -3
  42. classes/WpMatomo/Admin/views/summary.php +87 -54
  43. classes/WpMatomo/Admin/views/systemreport.php +89 -68
  44. classes/WpMatomo/Admin/views/tracking.php +53 -51
  45. classes/WpMatomo/Annotations.php +8 -6
  46. classes/WpMatomo/Bootstrap.php +10 -4
  47. classes/WpMatomo/Capabilities.php +9 -9
  48. classes/WpMatomo/Commands/MatomoCommands.php +12 -9
  49. classes/WpMatomo/Compatibility.php +8 -9
  50. classes/WpMatomo/Db/Settings.php +30 -15
  51. classes/WpMatomo/Db/WordPress.php +6 -6
  52. classes/WpMatomo/Db/WordPressDbStatement.php +14 -3
  53. classes/WpMatomo/Db/WordPressTracker.php +4 -4
  54. classes/WpMatomo/Ecommerce/Base.php +23 -13
  55. classes/WpMatomo/Ecommerce/EasyDigitalDownloads.php +23 -23
  56. classes/WpMatomo/Ecommerce/MatomoTestEcommerce.php +37 -0
  57. classes/WpMatomo/Ecommerce/MemberPress.php +24 -23
  58. classes/WpMatomo/Ecommerce/Woocommerce.php +57 -46
  59. classes/WpMatomo/Email.php +97 -94
  60. classes/WpMatomo/Installer.php +71 -62
  61. classes/WpMatomo/Logger.php +15 -14
  62. classes/WpMatomo/OptOut.php +45 -25
  63. classes/WpMatomo/Paths.php +12 -4
  64. classes/WpMatomo/PrivacyBadge.php +3 -5
  65. classes/WpMatomo/RedirectOnActivation.php +8 -6
  66. classes/WpMatomo/Referral.php +10 -9
  67. classes/WpMatomo/Report/Data.php +3 -4
  68. classes/WpMatomo/Report/Dates.php +3 -5
  69. classes/WpMatomo/Report/Metadata.php +34 -31
  70. classes/WpMatomo/Report/Renderer.php +30 -29
  71. classes/WpMatomo/Report/views/table.php +1 -1
  72. classes/WpMatomo/Report/views/table_map_no_dimension.php +11 -5
  73. classes/WpMatomo/Report/views/table_no_dimension.php +1 -1
  74. classes/WpMatomo/Roles.php +13 -13
  75. classes/WpMatomo/ScheduledTasks.php +89 -75
  76. classes/WpMatomo/Settings.php +39 -39
  77. classes/WpMatomo/Site.php +0 -1
  78. classes/WpMatomo/Site/Sync.php +89 -84
  79. classes/WpMatomo/Site/Sync/SyncConfig.php +106 -109
  80. classes/WpMatomo/TrackingCode.php +22 -16
  81. classes/WpMatomo/TrackingCode/TrackingCodeGenerator.php +33 -32
  82. classes/WpMatomo/Uninstaller.php +11 -2
  83. classes/WpMatomo/Updater.php +42 -35
  84. classes/WpMatomo/Updater/UpdateInProgressException.php +7 -3
  85. classes/WpMatomo/User.php +0 -3
  86. classes/WpMatomo/User/Sync.php +90 -87
  87. classes/WpMatomo/views/referral.php +8 -7
  88. config/config.php +1 -0
  89. matomo.php +56 -52
  90. node_modules/chart.js/LICENSE.md +9 -0
  91. node_modules/chart.js/README.md +36 -0
  92. node_modules/chart.js/dist/chart.min.js +13 -0
  93. package-lock.json +11 -0
  94. plugins/WordPress/Menu.php +0 -1
  95. plugins/WordPress/WordPress.php +7 -2
  96. plugins/WordPress/stylesheets/user.css +3 -0
  97. readme.txt +1 -1
LEGALNOTICE CHANGED
@@ -42,4 +42,9 @@ See a list of all components/libraries and its licenses in Matomo in `app/LEGALN
42
 
43
  THIRD-PARTY CONTENT
44
 
45
- See the list in `app/LEGALNOTICE`
 
 
 
 
 
42
 
43
  THIRD-PARTY CONTENT
44
 
45
+ Name: chart.js
46
+ Link: https://www.chartjs.org/
47
+ License: The MIT License (MIT) see node_modules/chart.js/LICENSE.md
48
+
49
+ For more third party content see the list in `app/LEGALNOTICE`.
50
+
assets/chart.js ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ jQuery(document).ready(function(){
2
+ jQuery('.matomo-table[data-chart]').each(function() {
3
+ let $this = jQuery(this);
4
+ let $postbox = $this.parents('div.postbox');
5
+ let $table = $postbox.find('table');
6
+ $table.hide();
7
+ let $canvas = jQuery('<canvas/>',{'id':$this.attr('data-chart')});
8
+ $canvas.insertAfter($table);
9
+ let data = [];
10
+ let labels = [];
11
+ let title = $postbox.find('h2').text();
12
+ let $row;
13
+ let value;
14
+ $table.find('tr').each(function() {
15
+ $row = jQuery(this);
16
+ value = $row.find('td:nth-child(2)').text();
17
+ if ( '-' === value ) {
18
+ value = 0;
19
+ }
20
+ data.push(value);
21
+ labels.push($row.find('td:nth-child(1)').text());
22
+ });
23
+
24
+ var myChart = new Chart($canvas, {
25
+ type: 'line',
26
+ data: {
27
+ labels: labels.reverse(),
28
+ datasets: [{
29
+ label: title,
30
+ data: data.reverse(),
31
+ borderColor: "#55bae7",
32
+ pointBackgroundColor: "#55bae7",
33
+ pointBorderColor: "#55bae7",
34
+ pointHoverBackgroundColor: "#55bae7",
35
+ pointHoverBorderColor: "#55bae7",
36
+ }]
37
+ },
38
+ options: {
39
+ plugins: {
40
+ legend: {
41
+ display: false
42
+ }
43
+ },
44
+ scales: {
45
+ y: {
46
+ beginAtZero: true
47
+ }
48
+ }
49
+ }
50
+ });
51
+ });
52
+ });
assets/js/blocks/matomo_opt_out.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function (blocks, i18n, element) {
2
+ var el = element.createElement;
3
+ var __ = i18n.__;
4
+
5
+ const matomo_icon = el('img', {src: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgd2lkdGg9IjI0LjM0NTg3NW1tIgogICBoZWlnaHQ9IjEzLjg0NzIwOG1tIgogICB2aWV3Qm94PSIwIDAgMjQuMzQ1ODc1IDEzLjg0NzIwOCIKICAgdmVyc2lvbj0iMS4xIgogICBpZD0ic3ZnMzk3MiIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC45Mi41ICgyMDYwZWMxZjlmLCAyMDIwLTA0LTA4KSIKICAgc29kaXBvZGk6ZG9jbmFtZT0ibG9nby5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnMzOTY2Ij4KICAgIDxjbGlwUGF0aAogICAgICAgaWQ9IlNWR0lEXzJfIj4KICAgICAgPHVzZQogICAgICAgICBpZD0idXNlMzgzMyIKICAgICAgICAgc3R5bGU9Im92ZXJmbG93OnZpc2libGUiCiAgICAgICAgIHhsaW5rOmhyZWY9IiNTVkdJRF8xXyIKICAgICAgICAgeD0iMCIKICAgICAgICAgeT0iMCIKICAgICAgICAgd2lkdGg9IjEwMCUiCiAgICAgICAgIGhlaWdodD0iMTAwJSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8cGF0aAogICAgICAgZD0ibSAxNDEuNzEsMTE1LjUyIDAuMDEsLTAuMDEgLTAuMjQsLTAuMzYgYyAtMC4wNCwtMC4wNiAtMC4wNywtMC4xMSAtMC4xMSwtMC4xNyBsIC0xNi4yNCwtMjQuNzQgLTAuMDEsMC4wMSBjIC0yLjMyLC0zLjc2IC02LjQ1LC02LjI3IC0xMS4xOSwtNi4yNyAtNy4yNiwwIC0xMy4xNSw1Ljg5IC0xMy4xNSwxMy4xNSAwLDMuMzcgMS4yOCw2LjQzIDMuMzYsOC43NSBsIC0wLjAxLDAuMDEgMTUuMzYsMjMuNjQgYyAwLjA3LDAuMSAwLjEzLDAuMiAwLjIsMC4zIGwgMC4wOCwwLjEzIDAuMDEsLTAuMDEgYyAyLjM4LDMuMzggNi4zLDUuNTkgMTAuNzUsNS41OSA3LjI2LDAgMTMuMTUsLTUuODkgMTMuMTUsLTEzLjE1IC0wLjAyLC0yLjUxIC0wLjc0LC00Ljg2IC0xLjk3LC02Ljg3IHoiCiAgICAgICBpZD0iU1ZHSURfMV8iCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogIDwvZGVmcz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9ImJhc2UiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6em9vbT0iOS4yNjcxNTc5IgogICAgIGlua3NjYXBlOmN4PSI0Ni4wMDc5NDYiCiAgICAgaW5rc2NhcGU6Y3k9IjI2LjE2Nzk0NSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0ibW0iCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEyMDgiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNjU2IgogICAgIGlua3NjYXBlOndpbmRvdy14PSI3MiIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMjciCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMC4xIgogICAgIGZpdC1tYXJnaW4tbGVmdD0iMC4xIgogICAgIGZpdC1tYXJnaW4tcmlnaHQ9IjAuMSIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMC4xIiAvPgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTM5NjkiPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgICAgPGRjOnRpdGxlPjwvZGM6dGl0bGU+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxnCiAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgaW5rc2NhcGU6Z3JvdXBtb2RlPSJsYXllciIKICAgICBpZD0ibGF5ZXIxIgogICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjguNDM0MiwtMTcwLjYzNTkyKSI+CiAgICA8ZwogICAgICAgaWQ9ImczODg4IgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC4yNjQ1ODMzMywwLDAsMC4yNjQ1ODMzMywxMTQuNjY3MzksMTQ4LjUxNjIxKSI+CiAgICAgIDxnCiAgICAgICAgIGlkPSJnMzg2MCI+CiAgICAgICAgPHBhdGgKICAgICAgICAgICBpZD0icGF0aDM4MjYiCiAgICAgICAgICAgZD0ibSAxNDEuNzEsMTE1LjUyIDAuMDEsLTAuMDEgLTAuMjQsLTAuMzYgYyAtMC4wNCwtMC4wNiAtMC4wNywtMC4xMSAtMC4xMSwtMC4xNyBsIC0xNi4yNCwtMjQuNzQgLTIxLjAxLDE1LjY1IDE1LjM2LDIzLjY0IGMgMC4wNywwLjEgMC4xMywwLjIgMC4yLDAuMyBsIDAuMDgsMC4xMyAwLjAxLC0wLjAxIGMgMi4zOCwzLjM4IDYuMyw1LjU5IDEwLjc1LDUuNTkgNy4yNiwwIDEzLjE1LC01Ljg5IDEzLjE1LC0xMy4xNSAtMC4wMSwtMi41MSAtMC43MywtNC44NiAtMS45NiwtNi44NyB6IgogICAgICAgICAgIGNsYXNzPSJzdDEiCiAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICBzdHlsZT0iZmlsbDojOTVjNzQ4IiAvPgogICAgICAgIDxjaXJjbGUKICAgICAgICAgICBpZD0iY2lyY2xlMzgyOCIKICAgICAgICAgICByPSIxMy4xNSIKICAgICAgICAgICBjeT0iMTIyLjQiCiAgICAgICAgICAgY3g9IjY1LjU1OTk5OCIKICAgICAgICAgICBjbGFzcz0ic3QyIgogICAgICAgICAgIHN0eWxlPSJmaWxsOiMzNWJmYzAiIC8+CiAgICAgICAgPGcKICAgICAgICAgICBpZD0iZzM4NDgiPgogICAgICAgICAgPGRlZnMKICAgICAgICAgICAgIGlkPSJkZWZzMzgzMSI+CiAgICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICAgIGQ9Im0gMTQxLjcxLDExNS41MiAwLjAxLC0wLjAxIC0wLjI0LC0wLjM2IGMgLTAuMDQsLTAuMDYgLTAuMDcsLTAuMTEgLTAuMTEsLTAuMTcgbCAtMTYuMjQsLTI0Ljc0IC0wLjAxLDAuMDEgYyAtMi4zMiwtMy43NiAtNi40NSwtNi4yNyAtMTEuMTksLTYuMjcgLTcuMjYsMCAtMTMuMTUsNS44OSAtMTMuMTUsMTMuMTUgMCwzLjM3IDEuMjgsNi40MyAzLjM2LDguNzUgbCAtMC4wMSwwLjAxIDE1LjM2LDIzLjY0IGMgMC4wNywwLjEgMC4xMywwLjIgMC4yLDAuMyBsIDAuMDgsMC4xMyAwLjAxLC0wLjAxIGMgMi4zOCwzLjM4IDYuMyw1LjU5IDEwLjc1LDUuNTkgNy4yNiwwIDEzLjE1LC01Ljg5IDEzLjE1LC0xMy4xNSAtMC4wMiwtMi41MSAtMC43NCwtNC44NiAtMS45NywtNi44NyB6IgogICAgICAgICAgICAgICBpZD0icGF0aDQwMDUiCiAgICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICAgICAgICA8L2RlZnM+CiAgICAgICAgICA8Y2xpcFBhdGgKICAgICAgICAgICAgIGlkPSJjbGlwUGF0aDM5NDgiPgogICAgICAgICAgICA8dXNlCiAgICAgICAgICAgICAgIGlkPSJ1c2UzOTQ2IgogICAgICAgICAgICAgICBzdHlsZT0ib3ZlcmZsb3c6dmlzaWJsZSIKICAgICAgICAgICAgICAgeGxpbms6aHJlZj0iI1NWR0lEXzFfIgogICAgICAgICAgICAgICB4PSIwIgogICAgICAgICAgICAgICB5PSIwIgogICAgICAgICAgICAgICB3aWR0aD0iMTAwJSIKICAgICAgICAgICAgICAgaGVpZ2h0PSIxMDAlIiAvPgogICAgICAgICAgPC9jbGlwUGF0aD4KICAgICAgICAgIDxnCiAgICAgICAgICAgICBpZD0iZzM4NDYiCiAgICAgICAgICAgICBjbGlwLXBhdGg9InVybCgjU1ZHSURfMl8pIgogICAgICAgICAgICAgY2xhc3M9InN0MyI+CiAgICAgICAgICAgIDxpbWFnZQogICAgICAgICAgICAgICBpZD0iaW1hZ2UzODM2IgogICAgICAgICAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSg0OC43ODE1LDc4LjIzNTcpIgogICAgICAgICAgICAgICB4bGluazpocmVmPSJkYXRhOmltYWdlL3BuZztiYXNlNjQsaVZCT1J3MEtHZ29BQUFBTlNVaEVVZ0FBQUZjQUFBQkNDQVlBQUFBaTAwcEVBQUFBQ1hCSVdYTUFBQXNTQUFBTEVnSFMzWDc4QUFBQSBHWFJGV0hSVGIyWjBkMkZ5WlFCQlpHOWlaU0JKYldGblpWSmxZV1I1Y2NsbFBBQUFFYzVKUkVGVWVOcnNYRmw3MnppeVJRRWt0ZHVTIDdlenA2ZW4welBOOXVQLy9KOXp2NjNucmg4azJTWng0azdXTEcxRDNGRURac3VNa1hpUmJtUm5tWTBSVEZBRWNGazZkS2dCVTZyL2IgMmpiYWtQS1g2OEhmT1A3cHlxY0hBcFIyZDNmcDFhdFhOQndPS2M5emNzN0plWTdqV0RVYURSZEZFZi94eHgrODFFRCsyY3FuKzdUUSBuWjBkL2VUSkU1Mk5SanBoTnE1ZU4xU1dwb2dpSTQzRHBneVJNMlZwMlpoU3oyWTJhelpkclZaemYvNzVwN3REUXgray9Qc0FsN3JkIHJrYkREQ3pFb0tLeHNpcWh3aVdzYlkzSUpFd2NzZE5hRWJNbXR1eW9VR3d6cmVMTTFWVEJ6SG1hcGlVc3lyNSsvWHJSU043MDh0Y0ogTGoyVDd0ZHE2ZUhPRG5wYm5MQzFkYTFVazBrM3RlTW1ybWt4S2ZsTWlNbWd0bWdkRitSVWlvcE44ZDBNcloyUnRUT3I5UnlXbFpiVCBhZkg0OE5EK24xTHVCMWIwME9XdkRWemEyOXVqNTl2YmtadWtjUmJYR3NyWURtcXpoZSsyRmV1ZS95VDVtN2RRdlFiRGhGQVpHQkhuIE9KN2kreUhPRDNITkVPMFk0THNoQUJoUm9hZE5ubVZwdDFzdWRWWGVzUExYQnE1djJMTnVOeXJtOHhyNHJJMysxbFdhOXBqVkkzYnEgTVVwOUJCdnA0Yk9MNnp2WTYwSjNZam40bCtQdldkVzRnU0k2d2ZFaGFYV0VoaC9DWHZwT3VWSEQybm5aNlJSWE5QQ2h5MThmdUdpWSBsb1k1YVpqV0hVdTBSOW84UmRFdlVmTVhMTDJWK0JHeGIxZ2JleE9WU0ZBekxSWEVjWW5qRE1jVDFHNkU0ejdPSHBEaWZYVExqN2prIEU3cjNZVTJwZ1hadWRxbUJhZ0hzUFpkdnJ3STNXaVd3ditGaDFiUE1ETk0waVV6Y1FRTWVvU0V2NEJCK3hWZS9vZlJmaVBneHFyRlQgTmF5T3ZZYnpFWjgvYUtsb2dUM0ZkVE9jM01NOWRwZ0ozUmw4Q2JWRVFDdGpyWnl4YkFjRC9oOWMvdy84NERIMjNRY28vMzhCYk1YQiBGd0EycTZRRGFyZE4xTzRsRWNWdGJkUWV1dHBMVk80VnZ2b2JxdjQ3S2ZvTHJudUt2M2NDMzZtVzc1S0thdDZwK0oxaWFiQnZPRW5qIHFZSGpScmlHSTgxZU1jRWdWWW51bnBPTnk3SVJ1Ny9QWnFyV2JPcFpweE5IcUltT0RDeFd2VmhiK1Jyc3kzQisxcGJqWnRPT3B0T3YgTEhkbDRQWjZQYlBWNjhYb1hFMkRKdzJvbjZNTHdwalYzN0QvanBML2dncUtjVWwzYkMwYVZQVWVxWWRlMnVWY0ZCcktjYmlPcE92RyBhREF3MEdKb0Jhd3daMlVoc0xSTjIyMHU4SEFwaWhwRzZ4NnM2em02czFqcjM5ZFF2a1FjQlk0eXJYWE90VnFKOXNPSUI3eHlXdmdGIHBYWFF4Q0xMYWdodE9zcVlQWngrZ2YydjFTNFdERWZDSFhRYzZZcm9ocXkvd2ZsbklXbDFqZmFOSjVHZy9qdDBQeTV4SC9BaXB4VHAgUENaakVSUUkyZ1FlaEx6U08zQll6OURTWC9HVFgvRWpzV0NoaUswN2xpK25MVDdFNmMyVnFBcWlXV1J0TmltS1VsMlNaNnV3WE5KQyBCNzFlb3VLNGpjSWVrZFl2NFpsL2w2Nkk3OEYzNm9tWFBvSGpvcXJDUDNLbXRMVHJKZXZ5djBQdkxORTl4ZkdrT0puRHlUaDh4bzRCIElHbnArcUhYVUNpZlF2bU51NVl2WFFiRzZ3QjhqbTltYU9zTXNkMGNuem1jcVQwOVBlV1ZXUzY2Zzk3YTJrS0U0eHB3SE5zd25rY3MgbnBub0Y5UUtubG50UXNKMCtHYkFMbSs2K3BRdUxJSWZHaFZjcTNndWVwVEZnb1Q3akpHR2l6TVM3UXBhVUZLUFBXaFRrVnpvTWJ5SyA4bHVlRGhTTG94dmduc2V5bzMwRGFMblpaRDdQMVpKamkxWklCMjA4UGFHRDUrRXJmb0ZuRE0vTTIzd3ppL2xSQTlFN1ZLQUZWbk4wIFZVUlRsTU5zZ1RkbktCTmRuNlJNY0NzSjBDMWNWOE0xMzZPQ2E1VmZjVEFVQTVTRDRqMzhMUTlRbk9PaDZHSkVnWE44bGxVWmJGWkogQjhyVEFiM0M4ZThlWUdoTTdCM3NTZFd0N3FxcnRmUktXZ2FKbERpWEVnMjFPRVlaMUNGNXFFRVpQTUhsUFpLUVY1elIrVU82NnliVyBtWHVwcGhHNUVZa1ZUMkxtZEd0M3QxZzR0bHRicm1TWXVoRHJFcThyejNNQVY1d1kwVXN2ZDRqRXFiUXJhMXNGc05YdldTS3BXc1hoIEM4Y2kxQ0RXM0ZlU2hHR3hicXJEVWhOWWRzU3JBZFZ6YjlVRGFpRXZRVUkzV3pqWnhIRmlvOGhvK05SS0s5OGFYQ3FLUWtNRXhWRFUgTFNkQUtucUtoa0FWOEhNUHRBQit6ck9yaWdRWHprVWVtRmhzRjQzS3dQV3A2TjZnV1pWNGM0UzBMTWZSSmNkMDUvS0ZXbkFqUEdCUSBCSGdjSVYwZDVTZVFaTkRBckdkbHVhZ24zd3BjU2VIdGRqb1JmbHgzV20vNUtFYnhNM0NkT0REUHN3QTQ4Q3pmbXVkK3lMOHNRUUN6IFJGdkN2NkFGbG1nckZja0Zla0pZQzQwYUxHbWx1Wk9xSnhqUWdVZzBBMnNPa3MwNXhCU1dicTBXdkJNckNsTTRsOWc0N3VDcDdhSUIgRXJ2RFl0VVQ5dDRjWGhWUGMwVjA4QzBMUnNOOElOQUd3SHZDdlNMMHE2U0xKR0p3M2tkZGhsZGJCNllnQjlCMHJrU3Q1SUZKWWplRyBCZDg2aUtCeEc1Rmx0eHRyWTVxUVgxMG9CSWw2bmxWaEpUd29peVdqMFpJZjViV2xOTDJsc085NVFqMWJ2cVVBVzNLd3dyTWtrc3p6IDdwbEtXVkhCTUJzOFNCUW5JWGpCa2tYVFZPSUVoTFlXaFhBN25YdEIwenJuTlMwYUNPbEZMMEtnd0JKYU5vSm5YamtkZkk5LzJZc0kgUEZRQW0xV1cybHdLR2xaVkQvWTlSSnluQkM0S3dRUHgzT2VBY2M0NFo1TjZuVytzYzcvU3RDSEVGV0Jmc253UzdWYnFvTFpHT3JpYSBIa0taRWtRSURSUjByb2xqYjkwQitGWFVSenFJS0lHTVBQM3dCTGNkU2FRR1k4dHpJbnUwdjM5ank3MUFCeUR1SHA3YUUzU0o1NmczIHJKWWZoOHorMnB6WWowWlNUUFdIcVVEMTU4VFJvQXV2cWk0c1doci9GV2hmaXJaUGZKU20xQWltUEFXNDJSemViREtaM014eXZ3cHggdFY1b1dvbkUxcUZwYnd1d1dSNlI0SEM0S3F1VmZJSU5DU09mU0JkZ1Q0bWRCQkV6T1BmY0JIMTdmY3U5Um9pN0xrMTdVNERYV2E1RCBPMjJRZko0S0FLemtGZFN4Z0F5RGsrUk5mbnA2Nm00QzdrVTZZTzVCYnp4bXAxNDhNQjNjNThZZVhPYWlpZ2FIT0Q0Q1JSeWdieHdwIEdlNEJ1RG1pcXRsc2RuMXdyd3B4dVFweE9hVHhlampmckJMTHhGZVBndjdzWUxzcW5FMkZYMzBtVEtuUE1xWUdtampDMzJPcmRWcXYgMSsxU1B2ZUg0SjZGdUlqdG1qRDdIdkI3QXFuOC9BellFRzVLS0NoOFZGNEtOU1ZMUlpjODljOEd0RFJBa3ZPUVhXcUs2dmR4L0FVdCArb2kyZlFZZG5DaXRKMWxaNW5Cazl0b0RsQkxpZGhEaXNnaHhyVHVBYjRjbHhhWjRsOExnbmpnUDhaNlNOQkZwUW53NXVVd1VYUnBHICtkbXMyUnNOVjFZTGF6a0dCcDhGWVB4OUpCUmgwblRlR28zS2o1Y280WnZnbm9XNFJaRndrclJnZkYxSGFnZFBxa3NpekFPRUVnbVYga0NSNmFkQmU3QlNSazc5djdXeWdMK2pQK0ZLMFJCdFBCOEdKNVFnU3h2ZzhRYnRndGJ5UDR3TTA0aFJjTzdYRzVPUFo3TnBENjk2SiB4YTFXYkxSdUVPazJITmkySko1eFl4a0J4Wk9rQVlBZEI3WERZSXd6cnFIcW5vbk1ZdkVqQUFvU2pVa1MzTTBGMEJJYVZ4SGNwb0s4IDdNUm1LdVJyRHpsdzdSZllzNHcraktaNW5nMm1VenY1eG95YjZDb25KaU81enRvRWlEVk5pTnNCRWtuR0NRS2FqbUdlNkJJUTBvcEYgOHhVK1VSMFNHS1JDVWxyVVF3ZXc3YWlRcWQ5bG1mV2lXQjVTQ3hXczhUbGRiS29USzgvVkFTaEEwVDZNNjVOWUxZQWV5Tnl4cU5FbyBKZ2NIVGwwVFhPL0U4anhQWW1NYUVoZ0F0WFpJeENnWkVPeFhDV3FoaEFrZUxmaVdFUFZ4NkJaQ0VheGlnQ2ZqL1RKTlNQSzhqME5TIGg1L2k5NC94b1BZazMwbytCeUhPYnVQazI3bW1sV0NCejNqMm95YTE3eHlmSVBRWEo1YU54Mk9ydmpNUkw3cENlaGtBNEJQQjJCdncgaGpFQWN6SUpUUXBrNG9sRGw4QkZZeFNXc3FhQ2JDbVRXMzJ0Y0wwaDUrUmh5RmhUbDJXZWxmQ1ZIOUNUZUp4S0g2TDcwWm9Md05JRyAwc0dwOHZYblQ2Q0JULzdZMnFISjgyODZzVytCNnhPOXNGeURHOFd4MWdsdUhsZVpvSmtmU21ZYUs0UjdPRDlCMkRkMUdzV1VoVFVWIFZrNmtGMEo2U0xjSU42czVCNjRpZllyZnlXekJDUWRKNHkrcTBvVm1pYWMza0E1OGtQQVI0SDVBM2IrdzQ3NkQ5RUtGdituRXZndXUgZVBNb2lqVExzZFl5RndBeWhHWk9yRTdMeERROTBscmpXT2UxUnFNOC9QQ0JqVEVCWE9ka3lqdHQ3ZTdxMldnMHQyVTVUMHcwaFpTVCB5dWJPa3pKSDFUQjdqY0xETTFWdTlxSHA0U282Z0RMZ0Q2alVQZ3ZReG94Z2ZHbC9QQzVIMTVqOGZNRmlndDZIYlRKYmhMc2lRYVlzIEV4N1lsUUJvU3BxbVJWN01BR0ErR0F6S0Zrb0U3L0RsZXd6SFl4cUJqeUt0eSsxMlc4YjVyZWF6OGhvVXBtMzZlVnFLZVFGdy9JRDAgOEQwNjJKZWhjMURkRU5wMEhvTU9QdjJBRHE0Q0Y1STJjUUN1Vk5ibUxpdkFqNUVWZTdKa0M4Y3VSUkFvWVY3UjcvZnR5Y21KT3pvNiArcW9BZ0s2cW9XV1d0UVFtanJuWmFDaG5aV0NQWWppL3BrelpZMUVOck9TNGhnWXNSbWlqcXRMMEFIU3dIT0lLSFh3Q3VCL1IyVDdEIDJrNXVRZ2RxT1E5NmxxTGprTkJ2eExIU1JlRktFNVZhNmRTVVVRcDFtclZhclFLZ0NyRFhXaE1BbWxGbENhTUhiVVFBV1F1OHdnemEgUjI0MUN2UlFQMHRzbjArRXUwOXdmWjZXQWgyZ3Q1Tk04UGdYYXZBYW4yOVpISmt4L2R6YTJlbDRYUFN6N05xTFhTN29UQUNoc2l4VCBNWUJvZFRxMkpDNlRXbElZYllyandiRlBUSHorL1BrbWl6MDh3SWoybEl4a3NnNXpPZ2lLUWpxS1RPZXR3RjFNNFl4VUdFbWxld1JZIHJMYmdNTERaOS95cTZDMXM3QTMrL29ENkhrVlpOcWtOaC9sK3lCL2NEbHhwRUp3U2w5WWlER05PMDlSbWVlWnkvRHM4UEhURDRmQlcgYThFU0FFeTFtdEtOQmp1MW1EWURnRm1GdWJCVWhjcmV3ZkY5aHNqQmlSRkpia1RXUFh4Qm5kNlRXQzJwOTZqRWx3Z0JBK3FjanZyOSBjbnJESlZyUkZjU3U1dk01WTZmbGMzZlpKSlUweXpLM2xXVkZNMjZFN0ZJRUhSM3l3R0dTTWZzOXZrZDV0cElROXlhV3U3Wk5LR2U3IEtMZ0pqNVpGRWN2YUdRcnl3aXNGOWhUQmRaSzVDSkpOQytmWFNROExUUnZVQVN2SXJqTTZlTytCMW5wTWNaeWgxOXJiR05sOWluZEcgV0szbUxXZmpScEl4cEEwYUlITzVZTDNVOWt1V0Z1c1UxaS9QVmhiaWJvVGxLaFU4UmdHbkpoSk5acVk0Q2VuWTV4ZkVVaE94M3BEUyBKRkVTOGFVOE1LMlVEa0tPWklLeWpvUHNVbTgwc3lpRWovREN4OUMwazJRd3lEL2QwSWs5R0xpeUljSlJZc0VBbDZNb1pycC9lWGFKIERoZ1dxOTZnRHEraGFkL0RqeDg0aEw2eVd2STJUdXhCd1Yzdzd3UEpzNFU2U0xGTHNQQUZ2ZVU5enY4VE4zNTNGMDI3TWVBK2tEejcgSGgyOFVSVG9JQUlkM0ViVGJoUzRNcjg5azZVYlVhUmluYkRHUDZYOWdLWk13SThxY092VnpNWG9qdnk3QUxha0MrcEF2VjJpZ3krciBvb01IQi9lYThxeEdZUmIzQW1COUM0RDVRaXBSMXZSS3NBQzVoUzllQTh5M1VDNzdxNlNEalFCWHRwclFRNk91NHEyMjUwUUtTUTVkIE9iTmFOUW9TbkJ0ZFdJeDNIWURQZ1EzTHFtU2cwZWRveFlscHhXOFc2bUNWZExBeDRDN0xNMk5BdWRhR3hDNDhuSkozSUpCazB2ekkgY1V4K1lQTnNaYzFsQjNjNW9xem1ISGlMbFp5MExKSStramtIUG5lZzZEVUpIVGgzYUZkTUJ4c0Q3ckk4azVuWk1jVk95Mmk5RHF2cCBLdEFNaFpVNmxXb2dQd2Y1SEZWYXpuQnhsVDRVYTgwbDBWOWx1NDdJanlxb3QwRjZxZmZRMlB0c3pXbXV5cFhTd1VhQit4WC94bDcvIEl1N0g3a0VNODAxOG9qY000MysxeXdJL0NxRG1hckhLUjFTQmpOMlJPb1MxZmdyQWVpcDRwMWp2VytKK21lZVQ3bmlZZjVoTW5GcnggMjZBMlplenFMRHpPMm13YmlVbFZYZ3pCRTlvSnRsckwxRTFaTlRsakdTZ2xQK3RIMXZHMktrMGMwL243RW16MXZvUjVHTGRUb2d3TyAvZnNTb0dseHdiK1VkWitOY3NlUkxTZmNhUlFuaC9zckIzYlR3RlhIa2oxTFUvZTAzUzVhY1NRekI1V05JZ2NtTG1TNENkWTNKSm0yIHlUSmN6ekpYYlpHUFNBSnR5TEJ5OWE0RVA2SWdhVVQvcG84RGRJZ3ZBRjZpc1VONHpYNXN5ekUwZHZaNU1DaVAxL1Qrc2syYzdVTE4gWnBPZTd1M3BwakZ4a2RzNjY2ak4yc3BzZHV4cUYrRHVLbGwrR2w2aklrdFFHMkVXejlrN2FpYXFlbzBLR3RqSHlST0FmNnBsRWFEViBJNlB5YVZTdjV4N1k0Mk5XLzBIZ25nSDh2RjdYVGFYTnBOV3VBVHBaVE5lRzViYjl4RURtRHF5eXpZcXJUSnFzQ2ZNcm1EemZ3bG1OIFpZNEZUb3lKM1pobERFeVdrK1kyM1o0TWl2ZTFtbDBuc0pzTXJ0LzJKRHhMRXJLOW5tbTMyM0ZaRkhGa2pDekRxbE1wczNwY0E1NnYgRGc2T1ZmVmVNQ1d6ZjVneUFEb0gzaWtDNXhRNk5pMWgwWEVjRi8yREE5dUZsbjI3L2xjWi9oUlRPYjBWQTF3b1g2MjN0N2NqbTJiRyBsQzR1RGNYbzZwR0x0SEV1T0RRakM2MnRLMlZoU0dTNUtPcXh4Yy9LbzZNaksxcjY0T0JncmRhNmtWTHNCenFZcDlPcG4zd2lHblkyIGxzbmNxbWozdWptenl4MENBQWRMaFh5YlEyRE1ramhLbzFxU25wNGNGZk95RkFkblpReHdlc1Y3YVA3VExmZktlbmU3WFpuYlJtbWEga294WWh4bFZZWGxvbzlId24rL2V2YnMzSy8xM0F2ZDc3V0QxMyszZmYvdC9BUVlBNjZnMU1DdTRUUjhBQUFBQVNVVk9SSzVDWUlJPSIKICAgICAgICAgICAgICAgaGVpZ2h0PSI2NSIKICAgICAgICAgICAgICAgd2lkdGg9Ijg2IgogICAgICAgICAgICAgICBzdHlsZT0ib3ZlcmZsb3c6dmlzaWJsZTtvcGFjaXR5OjAuNSIgLz4KICAgICAgICAgICAgPGcKICAgICAgICAgICAgICAgaWQ9ImczODQ0Ij4KICAgICAgICAgICAgICA8ZwogICAgICAgICAgICAgICAgIGlkPSJnMzg0MiI+CiAgICAgICAgICAgICAgICA8cGF0aAogICAgICAgICAgICAgICAgICAgaWQ9InBhdGgzODM4IgogICAgICAgICAgICAgICAgICAgZD0ibSAxMjcuMTQsOTcuMTUgYyAwLC03LjI2IC01Ljg5LC0xMy4xNSAtMTMuMTUsLTEzLjE1IC03LjI2LDAgLTEzLjE1LDUuODkgLTEzLjE1LDEzLjE1IDAsMi42NyAwLjgsNS4xNSAyLjE3LDcuMjIgMCwwIDAsMCAwLDAgSCAxMDMgTCA5My4wMiw5MC4yNiBIIDkzIEMgOTAuNjgsODYuNTEgODYuNTUsODQgODEuODEsODQgYyAtNC43NCwwIC04Ljg3LDIuNTEgLTExLjE4LDYuMjYgaCAtMC4wMSBsIC0xNS45LDI0Ljc4IGMgMi4zNiwtMy40OSA2LjM2LC01Ljc5IDEwLjksLTUuNzkgNC43NywwIDguOTQsMi41NSAxMS4yNCw2LjM2IGggMC4wMiBsIDEwLjM5LDE0LjU5IGggMC4wMiBjIDIuMzksMy4yNCA2LjIzLDUuMzYgMTAuNTcsNS4zNiA0LjM0LDAgOC4xOCwtMi4xMSAxMC41NywtNS4zNiBoIDAuMDIgbCAwLjExLC0wLjE3IGMgMC4yNywtMC4zOCAwLjUyLC0wLjc3IDAuNzYsLTEuMTggbCAxNS41NywtMjQuMzMgYyAwLDAgMCwwIDAsMCAxLjQxLC0yLjEgMi4yNSwtNC42NCAyLjI1LC03LjM3IHogTSAxMDQsMTA1LjY5IGMgLTAuMDEsLTAuMDIgLTAuMDMsLTAuMDMgLTAuMDUsLTAuMDUgMC4wMiwwLjAxIDAuMDMsMC4wMyAwLjA1LDAuMDUgeiBtIDAuOTcsMS4wMSBjIDAuMDgsMC4wNyAwLjE2LDAuMTQgMC4yMywwLjIxIC0wLjA3LC0wLjA2IC0wLjE1LC0wLjEzIC0wLjIzLC0wLjIxIHogbSAxLjE3LDAuOTggYyAwLjA5LDAuMDYgMC4xNywwLjEzIDAuMjYsMC4xOSAtMC4wOSwtMC4wNiAtMC4xNywtMC4xMiAtMC4yNiwtMC4xOSB6IG0gMS40OSwwLjk4IGMgMC4wMSwwIDAuMDIsMC4wMSAwLjAzLDAuMDIgLTAuMDEsLTAuMDEgLTAuMDIsLTAuMDIgLTAuMDMsLTAuMDIgeiBtIDEuNCwwLjY2IGMgMC4xMSwwLjA0IDAuMjIsMC4wOCAwLjM0LDAuMTIgLTAuMTIsLTAuMDQgLTAuMjMsLTAuMDcgLTAuMzQsLTAuMTIgeiBtIDEuNDEsMC40OCBjIDAuMTYsMC4wNCAwLjMxLDAuMDggMC40NywwLjEyIC0wLjE1LC0wLjA0IC0wLjMxLC0wLjA4IC0wLjQ3LC0wLjEyIHogbSAxLjU0LDAuMzMgYyAwLjE1LDAuMDIgMC4zLDAuMDUgMC40NiwwLjA3IC0wLjE2LC0wLjAyIC0wLjMxLC0wLjA1IC0wLjQ2LC0wLjA3IHogbSAxMC42NSwtMy4wOSBjIDAuMTIsLTAuMSAwLjIzLC0wLjIxIDAuMzUsLTAuMzEgLTAuMTEsMC4xIC0wLjIzLDAuMiAtMC4zNSwwLjMxIHogbSAtNy4xMSwzLjE2IGMgMC4xNiwtMC4wMiAwLjMxLC0wLjA1IDAuNDYsLTAuMDcgLTAuMTUsMC4wMiAtMC4zLDAuMDUgLTAuNDYsMC4wNyB6IG0gMS40OSwtMC4yNyBjIDAuMTcsLTAuMDQgMC4zMywtMC4wOCAwLjUsLTAuMTMgLTAuMTYsMC4wNSAtMC4zMywwLjA5IC0wLjUsMC4xMyB6IG0gMS40OSwtMC40NSBjIDAuMTQsLTAuMDUgMC4yOCwtMC4xIDAuNDIsLTAuMTUgLTAuMTQsMC4wNiAtMC4yOCwwLjEgLTAuNDIsMC4xNSB6IG0gMS43NCwtMC43NiBjIDAsMCAwLDAgMCwwIDAsMCAwLDAgMCwwIHogbSAxLjI0LC0wLjc4IGMgMC4xMSwtMC4wOCAwLjIyLC0wLjE2IDAuMzMsLTAuMjQgLTAuMTEsMC4wOCAtMC4yMSwwLjE2IC0wLjMzLDAuMjQgeiBtIDIuMjMsLTEuOTcgYyAwLjA5LC0wLjEgMC4xOSwtMC4yIDAuMjgsLTAuMzEgLTAuMDksMC4xMSAtMC4xOCwwLjIxIC0wLjI4LDAuMzEgeiIKICAgICAgICAgICAgICAgICAgIGNsYXNzPSJzdDQiCiAgICAgICAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzMyNTJhMCIgLz4KICAgICAgICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICAgICAgICBpZD0icGF0aDM4NDAiCiAgICAgICAgICAgICAgICAgICBkPSJtIDU0LjQsMTE1LjU2IGMgMC4xLC0wLjE2IDAuMTksLTAuMzIgMC4yOSwtMC40OCBsIC0wLjMxLDAuNDggeiIKICAgICAgICAgICAgICAgICAgIGNsYXNzPSJzdDQiCiAgICAgICAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzMyNTJhMCIgLz4KICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgIDwvZz4KICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICAgICAgPGNpcmNsZQogICAgICAgICAgIGlkPSJjaXJjbGUzODUwIgogICAgICAgICAgIHI9IjEzLjE4IgogICAgICAgICAgIGN5PSI5Ny4xNjk5OTgiCiAgICAgICAgICAgY3g9IjExMy45NiIKICAgICAgICAgICBjbGFzcz0ic3Q1IgogICAgICAgICAgIHN0eWxlPSJmaWxsOiNmMzgzMzQiIC8+CiAgICAgICAgPGcKICAgICAgICAgICBpZD0iZzM4NTgiPgogICAgICAgICAgPHBhdGgKICAgICAgICAgICAgIGlkPSJwYXRoMzg1MiIKICAgICAgICAgICAgIGQ9Im0gMTEzLjkzLDExMC4zIGMgLTQuNTksMCAtOC42MywtMi4zNiAtMTAuOTgsLTUuOTMgaCAtMC4wMSBMIDkyLjk2LDkwLjI2IEggOTIuOTUgQyA5MC42Myw4Ni41MSA4Ni40OSw4NCA4MS43Niw4NCBjIC00LjczLDAgLTguODcsMi41MSAtMTEuMTgsNi4yNiBoIC0wLjAxIGwgLTE1LjksMjQuNzggYyAyLjM2LC0zLjQ5IDYuMzYsLTUuNzkgMTAuOSwtNS43OSA0Ljc3LDAgOC45NCwyLjU1IDExLjI0LDYuMzYgaCAwLjAyIGwgMTAuMzksMTQuNTkgaCAwLjAyIGMgMi4zOSwzLjI0IDYuMjMsNS4zNiAxMC41Nyw1LjM2IDQuMzQsMCA4LjE4LC0yLjExIDEwLjU3LC01LjM2IGggMC4wMiBsIDAuMTEsLTAuMTcgYyAwLjI3LC0wLjM4IDAuNTIsLTAuNzcgMC43NiwtMS4xOCBsIDE1LjYyLC0yNC4zMyBjIC0yLjM4LDMuNDkgLTYuNDMsNS43OCAtMTAuOTYsNS43OCB6IgogICAgICAgICAgICAgY2xhc3M9InN0NiIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojMzE1MmEwIiAvPgogICAgICAgICAgPHBhdGgKICAgICAgICAgICAgIGlkPSJwYXRoMzg1NCIKICAgICAgICAgICAgIGQ9Im0gNTQuMzMsMTE1LjU2IGggMC4wMiBjIDAuMSwtMC4xNiAwLjE5LC0wLjMyIDAuMjksLTAuNDggeiIKICAgICAgICAgICAgIGNsYXNzPSJzdDYiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzMxNTJhMCIgLz4KICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICBpZD0icGF0aDM4NTYiCiAgICAgICAgICAgICBkPSJNIDEyNC44OCwxMDQuNDMiCiAgICAgICAgICAgICBjbGFzcz0ic3Q2IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTUyYTAiIC8+CiAgICAgICAgPC9nPgogICAgICA8L2c+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K'});
6
+
7
+ blocks.registerBlockType('matomo/matomo-opt-out', {
8
+ title: __('Matomo opt out', 'matomo'),
9
+ icon: matomo_icon,
10
+ category: 'text',
11
+ example: {},
12
+ edit: function () {
13
+ return __('Matomo opt out', 'matomo');
14
+ },
15
+ save: function () {
16
+ return '[matomo_opt_out]';
17
+ },
18
+ });
19
+ })(window.wp.blocks, window.wp.i18n, window.wp.element);
classes/WpMatomo.php CHANGED
@@ -11,32 +11,32 @@ if ( ! defined( 'ABSPATH' ) ) {
11
  exit; // if accessed directly
12
  }
13
 
14
- use WpMatomo\Admin\Menu;
 
15
  use WpMatomo\Admin\Dashboard;
 
 
 
 
 
16
  use WpMatomo\Commands\MatomoCommands;
17
  use WpMatomo\Ecommerce\EasyDigitalDownloads;
18
  use WpMatomo\Ecommerce\MemberPress;
 
 
19
  use WpMatomo\OptOut;
20
  use WpMatomo\Paths;
21
- use WpMatomo\ScheduledTasks;
22
- use \WpMatomo\Site\Sync as SiteSync;
23
- use WpMatomo\AjaxTracker;
24
- use \WpMatomo\User\Sync as UserSync;
25
- use \WpMatomo\Installer;
26
- use \WpMatomo\Updater;
27
- use \WpMatomo\Roles;
28
- use \WpMatomo\Annotations;
29
- use \WpMatomo\TrackingCode;
30
- use \WpMatomo\Settings;
31
- use \WpMatomo\Capabilities;
32
- use \WpMatomo\Ecommerce\Woocommerce;
33
- use \WpMatomo\Report\Renderer;
34
- use WpMatomo\API;
35
- use \WpMatomo\Admin\Admin;
36
  use WpMatomo\RedirectOnActivation;
 
 
 
 
 
 
 
 
37
 
38
  class WpMatomo {
39
-
40
  /**
41
  * @var Settings
42
  */
@@ -58,7 +58,7 @@ class WpMatomo {
58
  return;
59
  }
60
 
61
- add_action( 'init', array( $this, 'init_plugin' ) );
62
 
63
  $capabilities = new Capabilities( self::$settings );
64
  $capabilities->register_hooks();
@@ -97,10 +97,13 @@ class WpMatomo {
97
  $referral->register_hooks();
98
  }
99
 
 
 
 
100
  /*
101
  * @see https://github.com/matomo-org/matomo-for-wordpress/issues/434
102
  */
103
- $redirect = new RedirectOnActivation($this);
104
  $redirect->register_hooks();
105
  }
106
 
@@ -115,10 +118,10 @@ class WpMatomo {
115
 
116
  add_filter(
117
  'plugin_action_links_' . plugin_basename( MATOMO_ANALYTICS_FILE ),
118
- array(
119
  $this,
120
  'add_settings_link',
121
- )
122
  );
123
  }
124
 
@@ -134,7 +137,7 @@ class WpMatomo {
134
  $upload_path = $paths->get_upload_base_dir();
135
 
136
  if ( $upload_path
137
- && ! is_writable( dirname( $upload_path ) ) ) {
138
  add_action(
139
  'init',
140
  function () use ( $upload_path ) {
@@ -142,7 +145,7 @@ class WpMatomo {
142
  add_action(
143
  'admin_notices',
144
  function () use ( $upload_path ) {
145
- echo '<div class="error"><p>' . sprintf( __( 'Matomo Analytics requires the uploads directory %s to be writable. Please make the directory writable for it to work.', 'matomo' ), '(' . esc_html( dirname( $upload_path ) ) . ')' ) . '</p></div>';
146
  }
147
  );
148
  }
@@ -157,24 +160,13 @@ class WpMatomo {
157
 
158
  public static function is_admin_user() {
159
  if ( ! function_exists( 'is_multisite' )
160
- || ! is_multisite() ) {
161
  return current_user_can( 'administrator' );
162
  }
163
 
164
  return is_super_admin();
165
  }
166
 
167
- private static function get_active_plugins() {
168
- $plugins = [];
169
- if ( function_exists( 'is_multisite' ) && is_multisite() ) {
170
- $muplugins = get_site_option( 'active_sitewide_plugins' );
171
- $plugins = array_keys( $muplugins );
172
- }
173
- $plugins = array_merge( (array) get_option( 'active_plugins', array() ), $plugins );
174
-
175
- return $plugins;
176
- }
177
-
178
  public static function is_safe_mode() {
179
  if ( defined( 'MATOMO_SAFE_MODE' ) ) {
180
  return MATOMO_SAFE_MODE;
@@ -183,14 +175,29 @@ class WpMatomo {
183
  // we are not using is_plugin_active() for performance reasons
184
  $active_plugins = self::get_active_plugins();
185
 
186
- if ( in_array( 'wp-rss-aggregator/wp-rss-aggregator.php', $active_plugins )
187
- || in_array( 'wp-defender/wp-defender.php', $active_plugins ) ) {
188
  return true;
189
  }
190
 
191
  return false;
192
  }
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  public function add_settings_link( $links ) {
195
  $get_started = new \WpMatomo\Admin\GetStarted( self::$settings );
196
 
@@ -204,13 +211,11 @@ class WpMatomo {
204
  }
205
 
206
  public function init_plugin() {
207
- if ( ( is_admin() || matomo_is_app_request() )
208
- && ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) ) {
209
  $installer = new Installer( self::$settings );
210
  $installer->register_hooks();
211
  if ( $installer->looks_like_it_is_installed() ) {
212
- if ( is_admin()
213
- && ( ! defined( 'MATOMO_ENABLE_AUTO_UPGRADE' ) || MATOMO_ENABLE_AUTO_UPGRADE ) ) {
214
  $updater = new Updater( self::$settings );
215
  $updater->update_if_needed();
216
  }
@@ -228,8 +233,8 @@ class WpMatomo {
228
  }
229
  $tracking_code = new TrackingCode( self::$settings );
230
  if ( self::$settings->is_tracking_enabled()
231
- && self::$settings->get_global_option( 'track_ecommerce' )
232
- && ! $tracking_code->is_hidden_user() ) {
233
  $tracker = new AjaxTracker( self::$settings );
234
 
235
  $woocommerce = new Woocommerce( $tracker );
@@ -244,8 +249,4 @@ class WpMatomo {
244
  do_action( 'matomo_ecommerce_init', $tracker );
245
  }
246
  }
247
-
248
- public static function should_disable_addhandler() {
249
- return defined( 'MATOMO_DISABLE_ADDHANDLER' ) && MATOMO_DISABLE_ADDHANDLER;
250
- }
251
  }
11
  exit; // if accessed directly
12
  }
13
 
14
+ use WpMatomo\Admin\Admin;
15
+ use WpMatomo\Admin\Chart;
16
  use WpMatomo\Admin\Dashboard;
17
+ use WpMatomo\Admin\Menu;
18
+ use WpMatomo\AjaxTracker;
19
+ use WpMatomo\Annotations;
20
+ use WpMatomo\API;
21
+ use WpMatomo\Capabilities;
22
  use WpMatomo\Commands\MatomoCommands;
23
  use WpMatomo\Ecommerce\EasyDigitalDownloads;
24
  use WpMatomo\Ecommerce\MemberPress;
25
+ use WpMatomo\Ecommerce\Woocommerce;
26
+ use WpMatomo\Installer;
27
  use WpMatomo\OptOut;
28
  use WpMatomo\Paths;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  use WpMatomo\RedirectOnActivation;
30
+ use WpMatomo\Report\Renderer;
31
+ use WpMatomo\Roles;
32
+ use WpMatomo\ScheduledTasks;
33
+ use WpMatomo\Settings;
34
+ use WpMatomo\Site\Sync as SiteSync;
35
+ use WpMatomo\TrackingCode;
36
+ use WpMatomo\Updater;
37
+ use WpMatomo\User\Sync as UserSync;
38
 
39
  class WpMatomo {
 
40
  /**
41
  * @var Settings
42
  */
58
  return;
59
  }
60
 
61
+ add_action( 'init', [ $this, 'init_plugin' ] );
62
 
63
  $capabilities = new Capabilities( self::$settings );
64
  $capabilities->register_hooks();
97
  $referral->register_hooks();
98
  }
99
 
100
+ $chart = new Chart();
101
+ $chart->register_hooks();
102
+
103
  /*
104
  * @see https://github.com/matomo-org/matomo-for-wordpress/issues/434
105
  */
106
+ $redirect = new RedirectOnActivation( $this );
107
  $redirect->register_hooks();
108
  }
109
 
118
 
119
  add_filter(
120
  'plugin_action_links_' . plugin_basename( MATOMO_ANALYTICS_FILE ),
121
+ [
122
  $this,
123
  'add_settings_link',
124
+ ]
125
  );
126
  }
127
 
137
  $upload_path = $paths->get_upload_base_dir();
138
 
139
  if ( $upload_path
140
+ && ! is_writable( dirname( $upload_path ) ) ) {
141
  add_action(
142
  'init',
143
  function () use ( $upload_path ) {
145
  add_action(
146
  'admin_notices',
147
  function () use ( $upload_path ) {
148
+ echo '<div class="error"><p>' . sprintf( esc_html__( 'Matomo Analytics requires the uploads directory %s to be writable. Please make the directory writable for it to work.', 'matomo' ), '(' . esc_html( dirname( $upload_path ) ) . ')' ) . '</p></div>';
149
  }
150
  );
151
  }
160
 
161
  public static function is_admin_user() {
162
  if ( ! function_exists( 'is_multisite' )
163
+ || ! is_multisite() ) {
164
  return current_user_can( 'administrator' );
165
  }
166
 
167
  return is_super_admin();
168
  }
169
 
 
 
 
 
 
 
 
 
 
 
 
170
  public static function is_safe_mode() {
171
  if ( defined( 'MATOMO_SAFE_MODE' ) ) {
172
  return MATOMO_SAFE_MODE;
175
  // we are not using is_plugin_active() for performance reasons
176
  $active_plugins = self::get_active_plugins();
177
 
178
+ if ( in_array( 'wp-rss-aggregator/wp-rss-aggregator.php', $active_plugins, true )
179
+ || in_array( 'wp-defender/wp-defender.php', $active_plugins, true ) ) {
180
  return true;
181
  }
182
 
183
  return false;
184
  }
185
 
186
+ private static function get_active_plugins() {
187
+ $plugins = [];
188
+ if ( function_exists( 'is_multisite' ) && is_multisite() ) {
189
+ $muplugins = get_site_option( 'active_sitewide_plugins' );
190
+ $plugins = array_keys( $muplugins );
191
+ }
192
+ $plugins = array_merge( (array) get_option( 'active_plugins', [] ), $plugins );
193
+
194
+ return $plugins;
195
+ }
196
+
197
+ public static function should_disable_addhandler() {
198
+ return defined( 'MATOMO_DISABLE_ADDHANDLER' ) && MATOMO_DISABLE_ADDHANDLER;
199
+ }
200
+
201
  public function add_settings_link( $links ) {
202
  $get_started = new \WpMatomo\Admin\GetStarted( self::$settings );
203
 
211
  }
212
 
213
  public function init_plugin() {
214
+ if ( ( is_admin() || matomo_is_app_request() ) && ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) ) {
 
215
  $installer = new Installer( self::$settings );
216
  $installer->register_hooks();
217
  if ( $installer->looks_like_it_is_installed() ) {
218
+ if ( is_admin() && ( ! defined( 'MATOMO_ENABLE_AUTO_UPGRADE' ) || MATOMO_ENABLE_AUTO_UPGRADE ) ) {
 
219
  $updater = new Updater( self::$settings );
220
  $updater->update_if_needed();
221
  }
233
  }
234
  $tracking_code = new TrackingCode( self::$settings );
235
  if ( self::$settings->is_tracking_enabled()
236
+ && self::$settings->get_global_option( 'track_ecommerce' )
237
+ && ! $tracking_code->is_hidden_user() ) {
238
  $tracker = new AjaxTracker( self::$settings );
239
 
240
  $woocommerce = new Woocommerce( $tracker );
249
  do_action( 'matomo_ecommerce_init', $tracker );
250
  }
251
  }
 
 
 
 
252
  }
classes/WpMatomo/API.php CHANGED
@@ -9,31 +9,36 @@
9
 
10
  namespace WpMatomo;
11
 
 
12
  use Piwik\API\Request;
13
  use Piwik\Common;
 
 
14
 
15
  if ( ! defined( 'ABSPATH' ) ) {
16
  exit; // if accessed directly
17
  }
18
-
 
 
19
  class API {
20
  const VERSION = 'matomo/v1';
21
 
22
  const ROUTE_HIT = 'hit';
23
 
24
  public function register_hooks() {
25
- add_action( 'rest_api_init', array( $this, 'register_routes' ) );
26
  }
27
 
28
  public function register_routes() {
29
  register_rest_route(
30
  self::VERSION,
31
  '/' . self::ROUTE_HIT . '/',
32
- array(
33
- 'methods' => array( 'GET', 'POST' ),
34
  'permission_callback' => '__return_true',
35
- 'callback' => array( $this, 'hit' ),
36
- )
37
  );
38
  $this->register_route( 'API', 'getProcessedReport' );
39
  $this->register_route( 'API', 'getReportMetadata' );
@@ -88,18 +93,20 @@ class API {
88
  if ( empty( $_GET ) && empty( $_POST ) && empty( $_POST['idsite'] ) && empty( $_GET['idsite'] ) ) {
89
  // todo if uploads dir is not writable, we may want to generate the matomo.js here and save it as an
90
  // option... then we could also save it compressed
91
- $paths = new Paths();
92
- $path = $paths->get_matomo_js_upload_path();
 
93
  header( 'Content-Type: application/javascript' );
94
  header( 'Content-Length: ' . ( filesize( $path ) ) );
95
- readfile( $paths->get_upload_base_dir() . '/matomo.js' ); // Reading the file into the output buffer
 
96
  exit;
97
  }
98
  include_once plugin_dir_path( MATOMO_ANALYTICS_FILE ) . 'app/piwik.php';
99
  exit;
100
  }
101
 
102
- public function execute_api_method( \WP_REST_Request $request ) {
103
  $attributes = $request->get_attributes();
104
  $method = $attributes['matomoModule'] . '.' . $attributes['matomoMethod'];
105
 
@@ -135,7 +142,7 @@ class API {
135
  * @api
136
  */
137
  public function register_route( $api_module, $api_method ) {
138
- $methods = array(
139
  'get' => 'GET',
140
  'edit' => 'PUT',
141
  'update' => 'PUT',
@@ -147,8 +154,8 @@ class API {
147
  'send' => 'POST',
148
  'delete' => 'DELETE',
149
  'remove' => 'DELETE',
150
- );
151
- $starts_with_keep_prefix = array( 'anonymize', 'invalidate', 'run', 'send' );
152
 
153
  $method = 'GET';
154
  $wp_api_module = $this->to_snake_case( $api_module );
@@ -170,13 +177,13 @@ class API {
170
  register_rest_route(
171
  self::VERSION,
172
  '/' . $wp_api_module . '/' . $wp_api_action . '/',
173
- array(
174
- 'methods' => $method,
175
- 'callback' => array( $this, 'execute_api_method' ),
176
  'permission_callback' => '__return_true', // permissions are checked in the method itself
177
- 'matomoModule' => $api_module,
178
- 'matomoMethod' => $api_method,
179
- )
180
  );
181
  }
182
 
@@ -186,7 +193,7 @@ class API {
186
  $idsite = $site->get_current_matomo_site_id();
187
 
188
  if ( ! $idsite ) {
189
- return new \WP_Error( 'Site not found. Make sure it is synced' );
190
  }
191
 
192
  $params['idSite'] = $idsite;
@@ -203,19 +210,18 @@ class API {
203
 
204
  // refs https://github.com/matomo-org/wp-matomo/issues/370 ensuring segment will be used from default request when
205
  // creating new request object and not the encoded segment
206
- if (isset($params['segment'])) {
207
- if (isset($_GET['segment']) || isset($_POST['segment'])) {
208
- unset($params['segment']); // matomo will read the segment from default request
209
- } elseif (!empty($params['segment']) && is_string($params['segment'])) {
210
  // manually unsanitize this value
211
- $params['segment'] = Common::unsanitizeInputValue($params['segment']);
212
  }
213
  }
214
 
215
-
216
  try {
217
  $result = Request::processRequest( $api_method, $params );
218
- } catch ( \Exception $e ) {
219
  $code = 'matomo_error';
220
  if ( $e->getCode() ) {
221
  $code .= '_' . $code;
@@ -224,7 +230,7 @@ class API {
224
  $code = str_replace( 'piwik', 'matomo', $this->to_snake_case( get_class( $e ) ) );
225
  }
226
 
227
- return new \WP_Error( $code, $e->getMessage() );
228
  }
229
 
230
  return $result;
9
 
10
  namespace WpMatomo;
11
 
12
+ use Exception;
13
  use Piwik\API\Request;
14
  use Piwik\Common;
15
+ use WP_Error;
16
+ use WP_REST_Request;
17
 
18
  if ( ! defined( 'ABSPATH' ) ) {
19
  exit; // if accessed directly
20
  }
21
+ /**
22
+ * phpcs:disable WordPress.Security.NonceVerification.Missing
23
+ */
24
  class API {
25
  const VERSION = 'matomo/v1';
26
 
27
  const ROUTE_HIT = 'hit';
28
 
29
  public function register_hooks() {
30
+ add_action( 'rest_api_init', [ $this, 'register_routes' ] );
31
  }
32
 
33
  public function register_routes() {
34
  register_rest_route(
35
  self::VERSION,
36
  '/' . self::ROUTE_HIT . '/',
37
+ [
38
+ 'methods' => [ 'GET', 'POST' ],
39
  'permission_callback' => '__return_true',
40
+ 'callback' => [ $this, 'hit' ],
41
+ ]
42
  );
43
  $this->register_route( 'API', 'getProcessedReport' );
44
  $this->register_route( 'API', 'getReportMetadata' );
93
  if ( empty( $_GET ) && empty( $_POST ) && empty( $_POST['idsite'] ) && empty( $_GET['idsite'] ) ) {
94
  // todo if uploads dir is not writable, we may want to generate the matomo.js here and save it as an
95
  // option... then we could also save it compressed
96
+ $paths = new Paths();
97
+ $path = $paths->get_matomo_js_upload_path();
98
+ $wp_filesystem = $paths->get_file_system();
99
  header( 'Content-Type: application/javascript' );
100
  header( 'Content-Length: ' . ( filesize( $path ) ) );
101
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
102
+ echo $wp_filesystem->get_contents( $paths->get_upload_base_dir() . '/matomo.js' ); // Reading the file into the output buffer
103
  exit;
104
  }
105
  include_once plugin_dir_path( MATOMO_ANALYTICS_FILE ) . 'app/piwik.php';
106
  exit;
107
  }
108
 
109
+ public function execute_api_method( WP_REST_Request $request ) {
110
  $attributes = $request->get_attributes();
111
  $method = $attributes['matomoModule'] . '.' . $attributes['matomoMethod'];
112
 
142
  * @api
143
  */
144
  public function register_route( $api_module, $api_method ) {
145
+ $methods = [
146
  'get' => 'GET',
147
  'edit' => 'PUT',
148
  'update' => 'PUT',
154
  'send' => 'POST',
155
  'delete' => 'DELETE',
156
  'remove' => 'DELETE',
157
+ ];
158
+ $starts_with_keep_prefix = [ 'anonymize', 'invalidate', 'run', 'send' ];
159
 
160
  $method = 'GET';
161
  $wp_api_module = $this->to_snake_case( $api_module );
177
  register_rest_route(
178
  self::VERSION,
179
  '/' . $wp_api_module . '/' . $wp_api_action . '/',
180
+ [
181
+ 'methods' => $method,
182
+ 'callback' => [ $this, 'execute_api_method' ],
183
  'permission_callback' => '__return_true', // permissions are checked in the method itself
184
+ 'matomoModule' => $api_module,
185
+ 'matomoMethod' => $api_method,
186
+ ]
187
  );
188
  }
189
 
193
  $idsite = $site->get_current_matomo_site_id();
194
 
195
  if ( ! $idsite ) {
196
+ return new WP_Error( 'Site not found. Make sure it is synced' );
197
  }
198
 
199
  $params['idSite'] = $idsite;
210
 
211
  // refs https://github.com/matomo-org/wp-matomo/issues/370 ensuring segment will be used from default request when
212
  // creating new request object and not the encoded segment
213
+ if ( isset( $params['segment'] ) ) {
214
+ if ( isset( $_GET['segment'] ) || isset( $_POST['segment'] ) ) {
215
+ unset( $params['segment'] ); // matomo will read the segment from default request
216
+ } elseif ( ! empty( $params['segment'] ) && is_string( $params['segment'] ) ) {
217
  // manually unsanitize this value
218
+ $params['segment'] = Common::unsanitizeInputValue( $params['segment'] );
219
  }
220
  }
221
 
 
222
  try {
223
  $result = Request::processRequest( $api_method, $params );
224
+ } catch ( Exception $e ) {
225
  $code = 'matomo_error';
226
  if ( $e->getCode() ) {
227
  $code .= '_' . $code;
230
  $code = str_replace( 'piwik', 'matomo', $this->to_snake_case( get_class( $e ) ) );
231
  }
232
 
233
+ return new WP_Error( $code, $e->getMessage() );
234
  }
235
 
236
  return $result;
classes/WpMatomo/Access.php CHANGED
@@ -16,12 +16,12 @@ if ( ! defined( 'ABSPATH' ) ) {
16
  }
17
 
18
  class Access {
19
- public static $matomo_permissions = array(
20
  Capabilities::KEY_NONE => 'None',
21
  Capabilities::KEY_VIEW => 'View',
22
  Capabilities::KEY_WRITE => 'Write',
23
  Capabilities::KEY_ADMIN => 'Admin',
24
- );
25
 
26
  /**
27
  * @var Settings
@@ -47,7 +47,7 @@ class Access {
47
  $roles = new Roles( $this->settings );
48
  $available_roles = $roles->get_available_roles_for_configuration();
49
 
50
- $caps_to_store = array();
51
  foreach ( $values as $role => $matomo_permission ) {
52
  if ( isset( $available_roles[ $role ] ) &&
53
  $wp_roles->is_role( $role )
@@ -58,7 +58,7 @@ class Access {
58
 
59
  // we can't add the capabilities to the role directly using say $wp_roles->add_role cause it would not be
60
  // synced across sites when the plugin is network activated
61
- $this->settings->apply_changes( array( Settings::OPTION_KEY_CAPS_ACCESS => $caps_to_store ) );
62
 
63
  $sync = new Sync();
64
  $sync->sync_current_users();
@@ -70,5 +70,4 @@ class Access {
70
  wp_schedule_single_event( time() + 10, ScheduledTasks::EVENT_SYNC );
71
  }
72
  }
73
-
74
  }
16
  }
17
 
18
  class Access {
19
+ public static $matomo_permissions = [
20
  Capabilities::KEY_NONE => 'None',
21
  Capabilities::KEY_VIEW => 'View',
22
  Capabilities::KEY_WRITE => 'Write',
23
  Capabilities::KEY_ADMIN => 'Admin',
24
+ ];
25
 
26
  /**
27
  * @var Settings
47
  $roles = new Roles( $this->settings );
48
  $available_roles = $roles->get_available_roles_for_configuration();
49
 
50
+ $caps_to_store = [];
51
  foreach ( $values as $role => $matomo_permission ) {
52
  if ( isset( $available_roles[ $role ] ) &&
53
  $wp_roles->is_role( $role )
58
 
59
  // we can't add the capabilities to the role directly using say $wp_roles->add_role cause it would not be
60
  // synced across sites when the plugin is network activated
61
+ $this->settings->apply_changes( [ Settings::OPTION_KEY_CAPS_ACCESS => $caps_to_store ] );
62
 
63
  $sync = new Sync();
64
  $sync->sync_current_users();
70
  wp_schedule_single_event( time() + 10, ScheduledTasks::EVENT_SYNC );
71
  }
72
  }
 
73
  }
classes/WpMatomo/Admin/AccessSettings.php CHANGED
@@ -11,8 +11,8 @@ namespace WpMatomo\Admin;
11
 
12
  use WpMatomo\Access;
13
  use WpMatomo\Capabilities;
14
- use WpMatomo\Settings;
15
  use WpMatomo\Roles;
 
16
 
17
  if ( ! defined( 'ABSPATH' ) ) {
18
  exit; // if accessed directly
@@ -41,27 +41,27 @@ class AccessSettings implements AdminSettingsInterface {
41
  return esc_html__( 'Access', 'matomo' );
42
  }
43
 
 
 
 
 
 
 
 
 
 
44
  private function update_if_submitted() {
45
  if ( isset( $_POST )
46
  && ! empty( $_POST[ self::FORM_NAME ] )
47
  && is_admin()
48
  && check_admin_referer( self::NONCE_NAME )
49
  && current_user_can( Capabilities::KEY_SUPERUSER ) ) {
50
- $this->access->save( $_POST[ self::FORM_NAME ] );
 
51
 
52
  return true;
53
  }
54
 
55
  return false;
56
  }
57
-
58
- public function show_settings() {
59
- $this->update_if_submitted();
60
-
61
- $access = $this->access;
62
- $roles = new Roles( $this->settings );
63
- $capabilites = new Capabilities( $this->settings );
64
- include dirname( __FILE__ ) . '/views/access.php';
65
- }
66
-
67
  }
11
 
12
  use WpMatomo\Access;
13
  use WpMatomo\Capabilities;
 
14
  use WpMatomo\Roles;
15
+ use WpMatomo\Settings;
16
 
17
  if ( ! defined( 'ABSPATH' ) ) {
18
  exit; // if accessed directly
41
  return esc_html__( 'Access', 'matomo' );
42
  }
43
 
44
+ public function show_settings() {
45
+ $this->update_if_submitted();
46
+
47
+ $access = $this->access;
48
+ $roles = new Roles( $this->settings );
49
+ $capabilites = new Capabilities( $this->settings );
50
+ include dirname( __FILE__ ) . '/views/access.php';
51
+ }
52
+
53
  private function update_if_submitted() {
54
  if ( isset( $_POST )
55
  && ! empty( $_POST[ self::FORM_NAME ] )
56
  && is_admin()
57
  && check_admin_referer( self::NONCE_NAME )
58
  && current_user_can( Capabilities::KEY_SUPERUSER ) ) {
59
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
60
+ $this->access->save( wp_unslash( $_POST[ self::FORM_NAME ] ) );
61
 
62
  return true;
63
  }
64
 
65
  return false;
66
  }
 
 
 
 
 
 
 
 
 
 
67
  }
classes/WpMatomo/Admin/Admin.php CHANGED
@@ -20,16 +20,15 @@ class Admin {
20
  * @param Settings $settings
21
  */
22
  public function __construct( $settings, $init_menu = true ) {
23
- if ($init_menu) {
24
  new Menu( $settings );
25
  }
26
 
27
- add_action( 'admin_enqueue_scripts', array( $this, 'load_scripts' ) );
28
  }
29
 
30
  public function load_scripts() {
31
  wp_enqueue_style( 'matomo_admin_css', plugins_url( 'assets/css/admin-style.css', MATOMO_ANALYTICS_FILE ), false, '1.0.0' );
32
- wp_enqueue_script( 'matomo_admin_js', plugins_url( 'assets/js/admin.js', MATOMO_ANALYTICS_FILE ), array( 'jquery' ), '1.0', true );
33
  }
34
-
35
  }
20
  * @param Settings $settings
21
  */
22
  public function __construct( $settings, $init_menu = true ) {
23
+ if ( $init_menu ) {
24
  new Menu( $settings );
25
  }
26
 
27
+ add_action( 'admin_enqueue_scripts', [ $this, 'load_scripts' ] );
28
  }
29
 
30
  public function load_scripts() {
31
  wp_enqueue_style( 'matomo_admin_css', plugins_url( 'assets/css/admin-style.css', MATOMO_ANALYTICS_FILE ), false, '1.0.0' );
32
+ wp_enqueue_script( 'matomo_admin_js', plugins_url( 'assets/js/admin.js', MATOMO_ANALYTICS_FILE ), [ 'jquery' ], '1.0', true );
33
  }
 
34
  }
classes/WpMatomo/Admin/AdminSettings.php CHANGED
@@ -9,9 +9,6 @@
9
 
10
  namespace WpMatomo\Admin;
11
 
12
- use Piwik\Cache;
13
- use Piwik\Option;
14
- use Piwik\Plugins\SitesManager\API;
15
  use WpMatomo\Access;
16
  use WpMatomo\Settings;
17
 
@@ -20,12 +17,12 @@ if ( ! defined( 'ABSPATH' ) ) {
20
  }
21
 
22
  class AdminSettings {
23
- const TAB_TRACKING = 'tracking';
24
- const TAB_ACCESS = 'access';
25
  const TAB_EXCLUSIONS = 'exlusions';
26
- const TAB_PRIVACY = 'privacy';
27
  const TAB_GEOLOCATION = 'geolocation';
28
- const TAB_ADVANCED = 'advanced';
29
 
30
  /**
31
  * @var Settings
@@ -40,10 +37,10 @@ class AdminSettings {
40
  global $_parent_pages;
41
  $menu_slug = Menu::SLUG_SETTINGS;
42
 
43
- if (is_multisite() && is_network_admin()) {
44
- if ( isset( $_parent_pages[$menu_slug] ) ) {
45
- $parent_slug = $_parent_pages[$menu_slug];
46
- if ( $parent_slug && ! isset( $_parent_pages[$parent_slug] ) ) {
47
  $url = network_admin_url( add_query_arg( 'page', $menu_slug, $parent_slug ) );
48
  } else {
49
  $url = network_admin_url( 'admin.php?page=' . $menu_slug );
@@ -51,51 +48,52 @@ class AdminSettings {
51
  } else {
52
  $url = '';
53
  }
54
-
55
- $url = esc_url( $url );
56
  } else {
57
  $url = menu_page_url( $menu_slug, false );
58
  }
59
- return add_query_arg( array( 'tab' => $tab ), $url );
 
60
  }
61
 
62
  public function show() {
63
- $access = new Access( $this->settings );
64
  $access_settings = new AccessSettings( $access, $this->settings );
65
- $tracking = new TrackingSettings( $this->settings );
66
- $exclusions = new ExclusionSettings( $this->settings );
67
- $geolocation = new GeolocationSettings( $this->settings );
68
- $privacy = new PrivacySettings( $this->settings );
69
- $advanced = new AdvancedSettings( $this->settings );
70
- $setting_tabs = array(
71
- self::TAB_TRACKING => $tracking,
72
- self::TAB_ACCESS => $access_settings,
73
- self::TAB_PRIVACY => $privacy,
74
- self::TAB_EXCLUSIONS => $exclusions,
75
  self::TAB_GEOLOCATION => $geolocation,
76
- self::TAB_ADVANCED => $advanced,
77
- );
78
 
79
  $active_tab = self::TAB_TRACKING;
80
 
81
- if ($this->settings->is_network_enabled() && !is_network_admin()){
82
- $active_tab = self::TAB_EXCLUSIONS;
83
- $setting_tabs = array(
84
  self::TAB_EXCLUSIONS => $exclusions,
85
- self::TAB_PRIVACY => $privacy,
86
- );
87
  }
88
 
89
  $setting_tabs = apply_filters( 'matomo_setting_tabs', $setting_tabs, $this->settings );
90
 
91
- if ( ! empty( $_GET['tab'] ) && isset( $setting_tabs[ $_GET['tab'] ] ) ) {
92
- $active_tab = $_GET['tab'];
 
 
 
93
  }
94
 
95
- $content_tab = $setting_tabs[ $active_tab ];
96
  $matomo_settings = $this->settings;
97
 
98
  include dirname( __FILE__ ) . '/views/settings.php';
99
  }
100
-
101
  }
9
 
10
  namespace WpMatomo\Admin;
11
 
 
 
 
12
  use WpMatomo\Access;
13
  use WpMatomo\Settings;
14
 
17
  }
18
 
19
  class AdminSettings {
20
+ const TAB_TRACKING = 'tracking';
21
+ const TAB_ACCESS = 'access';
22
  const TAB_EXCLUSIONS = 'exlusions';
23
+ const TAB_PRIVACY = 'privacy';
24
  const TAB_GEOLOCATION = 'geolocation';
25
+ const TAB_ADVANCED = 'advanced';
26
 
27
  /**
28
  * @var Settings
37
  global $_parent_pages;
38
  $menu_slug = Menu::SLUG_SETTINGS;
39
 
40
+ if ( is_multisite() && is_network_admin() ) {
41
+ if ( isset( $_parent_pages[ $menu_slug ] ) ) {
42
+ $parent_slug = $_parent_pages[ $menu_slug ];
43
+ if ( $parent_slug && ! isset( $_parent_pages[ $parent_slug ] ) ) {
44
  $url = network_admin_url( add_query_arg( 'page', $menu_slug, $parent_slug ) );
45
  } else {
46
  $url = network_admin_url( 'admin.php?page=' . $menu_slug );
48
  } else {
49
  $url = '';
50
  }
 
 
51
  } else {
52
  $url = menu_page_url( $menu_slug, false );
53
  }
54
+
55
+ return add_query_arg( [ 'tab' => $tab ], $url );
56
  }
57
 
58
  public function show() {
59
+ $access = new Access( $this->settings );
60
  $access_settings = new AccessSettings( $access, $this->settings );
61
+ $tracking = new TrackingSettings( $this->settings );
62
+ $exclusions = new ExclusionSettings( $this->settings );
63
+ $geolocation = new GeolocationSettings( $this->settings );
64
+ $privacy = new PrivacySettings( $this->settings );
65
+ $advanced = new AdvancedSettings( $this->settings );
66
+ $setting_tabs = [
67
+ self::TAB_TRACKING => $tracking,
68
+ self::TAB_ACCESS => $access_settings,
69
+ self::TAB_PRIVACY => $privacy,
70
+ self::TAB_EXCLUSIONS => $exclusions,
71
  self::TAB_GEOLOCATION => $geolocation,
72
+ self::TAB_ADVANCED => $advanced,
73
+ ];
74
 
75
  $active_tab = self::TAB_TRACKING;
76
 
77
+ if ( $this->settings->is_network_enabled() && ! is_network_admin() ) {
78
+ $active_tab = self::TAB_EXCLUSIONS;
79
+ $setting_tabs = [
80
  self::TAB_EXCLUSIONS => $exclusions,
81
+ self::TAB_PRIVACY => $privacy,
82
+ ];
83
  }
84
 
85
  $setting_tabs = apply_filters( 'matomo_setting_tabs', $setting_tabs, $this->settings );
86
 
87
+ if ( ! empty( $_GET['tab'] ) ) {
88
+ $tab = sanitize_text_field( wp_unslash( $_GET['tab'] ) );
89
+ if ( isset( $setting_tabs[ $tab ] ) ) {
90
+ $active_tab = $tab;
91
+ }
92
  }
93
 
94
+ $content_tab = $setting_tabs[ $active_tab ];
95
  $matomo_settings = $this->settings;
96
 
97
  include dirname( __FILE__ ) . '/views/settings.php';
98
  }
 
99
  }
classes/WpMatomo/Admin/AdvancedSettings.php CHANGED
@@ -9,7 +9,6 @@
9
 
10
  namespace WpMatomo\Admin;
11
 
12
- use Piwik\Config;
13
  use Piwik\IP;
14
  use WpMatomo\Bootstrap;
15
  use WpMatomo\Capabilities;
@@ -19,12 +18,14 @@ use WpMatomo\Site\Sync\SyncConfig as SiteConfigSync;
19
  if ( ! defined( 'ABSPATH' ) ) {
20
  exit; // if accessed directly
21
  }
22
-
 
 
23
  class AdvancedSettings implements AdminSettingsInterface {
24
- const FORM_NAME = 'matomo';
25
- const NONCE_NAME = 'matomo_advanced';
26
 
27
- public static $valid_host_headers = array(
28
  'HTTP_CLIENT_IP',
29
  'HTTP_X_REAL_IP',
30
  'HTTP_X_FORWARDED_FOR',
@@ -34,7 +35,7 @@ class AdvancedSettings implements AdminSettingsInterface {
34
  'HTTP_CF_CONNECTING_IP',
35
  'HTTP_TRUE_CLIENT_IP',
36
  'HTTP_X_CLUSTER_CLIENT_IP',
37
- );
38
 
39
  /**
40
  * @var Settings
@@ -50,7 +51,7 @@ class AdvancedSettings implements AdminSettingsInterface {
50
  * @param Settings $settings
51
  */
52
  public function __construct( $settings ) {
53
- $this->settings = $settings;
54
  $this->site_config_sync = new SiteConfigSync( $settings );
55
  }
56
 
@@ -58,13 +59,27 @@ class AdvancedSettings implements AdminSettingsInterface {
58
  return esc_html__( 'Advanced', 'matomo' );
59
  }
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  private function update_if_submitted() {
62
  if ( isset( $_POST )
63
  && ! empty( $_POST[ self::FORM_NAME ] )
64
  && is_admin()
65
  && check_admin_referer( self::NONCE_NAME )
66
  && $this->can_user_manage() ) {
67
-
68
  $this->apply_settings();
69
 
70
  return true;
@@ -78,40 +93,24 @@ class AdvancedSettings implements AdminSettingsInterface {
78
  }
79
 
80
  private function apply_settings() {
81
- if (!defined('MATOMO_REMOVE_ALL_DATA')) {
82
- $this->settings->apply_changes(array(
83
- Settings::DELETE_ALL_DATA_ON_UNINSTALL => !empty($_POST['matomo']['delete_all_data'])
84
- ));
85
- }
86
-
87
- $client_headers = [];
88
- if (!empty($_POST[ self::FORM_NAME ]['proxy_client_header'])) {
89
- $client_header = $_POST[ self::FORM_NAME ]['proxy_client_header'];
90
- if (in_array($client_header, self::$valid_host_headers, true)) {
91
- $client_headers[] = $client_header;
92
- }
93
- }
94
-
95
- $this->site_config_sync->set_config_value('General', 'proxy_client_headers', $client_headers);
96
-
97
- return true;
98
- }
99
-
100
- public function show_settings() {
101
- $was_updated = $this->update_if_submitted();
102
 
103
- $matomo_client_headers = $this->site_config_sync->get_config_value('General', 'proxy_client_headers');
104
- if (empty($matomo_client_headers)) {
105
- $matomo_client_headers = array();
 
 
 
106
  }
107
 
108
- Bootstrap::do_bootstrap();
109
- $matomo_detected_ip = IP::getIpFromHeader();
110
- $matomo_delete_all_data = $this->settings->should_delete_all_data_on_uninstall();
111
 
112
- include dirname( __FILE__ ) . '/views/advanced_settings.php';
113
  }
114
-
115
-
116
-
117
  }
9
 
10
  namespace WpMatomo\Admin;
11
 
 
12
  use Piwik\IP;
13
  use WpMatomo\Bootstrap;
14
  use WpMatomo\Capabilities;
18
  if ( ! defined( 'ABSPATH' ) ) {
19
  exit; // if accessed directly
20
  }
21
+ /**
22
+ * phpcs:disable WordPress.Security.NonceVerification.Missing
23
+ */
24
  class AdvancedSettings implements AdminSettingsInterface {
25
+ const FORM_NAME = 'matomo';
26
+ const NONCE_NAME = 'matomo_advanced';
27
 
28
+ public static $valid_host_headers = [
29
  'HTTP_CLIENT_IP',
30
  'HTTP_X_REAL_IP',
31
  'HTTP_X_FORWARDED_FOR',
35
  'HTTP_CF_CONNECTING_IP',
36
  'HTTP_TRUE_CLIENT_IP',
37
  'HTTP_X_CLUSTER_CLIENT_IP',
38
+ ];
39
 
40
  /**
41
  * @var Settings
51
  * @param Settings $settings
52
  */
53
  public function __construct( $settings ) {
54
+ $this->settings = $settings;
55
  $this->site_config_sync = new SiteConfigSync( $settings );
56
  }
57
 
59
  return esc_html__( 'Advanced', 'matomo' );
60
  }
61
 
62
+ public function show_settings() {
63
+ $was_updated = $this->update_if_submitted();
64
+
65
+ $matomo_client_headers = $this->site_config_sync->get_config_value( 'General', 'proxy_client_headers' );
66
+ if ( empty( $matomo_client_headers ) ) {
67
+ $matomo_client_headers = [];
68
+ }
69
+
70
+ Bootstrap::do_bootstrap();
71
+ $matomo_detected_ip = IP::getIpFromHeader();
72
+ $matomo_delete_all_data = $this->settings->should_delete_all_data_on_uninstall();
73
+
74
+ include dirname( __FILE__ ) . '/views/advanced_settings.php';
75
+ }
76
+
77
  private function update_if_submitted() {
78
  if ( isset( $_POST )
79
  && ! empty( $_POST[ self::FORM_NAME ] )
80
  && is_admin()
81
  && check_admin_referer( self::NONCE_NAME )
82
  && $this->can_user_manage() ) {
 
83
  $this->apply_settings();
84
 
85
  return true;
93
  }
94
 
95
  private function apply_settings() {
96
+ if ( ! defined( 'MATOMO_REMOVE_ALL_DATA' ) ) {
97
+ $this->settings->apply_changes(
98
+ [
99
+ Settings::DELETE_ALL_DATA_ON_UNINSTALL => ! empty( $_POST['matomo']['delete_all_data'] ),
100
+ ]
101
+ );
102
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
+ $client_headers = [];
105
+ if ( ! empty( $_POST[ self::FORM_NAME ]['proxy_client_header'] ) ) {
106
+ $client_header = sanitize_text_field( wp_unslash( $_POST[ self::FORM_NAME ]['proxy_client_header'] ) );
107
+ if ( in_array( $client_header, self::$valid_host_headers, true ) ) {
108
+ $client_headers[] = $client_header;
109
+ }
110
  }
111
 
112
+ $this->site_config_sync->set_config_value( 'General', 'proxy_client_headers', $client_headers );
 
 
113
 
114
+ return true;
115
  }
 
 
 
116
  }
classes/WpMatomo/Admin/Chart.php ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @package matomo
4
+ */
5
+ namespace WpMatomo\Admin;
6
+
7
+ if ( ! defined( 'ABSPATH' ) ) {
8
+ exit; // if accessed directly
9
+ }
10
+
11
+ class Chart {
12
+ public function register_hooks() {
13
+ add_action( 'matomo_load_chartjs', [ $this, 'load_chartjs' ] );
14
+ }
15
+
16
+ public function load_chartjs() {
17
+ wp_enqueue_script( 'chart.js', plugins_url( 'node_modules/chart.js/dist/chart.min.js', MATOMO_ANALYTICS_FILE ), [], '1.0.0', true );
18
+ wp_enqueue_script( 'matomo_chart.js', plugins_url( 'assets/chart.js', MATOMO_ANALYTICS_FILE ), [], '1.0.0', true );
19
+ }
20
+ }
classes/WpMatomo/Admin/CookieConsent.php CHANGED
@@ -7,7 +7,6 @@ if ( ! defined( 'ABSPATH' ) ) {
7
  }
8
 
9
  class CookieConsent {
10
-
11
  const REQUIRE_COOKIE_CONSENT = 'cookie';
12
 
13
  const REQUIRE_TRACKING_CONSENT = 'tracking';
@@ -19,26 +18,26 @@ class CookieConsent {
19
  */
20
  public static function get_available_options() {
21
  return [
22
- self::REQUIRE_NONE => __( 'None', 'matomo' ),
23
- self::REQUIRE_COOKIE_CONSENT => __('Require cookie consent', 'matomo'),
24
- self::REQUIRE_TRACKING_CONSENT => __('Require tracking consent', 'matomo')
25
  ];
26
-
27
  }
 
28
  /**
29
  * @param string $tracking_mode
30
- * @see CookieConsent::REQUIRE_COOKIE_CONSENT
 
31
  * @see CookieConsent::REQUIRE_NONE
32
  * @see CookieConsent::REQUIRE_TRACKING_CONSENT
33
- * @return string
34
  */
35
  public function get_tracking_consent_option( $tracking_mode ) {
36
- switch( $tracking_mode ) {
37
  case self::REQUIRE_TRACKING_CONSENT:
38
  $tracking_code = <<<JAVASCRIPT
39
  _paq.push(['requireConsent']);
40
  JAVASCRIPT;
41
- ;
42
  break;
43
  case self::REQUIRE_COOKIE_CONSENT:
44
  $tracking_code = <<<JAVASCRIPT
@@ -47,8 +46,9 @@ JAVASCRIPT;
47
  break;
48
  case self::REQUIRE_NONE:
49
  default:
50
- $tracking_code = '';
51
  }
 
52
  return $tracking_code;
53
  }
54
  }
7
  }
8
 
9
  class CookieConsent {
 
10
  const REQUIRE_COOKIE_CONSENT = 'cookie';
11
 
12
  const REQUIRE_TRACKING_CONSENT = 'tracking';
18
  */
19
  public static function get_available_options() {
20
  return [
21
+ self::REQUIRE_NONE => __( 'None', 'matomo' ),
22
+ self::REQUIRE_COOKIE_CONSENT => __( 'Require cookie consent', 'matomo' ),
23
+ self::REQUIRE_TRACKING_CONSENT => __( 'Require tracking consent', 'matomo' ),
24
  ];
 
25
  }
26
+
27
  /**
28
  * @param string $tracking_mode
29
+ *
30
+ * @return string
31
  * @see CookieConsent::REQUIRE_NONE
32
  * @see CookieConsent::REQUIRE_TRACKING_CONSENT
33
+ * @see CookieConsent::REQUIRE_COOKIE_CONSENT
34
  */
35
  public function get_tracking_consent_option( $tracking_mode ) {
36
+ switch ( $tracking_mode ) {
37
  case self::REQUIRE_TRACKING_CONSENT:
38
  $tracking_code = <<<JAVASCRIPT
39
  _paq.push(['requireConsent']);
40
  JAVASCRIPT;
 
41
  break;
42
  case self::REQUIRE_COOKIE_CONSENT:
43
  $tracking_code = <<<JAVASCRIPT
46
  break;
47
  case self::REQUIRE_NONE:
48
  default:
49
+ $tracking_code = '';
50
  }
51
+
52
  return $tracking_code;
53
  }
54
  }
classes/WpMatomo/Admin/Dashboard.php CHANGED
@@ -9,6 +9,7 @@
9
 
10
  namespace WpMatomo\Admin;
11
 
 
12
  use WpMatomo\Capabilities;
13
  use WpMatomo\Logger;
14
  use WpMatomo\Report\Dates;
@@ -21,109 +22,120 @@ if ( ! defined( 'ABSPATH' ) ) {
21
  }
22
 
23
  class Dashboard {
24
-
25
  const DASHBOARD_USER_OPTION = 'matomo_dashboard_widgets';
26
 
27
  public function register_hooks() {
28
- add_action( 'wp_dashboard_setup', array( $this, 'add_dashboard_widgets' ) );
29
  }
30
 
31
- public function add_dashboard_widgets()
32
- {
33
  $widgets = $this->get_widgets();
34
- if (!empty($widgets) && is_array($widgets) && current_user_can(Capabilities::KEY_VIEW)) {
35
- foreach ($widgets as $widget) {
36
-
37
- try {
38
-
39
- $widget_meta = $this->is_valid_widget($widget['unique_id'], $widget['date']);
40
- if (!empty($widget_meta['report']['name'])) {
41
- $id = 'matomo_dashboard_widget_' . $widget['unique_id'] . '_' . $widget['date'];
42
-
43
- $title = $widget_meta['report']['name'] . ' - ' . $widget_meta['date'] . ' - Matomo';
44
- wp_add_dashboard_widget( $id, esc_html($title), function () use ($widget) {
45
- $renderer = new Renderer();
46
- echo $renderer->show_report(array(
47
- 'unique_id' => $widget['unique_id'],
48
- 'report_date' => $widget['date'],
49
- 'limit' => 10,
50
- ));
51
- });
52
- }
53
- } catch (\Exception $e) {
54
- // dont want to break dashboard if there is any issue with matomo ... eg in case bootstrap fails
55
- // or is reinstalled but matomo not yet fully installed etc
56
- $logger = new Logger();
57
- $logger->log(sprintf('Failed to add Matomo widget %s to dashboard: %s', wp_json_encode($widget), $e->getMessage()));
58
- }
 
 
 
 
 
 
 
 
 
59
  }
60
  }
61
  }
62
 
63
- public function is_valid_widget( $unique_id, $date )
64
- {
65
- if (empty($unique_id) || empty($date)) {
 
 
 
 
 
 
 
 
66
  return false;
67
  }
68
 
69
  $metadata = new Metadata();
70
- $report = $metadata->find_report_by_unique_id( $unique_id );
71
 
72
- if (empty($report)) {
73
  return false;
74
  }
75
 
76
  $report_dates_obj = new Dates();
77
  $report_dates = $report_dates_obj->get_supported_dates();
78
 
79
- if (empty($report_dates[$date])) {
80
  return false;
81
  }
82
 
83
- return array('report' => $report, 'date' => $report_dates[$date]);
 
 
 
84
  }
85
 
86
- public function has_widget($report_unique_id, $report_date)
87
- {
88
  $widgets = $this->get_widgets();
89
- foreach ($widgets as $index => $widget) {
90
- if ($widget['unique_id'] === $report_unique_id && $widget['date'] === $report_date) {
91
  return true;
92
  }
93
  }
 
94
  return false;
95
  }
96
 
97
- public function toggle_widget($report_unique_id, $report_date)
98
- {
99
  $widgets = $this->get_widgets();
100
- foreach ($widgets as $index => $widget) {
101
- if ($widget['unique_id'] === $report_unique_id && $widget['date'] === $report_date) {
102
- unset($widgets[$index]);
103
- $this->set_widgets(array_values($widgets));
 
104
  return;
105
  }
106
  }
107
- $widgets[] = array('unique_id' => $report_unique_id, 'date' => $report_date);
108
-
109
- $this->set_widgets($widgets);
110
- }
111
 
112
- public function get_widgets()
113
- {
114
- $meta = get_user_meta(get_current_user_id(), self::DASHBOARD_USER_OPTION, true);
115
- if (empty($meta)) {
116
- $meta = array();
117
- }
118
- return $meta;
119
  }
120
 
121
- private function set_widgets($widgets)
122
- {
123
- update_user_meta(get_current_user_id(),self::DASHBOARD_USER_OPTION, $widgets);
124
  }
125
 
126
  public function uninstall() {
127
- Uninstaller::uninstall_user_meta(self::DASHBOARD_USER_OPTION);
128
  }
129
  }
9
 
10
  namespace WpMatomo\Admin;
11
 
12
+ use Exception;
13
  use WpMatomo\Capabilities;
14
  use WpMatomo\Logger;
15
  use WpMatomo\Report\Dates;
22
  }
23
 
24
  class Dashboard {
 
25
  const DASHBOARD_USER_OPTION = 'matomo_dashboard_widgets';
26
 
27
  public function register_hooks() {
28
+ add_action( 'wp_dashboard_setup', [ $this, 'add_dashboard_widgets' ] );
29
  }
30
 
31
+ public function add_dashboard_widgets() {
 
32
  $widgets = $this->get_widgets();
33
+ if ( ! empty( $widgets ) && is_array( $widgets ) && current_user_can( Capabilities::KEY_VIEW ) ) {
34
+ do_action( 'matomo_load_chartjs' );
35
+ foreach ( $widgets as $widget ) {
36
+ try {
37
+ $widget_meta = $this->is_valid_widget( $widget['unique_id'], $widget['date'] );
38
+ if ( ! empty( $widget_meta['report']['name'] ) ) {
39
+ $id = 'matomo_dashboard_widget_' . $widget['unique_id'] . '_' . $widget['date'];
40
+
41
+ $title = $widget_meta['report']['name'] . ' - ' . $widget_meta['date'] . ' - Matomo';
42
+
43
+ wp_add_dashboard_widget(
44
+ $id,
45
+ esc_html( $title ),
46
+ function () use ( $widget ) {
47
+ $renderer = new Renderer();
48
+ // do not escape the content, we want the HTML
49
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
50
+ echo $renderer->show_report(
51
+ [
52
+ 'unique_id' => $widget['unique_id'],
53
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
54
+ 'report_date' => $widget['date'],
55
+ 'limit' => 10,
56
+ ]
57
+ );
58
+ }
59
+ );
60
+ }
61
+ } catch ( Exception $e ) {
62
+ // dont want to break dashboard if there is any issue with matomo ... eg in case bootstrap fails
63
+ // or is reinstalled but matomo not yet fully installed etc
64
+ $logger = new Logger();
65
+ $logger->log( sprintf( 'Failed to add Matomo widget %s to dashboard: %s', wp_json_encode( $widget ), $e->getMessage() ) );
66
+ }
67
  }
68
  }
69
  }
70
 
71
+ public function get_widgets() {
72
+ $meta = get_user_meta( get_current_user_id(), self::DASHBOARD_USER_OPTION, true );
73
+ if ( empty( $meta ) ) {
74
+ $meta = [];
75
+ }
76
+
77
+ return $meta;
78
+ }
79
+
80
+ public function is_valid_widget( $unique_id, $date ) {
81
+ if ( empty( $unique_id ) || empty( $date ) ) {
82
  return false;
83
  }
84
 
85
  $metadata = new Metadata();
86
+ $report = $metadata->find_report_by_unique_id( $unique_id );
87
 
88
+ if ( empty( $report ) ) {
89
  return false;
90
  }
91
 
92
  $report_dates_obj = new Dates();
93
  $report_dates = $report_dates_obj->get_supported_dates();
94
 
95
+ if ( empty( $report_dates[ $date ] ) ) {
96
  return false;
97
  }
98
 
99
+ return [
100
+ 'report' => $report,
101
+ 'date' => $report_dates[ $date ],
102
+ ];
103
  }
104
 
105
+ public function has_widget( $report_unique_id, $report_date ) {
 
106
  $widgets = $this->get_widgets();
107
+ foreach ( $widgets as $index => $widget ) {
108
+ if ( $widget['unique_id'] === $report_unique_id && $widget['date'] === $report_date ) {
109
  return true;
110
  }
111
  }
112
+
113
  return false;
114
  }
115
 
116
+ public function toggle_widget( $report_unique_id, $report_date ) {
 
117
  $widgets = $this->get_widgets();
118
+ foreach ( $widgets as $index => $widget ) {
119
+ if ( $widget['unique_id'] === $report_unique_id && $widget['date'] === $report_date ) {
120
+ unset( $widgets[ $index ] );
121
+ $this->set_widgets( array_values( $widgets ) );
122
+
123
  return;
124
  }
125
  }
126
+ $widgets[] = [
127
+ 'unique_id' => $report_unique_id,
128
+ 'date' => $report_date,
129
+ ];
130
 
131
+ $this->set_widgets( $widgets );
 
 
 
 
 
 
132
  }
133
 
134
+ private function set_widgets( $widgets ) {
135
+ update_user_meta( get_current_user_id(), self::DASHBOARD_USER_OPTION, $widgets );
 
136
  }
137
 
138
  public function uninstall() {
139
+ Uninstaller::uninstall_user_meta( self::DASHBOARD_USER_OPTION );
140
  }
141
  }
classes/WpMatomo/Admin/ExclusionSettings.php CHANGED
@@ -36,6 +36,24 @@ class ExclusionSettings implements AdminSettingsInterface {
36
  return esc_html__( 'Exclusions', 'matomo' );
37
  }
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  private function update_if_submitted() {
40
  if ( isset( $_POST )
41
  && ! empty( $_POST[ self::FORM_NAME ] )
@@ -43,8 +61,8 @@ class ExclusionSettings implements AdminSettingsInterface {
43
  && check_admin_referer( self::NONCE_NAME )
44
  && current_user_can( Capabilities::KEY_SUPERUSER ) ) {
45
  Bootstrap::do_bootstrap();
46
-
47
- $post = $_POST[ self::FORM_NAME ];
48
 
49
  $api = API::getInstance();
50
  if ( isset( $post['excluded_ips'] ) ) {
@@ -69,11 +87,12 @@ class ExclusionSettings implements AdminSettingsInterface {
69
  }
70
 
71
  $keep_fragments = ! empty( $post['keep_url_fragments'] );
 
72
  if ( $keep_fragments != $api->getKeepURLFragmentsGlobal() ) {
73
  $api->setKeepURLFragmentsGlobal( $keep_fragments );
74
  }
75
 
76
- $setting_values = array( Settings::OPTION_KEY_STEALTH => array() );
77
  if ( ! empty( $post[ Settings::OPTION_KEY_STEALTH ] ) ) {
78
  $setting_values[ Settings::OPTION_KEY_STEALTH ] = $post[ Settings::OPTION_KEY_STEALTH ];
79
  }
@@ -104,24 +123,12 @@ class ExclusionSettings implements AdminSettingsInterface {
104
  return implode( "\n", array_filter( explode( ',', $value ) ) );
105
  }
106
 
107
- public function show_settings() {
108
- global $wp_roles;
109
-
110
- $was_updated = $this->update_if_submitted();
111
-
112
- Bootstrap::do_bootstrap();
113
-
114
- $api = API::getInstance();
115
- $excluded_ips = $this->from_comma_list( $api->getExcludedIpsGlobal() );
116
- $excluded_query_params = $this->from_comma_list( $api->getExcludedQueryParametersGlobal() );
117
- $excluded_user_agents = $this->from_comma_list( $api->getExcludedUserAgentsGlobal() );
118
- $keep_url_fragments = $api->getKeepURLFragmentsGlobal();
119
- $current_ip = $this->get_current_ip();
120
- $settings = $this->settings;
121
-
122
- include dirname( __FILE__ ) . '/views/exclusion_settings.php';
123
- }
124
-
125
  private function get_current_ip() {
126
  if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
127
  $ip = $_SERVER['HTTP_CLIENT_IP'];
@@ -133,5 +140,4 @@ class ExclusionSettings implements AdminSettingsInterface {
133
 
134
  return $ip;
135
  }
136
-
137
  }
36
  return esc_html__( 'Exclusions', 'matomo' );
37
  }
38
 
39
+ public function show_settings() {
40
+ global $wp_roles;
41
+
42
+ $was_updated = $this->update_if_submitted();
43
+
44
+ Bootstrap::do_bootstrap();
45
+
46
+ $api = API::getInstance();
47
+ $excluded_ips = $this->from_comma_list( $api->getExcludedIpsGlobal() );
48
+ $excluded_query_params = $this->from_comma_list( $api->getExcludedQueryParametersGlobal() );
49
+ $excluded_user_agents = $this->from_comma_list( $api->getExcludedUserAgentsGlobal() );
50
+ $keep_url_fragments = $api->getKeepURLFragmentsGlobal();
51
+ $current_ip = $this->get_current_ip();
52
+ $settings = $this->settings;
53
+
54
+ include dirname( __FILE__ ) . '/views/exclusion_settings.php';
55
+ }
56
+
57
  private function update_if_submitted() {
58
  if ( isset( $_POST )
59
  && ! empty( $_POST[ self::FORM_NAME ] )
61
  && check_admin_referer( self::NONCE_NAME )
62
  && current_user_can( Capabilities::KEY_SUPERUSER ) ) {
63
  Bootstrap::do_bootstrap();
64
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
65
+ $post = wp_unslash( $_POST[ self::FORM_NAME ] );
66
 
67
  $api = API::getInstance();
68
  if ( isset( $post['excluded_ips'] ) ) {
87
  }
88
 
89
  $keep_fragments = ! empty( $post['keep_url_fragments'] );
90
+ // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
91
  if ( $keep_fragments != $api->getKeepURLFragmentsGlobal() ) {
92
  $api->setKeepURLFragmentsGlobal( $keep_fragments );
93
  }
94
 
95
+ $setting_values = [ Settings::OPTION_KEY_STEALTH => [] ];
96
  if ( ! empty( $post[ Settings::OPTION_KEY_STEALTH ] ) ) {
97
  $setting_values[ Settings::OPTION_KEY_STEALTH ] = $post[ Settings::OPTION_KEY_STEALTH ];
98
  }
123
  return implode( "\n", array_filter( explode( ',', $value ) ) );
124
  }
125
 
126
+ /**
127
+ * do not sanitize $_SERVER variables
128
+ * phpcs:disable WordPress.Security.ValidatedSanitizedInput
129
+ *
130
+ * @return mixed|string
131
+ */
 
 
 
 
 
 
 
 
 
 
 
 
132
  private function get_current_ip() {
133
  if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
134
  $ip = $_SERVER['HTTP_CLIENT_IP'];
140
 
141
  return $ip;
142
  }
 
143
  }
classes/WpMatomo/Admin/GeolocationSettings.php CHANGED
@@ -19,7 +19,7 @@ if ( ! defined( 'ABSPATH' ) ) {
19
 
20
  class GeolocationSettings implements AdminSettingsInterface {
21
  const NONCE_NAME = 'matomo_geolocation';
22
- const FORM_NAME = 'matomo_maxmind_license';
23
 
24
  /**
25
  * @var Settings
@@ -34,24 +34,33 @@ class GeolocationSettings implements AdminSettingsInterface {
34
  return esc_html__( 'Geolocation', 'matomo' );
35
  }
36
 
 
 
 
 
 
 
 
 
37
  private function update_if_submitted() {
38
  if ( isset( $_POST )
39
  && isset( $_POST[ self::FORM_NAME ] )
40
  && is_admin()
41
  && check_admin_referer( self::NONCE_NAME )
42
  && current_user_can( Capabilities::KEY_SUPERUSER ) ) {
 
43
 
44
- $maxmind_license = trim(stripslashes($_POST[ self::FORM_NAME ]));
45
-
46
- if (empty($maxmind_license)) {
47
  $maxmind_license = '';
48
- } elseif (strlen($maxmind_license) > 20 || strlen($maxmind_license) < 7 || !ctype_graph($maxmind_license)) {
49
  return false;
50
  }
51
 
52
- $this->settings->apply_changes(array(
53
- 'maxmind_license_key' => $maxmind_license
54
- ));
 
 
55
 
56
  // update geoip in the backgronud
57
  wp_schedule_single_event( time() + 10, ScheduledTasks::EVENT_GEOIP );
@@ -59,13 +68,4 @@ class GeolocationSettings implements AdminSettingsInterface {
59
  return true;
60
  }
61
  }
62
-
63
- public function show_settings() {
64
- $invalid_format = $this->update_if_submitted() === false;
65
-
66
- $current_maxmind_license = $this->settings->get_global_option('maxmind_license_key');
67
-
68
- include dirname( __FILE__ ) . '/views/geolocation_settings.php';
69
- }
70
-
71
  }
19
 
20
  class GeolocationSettings implements AdminSettingsInterface {
21
  const NONCE_NAME = 'matomo_geolocation';
22
+ const FORM_NAME = 'matomo_maxmind_license';
23
 
24
  /**
25
  * @var Settings
34
  return esc_html__( 'Geolocation', 'matomo' );
35
  }
36
 
37
+ public function show_settings() {
38
+ $invalid_format = $this->update_if_submitted() === false;
39
+
40
+ $current_maxmind_license = $this->settings->get_global_option( 'maxmind_license_key' );
41
+
42
+ include dirname( __FILE__ ) . '/views/geolocation_settings.php';
43
+ }
44
+
45
  private function update_if_submitted() {
46
  if ( isset( $_POST )
47
  && isset( $_POST[ self::FORM_NAME ] )
48
  && is_admin()
49
  && check_admin_referer( self::NONCE_NAME )
50
  && current_user_can( Capabilities::KEY_SUPERUSER ) ) {
51
+ $maxmind_license = trim( stripslashes( sanitize_text_field( wp_unslash( $_POST[ self::FORM_NAME ] ) ) ) );
52
 
53
+ if ( empty( $maxmind_license ) ) {
 
 
54
  $maxmind_license = '';
55
+ } elseif ( strlen( $maxmind_license ) > 20 || strlen( $maxmind_license ) < 7 || ! ctype_graph( $maxmind_license ) ) {
56
  return false;
57
  }
58
 
59
+ $this->settings->apply_changes(
60
+ [
61
+ 'maxmind_license_key' => $maxmind_license,
62
+ ]
63
+ );
64
 
65
  // update geoip in the backgronud
66
  wp_schedule_single_event( time() + 10, ScheduledTasks::EVENT_GEOIP );
68
  return true;
69
  }
70
  }
 
 
 
 
 
 
 
 
 
71
  }
classes/WpMatomo/Admin/GetStarted.php CHANGED
@@ -31,6 +31,15 @@ class GetStarted {
31
  $this->settings = $settings;
32
  }
33
 
 
 
 
 
 
 
 
 
 
34
  private function update_if_submitted() {
35
  if ( isset( $_POST )
36
  && ! empty( $_POST[ self::FORM_NAME ] )
@@ -40,16 +49,16 @@ class GetStarted {
40
  if ( ! empty( $_POST[ self::FORM_NAME ][ Settings::SHOW_GET_STARTED_PAGE ] )
41
  && 'no' === $_POST[ self::FORM_NAME ][ Settings::SHOW_GET_STARTED_PAGE ] ) {
42
  $this->settings->apply_changes(
43
- array(
44
  Settings::SHOW_GET_STARTED_PAGE => 0,
45
- )
46
  );
47
 
48
  return true;
49
  }
50
  if ( ! empty( $_POST[ self::FORM_NAME ]['track_mode'] )
51
  && TrackingSettings::TRACK_MODE_DEFAULT === $_POST[ self::FORM_NAME ]['track_mode'] ) {
52
- $this->settings->apply_tracking_related_changes( array( 'track_mode' => TrackingSettings::TRACK_MODE_DEFAULT ) );
53
 
54
  return true;
55
  }
@@ -63,15 +72,4 @@ class GetStarted {
63
 
64
  return $tracking_settings->can_user_manage();
65
  }
66
-
67
- public function show() {
68
- $was_updated = $this->update_if_submitted();
69
- $settings = $this->settings;
70
- $can_user_edit = $this->can_user_manage();
71
- $show_this_page = $this->settings->get_global_option( Settings::SHOW_GET_STARTED_PAGE );
72
-
73
- include dirname( __FILE__ ) . '/views/get_started.php';
74
- }
75
-
76
-
77
  }
31
  $this->settings = $settings;
32
  }
33
 
34
+ public function show() {
35
+ $was_updated = $this->update_if_submitted();
36
+ $settings = $this->settings;
37
+ $can_user_edit = $this->can_user_manage();
38
+ $show_this_page = $this->settings->get_global_option( Settings::SHOW_GET_STARTED_PAGE );
39
+
40
+ include dirname( __FILE__ ) . '/views/get_started.php';
41
+ }
42
+
43
  private function update_if_submitted() {
44
  if ( isset( $_POST )
45
  && ! empty( $_POST[ self::FORM_NAME ] )
49
  if ( ! empty( $_POST[ self::FORM_NAME ][ Settings::SHOW_GET_STARTED_PAGE ] )
50
  && 'no' === $_POST[ self::FORM_NAME ][ Settings::SHOW_GET_STARTED_PAGE ] ) {
51
  $this->settings->apply_changes(
52
+ [
53
  Settings::SHOW_GET_STARTED_PAGE => 0,
54
+ ]
55
  );
56
 
57
  return true;
58
  }
59
  if ( ! empty( $_POST[ self::FORM_NAME ]['track_mode'] )
60
  && TrackingSettings::TRACK_MODE_DEFAULT === $_POST[ self::FORM_NAME ]['track_mode'] ) {
61
+ $this->settings->apply_tracking_related_changes( [ 'track_mode' => TrackingSettings::TRACK_MODE_DEFAULT ] );
62
 
63
  return true;
64
  }
72
 
73
  return $tracking_settings->can_user_manage();
74
  }
 
 
 
 
 
 
 
 
 
 
 
75
  }
classes/WpMatomo/Admin/Info.php CHANGED
@@ -21,48 +21,50 @@ class Info {
21
 
22
  private function update_if_submitted() {
23
  if ( isset( $_POST )
24
- && !empty( $_POST[ self::FORM_NAME ] )
25
- && is_admin()
26
- && check_admin_referer( self::NONCE_NAME )
27
- && $this->show_newsletter_signup()
28
- && current_user_can( Capabilities::KEY_VIEW ) ) {
29
-
30
- $user = wp_get_current_user();
31
- $locale = explode('_', get_user_locale($user->ID));
32
- wp_remote_get('https://api.matomo.org/1.0/subscribeNewsletter/?' . http_build_query(array(
33
- 'email' => $user->user_email,
34
- 'wordpress' => 1,
35
- 'language' => $locale[0],
36
- )));
37
- update_user_meta($user->ID, self::FORM_NAME, '1');
 
 
 
38
 
39
  return true;
40
  }
41
  }
42
 
43
  private function show_newsletter_signup() {
44
- if (!is_user_logged_in()) {
45
  return false;
46
  }
47
 
48
  $user = wp_get_current_user();
49
- return !get_user_meta($user->ID, self::FORM_NAME, true);
 
50
  }
51
 
52
  public function show() {
53
- $this->render('info');
54
  }
55
 
56
  public function show_multisite() {
57
- $this->render('info_multisite');
58
  }
59
 
60
- private function render($template) {
61
  $signedup_newsletter = $this->update_if_submitted();
62
  $show_newsletter = $this->show_newsletter_signup();
63
 
64
  include dirname( __FILE__ ) . '/views/' . $template . '.php';
65
  }
66
-
67
-
68
  }
21
 
22
  private function update_if_submitted() {
23
  if ( isset( $_POST )
24
+ && ! empty( $_POST[ self::FORM_NAME ] )
25
+ && is_admin()
26
+ && check_admin_referer( self::NONCE_NAME )
27
+ && $this->show_newsletter_signup()
28
+ && current_user_can( Capabilities::KEY_VIEW ) ) {
29
+ $user = wp_get_current_user();
30
+ $locale = explode( '_', get_user_locale( $user->ID ) );
31
+ wp_remote_get(
32
+ 'https://api.matomo.org/1.0/subscribeNewsletter/?' . http_build_query(
33
+ [
34
+ 'email' => $user->user_email,
35
+ 'wordpress' => 1,
36
+ 'language' => $locale[0],
37
+ ]
38
+ )
39
+ );
40
+ update_user_meta( $user->ID, self::FORM_NAME, '1' );
41
 
42
  return true;
43
  }
44
  }
45
 
46
  private function show_newsletter_signup() {
47
+ if ( ! is_user_logged_in() ) {
48
  return false;
49
  }
50
 
51
  $user = wp_get_current_user();
52
+
53
+ return ! get_user_meta( $user->ID, self::FORM_NAME, true );
54
  }
55
 
56
  public function show() {
57
+ $this->render( 'info' );
58
  }
59
 
60
  public function show_multisite() {
61
+ $this->render( 'info_multisite' );
62
  }
63
 
64
+ private function render( $template ) {
65
  $signedup_newsletter = $this->update_if_submitted();
66
  $show_newsletter = $this->show_newsletter_signup();
67
 
68
  include dirname( __FILE__ ) . '/views/' . $template . '.php';
69
  }
 
 
70
  }
classes/WpMatomo/Admin/Marketplace.php CHANGED
@@ -31,5 +31,4 @@ class Marketplace {
31
 
32
  include dirname( __FILE__ ) . '/views/marketplace.php';
33
  }
34
-
35
  }
31
 
32
  include dirname( __FILE__ ) . '/views/marketplace.php';
33
  }
 
34
  }
classes/WpMatomo/Admin/Menu.php CHANGED
@@ -52,15 +52,15 @@ class Menu {
52
  public function __construct( $settings ) {
53
  $this->settings = $settings;
54
  // Hook for adding admin menus
55
- add_action( 'admin_menu', array( $this, 'add_menu' ) );
56
- add_action( 'network_admin_menu', array( $this, 'add_menu' ) );
57
- add_action( 'admin_head', array( $this, 'menu_external_icons' ) );
58
 
59
  // as we are redirecting we need to perform the redirect as soon as possible before WP has eg echoed the header
60
- add_action( 'load-matomo-analytics_page_' . self::SLUG_REPORTING, array( $this, 'reporting' ) );
61
- add_action( 'load-' . self::$parent_slug . '_page_' . self::SLUG_REPORTING, array( $this, 'reporting' ) );
62
- add_action( 'load-matomo-analytics_page_' . self::SLUG_TAGMANAGER, array( $this, 'tagmanager' ) );
63
- add_action( 'load-' . self::$parent_slug . '_page_' . self::SLUG_TAGMANAGER, array( $this, 'tagmanager' ) );
64
  }
65
 
66
  public function add_menu() {
@@ -75,19 +75,19 @@ class Menu {
75
  add_menu_page( 'Matomo Analytics', 'Matomo Analytics', self::CAP_NOT_EXISTS, 'matomo', null, 'dashicons-analytics' );
76
 
77
  if ( $this->settings->get_global_option( Settings::SHOW_GET_STARTED_PAGE ) && $get_started->can_user_manage() ) {
78
- if (!is_multisite() || !is_network_admin()) {
79
- add_submenu_page(
80
- self::$parent_slug,
81
- __( 'Get Started', 'matomo' ),
82
- __( 'Get Started', 'matomo' ),
83
- Capabilities::KEY_SUPERUSER,
84
- self::SLUG_GET_STARTED,
85
- array(
86
- $get_started,
87
- 'show',
88
- )
89
- );
90
- }
91
  }
92
 
93
  if ( is_network_admin() ) {
@@ -97,10 +97,10 @@ class Menu {
97
  __( 'Multi Site', 'matomo' ),
98
  Capabilities::KEY_SUPERUSER,
99
  'matomo-multisite',
100
- array(
101
  $info,
102
  'show_multisite',
103
- )
104
  );
105
  } else {
106
  add_submenu_page(
@@ -109,10 +109,10 @@ class Menu {
109
  __( 'Summary', 'matomo' ),
110
  Capabilities::KEY_VIEW,
111
  self::SLUG_REPORT_SUMMARY,
112
- array(
113
  $summary,
114
  'show',
115
- )
116
  );
117
 
118
  // the network itself is not a blog
@@ -122,30 +122,29 @@ class Menu {
122
  __( 'Reporting', 'matomo' ),
123
  Capabilities::KEY_VIEW,
124
  self::SLUG_REPORTING,
125
- array(
126
  $this,
127
  'reporting',
128
- )
129
  );
130
  // the network itself is not a blog
131
- if ( matomo_has_tag_manager() ) {
132
- add_submenu_page(
133
- self::$parent_slug,
134
- __( 'Tag Manager', 'matomo' ),
135
- __( 'Tag Manager', 'matomo' ),
136
- Capabilities::KEY_WRITE,
137
- self::SLUG_TAGMANAGER,
138
- array(
139
- $this,
140
- 'tagmanager',
141
- )
142
- );
143
- }
144
-
145
  }
146
 
147
- // we always show settings except when multi site is used, plugin is not network enabled, and we are in network admin
148
- $can_matomo_be_managed = ( !is_multisite() || $this->settings->is_network_enabled() || !is_network_admin() );
149
 
150
  if ( $can_matomo_be_managed ) {
151
  add_submenu_page(
@@ -154,10 +153,10 @@ class Menu {
154
  __( 'Settings', 'matomo' ),
155
  Capabilities::KEY_SUPERUSER,
156
  self::SLUG_SETTINGS,
157
- array(
158
  $admin_settings,
159
  'show',
160
- )
161
  );
162
  }
163
 
@@ -168,10 +167,10 @@ class Menu {
168
  __( 'Marketplace', 'matomo' ),
169
  Capabilities::KEY_VIEW,
170
  self::SLUG_MARKETPLACE,
171
- array(
172
  $marketplace,
173
  'show',
174
- )
175
  );
176
  }
177
 
@@ -182,10 +181,10 @@ class Menu {
182
  __( 'Diagnostics', 'matomo' ),
183
  Capabilities::KEY_SUPERUSER,
184
  self::SLUG_SYSTEM_REPORT,
185
- array(
186
  $system_report,
187
  'show',
188
- )
189
  );
190
  }
191
 
@@ -195,10 +194,10 @@ class Menu {
195
  __( 'About', 'matomo' ),
196
  Capabilities::KEY_VIEW,
197
  self::SLUG_ABOUT,
198
- array(
199
  $info,
200
  'show',
201
- )
202
  );
203
  }
204
 
@@ -210,6 +209,8 @@ class Menu {
210
  $tagmanager = __( 'Tag Manager', 'matomo' );
211
  foreach ( $submenu[ self::$parent_slug ] as $key => $menu_item ) {
212
  if ( 0 === strpos( $menu_item[0], $reporting ) || 0 === strpos( $menu_item[0], $tagmanager ) ) {
 
 
213
  $submenu[ self::$parent_slug ][ $key ][0] .= ' <span class="dashicons-before dashicons-external"></span>';
214
  }
215
  }
@@ -217,7 +218,7 @@ class Menu {
217
  }
218
 
219
  public static function get_matomo_goto_url( $goto ) {
220
- return add_query_arg( array( 'goto' => $goto ), menu_page_url( self::SLUG_REPORTING, false ) );
221
  }
222
 
223
  public static function get_reporting_url() {
@@ -233,7 +234,7 @@ class Menu {
233
 
234
  public function reporting() {
235
  if ( ! empty( $_GET['goto'] ) ) {
236
- switch ( $_GET['goto'] ) {
237
  case self::REPORTING_GOTO_ADMIN:
238
  $this->go_to_matomo_page( 'CoreAdminHome', 'home', Capabilities::KEY_SUPERUSER );
239
  break;
@@ -264,26 +265,26 @@ class Menu {
264
  $idsite = $site->get_current_matomo_site_id();
265
 
266
  if ( $idsite ) {
267
- $url = add_query_arg( array( 'idSite' => (int) $idsite ), $url );
268
  }
269
 
270
  if ( ! empty( $_GET['report_date'] ) ) {
271
- $url = add_query_arg(
272
- array(
 
273
  'module' => 'CoreHome',
274
  'action' => 'index',
275
- ),
276
  $url
277
  );
278
 
279
-
280
  $date = new Dates();
281
- list( $period, $date ) = $date->detect_period_and_date( $_GET['report_date'] );
282
  $url = add_query_arg(
283
- array(
284
  'period' => $period,
285
  'date' => $date,
286
- ),
287
  $url
288
  );
289
  }
@@ -295,7 +296,7 @@ class Menu {
295
  /**
296
  * @api
297
  */
298
- public static function get_matomo_reporting_url( $category, $subcategory, $params = array() ) {
299
  $site = new Site();
300
  $idsite = $site->get_current_matomo_site_id();
301
 
@@ -330,7 +331,7 @@ class Menu {
330
  /**
331
  * @api
332
  */
333
- public static function get_matomo_action_url( $module, $action, $params = array() ) {
334
  $site = new Site();
335
  $idsite = $site->get_current_matomo_site_id();
336
 
@@ -372,5 +373,4 @@ class Menu {
372
  wp_safe_redirect( $url );
373
  exit;
374
  }
375
-
376
  }
52
  public function __construct( $settings ) {
53
  $this->settings = $settings;
54
  // Hook for adding admin menus
55
+ add_action( 'admin_menu', [ $this, 'add_menu' ] );
56
+ add_action( 'network_admin_menu', [ $this, 'add_menu' ] );
57
+ add_action( 'admin_head', [ $this, 'menu_external_icons' ] );
58
 
59
  // as we are redirecting we need to perform the redirect as soon as possible before WP has eg echoed the header
60
+ add_action( 'load-matomo-analytics_page_' . self::SLUG_REPORTING, [ $this, 'reporting' ] );
61
+ add_action( 'load-' . self::$parent_slug . '_page_' . self::SLUG_REPORTING, [ $this, 'reporting' ] );
62
+ add_action( 'load-matomo-analytics_page_' . self::SLUG_TAGMANAGER, [ $this, 'tagmanager' ] );
63
+ add_action( 'load-' . self::$parent_slug . '_page_' . self::SLUG_TAGMANAGER, [ $this, 'tagmanager' ] );
64
  }
65
 
66
  public function add_menu() {
75
  add_menu_page( 'Matomo Analytics', 'Matomo Analytics', self::CAP_NOT_EXISTS, 'matomo', null, 'dashicons-analytics' );
76
 
77
  if ( $this->settings->get_global_option( Settings::SHOW_GET_STARTED_PAGE ) && $get_started->can_user_manage() ) {
78
+ if ( ! is_multisite() || ! is_network_admin() ) {
79
+ add_submenu_page(
80
+ self::$parent_slug,
81
+ __( 'Get Started', 'matomo' ),
82
+ __( 'Get Started', 'matomo' ),
83
+ Capabilities::KEY_SUPERUSER,
84
+ self::SLUG_GET_STARTED,
85
+ [
86
+ $get_started,
87
+ 'show',
88
+ ]
89
+ );
90
+ }
91
  }
92
 
93
  if ( is_network_admin() ) {
97
  __( 'Multi Site', 'matomo' ),
98
  Capabilities::KEY_SUPERUSER,
99
  'matomo-multisite',
100
+ [
101
  $info,
102
  'show_multisite',
103
+ ]
104
  );
105
  } else {
106
  add_submenu_page(
109
  __( 'Summary', 'matomo' ),
110
  Capabilities::KEY_VIEW,
111
  self::SLUG_REPORT_SUMMARY,
112
+ [
113
  $summary,
114
  'show',
115
+ ]
116
  );
117
 
118
  // the network itself is not a blog
122
  __( 'Reporting', 'matomo' ),
123
  Capabilities::KEY_VIEW,
124
  self::SLUG_REPORTING,
125
+ [
126
  $this,
127
  'reporting',
128
+ ]
129
  );
130
  // the network itself is not a blog
131
+ if ( matomo_has_tag_manager() ) {
132
+ add_submenu_page(
133
+ self::$parent_slug,
134
+ __( 'Tag Manager', 'matomo' ),
135
+ __( 'Tag Manager', 'matomo' ),
136
+ Capabilities::KEY_WRITE,
137
+ self::SLUG_TAGMANAGER,
138
+ [
139
+ $this,
140
+ 'tagmanager',
141
+ ]
142
+ );
143
+ }
 
144
  }
145
 
146
+ // we always show settings except when multi site is used, plugin is not network enabled, and we are in network admin
147
+ $can_matomo_be_managed = ( ! is_multisite() || $this->settings->is_network_enabled() || ! is_network_admin() );
148
 
149
  if ( $can_matomo_be_managed ) {
150
  add_submenu_page(
153
  __( 'Settings', 'matomo' ),
154
  Capabilities::KEY_SUPERUSER,
155
  self::SLUG_SETTINGS,
156
+ [
157
  $admin_settings,
158
  'show',
159
+ ]
160
  );
161
  }
162
 
167
  __( 'Marketplace', 'matomo' ),
168
  Capabilities::KEY_VIEW,
169
  self::SLUG_MARKETPLACE,
170
+ [
171
  $marketplace,
172
  'show',
173
+ ]
174
  );
175
  }
176
 
181
  __( 'Diagnostics', 'matomo' ),
182
  Capabilities::KEY_SUPERUSER,
183
  self::SLUG_SYSTEM_REPORT,
184
+ [
185
  $system_report,
186
  'show',
187
+ ]
188
  );
189
  }
190
 
194
  __( 'About', 'matomo' ),
195
  Capabilities::KEY_VIEW,
196
  self::SLUG_ABOUT,
197
+ [
198
  $info,
199
  'show',
200
+ ]
201
  );
202
  }
203
 
209
  $tagmanager = __( 'Tag Manager', 'matomo' );
210
  foreach ( $submenu[ self::$parent_slug ] as $key => $menu_item ) {
211
  if ( 0 === strpos( $menu_item[0], $reporting ) || 0 === strpos( $menu_item[0], $tagmanager ) ) {
212
+ // No other choice
213
+ // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
214
  $submenu[ self::$parent_slug ][ $key ][0] .= ' <span class="dashicons-before dashicons-external"></span>';
215
  }
216
  }
218
  }
219
 
220
  public static function get_matomo_goto_url( $goto ) {
221
+ return add_query_arg( [ 'goto' => $goto ], menu_page_url( self::SLUG_REPORTING, false ) );
222
  }
223
 
224
  public static function get_reporting_url() {
234
 
235
  public function reporting() {
236
  if ( ! empty( $_GET['goto'] ) ) {
237
+ switch ( sanitize_text_field( wp_unslash( $_GET['goto'] ) ) ) {
238
  case self::REPORTING_GOTO_ADMIN:
239
  $this->go_to_matomo_page( 'CoreAdminHome', 'home', Capabilities::KEY_SUPERUSER );
240
  break;
265
  $idsite = $site->get_current_matomo_site_id();
266
 
267
  if ( $idsite ) {
268
+ $url = add_query_arg( [ 'idSite' => (int) $idsite ], $url );
269
  }
270
 
271
  if ( ! empty( $_GET['report_date'] ) ) {
272
+ $report_date = sanitize_text_field( wp_unslash( $_GET['report_date'] ) );
273
+ $url = add_query_arg(
274
+ [
275
  'module' => 'CoreHome',
276
  'action' => 'index',
277
+ ],
278
  $url
279
  );
280
 
 
281
  $date = new Dates();
282
+ list( $period, $date ) = $date->detect_period_and_date( $report_date );
283
  $url = add_query_arg(
284
+ [
285
  'period' => $period,
286
  'date' => $date,
287
+ ],
288
  $url
289
  );
290
  }
296
  /**
297
  * @api
298
  */
299
+ public static function get_matomo_reporting_url( $category, $subcategory, $params = [] ) {
300
  $site = new Site();
301
  $idsite = $site->get_current_matomo_site_id();
302
 
331
  /**
332
  * @api
333
  */
334
+ public static function get_matomo_action_url( $module, $action, $params = [] ) {
335
  $site = new Site();
336
  $idsite = $site->get_current_matomo_site_id();
337
 
373
  wp_safe_redirect( $url );
374
  exit;
375
  }
 
376
  }
classes/WpMatomo/Admin/PrivacySettings.php CHANGED
@@ -19,21 +19,21 @@ class PrivacySettings implements AdminSettingsInterface {
19
  const EXAMPLE_MINIMAL = '[matomo_opt_out]';
20
  const EXAMPLE_FULL = '[matomo_opt_out language=de]';
21
 
22
- /**
23
- * @var Settings
24
- */
25
- private $settings;
26
 
27
- public function __construct( Settings $settings ) {
28
- $this->settings = $settings;
29
- }
30
 
31
  public function get_title() {
32
  return esc_html__( 'Privacy & GDPR', 'matomo' );
33
  }
34
 
35
  public function show_settings() {
36
- $matomo_settings = $this->settings;
37
 
38
  include dirname( __FILE__ ) . '/views/privacy_gdpr.php';
39
  }
19
  const EXAMPLE_MINIMAL = '[matomo_opt_out]';
20
  const EXAMPLE_FULL = '[matomo_opt_out language=de]';
21
 
22
+ /**
23
+ * @var Settings
24
+ */
25
+ private $settings;
26
 
27
+ public function __construct( Settings $settings ) {
28
+ $this->settings = $settings;
29
+ }
30
 
31
  public function get_title() {
32
  return esc_html__( 'Privacy & GDPR', 'matomo' );
33
  }
34
 
35
  public function show_settings() {
36
+ $matomo_settings = $this->settings;
37
 
38
  include dirname( __FILE__ ) . '/views/privacy_gdpr.php';
39
  }
classes/WpMatomo/Admin/SafeModeMenu.php CHANGED
@@ -9,6 +9,7 @@
9
 
10
  namespace WpMatomo\Admin;
11
 
 
12
  use WpMatomo\Settings;
13
 
14
  if ( ! defined( 'ABSPATH' ) ) {
@@ -28,12 +29,12 @@ class SafeModeMenu {
28
  */
29
  public function __construct( $settings ) {
30
  $this->settings = $settings;
31
- add_action( 'admin_menu', array( $this, 'add_menu' ) );
32
- add_action( 'network_admin_menu', array( $this, 'add_menu' ) );
33
  }
34
 
35
  public function add_menu() {
36
- if ( ! \WpMatomo::is_admin_user() ) {
37
  return;
38
  }
39
 
@@ -47,11 +48,10 @@ class SafeModeMenu {
47
  __( 'System Report', 'matomo' ),
48
  'administrator',
49
  Menu::SLUG_SYSTEM_REPORT,
50
- array(
51
  $system_report,
52
  'show',
53
- )
54
  );
55
  }
56
-
57
  }
9
 
10
  namespace WpMatomo\Admin;
11
 
12
+ use WpMatomo;
13
  use WpMatomo\Settings;
14
 
15
  if ( ! defined( 'ABSPATH' ) ) {
29
  */
30
  public function __construct( $settings ) {
31
  $this->settings = $settings;
32
+ add_action( 'admin_menu', [ $this, 'add_menu' ] );
33
+ add_action( 'network_admin_menu', [ $this, 'add_menu' ] );
34
  }
35
 
36
  public function add_menu() {
37
+ if ( ! WpMatomo::is_admin_user() ) {
38
  return;
39
  }
40
 
48
  __( 'System Report', 'matomo' ),
49
  'administrator',
50
  Menu::SLUG_SYSTEM_REPORT,
51
+ [
52
  $system_report,
53
  'show',
54
+ ]
55
  );
56
  }
 
57
  }
classes/WpMatomo/Admin/Summary.php CHANGED
@@ -14,13 +14,13 @@ use WpMatomo\Report\Dates;
14
  use WpMatomo\Report\Metadata;
15
  use WpMatomo\Report\Renderer;
16
  use WpMatomo\Settings;
 
17
 
18
  if ( ! defined( 'ABSPATH' ) ) {
19
  exit; // if accessed directly
20
  }
21
 
22
  class Summary {
23
-
24
  const NONCE_DASHBOARD = 'matomo_pin_dashboard';
25
 
26
  /**
@@ -36,20 +36,19 @@ class Summary {
36
  }
37
 
38
  private function pin_if_submitted() {
39
- if ( ! empty( $_GET[ 'pin' ] )
40
- && ! empty( $_GET[ 'report_uniqueid' ] )
41
- && ! empty( $_GET[ 'report_date' ] )
42
- && is_admin()
43
- && check_admin_referer( self::NONCE_DASHBOARD )
44
- && is_user_logged_in()
45
- && current_user_can( Capabilities::KEY_VIEW ) ) {
46
- $unique_id = $_GET[ 'report_uniqueid' ];
47
- $date = $_GET[ 'report_date' ];
48
-
49
  $dashobard = new Dashboard();
50
- if ($dashobard->is_valid_widget($unique_id, $date)) {
51
  $dashobard->toggle_widget( $unique_id, $date );
52
- return true;
53
  }
54
  }
55
 
@@ -57,6 +56,8 @@ class Summary {
57
  }
58
 
59
  public function show() {
 
 
60
  $matomo_pinned = $this->pin_if_submitted();
61
 
62
  $settings = $this->settings;
@@ -67,9 +68,42 @@ class Summary {
67
  $report_dates_obj = new Dates();
68
  $report_dates = $report_dates_obj->get_supported_dates();
69
 
70
- $report_date = Dates::YESTERDAY;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  if ( isset( $_GET['report_date'] ) && isset( $report_dates[ $_GET['report_date'] ] ) ) {
72
- $report_date = $_GET['report_date'];
73
  }
74
 
75
  list( $report_period_selected, $report_date_selected ) = $report_dates_obj->detect_period_and_date( $report_date );
@@ -78,22 +112,24 @@ class Summary {
78
 
79
  $matomo_dashboard = new Dashboard();
80
 
81
- $wp_version = get_bloginfo( 'version' );
82
- $matomo_is_version_pre55 = empty($wp_version) || version_compare($wp_version, '5.5.0') === -1;
83
 
84
  include dirname( __FILE__ ) . '/views/summary.php';
85
  }
86
 
87
  private function get_reports_to_show() {
88
- $reports_to_show = array(
 
89
  'VisitsSummary_get',
90
  'UserCountry_getCountry',
 
91
  'DevicesDetection_getType',
 
92
  'Resolution_getResolution',
93
  'DevicesDetection_getOsFamilies',
94
  'DevicesDetection_getBrowsers',
95
  'VisitTime_getVisitInformationPerServerTime',
96
- 'Actions_get',
97
  'Actions_getPageTitles',
98
  'Actions_getEntryPageTitles',
99
  'Actions_getExitPageTitles',
@@ -102,18 +138,16 @@ class Summary {
102
  'Referrers_getAll',
103
  'Referrers_getSocials',
104
  'Referrers_getCampaigns',
105
- 'Goals_get',
106
- );
107
 
108
  if ( $this->settings->get_global_option( 'track_ecommerce' ) ) {
109
  $reports_to_show[] = 'Goals_get_idGoal--ecommerceOrder';
110
  $reports_to_show[] = 'Goals_getItemsName';
111
  }
112
 
113
- $reports_to_show[] = Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME;
114
  $reports_to_show = apply_filters( 'matomo_report_summary_report_ids', $reports_to_show );
115
 
116
- $report_metadata = array();
117
  $metadata = new Metadata();
118
  foreach ( $reports_to_show as $report_unique_id ) {
119
  $report = $metadata->find_report_by_unique_id( $report_unique_id );
@@ -128,5 +162,4 @@ class Summary {
128
 
129
  return $report_metadata;
130
  }
131
-
132
  }
14
  use WpMatomo\Report\Metadata;
15
  use WpMatomo\Report\Renderer;
16
  use WpMatomo\Settings;
17
+ use Piwik\Plugins\UsersManager\UserPreferences;
18
 
19
  if ( ! defined( 'ABSPATH' ) ) {
20
  exit; // if accessed directly
21
  }
22
 
23
  class Summary {
 
24
  const NONCE_DASHBOARD = 'matomo_pin_dashboard';
25
 
26
  /**
36
  }
37
 
38
  private function pin_if_submitted() {
39
+ if ( ! empty( $_GET['pin'] )
40
+ && ! empty( $_GET['report_uniqueid'] )
41
+ && ! empty( $_GET['report_date'] )
42
+ && is_admin()
43
+ && check_admin_referer( self::NONCE_DASHBOARD )
44
+ && is_user_logged_in()
45
+ && current_user_can( Capabilities::KEY_VIEW ) ) {
46
+ $unique_id = sanitize_text_field( wp_unslash( $_GET['report_uniqueid'] ) );
47
+ $date = sanitize_text_field( wp_unslash( $_GET['report_date'] ) );
 
48
  $dashobard = new Dashboard();
49
+ if ( $dashobard->is_valid_widget( $unique_id, $date ) ) {
50
  $dashobard->toggle_widget( $unique_id, $date );
51
+ return true;
52
  }
53
  }
54
 
56
  }
57
 
58
  public function show() {
59
+ do_action( 'matomo_load_chartjs' );
60
+
61
  $matomo_pinned = $this->pin_if_submitted();
62
 
63
  $settings = $this->settings;
68
  $report_dates_obj = new Dates();
69
  $report_dates = $report_dates_obj->get_supported_dates();
70
 
71
+ $user_preference = new UserPreferences();
72
+ $default_date = $user_preference->getDefaultDate();
73
+ $report_period = $user_preference->getDefaultPeriod();
74
+ switch ( $report_period ) {
75
+ case 'day':
76
+ $report_date = $default_date;
77
+ break;
78
+ case 'year':
79
+ case 'month':
80
+ case 'week':
81
+ switch ( $default_date ) {
82
+ case 'yesterday':
83
+ $report_date = 'last' . $report_period;
84
+ break;
85
+ case 'today':
86
+ $report_date = 'this' . $report_period;
87
+ break;
88
+ }
89
+ break;
90
+ case 'range':
91
+ switch ( $default_date ) {
92
+ case 'previous30':
93
+ $report_date = 'lastmonth';
94
+ break;
95
+ case 'previous7':
96
+ $report_date = 'lastweek';
97
+ break;
98
+ case 'last30':
99
+ $report_date = 'thismonth';
100
+ break;
101
+ case 'last7':
102
+ $report_date = 'thisweek';
103
+ }
104
+ }
105
  if ( isset( $_GET['report_date'] ) && isset( $report_dates[ $_GET['report_date'] ] ) ) {
106
+ $report_date = sanitize_text_field( wp_unslash( $_GET['report_date'] ) );
107
  }
108
 
109
  list( $report_period_selected, $report_date_selected ) = $report_dates_obj->detect_period_and_date( $report_date );
112
 
113
  $matomo_dashboard = new Dashboard();
114
 
115
+ $wp_version = get_bloginfo( 'version' );
116
+ $matomo_is_version_pre55 = empty( $wp_version ) || version_compare( $wp_version, '5.5.0' ) === - 1;
117
 
118
  include dirname( __FILE__ ) . '/views/summary.php';
119
  }
120
 
121
  private function get_reports_to_show() {
122
+ $reports_to_show = [
123
+ Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME,
124
  'VisitsSummary_get',
125
  'UserCountry_getCountry',
126
+ 'Actions_get',
127
  'DevicesDetection_getType',
128
+ 'Goals_get',
129
  'Resolution_getResolution',
130
  'DevicesDetection_getOsFamilies',
131
  'DevicesDetection_getBrowsers',
132
  'VisitTime_getVisitInformationPerServerTime',
 
133
  'Actions_getPageTitles',
134
  'Actions_getEntryPageTitles',
135
  'Actions_getExitPageTitles',
138
  'Referrers_getAll',
139
  'Referrers_getSocials',
140
  'Referrers_getCampaigns',
141
+ ];
 
142
 
143
  if ( $this->settings->get_global_option( 'track_ecommerce' ) ) {
144
  $reports_to_show[] = 'Goals_get_idGoal--ecommerceOrder';
145
  $reports_to_show[] = 'Goals_getItemsName';
146
  }
147
 
 
148
  $reports_to_show = apply_filters( 'matomo_report_summary_report_ids', $reports_to_show );
149
 
150
+ $report_metadata = [];
151
  $metadata = new Metadata();
152
  foreach ( $reports_to_show as $report_unique_id ) {
153
  $report = $metadata->find_report_by_unique_id( $report_unique_id );
162
 
163
  return $report_metadata;
164
  }
 
165
  }
classes/WpMatomo/Admin/SystemReport.php CHANGED
@@ -9,6 +9,8 @@
9
 
10
  namespace WpMatomo\Admin;
11
 
 
 
12
  use Piwik\CliMulti;
13
  use Piwik\Common;
14
  use Piwik\Config;
@@ -23,6 +25,7 @@ use Piwik\Plugins\UserCountry\LocationProvider;
23
  use Piwik\SettingsPiwik;
24
  use Piwik\Tracker\Failures;
25
  use Piwik\Version;
 
26
  use WpMatomo\Bootstrap;
27
  use WpMatomo\Capabilities;
28
  use WpMatomo\Installer;
@@ -39,6 +42,20 @@ if ( ! defined( 'ABSPATH' ) ) {
39
  exit; // if accessed directly
40
  }
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  class SystemReport {
43
  const NONCE_NAME = 'matomo_troubleshooting';
44
  const TROUBLESHOOT_SYNC_USERS = 'matomo_troubleshooting_action_site_users';
@@ -51,21 +68,32 @@ class SystemReport {
51
  const TROUBLESHOOT_CLEAR_LOGS = 'matomo_troubleshooting_action_clear_logs';
52
  const TROUBLESHOOT_RUN_UPDATER = 'matomo_troubleshooting_action_run_updater';
53
 
54
- private $not_compatible_plugins = array(
55
- 'background-manager', // Uses an old version of Twig and plugin is no longer maintained.
56
- 'all-in-one-event-calendar', // Uses an old version of Twig
57
- 'data-tables-generator-by-supsystic', // uses an old version of twig causing some styles to go funny in the reporting and admin
58
- 'tweet-old-post-pro', // uses a newer version of monolog
59
- 'wp-rss-aggregator', // see https://wordpress.org/support/topic/critical-error-after-upgrade/ conflict re php-di version
60
- 'wp-defender', // see https://wordpress.org/support/topic/critical-error-after-upgrade/ conflict re php-di version
61
- 'age-verification-for-woocommerce', // see https://github.com/matomo-org/wp-matomo/issues/428
62
- 'minify-html-markup', // see https://wordpress.org/support/topic/graphs-are-not-displayed-in-the-visits-overview-widget/#post-14298068
63
- 'bigbuy-wc-dropshipping-connector', // see https://wordpress.org/support/topic/20-total-errors-during-this-script-execution/
64
- 'google-listings-and-ads', // see https://wordpress.org/support/topic/20-total-errors-during-this-script-execution/
65
- 'accelerated-mobile-pages' // see https://wordpress.org/support/topic/receiving-errors-from-my-plesk-server/
66
- );
67
-
68
- private $valid_tabs = array( 'troubleshooting' );
 
 
 
 
 
 
 
 
 
 
 
69
 
70
  /**
71
  * @var Settings
@@ -81,12 +109,12 @@ class SystemReport {
81
  /**
82
  * @var \WpMatomo\Db\Settings
83
  */
84
- public $dbSettings;
85
 
86
  public function __construct( Settings $settings ) {
87
- $this->settings = $settings;
88
- $this->logger = new Logger();
89
- $this->dbSettings = new \WpMatomo\Db\Settings();
90
  }
91
 
92
  public function get_not_compatible_plugins() {
@@ -102,36 +130,38 @@ class SystemReport {
102
  Bootstrap::do_bootstrap();
103
  $scheduled_tasks = new ScheduledTasks( $this->settings );
104
 
105
- if (!defined('PIWIK_ARCHIVE_NO_TRUNCATE')) {
106
- define('PIWIK_ARCHIVE_NO_TRUNCATE', 1); // when triggering it manually, we prefer the full error message
 
107
  }
108
 
109
  try {
110
  // force invalidation of archive to ensure it actually will rearchive the data
111
- $site = new Site();
112
  $idsite = $site->get_current_matomo_site_id();
113
- if ($idsite) {
114
- $timezone = \Piwik\Site::getTimezoneFor($idsite);
115
- $now_string = \Piwik\Date::factory('now', $timezone)->toString();
116
- foreach (array('day') as $period) {
117
- API::getInstance()->invalidateArchivedReports($idsite, $now_string, $period, false, false);
118
  }
119
  }
120
- } catch (\Exception $e) {
121
- $this->logger->log_exception('archive_invalidate', $e);
122
  }
123
 
124
  try {
125
- $errors = $scheduled_tasks->archive( $force = true, $throw_exception = false );
126
- } catch (\Exception $e) {
127
- echo '<div class="error"><p>' . esc_html__('Matomo Archive Error', 'matomo') . ': '. esc_html(matomo_anonymize_value($e->getMessage() . ' =>' . $this->logger->get_readable_trace($e))) . '</p></div>';
128
  throw $e;
129
  }
130
 
131
  if ( ! empty( $errors ) ) {
132
  echo '<div class="notice notice-warning"><p>Matomo Archive Warnings: ';
133
- foreach ($errors as $error) {
134
- echo nl2br(esc_html(matomo_anonymize_value(var_export($error, 1))));
 
135
  echo '<br/>';
136
  }
137
  echo '</p></div>';
@@ -191,47 +221,52 @@ class SystemReport {
191
  $settings = $this->settings;
192
 
193
  $matomo_active_tab = '';
194
- if ( isset( $_GET['tab'] ) && in_array( $_GET['tab'], $this->valid_tabs, true ) ) {
195
- $matomo_active_tab = $_GET['tab'];
 
 
 
 
196
  }
197
 
198
- $matomo_tables = array();
199
  if ( empty( $matomo_active_tab ) ) {
 
200
  $this->initial_error_reporting = @error_reporting();
201
- $matomo_tables = array(
202
- array(
203
  'title' => 'Matomo',
204
  'rows' => $this->get_matomo_info(),
205
  'has_comments' => true,
206
- ),
207
- array(
208
- 'title' => 'WordPress',
209
- 'rows' => $this->get_wordpress_info(),
210
  'has_comments' => true,
211
- ),
212
- array(
213
  'title' => 'WordPress Plugins',
214
  'rows' => $this->get_plugins_info(),
215
  'has_comments' => true,
216
- ),
217
- array(
218
  'title' => 'Server',
219
  'rows' => $this->get_server_info(),
220
  'has_comments' => true,
221
- ),
222
- array(
223
  'title' => 'Database',
224
  'rows' => $this->get_db_info(),
225
  'has_comments' => true,
226
- ),
227
- array(
228
  'title' => 'Browser',
229
  'rows' => $this->get_browser_info(),
230
  'has_comments' => true,
231
- ),
232
- );
233
  }
234
- $matomo_tables = apply_filters('matomo_systemreport_tables', $matomo_tables);
235
  $matomo_tables = $this->add_errors_first( $matomo_tables );
236
  $matomo_has_warning_and_no_errors = $this->has_only_warnings_no_error( $matomo_tables );
237
 
@@ -258,11 +293,11 @@ class SystemReport {
258
  }
259
 
260
  private function add_errors_first( $report_tables ) {
261
- $errors = array(
262
  'title' => 'Errors',
263
- 'rows' => array(),
264
  'has_comments' => true,
265
- );
266
  foreach ( $report_tables as $report_table ) {
267
  foreach ( $report_table['rows'] as $row ) {
268
  if ( ! empty( $row['is_error'] ) ) {
@@ -293,28 +328,28 @@ class SystemReport {
293
  $comment .= sprintf( esc_html__( '%s is not writable. ', 'matomo' ), $title );
294
  }
295
 
296
- $rows[] = array(
297
  'name' => sprintf( esc_html__( '%s exists and is writable.', 'matomo' ), $title ),
298
  'value' => $file_exists && $file_readable && $file_writable ? esc_html__( 'Yes', 'matomo' ) : esc_html__( 'No', 'matomo' ),
299
  'comment' => $comment,
300
  'is_error' => $required && ( ! $file_exists || ! $file_readable ),
301
  'is_warning' => ! $required && ( ! $file_exists || ! $file_readable ),
302
- );
303
 
304
  return $rows;
305
  }
306
 
307
  private function get_matomo_info() {
308
- $rows = array();
309
 
310
  $plugin_data = get_plugin_data( MATOMO_ANALYTICS_FILE, $markup = false, $translate = false );
311
- $install_time = get_option(Installer::OPTION_NAME_INSTALL_DATE);
312
 
313
- $rows[] = array(
314
  'name' => esc_html__( 'Matomo Plugin Version', 'matomo' ),
315
  'value' => $plugin_data['Version'],
316
  'comment' => '',
317
- );
318
 
319
  $paths = new Paths();
320
  $path_config_file = $paths->get_config_ini_path();
@@ -323,21 +358,23 @@ class SystemReport {
323
  $path_tracker_file = $paths->get_matomo_js_upload_path();
324
  $rows = $this->check_file_exists_and_writable( $rows, $path_tracker_file, 'JS Tracker', false );
325
 
326
- $rows[] = array(
327
  'name' => esc_html__( 'Plugin directories', 'matomo' ),
328
  'value' => ! empty( $GLOBALS['MATOMO_PLUGIN_DIRS'] ) ? 'Yes' : 'No',
329
  'comment' => ! empty( $GLOBALS['MATOMO_PLUGIN_DIRS'] ) ? wp_json_encode( $GLOBALS['MATOMO_PLUGIN_DIRS'] ) : '',
330
- );
331
 
332
  $tmp_dir = $paths->get_tmp_dir();
333
 
334
- $rows[] = array(
335
  'name' => esc_html__( 'Tmp directory writable', 'matomo' ),
336
  'value' => is_writable( $tmp_dir ),
337
  'comment' => $tmp_dir,
338
- );
339
 
340
  if ( ! empty( $_SERVER['MATOMO_WP_ROOT_PATH'] ) ) {
 
 
341
  $custom_path = rtrim( $_SERVER['MATOMO_WP_ROOT_PATH'], '/' ) . '/wp-load.php';
342
  $path_exists = file_exists( $custom_path );
343
  $comment = '';
@@ -345,147 +382,146 @@ class SystemReport {
345
  $comment = 'It seems the path does not point to the WP root directory.';
346
  }
347
 
348
- $rows[] = array(
349
  'name' => 'Custom MATOMO_WP_ROOT_PATH',
350
  'value' => $path_exists,
351
  'is_error' => ! $path_exists,
352
  'comment' => $comment,
353
- );
354
  }
355
 
356
  $report = null;
357
 
358
- if ( ! \WpMatomo::is_safe_mode() ) {
359
  try {
360
  Bootstrap::do_bootstrap();
361
  /** @var DiagnosticService $service */
362
  $service = StaticContainer::get( DiagnosticService::class );
363
  $report = $service->runDiagnostics();
364
 
365
- $rows[] = array(
366
  'name' => esc_html__( 'Matomo Version', 'matomo' ),
367
  'value' => \Piwik\Version::VERSION,
368
  'comment' => '',
369
- );
370
- } catch ( \Exception $e ) {
371
- $rows[] = array(
372
  'name' => esc_html__( 'Matomo System Check', 'matomo' ),
373
  'value' => 'Failed to run Matomo system check.',
374
  'comment' => $e->getMessage(),
375
- );
376
  }
377
  }
378
 
379
  $site = new Site();
380
  $idsite = $site->get_current_matomo_site_id();
381
 
382
- $rows[] = array(
383
  'name' => esc_html__( 'Matomo Blog idSite', 'matomo' ),
384
  'value' => $idsite,
385
  'comment' => '',
386
- );
387
 
388
  $install_date = '';
389
- if (!empty($install_time)) {
390
- $install_date = 'Install date: '. $this->convert_time_to_date($install_time, true, false);
391
  }
392
 
393
- $rows[] = array(
394
  'name' => esc_html__( 'Matomo Install Version', 'matomo' ),
395
- 'value' => get_option(Installer::OPTION_NAME_INSTALL_VERSION),
396
  'comment' => $install_date,
397
- );
398
-
399
- $wpmatomo_updater = new \WpMatomo\Updater($this->settings);
400
- if (!\WpMatomo::is_safe_mode()) {
401
 
 
 
402
  $outstanding_updates = $wpmatomo_updater->get_plugins_requiring_update();
403
  $upgrade_in_progress = $wpmatomo_updater->is_upgrade_in_progress();
404
- $rows[] = array(
405
- 'name' => 'Upgrades outstanding',
406
- 'value' => !empty($outstanding_updates),
407
- 'comment' => !empty($outstanding_updates) ? json_encode($outstanding_updates) : '',
408
- );
409
- $rows[] = array(
410
- 'name' => 'Upgrade in progress',
411
- 'value' => $upgrade_in_progress,
412
- 'comment' => '',
413
- );
414
  }
415
 
416
- if (!$wpmatomo_updater->load_plugin_functions()) {
417
  // this should actually never happen...
418
- $rows[] = array(
419
- 'name' => 'Matomo Upgrade Plugin Functions',
420
- 'is_warning' => true,
421
- 'value' => false,
422
- 'comment' => 'Function "get_plugin_data" not available. There may be an issue with upgrades not being executed. Please reach out to us.',
423
- );
424
  }
425
 
426
- $rows[] = array(
427
  'section' => 'Endpoints',
428
- );
429
 
430
- $rows[] = array(
431
  'name' => 'Matomo JavaScript Tracker URL',
432
  'value' => '',
433
  'comment' => $paths->get_js_tracker_url_in_matomo_dir(),
434
- );
435
 
436
- $rows[] = array(
437
  'name' => 'Matomo JavaScript Tracker - WP Rest API',
438
  'value' => '',
439
  'comment' => $paths->get_js_tracker_rest_api_endpoint(),
440
- );
441
 
442
- $rows[] = array(
443
  'name' => 'Matomo HTTP Tracking API',
444
  'value' => '',
445
  'comment' => $paths->get_tracker_api_url_in_matomo_dir(),
446
- );
447
 
448
- $rows[] = array(
449
  'name' => 'Matomo HTTP Tracking API - WP Rest API',
450
  'value' => '',
451
  'comment' => $paths->get_tracker_api_rest_api_endpoint(),
452
- );
453
 
454
- $matomo_plugin_dir_name = basename(dirname(MATOMO_ANALYTICS_FILE));
455
- if ($matomo_plugin_dir_name !== 'matomo') {
456
- $rows[] = array(
457
- 'name' => 'Matomo Plugin Name is correct',
458
- 'value' => false,
459
  'is_error' => true,
460
- 'comment' => 'The plugin name should be "matomo" but seems to be "' . $matomo_plugin_dir_name . '". As a result, admin pages and other features might not work. You might need to rename the directory name of this plugin and reactive the plugin.',
461
- );
462
- } elseif (!is_plugin_active('matomo/matomo.php')) {
463
- $rows[] = array(
464
- 'name' => 'Matomo Plugin not active',
465
- 'value' => false,
466
  'is_error' => true,
467
- 'comment' => 'It seems WordPress thinks that `matomo/matomo.php` is not active. As a result Matomo reporting and admin pages may not work. You may be able to fix this by deactivating and activating the Matomo Analytics plugin. One of the reasons this could happen is that you used to have Matomo installed in the wrong folder.',
468
- );
469
  }
470
 
471
- $rows[] = array(
472
  'section' => 'Crons',
473
- );
474
 
475
  $scheduled_tasks = new ScheduledTasks( $this->settings );
476
  $all_events = $scheduled_tasks->get_all_events();
477
 
478
- $rows[] = array(
479
  'name' => esc_html__( 'Server time', 'matomo' ),
480
  'value' => $this->convert_time_to_date( time(), false ),
481
  'comment' => '',
482
- );
483
 
484
- $rows[] = array(
485
  'name' => esc_html__( 'Blog time', 'matomo' ),
486
  'value' => $this->convert_time_to_date( time(), true ),
487
  'comment' => esc_html__( 'Below dates are shown in blog timezone', 'matomo' ),
488
- );
489
 
490
  foreach ( $all_events as $event_name => $event_config ) {
491
  $last_run_before = $scheduled_tasks->get_last_time_before_cron( $event_name );
@@ -497,133 +533,131 @@ class SystemReport {
497
  $comment .= ' Last ended: ' . $this->convert_time_to_date( $last_run_after, true, true ) . '.';
498
  $comment .= ' Interval: ' . $event_config['interval'];
499
 
500
- $rows[] = array(
501
  'name' => $event_config['name'],
502
  'value' => 'Next run: ' . $this->convert_time_to_date( $next_scheduled, true, true ),
503
  'comment' => $comment,
504
- );
505
  }
506
 
507
  $suports_async = false;
508
- if ( ! \WpMatomo::is_safe_mode() && $report ) {
509
- $rows[] = array(
510
  'section' => esc_html__( 'Mandatory checks', 'matomo' ),
511
- );
512
 
513
  $rows = $this->add_diagnostic_results( $rows, $report->getMandatoryDiagnosticResults() );
514
 
515
- $rows[] = array(
516
  'section' => esc_html__( 'Optional checks', 'matomo' ),
517
- );
518
  $rows = $this->add_diagnostic_results( $rows, $report->getOptionalDiagnosticResults() );
519
 
520
- $cli_multi = new CliMulti();
521
  $suports_async = $cli_multi->supportsAsync();
522
 
523
- $rows[] = array(
524
  'name' => 'Supports Async Archiving',
525
  'value' => $suports_async,
526
  'comment' => '',
527
- );
528
 
529
  $location_provider = LocationProvider::getCurrentProvider();
530
- if ($location_provider) {
531
- $rows[] = array(
532
  'name' => 'Location provider ID',
533
  'value' => $location_provider->getId(),
534
  'comment' => '',
535
- );
536
- $rows[] = array(
537
  'name' => 'Location provider available',
538
  'value' => $location_provider->isAvailable(),
539
  'comment' => '',
540
- );
541
- $rows[] = array(
542
  'name' => 'Location provider working',
543
  'value' => $location_provider->isWorking(),
544
  'comment' => '',
545
- );
546
  }
547
 
548
- if ( ! \WpMatomo::is_safe_mode() ) {
549
  Bootstrap::do_bootstrap();
550
  $general = Config::getInstance()->General;
551
-
552
- if (empty($general['proxy_client_headers'])) {
553
- foreach (AdvancedSettings::$valid_host_headers as $header) {
554
- if (!empty($_SERVER[$header])) {
555
- $rows[] = array(
556
- 'name' => 'Proxy header',
557
- 'value' => $header,
558
  'is_warning' => true,
559
- 'comment' => 'A proxy header is set which means you maybe need to configure a proxy header in the Advanced settings to make location reporting work. If the location in your reports is detected correctly, you can ignore this warning. Learn more: https://matomo.org/faq/wordpress/how-do-i-fix-the-proxy-header-warning-in-the-matomo-for-wordpress-system-report/',
560
- );
561
  }
562
  }
563
  }
564
- $incompatible_plugins = Plugin\Manager::getInstance()->getIncompatiblePlugins(Version::VERSION);
565
- if (!empty($incompatible_plugins)) {
566
- $rows[] = array(
567
- 'section' => esc_html__( 'Incompatible Matomo plugins', 'matomo' ),
568
- );
569
- foreach ($incompatible_plugins as $plugin) {
570
- $rows[] = array(
571
- 'name' => 'Plugin has missing dependencies',
572
- 'value' => $plugin->getPluginName(),
573
- 'is_error' => true,
574
- 'comment' => $plugin->getMissingDependenciesAsString(Version::VERSION) . ' If the plugin requires a different Matomo version you may need to update it. If you no longer use it consider uninstalling it.',
575
- );
576
- }
577
-
578
- }
579
  }
580
 
581
  $num_days_check_visits = 5;
582
- $had_visits = $this->had_visits_in_last_days($num_days_check_visits);
583
- if ($had_visits === false || $had_visits === true) {
584
  // do not show info if we could not detect it (had_visits === null)
585
  $comment = '';
586
- if (!$had_visits) {
587
  $comment = 'It looks like there were no visits in the last ' . $num_days_check_visits . ' days. This may be expected if tracking is disabled, you have not added the tracking code, or your website does not have many visitors in general and you exclude your own visits.';
588
  }
589
 
590
- $rows[] = array(
591
- 'name' => 'Had visit in last ' . $num_days_check_visits . ' days',
592
- 'value' => $had_visits,
593
- 'is_warning' => !$had_visits && $this->settings->is_tracking_enabled(),
594
- 'comment' => $comment,
595
- );
596
  }
597
 
598
- if ( ! \WpMatomo::is_safe_mode() ) {
599
  Bootstrap::do_bootstrap();
600
  $matomo_url = SettingsPiwik::getPiwikUrl();
601
- $rows[] = array(
602
  'name' => 'Matomo URL',
603
  'comment' => $matomo_url,
604
  'value' => ! empty( $matomo_url ),
605
- );
606
  }
607
-
608
  }
609
 
610
- $rows[] = array(
611
  'section' => 'Matomo Settings',
612
- );
613
 
614
  // always show these settings
615
- $global_settings_always_show = array(
616
  'track_mode',
617
  'track_codeposition',
618
  'track_api_endpoint',
619
  'track_js_endpoint',
620
- );
621
  foreach ( $global_settings_always_show as $key ) {
622
- $rows[] = array(
623
  'name' => ucfirst( str_replace( '_', ' ', $key ) ),
624
  'value' => $this->settings->get_global_option( $key ),
625
  'comment' => '',
626
- );
627
  }
628
 
629
  // otherwise show only few customised settings
@@ -635,132 +669,130 @@ class SystemReport {
635
  $val = implode( ', ', $val );
636
  }
637
 
638
- $rows[] = array(
639
  'name' => ucfirst( str_replace( '_', ' ', $key ) ),
640
  'value' => $val,
641
  'comment' => '',
642
- );
643
  }
644
  }
645
 
646
- $rows[] = array(
647
  'section' => 'Logs',
648
- );
649
 
650
  $error_log_entries = $this->logger->get_last_logged_entries();
651
-
652
- if ( ! empty( $error_log_entries ) ) {
653
 
 
654
  foreach ( $error_log_entries as $error ) {
655
- if (!empty($install_time)
656
- && is_numeric($install_time)
657
- && !empty($error['name'])
658
- && !empty($error['value'])
659
- && is_numeric($error['value'])
660
- && $error['name'] === 'cron_sync'
661
- && $error['value'] < ($install_time + 300)) {
662
  // the first sync might right after the installation
663
  continue;
664
  }
665
 
666
  // we only consider plugin_updates as errors only if there are still outstanding updates
667
- $is_plugin_update_error = !empty($error['name']) && $error['name'] === 'plugin_update'
668
- && !empty($outstanding_updates);
669
 
670
- $skip_plugin_update = !empty($error['name']) && $error['name'] === 'plugin_update'
671
- && empty($outstanding_updates);
672
 
673
- if (empty($error['comment']) && $error['comment'] !== '0') {
674
  $error['comment'] = '';
675
  }
676
 
677
- $error['value'] = $this->convert_time_to_date( $error['value'], true, false );
678
- $error['is_warning'] = !empty($error['name']) && stripos($error['name'], 'archiv') !== false && $error['name'] !== 'archive_boot';
679
- $error['is_error'] = $is_plugin_update_error;
680
- if ($is_plugin_update_error) {
681
  $error['comment'] = 'Please reach out to us and include the copied system report (see https://matomo.org/faq/wordpress/how-do-i-troubleshoot-a-failed-database-upgrade-in-matomo-for-wordpress/ for more info)<br><br>You can also retry the update manually by clicking in the top on the "Troubleshooting" tab and then clicking on the "Run updater" button.' . $error['comment'];
682
- } elseif ($skip_plugin_update) {
683
  $error['comment'] = 'As there are no outstanding plugin updates it looks like this log can be ignored.<br><br>' . $error['comment'];
684
  }
685
- $error['comment'] = matomo_anonymize_value($error['comment']);
686
- $rows[] = $error;
687
  }
688
 
689
  foreach ( $error_log_entries as $error ) {
690
- if ($suports_async
691
- && !empty($error['value']) && is_string($error['value'])
692
- && strpos($error['value'], __( 'Your PHP installation appears to be missing the MySQL extension which is required by WordPress.' )) > 0) {
693
-
694
- $rows[] = array(
695
- 'name' => 'Cli has no MySQL',
696
- 'value' => true,
697
- 'comment' => 'It looks like MySQL is not available on CLI. Please read our FAQ on how to fix this issue: https://matomo.org/faq/wordpress/how-do-i-fix-the-error-your-php-installation-appears-to-be-missing-the-mysql-extension-which-is-required-by-wordpress-in-matomo-system-report/ ',
698
- 'is_error' => true
699
- );
700
  }
701
  }
702
  } else {
703
- $rows[] = array(
704
- 'name' => __('None', 'matomo'),
705
  'value' => '',
706
  'comment' => '',
707
- );
708
  }
709
 
710
-
711
- if ( ! \WpMatomo::is_safe_mode() ) {
712
  Bootstrap::do_bootstrap();
713
  $trackfailures = [];
714
  try {
715
  $tracking_failures = new Failures();
716
- $trackfailures = $tracking_failures->getAllFailures();
717
- } catch (\Exception $e) {
 
718
  // ignored in case not set up yet etc.
719
  }
720
- if (!empty($trackfailures)) {
721
- $rows[] = array(
722
  'section' => 'Tracking failures',
723
- );
724
- foreach ($trackfailures as $failure) {
725
- $comment = sprintf('Solution: %s<br>More info: %s<br>Date: %s<br>Request URL: %s',
726
- $failure['solution'], $failure['solution_url'],
727
- $failure['pretty_date_first_occurred'], $failure['request_url']);
728
- $rows[] = array(
729
- 'name' => $failure['problem'],
730
- 'is_warning' => true,
731
- 'value' => '',
732
- 'comment' => $comment,
733
  );
 
 
 
 
 
 
734
  }
735
-
736
  }
737
-
738
  }
739
 
740
-
741
  return $rows;
742
  }
743
 
744
- private function had_visits_in_last_days($numDays)
745
- {
746
  global $wpdb;
747
 
748
- if (\WpMatomo::is_safe_mode()) {
749
  return null;
750
  }
751
 
752
- $days_in_seconds = $numDays * 86400;
753
 
754
- $prefix_table = $this->dbSettings->prefix_table_name('log_visit');
755
 
756
  $suppress_errors = $wpdb->suppress_errors;
757
  $wpdb->suppress_errors( true );// prevent any of this showing in logs just in case
758
 
759
  try {
760
  $time = gmdate( 'Y-m-d H:i:s', time() - $days_in_seconds );
761
- $sql = $wpdb->prepare('SELECT idsite from ' . $prefix_table . ' where visit_last_action_time > %s LIMIT 1', $time );
762
- $row = $wpdb->get_var( $sql );
763
- } catch ( \Exception $e ) {
764
  $row = null;
765
  }
766
 
@@ -769,8 +801,8 @@ class SystemReport {
769
  // 0 === had no visit
770
  // 1 === had visit
771
  // null === sum error... eg table was not correctly installed
772
- if ($row !== null) {
773
- $row = !empty($row);
774
  }
775
 
776
  return $row;
@@ -781,7 +813,7 @@ class SystemReport {
781
  return esc_html__( 'Unknown', 'matomo' );
782
  }
783
 
784
- $date = gmdate( 'Y-m-d H:i:s', (int)$time );
785
 
786
  if ( $in_blog_timezone ) {
787
  $date = get_date_from_gmt( $date, 'Y-m-d H:i:s' );
@@ -789,7 +821,7 @@ class SystemReport {
789
 
790
  if ( $print_diff && class_exists( '\Piwik\Metrics\Formatter' ) ) {
791
  $formatter = new \Piwik\Metrics\Formatter();
792
- $date .= ' (' . $formatter->getPrettyTimeFromSeconds( $time - time(), true, false ) . ')';
793
  }
794
 
795
  return $date;
@@ -818,13 +850,13 @@ class SystemReport {
818
  }
819
  }
820
 
821
- $rows[] = array(
822
  'name' => $result->getLabel(),
823
  'value' => $result->getStatus() . ' ' . $result->getLongErrorMessage(),
824
  'comment' => $comment,
825
  'is_warning' => $result->getStatus() === DiagnosticResult::STATUS_WARNING,
826
  'is_error' => $result->getStatus() === DiagnosticResult::STATUS_ERROR,
827
- );
828
  }
829
 
830
  return $rows;
@@ -842,245 +874,272 @@ class SystemReport {
842
  $is_network_enabled = $settings->is_network_enabled();
843
  }
844
 
845
- $rows = array();
846
- $rows[] = array(
847
  'name' => 'Home URL',
848
  'value' => home_url(),
849
- );
850
- $rows[] = array(
851
  'name' => 'Site URL',
852
  'value' => site_url(),
853
- );
854
- $rows[] = array(
855
  'name' => 'WordPress Version',
856
  'value' => get_bloginfo( 'version' ),
857
- );
858
- $rows[] = array(
859
  'name' => 'Number of blogs',
860
  'value' => $num_blogs,
861
- );
862
- $rows[] = array(
863
  'name' => 'Multisite Enabled',
864
  'value' => $is_multi_site,
865
- );
866
- $rows[] = array(
867
  'name' => 'Network Enabled',
868
  'value' => $is_network_enabled,
869
- );
870
- $consts = array('WP_DEBUG', 'WP_DEBUG_DISPLAY', 'WP_DEBUG_LOG', 'DISABLE_WP_CRON', 'FORCE_SSL_ADMIN', 'WP_CACHE',
871
- 'CONCATENATE_SCRIPTS', 'COMPRESS_SCRIPTS', 'COMPRESS_CSS', 'ENFORCE_GZIP', 'WP_LOCAL_DEV',
872
- 'WP_CONTENT_URL', 'WP_CONTENT_DIR', 'UPLOADS', 'BLOGUPLOADDIR',
873
- 'DIEONDBERROR', 'WPLANG', 'ALTERNATE_WP_CRON', 'WP_CRON_LOCK_TIMEOUT', 'WP_DISABLE_FATAL_ERROR_HANDLER',
874
- 'MATOMO_SUPPORT_ASYNC_ARCHIVING', 'MATOMO_TRIGGER_BROWSER_ARCHIVING', 'MATOMO_ENABLE_TAG_MANAGER', 'MATOMO_SUPPRESS_DB_ERRORS', 'MATOMO_ENABLE_AUTO_UPGRADE',
875
- 'MATOMO_DEBUG', 'MATOMO_SAFE_MODE', 'MATOMO_GLOBAL_UPLOAD_DIR', 'MATOMO_LOGIN_REDIRECT');
876
- foreach ($consts as $const) {
877
- $rows[] = array(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
878
  'name' => $const,
879
- 'value' => defined( $const ) ? constant( $const) : '-',
880
- );
881
  }
882
 
883
- $rows[] = array(
884
  'name' => 'Permalink Structure',
885
  'value' => get_option( 'permalink_structure' ) ? get_option( 'permalink_structure' ) : 'Default',
886
- );
887
 
888
- $rows[] = array(
889
  'name' => 'Possibly uses symlink',
890
  'value' => strpos( __DIR__, ABSPATH ) === false && strpos( __DIR__, WP_CONTENT_DIR ) === false,
891
- );
892
 
893
  $upload_dir = wp_upload_dir();
894
- $rows[] = array(
895
  'name' => 'Upload base url',
896
  'value' => $upload_dir['baseurl'],
897
- );
898
 
899
- $rows[] = array(
900
  'name' => 'Upload base dir',
901
  'value' => $upload_dir['basedir'],
902
- );
903
 
904
- $rows[] = array(
905
  'name' => 'Upload url',
906
  'value' => $upload_dir['url'],
907
- );
908
 
909
- foreach (['upload_path', 'upload_url_path'] as $option_read) {
910
- $rows[] = array(
911
  'name' => 'Custom ' . $option_read,
912
  'value' => get_option( $option_read ),
913
- );
914
  }
915
 
916
- if (is_plugin_active('wp-piwik/wp-piwik.php')) {
917
- $rows[] = array(
918
- 'name' => 'WP-Matomo (WP-Piwik) activated',
919
- 'value' => true,
920
  'is_warning' => true,
921
- 'comment' => 'It is usually not recommended or needed to run Matomo for WordPress and WP-Matomo at the same time. To learn more about the differences between the two plugins view this URL: https://matomo.org/faq/wordpress/why-are-there-two-different-matomo-for-wordpress-plugins-what-is-the-difference-to-wp-matomo-integration-plugin/'
922
- );
923
 
924
- $mode = get_option ( 'wp-piwik_global-piwik_mode' );
925
- if (function_exists('get_site_option') && is_plugin_active_for_network ( 'wp-piwik/wp-piwik.php' )) {
926
- $mode = get_site_option ( 'wp-piwik_global-piwik_mode');
927
  }
928
- if (!empty($mode)) {
929
- $rows[] = array(
930
- 'name' => 'WP-Matomo mode',
931
- 'value' => $mode,
932
- 'is_warning' => $mode === 'php' || $mode === 'PHP',
933
- 'comment' => 'WP-Matomo is configured in "PHP mode". This is known to cause issues with Matomo for WordPress. We recommend you either deactivate WP-Matomo or you go "Settings => WP-Matomo" and change the "Matomo Mode" in the "Connect to Matomo" section to "Self-hosted HTTP API".'
934
- );
935
  }
936
  }
937
 
938
  $compatible_content_dir = matomo_has_compatible_content_dir();
939
- if ($compatible_content_dir === true) {
940
- $rows[] = array(
941
  'name' => 'Compatible content directory',
942
  'value' => true,
943
- );
944
  } else {
945
- $rows[] = array(
946
- 'name' => 'Compatible content directory',
947
- 'value' => $compatible_content_dir,
948
  'is_warning' => true,
949
- 'comment' => __( 'It looks like you are maybe using a custom WordPress content directory. The Matomo reporting/admin pages might not work. You may be able to workaround this.', 'matomo' ) . ' ' . __( 'Learn more', 'matomo' ) . ': https://matomo.org/faq/wordpress/how-do-i-make-matomo-for-wordpress-work-when-i-have-a-custom-content-directory/'
950
- );
951
  }
952
 
953
  return $rows;
954
  }
955
 
956
  private function get_server_info() {
957
- $rows = array();
958
 
959
  if ( ! empty( $_SERVER['SERVER_SOFTWARE'] ) ) {
960
- $rows[] = array(
961
  'name' => 'Server Info',
962
- 'value' => $_SERVER['SERVER_SOFTWARE'],
963
- );
 
964
  }
965
  if ( PHP_OS ) {
966
- $rows[] = array(
967
  'name' => 'PHP OS',
968
  'value' => PHP_OS,
969
- );
970
  }
971
- $rows[] = array(
972
  'name' => 'PHP Version',
973
  'value' => phpversion(),
974
- );
975
- $rows[] = array(
976
  'name' => 'PHP SAPI',
977
  'value' => php_sapi_name(),
978
- );
979
- if (defined('PHP_BINARY') && PHP_BINARY) {
980
- $rows[] = array(
981
  'name' => 'PHP Binary Name',
982
- 'value' => @basename(PHP_BINARY),
983
- );
984
  }
985
  // we report error reporting before matomo bootstraped and after to see if Matomo changed it successfully etc
986
- $rows[] = array(
987
  'name' => 'PHP Error Reporting',
988
- 'value' => $this->initial_error_reporting . ' After bootstrap: ' . @error_reporting()
989
- );
990
- if (!\WpMatomo::is_safe_mode()) {
 
991
  Bootstrap::do_bootstrap();
992
- $cliPhp = new CliMulti\CliPhp();
993
- $binary = $cliPhp->findPhpBinary();
994
- if (!empty($binary)) {
995
- $binary = basename($binary);
996
- $rows[] = array(
997
  'name' => 'PHP Found Binary',
998
  'value' => $binary,
999
- );
1000
  }
1001
  }
1002
- $rows[] = array(
1003
  'name' => 'Timezone',
1004
  'value' => date_default_timezone_get(),
1005
- );
1006
- if (function_exists('wp_timezone_string')) {
1007
- $rows[] = array(
1008
  'name' => 'WP timezone',
1009
  'value' => wp_timezone_string(),
1010
- );
1011
  }
1012
- $rows[] = array(
1013
  'name' => 'Locale',
1014
  'value' => get_locale(),
1015
- );
1016
- if (function_exists('get_user_locale')) {
1017
- $rows[] = array(
1018
  'name' => 'User Locale',
1019
  'value' => get_user_locale(),
1020
- );
1021
  }
1022
 
1023
- $rows[] = array(
1024
  'name' => 'Memory Limit',
1025
  'value' => @ini_get( 'memory_limit' ),
1026
  'comment' => 'At least 128MB recommended. Depending on your traffic 256MB or more may be needed.',
1027
- );
1028
 
1029
- $rows[] = array(
1030
  'name' => 'WP Memory Limit',
1031
  'value' => defined( 'WP_MEMORY_LIMIT' ) ? WP_MEMORY_LIMIT : '',
1032
  'comment' => '',
1033
- );
1034
 
1035
- $rows[] = array(
1036
  'name' => 'WP Max Memory Limit',
1037
  'value' => defined( 'WP_MAX_MEMORY_LIMIT' ) ? WP_MAX_MEMORY_LIMIT : '',
1038
  'comment' => '',
1039
- );
1040
-
1041
- if (function_exists('timezone_version_get')) {
1042
- $rows[] = array(
1043
  'name' => 'Timezone version',
1044
  'value' => timezone_version_get(),
1045
- );
1046
  }
1047
-
1048
- $rows[] = array(
1049
  'name' => 'Time',
1050
  'value' => time(),
1051
- );
1052
 
1053
- $rows[] = array(
1054
  'name' => 'Max Execution Time',
1055
  'value' => ini_get( 'max_execution_time' ),
1056
- );
1057
- $rows[] = array(
1058
  'name' => 'Max Post Size',
1059
  'value' => ini_get( 'post_max_size' ),
1060
- );
1061
- $rows[] = array(
1062
  'name' => 'Max Upload Size',
1063
  'value' => wp_max_upload_size(),
1064
- );
1065
- $rows[] = array(
1066
  'name' => 'Max Input Vars',
1067
  'value' => ini_get( 'max_input_vars' ),
1068
- );
1069
 
1070
- $disabled_functions = ini_get('disable_functions');
1071
- $rows[] = array(
1072
- 'name' => 'Disabled PHP functions',
1073
- 'value' => !empty($disabled_functions),
1074
- 'comment' => !empty($disabled_functions) ? $disabled_functions : ''
1075
- );
1076
 
1077
  $zlib_compression = ini_get( 'zlib.output_compression' );
1078
- $row = array(
1079
  'name' => 'zlib.output_compression is off',
1080
- 'value' => $zlib_compression !== '1',
1081
- );
1082
 
1083
- if ( $zlib_compression === '1' ) {
1084
  $row['is_error'] = true;
1085
  $row['comment'] = 'You need to set "zlib.output_compression" in your php.ini to "Off".';
1086
  }
@@ -1089,108 +1148,108 @@ class SystemReport {
1089
  if ( function_exists( 'curl_version' ) ) {
1090
  $curl_version = curl_version();
1091
  $curl_version = $curl_version['version'] . ', ' . $curl_version['ssl_version'];
1092
- $rows[] = array(
1093
  'name' => 'Curl Version',
1094
  'value' => $curl_version,
1095
- );
1096
  }
1097
 
1098
  $suhosin_installed = ( extension_loaded( 'suhosin' ) || ( defined( 'SUHOSIN_PATCH' ) && constant( 'SUHOSIN_PATCH' ) ) );
1099
- $rows[] = array(
1100
- 'name' => 'Suhosin installed',
1101
- 'value' => !empty($suhosin_installed),
1102
- 'comment' => ''
1103
- );
1104
 
1105
  return $rows;
1106
  }
1107
 
1108
  private function get_browser_info() {
1109
- $rows = array();
1110
 
1111
- if (!empty($_SERVER['HTTP_USER_AGENT'])) {
1112
- $rows[] = array(
1113
  'name' => 'Browser',
1114
  'value' => '',
1115
- 'comment' => $_SERVER['HTTP_USER_AGENT']
1116
- );
 
1117
  }
1118
- if (!\WpMatomo::is_safe_mode()) {
1119
  Bootstrap::do_bootstrap();
1120
  try {
1121
- if (!empty($_SERVER['HTTP_USER_AGENT'])) {
1122
- $detector = StaticContainer::get(DeviceDetectorFactory::class)->makeInstance($_SERVER['HTTP_USER_AGENT']);
1123
- $client = $detector->getClient();
1124
- if (!empty($client['name']) && $client['name'] === 'Microsoft Edge' && (int) $client['version'] >= 85) {
1125
- $rows[] = array(
1126
- 'name' => 'Browser Compatibility',
 
1127
  'is_warning' => true,
1128
- 'value' => 'Yes',
1129
- 'comment' => 'Because you are using MS Edge browser, you may see a warning like "This site has been reported as unsafe" from "Microsoft Defender SmartScreen" when you view the Matomo Reporting, Admin or Tag Manager page. This is a false alert and you can safely ignore this warning by clicking on the icon next to the URL (in the address bar) and choosing either "Report as safe" (preferred) or "Show unsafe content". We are hoping to get this false warning removed in the future.'
1130
- );
1131
  }
1132
  }
1133
-
1134
- } catch (\Exception $e) {
1135
-
1136
  }
1137
 
1138
- $rows[] = array(
1139
  'name' => 'Language',
1140
  'value' => Common::getBrowserLanguage(),
1141
- 'comment' => ''
1142
- );
1143
  }
1144
 
1145
-
1146
  return $rows;
1147
  }
1148
 
1149
  private function get_db_info() {
1150
  global $wpdb;
1151
- $rows = array();
1152
 
1153
- $rows[] = array(
1154
  'name' => 'MySQL Version',
1155
  'value' => ! empty( $wpdb->is_mysql ) ? $wpdb->db_version() : '',
1156
  'comment' => '',
1157
- );
1158
 
1159
- $rows[] = array(
1160
  'name' => 'Mysqli Connect',
1161
  'value' => function_exists( 'mysqli_connect' ),
1162
  'comment' => '',
1163
- );
1164
- $rows[] = array(
1165
  'name' => 'Force MySQL over Mysqli',
1166
  'value' => defined( 'WP_USE_EXT_MYSQL' ) && WP_USE_EXT_MYSQL,
1167
  'comment' => '',
1168
- );
1169
 
1170
- $rows[] = array(
1171
  'name' => 'DB Prefix',
1172
  'value' => $wpdb->prefix,
1173
- );
1174
 
1175
- $rows[] = array(
1176
  'name' => 'DB CHARSET',
1177
- 'value' => defined('DB_CHARSET') ? DB_CHARSET : '',
1178
- );
1179
 
1180
- $rows[] = array(
1181
  'name' => 'DB COLLATE',
1182
- 'value' => defined('DB_COLLATE') ? DB_COLLATE : '',
1183
- );
1184
 
1185
- $rows[] = array(
1186
  'name' => 'SHOW ERRORS',
1187
- 'value' => !empty($wpdb->show_errors),
1188
- );
1189
 
1190
- $rows[] = array(
1191
  'name' => 'SUPPRESS ERRORS',
1192
- 'value' => !empty($wpdb->suppress_errors),
1193
- );
1194
 
1195
  if ( method_exists( $wpdb, 'parse_db_host' ) ) {
1196
  $host_data = $wpdb->parse_db_host( DB_HOST );
@@ -1198,41 +1257,52 @@ class SystemReport {
1198
  list( $host, $port, $socket, $is_ipv6 ) = $host_data;
1199
  }
1200
 
1201
- $rows[] = array(
1202
  'name' => 'Uses Socket',
1203
  'value' => ! empty( $socket ),
1204
- );
1205
- $rows[] = array(
1206
  'name' => 'Uses IPv6',
1207
  'value' => ! empty( $is_ipv6 ),
1208
- );
1209
  }
1210
 
1211
- $rows[] = array(
1212
  'name' => 'Matomo tables found',
1213
  'value' => $this->get_num_matomo_tables(),
1214
- );
1215
-
1216
- $missing_tables = $this->get_missing_tables();
1217
- $has_missing_tables = ( count($missing_tables) > 0 );
1218
- $rows[] = array(
1219
- 'name' => 'DB tables exist',
1220
- 'value' => ( ! $has_missing_tables ) ,
1221
- 'comment' => $has_missing_tables ? sprintf( __('Some tables may be missing: %s', 'matomo'), implode(', ', $missing_tables ) ) : '',
1222
- 'is_error' => $has_missing_tables
1223
- );
1224
-
1225
- foreach (['user', 'site'] as $table) {
1226
- $rows[] = array(
1227
- 'name' => 'Matomo '.$table.'s found',
1228
- 'value' => $this->get_num_entries_in_table($table),
1229
- );
1230
  }
1231
 
1232
  $grants = $this->get_db_grants();
1233
 
1234
  // we only show these grants for security reasons as only they are needed and we don't need to know any other ones
1235
- $needed_grants = array( 'SELECT', 'INSERT', 'UPDATE', 'INDEX', 'DELETE', 'CREATE', 'DROP', 'ALTER', 'CREATE TEMPORARY TABLES', 'LOCK TABLES' );
 
 
 
 
 
 
 
 
 
 
 
1236
  if ( in_array( 'ALL PRIVILEGES', $grants, true ) ) {
1237
  // ALL PRIVILEGES may be used pre MySQL 8.0
1238
  $grants = $needed_grants;
@@ -1243,27 +1313,27 @@ class SystemReport {
1243
  if ( empty( $grants )
1244
  || ! is_array( $grants )
1245
  || count( $grants_missing ) === count( $needed_grants ) ) {
1246
- $rows[] = array(
1247
  'name' => esc_html__( 'Required permissions', 'matomo' ),
1248
  'value' => esc_html__( 'Failed to detect granted permissions', 'matomo' ),
1249
  'comment' => esc_html__( 'Please check your MySQL user has these permissions (grants):', 'matomo' ) . '<br />' . implode( ', ', $needed_grants ),
1250
  'is_warning' => false,
1251
- );
1252
  } else {
1253
  if ( ! empty( $grants_missing ) ) {
1254
- $rows[] = array(
1255
  'name' => esc_html__( 'Required permissions', 'matomo' ),
1256
  'value' => esc_html__( 'Error', 'matomo' ),
1257
  'comment' => esc_html__( 'Missing permissions', 'matomo' ) . ': ' . implode( ', ', $grants_missing ) . '. ' . __( 'Please check if any of these MySQL permission (grants) are missing and add them if needed.', 'matomo' ) . ' ' . __( 'Learn more', 'matomo' ) . ': https://matomo.org/faq/troubleshooting/how-do-i-check-if-my-mysql-user-has-all-required-grants/',
1258
  'is_warning' => true,
1259
- );
1260
  } else {
1261
- $rows[] = array(
1262
  'name' => esc_html__( 'Required permissions', 'matomo' ),
1263
  'value' => esc_html__( 'OK', 'matomo' ),
1264
  'comment' => '',
1265
  'is_warning' => false,
1266
- );
1267
  }
1268
  }
1269
 
@@ -1276,31 +1346,33 @@ class SystemReport {
1276
  public function get_missing_tables() {
1277
  global $wpdb;
1278
 
1279
- $required_matomo_tables = $this->dbSettings->get_matomo_tables();
1280
- $required_matomo_tables = array_map( array( $this->dbSettings, 'prefix_table_name' ), $required_matomo_tables );
1281
 
1282
- $existing_tables = array();
1283
  try {
1284
- $prefix = $this->dbSettings->prefix_table_name('');
1285
  $existing_tables = $wpdb->get_col( 'SHOW TABLES LIKE "' . $prefix . '%"' );
1286
- } catch (\Exception $e) {
1287
  $this->logger->log( 'no show tables: ' . $e->getMessage() );
1288
  }
 
1289
  return array_diff( $required_matomo_tables, $existing_tables );
1290
  }
1291
 
1292
- private function get_num_entries_in_table($table) {
1293
  global $wpdb;
1294
 
1295
- $prefix = $this->dbSettings->prefix_table_name($table);
1296
 
1297
  $results = null;
1298
  try {
1299
- $results = $wpdb->get_var('select count(*) from '.$prefix);
1300
- } catch (\Exception $e) {
 
1301
  }
1302
 
1303
- if (isset($results) && is_numeric($results)) {
1304
  return $results;
1305
  }
1306
 
@@ -1310,17 +1382,17 @@ class SystemReport {
1310
  private function get_num_matomo_tables() {
1311
  global $wpdb;
1312
 
1313
- $prefix = $this->dbSettings->prefix_table_name('');
1314
 
1315
  $results = null;
1316
  try {
1317
- $results = $wpdb->get_results('show tables like "'.$prefix.'%"');
1318
- } catch (\Exception $e) {
1319
- $this->logger->log('no show tables: ' . $e->getMessage());
1320
  }
1321
 
1322
- if (is_array($results)) {
1323
- return count($results);
1324
  }
1325
 
1326
  return 'show tables not working';
@@ -1334,24 +1406,24 @@ class SystemReport {
1334
 
1335
  try {
1336
  $values = $wpdb->get_results( 'SHOW GRANTS', ARRAY_N );
1337
- } catch ( \Exception $e ) {
1338
  // We ignore any possible error in case of permission or not supported etc.
1339
- $values = array();
1340
  }
1341
 
1342
  $wpdb->suppress_errors( $suppress_errors );
1343
 
1344
- $grants = array();
1345
  foreach ( $values as $index => $value ) {
1346
  if ( empty( $value[0] ) || ! is_string( $value[0] ) ) {
1347
  continue;
1348
  }
1349
 
1350
  if ( stripos( $value[0], 'ALL PRIVILEGES' ) !== false ) {
1351
- return array( 'ALL PRIVILEGES' ); // the split on empty string wouldn't work otherwise
1352
  }
1353
 
1354
- foreach ( array( ' ON ', ' TO ', ' IDENTIFIED ', ' BY ' ) as $keyword ) {
1355
  if ( stripos( $values[ $index ][0], $keyword ) !== false ) {
1356
  // make sure to never show by any accident a db user or password by cutting anything after on/to
1357
  $values[ $index ][0] = substr( $value[0], 0, stripos( $value[0], $keyword ) );
@@ -1362,40 +1434,48 @@ class SystemReport {
1362
  }
1363
  }
1364
  // make sure to never show by any accident a db user or password
1365
- $values[ $index ][0] = str_replace( array( DB_USER, DB_PASSWORD ), array( 'DB_USER', 'DB_PASS' ), $values[ $index ][0] );
 
 
 
 
 
 
 
1366
 
1367
  $grants = array_merge( $grants, explode( ',', $values[ $index ][0] ) );
1368
  }
1369
  $grants = array_map( 'trim', $grants );
1370
  $grants = array_map( 'strtoupper', $grants );
1371
  $grants = array_unique( $grants );
 
1372
  return $grants;
1373
  }
1374
 
1375
  private function get_plugins_info() {
1376
- $rows = array();
1377
  $mu_plugins = get_mu_plugins();
1378
 
1379
  if ( ! empty( $mu_plugins ) ) {
1380
- $rows[] = array(
1381
  'section' => 'MU Plugins',
1382
- );
1383
 
1384
  foreach ( $mu_plugins as $mu_pin ) {
1385
  $comment = '';
1386
  if ( ! empty( $plugin['Network'] ) ) {
1387
  $comment = 'Network enabled';
1388
  }
1389
- $rows[] = array(
1390
  'name' => $mu_pin['Name'],
1391
  'value' => $mu_pin['Version'],
1392
  'comment' => $comment,
1393
- );
1394
  }
1395
 
1396
- $rows[] = array(
1397
  'section' => 'Plugins',
1398
- );
1399
  }
1400
 
1401
  $plugins = get_plugins();
@@ -1405,79 +1485,76 @@ class SystemReport {
1405
  if ( ! empty( $plugin['Network'] ) ) {
1406
  $comment = 'Network enabled';
1407
  }
1408
- $rows[] = array(
1409
  'name' => $plugin['Name'],
1410
  'value' => $plugin['Version'],
1411
  'comment' => $comment,
1412
- );
1413
  }
1414
 
1415
- $active_plugins = get_option( 'active_plugins', array() );
1416
 
1417
  if ( ! empty( $active_plugins ) && is_array( $active_plugins ) ) {
1418
  $active_plugins = array_map(
1419
  function ( $active_plugin ) {
1420
  $parts = explode( '/', trim( $active_plugin ) );
 
1421
  return trim( $parts[0] );
1422
  },
1423
  $active_plugins
1424
  );
1425
 
1426
- $rows[] = array(
1427
  'name' => 'Active Plugins',
1428
  'value' => count( $active_plugins ),
1429
  'comment' => implode( ' ', $active_plugins ),
1430
- );
1431
 
1432
  $used_not_compatible = array_intersect( $active_plugins, $this->not_compatible_plugins );
1433
  if ( ! empty( $used_not_compatible ) ) {
1434
-
1435
  $additional_comment = '';
1436
- if (in_array('tweet-old-post-pro', $used_not_compatible)) {
1437
  $additional_comment .= '<br><br>A workaround for Revive Old Posts Pro may be to add the following line to your "wp-config.php". <br><code>define( \'MATOMO_SUPPORT_ASYNC_ARCHIVING\', false );</code>.';
1438
  }
1439
- if (in_array('secupress', $used_not_compatible)) {
1440
  $additional_comment .= '<br><br>If reports aren\'t being generated then you may need to disable the feature "Firewall -> Block Bad Request Methods" in SecuPress (if it is enabled) or add the following line to your "wp-config.php": <br><code>define( \'MATOMO_SUPPORT_ASYNC_ARCHIVING\', false );</code>.';
1441
  }
1442
 
1443
  $is_warning = true;
1444
- $is_error = false;
1445
- if (in_array('cookiebot', $used_not_compatible)) {
1446
  $is_warning = false;
1447
- $is_error = true;
1448
  }
1449
 
1450
- $rows[] = array(
1451
- 'name' => __( 'Not compatible plugins', 'matomo' ),
1452
- 'value' => count( $used_not_compatible ),
1453
- 'comment' => implode( ', ', $used_not_compatible ) . '<br><br> Matomo may work fine when using these plugins but there may be some issues. For more information see<br>https://matomo.org/faq/wordpress/which-plugins-is-matomo-for-wordpress-known-to-be-not-compatible-with/ ' . $additional_comment,
1454
  'is_warning' => $is_warning,
1455
- 'is_error' => $is_error,
1456
- );
1457
  }
1458
  }
1459
 
1460
- $rows[] = array(
1461
- 'name' => 'Theme',
1462
- 'value' => function_exists('get_template') ? get_template() : '',
1463
- 'comment' => get_option('stylesheet')
1464
- );
1465
-
1466
 
1467
- if ( is_plugin_active('better-wp-security/better-wp-security.php')) {
1468
- if (method_exists('\ITSEC_Modules', 'get_setting')
1469
- && \ITSEC_Modules::get_setting( 'system-tweaks', 'long_url_strings' ) ) {
1470
- $rows[] = array(
1471
  'name' => 'iThemes Security Long URLs Enabled',
1472
  'value' => true,
1473
  'comment' => 'Tracking might not work because it looks like you have Long URLs disabled in iThemes Security. To fix this please go to "Security -> Settings -> System Tweaks" and disable the setting "Long URL Strings".',
1474
  'is_error' => true,
1475
- );
1476
  }
1477
  }
1478
 
1479
  return $rows;
1480
  }
1481
-
1482
-
1483
  }
9
 
10
  namespace WpMatomo\Admin;
11
 
12
+ use Exception;
13
+ use ITSEC_Modules;
14
  use Piwik\CliMulti;
15
  use Piwik\Common;
16
  use Piwik\Config;
25
  use Piwik\SettingsPiwik;
26
  use Piwik\Tracker\Failures;
27
  use Piwik\Version;
28
+ use WpMatomo;
29
  use WpMatomo\Bootstrap;
30
  use WpMatomo\Capabilities;
31
  use WpMatomo\Installer;
42
  exit; // if accessed directly
43
  }
44
 
45
+ /**
46
+ * error_reporting is required for this page
47
+ * phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_error_reporting
48
+ *
49
+ * We want a real data, not something coming from cache
50
+ * phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
51
+ *
52
+ * This is a report error, so silent the possible errors
53
+ * phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
54
+ *
55
+ * We cannot use parameters of statements as this is the table names we build
56
+ * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
57
+ * phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
58
+ */
59
  class SystemReport {
60
  const NONCE_NAME = 'matomo_troubleshooting';
61
  const TROUBLESHOOT_SYNC_USERS = 'matomo_troubleshooting_action_site_users';
68
  const TROUBLESHOOT_CLEAR_LOGS = 'matomo_troubleshooting_action_clear_logs';
69
  const TROUBLESHOOT_RUN_UPDATER = 'matomo_troubleshooting_action_run_updater';
70
 
71
+ private $not_compatible_plugins = [
72
+ 'background-manager',
73
+ // Uses an old version of Twig and plugin is no longer maintained.
74
+ 'all-in-one-event-calendar',
75
+ // Uses an old version of Twig
76
+ 'data-tables-generator-by-supsystic',
77
+ // uses an old version of twig causing some styles to go funny in the reporting and admin
78
+ 'tweet-old-post-pro',
79
+ // uses a newer version of monolog
80
+ 'wp-rss-aggregator',
81
+ // see https://wordpress.org/support/topic/critical-error-after-upgrade/ conflict re php-di version
82
+ 'wp-defender',
83
+ // see https://wordpress.org/support/topic/critical-error-after-upgrade/ conflict re php-di version
84
+ 'age-verification-for-woocommerce',
85
+ // see https://github.com/matomo-org/wp-matomo/issues/428
86
+ 'minify-html-markup',
87
+ // see https://wordpress.org/support/topic/graphs-are-not-displayed-in-the-visits-overview-widget/#post-14298068
88
+ 'bigbuy-wc-dropshipping-connector',
89
+ // see https://wordpress.org/support/topic/20-total-errors-during-this-script-execution/
90
+ 'google-listings-and-ads',
91
+ // see https://wordpress.org/support/topic/20-total-errors-during-this-script-execution/
92
+ 'accelerated-mobile-pages',
93
+ // see https://wordpress.org/support/topic/receiving-errors-from-my-plesk-server/
94
+ ];
95
+
96
+ private $valid_tabs = [ 'troubleshooting' ];
97
 
98
  /**
99
  * @var Settings
109
  /**
110
  * @var \WpMatomo\Db\Settings
111
  */
112
+ public $db_settings;
113
 
114
  public function __construct( Settings $settings ) {
115
+ $this->settings = $settings;
116
+ $this->logger = new Logger();
117
+ $this->db_settings = new \WpMatomo\Db\Settings();
118
  }
119
 
120
  public function get_not_compatible_plugins() {
130
  Bootstrap::do_bootstrap();
131
  $scheduled_tasks = new ScheduledTasks( $this->settings );
132
 
133
+ if ( ! defined( 'PIWIK_ARCHIVE_NO_TRUNCATE' ) ) {
134
+ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
135
+ define( 'PIWIK_ARCHIVE_NO_TRUNCATE', 1 ); // when triggering it manually, we prefer the full error message
136
  }
137
 
138
  try {
139
  // force invalidation of archive to ensure it actually will rearchive the data
140
+ $site = new Site();
141
  $idsite = $site->get_current_matomo_site_id();
142
+ if ( $idsite ) {
143
+ $timezone = \Piwik\Site::getTimezoneFor( $idsite );
144
+ $now_string = \Piwik\Date::factory( 'now', $timezone )->toString();
145
+ foreach ( [ 'day' ] as $period ) {
146
+ API::getInstance()->invalidateArchivedReports( $idsite, $now_string, $period, false, false );
147
  }
148
  }
149
+ } catch ( Exception $e ) {
150
+ $this->logger->log_exception( 'archive_invalidate', $e );
151
  }
152
 
153
  try {
154
+ $errors = $scheduled_tasks->archive( true, false );
155
+ } catch ( Exception $e ) {
156
+ echo '<div class="error"><p>' . esc_html__( 'Matomo Archive Error', 'matomo' ) . ': ' . esc_html( matomo_anonymize_value( $e->getMessage() . ' =>' . $this->logger->get_readable_trace( $e ) ) ) . '</p></div>';
157
  throw $e;
158
  }
159
 
160
  if ( ! empty( $errors ) ) {
161
  echo '<div class="notice notice-warning"><p>Matomo Archive Warnings: ';
162
+ foreach ( $errors as $error ) {
163
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
164
+ echo nl2br( esc_html( matomo_anonymize_value( var_export( $error, 1 ) ) ) );
165
  echo '<br/>';
166
  }
167
  echo '</p></div>';
221
  $settings = $this->settings;
222
 
223
  $matomo_active_tab = '';
224
+
225
+ if ( isset( $_GET['tab'] ) ) {
226
+ $tab = sanitize_text_field( wp_unslash( $_GET['tab'] ) );
227
+ if ( in_array( $tab, $this->valid_tabs, true ) ) {
228
+ $matomo_active_tab = $tab;
229
+ }
230
  }
231
 
232
+ $matomo_tables = [];
233
  if ( empty( $matomo_active_tab ) ) {
234
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting
235
  $this->initial_error_reporting = @error_reporting();
236
+ $matomo_tables = [
237
+ [
238
  'title' => 'Matomo',
239
  'rows' => $this->get_matomo_info(),
240
  'has_comments' => true,
241
+ ],
242
+ [
243
+ 'title' => 'WordPress',
244
+ 'rows' => $this->get_wordpress_info(),
245
  'has_comments' => true,
246
+ ],
247
+ [
248
  'title' => 'WordPress Plugins',
249
  'rows' => $this->get_plugins_info(),
250
  'has_comments' => true,
251
+ ],
252
+ [
253
  'title' => 'Server',
254
  'rows' => $this->get_server_info(),
255
  'has_comments' => true,
256
+ ],
257
+ [
258
  'title' => 'Database',
259
  'rows' => $this->get_db_info(),
260
  'has_comments' => true,
261
+ ],
262
+ [
263
  'title' => 'Browser',
264
  'rows' => $this->get_browser_info(),
265
  'has_comments' => true,
266
+ ],
267
+ ];
268
  }
269
+ $matomo_tables = apply_filters( 'matomo_systemreport_tables', $matomo_tables );
270
  $matomo_tables = $this->add_errors_first( $matomo_tables );
271
  $matomo_has_warning_and_no_errors = $this->has_only_warnings_no_error( $matomo_tables );
272
 
293
  }
294
 
295
  private function add_errors_first( $report_tables ) {
296
+ $errors = [
297
  'title' => 'Errors',
298
+ 'rows' => [],
299
  'has_comments' => true,
300
+ ];
301
  foreach ( $report_tables as $report_table ) {
302
  foreach ( $report_table['rows'] as $row ) {
303
  if ( ! empty( $row['is_error'] ) ) {
328
  $comment .= sprintf( esc_html__( '%s is not writable. ', 'matomo' ), $title );
329
  }
330
 
331
+ $rows[] = [
332
  'name' => sprintf( esc_html__( '%s exists and is writable.', 'matomo' ), $title ),
333
  'value' => $file_exists && $file_readable && $file_writable ? esc_html__( 'Yes', 'matomo' ) : esc_html__( 'No', 'matomo' ),
334
  'comment' => $comment,
335
  'is_error' => $required && ( ! $file_exists || ! $file_readable ),
336
  'is_warning' => ! $required && ( ! $file_exists || ! $file_readable ),
337
+ ];
338
 
339
  return $rows;
340
  }
341
 
342
  private function get_matomo_info() {
343
+ $rows = [];
344
 
345
  $plugin_data = get_plugin_data( MATOMO_ANALYTICS_FILE, $markup = false, $translate = false );
346
+ $install_time = get_option( Installer::OPTION_NAME_INSTALL_DATE );
347
 
348
+ $rows[] = [
349
  'name' => esc_html__( 'Matomo Plugin Version', 'matomo' ),
350
  'value' => $plugin_data['Version'],
351
  'comment' => '',
352
+ ];
353
 
354
  $paths = new Paths();
355
  $path_config_file = $paths->get_config_ini_path();
358
  $path_tracker_file = $paths->get_matomo_js_upload_path();
359
  $rows = $this->check_file_exists_and_writable( $rows, $path_tracker_file, 'JS Tracker', false );
360
 
361
+ $rows[] = [
362
  'name' => esc_html__( 'Plugin directories', 'matomo' ),
363
  'value' => ! empty( $GLOBALS['MATOMO_PLUGIN_DIRS'] ) ? 'Yes' : 'No',
364
  'comment' => ! empty( $GLOBALS['MATOMO_PLUGIN_DIRS'] ) ? wp_json_encode( $GLOBALS['MATOMO_PLUGIN_DIRS'] ) : '',
365
+ ];
366
 
367
  $tmp_dir = $paths->get_tmp_dir();
368
 
369
+ $rows[] = [
370
  'name' => esc_html__( 'Tmp directory writable', 'matomo' ),
371
  'value' => is_writable( $tmp_dir ),
372
  'comment' => $tmp_dir,
373
+ ];
374
 
375
  if ( ! empty( $_SERVER['MATOMO_WP_ROOT_PATH'] ) ) {
376
+ // we can have / in this value
377
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
378
  $custom_path = rtrim( $_SERVER['MATOMO_WP_ROOT_PATH'], '/' ) . '/wp-load.php';
379
  $path_exists = file_exists( $custom_path );
380
  $comment = '';
382
  $comment = 'It seems the path does not point to the WP root directory.';
383
  }
384
 
385
+ $rows[] = [
386
  'name' => 'Custom MATOMO_WP_ROOT_PATH',
387
  'value' => $path_exists,
388
  'is_error' => ! $path_exists,
389
  'comment' => $comment,
390
+ ];
391
  }
392
 
393
  $report = null;
394
 
395
+ if ( ! WpMatomo::is_safe_mode() ) {
396
  try {
397
  Bootstrap::do_bootstrap();
398
  /** @var DiagnosticService $service */
399
  $service = StaticContainer::get( DiagnosticService::class );
400
  $report = $service->runDiagnostics();
401
 
402
+ $rows[] = [
403
  'name' => esc_html__( 'Matomo Version', 'matomo' ),
404
  'value' => \Piwik\Version::VERSION,
405
  'comment' => '',
406
+ ];
407
+ } catch ( Exception $e ) {
408
+ $rows[] = [
409
  'name' => esc_html__( 'Matomo System Check', 'matomo' ),
410
  'value' => 'Failed to run Matomo system check.',
411
  'comment' => $e->getMessage(),
412
+ ];
413
  }
414
  }
415
 
416
  $site = new Site();
417
  $idsite = $site->get_current_matomo_site_id();
418
 
419
+ $rows[] = [
420
  'name' => esc_html__( 'Matomo Blog idSite', 'matomo' ),
421
  'value' => $idsite,
422
  'comment' => '',
423
+ ];
424
 
425
  $install_date = '';
426
+ if ( ! empty( $install_time ) ) {
427
+ $install_date = 'Install date: ' . $this->convert_time_to_date( $install_time, true, false );
428
  }
429
 
430
+ $rows[] = [
431
  'name' => esc_html__( 'Matomo Install Version', 'matomo' ),
432
+ 'value' => get_option( Installer::OPTION_NAME_INSTALL_VERSION ),
433
  'comment' => $install_date,
434
+ ];
 
 
 
435
 
436
+ $wpmatomo_updater = new \WpMatomo\Updater( $this->settings );
437
+ if ( ! WpMatomo::is_safe_mode() ) {
438
  $outstanding_updates = $wpmatomo_updater->get_plugins_requiring_update();
439
  $upgrade_in_progress = $wpmatomo_updater->is_upgrade_in_progress();
440
+ $rows[] = [
441
+ 'name' => 'Upgrades outstanding',
442
+ 'value' => ! empty( $outstanding_updates ),
443
+ 'comment' => ! empty( $outstanding_updates ) ? wp_json_encode( $outstanding_updates ) : '',
444
+ ];
445
+ $rows[] = [
446
+ 'name' => 'Upgrade in progress',
447
+ 'value' => $upgrade_in_progress,
448
+ 'comment' => '',
449
+ ];
450
  }
451
 
452
+ if ( ! $wpmatomo_updater->load_plugin_functions() ) {
453
  // this should actually never happen...
454
+ $rows[] = [
455
+ 'name' => 'Matomo Upgrade Plugin Functions',
456
+ 'is_warning' => true,
457
+ 'value' => false,
458
+ 'comment' => 'Function "get_plugin_data" not available. There may be an issue with upgrades not being executed. Please reach out to us.',
459
+ ];
460
  }
461
 
462
+ $rows[] = [
463
  'section' => 'Endpoints',
464
+ ];
465
 
466
+ $rows[] = [
467
  'name' => 'Matomo JavaScript Tracker URL',
468
  'value' => '',
469
  'comment' => $paths->get_js_tracker_url_in_matomo_dir(),
470
+ ];
471
 
472
+ $rows[] = [
473
  'name' => 'Matomo JavaScript Tracker - WP Rest API',
474
  'value' => '',
475
  'comment' => $paths->get_js_tracker_rest_api_endpoint(),
476
+ ];
477
 
478
+ $rows[] = [
479
  'name' => 'Matomo HTTP Tracking API',
480
  'value' => '',
481
  'comment' => $paths->get_tracker_api_url_in_matomo_dir(),
482
+ ];
483
 
484
+ $rows[] = [
485
  'name' => 'Matomo HTTP Tracking API - WP Rest API',
486
  'value' => '',
487
  'comment' => $paths->get_tracker_api_rest_api_endpoint(),
488
+ ];
489
 
490
+ $matomo_plugin_dir_name = basename( dirname( MATOMO_ANALYTICS_FILE ) );
491
+ if ( 'matomo' !== $matomo_plugin_dir_name ) {
492
+ $rows[] = [
493
+ 'name' => 'Matomo Plugin Name is correct',
494
+ 'value' => false,
495
  'is_error' => true,
496
+ 'comment' => 'The plugin name should be "matomo" but seems to be "' . $matomo_plugin_dir_name . '". As a result, admin pages and other features might not work. You might need to rename the directory name of this plugin and reactive the plugin.',
497
+ ];
498
+ } elseif ( ! is_plugin_active( 'matomo/matomo.php' ) ) {
499
+ $rows[] = [
500
+ 'name' => 'Matomo Plugin not active',
501
+ 'value' => false,
502
  'is_error' => true,
503
+ 'comment' => 'It seems WordPress thinks that `matomo/matomo.php` is not active. As a result Matomo reporting and admin pages may not work. You may be able to fix this by deactivating and activating the Matomo Analytics plugin. One of the reasons this could happen is that you used to have Matomo installed in the wrong folder.',
504
+ ];
505
  }
506
 
507
+ $rows[] = [
508
  'section' => 'Crons',
509
+ ];
510
 
511
  $scheduled_tasks = new ScheduledTasks( $this->settings );
512
  $all_events = $scheduled_tasks->get_all_events();
513
 
514
+ $rows[] = [
515
  'name' => esc_html__( 'Server time', 'matomo' ),
516
  'value' => $this->convert_time_to_date( time(), false ),
517
  'comment' => '',
518
+ ];
519
 
520
+ $rows[] = [
521
  'name' => esc_html__( 'Blog time', 'matomo' ),
522
  'value' => $this->convert_time_to_date( time(), true ),
523
  'comment' => esc_html__( 'Below dates are shown in blog timezone', 'matomo' ),
524
+ ];
525
 
526
  foreach ( $all_events as $event_name => $event_config ) {
527
  $last_run_before = $scheduled_tasks->get_last_time_before_cron( $event_name );
533
  $comment .= ' Last ended: ' . $this->convert_time_to_date( $last_run_after, true, true ) . '.';
534
  $comment .= ' Interval: ' . $event_config['interval'];
535
 
536
+ $rows[] = [
537
  'name' => $event_config['name'],
538
  'value' => 'Next run: ' . $this->convert_time_to_date( $next_scheduled, true, true ),
539
  'comment' => $comment,
540
+ ];
541
  }
542
 
543
  $suports_async = false;
544
+ if ( ! WpMatomo::is_safe_mode() && $report ) {
545
+ $rows[] = [
546
  'section' => esc_html__( 'Mandatory checks', 'matomo' ),
547
+ ];
548
 
549
  $rows = $this->add_diagnostic_results( $rows, $report->getMandatoryDiagnosticResults() );
550
 
551
+ $rows[] = [
552
  'section' => esc_html__( 'Optional checks', 'matomo' ),
553
+ ];
554
  $rows = $this->add_diagnostic_results( $rows, $report->getOptionalDiagnosticResults() );
555
 
556
+ $cli_multi = new CliMulti();
557
  $suports_async = $cli_multi->supportsAsync();
558
 
559
+ $rows[] = [
560
  'name' => 'Supports Async Archiving',
561
  'value' => $suports_async,
562
  'comment' => '',
563
+ ];
564
 
565
  $location_provider = LocationProvider::getCurrentProvider();
566
+ if ( $location_provider ) {
567
+ $rows[] = [
568
  'name' => 'Location provider ID',
569
  'value' => $location_provider->getId(),
570
  'comment' => '',
571
+ ];
572
+ $rows[] = [
573
  'name' => 'Location provider available',
574
  'value' => $location_provider->isAvailable(),
575
  'comment' => '',
576
+ ];
577
+ $rows[] = [
578
  'name' => 'Location provider working',
579
  'value' => $location_provider->isWorking(),
580
  'comment' => '',
581
+ ];
582
  }
583
 
584
+ if ( ! WpMatomo::is_safe_mode() ) {
585
  Bootstrap::do_bootstrap();
586
  $general = Config::getInstance()->General;
587
+
588
+ if ( empty( $general['proxy_client_headers'] ) ) {
589
+ foreach ( AdvancedSettings::$valid_host_headers as $header ) {
590
+ if ( ! empty( $_SERVER[ $header ] ) ) {
591
+ $rows[] = [
592
+ 'name' => 'Proxy header',
593
+ 'value' => $header,
594
  'is_warning' => true,
595
+ 'comment' => 'A proxy header is set which means you maybe need to configure a proxy header in the Advanced settings to make location reporting work. If the location in your reports is detected correctly, you can ignore this warning. Learn more: https://matomo.org/faq/wordpress/how-do-i-fix-the-proxy-header-warning-in-the-matomo-for-wordpress-system-report/',
596
+ ];
597
  }
598
  }
599
  }
600
+ $incompatible_plugins = Plugin\Manager::getInstance()->getIncompatiblePlugins( Version::VERSION );
601
+ if ( ! empty( $incompatible_plugins ) ) {
602
+ $rows[] = [
603
+ 'section' => esc_html__( 'Incompatible Matomo plugins', 'matomo' ),
604
+ ];
605
+ foreach ( $incompatible_plugins as $plugin ) {
606
+ $rows[] = [
607
+ 'name' => 'Plugin has missing dependencies',
608
+ 'value' => $plugin->getPluginName(),
609
+ 'is_error' => true,
610
+ 'comment' => $plugin->getMissingDependenciesAsString( Version::VERSION ) . ' If the plugin requires a different Matomo version you may need to update it. If you no longer use it consider uninstalling it.',
611
+ ];
612
+ }
613
+ }
 
614
  }
615
 
616
  $num_days_check_visits = 5;
617
+ $had_visits = $this->had_visits_in_last_days( $num_days_check_visits );
618
+ if ( false === $had_visits || true === $had_visits ) {
619
  // do not show info if we could not detect it (had_visits === null)
620
  $comment = '';
621
+ if ( ! $had_visits ) {
622
  $comment = 'It looks like there were no visits in the last ' . $num_days_check_visits . ' days. This may be expected if tracking is disabled, you have not added the tracking code, or your website does not have many visitors in general and you exclude your own visits.';
623
  }
624
 
625
+ $rows[] = [
626
+ 'name' => 'Had visit in last ' . $num_days_check_visits . ' days',
627
+ 'value' => $had_visits,
628
+ 'is_warning' => ! $had_visits && $this->settings->is_tracking_enabled(),
629
+ 'comment' => $comment,
630
+ ];
631
  }
632
 
633
+ if ( ! WpMatomo::is_safe_mode() ) {
634
  Bootstrap::do_bootstrap();
635
  $matomo_url = SettingsPiwik::getPiwikUrl();
636
+ $rows[] = [
637
  'name' => 'Matomo URL',
638
  'comment' => $matomo_url,
639
  'value' => ! empty( $matomo_url ),
640
+ ];
641
  }
 
642
  }
643
 
644
+ $rows[] = [
645
  'section' => 'Matomo Settings',
646
+ ];
647
 
648
  // always show these settings
649
+ $global_settings_always_show = [
650
  'track_mode',
651
  'track_codeposition',
652
  'track_api_endpoint',
653
  'track_js_endpoint',
654
+ ];
655
  foreach ( $global_settings_always_show as $key ) {
656
+ $rows[] = [
657
  'name' => ucfirst( str_replace( '_', ' ', $key ) ),
658
  'value' => $this->settings->get_global_option( $key ),
659
  'comment' => '',
660
+ ];
661
  }
662
 
663
  // otherwise show only few customised settings
669
  $val = implode( ', ', $val );
670
  }
671
 
672
+ $rows[] = [
673
  'name' => ucfirst( str_replace( '_', ' ', $key ) ),
674
  'value' => $val,
675
  'comment' => '',
676
+ ];
677
  }
678
  }
679
 
680
+ $rows[] = [
681
  'section' => 'Logs',
682
+ ];
683
 
684
  $error_log_entries = $this->logger->get_last_logged_entries();
 
 
685
 
686
+ if ( ! empty( $error_log_entries ) ) {
687
  foreach ( $error_log_entries as $error ) {
688
+ if ( ! empty( $install_time )
689
+ && is_numeric( $install_time )
690
+ && ! empty( $error['name'] )
691
+ && ! empty( $error['value'] )
692
+ && is_numeric( $error['value'] )
693
+ && 'cron_sync' === $error['name']
694
+ && $error['value'] < ( $install_time + 300 ) ) {
695
  // the first sync might right after the installation
696
  continue;
697
  }
698
 
699
  // we only consider plugin_updates as errors only if there are still outstanding updates
700
+ $is_plugin_update_error = ! empty( $error['name'] ) && 'plugin_update' === $error['name']
701
+ && ! empty( $outstanding_updates );
702
 
703
+ $skip_plugin_update = ! empty( $error['name'] ) && 'plugin_update' === $error['name']
704
+ && empty( $outstanding_updates );
705
 
706
+ if ( empty( $error['comment'] ) && '0' !== $error['comment'] ) {
707
  $error['comment'] = '';
708
  }
709
 
710
+ $error['value'] = $this->convert_time_to_date( $error['value'], true, false );
711
+ $error['is_warning'] = ! empty( $error['name'] ) && stripos( $error['name'], 'archiv' ) !== false && 'archive_boot' !== $error['name'];
712
+ $error['is_error'] = $is_plugin_update_error;
713
+ if ( $is_plugin_update_error ) {
714
  $error['comment'] = 'Please reach out to us and include the copied system report (see https://matomo.org/faq/wordpress/how-do-i-troubleshoot-a-failed-database-upgrade-in-matomo-for-wordpress/ for more info)<br><br>You can also retry the update manually by clicking in the top on the "Troubleshooting" tab and then clicking on the "Run updater" button.' . $error['comment'];
715
+ } elseif ( $skip_plugin_update ) {
716
  $error['comment'] = 'As there are no outstanding plugin updates it looks like this log can be ignored.<br><br>' . $error['comment'];
717
  }
718
+ $error['comment'] = matomo_anonymize_value( $error['comment'] );
719
+ $rows[] = $error;
720
  }
721
 
722
  foreach ( $error_log_entries as $error ) {
723
+ if ( $suports_async
724
+ && ! empty( $error['value'] ) && is_string( $error['value'] )
725
+ && strpos( $error['value'], __( 'Your PHP installation appears to be missing the MySQL extension which is required by WordPress.', 'matomo' ) ) > 0 ) {
726
+ $rows[] = [
727
+ 'name' => 'Cli has no MySQL',
728
+ 'value' => true,
729
+ 'comment' => 'It looks like MySQL is not available on CLI. Please read our FAQ on how to fix this issue: https://matomo.org/faq/wordpress/how-do-i-fix-the-error-your-php-installation-appears-to-be-missing-the-mysql-extension-which-is-required-by-wordpress-in-matomo-system-report/ ',
730
+ 'is_error' => true,
731
+ ];
 
732
  }
733
  }
734
  } else {
735
+ $rows[] = [
736
+ 'name' => __( 'None', 'matomo' ),
737
  'value' => '',
738
  'comment' => '',
739
+ ];
740
  }
741
 
742
+ if ( ! WpMatomo::is_safe_mode() ) {
 
743
  Bootstrap::do_bootstrap();
744
  $trackfailures = [];
745
  try {
746
  $tracking_failures = new Failures();
747
+ $trackfailures = $tracking_failures->getAllFailures();
748
+ // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
749
+ } catch ( Exception $e ) {
750
  // ignored in case not set up yet etc.
751
  }
752
+ if ( ! empty( $trackfailures ) ) {
753
+ $rows[] = [
754
  'section' => 'Tracking failures',
755
+ ];
756
+ foreach ( $trackfailures as $failure ) {
757
+ $comment = sprintf(
758
+ 'Solution: %s<br>More info: %s<br>Date: %s<br>Request URL: %s',
759
+ $failure['solution'],
760
+ $failure['solution_url'],
761
+ $failure['pretty_date_first_occurred'],
762
+ $failure['request_url']
 
 
763
  );
764
+ $rows[] = [
765
+ 'name' => $failure['problem'],
766
+ 'is_warning' => true,
767
+ 'value' => '',
768
+ 'comment' => $comment,
769
+ ];
770
  }
 
771
  }
 
772
  }
773
 
 
774
  return $rows;
775
  }
776
 
777
+ private function had_visits_in_last_days( $num_days ) {
 
778
  global $wpdb;
779
 
780
+ if ( WpMatomo::is_safe_mode() ) {
781
  return null;
782
  }
783
 
784
+ $days_in_seconds = $num_days * 86400;
785
 
786
+ $prefix_table = $this->db_settings->prefix_table_name( 'log_visit' );
787
 
788
  $suppress_errors = $wpdb->suppress_errors;
789
  $wpdb->suppress_errors( true );// prevent any of this showing in logs just in case
790
 
791
  try {
792
  $time = gmdate( 'Y-m-d H:i:s', time() - $days_in_seconds );
793
+ $sql = $wpdb->prepare( 'SELECT idsite from ' . $prefix_table . ' where visit_last_action_time > %s LIMIT 1', $time );
794
+ $row = $wpdb->get_var( $sql );
795
+ } catch ( Exception $e ) {
796
  $row = null;
797
  }
798
 
801
  // 0 === had no visit
802
  // 1 === had visit
803
  // null === sum error... eg table was not correctly installed
804
+ if ( null !== $row ) {
805
+ $row = ! empty( $row );
806
  }
807
 
808
  return $row;
813
  return esc_html__( 'Unknown', 'matomo' );
814
  }
815
 
816
+ $date = gmdate( 'Y-m-d H:i:s', (int) $time );
817
 
818
  if ( $in_blog_timezone ) {
819
  $date = get_date_from_gmt( $date, 'Y-m-d H:i:s' );
821
 
822
  if ( $print_diff && class_exists( '\Piwik\Metrics\Formatter' ) ) {
823
  $formatter = new \Piwik\Metrics\Formatter();
824
+ $date .= ' (' . $formatter->getPrettyTimeFromSeconds( $time - time(), true, false ) . ')';
825
  }
826
 
827
  return $date;
850
  }
851
  }
852
 
853
+ $rows[] = [
854
  'name' => $result->getLabel(),
855
  'value' => $result->getStatus() . ' ' . $result->getLongErrorMessage(),
856
  'comment' => $comment,
857
  'is_warning' => $result->getStatus() === DiagnosticResult::STATUS_WARNING,
858
  'is_error' => $result->getStatus() === DiagnosticResult::STATUS_ERROR,
859
+ ];
860
  }
861
 
862
  return $rows;
874
  $is_network_enabled = $settings->is_network_enabled();
875
  }
876
 
877
+ $rows = [];
878
+ $rows[] = [
879
  'name' => 'Home URL',
880
  'value' => home_url(),
881
+ ];
882
+ $rows[] = [
883
  'name' => 'Site URL',
884
  'value' => site_url(),
885
+ ];
886
+ $rows[] = [
887
  'name' => 'WordPress Version',
888
  'value' => get_bloginfo( 'version' ),
889
+ ];
890
+ $rows[] = [
891
  'name' => 'Number of blogs',
892
  'value' => $num_blogs,
893
+ ];
894
+ $rows[] = [
895
  'name' => 'Multisite Enabled',
896
  'value' => $is_multi_site,
897
+ ];
898
+ $rows[] = [
899
  'name' => 'Network Enabled',
900
  'value' => $is_network_enabled,
901
+ ];
902
+ $consts = [
903
+ 'WP_DEBUG',
904
+ 'WP_DEBUG_DISPLAY',
905
+ 'WP_DEBUG_LOG',
906
+ 'DISABLE_WP_CRON',
907
+ 'FORCE_SSL_ADMIN',
908
+ 'WP_CACHE',
909
+ 'CONCATENATE_SCRIPTS',
910
+ 'COMPRESS_SCRIPTS',
911
+ 'COMPRESS_CSS',
912
+ 'ENFORCE_GZIP',
913
+ 'WP_LOCAL_DEV',
914
+ 'WP_CONTENT_URL',
915
+ 'WP_CONTENT_DIR',
916
+ 'UPLOADS',
917
+ 'BLOGUPLOADDIR',
918
+ 'DIEONDBERROR',
919
+ 'WPLANG',
920
+ 'ALTERNATE_WP_CRON',
921
+ 'WP_CRON_LOCK_TIMEOUT',
922
+ 'WP_DISABLE_FATAL_ERROR_HANDLER',
923
+ 'MATOMO_SUPPORT_ASYNC_ARCHIVING',
924
+ 'MATOMO_TRIGGER_BROWSER_ARCHIVING',
925
+ 'MATOMO_ENABLE_TAG_MANAGER',
926
+ 'MATOMO_SUPPRESS_DB_ERRORS',
927
+ 'MATOMO_ENABLE_AUTO_UPGRADE',
928
+ 'MATOMO_DEBUG',
929
+ 'MATOMO_SAFE_MODE',
930
+ 'MATOMO_GLOBAL_UPLOAD_DIR',
931
+ 'MATOMO_LOGIN_REDIRECT',
932
+ ];
933
+ foreach ( $consts as $const ) {
934
+ $rows[] = [
935
  'name' => $const,
936
+ 'value' => defined( $const ) ? constant( $const ) : '-',
937
+ ];
938
  }
939
 
940
+ $rows[] = [
941
  'name' => 'Permalink Structure',
942
  'value' => get_option( 'permalink_structure' ) ? get_option( 'permalink_structure' ) : 'Default',
943
+ ];
944
 
945
+ $rows[] = [
946
  'name' => 'Possibly uses symlink',
947
  'value' => strpos( __DIR__, ABSPATH ) === false && strpos( __DIR__, WP_CONTENT_DIR ) === false,
948
+ ];
949
 
950
  $upload_dir = wp_upload_dir();
951
+ $rows[] = [
952
  'name' => 'Upload base url',
953
  'value' => $upload_dir['baseurl'],
954
+ ];
955
 
956
+ $rows[] = [
957
  'name' => 'Upload base dir',
958
  'value' => $upload_dir['basedir'],
959
+ ];
960
 
961
+ $rows[] = [
962
  'name' => 'Upload url',
963
  'value' => $upload_dir['url'],
964
+ ];
965
 
966
+ foreach ( [ 'upload_path', 'upload_url_path' ] as $option_read ) {
967
+ $rows[] = [
968
  'name' => 'Custom ' . $option_read,
969
  'value' => get_option( $option_read ),
970
+ ];
971
  }
972
 
973
+ if ( is_plugin_active( 'wp-piwik/wp-piwik.php' ) ) {
974
+ $rows[] = [
975
+ 'name' => 'WP-Matomo (WP-Piwik) activated',
976
+ 'value' => true,
977
  'is_warning' => true,
978
+ 'comment' => 'It is usually not recommended or needed to run Matomo for WordPress and WP-Matomo at the same time. To learn more about the differences between the two plugins view this URL: https://matomo.org/faq/wordpress/why-are-there-two-different-matomo-for-wordpress-plugins-what-is-the-difference-to-wp-matomo-integration-plugin/',
979
+ ];
980
 
981
+ $mode = get_option( 'wp-piwik_global-piwik_mode' );
982
+ if ( function_exists( 'get_site_option' ) && is_plugin_active_for_network( 'wp-piwik/wp-piwik.php' ) ) {
983
+ $mode = get_site_option( 'wp-piwik_global-piwik_mode' );
984
  }
985
+ if ( ! empty( $mode ) ) {
986
+ $rows[] = [
987
+ 'name' => 'WP-Matomo mode',
988
+ 'value' => $mode,
989
+ 'is_warning' => 'php' === $mode || 'PHP' === $mode,
990
+ 'comment' => 'WP-Matomo is configured in "PHP mode". This is known to cause issues with Matomo for WordPress. We recommend you either deactivate WP-Matomo or you go "Settings => WP-Matomo" and change the "Matomo Mode" in the "Connect to Matomo" section to "Self-hosted HTTP API".',
991
+ ];
992
  }
993
  }
994
 
995
  $compatible_content_dir = matomo_has_compatible_content_dir();
996
+ if ( true === $compatible_content_dir ) {
997
+ $rows[] = [
998
  'name' => 'Compatible content directory',
999
  'value' => true,
1000
+ ];
1001
  } else {
1002
+ $rows[] = [
1003
+ 'name' => 'Compatible content directory',
1004
+ 'value' => $compatible_content_dir,
1005
  'is_warning' => true,
1006
+ 'comment' => __( 'It looks like you are maybe using a custom WordPress content directory. The Matomo reporting/admin pages might not work. You may be able to workaround this.', 'matomo' ) . ' ' . __( 'Learn more', 'matomo' ) . ': https://matomo.org/faq/wordpress/how-do-i-make-matomo-for-wordpress-work-when-i-have-a-custom-content-directory/',
1007
+ ];
1008
  }
1009
 
1010
  return $rows;
1011
  }
1012
 
1013
  private function get_server_info() {
1014
+ $rows = [];
1015
 
1016
  if ( ! empty( $_SERVER['SERVER_SOFTWARE'] ) ) {
1017
+ $rows[] = [
1018
  'name' => 'Server Info',
1019
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1020
+ 'value' => sanitize_text_field( $_SERVER['SERVER_SOFTWARE'] ),
1021
+ ];
1022
  }
1023
  if ( PHP_OS ) {
1024
+ $rows[] = [
1025
  'name' => 'PHP OS',
1026
  'value' => PHP_OS,
1027
+ ];
1028
  }
1029
+ $rows[] = [
1030
  'name' => 'PHP Version',
1031
  'value' => phpversion(),
1032
+ ];
1033
+ $rows[] = [
1034
  'name' => 'PHP SAPI',
1035
  'value' => php_sapi_name(),
1036
+ ];
1037
+ if ( defined( 'PHP_BINARY' ) && PHP_BINARY ) {
1038
+ $rows[] = [
1039
  'name' => 'PHP Binary Name',
1040
+ 'value' => @basename( PHP_BINARY ),
1041
+ ];
1042
  }
1043
  // we report error reporting before matomo bootstraped and after to see if Matomo changed it successfully etc
1044
+ $rows[] = [
1045
  'name' => 'PHP Error Reporting',
1046
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting
1047
+ 'value' => $this->initial_error_reporting . ' After bootstrap: ' . @error_reporting(),
1048
+ ];
1049
+ if ( ! WpMatomo::is_safe_mode() ) {
1050
  Bootstrap::do_bootstrap();
1051
+ $cli_php = new CliMulti\CliPhp();
1052
+ $binary = $cli_php->findPhpBinary();
1053
+ if ( ! empty( $binary ) ) {
1054
+ $binary = basename( $binary );
1055
+ $rows[] = [
1056
  'name' => 'PHP Found Binary',
1057
  'value' => $binary,
1058
+ ];
1059
  }
1060
  }
1061
+ $rows[] = [
1062
  'name' => 'Timezone',
1063
  'value' => date_default_timezone_get(),
1064
+ ];
1065
+ if ( function_exists( 'wp_timezone_string' ) ) {
1066
+ $rows[] = [
1067
  'name' => 'WP timezone',
1068
  'value' => wp_timezone_string(),
1069
+ ];
1070
  }
1071
+ $rows[] = [
1072
  'name' => 'Locale',
1073
  'value' => get_locale(),
1074
+ ];
1075
+ if ( function_exists( 'get_user_locale' ) ) {
1076
+ $rows[] = [
1077
  'name' => 'User Locale',
1078
  'value' => get_user_locale(),
1079
+ ];
1080
  }
1081
 
1082
+ $rows[] = [
1083
  'name' => 'Memory Limit',
1084
  'value' => @ini_get( 'memory_limit' ),
1085
  'comment' => 'At least 128MB recommended. Depending on your traffic 256MB or more may be needed.',
1086
+ ];
1087
 
1088
+ $rows[] = [
1089
  'name' => 'WP Memory Limit',
1090
  'value' => defined( 'WP_MEMORY_LIMIT' ) ? WP_MEMORY_LIMIT : '',
1091
  'comment' => '',
1092
+ ];
1093
 
1094
+ $rows[] = [
1095
  'name' => 'WP Max Memory Limit',
1096
  'value' => defined( 'WP_MAX_MEMORY_LIMIT' ) ? WP_MAX_MEMORY_LIMIT : '',
1097
  'comment' => '',
1098
+ ];
1099
+
1100
+ if ( function_exists( 'timezone_version_get' ) ) {
1101
+ $rows[] = [
1102
  'name' => 'Timezone version',
1103
  'value' => timezone_version_get(),
1104
+ ];
1105
  }
1106
+
1107
+ $rows[] = [
1108
  'name' => 'Time',
1109
  'value' => time(),
1110
+ ];
1111
 
1112
+ $rows[] = [
1113
  'name' => 'Max Execution Time',
1114
  'value' => ini_get( 'max_execution_time' ),
1115
+ ];
1116
+ $rows[] = [
1117
  'name' => 'Max Post Size',
1118
  'value' => ini_get( 'post_max_size' ),
1119
+ ];
1120
+ $rows[] = [
1121
  'name' => 'Max Upload Size',
1122
  'value' => wp_max_upload_size(),
1123
+ ];
1124
+ $rows[] = [
1125
  'name' => 'Max Input Vars',
1126
  'value' => ini_get( 'max_input_vars' ),
1127
+ ];
1128
 
1129
+ $disabled_functions = ini_get( 'disable_functions' );
1130
+ $rows[] = [
1131
+ 'name' => 'Disabled PHP functions',
1132
+ 'value' => ! empty( $disabled_functions ),
1133
+ 'comment' => ! empty( $disabled_functions ) ? $disabled_functions : '',
1134
+ ];
1135
 
1136
  $zlib_compression = ini_get( 'zlib.output_compression' );
1137
+ $row = [
1138
  'name' => 'zlib.output_compression is off',
1139
+ 'value' => '1' !== $zlib_compression,
1140
+ ];
1141
 
1142
+ if ( '1' === $zlib_compression ) {
1143
  $row['is_error'] = true;
1144
  $row['comment'] = 'You need to set "zlib.output_compression" in your php.ini to "Off".';
1145
  }
1148
  if ( function_exists( 'curl_version' ) ) {
1149
  $curl_version = curl_version();
1150
  $curl_version = $curl_version['version'] . ', ' . $curl_version['ssl_version'];
1151
+ $rows[] = [
1152
  'name' => 'Curl Version',
1153
  'value' => $curl_version,
1154
+ ];
1155
  }
1156
 
1157
  $suhosin_installed = ( extension_loaded( 'suhosin' ) || ( defined( 'SUHOSIN_PATCH' ) && constant( 'SUHOSIN_PATCH' ) ) );
1158
+ $rows[] = [
1159
+ 'name' => 'Suhosin installed',
1160
+ 'value' => ! empty( $suhosin_installed ),
1161
+ 'comment' => '',
1162
+ ];
1163
 
1164
  return $rows;
1165
  }
1166
 
1167
  private function get_browser_info() {
1168
+ $rows = [];
1169
 
1170
+ if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
1171
+ $rows[] = [
1172
  'name' => 'Browser',
1173
  'value' => '',
1174
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1175
+ 'comment' => sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ),
1176
+ ];
1177
  }
1178
+ if ( ! WpMatomo::is_safe_mode() ) {
1179
  Bootstrap::do_bootstrap();
1180
  try {
1181
+ if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
1182
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1183
+ $detector = StaticContainer::get( DeviceDetectorFactory::class )->makeInstance( sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ) );
1184
+ $client = $detector->getClient();
1185
+ if ( ! empty( $client['name'] ) && 'Microsoft Edge' === $client['name'] && (int) $client['version'] >= 85 ) {
1186
+ $rows[] = [
1187
+ 'name' => 'Browser Compatibility',
1188
  'is_warning' => true,
1189
+ 'value' => 'Yes',
1190
+ 'comment' => 'Because you are using MS Edge browser, you may see a warning like "This site has been reported as unsafe" from "Microsoft Defender SmartScreen" when you view the Matomo Reporting, Admin or Tag Manager page. This is a false alert and you can safely ignore this warning by clicking on the icon next to the URL (in the address bar) and choosing either "Report as safe" (preferred) or "Show unsafe content". We are hoping to get this false warning removed in the future.',
1191
+ ];
1192
  }
1193
  }
1194
+ } catch ( Exception $e ) {
1195
+ $this->logger->log( $e->getMessage() );
 
1196
  }
1197
 
1198
+ $rows[] = [
1199
  'name' => 'Language',
1200
  'value' => Common::getBrowserLanguage(),
1201
+ 'comment' => '',
1202
+ ];
1203
  }
1204
 
 
1205
  return $rows;
1206
  }
1207
 
1208
  private function get_db_info() {
1209
  global $wpdb;
1210
+ $rows = [];
1211
 
1212
+ $rows[] = [
1213
  'name' => 'MySQL Version',
1214
  'value' => ! empty( $wpdb->is_mysql ) ? $wpdb->db_version() : '',
1215
  'comment' => '',
1216
+ ];
1217
 
1218
+ $rows[] = [
1219
  'name' => 'Mysqli Connect',
1220
  'value' => function_exists( 'mysqli_connect' ),
1221
  'comment' => '',
1222
+ ];
1223
+ $rows[] = [
1224
  'name' => 'Force MySQL over Mysqli',
1225
  'value' => defined( 'WP_USE_EXT_MYSQL' ) && WP_USE_EXT_MYSQL,
1226
  'comment' => '',
1227
+ ];
1228
 
1229
+ $rows[] = [
1230
  'name' => 'DB Prefix',
1231
  'value' => $wpdb->prefix,
1232
+ ];
1233
 
1234
+ $rows[] = [
1235
  'name' => 'DB CHARSET',
1236
+ 'value' => defined( 'DB_CHARSET' ) ? DB_CHARSET : '',
1237
+ ];
1238
 
1239
+ $rows[] = [
1240
  'name' => 'DB COLLATE',
1241
+ 'value' => defined( 'DB_COLLATE' ) ? DB_COLLATE : '',
1242
+ ];
1243
 
1244
+ $rows[] = [
1245
  'name' => 'SHOW ERRORS',
1246
+ 'value' => ! empty( $wpdb->show_errors ),
1247
+ ];
1248
 
1249
+ $rows[] = [
1250
  'name' => 'SUPPRESS ERRORS',
1251
+ 'value' => ! empty( $wpdb->suppress_errors ),
1252
+ ];
1253
 
1254
  if ( method_exists( $wpdb, 'parse_db_host' ) ) {
1255
  $host_data = $wpdb->parse_db_host( DB_HOST );
1257
  list( $host, $port, $socket, $is_ipv6 ) = $host_data;
1258
  }
1259
 
1260
+ $rows[] = [
1261
  'name' => 'Uses Socket',
1262
  'value' => ! empty( $socket ),
1263
+ ];
1264
+ $rows[] = [
1265
  'name' => 'Uses IPv6',
1266
  'value' => ! empty( $is_ipv6 ),
1267
+ ];
1268
  }
1269
 
1270
+ $rows[] = [
1271
  'name' => 'Matomo tables found',
1272
  'value' => $this->get_num_matomo_tables(),
1273
+ ];
1274
+
1275
+ $missing_tables = $this->get_missing_tables();
1276
+ $has_missing_tables = ( count( $missing_tables ) > 0 );
1277
+ $rows[] = [
1278
+ 'name' => 'DB tables exist',
1279
+ 'value' => ( ! $has_missing_tables ),
1280
+ 'comment' => $has_missing_tables ? sprintf( __( 'Some tables may be missing: %s', 'matomo' ), implode( ', ', $missing_tables ) ) : '',
1281
+ 'is_error' => $has_missing_tables,
1282
+ ];
1283
+
1284
+ foreach ( [ 'user', 'site' ] as $table ) {
1285
+ $rows[] = [
1286
+ 'name' => 'Matomo ' . $table . 's found',
1287
+ 'value' => $this->get_num_entries_in_table( $table ),
1288
+ ];
1289
  }
1290
 
1291
  $grants = $this->get_db_grants();
1292
 
1293
  // we only show these grants for security reasons as only they are needed and we don't need to know any other ones
1294
+ $needed_grants = [
1295
+ 'SELECT',
1296
+ 'INSERT',
1297
+ 'UPDATE',
1298
+ 'INDEX',
1299
+ 'DELETE',
1300
+ 'CREATE',
1301
+ 'DROP',
1302
+ 'ALTER',
1303
+ 'CREATE TEMPORARY TABLES',
1304
+ 'LOCK TABLES',
1305
+ ];
1306
  if ( in_array( 'ALL PRIVILEGES', $grants, true ) ) {
1307
  // ALL PRIVILEGES may be used pre MySQL 8.0
1308
  $grants = $needed_grants;
1313
  if ( empty( $grants )
1314
  || ! is_array( $grants )
1315
  || count( $grants_missing ) === count( $needed_grants ) ) {
1316
+ $rows[] = [
1317
  'name' => esc_html__( 'Required permissions', 'matomo' ),
1318
  'value' => esc_html__( 'Failed to detect granted permissions', 'matomo' ),
1319
  'comment' => esc_html__( 'Please check your MySQL user has these permissions (grants):', 'matomo' ) . '<br />' . implode( ', ', $needed_grants ),
1320
  'is_warning' => false,
1321
+ ];
1322
  } else {
1323
  if ( ! empty( $grants_missing ) ) {
1324
+ $rows[] = [
1325
  'name' => esc_html__( 'Required permissions', 'matomo' ),
1326
  'value' => esc_html__( 'Error', 'matomo' ),
1327
  'comment' => esc_html__( 'Missing permissions', 'matomo' ) . ': ' . implode( ', ', $grants_missing ) . '. ' . __( 'Please check if any of these MySQL permission (grants) are missing and add them if needed.', 'matomo' ) . ' ' . __( 'Learn more', 'matomo' ) . ': https://matomo.org/faq/troubleshooting/how-do-i-check-if-my-mysql-user-has-all-required-grants/',
1328
  'is_warning' => true,
1329
+ ];
1330
  } else {
1331
+ $rows[] = [
1332
  'name' => esc_html__( 'Required permissions', 'matomo' ),
1333
  'value' => esc_html__( 'OK', 'matomo' ),
1334
  'comment' => '',
1335
  'is_warning' => false,
1336
+ ];
1337
  }
1338
  }
1339
 
1346
  public function get_missing_tables() {
1347
  global $wpdb;
1348
 
1349
+ $required_matomo_tables = $this->db_settings->get_matomo_tables();
1350
+ $required_matomo_tables = array_map( [ $this->db_settings, 'prefix_table_name' ], $required_matomo_tables );
1351
 
1352
+ $existing_tables = [];
1353
  try {
1354
+ $prefix = $this->db_settings->prefix_table_name( '' );
1355
  $existing_tables = $wpdb->get_col( 'SHOW TABLES LIKE "' . $prefix . '%"' );
1356
+ } catch ( Exception $e ) {
1357
  $this->logger->log( 'no show tables: ' . $e->getMessage() );
1358
  }
1359
+
1360
  return array_diff( $required_matomo_tables, $existing_tables );
1361
  }
1362
 
1363
+ private function get_num_entries_in_table( $table ) {
1364
  global $wpdb;
1365
 
1366
+ $prefix = $this->db_settings->prefix_table_name( $table );
1367
 
1368
  $results = null;
1369
  try {
1370
+ $results = $wpdb->get_var( 'select count(*) from ' . $prefix );
1371
+ } catch ( Exception $e ) {
1372
+ $this->logger->log( 'no count(*): ' . $e->getMessage() );
1373
  }
1374
 
1375
+ if ( isset( $results ) && is_numeric( $results ) ) {
1376
  return $results;
1377
  }
1378
 
1382
  private function get_num_matomo_tables() {
1383
  global $wpdb;
1384
 
1385
+ $prefix = $this->db_settings->prefix_table_name( '' );
1386
 
1387
  $results = null;
1388
  try {
1389
+ $results = $wpdb->get_results( 'show tables like "' . $prefix . '%"' );
1390
+ } catch ( Exception $e ) {
1391
+ $this->logger->log( 'no show tables: ' . $e->getMessage() );
1392
  }
1393
 
1394
+ if ( is_array( $results ) ) {
1395
+ return count( $results );
1396
  }
1397
 
1398
  return 'show tables not working';
1406
 
1407
  try {
1408
  $values = $wpdb->get_results( 'SHOW GRANTS', ARRAY_N );
1409
+ } catch ( Exception $e ) {
1410
  // We ignore any possible error in case of permission or not supported etc.
1411
+ $values = [];
1412
  }
1413
 
1414
  $wpdb->suppress_errors( $suppress_errors );
1415
 
1416
+ $grants = [];
1417
  foreach ( $values as $index => $value ) {
1418
  if ( empty( $value[0] ) || ! is_string( $value[0] ) ) {
1419
  continue;
1420
  }
1421
 
1422
  if ( stripos( $value[0], 'ALL PRIVILEGES' ) !== false ) {
1423
+ return [ 'ALL PRIVILEGES' ]; // the split on empty string wouldn't work otherwise
1424
  }
1425
 
1426
+ foreach ( [ ' ON ', ' TO ', ' IDENTIFIED ', ' BY ' ] as $keyword ) {
1427
  if ( stripos( $values[ $index ][0], $keyword ) !== false ) {
1428
  // make sure to never show by any accident a db user or password by cutting anything after on/to
1429
  $values[ $index ][0] = substr( $value[0], 0, stripos( $value[0], $keyword ) );
1434
  }
1435
  }
1436
  // make sure to never show by any accident a db user or password
1437
+ $values[ $index ][0] = str_replace(
1438
+ [ DB_USER, DB_PASSWORD ],
1439
+ [
1440
+ 'DB_USER',
1441
+ 'DB_PASS',
1442
+ ],
1443
+ $values[ $index ][0]
1444
+ );
1445
 
1446
  $grants = array_merge( $grants, explode( ',', $values[ $index ][0] ) );
1447
  }
1448
  $grants = array_map( 'trim', $grants );
1449
  $grants = array_map( 'strtoupper', $grants );
1450
  $grants = array_unique( $grants );
1451
+
1452
  return $grants;
1453
  }
1454
 
1455
  private function get_plugins_info() {
1456
+ $rows = [];
1457
  $mu_plugins = get_mu_plugins();
1458
 
1459
  if ( ! empty( $mu_plugins ) ) {
1460
+ $rows[] = [
1461
  'section' => 'MU Plugins',
1462
+ ];
1463
 
1464
  foreach ( $mu_plugins as $mu_pin ) {
1465
  $comment = '';
1466
  if ( ! empty( $plugin['Network'] ) ) {
1467
  $comment = 'Network enabled';
1468
  }
1469
+ $rows[] = [
1470
  'name' => $mu_pin['Name'],
1471
  'value' => $mu_pin['Version'],
1472
  'comment' => $comment,
1473
+ ];
1474
  }
1475
 
1476
+ $rows[] = [
1477
  'section' => 'Plugins',
1478
+ ];
1479
  }
1480
 
1481
  $plugins = get_plugins();
1485
  if ( ! empty( $plugin['Network'] ) ) {
1486
  $comment = 'Network enabled';
1487
  }
1488
+ $rows[] = [
1489
  'name' => $plugin['Name'],
1490
  'value' => $plugin['Version'],
1491
  'comment' => $comment,
1492
+ ];
1493
  }
1494
 
1495
+ $active_plugins = get_option( 'active_plugins', [] );
1496
 
1497
  if ( ! empty( $active_plugins ) && is_array( $active_plugins ) ) {
1498
  $active_plugins = array_map(
1499
  function ( $active_plugin ) {
1500
  $parts = explode( '/', trim( $active_plugin ) );
1501
+
1502
  return trim( $parts[0] );
1503
  },
1504
  $active_plugins
1505
  );
1506
 
1507
+ $rows[] = [
1508
  'name' => 'Active Plugins',
1509
  'value' => count( $active_plugins ),
1510
  'comment' => implode( ' ', $active_plugins ),
1511
+ ];
1512
 
1513
  $used_not_compatible = array_intersect( $active_plugins, $this->not_compatible_plugins );
1514
  if ( ! empty( $used_not_compatible ) ) {
 
1515
  $additional_comment = '';
1516
+ if ( in_array( 'tweet-old-post-pro', $used_not_compatible, true ) ) {
1517
  $additional_comment .= '<br><br>A workaround for Revive Old Posts Pro may be to add the following line to your "wp-config.php". <br><code>define( \'MATOMO_SUPPORT_ASYNC_ARCHIVING\', false );</code>.';
1518
  }
1519
+ if ( in_array( 'secupress', $used_not_compatible, true ) ) {
1520
  $additional_comment .= '<br><br>If reports aren\'t being generated then you may need to disable the feature "Firewall -> Block Bad Request Methods" in SecuPress (if it is enabled) or add the following line to your "wp-config.php": <br><code>define( \'MATOMO_SUPPORT_ASYNC_ARCHIVING\', false );</code>.';
1521
  }
1522
 
1523
  $is_warning = true;
1524
+ $is_error = false;
1525
+ if ( in_array( 'cookiebot', $used_not_compatible, true ) ) {
1526
  $is_warning = false;
1527
+ $is_error = true;
1528
  }
1529
 
1530
+ $rows[] = [
1531
+ 'name' => __( 'Not compatible plugins', 'matomo' ),
1532
+ 'value' => count( $used_not_compatible ),
1533
+ 'comment' => implode( ', ', $used_not_compatible ) . '<br><br> Matomo may work fine when using these plugins but there may be some issues. For more information see<br>https://matomo.org/faq/wordpress/which-plugins-is-matomo-for-wordpress-known-to-be-not-compatible-with/ ' . $additional_comment,
1534
  'is_warning' => $is_warning,
1535
+ 'is_error' => $is_error,
1536
+ ];
1537
  }
1538
  }
1539
 
1540
+ $rows[] = [
1541
+ 'name' => 'Theme',
1542
+ 'value' => function_exists( 'get_template' ) ? get_template() : '',
1543
+ 'comment' => get_option( 'stylesheet' ),
1544
+ ];
 
1545
 
1546
+ if ( is_plugin_active( 'better-wp-security/better-wp-security.php' ) ) {
1547
+ if ( method_exists( '\ITSEC_Modules', 'get_setting' )
1548
+ && ITSEC_Modules::get_setting( 'system-tweaks', 'long_url_strings' ) ) {
1549
+ $rows[] = [
1550
  'name' => 'iThemes Security Long URLs Enabled',
1551
  'value' => true,
1552
  'comment' => 'Tracking might not work because it looks like you have Long URLs disabled in iThemes Security. To fix this please go to "Security -> Settings -> System Tweaks" and disable the setting "Long URL Strings".',
1553
  'is_error' => true,
1554
+ ];
1555
  }
1556
  }
1557
 
1558
  return $rows;
1559
  }
 
 
1560
  }
classes/WpMatomo/Admin/TrackingSettings.php CHANGED
@@ -9,6 +9,7 @@
9
 
10
  namespace WpMatomo\Admin;
11
 
 
12
  use WpMatomo\Capabilities;
13
  use WpMatomo\Settings;
14
  use WpMatomo\Site;
@@ -18,7 +19,10 @@ use WpMatomo\TrackingCode\TrackingCodeGenerator;
18
  if ( ! defined( 'ABSPATH' ) ) {
19
  exit; // if accessed directly
20
  }
21
-
 
 
 
22
  class TrackingSettings implements AdminSettingsInterface {
23
  const FORM_NAME = 'matomo';
24
  const NONCE_NAME = 'matomo_settings';
@@ -59,7 +63,7 @@ class TrackingSettings implements AdminSettingsInterface {
59
  }
60
 
61
  private function apply_settings() {
62
- $keys_to_keep = array(
63
  'track_mode',
64
  'track_across',
65
  'track_across_alias',
@@ -96,46 +100,45 @@ class TrackingSettings implements AdminSettingsInterface {
96
  'track_js_endpoint',
97
  'track_jserrors',
98
  'track_api_endpoint',
99
- Settings::SITE_CURRENCY
100
- );
101
 
102
  if ( matomo_has_tag_manager() ) {
103
  $keys_to_keep[] = 'tagmanger_container_ids';
104
  }
105
 
106
- $values = array();
107
 
108
  // default value in case no role/ post type is selected to make sure we unset it if no role /post type is selected
109
- $values['add_post_annotations'] = array();
110
- $values['tagmanger_container_ids'] = array();
111
 
112
  $valid_currencies = $this->get_supported_currencies();
113
 
114
- if ( !empty( $_POST[ self::FORM_NAME ]['tracker_debug'] ) ) {
115
  $site_config_sync = new SiteConfigSync( $this->settings );
116
- switch ($_POST[ self::FORM_NAME ]['tracker_debug']) {
117
  case 'always':
118
- $site_config_sync->set_config_value('Tracker', 'debug', 1);
119
- $site_config_sync->set_config_value('Tracker', 'debug_on_demand', 0);
120
  break;
121
  case 'on_demand':
122
- $site_config_sync->set_config_value('Tracker', 'debug', 0);
123
- $site_config_sync->set_config_value('Tracker', 'debug_on_demand', 1);
124
  break;
125
  default:
126
- $site_config_sync->set_config_value('Tracker', 'debug', 0);
127
- $site_config_sync->set_config_value('Tracker', 'debug_on_demand', 0);
128
  }
129
  }
130
 
131
- if ( empty( $_POST[ self::FORM_NAME ][Settings::SITE_CURRENCY] )
132
- || !array_key_exists( $_POST[ self::FORM_NAME ][Settings::SITE_CURRENCY], $valid_currencies ) ) {
133
- $_POST[ self::FORM_NAME ][Settings::SITE_CURRENCY] = 'USD';
134
  }
135
 
136
  if ( ! empty( $_POST[ self::FORM_NAME ]['track_mode'] ) ) {
137
- $track_mode = $_POST[ self::FORM_NAME ]['track_mode'];
138
-
139
  if ( self::TRACK_MODE_TAGMANAGER === $track_mode ) {
140
  // no noscript mode in this case
141
  $_POST[ self::FORM_NAME ]['track_noscript'] = '';
@@ -146,12 +149,18 @@ class TrackingSettings implements AdminSettingsInterface {
146
  if ( $this->must_update_tracker() === true ) {
147
  // We want to keep the tracking code when user switches between disabled and manually or disabled to disabled.
148
  if ( ! empty( $_POST[ self::FORM_NAME ]['tracking_code'] ) ) {
 
 
149
  $_POST[ self::FORM_NAME ]['tracking_code'] = stripslashes( $_POST[ self::FORM_NAME ]['tracking_code'] );
 
150
  } else {
151
  $_POST[ self::FORM_NAME ]['tracking_code'] = '';
152
  }
153
  if ( ! empty( $_POST[ self::FORM_NAME ]['noscript_code'] ) ) {
 
 
154
  $_POST[ self::FORM_NAME ]['noscript_code'] = stripslashes( $_POST[ self::FORM_NAME ]['noscript_code'] );
 
155
  } else {
156
  $_POST[ self::FORM_NAME ]['noscript_code'] = '';
157
  }
@@ -160,42 +169,50 @@ class TrackingSettings implements AdminSettingsInterface {
160
  $_POST[ self::FORM_NAME ]['tracking_code'] = '';
161
  }
162
  }
163
-
164
  foreach ( $_POST[ self::FORM_NAME ] as $name => $value ) {
165
  if ( in_array( $name, $keys_to_keep, true ) ) {
166
  $values[ $name ] = $value;
167
  }
168
  }
169
-
170
  $this->settings->apply_tracking_related_changes( $values );
171
 
172
  return true;
173
  }
174
 
 
 
 
 
 
 
175
  /**
176
  * Reauires form to be posted
 
177
  * @return bool
178
  */
179
- private function must_update_tracker () {
180
- $track_mode = $_POST[ self::FORM_NAME ]['track_mode'];
181
  $previus_track_mode = $this->settings->get_global_option( 'track_mode' );
182
  $must_update = false;
183
  if ( self::TRACK_MODE_MANUALLY === $track_mode
184
- || (self::TRACK_MODE_DISABLED === $track_mode &&
185
- in_array( $previus_track_mode, array( self::TRACK_MODE_DISABLED, self::TRACK_MODE_MANUALLY ) )) ) {
186
  // We want to keep the tracking code when user switches between disabled and manually or disabled to disabled.
187
  $must_update = true;
188
  }
 
189
  return $must_update;
190
  }
191
 
192
  /**
193
  * @return bool
194
  */
195
- private function form_submitted () {
196
- return isset( $_POST ) && ! empty( $_POST[ self::FORM_NAME ] )
197
- && is_admin()
198
- && $this->can_user_manage();
199
  }
200
 
201
  /**
@@ -203,50 +220,55 @@ class TrackingSettings implements AdminSettingsInterface {
203
  *
204
  * @return bool
205
  */
206
- private function has_valid_html_comments ($field) {
207
  $valid = true;
208
  if ( $this->form_submitted() === true ) {
209
  if ( $this->must_update_tracker() === true ) {
210
- if ( ! empty( $_POST[ self::FORM_NAME ][$field] ) ) {
211
- $valid = $this->validate_html_comments( $_POST[ self::FORM_NAME ][$field] );
 
 
212
  }
213
  }
214
  }
 
215
  return $valid;
216
  }
217
 
218
  /**
219
  * @param string $html html content to validate
 
220
  * @returns boolean
221
  */
222
  public function validate_html_comments( $html ) {
223
- $opening = substr_count( $html, '<!--' );
224
- $closing = substr_count( $html, '-->' );
225
- return ( $opening === $closing );
 
226
  }
227
 
228
  public function show_settings() {
229
- $was_updated = false;
230
- $errors = [];
231
  if ( $this->has_valid_html_comments( 'tracking_code' ) !== true ) {
232
- $errors[] = __( 'Settings have not been saved. There is an issue with the HTML comments in the field "Tracking code". Make sure all opened comments (<!--) are closed (-->) correctly.', 'matomo' );
233
  }
234
  if ( $this->has_valid_html_comments( 'noscript_code' ) !== true ) {
235
- $errors[] = __( 'Settings have not been saved. There is an issue with the HTML comments in the field "Noscript code". Make sure all opened comments (<!--) are closed (-->) correctly.', 'matomo' );
236
  }
237
- if ( count($errors) === 0 ) {
238
  $was_updated = $this->update_if_submitted();
239
  }
240
 
241
- $settings = $this->settings;
242
 
243
  $containers = $this->get_active_containers();
244
 
245
- $track_modes = array(
246
  self::TRACK_MODE_DISABLED => esc_html__( 'Disabled', 'matomo' ),
247
  self::TRACK_MODE_DEFAULT => esc_html__( 'Default tracking', 'matomo' ),
248
  self::TRACK_MODE_MANUALLY => esc_html__( 'Enter manually', 'matomo' ),
249
- );
250
 
251
  if ( ! empty( $containers ) ) {
252
  $track_modes[ self::TRACK_MODE_TAGMANAGER ] = esc_html__( 'Tag Manager', 'matomo' );
@@ -268,46 +290,46 @@ class TrackingSettings implements AdminSettingsInterface {
268
  /**
269
  * @return string[]
270
  */
271
- private function get_cookie_consent_modes()
272
- {
273
  $modes = [];
274
- foreach(CookieConsent::get_available_options() as $option => $description) {
275
- $modes[$option] = $description;
276
  }
 
277
  return $modes;
278
  }
279
 
280
- private function get_supported_currencies()
281
- {
282
- $all = include dirname( MATOMO_ANALYTICS_FILE ) . '/app/core/Intl/Data/Resources/currencies.php';
283
- $currencies = array();
284
- foreach ($all as $key => $single) {
285
- $currencies[$key] = $single[0] . ' ' . $single[1];
286
  }
 
287
  return $currencies;
288
  }
289
 
290
  public function get_active_containers() {
291
  // we don't use Matomo API here to avoid needing to bootstrap Matomo which is slow and could break things
292
- $containers = array();
293
  if ( matomo_has_tag_manager() ) {
294
  global $wpdb;
295
- $dbsettings = new \WpMatomo\Db\Settings();
296
- $container_table = $dbsettings->prefix_table_name( 'tagmanager_container' );
297
  try {
 
298
  $containers = $wpdb->get_results( sprintf( 'SELECT `idcontainer`, `name` FROM %s where `status` = "active"', $container_table ) );
299
- } catch ( \Exception $e ) {
 
300
  // table may not exist yet etc
301
- $containers = array();
302
  }
303
  }
304
- $by_id = array();
305
  foreach ( $containers as $container ) {
306
  $by_id[ $container->idcontainer ] = $container->name;
307
  }
308
 
309
  return $by_id;
310
  }
311
-
312
-
313
  }
9
 
10
  namespace WpMatomo\Admin;
11
 
12
+ use Exception;
13
  use WpMatomo\Capabilities;
14
  use WpMatomo\Settings;
15
  use WpMatomo\Site;
19
  if ( ! defined( 'ABSPATH' ) ) {
20
  exit; // if accessed directly
21
  }
22
+ /**
23
+ * @todo set up the nonce verification
24
+ * phpcs:disable WordPress.Security.NonceVerification.Missing
25
+ */
26
  class TrackingSettings implements AdminSettingsInterface {
27
  const FORM_NAME = 'matomo';
28
  const NONCE_NAME = 'matomo_settings';
63
  }
64
 
65
  private function apply_settings() {
66
+ $keys_to_keep = [
67
  'track_mode',
68
  'track_across',
69
  'track_across_alias',
100
  'track_js_endpoint',
101
  'track_jserrors',
102
  'track_api_endpoint',
103
+ Settings::SITE_CURRENCY,
104
+ ];
105
 
106
  if ( matomo_has_tag_manager() ) {
107
  $keys_to_keep[] = 'tagmanger_container_ids';
108
  }
109
 
110
+ $values = [];
111
 
112
  // default value in case no role/ post type is selected to make sure we unset it if no role /post type is selected
113
+ $values['add_post_annotations'] = [];
114
+ $values['tagmanger_container_ids'] = [];
115
 
116
  $valid_currencies = $this->get_supported_currencies();
117
 
118
+ if ( ! empty( $_POST[ self::FORM_NAME ]['tracker_debug'] ) ) {
119
  $site_config_sync = new SiteConfigSync( $this->settings );
120
+ switch ( $_POST[ self::FORM_NAME ]['tracker_debug'] ) {
121
  case 'always':
122
+ $site_config_sync->set_config_value( 'Tracker', 'debug', 1 );
123
+ $site_config_sync->set_config_value( 'Tracker', 'debug_on_demand', 0 );
124
  break;
125
  case 'on_demand':
126
+ $site_config_sync->set_config_value( 'Tracker', 'debug', 0 );
127
+ $site_config_sync->set_config_value( 'Tracker', 'debug_on_demand', 1 );
128
  break;
129
  default:
130
+ $site_config_sync->set_config_value( 'Tracker', 'debug', 0 );
131
+ $site_config_sync->set_config_value( 'Tracker', 'debug_on_demand', 0 );
132
  }
133
  }
134
 
135
+ if ( empty( $_POST[ self::FORM_NAME ][ Settings::SITE_CURRENCY ] )
136
+ || ! array_key_exists( sanitize_text_field( wp_unslash( $_POST[ self::FORM_NAME ][ Settings::SITE_CURRENCY ] ) ), $valid_currencies ) ) {
137
+ $_POST[ self::FORM_NAME ][ Settings::SITE_CURRENCY ] = 'USD';
138
  }
139
 
140
  if ( ! empty( $_POST[ self::FORM_NAME ]['track_mode'] ) ) {
141
+ $track_mode = $this->get_track_mode();
 
142
  if ( self::TRACK_MODE_TAGMANAGER === $track_mode ) {
143
  // no noscript mode in this case
144
  $_POST[ self::FORM_NAME ]['track_noscript'] = '';
149
  if ( $this->must_update_tracker() === true ) {
150
  // We want to keep the tracking code when user switches between disabled and manually or disabled to disabled.
151
  if ( ! empty( $_POST[ self::FORM_NAME ]['tracking_code'] ) ) {
152
+ // don't process, this is a script
153
+ // phpcs:disable WordPress.Security.ValidatedSanitizedInput
154
  $_POST[ self::FORM_NAME ]['tracking_code'] = stripslashes( $_POST[ self::FORM_NAME ]['tracking_code'] );
155
+ // phpcs:enable WordPress.Security.ValidatedSanitizedInput
156
  } else {
157
  $_POST[ self::FORM_NAME ]['tracking_code'] = '';
158
  }
159
  if ( ! empty( $_POST[ self::FORM_NAME ]['noscript_code'] ) ) {
160
+ // don't process, this is a script
161
+ // phpcs:disable WordPress.Security.ValidatedSanitizedInput
162
  $_POST[ self::FORM_NAME ]['noscript_code'] = stripslashes( $_POST[ self::FORM_NAME ]['noscript_code'] );
163
+ // phpcs:enable WordPress.Security.ValidatedSanitizedInput
164
  } else {
165
  $_POST[ self::FORM_NAME ]['noscript_code'] = '';
166
  }
169
  $_POST[ self::FORM_NAME ]['tracking_code'] = '';
170
  }
171
  }
172
+ // phpcs:disable WordPress.Security.ValidatedSanitizedInput
173
  foreach ( $_POST[ self::FORM_NAME ] as $name => $value ) {
174
  if ( in_array( $name, $keys_to_keep, true ) ) {
175
  $values[ $name ] = $value;
176
  }
177
  }
178
+ // phpcs:enable WordPress.Security.ValidatedSanitizedInput
179
  $this->settings->apply_tracking_related_changes( $values );
180
 
181
  return true;
182
  }
183
 
184
+ private function get_track_mode() {
185
+ if ( ! empty( $_POST[ self::FORM_NAME ]['track_mode'] ) ) {
186
+ return sanitize_text_field( wp_unslash( $_POST[ self::FORM_NAME ]['track_mode'] ) );
187
+ }
188
+ return '';
189
+ }
190
  /**
191
  * Reauires form to be posted
192
+ *
193
  * @return bool
194
  */
195
+ private function must_update_tracker() {
196
+ $track_mode = $this->get_track_mode();
197
  $previus_track_mode = $this->settings->get_global_option( 'track_mode' );
198
  $must_update = false;
199
  if ( self::TRACK_MODE_MANUALLY === $track_mode
200
+ || ( self::TRACK_MODE_DISABLED === $track_mode &&
201
+ in_array( $previus_track_mode, [ self::TRACK_MODE_DISABLED, self::TRACK_MODE_MANUALLY ], true ) ) ) {
202
  // We want to keep the tracking code when user switches between disabled and manually or disabled to disabled.
203
  $must_update = true;
204
  }
205
+
206
  return $must_update;
207
  }
208
 
209
  /**
210
  * @return bool
211
  */
212
+ private function form_submitted() {
213
+ return isset( $_POST ) && ! empty( $_POST[ self::FORM_NAME ] )
214
+ && is_admin()
215
+ && $this->can_user_manage();
216
  }
217
 
218
  /**
220
  *
221
  * @return bool
222
  */
223
+ private function has_valid_html_comments( $field ) {
224
  $valid = true;
225
  if ( $this->form_submitted() === true ) {
226
  if ( $this->must_update_tracker() === true ) {
227
+ if ( ! empty( $_POST[ self::FORM_NAME ][ $field ] ) ) {
228
+ // phpcs:disable WordPress.Security.ValidatedSanitizedInput
229
+ $valid = $this->validate_html_comments( $_POST[ self::FORM_NAME ][ $field ] );
230
+ // phpcs:enable WordPress.Security.ValidatedSanitizedInput
231
  }
232
  }
233
  }
234
+
235
  return $valid;
236
  }
237
 
238
  /**
239
  * @param string $html html content to validate
240
+ *
241
  * @returns boolean
242
  */
243
  public function validate_html_comments( $html ) {
244
+ $opening = substr_count( $html, '<!--' );
245
+ $closing = substr_count( $html, '-->' );
246
+
247
+ return ( $opening === $closing );
248
  }
249
 
250
  public function show_settings() {
251
+ $was_updated = false;
252
+ $settings_errors = [];
253
  if ( $this->has_valid_html_comments( 'tracking_code' ) !== true ) {
254
+ $settings_errors[] = __( 'Settings have not been saved. There is an issue with the HTML comments in the field "Tracking code". Make sure all opened comments (<!--) are closed (-->) correctly.', 'matomo' );
255
  }
256
  if ( $this->has_valid_html_comments( 'noscript_code' ) !== true ) {
257
+ $settings_errors[] = __( 'Settings have not been saved. There is an issue with the HTML comments in the field "Noscript code". Make sure all opened comments (<!--) are closed (-->) correctly.', 'matomo' );
258
  }
259
+ if ( count( $settings_errors ) === 0 ) {
260
  $was_updated = $this->update_if_submitted();
261
  }
262
 
263
+ $settings = $this->settings;
264
 
265
  $containers = $this->get_active_containers();
266
 
267
+ $track_modes = [
268
  self::TRACK_MODE_DISABLED => esc_html__( 'Disabled', 'matomo' ),
269
  self::TRACK_MODE_DEFAULT => esc_html__( 'Default tracking', 'matomo' ),
270
  self::TRACK_MODE_MANUALLY => esc_html__( 'Enter manually', 'matomo' ),
271
+ ];
272
 
273
  if ( ! empty( $containers ) ) {
274
  $track_modes[ self::TRACK_MODE_TAGMANAGER ] = esc_html__( 'Tag Manager', 'matomo' );
290
  /**
291
  * @return string[]
292
  */
293
+ private function get_cookie_consent_modes() {
 
294
  $modes = [];
295
+ foreach ( CookieConsent::get_available_options() as $option => $description ) {
296
+ $modes[ $option ] = $description;
297
  }
298
+
299
  return $modes;
300
  }
301
 
302
+ private function get_supported_currencies() {
303
+ $all = include dirname( MATOMO_ANALYTICS_FILE ) . '/app/core/Intl/Data/Resources/currencies.php';
304
+ $currencies = [];
305
+ foreach ( $all as $key => $single ) {
306
+ $currencies[ $key ] = $single[0] . ' ' . $single[1];
 
307
  }
308
+
309
  return $currencies;
310
  }
311
 
312
  public function get_active_containers() {
313
  // we don't use Matomo API here to avoid needing to bootstrap Matomo which is slow and could break things
314
+ $containers = [];
315
  if ( matomo_has_tag_manager() ) {
316
  global $wpdb;
317
+ $db_settings = new \WpMatomo\Db\Settings();
318
+ $container_table = $db_settings->prefix_table_name( 'tagmanager_container' );
319
  try {
320
+ // phpcs:disable WordPress.DB
321
  $containers = $wpdb->get_results( sprintf( 'SELECT `idcontainer`, `name` FROM %s where `status` = "active"', $container_table ) );
322
+ // phpcs:enable WordPress.DB
323
+ } catch ( Exception $e ) {
324
  // table may not exist yet etc
325
+ $containers = [];
326
  }
327
  }
328
+ $by_id = [];
329
  foreach ( $containers as $container ) {
330
  $by_id[ $container->idcontainer ] = $container->name;
331
  }
332
 
333
  return $by_id;
334
  }
 
 
335
  }
classes/WpMatomo/Admin/TrackingSettings/Forms.php CHANGED
@@ -14,6 +14,7 @@
14
  namespace WpMatomo\Admin\TrackingSettings;
15
 
16
  use Piwik\Config;
 
17
  use WpMatomo\Admin\TrackingSettings;
18
  use WpMatomo\Bootstrap;
19
  use WpMatomo\Settings;
@@ -21,7 +22,10 @@ use WpMatomo\Settings;
21
  if ( ! defined( 'ABSPATH' ) ) {
22
  exit; // if accessed directly
23
  }
24
-
 
 
 
25
  class Forms {
26
  /**
27
  * @var Settings
@@ -59,7 +63,7 @@ class Forms {
59
  * @param string $on_change javascript for onchange event (default: empty)
60
  */
61
  public function show_checkbox( $id, $name, $description, $is_hidden = false, $group_name = '', $hide_description = true, $on_change = '' ) {
62
- printf( '<tr class="' . esc_attr( $group_name ) . ( $is_hidden ? ' hidden' : '' ) . '"><th scope="row"><label for="%2$s">%s</label>:</th><td><input type="checkbox" value="1"' . ( $this->settings->get_global_option( $id ) ? ' checked="checked"' : '' ) . ' onchange="jQuery(\'#%s\').val(this.checked?1:0);%s" /><input id="%2$s" type="hidden" name="' . esc_attr( TrackingSettings::FORM_NAME ) . '[%2$s]" value="' . (int) $this->settings->get_global_option( $id ) . '" /> %s</td></tr>', esc_html( $name ), $id, $on_change, $this->get_description( $id, $description, $hide_description ) );
63
  }
64
 
65
  /**
@@ -92,8 +96,8 @@ class Forms {
92
  *
93
  * @param string $text Text to show
94
  */
95
- public function show_text( $text , $group_name = '' ) {
96
- printf( '<tr class="%s"><td colspan="2"><p>%s</p></td></tr>', $group_name, esc_html( $text ) );
97
  }
98
 
99
  /**
@@ -101,8 +105,8 @@ class Forms {
101
  *
102
  * @param string $text Text to show
103
  */
104
- public function show_headline( $text , $group_name = '') {
105
- printf( '<tr class="%s"><td colspan="2"><h3>%s</h3></td></tr>', $group_name, esc_html( $text ) );
106
  }
107
 
108
  /**
@@ -134,23 +138,24 @@ class Forms {
134
  * @param boolean $hide_description $hideDescription set to false to show description initially (default: true)
135
  * @param boolean $global set to false if the textarea shows a site-specific option (default: true)
136
  */
137
- public function show_select( $id, $name, $options = array(), $description = '', $on_change = '', $is_hidden = false, $group_name = '', $hide_description = true, $global = true ) {
138
  $options_list = '';
139
 
140
- if ($id === 'tracker_debug' && !\WpMatomo::is_safe_mode() && !$this->settings->is_network_enabled()) {
141
  Bootstrap::do_bootstrap();
142
- if (Config::getInstance()->Tracker['debug']) {
143
  $default = 'always';
144
- } elseif (Config::getInstance()->Tracker['debug_on_demand']) {
145
  $default = 'on_demand';
146
  } else {
147
  $default = 'disabled';
148
  }
149
  } else {
150
- $default = $global ? $this->settings->get_global_option( $id ) : $this->settings->get_option( $id );
151
  }
152
  if ( is_array( $options ) ) {
153
  foreach ( $options as $key => $value ) {
 
154
  $options_list .= sprintf( '<option value="%s"' . ( $key == $default ? ' selected="selected"' : '' ) . '>%s</option>', esc_attr( $key ), esc_html( $value ) );
155
  }
156
  }
@@ -172,5 +177,4 @@ class Forms {
172
  public function show_box( $type, $icon, $content ) {
173
  printf( '<tr><td colspan="2"><div class="%s"><p><span class="dashicons dashicons-%s"></span> %s</p></div></td></tr>', esc_attr( $type ), esc_attr( $icon ), esc_html( $content ) );
174
  }
175
-
176
  }
14
  namespace WpMatomo\Admin\TrackingSettings;
15
 
16
  use Piwik\Config;
17
+ use WpMatomo;
18
  use WpMatomo\Admin\TrackingSettings;
19
  use WpMatomo\Bootstrap;
20
  use WpMatomo\Settings;
22
  if ( ! defined( 'ABSPATH' ) ) {
23
  exit; // if accessed directly
24
  }
25
+ /**
26
+ * we deal with HTML
27
+ * phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
28
+ */
29
  class Forms {
30
  /**
31
  * @var Settings
63
  * @param string $on_change javascript for onchange event (default: empty)
64
  */
65
  public function show_checkbox( $id, $name, $description, $is_hidden = false, $group_name = '', $hide_description = true, $on_change = '' ) {
66
+ printf( '<tr class="' . esc_attr( $group_name ) . ( $is_hidden ? ' hidden' : '' ) . '"><th scope="row"><label for="%2$s">%s</label>:</th><td><input type="checkbox" value="1"' . ( $this->settings->get_global_option( $id ) ? ' checked="checked"' : '' ) . ' onchange="jQuery(\'#%s\').val(this.checked?1:0);%s" /><input id="%2$s" type="hidden" name="' . esc_attr( TrackingSettings::FORM_NAME ) . '[%2$s]" value="' . (int) $this->settings->get_global_option( $id ) . '" /> %s</td></tr>', esc_html( $name ), esc_attr( $id ), $on_change, $this->get_description( $id, $description, $hide_description ) );
67
  }
68
 
69
  /**
96
  *
97
  * @param string $text Text to show
98
  */
99
+ public function show_text( $text, $group_name = '' ) {
100
+ printf( '<tr class="%s"><td colspan="2"><p>%s</p></td></tr>', esc_attr( $group_name ), esc_html( $text ) );
101
  }
102
 
103
  /**
105
  *
106
  * @param string $text Text to show
107
  */
108
+ public function show_headline( $text, $group_name = '' ) {
109
+ printf( '<tr class="%s"><td colspan="2"><h3>%s</h3></td></tr>', esc_attr( $group_name ), esc_html( $text ) );
110
  }
111
 
112
  /**
138
  * @param boolean $hide_description $hideDescription set to false to show description initially (default: true)
139
  * @param boolean $global set to false if the textarea shows a site-specific option (default: true)
140
  */
141
+ public function show_select( $id, $name, $options = [], $description = '', $on_change = '', $is_hidden = false, $group_name = '', $hide_description = true, $global = true ) {
142
  $options_list = '';
143
 
144
+ if ( 'tracker_debug' === $id && ! WpMatomo::is_safe_mode() && ! $this->settings->is_network_enabled() ) {
145
  Bootstrap::do_bootstrap();
146
+ if ( Config::getInstance()->Tracker['debug'] ) {
147
  $default = 'always';
148
+ } elseif ( Config::getInstance()->Tracker['debug_on_demand'] ) {
149
  $default = 'on_demand';
150
  } else {
151
  $default = 'disabled';
152
  }
153
  } else {
154
+ $default = $global ? $this->settings->get_global_option( $id ) : $this->settings->get_option( $id );
155
  }
156
  if ( is_array( $options ) ) {
157
  foreach ( $options as $key => $value ) {
158
+ // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
159
  $options_list .= sprintf( '<option value="%s"' . ( $key == $default ? ' selected="selected"' : '' ) . '>%s</option>', esc_attr( $key ), esc_html( $value ) );
160
  }
161
  }
177
  public function show_box( $type, $icon, $content ) {
178
  printf( '<tr><td colspan="2"><div class="%s"><p><span class="dashicons dashicons-%s"></span> %s</p></div></td></tr>', esc_attr( $type ), esc_attr( $icon ), esc_html( $content ) );
179
  }
 
180
  }
classes/WpMatomo/Admin/views/access.php CHANGED
@@ -12,10 +12,12 @@ if ( ! defined( 'ABSPATH' ) ) {
12
 
13
  use WpMatomo\Access;
14
  use WpMatomo\Admin\AccessSettings;
 
 
15
 
16
  /** @var Access $access */
17
- /** @var \WpMatomo\Roles $roles */
18
- /** @var \WpMatomo\Capabilities $capabilites */
19
  ?>
20
 
21
  <p><?php esc_html_e( 'Manage which roles can view and manage your reporting data.', 'matomo' ); ?></p>
@@ -35,7 +37,7 @@ use WpMatomo\Admin\AccessSettings;
35
  foreach ( $roles->get_available_roles_for_configuration() as $matomo_role_id => $matomo_role_name ) {
36
  echo '<tr><td>';
37
  echo esc_html( $matomo_role_name ) . '</td>';
38
- echo "<td><select name='" . AccessSettings::FORM_NAME . '[' . esc_attr( $matomo_role_id ) . "]'>";
39
  $matomo_value = $access->get_permission_for_role( $matomo_role_id );
40
  foreach ( Access::$matomo_permissions as $matomo_permission => $matomo_display_name ) {
41
  echo "<option value='" . esc_attr( $matomo_permission ) . "' " . ( $matomo_value === $matomo_permission ? 'selected' : '' ) . '>' . esc_html( $matomo_display_name ) . '</option>';
@@ -52,30 +54,34 @@ use WpMatomo\Admin\AccessSettings;
52
  </form>
53
 
54
  <p>
55
- <?php
56
- if (!is_multisite()) {
57
- esc_html_e( 'A user with role administrator automatically has the super user role.', 'matomo' );
58
- }
59
- ?>
60
  <?php esc_html_e( 'Learn about the differences between these Matomo roles:', 'matomo' ); ?>
61
- <a href="https://matomo.org/faq/general/faq_70/" target="_blank" rel="noopener"><?php esc_html_e( 'View', 'matomo' ); ?></a>,
 
62
  <a href="https://matomo.org/faq/general/faq_26910/" target="_blank"
63
  rel="noopener"><?php esc_html_e( 'Write', 'matomo' ); ?></a>,
64
- <a href="https://matomo.org/faq/general/faq_69/" target="_blank" rel="noopener"><?php esc_html_e( 'Admin', 'matomo' ); ?></a>,
 
65
  <a href="https://matomo.org/faq/general/faq_35/" target="_blank"
66
  rel="noopener"><?php esc_html_e( 'Super User', 'matomo' ); ?></a><br/>
67
- <?php esc_html_e( 'Want to redirect to the home page when not logged in?', 'matomo' ); ?> <a href="https://matomo.org/faq/wordpress/how-do-i-hide-my-wordpress-login-url-when-someone-accesses-a-matomo-report-directly/" target="_blank" rel="noreferrer noopener"><?php esc_html_e( 'Learn more', 'matomo' ); ?></a>
 
 
68
  </p>
69
 
70
  <h2><?php esc_html_e( 'Roles', 'matomo' ); ?></h2>
71
  <p>
72
- <?php
73
- esc_html_e(
74
- 'Want to give individual users access to Matomo? Simply create a user in your WordPress and assign of these roles
75
  to the user:',
76
- 'matomo'
77
- )
78
- ?>
79
  </p>
80
  <ul class="matomo-list">
81
  <?php foreach ( $roles->get_matomo_roles() as $matomo_role_config ) { ?>
@@ -85,18 +91,20 @@ esc_html_e(
85
 
86
  <h2><?php esc_html_e( 'Capabilities', 'matomo' ); ?></h2>
87
  <p>
88
- <?php
89
- esc_html_e(
90
- 'You can also install a WordPress plugin which lets you manage capabilities for each individual users. These are
91
  the supported capabilities:',
92
- 'matomo'
93
- )
94
- ?>
95
  </p>
96
  <ul class="matomo-list">
97
  <?php
98
  foreach ( $capabilites->get_all_capabilities_sorted_by_highest_permission() as $matomo_cap_name ) {
99
  ?>
100
  <li><?php echo esc_html( $matomo_cap_name ); ?></li>
101
- <?php } ?>
102
- </ul>
 
 
12
 
13
  use WpMatomo\Access;
14
  use WpMatomo\Admin\AccessSettings;
15
+ use WpMatomo\Capabilities;
16
+ use WpMatomo\Roles;
17
 
18
  /** @var Access $access */
19
+ /** @var Roles $roles */
20
+ /** @var Capabilities $capabilites */
21
  ?>
22
 
23
  <p><?php esc_html_e( 'Manage which roles can view and manage your reporting data.', 'matomo' ); ?></p>
37
  foreach ( $roles->get_available_roles_for_configuration() as $matomo_role_id => $matomo_role_name ) {
38
  echo '<tr><td>';
39
  echo esc_html( $matomo_role_name ) . '</td>';
40
+ echo "<td><select name='" . esc_attr( AccessSettings::FORM_NAME ) . '[' . esc_attr( $matomo_role_id ) . "]'>";
41
  $matomo_value = $access->get_permission_for_role( $matomo_role_id );
42
  foreach ( Access::$matomo_permissions as $matomo_permission => $matomo_display_name ) {
43
  echo "<option value='" . esc_attr( $matomo_permission ) . "' " . ( $matomo_value === $matomo_permission ? 'selected' : '' ) . '>' . esc_html( $matomo_display_name ) . '</option>';
54
  </form>
55
 
56
  <p>
57
+ <?php
58
+ if ( ! is_multisite() ) {
59
+ esc_html_e( 'A user with role administrator automatically has the super user role.', 'matomo' );
60
+ }
61
+ ?>
62
  <?php esc_html_e( 'Learn about the differences between these Matomo roles:', 'matomo' ); ?>
63
+ <a href="https://matomo.org/faq/general/faq_70/" target="_blank"
64
+ rel="noopener"><?php esc_html_e( 'View', 'matomo' ); ?></a>,
65
  <a href="https://matomo.org/faq/general/faq_26910/" target="_blank"
66
  rel="noopener"><?php esc_html_e( 'Write', 'matomo' ); ?></a>,
67
+ <a href="https://matomo.org/faq/general/faq_69/" target="_blank"
68
+ rel="noopener"><?php esc_html_e( 'Admin', 'matomo' ); ?></a>,
69
  <a href="https://matomo.org/faq/general/faq_35/" target="_blank"
70
  rel="noopener"><?php esc_html_e( 'Super User', 'matomo' ); ?></a><br/>
71
+ <?php esc_html_e( 'Want to redirect to the home page when not logged in?', 'matomo' ); ?> <a
72
+ href="https://matomo.org/faq/wordpress/how-do-i-hide-my-wordpress-login-url-when-someone-accesses-a-matomo-report-directly/"
73
+ target="_blank" rel="noreferrer noopener"><?php esc_html_e( 'Learn more', 'matomo' ); ?></a>
74
  </p>
75
 
76
  <h2><?php esc_html_e( 'Roles', 'matomo' ); ?></h2>
77
  <p>
78
+ <?php
79
+ esc_html_e(
80
+ 'Want to give individual users access to Matomo? Simply create a user in your WordPress and assign of these roles
81
  to the user:',
82
+ 'matomo'
83
+ )
84
+ ?>
85
  </p>
86
  <ul class="matomo-list">
87
  <?php foreach ( $roles->get_matomo_roles() as $matomo_role_config ) { ?>
91
 
92
  <h2><?php esc_html_e( 'Capabilities', 'matomo' ); ?></h2>
93
  <p>
94
+ <?php
95
+ esc_html_e(
96
+ 'You can also install a WordPress plugin which lets you manage capabilities for each individual users. These are
97
  the supported capabilities:',
98
+ 'matomo'
99
+ )
100
+ ?>
101
  </p>
102
  <ul class="matomo-list">
103
  <?php
104
  foreach ( $capabilites->get_all_capabilities_sorted_by_highest_permission() as $matomo_cap_name ) {
105
  ?>
106
  <li><?php echo esc_html( $matomo_cap_name ); ?></li>
107
+ <?php
108
+ }
109
+ ?>
110
+ </ul>
classes/WpMatomo/Admin/views/advanced_settings.php CHANGED
@@ -10,7 +10,10 @@
10
  * https://github.com/braekling/WP-Matomo
11
  *
12
  */
13
-
 
 
 
14
  use WpMatomo\Admin\AdvancedSettings;
15
 
16
  if ( ! defined( 'ABSPATH' ) ) {
@@ -29,45 +32,55 @@ if ( $was_updated ) {
29
  <form method="post">
30
  <?php wp_nonce_field( AdvancedSettings::NONCE_NAME ); ?>
31
 
32
- <p><?php esc_html_e( 'Advanced settings', 'matomo' ); ?></p>
33
- <table class="matomo-tracking-form widefat">
34
- <tbody>
35
- <tr>
36
- <th width="20%" scope="row"><label for="matomo[proxy_client_header]"><?php esc_html_e( 'Proxy IP headers', 'matomo' ) ?>:</label>
37
- </th>
38
- <td>
 
39
  <?php
40
- echo '<span style="white-space: nowrap;display: inline-block;"><input type="radio" ' . ( empty($matomo_client_headers) ? 'checked="checked" ' : '' ) . ' value="REMOTE_ADDR" name="matomo[proxy_client_header]" /> <code>REMOTE_ADDR</code> ' . ( ! empty( $_SERVER[ 'REMOTE_ADDR' ] ) ? esc_html( $_SERVER[ 'REMOTE_ADDR' ] ) : esc_html__( 'No value found', 'matomo' ) ) . ' (' . esc_html__( 'Default', 'matomo' ) .')</span>';
41
  foreach ( AdvancedSettings::$valid_host_headers as $host_header ) {
42
- echo '<span style="white-space: nowrap;display: inline-block;"><input type="radio" ' . ( in_array( $host_header, $matomo_client_headers, true ) ? 'checked="checked" ' : '' ) . 'value="'. esc_attr($host_header).'" name="matomo[proxy_client_header]" /> <code>' . $host_header . '</code> ' . ( ! empty( $_SERVER[ $host_header ] ) ? ('<strong>'. esc_html( $_SERVER[ $host_header ] ) . '</strong>') : esc_html__( 'No value found', 'matomo' ) ) . ' &nbsp; </span>';
 
43
  }
44
  ?>
45
- </td>
46
- <td width="50%">
47
- <?php esc_html_e( 'We detected you have the following IP address:', 'matomo' ) ?>
48
- <?php echo esc_html( $matomo_detected_ip ) ?> <br>
49
- <?php echo sprintf(esc_html__( 'To compare this value with your actual IP address %1$splease click here%2$s.', 'matomo' ), '<a rel="noreferrer noopener" target="_blank" href="https://matomo.org/ip.php">', '</a>') ?><br><br>
50
- <?php esc_html_e( 'Should your IP address not match the above value, your WordPress might be behind a proxy and you may need to select a different HTTP header depending on which of the values on the left shows your correct IP address.', 'matomo' ) ?>
51
- </td>
52
- </tr>
53
- <?php if (!defined('MATOMO_REMOVE_ALL_DATA')) { ?>
54
- <tr>
55
- <th width="20%" scope="row"><label for="matomo[delete_all_data]"><?php esc_html_e( 'Delete all data on uninstall', 'matomo' ) ?>:</label>
56
- </th>
57
- <td>
58
- <?php
59
- echo '<span style="white-space: nowrap;display: inline-block;"><input type="checkbox" ' . ( !empty($matomo_delete_all_data) ? 'checked="checked" ' : '' ) . ' value="1" name="matomo[delete_all_data]" /> '.esc_html__( 'Yes', 'matomo' ).'</span>';
60
- ?>
61
- </td>
62
- <td width="50%">
63
- By default, when you uninstall the Matomo plugin, all data is deleted and cannot be restored unless you have backups. When you disable this feature, the tracked data in the database will be kept. This can be useful to prevent accidental deletion of all your historical analytics data when you uninstall the plugin. <a href="https://matomo.org/faq/wordpress/how-do-i-delete-or-reset-the-matomo-for-wordpress-data-completely/" target="_blank" rel="noreferrer noopener">Learn more</a>
64
- </td>
65
- </tr>
66
- <?php } ?>
67
- <tr>
68
- <td colspan="3"><p class="submit"><input name="Submit" type="submit" class="button-primary"
69
- value="<?php esc_attr_e( 'Save Changes', 'matomo' ) ?>"/></p></td>
70
- </tr>
71
- </tbody>
72
- </table>
73
- </form>
 
 
 
 
 
 
 
 
10
  * https://github.com/braekling/WP-Matomo
11
  *
12
  */
13
+ /**
14
+ * phpcs consider all our variables as global and want them prefixed with matomo
15
+ * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
16
+ */
17
  use WpMatomo\Admin\AdvancedSettings;
18
 
19
  if ( ! defined( 'ABSPATH' ) ) {
32
  <form method="post">
33
  <?php wp_nonce_field( AdvancedSettings::NONCE_NAME ); ?>
34
 
35
+ <p><?php esc_html_e( 'Advanced settings', 'matomo' ); ?></p>
36
+ <table class="matomo-tracking-form widefat">
37
+ <tbody>
38
+ <tr>
39
+ <th width="20%" scope="row"><label
40
+ for="matomo[proxy_client_header]"><?php esc_html_e( 'Proxy IP headers', 'matomo' ); ?>:</label>
41
+ </th>
42
+ <td>
43
  <?php
44
+ echo '<span style="white-space: nowrap;display: inline-block;"><input type="radio" ' . ( empty( $matomo_client_headers ) ? 'checked="checked" ' : '' ) . ' value="REMOTE_ADDR" name="matomo[proxy_client_header]" /> <code>REMOTE_ADDR</code> ' . ( ! empty( $_SERVER['REMOTE_ADDR'] ) ? esc_html( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) : esc_html__( 'No value found', 'matomo' ) ) . ' (' . esc_html__( 'Default', 'matomo' ) . ')</span>';
45
  foreach ( AdvancedSettings::$valid_host_headers as $host_header ) {
46
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
47
+ echo '<span style="white-space: nowrap;display: inline-block;"><input type="radio" ' . ( in_array( $host_header, $matomo_client_headers, true ) ? 'checked="checked" ' : '' ) . 'value="' . esc_attr( $host_header ) . '" name="matomo[proxy_client_header]" /> <code>' . esc_html( $host_header ) . '</code> ' . ( ! empty( $_SERVER[ $host_header ] ) ? ( '<strong>' . esc_html( sanitize_text_field( wp_unslash( $_SERVER[ $host_header ] ) ) ) . '</strong>' ) : esc_html__( 'No value found', 'matomo' ) ) . ' &nbsp; </span>';
48
  }
49
  ?>
50
+ </td>
51
+ <td width="50%">
52
+ <?php esc_html_e( 'We detected you have the following IP address:', 'matomo' ); ?>
53
+ <?php echo esc_html( $matomo_detected_ip ); ?> <br>
54
+ <?php echo sprintf( esc_html__( 'To compare this value with your actual IP address %1$splease click here%2$s.', 'matomo' ), '<a rel="noreferrer noopener" target="_blank" href="https://matomo.org/ip.php">', '</a>' ); ?>
55
+ <br><br>
56
+ <?php esc_html_e( 'Should your IP address not match the above value, your WordPress might be behind a proxy and you may need to select a different HTTP header depending on which of the values on the left shows your correct IP address.', 'matomo' ); ?>
57
+ </td>
58
+ </tr>
59
+ <?php if ( ! defined( 'MATOMO_REMOVE_ALL_DATA' ) ) { ?>
60
+ <tr>
61
+ <th width="20%" scope="row"><label
62
+ for="matomo[delete_all_data]"><?php esc_html_e( 'Delete all data on uninstall', 'matomo' ); ?>
63
+ :</label>
64
+ </th>
65
+ <td>
66
+ <?php
67
+ echo '<span style="white-space: nowrap;display: inline-block;"><input type="checkbox" ' . ( ! empty( $matomo_delete_all_data ) ? 'checked="checked" ' : '' ) . ' value="1" name="matomo[delete_all_data]" /> ' . esc_html__( 'Yes', 'matomo' ) . '</span>';
68
+ ?>
69
+ </td>
70
+ <td width="50%">
71
+ By default, when you uninstall the Matomo plugin, all data is deleted and cannot be restored unless
72
+ you have backups. When you disable this feature, the tracked data in the database will be kept. This
73
+ can be useful to prevent accidental deletion of all your historical analytics data when you
74
+ uninstall the plugin. <a
75
+ href="https://matomo.org/faq/wordpress/how-do-i-delete-or-reset-the-matomo-for-wordpress-data-completely/"
76
+ target="_blank" rel="noreferrer noopener">Learn more</a>
77
+ </td>
78
+ </tr>
79
+ <?php } ?>
80
+ <tr>
81
+ <td colspan="3"><p class="submit"><input name="Submit" type="submit" class="button-primary"
82
+ value="<?php esc_attr_e( 'Save Changes', 'matomo' ); ?>"/></p></td>
83
+ </tr>
84
+ </tbody>
85
+ </table>
86
+ </form>
classes/WpMatomo/Admin/views/exclusion_settings.php CHANGED
@@ -7,13 +7,13 @@
7
  * @package matomo
8
  * Code Based on
9
  * @author Andr&eacute; Br&auml;kling
10
- * @package WP_Matomo
11
  * https://github.com/braekling/matomo
12
  *
13
  */
14
 
15
  use Piwik\Piwik;
16
  use WpMatomo\Admin\ExclusionSettings;
 
17
 
18
  if ( ! defined( 'ABSPATH' ) ) {
19
  exit;
@@ -25,7 +25,7 @@ if ( ! defined( 'ABSPATH' ) ) {
25
  /** @var string $excluded_user_agents */
26
  /** @var string $excluded_query_params */
27
  /** @var bool|string|int $keep_url_fragments */
28
- /** @var \WpMatomo\Settings $settings */
29
 
30
  ?>
31
 
@@ -34,120 +34,132 @@ if ( $was_updated ) {
34
  include 'update_notice_clear_cache.php';
35
  }
36
  ?>
37
- <?php if ($settings->is_network_enabled() && is_network_admin()) { ?>
38
- <h2>Exclusion settings</h2>
39
- <p>
40
- Exclusion settings have to be configured on a per blog basis.
41
- Should you wish to change any setting, please go to the Matomo exclusion settings within each blog.
42
- We are hoping to improve this in the future.
43
- </p>
44
  <?php } else { ?>
45
 
46
- <form method="post">
47
- <?php wp_nonce_field( ExclusionSettings::NONCE_NAME ); ?>
48
 
49
- <p><?php esc_html_e( 'Configure exclusions.', 'matomo' ); ?></p>
50
- <table class="matomo-tracking-form widefat">
51
- <tbody>
52
 
53
- <tr>
54
- <th width="20%" scope="row"><label><?php esc_html_e( 'Tracking filter', 'matomo' ); ?></label>:
55
- </th>
56
- <td>
57
- <?php
58
- $matomo_tracking_caps = \WpMatomo\Settings::OPTION_KEY_STEALTH;
59
- $matomo_filter = $settings->get_global_option( $matomo_tracking_caps );
60
- foreach ( $wp_roles->role_names as $matomo_key => $matomo_name ) {
61
- echo '<input type="checkbox" ' . ( isset( $matomo_filter [ $matomo_key ] ) && $matomo_filter [ $matomo_key ] ? 'checked="checked" ' : '' ) . 'value="1" name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[' . esc_attr( $matomo_tracking_caps ) . '][' . esc_attr( $matomo_key ) . ']" /> ' . esc_html( $matomo_name ) . ' &nbsp; <br />';
62
- }
63
- ?>
64
- </td>
65
- <td width="50%">
66
- <?php echo sprintf( esc_html__( 'Choose users by user role you do %1$snot%2$s want to track.', 'matomo' ), '<strong>', '</strong>' ); ?>
67
- <?php if ($settings->is_network_enabled()) { ?>
68
- <br><p><strong>This setting will be applied to all blogs. Changing it here also changes it for other blogs.</strong></p>
69
- <?php } ?>
70
- </td>
71
- </tr>
72
- <tr>
73
- <th width="20%" scope="row"><label><?php echo esc_html( Piwik::translate( 'SitesManager_GlobalListExcludedIps' ) ); ?></label>:
74
- </th>
75
- <td width="30%">
76
- <?php echo sprintf( '<textarea cols="40" rows="4" id="%1$s" name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">%2$s</textarea>', 'excluded_ips', esc_html( $excluded_ips ) ); ?>
77
- </td>
78
- <td width="50%">
79
- <?php
80
- echo Piwik::translate(
81
- 'SitesManager_HelpExcludedIpAddresses',
82
- array(
83
- '1.2.3.4/24',
84
- '1.2.3.*',
85
- '1.2.*.*',
 
 
 
 
 
86
  )
87
- )
88
- ?>
89
- <br/>
90
- <?php echo esc_html( Piwik::translate( 'SitesManager_YourCurrentIpAddressIs', esc_html( $current_ip ) ) ); ?>
91
- </td>
92
- </tr>
93
- <tr>
94
- <th scope="row"><label><?php echo esc_html( Piwik::translate( 'SitesManager_GlobalListExcludedQueryParameters' ) ); ?></label>:
95
- </th>
96
- <td>
97
- <?php echo sprintf( '<textarea cols="40" rows="4" id="%1$s" name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">%2$s</textarea>', 'excluded_query_parameters', esc_html( $excluded_query_params ) ); ?>
98
- </td>
99
- <td>
100
- <?php echo esc_html( Piwik::translate( 'SitesManager_ListOfQueryParametersToExclude', '/^sess.*|.*[dD]ate$/' ) ); ?>
101
- <?php echo esc_html( Piwik::translate( 'SitesManager_PiwikWillAutomaticallyExcludeCommonSessionParameters', 'phpsessid, sessionid, ...' ) ); ?>
102
- </td>
103
- </tr>
104
- <tr>
105
- <th scope="row"><label><?php echo esc_html( Piwik::translate( 'SitesManager_GlobalListExcludedUserAgents' ) ); ?></label>:
106
- </th>
107
- <td>
108
- <?php echo sprintf( '<textarea cols="40" rows="4" id="%1$s" name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">%2$s</textarea>', 'excluded_user_agents', esc_html( $excluded_user_agents ) ); ?>
109
- </td>
110
- <td>
 
111
 
112
- <?php echo esc_html( Piwik::translate( 'SitesManager_GlobalExcludedUserAgentHelp1' ) ); ?>
113
- <br/>
114
- <?php echo esc_html( Piwik::translate( 'SitesManager_GlobalListExcludedUserAgents_Desc' ) ); ?>
115
- <?php echo esc_html( Piwik::translate( 'SitesManager_GlobalExcludedUserAgentHelp2' ) ); ?>
116
 
117
- </td>
118
- </tr>
119
- <tr>
120
- <th scope="row"><label><?php echo esc_html( Piwik::translate( 'SitesManager_KeepURLFragmentsLong' ) ); ?></label>:
121
- </th>
122
- <td>
123
- <?php echo sprintf( '<input type="checkbox" value="1" %2$s name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">', 'keep_url_fragments', $keep_url_fragments ? ' checked="checked"' : '' ); ?>
124
- </td>
125
- <td>
 
 
 
 
126
 
127
- <?php
128
- echo Piwik::translate(
129
- 'SitesManager_KeepURLFragmentsHelp',
130
- array(
131
- '<em>#</em>',
132
- '<em>example.org/index.html#first_section</em>',
133
- '<em>example.org/index.html</em>',
 
 
 
134
  )
135
- )
136
- ?>
137
- <br/>
138
- <?php echo esc_html( Piwik::translate( 'SitesManager_KeepURLFragmentsHelp2' ) ); ?>
139
 
140
- </td>
141
- </tr>
142
- <tr>
143
- <td colspan="3">
144
- <p class="submit"><input name="Submit" type="submit" class="button-primary"
145
- value="<?php echo esc_attr__( 'Save Changes', 'matomo' ); ?>"/></p>
146
- </td>
147
- </tr>
148
 
149
- </tbody>
150
- </table>
151
- </form>
152
 
153
- <?php } ?>
7
  * @package matomo
8
  * Code Based on
9
  * @author Andr&eacute; Br&auml;kling
 
10
  * https://github.com/braekling/matomo
11
  *
12
  */
13
 
14
  use Piwik\Piwik;
15
  use WpMatomo\Admin\ExclusionSettings;
16
+ use WpMatomo\Settings;
17
 
18
  if ( ! defined( 'ABSPATH' ) ) {
19
  exit;
25
  /** @var string $excluded_user_agents */
26
  /** @var string $excluded_query_params */
27
  /** @var bool|string|int $keep_url_fragments */
28
+ /** @var Settings $settings */
29
 
30
  ?>
31
 
34
  include 'update_notice_clear_cache.php';
35
  }
36
  ?>
37
+ <?php if ( $settings->is_network_enabled() && is_network_admin() ) { ?>
38
+ <h2>Exclusion settings</h2>
39
+ <p>
40
+ Exclusion settings have to be configured on a per blog basis.
41
+ Should you wish to change any setting, please go to the Matomo exclusion settings within each blog.
42
+ We are hoping to improve this in the future.
43
+ </p>
44
  <?php } else { ?>
45
 
46
+ <form method="post">
47
+ <?php wp_nonce_field( ExclusionSettings::NONCE_NAME ); ?>
48
 
49
+ <p><?php esc_html_e( 'Configure exclusions.', 'matomo' ); ?></p>
50
+ <table class="matomo-tracking-form widefat">
51
+ <tbody>
52
 
53
+ <tr>
54
+ <th width="20%" scope="row"><label><?php esc_html_e( 'Tracking filter', 'matomo' ); ?></label>:
55
+ </th>
56
+ <td>
57
+ <?php
58
+ $matomo_tracking_caps = Settings::OPTION_KEY_STEALTH;
59
+ $matomo_filter = $settings->get_global_option( $matomo_tracking_caps );
60
+ foreach ( $wp_roles->role_names as $matomo_key => $matomo_name ) {
61
+ echo '<input type="checkbox" ' . ( isset( $matomo_filter [ $matomo_key ] ) && $matomo_filter [ $matomo_key ] ? 'checked="checked" ' : '' ) . 'value="1" name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[' . esc_attr( $matomo_tracking_caps ) . '][' . esc_attr( $matomo_key ) . ']" /> ' . esc_html( $matomo_name ) . ' &nbsp; <br />';
62
+ }
63
+ ?>
64
+ </td>
65
+ <td width="50%">
66
+ <?php echo sprintf( esc_html__( 'Choose users by user role you do %1$snot%2$s want to track.', 'matomo' ), '<strong>', '</strong>' ); ?>
67
+ <?php if ( $settings->is_network_enabled() ) { ?>
68
+ <br><p><strong>This setting will be applied to all blogs. Changing it here also changes it for
69
+ other blogs.</strong></p>
70
+ <?php } ?>
71
+ </td>
72
+ </tr>
73
+ <tr>
74
+ <th width="20%" scope="row">
75
+ <label><?php echo esc_html( Piwik::translate( 'SitesManager_GlobalListExcludedIps' ) ); ?></label>:
76
+ </th>
77
+ <td width="30%">
78
+ <?php echo sprintf( '<textarea cols="40" rows="4" id="%1$s" name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">%2$s</textarea>', 'excluded_ips', esc_html( $excluded_ips ) ); ?>
79
+ </td>
80
+ <td width="50%">
81
+ <?php
82
+ echo esc_html(
83
+ Piwik::translate(
84
+ 'SitesManager_HelpExcludedIpAddresses',
85
+ [
86
+ '1.2.3.4/24',
87
+ '1.2.3.*',
88
+ '1.2.*.*',
89
+ ]
90
+ )
91
  )
92
+ ?>
93
+ <br/>
94
+ <?php echo esc_html( Piwik::translate( 'SitesManager_YourCurrentIpAddressIs', esc_html( $current_ip ) ) ); ?>
95
+ </td>
96
+ </tr>
97
+ <tr>
98
+ <th scope="row">
99
+ <label><?php echo esc_html( Piwik::translate( 'SitesManager_GlobalListExcludedQueryParameters' ) ); ?></label>:
100
+ </th>
101
+ <td>
102
+ <?php echo sprintf( '<textarea cols="40" rows="4" id="%1$s" name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">%2$s</textarea>', 'excluded_query_parameters', esc_html( $excluded_query_params ) ); ?>
103
+ </td>
104
+ <td>
105
+ <?php echo esc_html( Piwik::translate( 'SitesManager_ListOfQueryParametersToExclude', '/^sess.*|.*[dD]ate$/' ) ); ?>
106
+ <?php echo esc_html( Piwik::translate( 'SitesManager_PiwikWillAutomaticallyExcludeCommonSessionParameters', 'phpsessid, sessionid, ...' ) ); ?>
107
+ </td>
108
+ </tr>
109
+ <tr>
110
+ <th scope="row">
111
+ <label><?php echo esc_html( Piwik::translate( 'SitesManager_GlobalListExcludedUserAgents' ) ); ?></label>:
112
+ </th>
113
+ <td>
114
+ <?php echo sprintf( '<textarea cols="40" rows="4" id="%1$s" name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">%2$s</textarea>', 'excluded_user_agents', esc_html( $excluded_user_agents ) ); ?>
115
+ </td>
116
+ <td>
117
 
118
+ <?php echo esc_html( Piwik::translate( 'SitesManager_GlobalExcludedUserAgentHelp1' ) ); ?>
119
+ <br/>
120
+ <?php echo esc_html( Piwik::translate( 'SitesManager_GlobalListExcludedUserAgents_Desc' ) ); ?>
121
+ <?php echo esc_html( Piwik::translate( 'SitesManager_GlobalExcludedUserAgentHelp2' ) ); ?>
122
 
123
+ </td>
124
+ </tr>
125
+ <tr>
126
+ <th scope="row">
127
+ <label><?php echo esc_html( Piwik::translate( 'SitesManager_KeepURLFragmentsLong' ) ); ?></label>:
128
+ </th>
129
+ <td>
130
+ <?php
131
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
132
+ echo sprintf( '<input type="checkbox" value="1" %2$s name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">', 'keep_url_fragments', $keep_url_fragments ? ' checked="checked"' : '' );
133
+ ?>
134
+ </td>
135
+ <td>
136
 
137
+ <?php
138
+ echo esc_html(
139
+ Piwik::translate(
140
+ 'SitesManager_KeepURLFragmentsHelp',
141
+ [
142
+ '<em>#</em>',
143
+ '<em>example.org/index.html#first_section</em>',
144
+ '<em>example.org/index.html</em>',
145
+ ]
146
+ )
147
  )
148
+ ?>
149
+ <br/>
150
+ <?php echo esc_html( Piwik::translate( 'SitesManager_KeepURLFragmentsHelp2' ) ); ?>
 
151
 
152
+ </td>
153
+ </tr>
154
+ <tr>
155
+ <td colspan="3">
156
+ <p class="submit"><input name="Submit" type="submit" class="button-primary"
157
+ value="<?php echo esc_attr__( 'Save Changes', 'matomo' ); ?>"/></p>
158
+ </td>
159
+ </tr>
160
 
161
+ </tbody>
162
+ </table>
163
+ </form>
164
 
165
+ <?php } ?>
classes/WpMatomo/Admin/views/geolocation_settings.php CHANGED
@@ -7,9 +7,7 @@
7
  * @package matomo
8
  * Code Based on
9
  * @author Andr&eacute; Br&auml;kling
10
- * @package WP_Matomo
11
  * https://github.com/braekling/matomo
12
- *
13
  */
14
 
15
  use WpMatomo\Admin\GeolocationSettings;
@@ -21,11 +19,11 @@ if ( ! defined( 'ABSPATH' ) ) {
21
  /** @var bool $invalid_format */
22
  /** @var string $current_maxmind_license */
23
 
24
- if ($invalid_format) { ?>
25
- <div class="updated notice error">
26
- <p><?php esc_html_e( 'It looks like the MaxMind license key has a wrong format.', 'matomo' ); ?></p>
27
- </div>
28
- <?php
29
  }
30
  ?>
31
 
@@ -33,39 +31,43 @@ if ($invalid_format) { ?>
33
  <?php wp_nonce_field( GeolocationSettings::NONCE_NAME ); ?>
34
 
35
  <p>
36
- <?php esc_html_e( 'On this page you can configure how Matomo detects the locations of your visitors.', 'matomo' ); ?>
37
- </p>
38
- <p>
39
- <?php esc_html_e('To detect the location of a visitor, the IP address of a visitor is looked up in a so called geolocation database. This is automatically taken care of for you. However, the freely available database DB-IP we are using is sometimes less accurate than other freely available geolocation databases. This applies to the free and paid version of DB-IP. An alternative geolocation database is called MaxMind which has a free and a paid version as well. Because of GDPR we cannot configure this database automatically for you.', 'matomo'); ?>
40
- <br><br>
41
- <?php
42
- echo sprintf(
43
- __( 'To use MaxMind instead of the default DB-IP geolocation database %1$s get a MaxMind license key%2$s and then configure this key below.', 'matomo' ),
44
- '<a target="_blank" rel="noreferrer noopener" href="https://matomo.org/faq/how-to/how-do-i-get-a-license-key-for-the-maxmind-geolocation-database/">', '</a>'
45
- );
46
- ?>
47
- </p>
 
48
 
49
  <table class="matomo-tracking-form widefat">
50
  <tbody>
51
  <tr>
52
- <th scope="row" style="vertical-align: top;">
53
- <label for="<?php echo esc_attr( GeolocationSettings::FORM_NAME ) ?>"><?php esc_html_e( 'MaxMind License Key', 'matomo' ); ?></label>:
54
  </th>
55
  <td>
56
  <input size="20" type="text" maxlength="20"
57
- id="<?php echo esc_attr( GeolocationSettings::FORM_NAME ) ?>"
58
- name="<?php echo esc_attr( GeolocationSettings::FORM_NAME ) ?>" value="<?php echo esc_attr($current_maxmind_license) ?>">
 
 
 
 
 
 
 
 
 
 
 
 
59
  </td>
60
- <td>
61
- <?php if (!empty($current_maxmind_license)) {?>
62
- <p style="color: green;"><span class="dashicons dashicons-yes" ></span> <?php esc_html_e('MaxMind is configured.', 'matomo') ?></p>
63
- <?php }?>
64
- <p>
65
- <?php esc_html_e('Leave the field empty and click on "Save Changes" to configure the default DB-IP database.', 'matomo') ?>
66
- <?php esc_html_e('When configured, your WordPress will send an HTTP request to a MaxMind server to download an approx. 60MB database and store it in your "wp-content/uploads/matomo" directory.', 'matomo') ?>
67
- </p>
68
- </td>
69
  </tr>
70
  <tr>
71
  <td colspan="3">
7
  * @package matomo
8
  * Code Based on
9
  * @author Andr&eacute; Br&auml;kling
 
10
  * https://github.com/braekling/matomo
 
11
  */
12
 
13
  use WpMatomo\Admin\GeolocationSettings;
19
  /** @var bool $invalid_format */
20
  /** @var string $current_maxmind_license */
21
 
22
+ if ( $invalid_format ) { ?>
23
+ <div class="updated notice error">
24
+ <p><?php esc_html_e( 'It looks like the MaxMind license key has a wrong format.', 'matomo' ); ?></p>
25
+ </div>
26
+ <?php
27
  }
28
  ?>
29
 
31
  <?php wp_nonce_field( GeolocationSettings::NONCE_NAME ); ?>
32
 
33
  <p>
34
+ <?php esc_html_e( 'On this page you can configure how Matomo detects the locations of your visitors.', 'matomo' ); ?>
35
+ </p>
36
+ <p>
37
+ <?php esc_html_e( 'To detect the location of a visitor, the IP address of a visitor is looked up in a so called geolocation database. This is automatically taken care of for you. However, the freely available database DB-IP we are using is sometimes less accurate than other freely available geolocation databases. This applies to the free and paid version of DB-IP. An alternative geolocation database is called MaxMind which has a free and a paid version as well. Because of GDPR we cannot configure this database automatically for you.', 'matomo' ); ?>
38
+ <br><br>
39
+ <?php
40
+ echo sprintf(
41
+ esc_html__( 'To use MaxMind instead of the default DB-IP geolocation database %1$s get a MaxMind license key%2$s and then configure this key below.', 'matomo' ),
42
+ '<a target="_blank" rel="noreferrer noopener" href="https://matomo.org/faq/how-to/how-do-i-get-a-license-key-for-the-maxmind-geolocation-database/">',
43
+ '</a>'
44
+ );
45
+ ?>
46
+ </p>
47
 
48
  <table class="matomo-tracking-form widefat">
49
  <tbody>
50
  <tr>
51
+ <th scope="row" style="vertical-align: top;">
52
+ <label for="<?php echo esc_attr( GeolocationSettings::FORM_NAME ); ?>"><?php esc_html_e( 'MaxMind License Key', 'matomo' ); ?></label>:
53
  </th>
54
  <td>
55
  <input size="20" type="text" maxlength="20"
56
+ id="<?php echo esc_attr( GeolocationSettings::FORM_NAME ); ?>"
57
+ name="<?php echo esc_attr( GeolocationSettings::FORM_NAME ); ?>"
58
+ value="<?php echo esc_attr( $current_maxmind_license ); ?>">
59
+ </td>
60
+ <td>
61
+ <?php if ( ! empty( $current_maxmind_license ) ) { ?>
62
+ <p style="color: green;"><span
63
+ class="dashicons dashicons-yes"></span> <?php esc_html_e( 'MaxMind is configured.', 'matomo' ); ?>
64
+ </p>
65
+ <?php } ?>
66
+ <p>
67
+ <?php esc_html_e( 'Leave the field empty and click on "Save Changes" to configure the default DB-IP database.', 'matomo' ); ?>
68
+ <?php esc_html_e( 'When configured, your WordPress will send an HTTP request to a MaxMind server to download an approx. 60MB database and store it in your "wp-content/uploads/matomo" directory.', 'matomo' ); ?>
69
+ </p>
70
  </td>
 
 
 
 
 
 
 
 
 
71
  </tr>
72
  <tr>
73
  <td colspan="3">
classes/WpMatomo/Admin/views/get_started.php CHANGED
@@ -6,17 +6,21 @@
6
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7
  * @package matomo
8
  */
9
-
 
 
 
10
  use WpMatomo\Admin\AdminSettings;
 
11
  use WpMatomo\Admin\Menu;
12
  use WpMatomo\Admin\TrackingSettings;
13
- use WpMatomo\Admin\GetStarted;
14
 
15
  if ( ! defined( 'ABSPATH' ) ) {
16
  exit;
17
  }
18
 
19
- /** @var \WpMatomo\Settings $settings */
20
  /** @var bool $can_user_edit */
21
  /** @var bool $was_updated */
22
  /** @var bool $show_this_page */
@@ -38,15 +42,18 @@ if ( empty( $show_this_page ) ) {
38
  ?>
39
 
40
  <?php if ( $settings->is_tracking_enabled() ) { ?>
41
- <h2>1. <?php esc_html_e( 'Tracking is enabled', 'matomo' ); ?> <span class="dashicons dashicons-yes" style="color: green;"></span></h2>
42
- <p><?php esc_html_e('Tracking should be working now and you don\'t have to do anything else to set up tracking.') ?> <a href="<?php echo AdminSettings::make_url( AdminSettings::TAB_TRACKING ); ?>"><?php esc_html_e( 'Click here to optionally configure the tracking code to your liking (not required).', 'matomo' ); ?></a></p>
 
 
 
43
 
44
  <?php } else { ?>
45
  <h2>1. <?php esc_html_e( 'Enable tracking', 'matomo' ); ?></h2>
46
 
47
  <form
48
- method="post"><?php esc_html_e( 'Tracking is currently disabled', 'matomo' ); ?> <?php wp_nonce_field( GetStarted::NONCE_NAME ); ?>
49
- <input type="hidden" name="<?php echo GetStarted::FORM_NAME; ?>[track_mode]"
50
  value="<?php echo esc_attr( TrackingSettings::TRACK_MODE_DEFAULT ); ?>">
51
  <input type="submit" class="button-primary" value="<?php esc_html_e( 'Enable tracking now', 'matomo' ); ?>">
52
  </form>
@@ -54,17 +61,18 @@ if ( empty( $show_this_page ) ) {
54
 
55
  <h2>2. <?php esc_html_e( 'Update your privacy page', 'matomo' ); ?></h2>
56
 
57
- <?php echo sprintf( esc_html__( 'Give your users the chance to opt-out of tracking by adding the shortcode %1$s to your privacy page. You can %2$stweak the opt-out to your liking - see the Privacy Settings%3$s.', 'matomo' ), '<code>[matomo_opt_out]</code>', '<a href="' . AdminSettings::make_url( AdminSettings::TAB_PRIVACY ) . '">', '</a>' ); ?>
58
 
59
  <?php esc_html_e( 'You may also need to mention that you are using Matomo Analytics on your website.', 'matomo' ); ?>
60
- <?php echo sprintf(esc_html__( 'By %1$sdisabling cookies in the tracking settings%2$s, you might not need to ask for any cookie or tracking consent if the GDPR or ePrivacy applies to you %3$s(learn more)%4$s.', 'matomo' ), '<a href="'.AdminSettings::make_url( AdminSettings::TAB_TRACKING ).'" target="_blank" rel="noreferrer noopener">', '</a>', '<a href="https://matomo.org/faq/new-to-piwik/how-do-i-use-matomo-analytics-without-consent-or-cookie-banner/" target="_blank" rel="noreferrer noopener">', '</a>'); ?>
61
 
62
  <h2>3. <?php esc_html_e( 'Done', 'matomo' ); ?></h2>
63
  <form method="post">
64
  <?php wp_nonce_field( GetStarted::NONCE_NAME ); ?>
65
  <input type="hidden" name="<?php echo esc_attr( GetStarted::FORM_NAME ); ?>[show_get_started_page]"
66
  value="no">
67
- <input type="submit" class="button-primary" value="<?php esc_html_e( 'Don\'t show this page anymore', 'matomo' ); ?>">
 
68
  </form>
69
  <p>
70
  <br/>
@@ -73,5 +81,6 @@ if ( empty( $show_this_page ) ) {
73
  <?php require 'info_shared.php'; ?>
74
  <?php
75
  $show_troubleshooting_link = false;
76
- require 'info_help.php'; ?>
 
77
  </div>
6
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7
  * @package matomo
8
  */
9
+ /**
10
+ * phpcs considers all of our variables as global and want them prefixed with matomo
11
+ * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
12
+ */
13
  use WpMatomo\Admin\AdminSettings;
14
+ use WpMatomo\Admin\GetStarted;
15
  use WpMatomo\Admin\Menu;
16
  use WpMatomo\Admin\TrackingSettings;
17
+ use WpMatomo\Settings;
18
 
19
  if ( ! defined( 'ABSPATH' ) ) {
20
  exit;
21
  }
22
 
23
+ /** @var Settings $settings */
24
  /** @var bool $can_user_edit */
25
  /** @var bool $was_updated */
26
  /** @var bool $show_this_page */
42
  ?>
43
 
44
  <?php if ( $settings->is_tracking_enabled() ) { ?>
45
+ <h2>1. <?php esc_html_e( 'Tracking is enabled', 'matomo' ); ?> <span class="dashicons dashicons-yes"
46
+ style="color: green;"></span></h2>
47
+ <p><?php esc_html_e( 'Tracking should be working now and you don\'t have to do anything else to set up tracking.', 'matomo' ); ?>
48
+ <a href="<?php echo esc_url( AdminSettings::make_url( AdminSettings::TAB_TRACKING ) ); ?>"><?php esc_html_e( 'Click here to optionally configure the tracking code to your liking (not required).', 'matomo' ); ?></a>
49
+ </p>
50
 
51
  <?php } else { ?>
52
  <h2>1. <?php esc_html_e( 'Enable tracking', 'matomo' ); ?></h2>
53
 
54
  <form
55
+ method="post"><?php esc_html_e( 'Tracking is currently disabled', 'matomo' ); ?> <?php wp_nonce_field( GetStarted::NONCE_NAME ); ?>
56
+ <input type="hidden" name="<?php echo esc_attr( GetStarted::FORM_NAME ); ?>[track_mode]"
57
  value="<?php echo esc_attr( TrackingSettings::TRACK_MODE_DEFAULT ); ?>">
58
  <input type="submit" class="button-primary" value="<?php esc_html_e( 'Enable tracking now', 'matomo' ); ?>">
59
  </form>
61
 
62
  <h2>2. <?php esc_html_e( 'Update your privacy page', 'matomo' ); ?></h2>
63
 
64
+ <?php echo sprintf( esc_html__( 'Give your users the chance to opt-out of tracking by either adding the shortcode %1$s or by adding the "Matomo opt out" block to your privacy page. You can %2$stweak the opt-out to your liking - see the Privacy Settings%3$s.', 'matomo' ), '<code>[matomo_opt_out]</code>', '<a href="' . esc_url( AdminSettings::make_url( AdminSettings::TAB_PRIVACY ) ) . '">', '</a>' ); ?>
65
 
66
  <?php esc_html_e( 'You may also need to mention that you are using Matomo Analytics on your website.', 'matomo' ); ?>
67
+ <?php echo sprintf( esc_html__( 'By %1$sdisabling cookies in the tracking settings%2$s, you might not need to ask for any cookie or tracking consent if the GDPR or ePrivacy applies to you %3$s(learn more)%4$s.', 'matomo' ), '<a href="' . esc_url( AdminSettings::make_url( AdminSettings::TAB_TRACKING ) ) . '" target="_blank" rel="noreferrer noopener">', '</a>', '<a href="https://matomo.org/faq/new-to-piwik/how-do-i-use-matomo-analytics-without-consent-or-cookie-banner/" target="_blank" rel="noreferrer noopener">', '</a>' ); ?>
68
 
69
  <h2>3. <?php esc_html_e( 'Done', 'matomo' ); ?></h2>
70
  <form method="post">
71
  <?php wp_nonce_field( GetStarted::NONCE_NAME ); ?>
72
  <input type="hidden" name="<?php echo esc_attr( GetStarted::FORM_NAME ); ?>[show_get_started_page]"
73
  value="no">
74
+ <input type="submit" class="button-primary"
75
+ value="<?php esc_html_e( 'Don\'t show this page anymore', 'matomo' ); ?>">
76
  </form>
77
  <p>
78
  <br/>
81
  <?php require 'info_shared.php'; ?>
82
  <?php
83
  $show_troubleshooting_link = false;
84
+ require 'info_help.php';
85
+ ?>
86
  </div>
classes/WpMatomo/Admin/views/info.php CHANGED
@@ -6,8 +6,11 @@
6
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7
  * @package matomo
8
  */
9
-
10
- use \WpMatomo\Admin\Menu;
 
 
 
11
 
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit;
@@ -26,36 +29,36 @@ if ( ! defined( 'ABSPATH' ) ) {
26
 
27
  <h2><?php esc_html_e( 'Support the project', 'matomo' ); ?></h2>
28
  <p>
29
- <?php
30
- echo sprintf(
31
- esc_html__(
32
- 'Matomo is a collaborative project brought to you by %1$sMatomo team%2$s members as well as many other contributors around the globe. If you like Matomo,
33
  %3$splease give us a review%4$s and spread the word about us.',
34
- 'matomo'
35
- ),
36
- '<a target="_blank" rel="noreferrer noopener" href="https://matomo.org/team/">',
37
- '</a>',
38
- '<a target="_blank" rel="noreferrer noopener" href="https://wordpress.org/support/plugin/matomo/reviews/?rate=5#new-post">',
39
- '<span class="dashicons-before dashicons-star-filled" style="color:gold;"></span><span class="dashicons-before dashicons-star-filled" style="color:gold;"></span><span class="dashicons-before dashicons-star-filled" style="color:gold;"></span><span class="dashicons-before dashicons-star-filled" style="color:gold;"></span><span class="dashicons-before dashicons-star-filled" style="color:gold;"></span></a>'
40
- );
41
- ?>
42
  <br/><br/>
43
- Matomo will always cost you nothing to use, but that doesn't mean it costs us nothing to make.
44
- Matomo needs your continued support to grow and thrive.
45
- <?php
46
  echo sprintf(
47
  esc_html__(
48
  'You can also help us by %1$sdonating%2$s or by %3$spurchasing premium plugins%4$s which fund the
49
  development of the free Matomo Analytics version.',
50
  'matomo'
51
  ),
52
- '<a href="' . Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_ADMIN ) . '">',
53
  '</a>',
54
  '<a href="https://plugins.matomo.org/premium" target="_blank" rel="noreferrer noopener">',
55
  '</a>'
56
  );
57
  ?>
58
- Every penny will help.
59
  </p>
60
 
61
  <?php require 'info_newsletter.php'; ?>
@@ -69,18 +72,18 @@ if ( ! defined( 'ABSPATH' ) ) {
69
  <ul>
70
  <li>
71
  <a target="_blank" rel="noreferrer noopener" href="https://matomo.org/newsletter/"><span
72
- class="dashicons-before dashicons-email"></span></a>
73
  <a target="_blank" rel="noreferrer noopener"
74
  href="https://matomo.org/newsletter/"><?php esc_html_e( 'Newsletter', 'matomo' ); ?></a>
75
  </li>
76
  <li>
77
  <a target="_blank" rel="noreferrer noopener" href="https://www.facebook.com/Matomo.org"><span
78
- class="dashicons-before dashicons-facebook"></span></a>
79
  <a target="_blank" rel="noreferrer noopener" href="https://www.facebook.com/Matomo.org">Facebook</a>
80
  </li>
81
  <li>
82
  <a target="_blank" rel="noreferrer noopener" href="https://twitter.com/matomo_org"><span
83
- class="dashicons-before dashicons-twitter"></span></a>
84
  <a target="_blank" rel="noreferrer noopener" href="https://twitter.com/matomo_org">Twitter</a>
85
  </li>
86
  <li>
6
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7
  * @package matomo
8
  */
9
+ /**
10
+ * phpcs considers all of our variables as global and want them prefixed with matomo
11
+ * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
12
+ */
13
+ use WpMatomo\Admin\Menu;
14
 
15
  if ( ! defined( 'ABSPATH' ) ) {
16
  exit;
29
 
30
  <h2><?php esc_html_e( 'Support the project', 'matomo' ); ?></h2>
31
  <p>
32
+ <?php
33
+ echo sprintf(
34
+ esc_html__(
35
+ 'Matomo is a collaborative project brought to you by %1$sMatomo team%2$s members as well as many other contributors around the globe. If you like Matomo,
36
  %3$splease give us a review%4$s and spread the word about us.',
37
+ 'matomo'
38
+ ),
39
+ '<a target="_blank" rel="noreferrer noopener" href="https://matomo.org/team/">',
40
+ '</a>',
41
+ '<a target="_blank" rel="noreferrer noopener" href="https://wordpress.org/support/plugin/matomo/reviews/?rate=5#new-post">',
42
+ '<span class="dashicons-before dashicons-star-filled" style="color:gold;"></span><span class="dashicons-before dashicons-star-filled" style="color:gold;"></span><span class="dashicons-before dashicons-star-filled" style="color:gold;"></span><span class="dashicons-before dashicons-star-filled" style="color:gold;"></span><span class="dashicons-before dashicons-star-filled" style="color:gold;"></span></a>'
43
+ );
44
+ ?>
45
  <br/><br/>
46
+ Matomo will always cost you nothing to use, but that doesn't mean it costs us nothing to make.
47
+ Matomo needs your continued support to grow and thrive.
48
+ <?php
49
  echo sprintf(
50
  esc_html__(
51
  'You can also help us by %1$sdonating%2$s or by %3$spurchasing premium plugins%4$s which fund the
52
  development of the free Matomo Analytics version.',
53
  'matomo'
54
  ),
55
+ '<a href="' . esc_url( Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_ADMIN ) ) . '">',
56
  '</a>',
57
  '<a href="https://plugins.matomo.org/premium" target="_blank" rel="noreferrer noopener">',
58
  '</a>'
59
  );
60
  ?>
61
+ Every penny will help.
62
  </p>
63
 
64
  <?php require 'info_newsletter.php'; ?>
72
  <ul>
73
  <li>
74
  <a target="_blank" rel="noreferrer noopener" href="https://matomo.org/newsletter/"><span
75
+ class="dashicons-before dashicons-email"></span></a>
76
  <a target="_blank" rel="noreferrer noopener"
77
  href="https://matomo.org/newsletter/"><?php esc_html_e( 'Newsletter', 'matomo' ); ?></a>
78
  </li>
79
  <li>
80
  <a target="_blank" rel="noreferrer noopener" href="https://www.facebook.com/Matomo.org"><span
81
+ class="dashicons-before dashicons-facebook"></span></a>
82
  <a target="_blank" rel="noreferrer noopener" href="https://www.facebook.com/Matomo.org">Facebook</a>
83
  </li>
84
  <li>
85
  <a target="_blank" rel="noreferrer noopener" href="https://twitter.com/matomo_org"><span
86
+ class="dashicons-before dashicons-twitter"></span></a>
87
  <a target="_blank" rel="noreferrer noopener" href="https://twitter.com/matomo_org">Twitter</a>
88
  </li>
89
  <li>
classes/WpMatomo/Admin/views/info_bug_report.php CHANGED
@@ -13,17 +13,17 @@ if ( ! defined( 'ABSPATH' ) ) {
13
  ?>
14
  <h2><?php esc_html_e( 'Do you have a bug to report or a feature request?', 'matomo' ); ?></h2>
15
  <p>
16
- <?php
17
- echo sprintf(
18
- esc_html__( 'Please read the recommendations on writing a good %1$sbug report%2$s or %3$sfeature request%4$s. Then register or login to %5$sour issue tracker%6$s and create a %7$snew issue%8$s.', 'matomo' ),
19
- '<a target="_blank" rel="noreferrer noopener" href="https://developer.matomo.org/guides/core-team-workflow#submitting-a-bug-report">',
20
- '</a>',
21
- '<a target="_blank" rel="noreferrer noopener" href="https://developer.matomo.org/guides/core-team-workflow#submitting-a-feature-request">',
22
- '</a>',
23
- '<a target="_blank" rel="noreferrer noopener" href="https://github.com/matomo-org/wp-matomo/issues">',
24
- '</a>',
25
- '<a target="_blank" rel="noreferrer noopener" href="https://github.com/matomo-org/wp-matomo/issues/new">',
26
- '</a>'
27
- );
28
- ?>
29
- </p>
13
  ?>
14
  <h2><?php esc_html_e( 'Do you have a bug to report or a feature request?', 'matomo' ); ?></h2>
15
  <p>
16
+ <?php
17
+ echo sprintf(
18
+ esc_html__( 'Please read the recommendations on writing a good %1$sbug report%2$s or %3$sfeature request%4$s. Then register or login to %5$sour issue tracker%6$s and create a %7$snew issue%8$s.', 'matomo' ),
19
+ '<a target="_blank" rel="noreferrer noopener" href="https://developer.matomo.org/guides/core-team-workflow#submitting-a-bug-report">',
20
+ '</a>',
21
+ '<a target="_blank" rel="noreferrer noopener" href="https://developer.matomo.org/guides/core-team-workflow#submitting-a-feature-request">',
22
+ '</a>',
23
+ '<a target="_blank" rel="noreferrer noopener" href="https://github.com/matomo-org/wp-matomo/issues">',
24
+ '</a>',
25
+ '<a target="_blank" rel="noreferrer noopener" href="https://github.com/matomo-org/wp-matomo/issues/new">',
26
+ '</a>'
27
+ );
28
+ ?>
29
+ </p>
classes/WpMatomo/Admin/views/info_help.php CHANGED
@@ -7,6 +7,8 @@
7
  * @package matomo
8
  */
9
 
 
 
10
  if ( ! defined( 'ABSPATH' ) ) {
11
  exit; // if accessed directly
12
  }
@@ -22,10 +24,12 @@ if ( ! defined( 'ABSPATH' ) ) {
22
  href="https://matomo.org/docs/"><?php esc_html_e( 'User guides', 'matomo' ); ?></a>
23
  - <?php esc_html_e( 'Learn how to configure Matomo and how to effectively analyse your data', 'matomo' ); ?>
24
  </li>
25
- <li><a target="_blank" rel="noreferrer noopener" href="https://matomo.org/faq/wordpress/"><?php esc_html_e( 'Matomo for WordPress FAQs', 'matomo' ); ?></a>
 
26
  - <?php esc_html_e( 'Get answers to frequently asked questions', 'matomo' ); ?>
27
  </li>
28
- <li><a target="_blank" rel="noreferrer noopener" href="https://matomo.org/faq/"><?php esc_html_e( 'General FAQs', 'matomo' ); ?></a>
 
29
  - <?php esc_html_e( 'Get answers to frequently asked questions', 'matomo' ); ?>
30
  </li>
31
  <li><a target="_blank" rel="noreferrer noopener"
@@ -41,9 +45,9 @@ if ( ! defined( 'ABSPATH' ) ) {
41
  - <?php esc_html_e( 'Let our experienced team assist you online on how to best utilise Matomo', 'matomo' ); ?>
42
  </li>
43
  <?php if ( ! empty( $show_troubleshooting_link ) ) { ?>
44
- <li><a
45
- href="<?php echo esc_url( add_query_arg( array( 'tab' => 'troubleshooting' ), menu_page_url( \WpMatomo\Admin\Menu::SLUG_SYSTEM_REPORT, false ) ) ); ?>"><?php esc_html_e( 'Troubleshooting', 'matomo' ); ?></a>
46
- - <?php esc_html_e( 'Click here if you are having Trouble with Matomo', 'matomo' ); ?>
47
- </li>
48
  <?php } ?>
49
  </ul>
7
  * @package matomo
8
  */
9
 
10
+ use WpMatomo\Admin\Menu;
11
+
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit; // if accessed directly
14
  }
24
  href="https://matomo.org/docs/"><?php esc_html_e( 'User guides', 'matomo' ); ?></a>
25
  - <?php esc_html_e( 'Learn how to configure Matomo and how to effectively analyse your data', 'matomo' ); ?>
26
  </li>
27
+ <li><a target="_blank" rel="noreferrer noopener"
28
+ href="https://matomo.org/faq/wordpress/"><?php esc_html_e( 'Matomo for WordPress FAQs', 'matomo' ); ?></a>
29
  - <?php esc_html_e( 'Get answers to frequently asked questions', 'matomo' ); ?>
30
  </li>
31
+ <li><a target="_blank" rel="noreferrer noopener"
32
+ href="https://matomo.org/faq/"><?php esc_html_e( 'General FAQs', 'matomo' ); ?></a>
33
  - <?php esc_html_e( 'Get answers to frequently asked questions', 'matomo' ); ?>
34
  </li>
35
  <li><a target="_blank" rel="noreferrer noopener"
45
  - <?php esc_html_e( 'Let our experienced team assist you online on how to best utilise Matomo', 'matomo' ); ?>
46
  </li>
47
  <?php if ( ! empty( $show_troubleshooting_link ) ) { ?>
48
+ <li><a
49
+ href="<?php echo esc_url( add_query_arg( [ 'tab' => 'troubleshooting' ], menu_page_url( Menu::SLUG_SYSTEM_REPORT, false ) ) ); ?>"><?php esc_html_e( 'Troubleshooting', 'matomo' ); ?></a>
50
+ - <?php esc_html_e( 'Click here if you are having Trouble with Matomo', 'matomo' ); ?>
51
+ </li>
52
  <?php } ?>
53
  </ul>
classes/WpMatomo/Admin/views/info_high_traffic.php CHANGED
@@ -12,17 +12,17 @@ if ( ! defined( 'ABSPATH' ) ) {
12
  }
13
  ?>
14
  <p>If your website gets a lot of traffic we recommend installing <a href="https://matomo.org/matomo-on-premise/"
15
- target="_blank" rel="noreferrer noopener">Matomo
16
  On-Premise</a>
17
  separately (it's free as well) in combination with the <a href="https://wordpress.org/plugins/wp-piwik/"
18
- target="_blank"
19
- rel="noreferrer noopener">WP-Matomo</a> WordPress
20
  plugin.
21
  It's free to install and has the same requirements as WordPress.
22
  Your Matomo will then run a lot faster and it allows you to put your Matomo installation on a separate server if
23
  needed.
24
  <br/><br/>Don't want all the hassle of maintaining a Matomo? <a href="http://matomo.org/start-free-analytics-trial/"
25
- rel="noreferrer noopener" target="_blank">Sign up
26
  for a free Matomo Cloud trial</a>. We can migrate all your data onto our Cloud for free. 100% data ownership
27
  guaranteed.
28
  </p>
12
  }
13
  ?>
14
  <p>If your website gets a lot of traffic we recommend installing <a href="https://matomo.org/matomo-on-premise/"
15
+ target="_blank" rel="noreferrer noopener">Matomo
16
  On-Premise</a>
17
  separately (it's free as well) in combination with the <a href="https://wordpress.org/plugins/wp-piwik/"
18
+ target="_blank"
19
+ rel="noreferrer noopener">WP-Matomo</a> WordPress
20
  plugin.
21
  It's free to install and has the same requirements as WordPress.
22
  Your Matomo will then run a lot faster and it allows you to put your Matomo installation on a separate server if
23
  needed.
24
  <br/><br/>Don't want all the hassle of maintaining a Matomo? <a href="http://matomo.org/start-free-analytics-trial/"
25
+ rel="noreferrer noopener" target="_blank">Sign up
26
  for a free Matomo Cloud trial</a>. We can migrate all your data onto our Cloud for free. 100% data ownership
27
  guaranteed.
28
  </p>
classes/WpMatomo/Admin/views/info_multisite.php CHANGED
@@ -6,11 +6,14 @@
6
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7
  * @package matomo
8
  */
 
 
 
9
  if ( ! defined( 'ABSPATH' ) ) {
10
  exit;
11
  }
12
 
13
- /** @var \WpMatomo\Settings $settings */
14
  ?>
15
 
16
  <div class="wrap">
@@ -29,25 +32,25 @@ if ( ! defined( 'ABSPATH' ) ) {
29
  </p>
30
  <h2><?php esc_html_e( 'Managing many sites?', 'matomo' ); ?></h2>
31
  <p>
32
- <?php
33
- echo sprintf(
34
- __(
35
- 'If you are managing quite a few sites or have quite a bit of traffic then we recommend installing %1$sMatomo On-Premise%2$s separately outside WordPress (it\'s free as well) and use it in combination with the %3$sWP-Matomo%4$s WordPress plugin.
36
  Your Matomo will then run a lot faster, you can put Matomo on a separate server if needed, and it allows you to make use of additional features such as %5$sRoll-Up Reporting%6$s.',
37
- 'matomo'
38
- ),
39
- '<a href="https://matomo.org/matomo-on-premise/" target="_blank" rel="noreferrer noopener">',
40
- '</a>',
41
- '<a href="https://wordpress.org/plugins/wp-piwik/" target="_blank" rel="noreferrer noopener">',
42
- '</a>',
43
- '<a href="https://plugins.matomo.org/RollUpReporting" target="_blank" rel="noreferrer noopener">',
44
- '</a>'
45
- );
46
- ?>
47
 
48
  <br/><br/><?php esc_html_e( 'Don\'t want all the hassle of maintaining a Matomo?', 'matomo' ); ?> <a
49
- href="http://matomo.org/start-free-analytics-trial/" rel="noreferrer noopener"
50
- target="_blank"><?php esc_html_e( 'Sign up for a free Matomo Cloud trial', 'matomo' ); ?></a>. <?php esc_html_e( 'We can migrate all your data onto our Cloud for free. 100% data ownership guaranteed.', 'matomo' ); ?>
51
  </p>
52
 
53
  <h2><?php esc_html_e( 'Matomo sites', 'matomo' ); ?></h2>
@@ -57,8 +60,8 @@ if ( ! defined( 'ABSPATH' ) ) {
57
  foreach ( get_sites() as $matomo_site ) {
58
  /** @var WP_Site $matomo_site */
59
  switch_to_blog( $matomo_site->blog_id );
60
- if (function_exists('is_plugin_active') && is_plugin_active('matomo/matomo.php')) {
61
- echo '<li><a href="' . esc_url(admin_url( 'admin.php?page=matomo-reporting' )) . '">' . esc_html($matomo_site->blogname) . ' (Site ID: ' . esc_html($matomo_site->blog_id) . ')</a></li>';
62
  }
63
  restore_current_blog();
64
  }
6
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7
  * @package matomo
8
  */
9
+
10
+ use WpMatomo\Settings;
11
+
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit;
14
  }
15
 
16
+ /** @var Settings $settings */
17
  ?>
18
 
19
  <div class="wrap">
32
  </p>
33
  <h2><?php esc_html_e( 'Managing many sites?', 'matomo' ); ?></h2>
34
  <p>
35
+ <?php
36
+ echo sprintf(
37
+ esc_html__(
38
+ 'If you are managing quite a few sites or have quite a bit of traffic then we recommend installing %1$sMatomo On-Premise%2$s separately outside WordPress (it\'s free as well) and use it in combination with the %3$sWP-Matomo%4$s WordPress plugin.
39
  Your Matomo will then run a lot faster, you can put Matomo on a separate server if needed, and it allows you to make use of additional features such as %5$sRoll-Up Reporting%6$s.',
40
+ 'matomo'
41
+ ),
42
+ '<a href="https://matomo.org/matomo-on-premise/" target="_blank" rel="noreferrer noopener">',
43
+ '</a>',
44
+ '<a href="https://wordpress.org/plugins/wp-piwik/" target="_blank" rel="noreferrer noopener">',
45
+ '</a>',
46
+ '<a href="https://plugins.matomo.org/RollUpReporting" target="_blank" rel="noreferrer noopener">',
47
+ '</a>'
48
+ );
49
+ ?>
50
 
51
  <br/><br/><?php esc_html_e( 'Don\'t want all the hassle of maintaining a Matomo?', 'matomo' ); ?> <a
52
+ href="http://matomo.org/start-free-analytics-trial/" rel="noreferrer noopener"
53
+ target="_blank"><?php esc_html_e( 'Sign up for a free Matomo Cloud trial', 'matomo' ); ?></a>. <?php esc_html_e( 'We can migrate all your data onto our Cloud for free. 100% data ownership guaranteed.', 'matomo' ); ?>
54
  </p>
55
 
56
  <h2><?php esc_html_e( 'Matomo sites', 'matomo' ); ?></h2>
60
  foreach ( get_sites() as $matomo_site ) {
61
  /** @var WP_Site $matomo_site */
62
  switch_to_blog( $matomo_site->blog_id );
63
+ if ( function_exists( 'is_plugin_active' ) && is_plugin_active( 'matomo/matomo.php' ) ) {
64
+ echo '<li><a href="' . esc_url( admin_url( 'admin.php?page=matomo-reporting' ) ) . '">' . esc_html( $matomo_site->blogname ) . ' (Site ID: ' . esc_html( $matomo_site->blog_id ) . ')</a></li>';
65
  }
66
  restore_current_blog();
67
  }
classes/WpMatomo/Admin/views/info_newsletter.php CHANGED
@@ -7,7 +7,7 @@
7
  * @package matomo
8
  */
9
 
10
- use \WpMatomo\Admin\Info;
11
 
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit;
@@ -15,15 +15,15 @@ if ( ! defined( 'ABSPATH' ) ) {
15
  /** @var bool $signedup_newsletter */
16
  /** @var bool $show_newsletter */
17
 
18
- if ($signedup_newsletter) {
19
- ?>
20
- <div class="notice notice-success is-dismissible">
21
- <p><?php esc_html_e('Thank you for signing up to our newsletter.', 'matomo'); ?></p>
22
- </div>
23
- <?php
24
  return;
25
  }
26
- if (!$show_newsletter) {
27
  return;
28
  }
29
  ?>
@@ -33,14 +33,14 @@ if (!$show_newsletter) {
33
  <form method="post">
34
  <p>
35
  <?php wp_nonce_field( Info::NONCE_NAME ); ?>
36
- <input type="checkbox" id="<?php echo Info::FORM_NAME ?>" name="<?php echo Info::FORM_NAME ?>" value="1">
37
- <label for="<?php echo Info::FORM_NAME ?>">
38
- <?php esc_html_e('Subscribe to our newsletter to receive regular information about Matomo, web analytics, and privacy. You can unsubscribe from it any time.', 'matomo'); ?>
39
- <?php esc_html_e('This service uses MadMimi.', 'matomo'); ?>
40
- <?php echo sprintf(esc_html__('Learn more about it on our %1$sPrivacy Policy page%2$s.', 'matomo'), '<a href="https://matomo.org/privacy-policy/" target="_blank" rel="noreferrer noopener">', '</a>'); ?>
41
  </label>
42
  <br><br>
43
- <input type="submit" class="button-secondary" value="<?php esc_attr_e('Subscribe', 'matomo');?>">
44
  </p>
45
  </form>
46
- </div>
7
  * @package matomo
8
  */
9
 
10
+ use WpMatomo\Admin\Info;
11
 
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit;
15
  /** @var bool $signedup_newsletter */
16
  /** @var bool $show_newsletter */
17
 
18
+ if ( $signedup_newsletter ) {
19
+ ?>
20
+ <div class="notice notice-success is-dismissible">
21
+ <p><?php esc_html_e( 'Thank you for signing up to our newsletter.', 'matomo' ); ?></p>
22
+ </div>
23
+ <?php
24
  return;
25
  }
26
+ if ( ! $show_newsletter ) {
27
  return;
28
  }
29
  ?>
33
  <form method="post">
34
  <p>
35
  <?php wp_nonce_field( Info::NONCE_NAME ); ?>
36
+ <input type="checkbox" id="<?php echo esc_attr( Info::FORM_NAME ); ?>" name="<?php echo esc_attr( Info::FORM_NAME ); ?>" value="1">
37
+ <label for="<?php echo esc_attr( Info::FORM_NAME ); ?>">
38
+ <?php esc_html_e( 'Subscribe to our newsletter to receive regular information about Matomo, web analytics, and privacy. You can unsubscribe from it any time.', 'matomo' ); ?>
39
+ <?php esc_html_e( 'This service uses MadMimi.', 'matomo' ); ?>
40
+ <?php echo sprintf( esc_html__( 'Learn more about it on our %1$sPrivacy Policy page%2$s.', 'matomo' ), '<a href="https://matomo.org/privacy-policy/" target="_blank" rel="noreferrer noopener">', '</a>' ); ?>
41
  </label>
42
  <br><br>
43
+ <input type="submit" class="button-secondary" value="<?php esc_attr_e( 'Subscribe', 'matomo' ); ?>">
44
  </p>
45
  </form>
46
+ </div>
classes/WpMatomo/Admin/views/info_shared.php CHANGED
@@ -11,12 +11,12 @@ if ( ! defined( 'ABSPATH' ) ) {
11
  exit; // if accessed directly
12
  }
13
  ?>
14
- <h1><?php esc_html_e( 'About', 'matomo' ); ?> <?php matomo_header_icon(true); ?> </h1>
15
 
16
  <p>
17
  <?php
18
  echo sprintf(
19
- __(
20
  '%1$sMatomo Analytics%2$s is the most powerful
21
  analytics platform for WordPress, designed for your success. It is our mission to help you grow
22
  your business while giving you %3$sfull control over your data%4$s. All
11
  exit; // if accessed directly
12
  }
13
  ?>
14
+ <h1><?php esc_html_e( 'About', 'matomo' ); ?><?php matomo_header_icon( true ); ?> </h1>
15
 
16
  <p>
17
  <?php
18
  echo sprintf(
19
+ esc_html__(
20
  '%1$sMatomo Analytics%2$s is the most powerful
21
  analytics platform for WordPress, designed for your success. It is our mission to help you grow
22
  your business while giving you %3$sfull control over your data%4$s. All
classes/WpMatomo/Admin/views/marketplace.php CHANGED
@@ -13,11 +13,11 @@ if ( ! defined( 'ABSPATH' ) ) {
13
 
14
  /** @var \WpMatomo\Settings $settings */
15
  $matomo_extra_url_params = '&' . http_build_query(
16
- array(
17
  'php' => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION,
18
  'matomo' => $settings->get_global_option( 'core_version' ),
19
  'wp_version' => ! empty( $GLOBALS['wp_version'] ) ? $GLOBALS['wp_version'] : '',
20
- )
21
  );
22
  ?>
23
  <div class="wrap">
@@ -30,256 +30,287 @@ $matomo_extra_url_params = '&' . http_build_query(
30
 
31
  <div id="icon-plugins" class="icon32"></div>
32
 
33
- <h1><?php matomo_header_icon(); ?> <?php esc_html_e( 'Discover new functionality for your Matomo', 'matomo' ); ?></h1>
34
 
35
  <?php if ( ! is_plugin_active( MATOMO_MARKETPLACE_PLUGIN_NAME ) ) { ?>
36
- <div class="updated notice matomo-marketplace-notice">
37
- <p><?php echo sprintf( esc_html__( 'Easily install over 100 free plugins & %1$spremium features%2$s for Matomo with just a click' ), '<span style="white-space: nowrap;">', '</span>' ); ?>
38
- </p>
39
- <p><a href="https://builds.matomo.org/matomo-marketplace-for-wordpress-latest.zip" rel="noreferrer noopener" class="button"><?php esc_html_e( 'Download Matomo Marketplace for WordPress', 'matomo' ); ?></a>
 
40
 
41
- <a target="_blank" href="https://matomo.org/faq/wordpress/how-do-i-install-a-matomo-marketplace-plugin-in-matomo-for-wordpress/"><span class="dashicons-before dashicons-video-alt3"></span></a> <a target="_blank" href="https://matomo.org/faq/wordpress/how-do-i-install-a-matomo-marketplace-plugin-in-matomo-for-wordpress/"><?php esc_html_e( 'Install instructions', 'matomo' ); ?></a>
42
- </p>
43
- </div>
 
 
 
44
  <?php } ?>
45
 
46
  <?php
47
- function matomo_show_tables($matomo_feature_sections) {
48
-
49
- foreach ( $matomo_feature_sections as $matomo_feature_section ) {
50
- $matomo_feature_section['features'] = array_filter($matomo_feature_section['features']);
51
- $matomo_num_features_in_block = count( $matomo_feature_section['features'] );
52
 
53
- echo '<h2>' . esc_html( $matomo_feature_section['title'] ) . '</h2>';
54
- echo '<div class="wp-list-table widefat plugin-install matomo-plugin-list matomo-plugin-row-' . $matomo_num_features_in_block . '"><div id="the-list">';
55
 
56
- foreach ( $matomo_feature_section['features'] as $matomo_index => $matomo_feature ) {
57
- $matomo_style = '';
58
- $matomo_is_3_columns = $matomo_num_features_in_block === 3;
59
- if ( $matomo_is_3_columns ) {
60
- $matomo_style = 'width: calc(33% - 8px);min-width:282px;max-width:350px;';
61
- if ( $matomo_index % 3 === 2 ) {
62
- $matomo_style .= 'clear: inherit;margin-right: 0;margin-left: 16px;';
63
- }
64
- }
65
- ?>
66
- <div class="plugin-card" style="<?php echo $matomo_style; ?>">
67
- <?php
68
- if ( $matomo_is_3_columns && ! empty( $matomo_feature['image'] ) ) {
69
- ?>
70
- <a
71
- href="<?php echo esc_url( $matomo_feature['url'] ); ?>"
72
- rel="noreferrer noopener" target="_blank"
73
- class="thickbox open-plugin-details-modal"><img
74
- src="<?php echo esc_url( $matomo_feature['image'] ); ?>"
75
- style="height: 80px;width:100%;object-fit: cover;" alt=""></a><?php } ?>
 
 
 
76
 
77
- <div class="plugin-card-top">
78
- <div class="
79
  <?php
80
- if ( ! $matomo_is_3_columns ) {
81
- ?>
82
- name column-name<?php } ?>" style="margin-right: 0;<?php if ( empty( $matomo_feature['image'] )) { echo 'margin-left: 0;'; } ?>">
83
- <h3>
84
- <a href="<?php echo esc_url( !empty($matomo_feature['video']) ? $matomo_feature['video'] : $matomo_feature['url'] ); ?>"
85
- rel="noreferrer noopener" target="_blank"
86
- class="thickbox open-plugin-details-modal">
87
- <?php echo esc_html( $matomo_feature['name'] ); ?>
88
- </a>
89
- <?php
90
- if ( ! $matomo_is_3_columns && ! empty( $matomo_feature['image'] ) ) {
91
- ?>
92
- <a
93
- href="<?php echo esc_url( $matomo_feature['url'] ); ?>"
94
- rel="noreferrer noopener" target="_blank"
95
- class="thickbox open-plugin-details-modal"><img
96
- src="<?php echo esc_url( $matomo_feature['image'] ); ?>" class="plugin-icon"
97
- style="object-fit: cover;"
98
- alt=""></a><?php } ?>
99
- </h3>
100
- </div>
101
- <div class="
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  <?php
103
- if ( ! $matomo_is_3_columns ) {
104
- ?>
105
- desc column-description<?php } ?>"
106
- style="margin-right: 0;<?php if ( empty( $matomo_feature['image'] )) { echo 'margin-left: 0;'; } ?>">
107
- <p class="matomo-description"><?php echo esc_html( $matomo_feature['description'] ); ?>
108
- <?php if (!empty($matomo_feature['video'])) {
109
- echo ' <a target="_blank" rel="noreferrer noopener" style="white-space: nowrap;" href="'. esc_url($matomo_feature['video']).'"><span class="dashicons dashicons-video-alt3"></span> '. esc_html__( 'Learn more', 'matomo' ).'</a>';
110
- } elseif (!empty($matomo_feature['url'])) {
111
- echo ' <a target="_blank" rel="noreferrer noopener" style="white-space: nowrap;" href="'. esc_url($matomo_feature['url']).'">'. esc_html__( 'Learn more', 'matomo' ).'</a>';
112
- } ?></p>
113
- <?php if ( ! empty( $matomo_feature['price'] )) {?><p class="authors"><a class="button-primary"
114
- rel="noreferrer noopener" target="_blank"
115
- href="<?php echo esc_url( ! empty( $matomo_feature['download_url'] ) ? $matomo_feature['download_url'] : $matomo_feature['url'] ); ?>">
116
- <?php
117
- if ($matomo_feature['price'] === 'free' ) {
118
- esc_html_e('Download', 'matomo');
119
- } else {
120
- echo esc_html( $matomo_feature['price'] );
121
- } ?>
122
- </a>
123
- </p><?php } ?>
124
- </div>
125
- </div>
126
- </div>
127
- <?php
128
- }
129
- echo '';
130
- echo '</div><div style="clear: both"></div>';
131
- if (!empty($matomo_feature_section['more_url'])) {
132
- echo '<a target="_blank" rel="noreferrer noopener" href="'.esc_attr($matomo_feature_section['more_url']).'"><span class="dashicons dashicons-arrow-right-alt2"></span>'. esc_html($matomo_feature_section['more_text']).'</a>';
133
- }
134
- echo '</div>';
135
- }
136
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
- $matomo_feature_sections = array(
139
- array(
140
- 'title' => 'Top free plugins',
141
- 'more_url' => 'https://plugins.matomo.org/free?wp=1',
142
  'more_text' => 'Browse all free plugins',
143
- 'features' =>
144
- array(
145
- array(
146
  'name' => 'Marketing Campaigns Reporting',
147
  'description' => 'Measure the effectiveness of your marketing campaigns. Track up to five channels instead of two: campaign, source, medium, keyword, content.',
148
  'price' => 'free',
149
  'download_url' => 'https://plugins.matomo.org/api/2.0/plugins/MarketingCampaignsReporting/download/latest?wp=1' . $matomo_extra_url_params,
150
  'url' => 'https://plugins.matomo.org/MarketingCampaignsReporting?wp=1&pk_campaign=WP&pk_source=Plugin',
151
  'image' => '',
152
- ),
153
- array(
154
  'name' => 'Custom Alerts',
155
  'description' => 'Create custom Alerts to be notified of important changes on your website or app!',
156
  'price' => 'free',
157
  'download_url' => 'https://plugins.matomo.org/api/2.0/plugins/CustomAlerts/download/latest?wp=1' . $matomo_extra_url_params,
158
  'url' => 'https://plugins.matomo.org/CustomAlerts?wp=1&pk_campaign=WP&pk_source=Plugin',
159
  'image' => '',
160
- ),
161
- ),
162
- ),
163
- );
164
 
165
- matomo_show_tables($matomo_feature_sections);
166
 
167
- echo '<br>';
168
 
169
- $matomo_feature_sections = array(
170
- array(
171
- 'title' => 'Most popular premium features',
172
- 'features' =>
173
- array(
174
- array(
175
- 'name' => 'Heatmap & Session Recording',
176
- 'description' => 'Truly understand your visitors by seeing where they click, hover, type and scroll. Replay their actions in a video and ultimately increase conversions.',
177
- 'price' => '99EUR / 119USD',
178
- 'url' => 'https://plugins.matomo.org/HeatmapSessionRecording?wp=1',
179
- 'image' => '',
180
- ),
181
- array(
182
- 'name' => 'Custom Reports',
183
- 'description' => 'Pull out the information you need in order to be successful. Develop your custom strategy to meet your individualized goals while saving money & time.',
184
- 'price' => '99EUR / 119USD',
185
- 'url' => 'https://plugins.matomo.org/CustomReports?wp=1',
186
- 'image' => '',
187
- ),
188
 
189
- array(
190
- 'name' => 'Premium Bundle',
191
- 'description' => 'All premium features in one bundle, make the most out of your Matomo for WordPress and enjoy discounts of over 25%!',
192
- 'price' => '499EUR / 579USD',
193
- 'url' => 'https://plugins.matomo.org/WpPremiumBundle?wp=1',
194
- 'image' => '',
195
- )
196
- ),
197
- ),
198
- array(
199
  'title' => 'Most popular content engagement',
200
  'features' =>
201
- array(
202
- array(
203
  'name' => 'Form Analytics',
204
  'description' => 'Increase conversions on your online forms and lose less visitors by learning everything about your users behavior and their pain points on your forms.',
205
  'price' => '79EUR / 89USD',
206
  'url' => 'https://plugins.matomo.org/FormAnalytics?wp=1',
207
  'image' => '',
208
- ),
209
- array(
210
  'name' => 'Video & Audio Analytics',
211
  'description' => 'Grow your business with advanced video & audio analytics. Get powerful insights into how your audience watches your videos and listens to your audio.',
212
  'price' => '79EUR / 89USD',
213
  'url' => 'https://plugins.matomo.org/MediaAnalytics?wp=1',
214
  'image' => '',
215
- ),
216
- array(
217
  'name' => 'Users Flow',
218
  'description' => 'Users Flow is a visual representation of the most popular paths your users take through your website & app which lets you understand your users needs.',
219
  'price' => '39EUR / 39USD',
220
  'url' => 'https://plugins.matomo.org/UsersFlow?wp=1',
221
  'image' => '',
222
- ),
223
- ),
224
- ),
225
- array(
226
  'title' => 'Most popular acquisition & SEO features',
227
  'features' =>
228
- array(
229
- array(
230
  'name' => 'Search Engine Keywords Performance',
231
  'description' => 'All keywords searched by your users on search engines are now visible into your Referrers reports! The ultimate solution to \'Keyword not defined\'.',
232
  'price' => '69EUR / 79USD',
233
  'url' => 'https://plugins.matomo.org/SearchEngineKeywordsPerformance?wp=1',
234
  'image' => '',
235
- ),
236
- array(
237
  'name' => 'Advertising Conversion Export',
238
  'description' => 'Provides an export of attributed goal conversions for usage in ad networks like Google Ads so you no longer need a conversion pixel.',
239
  'price' => '79EUR / 89USD',
240
  'url' => 'https://plugins.matomo.org/AdvertisingConversionExport?wp=1',
241
  'image' => '',
242
- ),
243
- array(
244
  'name' => 'Multi Attribution',
245
  'description' => 'Get a clear understanding of how much credit each of your marketing channel is actually responsible for to shift your marketing efforts wisely.',
246
  'price' => '39EUR / 39USD',
247
  'url' => 'https://plugins.matomo.org/MultiChannelConversionAttribution?wp=1',
248
  'image' => '',
249
- ),
250
- /*
251
- array(
252
- 'name' => 'Activity Log',
253
- 'description' => 'Truly understand your visitors by seeing where they click, hover, type and scroll. Replay their actions in a video and ultimately increase conversions',
254
- 'price' => '19EUR / 19USD',
255
- 'url' => 'https://plugins.matomo.org/ActivityLog?wp=1',
256
- 'image' => '',
257
- ),*/
258
- ),
259
- ),
260
- array(
261
  'title' => 'Other premium features',
262
  'features' =>
263
- array(
264
- array(
265
  'name' => 'Funnels',
266
  'description' => 'Identify and understand where your visitors drop off to increase your conversions, sales and revenue with your existing traffic.',
267
  'price' => '89EUR / 99USD',
268
  'url' => 'https://plugins.matomo.org/Funnels?wp=1',
269
  'image' => '',
270
- ),
271
- array(
272
  'name' => 'Cohorts',
273
  'description' => 'Track your retention efforts over time and keep your visitors engaged and coming back for more.',
274
  'price' => '49EUR / 59USD',
275
  'url' => 'https://plugins.matomo.org/Cohorts?wp=1',
276
  'image' => '',
277
- ),
278
- ),
279
- ),
280
- );
281
 
282
- matomo_show_tables($matomo_feature_sections);
283
 
284
  ?>
285
 
13
 
14
  /** @var \WpMatomo\Settings $settings */
15
  $matomo_extra_url_params = '&' . http_build_query(
16
+ [
17
  'php' => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION,
18
  'matomo' => $settings->get_global_option( 'core_version' ),
19
  'wp_version' => ! empty( $GLOBALS['wp_version'] ) ? $GLOBALS['wp_version'] : '',
20
+ ]
21
  );
22
  ?>
23
  <div class="wrap">
30
 
31
  <div id="icon-plugins" class="icon32"></div>
32
 
33
+ <h1><?php matomo_header_icon(); ?><?php esc_html_e( 'Discover new functionality for your Matomo', 'matomo' ); ?></h1>
34
 
35
  <?php if ( ! is_plugin_active( MATOMO_MARKETPLACE_PLUGIN_NAME ) ) { ?>
36
+ <div class="updated notice matomo-marketplace-notice">
37
+ <p><?php echo sprintf( esc_html__( 'Easily install over 100 free plugins & %1$spremium features%2$s for Matomo with just a click', 'matomo' ), '<span style="white-space: nowrap;">', '</span>' ); ?>
38
+ </p>
39
+ <p><a href="https://builds.matomo.org/matomo-marketplace-for-wordpress-latest.zip" rel="noreferrer noopener"
40
+ class="button"><?php esc_html_e( 'Download Matomo Marketplace for WordPress', 'matomo' ); ?></a>
41
 
42
+ <a target="_blank"
43
+ href="https://matomo.org/faq/wordpress/how-do-i-install-a-matomo-marketplace-plugin-in-matomo-for-wordpress/"><span
44
+ class="dashicons-before dashicons-video-alt3"></span></a> <a target="_blank"
45
+ href="https://matomo.org/faq/wordpress/how-do-i-install-a-matomo-marketplace-plugin-in-matomo-for-wordpress/"><?php esc_html_e( 'Install instructions', 'matomo' ); ?></a>
46
+ </p>
47
+ </div>
48
  <?php } ?>
49
 
50
  <?php
51
+ function matomo_show_tables( $matomo_feature_sections ) {
52
+ foreach ( $matomo_feature_sections as $matomo_feature_section ) {
53
+ $matomo_feature_section['features'] = array_filter( $matomo_feature_section['features'] );
54
+ $matomo_num_features_in_block = count( $matomo_feature_section['features'] );
 
55
 
56
+ echo '<h2>' . esc_html( $matomo_feature_section['title'] ) . '</h2>';
57
+ echo '<div class="wp-list-table widefat plugin-install matomo-plugin-list matomo-plugin-row-' . esc_html( $matomo_num_features_in_block ) . '"><div id="the-list">';
58
 
59
+ foreach ( $matomo_feature_section['features'] as $matomo_index => $matomo_feature ) {
60
+ $matomo_style = '';
61
+ $matomo_is_3_columns = 3 === $matomo_num_features_in_block;
62
+ if ( $matomo_is_3_columns ) {
63
+ $matomo_style = 'width: calc(33% - 8px);min-width:282px;max-width:350px;';
64
+ if ( 2 === $matomo_index % 3 ) {
65
+ $matomo_style .= 'clear: inherit;margin-right: 0;margin-left: 16px;';
66
+ }
67
+ }
68
+ ?>
69
+ <div class="plugin-card" style="<?php echo esc_attr( $matomo_style ); ?>">
70
+ <?php
71
+ if ( $matomo_is_3_columns && ! empty( $matomo_feature['image'] ) ) {
72
+ ?>
73
+ <a
74
+ href="<?php echo esc_url( $matomo_feature['url'] ); ?>"
75
+ rel="noreferrer noopener" target="_blank"
76
+ class="thickbox open-plugin-details-modal"><img
77
+ src="<?php echo esc_url( $matomo_feature['image'] ); ?>"
78
+ style="height: 80px;width:100%;object-fit: cover;" alt=""></a>
79
+ <?php
80
+ }
81
+ ?>
82
 
83
+ <div class="plugin-card-top">
84
+ <div class="
85
  <?php
86
+ if ( ! $matomo_is_3_columns ) {
87
+ ?>
88
+ name column-name
89
+ <?php
90
+ }
91
+ ?>
92
+ " style="margin-right: 0;
93
+ <?php
94
+ if ( empty( $matomo_feature['image'] ) ) {
95
+ echo 'margin-left: 0;';
96
+ }
97
+ ?>
98
+ ">
99
+ <h3>
100
+ <a href="<?php echo esc_url( ! empty( $matomo_feature['video'] ) ? $matomo_feature['video'] : $matomo_feature['url'] ); ?>"
101
+ rel="noreferrer noopener" target="_blank"
102
+ class="thickbox open-plugin-details-modal">
103
+ <?php echo esc_html( $matomo_feature['name'] ); ?>
104
+ </a>
105
+ <?php
106
+ if ( ! $matomo_is_3_columns && ! empty( $matomo_feature['image'] ) ) {
107
+ ?>
108
+ <a
109
+ href="<?php echo esc_url( $matomo_feature['url'] ); ?>"
110
+ rel="noreferrer noopener" target="_blank"
111
+ class="thickbox open-plugin-details-modal"><img
112
+ src="<?php echo esc_url( $matomo_feature['image'] ); ?>" class="plugin-icon"
113
+ style="object-fit: cover;"
114
+ alt=""></a>
115
+ <?php
116
+ }
117
+ ?>
118
+ </h3>
119
+ </div>
120
+ <div class="
121
  <?php
122
+ if ( ! $matomo_is_3_columns ) {
123
+ ?>
124
+ desc column-description
125
+ <?php
126
+ }
127
+ ?>
128
+ "
129
+ style="margin-right: 0;
130
+ <?php
131
+ if ( empty( $matomo_feature['image'] ) ) {
132
+ echo 'margin-left: 0;';
133
+ }
134
+ ?>
135
+ ">
136
+ <p class="matomo-description"><?php echo esc_html( $matomo_feature['description'] ); ?>
137
+ <?php
138
+ if ( ! empty( $matomo_feature['video'] ) ) {
139
+ echo ' <a target="_blank" rel="noreferrer noopener" style="white-space: nowrap;" href="' . esc_url( $matomo_feature['video'] ) . '"><span class="dashicons dashicons-video-alt3"></span> ' . esc_html__( 'Learn more', 'matomo' ) . '</a>';
140
+ } elseif ( ! empty( $matomo_feature['url'] ) ) {
141
+ echo ' <a target="_blank" rel="noreferrer noopener" style="white-space: nowrap;" href="' . esc_url( $matomo_feature['url'] ) . '">' . esc_html__( 'Learn more', 'matomo' ) . '</a>';
142
+ }
143
+ ?>
144
+ </p>
145
+ <?php
146
+ if ( ! empty( $matomo_feature['price'] ) ) {
147
+ ?>
148
+ <p class="authors"><a class="button-primary"
149
+ rel="noreferrer noopener" target="_blank"
150
+ href="<?php echo esc_url( ! empty( $matomo_feature['download_url'] ) ? $matomo_feature['download_url'] : $matomo_feature['url'] ); ?>">
151
+ <?php
152
+ if ( 'free' === $matomo_feature['price'] ) {
153
+ esc_html_e( 'Download', 'matomo' );
154
+ } else {
155
+ echo esc_html( $matomo_feature['price'] );
156
+ }
157
+ ?>
158
+ </a>
159
+ </p>
160
+ <?php
161
+ }
162
+ ?>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ <?php
167
+ }
168
+ echo '';
169
+ echo '</div><div style="clear: both"></div>';
170
+ if ( ! empty( $matomo_feature_section['more_url'] ) ) {
171
+ echo '<a target="_blank" rel="noreferrer noopener" href="' . esc_attr( $matomo_feature_section['more_url'] ) . '"><span class="dashicons dashicons-arrow-right-alt2"></span>' . esc_html( $matomo_feature_section['more_text'] ) . '</a>';
172
+ }
173
+ echo '</div>';
174
+ }
175
+ }
176
 
177
+ $matomo_feature_sections = [
178
+ [
179
+ 'title' => 'Top free plugins',
180
+ 'more_url' => 'https://plugins.matomo.org/free?wp=1',
181
  'more_text' => 'Browse all free plugins',
182
+ 'features' =>
183
+ [
184
+ [
185
  'name' => 'Marketing Campaigns Reporting',
186
  'description' => 'Measure the effectiveness of your marketing campaigns. Track up to five channels instead of two: campaign, source, medium, keyword, content.',
187
  'price' => 'free',
188
  'download_url' => 'https://plugins.matomo.org/api/2.0/plugins/MarketingCampaignsReporting/download/latest?wp=1' . $matomo_extra_url_params,
189
  'url' => 'https://plugins.matomo.org/MarketingCampaignsReporting?wp=1&pk_campaign=WP&pk_source=Plugin',
190
  'image' => '',
191
+ ],
192
+ [
193
  'name' => 'Custom Alerts',
194
  'description' => 'Create custom Alerts to be notified of important changes on your website or app!',
195
  'price' => 'free',
196
  'download_url' => 'https://plugins.matomo.org/api/2.0/plugins/CustomAlerts/download/latest?wp=1' . $matomo_extra_url_params,
197
  'url' => 'https://plugins.matomo.org/CustomAlerts?wp=1&pk_campaign=WP&pk_source=Plugin',
198
  'image' => '',
199
+ ],
200
+ ],
201
+ ],
202
+ ];
203
 
204
+ matomo_show_tables( $matomo_feature_sections );
205
 
206
+ echo '<br>';
207
 
208
+ $matomo_feature_sections = [
209
+ [
210
+ 'title' => 'Most popular premium features',
211
+ 'features' =>
212
+ [
213
+ [
214
+ 'name' => 'Heatmap & Session Recording',
215
+ 'description' => 'Truly understand your visitors by seeing where they click, hover, type and scroll. Replay their actions in a video and ultimately increase conversions.',
216
+ 'price' => '99EUR / 119USD',
217
+ 'url' => 'https://plugins.matomo.org/HeatmapSessionRecording?wp=1',
218
+ 'image' => '',
219
+ ],
220
+ [
221
+ 'name' => 'Custom Reports',
222
+ 'description' => 'Pull out the information you need in order to be successful. Develop your custom strategy to meet your individualized goals while saving money & time.',
223
+ 'price' => '99EUR / 119USD',
224
+ 'url' => 'https://plugins.matomo.org/CustomReports?wp=1',
225
+ 'image' => '',
226
+ ],
227
 
228
+ [
229
+ 'name' => 'Premium Bundle',
230
+ 'description' => 'All premium features in one bundle, make the most out of your Matomo for WordPress and enjoy discounts of over 25%!',
231
+ 'price' => '499EUR / 579USD',
232
+ 'url' => 'https://plugins.matomo.org/WpPremiumBundle?wp=1',
233
+ 'image' => '',
234
+ ],
235
+ ],
236
+ ],
237
+ [
238
  'title' => 'Most popular content engagement',
239
  'features' =>
240
+ [
241
+ [
242
  'name' => 'Form Analytics',
243
  'description' => 'Increase conversions on your online forms and lose less visitors by learning everything about your users behavior and their pain points on your forms.',
244
  'price' => '79EUR / 89USD',
245
  'url' => 'https://plugins.matomo.org/FormAnalytics?wp=1',
246
  'image' => '',
247
+ ],
248
+ [
249
  'name' => 'Video & Audio Analytics',
250
  'description' => 'Grow your business with advanced video & audio analytics. Get powerful insights into how your audience watches your videos and listens to your audio.',
251
  'price' => '79EUR / 89USD',
252
  'url' => 'https://plugins.matomo.org/MediaAnalytics?wp=1',
253
  'image' => '',
254
+ ],
255
+ [
256
  'name' => 'Users Flow',
257
  'description' => 'Users Flow is a visual representation of the most popular paths your users take through your website & app which lets you understand your users needs.',
258
  'price' => '39EUR / 39USD',
259
  'url' => 'https://plugins.matomo.org/UsersFlow?wp=1',
260
  'image' => '',
261
+ ],
262
+ ],
263
+ ],
264
+ [
265
  'title' => 'Most popular acquisition & SEO features',
266
  'features' =>
267
+ [
268
+ [
269
  'name' => 'Search Engine Keywords Performance',
270
  'description' => 'All keywords searched by your users on search engines are now visible into your Referrers reports! The ultimate solution to \'Keyword not defined\'.',
271
  'price' => '69EUR / 79USD',
272
  'url' => 'https://plugins.matomo.org/SearchEngineKeywordsPerformance?wp=1',
273
  'image' => '',
274
+ ],
275
+ [
276
  'name' => 'Advertising Conversion Export',
277
  'description' => 'Provides an export of attributed goal conversions for usage in ad networks like Google Ads so you no longer need a conversion pixel.',
278
  'price' => '79EUR / 89USD',
279
  'url' => 'https://plugins.matomo.org/AdvertisingConversionExport?wp=1',
280
  'image' => '',
281
+ ],
282
+ [
283
  'name' => 'Multi Attribution',
284
  'description' => 'Get a clear understanding of how much credit each of your marketing channel is actually responsible for to shift your marketing efforts wisely.',
285
  'price' => '39EUR / 39USD',
286
  'url' => 'https://plugins.matomo.org/MultiChannelConversionAttribution?wp=1',
287
  'image' => '',
288
+ ],
289
+ ],
290
+ ],
291
+ [
 
 
 
 
 
 
 
 
292
  'title' => 'Other premium features',
293
  'features' =>
294
+ [
295
+ [
296
  'name' => 'Funnels',
297
  'description' => 'Identify and understand where your visitors drop off to increase your conversions, sales and revenue with your existing traffic.',
298
  'price' => '89EUR / 99USD',
299
  'url' => 'https://plugins.matomo.org/Funnels?wp=1',
300
  'image' => '',
301
+ ],
302
+ [
303
  'name' => 'Cohorts',
304
  'description' => 'Track your retention efforts over time and keep your visitors engaged and coming back for more.',
305
  'price' => '49EUR / 59USD',
306
  'url' => 'https://plugins.matomo.org/Cohorts?wp=1',
307
  'image' => '',
308
+ ],
309
+ ],
310
+ ],
311
+ ];
312
 
313
+ matomo_show_tables( $matomo_feature_sections );
314
 
315
  ?>
316
 
classes/WpMatomo/Admin/views/privacy_gdpr.php CHANGED
@@ -11,81 +11,89 @@
11
 
12
  use WpMatomo\Admin\Menu;
13
  use WpMatomo\Admin\PrivacySettings;
 
14
 
15
  if ( ! defined( 'ABSPATH' ) ) {
16
  exit;
17
  }
18
- /** @var \WpMatomo\Settings $matomo_settings */
19
 
20
  ?>
21
 
22
  <h2><?php esc_html_e( 'Matomo ensures the privacy of your users and analytics data! YOU keep control of your data.', 'matomo' ); ?></h2>
23
 
24
  <blockquote
25
- class="matomo-blockquote"><?php esc_html_e( 'One of Matomo\'s guiding principles: respecting privacy', 'matomo' ); ?></blockquote>
26
  <p>
27
  <?php esc_html_e( 'Matomo Analytics is privacy by design. All data collected is stored only within your own MySQL database, no other business (or Matomo team member) can access any of this information, and logs or report data will never be sent to other servers by Matomo', 'matomo' ); ?>
28
  .
29
 
30
  <?php
31
  echo sprintf(
32
- __( 'The source code of the software is open-source so hundreds of people have reviewed it to ensure it is %1$ssecure%2$s and keeps your data private.', 'matomo' ),
33
  '<a href="https://matomo.org/security/" rel="noreferrer noopener">',
34
  '</a>'
35
  );
36
  ?>
37
  </p>
38
- <?php if ($matomo_settings->is_network_enabled() && is_network_admin()) { ?>
39
- <h2>Configure privacy settings</h2>
40
- <p>
41
- Currently, privacy settings have to be configured on a per blog basis.
42
- IP addresses are anonmyised by default. Should you wish to change any privacy setting, please go to the Matomo privacy settings within each blog.
43
- We are hoping to improve this in the future.
44
- </p>
 
45
  <?php } else { ?>
46
 
47
- <h2>
48
- <?php esc_html_e( 'Ways Matomo protects the privacy of your users and customers', 'matomo' ); ?>
49
- </h2>
50
- <p><?php esc_html_e( 'Although Matomo Analytics is a web analytics software that has a purpose to track user activity on your website, we take privacy very seriously.', 'matomo' ); ?></p>
51
- <p><?php esc_html_e( 'Privacy is a fundamental right so by using Matomo you can rest assured you have 100% control over that data and can protect your user\'s privacy as it\'s on your own server.', 'matomo' ); ?></p>
52
 
53
- <ul class="matomo-list">
54
- <li>
55
- <a href="<?php echo Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_ANONYMIZE_DATA ); ?>"><?php esc_html_e( 'Anonymise data and IP addresses', 'matomo' ); ?></a>
56
- </li>
57
- <li>
58
- <a href="<?php echo Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_DATA_RETENTION ); ?>"><?php esc_html_e( 'Configure data retention', 'matomo' ); ?></a>
59
- </li>
60
- <li>
61
- <a href="<?php echo Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_OPTOUT ); ?>"><?php esc_html_e( 'Matomo has an opt-out mechanism which lets users opt-out of web analytics tracking', 'matomo' ); ?></a>
62
- (<?php esc_html_e( 'see below for the shortcode', 'matomo' ); ?>)
63
- </li>
64
- <li>
65
- <a href="<?php echo Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_ASK_CONSENT ); ?>"><?php esc_html_e( 'Asking for consent', 'matomo' ); ?></a>
66
- </li>
67
- <li>
68
- <a href="<?php echo Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_GDPR_OVERVIEW ); ?>"><?php esc_html_e( 'GDPR overview', 'matomo' ); ?></a>
69
- </li>
70
- <li>
71
- <a href="<?php echo Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_GDPR_TOOLS ); ?>"><?php esc_html_e( 'GDPR tools', 'matomo' ); ?></a>
72
- </li>
73
- </ul>
74
  <?php } ?>
75
  <h2>
76
  <?php esc_html_e( 'Let users opt-out of tracking', 'matomo' ); ?>
77
  </h2>
78
- <p>
 
 
79
  <?php
80
  echo sprintf(
81
- __( 'Use the short code %1$s to embed the opt out iframe into your website.', 'matomo' ),
82
  '<code>' . esc_html( PrivacySettings::EXAMPLE_MINIMAL ) . '</code>'
83
  );
84
  ?>
85
- <br/>
86
  <?php esc_html_e( 'You can use these short code options:', 'matomo' ); ?>
87
- </p>
88
  <ul class="matomo-list">
89
- <li>language - eg de or en. <?php esc_html_e( 'By default the language is detected automatically based on the user\'s browser', 'matomo' ); ?></li>
 
 
 
 
 
 
90
  </ul>
91
- <p><?php esc_html_e( 'Example', 'matomo' ); ?>: <code><?php echo esc_html( PrivacySettings::EXAMPLE_FULL ); ?></code></p>
11
 
12
  use WpMatomo\Admin\Menu;
13
  use WpMatomo\Admin\PrivacySettings;
14
+ use WpMatomo\Settings;
15
 
16
  if ( ! defined( 'ABSPATH' ) ) {
17
  exit;
18
  }
19
+ /** @var Settings $matomo_settings */
20
 
21
  ?>
22
 
23
  <h2><?php esc_html_e( 'Matomo ensures the privacy of your users and analytics data! YOU keep control of your data.', 'matomo' ); ?></h2>
24
 
25
  <blockquote
26
+ class="matomo-blockquote"><?php esc_html_e( 'One of Matomo\'s guiding principles: respecting privacy', 'matomo' ); ?></blockquote>
27
  <p>
28
  <?php esc_html_e( 'Matomo Analytics is privacy by design. All data collected is stored only within your own MySQL database, no other business (or Matomo team member) can access any of this information, and logs or report data will never be sent to other servers by Matomo', 'matomo' ); ?>
29
  .
30
 
31
  <?php
32
  echo sprintf(
33
+ esc_html__( 'The source code of the software is open-source so hundreds of people have reviewed it to ensure it is %1$ssecure%2$s and keeps your data private.', 'matomo' ),
34
  '<a href="https://matomo.org/security/" rel="noreferrer noopener">',
35
  '</a>'
36
  );
37
  ?>
38
  </p>
39
+ <?php if ( $matomo_settings->is_network_enabled() && is_network_admin() ) { ?>
40
+ <h2>Configure privacy settings</h2>
41
+ <p>
42
+ Currently, privacy settings have to be configured on a per blog basis.
43
+ IP addresses are anonmyised by default. Should you wish to change any privacy setting, please go to the Matomo
44
+ privacy settings within each blog.
45
+ We are hoping to improve this in the future.
46
+ </p>
47
  <?php } else { ?>
48
 
49
+ <h2>
50
+ <?php esc_html_e( 'Ways Matomo protects the privacy of your users and customers', 'matomo' ); ?>
51
+ </h2>
52
+ <p><?php esc_html_e( 'Although Matomo Analytics is a web analytics software that has a purpose to track user activity on your website, we take privacy very seriously.', 'matomo' ); ?></p>
53
+ <p><?php esc_html_e( 'Privacy is a fundamental right so by using Matomo you can rest assured you have 100% control over that data and can protect your user\'s privacy as it\'s on your own server.', 'matomo' ); ?></p>
54
 
55
+ <ul class="matomo-list">
56
+ <li>
57
+ <a href="<?php echo esc_url( Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_ANONYMIZE_DATA ) ); ?>"><?php esc_html_e( 'Anonymise data and IP addresses', 'matomo' ); ?></a>
58
+ </li>
59
+ <li>
60
+ <a href="<?php echo esc_url( Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_DATA_RETENTION ) ); ?>"><?php esc_html_e( 'Configure data retention', 'matomo' ); ?></a>
61
+ </li>
62
+ <li>
63
+ <a href="<?php echo esc_url( Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_OPTOUT ) ); ?>"><?php esc_html_e( 'Matomo has an opt-out mechanism which lets users opt-out of web analytics tracking', 'matomo' ); ?></a>
64
+ (<?php esc_html_e( 'see below for the shortcode', 'matomo' ); ?>)
65
+ </li>
66
+ <li>
67
+ <a href="<?php echo esc_url( Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_ASK_CONSENT ) ); ?>"><?php esc_html_e( 'Asking for consent', 'matomo' ); ?></a>
68
+ </li>
69
+ <li>
70
+ <a href="<?php echo esc_url( Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_GDPR_OVERVIEW ) ); ?>"><?php esc_html_e( 'GDPR overview', 'matomo' ); ?></a>
71
+ </li>
72
+ <li>
73
+ <a href="<?php echo esc_url( Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_GDPR_TOOLS ) ); ?>"><?php esc_html_e( 'GDPR tools', 'matomo' ); ?></a>
74
+ </li>
75
+ </ul>
76
  <?php } ?>
77
  <h2>
78
  <?php esc_html_e( 'Let users opt-out of tracking', 'matomo' ); ?>
79
  </h2>
80
+ <p><?php esc_html_e( 'You have two options to embed the opt out iframe into your website:', 'matomo' ); ?></p>
81
+ <ul class="matomo-list">
82
+ <li>
83
  <?php
84
  echo sprintf(
85
+ esc_html__( 'Use the short code %1$s.', 'matomo' ),
86
  '<code>' . esc_html( PrivacySettings::EXAMPLE_MINIMAL ) . '</code>'
87
  );
88
  ?>
89
+ <br/>
90
  <?php esc_html_e( 'You can use these short code options:', 'matomo' ); ?>
 
91
  <ul class="matomo-list">
92
+ <li>language - eg de or
93
+ en. <?php esc_html_e( 'By default the language is detected automatically based on the user\'s browser', 'matomo' ); ?></li>
94
+ </ul>
95
+
96
+ <?php esc_html_e( 'Example', 'matomo' ); ?>: <code><?php echo esc_html( PrivacySettings::EXAMPLE_FULL ); ?></code>
97
+ </li>
98
+ <li><?php esc_html_e( 'Or you can add the "Matomo opt out" block directly to your page.', 'matomo' ); ?></li>
99
  </ul>
 
classes/WpMatomo/Admin/views/settings.php CHANGED
@@ -11,6 +11,7 @@ use WpMatomo\Admin\AdminSettings;
11
  use WpMatomo\Admin\AdminSettingsInterface;
12
  use WpMatomo\Admin\Menu;
13
  use WpMatomo\Capabilities;
 
14
 
15
  if ( ! defined( 'ABSPATH' ) ) {
16
  exit;
@@ -18,37 +19,39 @@ if ( ! defined( 'ABSPATH' ) ) {
18
  /** @var AdminSettingsInterface[] $setting_tabs */
19
  /** @var AdminSettingsInterface $content_tab */
20
  /** @var string $active_tab */
21
- /** @var \WpMatomo\Settings $matomo_settings */
22
  ?>
23
  <div class="wrap">
24
  <div id="icon-plugins" class="icon32"></div>
25
- <h1><?php matomo_header_icon(); ?> <?php esc_html_e( 'Settings', 'matomo' ); ?></h1>
26
- <?php
27
- if ( $matomo_settings->is_network_enabled() && is_network_admin() ) {
28
- echo '<div class="notice notice-info is-dismissible"><br>You are running Matomo in network mode. This means below settings will be applied to all blogs in your network.<br><br></div>';
29
- } elseif ($matomo_settings->is_network_enabled() && !is_network_admin()) {
30
- echo '<div class="notice notice-info is-dismissible"><br>';
31
- esc_html_e('You are running Matomo in network mode.', 'matomo');
32
- echo ' ';
33
- echo 'Below settings aren\'t applied for all blogs but have to be configured for each blog separately. We are hoping to improve this in the future. Any setting within the Matomo admin is configured on a per blog basis as well. Only you as a Matomo super user can see these settings.<br><br></div>';
34
- }
35
- ?>
36
  <h2 class="nav-tab-wrapper">
37
  <?php foreach ( $setting_tabs as $matomo_setting_slug => $matomo_setting_tab ) { ?>
38
- <a href="<?php echo AdminSettings::make_url( $matomo_setting_slug ); ?>"
39
  class="nav-tab <?php echo $active_tab === $matomo_setting_slug ? 'nav-tab-active' : ''; ?>"
40
  ><?php echo esc_html( $matomo_setting_tab->get_title() ); ?></a>
41
  <?php } ?>
42
 
43
  <?php
44
  if ( current_user_can( Capabilities::KEY_SUPERUSER )
45
- && ! is_network_admin() ) {
46
  ?>
47
- <a href="<?php echo Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_ADMIN ); ?>" class="nav-tab"
48
  ><?php esc_html_e( 'Matomo Admin', 'matomo' ); ?> <span class="dashicons-before dashicons-external"></span></a>
49
 
50
- <?php } ?>
 
 
51
  </h2>
52
 
53
- <?php echo $content_tab->show_settings(); ?>
54
  </div>
11
  use WpMatomo\Admin\AdminSettingsInterface;
12
  use WpMatomo\Admin\Menu;
13
  use WpMatomo\Capabilities;
14
+ use WpMatomo\Settings;
15
 
16
  if ( ! defined( 'ABSPATH' ) ) {
17
  exit;
19
  /** @var AdminSettingsInterface[] $setting_tabs */
20
  /** @var AdminSettingsInterface $content_tab */
21
  /** @var string $active_tab */
22
+ /** @var Settings $matomo_settings */
23
  ?>
24
  <div class="wrap">
25
  <div id="icon-plugins" class="icon32"></div>
26
+ <h1><?php matomo_header_icon(); ?><?php esc_html_e( 'Settings', 'matomo' ); ?></h1>
27
+ <?php
28
+ if ( $matomo_settings->is_network_enabled() && is_network_admin() ) {
29
+ echo '<div class="notice notice-info is-dismissible"><br>You are running Matomo in network mode. This means below settings will be applied to all blogs in your network.<br><br></div>';
30
+ } elseif ( $matomo_settings->is_network_enabled() && ! is_network_admin() ) {
31
+ echo '<div class="notice notice-info is-dismissible"><br>';
32
+ esc_html_e( 'You are running Matomo in network mode.', 'matomo' );
33
+ echo ' ';
34
+ echo 'Below settings aren\'t applied for all blogs but have to be configured for each blog separately. We are hoping to improve this in the future. Any setting within the Matomo admin is configured on a per blog basis as well. Only you as a Matomo super user can see these settings.<br><br></div>';
35
+ }
36
+ ?>
37
  <h2 class="nav-tab-wrapper">
38
  <?php foreach ( $setting_tabs as $matomo_setting_slug => $matomo_setting_tab ) { ?>
39
+ <a href="<?php echo esc_url( AdminSettings::make_url( $matomo_setting_slug ) ); ?>"
40
  class="nav-tab <?php echo $active_tab === $matomo_setting_slug ? 'nav-tab-active' : ''; ?>"
41
  ><?php echo esc_html( $matomo_setting_tab->get_title() ); ?></a>
42
  <?php } ?>
43
 
44
  <?php
45
  if ( current_user_can( Capabilities::KEY_SUPERUSER )
46
+ && ! is_network_admin() ) {
47
  ?>
48
+ <a href="<?php echo esc_url( Menu::get_matomo_goto_url( Menu::REPORTING_GOTO_ADMIN ) ); ?>" class="nav-tab"
49
  ><?php esc_html_e( 'Matomo Admin', 'matomo' ); ?> <span class="dashicons-before dashicons-external"></span></a>
50
 
51
+ <?php
52
+ }
53
+ ?>
54
  </h2>
55
 
56
+ <?php $content_tab->show_settings(); ?>
57
  </div>
classes/WpMatomo/Admin/views/settings_errors.php CHANGED
@@ -6,13 +6,17 @@
6
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7
  * @package matomo
8
  */
9
- /** @var string[] $errors */
 
 
 
 
10
  if ( ! defined( 'ABSPATH' ) ) {
11
  exit;
12
  }
13
  ?>
14
  <div class="updated error">
15
- <?php foreach ( $errors as $error ) : ?>
16
- <p><?php echo esc_html( $error ); ?></p>
17
  <?php endforeach; ?>
18
  </div>
6
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7
  * @package matomo
8
  */
9
+ /**
10
+ * phpcs considers all of our variables as global and want them prefixed with matomo
11
+ * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
12
+ */
13
+ /** @var string[] $settings_errors */
14
  if ( ! defined( 'ABSPATH' ) ) {
15
  exit;
16
  }
17
  ?>
18
  <div class="updated error">
19
+ <?php foreach ( $settings_errors as $setting_error ) : ?>
20
+ <p><?php echo esc_html( $setting_error ); ?></p>
21
  <?php endforeach; ?>
22
  </div>
classes/WpMatomo/Admin/views/summary.php CHANGED
@@ -6,8 +6,13 @@
6
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7
  * @package matomo
8
  */
9
-
 
 
 
 
10
  use WpMatomo\Admin\Menu;
 
11
  use WpMatomo\Report\Dates;
12
 
13
  if ( ! defined( 'ABSPATH' ) ) {
@@ -23,32 +28,34 @@ if ( ! defined( 'ABSPATH' ) ) {
23
  /** @var bool $matomo_pinned */
24
  /** @var bool $is_tracking */
25
  /** @var bool $matomo_is_version_pre55 */
26
- /** @var \WpMatomo\Admin\Dashboard $matomo_dashboard */
27
  global $wp;
28
 
29
- $matomo_dashboard_nonce = wp_create_nonce(\WpMatomo\Admin\Summary::NONCE_DASHBOARD);
30
  ?>
31
  <?php
32
- if ($matomo_pinned) {
33
- echo '<div class="notice notice-success"><p>' . esc_html__( 'Dashboard updated.', 'matomo' ) . '</p></div>';
34
- }
35
- if ($matomo_is_version_pre55) {
36
- echo '<style type="text/css">.handle-actions { position: absolute; right: 0;top: 0;}</style>';
37
- }
38
  ?>
39
  <?php if ( ! $is_tracking ) { ?>
40
- <div class="notice notice-warning"><p><?php esc_html_e( 'Matomo Tracking is not enabled. If you have added the Matomo tracking code in a different way, for example using a consent plugin, then you can ignore this message.', 'matomo' ); ?></p></div>
 
 
41
  <?php } ?>
42
  <div class="wrap">
43
  <div id="icon-plugins" class="icon32"></div>
44
- <h1><?php matomo_header_icon(); ?> <?php esc_html_e( 'Summary', 'matomo' ); ?></h1>
45
  <?php
46
  if ( Dates::TODAY === $report_date ) {
47
  echo '<div class="notice notice-info" style="padding:8px;">' . esc_html__( 'Reports for today are only refreshed approximately every hour through the WordPress cronjob.', 'matomo' ) . '</div>';
48
  }
49
  ?>
50
  <p><?php esc_html_e( 'Looking for all reports and advanced features like segmentation, real time reports, and more?', 'matomo' ); ?>
51
- <a href="<?php echo add_query_arg( array( 'report_date' => $report_date ), menu_page_url( Menu::SLUG_REPORTING, false ) ); ?>"
52
  ><?php esc_html_e( 'View full reporting', 'matomo' ); ?></a>
53
  <br/><br/>
54
  <?php esc_html_e( 'Change date:', 'matomo' ); ?>
@@ -58,16 +65,16 @@ $matomo_dashboard_nonce = wp_create_nonce(\WpMatomo\Admin\Summary::NONCE_DASHBOA
58
  if ( $report_date === $matomo_report_date_key ) {
59
  $matomo_button_class = 'button-primary';
60
  }
61
- echo '<a href="' . esc_url( add_query_arg( array( 'report_date' => $matomo_report_date_key ), menu_page_url( Menu::SLUG_REPORT_SUMMARY, false ) ) ) . '" class="' . $matomo_button_class . '">' . esc_html( $matomo_report_name ) . '</a> ';
62
  }
63
  ?>
64
 
65
  <div id="dashboard-widgets" class="metabox-holder has-right-sidebar matomo-dashboard-container">
66
  <?php
67
- $matomo_columns = array( 1, 0 );
68
  foreach ( $matomo_columns as $matomo_column_index => $matomo_column_modulo ) {
69
  ?>
70
- <div id="postbox-container-<?php echo( $matomo_column_index + 1 ); ?>" class="postbox-container">
71
  <div id="normal-sortables" class="meta-box-sortables ui-sortable">
72
  <?php
73
  foreach ( $reports_to_show as $matomo_index => $matomo_report_meta ) {
@@ -77,58 +84,84 @@ $matomo_dashboard_nonce = wp_create_nonce(\WpMatomo\Admin\Summary::NONCE_DASHBOA
77
  $shortcode = sprintf( '[matomo_report unique_id=%s report_date=%s limit=10]', $matomo_report_meta['uniqueId'], $report_date );
78
  ?>
79
  <div class="postbox ">
80
- <div class="postbox-header">
81
- <h2 class="hndle ui-sortable-handle"
82
- style="cursor: help;"
83
- title="<?php echo ! empty( $matomo_report_meta['documentation'] ) ? ( wp_strip_all_tags( $matomo_report_meta['documentation'] ) . ' ' ) : null; ?><?php esc_html_e( 'You can embed this report on any page using the shortcode:', 'matomo' ); ?> <?php echo esc_attr( $shortcode ); ?>"
84
- >
85
- <?php echo esc_html( $matomo_report_meta['name'] ); ?></h2>
86
- <div class="handle-actions hide-if-no-js">
87
- <?php if ( ! empty( $matomo_report_meta['page'] ) ) { ?>
88
- <button type="button" class="handlediv" aria-expanded="true"
89
- title="<?php esc_html_e( 'Click to view the report in detail', 'matomo' ); ?>"><a
90
- href="
91
  <?php
92
 
93
- echo Menu::get_matomo_reporting_url(
94
- $matomo_report_meta['page']['category'],
95
- $matomo_report_meta['page']['subcategory'],
96
- array(
97
- 'period' => $report_period_selected,
98
- 'date' => $report_date_selected,
99
- )
100
- );
 
 
101
  ?>
102
- " style="color: inherit;text-decoration: none;" target="_blank" rel="noreferrer noopener"
103
- class="dashicons-before dashicons-external" aria-hidden="true"></a></button>
104
- <?php } ?>
 
 
105
 
106
- <?php
107
- $matomo_is_dashboard_widget = $matomo_dashboard->has_widget($matomo_report_meta['uniqueId'], $report_date);
108
- ?>
109
- <button type="button" class="handlediv" aria-expanded="true"
110
- title="<?php if ($matomo_is_dashboard_widget) { esc_html_e( 'Click to remove this report from the WordPress admin dashboard', 'matomo' ); } else { esc_html_e( 'Click to add this report to the WordPress admin dashboard', 'matomo' ); } ?>"><a
111
- href="
 
 
 
 
 
112
  <?php
113
- echo esc_url(add_query_arg(array(
114
- 'pin' => true,
115
- '_wpnonce' => $matomo_dashboard_nonce,
116
- 'report_uniqueid' => $matomo_report_meta['uniqueId'],
117
- 'report_date' => $report_date,
118
- ), menu_page_url(Menu::SLUG_REPORT_SUMMARY, false)));
 
 
 
 
 
119
  ?>
120
- " style="color: inherit;text-decoration: none;<?php if ($matomo_is_dashboard_widget) { echo 'opacity: 0.4 !important'; } ?>"
121
- class="dashicons-before dashicons-admin-post" aria-hidden="true"></a></button>
 
 
 
 
 
 
 
122
 
123
- </div></div>
 
124
  <div>
125
  <?php echo do_shortcode( $shortcode ); ?>
126
  </div>
127
  </div>
128
- <?php } ?>
 
 
129
  </div>
130
  </div>
131
- <?php } ?>
 
 
132
  </div>
133
 
134
  <p style="clear:both;">
6
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7
  * @package matomo
8
  */
9
+ /**
10
+ * phpcs considers all of our variables as global and want them prefixed with matomo
11
+ * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
12
+ */
13
+ use WpMatomo\Admin\Dashboard;
14
  use WpMatomo\Admin\Menu;
15
+ use WpMatomo\Admin\Summary;
16
  use WpMatomo\Report\Dates;
17
 
18
  if ( ! defined( 'ABSPATH' ) ) {
28
  /** @var bool $matomo_pinned */
29
  /** @var bool $is_tracking */
30
  /** @var bool $matomo_is_version_pre55 */
31
+ /** @var Dashboard $matomo_dashboard */
32
  global $wp;
33
 
34
+ $matomo_dashboard_nonce = wp_create_nonce( Summary::NONCE_DASHBOARD );
35
  ?>
36
  <?php
37
+ if ( $matomo_pinned ) {
38
+ echo '<div class="notice notice-success"><p>' . esc_html__( 'Dashboard updated.', 'matomo' ) . '</p></div>';
39
+ }
40
+ if ( $matomo_is_version_pre55 ) {
41
+ echo '<style type="text/css">.handle-actions { position: absolute; right: 0;top: 0;}</style>';
42
+ }
43
  ?>
44
  <?php if ( ! $is_tracking ) { ?>
45
+ <div class="notice notice-warning">
46
+ <p><?php esc_html_e( 'Matomo Tracking is not enabled. If you have added the Matomo tracking code in a different way, for example using a consent plugin, then you can ignore this message.', 'matomo' ); ?></p>
47
+ </div>
48
  <?php } ?>
49
  <div class="wrap">
50
  <div id="icon-plugins" class="icon32"></div>
51
+ <h1><?php matomo_header_icon(); ?><?php esc_html_e( 'Summary', 'matomo' ); ?></h1>
52
  <?php
53
  if ( Dates::TODAY === $report_date ) {
54
  echo '<div class="notice notice-info" style="padding:8px;">' . esc_html__( 'Reports for today are only refreshed approximately every hour through the WordPress cronjob.', 'matomo' ) . '</div>';
55
  }
56
  ?>
57
  <p><?php esc_html_e( 'Looking for all reports and advanced features like segmentation, real time reports, and more?', 'matomo' ); ?>
58
+ <a href="<?php echo esc_url( add_query_arg( [ 'report_date' => $report_date ], menu_page_url( Menu::SLUG_REPORTING, false ) ) ); ?>"
59
  ><?php esc_html_e( 'View full reporting', 'matomo' ); ?></a>
60
  <br/><br/>
61
  <?php esc_html_e( 'Change date:', 'matomo' ); ?>
65
  if ( $report_date === $matomo_report_date_key ) {
66
  $matomo_button_class = 'button-primary';
67
  }
68
+ echo '<a href="' . esc_url( add_query_arg( [ 'report_date' => $matomo_report_date_key ], menu_page_url( Menu::SLUG_REPORT_SUMMARY, false ) ) ) . '" class="' . esc_attr( $matomo_button_class ) . '">' . esc_html( $matomo_report_name ) . '</a> ';
69
  }
70
  ?>
71
 
72
  <div id="dashboard-widgets" class="metabox-holder has-right-sidebar matomo-dashboard-container">
73
  <?php
74
+ $matomo_columns = [ 1, 0 ];
75
  foreach ( $matomo_columns as $matomo_column_index => $matomo_column_modulo ) {
76
  ?>
77
+ <div id="postbox-container-<?php echo ( esc_html( $matomo_column_index + 1 ) ); ?>" class="postbox-container">
78
  <div id="normal-sortables" class="meta-box-sortables ui-sortable">
79
  <?php
80
  foreach ( $reports_to_show as $matomo_index => $matomo_report_meta ) {
84
  $shortcode = sprintf( '[matomo_report unique_id=%s report_date=%s limit=10]', $matomo_report_meta['uniqueId'], $report_date );
85
  ?>
86
  <div class="postbox ">
87
+ <div class="postbox-header">
88
+ <h2 class="hndle ui-sortable-handle"
89
+ style="cursor: help;"
90
+ title="<?php echo ! empty( $matomo_report_meta['documentation'] ) ? ( esc_html( wp_strip_all_tags( $matomo_report_meta['documentation'] ) . ' ' ) ) : null; ?><?php esc_html_e( 'You can embed this report on any page using the shortcode:', 'matomo' ); ?> <?php echo esc_attr( $shortcode ); ?>">
91
+ <?php echo esc_html( $matomo_report_meta['name'] ); ?></h2>
92
+ <div class="handle-actions hide-if-no-js">
93
+ <?php if ( ! empty( $matomo_report_meta['page'] ) ) { ?>
94
+ <button type="button" class="handlediv" aria-expanded="true"
95
+ title="<?php esc_html_e( 'Click to view the report in detail', 'matomo' ); ?>">
96
+ <a
97
+ href="
98
  <?php
99
 
100
+ echo esc_url(
101
+ Menu::get_matomo_reporting_url(
102
+ $matomo_report_meta['page']['category'],
103
+ $matomo_report_meta['page']['subcategory'],
104
+ [
105
+ 'period' => $report_period_selected,
106
+ 'date' => $report_date_selected,
107
+ ]
108
+ )
109
+ );
110
  ?>
111
+ " style="color: inherit;text-decoration: none;" target="_blank"
112
+ rel="noreferrer noopener"
113
+ class="dashicons-before dashicons-external" aria-hidden="true"></a>
114
+ </button>
115
+ <?php } ?>
116
 
117
+ <?php $matomo_is_dashboard_widget = $matomo_dashboard->has_widget( $matomo_report_meta['uniqueId'], $report_date ); ?>
118
+ <?php // phpcs:ignore Squiz.PHP.EmbeddedPhp.ContentBeforeOpen ?>
119
+ <button type="button" class="handlediv" aria-expanded="true" title="<?php
120
+ if ( $matomo_is_dashboard_widget ) {
121
+ esc_html_e( 'Click to remove this report from the WordPress admin dashboard', 'matomo' );
122
+ } else {
123
+ esc_html_e( 'Click to add this report to the WordPress admin dashboard', 'matomo' );
124
+ }
125
+ // phpcs:ignore Squiz.PHP.EmbeddedPhp.ContentAfterEnd
126
+ ?>"><a
127
+ href="
128
  <?php
129
+ echo esc_url(
130
+ add_query_arg(
131
+ [
132
+ 'pin' => true,
133
+ '_wpnonce' => $matomo_dashboard_nonce,
134
+ 'report_uniqueid' => $matomo_report_meta['uniqueId'],
135
+ 'report_date' => $report_date,
136
+ ],
137
+ menu_page_url( Menu::SLUG_REPORT_SUMMARY, false )
138
+ )
139
+ );
140
  ?>
141
+ " style="color: inherit;text-decoration: none;
142
+ <?php
143
+ if ( $matomo_is_dashboard_widget ) {
144
+ echo 'opacity: 0.4 !important';
145
+ }
146
+ ?>
147
+ "
148
+ class="dashicons-before dashicons-admin-post" aria-hidden="true"></a>
149
+ </button>
150
 
151
+ </div>
152
+ </div>
153
  <div>
154
  <?php echo do_shortcode( $shortcode ); ?>
155
  </div>
156
  </div>
157
+ <?php
158
+ }
159
+ ?>
160
  </div>
161
  </div>
162
+ <?php
163
+ }
164
+ ?>
165
  </div>
166
 
167
  <p style="clear:both;">
classes/WpMatomo/Admin/views/systemreport.php CHANGED
@@ -6,7 +6,10 @@
6
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7
  * @package matomo
8
  */
9
-
 
 
 
10
  if ( ! defined( 'ABSPATH' ) ) {
11
  exit;
12
  }
@@ -25,11 +28,11 @@ use WpMatomo\Admin\SystemReport;
25
  if ( ! function_exists( 'matomo_format_value_text' ) ) {
26
  function matomo_format_value_text( $value ) {
27
  if ( is_string( $value ) && ! empty( $value ) ) {
28
- $matomo_format = array(
29
  '<br />' => ' ',
30
  '<br/>' => ' ',
31
  '<br>' => ' ',
32
- );
33
  foreach ( $matomo_format as $search => $replace ) {
34
  $value = str_replace( $search, $replace, $value );
35
  }
@@ -44,9 +47,9 @@ if ( ! function_exists( 'matomo_format_value_text' ) ) {
44
  <?php
45
  if ( $matomo_has_warning_and_no_errors ) {
46
  ?>
47
- <div class="notice notice-warning">
48
- <p><?php esc_html_e( 'There are some issues with your system. Matomo will run, but you might experience some minor problems. See below for more information.', 'matomo' ); ?></p>
49
- </div>
50
  <?php
51
  }
52
  ?>
@@ -56,12 +59,12 @@ if ( ! function_exists( 'matomo_format_value_text' ) ) {
56
  </div>
57
  <?php } ?>
58
  <div id="icon-plugins" class="icon32"></div>
59
- <h1><?php matomo_header_icon(); ?> <?php esc_html_e( 'Diagnostics', 'matomo' ); ?></h1>
60
 
61
  <h2 class="nav-tab-wrapper">
62
- <a href="?page=<?php echo Menu::SLUG_SYSTEM_REPORT; ?>"
63
  class="nav-tab <?php echo empty( $matomo_active_tab ) ? 'nav-tab-active' : ''; ?>"> System report</a>
64
- <a href="?page=<?php echo Menu::SLUG_SYSTEM_REPORT; ?>&tab=troubleshooting"
65
  class="nav-tab <?php echo 'troubleshooting' === $matomo_active_tab ? 'nav-tab-active' : ''; ?>">Troubleshooting</a>
66
  </h2>
67
 
@@ -78,7 +81,7 @@ if ( ! function_exists( 'matomo_format_value_text' ) ) {
78
  id="matomo_system_report_info">
79
  <?php
80
  foreach ( $matomo_tables as $matomo_table ) {
81
- if (empty($matomo_table['rows'])) {
82
  continue;
83
  }
84
  echo '# ' . esc_html( $matomo_table['title'] ) . "\n";
@@ -111,9 +114,9 @@ if ( ! function_exists( 'matomo_format_value_text' ) ) {
111
 
112
  <?php
113
  foreach ( $matomo_tables as $matomo_table ) {
114
- if (empty($matomo_table['rows'])) {
115
- continue;
116
- }
117
  echo '<h2>' . esc_html( $matomo_table['title'] ) . "</h2><table class='widefat'><thead></thead><tbody>";
118
  foreach ( $matomo_table['rows'] as $matomo_row ) {
119
  if ( ! empty( $matomo_row['section'] ) ) {
@@ -136,7 +139,7 @@ if ( ! function_exists( 'matomo_format_value_text' ) ) {
136
  echo "<td width='30%'>" . esc_html( $matomo_row['name'] ) . '</td>';
137
  echo "<td width='" . ( ! empty( $matomo_table['has_comments'] ) ? 20 : 70 ) . "%'>" . esc_html( $matomo_value ) . '</td>';
138
  if ( ! empty( $matomo_table['has_comments'] ) ) {
139
- $matomo_replaced_elements = array(
140
  '<code>' => '__#CODEBACKUP#__',
141
  '</code>' => '__##CODEBACKUP##__',
142
  '<pre style="overflow-x: scroll;max-width: 600px;">' => '__#PREBACKUP#__',
@@ -144,10 +147,11 @@ if ( ! function_exists( 'matomo_format_value_text' ) ) {
144
  '<br/>' => '__#BRBACKUP#__',
145
  '<br />' => '__#BRBACKUP#__',
146
  '<br>' => '__#BRBACKUP#__',
147
- );
148
  $matomo_comment = isset( $matomo_row['comment'] ) ? $matomo_row['comment'] : '';
149
  $matomo_replaced = str_replace( array_keys( $matomo_replaced_elements ), array_values( $matomo_replaced_elements ), $matomo_comment );
150
  $matomo_escaped = esc_html( $matomo_replaced );
 
151
  echo "<td width='50%' class='matomo-systemreport-comment'>" . str_replace( array_values( $matomo_replaced_elements ), array_keys( $matomo_replaced_elements ), $matomo_escaped ) . '</td>';
152
  }
153
 
@@ -163,51 +167,54 @@ if ( ! function_exists( 'matomo_format_value_text' ) ) {
163
  <form method="post">
164
  <?php wp_nonce_field( SystemReport::NONCE_NAME ); ?>
165
 
166
- <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_ARCHIVE_NOW ); ?>" type="submit"
167
- class='button-primary'
168
- title="<?php esc_attr_e( 'If reports show no data even though they should, you may try to see if report generation works when manually triggering the report generation.', 'matomo' ) ?>"
169
- value="<?php esc_html_e( 'Archive reports', 'matomo' ); ?>">
170
- <br/><br/>
171
- <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_CLEAR_MATOMO_CACHE ); ?>" type="submit"
172
- class='button-primary'
173
- title="<?php esc_attr_e( 'Will reset / empty the Matomo cache which can be helpful if something is not working as expected for example after an update.', 'matomo' ) ?>"
174
- value="<?php esc_html_e( 'Clear Matomo cache', 'matomo' ); ?>">
175
- <br/><br/>
176
- <?php if (!empty($matomo_has_exception_logs)) { ?>
177
- <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_CLEAR_LOGS ); ?>" type="submit"
178
- class='button-primary'
179
- title="<?php esc_attr_e( 'Removes all stored Matomo logs that are shown in the system report', 'matomo' ) ?>"
180
- value="<?php esc_html_e( 'Clear system report logs', 'matomo' ); ?>">
181
- <br/><br/>
182
  <?php } ?>
183
 
184
- <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_UPDATE_GEOIP_DB ); ?>" type="submit"
185
- class='button-primary'
186
- title="<?php esc_attr_e( 'Updates the geolocation database which is used to detect the location (city/region/country) of visitors. This task is performed automatically. If the geolocation DB is not loaded or updated, you may need to trigger it manually to find the error which is causing it.', 'matomo' ) ?>"
187
- value="<?php esc_html_e( 'Install/Update Geo-IP DB', 'matomo' ); ?>">
188
- <br/><br/>
189
-
190
  <?php if ( ! $settings->is_network_enabled() || ! is_network_admin() ) { ?>
191
- <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_SYNC_USERS ); ?>" type="submit" class='button-primary'
192
- title="<?php esc_attr_e( 'Users are synced automatically. If for some reason a user cannot access Matomo pages even though the user has the permission, then triggering a manual sync may help to fix this issue immediately or it may show which error prevents the automatic syncing.', 'matomo' ) ?>"
193
- value="<?php esc_html_e( 'Sync users', 'matomo' ); ?>">
194
- <br/><br/>
195
- <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_SYNC_SITE ); ?>" type="submit" class='button-primary'
196
- title="<?php esc_attr_e( 'Sites / blogs are synced automatically. If for some reason Matomo is not showing up for a specific blog, then triggering a manual sync may help to fix this issue immediately or it may show which error prevents the automatic syncing.', 'matomo' ) ?>"
197
- value="<?php esc_html_e( 'Sync site (blog)', 'matomo' ); ?>">
198
- <br/><br/>
199
- <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_RUN_UPDATER ); ?>" type="submit" class='button-primary'
200
- title="<?php esc_attr_e( 'Force trigger a Matomo update in case it failed error', 'matomo' ) ?>"
201
- value="<?php esc_html_e( 'Run Updater', 'matomo' ); ?>">
 
 
 
202
  <?php } ?>
203
  <?php if ( $settings->is_network_enabled() ) { ?>
204
- <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_SYNC_ALL_USERS ); ?>" type="submit"
205
- class='button-primary'
206
- title="<?php esc_attr_e( 'Users are synced automatically. If for some reason a user cannot access Matomo pages even though the user has the permission, then triggering a manual sync may help to fix this issue immediately or it may show which error prevents the automatic syncing.', 'matomo' ) ?>"
207
- value="<?php esc_html_e( 'Sync all users across sites / blogs', 'matomo' ); ?>">
208
- <br/><br/>
209
  <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_SYNC_ALL_SITES ); ?>" type="submit"
210
- title="<?php esc_attr_e( 'Sites / blogs are synced automatically. If for some reason Matomo is not showing up for a specific blog, then triggering a manual sync may help to fix this issue immediately or it may show which error prevents the automatic syncing.', 'matomo' ) ?>"
211
  class='button-primary'
212
  value="<?php esc_html_e( 'Sync all sites (blogs)', 'matomo' ); ?>">
213
  <?php } ?>
@@ -219,11 +226,25 @@ if ( ! function_exists( 'matomo_format_value_text' ) ) {
219
  ?>
220
  <h3><?php esc_html_e( 'Popular Troubleshooting FAQs', 'matomo' ); ?></h3>
221
  <ul class="matomo-list">
222
- <li><a href="https://matomo.org/faq/wordpress/matomo-for-wordpress-is-not-showing-any-statistics-not-archiving-how-do-i-fix-it/" target="_blank" rel="noreferrer noopener"><?php esc_html_e( 'Matomo is not showing any statistics / reports, how do I fix it?', 'matomo' ); ?></a></li>
223
- <li><a href="https://matomo.org/faq/wordpress/i-cannot-open-backend-page-how-do-i-troubleshoot-it/" target="_blank" rel="noreferrer noopener"><?php esc_html_e( 'I cannot open the Matomo Reporting, Admin, or Tag Manager page, how do I troubleshoot it?', 'matomo' ); ?></a></li>
224
- <li><a href="https://matomo.org/faq/wordpress/i-have-a-problem-how-do-i-troubleshoot-and-enable-wp_debug/" target="_blank" rel="noreferrer noopener"><?php esc_html_e( 'I have an issue with the plugin, how do I troubleshoot and enable debug mode?', 'matomo' ); ?></a></li>
225
- <li><a href="https://matomo.org/faq/wordpress/how-do-i-manually-delete-all-matomo-for-wordpress-data/" target="_blank" rel="noreferrer noopener"><?php esc_html_e( 'How do I manually delete or reset all Matomo for WordPress data?', 'matomo' ); ?></a></li>
226
- <li><a href="https://matomo.org/faq/wordpress/" target="_blank" rel="noreferrer noopener"><?php esc_html_e( 'View all FAQs', 'matomo' ); ?></a></li>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  </ul>
228
  <?php include 'info_bug_report.php'; ?>
229
  <h4><?php esc_html_e( 'Before you create an issue', 'matomo' ); ?></h4>
@@ -235,15 +256,15 @@ if ( ! function_exists( 'matomo_format_value_text' ) ) {
235
  </p>
236
  <h3><?php esc_html_e( 'Having performance issues?', 'matomo' ); ?></h3>
237
  <p>
238
- <?php
239
- echo sprintf(
240
- esc_html__( 'You may want to disable %1$s in your %2$s and set up an actual cronjob and %3$scheck out our recommended server sizing%4$s.', 'matomo' ),
241
- '<code>DISABLE_WP_CRON</code>',
242
- '<code>wp-config.php</code>',
243
- '<a target="_blank" rel="noreferrer noopener" href="https://matomo.org/docs/requirements/#recommended-servers-sizing-cpu-ram-disks">',
244
- '</a>'
245
- );
246
- ?>
247
  </p>
248
  <?php include 'info_high_traffic.php'; ?>
249
  <?php } ?>
6
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7
  * @package matomo
8
  */
9
+ /**
10
+ * phpcs considers all of our variables as global and want them prefixed with matomo
11
+ * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
12
+ */
13
  if ( ! defined( 'ABSPATH' ) ) {
14
  exit;
15
  }
28
  if ( ! function_exists( 'matomo_format_value_text' ) ) {
29
  function matomo_format_value_text( $value ) {
30
  if ( is_string( $value ) && ! empty( $value ) ) {
31
+ $matomo_format = [
32
  '<br />' => ' ',
33
  '<br/>' => ' ',
34
  '<br>' => ' ',
35
+ ];
36
  foreach ( $matomo_format as $search => $replace ) {
37
  $value = str_replace( $search, $replace, $value );
38
  }
47
  <?php
48
  if ( $matomo_has_warning_and_no_errors ) {
49
  ?>
50
+ <div class="notice notice-warning">
51
+ <p><?php esc_html_e( 'There are some issues with your system. Matomo will run, but you might experience some minor problems. See below for more information.', 'matomo' ); ?></p>
52
+ </div>
53
  <?php
54
  }
55
  ?>
59
  </div>
60
  <?php } ?>
61
  <div id="icon-plugins" class="icon32"></div>
62
+ <h1><?php matomo_header_icon(); ?><?php esc_html_e( 'Diagnostics', 'matomo' ); ?></h1>
63
 
64
  <h2 class="nav-tab-wrapper">
65
+ <a href="?page=<?php echo esc_attr( Menu::SLUG_SYSTEM_REPORT ); ?>"
66
  class="nav-tab <?php echo empty( $matomo_active_tab ) ? 'nav-tab-active' : ''; ?>"> System report</a>
67
+ <a href="?page=<?php echo esc_attr( Menu::SLUG_SYSTEM_REPORT ); ?>&tab=troubleshooting"
68
  class="nav-tab <?php echo 'troubleshooting' === $matomo_active_tab ? 'nav-tab-active' : ''; ?>">Troubleshooting</a>
69
  </h2>
70
 
81
  id="matomo_system_report_info">
82
  <?php
83
  foreach ( $matomo_tables as $matomo_table ) {
84
+ if ( empty( $matomo_table['rows'] ) ) {
85
  continue;
86
  }
87
  echo '# ' . esc_html( $matomo_table['title'] ) . "\n";
114
 
115
  <?php
116
  foreach ( $matomo_tables as $matomo_table ) {
117
+ if ( empty( $matomo_table['rows'] ) ) {
118
+ continue;
119
+ }
120
  echo '<h2>' . esc_html( $matomo_table['title'] ) . "</h2><table class='widefat'><thead></thead><tbody>";
121
  foreach ( $matomo_table['rows'] as $matomo_row ) {
122
  if ( ! empty( $matomo_row['section'] ) ) {
139
  echo "<td width='30%'>" . esc_html( $matomo_row['name'] ) . '</td>';
140
  echo "<td width='" . ( ! empty( $matomo_table['has_comments'] ) ? 20 : 70 ) . "%'>" . esc_html( $matomo_value ) . '</td>';
141
  if ( ! empty( $matomo_table['has_comments'] ) ) {
142
+ $matomo_replaced_elements = [
143
  '<code>' => '__#CODEBACKUP#__',
144
  '</code>' => '__##CODEBACKUP##__',
145
  '<pre style="overflow-x: scroll;max-width: 600px;">' => '__#PREBACKUP#__',
147
  '<br/>' => '__#BRBACKUP#__',
148
  '<br />' => '__#BRBACKUP#__',
149
  '<br>' => '__#BRBACKUP#__',
150
+ ];
151
  $matomo_comment = isset( $matomo_row['comment'] ) ? $matomo_row['comment'] : '';
152
  $matomo_replaced = str_replace( array_keys( $matomo_replaced_elements ), array_values( $matomo_replaced_elements ), $matomo_comment );
153
  $matomo_escaped = esc_html( $matomo_replaced );
154
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
155
  echo "<td width='50%' class='matomo-systemreport-comment'>" . str_replace( array_values( $matomo_replaced_elements ), array_keys( $matomo_replaced_elements ), $matomo_escaped ) . '</td>';
156
  }
157
 
167
  <form method="post">
168
  <?php wp_nonce_field( SystemReport::NONCE_NAME ); ?>
169
 
170
+ <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_ARCHIVE_NOW ); ?>" type="submit"
171
+ class='button-primary'
172
+ title="<?php esc_attr_e( 'If reports show no data even though they should, you may try to see if report generation works when manually triggering the report generation.', 'matomo' ); ?>"
173
+ value="<?php esc_html_e( 'Archive reports', 'matomo' ); ?>">
174
+ <br/><br/>
175
+ <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_CLEAR_MATOMO_CACHE ); ?>" type="submit"
176
+ class='button-primary'
177
+ title="<?php esc_attr_e( 'Will reset / empty the Matomo cache which can be helpful if something is not working as expected for example after an update.', 'matomo' ); ?>"
178
+ value="<?php esc_html_e( 'Clear Matomo cache', 'matomo' ); ?>">
179
+ <br/><br/>
180
+ <?php if ( ! empty( $matomo_has_exception_logs ) ) { ?>
181
+ <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_CLEAR_LOGS ); ?>" type="submit"
182
+ class='button-primary'
183
+ title="<?php esc_attr_e( 'Removes all stored Matomo logs that are shown in the system report', 'matomo' ); ?>"
184
+ value="<?php esc_html_e( 'Clear system report logs', 'matomo' ); ?>">
185
+ <br/><br/>
186
  <?php } ?>
187
 
188
+ <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_UPDATE_GEOIP_DB ); ?>" type="submit"
189
+ class='button-primary'
190
+ title="<?php esc_attr_e( 'Updates the geolocation database which is used to detect the location (city/region/country) of visitors. This task is performed automatically. If the geolocation DB is not loaded or updated, you may need to trigger it manually to find the error which is causing it.', 'matomo' ); ?>"
191
+ value="<?php esc_html_e( 'Install/Update Geo-IP DB', 'matomo' ); ?>">
192
+ <br/><br/>
193
+
194
  <?php if ( ! $settings->is_network_enabled() || ! is_network_admin() ) { ?>
195
+ <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_SYNC_USERS ); ?>" type="submit"
196
+ class='button-primary'
197
+ title="<?php esc_attr_e( 'Users are synced automatically. If for some reason a user cannot access Matomo pages even though the user has the permission, then triggering a manual sync may help to fix this issue immediately or it may show which error prevents the automatic syncing.', 'matomo' ); ?>"
198
+ value="<?php esc_html_e( 'Sync users', 'matomo' ); ?>">
199
+ <br/><br/>
200
+ <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_SYNC_SITE ); ?>" type="submit"
201
+ class='button-primary'
202
+ title="<?php esc_attr_e( 'Sites / blogs are synced automatically. If for some reason Matomo is not showing up for a specific blog, then triggering a manual sync may help to fix this issue immediately or it may show which error prevents the automatic syncing.', 'matomo' ); ?>"
203
+ value="<?php esc_html_e( 'Sync site (blog)', 'matomo' ); ?>">
204
+ <br/><br/>
205
+ <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_RUN_UPDATER ); ?>" type="submit"
206
+ class='button-primary'
207
+ title="<?php esc_attr_e( 'Force trigger a Matomo update in case it failed error', 'matomo' ); ?>"
208
+ value="<?php esc_html_e( 'Run Updater', 'matomo' ); ?>">
209
  <?php } ?>
210
  <?php if ( $settings->is_network_enabled() ) { ?>
211
+ <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_SYNC_ALL_USERS ); ?>" type="submit"
212
+ class='button-primary'
213
+ title="<?php esc_attr_e( 'Users are synced automatically. If for some reason a user cannot access Matomo pages even though the user has the permission, then triggering a manual sync may help to fix this issue immediately or it may show which error prevents the automatic syncing.', 'matomo' ); ?>"
214
+ value="<?php esc_html_e( 'Sync all users across sites / blogs', 'matomo' ); ?>">
215
+ <br/><br/>
216
  <input name="<?php echo esc_attr( SystemReport::TROUBLESHOOT_SYNC_ALL_SITES ); ?>" type="submit"
217
+ title="<?php esc_attr_e( 'Sites / blogs are synced automatically. If for some reason Matomo is not showing up for a specific blog, then triggering a manual sync may help to fix this issue immediately or it may show which error prevents the automatic syncing.', 'matomo' ); ?>"
218
  class='button-primary'
219
  value="<?php esc_html_e( 'Sync all sites (blogs)', 'matomo' ); ?>">
220
  <?php } ?>
226
  ?>
227
  <h3><?php esc_html_e( 'Popular Troubleshooting FAQs', 'matomo' ); ?></h3>
228
  <ul class="matomo-list">
229
+ <li>
230
+ <a href="https://matomo.org/faq/wordpress/matomo-for-wordpress-is-not-showing-any-statistics-not-archiving-how-do-i-fix-it/"
231
+ target="_blank"
232
+ rel="noreferrer noopener"><?php esc_html_e( 'Matomo is not showing any statistics / reports, how do I fix it?', 'matomo' ); ?></a>
233
+ </li>
234
+ <li><a href="https://matomo.org/faq/wordpress/i-cannot-open-backend-page-how-do-i-troubleshoot-it/"
235
+ target="_blank"
236
+ rel="noreferrer noopener"><?php esc_html_e( 'I cannot open the Matomo Reporting, Admin, or Tag Manager page, how do I troubleshoot it?', 'matomo' ); ?></a>
237
+ </li>
238
+ <li><a href="https://matomo.org/faq/wordpress/i-have-a-problem-how-do-i-troubleshoot-and-enable-wp_debug/"
239
+ target="_blank"
240
+ rel="noreferrer noopener"><?php esc_html_e( 'I have an issue with the plugin, how do I troubleshoot and enable debug mode?', 'matomo' ); ?></a>
241
+ </li>
242
+ <li><a href="https://matomo.org/faq/wordpress/how-do-i-manually-delete-all-matomo-for-wordpress-data/"
243
+ target="_blank"
244
+ rel="noreferrer noopener"><?php esc_html_e( 'How do I manually delete or reset all Matomo for WordPress data?', 'matomo' ); ?></a>
245
+ </li>
246
+ <li><a href="https://matomo.org/faq/wordpress/" target="_blank"
247
+ rel="noreferrer noopener"><?php esc_html_e( 'View all FAQs', 'matomo' ); ?></a></li>
248
  </ul>
249
  <?php include 'info_bug_report.php'; ?>
250
  <h4><?php esc_html_e( 'Before you create an issue', 'matomo' ); ?></h4>
256
  </p>
257
  <h3><?php esc_html_e( 'Having performance issues?', 'matomo' ); ?></h3>
258
  <p>
259
+ <?php
260
+ echo sprintf(
261
+ esc_html__( 'You may want to disable %1$s in your %2$s and set up an actual cronjob and %3$scheck out our recommended server sizing%4$s.', 'matomo' ),
262
+ '<code>DISABLE_WP_CRON</code>',
263
+ '<code>wp-config.php</code>',
264
+ '<a target="_blank" rel="noreferrer noopener" href="https://matomo.org/docs/requirements/#recommended-servers-sizing-cpu-ram-disks">',
265
+ '</a>'
266
+ );
267
+ ?>
268
  </p>
269
  <?php include 'info_high_traffic.php'; ?>
270
  <?php } ?>
classes/WpMatomo/Admin/views/tracking.php CHANGED
@@ -10,7 +10,9 @@
10
  * https://github.com/braekling/WP-Matomo
11
  *
12
  */
13
-
 
 
14
  use WpMatomo\Admin\TrackingSettings;
15
  use WpMatomo\Paths;
16
 
@@ -23,7 +25,7 @@ if ( ! defined( 'ABSPATH' ) ) {
23
  /** @var array $containers */
24
  /** @var array $track_modes */
25
  /** @var array $matomo_currencies */
26
- /** @var string[] $errors */
27
  /** @var array $cookie_consent_modes */
28
 
29
  $matomo_form = new \WpMatomo\Admin\TrackingSettings\Forms( $settings );
@@ -34,18 +36,18 @@ $matomo_paths = new Paths();
34
  if ( $was_updated ) {
35
  include 'update_notice_clear_cache.php';
36
  }
37
- if ( count( $errors ) ) {
38
- include 'settings_errors.php';
39
  }
40
 
41
  ?>
42
  <form method="post">
43
  <?php wp_nonce_field( TrackingSettings::NONCE_NAME ); ?>
44
  <p>
45
- <?php esc_html_e( 'Here you can optionally configure the tracking to your liking if you want (you don\'t have to configure it).', 'matomo' );?>
46
- <?php esc_html_e( 'The configured tracking code will be embedded into your website automatically and you won\'t need to do anything unless you disabled the tracking.', 'matomo' );?>
47
- <?php esc_html_e( 'If you are seeing a tracking code below, you don\'t have to embed this tracking code into your site. The plugin does this automatically for you.', 'matomo' );?>
48
- </p>
49
  <table class="matomo-tracking-form widefat">
50
  <tbody>
51
 
@@ -56,22 +58,22 @@ if ( count( $errors ) ) {
56
  $matomo_is_not_generated_tracking = $matomo_is_not_tracking || $settings->get_global_option( 'track_mode' ) === TrackingSettings::TRACK_MODE_MANUALLY;
57
  $matomo_full_generated_tracking_group = 'matomo-track-option matomo-track-option-default ';
58
 
59
- $matomo_description = sprintf( '%s<br /><strong>%s:</strong> %s<br /><strong>%s:</strong> %s<br /><strong>%s:</strong> %s<br /><strong>%s:</strong> %s', esc_html__( 'You can choose between four tracking code modes:', 'matomo' ), esc_html__( 'Disabled', 'matomo' ), esc_html__( 'matomo will not add the tracking code. Use this, if you want to add the tracking code to your template files or you use another plugin to add the tracking code.', 'matomo' ), esc_html__( 'Default tracking', 'matomo' ), esc_html__( 'matomo will use Matomo\'s standard tracking code.', 'matomo' ) . ' ' .esc_html__('This mode is recommended for most use cases.', 'matomo'), esc_html__( 'Enter manually', 'matomo' ), esc_html__( 'Enter your own tracking code manually. You can choose one of the prior options, pre-configure your tracking code and switch to manually editing at last.', 'matomo' ) . ( $settings->is_network_enabled() ? ' ' . esc_html__( 'Use the placeholder {ID} to add the Matomo site ID.', 'matomo' ) : '' ), esc_html__( 'Tag Manager', 'matomo' ), esc_html__( 'If you have created containers in the Tag Manager, you can select one of them and it will embed the code for the container automatically.', 'matomo' ) );
60
  $matomo_form->show_select( 'track_mode', esc_html__( 'Add tracking code', 'matomo' ), $track_modes, $matomo_description, 'jQuery(\'tr.matomo-track-option\').addClass(\'hidden\'); jQuery(\'tr.matomo-track-option-\' + jQuery(\'#track_mode\').val()).removeClass(\'hidden\'); jQuery(\'#tracking_code, #noscript_code\').prop(\'readonly\', jQuery(\'#track_mode\').val() != \'manually\');' );
61
 
62
- $matomo_manually_network = '';
63
- if ( $settings->is_network_enabled() ) {
64
- $matomo_manually_network = ' ' . sprintf( esc_html__( 'You can use these variables: %1$s. %2$sLearn more%3$s', 'matomo' ), '{MATOMO_IDSITE}, {MATOMO_API_ENDPOINT}, {MATOMO_JS_ENDPOINT}', '<a href="https://matomo.org/faq/wordpress/how-can-i-configure-the-tracking-code-manually-when-i-have-wordpress-network-enabled-in-multisite-mode/" target="_blank" rel="noreferrer noopener">', '</a>' );
65
- }
66
 
67
  if ( ! empty( $containers ) ) {
68
  echo '<tr class="matomo-track-option matomo-track-option-tagmanager ' . ( $matomo_is_not_tracking ? ' hidden' : '' ) . '">';
69
  echo '<th scope="row"><label for="tagmanger_container_ids">' . esc_html__( 'Add these Tag Manager containers', 'matomo' ) . '</label>:</th><td>';
70
  $selected_container_ids = $settings->get_global_option( 'tagmanger_container_ids' );
71
  foreach ( $containers as $container_id => $container_name ) {
72
- echo '<input type="checkbox" ' . ( isset( $selected_container_ids [ $container_id ] ) && $selected_container_ids [ $container_id ] ? 'checked="checked" ' : '' ) . 'value="1" name="matomo[tagmanger_container_ids][' . $container_id . ']" /> ID:' . esc_html( $container_id ) . ' Name: ' . esc_html( $container_name ) . ' &nbsp; <br />';
73
  }
74
- echo '<br /><br /><a href="' . menu_page_url( \WpMatomo\Admin\Menu::SLUG_TAGMANAGER, false ) . '" rel="noreferrer noopener" target="_blank">Edit containers <span class="dashicons-before dashicons-external"></span></a>';
75
  echo '<br /><span class="dashicons dashicons-info-outline"></span> For Matomo to track you will need to add a Matomo Tag to the container. It otherwise won\'t track automatically.';
76
  echo '</td></tr>';
77
  }
@@ -79,9 +81,9 @@ if ( count( $errors ) ) {
79
  $matomo_form->show_textarea( 'tracking_code', esc_html__( 'Tracking code', 'matomo' ), 15, 'This is a preview of your current tracking code based on your configuration below. You don\'t need to do anything with it and this is purely for your information. If you choose to enter your tracking code manually, you can change it here. The tracking code is a piece of code that will be automatically embedded into your site and it is repsonsible for tracking your visitors. Have a look at the system report to get a list of all available JS tracker and tracking API endpoints. You don\'t need to embed this tracking code into your website, our plugin does this automatically.' . $matomo_manually_network, $matomo_is_not_tracking, 'matomo-track-option matomo-track-option-default matomo-track-option-tagmanager matomo-track-option-manually', ! $settings->is_network_enabled(), '', ( $settings->get_global_option( 'track_mode' ) !== 'manually' ), false );
80
 
81
 
82
- $matomo_form->show_select( \WpMatomo\Settings::SITE_CURRENCY, esc_html__( 'Currency', 'matomo' ), $matomo_currencies, esc_html__('Choose the currency which will be used in reports. The currency will be used if you have an ecommerce store or if you are using the Matomo goals feature and assign a monetary value to a goal.', 'matomo'), '' );
83
 
84
- $matomo_form->show_headline(esc_html__('Customise tracking (optional)', 'matomo'), 'matomo-track-option matomo-track-option-default matomo-track-option-manually matomo-track-option-tagmanager');
85
 
86
  $matomo_form->show_checkbox( 'disable_cookies', esc_html__( 'Disable cookies', 'matomo' ), esc_html__( 'Disable all tracking cookies for a visitor.', 'matomo' ), $matomo_is_not_generated_tracking, $matomo_full_generated_tracking_group );
87
 
@@ -93,22 +95,22 @@ if ( count( $errors ) ) {
93
 
94
  $matomo_form->show_checkbox( 'track_jserrors', esc_html__( 'Track JS errors', 'matomo' ), esc_html__( 'Enable to track JavaScript errors that occur on your website as Matomo events.', 'matomo' ) . ' ' . sprintf( esc_html__( 'See %1$sMatomo FAQ%2$s.', 'matomo' ), '<a href="https://matomo.org/faq/how-to/how-do-i-enable-basic-javascript-error-tracking-and-reporting-in-matomo-browser-console-error-messages/" rel="noreferrer noopener" target="_BLANK">', '</a>' ), $matomo_is_not_tracking, $matomo_full_generated_tracking_group );
95
 
96
- echo '<tr class="' . $matomo_full_generated_tracking_group . ' matomo-track-option-manually' . ( $matomo_is_not_tracking ? ' hidden' : '' ) . '">';
97
  echo '<th scope="row"><label for="add_post_annotations">' . esc_html__( 'Add annotation on new post of type', 'matomo' ) . '</label>:</th><td>';
98
  $matomo_filter = $settings->get_global_option( 'add_post_annotations' );
99
- foreach ( get_post_types( array(), 'objects' ) as $post_type ) {
100
- echo '<input type="checkbox" ' . ( isset( $matomo_filter [ $post_type->name ] ) && $matomo_filter [ $post_type->name ] ? 'checked="checked" ' : '' ) . 'value="1" name="matomo[add_post_annotations][' . $post_type->name . ']" /> ' . $post_type->label . ' &nbsp; ';
101
  }
102
  echo '<span class="dashicons dashicons-editor-help" style="cursor: pointer;" onclick="jQuery(\'#add_post_annotations-desc\').toggleClass(\'hidden\');"></span> <p class="description hidden" id="add_post_annotations-desc">' . sprintf( esc_html__( 'See %1$sMatomo documentation%2$s.', 'matomo' ), '<a href="https://matomo.org/docs/annotations/" rel="noreferrer noopener" target="_BLANK">', '</a>' ) . '</p></td></tr>';
103
 
104
  $matomo_form->show_select(
105
  'track_content',
106
  __( 'Enable content tracking', 'matomo' ),
107
- array(
108
  'disabled' => esc_html__( 'Disabled', 'matomo' ),
109
  'all' => esc_html__( 'Track all content blocks', 'matomo' ),
110
  'visible' => esc_html__( 'Track only visible content blocks', 'matomo' ),
111
- ),
112
  __( 'Content tracking allows you to track interaction with the content of a web page or application.', 'matomo' ) . ' ' . sprintf( esc_html__( 'See %1$sMatomo documentation%2$s.', 'matomo' ), '<a href="https://developer.matomo.org/guides/content-tracking" rel="noreferrer noopener" target="_BLANK">', '</a>' ),
113
  '',
114
  $matomo_is_not_tracking,
@@ -146,13 +148,13 @@ if ( count( $errors ) ) {
146
  $matomo_form->show_select(
147
  'track_user_id',
148
  __( 'User ID Tracking', 'matomo' ),
149
- array(
150
  'disabled' => esc_html__( 'Disabled', 'matomo' ),
151
  'uid' => esc_html__( 'WP User ID', 'matomo' ),
152
  'email' => esc_html__( 'Email Address', 'matomo' ),
153
  'username' => esc_html__( 'Username', 'matomo' ),
154
  'displayname' => esc_html__( 'Display Name (Not Recommended!)', 'matomo' ),
155
- ),
156
  __( 'When a user is logged in to WordPress, track their &quot;User ID&quot;. You can select which field from the User\'s profile is tracked as the &quot;User ID&quot;. When enabled, Tracking based on Email Address is recommended.', 'matomo' ),
157
  '',
158
  $matomo_is_not_tracking,
@@ -176,10 +178,10 @@ if ( count( $errors ) ) {
176
  $matomo_form->show_select(
177
  'force_protocol',
178
  __( 'Force Matomo to use a specific protocol', 'matomo' ),
179
- array(
180
  'disabled' => esc_html__( 'Disabled (default)', 'matomo' ),
181
  'https' => esc_html__( 'https (SSL)', 'matomo' ),
182
- ),
183
  __( 'Choose if you want to explicitly want to force Matomo to use HTTP or HTTPS. Does not work with a CDN URL.', 'matomo' ),
184
  '',
185
  $matomo_is_not_tracking,
@@ -188,10 +190,10 @@ if ( count( $errors ) ) {
188
  $matomo_form->show_select(
189
  'track_codeposition',
190
  __( 'JavaScript code position', 'matomo' ),
191
- array(
192
  'footer' => esc_html__( 'Footer', 'matomo' ),
193
  'header' => esc_html__( 'Header', 'matomo' ),
194
- ),
195
  __( 'Choose whether the JavaScript code is added to the footer or the header.', 'matomo' ),
196
  '',
197
  $matomo_is_not_tracking,
@@ -200,11 +202,11 @@ if ( count( $errors ) ) {
200
  $matomo_form->show_select(
201
  'track_api_endpoint',
202
  __( 'Endpoint for HTTP Tracking API', 'matomo' ),
203
- array(
204
  'default' => esc_html__( 'Default', 'matomo' ),
205
  'restapi' => esc_html__( 'Through WordPress Rest API', 'matomo' ),
206
- ),
207
- __( 'By default the HTTP Tracking API points to your Matomo plugin directory "' . esc_html( $matomo_paths->get_tracker_api_url_in_matomo_dir() ) . '". You can choose to use the WP Rest API (' . esc_html( $matomo_paths->get_tracker_api_rest_api_endpoint() ) . ') instead for example to hide matomo.php or if the other URL doesn\'t work for you. Note: If the tracking mode "Tag Manager" is selected, then this URL currently only applies to the feed tracking.', 'matomo' ),
208
  '',
209
  $matomo_is_not_tracking,
210
  $matomo_full_generated_tracking_group . ' matomo-track-option-manually matomo-track-option-tagmanager'
@@ -213,35 +215,35 @@ if ( count( $errors ) ) {
213
  $matomo_form->show_select(
214
  'track_js_endpoint',
215
  __( 'Endpoint for JavaScript tracker', 'matomo' ),
216
- array(
217
  'default' => esc_html__( 'Default', 'matomo' ),
218
  'restapi' => esc_html__( 'Through WordPress Rest API (slower)', 'matomo' ),
219
- 'plugin' => esc_html__( 'Plugin (an alternative JS file if the default is blocked by the webserver)', 'matomo' ),
220
- ),
221
- __( 'By default the JS tracking code will be loaded from "' . esc_html( $matomo_paths->get_js_tracker_url_in_matomo_dir() ) . '". You can choose to serve the JS file through the WP Rest API (' . esc_html( $matomo_paths->get_js_tracker_rest_api_endpoint() ) . ') for example to hide matomo.js. Please note that this means every request to the JavaScript file will launch WordPress PHP and therefore will be slower compared to your webserver serving the JS file directly. Using the "Plugin" method will cause issues with our paid Heatmap and Session Recording, Form Analytics, and Media Analyics plugin.', 'matomo' ),
222
  '',
223
  $matomo_is_not_tracking,
224
  $matomo_full_generated_tracking_group
225
  );
226
 
227
- $matomo_form->show_select( 'cookie_consent', esc_html__( 'Custom consent screen', 'matomo' ), $cookie_consent_modes, sprintf(esc_html__( 'Activates a specific Matomo consent mode. Only configure a consent mode if you are implementing a consent screen yourself. This requires a custom consent implementation. For more information please read this %1$sFAQ%2$s (this option will take care of step 1 for you). By default no consent mode is applied.', 'matomo' ), '<a href="https://developer.matomo.org/guides/tracking-consent" rel="noreferrer noopener" target="_blank">', '</a>'), '', $matomo_is_not_generated_tracking, $matomo_full_generated_tracking_group );
228
 
229
- $matomo_form->show_headline(esc_html__('For Developers', 'matomo'), 'matomo-track-option matomo-track-option-default matomo-track-option-disabled matomo-track-option-manually matomo-track-option-tagmanager');
230
 
231
  $matomo_form->show_select(
232
  'tracker_debug',
233
  __( 'Tracker Debug Mode', 'matomo' ),
234
- array(
235
- 'disabled' => esc_html__( 'Disabled (recommended)', 'matomo' ),
236
  'always' => esc_html__( 'Always enabled', 'matomo' ),
237
- 'on_demand' => esc_html__( 'Enabled on demand', 'matomo' ),
238
- ),
239
  __( 'For security and privacy reasons you should only enable this setting for as short time of a time as possible.', 'matomo' ),
240
  '',
241
  $matomo_is_not_tracking,
242
  $matomo_full_generated_tracking_group . ' matomo-track-option-disabled matomo-track-option-manually matomo-track-option-tagmanager'
243
  );
244
-
245
  echo $matomo_submit_button;
246
  ?>
247
 
@@ -250,13 +252,13 @@ if ( count( $errors ) ) {
250
  </form>
251
 
252
  <?php if ( $matomo_is_not_tracking && ! $settings->is_network_enabled() ) { // Can't show it for multisite as idsite and url is always different. ?>
253
- <div id="matomo_default_tracking_code">
254
- <h2><?php esc_html_e( 'JavaScript tracking code', 'matomo' ); ?></h2>
255
- <p>
256
- <?php echo sprintf( esc_html__( 'Wanting to embed the tracking code manually into your site or using a different plugin? No problem! Simply copy/paste below tracking code. Want to adjust it? %1$sCheck out our developer documentation.%2$s', 'matomo' ), '<a href="https://developer.matomo.org/guides/tracking-javascript-guide" target="_blank" rel="noreferrer noopener">', '</a>' ); ?>
257
- </p>
258
- <?php echo '<pre><textarea>' . esc_html( implode( ";\n", explode( ';', $matomo_default_tracking_code['script'] ) ) ) . '</textarea></pre>'; ?>
259
- <h3><?php esc_html_e( 'NoScript tracking code', 'matomo' ); ?></h3>
260
- <?php echo '<pre><textarea class="no_script">' . esc_html( $matomo_default_tracking_code['noscript'] ) . '</textarea></pre>'; ?>
261
- </div>
262
  <?php } ?>
10
  * https://github.com/braekling/WP-Matomo
11
  *
12
  */
13
+ /**
14
+ * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
15
+ */
16
  use WpMatomo\Admin\TrackingSettings;
17
  use WpMatomo\Paths;
18
 
25
  /** @var array $containers */
26
  /** @var array $track_modes */
27
  /** @var array $matomo_currencies */
28
+ /** @var string[] $settings_errors */
29
  /** @var array $cookie_consent_modes */
30
 
31
  $matomo_form = new \WpMatomo\Admin\TrackingSettings\Forms( $settings );
36
  if ( $was_updated ) {
37
  include 'update_notice_clear_cache.php';
38
  }
39
+ if ( count( $settings_errors ) ) {
40
+ include 'settings_errors.php';
41
  }
42
 
43
  ?>
44
  <form method="post">
45
  <?php wp_nonce_field( TrackingSettings::NONCE_NAME ); ?>
46
  <p>
47
+ <?php esc_html_e( 'Here you can optionally configure the tracking to your liking if you want (you don\'t have to configure it).', 'matomo' ); ?>
48
+ <?php esc_html_e( 'The configured tracking code will be embedded into your website automatically and you won\'t need to do anything unless you disabled the tracking.', 'matomo' ); ?>
49
+ <?php esc_html_e( 'If you are seeing a tracking code below, you don\'t have to embed this tracking code into your site. The plugin does this automatically for you.', 'matomo' ); ?>
50
+ </p>
51
  <table class="matomo-tracking-form widefat">
52
  <tbody>
53
 
58
  $matomo_is_not_generated_tracking = $matomo_is_not_tracking || $settings->get_global_option( 'track_mode' ) === TrackingSettings::TRACK_MODE_MANUALLY;
59
  $matomo_full_generated_tracking_group = 'matomo-track-option matomo-track-option-default ';
60
 
61
+ $matomo_description = sprintf( '%s<br /><strong>%s:</strong> %s<br /><strong>%s:</strong> %s<br /><strong>%s:</strong> %s<br /><strong>%s:</strong> %s', esc_html__( 'You can choose between four tracking code modes:', 'matomo' ), esc_html__( 'Disabled', 'matomo' ), esc_html__( 'matomo will not add the tracking code. Use this, if you want to add the tracking code to your template files or you use another plugin to add the tracking code.', 'matomo' ), esc_html__( 'Default tracking', 'matomo' ), esc_html__( 'matomo will use Matomo\'s standard tracking code.', 'matomo' ) . ' ' . esc_html__( 'This mode is recommended for most use cases.', 'matomo' ), esc_html__( 'Enter manually', 'matomo' ), esc_html__( 'Enter your own tracking code manually. You can choose one of the prior options, pre-configure your tracking code and switch to manually editing at last.', 'matomo' ) . ( $settings->is_network_enabled() ? ' ' . esc_html__( 'Use the placeholder {ID} to add the Matomo site ID.', 'matomo' ) : '' ), esc_html__( 'Tag Manager', 'matomo' ), esc_html__( 'If you have created containers in the Tag Manager, you can select one of them and it will embed the code for the container automatically.', 'matomo' ) );
62
  $matomo_form->show_select( 'track_mode', esc_html__( 'Add tracking code', 'matomo' ), $track_modes, $matomo_description, 'jQuery(\'tr.matomo-track-option\').addClass(\'hidden\'); jQuery(\'tr.matomo-track-option-\' + jQuery(\'#track_mode\').val()).removeClass(\'hidden\'); jQuery(\'#tracking_code, #noscript_code\').prop(\'readonly\', jQuery(\'#track_mode\').val() != \'manually\');' );
63
 
64
+ $matomo_manually_network = '';
65
+ if ( $settings->is_network_enabled() ) {
66
+ $matomo_manually_network = ' ' . sprintf( esc_html__( 'You can use these variables: %1$s. %2$sLearn more%3$s', 'matomo' ), '{MATOMO_IDSITE}, {MATOMO_API_ENDPOINT}, {MATOMO_JS_ENDPOINT}', '<a href="https://matomo.org/faq/wordpress/how-can-i-configure-the-tracking-code-manually-when-i-have-wordpress-network-enabled-in-multisite-mode/" target="_blank" rel="noreferrer noopener">', '</a>' );
67
+ }
68
 
69
  if ( ! empty( $containers ) ) {
70
  echo '<tr class="matomo-track-option matomo-track-option-tagmanager ' . ( $matomo_is_not_tracking ? ' hidden' : '' ) . '">';
71
  echo '<th scope="row"><label for="tagmanger_container_ids">' . esc_html__( 'Add these Tag Manager containers', 'matomo' ) . '</label>:</th><td>';
72
  $selected_container_ids = $settings->get_global_option( 'tagmanger_container_ids' );
73
  foreach ( $containers as $container_id => $container_name ) {
74
+ echo '<input type="checkbox" ' . ( isset( $selected_container_ids [ $container_id ] ) && $selected_container_ids [ $container_id ] ? 'checked="checked" ' : '' ) . 'value="1" name="matomo[tagmanger_container_ids][' . esc_attr( $container_id ) . ']" /> ID:' . esc_html( $container_id ) . ' Name: ' . esc_html( $container_name ) . ' &nbsp; <br />';
75
  }
76
+ echo '<br /><br /><a href="' . esc_url( menu_page_url( \WpMatomo\Admin\Menu::SLUG_TAGMANAGER, false ) ) . '" rel="noreferrer noopener" target="_blank">Edit containers <span class="dashicons-before dashicons-external"></span></a>';
77
  echo '<br /><span class="dashicons dashicons-info-outline"></span> For Matomo to track you will need to add a Matomo Tag to the container. It otherwise won\'t track automatically.';
78
  echo '</td></tr>';
79
  }
81
  $matomo_form->show_textarea( 'tracking_code', esc_html__( 'Tracking code', 'matomo' ), 15, 'This is a preview of your current tracking code based on your configuration below. You don\'t need to do anything with it and this is purely for your information. If you choose to enter your tracking code manually, you can change it here. The tracking code is a piece of code that will be automatically embedded into your site and it is repsonsible for tracking your visitors. Have a look at the system report to get a list of all available JS tracker and tracking API endpoints. You don\'t need to embed this tracking code into your website, our plugin does this automatically.' . $matomo_manually_network, $matomo_is_not_tracking, 'matomo-track-option matomo-track-option-default matomo-track-option-tagmanager matomo-track-option-manually', ! $settings->is_network_enabled(), '', ( $settings->get_global_option( 'track_mode' ) !== 'manually' ), false );
82
 
83
 
84
+ $matomo_form->show_select( \WpMatomo\Settings::SITE_CURRENCY, esc_html__( 'Currency', 'matomo' ), $matomo_currencies, esc_html__( 'Choose the currency which will be used in reports. The currency will be used if you have an ecommerce store or if you are using the Matomo goals feature and assign a monetary value to a goal.', 'matomo' ), '' );
85
 
86
+ $matomo_form->show_headline( esc_html__( 'Customise tracking (optional)', 'matomo' ), 'matomo-track-option matomo-track-option-default matomo-track-option-manually matomo-track-option-tagmanager' );
87
 
88
  $matomo_form->show_checkbox( 'disable_cookies', esc_html__( 'Disable cookies', 'matomo' ), esc_html__( 'Disable all tracking cookies for a visitor.', 'matomo' ), $matomo_is_not_generated_tracking, $matomo_full_generated_tracking_group );
89
 
95
 
96
  $matomo_form->show_checkbox( 'track_jserrors', esc_html__( 'Track JS errors', 'matomo' ), esc_html__( 'Enable to track JavaScript errors that occur on your website as Matomo events.', 'matomo' ) . ' ' . sprintf( esc_html__( 'See %1$sMatomo FAQ%2$s.', 'matomo' ), '<a href="https://matomo.org/faq/how-to/how-do-i-enable-basic-javascript-error-tracking-and-reporting-in-matomo-browser-console-error-messages/" rel="noreferrer noopener" target="_BLANK">', '</a>' ), $matomo_is_not_tracking, $matomo_full_generated_tracking_group );
97
 
98
+ echo '<tr class="' . esc_attr( $matomo_full_generated_tracking_group ) . ' matomo-track-option-manually' . ( $matomo_is_not_tracking ? ' hidden' : '' ) . '">';
99
  echo '<th scope="row"><label for="add_post_annotations">' . esc_html__( 'Add annotation on new post of type', 'matomo' ) . '</label>:</th><td>';
100
  $matomo_filter = $settings->get_global_option( 'add_post_annotations' );
101
+ foreach ( get_post_types( [], 'objects' ) as $object_post_type ) {
102
+ echo '<input type="checkbox" ' . ( isset( $matomo_filter [ $object_post_type->name ] ) && $matomo_filter [ $object_post_type->name ] ? 'checked="checked" ' : '' ) . 'value="1" name="matomo[add_post_annotations][' . esc_attr( $object_post_type->name ) . ']" /> ' . esc_html( $object_post_type->label ) . ' &nbsp; ';
103
  }
104
  echo '<span class="dashicons dashicons-editor-help" style="cursor: pointer;" onclick="jQuery(\'#add_post_annotations-desc\').toggleClass(\'hidden\');"></span> <p class="description hidden" id="add_post_annotations-desc">' . sprintf( esc_html__( 'See %1$sMatomo documentation%2$s.', 'matomo' ), '<a href="https://matomo.org/docs/annotations/" rel="noreferrer noopener" target="_BLANK">', '</a>' ) . '</p></td></tr>';
105
 
106
  $matomo_form->show_select(
107
  'track_content',
108
  __( 'Enable content tracking', 'matomo' ),
109
+ [
110
  'disabled' => esc_html__( 'Disabled', 'matomo' ),
111
  'all' => esc_html__( 'Track all content blocks', 'matomo' ),
112
  'visible' => esc_html__( 'Track only visible content blocks', 'matomo' ),
113
+ ],
114
  __( 'Content tracking allows you to track interaction with the content of a web page or application.', 'matomo' ) . ' ' . sprintf( esc_html__( 'See %1$sMatomo documentation%2$s.', 'matomo' ), '<a href="https://developer.matomo.org/guides/content-tracking" rel="noreferrer noopener" target="_BLANK">', '</a>' ),
115
  '',
116
  $matomo_is_not_tracking,
148
  $matomo_form->show_select(
149
  'track_user_id',
150
  __( 'User ID Tracking', 'matomo' ),
151
+ [
152
  'disabled' => esc_html__( 'Disabled', 'matomo' ),
153
  'uid' => esc_html__( 'WP User ID', 'matomo' ),
154
  'email' => esc_html__( 'Email Address', 'matomo' ),
155
  'username' => esc_html__( 'Username', 'matomo' ),
156
  'displayname' => esc_html__( 'Display Name (Not Recommended!)', 'matomo' ),
157
+ ],
158
  __( 'When a user is logged in to WordPress, track their &quot;User ID&quot;. You can select which field from the User\'s profile is tracked as the &quot;User ID&quot;. When enabled, Tracking based on Email Address is recommended.', 'matomo' ),
159
  '',
160
  $matomo_is_not_tracking,
178
  $matomo_form->show_select(
179
  'force_protocol',
180
  __( 'Force Matomo to use a specific protocol', 'matomo' ),
181
+ [
182
  'disabled' => esc_html__( 'Disabled (default)', 'matomo' ),
183
  'https' => esc_html__( 'https (SSL)', 'matomo' ),
184
+ ],
185
  __( 'Choose if you want to explicitly want to force Matomo to use HTTP or HTTPS. Does not work with a CDN URL.', 'matomo' ),
186
  '',
187
  $matomo_is_not_tracking,
190
  $matomo_form->show_select(
191
  'track_codeposition',
192
  __( 'JavaScript code position', 'matomo' ),
193
+ [
194
  'footer' => esc_html__( 'Footer', 'matomo' ),
195
  'header' => esc_html__( 'Header', 'matomo' ),
196
+ ],
197
  __( 'Choose whether the JavaScript code is added to the footer or the header.', 'matomo' ),
198
  '',
199
  $matomo_is_not_tracking,
202
  $matomo_form->show_select(
203
  'track_api_endpoint',
204
  __( 'Endpoint for HTTP Tracking API', 'matomo' ),
205
+ [
206
  'default' => esc_html__( 'Default', 'matomo' ),
207
  'restapi' => esc_html__( 'Through WordPress Rest API', 'matomo' ),
208
+ ],
209
+ sprintf( __( 'By default the HTTP Tracking API points to your Matomo plugin directory "%1$s". You can choose to use the WP Rest API (%2$s) instead for example to hide matomo.php or if the other URL doesn\'t work for you. Note: If the tracking mode "Tag Manager" is selected, then this URL currently only applies to the feed tracking.', 'matomo' ), esc_html( $matomo_paths->get_tracker_api_url_in_matomo_dir() ), esc_html( $matomo_paths->get_tracker_api_rest_api_endpoint() ) ),
210
  '',
211
  $matomo_is_not_tracking,
212
  $matomo_full_generated_tracking_group . ' matomo-track-option-manually matomo-track-option-tagmanager'
215
  $matomo_form->show_select(
216
  'track_js_endpoint',
217
  __( 'Endpoint for JavaScript tracker', 'matomo' ),
218
+ [
219
  'default' => esc_html__( 'Default', 'matomo' ),
220
  'restapi' => esc_html__( 'Through WordPress Rest API (slower)', 'matomo' ),
221
+ 'plugin' => esc_html__( 'Plugin (an alternative JS file if the default is blocked by the webserver)', 'matomo' ),
222
+ ],
223
+ sprintf( __( 'By default the JS tracking code will be loaded from "%1$s". You can choose to serve the JS file through the WP Rest API (%2$s) for example to hide matomo.js. Please note that this means every request to the JavaScript file will launch WordPress PHP and therefore will be slower compared to your webserver serving the JS file directly. Using the "Plugin" method will cause issues with our paid Heatmap and Session Recording, Form Analytics, and Media Analyics plugin.', 'matomo' ), esc_html( $matomo_paths->get_js_tracker_url_in_matomo_dir() ), esc_html( $matomo_paths->get_js_tracker_rest_api_endpoint() ) ),
224
  '',
225
  $matomo_is_not_tracking,
226
  $matomo_full_generated_tracking_group
227
  );
228
 
229
+ $matomo_form->show_select( 'cookie_consent', esc_html__( 'Custom consent screen', 'matomo' ), $cookie_consent_modes, sprintf( esc_html__( 'Activates a specific Matomo consent mode. Only configure a consent mode if you are implementing a consent screen yourself. This requires a custom consent implementation. For more information please read this %1$sFAQ%2$s (this option will take care of step 1 for you). By default no consent mode is applied.', 'matomo' ), '<a href="https://developer.matomo.org/guides/tracking-consent" rel="noreferrer noopener" target="_blank">', '</a>' ), '', $matomo_is_not_generated_tracking, $matomo_full_generated_tracking_group );
230
 
231
+ $matomo_form->show_headline( esc_html__( 'For Developers', 'matomo' ), 'matomo-track-option matomo-track-option-default matomo-track-option-disabled matomo-track-option-manually matomo-track-option-tagmanager' );
232
 
233
  $matomo_form->show_select(
234
  'tracker_debug',
235
  __( 'Tracker Debug Mode', 'matomo' ),
236
+ [
237
+ 'disabled' => esc_html__( 'Disabled (recommended)', 'matomo' ),
238
  'always' => esc_html__( 'Always enabled', 'matomo' ),
239
+ 'on_demand' => esc_html__( 'Enabled on demand', 'matomo' ),
240
+ ],
241
  __( 'For security and privacy reasons you should only enable this setting for as short time of a time as possible.', 'matomo' ),
242
  '',
243
  $matomo_is_not_tracking,
244
  $matomo_full_generated_tracking_group . ' matomo-track-option-disabled matomo-track-option-manually matomo-track-option-tagmanager'
245
  );
246
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
247
  echo $matomo_submit_button;
248
  ?>
249
 
252
  </form>
253
 
254
  <?php if ( $matomo_is_not_tracking && ! $settings->is_network_enabled() ) { // Can't show it for multisite as idsite and url is always different. ?>
255
+ <div id="matomo_default_tracking_code">
256
+ <h2><?php esc_html_e( 'JavaScript tracking code', 'matomo' ); ?></h2>
257
+ <p>
258
+ <?php echo sprintf( esc_html__( 'Wanting to embed the tracking code manually into your site or using a different plugin? No problem! Simply copy/paste below tracking code. Want to adjust it? %1$sCheck out our developer documentation.%2$s', 'matomo' ), '<a href="https://developer.matomo.org/guides/tracking-javascript-guide" target="_blank" rel="noreferrer noopener">', '</a>' ); ?>
259
+ </p>
260
+ <?php echo '<pre><textarea>' . esc_html( implode( ";\n", explode( ';', $matomo_default_tracking_code['script'] ) ) ) . '</textarea></pre>'; ?>
261
+ <h3><?php esc_html_e( 'NoScript tracking code', 'matomo' ); ?></h3>
262
+ <?php echo '<pre><textarea class="no_script">' . esc_html( $matomo_default_tracking_code['noscript'] ) . '</textarea></pre>'; ?>
263
+ </div>
264
  <?php } ?>
classes/WpMatomo/Annotations.php CHANGED
@@ -9,6 +9,8 @@
9
 
10
  namespace WpMatomo;
11
 
 
 
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit; // if accessed directly
14
  }
@@ -33,7 +35,7 @@ class Annotations {
33
  }
34
 
35
  public function register_hooks() {
36
- add_action( 'transition_post_status', array( $this, 'add_annotation' ), 10, 3 );
37
  }
38
 
39
  /**
@@ -70,13 +72,13 @@ class Annotations {
70
  $logger = $this->logger;
71
  \Piwik\Access::doAsSuperUser(
72
  function () use ( $post, $logger, $idsite ) {
73
- $note = esc_html__( 'Published:', 'matomo' ) . ' ' . $post->post_title . ' - URL: ' . get_permalink( $post->ID );
74
- \Piwik\Plugins\Annotations\API::unsetInstance();// make sure latest instance will be loaded with all up to date dependencies... mainly needed for tests
75
- $id = \Piwik\Plugins\Annotations\API::getInstance()->add( $idsite, gmdate( 'Y-m-d' ), $note );
76
- $logger->log( 'Add post annotation. ' . $note . ' - ' . wp_json_encode( $id ) );
77
  }
78
  );
79
- } catch ( \Exception $e ) {
80
  $this->logger->log( 'Add post annotation failed: ' . $e->getMessage() );
81
 
82
  return;
9
 
10
  namespace WpMatomo;
11
 
12
+ use Exception;
13
+
14
  if ( ! defined( 'ABSPATH' ) ) {
15
  exit; // if accessed directly
16
  }
35
  }
36
 
37
  public function register_hooks() {
38
+ add_action( 'transition_post_status', [ $this, 'add_annotation' ], 10, 3 );
39
  }
40
 
41
  /**
72
  $logger = $this->logger;
73
  \Piwik\Access::doAsSuperUser(
74
  function () use ( $post, $logger, $idsite ) {
75
+ $note = esc_html__( 'Published:', 'matomo' ) . ' ' . $post->post_title . ' - URL: ' . get_permalink( $post->ID );
76
+ \Piwik\Plugins\Annotations\API::unsetInstance();// make sure latest instance will be loaded with all up to date dependencies... mainly needed for tests
77
+ $id = \Piwik\Plugins\Annotations\API::getInstance()->add( $idsite, gmdate( 'Y-m-d' ), $note );
78
+ $logger->log( 'Add post annotation. ' . $note . ' - ' . wp_json_encode( $id ) );
79
  }
80
  );
81
+ } catch ( Exception $e ) {
82
  $this->logger->log( 'Add post annotation failed: ' . $e->getMessage() );
83
 
84
  return;
classes/WpMatomo/Bootstrap.php CHANGED
@@ -9,10 +9,16 @@
9
 
10
  namespace WpMatomo;
11
 
 
 
 
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit; // if accessed directly
14
  }
15
-
 
 
 
16
  class Bootstrap {
17
  /**
18
  * Tests only
@@ -70,11 +76,11 @@ class Bootstrap {
70
 
71
  include_once 'Db/WordPress.php';
72
 
73
- $environment = new \Piwik\Application\Environment( null );
74
  $environment->init();
75
 
76
- \Piwik\FrontController::unsetInstance();
77
- $controller = \Piwik\FrontController::getInstance();
78
  $controller->init();
79
 
80
  add_action(
9
 
10
  namespace WpMatomo;
11
 
12
+ use Piwik\Application\Environment;
13
+ use Piwik\FrontController;
14
+
15
  if ( ! defined( 'ABSPATH' ) ) {
16
  exit; // if accessed directly
17
  }
18
+ /**
19
+ * piwik constants
20
+ * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
21
+ */
22
  class Bootstrap {
23
  /**
24
  * Tests only
76
 
77
  include_once 'Db/WordPress.php';
78
 
79
+ $environment = new Environment( null );
80
  $environment->init();
81
 
82
+ FrontController::unsetInstance();
83
+ $controller = FrontController::getInstance();
84
  $controller->init();
85
 
86
  add_action(
classes/WpMatomo/Capabilities.php CHANGED
@@ -17,6 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) {
17
  }
18
 
19
  class Capabilities {
 
20
  const KEY_NONE = 'none_matomo';
21
 
22
  /**
@@ -50,9 +51,9 @@ class Capabilities {
50
  }
51
 
52
  public function register_hooks() {
53
- add_action( 'wp_roles_init', array( $this, 'add_capabilities_to_roles' ) );
54
- add_filter( 'user_has_cap', array( $this, 'add_capabilities_to_user' ), 10, 4 );
55
- add_filter( 'map_meta_cap', array( $this, 'map_meta_cap' ), 10, 4 );
56
  }
57
 
58
  /**
@@ -61,9 +62,9 @@ class Capabilities {
61
  * @internal
62
  */
63
  public function remove_hooks() {
64
- remove_action( 'wp_roles_init', array( $this, 'add_capabilities_to_roles' ) );
65
- remove_filter( 'user_has_cap', array( $this, 'add_capabilities_to_user' ), 10 );
66
- remove_filter( 'map_meta_cap', array( $this, 'map_meta_cap' ), 10 );
67
  }
68
 
69
  public function map_meta_cap( $caps, $cap, $user_id, $args ) {
@@ -156,12 +157,12 @@ class Capabilities {
156
  }
157
 
158
  public function get_all_capabilities_sorted_by_highest_permission() {
159
- return array(
160
  self::KEY_SUPERUSER,
161
  self::KEY_ADMIN,
162
  self::KEY_WRITE,
163
  self::KEY_VIEW,
164
- );
165
  }
166
 
167
  protected function has_any_higher_permission( $cap_to_find, $allcaps ) {
@@ -182,5 +183,4 @@ class Capabilities {
182
 
183
  return false;
184
  }
185
-
186
  }
17
  }
18
 
19
  class Capabilities {
20
+
21
  const KEY_NONE = 'none_matomo';
22
 
23
  /**
51
  }
52
 
53
  public function register_hooks() {
54
+ add_action( 'wp_roles_init', [ $this, 'add_capabilities_to_roles' ] );
55
+ add_filter( 'user_has_cap', [ $this, 'add_capabilities_to_user' ], 10, 4 );
56
+ add_filter( 'map_meta_cap', [ $this, 'map_meta_cap' ], 10, 4 );
57
  }
58
 
59
  /**
62
  * @internal
63
  */
64
  public function remove_hooks() {
65
+ remove_action( 'wp_roles_init', [ $this, 'add_capabilities_to_roles' ] );
66
+ remove_filter( 'user_has_cap', [ $this, 'add_capabilities_to_user' ], 10 );
67
+ remove_filter( 'map_meta_cap', [ $this, 'map_meta_cap' ], 10 );
68
  }
69
 
70
  public function map_meta_cap( $caps, $cap, $user_id, $args ) {
157
  }
158
 
159
  public function get_all_capabilities_sorted_by_highest_permission() {
160
+ return [
161
  self::KEY_SUPERUSER,
162
  self::KEY_ADMIN,
163
  self::KEY_WRITE,
164
  self::KEY_VIEW,
165
+ ];
166
  }
167
 
168
  protected function has_any_higher_permission( $cap_to_find, $allcaps ) {
183
 
184
  return false;
185
  }
 
186
  }
classes/WpMatomo/Commands/MatomoCommands.php CHANGED
@@ -9,11 +9,12 @@
9
 
10
  namespace WpMatomo\Commands;
11
 
 
 
 
12
  use WpMatomo\Installer;
13
  use WpMatomo\Settings;
14
  use WpMatomo\Uninstaller;
15
- use WP_CLI;
16
- use WP_CLI_Command;
17
  use WpMatomo\Updater;
18
 
19
  if ( ! defined( 'ABSPATH' ) ) {
@@ -52,6 +53,7 @@ class MatomoCommands extends WP_CLI_Command {
52
 
53
  WP_CLI::success( 'Uninstalled Matomo Analytics' );
54
  }
 
55
  /**
56
  * Updates Matomo.
57
  *
@@ -67,17 +69,17 @@ class MatomoCommands extends WP_CLI_Command {
67
  * @when after_wp_load
68
  */
69
  public function update( $args, $assoc_args ) {
70
- if ( function_exists('is_multisite') && is_multisite() && function_exists( 'get_sites' ) ) {
71
  foreach ( get_sites() as $site ) {
72
- /** @var \WP_Site $site */
73
  switch_to_blog( $site->blog_id );
74
  // this way we make sure all blogs get updated eventually
75
  WP_CLI::log( 'Blog ID' . $site->blog_id );
76
- $this->_doUpdate( ! empty( $assoc_args['force'] ) );
77
  restore_current_blog();
78
  }
79
  } else {
80
- $this->_doUpdate( ! empty( $assoc_args['force'] ) );
81
  }
82
 
83
  WP_CLI::success( 'Matomo Analytics Updater finished' );
@@ -86,12 +88,13 @@ class MatomoCommands extends WP_CLI_Command {
86
  /**
87
  * @param $assoc_args
88
  */
89
- public function _doUpdate( $force ) {
90
  $settings = new Settings();
91
 
92
  $installer = new Installer( $settings );
93
  if ( ! $installer->looks_like_it_is_installed() ) {
94
  WP_CLI::log( 'Skipping as looks like Matomo is not yet installed' );
 
95
  return;
96
  }
97
 
@@ -109,7 +112,7 @@ class MatomoCommands extends WP_CLI_Command {
109
  WP_CLI::add_command(
110
  'matomo',
111
  '\WpMatomo\Commands\MatomoCommands',
112
- array(
113
  'shortdesc' => 'Manage your Matomo Analytics. Commands are recommended only to be used in development mode',
114
- )
115
  );
9
 
10
  namespace WpMatomo\Commands;
11
 
12
+ use WP_CLI;
13
+ use WP_CLI_Command;
14
+ use WP_Site;
15
  use WpMatomo\Installer;
16
  use WpMatomo\Settings;
17
  use WpMatomo\Uninstaller;
 
 
18
  use WpMatomo\Updater;
19
 
20
  if ( ! defined( 'ABSPATH' ) ) {
53
 
54
  WP_CLI::success( 'Uninstalled Matomo Analytics' );
55
  }
56
+
57
  /**
58
  * Updates Matomo.
59
  *
69
  * @when after_wp_load
70
  */
71
  public function update( $args, $assoc_args ) {
72
+ if ( function_exists( 'is_multisite' ) && is_multisite() && function_exists( 'get_sites' ) ) {
73
  foreach ( get_sites() as $site ) {
74
+ /** @var WP_Site $site */
75
  switch_to_blog( $site->blog_id );
76
  // this way we make sure all blogs get updated eventually
77
  WP_CLI::log( 'Blog ID' . $site->blog_id );
78
+ $this->do_update( ! empty( $assoc_args['force'] ) );
79
  restore_current_blog();
80
  }
81
  } else {
82
+ $this->do_update( ! empty( $assoc_args['force'] ) );
83
  }
84
 
85
  WP_CLI::success( 'Matomo Analytics Updater finished' );
88
  /**
89
  * @param $assoc_args
90
  */
91
+ private function do_update( $force ) {
92
  $settings = new Settings();
93
 
94
  $installer = new Installer( $settings );
95
  if ( ! $installer->looks_like_it_is_installed() ) {
96
  WP_CLI::log( 'Skipping as looks like Matomo is not yet installed' );
97
+
98
  return;
99
  }
100
 
112
  WP_CLI::add_command(
113
  'matomo',
114
  '\WpMatomo\Commands\MatomoCommands',
115
+ [
116
  'shortdesc' => 'Manage your Matomo Analytics. Commands are recommended only to be used in development mode',
117
+ ]
118
  );
classes/WpMatomo/Compatibility.php CHANGED
@@ -14,7 +14,6 @@ if ( ! defined( 'ABSPATH' ) ) {
14
  }
15
 
16
  class Compatibility {
17
-
18
  public function register_hooks() {
19
  $this->ithemes_security();
20
  }
@@ -35,13 +34,13 @@ class Compatibility {
35
  // todo ideally we would make the plugins path relative and match the specific path...
36
  // like preg_quote(relative_wp_content_dir)...
37
  $is_wp_content_dir_compatible = defined( 'WP_CONTENT_DIR' )
38
- && ABSPATH . 'wp-content' === rtrim( WP_CONTENT_DIR, '/' );
39
  if ( $rules
40
- && $is_wp_content_dir_compatible
41
- && is_string( $rules )
42
- && strpos( $rules, 'RewriteEngine On' ) > 0
43
- && strpos( $rules, 'content' ) > 0
44
- && strpos( $rules, 'plugins' ) > 0 ) {
45
  $rules = '
46
  <IfModule mod_rewrite.c>
47
  RewriteEngine On
@@ -51,11 +50,11 @@ class Compatibility {
51
  </IfModule>
52
  ' . $rules;
53
  }
 
54
  return $rules;
55
  },
56
  9999999991,
57
- $acceptedArgs = 1
58
  );
59
  }
60
-
61
  }
14
  }
15
 
16
  class Compatibility {
 
17
  public function register_hooks() {
18
  $this->ithemes_security();
19
  }
34
  // todo ideally we would make the plugins path relative and match the specific path...
35
  // like preg_quote(relative_wp_content_dir)...
36
  $is_wp_content_dir_compatible = defined( 'WP_CONTENT_DIR' )
37
+ && ABSPATH . 'wp-content' === rtrim( WP_CONTENT_DIR, '/' );
38
  if ( $rules
39
+ && $is_wp_content_dir_compatible
40
+ && is_string( $rules )
41
+ && strpos( $rules, 'RewriteEngine On' ) > 0
42
+ && strpos( $rules, 'content' ) > 0
43
+ && strpos( $rules, 'plugins' ) > 0 ) {
44
  $rules = '
45
  <IfModule mod_rewrite.c>
46
  RewriteEngine On
50
  </IfModule>
51
  ' . $rules;
52
  }
53
+
54
  return $rules;
55
  },
56
  9999999991,
57
+ $accepted_args = 1
58
  );
59
  }
 
60
  }
classes/WpMatomo/Db/Settings.php CHANGED
@@ -12,7 +12,17 @@ namespace WpMatomo\Db;
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit; // if accessed directly
14
  }
15
-
 
 
 
 
 
 
 
 
 
 
16
  class Settings {
17
 
18
  /**
@@ -23,7 +33,7 @@ class Settings {
23
  * @return string
24
  * @api
25
  */
26
- public function prefix_table_name( $table_name_to_prefix = '') {
27
  global $wpdb;
28
 
29
  return $wpdb->prefix . MATOMO_DATABASE_PREFIX . $table_name_to_prefix;
@@ -35,7 +45,7 @@ class Settings {
35
  public function get_matomo_tables() {
36
  // we need to hard code them unfortunately for tests cause there are temporary tables used and we can't find a
37
  // list of existing temp tables
38
- $tables = array(
39
  'access',
40
  'archive_invalidations',
41
  'brute_force_log',
@@ -65,24 +75,30 @@ class Settings {
65
  'user_dashboard',
66
  'user_language',
67
  'user_token_auth',
68
- );
69
- if ( !is_multisite() ) {
70
- $tables = array_merge($tables, ['tagmanager_container',
71
- 'tagmanager_container_release',
72
- 'tagmanager_container_version',
73
- 'tagmanager_tag',
74
- 'tagmanager_trigger',
75
- 'tagmanager_variable'] );
 
 
 
 
 
76
  }
 
77
  return $tables;
78
  }
79
 
80
  public function get_installed_matomo_tables() {
81
  global $wpdb;
82
 
83
- $table_names = array();
84
 
85
- $tables = $wpdb->get_results( 'SHOW TABLES LIKE "' . $this->prefix_table_name() . '%"', ARRAY_N );
86
  foreach ( $tables as $table_name_to_look_for ) {
87
  $table_names[] = array_shift( $table_name_to_look_for );
88
  }
@@ -98,7 +114,7 @@ class Settings {
98
  $table_names_to_look_for = apply_filters( 'matomo_install_tables', $table_names_to_look_for );
99
 
100
  foreach ( $table_names_to_look_for as $table_name_to_look_for ) {
101
- $table_name_to_test = $this->prefix_table_name($table_name_to_look_for);
102
  if ( ! in_array( $table_name_to_test, $table_names, true ) ) {
103
  $table_names[] = $table_name_to_test;
104
  }
@@ -106,5 +122,4 @@ class Settings {
106
 
107
  return $table_names;
108
  }
109
-
110
  }
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit; // if accessed directly
14
  }
15
+ /**
16
+ * We want a real data, not something coming from cache
17
+ * phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
18
+ *
19
+ * This is a report error, so silent the possible errors
20
+ * phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
21
+ *
22
+ * We cannot use parameters of statements as this is the table names we build
23
+ * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
24
+ * phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
25
+ */
26
  class Settings {
27
 
28
  /**
33
  * @return string
34
  * @api
35
  */
36
+ public function prefix_table_name( $table_name_to_prefix = '' ) {
37
  global $wpdb;
38
 
39
  return $wpdb->prefix . MATOMO_DATABASE_PREFIX . $table_name_to_prefix;
45
  public function get_matomo_tables() {
46
  // we need to hard code them unfortunately for tests cause there are temporary tables used and we can't find a
47
  // list of existing temp tables
48
+ $tables = [
49
  'access',
50
  'archive_invalidations',
51
  'brute_force_log',
75
  'user_dashboard',
76
  'user_language',
77
  'user_token_auth',
78
+ ];
79
+ if ( ! is_multisite() ) {
80
+ $tables = array_merge(
81
+ $tables,
82
+ [
83
+ 'tagmanager_container',
84
+ 'tagmanager_container_release',
85
+ 'tagmanager_container_version',
86
+ 'tagmanager_tag',
87
+ 'tagmanager_trigger',
88
+ 'tagmanager_variable',
89
+ ]
90
+ );
91
  }
92
+
93
  return $tables;
94
  }
95
 
96
  public function get_installed_matomo_tables() {
97
  global $wpdb;
98
 
99
+ $table_names = [];
100
 
101
+ $tables = $wpdb->get_results( 'SHOW TABLES LIKE "' . $this->prefix_table_name() . '%"', ARRAY_N );
102
  foreach ( $tables as $table_name_to_look_for ) {
103
  $table_names[] = array_shift( $table_name_to_look_for );
104
  }
114
  $table_names_to_look_for = apply_filters( 'matomo_install_tables', $table_names_to_look_for );
115
 
116
  foreach ( $table_names_to_look_for as $table_name_to_look_for ) {
117
+ $table_name_to_test = $this->prefix_table_name( $table_name_to_look_for );
118
  if ( ! in_array( $table_name_to_test, $table_names, true ) ) {
119
  $table_names[] = $table_name_to_test;
120
  }
122
 
123
  return $table_names;
124
  }
 
125
  }
classes/WpMatomo/Db/WordPress.php CHANGED
@@ -347,10 +347,10 @@ class WordPress extends Mysqli {
347
  }
348
 
349
  if ( defined( 'WP_DEBUG' )
350
- && WP_DEBUG
351
- && defined( 'WP_DEBUG_DISPLAY' )
352
- && WP_DEBUG_DISPLAY
353
- && ! is_admin() ) {
354
  // prevent showing some notices in frontend eg if cronjob runs there
355
 
356
  $is_likely_dedicated_cron = defined( 'DOING_CRON' ) && DOING_CRON && defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
@@ -363,8 +363,8 @@ class WordPress extends Mysqli {
363
  }
364
 
365
  if ( ( stripos( $sql, '/* WP IGNORE ERROR */' ) !== false )
366
- || stripos( $sql, 'SELECT @@TX_ISOLATION' ) !== false
367
- || stripos( $sql, 'SELECT @@transaction_isolation' ) !== false ) {
368
  // prevent notices for queries that are expected to fail
369
  // SELECT 1 FROM wp_matomo_logtmpsegment1cc77bce7a13181081e44ea6ffc0a9fd LIMIT 1 => runs to detect if temp table exists or not and regularly the query fails which is expected
370
  // SELECT @@TX_ISOLATION => not available in all mysql versions
347
  }
348
 
349
  if ( defined( 'WP_DEBUG' )
350
+ && WP_DEBUG
351
+ && defined( 'WP_DEBUG_DISPLAY' )
352
+ && WP_DEBUG_DISPLAY
353
+ && ! is_admin() ) {
354
  // prevent showing some notices in frontend eg if cronjob runs there
355
 
356
  $is_likely_dedicated_cron = defined( 'DOING_CRON' ) && DOING_CRON && defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
363
  }
364
 
365
  if ( ( stripos( $sql, '/* WP IGNORE ERROR */' ) !== false )
366
+ || stripos( $sql, 'SELECT @@TX_ISOLATION' ) !== false
367
+ || stripos( $sql, 'SELECT @@transaction_isolation' ) !== false ) {
368
  // prevent notices for queries that are expected to fail
369
  // SELECT 1 FROM wp_matomo_logtmpsegment1cc77bce7a13181081e44ea6ffc0a9fd LIMIT 1 => runs to detect if temp table exists or not and regularly the query fails which is expected
370
  // SELECT @@TX_ISOLATION => not available in all mysql versions
classes/WpMatomo/Db/WordPressDbStatement.php CHANGED
@@ -9,12 +9,23 @@
9
 
10
  namespace Piwik\Db\Adapter;
11
 
 
 
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit; // if accessed directly
14
  }
15
-
16
- class WordPressDbStatement extends \Zend_Db_Statement {
17
-
 
 
 
 
 
 
 
 
 
18
  private $result;
19
  private $sql;
20
 
9
 
10
  namespace Piwik\Db\Adapter;
11
 
12
+ use Zend_Db_Statement;
13
+
14
  if ( ! defined( 'ABSPATH' ) ) {
15
  exit; // if accessed directly
16
  }
17
+ /**
18
+ * We want a real data, not something coming from cache
19
+ * phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
20
+ *
21
+ * This is a report error, so silent the possible errors
22
+ * phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
23
+ *
24
+ * We cannot use parameters of statements as this is the table names we build
25
+ * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
26
+ * phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
27
+ */
28
+ class WordPressDbStatement extends Zend_Db_Statement {
29
  private $result;
30
  private $sql;
31
 
classes/WpMatomo/Db/WordPressTracker.php CHANGED
@@ -76,10 +76,10 @@ class WordPress extends Mysqli {
76
  */
77
  private function before_execute_query( $wpdb, $sql ) {
78
  if ( ! $wpdb->suppress_errors
79
- && defined( 'WP_DEBUG' )
80
- && WP_DEBUG
81
- && defined( 'WP_DEBUG_DISPLAY' )
82
- && WP_DEBUG_DISPLAY ) {
83
  // we want to prevent showing these notices
84
  if ( defined( 'MATOMO_SUPPRESS_DB_ERRORS' ) ) {
85
  if ( MATOMO_SUPPRESS_DB_ERRORS === true ) {
76
  */
77
  private function before_execute_query( $wpdb, $sql ) {
78
  if ( ! $wpdb->suppress_errors
79
+ && defined( 'WP_DEBUG' )
80
+ && WP_DEBUG
81
+ && defined( 'WP_DEBUG_DISPLAY' )
82
+ && WP_DEBUG_DISPLAY ) {
83
  // we want to prevent showing these notices
84
  if ( defined( 'MATOMO_SUPPRESS_DB_ERRORS' ) ) {
85
  if ( MATOMO_SUPPRESS_DB_ERRORS === true ) {
classes/WpMatomo/Ecommerce/Base.php CHANGED
@@ -9,10 +9,12 @@
9
 
10
  namespace WpMatomo\Ecommerce;
11
 
 
 
12
  use WpMatomo\Admin\TrackingSettings;
 
13
  use WpMatomo\Logger;
14
  use WpMatomo\Settings;
15
- use WpMatomo\AjaxTracker;
16
 
17
  if ( ! defined( 'ABSPATH' ) ) {
18
  exit; // if accessed directly
@@ -40,7 +42,7 @@ class Base {
40
  */
41
  protected $cart_update_queue = '';
42
 
43
- private $ajax_tracker_calls = array();
44
 
45
  public function __construct( AjaxTracker $tracker ) {
46
  $this->logger = new Logger();
@@ -52,18 +54,20 @@ class Base {
52
 
53
  public function register_hooks() {
54
  if ( ! is_admin() ) {
55
- add_action( 'wp_footer', array( $this, 'on_print_queues' ), 99999, 0 );
56
  }
57
  }
58
 
59
  public function on_print_queues() {
60
  // we need to queue in case there are multiple cart updates within one page load
61
  if ( ! empty( $this->cart_update_queue ) ) {
 
62
  echo $this->cart_update_queue;
63
  }
64
  }
65
 
66
  protected function has_order_been_tracked_already( $order_id ) {
 
67
  return get_post_meta( $order_id, $this->key_order_tracked, true ) == 1;
68
  }
69
 
@@ -72,8 +76,8 @@ class Base {
72
  }
73
 
74
  protected function should_track_background() {
75
- return (defined( 'DOING_AJAX' ) && DOING_AJAX)
76
- || \WpMatomo::$settings->get_global_option('track_mode') === TrackingSettings::TRACK_MODE_TAGMANAGER;
77
  }
78
 
79
  protected function make_matomo_js_tracker_call( $params ) {
@@ -87,22 +91,22 @@ class Base {
87
  protected function wrap_script( $script ) {
88
  if ( $this->should_track_background() ) {
89
  foreach ( $this->ajax_tracker_calls as $call ) {
90
- $methods = array(
91
  'addEcommerceItem' => 'addEcommerceItem',
92
  'trackEcommerceOrder' => 'doTrackEcommerceOrder',
93
  'trackEcommerceCartUpdate' => 'doTrackEcommerceCartUpdate',
94
- );
95
  if ( ! empty( $call[0] ) && ! empty( $methods[ $call[0] ] ) ) {
96
  try {
97
  $tracker_method = $methods[ $call[0] ];
98
  array_shift( $call );
99
- call_user_func_array( array( $this->tracker, $tracker_method ), $call );
100
- } catch (\Exception $e) {
101
- $this->logger->log_exception($call[0], $e);
102
  }
103
  }
104
  }
105
- $this->ajax_tracker_calls = array();
106
 
107
  return '';
108
  }
@@ -111,7 +115,13 @@ class Base {
111
  return '';
112
  }
113
 
114
- return '<script type="text/javascript">' . $script . '</script>';
115
- }
 
 
 
 
116
 
 
 
117
  }
9
 
10
  namespace WpMatomo\Ecommerce;
11
 
12
+ use Exception;
13
+ use WpMatomo;
14
  use WpMatomo\Admin\TrackingSettings;
15
+ use WpMatomo\AjaxTracker;
16
  use WpMatomo\Logger;
17
  use WpMatomo\Settings;
 
18
 
19
  if ( ! defined( 'ABSPATH' ) ) {
20
  exit; // if accessed directly
42
  */
43
  protected $cart_update_queue = '';
44
 
45
+ private $ajax_tracker_calls = [];
46
 
47
  public function __construct( AjaxTracker $tracker ) {
48
  $this->logger = new Logger();
54
 
55
  public function register_hooks() {
56
  if ( ! is_admin() ) {
57
+ add_action( 'wp_footer', [ $this, 'on_print_queues' ], 99999, 0 );
58
  }
59
  }
60
 
61
  public function on_print_queues() {
62
  // we need to queue in case there are multiple cart updates within one page load
63
  if ( ! empty( $this->cart_update_queue ) ) {
64
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
65
  echo $this->cart_update_queue;
66
  }
67
  }
68
 
69
  protected function has_order_been_tracked_already( $order_id ) {
70
+ // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
71
  return get_post_meta( $order_id, $this->key_order_tracked, true ) == 1;
72
  }
73
 
76
  }
77
 
78
  protected function should_track_background() {
79
+ return ( defined( 'DOING_AJAX' ) && DOING_AJAX )
80
+ || WpMatomo::$settings->get_global_option( 'track_mode' ) === TrackingSettings::TRACK_MODE_TAGMANAGER;
81
  }
82
 
83
  protected function make_matomo_js_tracker_call( $params ) {
91
  protected function wrap_script( $script ) {
92
  if ( $this->should_track_background() ) {
93
  foreach ( $this->ajax_tracker_calls as $call ) {
94
+ $methods = [
95
  'addEcommerceItem' => 'addEcommerceItem',
96
  'trackEcommerceOrder' => 'doTrackEcommerceOrder',
97
  'trackEcommerceCartUpdate' => 'doTrackEcommerceCartUpdate',
98
+ ];
99
  if ( ! empty( $call[0] ) && ! empty( $methods[ $call[0] ] ) ) {
100
  try {
101
  $tracker_method = $methods[ $call[0] ];
102
  array_shift( $call );
103
+ call_user_func_array( [ $this->tracker, $tracker_method ], $call );
104
+ } catch ( Exception $e ) {
105
+ $this->logger->log_exception( $call[0], $e );
106
  }
107
  }
108
  }
109
+ $this->ajax_tracker_calls = [];
110
 
111
  return '';
112
  }
115
  return '';
116
  }
117
 
118
+ if ( function_exists( 'wp_get_inline_script_tag' ) ) {
119
+ $script = wp_get_inline_script_tag( $script );
120
+ } else {
121
+ // line feed is required to match the wp_get_inline_script_tag output
122
+ $script = '<script >' . PHP_EOL . $script . PHP_EOL . '</script>' . PHP_EOL;
123
+ }
124
 
125
+ return $script;
126
+ }
127
  }
classes/WpMatomo/Ecommerce/EasyDigitalDownloads.php CHANGED
@@ -9,26 +9,27 @@
9
 
10
  namespace WpMatomo\Ecommerce;
11
 
 
 
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit; // if accessed directly
14
  }
15
 
16
  class EasyDigitalDownloads extends Base {
17
-
18
  public function register_hooks() {
19
  if ( ! is_admin() ) {
20
- add_action( 'template_redirect', array( $this, 'on_product_view' ), 99999, 0 );
21
  }
22
 
23
  parent::register_hooks();
24
 
25
  // these actions may be triggered in admin when ajax is used
26
- add_action( 'edd_payment_receipt_after_table', array( $this, 'on_order' ), 99999, 2 );
27
- add_action( 'edd_post_remove_from_cart', array( $this, 'on_cart_update' ), 99999, 0 );
28
- add_action( 'edd_post_add_to_cart', array( $this, 'on_cart_update' ), 99999, 0 );
29
- add_action( 'edd_cart_discounts_removed', array( $this, 'on_cart_update' ), 99999, 0 );
30
- add_action( 'edd_after_set_cart_item_quantity', array( $this, 'on_cart_update' ), 99999, 0 );
31
- add_action( 'edd_cart_discount_set', array( $this, 'on_cart_update' ), 99999, 0 );
32
  }
33
 
34
  public function on_cart_update() {
@@ -43,7 +44,7 @@ class EasyDigitalDownloads extends Base {
43
 
44
  $tracking_code = '';
45
  foreach ( $contents as $key => $item ) {
46
- $download = new \EDD_Download( $item['id'] );
47
 
48
  // If the item is not a download or it's status has changed since it was added to the cart.
49
  if ( empty( $download->ID ) || ! $download->can_purchase() ) {
@@ -64,7 +65,7 @@ class EasyDigitalDownloads extends Base {
64
  $categories = $this->get_product_categories( $download->ID );
65
  $quantity = isset( $item['quantity'] ) ? $item['quantity'] : 0;
66
 
67
- $params = array( 'addEcommerceItem', $sku, $name, $categories, $price, $quantity );
68
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
69
  }
70
 
@@ -75,7 +76,7 @@ class EasyDigitalDownloads extends Base {
75
  $total = $cart->get_total();
76
  }
77
 
78
- $tracking_code .= $this->make_matomo_js_tracker_call( array( 'trackEcommerceCartUpdate', $total ) );
79
 
80
  // we can't echo directly as we wouldn't know where in the template rendering stage we are and whether
81
  // we're supposed to print or not etc
@@ -90,7 +91,7 @@ class EasyDigitalDownloads extends Base {
90
  }
91
 
92
  /**
93
- * @param \EDD_Download $download
94
  *
95
  * @return mixed
96
  */
@@ -118,18 +119,18 @@ class EasyDigitalDownloads extends Base {
118
  return;
119
  }
120
 
121
- $download = new \EDD_Download( $download_id );
122
 
123
  $sku = $this->get_sku( $download, $download_id );
124
 
125
- $params = array(
126
  'setEcommerceView',
127
  $sku,
128
  $download->get_name(),
129
  $this->get_product_categories( $download_id ),
130
  $download->get_price(),
131
- );
132
-
133
  echo $this->wrap_script( $this->make_matomo_js_tracker_call( $params ) );
134
  }
135
 
@@ -175,7 +176,7 @@ class EasyDigitalDownloads extends Base {
175
  $name .= ' - ' . edd_get_price_option_name( $item['id'], $price_id );
176
  }
177
 
178
- $download = new \EDD_Download( $item['id'] );
179
  $sku = $this->get_sku( $download, $item['id'] );
180
 
181
  $price = 0;
@@ -183,14 +184,14 @@ class EasyDigitalDownloads extends Base {
183
  $price = $item['item_price'];
184
  }
185
 
186
- $params = array(
187
  'addEcommerceItem',
188
  $sku,
189
  $name,
190
  $this->get_product_categories( $item['id'] ),
191
  $price,
192
  $item['quantity'],
193
- );
194
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
195
  }
196
  }
@@ -208,7 +209,7 @@ class EasyDigitalDownloads extends Base {
208
  $discount = reset( $discount );
209
  }
210
 
211
- $params = array(
212
  'trackEcommerceOrder',
213
  '' . $order_id_to_track,
214
  $grand_total ? $grand_total : 0,
@@ -216,11 +217,10 @@ class EasyDigitalDownloads extends Base {
216
  edd_use_taxes() ? edd_get_payment_tax( $payment->ID, $payment_meta ) : '0',
217
  $shipping = 0,
218
  $discount,
219
- );
220
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
221
-
222
  echo $this->wrap_script( $tracking_code );
223
  }
224
  }
225
-
226
  }
9
 
10
  namespace WpMatomo\Ecommerce;
11
 
12
+ use EDD_Download;
13
+
14
  if ( ! defined( 'ABSPATH' ) ) {
15
  exit; // if accessed directly
16
  }
17
 
18
  class EasyDigitalDownloads extends Base {
 
19
  public function register_hooks() {
20
  if ( ! is_admin() ) {
21
+ add_action( 'template_redirect', [ $this, 'on_product_view' ], 99999, 0 );
22
  }
23
 
24
  parent::register_hooks();
25
 
26
  // these actions may be triggered in admin when ajax is used
27
+ add_action( 'edd_payment_receipt_after_table', [ $this, 'on_order' ], 99999, 2 );
28
+ add_action( 'edd_post_remove_from_cart', [ $this, 'on_cart_update' ], 99999, 0 );
29
+ add_action( 'edd_post_add_to_cart', [ $this, 'on_cart_update' ], 99999, 0 );
30
+ add_action( 'edd_cart_discounts_removed', [ $this, 'on_cart_update' ], 99999, 0 );
31
+ add_action( 'edd_after_set_cart_item_quantity', [ $this, 'on_cart_update' ], 99999, 0 );
32
+ add_action( 'edd_cart_discount_set', [ $this, 'on_cart_update' ], 99999, 0 );
33
  }
34
 
35
  public function on_cart_update() {
44
 
45
  $tracking_code = '';
46
  foreach ( $contents as $key => $item ) {
47
+ $download = new EDD_Download( $item['id'] );
48
 
49
  // If the item is not a download or it's status has changed since it was added to the cart.
50
  if ( empty( $download->ID ) || ! $download->can_purchase() ) {
65
  $categories = $this->get_product_categories( $download->ID );
66
  $quantity = isset( $item['quantity'] ) ? $item['quantity'] : 0;
67
 
68
+ $params = [ 'addEcommerceItem', $sku, $name, $categories, $price, $quantity ];
69
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
70
  }
71
 
76
  $total = $cart->get_total();
77
  }
78
 
79
+ $tracking_code .= $this->make_matomo_js_tracker_call( [ 'trackEcommerceCartUpdate', $total ] );
80
 
81
  // we can't echo directly as we wouldn't know where in the template rendering stage we are and whether
82
  // we're supposed to print or not etc
91
  }
92
 
93
  /**
94
+ * @param EDD_Download $download
95
  *
96
  * @return mixed
97
  */
119
  return;
120
  }
121
 
122
+ $download = new EDD_Download( $download_id );
123
 
124
  $sku = $this->get_sku( $download, $download_id );
125
 
126
+ $params = [
127
  'setEcommerceView',
128
  $sku,
129
  $download->get_name(),
130
  $this->get_product_categories( $download_id ),
131
  $download->get_price(),
132
+ ];
133
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
134
  echo $this->wrap_script( $this->make_matomo_js_tracker_call( $params ) );
135
  }
136
 
176
  $name .= ' - ' . edd_get_price_option_name( $item['id'], $price_id );
177
  }
178
 
179
+ $download = new EDD_Download( $item['id'] );
180
  $sku = $this->get_sku( $download, $item['id'] );
181
 
182
  $price = 0;
184
  $price = $item['item_price'];
185
  }
186
 
187
+ $params = [
188
  'addEcommerceItem',
189
  $sku,
190
  $name,
191
  $this->get_product_categories( $item['id'] ),
192
  $price,
193
  $item['quantity'],
194
+ ];
195
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
196
  }
197
  }
209
  $discount = reset( $discount );
210
  }
211
 
212
+ $params = [
213
  'trackEcommerceOrder',
214
  '' . $order_id_to_track,
215
  $grand_total ? $grand_total : 0,
217
  edd_use_taxes() ? edd_get_payment_tax( $payment->ID, $payment_meta ) : '0',
218
  $shipping = 0,
219
  $discount,
220
+ ];
221
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
222
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
223
  echo $this->wrap_script( $tracking_code );
224
  }
225
  }
 
226
  }
classes/WpMatomo/Ecommerce/MatomoTestEcommerce.php ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace WpMatomo\Ecommerce;
4
+
5
+ /**
6
+ * this class is required only for phpunit tests.
7
+ * It allow to change the visibility of some methods of the Base class
8
+ * and so allow to test them in the unit tests
9
+ *
10
+ * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found
11
+ */
12
+ class MatomoTestEcommerce extends Base {
13
+
14
+ /**
15
+ * Render public the wrap_script method. Required for the unit tests
16
+ *
17
+ * @param string $script
18
+ *
19
+ * @return string
20
+ * @see Base::wrap_script()
21
+ */
22
+ public function wrap_script( $script ) {
23
+ return parent::wrap_script( $script );
24
+ }
25
+
26
+ /**
27
+ * Render public the wrap_script method. Required for the unit tests
28
+ *
29
+ * @param [] $params
30
+ *
31
+ * @return string
32
+ * @see Base::make_matomo_js_tracker_call()
33
+ */
34
+ public function make_matomo_js_tracker_call( $params ) {
35
+ return parent::make_matomo_js_tracker_call( $params );
36
+ }
37
+ }
classes/WpMatomo/Ecommerce/MemberPress.php CHANGED
@@ -9,41 +9,43 @@
9
 
10
  namespace WpMatomo\Ecommerce;
11
 
 
 
 
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit; // if accessed directly
14
  }
15
 
16
  class MemberPress extends Base {
17
-
18
  public function register_hooks() {
19
  if ( ! is_admin() ) {
20
  parent::register_hooks();
21
 
22
- add_action( 'template_redirect', array( $this, 'on_product_view' ), 99999, 0 );
23
- add_action( 'wp_footer', array( $this, 'on_order' ), 99999, 2 );
24
- add_action( 'mepr-signup', array( $this, 'on_cart_update' ), 99999, 1 );
25
  }
26
  }
27
 
28
  /**
29
- * @param \MeprTransaction $transaction
30
  */
31
  public function on_cart_update( $transaction ) {
32
  $tracking_code = '';
33
  $sku = $transaction->id;
34
  $product = $transaction->product();
35
- $params = array(
36
  'addEcommerceItem',
37
  $sku,
38
  $product->post_title,
39
- $categories = array(),
40
  $transaction->amount,
41
  1,
42
- );
43
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
44
 
45
  $total = $transaction->total;
46
- $tracking_code .= $this->make_matomo_js_tracker_call( array( 'trackEcommerceCartUpdate', $total ) );
47
 
48
  // we can't echo directly as we wouldn't know where in the template rendering stage we are and whether
49
  // we're supposed to print or not etc
@@ -66,18 +68,18 @@ class MemberPress extends Base {
66
  return;
67
  }
68
 
69
- $product = new \MeprProduct( $product_id );
70
 
71
  $sku = $product_id;
72
 
73
- $params = array(
74
  'setEcommerceView',
75
  '' . $sku,
76
  $product->post_title,
77
- $categories = array(),
78
  $product->price,
79
- );
80
-
81
  echo $this->wrap_script( $this->make_matomo_js_tracker_call( $params ) );
82
  }
83
 
@@ -85,13 +87,13 @@ class MemberPress extends Base {
85
  if ( isset( $_GET['membership'] )
86
  && isset( $_GET['trans_num'] )
87
  && class_exists( '\MeprTransaction' ) ) {
88
- $txn = \MeprTransaction::get_one_by_trans_num($_GET['trans_num'] );
89
  if ( isset( $txn->id ) && $txn->id > 0 ) {
90
  if ( $this->has_order_been_tracked_already( $txn->id ) ) {
91
  return;
92
  }
93
  $this->set_order_been_tracked( $txn->id );
94
- $transaction = new \MeprTransaction( $txn->id );
95
  $order_id_to_track = $txn->trans_num;
96
  $product = $transaction->product();
97
 
@@ -101,16 +103,16 @@ class MemberPress extends Base {
101
  $discount = $product->price - $txn->amount;
102
  }
103
  $tracking_code = '';
104
- $params = array(
105
  'addEcommerceItem',
106
  '' . $product->ID,
107
  $product->post_title,
108
- array(),
109
  $txn->amount,
110
  1,
111
- );
112
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
113
- $params = array(
114
  'trackEcommerceOrder',
115
  '' . $order_id_to_track,
116
  $txn->total,
@@ -118,12 +120,11 @@ class MemberPress extends Base {
118
  $txn->tax_amount,
119
  $shipping = 0,
120
  $discount,
121
- );
122
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
123
-
124
  echo $this->wrap_script( $tracking_code );
125
  }
126
  }
127
  }
128
-
129
  }
9
 
10
  namespace WpMatomo\Ecommerce;
11
 
12
+ use MeprProduct;
13
+ use MeprTransaction;
14
+
15
  if ( ! defined( 'ABSPATH' ) ) {
16
  exit; // if accessed directly
17
  }
18
 
19
  class MemberPress extends Base {
 
20
  public function register_hooks() {
21
  if ( ! is_admin() ) {
22
  parent::register_hooks();
23
 
24
+ add_action( 'template_redirect', [ $this, 'on_product_view' ], 99999, 0 );
25
+ add_action( 'wp_footer', [ $this, 'on_order' ], 99999, 2 );
26
+ add_action( 'mepr-signup', [ $this, 'on_cart_update' ], 99999, 1 );
27
  }
28
  }
29
 
30
  /**
31
+ * @param MeprTransaction $transaction
32
  */
33
  public function on_cart_update( $transaction ) {
34
  $tracking_code = '';
35
  $sku = $transaction->id;
36
  $product = $transaction->product();
37
+ $params = [
38
  'addEcommerceItem',
39
  $sku,
40
  $product->post_title,
41
+ $categories = [],
42
  $transaction->amount,
43
  1,
44
+ ];
45
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
46
 
47
  $total = $transaction->total;
48
+ $tracking_code .= $this->make_matomo_js_tracker_call( [ 'trackEcommerceCartUpdate', $total ] );
49
 
50
  // we can't echo directly as we wouldn't know where in the template rendering stage we are and whether
51
  // we're supposed to print or not etc
68
  return;
69
  }
70
 
71
+ $product = new MeprProduct( $product_id );
72
 
73
  $sku = $product_id;
74
 
75
+ $params = [
76
  'setEcommerceView',
77
  '' . $sku,
78
  $product->post_title,
79
+ $categories = [],
80
  $product->price,
81
+ ];
82
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
83
  echo $this->wrap_script( $this->make_matomo_js_tracker_call( $params ) );
84
  }
85
 
87
  if ( isset( $_GET['membership'] )
88
  && isset( $_GET['trans_num'] )
89
  && class_exists( '\MeprTransaction' ) ) {
90
+ $txn = MeprTransaction::get_one_by_trans_num( sanitize_text_field( wp_unslash( $_GET['trans_num'] ) ) );
91
  if ( isset( $txn->id ) && $txn->id > 0 ) {
92
  if ( $this->has_order_been_tracked_already( $txn->id ) ) {
93
  return;
94
  }
95
  $this->set_order_been_tracked( $txn->id );
96
+ $transaction = new MeprTransaction( $txn->id );
97
  $order_id_to_track = $txn->trans_num;
98
  $product = $transaction->product();
99
 
103
  $discount = $product->price - $txn->amount;
104
  }
105
  $tracking_code = '';
106
+ $params = [
107
  'addEcommerceItem',
108
  '' . $product->ID,
109
  $product->post_title,
110
+ [],
111
  $txn->amount,
112
  1,
113
+ ];
114
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
115
+ $params = [
116
  'trackEcommerceOrder',
117
  '' . $order_id_to_track,
118
  $txn->total,
120
  $txn->tax_amount,
121
  $shipping = 0,
122
  $discount,
123
+ ];
124
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
125
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
126
  echo $this->wrap_script( $tracking_code );
127
  }
128
  }
129
  }
 
130
  }
classes/WpMatomo/Ecommerce/Woocommerce.php CHANGED
@@ -9,12 +9,15 @@
9
 
10
  namespace WpMatomo\Ecommerce;
11
 
 
 
 
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit; // if accessed directly
14
  }
15
 
16
  class Woocommerce extends Base {
17
- private $order_status_ignore = array( 'cancelled', 'failed', 'refunded' );
18
 
19
  public function register_hooks() {
20
  if ( is_admin() ) {
@@ -23,30 +26,38 @@ class Woocommerce extends Base {
23
 
24
  parent::register_hooks();
25
 
26
- add_action( 'wp_head', array( $this, 'maybe_track_order_complete' ), 99999 );
27
- add_action( 'woocommerce_after_single_product', array( $this, 'on_product_view' ), 99999, $args = 0 );
28
- add_action( 'woocommerce_add_to_cart', array( $this, 'on_cart_updated_safe' ), 99999, 0 );
29
- add_action( 'woocommerce_cart_item_removed', array( $this, 'on_cart_updated_safe' ), 99999, 0 );
30
- add_action( 'woocommerce_cart_item_restored', array( $this, 'on_cart_updated_safe' ), 99999, 0 );
31
- add_action( 'woocommerce_cart_item_set_quantity', array( $this, 'on_cart_updated_safe' ), 99999, 0 );
32
- add_action('woocommerce_thankyou', array($this, 'anonymise_orderid_in_url'), 1, 1);
33
 
34
- if (!$this->should_track_background()) {
35
  // prevent possibly executing same event twice where eg first a PHP Matomo tracker request is created
36
  // because of woocommerce_applied_coupon and then also because of woocommerce_update_cart_action_cart_updated itself
37
  // causing two tracking requests to be issues from the server. refs #215
38
  // when not ajax mode the later event will simply overwrite the first and it should be fine.
39
- add_filter( 'woocommerce_update_cart_action_cart_updated', array( $this, 'on_cart_updated_safe' ), 99999, 1 );
 
 
 
 
 
 
 
 
40
  }
41
 
42
- add_action( 'woocommerce_applied_coupon', array( $this, 'on_coupon_updated_safe' ), 99999, 0 );
43
- add_action( 'woocommerce_removed_coupon', array( $this, 'on_coupon_updated_safe' ), 99999, 0 );
44
  }
45
 
46
- public function anonymise_orderid_in_url($order_id)
47
- {
48
- if ( !empty($order_id) && is_numeric($order_id)) {
49
  $order_id = (int) $order_id;
 
50
  echo "<script>(function () {
51
  if (location.href) {
52
  window._paq = window._paq || [];
@@ -66,28 +77,28 @@ class Woocommerce extends Base {
66
  if ( function_exists( 'is_order_received_page' ) && is_order_received_page() ) {
67
  $order_id = isset( $wp->query_vars['order-received'] ) ? $wp->query_vars['order-received'] : 0;
68
  if ( ! empty( $order_id ) && $order_id > 0 ) {
 
69
  echo $this->on_order( $order_id );
70
  }
71
  }
72
  }
73
 
74
- public function on_coupon_updated_safe( ) {
75
-
76
  try {
77
- $val = $this->on_cart_updated($val= null, true);
78
- } catch (\Exception $e) {
79
- $this->logger->log_exception('woo_on_cart_update', $e);
 
80
  }
81
 
82
  return $val;
83
  }
84
 
85
  public function on_cart_updated_safe( $val = null ) {
86
-
87
  try {
88
- $val = $this->on_cart_updated($val);
89
- } catch (\Exception $e) {
90
- $this->logger->log_exception('woo_on_cart_update', $e);
91
  }
92
 
93
  return $val;
@@ -104,7 +115,7 @@ class Woocommerce extends Base {
104
 
105
  /** @var \WC_Cart $cart */
106
  $cart = $woocommerce->cart;
107
- if (!$is_coupon_update) {
108
  // can cause cart coupon not to be applied when WooCommerce Subscriptions is used.
109
  $cart->calculate_totals();
110
  }
@@ -113,7 +124,7 @@ class Woocommerce extends Base {
113
  $tracking_code = '';
114
 
115
  foreach ( $cart_content as $item ) {
116
- /** @var \WC_Product $product */
117
  $product = wc_get_product( $item['product_id'] );
118
 
119
  if ( $this->isWC3() ) {
@@ -126,7 +137,7 @@ class Woocommerce extends Base {
126
  }
127
  }
128
  } else {
129
- $order = new \WC_Order( null );
130
  $product_or_variation = $order->get_product_from_item( $item );
131
  }
132
 
@@ -145,7 +156,7 @@ class Woocommerce extends Base {
145
  $title = $product->get_title();
146
  $categories = $this->get_product_categories( $product );
147
  $quantity = isset( $item['quantity'] ) ? $item['quantity'] : 0;
148
- $params = array( 'addEcommerceItem', '' . $sku, $title, $categories, $price, $quantity );
149
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
150
  }
151
 
@@ -156,7 +167,7 @@ class Woocommerce extends Base {
156
  $total = $cart->cart_contents_total;
157
  }
158
 
159
- $tracking_code .= $this->make_matomo_js_tracker_call( array( 'trackEcommerceCartUpdate', $total ) );
160
 
161
  $this->cart_update_queue = $this->wrap_script( $tracking_code );
162
  $this->logger->log( 'Tracked ecommerce cart update: ' . $this->cart_update_queue );
@@ -200,20 +211,20 @@ class Woocommerce extends Base {
200
  $product_details = $this->get_product_details( $order, $item );
201
 
202
  if ( ! empty( $product_details ) ) {
203
- $params = array(
204
  'addEcommerceItem',
205
  '' . $product_details['sku'],
206
  $product_details['title'],
207
  $product_details['categories'],
208
  $product_details['price'],
209
  $product_details['quantity'],
210
- );
211
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
212
  }
213
  }
214
  }
215
 
216
- $params = array(
217
  'trackEcommerceOrder',
218
  '' . $order_id_to_track,
219
  $order->get_total(),
@@ -221,7 +232,7 @@ class Woocommerce extends Base {
221
  $order->get_cart_tax(),
222
  $this->isWC3() ? $order->get_shipping_total() : $order->get_total_shipping(),
223
  $order->get_total_discount(),
224
- );
225
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
226
 
227
  $this->logger->log( sprintf( 'Tracked ecommerce order %s with number %s', $order_id, $order_id_to_track ) );
@@ -239,7 +250,7 @@ class Woocommerce extends Base {
239
  }
240
 
241
  /**
242
- * @param \WC_Product $product
243
  */
244
  private function get_sku( $product ) {
245
  if ( $product && $product->get_sku() ) {
@@ -250,7 +261,7 @@ class Woocommerce extends Base {
250
  }
251
 
252
  /**
253
- * @param \WC_Product $product
254
  */
255
  private function get_product_id( $product ) {
256
  if ( ! $product ) {
@@ -265,7 +276,7 @@ class Woocommerce extends Base {
265
  }
266
 
267
  /**
268
- * @param \WC_Order $order
269
  * @param $item
270
  *
271
  * @return mixed
@@ -273,10 +284,10 @@ class Woocommerce extends Base {
273
  private function get_product_details( $order, $item ) {
274
  $product_or_variation = false;
275
  if ( $this->isWC3() && ! empty( $item ) && is_object( $item ) && method_exists( $item, 'get_product' ) && is_callable(
276
- array(
277
  $item,
278
  'get_product',
279
- )
280
  ) ) {
281
  $product_or_variation = $item->get_product();
282
  } elseif ( method_exists( $order, 'get_product_from_item' ) ) {
@@ -303,17 +314,17 @@ class Woocommerce extends Base {
303
  $categories = $this->get_product_categories( $product );
304
  $quantity = $item['qty'];
305
 
306
- return array(
307
  'sku' => $sku,
308
  'title' => $title,
309
  'categories' => $categories,
310
  'quantity' => $quantity,
311
  'price' => $price,
312
- );
313
  }
314
 
315
  /**
316
- * @param \WC_Product $product
317
  *
318
  * @return array
319
  */
@@ -322,7 +333,7 @@ class Woocommerce extends Base {
322
 
323
  $category_terms = get_the_terms( $product_id, 'product_cat' );
324
 
325
- $categories = array();
326
 
327
  if ( is_wp_error( $category_terms ) ) {
328
  return $categories;
@@ -348,17 +359,17 @@ class Woocommerce extends Base {
348
  return;
349
  }
350
 
351
- /** @var \WC_Product $product */
352
- $params = array(
353
  'setEcommerceView',
354
  $this->get_sku( $product ),
355
  $product->get_title(),
356
  $this->get_product_categories( $product ),
357
  $product->get_price(),
358
- );
359
 
360
  // we're not using wc_enqueue_js eg to prevent sometimes this code from being minified on some JS minifier plugins
 
361
  echo $this->wrap_script( $this->make_matomo_js_tracker_call( $params ) );
362
  }
363
-
364
  }
9
 
10
  namespace WpMatomo\Ecommerce;
11
 
12
+ use WC_Order;
13
+ use WC_Product;
14
+
15
  if ( ! defined( 'ABSPATH' ) ) {
16
  exit; // if accessed directly
17
  }
18
 
19
  class Woocommerce extends Base {
20
+ private $order_status_ignore = [ 'cancelled', 'failed', 'refunded' ];
21
 
22
  public function register_hooks() {
23
  if ( is_admin() ) {
26
 
27
  parent::register_hooks();
28
 
29
+ add_action( 'wp_head', [ $this, 'maybe_track_order_complete' ], 99999 );
30
+ add_action( 'woocommerce_after_single_product', [ $this, 'on_product_view' ], 99999, $args = 0 );
31
+ add_action( 'woocommerce_add_to_cart', [ $this, 'on_cart_updated_safe' ], 99999, 0 );
32
+ add_action( 'woocommerce_cart_item_removed', [ $this, 'on_cart_updated_safe' ], 99999, 0 );
33
+ add_action( 'woocommerce_cart_item_restored', [ $this, 'on_cart_updated_safe' ], 99999, 0 );
34
+ add_action( 'woocommerce_cart_item_set_quantity', [ $this, 'on_cart_updated_safe' ], 99999, 0 );
35
+ add_action( 'woocommerce_thankyou', [ $this, 'anonymise_orderid_in_url' ], 1, 1 );
36
 
37
+ if ( ! $this->should_track_background() ) {
38
  // prevent possibly executing same event twice where eg first a PHP Matomo tracker request is created
39
  // because of woocommerce_applied_coupon and then also because of woocommerce_update_cart_action_cart_updated itself
40
  // causing two tracking requests to be issues from the server. refs #215
41
  // when not ajax mode the later event will simply overwrite the first and it should be fine.
42
+ add_filter(
43
+ 'woocommerce_update_cart_action_cart_updated',
44
+ [
45
+ $this,
46
+ 'on_cart_updated_safe',
47
+ ],
48
+ 99999,
49
+ 1
50
+ );
51
  }
52
 
53
+ add_action( 'woocommerce_applied_coupon', [ $this, 'on_coupon_updated_safe' ], 99999, 0 );
54
+ add_action( 'woocommerce_removed_coupon', [ $this, 'on_coupon_updated_safe' ], 99999, 0 );
55
  }
56
 
57
+ public function anonymise_orderid_in_url( $order_id ) {
58
+ if ( ! empty( $order_id ) && is_numeric( $order_id ) ) {
 
59
  $order_id = (int) $order_id;
60
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
61
  echo "<script>(function () {
62
  if (location.href) {
63
  window._paq = window._paq || [];
77
  if ( function_exists( 'is_order_received_page' ) && is_order_received_page() ) {
78
  $order_id = isset( $wp->query_vars['order-received'] ) ? $wp->query_vars['order-received'] : 0;
79
  if ( ! empty( $order_id ) && $order_id > 0 ) {
80
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
81
  echo $this->on_order( $order_id );
82
  }
83
  }
84
  }
85
 
86
+ public function on_coupon_updated_safe() {
 
87
  try {
88
+ $val = null;
89
+ $val = $this->on_cart_updated( $val, true );
90
+ } catch ( \Exception $e ) {
91
+ $this->logger->log_exception( 'woo_on_cart_update', $e );
92
  }
93
 
94
  return $val;
95
  }
96
 
97
  public function on_cart_updated_safe( $val = null ) {
 
98
  try {
99
+ $val = $this->on_cart_updated( $val );
100
+ } catch ( \Exception $e ) {
101
+ $this->logger->log_exception( 'woo_on_cart_update', $e );
102
  }
103
 
104
  return $val;
115
 
116
  /** @var \WC_Cart $cart */
117
  $cart = $woocommerce->cart;
118
+ if ( ! $is_coupon_update ) {
119
  // can cause cart coupon not to be applied when WooCommerce Subscriptions is used.
120
  $cart->calculate_totals();
121
  }
124
  $tracking_code = '';
125
 
126
  foreach ( $cart_content as $item ) {
127
+ /** @var WC_Product $product */
128
  $product = wc_get_product( $item['product_id'] );
129
 
130
  if ( $this->isWC3() ) {
137
  }
138
  }
139
  } else {
140
+ $order = new WC_Order( null );
141
  $product_or_variation = $order->get_product_from_item( $item );
142
  }
143
 
156
  $title = $product->get_title();
157
  $categories = $this->get_product_categories( $product );
158
  $quantity = isset( $item['quantity'] ) ? $item['quantity'] : 0;
159
+ $params = [ 'addEcommerceItem', '' . $sku, $title, $categories, $price, $quantity ];
160
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
161
  }
162
 
167
  $total = $cart->cart_contents_total;
168
  }
169
 
170
+ $tracking_code .= $this->make_matomo_js_tracker_call( [ 'trackEcommerceCartUpdate', $total ] );
171
 
172
  $this->cart_update_queue = $this->wrap_script( $tracking_code );
173
  $this->logger->log( 'Tracked ecommerce cart update: ' . $this->cart_update_queue );
211
  $product_details = $this->get_product_details( $order, $item );
212
 
213
  if ( ! empty( $product_details ) ) {
214
+ $params = [
215
  'addEcommerceItem',
216
  '' . $product_details['sku'],
217
  $product_details['title'],
218
  $product_details['categories'],
219
  $product_details['price'],
220
  $product_details['quantity'],
221
+ ];
222
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
223
  }
224
  }
225
  }
226
 
227
+ $params = [
228
  'trackEcommerceOrder',
229
  '' . $order_id_to_track,
230
  $order->get_total(),
232
  $order->get_cart_tax(),
233
  $this->isWC3() ? $order->get_shipping_total() : $order->get_total_shipping(),
234
  $order->get_total_discount(),
235
+ ];
236
  $tracking_code .= $this->make_matomo_js_tracker_call( $params );
237
 
238
  $this->logger->log( sprintf( 'Tracked ecommerce order %s with number %s', $order_id, $order_id_to_track ) );
250
  }
251
 
252
  /**
253
+ * @param WC_Product $product
254
  */
255
  private function get_sku( $product ) {
256
  if ( $product && $product->get_sku() ) {
261
  }
262
 
263
  /**
264
+ * @param WC_Product $product
265
  */
266
  private function get_product_id( $product ) {
267
  if ( ! $product ) {
276
  }
277
 
278
  /**
279
+ * @param WC_Order $order
280
  * @param $item
281
  *
282
  * @return mixed
284
  private function get_product_details( $order, $item ) {
285
  $product_or_variation = false;
286
  if ( $this->isWC3() && ! empty( $item ) && is_object( $item ) && method_exists( $item, 'get_product' ) && is_callable(
287
+ [
288
  $item,
289
  'get_product',
290
+ ]
291
  ) ) {
292
  $product_or_variation = $item->get_product();
293
  } elseif ( method_exists( $order, 'get_product_from_item' ) ) {
314
  $categories = $this->get_product_categories( $product );
315
  $quantity = $item['qty'];
316
 
317
+ return [
318
  'sku' => $sku,
319
  'title' => $title,
320
  'categories' => $categories,
321
  'quantity' => $quantity,
322
  'price' => $price,
323
+ ];
324
  }
325
 
326
  /**
327
+ * @param WC_Product $product
328
  *
329
  * @return array
330
  */
333
 
334
  $category_terms = get_the_terms( $product_id, 'product_cat' );
335
 
336
+ $categories = [];
337
 
338
  if ( is_wp_error( $category_terms ) ) {
339
  return $categories;
359
  return;
360
  }
361
 
362
+ /** @var WC_Product $product */
363
+ $params = [
364
  'setEcommerceView',
365
  $this->get_sku( $product ),
366
  $product->get_title(),
367
  $this->get_product_categories( $product ),
368
  $product->get_price(),
369
+ ];
370
 
371
  // we're not using wc_enqueue_js eg to prevent sometimes this code from being minified on some JS minifier plugins
372
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
373
  echo $this->wrap_script( $this->make_matomo_js_tracker_call( $params ) );
374
  }
 
375
  }
classes/WpMatomo/Email.php CHANGED
@@ -8,9 +8,13 @@
8
  */
9
 
10
  namespace WpMatomo;
 
 
11
  use PHPMailer\PHPMailer\PHPMailer;
12
  use Piwik\Common;
13
  use Piwik\Mail;
 
 
14
 
15
  if ( ! defined( 'ABSPATH' ) ) {
16
  exit; // if accessed directly
@@ -19,145 +23,144 @@ if ( ! defined( 'ABSPATH' ) ) {
19
  class Email {
20
 
21
  /**
22
- * @var \WP_Error|null
23
  */
24
- private $wpMailError;
25
 
26
- private $wpContentType = null;
27
- /**
28
- * @var Mail
29
- */
30
  private $mail;
31
 
32
- public function onError($error) {
33
- $this->wpMailError = $error;
34
  }
35
 
36
- public function setContentType($contentType) {
37
- if (!empty($this->wpContentType)) {
38
- return $this->wpContentType;
39
  }
40
 
41
- return $contentType;
42
  }
43
 
44
- public function send(Mail $mail)
45
- {
46
- $this->wpContentType = null;
47
- $this->wpMailError = null;
48
 
49
  $this->mail = $mail;
50
 
51
- if ($mail->getBodyHtml()) {
52
- $content = $mail->getBodyHtml();
53
- $this->wpContentType = 'text/html';
54
- } elseif ($mail->getBodyText()) {
55
- $content = $mail->getBodyText();
56
- $this->wpContentType = 'text/plain';
57
  } else {
58
  // seems no content...
59
- $content = '';
60
  }
61
 
62
- $attachments = $mail->getAttachments();
63
 
64
- $recipients = array_keys($mail->getRecipients());
65
 
66
- $this->sendMailThroughWordPress($recipients, $mail->getSubject(), $content, $attachments);
67
  }
68
 
69
- private function rememberMailSent(){
70
-
71
- $history = \WpMatomo::$settings->get_global_option( 'mail_history' );
72
  if ( empty( $history ) || ! is_array( $history ) ) {
73
- $history = array();
74
  }
75
 
76
  // allows us to see if there is a WP Mail issue or a Matomo issue
77
  array_unshift( $history, gmdate( 'Y-m-d H:i:s', time() ) );
78
  $history = array_slice( $history, 0, 3 ); // keep only the last 3 versions
79
- \WpMatomo::$settings->set_global_option( 'mail_history', $history );
80
- \WpMatomo::$settings->save();
81
  }
82
 
83
- private function sendMailThroughWordPress($recipients, $subject, $content, $attachments) {
 
84
 
85
- $this->wpMailError = null;
 
86
 
87
- add_action( 'wp_mail_failed' , array($this, 'onError') );
88
- add_filter( 'wp_mail_content_type' , array($this, 'setContentType'));
89
-
90
- $this->rememberMailSent();
91
 
92
  $header = '';
93
 
94
- if (!empty($attachments)) {
95
-
96
- $random_id = Common::generateUniqId();
97
- $header = 'X-Matomo: ' . $random_id;
98
- $executed_action = false;
99
-
100
- add_action('phpmailer_init', function ($phpmailer) use ($attachments, $subject, $random_id, &$executed_action) {
101
- /** @var PHPMailer $phpmailer */
102
- if ($executed_action) {
103
- return; // already done, do not execute another time
104
- }
105
- $executed_action = true;
106
- $match = false;
107
- foreach ($phpmailer->getCustomHeaders() as $header) {
108
- if (isset($header[0]) && isset($header[1]) &&
109
- is_string($header[0]) && is_string($header[1]) &&
110
- Common::mb_strtolower($header[0]) === 'x-matomo' && $random_id === trim($header[1])) {
111
- $match = true;
112
- }
113
- }
114
- if (!$match) {
115
- return; // attachments aren't for this mail
116
- }
117
- foreach ($attachments as $attachment) {
118
- if (!empty($attachment['cid'])) {
119
- $phpmailer->addStringEmbeddedImage(
120
- $attachment['content'],
121
- $attachment['cid'],
122
- $attachment['filename'],
123
- PHPMailer::ENCODING_BASE64,
124
- $attachment['mimetype']
125
- );
126
- } else {
127
- $phpmailer->addStringAttachment(
128
- $attachment['content'],
129
- $attachment['filename'],
130
- PHPMailer::ENCODING_BASE64,
131
- $attachment['mimetype']
132
- );
133
- }
134
- }
135
- });
136
- }
 
 
 
137
 
138
  $success = wp_mail( $recipients, $subject, $content, $header );
139
 
140
- remove_action( 'wp_mail_failed', array($this, 'onError') );
141
- remove_filter('wp_mail_content_type', array($this, 'setContentType'));
142
 
143
- if (!$success) {
144
  $message = 'Error unknown.';
145
- if (!empty($this->wpMailError) && is_object($this->wpMailError) && $this->wpMailError instanceof \WP_Error) {
146
- $message = $this->wpMailError->get_error_message();
147
  }
148
- if ($this->mail && $this->mail->getAttachments()) {
149
  $message .= ' (has attachments)';
150
  }
151
- if ($this->wpContentType) {
152
- $message .= ' (type '. $this->wpContentType .')';
153
  }
154
  $logger = new Logger();
155
- $logger->log_exception('mail_error', new \Exception($message));
156
- $logger->log('Matomo mail failed with subject '. $subject . ': ' . $message);
157
  }
158
 
159
- $this->wpContentType = null;
160
- $this->wpMailError = null;
161
  }
162
-
163
  }
8
  */
9
 
10
  namespace WpMatomo;
11
+
12
+ use Exception;
13
  use PHPMailer\PHPMailer\PHPMailer;
14
  use Piwik\Common;
15
  use Piwik\Mail;
16
+ use WP_Error;
17
+ use WpMatomo;
18
 
19
  if ( ! defined( 'ABSPATH' ) ) {
20
  exit; // if accessed directly
23
  class Email {
24
 
25
  /**
26
+ * @var WP_Error|null
27
  */
28
+ private $wp_email_error;
29
 
30
+ private $wp_content_type = null;
31
+ /**
32
+ * @var Mail
33
+ */
34
  private $mail;
35
 
36
+ public function on_error( $error ) {
37
+ $this->wp_email_error = $error;
38
  }
39
 
40
+ public function set_content_type( $content_type ) {
41
+ if ( ! empty( $this->wp_content_type ) ) {
42
+ return $this->wp_content_type;
43
  }
44
 
45
+ return $content_type;
46
  }
47
 
48
+ public function send( Mail $mail ) {
49
+ $this->wp_content_type = null;
50
+ $this->wp_email_error = null;
 
51
 
52
  $this->mail = $mail;
53
 
54
+ if ( $mail->getBodyHtml() ) {
55
+ $content = $mail->getBodyHtml();
56
+ $this->wp_content_type = 'text/html';
57
+ } elseif ( $mail->getBodyText() ) {
58
+ $content = $mail->getBodyText();
59
+ $this->wp_content_type = 'text/plain';
60
  } else {
61
  // seems no content...
62
+ $content = '';
63
  }
64
 
65
+ $attachments = $mail->getAttachments();
66
 
67
+ $recipients = array_keys( $mail->getRecipients() );
68
 
69
+ $this->send_mail_through_wordpress( $recipients, $mail->getSubject(), $content, $attachments );
70
  }
71
 
72
+ private function remember_mail_sent() {
73
+ $history = WpMatomo::$settings->get_global_option( 'mail_history' );
 
74
  if ( empty( $history ) || ! is_array( $history ) ) {
75
+ $history = [];
76
  }
77
 
78
  // allows us to see if there is a WP Mail issue or a Matomo issue
79
  array_unshift( $history, gmdate( 'Y-m-d H:i:s', time() ) );
80
  $history = array_slice( $history, 0, 3 ); // keep only the last 3 versions
81
+ WpMatomo::$settings->set_global_option( 'mail_history', $history );
82
+ WpMatomo::$settings->save();
83
  }
84
 
85
+ private function send_mail_through_wordpress( $recipients, $subject, $content, $attachments ) {
86
+ $this->wp_email_error = null;
87
 
88
+ add_action( 'wp_mail_failed', [ $this, 'on_error' ] );
89
+ add_filter( 'wp_mail_content_type', [ $this, 'set_content_type' ] );
90
 
91
+ $this->remember_mail_sent();
 
 
 
92
 
93
  $header = '';
94
 
95
+ if ( ! empty( $attachments ) ) {
96
+ $random_id = Common::generateUniqId();
97
+ $header = 'X-Matomo: ' . $random_id;
98
+ $executed_action = false;
99
+
100
+ add_action(
101
+ 'phpmailer_init',
102
+ function ( $phpmailer ) use ( $attachments, $subject, $random_id, &$executed_action ) {
103
+ /** @var PHPMailer $phpmailer */
104
+ if ( $executed_action ) {
105
+ return; // already done, do not execute another time
106
+ }
107
+ $executed_action = true;
108
+ $match = false;
109
+ foreach ( $phpmailer->getCustomHeaders() as $header ) {
110
+ if ( isset( $header[0] ) && isset( $header[1] ) &&
111
+ is_string( $header[0] ) && is_string( $header[1] ) &&
112
+ 'x-matomo' === Common::mb_strtolower( $header[0] ) &&
113
+ trim( $header[1] ) === $random_id ) {
114
+ $match = true;
115
+ }
116
+ }
117
+ if ( ! $match ) {
118
+ return; // attachments aren't for this mail
119
+ }
120
+ foreach ( $attachments as $attachment ) {
121
+ if ( ! empty( $attachment['cid'] ) ) {
122
+ $phpmailer->addStringEmbeddedImage(
123
+ $attachment['content'],
124
+ $attachment['cid'],
125
+ $attachment['filename'],
126
+ PHPMailer::ENCODING_BASE64,
127
+ $attachment['mimetype']
128
+ );
129
+ } else {
130
+ $phpmailer->addStringAttachment(
131
+ $attachment['content'],
132
+ $attachment['filename'],
133
+ PHPMailer::ENCODING_BASE64,
134
+ $attachment['mimetype']
135
+ );
136
+ }
137
+ }
138
+ }
139
+ );
140
+ }
141
 
142
  $success = wp_mail( $recipients, $subject, $content, $header );
143
 
144
+ remove_action( 'wp_mail_failed', [ $this, 'on_error' ] );
145
+ remove_filter( 'wp_mail_content_type', [ $this, 'set_content_type' ] );
146
 
147
+ if ( ! $success ) {
148
  $message = 'Error unknown.';
149
+ if ( ! empty( $this->wp_email_error ) && is_object( $this->wp_email_error ) && $this->wp_email_error instanceof WP_Error ) {
150
+ $message = $this->wp_email_error->get_error_message();
151
  }
152
+ if ( $this->mail && $this->mail->getAttachments() ) {
153
  $message .= ' (has attachments)';
154
  }
155
+ if ( $this->wp_content_type ) {
156
+ $message .= ' (type ' . $this->wp_content_type . ')';
157
  }
158
  $logger = new Logger();
159
+ $logger->log_exception( 'mail_error', new Exception( $message ) );
160
+ $logger->log( 'Matomo mail failed with subject ' . $subject . ': ' . $message );
161
  }
162
 
163
+ $this->wp_content_type = null;
164
+ $this->wp_email_error = null;
165
  }
 
166
  }
classes/WpMatomo/Installer.php CHANGED
@@ -9,6 +9,8 @@
9
 
10
  namespace WpMatomo;
11
 
 
 
12
  use Piwik\Common;
13
  use Piwik\Config;
14
  use Piwik\Container\StaticContainer;
@@ -16,6 +18,7 @@ use Piwik\DbHelper;
16
  use Piwik\Exception\NotYetInstalledException;
17
  use Piwik\Plugin\API as PluginApi;
18
  use Piwik\SettingsPiwik;
 
19
  use WpMatomo\Site\Sync;
20
 
21
  if ( ! defined( 'ABSPATH' ) ) {
@@ -23,8 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) {
23
  }
24
 
25
  class Installer {
26
-
27
- const OPTION_NAME_INSTALL_DATE = 'matomo-install-date';
28
  const OPTION_NAME_INSTALL_VERSION = 'matomo-install-version';
29
 
30
  /**
@@ -43,7 +45,7 @@ class Installer {
43
  }
44
 
45
  public function register_hooks() {
46
- add_action( 'activate_matomo', array( $this, 'install' ) );
47
  }
48
 
49
  public function looks_like_it_is_installed() {
@@ -65,9 +67,11 @@ class Installer {
65
  return SettingsPiwik::isMatomoInstalled();
66
  } catch ( NotYetInstalledException $e ) {
67
  // not yet installed.... we will need to install it
 
 
 
 
68
  }
69
-
70
- return false;
71
  }
72
 
73
  public function can_be_installed() {
@@ -85,6 +89,7 @@ class Installer {
85
  try {
86
  // prevent session related errors during install making it more stable
87
  if ( ! defined( 'PIWIK_ENABLE_SESSION_START' ) ) {
 
88
  define( 'PIWIK_ENABLE_SESSION_START', false );
89
  }
90
 
@@ -113,9 +118,9 @@ class Installer {
113
  // also to set up all the other users
114
  wp_schedule_single_event( time() + 45, ScheduledTasks::EVENT_SYNC );
115
 
116
- update_option(self::OPTION_NAME_INSTALL_DATE, time());
117
  $plugin_data = get_plugin_data( MATOMO_ANALYTICS_FILE, $markup = false, $translate = false );
118
- if ( ! empty( $plugin_data['Version'] )) {
119
  update_option( self::OPTION_NAME_INSTALL_VERSION, $plugin_data['Version'] );
120
  }
121
 
@@ -127,7 +132,7 @@ class Installer {
127
  $this->logger->log( 'Matomo will now init the environment' );
128
  $environment = new \Piwik\Application\Environment( null );
129
  $environment->init();
130
- } catch ( \Exception $e ) {
131
  $this->logger->log( 'Ignoring error environment init' );
132
  $this->logger->log_exception( 'install_env_init', $e );
133
  }
@@ -138,7 +143,7 @@ class Installer {
138
  \Piwik\FrontController::unsetInstance(); // make sure we're loading the latest instance
139
  $controller = \Piwik\FrontController::getInstance();
140
  $controller->init();
141
- } catch ( \Exception $e ) {
142
  $this->logger->log( 'Ignoring error frontcontroller init' );
143
  $this->logger->log_exception( 'install_front_init', $e );
144
  }
@@ -147,14 +152,14 @@ class Installer {
147
  // sync user now again after installing plugins...
148
  // before eg the users_language table would not have been available yet
149
  $this->create_user();
150
- } catch ( \Exception $e ) {
151
  $this->logger->log_exception( 'install_create_user', $e );
152
  }
153
 
154
  try {
155
  // update plugins if there are any
156
  $this->update_components();
157
- } catch ( \Exception $e ) {
158
  $this->logger->log_exception( 'install_update_comp', $e );
159
  }
160
 
@@ -166,9 +171,9 @@ class Installer {
166
 
167
  $this->logger->log( 'Emptying some caches' );
168
 
169
- \Piwik\Singleton::clearAll();
170
  PluginApi::unsetAllInstances();
171
- \Piwik\Cache::flushAll();
172
 
173
  $this->logger->log( 'Matomo install finished' );
174
  }
@@ -176,38 +181,37 @@ class Installer {
176
  return true;
177
  }
178
 
179
- public function set_matomo_url()
180
- {
181
  // note that the full url might not be possible to be set if the cron is executed on cli and it maybe doesn't have
182
  // the host or if a plugin overwrites the constant of WP_PLUGIN_URL which is used in plugins_url() to not include domain
183
  // see https://www.google.com/url?q=https://wordpress.org/support/topic/no-metrics-showing/%23topic-14362043-replies&source=gmail&ust=1620409922890000&usg=AFQjCNHyzG5-9v0A8bjg8aLVVbYSWxkTxg
184
 
185
- $matomo_url = SettingsPiwik::getPiwikUrl();
186
  $plugins_url = plugins_url( 'app', MATOMO_ANALYTICS_FILE );
187
  // need to make sure to update plugins url if it changes eg if installed somewhere else or domain changes
188
 
189
- if ($matomo_url
190
- && $plugins_url === $matomo_url
191
- && parse_url($matomo_url, PHP_URL_SCHEME)
192
- && parse_url($matomo_url, PHP_URL_HOST)
193
  ) {
194
  // if currently no scheme or host is set then we'll make sure to overwrite it
195
  return;
196
  }
197
 
198
- if (!$plugins_url) {
199
  return;
200
  }
201
 
202
- $has_host = parse_url($plugins_url, PHP_URL_HOST);
203
 
204
- if (!$has_host) {
205
  return;
206
  }
207
 
208
- $has_scheme = parse_url($plugins_url, PHP_URL_SCHEME);
209
 
210
- if (!$has_scheme) {
211
  return;
212
  }
213
 
@@ -226,16 +230,16 @@ class Installer {
226
 
227
  try {
228
  $db_infos = self::get_db_infos();
229
- $config = Config::getInstance();
230
- if (isset($config)) {
231
- $db_infos = array_merge($config->database, $db_infos);
232
  }
233
  $config->database = $db_infos;
234
 
235
  DbHelper::checkDatabaseVersion();
236
- } catch ( \Exception $e ) {
237
  $message = sprintf( 'Database info detection failed with %s in %s:%s.', $e->getMessage(), $e->getFile(), $e->getLine() );
238
- throw new \Exception( $message, $e->getCode(), $e );
239
  }
240
 
241
  $tables_installed = DbHelper::getTablesInstalled();
@@ -254,29 +258,34 @@ class Installer {
254
  private function create_config( $db_info ) {
255
  $this->logger->log( 'Matomo is now creating the config' );
256
  $domain = home_url();
257
- $general = array(
258
- 'trusted_hosts' => array( $domain ),
259
  'salt' => Common::generateUniqId(),
260
- );
261
  $config = Config::getInstance();
262
  $path = $config->getLocalPath();
263
  if ( ! is_dir( dirname( $path ) ) ) {
264
  wp_mkdir_p( dirname( $path ) );
265
  }
266
- $db_default = array();
267
- $general_default = array();
268
  if ( $config->database ) {
269
  $db_default = $config->database;
270
  }
 
271
  if ( $config->General ) {
 
272
  $general_default = $config->General;
273
  }
274
  $config->database = array_merge( $db_default, $db_info );
275
- $config->General = array_merge( $general_default, $general );
 
276
  $config->forceSave();
277
 
278
  $mode = 0664;
279
- @chmod( $config->getLocalPath(), $mode );
 
 
280
  }
281
 
282
  private function create_website() {
@@ -293,33 +302,34 @@ class Installer {
293
 
294
  /**
295
  * @param array $default params
 
296
  * @return array
297
  */
298
- public static function get_db_infos( $default = array() ) {
299
  global $wpdb;
300
 
301
- $socket = '';
302
  $host_data = null;
303
- $host = null;
304
- $port = 3306;
305
- if (method_exists($wpdb, 'parse_db_host')) {
306
  // WP 4.9+
307
  $host_data = $wpdb->parse_db_host( DB_HOST );
308
- if ($host_data) {
309
  list( $host, $port, $socket, $is_ipv6 ) = $host_data;
310
- if (!$port && !$socket) {
311
  $port = 3306;
312
  }
313
  }
314
  }
315
 
316
- if (!$host_data || !$host) {
317
  // WP 4.8 and older
318
- // in case DB credentials change in wordpress, we need to apply these changes here as well on demand
319
- $hostParts = explode(':', DB_HOST);
320
- $host = $hostParts[0];
321
- if (count($hostParts) === 2 && is_numeric($hostParts[1])) {
322
- $port = $hostParts[1];
323
  } else {
324
  $port = 3306;
325
  }
@@ -327,20 +337,20 @@ class Installer {
327
 
328
  $charset = $wpdb->charset ? $wpdb->charset : 'utf8';
329
 
330
- $database = array(
331
- 'host' => $host,
332
- 'port' => $port,
333
- 'username' => DB_USER,
334
- 'password' => DB_PASSWORD,
335
- 'dbname' => DB_NAME,
336
- 'charset' => $charset,
337
  'tables_prefix' => $wpdb->prefix . MATOMO_DATABASE_PREFIX,
338
- 'adapter' => 'WordPress',
339
- );
340
- if (!empty($socket)) {
341
  $database['unix_socket'] = $socket;
342
  }
343
- $database = array_merge($default, $database);
344
 
345
  return $database;
346
  }
@@ -351,5 +361,4 @@ class Installer {
351
  $updater = new Updater( $this->settings );
352
  $updater->update();
353
  }
354
-
355
  }
9
 
10
  namespace WpMatomo;
11
 
12
+ use Exception;
13
+ use Piwik\Cache;
14
  use Piwik\Common;
15
  use Piwik\Config;
16
  use Piwik\Container\StaticContainer;
18
  use Piwik\Exception\NotYetInstalledException;
19
  use Piwik\Plugin\API as PluginApi;
20
  use Piwik\SettingsPiwik;
21
+ use Piwik\Singleton;
22
  use WpMatomo\Site\Sync;
23
 
24
  if ( ! defined( 'ABSPATH' ) ) {
26
  }
27
 
28
  class Installer {
29
+ const OPTION_NAME_INSTALL_DATE = 'matomo-install-date';
 
30
  const OPTION_NAME_INSTALL_VERSION = 'matomo-install-version';
31
 
32
  /**
45
  }
46
 
47
  public function register_hooks() {
48
+ add_action( 'activate_matomo', [ $this, 'install' ] );
49
  }
50
 
51
  public function looks_like_it_is_installed() {
67
  return SettingsPiwik::isMatomoInstalled();
68
  } catch ( NotYetInstalledException $e ) {
69
  // not yet installed.... we will need to install it
70
+ return false;
71
+ } catch ( \Zend_Db_Statement_Exception $e ) {
72
+ // not yet installed.... we will need to install it
73
+ return false;
74
  }
 
 
75
  }
76
 
77
  public function can_be_installed() {
89
  try {
90
  // prevent session related errors during install making it more stable
91
  if ( ! defined( 'PIWIK_ENABLE_SESSION_START' ) ) {
92
+ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
93
  define( 'PIWIK_ENABLE_SESSION_START', false );
94
  }
95
 
118
  // also to set up all the other users
119
  wp_schedule_single_event( time() + 45, ScheduledTasks::EVENT_SYNC );
120
 
121
+ update_option( self::OPTION_NAME_INSTALL_DATE, time() );
122
  $plugin_data = get_plugin_data( MATOMO_ANALYTICS_FILE, $markup = false, $translate = false );
123
+ if ( ! empty( $plugin_data['Version'] ) ) {
124
  update_option( self::OPTION_NAME_INSTALL_VERSION, $plugin_data['Version'] );
125
  }
126
 
132
  $this->logger->log( 'Matomo will now init the environment' );
133
  $environment = new \Piwik\Application\Environment( null );
134
  $environment->init();
135
+ } catch ( Exception $e ) {
136
  $this->logger->log( 'Ignoring error environment init' );
137
  $this->logger->log_exception( 'install_env_init', $e );
138
  }
143
  \Piwik\FrontController::unsetInstance(); // make sure we're loading the latest instance
144
  $controller = \Piwik\FrontController::getInstance();
145
  $controller->init();
146
+ } catch ( Exception $e ) {
147
  $this->logger->log( 'Ignoring error frontcontroller init' );
148
  $this->logger->log_exception( 'install_front_init', $e );
149
  }
152
  // sync user now again after installing plugins...
153
  // before eg the users_language table would not have been available yet
154
  $this->create_user();
155
+ } catch ( Exception $e ) {
156
  $this->logger->log_exception( 'install_create_user', $e );
157
  }
158
 
159
  try {
160
  // update plugins if there are any
161
  $this->update_components();
162
+ } catch ( Exception $e ) {
163
  $this->logger->log_exception( 'install_update_comp', $e );
164
  }
165
 
171
 
172
  $this->logger->log( 'Emptying some caches' );
173
 
174
+ Singleton::clearAll();
175
  PluginApi::unsetAllInstances();
176
+ Cache::flushAll();
177
 
178
  $this->logger->log( 'Matomo install finished' );
179
  }
181
  return true;
182
  }
183
 
184
+ public function set_matomo_url() {
 
185
  // note that the full url might not be possible to be set if the cron is executed on cli and it maybe doesn't have
186
  // the host or if a plugin overwrites the constant of WP_PLUGIN_URL which is used in plugins_url() to not include domain
187
  // see https://www.google.com/url?q=https://wordpress.org/support/topic/no-metrics-showing/%23topic-14362043-replies&source=gmail&ust=1620409922890000&usg=AFQjCNHyzG5-9v0A8bjg8aLVVbYSWxkTxg
188
 
189
+ $matomo_url = SettingsPiwik::getPiwikUrl();
190
  $plugins_url = plugins_url( 'app', MATOMO_ANALYTICS_FILE );
191
  // need to make sure to update plugins url if it changes eg if installed somewhere else or domain changes
192
 
193
+ if ( $matomo_url
194
+ && $plugins_url === $matomo_url
195
+ && wp_parse_url( $matomo_url, PHP_URL_SCHEME )
196
+ && wp_parse_url( $matomo_url, PHP_URL_HOST )
197
  ) {
198
  // if currently no scheme or host is set then we'll make sure to overwrite it
199
  return;
200
  }
201
 
202
+ if ( ! $plugins_url ) {
203
  return;
204
  }
205
 
206
+ $has_host = wp_parse_url( $plugins_url, PHP_URL_HOST );
207
 
208
+ if ( ! $has_host ) {
209
  return;
210
  }
211
 
212
+ $has_scheme = wp_parse_url( $plugins_url, PHP_URL_SCHEME );
213
 
214
+ if ( ! $has_scheme ) {
215
  return;
216
  }
217
 
230
 
231
  try {
232
  $db_infos = self::get_db_infos();
233
+ $config = Config::getInstance();
234
+ if ( isset( $config ) ) {
235
+ $db_infos = array_merge( $config->database, $db_infos );
236
  }
237
  $config->database = $db_infos;
238
 
239
  DbHelper::checkDatabaseVersion();
240
+ } catch ( Exception $e ) {
241
  $message = sprintf( 'Database info detection failed with %s in %s:%s.', $e->getMessage(), $e->getFile(), $e->getLine() );
242
+ throw new Exception( $message, $e->getCode(), $e );
243
  }
244
 
245
  $tables_installed = DbHelper::getTablesInstalled();
258
  private function create_config( $db_info ) {
259
  $this->logger->log( 'Matomo is now creating the config' );
260
  $domain = home_url();
261
+ $general = [
262
+ 'trusted_hosts' => [ $domain ],
263
  'salt' => Common::generateUniqId(),
264
+ ];
265
  $config = Config::getInstance();
266
  $path = $config->getLocalPath();
267
  if ( ! is_dir( dirname( $path ) ) ) {
268
  wp_mkdir_p( dirname( $path ) );
269
  }
270
+ $db_default = [];
271
+ $general_default = [];
272
  if ( $config->database ) {
273
  $db_default = $config->database;
274
  }
275
+ // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
276
  if ( $config->General ) {
277
+ // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
278
  $general_default = $config->General;
279
  }
280
  $config->database = array_merge( $db_default, $db_info );
281
+ // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
282
+ $config->General = array_merge( $general_default, $general );
283
  $config->forceSave();
284
 
285
  $mode = 0664;
286
+ if ( ! chmod( $config->getLocalPath(), $mode ) ) {
287
+ $this->logger->log( "Can't chmod " . $config->getLocalPath() );
288
+ }
289
  }
290
 
291
  private function create_website() {
302
 
303
  /**
304
  * @param array $default params
305
+ *
306
  * @return array
307
  */
308
+ public static function get_db_infos( $default = [] ) {
309
  global $wpdb;
310
 
311
+ $socket = '';
312
  $host_data = null;
313
+ $host = null;
314
+ $port = 3306;
315
+ if ( method_exists( $wpdb, 'parse_db_host' ) ) {
316
  // WP 4.9+
317
  $host_data = $wpdb->parse_db_host( DB_HOST );
318
+ if ( $host_data ) {
319
  list( $host, $port, $socket, $is_ipv6 ) = $host_data;
320
+ if ( ! $port && ! $socket ) {
321
  $port = 3306;
322
  }
323
  }
324
  }
325
 
326
+ if ( ! $host_data || ! $host ) {
327
  // WP 4.8 and older
328
+ // in case DB credentials change in WordPress, we need to apply these changes here as well on demand
329
+ $host_parts = explode( ':', DB_HOST );
330
+ $host = $host_parts[0];
331
+ if ( count( $host_parts ) === 2 && is_numeric( $host_parts[1] ) ) {
332
+ $port = $host_parts[1];
333
  } else {
334
  $port = 3306;
335
  }
337
 
338
  $charset = $wpdb->charset ? $wpdb->charset : 'utf8';
339
 
340
+ $database = [
341
+ 'host' => $host,
342
+ 'port' => $port,
343
+ 'username' => DB_USER,
344
+ 'password' => DB_PASSWORD,
345
+ 'dbname' => DB_NAME,
346
+ 'charset' => $charset,
347
  'tables_prefix' => $wpdb->prefix . MATOMO_DATABASE_PREFIX,
348
+ 'adapter' => 'WordPress',
349
+ ];
350
+ if ( ! empty( $socket ) ) {
351
  $database['unix_socket'] = $socket;
352
  }
353
+ $database = array_merge( $default, $database );
354
 
355
  return $database;
356
  }
361
  $updater = new Updater( $this->settings );
362
  $updater->update();
363
  }
 
364
  }
classes/WpMatomo/Logger.php CHANGED
@@ -16,26 +16,26 @@ if ( ! defined( 'ABSPATH' ) ) {
16
  }
17
 
18
  class Logger {
19
- const LEVEL_NONE = 99;
20
  const LEVEL_DEBUG = 1;
21
- const LEVEL_INFO = 3;
22
 
23
- private function get_log_level()
24
- {
25
- if ( defined('MATOMO_DEBUG')) {
26
- if (MATOMO_DEBUG) {
27
  return self::LEVEL_DEBUG;
28
  }
 
29
  return self::LEVEL_NONE;
30
  }
31
 
32
  return self::LEVEL_INFO;
33
  }
34
 
35
- public function log( $message , $mode = 3) {
36
  $log_level = $this->get_log_level();
37
 
38
- if ($log_level > $mode) {
39
  return;
40
  }
41
 
@@ -55,12 +55,12 @@ class Logger {
55
  private function persist( $key, $message ) {
56
  $id = $this->make_id();
57
  $logs = $this->get_last_logged_entries();
58
- $logs[] = array(
59
  'name' => $key,
60
  'value' => time(),
61
  'comment' => $message,
62
- );
63
- $logs = array_slice( $logs, -6 );
64
  update_option( $id, $logs );
65
  }
66
 
@@ -72,7 +72,7 @@ class Logger {
72
  $id = $this->make_id();
73
  $logs = get_option( $id );
74
  if ( empty( $logs ) ) {
75
- $logs = array();
76
  }
77
 
78
  // remove any entry older than 1 week
@@ -80,9 +80,11 @@ class Logger {
80
  $logs,
81
  function ( $log ) {
82
  $one_week_seconds = 604800;
 
83
  return ! empty( $log['value'] ) && is_numeric( $log['value'] ) && ( time() - $log['value'] ) <= $one_week_seconds;
84
  }
85
  );
 
86
  return $logs;
87
  }
88
 
@@ -104,10 +106,9 @@ class Logger {
104
  }
105
 
106
  public function log_exception( $key, Exception $e ) {
107
- $trace = $this->get_readable_trace($e);
108
  $message = $e->getMessage() . ' => ' . $trace;
109
  $this->log( 'Matomo error: ' . $message );
110
  $this->persist( $key, $message );
111
  }
112
-
113
  }
16
  }
17
 
18
  class Logger {
19
+ const LEVEL_NONE = 99;
20
  const LEVEL_DEBUG = 1;
21
+ const LEVEL_INFO = 3;
22
 
23
+ private function get_log_level() {
24
+ if ( defined( 'MATOMO_DEBUG' ) ) {
25
+ if ( MATOMO_DEBUG ) {
 
26
  return self::LEVEL_DEBUG;
27
  }
28
+
29
  return self::LEVEL_NONE;
30
  }
31
 
32
  return self::LEVEL_INFO;
33
  }
34
 
35
+ public function log( $message, $mode = 3 ) {
36
  $log_level = $this->get_log_level();
37
 
38
+ if ( $log_level > $mode ) {
39
  return;
40
  }
41
 
55
  private function persist( $key, $message ) {
56
  $id = $this->make_id();
57
  $logs = $this->get_last_logged_entries();
58
+ $logs[] = [
59
  'name' => $key,
60
  'value' => time(),
61
  'comment' => $message,
62
+ ];
63
+ $logs = array_slice( $logs, - 6 );
64
  update_option( $id, $logs );
65
  }
66
 
72
  $id = $this->make_id();
73
  $logs = get_option( $id );
74
  if ( empty( $logs ) ) {
75
+ $logs = [];
76
  }
77
 
78
  // remove any entry older than 1 week
80
  $logs,
81
  function ( $log ) {
82
  $one_week_seconds = 604800;
83
+
84
  return ! empty( $log['value'] ) && is_numeric( $log['value'] ) && ( time() - $log['value'] ) <= $one_week_seconds;
85
  }
86
  );
87
+
88
  return $logs;
89
  }
90
 
106
  }
107
 
108
  public function log_exception( $key, Exception $e ) {
109
+ $trace = $this->get_readable_trace( $e );
110
  $message = $e->getMessage() . ' => ' . $trace;
111
  $this->log( 'Matomo error: ' . $message );
112
  $this->persist( $key, $message );
113
  }
 
114
  }
classes/WpMatomo/OptOut.php CHANGED
@@ -11,83 +11,103 @@ namespace WpMatomo;
11
 
12
  use Piwik\Piwik;
13
  use Piwik\Plugins\PrivacyManager\DoNotTrackHeaderChecker;
 
14
 
15
  if ( ! defined( 'ABSPATH' ) ) {
16
  exit; // if accessed directly
17
  }
18
 
19
  class OptOut {
20
-
21
  private $language = null;
22
 
23
  public function register_hooks() {
24
  add_shortcode( 'matomo_opt_out', array( $this, 'show_opt_out' ) );
25
  add_action( 'wp_enqueue_scripts', array( $this, 'load_scripts' ) );
 
26
  }
27
 
28
  public function load_scripts() {
29
- if (!is_admin()) {
30
- wp_register_script( 'matomo_opt_out_js', plugins_url( 'assets/js/optout.js', MATOMO_ANALYTICS_FILE ), array(), null, true );
31
  }
32
  }
33
-
34
- private function translate($id)
35
- {
36
- return esc_html(Piwik::translate($id, array(), $this->language));
37
  }
38
 
39
  public function show_opt_out( $atts ) {
40
  $a = shortcode_atts(
41
- array(
42
  'language' => null,
43
- ),
44
  $atts
45
  );
46
- if (!empty($a['language']) && strlen($a['language']) < 6) {
47
  $this->language = $a['language'];
48
  }
49
 
50
  try {
51
  Bootstrap::do_bootstrap();
52
- } catch (\Throwable $e ) {
53
  $logger = new Logger();
54
- $logger->log_exception('optout', $e);
 
55
  return '<p>An error occurred. Please check Matomo system report in WP-Admin.</p>';
56
  }
57
 
58
  $dnt_checker = new DoNotTrackHeaderChecker();
59
  $dnt_enabled = $dnt_checker->isDoNotTrackFound();
60
 
61
- if (!empty($dnt_enabled)) {
62
- return '<p>'. $this->translate('CoreAdminHome_OptOutDntFound').'</p>';
63
  }
64
 
65
  wp_enqueue_script( 'matomo_opt_out_js' );
66
 
67
- $track_visits = empty($_COOKIE['mtm_consent_removed']);
68
 
69
- $style_tracking_enabled = '';
70
  $style_tracking_disabled = '';
71
- $checkbox_attr = '';
72
- if ($track_visits) {
73
  $style_tracking_enabled = 'style="display:none;"';
74
- $checkbox_attr = 'checked="checked"';
75
  } else {
76
  $style_tracking_disabled = 'style="display:none;"';
77
  }
78
 
79
- $content = '<p id="matomo_opted_out_intro" ' . $style_tracking_enabled . '>' . $this->translate('CoreAdminHome_OptOutComplete') . ' ' . $this->translate('CoreAdminHome_OptOutCompleteBis') . '</p>';
80
- $content .= '<p id="matomo_opted_in_intro" ' .$style_tracking_disabled . '>' . $this->translate('CoreAdminHome_YouMayOptOut2') . ' ' . $this->translate('CoreAdminHome_YouMayOptOut3') . '</p>';
81
 
82
  $content .= '<form>
83
- <input type="checkbox" id="matomo_optout_checkbox" '.$checkbox_attr.'/>
84
  <label for="matomo_optout_checkbox"><strong>
85
- <span id="matomo_opted_in_label" '.$style_tracking_disabled.'>'.$this->translate('CoreAdminHome_YouAreNotOptedOut') .' ' . $this->translate('CoreAdminHome_UncheckToOptOut') . '</span>
86
- <span id="matomo_opted_out_label" '.$style_tracking_enabled.'>'.$this->translate('CoreAdminHome_YouAreOptedOut') .' ' . $this->translate('CoreAdminHome_CheckToOptIn') . '</span>
87
  </strong></label></form>';
88
  $content .= '<noscript><p><strong style="color: #ff0000;">This opt out feature requires JavaScript.</strong></p></noscript>';
89
- $content .= '<p id="matomo_outout_err_cookies" style="display: none;"><strong>' . $this->translate('CoreAdminHome_OptOutErrorNoCookies') . '</strong></p>';
 
90
  return $content;
91
  }
92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  }
11
 
12
  use Piwik\Piwik;
13
  use Piwik\Plugins\PrivacyManager\DoNotTrackHeaderChecker;
14
+ use Throwable;
15
 
16
  if ( ! defined( 'ABSPATH' ) ) {
17
  exit; // if accessed directly
18
  }
19
 
20
  class OptOut {
 
21
  private $language = null;
22
 
23
  public function register_hooks() {
24
  add_shortcode( 'matomo_opt_out', array( $this, 'show_opt_out' ) );
25
  add_action( 'wp_enqueue_scripts', array( $this, 'load_scripts' ) );
26
+ add_action( 'init', [$this, 'load_block'] );
27
  }
28
 
29
  public function load_scripts() {
30
+ if ( ! is_admin() ) {
31
+ wp_register_script( 'matomo_opt_out_js', plugins_url( 'assets/js/optout.js', MATOMO_ANALYTICS_FILE ), [], 1, true );
32
  }
33
  }
34
+
35
+ private function translate( $id ) {
36
+ return esc_html( Piwik::translate( $id, [], $this->language ) );
 
37
  }
38
 
39
  public function show_opt_out( $atts ) {
40
  $a = shortcode_atts(
41
+ [
42
  'language' => null,
43
+ ],
44
  $atts
45
  );
46
+ if ( ! empty( $a['language'] ) && strlen( $a['language'] ) < 6 ) {
47
  $this->language = $a['language'];
48
  }
49
 
50
  try {
51
  Bootstrap::do_bootstrap();
52
+ } catch ( Throwable $e ) {
53
  $logger = new Logger();
54
+ $logger->log_exception( 'optout', $e );
55
+
56
  return '<p>An error occurred. Please check Matomo system report in WP-Admin.</p>';
57
  }
58
 
59
  $dnt_checker = new DoNotTrackHeaderChecker();
60
  $dnt_enabled = $dnt_checker->isDoNotTrackFound();
61
 
62
+ if ( ! empty( $dnt_enabled ) ) {
63
+ return '<p>' . $this->translate( 'CoreAdminHome_OptOutDntFound' ) . '</p>';
64
  }
65
 
66
  wp_enqueue_script( 'matomo_opt_out_js' );
67
 
68
+ $track_visits = empty( $_COOKIE['mtm_consent_removed'] );
69
 
70
+ $style_tracking_enabled = '';
71
  $style_tracking_disabled = '';
72
+ $checkbox_attr = '';
73
+ if ( $track_visits ) {
74
  $style_tracking_enabled = 'style="display:none;"';
75
+ $checkbox_attr = 'checked="checked"';
76
  } else {
77
  $style_tracking_disabled = 'style="display:none;"';
78
  }
79
 
80
+ $content = '<p id="matomo_opted_out_intro" ' . $style_tracking_enabled . '>' . $this->translate( 'CoreAdminHome_OptOutComplete' ) . ' ' . $this->translate( 'CoreAdminHome_OptOutCompleteBis' ) . '</p>';
81
+ $content .= '<p id="matomo_opted_in_intro" ' . $style_tracking_disabled . '>' . $this->translate( 'CoreAdminHome_YouMayOptOut2' ) . ' ' . $this->translate( 'CoreAdminHome_YouMayOptOut3' ) . '</p>';
82
 
83
  $content .= '<form>
84
+ <input type="checkbox" id="matomo_optout_checkbox" ' . $checkbox_attr . '/>
85
  <label for="matomo_optout_checkbox"><strong>
86
+ <span id="matomo_opted_in_label" ' . $style_tracking_disabled . '>' . $this->translate( 'CoreAdminHome_YouAreNotOptedOut' ) . ' ' . $this->translate( 'CoreAdminHome_UncheckToOptOut' ) . '</span>
87
+ <span id="matomo_opted_out_label" ' . $style_tracking_enabled . '>' . $this->translate( 'CoreAdminHome_YouAreOptedOut' ) . ' ' . $this->translate( 'CoreAdminHome_CheckToOptIn' ) . '</span>
88
  </strong></label></form>';
89
  $content .= '<noscript><p><strong style="color: #ff0000;">This opt out feature requires JavaScript.</strong></p></noscript>';
90
+ $content .= '<p id="matomo_outout_err_cookies" style="display: none;"><strong>' . $this->translate( 'CoreAdminHome_OptOutErrorNoCookies' ) . '</strong></p>';
91
+
92
  return $content;
93
  }
94
 
95
+ public function load_block() {
96
+ // before wordpress 5.0
97
+ if ( ! function_exists( 'register_block_type' ) ) {
98
+ // Gutenberg is not active.
99
+ return;
100
+ }
101
+
102
+ wp_register_script(
103
+ 'matomo-opt-out',
104
+ plugins_url( '/assets/js/blocks/matomo_opt_out.js', MATOMO_ANALYTICS_FILE ),
105
+ array( 'wp-blocks', 'wp-i18n', 'wp-element' ),
106
+ filemtime( plugin_dir_path( MATOMO_ANALYTICS_FILE ) . '/assets/js/blocks/matomo_opt_out.js' )
107
+ );
108
+
109
+ register_block_type( 'matomo/matomo-opt-out', array(
110
+ 'editor_script' => 'matomo-opt-out',
111
+ ) );
112
+ }
113
  }
classes/WpMatomo/Paths.php CHANGED
@@ -9,24 +9,32 @@
9
 
10
  namespace WpMatomo;
11
 
 
 
 
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit; // if accessed directly
14
  }
15
 
16
  class Paths {
17
 
18
- private function get_file_system() {
 
 
19
  if ( ! function_exists( 'WP_Filesystem' ) ) {
20
  require_once ABSPATH . '/wp-admin/includes/file.php';
21
- WP_Filesystem();
22
  }
23
 
 
 
 
 
24
  if ( ! class_exists( '\WP_Filesystem_Direct' ) ) {
25
  require_once ABSPATH . '/wp-admin/includes/class-wp-filesystem-base.php';
26
  require_once ABSPATH . '/wp-admin/includes/class-wp-filesystem-direct.php';
27
  }
28
 
29
- return new \WP_Filesystem_Direct( new \stdClass() );
30
  }
31
 
32
  public function get_upload_base_url() {
@@ -113,7 +121,7 @@ class Paths {
113
  $matomo_dir_parts = explode( DIRECTORY_SEPARATOR, $matomo_dir );
114
  $target_dir_parts = explode( DIRECTORY_SEPARATOR, $target_dir );
115
  $relative_directory = '';
116
- $add_at_the_end = array();
117
  $was_previous_same = false;
118
 
119
  foreach ( $target_dir_parts as $index => $part ) {
9
 
10
  namespace WpMatomo;
11
 
12
+ use stdClass;
13
+ use WP_Filesystem_Direct;
14
+
15
  if ( ! defined( 'ABSPATH' ) ) {
16
  exit; // if accessed directly
17
  }
18
 
19
  class Paths {
20
 
21
+ private static $host_init_filesystem = false;
22
+
23
+ public function get_file_system() {
24
  if ( ! function_exists( 'WP_Filesystem' ) ) {
25
  require_once ABSPATH . '/wp-admin/includes/file.php';
 
26
  }
27
 
28
+ if ( ! self::$host_init_filesystem ) {
29
+ self::$host_init_filesystem = true;
30
+ WP_Filesystem();
31
+ }
32
  if ( ! class_exists( '\WP_Filesystem_Direct' ) ) {
33
  require_once ABSPATH . '/wp-admin/includes/class-wp-filesystem-base.php';
34
  require_once ABSPATH . '/wp-admin/includes/class-wp-filesystem-direct.php';
35
  }
36
 
37
+ return new WP_Filesystem_Direct( new stdClass() );
38
  }
39
 
40
  public function get_upload_base_url() {
121
  $matomo_dir_parts = explode( DIRECTORY_SEPARATOR, $matomo_dir );
122
  $target_dir_parts = explode( DIRECTORY_SEPARATOR, $target_dir );
123
  $relative_directory = '';
124
+ $add_at_the_end = [];
125
  $was_previous_same = false;
126
 
127
  foreach ( $target_dir_parts as $index => $part ) {
classes/WpMatomo/PrivacyBadge.php CHANGED
@@ -14,17 +14,16 @@ if ( ! defined( 'ABSPATH' ) ) {
14
  }
15
 
16
  class PrivacyBadge {
17
-
18
  public function register_hooks() {
19
- add_shortcode( 'matomo_privacy_badge', array( $this, 'show_privacy_page' ) );
20
  }
21
 
22
  public function show_privacy_page( $atts ) {
23
  $a = shortcode_atts(
24
- array(
25
  'size' => '120',
26
  'align' => '',
27
- ),
28
  $atts
29
  );
30
 
@@ -40,5 +39,4 @@ class PrivacyBadge {
40
 
41
  return sprintf( '<img alt="%s" src="%s" %s>', $title, esc_attr( $url ), $option );
42
  }
43
-
44
  }
14
  }
15
 
16
  class PrivacyBadge {
 
17
  public function register_hooks() {
18
+ add_shortcode( 'matomo_privacy_badge', [ $this, 'show_privacy_page' ] );
19
  }
20
 
21
  public function show_privacy_page( $atts ) {
22
  $a = shortcode_atts(
23
+ [
24
  'size' => '120',
25
  'align' => '',
26
+ ],
27
  $atts
28
  );
29
 
39
 
40
  return sprintf( '<img alt="%s" src="%s" %s>', $title, esc_attr( $url ), $option );
41
  }
 
42
  }
classes/WpMatomo/RedirectOnActivation.php CHANGED
@@ -9,7 +9,7 @@
9
 
10
  namespace WpMatomo;
11
 
12
- use \WpMatomo\Admin\TrackingSettings;
13
 
14
  class RedirectOnActivation {
15
  /**
@@ -22,7 +22,7 @@ class RedirectOnActivation {
22
  }
23
 
24
  public function register_hooks() {
25
- register_activation_hook(MATOMO_ANALYTICS_FILE, [ $this, 'matomo_activate' ] );
26
  add_action( 'admin_init', [ $this, 'matomo_plugin_redirect' ] );
27
  }
28
 
@@ -36,6 +36,7 @@ class RedirectOnActivation {
36
  $this->redirect_to_getting_started();
37
  }
38
  }
 
39
  /**
40
  * We don't test the result of the wp_redirect method and we silent this method
41
  * as this method will not work during unit tests.
@@ -46,16 +47,17 @@ class RedirectOnActivation {
46
  */
47
  public function redirect_to_getting_started() {
48
  $redirect = false;
49
- if(!isset($_GET['activate-multi'])) {
50
- if
51
- (
52
  ( self::$settings->get_global_option( Settings::SHOW_GET_STARTED_PAGE ) === 1 ) &&
53
  ( self::$settings->get_global_option( 'track_mode' ) === TrackingSettings::TRACK_MODE_DISABLED )
54
  ) {
55
  $redirect = true;
56
- @wp_redirect( admin_url( 'admin.php?page=matomo-get-started' ) );
 
57
  }
58
  }
 
59
  return $redirect;
60
  }
61
  }
9
 
10
  namespace WpMatomo;
11
 
12
+ use WpMatomo\Admin\TrackingSettings;
13
 
14
  class RedirectOnActivation {
15
  /**
22
  }
23
 
24
  public function register_hooks() {
25
+ register_activation_hook( MATOMO_ANALYTICS_FILE, [ $this, 'matomo_activate' ] );
26
  add_action( 'admin_init', [ $this, 'matomo_plugin_redirect' ] );
27
  }
28
 
36
  $this->redirect_to_getting_started();
37
  }
38
  }
39
+
40
  /**
41
  * We don't test the result of the wp_redirect method and we silent this method
42
  * as this method will not work during unit tests.
47
  */
48
  public function redirect_to_getting_started() {
49
  $redirect = false;
50
+ if ( ! isset( $_GET['activate-multi'] ) ) {
51
+ if (
 
52
  ( self::$settings->get_global_option( Settings::SHOW_GET_STARTED_PAGE ) === 1 ) &&
53
  ( self::$settings->get_global_option( 'track_mode' ) === TrackingSettings::TRACK_MODE_DISABLED )
54
  ) {
55
  $redirect = true;
56
+ // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
57
+ @wp_safe_redirect( admin_url( 'admin.php?page=matomo-get-started' ) );
58
  }
59
  }
60
+
61
  return $redirect;
62
  }
63
  }
classes/WpMatomo/Referral.php CHANGED
@@ -17,10 +17,10 @@ if ( ! defined( 'ABSPATH' ) ) {
17
  * Every 90 days we show a please review notice until the user dismisses this notice or clicks on rate us.
18
  * We only show this notice on Matomo screens.
19
  *
20
- * @package WpMatomo
 
21
  */
22
  class Referral {
23
-
24
  const OPTION_NAME_REFERRAL_DISMISSED = 'matomo-referral-dismissed';
25
 
26
  /**
@@ -33,8 +33,9 @@ class Referral {
33
  }
34
 
35
  /**
36
- * @internal for tests only
37
  * @param int $time
 
 
38
  */
39
  public function set_time( $time ) {
40
  $this->time = $time;
@@ -75,6 +76,7 @@ class Referral {
75
  return false;
76
  }
77
  $screen = get_current_screen();
 
78
  return $screen && $screen->id && strpos( $screen->id, 'matomo-' ) === 0;
79
  }
80
 
@@ -83,8 +85,8 @@ class Referral {
83
  }
84
 
85
  public function dismiss_forever() {
86
- $tenYears = 60 * 60 * 24 * 365 * 10;
87
- update_option( self::OPTION_NAME_REFERRAL_DISMISSED, $this->time + $tenYears );
88
  }
89
 
90
  public function dismiss() {
@@ -106,17 +108,16 @@ class Referral {
106
  // the first time we check... we set it back 30 days cause we want to see first rating after 60 days
107
  $this->time = $this->time - $this->get_days_in_seconds( 30 );
108
  $this->dismiss();
 
109
  return false;
110
  }
111
 
112
- $ninetyDaysInSeconds = $this->get_days_in_seconds( 90 );
113
 
114
- if ( $this->time > ( $dismissed + $ninetyDaysInSeconds ) ) {
115
  return true;
116
  }
117
 
118
  return false;
119
  }
120
-
121
-
122
  }
17
  * Every 90 days we show a please review notice until the user dismisses this notice or clicks on rate us.
18
  * We only show this notice on Matomo screens.
19
  *
20
+ * @todo validate the nonce
21
+ * phpcs:disable WordPress.Security.NonceVerification.Missing
22
  */
23
  class Referral {
 
24
  const OPTION_NAME_REFERRAL_DISMISSED = 'matomo-referral-dismissed';
25
 
26
  /**
33
  }
34
 
35
  /**
 
36
  * @param int $time
37
+ *
38
+ * @internal for tests only
39
  */
40
  public function set_time( $time ) {
41
  $this->time = $time;
76
  return false;
77
  }
78
  $screen = get_current_screen();
79
+
80
  return $screen && $screen->id && strpos( $screen->id, 'matomo-' ) === 0;
81
  }
82
 
85
  }
86
 
87
  public function dismiss_forever() {
88
+ $ten_years = 60 * 60 * 24 * 365 * 10;
89
+ update_option( self::OPTION_NAME_REFERRAL_DISMISSED, $this->time + $ten_years );
90
  }
91
 
92
  public function dismiss() {
108
  // the first time we check... we set it back 30 days cause we want to see first rating after 60 days
109
  $this->time = $this->time - $this->get_days_in_seconds( 30 );
110
  $this->dismiss();
111
+
112
  return false;
113
  }
114
 
115
+ $ninety_days_in_seconds = $this->get_days_in_seconds( 90 );
116
 
117
+ if ( $this->time > ( $dismissed + $ninety_days_in_seconds ) ) {
118
  return true;
119
  }
120
 
121
  return false;
122
  }
 
 
123
  }
classes/WpMatomo/Report/Data.php CHANGED
@@ -35,10 +35,10 @@ class Data {
35
  Bootstrap::do_bootstrap();
36
 
37
  if ( empty( $idsite ) ) {
38
- return array();
39
  }
40
 
41
- $params = array(
42
  'apiModule' => $report_metadata['module'],
43
  'apiAction' => $report_metadata['action'],
44
  'filter_limit' => $filter_limit,
@@ -46,7 +46,7 @@ class Data {
46
  'period' => $period,
47
  'date' => $date,
48
  'idSite' => $idsite,
49
- );
50
  if ( ! empty( $report_metadata['parameters'] ) ) {
51
  $params = array_merge( $params, $report_metadata['parameters'] );
52
  }
@@ -55,5 +55,4 @@ class Data {
55
 
56
  return $report;
57
  }
58
-
59
  }
35
  Bootstrap::do_bootstrap();
36
 
37
  if ( empty( $idsite ) ) {
38
+ return [];
39
  }
40
 
41
+ $params = [
42
  'apiModule' => $report_metadata['module'],
43
  'apiAction' => $report_metadata['action'],
44
  'filter_limit' => $filter_limit,
46
  'period' => $period,
47
  'date' => $date,
48
  'idSite' => $idsite,
49
+ ];
50
  if ( ! empty( $report_metadata['parameters'] ) ) {
51
  $params = array_merge( $params, $report_metadata['parameters'] );
52
  }
55
 
56
  return $report;
57
  }
 
58
  }
classes/WpMatomo/Report/Dates.php CHANGED
@@ -23,7 +23,7 @@ class Dates {
23
  const THIS_YEAR = 'thisyear';
24
 
25
  public function get_supported_dates() {
26
- return array(
27
  self::YESTERDAY => 'Yesterday',
28
  self::TODAY => 'Today',
29
  self::THIS_WEEK => 'This week',
@@ -31,7 +31,7 @@ class Dates {
31
  self::THIS_MONTH => 'This month',
32
  self::LAST_MONTH => 'Last month',
33
  self::THIS_YEAR => 'This year',
34
- );
35
  }
36
 
37
  public function detect_period_and_date( $report_date ) {
@@ -74,8 +74,6 @@ class Dates {
74
  }
75
  }
76
 
77
- return array( $period, $date );
78
  }
79
-
80
-
81
  }
23
  const THIS_YEAR = 'thisyear';
24
 
25
  public function get_supported_dates() {
26
+ return [
27
  self::YESTERDAY => 'Yesterday',
28
  self::TODAY => 'Today',
29
  self::THIS_WEEK => 'This week',
31
  self::THIS_MONTH => 'This month',
32
  self::LAST_MONTH => 'Last month',
33
  self::THIS_YEAR => 'This year',
34
+ ];
35
  }
36
 
37
  public function detect_period_and_date( $report_date ) {
74
  }
75
  }
76
 
77
+ return [ $period, $date ];
78
  }
 
 
79
  }
classes/WpMatomo/Report/Metadata.php CHANGED
@@ -18,12 +18,13 @@ if ( ! defined( 'ABSPATH' ) ) {
18
  }
19
 
20
  class Metadata {
21
- public static $CACHE_ALL_REPORTS = array();
22
- public static $CACHE_ALL_REPORT_PAGES = array();
 
23
 
24
  public function get_all_reports() {
25
- if ( ! empty( self::$CACHE_ALL_REPORTS ) ) {
26
- return self::$CACHE_ALL_REPORTS;
27
  }
28
 
29
  $site = new Site();
@@ -34,19 +35,19 @@ class Metadata {
34
 
35
  $all_reports = Request::processRequest(
36
  'API.getReportMetadata',
37
- array(
38
  'idSite' => $idsite,
39
  'filter_limit' => - 1,
40
- )
41
  );
42
  foreach ( $all_reports as $single_report ) {
43
  if ( isset( $single_report['uniqueId'] ) ) {
44
- self::$CACHE_ALL_REPORTS[ $single_report['uniqueId'] ] = $single_report;
45
  }
46
  }
47
  }
48
 
49
- return self::$CACHE_ALL_REPORTS;
50
  }
51
 
52
  /**
@@ -54,13 +55,16 @@ class Metadata {
54
  * tests only
55
  */
56
  public static function clear_cache() {
57
- self::$CACHE_ALL_REPORTS = array();
58
- self::$CACHE_ALL_REPORT_PAGES = array();
59
  }
60
 
61
  public function find_report_by_unique_id( $unique_id ) {
62
- if ($unique_id === Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME) {
63
- return array('uniqueId' => Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME, 'name' => 'Visits over time');
 
 
 
64
  }
65
  $all_reports = self::get_all_reports();
66
 
@@ -70,8 +74,8 @@ class Metadata {
70
  }
71
 
72
  public function get_all_report_pages() {
73
- if ( ! empty( self::$CACHE_ALL_REPORT_PAGES ) ) {
74
- return self::$CACHE_ALL_REPORT_PAGES;
75
  }
76
 
77
  $site = new Site();
@@ -80,22 +84,22 @@ class Metadata {
80
  if ( $idsite ) {
81
  Bootstrap::do_bootstrap();
82
 
83
- self::$CACHE_ALL_REPORT_PAGES = Request::processRequest(
84
  'API.getReportPagesMetadata',
85
- array(
86
  'idSite' => $idsite,
87
  'filter_limit' => - 1,
88
- )
89
  );
90
  }
91
 
92
- return self::$CACHE_ALL_REPORT_PAGES;
93
  }
94
 
95
  public function find_report_page_params_by_report_metadata( $report_metadata ) {
96
  if ( empty( $report_metadata['module'] )
97
  || empty( $report_metadata['action'] ) ) {
98
- return array();
99
  }
100
 
101
  $report_pages = self::get_all_report_pages();
@@ -105,10 +109,10 @@ class Metadata {
105
  foreach ( $report_page['widgets'] as $widget ) {
106
  if ( ! empty( $widget['module'] ) && $widget['module'] === $report_metadata['module']
107
  && ! empty( $widget['action'] ) && $widget['action'] === $report_metadata['action'] ) {
108
- return array(
109
  'category' => $report_page['category']['id'],
110
  'subcategory' => $report_page['subcategory']['id'],
111
- );
112
  }
113
  }
114
  }
@@ -118,28 +122,27 @@ class Metadata {
118
  // we're hard coding some manually
119
 
120
  if ( 'Actions_get' === $report_metadata['uniqueId'] ) {
121
- return array(
122
  'category' => 'General_Visitors',
123
  'subcategory' => 'General_Overview',
124
- );
125
  } elseif ( 'Goals_get' === $report_metadata['uniqueId'] ) {
126
- return array(
127
  'category' => 'Goals_Goals',
128
  'subcategory' => 'General_Overview',
129
- );
130
  } elseif ( 'Goals_get_idGoal--ecommerceOrder' === $report_metadata['uniqueId'] ) {
131
- return array(
132
  'category' => 'Goals_Ecommerce',
133
  'subcategory' => 'General_Overview',
134
- );
135
  } elseif ( 'Goals_getItemsName' === $report_metadata['uniqueId'] ) {
136
- return array(
137
  'category' => 'Goals_Ecommerce',
138
  'subcategory' => 'Goals_Products',
139
- );
140
  }
141
 
142
- return array();
143
  }
144
-
145
  }
18
  }
19
 
20
  class Metadata {
21
+
22
+ public static $cache_all_reports = [];
23
+ public static $cache_all_report_pages = [];
24
 
25
  public function get_all_reports() {
26
+ if ( ! empty( self::$cache_all_reports ) ) {
27
+ return self::$cache_all_reports;
28
  }
29
 
30
  $site = new Site();
35
 
36
  $all_reports = Request::processRequest(
37
  'API.getReportMetadata',
38
+ [
39
  'idSite' => $idsite,
40
  'filter_limit' => - 1,
41
+ ]
42
  );
43
  foreach ( $all_reports as $single_report ) {
44
  if ( isset( $single_report['uniqueId'] ) ) {
45
+ self::$cache_all_reports[ $single_report['uniqueId'] ] = $single_report;
46
  }
47
  }
48
  }
49
 
50
+ return self::$cache_all_reports;
51
  }
52
 
53
  /**
55
  * tests only
56
  */
57
  public static function clear_cache() {
58
+ self::$cache_all_reports = [];
59
+ self::$cache_all_report_pages = [];
60
  }
61
 
62
  public function find_report_by_unique_id( $unique_id ) {
63
+ if ( Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME === $unique_id ) {
64
+ return [
65
+ 'uniqueId' => Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME,
66
+ 'name' => 'Visits over time',
67
+ ];
68
  }
69
  $all_reports = self::get_all_reports();
70
 
74
  }
75
 
76
  public function get_all_report_pages() {
77
+ if ( ! empty( self::$cache_all_report_pages ) ) {
78
+ return self::$cache_all_report_pages;
79
  }
80
 
81
  $site = new Site();
84
  if ( $idsite ) {
85
  Bootstrap::do_bootstrap();
86
 
87
+ self::$cache_all_report_pages = Request::processRequest(
88
  'API.getReportPagesMetadata',
89
+ [
90
  'idSite' => $idsite,
91
  'filter_limit' => - 1,
92
+ ]
93
  );
94
  }
95
 
96
+ return self::$cache_all_report_pages;
97
  }
98
 
99
  public function find_report_page_params_by_report_metadata( $report_metadata ) {
100
  if ( empty( $report_metadata['module'] )
101
  || empty( $report_metadata['action'] ) ) {
102
+ return [];
103
  }
104
 
105
  $report_pages = self::get_all_report_pages();
109
  foreach ( $report_page['widgets'] as $widget ) {
110
  if ( ! empty( $widget['module'] ) && $widget['module'] === $report_metadata['module']
111
  && ! empty( $widget['action'] ) && $widget['action'] === $report_metadata['action'] ) {
112
+ return [
113
  'category' => $report_page['category']['id'],
114
  'subcategory' => $report_page['subcategory']['id'],
115
+ ];
116
  }
117
  }
118
  }
122
  // we're hard coding some manually
123
 
124
  if ( 'Actions_get' === $report_metadata['uniqueId'] ) {
125
+ return [
126
  'category' => 'General_Visitors',
127
  'subcategory' => 'General_Overview',
128
+ ];
129
  } elseif ( 'Goals_get' === $report_metadata['uniqueId'] ) {
130
+ return [
131
  'category' => 'Goals_Goals',
132
  'subcategory' => 'General_Overview',
133
+ ];
134
  } elseif ( 'Goals_get_idGoal--ecommerceOrder' === $report_metadata['uniqueId'] ) {
135
+ return [
136
  'category' => 'Goals_Ecommerce',
137
  'subcategory' => 'General_Overview',
138
+ ];
139
  } elseif ( 'Goals_getItemsName' === $report_metadata['uniqueId'] ) {
140
+ return [
141
  'category' => 'Goals_Ecommerce',
142
  'subcategory' => 'Goals_Products',
143
+ ];
144
  }
145
 
146
+ return [];
147
  }
 
148
  }
classes/WpMatomo/Report/Renderer.php CHANGED
@@ -16,31 +16,33 @@ if ( ! defined( 'ABSPATH' ) ) {
16
  }
17
 
18
  class Renderer {
19
- CONST CUSTOM_UNIQUE_ID_VISITS_OVER_TIME = 'visits_over_time';
20
 
21
  public function register_hooks() {
22
- add_shortcode( 'matomo_report', array( $this, 'show_report' ) );
23
  }
24
 
25
- public function show_visits_over_time($limit)
26
- {
27
  $cannot_view = $this->check_cannot_view();
28
- if ($cannot_view) {
29
  return $cannot_view;
30
  }
31
 
32
- if (is_numeric($limit)) {
33
- $limit = (int) $limit;
34
- } else {
35
- $limit = 14;
36
- }
37
 
38
- $report_meta = array('module' => 'VisitsSummary', 'action' => 'get');
 
 
 
39
 
40
- $data = new Data();
41
- $report = $data->fetch_report($report_meta, 'day', 'last' . $limit, 'label', $limit);
42
  $first_metric_name = 'nb_visits';
43
-
44
  ob_start();
45
 
46
  include 'views/table_map_no_dimension.php';
@@ -48,8 +50,7 @@ class Renderer {
48
  return ob_get_clean();
49
  }
50
 
51
- private function check_cannot_view()
52
- {
53
  if ( ! current_user_can( Capabilities::KEY_VIEW ) ) {
54
  // not needed as processRequest checks permission anyway but it's faster this way and double ensures to not
55
  // letting users view it when they have no access.
@@ -59,25 +60,29 @@ class Renderer {
59
 
60
  public function show_report( $atts ) {
61
  $a = shortcode_atts(
62
- array(
63
  'unique_id' => '',
64
  'report_date' => Dates::YESTERDAY,
65
  'limit' => 10,
66
- ),
67
  $atts
68
  );
69
 
70
  $cannot_view = $this->check_cannot_view();
71
- if ($cannot_view) {
72
  return $cannot_view;
73
  }
74
 
75
- if ($a['unique_id'] === 'visits_over_time') {
76
- $is_default_limit = $a['limit'] === 10;
77
- if ($is_default_limit) {
78
- $a['limit'] = 14;
79
- }
80
- return $this->show_visits_over_time($a['limit']);
 
 
 
 
81
  }
82
 
83
  $metadata = new Metadata();
@@ -91,9 +96,6 @@ class Renderer {
91
  $first_metric_name = reset( $metric_keys );
92
  $first_metric_display_name = reset( $report_meta['metrics'] );
93
 
94
- $dates = new Dates();
95
- list( $period, $date ) = $dates->detect_period_and_date( $a['report_date'] );
96
-
97
  $report_data = new Data();
98
  $report = $report_data->fetch_report( $report_meta, $period, $date, $first_metric_name, $a['limit'] );
99
  $has_report_data = ! empty( $report['reportData'] ) && $report['reportData']->getRowsCount();
@@ -110,5 +112,4 @@ class Renderer {
110
 
111
  return ob_get_clean();
112
  }
113
-
114
  }
16
  }
17
 
18
  class Renderer {
19
+ const CUSTOM_UNIQUE_ID_VISITS_OVER_TIME = 'visits_over_time';
20
 
21
  public function register_hooks() {
22
+ add_shortcode( 'matomo_report', [ $this, 'show_report' ] );
23
  }
24
 
25
+ public function show_visits_over_time( $limit, $period ) {
 
26
  $cannot_view = $this->check_cannot_view();
27
+ if ( $cannot_view ) {
28
  return $cannot_view;
29
  }
30
 
31
+ if ( is_numeric( $limit ) ) {
32
+ $limit = (int) $limit;
33
+ } else {
34
+ $limit = 14;
35
+ }
36
 
37
+ $report_meta = [
38
+ 'module' => 'VisitsSummary',
39
+ 'action' => 'get',
40
+ ];
41
 
42
+ $data = new Data();
43
+ $report = $data->fetch_report( $report_meta, $period, 'last' . $limit, 'label', $limit );
44
  $first_metric_name = 'nb_visits';
45
+ $matomo_graph_data = ' data-chart="VisitsSumary"';
46
  ob_start();
47
 
48
  include 'views/table_map_no_dimension.php';
50
  return ob_get_clean();
51
  }
52
 
53
+ private function check_cannot_view() {
 
54
  if ( ! current_user_can( Capabilities::KEY_VIEW ) ) {
55
  // not needed as processRequest checks permission anyway but it's faster this way and double ensures to not
56
  // letting users view it when they have no access.
60
 
61
  public function show_report( $atts ) {
62
  $a = shortcode_atts(
63
+ [
64
  'unique_id' => '',
65
  'report_date' => Dates::YESTERDAY,
66
  'limit' => 10,
67
+ ],
68
  $atts
69
  );
70
 
71
  $cannot_view = $this->check_cannot_view();
72
+ if ( $cannot_view ) {
73
  return $cannot_view;
74
  }
75
 
76
+ $dates = new Dates();
77
+ list( $period, $date ) = $dates->detect_period_and_date( $a['report_date'] );
78
+
79
+ if ( 'visits_over_time' === $a['unique_id'] ) {
80
+ $is_default_limit = 10 === $a['limit'];
81
+ if ( $is_default_limit ) {
82
+ $a['limit'] = 14;
83
+ }
84
+
85
+ return $this->show_visits_over_time( $a['limit'], $period );
86
  }
87
 
88
  $metadata = new Metadata();
96
  $first_metric_name = reset( $metric_keys );
97
  $first_metric_display_name = reset( $report_meta['metrics'] );
98
 
 
 
 
99
  $report_data = new Data();
100
  $report = $report_data->fetch_report( $report_meta, $period, $date, $first_metric_name, $a['limit'] );
101
  $has_report_data = ! empty( $report['reportData'] ) && $report['reportData']->getRowsCount();
112
 
113
  return ob_get_clean();
114
  }
 
115
  }
classes/WpMatomo/Report/views/table.php CHANGED
@@ -38,7 +38,7 @@ if ( ! defined( 'ABSPATH' ) ) {
38
  $matomo_logo_image = '<img height="16" src="' . plugins_url( 'app/' . $matomo_logo, MATOMO_ANALYTICS_FILE ) . '"> ';
39
  }
40
  }
41
-
42
  echo '<tr><td width="75%">' . $matomo_logo_image . esc_html( $matomo_report_row['label'] ) . '</td><td width="25%">' . esc_html( $matomo_report_row[ $first_metric_name ] ) . '</td></tr>';
43
  }
44
  }
38
  $matomo_logo_image = '<img height="16" src="' . plugins_url( 'app/' . $matomo_logo, MATOMO_ANALYTICS_FILE ) . '"> ';
39
  }
40
  }
41
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
42
  echo '<tr><td width="75%">' . $matomo_logo_image . esc_html( $matomo_report_row['label'] ) . '</td><td width="25%">' . esc_html( $matomo_report_row[ $first_metric_name ] ) . '</td></tr>';
43
  }
44
  }
classes/WpMatomo/Report/views/table_map_no_dimension.php CHANGED
@@ -7,6 +7,8 @@
7
  * @package matomo
8
  */
9
 
 
 
10
  if ( ! defined( 'ABSPATH' ) ) {
11
  exit; // if accessed directly
12
  }
@@ -14,18 +16,22 @@ if ( ! defined( 'ABSPATH' ) ) {
14
  /** @var array $report */
15
  /** @var array $report_meta */
16
  /** @var string $first_metric_name */
 
 
 
 
17
  ?>
18
  <div class="table">
19
- <table class="widefat matomo-table">
20
 
21
  <tbody>
22
  <?php
23
  $matomo_report_metadata = $report['reportMetadata'];
24
- $matomo_tables = $report['reportData']->getDataTables();
25
- foreach (array_reverse($matomo_tables) as $matomo_report_date => $matomo_report_table ) {
26
- /** @var \Piwik\DataTable\Simple $matomo_report_table */
27
  echo '<tr><td width="75%">' . esc_html( $matomo_report_date ) . '</td><td width="25%">';
28
- if ($matomo_report_table->getFirstRow()) {
29
  echo esc_html( $matomo_report_table->getFirstRow()->getColumn( $first_metric_name ) );
30
  } else {
31
  echo '-';
7
  * @package matomo
8
  */
9
 
10
+ use Piwik\DataTable\Simple;
11
+
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit; // if accessed directly
14
  }
16
  /** @var array $report */
17
  /** @var array $report_meta */
18
  /** @var string $first_metric_name */
19
+ /** @var string $matomo_graph_data */
20
+ if ( ! isset( $matomo_graph_data ) ) :
21
+ $matomo_graph_data = '';
22
+ endif;
23
  ?>
24
  <div class="table">
25
+ <table class="widefat matomo-table" <?php echo esc_html( $matomo_graph_data ); ?>>
26
 
27
  <tbody>
28
  <?php
29
  $matomo_report_metadata = $report['reportMetadata'];
30
+ $matomo_tables = $report['reportData']->getDataTables();
31
+ foreach ( array_reverse( $matomo_tables, true ) as $matomo_report_date => $matomo_report_table ) {
32
+ /** @var Simple $matomo_report_table */
33
  echo '<tr><td width="75%">' . esc_html( $matomo_report_date ) . '</td><td width="25%">';
34
+ if ( $matomo_report_table->getFirstRow() ) {
35
  echo esc_html( $matomo_report_table->getFirstRow()->getColumn( $first_metric_name ) );
36
  } else {
37
  echo '-';
classes/WpMatomo/Report/views/table_no_dimension.php CHANGED
@@ -21,7 +21,7 @@ if ( ! defined( 'ABSPATH' ) ) {
21
  <table class="widefat matomo-table">
22
  <tbody>
23
  <?php
24
- $matomo_columns = ! empty( $report['columns'] ) ? $report['columns'] : array();
25
  foreach ( $report['reportData']->getRows() as $matomo_val => $matomo_row ) {
26
  foreach ( $matomo_row as $matomo_metric_name => $matomo_value ) {
27
  $matomo_display_name = ! empty( $matomo_columns[ $matomo_metric_name ] ) ? $matomo_columns[ $matomo_metric_name ] : $matomo_metric_name;
21
  <table class="widefat matomo-table">
22
  <tbody>
23
  <?php
24
+ $matomo_columns = ! empty( $report['columns'] ) ? $report['columns'] : [];
25
  foreach ( $report['reportData']->getRows() as $matomo_val => $matomo_row ) {
26
  foreach ( $matomo_row as $matomo_metric_name => $matomo_value ) {
27
  $matomo_display_name = ! empty( $matomo_columns[ $matomo_metric_name ] ) ? $matomo_columns[ $matomo_metric_name ] : $matomo_metric_name;
classes/WpMatomo/Roles.php CHANGED
@@ -31,13 +31,13 @@ class Roles {
31
  }
32
 
33
  public function register_hooks() {
34
- add_action( 'init', array( $this, 'add_roles' ) );
35
  }
36
 
37
  public function get_available_roles_for_configuration() {
38
  global $wp_roles;
39
  $is_network_enabled = $this->settings->is_network_enabled();
40
- $roles = array();
41
 
42
  foreach ( $wp_roles->role_names as $role_name => $name ) {
43
  if ( ! $is_network_enabled && 'administrator' === $role_name ) {
@@ -62,30 +62,30 @@ class Roles {
62
  }
63
 
64
  public function get_matomo_roles() {
65
- return array(
66
- self::ROLE_VIEW => array(
67
  'name' => 'Matomo View',
68
  'defaultCap' => Capabilities::KEY_VIEW,
69
- ),
70
- self::ROLE_WRITE => array(
71
  'name' => 'Matomo Write',
72
  'defaultCap' => Capabilities::KEY_WRITE,
73
- ),
74
- self::ROLE_ADMIN => array(
75
  'name' => 'Matomo Admin',
76
  'defaultCap' => Capabilities::KEY_ADMIN,
77
- ),
78
- self::ROLE_SUPERUSER => array(
79
  'name' => 'Matomo Super User',
80
  'defaultCap' => Capabilities::KEY_SUPERUSER,
81
- ),
82
- );
83
  }
84
 
85
  public function add_roles() {
86
  if ( ! $this->has_set_up_roles() ) {
87
  foreach ( $this->get_matomo_roles() as $role_name => $config ) {
88
- add_role( $role_name, $config['name'], array( $config['defaultCap'] => true ) );
89
  }
90
  $this->mark_roles_set_up();
91
  }
31
  }
32
 
33
  public function register_hooks() {
34
+ add_action( 'init', [ $this, 'add_roles' ] );
35
  }
36
 
37
  public function get_available_roles_for_configuration() {
38
  global $wp_roles;
39
  $is_network_enabled = $this->settings->is_network_enabled();
40
+ $roles = [];
41
 
42
  foreach ( $wp_roles->role_names as $role_name => $name ) {
43
  if ( ! $is_network_enabled && 'administrator' === $role_name ) {
62
  }
63
 
64
  public function get_matomo_roles() {
65
+ return [
66
+ self::ROLE_VIEW => [
67
  'name' => 'Matomo View',
68
  'defaultCap' => Capabilities::KEY_VIEW,
69
+ ],
70
+ self::ROLE_WRITE => [
71
  'name' => 'Matomo Write',
72
  'defaultCap' => Capabilities::KEY_WRITE,
73
+ ],
74
+ self::ROLE_ADMIN => [
75
  'name' => 'Matomo Admin',
76
  'defaultCap' => Capabilities::KEY_ADMIN,
77
+ ],
78
+ self::ROLE_SUPERUSER => [
79
  'name' => 'Matomo Super User',
80
  'defaultCap' => Capabilities::KEY_SUPERUSER,
81
+ ],
82
+ ];
83
  }
84
 
85
  public function add_roles() {
86
  if ( ! $this->has_set_up_roles() ) {
87
  foreach ( $this->get_matomo_roles() as $role_name => $config ) {
88
+ add_role( $role_name, $config['name'], [ $config['defaultCap'] => true ] );
89
  }
90
  $this->mark_roles_set_up();
91
  }
classes/WpMatomo/ScheduledTasks.php CHANGED
@@ -9,7 +9,7 @@
9
 
10
  namespace WpMatomo;
11
 
12
- use Piwik\Config;
13
  use Piwik\CronArchive;
14
  use Piwik\Filesystem;
15
  use Piwik\Option;
@@ -20,17 +20,18 @@ use Piwik\Plugins\GeoIp2\LocationProvider\GeoIp2\Php;
20
  use Piwik\Plugins\UserCountry\LocationProvider;
21
  use WpMatomo\Site\Sync as SiteSync;
22
  use WpMatomo\User\Sync as UserSync;
 
23
 
24
  if ( ! defined( 'ABSPATH' ) ) {
25
  exit; // if accessed directly
26
  }
27
 
28
  class ScheduledTasks {
29
- const EVENT_SYNC = 'matomo_scheduled_sync';
30
- const EVENT_DISABLE_ADDHANDLER = 'matomo_scheduled_disable_addhandler';
31
- const EVENT_ARCHIVE = 'matomo_scheduled_archive';
32
- const EVENT_GEOIP = 'matomo_scheduled_geoipdb';
33
- const EVENT_UPDATE = 'matomo_update_core';
34
 
35
  const KEY_BEFORE_CRON = 'before-cron-';
36
  const KEY_AFTER_CRON = 'after-cron-';
@@ -51,23 +52,28 @@ class ScheduledTasks {
51
  }
52
 
53
  public function add_weekly_schedule( $schedules ) {
54
- $schedules['matomo_monthly'] = array(
55
  'interval' => 60 * 60 * 24 * 30,
56
  'display' => __( 'Monthly', 'matomo' ),
57
- );
58
 
59
  return $schedules;
60
  }
61
 
62
  public function schedule() {
63
- add_action( self::EVENT_UPDATE, array( $this, 'perform_update' ) );
64
- add_filter( 'cron_schedules', array( $this, 'add_weekly_schedule' ) );
 
65
 
66
  $self = $this;
67
  $event_priority = 10;
68
 
 
 
 
69
  foreach ( $this->get_all_events() as $event_name => $event_config ) {
70
- if ( ! wp_next_scheduled( $event_name ) ) {
 
71
  wp_schedule_event( time(), $event_config['interval'], $event_name );
72
  }
73
 
@@ -82,7 +88,7 @@ class ScheduledTasks {
82
  );
83
 
84
  // actual event
85
- add_action( $event_name, array( $this, $event_config['method'] ), $event_priority, $accepted_args = 0 );
86
 
87
  // logging last execution end time
88
  add_action(
@@ -95,7 +101,7 @@ class ScheduledTasks {
95
  );
96
  }
97
 
98
- register_deactivation_hook( MATOMO_ANALYTICS_FILE, array( $this, 'uninstall' ) );
99
  }
100
 
101
  public function get_last_time_before_cron( $event_name ) {
@@ -117,77 +123,80 @@ class ScheduledTasks {
117
  }
118
 
119
  public function get_all_events() {
120
- $events = array(
121
- self::EVENT_SYNC => array(
122
  'name' => 'Sync users & sites',
123
  'interval' => 'daily',
124
  'method' => 'sync',
125
- ),
126
- self::EVENT_ARCHIVE => array(
127
  'name' => 'Archive',
128
  'interval' => 'hourly',
129
  'method' => 'archive',
130
- ),
131
- self::EVENT_GEOIP => array(
132
  'name' => 'Update GeoIP DB',
133
  'interval' => 'matomo_monthly',
134
  'method' => 'update_geo_ip2_db',
135
- ),
136
- );
137
- if ($this->settings->should_disable_addhandler()) {
138
- $events[self::EVENT_DISABLE_ADDHANDLER] = array(
139
  'name' => 'Disable AddHandler',
140
  'interval' => 'hourly',
141
  'method' => 'disable_add_handler',
142
- );
143
  }
 
144
  return $events;
145
  }
146
 
147
- public function disable_add_handler($forceUndo = false)
148
- {
149
  $disable_addhandler = $this->settings->should_disable_addhandler();
150
- if ($disable_addhandler) {
151
  $this->logger->log( 'Scheduled tasks disabling addhandler' );
152
  try {
153
  Bootstrap::do_bootstrap();
154
 
155
- $files = Filesystem::globr(dirname(MATOMO_ANALYTICS_FILE), '.htaccess');
156
- foreach ($files as $file) {
157
- if (is_readable($file)) {
158
- $content = file_get_contents($file);
159
- $search = 'AddHandler';
 
 
160
  $replace = '# AddHandler';
161
- if ($forceUndo) {
162
- $search = '# AddHandler';
163
  $replace = 'AddHandler';
164
  }
165
- if (strpos($content, $search) !== false && ($forceUndo || strpos($content,$replace) === false)) {
166
- if (is_writeable($file)) {
167
- $content = str_replace($search, $replace, $content);
168
- @file_put_contents($file, $content);
 
 
169
  } else {
170
- $this->logger->log('Cannot update file as not writable ' . $file);
171
  }
172
  }
173
  }
174
  }
175
- } catch ( \Exception $e ) {
176
  $this->logger->log_exception( 'disable_addhandler', $e );
177
  throw $e;
178
  }
179
  }
180
  }
181
 
182
- private function check_try_update()
183
- {
184
  try {
185
  $installer = new Installer( $this->settings );
186
  if ( $installer->looks_like_it_is_installed() ) {
187
  $updater = new Updater( $this->settings );
188
  $updater->update_if_needed();
189
  }
190
- } catch ( \Exception $e ) {
191
  // we don't want to rethrow exception otherwise some other blogs might never sync
192
  $this->logger->log_exception( 'check_try_update', $e );
193
  }
@@ -199,7 +208,7 @@ class ScheduledTasks {
199
  try {
200
  $updater = new Updater( $this->settings );
201
  $updater->update();
202
- } catch ( \Exception $e ) {
203
  $this->logger->log_exception( 'cron_update', $e );
204
  throw $e;
205
  }
@@ -210,21 +219,21 @@ class ScheduledTasks {
210
  try {
211
  Bootstrap::do_bootstrap();
212
 
213
- $maxmind_license = $this->settings->get_global_option('maxmind_license_key');
214
- if (empty($maxmind_license)) {
215
- $db_url = GeoIp2::getDbIpLiteUrl();
216
- $asn_url = GeoIp2::getDbIpLiteUrl('asn');
217
  } else {
218
- $db_url = 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&suffix=tar.gz&license_key=' . $maxmind_license;
219
  $asn_url = 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&suffix=tar.gz&license_key=' . $maxmind_license;
220
  }
221
 
222
- Option::set( GeoIP2AutoUpdater::LOC_URL_OPTION_NAME, $db_url);
223
 
224
- if (Manager::getInstance()->isPluginActivated('Provider')) {
225
- Option::set( GeoIP2AutoUpdater::ISP_URL_OPTION_NAME, $asn_url);
226
  } else {
227
- Option::delete(GeoIP2AutoUpdater::ISP_URL_OPTION_NAME);
228
  }
229
 
230
  $updater = new GeoIP2AutoUpdater();
@@ -232,7 +241,7 @@ class ScheduledTasks {
232
  if ( LocationProvider::getCurrentProviderId() !== Php::ID && LocationProvider::getProviderById( Php::ID ) ) {
233
  LocationProvider::setCurrentProvider( Php::ID );
234
  }
235
- } catch ( \Exception $e ) {
236
  $this->logger->log_exception( 'update_geoip2', $e );
237
  throw $e;
238
  }
@@ -246,11 +255,11 @@ class ScheduledTasks {
246
  try {
247
  // we update the matomo url if needed/when possible. eg an update may be needed when site_url changes
248
  $installer = new Installer( $this->settings );
249
- if ($installer->looks_like_it_is_installed()) {
250
  Bootstrap::do_bootstrap();
251
  $installer->set_matomo_url();
252
  }
253
- } catch (\Exception $e) {
254
  $this->logger->log_exception( 'matomo_url_sync', $e );
255
  }
256
 
@@ -259,7 +268,7 @@ class ScheduledTasks {
259
  $site->sync_all();
260
  $user = new UserSync();
261
  $user->sync_all();
262
- } catch ( \Exception $e ) {
263
  $this->logger->log_exception( 'cron_sync', $e );
264
  throw $e;
265
  }
@@ -274,27 +283,31 @@ class ScheduledTasks {
274
 
275
  // we don't want any error triggered when a user vistis the website
276
  // that's because cron might be triggered during a regular request from a regular user (unless WP CRON is disabled and triggered manually)
277
- $should_rethrow_exception = is_admin() || (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) || (defined('MATOMO_PHPUNIT_TEST') && MATOMO_PHPUNIT_TEST);
278
 
279
  $this->logger->log( 'Scheduled tasks archive data' );
280
 
281
  try {
282
  Bootstrap::do_bootstrap();
283
- } catch ( \Exception $e ) {
284
  $this->logger->log_exception( 'archive_bootstrap', $e );
285
- if ($should_rethrow_exception || $force) {
286
  // we want to trigger an exception if it was forced from the UI
287
  throw $e;
288
  }
289
  }
290
 
291
- $archiver = new CronArchive();
 
292
  $archiver->concurrentRequestsPerWebsite = 1;
293
- $archiver->maxConcurrentArchivers = 1;
 
294
 
295
  if ( $force ) {
296
- $archiver->shouldArchiveAllSites = true;
297
- $archiver->disableScheduledTasks = true;
 
 
298
  }
299
 
300
  if ( is_multisite() ) {
@@ -304,7 +317,8 @@ class ScheduledTasks {
304
  $blog_id = get_current_blog_id();
305
  $idsite = Site::get_matomo_site_id( $blog_id );
306
  if ( ! empty( $idsite ) ) {
307
- $archiver->shouldArchiveSpecifiedSites = array( $idsite );
 
308
  } else {
309
  // there is no site mapped to it so there's no point in archiving it
310
  return;
@@ -316,22 +330,22 @@ class ScheduledTasks {
316
  $archiver->main();
317
 
318
  $archive_errors = $archiver->getErrors();
319
-
320
- } catch ( \Exception $e ) {
321
- $this->logger->log_exception( 'archive_main' , $e);
322
  $archive_errors = $archiver->getErrors();
323
 
324
- if (!empty($archive_errors)) {
325
  $message = '';
326
- foreach ($archiver->getErrors() as $error) {
327
- $message .= var_export($error, 1) . ' ';
 
328
  }
329
- $message = new \Exception(trim($message));
330
- $this->logger->log_exception('archive_errors', $message);
331
  }
332
 
333
- if ($throw_exception) {
334
- if ($should_rethrow_exception) {
335
  throw $e;
336
  }
337
  // we otherwise only log the error but don't throw an exception
9
 
10
  namespace WpMatomo;
11
 
12
+ use Exception;
13
  use Piwik\CronArchive;
14
  use Piwik\Filesystem;
15
  use Piwik\Option;
20
  use Piwik\Plugins\UserCountry\LocationProvider;
21
  use WpMatomo\Site\Sync as SiteSync;
22
  use WpMatomo\User\Sync as UserSync;
23
+ use WpMatomo\Paths;
24
 
25
  if ( ! defined( 'ABSPATH' ) ) {
26
  exit; // if accessed directly
27
  }
28
 
29
  class ScheduledTasks {
30
+ const EVENT_SYNC = 'matomo_scheduled_sync';
31
+ const EVENT_DISABLE_ADDHANDLER = 'matomo_scheduled_disable_addhandler';
32
+ const EVENT_ARCHIVE = 'matomo_scheduled_archive';
33
+ const EVENT_GEOIP = 'matomo_scheduled_geoipdb';
34
+ const EVENT_UPDATE = 'matomo_update_core';
35
 
36
  const KEY_BEFORE_CRON = 'before-cron-';
37
  const KEY_AFTER_CRON = 'after-cron-';
52
  }
53
 
54
  public function add_weekly_schedule( $schedules ) {
55
+ $schedules['matomo_monthly'] = [
56
  'interval' => 60 * 60 * 24 * 30,
57
  'display' => __( 'Monthly', 'matomo' ),
58
+ ];
59
 
60
  return $schedules;
61
  }
62
 
63
  public function schedule() {
64
+
65
+ add_action( self::EVENT_UPDATE, [ $this, 'perform_update' ] );
66
+ add_filter( 'cron_schedules', [ $this, 'add_weekly_schedule' ] );
67
 
68
  $self = $this;
69
  $event_priority = 10;
70
 
71
+ $installer = new Installer( $this->settings );
72
+ $looks_installed = $installer->looks_like_it_is_installed(); // we only schedule events when Matomo looks installed but we still listen to the actions in case the app triggers a one time update.
73
+
74
  foreach ( $this->get_all_events() as $event_name => $event_config ) {
75
+
76
+ if ( $looks_installed && ! wp_next_scheduled( $event_name ) ) {
77
  wp_schedule_event( time(), $event_config['interval'], $event_name );
78
  }
79
 
88
  );
89
 
90
  // actual event
91
+ add_action( $event_name, [ $this, $event_config['method'] ], $event_priority, $accepted_args = 0 );
92
 
93
  // logging last execution end time
94
  add_action(
101
  );
102
  }
103
 
104
+ register_deactivation_hook( MATOMO_ANALYTICS_FILE, [ $this, 'uninstall' ] );
105
  }
106
 
107
  public function get_last_time_before_cron( $event_name ) {
123
  }
124
 
125
  public function get_all_events() {
126
+ $events = [
127
+ self::EVENT_SYNC => [
128
  'name' => 'Sync users & sites',
129
  'interval' => 'daily',
130
  'method' => 'sync',
131
+ ],
132
+ self::EVENT_ARCHIVE => [
133
  'name' => 'Archive',
134
  'interval' => 'hourly',
135
  'method' => 'archive',
136
+ ],
137
+ self::EVENT_GEOIP => [
138
  'name' => 'Update GeoIP DB',
139
  'interval' => 'matomo_monthly',
140
  'method' => 'update_geo_ip2_db',
141
+ ],
142
+ ];
143
+ if ( $this->settings->should_disable_addhandler() ) {
144
+ $events[ self::EVENT_DISABLE_ADDHANDLER ] = [
145
  'name' => 'Disable AddHandler',
146
  'interval' => 'hourly',
147
  'method' => 'disable_add_handler',
148
+ ];
149
  }
150
+
151
  return $events;
152
  }
153
 
154
+ public function disable_add_handler( $force_undo = false ) {
 
155
  $disable_addhandler = $this->settings->should_disable_addhandler();
156
+ if ( $disable_addhandler ) {
157
  $this->logger->log( 'Scheduled tasks disabling addhandler' );
158
  try {
159
  Bootstrap::do_bootstrap();
160
 
161
+ $files = Filesystem::globr( dirname( MATOMO_ANALYTICS_FILE ), '.htaccess' );
162
+ foreach ( $files as $file ) {
163
+ if ( is_readable( $file ) ) {
164
+ // we don't need to access remote files
165
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
166
+ $content = file_get_contents( $file );
167
+ $search = 'AddHandler';
168
  $replace = '# AddHandler';
169
+ if ( $force_undo ) {
170
+ $search = '# AddHandler';
171
  $replace = 'AddHandler';
172
  }
173
+ if ( strpos( $content, $search ) !== false && ( $force_undo || strpos( $content, $replace ) === false ) ) {
174
+ if ( is_writeable( $file ) ) {
175
+ $content = str_replace( $search, $replace, $content );
176
+ $paths = new Paths();
177
+ $wp_filesystem = $paths->get_file_system();
178
+ $wp_filesystem->put_contents( $file, $content );
179
  } else {
180
+ $this->logger->log( 'Cannot update file as not writable ' . $file );
181
  }
182
  }
183
  }
184
  }
185
+ } catch ( Exception $e ) {
186
  $this->logger->log_exception( 'disable_addhandler', $e );
187
  throw $e;
188
  }
189
  }
190
  }
191
 
192
+ private function check_try_update() {
 
193
  try {
194
  $installer = new Installer( $this->settings );
195
  if ( $installer->looks_like_it_is_installed() ) {
196
  $updater = new Updater( $this->settings );
197
  $updater->update_if_needed();
198
  }
199
+ } catch ( Exception $e ) {
200
  // we don't want to rethrow exception otherwise some other blogs might never sync
201
  $this->logger->log_exception( 'check_try_update', $e );
202
  }
208
  try {
209
  $updater = new Updater( $this->settings );
210
  $updater->update();
211
+ } catch ( Exception $e ) {
212
  $this->logger->log_exception( 'cron_update', $e );
213
  throw $e;
214
  }
219
  try {
220
  Bootstrap::do_bootstrap();
221
 
222
+ $maxmind_license = $this->settings->get_global_option( 'maxmind_license_key' );
223
+ if ( empty( $maxmind_license ) ) {
224
+ $db_url = GeoIp2::getDbIpLiteUrl();
225
+ $asn_url = GeoIp2::getDbIpLiteUrl( 'asn' );
226
  } else {
227
+ $db_url = 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&suffix=tar.gz&license_key=' . $maxmind_license;
228
  $asn_url = 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&suffix=tar.gz&license_key=' . $maxmind_license;
229
  }
230
 
231
+ Option::set( GeoIP2AutoUpdater::LOC_URL_OPTION_NAME, $db_url );
232
 
233
+ if ( Manager::getInstance()->isPluginActivated( 'Provider' ) ) {
234
+ Option::set( GeoIP2AutoUpdater::ISP_URL_OPTION_NAME, $asn_url );
235
  } else {
236
+ Option::delete( GeoIP2AutoUpdater::ISP_URL_OPTION_NAME );
237
  }
238
 
239
  $updater = new GeoIP2AutoUpdater();
241
  if ( LocationProvider::getCurrentProviderId() !== Php::ID && LocationProvider::getProviderById( Php::ID ) ) {
242
  LocationProvider::setCurrentProvider( Php::ID );
243
  }
244
+ } catch ( Exception $e ) {
245
  $this->logger->log_exception( 'update_geoip2', $e );
246
  throw $e;
247
  }
255
  try {
256
  // we update the matomo url if needed/when possible. eg an update may be needed when site_url changes
257
  $installer = new Installer( $this->settings );
258
+ if ( $installer->looks_like_it_is_installed() ) {
259
  Bootstrap::do_bootstrap();
260
  $installer->set_matomo_url();
261
  }
262
+ } catch ( Exception $e ) {
263
  $this->logger->log_exception( 'matomo_url_sync', $e );
264
  }
265
 
268
  $site->sync_all();
269
  $user = new UserSync();
270
  $user->sync_all();
271
+ } catch ( Exception $e ) {
272
  $this->logger->log_exception( 'cron_sync', $e );
273
  throw $e;
274
  }
283
 
284
  // we don't want any error triggered when a user vistis the website
285
  // that's because cron might be triggered during a regular request from a regular user (unless WP CRON is disabled and triggered manually)
286
+ $should_rethrow_exception = is_admin() || ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) || ( defined( 'MATOMO_PHPUNIT_TEST' ) && MATOMO_PHPUNIT_TEST );
287
 
288
  $this->logger->log( 'Scheduled tasks archive data' );
289
 
290
  try {
291
  Bootstrap::do_bootstrap();
292
+ } catch ( Exception $e ) {
293
  $this->logger->log_exception( 'archive_bootstrap', $e );
294
+ if ( $should_rethrow_exception || $force ) {
295
  // we want to trigger an exception if it was forced from the UI
296
  throw $e;
297
  }
298
  }
299
 
300
+ $archiver = new CronArchive();
301
+ // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
302
  $archiver->concurrentRequestsPerWebsite = 1;
303
+ // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
304
+ $archiver->maxConcurrentArchivers = 1;
305
 
306
  if ( $force ) {
307
+ // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
308
+ $archiver->shouldArchiveAllSites = true;
309
+ // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
310
+ $archiver->disableScheduledTasks = true;
311
  }
312
 
313
  if ( is_multisite() ) {
317
  $blog_id = get_current_blog_id();
318
  $idsite = Site::get_matomo_site_id( $blog_id );
319
  if ( ! empty( $idsite ) ) {
320
+ // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
321
+ $archiver->shouldArchiveSpecifiedSites = [ $idsite ];
322
  } else {
323
  // there is no site mapped to it so there's no point in archiving it
324
  return;
330
  $archiver->main();
331
 
332
  $archive_errors = $archiver->getErrors();
333
+ } catch ( Exception $e ) {
334
+ $this->logger->log_exception( 'archive_main', $e );
 
335
  $archive_errors = $archiver->getErrors();
336
 
337
+ if ( ! empty( $archive_errors ) ) {
338
  $message = '';
339
+ foreach ( $archiver->getErrors() as $error ) {
340
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
341
+ $message .= var_export( $error, 1 ) . ' ';
342
  }
343
+ $message = new Exception( trim( $message ) );
344
+ $this->logger->log_exception( 'archive_errors', $message );
345
  }
346
 
347
+ if ( $throw_exception ) {
348
+ if ( $should_rethrow_exception ) {
349
  throw $e;
350
  }
351
  // we otherwise only log the error but don't throw an exception
classes/WpMatomo/Settings.php CHANGED
@@ -21,7 +21,6 @@ if ( ! defined( 'ABSPATH' ) ) {
21
  }
22
 
23
  class Settings {
24
-
25
  const OPTION_PREFIX = 'matomo-';
26
  const GLOBAL_OPTION_PREFIX = 'matomo_global-';
27
  const OPTION = 'matomo-option';
@@ -55,13 +54,13 @@ class Settings {
55
  *
56
  * @var array
57
  */
58
- private $default_global_settings = array(
59
  // Plugin settings
60
  'last_settings_update' => 0,
61
  self::OPTION_LAST_TRACKING_SETTINGS_CHANGE => 0,
62
- self::OPTION_KEY_STEALTH => array(),
63
- self::OPTION_KEY_CAPS_ACCESS => array(),
64
- self::NETWORK_CONFIG_OPTIONS => array(),
65
  self::DELETE_ALL_DATA_ON_UNINSTALL => true,
66
  self::SITE_CURRENCY => 'USD',
67
  // User settings: Stats configuration
@@ -75,8 +74,8 @@ class Settings {
75
  'track_ecommerce' => true,
76
  'track_search' => false,
77
  'track_404' => false,
78
- 'tagmanger_container_ids' => array(),
79
- 'add_post_annotations' => array(),
80
  'add_customvars_box' => false,
81
  'js_manually' => '',
82
  'noscript_manually' => '',
@@ -85,8 +84,8 @@ class Settings {
85
  'set_link_classes' => '',
86
  'set_download_classes' => '',
87
  'core_version' => '',
88
- 'version_history' => array(),
89
- 'mail_history' => array(),
90
  'disable_cookies' => false,
91
  'cookie_consent' => CookieConsent::REQUIRE_NONE,
92
  'force_post' => false,
@@ -108,23 +107,23 @@ class Settings {
108
  'force_protocol' => 'disabled',
109
  'maxmind_license_key' => '',
110
  self::SHOW_GET_STARTED_PAGE => 1,
111
- );
112
 
113
  /**
114
  * Settings stored per blog
115
  *
116
  * @var array
117
  */
118
- private $default_blog_settings = array(
119
  'noscript_code' => '',
120
  'tracking_code' => '',
121
  self::OPTION_LAST_TRACKING_CODE_UPDATE => 0,
122
- );
123
 
124
- private $global_settings = array();
125
- private $blog_settings = array();
126
 
127
- private $settings_changed = array();
128
 
129
  /**
130
  * @var Logger
@@ -142,21 +141,21 @@ class Settings {
142
  }
143
 
144
  public function init_settings() {
145
- $this->settings_changed = array();
146
- $this->global_settings = array();
147
- $this->blog_settings = array();
148
 
149
  if ( $this->is_network_enabled() ) {
150
- $global_settings = get_site_option( self::OPTION_GLOBAL, array() );
151
  } else {
152
- $global_settings = get_option( self::OPTION_GLOBAL, array() );
153
  }
154
 
155
  if ( ! empty( $global_settings ) && is_array( $global_settings ) ) {
156
  $this->global_settings = $global_settings;
157
  }
158
 
159
- $settings = get_option( self::OPTION, array() );
160
 
161
  if ( ! empty( $settings ) && is_array( $settings ) ) {
162
  $this->blog_settings = $settings;
@@ -164,10 +163,11 @@ class Settings {
164
  }
165
 
166
  public function get_customised_global_settings() {
167
- $custom_settings = array();
168
 
169
  foreach ( $this->global_settings as $key => $val ) {
170
  if ( isset( $this->default_global_settings[ $key ] )
 
171
  && $this->default_global_settings[ $key ] != $val ) {
172
  $custom_settings[ $key ] = $val;
173
  }
@@ -223,7 +223,7 @@ class Settings {
223
  update_option( self::OPTION, $this->blog_settings );
224
 
225
  $keys_changed = array_values( array_unique( $this->settings_changed ) );
226
- $this->settings_changed = array();
227
 
228
  foreach ( $keys_changed as $key_changed ) {
229
  do_action( 'matomo_setting_change_' . $key_changed );
@@ -268,11 +268,12 @@ class Settings {
268
  }
269
 
270
  private function convert_type( $value, $type ) {
271
- if ( $type === 'array' && empty( $value ) ) {
272
- $value = array(); // prevent eg converting '' to array('')
273
  } else {
274
  settype( $value, $type );
275
  }
 
276
  return $value;
277
  }
278
 
@@ -289,7 +290,7 @@ class Settings {
289
  }
290
 
291
  if ( ! isset( $this->global_settings[ $key ] )
292
- || $this->global_settings[ $key ] !== $value ) {
293
  $this->settings_changed[] = $key;
294
  $this->logger->log( 'Changed global option ' . $key . ': ' . ( is_array( $value ) ? wp_json_encode( $value ) : $value ) );
295
 
@@ -310,7 +311,7 @@ class Settings {
310
  }
311
 
312
  if ( ! isset( $this->blog_settings[ $key ] )
313
- || $this->blog_settings[ $key ] !== $value ) {
314
  $this->settings_changed[] = $key;
315
  $this->logger->log( 'Changed option ' . $key . ': ' . $value );
316
  $this->blog_settings[ $key ] = $value;
@@ -335,12 +336,12 @@ class Settings {
335
  }
336
  }
337
 
338
- public function should_disable_addhandler()
339
- {
340
- if ($this->force_disable_addhandler) {
341
  return true;
342
  }
343
- return defined('MATOMO_DISABLE_ADDHANDLER') && MATOMO_DISABLE_ADDHANDLER;
 
344
  }
345
 
346
  /**
@@ -376,7 +377,7 @@ class Settings {
376
 
377
  private function should_save_tracking_code_across_sites() {
378
  return $this->is_network_enabled()
379
- && $this->get_global_option( 'track_mode' ) === TrackingSettings::TRACK_MODE_MANUALLY;
380
  }
381
 
382
  public function get_js_tracking_code() {
@@ -402,7 +403,7 @@ class Settings {
402
  public function get_tracking_cookie_domain() {
403
  if ( $this->get_global_option( 'track_across' )
404
  || $this->get_global_option( 'track_crossdomain_linking' ) ) {
405
- $host = @parse_url( home_url(), PHP_URL_HOST );
406
  if ( ! empty( $host ) ) {
407
  return '*.' . $host;
408
  }
@@ -411,13 +412,12 @@ class Settings {
411
  return '';
412
  }
413
 
414
- public function should_delete_all_data_on_uninstall()
415
- {
416
- if (defined( 'MATOMO_REMOVE_ALL_DATA' )) {
417
  return (bool) MATOMO_REMOVE_ALL_DATA;
418
  }
419
 
420
- return (bool) $this->get_global_option(self::DELETE_ALL_DATA_ON_UNINSTALL);
421
  }
422
 
423
  /**
@@ -455,7 +455,7 @@ class Settings {
455
  }
456
 
457
  public function is_tracking_enabled() {
458
- return $this->get_global_option( 'track_mode' ) != 'disabled';
459
  }
460
 
461
  /**
@@ -476,7 +476,7 @@ class Settings {
476
  }
477
 
478
  public function track_user_id_enabled() {
479
- return $this->get_global_option( 'track_user_id' ) != 'disabled';
480
  }
481
 
482
  public function track_search_enabled() {
21
  }
22
 
23
  class Settings {
 
24
  const OPTION_PREFIX = 'matomo-';
25
  const GLOBAL_OPTION_PREFIX = 'matomo_global-';
26
  const OPTION = 'matomo-option';
54
  *
55
  * @var array
56
  */
57
+ private $default_global_settings = [
58
  // Plugin settings
59
  'last_settings_update' => 0,
60
  self::OPTION_LAST_TRACKING_SETTINGS_CHANGE => 0,
61
+ self::OPTION_KEY_STEALTH => [],
62
+ self::OPTION_KEY_CAPS_ACCESS => [],
63
+ self::NETWORK_CONFIG_OPTIONS => [],
64
  self::DELETE_ALL_DATA_ON_UNINSTALL => true,
65
  self::SITE_CURRENCY => 'USD',
66
  // User settings: Stats configuration
74
  'track_ecommerce' => true,
75
  'track_search' => false,
76
  'track_404' => false,
77
+ 'tagmanger_container_ids' => [],
78
+ 'add_post_annotations' => [],
79
  'add_customvars_box' => false,
80
  'js_manually' => '',
81
  'noscript_manually' => '',
84
  'set_link_classes' => '',
85
  'set_download_classes' => '',
86
  'core_version' => '',
87
+ 'version_history' => [],
88
+ 'mail_history' => [],
89
  'disable_cookies' => false,
90
  'cookie_consent' => CookieConsent::REQUIRE_NONE,
91
  'force_post' => false,
107
  'force_protocol' => 'disabled',
108
  'maxmind_license_key' => '',
109
  self::SHOW_GET_STARTED_PAGE => 1,
110
+ ];
111
 
112
  /**
113
  * Settings stored per blog
114
  *
115
  * @var array
116
  */
117
+ private $default_blog_settings = [
118
  'noscript_code' => '',
119
  'tracking_code' => '',
120
  self::OPTION_LAST_TRACKING_CODE_UPDATE => 0,
121
+ ];
122
 
123
+ private $global_settings = [];
124
+ private $blog_settings = [];
125
 
126
+ private $settings_changed = [];
127
 
128
  /**
129
  * @var Logger
141
  }
142
 
143
  public function init_settings() {
144
+ $this->settings_changed = [];
145
+ $this->global_settings = [];
146
+ $this->blog_settings = [];
147
 
148
  if ( $this->is_network_enabled() ) {
149
+ $global_settings = get_site_option( self::OPTION_GLOBAL, [] );
150
  } else {
151
+ $global_settings = get_option( self::OPTION_GLOBAL, [] );
152
  }
153
 
154
  if ( ! empty( $global_settings ) && is_array( $global_settings ) ) {
155
  $this->global_settings = $global_settings;
156
  }
157
 
158
+ $settings = get_option( self::OPTION, [] );
159
 
160
  if ( ! empty( $settings ) && is_array( $settings ) ) {
161
  $this->blog_settings = $settings;
163
  }
164
 
165
  public function get_customised_global_settings() {
166
+ $custom_settings = [];
167
 
168
  foreach ( $this->global_settings as $key => $val ) {
169
  if ( isset( $this->default_global_settings[ $key ] )
170
+ // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
171
  && $this->default_global_settings[ $key ] != $val ) {
172
  $custom_settings[ $key ] = $val;
173
  }
223
  update_option( self::OPTION, $this->blog_settings );
224
 
225
  $keys_changed = array_values( array_unique( $this->settings_changed ) );
226
+ $this->settings_changed = [];
227
 
228
  foreach ( $keys_changed as $key_changed ) {
229
  do_action( 'matomo_setting_change_' . $key_changed );
268
  }
269
 
270
  private function convert_type( $value, $type ) {
271
+ if ( 'array' === $type && empty( $value ) ) {
272
+ $value = []; // prevent eg converting '' to array('')
273
  } else {
274
  settype( $value, $type );
275
  }
276
+
277
  return $value;
278
  }
279
 
290
  }
291
 
292
  if ( ! isset( $this->global_settings[ $key ] )
293
+ || $this->global_settings[ $key ] !== $value ) {
294
  $this->settings_changed[] = $key;
295
  $this->logger->log( 'Changed global option ' . $key . ': ' . ( is_array( $value ) ? wp_json_encode( $value ) : $value ) );
296
 
311
  }
312
 
313
  if ( ! isset( $this->blog_settings[ $key ] )
314
+ || $this->blog_settings[ $key ] !== $value ) {
315
  $this->settings_changed[] = $key;
316
  $this->logger->log( 'Changed option ' . $key . ': ' . $value );
317
  $this->blog_settings[ $key ] = $value;
336
  }
337
  }
338
 
339
+ public function should_disable_addhandler() {
340
+ if ( $this->force_disable_addhandler ) {
 
341
  return true;
342
  }
343
+
344
+ return defined( 'MATOMO_DISABLE_ADDHANDLER' ) && MATOMO_DISABLE_ADDHANDLER;
345
  }
346
 
347
  /**
377
 
378
  private function should_save_tracking_code_across_sites() {
379
  return $this->is_network_enabled()
380
+ && $this->get_global_option( 'track_mode' ) === TrackingSettings::TRACK_MODE_MANUALLY;
381
  }
382
 
383
  public function get_js_tracking_code() {
403
  public function get_tracking_cookie_domain() {
404
  if ( $this->get_global_option( 'track_across' )
405
  || $this->get_global_option( 'track_crossdomain_linking' ) ) {
406
+ $host = wp_parse_url( home_url(), PHP_URL_HOST );
407
  if ( ! empty( $host ) ) {
408
  return '*.' . $host;
409
  }
412
  return '';
413
  }
414
 
415
+ public function should_delete_all_data_on_uninstall() {
416
+ if ( defined( 'MATOMO_REMOVE_ALL_DATA' ) ) {
 
417
  return (bool) MATOMO_REMOVE_ALL_DATA;
418
  }
419
 
420
+ return (bool) $this->get_global_option( self::DELETE_ALL_DATA_ON_UNINSTALL );
421
  }
422
 
423
  /**
455
  }
456
 
457
  public function is_tracking_enabled() {
458
+ return $this->get_global_option( 'track_mode' ) !== 'disabled';
459
  }
460
 
461
  /**
476
  }
477
 
478
  public function track_user_id_enabled() {
479
+ return $this->get_global_option( 'track_user_id' ) !== 'disabled';
480
  }
481
 
482
  public function track_search_enabled() {
classes/WpMatomo/Site.php CHANGED
@@ -14,7 +14,6 @@ if ( ! defined( 'ABSPATH' ) ) {
14
  }
15
 
16
  class Site {
17
-
18
  const SITE_MAPPING_PREFIX = 'matomo-site-id-';
19
 
20
  /**
14
  }
15
 
16
  class Site {
 
17
  const SITE_MAPPING_PREFIX = 'matomo-site-id-';
18
 
19
  /**
classes/WpMatomo/Site/Sync.php CHANGED
@@ -9,14 +9,14 @@
9
 
10
  namespace WpMatomo\Site;
11
 
 
12
  use Piwik\Access;
13
- use Piwik\API\Request;
14
- use Piwik\Common;
15
  use Piwik\Config;
16
  use Piwik\Container\StaticContainer;
17
  use Piwik\Intl\Data\Provider\CurrencyDataProvider;
18
- use Piwik\Plugins\SitesManager\Model;
19
  use Piwik\Plugins\SitesManager;
 
 
20
  use WpMatomo\Bootstrap;
21
  use WpMatomo\Installer;
22
  use WpMatomo\Logger;
@@ -28,6 +28,10 @@ if ( ! defined( 'ABSPATH' ) ) {
28
  exit; // if accessed directly
29
  }
30
 
 
 
 
 
31
  class Sync {
32
  const MAX_LENGTH_SITE_NAME = 90;
33
 
@@ -41,33 +45,32 @@ class Sync {
41
  */
42
  private $settings;
43
 
44
- /**
45
- * @var SyncConfig
46
- */
47
  private $config_sync;
48
 
49
  public function __construct( Settings $settings ) {
50
- $this->logger = new Logger();
51
- $this->settings = $settings;
52
  $this->config_sync = new SyncConfig( $settings );
53
  }
54
 
55
  public function register_hooks() {
56
- add_action( 'update_option_blogname', array( $this, 'sync_current_site_ignore_error' ) );
57
- add_action( 'update_option_home', array( $this, 'sync_current_site_ignore_error' ) );
58
- add_action( 'update_option_siteurl', array( $this, 'sync_current_site_ignore_error' ) );
59
- add_action( 'update_option_timezone_string', array( $this, 'sync_current_site_ignore_error' ) );
60
- add_action( 'matomo_setting_change_track_ecommerce', array( $this, 'sync_current_site_ignore_error' ) );
61
- add_action( 'matomo_setting_change_site_currency', array( $this, 'sync_current_site_ignore_error' ) );
62
  }
63
 
64
- public function sync_current_site_ignore_error()
65
- {
66
  try {
67
  $this->sync_current_site();
68
- } catch (\Exception $e) {
69
- $this->logger->log( 'Ignoring site sync error: ' . $e->getMessage());
70
- $this->logger->log_exception('sync_site_ignore', $e);
71
  }
72
  }
73
 
@@ -78,7 +81,7 @@ class Sync {
78
 
79
  if ( is_multisite() && function_exists( 'get_sites' ) ) {
80
  foreach ( get_sites() as $site ) {
81
- /** @var \WP_Site $site */
82
  switch_to_blog( $site->blog_id );
83
  try {
84
  $installer = new Installer( $this->settings );
@@ -90,7 +93,7 @@ class Sync {
90
  Bootstrap::set_not_bootstrapped();
91
  $config = Config::getInstance();
92
  $installed = $config->PluginsInstalled;
93
- $installed['PluginsInstalled'] = array();
94
  $config->PluginsInstalled = $installed;
95
 
96
  if ( $installer->can_be_installed() ) {
@@ -101,7 +104,7 @@ class Sync {
101
  }
102
 
103
  $success = $this->sync_site( $site->blog_id, $site->blogname, $site->siteurl );
104
- } catch ( \Exception $e ) {
105
  $success = false;
106
  // we don't want to rethrow exception otherwise some other blogs might never sync
107
  $this->logger->log( 'Matomo error syncing site: ' . $e->getMessage() );
@@ -134,76 +137,77 @@ class Sync {
134
  $blog_name = substr( $blog_name, 0, self::MAX_LENGTH_SITE_NAME );
135
  }
136
 
137
- $track_ecommerce = (int) $this->settings->get_global_option( 'track_ecommerce' );
138
- $site_currency = $this->settings->get_global_option( Settings::SITE_CURRENCY );
139
  $detected_timezone = $this->detect_timezone();
140
 
141
- $dataProvider = StaticContainer::get(CurrencyDataProvider::class);
142
- $valid_currencies = $dataProvider->getCurrencyList();
143
- if (!array_key_exists($site_currency, $valid_currencies)){
144
  $site_currency = 'USD';
145
  }
146
 
147
  if ( ! empty( $idsite ) ) {
148
  $this->logger->log( 'Matomo site is known for blog (' . $idsite . ')... will update' );
149
 
150
- $sites_manager_model = new Model();
151
- $site = $sites_manager_model->getSiteFromId($idsite);
152
- if (!empty($site)) {
153
- // if site doesn't exist for some reason then we have to create it
154
- if ($site['name'] != $blog_name
155
- || $site['main_url'] != $blog_url
156
- || $site['ecommerce'] != $track_ecommerce
157
- || $site['currency'] != $site_currency
158
- || $site['timezone'] != $detected_timezone) {
159
-
160
- /** @var \WP_Site $site */
161
- $params = array(
162
- 'name' => $blog_name,
163
- 'main_url' => $blog_url,
164
- 'ecommerce' => $track_ecommerce,
165
- 'currency' => $site_currency,
166
- 'timezone' => $detected_timezone,
167
- );
168
- $sites_manager_model->updateSite( $params, $idsite );
169
-
170
- do_action( 'matomo_site_synced', $idsite, $blog_id );
171
-
172
- // no actual setting changed but we make sure the tracking code will be updated after an update
173
- $this->settings->apply_tracking_related_changes( array() );
174
- }
175
-
176
- $this->config_sync->sync_config_for_current_site();
177
-
178
- return true;
179
- }
 
180
  }
181
 
182
  $this->logger->log( 'Matomo site is not known for blog... will create site' );
183
 
184
- /** @var \WP_Site $site */
185
- $idsite = null;
186
 
187
  $this->set_enable_sites_admin( 1 );
188
 
189
  Access::doAsSuperUser(
190
  function () use ( $blog_name, $blog_url, $detected_timezone, $track_ecommerce, &$idsite, $site_currency ) {
191
- SitesManager\API::unsetInstance();
192
- // we need to unset the instance to make sure it fetches the
193
- // up to date dependencies eg current plugin manager etc
194
-
195
- $idsite = SitesManager\API::getInstance()->addSite(
196
- $blog_name,
197
- array( $blog_url ),
198
- $track_ecommerce,
199
- $site_search = null,
200
- $search_keyword_parameters = null,
201
- $search_category_parameters = null,
202
- $excluded_ips = null,
203
- $excluded_query_parameters = null,
204
- $detected_timezone,
205
- $site_currency
206
- );
207
  }
208
  );
209
  $this->set_enable_sites_admin( 0 );
@@ -218,7 +222,7 @@ class Sync {
218
 
219
  Site::map_matomo_site_id( $blog_id, $idsite );
220
 
221
- $this->config_sync->sync_config_for_current_site();
222
 
223
  do_action( 'matomo_site_synced', $idsite, $blog_id );
224
 
@@ -252,7 +256,7 @@ class Sync {
252
  return $timezone;
253
  }
254
 
255
- $dst = (bool) date( 'I' );
256
  foreach ( timezone_abbreviations_list() as $abbr ) {
257
  foreach ( $abbr as $city ) {
258
  if ( $dst === (bool) $city['dst']
@@ -280,16 +284,17 @@ class Sync {
280
 
281
  private function check_and_try_to_set_default_timezone( $timezone ) {
282
  try {
283
- Access::doAsSuperUser(function () use ($timezone) {
284
- // make sure we're loading the latest instance with all up to date dependencies... mainly needed for tests
285
- SitesManager\API::unsetInstance();
286
- SitesManager\API::getInstance()->setDefaultTimezone( $timezone );
287
- });
288
- } catch ( \Exception $e ) {
 
 
289
  return false;
290
  }
291
 
292
  return true;
293
  }
294
-
295
  }
9
 
10
  namespace WpMatomo\Site;
11
 
12
+ use Exception;
13
  use Piwik\Access;
 
 
14
  use Piwik\Config;
15
  use Piwik\Container\StaticContainer;
16
  use Piwik\Intl\Data\Provider\CurrencyDataProvider;
 
17
  use Piwik\Plugins\SitesManager;
18
+ use Piwik\Plugins\SitesManager\Model;
19
+ use WP_Site;
20
  use WpMatomo\Bootstrap;
21
  use WpMatomo\Installer;
22
  use WpMatomo\Logger;
28
  exit; // if accessed directly
29
  }
30
 
31
+ /**
32
+ * Properties coming from matomo
33
+ * phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
34
+ */
35
  class Sync {
36
  const MAX_LENGTH_SITE_NAME = 90;
37
 
45
  */
46
  private $settings;
47
 
48
+ /**
49
+ * @var SyncConfig
50
+ */
51
  private $config_sync;
52
 
53
  public function __construct( Settings $settings ) {
54
+ $this->logger = new Logger();
55
+ $this->settings = $settings;
56
  $this->config_sync = new SyncConfig( $settings );
57
  }
58
 
59
  public function register_hooks() {
60
+ add_action( 'update_option_blogname', [ $this, 'sync_current_site_ignore_error' ] );
61
+ add_action( 'update_option_home', [ $this, 'sync_current_site_ignore_error' ] );
62
+ add_action( 'update_option_siteurl', [ $this, 'sync_current_site_ignore_error' ] );
63
+ add_action( 'update_option_timezone_string', [ $this, 'sync_current_site_ignore_error' ] );
64
+ add_action( 'matomo_setting_change_track_ecommerce', [ $this, 'sync_current_site_ignore_error' ] );
65
+ add_action( 'matomo_setting_change_site_currency', [ $this, 'sync_current_site_ignore_error' ] );
66
  }
67
 
68
+ public function sync_current_site_ignore_error() {
 
69
  try {
70
  $this->sync_current_site();
71
+ } catch ( Exception $e ) {
72
+ $this->logger->log( 'Ignoring site sync error: ' . $e->getMessage() );
73
+ $this->logger->log_exception( 'sync_site_ignore', $e );
74
  }
75
  }
76
 
81
 
82
  if ( is_multisite() && function_exists( 'get_sites' ) ) {
83
  foreach ( get_sites() as $site ) {
84
+ /** @var WP_Site $site */
85
  switch_to_blog( $site->blog_id );
86
  try {
87
  $installer = new Installer( $this->settings );
93
  Bootstrap::set_not_bootstrapped();
94
  $config = Config::getInstance();
95
  $installed = $config->PluginsInstalled;
96
+ $installed['PluginsInstalled'] = [];
97
  $config->PluginsInstalled = $installed;
98
 
99
  if ( $installer->can_be_installed() ) {
104
  }
105
 
106
  $success = $this->sync_site( $site->blog_id, $site->blogname, $site->siteurl );
107
+ } catch ( Exception $e ) {
108
  $success = false;
109
  // we don't want to rethrow exception otherwise some other blogs might never sync
110
  $this->logger->log( 'Matomo error syncing site: ' . $e->getMessage() );
137
  $blog_name = substr( $blog_name, 0, self::MAX_LENGTH_SITE_NAME );
138
  }
139
 
140
+ $track_ecommerce = (int) $this->settings->get_global_option( 'track_ecommerce' );
141
+ $site_currency = $this->settings->get_global_option( Settings::SITE_CURRENCY );
142
  $detected_timezone = $this->detect_timezone();
143
 
144
+ $data_provider = StaticContainer::get( CurrencyDataProvider::class );
145
+ $valid_currencies = $data_provider->getCurrencyList();
146
+ if ( ! array_key_exists( $site_currency, $valid_currencies ) ) {
147
  $site_currency = 'USD';
148
  }
149
 
150
  if ( ! empty( $idsite ) ) {
151
  $this->logger->log( 'Matomo site is known for blog (' . $idsite . ')... will update' );
152
 
153
+ $sites_manager_model = new Model();
154
+ $site = $sites_manager_model->getSiteFromId( $idsite );
155
+ if ( ! empty( $site ) ) {
156
+ // if site doesn't exist for some reason then we have to create it
157
+ if ( $site['name'] !== $blog_name
158
+ || $site['main_url'] !== $blog_url
159
+ // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
160
+ || $site['ecommerce'] != $track_ecommerce
161
+ || $site['currency'] !== $site_currency
162
+ || $site['timezone'] !== $detected_timezone ) {
163
+
164
+ /** @var WP_Site $site */
165
+ $params = [
166
+ 'name' => $blog_name,
167
+ 'main_url' => $blog_url,
168
+ 'ecommerce' => $track_ecommerce,
169
+ 'currency' => $site_currency,
170
+ 'timezone' => $detected_timezone,
171
+ ];
172
+ $sites_manager_model->updateSite( $params, $idsite );
173
+
174
+ do_action( 'matomo_site_synced', $idsite, $blog_id );
175
+
176
+ // no actual setting changed but we make sure the tracking code will be updated after an update
177
+ $this->settings->apply_tracking_related_changes( [] );
178
+ }
179
+
180
+ $this->config_sync->sync_config_for_current_site();
181
+
182
+ return true;
183
+ }
184
  }
185
 
186
  $this->logger->log( 'Matomo site is not known for blog... will create site' );
187
 
188
+ /** @var WP_Site $site */
189
+ $idsite = null;
190
 
191
  $this->set_enable_sites_admin( 1 );
192
 
193
  Access::doAsSuperUser(
194
  function () use ( $blog_name, $blog_url, $detected_timezone, $track_ecommerce, &$idsite, $site_currency ) {
195
+ SitesManager\API::unsetInstance();
196
+ // we need to unset the instance to make sure it fetches the
197
+ // up to date dependencies eg current plugin manager etc
198
+
199
+ $idsite = SitesManager\API::getInstance()->addSite(
200
+ $blog_name,
201
+ [ $blog_url ],
202
+ $track_ecommerce,
203
+ $site_search = null,
204
+ $search_keyword_parameters = null,
205
+ $search_category_parameters = null,
206
+ $excluded_ips = null,
207
+ $excluded_query_parameters = null,
208
+ $detected_timezone,
209
+ $site_currency
210
+ );
211
  }
212
  );
213
  $this->set_enable_sites_admin( 0 );
222
 
223
  Site::map_matomo_site_id( $blog_id, $idsite );
224
 
225
+ $this->config_sync->sync_config_for_current_site();
226
 
227
  do_action( 'matomo_site_synced', $idsite, $blog_id );
228
 
256
  return $timezone;
257
  }
258
 
259
+ $dst = (bool) gmdate( 'I' );
260
  foreach ( timezone_abbreviations_list() as $abbr ) {
261
  foreach ( $abbr as $city ) {
262
  if ( $dst === (bool) $city['dst']
284
 
285
  private function check_and_try_to_set_default_timezone( $timezone ) {
286
  try {
287
+ Access::doAsSuperUser(
288
+ function () use ( $timezone ) {
289
+ // make sure we're loading the latest instance with all up to date dependencies... mainly needed for tests
290
+ SitesManager\API::unsetInstance();
291
+ SitesManager\API::getInstance()->setDefaultTimezone( $timezone );
292
+ }
293
+ );
294
+ } catch ( Exception $e ) {
295
  return false;
296
  }
297
 
298
  return true;
299
  }
 
300
  }
classes/WpMatomo/Site/Sync/SyncConfig.php CHANGED
@@ -10,121 +10,118 @@
10
  namespace WpMatomo\Site\Sync;
11
 
12
  use Piwik\Config as PiwikConfig;
 
13
  use WpMatomo\Bootstrap;
14
  use WpMatomo\Logger;
15
  use WpMatomo\ScheduledTasks;
16
  use WpMatomo\Settings;
17
 
18
  if ( ! defined( 'ABSPATH' ) ) {
19
- exit; // if accessed directly
20
  }
21
 
22
- class SyncConfig
23
- {
24
- /**
25
- * @var Logger
26
- */
27
- private $logger;
28
-
29
- /**
30
- * @var Settings
31
- */
32
- private $settings;
33
-
34
- public function __construct(Settings $settings)
35
- {
36
- $this->logger = new Logger();
37
- $this->settings = $settings;
38
- }
39
-
40
- public function sync_config_for_current_site()
41
- {
42
- if ($this->settings->is_network_enabled()) {
43
- $config = PiwikConfig::getInstance();
44
- $has_change = false;
45
- foreach ($this->get_all() as $category => $keys) {
46
- $cat = $config->{$category};
47
- if (empty($cat)) {
48
- $cat = array();
49
- }
50
-
51
- if (empty($keys) && !empty($cat)) {
52
- // need to unset all values
53
- $has_change = true;
54
- $config->{$category} = array();
55
- }
56
-
57
- if (!empty($keys)) {
58
- foreach ($keys as $key => $value) {
59
- if (!isset($cat[$key]) || $cat[$key] != $value) {
60
- $has_change = true;
61
- $cat[$key] = $value;
62
- $config->{$category} = $cat;
63
- }
64
- }
65
- }
66
- }
67
- if ($has_change) {
68
- $config->forceSave();
69
- }
70
- }
71
- }
72
-
73
- private function get_all()
74
- {
75
- $options = $this->settings->get_global_option(Settings::NETWORK_CONFIG_OPTIONS);
76
-
77
- if (empty($options) || !is_array($options)) {
78
- $options = array();
79
- }
80
-
81
- return $options;
82
- }
83
-
84
- public function get_config_value($group, $key)
85
- {
86
- if ($this->settings->is_network_enabled()) {
87
- $config = $this->get_all();
88
- if (isset($config[$group][$key])) {
89
- return $config[$group][$key];
90
- }
91
- } else {
92
- Bootstrap::do_bootstrap();
93
- $config = PiwikConfig::getInstance();
94
- $the_group = $config->{$group};
95
- if (!empty($the_group) && isset($the_group[$key])) {
96
- return $the_group[$key];
97
- }
98
- }
99
- }
100
-
101
- public function set_config_value($group, $key, $value)
102
- {
103
- if ($this->settings->is_network_enabled()) {
104
- $config = $this->get_all();
105
-
106
- if (!isset($config[$group])) {
107
- $config[$group] = array();
108
- }
109
- $config[$group][$key] = $value;
110
-
111
- $this->settings->apply_changes(array(
112
- Settings::NETWORK_CONFIG_OPTIONS => $config
113
- ));
114
- // need to update all config files
115
- wp_schedule_single_event( time() + 5, ScheduledTasks::EVENT_SYNC );
116
-
117
- } elseif (!\WpMatomo::is_safe_mode()) {
118
- Bootstrap::do_bootstrap();
119
- $config = PiwikConfig::getInstance();
120
- $the_group = $config->{$group};
121
- if (empty($the_group)) {
122
- $the_group = array();
123
- }
124
- $the_group[$key] = $value;
125
- $config->{$group} = $the_group;
126
- $config->forceSave();
127
- }
128
- }
129
-
130
  }
10
  namespace WpMatomo\Site\Sync;
11
 
12
  use Piwik\Config as PiwikConfig;
13
+ use WpMatomo;
14
  use WpMatomo\Bootstrap;
15
  use WpMatomo\Logger;
16
  use WpMatomo\ScheduledTasks;
17
  use WpMatomo\Settings;
18
 
19
  if ( ! defined( 'ABSPATH' ) ) {
20
+ exit; // if accessed directly
21
  }
22
 
23
+ class SyncConfig {
24
+
25
+ /**
26
+ * @var Logger
27
+ */
28
+ private $logger;
29
+
30
+ /**
31
+ * @var Settings
32
+ */
33
+ private $settings;
34
+
35
+ public function __construct( Settings $settings ) {
36
+ $this->logger = new Logger();
37
+ $this->settings = $settings;
38
+ }
39
+
40
+ public function sync_config_for_current_site() {
41
+ if ( $this->settings->is_network_enabled() ) {
42
+ $config = PiwikConfig::getInstance();
43
+ $has_change = false;
44
+ foreach ( $this->get_all() as $category => $keys ) {
45
+ $cat = $config->{$category};
46
+ if ( empty( $cat ) ) {
47
+ $cat = [];
48
+ }
49
+
50
+ if ( empty( $keys ) && ! empty( $cat ) ) {
51
+ // need to unset all values
52
+ $has_change = true;
53
+ $config->{$category} = [];
54
+ }
55
+
56
+ if ( ! empty( $keys ) ) {
57
+ foreach ( $keys as $key => $value ) {
58
+ // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
59
+ if ( ! isset( $cat[ $key ] ) || $cat[ $key ] != $value ) {
60
+ $has_change = true;
61
+ $cat[ $key ] = $value;
62
+ $config->{$category} = $cat;
63
+ }
64
+ }
65
+ }
66
+ }
67
+ if ( $has_change ) {
68
+ $config->forceSave();
69
+ }
70
+ }
71
+ }
72
+
73
+ private function get_all() {
74
+ $options = $this->settings->get_global_option( Settings::NETWORK_CONFIG_OPTIONS );
75
+
76
+ if ( empty( $options ) || ! is_array( $options ) ) {
77
+ $options = [];
78
+ }
79
+
80
+ return $options;
81
+ }
82
+
83
+ public function get_config_value( $group, $key ) {
84
+ if ( $this->settings->is_network_enabled() ) {
85
+ $config = $this->get_all();
86
+ if ( isset( $config[ $group ][ $key ] ) ) {
87
+ return $config[ $group ][ $key ];
88
+ }
89
+ } else {
90
+ Bootstrap::do_bootstrap();
91
+ $config = PiwikConfig::getInstance();
92
+ $the_group = $config->{$group};
93
+ if ( ! empty( $the_group ) && isset( $the_group[ $key ] ) ) {
94
+ return $the_group[ $key ];
95
+ }
96
+ }
97
+ }
98
+
99
+ public function set_config_value( $group, $key, $value ) {
100
+ if ( $this->settings->is_network_enabled() ) {
101
+ $config = $this->get_all();
102
+
103
+ if ( ! isset( $config[ $group ] ) ) {
104
+ $config[ $group ] = [];
105
+ }
106
+ $config[ $group ][ $key ] = $value;
107
+
108
+ $this->settings->apply_changes(
109
+ [
110
+ Settings::NETWORK_CONFIG_OPTIONS => $config,
111
+ ]
112
+ );
113
+ // need to update all config files
114
+ wp_schedule_single_event( time() + 5, ScheduledTasks::EVENT_SYNC );
115
+ } elseif ( ! WpMatomo::is_safe_mode() ) {
116
+ Bootstrap::do_bootstrap();
117
+ $config = PiwikConfig::getInstance();
118
+ $the_group = $config->{$group};
119
+ if ( empty( $the_group ) ) {
120
+ $the_group = [];
121
+ }
122
+ $the_group[ $key ] = $value;
123
+ $config->{$group} = $the_group;
124
+ $config->forceSave();
125
+ }
126
+ }
 
 
 
 
127
  }
classes/WpMatomo/TrackingCode.php CHANGED
@@ -13,9 +13,10 @@ if ( ! defined( 'ABSPATH' ) ) {
13
  exit; // if accessed directly
14
  }
15
 
16
- use \WpMatomo\TrackingCode\TrackingCodeGenerator;
17
 
18
  class TrackingCode {
 
19
  /**
20
  * @var Settings
21
  */
@@ -44,17 +45,17 @@ class TrackingCode {
44
  public function register_hooks() {
45
  if ( $this->settings->is_tracking_enabled() ) {
46
  if ( $this->settings->is_track_feed() ) {
47
- add_filter( 'the_excerpt_rss', array( $this, 'add_feed_tracking' ) );
48
- add_filter( 'the_content', array( $this, 'add_feed_tracking' ) );
49
  }
50
  if ( $this->settings->is_add_feed_campaign() ) {
51
- add_filter( 'post_link', array( $this, 'add_feed_campaign' ) );
52
  }
53
  if ( $this->settings->is_cross_domain_linking_enabled() ) {
54
- add_filter( 'wp_redirect', array( $this, 'forward_cross_domain_visitor_id' ) );
55
  }
56
 
57
- $is_admin = is_admin() || !empty($GLOBALS['MATOMO_LOADED_DIRECTLY']);
58
 
59
  if ( ! $is_admin || $this->settings->is_admin_tracking_enabled() ) {
60
  $prefix = 'wp';
@@ -67,10 +68,10 @@ class TrackingCode {
67
  $position = $prefix . '_footer';
68
  }
69
 
70
- add_action( $position, array( $this, 'add_javascript_code' ) );
71
 
72
  if ( $this->settings->is_add_no_script_code() ) {
73
- add_action( $prefix . '_footer', array( $this, 'add_noscript_code' ) );
74
  }
75
  }
76
  }
@@ -106,11 +107,13 @@ class TrackingCode {
106
  if ( $site_id ) {
107
  $tracking_code = str_replace( '{MATOMO_API_ENDPOINT}', wp_json_encode( $this->generator->get_tracker_endpoint() ), $tracking_code );
108
  $tracking_code = str_replace( '{MATOMO_JS_ENDPOINT}', wp_json_encode( $this->generator->get_js_endpoint() ), $tracking_code );
 
109
  echo str_replace( '{MATOMO_IDSITE}', $site_id, $tracking_code );
110
  } else {
111
  echo '<!-- Site not yet synced with Matomo, tracking code will be added later -->';
112
  }
113
  } else {
 
114
  echo $tracking_code;
115
  }
116
  }
@@ -129,12 +132,13 @@ class TrackingCode {
129
 
130
  if ( ! empty( $code ) ) {
131
  $this->logger->log( 'Add noscript code. Blog ID: ' . get_current_blog_id(), Logger::LEVEL_DEBUG );
132
- $contains_noscript_tag = stripos($code, '<noscript') !== false;
133
- if (!$contains_noscript_tag) {
134
  echo '<noscript>';
135
  }
 
136
  echo $code;
137
- if (!$contains_noscript_tag) {
138
  echo '</noscript>';
139
  }
140
  echo "\n";
@@ -153,7 +157,7 @@ class TrackingCode {
153
  */
154
  public function add_feed_campaign( $permalink ) {
155
  global $post;
156
- if ( is_feed() && !empty($post) ) {
157
  $this->logger->log( 'Add campaign to feed permalink.' );
158
  $sep = ( strpos( $permalink, '?' ) === false ? '?' : '&' );
159
  $permalink .= $sep . 'pk_campaign=' . rawurlencode( $this->settings->get_global_option( 'track_feed_campaign' ) ) . '&pk_kwd=' . rawurlencode( $post->post_name );
@@ -203,10 +207,12 @@ class TrackingCode {
203
  * @return string location extended by pk_vid URL parameter if the URL parameter is set
204
  */
205
  public function forward_cross_domain_visitor_id( $location ) {
206
- if ( ! empty( $_GET['pk_vid'] )
207
- && preg_match( '/^[a-zA-Z0-9]{24,60}$/', $_GET['pk_vid'] ) ) {
208
- // currently, the pk_vid parameter is 32 characters long, but it may vary over time.
209
- $location = add_query_arg( 'pk_vid', $_GET['pk_vid'], $location );
 
 
210
  }
211
 
212
  return $location;
13
  exit; // if accessed directly
14
  }
15
 
16
+ use WpMatomo\TrackingCode\TrackingCodeGenerator;
17
 
18
  class TrackingCode {
19
+
20
  /**
21
  * @var Settings
22
  */
45
  public function register_hooks() {
46
  if ( $this->settings->is_tracking_enabled() ) {
47
  if ( $this->settings->is_track_feed() ) {
48
+ add_filter( 'the_excerpt_rss', [ $this, 'add_feed_tracking' ] );
49
+ add_filter( 'the_content', [ $this, 'add_feed_tracking' ] );
50
  }
51
  if ( $this->settings->is_add_feed_campaign() ) {
52
+ add_filter( 'post_link', [ $this, 'add_feed_campaign' ] );
53
  }
54
  if ( $this->settings->is_cross_domain_linking_enabled() ) {
55
+ add_filter( 'wp_redirect', [ $this, 'forward_cross_domain_visitor_id' ] );
56
  }
57
 
58
+ $is_admin = is_admin() || ! empty( $GLOBALS['MATOMO_LOADED_DIRECTLY'] );
59
 
60
  if ( ! $is_admin || $this->settings->is_admin_tracking_enabled() ) {
61
  $prefix = 'wp';
68
  $position = $prefix . '_footer';
69
  }
70
 
71
+ add_action( $position, [ $this, 'add_javascript_code' ] );
72
 
73
  if ( $this->settings->is_add_no_script_code() ) {
74
+ add_action( $prefix . '_footer', [ $this, 'add_noscript_code' ] );
75
  }
76
  }
77
  }
107
  if ( $site_id ) {
108
  $tracking_code = str_replace( '{MATOMO_API_ENDPOINT}', wp_json_encode( $this->generator->get_tracker_endpoint() ), $tracking_code );
109
  $tracking_code = str_replace( '{MATOMO_JS_ENDPOINT}', wp_json_encode( $this->generator->get_js_endpoint() ), $tracking_code );
110
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
111
  echo str_replace( '{MATOMO_IDSITE}', $site_id, $tracking_code );
112
  } else {
113
  echo '<!-- Site not yet synced with Matomo, tracking code will be added later -->';
114
  }
115
  } else {
116
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
117
  echo $tracking_code;
118
  }
119
  }
132
 
133
  if ( ! empty( $code ) ) {
134
  $this->logger->log( 'Add noscript code. Blog ID: ' . get_current_blog_id(), Logger::LEVEL_DEBUG );
135
+ $contains_noscript_tag = stripos( $code, '<noscript' ) !== false;
136
+ if ( ! $contains_noscript_tag ) {
137
  echo '<noscript>';
138
  }
139
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
140
  echo $code;
141
+ if ( ! $contains_noscript_tag ) {
142
  echo '</noscript>';
143
  }
144
  echo "\n";
157
  */
158
  public function add_feed_campaign( $permalink ) {
159
  global $post;
160
+ if ( is_feed() && ! empty( $post ) ) {
161
  $this->logger->log( 'Add campaign to feed permalink.' );
162
  $sep = ( strpos( $permalink, '?' ) === false ? '?' : '&' );
163
  $permalink .= $sep . 'pk_campaign=' . rawurlencode( $this->settings->get_global_option( 'track_feed_campaign' ) ) . '&pk_kwd=' . rawurlencode( $post->post_name );
207
  * @return string location extended by pk_vid URL parameter if the URL parameter is set
208
  */
209
  public function forward_cross_domain_visitor_id( $location ) {
210
+ if ( ! empty( $_GET['pk_vid'] ) ) {
211
+ $pk_vid = sanitize_text_field( wp_unslash( $_GET['pk_vid'] ) );
212
+ if ( preg_match( '/^[a-zA-Z0-9]{24,60}$/', $pk_vid ) ) {
213
+ // currently, the pk_vid parameter is 32 characters long, but it may vary over time.
214
+ $location = add_query_arg( 'pk_vid', $pk_vid, $location );
215
+ }
216
  }
217
 
218
  return $location;
classes/WpMatomo/TrackingCode/TrackingCodeGenerator.php CHANGED
@@ -9,19 +9,21 @@
9
 
10
  namespace WpMatomo\TrackingCode;
11
 
12
- use WpMatomo\Admin\TrackingSettings;
13
  use WpMatomo\Admin\CookieConsent;
 
14
  use WpMatomo\Logger;
15
  use WpMatomo\Paths;
16
  use WpMatomo\Settings;
17
  use WpMatomo\Site;
 
 
18
 
19
  if ( ! defined( 'ABSPATH' ) ) {
20
  exit; // if accessed directly
21
  }
22
 
23
  class TrackingCodeGenerator {
24
-
25
  const TRACKPAGEVIEW = "_paq.push(['trackPageView']);";
26
  const MTM_INIT = 'var _mtm = _mtm || [];';
27
 
@@ -44,8 +46,8 @@ class TrackingCodeGenerator {
44
  }
45
 
46
  public function register_hooks() {
47
- add_action( 'matomo_site_synced', array( $this, 'update_tracking_code' ), $prio = 10, $args = 0 );
48
- add_action( 'matomo_tracking_settings_changed', array( $this, 'update_tracking_code' ), $prio = 10, $args = 0 );
49
  }
50
 
51
  public function update_tracking_code() {
@@ -57,7 +59,7 @@ class TrackingCodeGenerator {
57
  $track_mode = $this->settings->get_global_option( 'track_mode' );
58
 
59
  if ( ! $this->settings->is_tracking_enabled()
60
- || $track_mode == TrackingSettings::TRACK_MODE_MANUALLY ) {
61
  return false;
62
  }
63
 
@@ -79,10 +81,10 @@ class TrackingCodeGenerator {
79
  } elseif ( TrackingSettings::TRACK_MODE_TAGMANAGER === $track_mode && matomo_has_tag_manager() ) {
80
  $result = $this->prepare_tagmanger_code( $this->settings, $this->logger );
81
  } else {
82
- $result = array(
83
  'script' => '<!-- Matomo: no supported track_mode selected -->',
84
  'noscript' => '',
85
- );
86
  }
87
 
88
  if ( ! empty( $result['script'] ) ) {
@@ -141,14 +143,14 @@ class TrackingCodeGenerator {
141
  if ( $enabled
142
  && ctype_alnum( $container_id )
143
  && strlen( $container_id ) <= 16 ) {
144
- $container_url = $upload_url . '/container_' . urlencode( $container_id ) . '.js';
145
 
146
  $data_cf_async = '';
147
  if ( $settings->get_global_option( 'track_datacfasync' ) ) {
148
  $data_cf_async = 'data-cfasync="false"';
149
  }
150
 
151
- if ( $settings->get_global_option( 'force_protocol' ) == 'https' ) {
152
  $container_url = preg_replace( '(^http://)', 'https://', $container_url );
153
  }
154
 
@@ -165,10 +167,10 @@ g.type=\'text/javascript\'; g.async=true; g.src="' . $container_url . '"; s.pare
165
 
166
  $code .= '<!-- End Matomo Tag Manager -->';
167
 
168
- return array(
169
  'script' => $code,
170
  'noscript' => '',
171
- );
172
  }
173
 
174
  public function get_tracker_endpoint() {
@@ -190,11 +192,11 @@ g.type=\'text/javascript\'; g.async=true; g.src="' . $container_url . '"; s.pare
190
  }
191
 
192
  public function get_js_endpoint() {
193
- $paths = new Paths();
194
  if ( $this->settings->get_global_option( 'track_js_endpoint' ) === 'restapi' ) {
195
  $js_endpoint = $paths->get_js_tracker_rest_api_endpoint();
196
  } elseif ( $this->settings->get_global_option( 'track_js_endpoint' ) === 'plugin' ) {
197
- $js_endpoint = plugins_url( 'app/matomo.js', MATOMO_ANALYTICS_FILE );;
198
  } else {
199
  $js_endpoint = $paths->get_js_tracker_url_in_matomo_dir();
200
  }
@@ -214,14 +216,14 @@ g.type=\'text/javascript\'; g.async=true; g.src="' . $container_url . '"; s.pare
214
  * @return array
215
  */
216
  public function prepare_tracking_code( $idsite ) {
217
- $logLevel = is_admin() ? Logger::LEVEL_DEBUG : Logger::LEVEL_INFO;
218
 
219
- $this->logger->log( 'Apply tracking code changes:', $logLevel );
220
 
221
  $tracker_endpoint = $this->get_tracker_endpoint();
222
  $js_endpoint = $this->get_js_endpoint();
223
 
224
- $options = array();
225
 
226
  if ( $this->settings->get_global_option( 'set_download_extensions' ) ) {
227
  $options[] = "_paq.push(['setDownloadExtensions', " . wp_json_encode( $this->settings->get_global_option( 'set_download_extensions' ) ) . ']);';
@@ -254,11 +256,11 @@ g.type=\'text/javascript\'; g.async=true; g.src="' . $container_url . '"; s.pare
254
 
255
  if ( $track_across_alias ) {
256
  // todo detect more hosts such as when using WPML etc
257
- $hosts = array( @parse_url( home_url(), PHP_URL_HOST ) );
258
  $hosts = array_filter( $hosts );
259
  $hosts = array_map(
260
  function ( $host ) {
261
- return '*.' . $host;
262
  },
263
  $hosts
264
  );
@@ -270,8 +272,8 @@ g.type=\'text/javascript\'; g.async=true; g.src="' . $container_url . '"; s.pare
270
  $options[] = "_paq.push(['setRequestMethod', 'POST']);";
271
  }
272
 
273
- $cookieConsent = new CookieConsent();
274
- $cookie_consent_option = $cookieConsent->get_tracking_consent_option( $this->settings->get_global_option( 'cookie_consent' ) );
275
  // for unit test cases
276
  if ( ! empty( $cookie_consent_option ) ) {
277
  $options[] = $cookie_consent_option;
@@ -291,14 +293,14 @@ g.type=\'text/javascript\'; g.async=true; g.src="' . $container_url . '"; s.pare
291
  $options[] = "_paq.push(['enableHeartBeatTimer', " . intval( $this->settings->get_global_option( 'track_heartbeat' ) ) . ']);';
292
  }
293
 
294
- $data_cf_async = '';
295
  $data_of_async_option = [];
296
  if ( $this->settings->get_global_option( 'track_datacfasync' ) ) {
297
- $data_cf_async = 'data-cfasync="false"';
298
- $data_of_async_option['data-cfasync'] = "false";
299
  }
300
 
301
- $script = "var _paq = window._paq = window._paq || [];\n";
302
  $script .= implode( "\n", $options );
303
  $script .= self::TRACKPAGEVIEW;
304
  $script .= "_paq.push(['enableLinkTracking']);_paq.push(['alwaysUseSendBeacon']);";
@@ -320,20 +322,20 @@ g.type='text/javascript'; g.async=true; g.src=" . wp_json_encode( $js_endpoint )
320
  $script = '<script ' . $data_cf_async . ">\n" . $script . "\n</script>\n";
321
  }
322
 
323
- $script = '<!-- Matomo -->'.$script.'<!-- End Matomo Code -->';
324
 
325
  $no_script = '<noscript><p><img referrerpolicy="no-referrer-when-downgrade" src="' . esc_url( $tracker_endpoint ) . '?idsite=' . intval( $idsite ) . '&amp;rec=1" style="border:0;" alt="" /></p></noscript>';
326
 
327
  $script = apply_filters( 'matomo_tracking_code_script', $script, $idsite );
328
  $script = apply_filters( 'matomo_tracking_code_noscript', $script, $idsite );
329
 
330
- $this->logger->log( 'Finished tracking code: ' . $script, $logLevel );
331
- $this->logger->log( 'Finished noscript code: ' . $no_script, $logLevel);
332
 
333
- return array(
334
  'script' => $script,
335
  'noscript' => $no_script,
336
- );
337
  }
338
 
339
  private function apply_404_changes( $tracking_code ) {
@@ -348,7 +350,7 @@ g.type='text/javascript'; g.async=true; g.src=" . wp_json_encode( $js_endpoint )
348
 
349
  private function apply_search_changes( $tracking_code ) {
350
  $this->logger->log( 'Apply search tracking changes. Blog ID: ' . get_current_blog_id() );
351
- $obj_search = new \WP_Query( 's=' . get_search_query() . '&showposts=-1' );
352
  $int_result_count = $obj_search->post_count;
353
 
354
  $code = "window._paq = window._paq || []; window._paq.push(['trackSiteSearch','" . get_search_query() . "', false, " . $int_result_count . "]);\n";
@@ -360,7 +362,7 @@ g.type='text/javascript'; g.async=true; g.src=" . wp_json_encode( $js_endpoint )
360
 
361
  private function apply_user_tracking( $tracking_code ) {
362
  $user_id_to_track = null;
363
- if ( \is_user_logged_in() ) {
364
  // Get the User ID Admin option, and the current user's data
365
  $uid_from = $this->settings->get_global_option( 'track_user_id' );
366
  $current_user = wp_get_current_user(); // current user
@@ -385,5 +387,4 @@ g.type='text/javascript'; g.async=true; g.src=" . wp_json_encode( $js_endpoint )
385
 
386
  return $tracking_code;
387
  }
388
-
389
  }
9
 
10
  namespace WpMatomo\TrackingCode;
11
 
12
+ use WP_Query;
13
  use WpMatomo\Admin\CookieConsent;
14
+ use WpMatomo\Admin\TrackingSettings;
15
  use WpMatomo\Logger;
16
  use WpMatomo\Paths;
17
  use WpMatomo\Settings;
18
  use WpMatomo\Site;
19
+ // phpcs:ignore PHPCompatibility.UseDeclarations.NewUseConstFunction.Found
20
+ use function is_user_logged_in;
21
 
22
  if ( ! defined( 'ABSPATH' ) ) {
23
  exit; // if accessed directly
24
  }
25
 
26
  class TrackingCodeGenerator {
 
27
  const TRACKPAGEVIEW = "_paq.push(['trackPageView']);";
28
  const MTM_INIT = 'var _mtm = _mtm || [];';
29
 
46
  }
47
 
48
  public function register_hooks() {
49
+ add_action( 'matomo_site_synced', [ $this, 'update_tracking_code' ], $prio = 10, $args = 0 );
50
+ add_action( 'matomo_tracking_settings_changed', [ $this, 'update_tracking_code' ], $prio = 10, $args = 0 );
51
  }
52
 
53
  public function update_tracking_code() {
59
  $track_mode = $this->settings->get_global_option( 'track_mode' );
60
 
61
  if ( ! $this->settings->is_tracking_enabled()
62
+ || TrackingSettings::TRACK_MODE_MANUALLY === $track_mode ) {
63
  return false;
64
  }
65
 
81
  } elseif ( TrackingSettings::TRACK_MODE_TAGMANAGER === $track_mode && matomo_has_tag_manager() ) {
82
  $result = $this->prepare_tagmanger_code( $this->settings, $this->logger );
83
  } else {
84
+ $result = [
85
  'script' => '<!-- Matomo: no supported track_mode selected -->',
86
  'noscript' => '',
87
+ ];
88
  }
89
 
90
  if ( ! empty( $result['script'] ) ) {
143
  if ( $enabled
144
  && ctype_alnum( $container_id )
145
  && strlen( $container_id ) <= 16 ) {
146
+ $container_url = $upload_url . '/container_' . rawurlencode( $container_id ) . '.js';
147
 
148
  $data_cf_async = '';
149
  if ( $settings->get_global_option( 'track_datacfasync' ) ) {
150
  $data_cf_async = 'data-cfasync="false"';
151
  }
152
 
153
+ if ( $settings->get_global_option( 'force_protocol' ) === 'https' ) {
154
  $container_url = preg_replace( '(^http://)', 'https://', $container_url );
155
  }
156
 
167
 
168
  $code .= '<!-- End Matomo Tag Manager -->';
169
 
170
+ return [
171
  'script' => $code,
172
  'noscript' => '',
173
+ ];
174
  }
175
 
176
  public function get_tracker_endpoint() {
192
  }
193
 
194
  public function get_js_endpoint() {
195
+ $paths = new Paths();
196
  if ( $this->settings->get_global_option( 'track_js_endpoint' ) === 'restapi' ) {
197
  $js_endpoint = $paths->get_js_tracker_rest_api_endpoint();
198
  } elseif ( $this->settings->get_global_option( 'track_js_endpoint' ) === 'plugin' ) {
199
+ $js_endpoint = plugins_url( 'app/matomo.js', MATOMO_ANALYTICS_FILE );
200
  } else {
201
  $js_endpoint = $paths->get_js_tracker_url_in_matomo_dir();
202
  }
216
  * @return array
217
  */
218
  public function prepare_tracking_code( $idsite ) {
219
+ $log_level = is_admin() ? Logger::LEVEL_DEBUG : Logger::LEVEL_INFO;
220
 
221
+ $this->logger->log( 'Apply tracking code changes:', $log_level );
222
 
223
  $tracker_endpoint = $this->get_tracker_endpoint();
224
  $js_endpoint = $this->get_js_endpoint();
225
 
226
+ $options = [];
227
 
228
  if ( $this->settings->get_global_option( 'set_download_extensions' ) ) {
229
  $options[] = "_paq.push(['setDownloadExtensions', " . wp_json_encode( $this->settings->get_global_option( 'set_download_extensions' ) ) . ']);';
256
 
257
  if ( $track_across_alias ) {
258
  // todo detect more hosts such as when using WPML etc
259
+ $hosts = [ wp_parse_url( home_url(), PHP_URL_HOST ) ];
260
  $hosts = array_filter( $hosts );
261
  $hosts = array_map(
262
  function ( $host ) {
263
+ return '*.' . $host;
264
  },
265
  $hosts
266
  );
272
  $options[] = "_paq.push(['setRequestMethod', 'POST']);";
273
  }
274
 
275
+ $cookie_consent = new CookieConsent();
276
+ $cookie_consent_option = $cookie_consent->get_tracking_consent_option( $this->settings->get_global_option( 'cookie_consent' ) );
277
  // for unit test cases
278
  if ( ! empty( $cookie_consent_option ) ) {
279
  $options[] = $cookie_consent_option;
293
  $options[] = "_paq.push(['enableHeartBeatTimer', " . intval( $this->settings->get_global_option( 'track_heartbeat' ) ) . ']);';
294
  }
295
 
296
+ $data_cf_async = '';
297
  $data_of_async_option = [];
298
  if ( $this->settings->get_global_option( 'track_datacfasync' ) ) {
299
+ $data_cf_async = 'data-cfasync="false"';
300
+ $data_of_async_option['data-cfasync'] = 'false';
301
  }
302
 
303
+ $script = "var _paq = window._paq = window._paq || [];\n";
304
  $script .= implode( "\n", $options );
305
  $script .= self::TRACKPAGEVIEW;
306
  $script .= "_paq.push(['enableLinkTracking']);_paq.push(['alwaysUseSendBeacon']);";
322
  $script = '<script ' . $data_cf_async . ">\n" . $script . "\n</script>\n";
323
  }
324
 
325
+ $script = '<!-- Matomo -->' . $script . '<!-- End Matomo Code -->';
326
 
327
  $no_script = '<noscript><p><img referrerpolicy="no-referrer-when-downgrade" src="' . esc_url( $tracker_endpoint ) . '?idsite=' . intval( $idsite ) . '&amp;rec=1" style="border:0;" alt="" /></p></noscript>';
328
 
329
  $script = apply_filters( 'matomo_tracking_code_script', $script, $idsite );
330
  $script = apply_filters( 'matomo_tracking_code_noscript', $script, $idsite );
331
 
332
+ $this->logger->log( 'Finished tracking code: ' . $script, $log_level );
333
+ $this->logger->log( 'Finished noscript code: ' . $no_script, $log_level );
334
 
335
+ return [
336
  'script' => $script,
337
  'noscript' => $no_script,
338
+ ];
339
  }
340
 
341
  private function apply_404_changes( $tracking_code ) {
350
 
351
  private function apply_search_changes( $tracking_code ) {
352
  $this->logger->log( 'Apply search tracking changes. Blog ID: ' . get_current_blog_id() );
353
+ $obj_search = new WP_Query( 's=' . get_search_query() . '&showposts=-1' );
354
  $int_result_count = $obj_search->post_count;
355
 
356
  $code = "window._paq = window._paq || []; window._paq.push(['trackSiteSearch','" . get_search_query() . "', false, " . $int_result_count . "]);\n";
362
 
363
  private function apply_user_tracking( $tracking_code ) {
364
  $user_id_to_track = null;
365
+ if ( is_user_logged_in() ) {
366
  // Get the User ID Admin option, and the current user's data
367
  $uid_from = $this->settings->get_global_option( 'track_user_id' );
368
  $current_user = wp_get_current_user(); // current user
387
 
388
  return $tracking_code;
389
  }
 
390
  }
classes/WpMatomo/Uninstaller.php CHANGED
@@ -14,7 +14,15 @@ use WpMatomo\Admin\Dashboard;
14
  if ( ! defined( 'ABSPATH' ) ) {
15
  exit; // if accessed directly
16
  }
17
-
 
 
 
 
 
 
 
 
18
  class Uninstaller {
19
 
20
  /**
@@ -135,7 +143,7 @@ class Uninstaller {
135
  private function drop_tables() {
136
  global $wpdb;
137
 
138
- $db_settings = new \WpMatomo\Db\Settings();
139
  $installed_tables = $db_settings->get_installed_matomo_tables();
140
  $this->logger->log( sprintf( 'Matomo will now drop %s matomo tables', count( $installed_tables ) ) );
141
 
@@ -143,6 +151,7 @@ class Uninstaller {
143
  // temporary table are used in tests and just making sure they are being removed
144
  // $wpdb->query( "DROP TEMPORARY TABLE IF EXISTS `$tableName`" );
145
  // two spaces between drop and table so it won't be replaced in WP tests
 
146
  $wpdb->query( "DROP TABLE IF EXISTS `$table_name`" );
147
  }
148
  }
14
  if ( ! defined( 'ABSPATH' ) ) {
15
  exit; // if accessed directly
16
  }
17
+ /**
18
+ * We need to access db not cache
19
+ * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
20
+ * phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
21
+ *
22
+ * Table names management
23
+ * phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
24
+ * phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
25
+ */
26
  class Uninstaller {
27
 
28
  /**
143
  private function drop_tables() {
144
  global $wpdb;
145
 
146
+ $db_settings = new \WpMatomo\Db\Settings();
147
  $installed_tables = $db_settings->get_installed_matomo_tables();
148
  $this->logger->log( sprintf( 'Matomo will now drop %s matomo tables', count( $installed_tables ) ) );
149
 
151
  // temporary table are used in tests and just making sure they are being removed
152
  // $wpdb->query( "DROP TEMPORARY TABLE IF EXISTS `$tableName`" );
153
  // two spaces between drop and table so it won't be replaced in WP tests
154
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange
155
  $wpdb->query( "DROP TABLE IF EXISTS `$table_name`" );
156
  }
157
  }
classes/WpMatomo/Updater.php CHANGED
@@ -9,12 +9,15 @@
9
 
10
  namespace WpMatomo;
11
 
 
12
  use Piwik\Cache as PiwikCache;
13
  use Piwik\Filesystem;
14
  use Piwik\Option;
15
  use Piwik\Plugins\Installation\ServerFilesGenerator;
16
  use Piwik\SettingsServer;
17
  use Piwik\Version;
 
 
18
  use WpMatomo\Updater\UpdateInProgressException;
19
 
20
  if ( ! defined( 'ABSPATH' ) ) {
@@ -47,13 +50,12 @@ class Updater {
47
  return function_exists( 'get_plugin_data' );
48
  }
49
 
50
- public function get_plugins_requiring_update()
51
- {
52
  if ( ! $this->load_plugin_functions() ) {
53
  return [];
54
  }
55
 
56
- $keys = [];
57
  $plugin_files = $GLOBALS['MATOMO_PLUGIN_FILES'];
58
  if ( ! in_array( MATOMO_ANALYTICS_FILE, $plugin_files, true ) ) {
59
  $plugin_files[] = MATOMO_ANALYTICS_FILE;
@@ -70,7 +72,7 @@ class Updater {
70
  if ( ! Installer::is_intalled() ) {
71
  return [];
72
  }
73
- $keys[$key] = $plugin_data['Version'];
74
  }
75
  }
76
 
@@ -78,16 +80,17 @@ class Updater {
78
  }
79
 
80
  public function update_if_needed() {
81
- $executed_updates = array();
82
 
83
  $plugins_requiring_update = $this->get_plugins_requiring_update();
84
- foreach ($plugins_requiring_update as $key => $plugin_version) {
85
  try {
86
  $this->update();
87
  } catch ( UpdateInProgressException $e ) {
88
- $this->logger->log( 'Matomo update is already in progress');
 
89
  return; // we also don't execute any further update as they should be executed in another process
90
- }catch ( \Exception $e ) {
91
  $this->logger->log_exception( 'plugin_update', $e );
92
  continue;
93
  }
@@ -117,11 +120,11 @@ class Updater {
117
 
118
  $history = $this->settings->get_global_option( 'version_history' );
119
  if ( empty( $history ) || ! is_array( $history ) ) {
120
- $history = array();
121
  }
122
 
123
  if ( ! empty( $plugin_data['Version'] )
124
- && ! in_array( $plugin_data['Version'], $history, true ) ) {
125
  // this allows us to see which versions of matomo the user was using before this update so we better understand
126
  // which version maybe regressed something
127
  array_unshift( $history, $plugin_data['Version'] );
@@ -147,11 +150,13 @@ class Updater {
147
  );
148
 
149
  $upload_dir = $paths->get_upload_base_dir();
 
 
150
  if ( is_dir( $upload_dir ) && is_writable( $upload_dir ) ) {
151
- @file_put_contents( $upload_dir . '/index.php', '//hello' );
152
- @file_put_contents( $upload_dir . '/index.html', '//hello' );
153
- @file_put_contents( $upload_dir . '/index.htm', '//hello' );
154
- @file_put_contents(
155
  $upload_dir . '/.htaccess',
156
  '<Files ~ "(\.mmdb)$">
157
  ' . ServerFilesGenerator::getDenyHtaccessContent() . '
@@ -163,12 +168,12 @@ class Updater {
163
  }
164
  $config_dir = $paths->get_config_ini_path();
165
  if ( is_dir( $config_dir ) && is_writable( $config_dir ) ) {
166
- @file_put_contents( $config_dir . '/index.php', '//hello' );
167
- @file_put_contents( $config_dir . '/index.html', '//hello' );
168
- @file_put_contents( $config_dir . '/index.htm', '//hello' );
169
  }
170
 
171
- if ($this->settings->should_disable_addhandler()) {
172
  wp_schedule_single_event( time() + 10, ScheduledTasks::EVENT_DISABLE_ADDHANDLER );
173
  }
174
  }
@@ -178,9 +183,10 @@ class Updater {
178
  return 'no upgrader';
179
  }
180
 
181
- if (self::lock()) {
182
  // we can get the lock meaning no update is in progress
183
  self::unlock();
 
184
  return false;
185
  }
186
 
@@ -188,56 +194,57 @@ class Updater {
188
  }
189
 
190
  private static function load_upgrader() {
191
- if (!class_exists('\WP_Upgrader', false)) {
 
192
  @include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
193
  }
194
- return class_exists('\WP_Upgrader', false);
 
195
  }
196
 
197
- public static function lock()
198
- {
199
  // prevent the upgrade from being started several times at once
200
  // we lock for 4 minutes. In case of major Matomo upgrades the upgrade may take much longer but it should be
201
  // safe in this case to run the upgrade several times
202
  // important: we always need to use the same timeout otherwise if something did use `create_lock(2)` then
203
  // even though another job locked it for 4 minutes, the other job that locks it only for 2 seconds would release
204
  // the lock basically since WP does not remember the initialy set release timeout
205
- return self::load_upgrader() && \WP_Upgrader::create_lock(self::LOCK_NAME, 60*4);
206
  }
207
 
208
- public static function unlock()
209
- {
210
- return self::load_upgrader() && \WP_Upgrader::release_lock(self::LOCK_NAME);
211
  }
212
 
213
  private static function update_components() {
214
  $updater = new \Piwik\Updater();
215
- $components_with_update_file = $updater->getComponentUpdates( );
216
 
217
  if ( empty( $components_with_update_file ) ) {
218
  return false;
219
  }
220
 
221
- if (!self::lock()) {
222
  throw new UpdateInProgressException();
223
  }
224
 
225
  try {
226
- SettingsServer::setMaxExecutionTime(0);
227
 
228
- if (function_exists('ignore_user_abort')) {
229
- @ignore_user_abort(true);
 
230
  }
231
 
232
  $result = $updater->updateComponents( $components_with_update_file );
233
- } catch (\Exception $e) {
234
  self::unlock();
235
  throw $e;
236
  }
237
  self::unlock();
238
 
239
- if (!empty($result['errors'])) {
240
- throw new \Exception('Error while updating components: ' . implode(', ' , $result['errors']));
241
  }
242
 
243
  \Piwik\Updater::recordComponentSuccessfullyUpdated( 'core', Version::VERSION );
9
 
10
  namespace WpMatomo;
11
 
12
+ use Exception;
13
  use Piwik\Cache as PiwikCache;
14
  use Piwik\Filesystem;
15
  use Piwik\Option;
16
  use Piwik\Plugins\Installation\ServerFilesGenerator;
17
  use Piwik\SettingsServer;
18
  use Piwik\Version;
19
+ use WP_Upgrader;
20
+ use WpMatomo\Paths;
21
  use WpMatomo\Updater\UpdateInProgressException;
22
 
23
  if ( ! defined( 'ABSPATH' ) ) {
50
  return function_exists( 'get_plugin_data' );
51
  }
52
 
53
+ public function get_plugins_requiring_update() {
 
54
  if ( ! $this->load_plugin_functions() ) {
55
  return [];
56
  }
57
 
58
+ $keys = [];
59
  $plugin_files = $GLOBALS['MATOMO_PLUGIN_FILES'];
60
  if ( ! in_array( MATOMO_ANALYTICS_FILE, $plugin_files, true ) ) {
61
  $plugin_files[] = MATOMO_ANALYTICS_FILE;
72
  if ( ! Installer::is_intalled() ) {
73
  return [];
74
  }
75
+ $keys[ $key ] = $plugin_data['Version'];
76
  }
77
  }
78
 
80
  }
81
 
82
  public function update_if_needed() {
83
+ $executed_updates = [];
84
 
85
  $plugins_requiring_update = $this->get_plugins_requiring_update();
86
+ foreach ( $plugins_requiring_update as $key => $plugin_version ) {
87
  try {
88
  $this->update();
89
  } catch ( UpdateInProgressException $e ) {
90
+ $this->logger->log( 'Matomo update is already in progress' );
91
+
92
  return; // we also don't execute any further update as they should be executed in another process
93
+ } catch ( Exception $e ) {
94
  $this->logger->log_exception( 'plugin_update', $e );
95
  continue;
96
  }
120
 
121
  $history = $this->settings->get_global_option( 'version_history' );
122
  if ( empty( $history ) || ! is_array( $history ) ) {
123
+ $history = [];
124
  }
125
 
126
  if ( ! empty( $plugin_data['Version'] )
127
+ && ! in_array( $plugin_data['Version'], $history, true ) ) {
128
  // this allows us to see which versions of matomo the user was using before this update so we better understand
129
  // which version maybe regressed something
130
  array_unshift( $history, $plugin_data['Version'] );
150
  );
151
 
152
  $upload_dir = $paths->get_upload_base_dir();
153
+
154
+ $wp_filesystem = $paths->get_file_system();
155
  if ( is_dir( $upload_dir ) && is_writable( $upload_dir ) ) {
156
+ $wp_filesystem->put_contents( $upload_dir . '/index.php', '//hello' );
157
+ $wp_filesystem->put_contents( $upload_dir . '/index.html', '//hello' );
158
+ $wp_filesystem->put_contents( $upload_dir . '/index.htm', '//hello' );
159
+ $wp_filesystem->put_contents(
160
  $upload_dir . '/.htaccess',
161
  '<Files ~ "(\.mmdb)$">
162
  ' . ServerFilesGenerator::getDenyHtaccessContent() . '
168
  }
169
  $config_dir = $paths->get_config_ini_path();
170
  if ( is_dir( $config_dir ) && is_writable( $config_dir ) ) {
171
+ $wp_filesystem->put_contents( $config_dir . '/index.php', '//hello' );
172
+ $wp_filesystem->put_contents( $config_dir . '/index.html', '//hello' );
173
+ $wp_filesystem->put_contents( $config_dir . '/index.htm', '//hello' );
174
  }
175
 
176
+ if ( $this->settings->should_disable_addhandler() ) {
177
  wp_schedule_single_event( time() + 10, ScheduledTasks::EVENT_DISABLE_ADDHANDLER );
178
  }
179
  }
183
  return 'no upgrader';
184
  }
185
 
186
+ if ( self::lock() ) {
187
  // we can get the lock meaning no update is in progress
188
  self::unlock();
189
+
190
  return false;
191
  }
192
 
194
  }
195
 
196
  private static function load_upgrader() {
197
+ if ( ! class_exists( '\WP_Upgrader', false ) ) {
198
+ // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
199
  @include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
200
  }
201
+
202
+ return class_exists( '\WP_Upgrader', false );
203
  }
204
 
205
+ public static function lock() {
 
206
  // prevent the upgrade from being started several times at once
207
  // we lock for 4 minutes. In case of major Matomo upgrades the upgrade may take much longer but it should be
208
  // safe in this case to run the upgrade several times
209
  // important: we always need to use the same timeout otherwise if something did use `create_lock(2)` then
210
  // even though another job locked it for 4 minutes, the other job that locks it only for 2 seconds would release
211
  // the lock basically since WP does not remember the initialy set release timeout
212
+ return self::load_upgrader() && WP_Upgrader::create_lock( self::LOCK_NAME, 60 * 4 );
213
  }
214
 
215
+ public static function unlock() {
216
+ return self::load_upgrader() && WP_Upgrader::release_lock( self::LOCK_NAME );
 
217
  }
218
 
219
  private static function update_components() {
220
  $updater = new \Piwik\Updater();
221
+ $components_with_update_file = $updater->getComponentUpdates();
222
 
223
  if ( empty( $components_with_update_file ) ) {
224
  return false;
225
  }
226
 
227
+ if ( ! self::lock() ) {
228
  throw new UpdateInProgressException();
229
  }
230
 
231
  try {
232
+ SettingsServer::setMaxExecutionTime( 0 );
233
 
234
+ if ( function_exists( 'ignore_user_abort' ) ) {
235
+ // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
236
+ @ignore_user_abort( true );
237
  }
238
 
239
  $result = $updater->updateComponents( $components_with_update_file );
240
+ } catch ( Exception $e ) {
241
  self::unlock();
242
  throw $e;
243
  }
244
  self::unlock();
245
 
246
+ if ( ! empty( $result['errors'] ) ) {
247
+ throw new Exception( 'Error while updating components: ' . implode( ', ', $result['errors'] ) );
248
  }
249
 
250
  \Piwik\Updater::recordComponentSuccessfullyUpdated( 'core', Version::VERSION );
classes/WpMatomo/Updater/UpdateInProgressException.php CHANGED
@@ -9,13 +9,17 @@
9
 
10
  namespace WpMatomo\Updater;
11
 
 
 
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit; // if accessed directly
14
  }
15
 
16
- class UpdateInProgressException extends \Exception {
17
-
18
- public function __construct( $message = "Matomo upgrade is already in progress", $code = 0, $previous = null ) {
 
 
19
  parent::__construct( $message, $code, $previous );
20
  }
21
  }
9
 
10
  namespace WpMatomo\Updater;
11
 
12
+ use Exception;
13
+
14
  if ( ! defined( 'ABSPATH' ) ) {
15
  exit; // if accessed directly
16
  }
17
 
18
+ /**
19
+ * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found
20
+ */
21
+ class UpdateInProgressException extends Exception {
22
+ public function __construct( $message = 'Matomo upgrade is already in progress', $code = 0, $previous = null ) {
23
  parent::__construct( $message, $code, $previous );
24
  }
25
  }
classes/WpMatomo/User.php CHANGED
@@ -14,7 +14,6 @@ if ( ! defined( 'ABSPATH' ) ) {
14
  }
15
 
16
  class User {
17
-
18
  const USER_MAPPING_PREFIX = 'matomo-user-login-';
19
 
20
  /**
@@ -41,6 +40,4 @@ class User {
41
  public function uninstall() {
42
  Uninstaller::uninstall_options( self::USER_MAPPING_PREFIX );
43
  }
44
-
45
-
46
  }
14
  }
15
 
16
  class User {
 
17
  const USER_MAPPING_PREFIX = 'matomo-user-login-';
18
 
19
  /**
40
  public function uninstall() {
41
  Uninstaller::uninstall_options( self::USER_MAPPING_PREFIX );
42
  }
 
 
43
  }
classes/WpMatomo/User/Sync.php CHANGED
@@ -9,6 +9,7 @@
9
 
10
  namespace WpMatomo\User;
11
 
 
12
  use Piwik\Access;
13
  use Piwik\Access\Role\Admin;
14
  use Piwik\Access\Role\View;
@@ -18,8 +19,9 @@ use Piwik\Common;
18
  use Piwik\Date;
19
  use Piwik\Plugin;
20
  use Piwik\Plugins\LanguagesManager\API;
21
- use Piwik\Plugins\UsersManager\Model;
22
  use Piwik\Plugins\UsersManager;
 
 
23
  use WpMatomo\Bootstrap;
24
  use WpMatomo\Capabilities;
25
  use WpMatomo\Logger;
@@ -32,6 +34,7 @@ if ( ! defined( 'ABSPATH' ) ) {
32
  }
33
 
34
  class Sync {
 
35
  /**
36
  * actually allowed is 100 characters...
37
  * but we do -5 to have some room to append `wp_`.$login.XYZ if needed
@@ -48,21 +51,20 @@ class Sync {
48
  }
49
 
50
  public function register_hooks() {
51
- add_action( 'add_user_role', array( $this, 'sync_current_users_1000' ), $prio = 10, $args = 0 );
52
- add_action( 'remove_user_role', array( $this, 'sync_current_users_1000' ), $prio = 10, $args = 0 );
53
- add_action( 'add_user_to_blog', array( $this, 'sync_current_users_1000' ), $prio = 10, $args = 0 );
54
- add_action( 'remove_user_from_blog', array( $this, 'sync_current_users_1000' ), $prio = 10, $args = 0 );
55
- add_action( 'user_register', array( $this, 'sync_current_users_1000' ), $prio = 10, $args = 0 );
56
- add_action( 'profile_update', array( $this, 'sync_maybe_background' ), $prio = 10, $args = 0 );
57
  }
58
 
59
- public function sync_maybe_background()
60
- {
61
  global $pagenow;
62
- if ( is_admin() && $pagenow == 'users.php' ) {
63
  // eg for profile update we don't want to sync directly see #365 as it could cause issues with other plugins
64
  // if they eg alter `get_users` option
65
- wp_schedule_single_event(time() + 5, ScheduledTasks::EVENT_SYNC);
66
  } else {
67
  $this->sync_current_users_1000();
68
  }
@@ -77,10 +79,10 @@ class Sync {
77
 
78
  try {
79
  if ( $idsite ) {
80
- $users = $this->get_users( array('blog_id' => $site->blog_id ) );
81
  $this->sync_users( $users, $idsite );
82
  }
83
- } catch ( \Exception $e ) {
84
  // we don't want to rethrow exception otherwise some other blogs might never sync
85
  $this->logger->log_exception( 'user_sync ', $e );
86
  }
@@ -92,51 +94,51 @@ class Sync {
92
  }
93
  }
94
 
95
- private function get_users($options = array())
96
- {
97
- /** @var \WP_User[] $users */
98
- $users = get_users( $options );
99
-
100
- $current_user = wp_get_current_user();
101
- if (!empty($current_user) && !empty($current_user->user_login)) {
102
- // refs https://github.com/matomo-org/wp-matomo/issues/365
103
- // some other plugins may under circumstances overwrite the get_users query and not return all users
104
- // as a result we would delete some users in the matomo users table. this way we make sure at least the current
105
- // user will be added and not deleted even if the list of users is not complete
106
- $found = false;
107
- foreach ($users as $user) {
108
- if ($user->user_login === $current_user->user_login) {
109
- $found = true;
110
- break;
111
- }
112
- }
113
- if (!$found) {
114
- $users[] = $current_user;
115
- }
116
- }
117
-
118
- if (is_multisite()) {
119
- $super_admins = get_super_admins();
120
- if (!empty($super_admins)) {
121
- foreach ($super_admins as $super_admin) {
122
- $found = false;
123
- foreach ($users as $user) {
124
- if ($user->user_login === $super_admin) {
125
- $found = true;
126
- break;
127
- }
128
- }
129
- if (!$found) {
130
- $user = get_user_by('login', $super_admin);
131
- if (!empty($user)) {
132
- $users[] = $user;
133
- }
134
- }
135
- }
136
- }
137
- }
138
- return $users;
139
- }
140
 
141
  public function sync_current_users() {
142
  $idsite = Site::get_matomo_site_id( get_current_blog_id() );
@@ -149,9 +151,10 @@ class Sync {
149
  /**
150
  * similar method to sync_current_users which synchronise on the fly only if we have less than 1000 users.
151
  * Otherwise it will be done by a background task
152
- * @see Sync::sync_current_users()
153
- * @see https://github.com/matomo-org/matomo-for-wordpress/issues/460
154
  * @return void
 
 
155
  */
156
  public function sync_current_users_1000() {
157
  $idsite = Site::get_matomo_site_id( get_current_blog_id() );
@@ -162,11 +165,12 @@ class Sync {
162
  }
163
  }
164
  }
 
165
  /**
166
  * Sync all users. Make sure to always pass all sites that exist within a given site... you cannot just sync an individual
167
  * user... we would delete all other users
168
  *
169
- * @param \WP_User[] $users
170
  * @param $idsite
171
  */
172
  protected function sync_users( $users, $idsite ) {
@@ -174,8 +178,8 @@ class Sync {
174
 
175
  $this->logger->log( 'Matomo will now sync ' . count( $users ) . ' users' );
176
 
177
- $super_users = array();
178
- $logins_with_some_view_access = array( 'anonmyous' ); // may or may not exist... we don't want to delete this user though
179
  $user_model = new Model();
180
 
181
  // need to make sure we recreate new instance later with latest dependencies in case they changed
@@ -197,34 +201,34 @@ class Sync {
197
  $logins_with_some_view_access[] = $matomo_login;
198
  } elseif ( user_can( $user, Capabilities::KEY_ADMIN ) ) {
199
  $matomo_login = $this->ensure_user_exists( $user );
200
- $user_model->deleteUserAccess( $mapped_matomo_login, array( $idsite ) );
201
- $user_model->addUserAccess( $matomo_login, Admin::ID, array( $idsite ) );
202
  $user_model->setSuperUserAccess( $matomo_login, false );
203
  $logins_with_some_view_access[] = $matomo_login;
204
  } elseif ( user_can( $user, Capabilities::KEY_WRITE ) ) {
205
  $matomo_login = $this->ensure_user_exists( $user );
206
- $user_model->deleteUserAccess( $mapped_matomo_login, array( $idsite ) );
207
- $user_model->addUserAccess( $matomo_login, Write::ID, array( $idsite ) );
208
  $user_model->setSuperUserAccess( $matomo_login, false );
209
  $logins_with_some_view_access[] = $matomo_login;
210
  } elseif ( user_can( $user, Capabilities::KEY_VIEW ) ) {
211
  $matomo_login = $this->ensure_user_exists( $user );
212
- $user_model->deleteUserAccess( $mapped_matomo_login, array( $idsite ) );
213
- $user_model->addUserAccess( $matomo_login, View::ID, array( $idsite ) );
214
  $user_model->setSuperUserAccess( $matomo_login, false );
215
  $logins_with_some_view_access[] = $matomo_login;
216
- } elseif ($mapped_matomo_login) {
217
- $user_model->deleteUserAccess( $mapped_matomo_login, array( $idsite ) );
218
  }
219
 
220
  if ( $matomo_login ) {
221
- $locale = get_user_locale( $user->ID );
222
- $locale_dash = Common::mb_strtolower(str_replace('_', '-', $locale));
223
- $parts = [];
224
- if ($locale && in_array($locale_dash, ['zh-cn', 'zh-tw', 'pt-br', 'es-ar'], true)) {
225
- $parts = [$locale_dash];
226
- } elseif (!empty($locale) && is_string($locale)) {
227
- $parts = explode( '_', $locale );
228
  }
229
 
230
  if ( ! empty( $parts[0] ) ) {
@@ -237,8 +241,8 @@ class Sync {
237
  }
238
  }
239
  }
240
-
241
- if ($idsite != 1) {
242
  // only needed if the actual site is not the default site... makes sure when they click in Matomo
243
  // UI on "Dashboard" that the correct site is being opened by default
244
  // eg if the linked site is actually idSite=2.
@@ -254,10 +258,10 @@ class Sync {
254
  UsersManager\API::PREFERENCE_DEFAULT_REPORT,
255
  $idsite
256
  );
257
- } catch (\Exception $e) {
 
258
  // ignore any error for now
259
  }
260
-
261
  }
262
  );
263
  }
@@ -268,11 +272,10 @@ class Sync {
268
  }
269
 
270
  $logins_with_some_view_access = array_unique( $logins_with_some_view_access );
271
- $all_users = $user_model->getUsers( array() );
272
  foreach ( $all_users as $all_user ) {
273
  if ( ! in_array( $all_user['login'], $logins_with_some_view_access, true )
274
  && ! empty( $all_user['login'] ) ) {
275
-
276
  Access::doAsSuperUser(
277
  function () use ( $user_model, $all_user ) {
278
  $user_model->deleteUserOnly( $all_user['login'] );
@@ -285,7 +288,7 @@ class Sync {
285
  }
286
 
287
  /**
288
- * @param \WP_User $wp_user
289
  */
290
  protected function ensure_user_exists( $wp_user ) {
291
  $user_model = new Model();
@@ -299,7 +302,7 @@ class Sync {
299
  $user_in_matomo = $user_model->getUser( $matomo_user_login );
300
  } else {
301
  // wp usernames may include whitespace etc
302
- $login = preg_replace('/[^A-Za-zÄäÖöÜüß0-9_.@+-]+/D', '_', $login);
303
  $login = substr( $login, 0, self::MAX_USER_NAME_LENGTH );
304
 
305
  if ( ! $user_model->getUser( $login ) ) {
@@ -334,7 +337,7 @@ class Sync {
334
  User::map_matomo_user_login( $user_id, $matomo_user_login );
335
  } elseif ( $user_in_matomo['email'] !== $wp_user->user_email ) {
336
  $this->logger->log( 'Matomo is now updating the email for wpUserID ' . $user_id . ' matomo login ' . $matomo_user_login );
337
- $user_model->updateUserFields( $matomo_user_login, array( 'email' => $wp_user->user_email ) );
338
  }
339
 
340
  return $matomo_user_login;
9
 
10
  namespace WpMatomo\User;
11
 
12
+ use Exception;
13
  use Piwik\Access;
14
  use Piwik\Access\Role\Admin;
15
  use Piwik\Access\Role\View;
19
  use Piwik\Date;
20
  use Piwik\Plugin;
21
  use Piwik\Plugins\LanguagesManager\API;
 
22
  use Piwik\Plugins\UsersManager;
23
+ use Piwik\Plugins\UsersManager\Model;
24
+ use WP_User;
25
  use WpMatomo\Bootstrap;
26
  use WpMatomo\Capabilities;
27
  use WpMatomo\Logger;
34
  }
35
 
36
  class Sync {
37
+
38
  /**
39
  * actually allowed is 100 characters...
40
  * but we do -5 to have some room to append `wp_`.$login.XYZ if needed
51
  }
52
 
53
  public function register_hooks() {
54
+ add_action( 'add_user_role', [ $this, 'sync_current_users_1000' ], $prio = 10, $args = 0 );
55
+ add_action( 'remove_user_role', [ $this, 'sync_current_users_1000' ], $prio = 10, $args = 0 );
56
+ add_action( 'add_user_to_blog', [ $this, 'sync_current_users_1000' ], $prio = 10, $args = 0 );
57
+ add_action( 'remove_user_from_blog', [ $this, 'sync_current_users_1000' ], $prio = 10, $args = 0 );
58
+ add_action( 'user_register', [ $this, 'sync_current_users_1000' ], $prio = 10, $args = 0 );
59
+ add_action( 'profile_update', [ $this, 'sync_maybe_background' ], $prio = 10, $args = 0 );
60
  }
61
 
62
+ public function sync_maybe_background() {
 
63
  global $pagenow;
64
+ if ( is_admin() && 'users.php' === $pagenow ) {
65
  // eg for profile update we don't want to sync directly see #365 as it could cause issues with other plugins
66
  // if they eg alter `get_users` option
67
+ wp_schedule_single_event( time() + 5, ScheduledTasks::EVENT_SYNC );
68
  } else {
69
  $this->sync_current_users_1000();
70
  }
79
 
80
  try {
81
  if ( $idsite ) {
82
+ $users = $this->get_users( [ 'blog_id' => $site->blog_id ] );
83
  $this->sync_users( $users, $idsite );
84
  }
85
+ } catch ( Exception $e ) {
86
  // we don't want to rethrow exception otherwise some other blogs might never sync
87
  $this->logger->log_exception( 'user_sync ', $e );
88
  }
94
  }
95
  }
96
 
97
+ private function get_users( $options = [] ) {
98
+ /** @var WP_User[] $users */
99
+ $users = get_users( $options );
100
+
101
+ $current_user = wp_get_current_user();
102
+ if ( ! empty( $current_user ) && ! empty( $current_user->user_login ) ) {
103
+ // refs https://github.com/matomo-org/wp-matomo/issues/365
104
+ // some other plugins may under circumstances overwrite the get_users query and not return all users
105
+ // as a result we would delete some users in the matomo users table. this way we make sure at least the current
106
+ // user will be added and not deleted even if the list of users is not complete
107
+ $found = false;
108
+ foreach ( $users as $user ) {
109
+ if ( $user->user_login === $current_user->user_login ) {
110
+ $found = true;
111
+ break;
112
+ }
113
+ }
114
+ if ( ! $found ) {
115
+ $users[] = $current_user;
116
+ }
117
+ }
118
+
119
+ if ( is_multisite() ) {
120
+ $super_admins = get_super_admins();
121
+ if ( ! empty( $super_admins ) ) {
122
+ foreach ( $super_admins as $super_admin ) {
123
+ $found = false;
124
+ foreach ( $users as $user ) {
125
+ if ( $user->user_login === $super_admin ) {
126
+ $found = true;
127
+ break;
128
+ }
129
+ }
130
+ if ( ! $found ) {
131
+ $user = get_user_by( 'login', $super_admin );
132
+ if ( ! empty( $user ) ) {
133
+ $users[] = $user;
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ return $users;
141
+ }
142
 
143
  public function sync_current_users() {
144
  $idsite = Site::get_matomo_site_id( get_current_blog_id() );
151
  /**
152
  * similar method to sync_current_users which synchronise on the fly only if we have less than 1000 users.
153
  * Otherwise it will be done by a background task
154
+ *
 
155
  * @return void
156
+ * @see https://github.com/matomo-org/matomo-for-wordpress/issues/460
157
+ * @see Sync::sync_current_users()
158
  */
159
  public function sync_current_users_1000() {
160
  $idsite = Site::get_matomo_site_id( get_current_blog_id() );
165
  }
166
  }
167
  }
168
+
169
  /**
170
  * Sync all users. Make sure to always pass all sites that exist within a given site... you cannot just sync an individual
171
  * user... we would delete all other users
172
  *
173
+ * @param WP_User[] $users
174
  * @param $idsite
175
  */
176
  protected function sync_users( $users, $idsite ) {
178
 
179
  $this->logger->log( 'Matomo will now sync ' . count( $users ) . ' users' );
180
 
181
+ $super_users = [];
182
+ $logins_with_some_view_access = [ 'anonmyous' ]; // may or may not exist... we don't want to delete this user though
183
  $user_model = new Model();
184
 
185
  // need to make sure we recreate new instance later with latest dependencies in case they changed
201
  $logins_with_some_view_access[] = $matomo_login;
202
  } elseif ( user_can( $user, Capabilities::KEY_ADMIN ) ) {
203
  $matomo_login = $this->ensure_user_exists( $user );
204
+ $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] );
205
+ $user_model->addUserAccess( $matomo_login, Admin::ID, [ $idsite ] );
206
  $user_model->setSuperUserAccess( $matomo_login, false );
207
  $logins_with_some_view_access[] = $matomo_login;
208
  } elseif ( user_can( $user, Capabilities::KEY_WRITE ) ) {
209
  $matomo_login = $this->ensure_user_exists( $user );
210
+ $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] );
211
+ $user_model->addUserAccess( $matomo_login, Write::ID, [ $idsite ] );
212
  $user_model->setSuperUserAccess( $matomo_login, false );
213
  $logins_with_some_view_access[] = $matomo_login;
214
  } elseif ( user_can( $user, Capabilities::KEY_VIEW ) ) {
215
  $matomo_login = $this->ensure_user_exists( $user );
216
+ $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] );
217
+ $user_model->addUserAccess( $matomo_login, View::ID, [ $idsite ] );
218
  $user_model->setSuperUserAccess( $matomo_login, false );
219
  $logins_with_some_view_access[] = $matomo_login;
220
+ } elseif ( $mapped_matomo_login ) {
221
+ $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] );
222
  }
223
 
224
  if ( $matomo_login ) {
225
+ $locale = get_user_locale( $user->ID );
226
+ $locale_dash = Common::mb_strtolower( str_replace( '_', '-', $locale ) );
227
+ $parts = [];
228
+ if ( $locale && in_array( $locale_dash, [ 'zh-cn', 'zh-tw', 'pt-br', 'es-ar' ], true ) ) {
229
+ $parts = [ $locale_dash ];
230
+ } elseif ( ! empty( $locale ) && is_string( $locale ) ) {
231
+ $parts = explode( '_', $locale );
232
  }
233
 
234
  if ( ! empty( $parts[0] ) ) {
241
  }
242
  }
243
  }
244
+ // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
245
+ if ( 1 != $idsite ) {
246
  // only needed if the actual site is not the default site... makes sure when they click in Matomo
247
  // UI on "Dashboard" that the correct site is being opened by default
248
  // eg if the linked site is actually idSite=2.
258
  UsersManager\API::PREFERENCE_DEFAULT_REPORT,
259
  $idsite
260
  );
261
+ //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
262
+ } catch ( Exception $e ) {
263
  // ignore any error for now
264
  }
 
265
  }
266
  );
267
  }
272
  }
273
 
274
  $logins_with_some_view_access = array_unique( $logins_with_some_view_access );
275
+ $all_users = $user_model->getUsers( [] );
276
  foreach ( $all_users as $all_user ) {
277
  if ( ! in_array( $all_user['login'], $logins_with_some_view_access, true )
278
  && ! empty( $all_user['login'] ) ) {
 
279
  Access::doAsSuperUser(
280
  function () use ( $user_model, $all_user ) {
281
  $user_model->deleteUserOnly( $all_user['login'] );
288
  }
289
 
290
  /**
291
+ * @param WP_User $wp_user
292
  */
293
  protected function ensure_user_exists( $wp_user ) {
294
  $user_model = new Model();
302
  $user_in_matomo = $user_model->getUser( $matomo_user_login );
303
  } else {
304
  // wp usernames may include whitespace etc
305
+ $login = preg_replace( '/[^A-Za-zÄäÖöÜüß0-9_.@+-]+/D', '_', $login );
306
  $login = substr( $login, 0, self::MAX_USER_NAME_LENGTH );
307
 
308
  if ( ! $user_model->getUser( $login ) ) {
337
  User::map_matomo_user_login( $user_id, $matomo_user_login );
338
  } elseif ( $user_in_matomo['email'] !== $wp_user->user_email ) {
339
  $this->logger->log( 'Matomo is now updating the email for wpUserID ' . $user_id . ' matomo login ' . $matomo_user_login );
340
+ $user_model->updateUserFields( $matomo_user_login, [ 'email' => $wp_user->user_email ] );
341
  }
342
 
343
  return $matomo_user_login;
classes/WpMatomo/views/referral.php CHANGED
@@ -12,11 +12,12 @@ if ( ! defined( 'ABSPATH' ) ) {
12
  }
13
  ?>
14
  <div class="notice notice-info is-dismissible" id="matomo-referral">
15
- <p>
16
- <?php esc_html_e( 'Like Matomo? We would really appreciate if you took 1 minute to rate us.', 'matomo' ); ?>
17
 
18
- <a href="https://wordpress.org/support/plugin/matomo/reviews/?rate=5#new-post" target="_blank" rel="noreferrer noopener"
19
- class="button matomo-dismiss-forever"><?php esc_html_e( 'Rate Matomo', 'matomo' ) ?></a>
20
- </p>
21
- <div style="clear:both;"></div>
22
- </div>
 
12
  }
13
  ?>
14
  <div class="notice notice-info is-dismissible" id="matomo-referral">
15
+ <p>
16
+ <?php esc_html_e( 'Like Matomo? We would really appreciate if you took 1 minute to rate us.', 'matomo' ); ?>
17
 
18
+ <a href="https://wordpress.org/support/plugin/matomo/reviews/?rate=5#new-post" target="_blank"
19
+ rel="noreferrer noopener"
20
+ class="button matomo-dismiss-forever"><?php esc_html_e( 'Rate Matomo', 'matomo' ); ?></a>
21
+ </p>
22
+ <div style="clear:both;"></div>
23
+ </div>
config/config.php CHANGED
@@ -122,6 +122,7 @@ return array(
122
  $class_name = get_class($check);
123
  if ($class_name === 'Piwik\Plugins\Diagnostics\Diagnostic\ForceSSLCheck'
124
  || $class_name === 'Piwik\Plugins\Diagnostics\Diagnostic\LoadDataInfileCheck'
 
125
  || $class_name === 'Piwik\Plugins\Diagnostics\Diagnostic\RequiredPrivateDirectories' // it doesn't resolve config path correctly as it is outside matomo dir etc
126
  || $class_name === 'Piwik\Plugins\Diagnostics\Diagnostic\CronArchivingCheck'
127
  || $class_name === 'Piwik\Plugins\Diagnostics\Diagnostic\FileIntegrityCheck') {
122
  $class_name = get_class($check);
123
  if ($class_name === 'Piwik\Plugins\Diagnostics\Diagnostic\ForceSSLCheck'
124
  || $class_name === 'Piwik\Plugins\Diagnostics\Diagnostic\LoadDataInfileCheck'
125
+ || $class_name === 'Piwik\Plugins\CustomJsTracker\Diagnostic\TrackerJsCheck'
126
  || $class_name === 'Piwik\Plugins\Diagnostics\Diagnostic\RequiredPrivateDirectories' // it doesn't resolve config path correctly as it is outside matomo dir etc
127
  || $class_name === 'Piwik\Plugins\Diagnostics\Diagnostic\CronArchivingCheck'
128
  || $class_name === 'Piwik\Plugins\Diagnostics\Diagnostic\FileIntegrityCheck') {
matomo.php CHANGED
@@ -4,7 +4,7 @@
4
  * Description: The #1 Google Analytics alternative that gives you full control over your data and protects the privacy for your users. Free, secure and open.
5
  * Author: Matomo
6
  * Author URI: https://matomo.org
7
- * Version: 4.4.1
8
  * Domain Path: /languages
9
  * WC requires at least: 2.4.0
10
  * WC tested up to: 5.5.0
@@ -14,6 +14,9 @@
14
  * @link https://matomo.org
15
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
16
  * @package matomo
 
 
 
17
  */
18
  if ( ! defined( 'ABSPATH' ) ) {
19
  exit; // if accessed directly
@@ -25,7 +28,7 @@ if ( ! defined( 'MATOMO_ANALYTICS_FILE' ) ) {
25
  define( 'MATOMO_ANALYTICS_FILE', __FILE__ );
26
  }
27
 
28
- if ( ! defined('MATOMO_MARKETPLACE_PLUGIN_NAME' )) {
29
  define( 'MATOMO_MARKETPLACE_PLUGIN_NAME', 'matomo-marketplace-for-wordpress/matomo-marketplace-for-wordpress.php' );
30
  }
31
 
@@ -35,8 +38,8 @@ $GLOBALS['MATOMO_PLUGINS_ENABLED'] = array();
35
  $GLOBALS['MATOMO_PLUGIN_FILES'] = array( MATOMO_ANALYTICS_FILE );
36
 
37
  function matomo_has_compatible_content_dir() {
38
- if ( !empty( $_SERVER['MATOMO_WP_ROOT_PATH'] )
39
- && file_exists( rtrim($_SERVER['MATOMO_WP_ROOT_PATH'], '/') . '/wp-load.php' ) ) {
40
  return true;
41
  }
42
 
@@ -44,49 +47,50 @@ function matomo_has_compatible_content_dir() {
44
  return false;
45
  }
46
 
47
- $contentDir = rtrim(rtrim( WP_CONTENT_DIR, '/' ), DIRECTORY_SEPARATOR );
48
- $contentDir = wp_normalize_path($contentDir);
49
- $absPath = wp_normalize_path(ABSPATH);
50
 
51
- $absPaths = array(
52
- $absPath . 'wp-content',
53
- $absPath . '/wp-content',
54
- $absPath . DIRECTORY_SEPARATOR . 'wp-content'
55
  );
56
 
57
- if (in_array($contentDir, $absPaths, true)) {
58
  return true;
59
  }
60
 
61
  $wpload_base = '../../../wp-load.php';
62
  $wpload_full = dirname( __FILE__ ) . '/' . $wpload_base;
63
- if ( file_exists($wpload_full ) && is_readable( $wpload_full ) ) {
64
  return true;
65
- } elseif (realpath( $wpload_full ) && file_exists(realpath( $wpload_full )) && is_readable(realpath( $wpload_full ))) {
66
  return true;
67
- } elseif (!empty($_SERVER['SCRIPT_FILENAME']) && file_exists($_SERVER['SCRIPT_FILENAME'])) {
68
  // seems symlinked... eg the wp-content dir or wp-content/plugins dir is symlinked from some very much other place...
69
- $wpload_full = dirname($_SERVER['SCRIPT_FILENAME']) . '/' . $wpload_base;
70
- if ( file_exists($wpload_full ) ) {
71
  return true;
72
- } elseif (realpath( $wpload_full ) && file_exists(realpath( $wpload_full ))) {
73
  return true;
74
- } elseif (file_exists(dirname( $_SERVER['SCRIPT_FILENAME'] )) . '/wp-load.php') {
75
  return true;
76
  }
77
  }
78
 
79
  // look in plugins directory if there is a config file for us
80
- $wpload_config = dirname(__FILE__) . '/../matomo.wpload_dir.php';
81
- if (file_exists( $wpload_config) && is_readable($wpload_config)) {
82
- $content = @file_get_contents($wpload_config); // we do not include that file for security reasons
83
- if (!empty($content)) {
84
- $content = str_replace(array('<?php', 'exit;'), '', $content);
85
- $content = preg_replace('/\s/', '', $content);
86
- $content = trim(ltrim(trim($content), '#')); // the path may be commented out # /abs/path
87
- if (strpos($content, DIRECTORY_SEPARATOR) === 0) {
88
- $wpload_file = rtrim($content, DIRECTORY_SEPARATOR) . '/wp-load.php';
89
- return file_exists($wpload_file) && is_readable($wpload_file);
 
90
  }
91
  }
92
  }
@@ -96,10 +100,10 @@ function matomo_has_compatible_content_dir() {
96
 
97
  function matomo_header_icon( $full = false ) {
98
  $file = 'logo';
99
- if ($full) {
100
  $file = 'logo-full';
101
  }
102
- echo '<img height="32" src="' . plugins_url( 'assets/img/'.$file.'.png', MATOMO_ANALYTICS_FILE ) . '" class="matomo-header-icon">';
103
  }
104
 
105
  function matomo_is_app_request() {
@@ -109,7 +113,7 @@ function matomo_is_app_request() {
109
 
110
  function matomo_has_tag_manager() {
111
  if ( defined( 'MATOMO_ENABLE_TAG_MANAGER' ) ) {
112
- return !empty(MATOMO_ENABLE_TAG_MANAGER);
113
  }
114
 
115
  $is_multisite = function_exists( 'is_multisite' ) && is_multisite();
@@ -123,34 +127,34 @@ function matomo_has_tag_manager() {
123
  function matomo_anonymize_value( $value ) {
124
  if ( is_string( $value ) && ! empty( $value ) ) {
125
  $values_to_anonymize = array(
126
- ABSPATH => '$ABSPATH/',
127
- str_replace( '/', '\/', ABSPATH ) => '$ABSPATH\/',
128
- str_replace( '/', '\\', ABSPATH ) => '$ABSPATH\/',
129
- WP_CONTENT_DIR => '$WP_CONTENT_DIR/',
130
  str_replace( '/', '\\', WP_CONTENT_DIR ) => '$WP_CONTENT_DIR\\',
131
- home_url() => '$home_url',
132
- site_url() => '$site_url',
133
- DB_PASSWORD => '$DB_PASSWORD',
134
- DB_USER => '$DB_USER',
135
- DB_HOST => '$DB_HOST',
136
- DB_NAME => '$DB_NAME',
137
  );
138
- $keys = array('AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'AUTH_SALT', 'NONCE_KEY', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT', 'NONCE_SALT');
139
- foreach ($keys as $key) {
140
- if (defined($key)) {
141
- $const_value = constant($key);
142
- if (!empty($const_value) && is_string($const_value) && strlen($key) > 3) {
143
- $values_to_anonymize[$const_value] = '$' . $key;
144
  }
145
  }
146
  }
147
  foreach ( $values_to_anonymize as $search => $replace ) {
148
- if ($search) {
149
  $value = str_replace( $search, $replace, $value );
150
  }
151
  }
152
  // replace anything like token_auth etc or md5 or sha1 ...
153
- $value = preg_replace('/[[:xdigit:]]{31,80}/', 'TOKEN_REPLACED', $value);
154
  }
155
 
156
  return $value;
@@ -195,11 +199,11 @@ function matomo_add_plugin( $plugins_directory, $wp_plugin_file, $is_marketplace
195
  );
196
  }
197
 
198
- if (matomo_is_app_request() || !empty($GLOBALS['MATOMO_LOADED_DIRECTLY'])) {
199
  // prevent layout being broken when thegem theme is used. their lazy items class causes the reporting UI to not appear
200
  // because it creates a JS error because of escaping " too often. only breaks when " Activate image loading optimization (for desktops)"
201
  // is enabled in the general theme settings
202
- add_filter('thegem_lazy_items_need_process_content', '__return_false', 99999999, $args = 0);
203
  }
204
 
205
  require_once __DIR__ . DIRECTORY_SEPARATOR . 'classes' . DIRECTORY_SEPARATOR . 'WpMatomo.php';
4
  * Description: The #1 Google Analytics alternative that gives you full control over your data and protects the privacy for your users. Free, secure and open.
5
  * Author: Matomo
6
  * Author URI: https://matomo.org
7
+ * Version: 4.4.2
8
  * Domain Path: /languages
9
  * WC requires at least: 2.4.0
10
  * WC tested up to: 5.5.0
14
  * @link https://matomo.org
15
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
16
  * @package matomo
17
+ * phpcs:disable WordPress.Security.ValidatedSanitizedInput
18
+ * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
19
+ * phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
20
  */
21
  if ( ! defined( 'ABSPATH' ) ) {
22
  exit; // if accessed directly
28
  define( 'MATOMO_ANALYTICS_FILE', __FILE__ );
29
  }
30
 
31
+ if ( ! defined( 'MATOMO_MARKETPLACE_PLUGIN_NAME' ) ) {
32
  define( 'MATOMO_MARKETPLACE_PLUGIN_NAME', 'matomo-marketplace-for-wordpress/matomo-marketplace-for-wordpress.php' );
33
  }
34
 
38
  $GLOBALS['MATOMO_PLUGIN_FILES'] = array( MATOMO_ANALYTICS_FILE );
39
 
40
  function matomo_has_compatible_content_dir() {
41
+ if ( ! empty( $_SERVER['MATOMO_WP_ROOT_PATH'] )
42
+ && file_exists( rtrim( $_SERVER['MATOMO_WP_ROOT_PATH'], '/' ) . '/wp-load.php' ) ) {
43
  return true;
44
  }
45
 
47
  return false;
48
  }
49
 
50
+ $content_dir = rtrim( rtrim( WP_CONTENT_DIR, '/' ), DIRECTORY_SEPARATOR );
51
+ $content_dir = wp_normalize_path( $content_dir );
52
+ $abs_path = wp_normalize_path( ABSPATH );
53
 
54
+ $abs_paths = array(
55
+ $abs_path . 'wp-content',
56
+ $abs_path . '/wp-content',
57
+ $abs_path . DIRECTORY_SEPARATOR . 'wp-content',
58
  );
59
 
60
+ if ( in_array( $content_dir, $abs_paths, true ) ) {
61
  return true;
62
  }
63
 
64
  $wpload_base = '../../../wp-load.php';
65
  $wpload_full = dirname( __FILE__ ) . '/' . $wpload_base;
66
+ if ( file_exists( $wpload_full ) && is_readable( $wpload_full ) ) {
67
  return true;
68
+ } elseif ( realpath( $wpload_full ) && file_exists( realpath( $wpload_full ) ) && is_readable( realpath( $wpload_full ) ) ) {
69
  return true;
70
+ } elseif ( ! empty( $_SERVER['SCRIPT_FILENAME'] ) && file_exists( $_SERVER['SCRIPT_FILENAME'] ) ) {
71
  // seems symlinked... eg the wp-content dir or wp-content/plugins dir is symlinked from some very much other place...
72
+ $wpload_full = dirname( $_SERVER['SCRIPT_FILENAME'] ) . '/' . $wpload_base;
73
+ if ( file_exists( $wpload_full ) ) {
74
  return true;
75
+ } elseif ( realpath( $wpload_full ) && file_exists( realpath( $wpload_full ) ) ) {
76
  return true;
77
+ } elseif ( file_exists( dirname( $_SERVER['SCRIPT_FILENAME'] ) ) . '/wp-load.php' ) {
78
  return true;
79
  }
80
  }
81
 
82
  // look in plugins directory if there is a config file for us
83
+ $wpload_config = dirname( __FILE__ ) . '/../matomo.wpload_dir.php';
84
+ if ( file_exists( $wpload_config ) && is_readable( $wpload_config ) ) {
85
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
86
+ $content = @file_get_contents( $wpload_config ); // we do not include that file for security reasons
87
+ if ( ! empty( $content ) ) {
88
+ $content = str_replace( array( '<?php', 'exit;' ), '', $content );
89
+ $content = preg_replace( '/\s/', '', $content );
90
+ $content = trim( ltrim( trim( $content ), '#' ) ); // the path may be commented out # /abs/path
91
+ if ( strpos( $content, DIRECTORY_SEPARATOR ) === 0 ) {
92
+ $wpload_file = rtrim( $content, DIRECTORY_SEPARATOR ) . '/wp-load.php';
93
+ return file_exists( $wpload_file ) && is_readable( $wpload_file );
94
  }
95
  }
96
  }
100
 
101
  function matomo_header_icon( $full = false ) {
102
  $file = 'logo';
103
+ if ( $full ) {
104
  $file = 'logo-full';
105
  }
106
+ echo '<img height="32" src="' . esc_url( plugins_url( 'assets/img/' . $file . '.png', MATOMO_ANALYTICS_FILE ) ) . '" class="matomo-header-icon">';
107
  }
108
 
109
  function matomo_is_app_request() {
113
 
114
  function matomo_has_tag_manager() {
115
  if ( defined( 'MATOMO_ENABLE_TAG_MANAGER' ) ) {
116
+ return ! empty( MATOMO_ENABLE_TAG_MANAGER );
117
  }
118
 
119
  $is_multisite = function_exists( 'is_multisite' ) && is_multisite();
127
  function matomo_anonymize_value( $value ) {
128
  if ( is_string( $value ) && ! empty( $value ) ) {
129
  $values_to_anonymize = array(
130
+ ABSPATH => '$abs_path/',
131
+ str_replace( '/', '\/', ABSPATH ) => '$abs_path\/',
132
+ str_replace( '/', '\\', ABSPATH ) => '$abs_path\/',
133
+ WP_CONTENT_DIR => '$WP_CONTENT_DIR/',
134
  str_replace( '/', '\\', WP_CONTENT_DIR ) => '$WP_CONTENT_DIR\\',
135
+ home_url() => '$home_url',
136
+ site_url() => '$site_url',
137
+ DB_PASSWORD => '$DB_PASSWORD',
138
+ DB_USER => '$DB_USER',
139
+ DB_HOST => '$DB_HOST',
140
+ DB_NAME => '$DB_NAME',
141
  );
142
+ $keys = array( 'AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'AUTH_SALT', 'NONCE_KEY', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT', 'NONCE_SALT' );
143
+ foreach ( $keys as $key ) {
144
+ if ( defined( $key ) ) {
145
+ $const_value = constant( $key );
146
+ if ( ! empty( $const_value ) && is_string( $const_value ) && strlen( $key ) > 3 ) {
147
+ $values_to_anonymize[ $const_value ] = '$' . $key;
148
  }
149
  }
150
  }
151
  foreach ( $values_to_anonymize as $search => $replace ) {
152
+ if ( $search ) {
153
  $value = str_replace( $search, $replace, $value );
154
  }
155
  }
156
  // replace anything like token_auth etc or md5 or sha1 ...
157
+ $value = preg_replace( '/[[:xdigit:]]{31,80}/', 'TOKEN_REPLACED', $value );
158
  }
159
 
160
  return $value;
199
  );
200
  }
201
 
202
+ if ( matomo_is_app_request() || ! empty( $GLOBALS['MATOMO_LOADED_DIRECTLY'] ) ) {
203
  // prevent layout being broken when thegem theme is used. their lazy items class causes the reporting UI to not appear
204
  // because it creates a JS error because of escaping " too often. only breaks when " Activate image loading optimization (for desktops)"
205
  // is enabled in the general theme settings
206
+ add_filter( 'thegem_lazy_items_need_process_content', '__return_false', 99999999, $args = 0 );
207
  }
208
 
209
  require_once __DIR__ . DIRECTORY_SEPARATOR . 'classes' . DIRECTORY_SEPARATOR . 'WpMatomo.php';
node_modules/chart.js/LICENSE.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014-2021 Chart.js Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
node_modules/chart.js/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <p align="center">
2
+ <img src="https://www.chartjs.org/media/logo-title.svg"><br/>
3
+ Simple yet flexible JavaScript charting for designers & developers
4
+ </p>
5
+
6
+ <p align="center">
7
+ <a href="https://www.chartjs.org/docs/latest/getting-started/installation.html"><img src="https://img.shields.io/github/release/chartjs/Chart.js.svg?style=flat-square&maxAge=600" alt="Downloads"></a>
8
+ <a href="https://github.com/chartjs/Chart.js/actions?query=workflow%3ACI+branch%3Amaster"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/workflow/status/chartjs/Chart.js/CI"></a>
9
+ <a href="https://coveralls.io/github/chartjs/Chart.js?branch=master"><img src="https://img.shields.io/coveralls/chartjs/Chart.js.svg?style=flat-square&maxAge=600" alt="Coverage"></a>
10
+ <a href="https://github.com/chartjs/awesome"><img src="https://awesome.re/badge-flat2.svg" alt="Awesome"></a>
11
+ <a href="https://chartjs-slack.herokuapp.com/"><img src="https://img.shields.io/badge/slack-chartjs-blue.svg?style=flat-square&maxAge=3600" alt="Slack"></a>
12
+ </p>
13
+
14
+ ## Documentation
15
+
16
+ All the links point to the new version 3 of the lib.
17
+
18
+ * [Introduction](https://www.chartjs.org/docs/latest/)
19
+ * [Getting Started](https://www.chartjs.org/docs/latest/getting-started/index)
20
+ * [General](https://www.chartjs.org/docs/latest/general/data-structures)
21
+ * [Configuration](https://www.chartjs.org/docs/latest/configuration/index)
22
+ * [Charts](https://www.chartjs.org/docs/latest/charts/line)
23
+ * [Axes](https://www.chartjs.org/docs/latest/axes/index)
24
+ * [Developers](https://www.chartjs.org/docs/latest/developers/index)
25
+ * [Popular Extensions](https://github.com/chartjs/awesome)
26
+ * [Samples](https://www.chartjs.org/samples/)
27
+
28
+ In case you are looking for the docs of version 2, you will have to specify the specific version in the url like this: [https://www.chartjs.org/docs/2.9.4/](https://www.chartjs.org/docs/2.9.4/)
29
+
30
+ ## Contributing
31
+
32
+ Instructions on building and testing Chart.js can be found in [the documentation](https://www.chartjs.org/docs/master/developers/contributing.html#building-and-testing). Before submitting an issue or a pull request, please take a moment to look over the [contributing guidelines](https://www.chartjs.org/docs/master/developers/contributing) first. For support, please post questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/chartjs) with the `chartjs` tag.
33
+
34
+ ## License
35
+
36
+ Chart.js is available under the [MIT license](https://opensource.org/licenses/MIT).
node_modules/chart.js/dist/chart.min.js ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*!
2
+ * Chart.js v3.4.1
3
+ * https://www.chartjs.org
4
+ * (c) 2021 Chart.js Contributors
5
+ * Released under the MIT License
6
+ */
7
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";const t="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function e(e,i,n){const o=n||(t=>Array.prototype.slice.call(t));let s=!1,a=[];return function(...n){a=o(n),s||(s=!0,t.call(window,(()=>{s=!1,e.apply(i,a)})))}}function i(t,e){let i;return function(){return e?(clearTimeout(i),i=setTimeout(t,e)):t(),e}}const n=t=>"start"===t?"left":"end"===t?"right":"center",o=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,s=(t,e,i,n)=>t===(n?"left":"right")?i:"center"===t?(e+i)/2:e;var a=new class{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,n){const o=e.listeners[n],s=e.duration;o.forEach((n=>n({chart:t,initial:e.initial,numSteps:s,currentStep:Math.min(i-e.start,s)})))}_refresh(){const e=this;e._request||(e._running=!0,e._request=t.call(window,(()=>{e._update(),e._request=null,e._running&&e._refresh()})))}_update(t=Date.now()){const e=this;let i=0;e._charts.forEach(((n,o)=>{if(!n.running||!n.items.length)return;const s=n.items;let a,r=s.length-1,l=!1;for(;r>=0;--r)a=s[r],a._active?(a._total>n.duration&&(n.duration=a._total),a.tick(t),l=!0):(s[r]=s[s.length-1],s.pop());l&&(o.draw(),e._notify(o,n,t,"progress")),s.length||(n.running=!1,e._notify(o,n,t,"complete"),n.initial=!1),i+=s.length})),e._lastDate=t,0===i&&(e._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let n=i.length-1;for(;n>=0;--n)i[n].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}};
8
+ /*!
9
+ * @kurkle/color v0.1.9
10
+ * https://github.com/kurkle/color#readme
11
+ * (c) 2020 Jukka Kurkela
12
+ * Released under the MIT License
13
+ */const r={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},l="0123456789ABCDEF",c=t=>l[15&t],h=t=>l[(240&t)>>4]+l[15&t],d=t=>(240&t)>>4==(15&t);function u(t){var e=function(t){return d(t.r)&&d(t.g)&&d(t.b)&&d(t.a)}(t)?c:h;return t?"#"+e(t.r)+e(t.g)+e(t.b)+(t.a<255?e(t.a):""):t}function f(t){return t+.5|0}const g=(t,e,i)=>Math.max(Math.min(t,i),e);function p(t){return g(f(2.55*t),0,255)}function m(t){return g(f(255*t),0,255)}function x(t){return g(f(t/2.55)/100,0,1)}function b(t){return g(f(100*t),0,100)}const _=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const y=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function v(t,e,i){const n=e*Math.min(i,1-i),o=(e,o=(e+t/30)%12)=>i-n*Math.max(Math.min(o-3,9-o,1),-1);return[o(0),o(8),o(4)]}function w(t,e,i){const n=(n,o=(n+t/60)%6)=>i-i*e*Math.max(Math.min(o,4-o,1),0);return[n(5),n(3),n(1)]}function M(t,e,i){const n=v(t,1,.5);let o;for(e+i>1&&(o=1/(e+i),e*=o,i*=o),o=0;o<3;o++)n[o]*=1-e-i,n[o]+=e;return n}function k(t){const e=t.r/255,i=t.g/255,n=t.b/255,o=Math.max(e,i,n),s=Math.min(e,i,n),a=(o+s)/2;let r,l,c;return o!==s&&(c=o-s,l=a>.5?c/(2-o-s):c/(o+s),r=o===e?(i-n)/c+(i<n?6:0):o===i?(n-e)/c+2:(e-i)/c+4,r=60*r+.5),[0|r,l||0,a]}function S(t,e,i,n){return(Array.isArray(e)?t(e[0],e[1],e[2]):t(e,i,n)).map(m)}function P(t,e,i){return S(v,t,e,i)}function D(t){return(t%360+360)%360}function C(t){const e=y.exec(t);let i,n=255;if(!e)return;e[5]!==i&&(n=e[6]?p(+e[5]):m(+e[5]));const o=D(+e[2]),s=+e[3]/100,a=+e[4]/100;return i="hwb"===e[1]?function(t,e,i){return S(M,t,e,i)}(o,s,a):"hsv"===e[1]?function(t,e,i){return S(w,t,e,i)}(o,s,a):P(o,s,a),{r:i[0],g:i[1],b:i[2],a:n}}const O={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},T={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};let A;function L(t){A||(A=function(){const t={},e=Object.keys(T),i=Object.keys(O);let n,o,s,a,r;for(n=0;n<e.length;n++){for(a=r=e[n],o=0;o<i.length;o++)s=i[o],r=r.replace(s,O[s]);s=parseInt(T[a],16),t[r]=[s>>16&255,s>>8&255,255&s]}return t}(),A.transparent=[0,0,0,0]);const e=A[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}function R(t,e,i){if(t){let n=k(t);n[e]=Math.max(0,Math.min(n[e]+n[e]*i,0===e?360:1)),n=P(n),t.r=n[0],t.g=n[1],t.b=n[2]}}function E(t,e){return t?Object.assign(e||{},t):t}function z(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=m(t[3]))):(e=E(t,{r:0,g:0,b:0,a:1})).a=m(e.a),e}function I(t){return"r"===t.charAt(0)?function(t){const e=_.exec(t);let i,n,o,s=255;if(e){if(e[7]!==i){const t=+e[7];s=255&(e[8]?p(t):255*t)}return i=+e[1],n=+e[3],o=+e[5],i=255&(e[2]?p(i):i),n=255&(e[4]?p(n):n),o=255&(e[6]?p(o):o),{r:i,g:n,b:o,a:s}}}(t):C(t)}class F{constructor(t){if(t instanceof F)return t;const e=typeof t;let i;var n,o,s;"object"===e?i=z(t):"string"===e&&(s=(n=t).length,"#"===n[0]&&(4===s||5===s?o={r:255&17*r[n[1]],g:255&17*r[n[2]],b:255&17*r[n[3]],a:5===s?17*r[n[4]]:255}:7!==s&&9!==s||(o={r:r[n[1]]<<4|r[n[2]],g:r[n[3]]<<4|r[n[4]],b:r[n[5]]<<4|r[n[6]],a:9===s?r[n[7]]<<4|r[n[8]]:255})),i=o||L(t)||I(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=E(this._rgb);return t&&(t.a=x(t.a)),t}set rgb(t){this._rgb=z(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${x(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):this._rgb;var t}hexString(){return this._valid?u(this._rgb):this._rgb}hslString(){return this._valid?function(t){if(!t)return;const e=k(t),i=e[0],n=b(e[1]),o=b(e[2]);return t.a<255?`hsla(${i}, ${n}%, ${o}%, ${x(t.a)})`:`hsl(${i}, ${n}%, ${o}%)`}(this._rgb):this._rgb}mix(t,e){const i=this;if(t){const n=i.rgb,o=t.rgb;let s;const a=e===s?.5:e,r=2*a-1,l=n.a-o.a,c=((r*l==-1?r:(r+l)/(1+r*l))+1)/2;s=1-c,n.r=255&c*n.r+s*o.r+.5,n.g=255&c*n.g+s*o.g+.5,n.b=255&c*n.b+s*o.b+.5,n.a=a*n.a+(1-a)*o.a,i.rgb=n}return i}clone(){return new F(this.rgb)}alpha(t){return this._rgb.a=m(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=f(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return R(this._rgb,2,t),this}darken(t){return R(this._rgb,2,-t),this}saturate(t){return R(this._rgb,1,t),this}desaturate(t){return R(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=k(t);i[0]=D(i[0]+e),i=P(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function B(t){return new F(t)}const V=t=>t instanceof CanvasGradient||t instanceof CanvasPattern;function W(t){return V(t)?t:B(t)}function N(t){return V(t)?t:B(t).saturate(.5).darken(.1).hexString()}function H(){}const j=function(){let t=0;return function(){return t++}}();function $(t){return null==t}function Y(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.substr(0,7)&&"Array]"===e.substr(-6)}function U(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}const X=t=>("number"==typeof t||t instanceof Number)&&isFinite(+t);function q(t,e){return X(t)?t:e}function K(t,e){return void 0===t?e:t}const G=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:t/e,Z=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function Q(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function J(t,e,i,n){let o,s,a;if(Y(t))if(s=t.length,n)for(o=s-1;o>=0;o--)e.call(i,t[o],o);else for(o=0;o<s;o++)e.call(i,t[o],o);else if(U(t))for(a=Object.keys(t),s=a.length,o=0;o<s;o++)e.call(i,t[a[o]],a[o])}function tt(t,e){let i,n,o,s;if(!t||!e||t.length!==e.length)return!1;for(i=0,n=t.length;i<n;++i)if(o=t[i],s=e[i],o.datasetIndex!==s.datasetIndex||o.index!==s.index)return!1;return!0}function et(t){if(Y(t))return t.map(et);if(U(t)){const e=Object.create(null),i=Object.keys(t),n=i.length;let o=0;for(;o<n;++o)e[i[o]]=et(t[i[o]]);return e}return t}function it(t){return-1===["__proto__","prototype","constructor"].indexOf(t)}function nt(t,e,i,n){if(!it(t))return;const o=e[t],s=i[t];U(o)&&U(s)?ot(o,s,n):e[t]=et(s)}function ot(t,e,i){const n=Y(e)?e:[e],o=n.length;if(!U(t))return t;const s=(i=i||{}).merger||nt;for(let a=0;a<o;++a){if(!U(e=n[a]))continue;const o=Object.keys(e);for(let n=0,a=o.length;n<a;++n)s(o[n],t,e,i)}return t}function st(t,e){return ot(t,e,{merger:at})}function at(t,e,i){if(!it(t))return;const n=e[t],o=i[t];U(n)&&U(o)?st(n,o):Object.prototype.hasOwnProperty.call(e,t)||(e[t]=et(o))}function rt(t,e){const i=t.indexOf(".",e);return-1===i?t.length:i}function lt(t,e){if(""===e)return t;let i=0,n=rt(e,i);for(;t&&n>i;)t=t[e.substr(i,n-i)],i=n+1,n=rt(e,i);return t}function ct(t){return t.charAt(0).toUpperCase()+t.slice(1)}const ht=t=>void 0!==t,dt=t=>"function"==typeof t,ut=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0},ft=Object.create(null),gt=Object.create(null);function pt(t,e){if(!e)return t;const i=e.split(".");for(let e=0,n=i.length;e<n;++e){const n=i[e];t=t[n]||(t[n]=Object.create(null))}return t}function mt(t,e,i){return"string"==typeof e?ot(pt(t,e),i):ot(pt(t,""),e)}var xt=new class{constructor(t){this.animation=void 0,this.backgroundColor="rgba(0,0,0,0.1)",this.borderColor="rgba(0,0,0,0.1)",this.color="#666",this.datasets={},this.devicePixelRatio=t=>t.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>N(e.backgroundColor),this.hoverBorderColor=(t,e)=>N(e.borderColor),this.hoverColor=(t,e)=>N(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.describe(t)}set(t,e){return mt(this,t,e)}get(t){return pt(this,t)}describe(t,e){return mt(gt,t,e)}override(t,e){return mt(ft,t,e)}route(t,e,i,n){const o=pt(this,t),s=pt(this,i),a="_"+e;Object.defineProperties(o,{[a]:{value:o[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[a],e=s[n];return U(t)?Object.assign({},e,t):K(t,e)},set(t){this[a]=t}}})}}({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}});const bt=Math.PI,_t=2*bt,yt=_t+bt,vt=Number.POSITIVE_INFINITY,wt=bt/180,Mt=bt/2,kt=bt/4,St=2*bt/3,Pt=Math.log10,Dt=Math.sign;function Ct(t){const e=Math.round(t);t=At(t,e,t/1e3)?e:t;const i=Math.pow(10,Math.floor(Pt(t))),n=t/i;return(n<=1?1:n<=2?2:n<=5?5:10)*i}function Ot(t){const e=[],i=Math.sqrt(t);let n;for(n=1;n<i;n++)t%n==0&&(e.push(n),e.push(t/n));return i===(0|i)&&e.push(i),e.sort(((t,e)=>t-e)).pop(),e}function Tt(t){return!isNaN(parseFloat(t))&&isFinite(t)}function At(t,e,i){return Math.abs(t-e)<i}function Lt(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function Rt(t,e,i){let n,o,s;for(n=0,o=t.length;n<o;n++)s=t[n][i],isNaN(s)||(e.min=Math.min(e.min,s),e.max=Math.max(e.max,s))}function Et(t){return t*(bt/180)}function zt(t){return t*(180/bt)}function It(t){if(!X(t))return;let e=1,i=0;for(;Math.round(t*e)/e!==t;)e*=10,i++;return i}function Ft(t,e){const i=e.x-t.x,n=e.y-t.y,o=Math.sqrt(i*i+n*n);let s=Math.atan2(n,i);return s<-.5*bt&&(s+=_t),{angle:s,distance:o}}function Bt(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function Vt(t,e){return(t-e+yt)%_t-bt}function Wt(t){return(t%_t+_t)%_t}function Nt(t,e,i,n){const o=Wt(t),s=Wt(e),a=Wt(i),r=Wt(s-o),l=Wt(a-o),c=Wt(o-s),h=Wt(o-a);return o===s||o===a||n&&s===a||r>l&&c<h}function Ht(t,e,i){return Math.max(e,Math.min(i,t))}function jt(t){return Ht(t,-32768,32767)}function $t(t){return!t||$(t.size)||$(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function Yt(t,e,i,n,o){let s=e[o];return s||(s=e[o]=t.measureText(o).width,i.push(o)),s>n&&(n=s),n}function Ut(t,e,i,n){let o=(n=n||{}).data=n.data||{},s=n.garbageCollect=n.garbageCollect||[];n.font!==e&&(o=n.data={},s=n.garbageCollect=[],n.font=e),t.save(),t.font=e;let a=0;const r=i.length;let l,c,h,d,u;for(l=0;l<r;l++)if(d=i[l],null!=d&&!0!==Y(d))a=Yt(t,o,s,a,d);else if(Y(d))for(c=0,h=d.length;c<h;c++)u=d[c],null==u||Y(u)||(a=Yt(t,o,s,a,u));t.restore();const f=s.length/2;if(f>i.length){for(l=0;l<f;l++)delete o[s[l]];s.splice(0,f)}return a}function Xt(t,e,i){const n=t.currentDevicePixelRatio,o=0!==i?Math.max(i/2,.5):0;return Math.round((e-o)*n)/n+o}function qt(t,e){(e=e||t.getContext("2d")).save(),e.resetTransform(),e.clearRect(0,0,t.width,t.height),e.restore()}function Kt(t,e,i,n){let o,s,a,r,l;const c=e.pointStyle,h=e.rotation,d=e.radius;let u=(h||0)*wt;if(c&&"object"==typeof c&&(o=c.toString(),"[object HTMLImageElement]"===o||"[object HTMLCanvasElement]"===o))return t.save(),t.translate(i,n),t.rotate(u),t.drawImage(c,-c.width/2,-c.height/2,c.width,c.height),void t.restore();if(!(isNaN(d)||d<=0)){switch(t.beginPath(),c){default:t.arc(i,n,d,0,_t),t.closePath();break;case"triangle":t.moveTo(i+Math.sin(u)*d,n-Math.cos(u)*d),u+=St,t.lineTo(i+Math.sin(u)*d,n-Math.cos(u)*d),u+=St,t.lineTo(i+Math.sin(u)*d,n-Math.cos(u)*d),t.closePath();break;case"rectRounded":l=.516*d,r=d-l,s=Math.cos(u+kt)*r,a=Math.sin(u+kt)*r,t.arc(i-s,n-a,l,u-bt,u-Mt),t.arc(i+a,n-s,l,u-Mt,u),t.arc(i+s,n+a,l,u,u+Mt),t.arc(i-a,n+s,l,u+Mt,u+bt),t.closePath();break;case"rect":if(!h){r=Math.SQRT1_2*d,t.rect(i-r,n-r,2*r,2*r);break}u+=kt;case"rectRot":s=Math.cos(u)*d,a=Math.sin(u)*d,t.moveTo(i-s,n-a),t.lineTo(i+a,n-s),t.lineTo(i+s,n+a),t.lineTo(i-a,n+s),t.closePath();break;case"crossRot":u+=kt;case"cross":s=Math.cos(u)*d,a=Math.sin(u)*d,t.moveTo(i-s,n-a),t.lineTo(i+s,n+a),t.moveTo(i+a,n-s),t.lineTo(i-a,n+s);break;case"star":s=Math.cos(u)*d,a=Math.sin(u)*d,t.moveTo(i-s,n-a),t.lineTo(i+s,n+a),t.moveTo(i+a,n-s),t.lineTo(i-a,n+s),u+=kt,s=Math.cos(u)*d,a=Math.sin(u)*d,t.moveTo(i-s,n-a),t.lineTo(i+s,n+a),t.moveTo(i+a,n-s),t.lineTo(i-a,n+s);break;case"line":s=Math.cos(u)*d,a=Math.sin(u)*d,t.moveTo(i-s,n-a),t.lineTo(i+s,n+a);break;case"dash":t.moveTo(i,n),t.lineTo(i+Math.cos(u)*d,n+Math.sin(u)*d)}t.fill(),e.borderWidth>0&&t.stroke()}}function Gt(t,e,i){return i=i||.5,t&&t.x>e.left-i&&t.x<e.right+i&&t.y>e.top-i&&t.y<e.bottom+i}function Zt(t,e){t.save(),t.beginPath(),t.rect(e.left,e.top,e.right-e.left,e.bottom-e.top),t.clip()}function Qt(t){t.restore()}function Jt(t,e,i,n,o){if(!e)return t.lineTo(i.x,i.y);if("middle"===o){const n=(e.x+i.x)/2;t.lineTo(n,e.y),t.lineTo(n,i.y)}else"after"===o!=!!n?t.lineTo(e.x,i.y):t.lineTo(i.x,e.y);t.lineTo(i.x,i.y)}function te(t,e,i,n){if(!e)return t.lineTo(i.x,i.y);t.bezierCurveTo(n?e.cp1x:e.cp2x,n?e.cp1y:e.cp2y,n?i.cp2x:i.cp1x,n?i.cp2y:i.cp1y,i.x,i.y)}function ee(t,e,i,n,o,s={}){const a=Y(e)?e:[e],r=s.strokeWidth>0&&""!==s.strokeColor;let l,c;for(t.save(),t.font=o.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]);$(e.rotation)||t.rotate(e.rotation);e.color&&(t.fillStyle=e.color);e.textAlign&&(t.textAlign=e.textAlign);e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,s),l=0;l<a.length;++l)c=a[l],r&&(s.strokeColor&&(t.strokeStyle=s.strokeColor),$(s.strokeWidth)||(t.lineWidth=s.strokeWidth),t.strokeText(c,i,n,s.maxWidth)),t.fillText(c,i,n,s.maxWidth),ie(t,i,n,c,s),n+=o.lineHeight;t.restore()}function ie(t,e,i,n,o){if(o.strikethrough||o.underline){const s=t.measureText(n),a=e-s.actualBoundingBoxLeft,r=e+s.actualBoundingBoxRight,l=i-s.actualBoundingBoxAscent,c=i+s.actualBoundingBoxDescent,h=o.strikethrough?(l+c)/2:c;t.strokeStyle=t.fillStyle,t.beginPath(),t.lineWidth=o.decorationWidth||2,t.moveTo(a,h),t.lineTo(r,h),t.stroke()}}function ne(t,e){const{x:i,y:n,w:o,h:s,radius:a}=e;t.arc(i+a.topLeft,n+a.topLeft,a.topLeft,-Mt,bt,!0),t.lineTo(i,n+s-a.bottomLeft),t.arc(i+a.bottomLeft,n+s-a.bottomLeft,a.bottomLeft,bt,Mt,!0),t.lineTo(i+o-a.bottomRight,n+s),t.arc(i+o-a.bottomRight,n+s-a.bottomRight,a.bottomRight,Mt,0,!0),t.lineTo(i+o,n+a.topRight),t.arc(i+o-a.topRight,n+a.topRight,a.topRight,0,-Mt,!0),t.lineTo(i+a.topLeft,n)}function oe(t,e,i){i=i||(i=>t[i]<e);let n,o=t.length-1,s=0;for(;o-s>1;)n=s+o>>1,i(n)?s=n:o=n;return{lo:s,hi:o}}const se=(t,e,i)=>oe(t,i,(n=>t[n][e]<i)),ae=(t,e,i)=>oe(t,i,(n=>t[n][e]>=i));function re(t,e,i){let n=0,o=t.length;for(;n<o&&t[n]<e;)n++;for(;o>n&&t[o-1]>i;)o--;return n>0||o<t.length?t.slice(n,o):t}const le=["push","pop","shift","splice","unshift"];function ce(t,e){t._chartjs?t._chartjs.listeners.push(e):(Object.defineProperty(t,"_chartjs",{configurable:!0,enumerable:!1,value:{listeners:[e]}}),le.forEach((e=>{const i="_onData"+ct(e),n=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const o=n.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),o}})})))}function he(t,e){const i=t._chartjs;if(!i)return;const n=i.listeners,o=n.indexOf(e);-1!==o&&n.splice(o,1),n.length>0||(le.forEach((e=>{delete t[e]})),delete t._chartjs)}function de(t){const e=new Set;let i,n;for(i=0,n=t.length;i<n;++i)e.add(t[i]);return e.size===n?t:Array.from(e)}function ue(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function fe(t,e,i){let n;return"string"==typeof t?(n=parseInt(t,10),-1!==t.indexOf("%")&&(n=n/100*e.parentNode[i])):n=t,n}const ge=t=>window.getComputedStyle(t,null);function pe(t,e){return ge(t).getPropertyValue(e)}const me=["top","right","bottom","left"];function xe(t,e,i){const n={};i=i?"-"+i:"";for(let o=0;o<4;o++){const s=me[o];n[s]=parseFloat(t[e+"-"+s+i])||0}return n.width=n.left+n.right,n.height=n.top+n.bottom,n}function be(t,e){const{canvas:i,currentDevicePixelRatio:n}=e,o=ge(i),s="border-box"===o.boxSizing,a=xe(o,"padding"),r=xe(o,"border","width"),{x:l,y:c,box:h}=function(t,e){const i=t.native||t,n=i.touches,o=n&&n.length?n[0]:i,{offsetX:s,offsetY:a}=o;let r,l,c=!1;if(((t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot))(s,a,i.target))r=s,l=a;else{const t=e.getBoundingClientRect();r=o.clientX-t.left,l=o.clientY-t.top,c=!0}return{x:r,y:l,box:c}}(t,i),d=a.left+(h&&r.left),u=a.top+(h&&r.top);let{width:f,height:g}=e;return s&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/n),y:Math.round((c-u)/g*i.height/n)}}const _e=t=>Math.round(10*t)/10;function ye(t,e,i,n){const o=ge(t),s=xe(o,"margin"),a=fe(o.maxWidth,t,"clientWidth")||vt,r=fe(o.maxHeight,t,"clientHeight")||vt,l=function(t,e,i){let n,o;if(void 0===e||void 0===i){const s=ue(t);if(s){const t=s.getBoundingClientRect(),a=ge(s),r=xe(a,"border","width"),l=xe(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,n=fe(a.maxWidth,s,"clientWidth"),o=fe(a.maxHeight,s,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:n||vt,maxHeight:o||vt}}(t,e,i);let{width:c,height:h}=l;if("content-box"===o.boxSizing){const t=xe(o,"border","width"),e=xe(o,"padding");c-=e.width+t.width,h-=e.height+t.height}return c=Math.max(0,c-s.width),h=Math.max(0,n?Math.floor(c/n):h-s.height),c=_e(Math.min(c,a,l.maxWidth)),h=_e(Math.min(h,r,l.maxHeight)),c&&!h&&(h=_e(c/2)),{width:c,height:h}}function ve(t,e,i){const n=e||1,o=Math.floor(t.height*n),s=Math.floor(t.width*n);t.height=o/n,t.width=s/n;const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==n||a.height!==o||a.width!==s)&&(t.currentDevicePixelRatio=n,a.height=o,a.width=s,t.ctx.setTransform(n,0,0,n,0,0),!0)}const we=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};window.addEventListener("test",null,e),window.removeEventListener("test",null,e)}catch(t){}return t}();function Me(t,e){const i=pe(t,e),n=i&&i.match(/^(\d+)(\.\d+)?px$/);return n?+n[1]:void 0}function ke(t,e){return"native"in t?{x:t.x,y:t.y}:be(t,e)}function Se(t,e,i,n){const{controller:o,data:s,_sorted:a}=t,r=o._cachedMeta.iScale;if(r&&e===r.axis&&a&&s.length){const t=r._reversePixels?ae:se;if(!n)return t(s,e,i);if(o._sharedOptions){const n=s[0],o="function"==typeof n.getRange&&n.getRange(e);if(o){const n=t(s,e,i-o),a=t(s,e,i+o);return{lo:n.lo,hi:a.hi}}}}return{lo:0,hi:s.length-1}}function Pe(t,e,i,n,o){const s=t.getSortedVisibleDatasetMetas(),a=i[e];for(let t=0,i=s.length;t<i;++t){const{index:i,data:r}=s[t],{lo:l,hi:c}=Se(s[t],e,a,o);for(let t=l;t<=c;++t){const e=r[t];e.skip||n(e,i,t)}}}function De(t,e,i,n){const o=[];if(!Gt(e,t.chartArea,t._minPadding))return o;return Pe(t,i,e,(function(t,i,s){t.inRange(e.x,e.y,n)&&o.push({element:t,datasetIndex:i,index:s})}),!0),o}function Ce(t,e,i,n,o){const s=function(t){const e=-1!==t.indexOf("x"),i=-1!==t.indexOf("y");return function(t,n){const o=e?Math.abs(t.x-n.x):0,s=i?Math.abs(t.y-n.y):0;return Math.sqrt(Math.pow(o,2)+Math.pow(s,2))}}(i);let a=Number.POSITIVE_INFINITY,r=[];if(!Gt(e,t.chartArea,t._minPadding))return r;return Pe(t,i,e,(function(i,l,c){if(n&&!i.inRange(e.x,e.y,o))return;const h=i.getCenterPoint(o);if(!Gt(h,t.chartArea,t._minPadding))return;const d=s(e,h);d<a?(r=[{element:i,datasetIndex:l,index:c}],a=d):d===a&&r.push({element:i,datasetIndex:l,index:c})})),r}function Oe(t,e,i,n){const o=ke(e,t),s=[],a=i.axis,r="x"===a?"inXRange":"inYRange";let l=!1;return function(t,e){const i=t.getSortedVisibleDatasetMetas();let n,o,s;for(let t=0,a=i.length;t<a;++t){({index:n,data:o}=i[t]);for(let t=0,i=o.length;t<i;++t)s=o[t],s.skip||e(s,n,t)}}(t,((t,e,i)=>{t[r](o[a],n)&&s.push({element:t,datasetIndex:e,index:i}),t.inRange(o.x,o.y,n)&&(l=!0)})),i.intersect&&!l?[]:s}var Te={modes:{index(t,e,i,n){const o=ke(e,t),s=i.axis||"x",a=i.intersect?De(t,o,s,n):Ce(t,o,s,!1,n),r=[];return a.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=a[0].index,i=t.data[e];i&&!i.skip&&r.push({element:i,datasetIndex:t.index,index:e})})),r):[]},dataset(t,e,i,n){const o=ke(e,t),s=i.axis||"xy";let a=i.intersect?De(t,o,s,n):Ce(t,o,s,!1,n);if(a.length>0){const e=a[0].datasetIndex,i=t.getDatasetMeta(e).data;a=[];for(let t=0;t<i.length;++t)a.push({element:i[t],datasetIndex:e,index:t})}return a},point:(t,e,i,n)=>De(t,ke(e,t),i.axis||"xy",n),nearest:(t,e,i,n)=>Ce(t,ke(e,t),i.axis||"xy",i.intersect,n),x:(t,e,i,n)=>(i.axis="x",Oe(t,e,i,n)),y:(t,e,i,n)=>(i.axis="y",Oe(t,e,i,n))}};const Ae=new RegExp(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/),Le=new RegExp(/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/);function Re(t,e){const i=(""+t).match(Ae);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}function Ee(t,e){const i={},n=U(e),o=n?Object.keys(e):e,s=U(t)?n?i=>K(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of o)i[t]=+s(t)||0;return i}function ze(t){return Ee(t,{top:"y",right:"x",bottom:"y",left:"x"})}function Ie(t){return Ee(t,["topLeft","topRight","bottomLeft","bottomRight"])}function Fe(t){const e=ze(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Be(t,e){t=t||{},e=e||xt.font;let i=K(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let n=K(t.style,e.style);n&&!(""+n).match(Le)&&(console.warn('Invalid font style specified: "'+n+'"'),n="");const o={family:K(t.family,e.family),lineHeight:Re(K(t.lineHeight,e.lineHeight),i),size:i,style:n,weight:K(t.weight,e.weight),string:""};return o.string=$t(o),o}function Ve(t,e,i,n){let o,s,a,r=!0;for(o=0,s=t.length;o<s;++o)if(a=t[o],void 0!==a&&(void 0!==e&&"function"==typeof a&&(a=a(e),r=!1),void 0!==i&&Y(a)&&(a=a[i%a.length],r=!1),void 0!==a))return n&&!r&&(n.cacheable=!1),a}function We(t,e){const{min:i,max:n}=t;return{min:i-Math.abs(Z(e,i)),max:n+Z(e,n)}}const Ne=["left","top","right","bottom"];function He(t,e){return t.filter((t=>t.pos===e))}function je(t,e){return t.filter((t=>-1===Ne.indexOf(t.pos)&&t.box.axis===e))}function $e(t,e){return t.sort(((t,i)=>{const n=e?i:t,o=e?t:i;return n.weight===o.weight?n.index-o.index:n.weight-o.weight}))}function Ye(t,e,i,n){return Math.max(t[i],e[i])+Math.max(t[n],e[n])}function Ue(t,e){t.top=Math.max(t.top,e.top),t.left=Math.max(t.left,e.left),t.bottom=Math.max(t.bottom,e.bottom),t.right=Math.max(t.right,e.right)}function Xe(t,e,i){const n=i.box,o=t.maxPadding;U(i.pos)||(i.size&&(t[i.pos]-=i.size),i.size=i.horizontal?n.height:n.width,t[i.pos]+=i.size),n.getPadding&&Ue(o,n.getPadding());const s=Math.max(0,e.outerWidth-Ye(o,t,"left","right")),a=Math.max(0,e.outerHeight-Ye(o,t,"top","bottom")),r=s!==t.w,l=a!==t.h;return t.w=s,t.h=a,i.horizontal?{same:r,other:l}:{same:l,other:r}}function qe(t,e){const i=e.maxPadding;function n(t){const n={left:0,top:0,right:0,bottom:0};return t.forEach((t=>{n[t]=Math.max(e[t],i[t])})),n}return n(t?["left","right"]:["top","bottom"])}function Ke(t,e,i){const n=[];let o,s,a,r,l,c;for(o=0,s=t.length,l=0;o<s;++o){a=t[o],r=a.box,r.update(a.width||e.w,a.height||e.h,qe(a.horizontal,e));const{same:s,other:h}=Xe(e,i,a);l|=s&&n.length,c=c||h,r.fullSize||n.push(a)}return l&&Ke(n,e,i)||c}function Ge(t,e,i){const n=i.padding;let o,s,a,r,l=e.x,c=e.y;for(o=0,s=t.length;o<s;++o)a=t[o],r=a.box,a.horizontal?(r.left=r.fullSize?n.left:e.left,r.right=r.fullSize?i.outerWidth-n.right:e.left+e.w,r.top=c,r.bottom=c+r.height,r.width=r.right-r.left,c=r.bottom):(r.left=l,r.right=l+r.width,r.top=r.fullSize?n.top:e.top,r.bottom=r.fullSize?i.outerHeight-n.bottom:e.top+e.h,r.height=r.bottom-r.top,l=r.right);e.x=l,e.y=c}xt.set("layout",{padding:{top:0,right:0,bottom:0,left:0}});var Ze={addBox(t,e){t.boxes||(t.boxes=[]),e.fullSize=e.fullSize||!1,e.position=e.position||"top",e.weight=e.weight||0,e._layers=e._layers||function(){return[{z:0,draw(t){e.draw(t)}}]},t.boxes.push(e)},removeBox(t,e){const i=t.boxes?t.boxes.indexOf(e):-1;-1!==i&&t.boxes.splice(i,1)},configure(t,e,i){e.fullSize=i.fullSize,e.position=i.position,e.weight=i.weight},update(t,e,i,n){if(!t)return;const o=Fe(t.options.layout.padding),s=Math.max(e-o.width,0),a=Math.max(i-o.height,0),r=function(t){const e=function(t){const e=[];let i,n,o;for(i=0,n=(t||[]).length;i<n;++i)o=t[i],e.push({index:i,box:o,pos:o.position,horizontal:o.isHorizontal(),weight:o.weight});return e}(t),i=$e(e.filter((t=>t.box.fullSize)),!0),n=$e(He(e,"left"),!0),o=$e(He(e,"right")),s=$e(He(e,"top"),!0),a=$e(He(e,"bottom")),r=je(e,"x"),l=je(e,"y");return{fullSize:i,leftAndTop:n.concat(s),rightAndBottom:o.concat(l).concat(a).concat(r),chartArea:He(e,"chartArea"),vertical:n.concat(o).concat(l),horizontal:s.concat(a).concat(r)}}(t.boxes),l=r.vertical,c=r.horizontal;J(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const h=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:o,availableWidth:s,availableHeight:a,vBoxMaxWidth:s/2/h,hBoxMaxHeight:a/2}),u=Object.assign({},o);Ue(u,Fe(n));const f=Object.assign({maxPadding:u,w:s,h:a,x:o.left,y:o.top},o);!function(t,e){let i,n,o;for(i=0,n=t.length;i<n;++i)o=t[i],o.horizontal?(o.width=o.box.fullSize&&e.availableWidth,o.height=e.hBoxMaxHeight):(o.width=e.vBoxMaxWidth,o.height=o.box.fullSize&&e.availableHeight)}(l.concat(c),d),Ke(r.fullSize,f,d),Ke(l,f,d),Ke(c,f,d)&&Ke(l,f,d),function(t){const e=t.maxPadding;function i(i){const n=Math.max(e[i]-t[i],0);return t[i]+=n,n}t.y+=i("top"),t.x+=i("left"),i("right"),i("bottom")}(f),Ge(r.leftAndTop,f,d),f.x+=f.w,f.y+=f.h,Ge(r.rightAndBottom,f,d),t.chartArea={left:f.left,top:f.top,right:f.left+f.w,bottom:f.top+f.h,height:f.h,width:f.w},J(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(f.w,f.h)}))}};class Qe{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,n){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,n?Math.floor(e/n):i)}}isAttached(t){return!0}}class Je extends Qe{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}}const ti={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},ei=t=>null===t||""===t;const ii=!!we&&{passive:!0};function ni(t,e,i){t.canvas.removeEventListener(e,i,ii)}function oi(t,e,i){const n=t.canvas,o=n&&ue(n)||n,s=new MutationObserver((t=>{const e=ue(o);t.forEach((t=>{for(let n=0;n<t.addedNodes.length;n++){const s=t.addedNodes[n];s!==o&&s!==e||i(t.target)}}))}));return s.observe(document,{childList:!0,subtree:!0}),s}function si(t,e,i){const n=t.canvas,o=n&&ue(n);if(!o)return;const s=new MutationObserver((t=>{t.forEach((t=>{for(let e=0;e<t.removedNodes.length;e++)if(t.removedNodes[e]===n){i();break}}))}));return s.observe(o,{childList:!0}),s}const ai=new Map;let ri=0;function li(){const t=window.devicePixelRatio;t!==ri&&(ri=t,ai.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function ci(t,i,n){const o=t.canvas,s=o&&ue(o);if(!s)return;const a=e(((t,e)=>{const i=s.clientWidth;n(t,e),i<s.clientWidth&&n()}),window),r=new ResizeObserver((t=>{const e=t[0],i=e.contentRect.width,n=e.contentRect.height;0===i&&0===n||a(i,n)}));return r.observe(s),function(t,e){ai.size||window.addEventListener("resize",li),ai.set(t,e)}(t,a),r}function hi(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){ai.delete(t),ai.size||window.removeEventListener("resize",li)}(t)}function di(t,i,n){const o=t.canvas,s=e((e=>{null!==t.ctx&&n(function(t,e){const i=ti[t.type]||t.type,{x:n,y:o}=be(t,e);return{type:i,chart:e,native:t,x:void 0!==n?n:null,y:void 0!==o?o:null}}(e,t))}),t,(t=>{const e=t[0];return[e,e.offsetX,e.offsetY]}));return function(t,e,i){t.addEventListener(e,i,ii)}(o,i,s),s}class ui extends Qe{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,n=t.getAttribute("height"),o=t.getAttribute("width");if(t.$chartjs={initial:{height:n,width:o,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",ei(o)){const e=Me(t,"width");void 0!==e&&(t.width=e)}if(ei(n))if(""===t.style.height)t.height=t.width/(e||2);else{const e=Me(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e.$chartjs)return!1;const i=e.$chartjs.initial;["height","width"].forEach((t=>{const n=i[t];$(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e.$chartjs,!0}addEventListener(t,e,i){this.removeEventListener(t,e);const n=t.$proxies||(t.$proxies={}),o={attach:oi,detach:si,resize:ci}[e]||di;n[e]=o(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),n=i[e];if(!n)return;({attach:hi,detach:hi,resize:hi}[e]||ni)(t,e,n),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,n){return ye(t,e,i,n)}isAttached(t){const e=ue(t);return!(!e||!ue(e))}}var fi=Object.freeze({__proto__:null,BasePlatform:Qe,BasicPlatform:Je,DomPlatform:ui});const gi=t=>0===t||1===t,pi=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*_t/i),mi=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*_t/i)+1,xi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*Mt),easeOutSine:t=>Math.sin(t*Mt),easeInOutSine:t=>-.5*(Math.cos(bt*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>gi(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>gi(t)?t:pi(t,.075,.3),easeOutElastic:t=>gi(t)?t:mi(t,.075,.3),easeInOutElastic(t){const e=.1125;return gi(t)?t:t<.5?.5*pi(2*t,e,.45):.5+.5*mi(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-xi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*xi.easeInBounce(2*t):.5*xi.easeOutBounce(2*t-1)+.5},bi="transparent",_i={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const n=W(t||bi),o=n.valid&&W(e||bi);return o&&o.valid?o.mix(n,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class yi{constructor(t,e,i,n){const o=e[i];n=Ve([t.to,n,o,t.from]);const s=Ve([t.from,o,n]);this._active=!0,this._fn=t.fn||_i[t.type||typeof s],this._easing=xi[t.easing]||xi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=s,this._to=n,this._promises=void 0}active(){return this._active}update(t,e,i){const n=this;if(n._active){n._notify(!1);const o=n._target[n._prop],s=i-n._start,a=n._duration-s;n._start=i,n._duration=Math.floor(Math.max(a,t.duration)),n._total+=s,n._loop=!!t.loop,n._to=Ve([t.to,e,o,t.from]),n._from=Ve([t.from,o,e])}}cancel(){const t=this;t._active&&(t.tick(Date.now()),t._active=!1,t._notify(!1))}tick(t){const e=this,i=t-e._start,n=e._duration,o=e._prop,s=e._from,a=e._loop,r=e._to;let l;if(e._active=s!==r&&(a||i<n),!e._active)return e._target[o]=r,void e._notify(!0);i<0?e._target[o]=s:(l=i/n%2,l=a&&l>1?2-l:l,l=e._easing(Math.min(1,Math.max(0,l))),e._target[o]=e._fn(s,r,l))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t<i.length;t++)i[t][e]()}}xt.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0});const vi=Object.keys(xt.animation);xt.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),xt.set("animations",{colors:{type:"color",properties:["color","borderColor","backgroundColor"]},numbers:{type:"number",properties:["x","y","borderWidth","radius","tension"]}}),xt.describe("animations",{_fallback:"animation"}),xt.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}});class wi{constructor(t,e){this._chart=t,this._properties=new Map,this.configure(e)}configure(t){if(!U(t))return;const e=this._properties;Object.getOwnPropertyNames(t).forEach((i=>{const n=t[i];if(!U(n))return;const o={};for(const t of vi)o[t]=n[t];(Y(n.properties)&&n.properties||[i]).forEach((t=>{t!==i&&e.has(t)||e.set(t,o)}))}))}_animateOptions(t,e){const i=e.options,n=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!n)return[];const o=this._createAnimations(n,i);return i.$shared&&function(t,e){const i=[],n=Object.keys(e);for(let e=0;e<n.length;e++){const o=t[n[e]];o&&o.active()&&i.push(o.wait())}return Promise.all(i)}(t.options.$animations,i).then((()=>{t.options=i}),(()=>{})),o}_createAnimations(t,e){const i=this._properties,n=[],o=t.$animations||(t.$animations={}),s=Object.keys(e),a=Date.now();let r;for(r=s.length-1;r>=0;--r){const l=s[r];if("$"===l.charAt(0))continue;if("options"===l){n.push(...this._animateOptions(t,e));continue}const c=e[l];let h=o[l];const d=i.get(l);if(h){if(d&&h.active()){h.update(d,c,a);continue}h.cancel()}d&&d.duration?(o[l]=h=new yi(d,t,l,c),n.push(h)):t[l]=c}return n}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(a.add(this._chart,i),!0):void 0}}function Mi(t,e){const i=t&&t.options||{},n=i.reverse,o=void 0===i.min?e:0,s=void 0===i.max?e:0;return{start:n?s:o,end:n?o:s}}function ki(t,e){const i=[],n=t._getSortedDatasetMetas(e);let o,s;for(o=0,s=n.length;o<s;++o)i.push(n[o].index);return i}function Si(t,e,i,n){const o=t.keys,s="single"===n.mode;let a,r,l,c;if(null!==e){for(a=0,r=o.length;a<r;++a){if(l=+o[a],l===i){if(n.all)continue;break}c=t.values[l],X(c)&&(s||0===e||Dt(e)===Dt(c))&&(e+=c)}return e}}function Pi(t,e){const i=t&&t.options.stacked;return i||void 0===i&&void 0!==e.stack}function Di(t,e,i){const n=t[e]||(t[e]={});return n[i]||(n[i]={})}function Ci(t,e,i){for(const n of e.getMatchingVisibleMetas("bar").reverse()){const e=t[n.index];if(i&&e>0||!i&&e<0)return n.index}return null}function Oi(t,e){const{chart:i,_cachedMeta:n}=t,o=i._stacks||(i._stacks={}),{iScale:s,vScale:a,index:r}=n,l=s.axis,c=a.axis,h=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(s,a,n),d=e.length;let u;for(let t=0;t<d;++t){const i=e[t],{[l]:n,[c]:s}=i;u=(i._stacks||(i._stacks={}))[c]=Di(o,h,n),u[r]=s,u._top=Ci(u,a,!0),u._bottom=Ci(u,a,!1)}}function Ti(t,e){const i=t.scales;return Object.keys(i).filter((t=>i[t].axis===e)).shift()}function Ai(t,e){const i=t.vScale&&t.vScale.axis;if(i){e=e||t._parsed;for(const n of e){const e=n._stacks;if(!e||void 0===e[i]||void 0===e[i][t.index])return;delete e[i][t.index]}}}const Li=t=>"reset"===t||"none"===t,Ri=(t,e)=>e?t:Object.assign({},t);class Ei{constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.$context=void 0,this._syncList=[],this.initialize()}initialize(){const t=this,e=t._cachedMeta;t.configure(),t.linkScales(),e._stacked=Pi(e.vScale,e),t.addElements()}updateIndex(t){this.index!==t&&Ai(this._cachedMeta),this.index=t}linkScales(){const t=this,e=t.chart,i=t._cachedMeta,n=t.getDataset(),o=(t,e,i,n)=>"x"===t?e:"r"===t?n:i,s=i.xAxisID=K(n.xAxisID,Ti(e,"x")),a=i.yAxisID=K(n.yAxisID,Ti(e,"y")),r=i.rAxisID=K(n.rAxisID,Ti(e,"r")),l=i.indexAxis,c=i.iAxisID=o(l,s,a,r),h=i.vAxisID=o(l,a,s,r);i.xScale=t.getScaleForId(s),i.yScale=t.getScaleForId(a),i.rScale=t.getScaleForId(r),i.iScale=t.getScaleForId(c),i.vScale=t.getScaleForId(h)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&he(this._data,this),t._stacked&&Ai(t)}_dataCheck(){const t=this,e=t.getDataset(),i=e.data||(e.data=[]),n=t._data;if(U(i))t._data=function(t){const e=Object.keys(t),i=new Array(e.length);let n,o,s;for(n=0,o=e.length;n<o;++n)s=e[n],i[n]={x:s,y:t[s]};return i}(i);else if(n!==i){if(n){he(n,t);const e=t._cachedMeta;Ai(e),e._parsed=[]}i&&Object.isExtensible(i)&&ce(i,t),t._syncList=[],t._data=i}}addElements(){const t=this,e=t._cachedMeta;t._dataCheck(),t.datasetElementType&&(e.dataset=new t.datasetElementType)}buildOrUpdateElements(t){const e=this,i=e._cachedMeta,n=e.getDataset();let o=!1;e._dataCheck();const s=i._stacked;i._stacked=Pi(i.vScale,i),i.stack!==n.stack&&(o=!0,Ai(i),i.stack=n.stack),e._resyncElements(t),(o||s!==i._stacked)&&Oi(e,i._parsed)}configure(){const t=this,e=t.chart.config,i=e.datasetScopeKeys(t._type),n=e.getOptionScopes(t.getDataset(),i,!0);t.options=e.createResolver(n,t.getContext()),t._parsing=t.options.parsing}parse(t,e){const i=this,{_cachedMeta:n,_data:o}=i,{iScale:s,_stacked:a}=n,r=s.axis;let l,c,h,d=0===t&&e===o.length||n._sorted,u=t>0&&n._parsed[t-1];if(!1===i._parsing)n._parsed=o,n._sorted=!0,h=o;else{h=Y(o[t])?i.parseArrayData(n,o,t,e):U(o[t])?i.parseObjectData(n,o,t,e):i.parsePrimitiveData(n,o,t,e);const s=()=>null===c[r]||u&&c[r]<u[r];for(l=0;l<e;++l)n._parsed[l+t]=c=h[l],d&&(s()&&(d=!1),u=c);n._sorted=d}a&&Oi(i,h)}parsePrimitiveData(t,e,i,n){const{iScale:o,vScale:s}=t,a=o.axis,r=s.axis,l=o.getLabels(),c=o===s,h=new Array(n);let d,u,f;for(d=0,u=n;d<u;++d)f=d+i,h[d]={[a]:c||o.parse(l[f],f),[r]:s.parse(e[f],f)};return h}parseArrayData(t,e,i,n){const{xScale:o,yScale:s}=t,a=new Array(n);let r,l,c,h;for(r=0,l=n;r<l;++r)c=r+i,h=e[c],a[r]={x:o.parse(h[0],c),y:s.parse(h[1],c)};return a}parseObjectData(t,e,i,n){const{xScale:o,yScale:s}=t,{xAxisKey:a="x",yAxisKey:r="y"}=this._parsing,l=new Array(n);let c,h,d,u;for(c=0,h=n;c<h;++c)d=c+i,u=e[d],l[c]={x:o.parse(lt(u,a),d),y:s.parse(lt(u,r),d)};return l}getParsed(t){return this._cachedMeta._parsed[t]}getDataElement(t){return this._cachedMeta.data[t]}applyStack(t,e,i){const n=this.chart,o=this._cachedMeta,s=e[t.axis];return Si({keys:ki(n,!0),values:e._stacks[t.axis]},s,o.index,{mode:i})}updateRangeFromParsed(t,e,i,n){const o=i[e.axis];let s=null===o?NaN:o;const a=n&&i._stacks[e.axis];n&&a&&(n.values=a,t.min=Math.min(t.min,s),t.max=Math.max(t.max,s),s=Si(n,o,this._cachedMeta.index,{all:!0})),t.min=Math.min(t.min,s),t.max=Math.max(t.max,s)}getMinMax(t,e){const i=this,n=i._cachedMeta,o=n._parsed,s=n._sorted&&t===n.iScale,a=o.length,r=i._getOtherScale(t),l=e&&n._stacked&&{keys:ki(i.chart,!0),values:null},c={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:h,max:d}=function(t){const{min:e,max:i,minDefined:n,maxDefined:o}=t.getUserBounds();return{min:n?e:Number.NEGATIVE_INFINITY,max:o?i:Number.POSITIVE_INFINITY}}(r);let u,f,g,p;function m(){return g=o[u],f=g[t.axis],p=g[r.axis],!X(f)||h>p||d<p}for(u=0;u<a&&(m()||(i.updateRangeFromParsed(c,t,g,l),!s));++u);if(s)for(u=a-1;u>=0;--u)if(!m()){i.updateRangeFromParsed(c,t,g,l);break}return c}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let n,o,s;for(n=0,o=e.length;n<o;++n)s=e[n][t.axis],X(s)&&i.push(s);return i}getMaxOverflow(){return!1}getLabelAndValue(t){const e=this._cachedMeta,i=e.iScale,n=e.vScale,o=this.getParsed(t);return{label:i?""+i.getLabelForValue(o[i.axis]):"",value:n?""+n.getLabelForValue(o[n.axis]):""}}_update(t){const e=this,i=e._cachedMeta;e.configure(),e._cachedDataOpts={},e.update(t||"default"),i._clip=function(t){let e,i,n,o;return U(t)?(e=t.top,i=t.right,n=t.bottom,o=t.left):e=i=n=o=t,{top:e,right:i,bottom:n,left:o,disabled:!1===t}}(K(e.options.clip,function(t,e,i){if(!1===i)return!1;const n=Mi(t,i),o=Mi(e,i);return{top:o.end,right:n.end,bottom:o.start,left:n.start}}(i.xScale,i.yScale,e.getMaxOverflow())))}update(t){}draw(){const t=this,e=t._ctx,i=t.chart,n=t._cachedMeta,o=n.data||[],s=i.chartArea,a=[],r=t._drawStart||0,l=t._drawCount||o.length-r;let c;for(n.dataset&&n.dataset.draw(e,s,r,l),c=r;c<r+l;++c){const t=o[c];t.active?a.push(t):t.draw(e,s)}for(c=0;c<a.length;++c)a[c].draw(e,s)}getStyle(t,e){const i=e?"active":"default";return void 0===t&&this._cachedMeta.dataset?this.resolveDatasetElementOptions(i):this.resolveDataElementOptions(t||0,i)}getContext(t,e,i){const n=this,o=n.getDataset();let s;if(t>=0&&t<n._cachedMeta.data.length){const e=n._cachedMeta.data[t];s=e.$context||(e.$context=function(t,e,i){return Object.assign(Object.create(t),{active:!1,dataIndex:e,parsed:void 0,raw:void 0,element:i,index:e,mode:"default",type:"data"})}(n.getContext(),t,e)),s.parsed=n.getParsed(t),s.raw=o.data[t],s.index=s.dataIndex=t}else s=n.$context||(n.$context=function(t,e){return Object.assign(Object.create(t),{active:!1,dataset:void 0,datasetIndex:e,index:e,mode:"default",type:"dataset"})}(n.chart.getContext(),n.index)),s.dataset=o,s.index=s.datasetIndex=n.index;return s.active=!!e,s.mode=i,s}resolveDatasetElementOptions(t){return this._resolveElementOptions(this.datasetElementType.id,t)}resolveDataElementOptions(t,e){return this._resolveElementOptions(this.dataElementType.id,e,t)}_resolveElementOptions(t,e="default",i){const n=this,o="active"===e,s=n._cachedDataOpts,a=t+"-"+e,r=s[a],l=n.enableOptionSharing&&ht(i);if(r)return Ri(r,l);const c=n.chart.config,h=c.datasetElementScopeKeys(n._type,t),d=o?[`${t}Hover`,"hover",t,""]:[t,""],u=c.getOptionScopes(n.getDataset(),h),f=Object.keys(xt.elements[t]),g=c.resolveNamedOptions(u,f,(()=>n.getContext(i,o)),d);return g.$shared&&(g.$shared=l,s[a]=Object.freeze(Ri(g,l))),g}_resolveAnimations(t,e,i){const n=this,o=n.chart,s=n._cachedDataOpts,a=`animation-${e}`,r=s[a];if(r)return r;let l;if(!1!==o.options.animation){const o=n.chart.config,s=o.datasetAnimationScopeKeys(n._type,e),a=o.getOptionScopes(n.getDataset(),s);l=o.createResolver(a,n.getContext(t,i,e))}const c=new wi(o,l&&l.animations);return l&&l._cacheable&&(s[a]=Object.freeze(c)),c}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Li(t)||this.chart._animationsDisabled}updateElement(t,e,i,n){Li(n)?Object.assign(t,i):this._resolveAnimations(e,n).update(t,i)}updateSharedOptions(t,e,i){t&&!Li(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,n){t.active=n;const o=this.getStyle(e,n);this._resolveAnimations(e,i,n).update(t,{options:!n&&this.getSharedOptions(o)||o})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this,i=e._data,n=e._cachedMeta.data;for(const[t,i,n]of e._syncList)e[t](i,n);e._syncList=[];const o=n.length,s=i.length,a=Math.min(s,o);a&&e.parse(0,a),s>o?e._insertElements(o,s-o,t):s<o&&e._removeElements(s,o-s)}_insertElements(t,e,i=!0){const n=this,o=n._cachedMeta,s=o.data,a=t+e;let r;const l=t=>{for(t.length+=e,r=t.length-1;r>=a;r--)t[r]=t[r-e]};for(l(s),r=t;r<a;++r)s[r]=new n.dataElementType;n._parsing&&l(o._parsed),n.parse(t,e),i&&n.updateElements(s,t,e,"reset")}updateElements(t,e,i,n){}_removeElements(t,e){const i=this._cachedMeta;if(this._parsing){const n=i._parsed.splice(t,e);i._stacked&&Ai(i,n)}i.data.splice(t,e)}_onDataPush(){const t=arguments.length;this._syncList.push(["_insertElements",this.getDataset().data.length-t,t])}_onDataPop(){this._syncList.push(["_removeElements",this._cachedMeta.data.length-1,1])}_onDataShift(){this._syncList.push(["_removeElements",0,1])}_onDataSplice(t,e){this._syncList.push(["_removeElements",t,e]),this._syncList.push(["_insertElements",t,arguments.length-2])}_onDataUnshift(){this._syncList.push(["_insertElements",0,arguments.length])}}Ei.defaults={},Ei.prototype.datasetElementType=null,Ei.prototype.dataElementType=null;class zi{constructor(){this.x=void 0,this.y=void 0,this.active=!1,this.options=void 0,this.$animations=void 0}tooltipPosition(t){const{x:e,y:i}=this.getProps(["x","y"],t);return{x:e,y:i}}hasValue(){return Tt(this.x)&&Tt(this.y)}getProps(t,e){const i=this,n=this.$animations;if(!e||!n)return i;const o={};return t.forEach((t=>{o[t]=n[t]&&n[t].active()?n[t]._to:i[t]})),o}}zi.defaults={},zi.defaultRoutes=void 0;const Ii=new Map;function Fi(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let n=Ii.get(i);return n||(n=new Intl.NumberFormat(t,e),Ii.set(i,n)),n}(e,i).format(t)}const Bi={values:t=>Y(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const n=this.chart.options.locale;let o,s=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(o="scientific"),s=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=Pt(Math.abs(s)),r=Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:o,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),Fi(t,n,l)},logarithmic(t,e,i){if(0===t)return"0";const n=t/Math.pow(10,Math.floor(Pt(t)));return 1===n||2===n||5===n?Bi.numeric.call(this,t,e,i):""}};var Vi={formatters:Bi};function Wi(t,e){const i=t.options.ticks,n=i.maxTicksLimit||function(t){const e=t.options.offset,i=t._tickSize(),n=t._length/i+(e?0:1),o=t._maxLength/i;return Math.floor(Math.min(n,o))}(t),o=i.major.enabled?function(t){const e=[];let i,n;for(i=0,n=t.length;i<n;i++)t[i].major&&e.push(i);return e}(e):[],s=o.length,a=o[0],r=o[s-1],l=[];if(s>n)return function(t,e,i,n){let o,s=0,a=i[0];for(n=Math.ceil(n),o=0;o<t.length;o++)o===a&&(e.push(t[o]),s++,a=i[s*n])}(e,l,o,s/n),l;const c=function(t,e,i){const n=function(t){const e=t.length;let i,n;if(e<2)return!1;for(n=t[0],i=1;i<e;++i)if(t[i]-t[i-1]!==n)return!1;return n}(t),o=e.length/i;if(!n)return Math.max(o,1);const s=Ot(n);for(let t=0,e=s.length-1;t<e;t++){const e=s[t];if(e>o)return e}return Math.max(o,1)}(o,e,n);if(s>0){let t,i;const n=s>1?Math.round((r-a)/(s-1)):null;for(Ni(e,l,c,$(n)?0:a-n,a),t=0,i=s-1;t<i;t++)Ni(e,l,c,o[t],o[t+1]);return Ni(e,l,c,r,$(n)?e.length:r+n),l}return Ni(e,l,c),l}function Ni(t,e,i,n,o){const s=K(n,0),a=Math.min(K(o,t.length),t.length);let r,l,c,h=0;for(i=Math.ceil(i),o&&(r=o-n,i=r/Math.floor(r/i)),c=s;c<0;)h++,c=Math.round(s+h*i);for(l=Math.max(s,0);l<a;l++)l===c&&(e.push(t[l]),h++,c=Math.round(s+h*i))}xt.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",grace:0,grid:{display:!0,lineWidth:1,drawBorder:!0,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1,borderDash:[],borderDashOffset:0,borderWidth:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:Vi.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),xt.route("scale.ticks","color","","color"),xt.route("scale.grid","color","","borderColor"),xt.route("scale.grid","borderColor","","borderColor"),xt.route("scale.title","color","","color"),xt.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t}),xt.describe("scales",{_fallback:"scale"}),xt.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t});const Hi=(t,e,i)=>"top"===e||"left"===e?t[e]+i:t[e]-i;function ji(t,e){const i=[],n=t.length/e,o=t.length;let s=0;for(;s<o;s+=n)i.push(t[Math.floor(s)]);return i}function $i(t,e,i){const n=t.ticks.length,o=Math.min(e,n-1),s=t._startPixel,a=t._endPixel,r=1e-6;let l,c=t.getPixelForTick(o);if(!(i&&(l=1===n?Math.max(c-s,a-c):0===e?(t.getPixelForTick(1)-c)/2:(c-t.getPixelForTick(o-1))/2,c+=o<e?l:-l,c<s-r||c>a+r)))return c}function Yi(t){return t.drawTicks?t.tickLength:0}function Ui(t,e){if(!t.display)return 0;const i=Be(t.font,e),n=Fe(t.padding);return(Y(t.text)?t.text.length:1)*i.lineHeight+n.height}function Xi(t,e,i){let o=n(t);return(i&&"right"!==e||!i&&"right"===e)&&(o=(t=>"left"===t?"right":"right"===t?"left":t)(o)),o}class qi extends zi{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){const e=this;e.options=t.setContext(e.getContext()),e.axis=t.axis,e._userMin=e.parse(t.min),e._userMax=e.parse(t.max),e._suggestedMin=e.parse(t.suggestedMin),e._suggestedMax=e.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:n}=this;return t=q(t,Number.POSITIVE_INFINITY),e=q(e,Number.NEGATIVE_INFINITY),i=q(i,Number.POSITIVE_INFINITY),n=q(n,Number.NEGATIVE_INFINITY),{min:q(t,i),max:q(e,n),minDefined:X(t),maxDefined:X(e)}}getMinMax(t){const e=this;let i,{min:n,max:o,minDefined:s,maxDefined:a}=e.getUserBounds();if(s&&a)return{min:n,max:o};const r=e.getMatchingVisibleMetas();for(let l=0,c=r.length;l<c;++l)i=r[l].controller.getMinMax(e,t),s||(n=Math.min(n,i.min)),a||(o=Math.max(o,i.max));return{min:q(n,q(o,n)),max:q(o,q(n,o))}}getPadding(){const t=this;return{left:t.paddingLeft||0,top:t.paddingTop||0,right:t.paddingRight||0,bottom:t.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){Q(this.options.beforeUpdate,[this])}update(t,e,i){const n=this,o=n.options.ticks,s=o.sampleSize;n.beforeUpdate(),n.maxWidth=t,n.maxHeight=e,n._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),n.ticks=null,n._labelSizes=null,n._gridLineItems=null,n._labelItems=null,n.beforeSetDimensions(),n.setDimensions(),n.afterSetDimensions(),n._maxLength=n.isHorizontal()?n.width+i.left+i.right:n.height+i.top+i.bottom,n._dataLimitsCached||(n.beforeDataLimits(),n.determineDataLimits(),n.afterDataLimits(),n._range=We(n,n.options.grace),n._dataLimitsCached=!0),n.beforeBuildTicks(),n.ticks=n.buildTicks()||[],n.afterBuildTicks();const a=s<n.ticks.length;n._convertTicksToLabels(a?ji(n.ticks,s):n.ticks),n.configure(),n.beforeCalculateLabelRotation(),n.calculateLabelRotation(),n.afterCalculateLabelRotation(),o.display&&(o.autoSkip||"auto"===o.source)&&(n.ticks=Wi(n,n.ticks),n._labelSizes=null),a&&n._convertTicksToLabels(n.ticks),n.beforeFit(),n.fit(),n.afterFit(),n.afterUpdate()}configure(){const t=this;let e,i,n=t.options.reverse;t.isHorizontal()?(e=t.left,i=t.right):(e=t.top,i=t.bottom,n=!n),t._startPixel=e,t._endPixel=i,t._reversePixels=n,t._length=i-e,t._alignToPixels=t.options.alignToPixels}afterUpdate(){Q(this.options.afterUpdate,[this])}beforeSetDimensions(){Q(this.options.beforeSetDimensions,[this])}setDimensions(){const t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0}afterSetDimensions(){Q(this.options.afterSetDimensions,[this])}_callHooks(t){const e=this;e.chart.notifyPlugins(t,e.getContext()),Q(e.options[t],[e])}beforeDataLimits(){this._callHooks("beforeDataLimits")}determineDataLimits(){}afterDataLimits(){this._callHooks("afterDataLimits")}beforeBuildTicks(){this._callHooks("beforeBuildTicks")}buildTicks(){return[]}afterBuildTicks(){this._callHooks("afterBuildTicks")}beforeTickToLabelConversion(){Q(this.options.beforeTickToLabelConversion,[this])}generateTickLabels(t){const e=this,i=e.options.ticks;let n,o,s;for(n=0,o=t.length;n<o;n++)s=t[n],s.label=Q(i.callback,[s.value,n,t],e)}afterTickToLabelConversion(){Q(this.options.afterTickToLabelConversion,[this])}beforeCalculateLabelRotation(){Q(this.options.beforeCalculateLabelRotation,[this])}calculateLabelRotation(){const t=this,e=t.options,i=e.ticks,n=t.ticks.length,o=i.minRotation||0,s=i.maxRotation;let a,r,l,c=o;if(!t._isVisible()||!i.display||o>=s||n<=1||!t.isHorizontal())return void(t.labelRotation=o);const h=t._getLabelSizes(),d=h.widest.width,u=h.highest.height,f=Ht(t.chart.width-d,0,t.maxWidth);a=e.offset?t.maxWidth/n:f/(n-1),d+6>a&&(a=f/(n-(e.offset?.5:1)),r=t.maxHeight-Yi(e.grid)-i.padding-Ui(e.title,t.chart.options.font),l=Math.sqrt(d*d+u*u),c=zt(Math.min(Math.asin(Math.min((h.highest.height+6)/a,1)),Math.asin(Math.min(r/l,1))-Math.asin(u/l))),c=Math.max(o,Math.min(s,c))),t.labelRotation=c}afterCalculateLabelRotation(){Q(this.options.afterCalculateLabelRotation,[this])}beforeFit(){Q(this.options.beforeFit,[this])}fit(){const t=this,e={width:0,height:0},{chart:i,options:{ticks:n,title:o,grid:s}}=t,a=t._isVisible(),r=t.isHorizontal();if(a){const a=Ui(o,i.options.font);if(r?(e.width=t.maxWidth,e.height=Yi(s)+a):(e.height=t.maxHeight,e.width=Yi(s)+a),n.display&&t.ticks.length){const{first:i,last:o,widest:s,highest:a}=t._getLabelSizes(),l=2*n.padding,c=Et(t.labelRotation),h=Math.cos(c),d=Math.sin(c);if(r){const i=n.mirror?0:d*s.width+h*a.height;e.height=Math.min(t.maxHeight,e.height+i+l)}else{const i=n.mirror?0:h*s.width+d*a.height;e.width=Math.min(t.maxWidth,e.width+i+l)}t._calculatePadding(i,o,d,h)}}t._handleMargins(),r?(t.width=t._length=i.width-t._margins.left-t._margins.right,t.height=e.height):(t.width=e.width,t.height=t._length=i.height-t._margins.top-t._margins.bottom)}_calculatePadding(t,e,i,n){const o=this,{ticks:{align:s,padding:a},position:r}=o.options,l=0!==o.labelRotation,c="top"!==r&&"x"===o.axis;if(o.isHorizontal()){const r=o.getPixelForTick(0)-o.left,h=o.right-o.getPixelForTick(o.ticks.length-1);let d=0,u=0;l?c?(d=n*t.width,u=i*e.height):(d=i*t.height,u=n*e.width):"start"===s?u=e.width:"end"===s?d=t.width:(d=t.width/2,u=e.width/2),o.paddingLeft=Math.max((d-r+a)*o.width/(o.width-r),0),o.paddingRight=Math.max((u-h+a)*o.width/(o.width-h),0)}else{let i=e.height/2,n=t.height/2;"start"===s?(i=0,n=t.height):"end"===s&&(i=e.height,n=0),o.paddingTop=i+a,o.paddingBottom=n+a}}_handleMargins(){const t=this;t._margins&&(t._margins.left=Math.max(t.paddingLeft,t._margins.left),t._margins.top=Math.max(t.paddingTop,t._margins.top),t._margins.right=Math.max(t.paddingRight,t._margins.right),t._margins.bottom=Math.max(t.paddingBottom,t._margins.bottom))}afterFit(){Q(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){const e=this;let i,n;for(e.beforeTickToLabelConversion(),e.generateTickLabels(t),i=0,n=t.length;i<n;i++)$(t[i].label)&&(t.splice(i,1),n--,i--);e.afterTickToLabelConversion()}_getLabelSizes(){const t=this;let e=t._labelSizes;if(!e){const i=t.options.ticks.sampleSize;let n=t.ticks;i<n.length&&(n=ji(n,i)),t._labelSizes=e=t._computeLabelSizes(n,n.length)}return e}_computeLabelSizes(t,e){const{ctx:i,_longestTextCache:n}=this,o=[],s=[];let a,r,l,c,h,d,u,f,g,p,m,x=0,b=0;for(a=0;a<e;++a){if(c=t[a].label,h=this._resolveTickFontOptions(a),i.font=d=h.string,u=n[d]=n[d]||{data:{},gc:[]},f=h.lineHeight,g=p=0,$(c)||Y(c)){if(Y(c))for(r=0,l=c.length;r<l;++r)m=c[r],$(m)||Y(m)||(g=Yt(i,u.data,u.gc,g,m),p+=f)}else g=Yt(i,u.data,u.gc,g,c),p=f;o.push(g),s.push(p),x=Math.max(g,x),b=Math.max(p,b)}!function(t,e){J(t,(t=>{const i=t.gc,n=i.length/2;let o;if(n>e){for(o=0;o<n;++o)delete t.data[i[o]];i.splice(0,n)}}))}(n,e);const _=o.indexOf(x),y=s.indexOf(b),v=t=>({width:o[t]||0,height:s[t]||0});return{first:v(0),last:v(e-1),widest:v(_),highest:v(y),widths:o,heights:s}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){const e=this;e._reversePixels&&(t=1-t);const i=e._startPixel+t*e._length;return jt(e._alignToPixels?Xt(e.chart,i,0):i)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this,i=e.ticks||[];if(t>=0&&t<i.length){const n=i[t];return n.$context||(n.$context=function(t,e,i){return Object.assign(Object.create(t),{tick:i,index:e,type:"tick"})}(e.getContext(),t,n))}return e.$context||(e.$context=(n=e.chart.getContext(),o=e,Object.assign(Object.create(n),{scale:o,type:"scale"})));var n,o}_tickSize(){const t=this,e=t.options.ticks,i=Et(t.labelRotation),n=Math.abs(Math.cos(i)),o=Math.abs(Math.sin(i)),s=t._getLabelSizes(),a=e.autoSkipPadding||0,r=s?s.widest.width+a:0,l=s?s.highest.height+a:0;return t.isHorizontal()?l*n>r*o?r/n:l/o:l*o<r*n?l/n:r/o}_isVisible(){const t=this.options.display;return"auto"!==t?!!t:this.getMatchingVisibleMetas().length>0}_computeGridLineItems(t){const e=this,i=e.axis,n=e.chart,o=e.options,{grid:s,position:a}=o,r=s.offset,l=e.isHorizontal(),c=e.ticks.length+(r?1:0),h=Yi(s),d=[],u=s.setContext(e.getContext()),f=u.drawBorder?u.borderWidth:0,g=f/2,p=function(t){return Xt(n,t,f)};let m,x,b,_,y,v,w,M,k,S,P,D;if("top"===a)m=p(e.bottom),v=e.bottom-h,M=m-g,S=p(t.top)+g,D=t.bottom;else if("bottom"===a)m=p(e.top),S=t.top,D=p(t.bottom)-g,v=m+g,M=e.top+h;else if("left"===a)m=p(e.right),y=e.right-h,w=m-g,k=p(t.left)+g,P=t.right;else if("right"===a)m=p(e.left),k=t.left,P=p(t.right)-g,y=m+g,w=e.left+h;else if("x"===i){if("center"===a)m=p((t.top+t.bottom)/2+.5);else if(U(a)){const t=Object.keys(a)[0],i=a[t];m=p(e.chart.scales[t].getPixelForValue(i))}S=t.top,D=t.bottom,v=m+g,M=v+h}else if("y"===i){if("center"===a)m=p((t.left+t.right)/2);else if(U(a)){const t=Object.keys(a)[0],i=a[t];m=p(e.chart.scales[t].getPixelForValue(i))}y=m-g,w=y-h,k=t.left,P=t.right}const C=K(o.ticks.maxTicksLimit,c),O=Math.max(1,Math.ceil(c/C));for(x=0;x<c;x+=O){const t=s.setContext(e.getContext(x)),i=t.lineWidth,o=t.color,a=s.borderDash||[],c=t.borderDashOffset,h=t.tickWidth,u=t.tickColor,f=t.tickBorderDash||[],g=t.tickBorderDashOffset;b=$i(e,x,r),void 0!==b&&(_=Xt(n,b,i),l?y=w=k=P=_:v=M=S=D=_,d.push({tx1:y,ty1:v,tx2:w,ty2:M,x1:k,y1:S,x2:P,y2:D,width:i,color:o,borderDash:a,borderDashOffset:c,tickWidth:h,tickColor:u,tickBorderDash:f,tickBorderDashOffset:g}))}return e._ticksLength=c,e._borderValue=m,d}_computeLabelItems(t){const e=this,i=e.axis,n=e.options,{position:o,ticks:s}=n,a=e.isHorizontal(),r=e.ticks,{align:l,crossAlign:c,padding:h,mirror:d}=s,u=Yi(n.grid),f=u+h,g=d?-h:f,p=-Et(e.labelRotation),m=[];let x,b,_,y,v,w,M,k,S,P,D,C,O="middle";if("top"===o)w=e.bottom-g,M=e._getXAxisLabelAlignment();else if("bottom"===o)w=e.top+g,M=e._getXAxisLabelAlignment();else if("left"===o){const t=e._getYAxisLabelAlignment(u);M=t.textAlign,v=t.x}else if("right"===o){const t=e._getYAxisLabelAlignment(u);M=t.textAlign,v=t.x}else if("x"===i){if("center"===o)w=(t.top+t.bottom)/2+f;else if(U(o)){const t=Object.keys(o)[0],i=o[t];w=e.chart.scales[t].getPixelForValue(i)+f}M=e._getXAxisLabelAlignment()}else if("y"===i){if("center"===o)v=(t.left+t.right)/2-f;else if(U(o)){const t=Object.keys(o)[0],i=o[t];v=e.chart.scales[t].getPixelForValue(i)}M=e._getYAxisLabelAlignment(u).textAlign}"y"===i&&("start"===l?O="top":"end"===l&&(O="bottom"));const T=e._getLabelSizes();for(x=0,b=r.length;x<b;++x){_=r[x],y=_.label;const t=s.setContext(e.getContext(x));k=e.getPixelForTick(x)+s.labelOffset,S=e._resolveTickFontOptions(x),P=S.lineHeight,D=Y(y)?y.length:1;const i=D/2,n=t.color,l=t.textStrokeColor,h=t.textStrokeWidth;let u;if(a?(v=k,C="top"===o?"near"===c||0!==p?-D*P+P/2:"center"===c?-T.highest.height/2-i*P+P:-T.highest.height+P/2:"near"===c||0!==p?P/2:"center"===c?T.highest.height/2-i*P:T.highest.height-D*P,d&&(C*=-1)):(w=k,C=(1-D)*P/2),t.showLabelBackdrop){const e=Fe(t.backdropPadding),i=T.heights[x],n=T.widths[x];let o=w+C-e.top,s=v-e.left;switch(O){case"middle":o-=i/2;break;case"bottom":o-=i}switch(M){case"center":s-=n/2;break;case"right":s-=n}u={left:s,top:o,width:n+e.width,height:i+e.height,color:t.backdropColor}}m.push({rotation:p,label:y,font:S,color:n,strokeColor:l,strokeWidth:h,textOffset:C,textAlign:M,textBaseline:O,translation:[v,w],backdrop:u})}return m}_getXAxisLabelAlignment(){const{position:t,ticks:e}=this.options;if(-Et(this.labelRotation))return"top"===t?"left":"right";let i="center";return"start"===e.align?i="left":"end"===e.align&&(i="right"),i}_getYAxisLabelAlignment(t){const e=this,{position:i,ticks:{crossAlign:n,mirror:o,padding:s}}=e.options,a=t+s,r=e._getLabelSizes().widest.width;let l,c;return"left"===i?o?(l="left",c=e.right+s):(c=e.right-a,"near"===n?l="right":"center"===n?(l="center",c-=r/2):(l="left",c=e.left)):"right"===i?o?(l="right",c=e.left+s):(c=e.left+a,"near"===n?l="left":"center"===n?(l="center",c+=r/2):(l="right",c=e.right)):l="right",{textAlign:l,x:c}}_computeLabelArea(){const t=this;if(t.options.ticks.mirror)return;const e=t.chart,i=t.options.position;return"left"===i||"right"===i?{top:0,left:t.left,bottom:e.height,right:t.right}:"top"===i||"bottom"===i?{top:t.top,left:0,bottom:t.bottom,right:e.width}:void 0}drawBackground(){const{ctx:t,options:{backgroundColor:e},left:i,top:n,width:o,height:s}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(i,n,o,s),t.restore())}getLineWidthForValue(t){const e=this,i=e.options.grid;if(!e._isVisible()||!i.display)return 0;const n=e.ticks.findIndex((e=>e.value===t));if(n>=0){return i.setContext(e.getContext(n)).lineWidth}return 0}drawGrid(t){const e=this,i=e.options.grid,n=e.ctx,o=e._gridLineItems||(e._gridLineItems=e._computeGridLineItems(t));let s,a;const r=(t,e,i)=>{i.width&&i.color&&(n.save(),n.lineWidth=i.width,n.strokeStyle=i.color,n.setLineDash(i.borderDash||[]),n.lineDashOffset=i.borderDashOffset,n.beginPath(),n.moveTo(t.x,t.y),n.lineTo(e.x,e.y),n.stroke(),n.restore())};if(i.display)for(s=0,a=o.length;s<a;++s){const t=o[s];i.drawOnChartArea&&r({x:t.x1,y:t.y1},{x:t.x2,y:t.y2},t),i.drawTicks&&r({x:t.tx1,y:t.ty1},{x:t.tx2,y:t.ty2},{color:t.tickColor,width:t.tickWidth,borderDash:t.tickBorderDash,borderDashOffset:t.tickBorderDashOffset})}}drawBorder(){const t=this,{chart:e,ctx:i,options:{grid:n}}=t,o=n.setContext(t.getContext()),s=n.drawBorder?o.borderWidth:0;if(!s)return;const a=n.setContext(t.getContext(0)).lineWidth,r=t._borderValue;let l,c,h,d;t.isHorizontal()?(l=Xt(e,t.left,s)-s/2,c=Xt(e,t.right,a)+a/2,h=d=r):(h=Xt(e,t.top,s)-s/2,d=Xt(e,t.bottom,a)+a/2,l=c=r),i.save(),i.lineWidth=o.borderWidth,i.strokeStyle=o.borderColor,i.beginPath(),i.moveTo(l,h),i.lineTo(c,d),i.stroke(),i.restore()}drawLabels(t){const e=this;if(!e.options.ticks.display)return;const i=e.ctx,n=e._computeLabelArea();n&&Zt(i,n);const o=e._labelItems||(e._labelItems=e._computeLabelItems(t));let s,a;for(s=0,a=o.length;s<a;++s){const t=o[s],e=t.font,n=t.label;t.backdrop&&(i.fillStyle=t.backdrop.color,i.fillRect(t.backdrop.left,t.backdrop.top,t.backdrop.width,t.backdrop.height)),ee(i,n,0,t.textOffset,e,t)}n&&Qt(i)}drawTitle(){const{ctx:t,options:{position:e,title:i,reverse:n}}=this;if(!i.display)return;const s=Be(i.font),a=Fe(i.padding),r=i.align;let l=s.lineHeight/2;"bottom"===e?(l+=a.bottom,Y(i.text)&&(l+=s.lineHeight*(i.text.length-1))):l+=a.top;const{titleX:c,titleY:h,maxWidth:d,rotation:u}=function(t,e,i,n){const{top:s,left:a,bottom:r,right:l}=t;let c,h,d,u=0;return t.isHorizontal()?(h=o(n,a,l),d=Hi(t,i,e),c=l-a):(h=Hi(t,i,e),d=o(n,r,s),u="left"===i?-Mt:Mt),{titleX:h,titleY:d,maxWidth:c,rotation:u}}(this,l,e,r);ee(t,i.text,0,0,s,{color:i.color,maxWidth:d,rotation:u,textAlign:Xi(r,e,n),textBaseline:"middle",translation:[c,h]})}draw(t){const e=this;e._isVisible()&&(e.drawBackground(),e.drawGrid(t),e.drawBorder(),e.drawTitle(),e.drawLabels(t))}_layers(){const t=this,e=t.options,i=e.ticks&&e.ticks.z||0,n=e.grid&&e.grid.z||0;return t._isVisible()&&t.draw===qi.prototype.draw?[{z:n,draw(e){t.drawBackground(),t.drawGrid(e),t.drawTitle()}},{z:n+1,draw(){t.drawBorder()}},{z:i,draw(e){t.drawLabels(e)}}]:[{z:i,draw(e){t.draw(e)}}]}getMatchingVisibleMetas(t){const e=this,i=e.chart.getSortedVisibleDatasetMetas(),n=e.axis+"AxisID",o=[];let s,a;for(s=0,a=i.length;s<a;++s){const a=i[s];a[n]!==e.id||t&&a.type!==t||o.push(a)}return o}_resolveTickFontOptions(t){return Be(this.options.ticks.setContext(this.getContext(t)).font)}_maxDigits(){const t=this,e=t._resolveTickFontOptions(0).lineHeight;return(t.isHorizontal()?t.width:t.height)/e}}function Ki(t,e=[""],i=t,n,o=(()=>t[0])){ht(n)||(n=rn("_fallback",t));const s={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:i,_fallback:n,_getTarget:o,override:o=>Ki([o,...t],e,i,n)};return new Proxy(s,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,n)=>tn(i,n,(()=>function(t,e,i,n){let o;for(const s of e)if(o=rn(Qi(s,t),i),ht(o))return Ji(t,o)?sn(i,n,t,o):o}(n,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ln(t).includes(e),ownKeys:t=>ln(t),set:(t,e,i)=>((t._storage||(t._storage=o()))[e]=i,delete t[e],delete t._keys,!0)})}function Gi(t,e,i,n){const o={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Zi(t,n),setContext:e=>Gi(t,e,i,n),override:o=>Gi(t.override(o),e,i,n)};return new Proxy(o,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>tn(t,e,(()=>function(t,e,i){const{_proxy:n,_context:o,_subProxy:s,_descriptors:a}=t;let r=n[e];dt(r)&&a.isScriptable(e)&&(r=function(t,e,i,n){const{_proxy:o,_context:s,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t),e=e(s,a||n),r.delete(t),U(e)&&(e=sn(o._scopes,o,t,e));return e}(e,r,t,i));Y(r)&&r.length&&(r=function(t,e,i,n){const{_proxy:o,_context:s,_subProxy:a,_descriptors:r}=i;if(ht(s.index)&&n(t))e=e[s.index%e.length];else if(U(e[0])){const i=e,n=o._scopes.filter((t=>t!==i));e=[];for(const l of i){const i=sn(n,o,t,l);e.push(Gi(i,s,a&&a[t],r))}}return e}(e,r,t,a.isIndexable));Ji(e,r)&&(r=Gi(r,o,s&&s[e],a));return r}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,n)=>(t[i]=n,delete e[i],!0)})}function Zi(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:n=e.indexable,_allKeys:o=e.allKeys}=t;return{allKeys:o,scriptable:i,indexable:n,isScriptable:dt(i)?i:()=>i,isIndexable:dt(n)?n:()=>n}}const Qi=(t,e)=>t?t+ct(e):e,Ji=(t,e)=>U(e)&&"adapters"!==t;function tn(t,e,i){let n=t[e];return ht(n)||(n=i(),ht(n)&&(t[e]=n)),n}function en(t,e,i){return dt(t)?t(e,i):t}const nn=(t,e)=>!0===t?e:"string"==typeof t?lt(e,t):void 0;function on(t,e,i,n){for(const o of e){const e=nn(i,o);if(e){t.add(e);const o=en(e._fallback,i,e);if(ht(o)&&o!==i&&o!==n)return o}else if(!1===e&&ht(n)&&i!==n)return null}return!1}function sn(t,e,i,n){const o=e._rootScopes,s=en(e._fallback,i,n),a=[...t,...o],r=new Set;r.add(n);let l=an(r,a,i,s||i);return null!==l&&((!ht(s)||s===i||(l=an(r,a,s,l),null!==l))&&Ki(Array.from(r),[""],o,s,(()=>function(t,e,i){const n=t._getTarget();e in n||(n[e]={});const o=n[e];if(Y(o)&&U(i))return i;return o}(e,i,n))))}function an(t,e,i,n){for(;i;)i=on(t,e,i,n);return i}function rn(t,e){for(const i of e){if(!i)continue;const e=i[t];if(ht(e))return e}}function ln(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}const cn=Number.EPSILON||1e-14,hn=(t,e)=>e<t.length&&!t[e].skip&&t[e],dn=t=>"x"===t?"y":"x";function un(t,e,i,n){const o=t.skip?e:t,s=e,a=i.skip?e:i,r=Bt(s,o),l=Bt(a,s);let c=r/(r+l),h=l/(r+l);c=isNaN(c)?0:c,h=isNaN(h)?0:h;const d=n*c,u=n*h;return{previous:{x:s.x-d*(a.x-o.x),y:s.y-d*(a.y-o.y)},next:{x:s.x+u*(a.x-o.x),y:s.y+u*(a.y-o.y)}}}function fn(t,e="x"){const i=dn(e),n=t.length,o=Array(n).fill(0),s=Array(n);let a,r,l,c=hn(t,0);for(a=0;a<n;++a)if(r=l,l=c,c=hn(t,a+1),l){if(c){const t=c[e]-l[e];o[a]=0!==t?(c[i]-l[i])/t:0}s[a]=r?c?Dt(o[a-1])!==Dt(o[a])?0:(o[a-1]+o[a])/2:o[a-1]:o[a]}!function(t,e,i){const n=t.length;let o,s,a,r,l,c=hn(t,0);for(let h=0;h<n-1;++h)l=c,c=hn(t,h+1),l&&c&&(At(e[h],0,cn)?i[h]=i[h+1]=0:(o=i[h]/e[h],s=i[h+1]/e[h],r=Math.pow(o,2)+Math.pow(s,2),r<=9||(a=3/Math.sqrt(r),i[h]=o*a*e[h],i[h+1]=s*a*e[h])))}(t,o,s),function(t,e,i="x"){const n=dn(i),o=t.length;let s,a,r,l=hn(t,0);for(let c=0;c<o;++c){if(a=r,r=l,l=hn(t,c+1),!r)continue;const o=r[i],h=r[n];a&&(s=(o-a[i])/3,r[`cp1${i}`]=o-s,r[`cp1${n}`]=h-s*e[c]),l&&(s=(l[i]-o)/3,r[`cp2${i}`]=o+s,r[`cp2${n}`]=h+s*e[c])}}(t,s,e)}function gn(t,e,i){return Math.max(Math.min(t,i),e)}function pn(t,e,i,n,o){let s,a,r,l;if(e.spanGaps&&(t=t.filter((t=>!t.skip))),"monotone"===e.cubicInterpolationMode)fn(t,o);else{let i=n?t[t.length-1]:t[0];for(s=0,a=t.length;s<a;++s)r=t[s],l=un(i,r,t[Math.min(s+1,a-(n?0:1))%a],e.tension),r.cp1x=l.previous.x,r.cp1y=l.previous.y,r.cp2x=l.next.x,r.cp2y=l.next.y,i=r}e.capBezierPoints&&function(t,e){let i,n,o,s,a,r=Gt(t[0],e);for(i=0,n=t.length;i<n;++i)a=s,s=r,r=i<n-1&&Gt(t[i+1],e),s&&(o=t[i],a&&(o.cp1x=gn(o.cp1x,e.left,e.right),o.cp1y=gn(o.cp1y,e.top,e.bottom)),r&&(o.cp2x=gn(o.cp2x,e.left,e.right),o.cp2y=gn(o.cp2y,e.top,e.bottom)))}(t,i)}function mn(t,e,i,n){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function xn(t,e,i,n){return{x:t.x+i*(e.x-t.x),y:"middle"===n?i<.5?t.y:e.y:"after"===n?i<1?t.y:e.y:i>0?e.y:t.y}}function bn(t,e,i,n){const o={x:t.cp2x,y:t.cp2y},s={x:e.cp1x,y:e.cp1y},a=mn(t,o,i),r=mn(o,s,i),l=mn(s,e,i),c=mn(a,r,i),h=mn(r,l,i);return mn(c,h,i)}function _n(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function yn(t,e){let i,n;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,n=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=n)}function vn(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function wn(t){return"angle"===t?{between:Nt,compare:Vt,normalize:Wt}:{between:(t,e,i)=>t>=Math.min(e,i)&&t<=Math.max(i,e),compare:(t,e)=>t-e,normalize:t=>t}}function Mn({start:t,end:e,count:i,loop:n,style:o}){return{start:t%i,end:e%i,loop:n&&(e-t+1)%i==0,style:o}}function kn(t,e,i){if(!i)return[t];const{property:n,start:o,end:s}=i,a=e.length,{compare:r,between:l,normalize:c}=wn(n),{start:h,end:d,loop:u,style:f}=function(t,e,i){const{property:n,start:o,end:s}=i,{between:a,normalize:r}=wn(n),l=e.length;let c,h,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,c=0,h=l;c<h&&a(r(e[d%l][n]),o,s);++c)d--,u--;d%=l,u%=l}return u<d&&(u+=l),{start:d,end:u,loop:f,style:t.style}}(t,e,i),g=[];let p,m,x,b=!1,_=null;const y=()=>b||l(o,x,p)&&0!==r(o,x),v=()=>!b||0===r(s,p)||l(s,x,p);for(let t=h,i=h;t<=d;++t)m=e[t%a],m.skip||(p=c(m[n]),p!==x&&(b=l(p,o,s),null===_&&y()&&(_=0===r(p,o)?t:i),null!==_&&v()&&(g.push(Mn({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,x=p));return null!==_&&g.push(Mn({start:_,end:d,loop:u,count:a,style:f})),g}function Sn(t,e){const i=[],n=t.segments;for(let o=0;o<n.length;o++){const s=kn(n[o],t.points,e);s.length&&i.push(...s)}return i}function Pn(t,e){const i=t.points,n=t.options.spanGaps,o=i.length;if(!o)return[];const s=!!t._loop,{start:a,end:r}=function(t,e,i,n){let o=0,s=e-1;if(i&&!n)for(;o<e&&!t[o].skip;)o++;for(;o<e&&t[o].skip;)o++;for(o%=e,i&&(s+=o);s>o&&t[s%e].skip;)s--;return s%=e,{start:o,end:s}}(i,o,s,n);if(!0===n)return Dn([{start:a,end:r,loop:s}],i,e);return Dn(function(t,e,i,n){const o=t.length,s=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%o];i.skip||i.stop?l.skip||(n=!1,s.push({start:e%o,end:(a-1)%o,loop:n}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&s.push({start:e%o,end:r%o,loop:n}),s}(i,a,r<a?r+o:r,!!t._fullLoop&&0===a&&r===o-1),i,e)}function Dn(t,e,i){return i&&i.setContext&&e?function(t,e,i){const n=e.length,o=[];let s=t[0].start,a=s;for(const r of t){let t,l,c=e[s%n];for(a=s+1;a<=r.end;a++){const h=e[a%n];l=Cn(i.setContext({type:"segment",p0:c,p1:h})),On(l,t)&&(o.push({start:s,end:a-1,loop:r.loop,style:t}),t=l,s=a-1),c=h,t=l}s<a-1&&(o.push({start:s,end:a-1,loop:r.loop,style:l}),s=a-1)}return o}(t,e,i):t}function Cn(t){return{backgroundColor:t.backgroundColor,borderCapStyle:t.borderCapStyle,borderDash:t.borderDash,borderDashOffset:t.borderDashOffset,borderJoinStyle:t.borderJoinStyle,borderWidth:t.borderWidth,borderColor:t.borderColor}}function On(t,e){return e&&JSON.stringify(t)!==JSON.stringify(e)}var Tn=Object.freeze({__proto__:null,easingEffects:xi,color:W,getHoverColor:N,noop:H,uid:j,isNullOrUndef:$,isArray:Y,isObject:U,isFinite:X,finiteOrDefault:q,valueOrDefault:K,toPercentage:G,toDimension:Z,callback:Q,each:J,_elementsEqual:tt,clone:et,_merger:nt,merge:ot,mergeIf:st,_mergerIf:at,_deprecated:function(t,e,i,n){void 0!==e&&console.warn(t+': "'+i+'" is deprecated. Please use "'+n+'" instead')},resolveObjectKey:lt,_capitalize:ct,defined:ht,isFunction:dt,setsEqual:ut,toFontString:$t,_measureText:Yt,_longestText:Ut,_alignPixel:Xt,clearCanvas:qt,drawPoint:Kt,_isPointInArea:Gt,clipArea:Zt,unclipArea:Qt,_steppedLineTo:Jt,_bezierCurveTo:te,renderText:ee,addRoundedRectPath:ne,_lookup:oe,_lookupByKey:se,_rlookupByKey:ae,_filterBetween:re,listenArrayEvents:ce,unlistenArrayEvents:he,_arrayUnique:de,_createResolver:Ki,_attachContext:Gi,_descriptors:Zi,splineCurve:un,splineCurveMonotone:fn,_updateBezierControlPoints:pn,_getParentNode:ue,getStyle:pe,getRelativePosition:be,getMaximumSize:ye,retinaScale:ve,supportsEventListenerOptions:we,readUsedSize:Me,fontString:function(t,e,i){return e+" "+t+"px "+i},requestAnimFrame:t,throttled:e,debounce:i,_toLeftRightCenter:n,_alignStartEnd:o,_textX:s,_pointInLine:mn,_steppedInterpolation:xn,_bezierInterpolation:bn,formatNumber:Fi,toLineHeight:Re,_readValueToProps:Ee,toTRBL:ze,toTRBLCorners:Ie,toPadding:Fe,toFont:Be,resolve:Ve,_addGrace:We,PI:bt,TAU:_t,PITAU:yt,INFINITY:vt,RAD_PER_DEG:wt,HALF_PI:Mt,QUARTER_PI:kt,TWO_THIRDS_PI:St,log10:Pt,sign:Dt,niceNum:Ct,_factorize:Ot,isNumber:Tt,almostEquals:At,almostWhole:Lt,_setMinAndMaxByKey:Rt,toRadians:Et,toDegrees:zt,_decimalPlaces:It,getAngleFromPoint:Ft,distanceBetweenPoints:Bt,_angleDiff:Vt,_normalizeAngle:Wt,_angleBetween:Nt,_limitValue:Ht,_int16Range:jt,getRtlAdapter:_n,overrideTextDirection:yn,restoreTextDirection:vn,_boundSegment:kn,_boundSegments:Sn,_computeSegments:Pn});class An{constructor(t,e,i){this.type=t,this.scope=e,this.override=i,this.items=Object.create(null)}isForType(t){return Object.prototype.isPrototypeOf.call(this.type.prototype,t.prototype)}register(t){const e=this,i=Object.getPrototypeOf(t);let n;(function(t){return"id"in t&&"defaults"in t})(i)&&(n=e.register(i));const o=e.items,s=t.id,a=e.scope+"."+s;if(!s)throw new Error("class does not have id: "+t);return s in o||(o[s]=t,function(t,e,i){const n=ot(Object.create(null),[i?xt.get(i):{},xt.get(e),t.defaults]);xt.set(e,n),t.defaultRoutes&&function(t,e){Object.keys(e).forEach((i=>{const n=i.split("."),o=n.pop(),s=[t].concat(n).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");xt.route(s,o,l,r)}))}(e,t.defaultRoutes);t.descriptors&&xt.describe(e,t.descriptors)}(t,a,n),e.override&&xt.override(t.id,t.overrides)),a}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,n=this.scope;i in e&&delete e[i],n&&i in xt[n]&&(delete xt[n][i],this.override&&delete ft[i])}}var Ln=new class{constructor(){this.controllers=new An(Ei,"datasets",!0),this.elements=new An(zi,"elements"),this.plugins=new An(Object,"plugins"),this.scales=new An(qi,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){const n=this;[...e].forEach((e=>{const o=i||n._getRegistryForType(e);i||o.isForType(e)||o===n.plugins&&e.id?n._exec(t,o,e):J(e,(e=>{const o=i||n._getRegistryForType(e);n._exec(t,o,e)}))}))}_exec(t,e,i){const n=ct(t);Q(i["before"+n],[],i),e[t](i),Q(i["after"+n],[],i)}_getRegistryForType(t){for(let e=0;e<this._typedRegistries.length;e++){const i=this._typedRegistries[e];if(i.isForType(t))return i}return this.plugins}_get(t,e,i){const n=e.get(t);if(void 0===n)throw new Error('"'+t+'" is not a registered '+i+".");return n}};class Rn{constructor(){this._init=[]}notify(t,e,i,n){const o=this;"beforeInit"===e&&(o._init=o._createDescriptors(t,!0),o._notify(o._init,t,"install"));const s=n?o._descriptors(t).filter(n):o._descriptors(t),a=o._notify(s,t,e,i);return"destroy"===e&&(o._notify(s,t,"stop"),o._notify(o._init,t,"uninstall")),a}_notify(t,e,i,n){n=n||{};for(const o of t){const t=o.plugin;if(!1===Q(t[i],[e,n,o.options],t)&&n.cancelable)return!1}return!0}invalidate(){$(this._cache)||(this._oldCache=this._cache,this._cache=void 0)}_descriptors(t){if(this._cache)return this._cache;const e=this._cache=this._createDescriptors(t);return this._notifyStateChanges(t),e}_createDescriptors(t,e){const i=t&&t.config,n=K(i.options&&i.options.plugins,{}),o=function(t){const e=[],i=Object.keys(Ln.plugins.items);for(let t=0;t<i.length;t++)e.push(Ln.getPlugin(i[t]));const n=t.plugins||[];for(let t=0;t<n.length;t++){const i=n[t];-1===e.indexOf(i)&&e.push(i)}return e}(i);return!1!==n||e?function(t,e,i,n){const o=[],s=t.getContext();for(let a=0;a<e.length;a++){const r=e[a],l=En(i[r.id],n);null!==l&&o.push({plugin:r,options:zn(t.config,r,l,s)})}return o}(t,o,n,e):[]}_notifyStateChanges(t){const e=this._oldCache||[],i=this._cache,n=(t,e)=>t.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(n(e,i),t,"stop"),this._notify(n(i,e),t,"start")}}function En(t,e){return e||!1!==t?!0===t?{}:t:null}function zn(t,e,i,n){const o=t.pluginScopeKeys(e),s=t.getOptionScopes(i,o);return t.createResolver(s,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function In(t,e){const i=xt.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function Fn(t,e){return"x"===t||"y"===t?t:e.axis||("top"===(i=e.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.charAt(0).toLowerCase();var i}function Bn(t){const e=t.options||(t.options={});e.plugins=K(e.plugins,{}),e.scales=function(t,e){const i=ft[t.type]||{scales:{}},n=e.scales||{},o=In(t.type,e),s=Object.create(null),a=Object.create(null);return Object.keys(n).forEach((t=>{const e=n[t],r=Fn(t,e),l=function(t,e){return t===e?"_index_":"_value_"}(r,o),c=i.scales||{};s[r]=s[r]||t,a[t]=st(Object.create(null),[{axis:r},e,c[r],c[l]])})),t.data.datasets.forEach((i=>{const o=i.type||t.type,r=i.indexAxis||In(o,e),l=(ft[o]||{}).scales||{};Object.keys(l).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,r),o=i[e+"AxisID"]||s[e]||e;a[o]=a[o]||Object.create(null),st(a[o],[{axis:e},n[o],l[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];st(e,[xt.scales[e.type],xt.scale])})),a}(t,e)}function Vn(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const Wn=new Map,Nn=new Set;function Hn(t,e){let i=Wn.get(t);return i||(i=e(),Wn.set(t,i),Nn.add(i)),i}const jn=(t,e,i)=>{const n=lt(e,i);void 0!==n&&t.add(n)};class $n{constructor(t){this._config=function(t){return(t=t||{}).data=Vn(t.data),Bn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=Vn(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),Bn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return Hn(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return Hn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return Hn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return Hn(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let n=i.get(t);return n&&!e||(n=new Map,i.set(t,n)),n}getOptionScopes(t,e,i){const{options:n,type:o}=this,s=this._cachedScopes(t,i),a=s.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>jn(r,t,e)))),e.forEach((t=>jn(r,n,t))),e.forEach((t=>jn(r,ft[o]||{},t))),e.forEach((t=>jn(r,xt,t))),e.forEach((t=>jn(r,gt,t)))}));const l=Array.from(r);return Nn.has(e)&&s.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,ft[e]||{},xt.datasets[e]||{},{type:e},xt,gt]}resolveNamedOptions(t,e,i,n=[""]){const o={$shared:!0},{resolver:s,subPrefixes:a}=Yn(this._resolverCache,t,n);let r=s;if(function(t,e){const{isScriptable:i,isIndexable:n}=Zi(t);for(const o of e)if(i(o)&&dt(t[o])||n(o)&&Y(t[o]))return!0;return!1}(s,e)){o.$shared=!1;r=Gi(s,i=dt(i)?i():i,this.createResolver(t,i,a))}for(const t of e)o[t]=r[t];return o}createResolver(t,e,i=[""],n){const{resolver:o}=Yn(this._resolverCache,t,i);return U(e)?Gi(o,e,void 0,n):o}}function Yn(t,e,i){let n=t.get(e);n||(n=new Map,t.set(e,n));const o=i.join();let s=n.get(o);if(!s){s={resolver:Ki(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},n.set(o,s)}return s}const Un=["top","bottom","left","right","chartArea"];function Xn(t,e){return"top"===t||"bottom"===t||-1===Un.indexOf(t)&&"x"===e}function qn(t,e){return function(i,n){return i[t]===n[t]?i[e]-n[e]:i[t]-n[t]}}function Kn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),Q(i&&i.onComplete,[t],e)}function Gn(t){const e=t.chart,i=e.options.animation;Q(i&&i.onProgress,[t],e)}function Zn(){return"undefined"!=typeof window&&"undefined"!=typeof document}function Qn(t){return Zn()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const Jn={},to=t=>{const e=Qn(t);return Object.values(Jn).filter((t=>t.canvas===e)).pop()};class eo{constructor(t,e){const n=this;this.config=e=new $n(e);const o=Qn(t),s=to(o);if(s)throw new Error("Canvas is already in use. Chart with ID '"+s.id+"' must be destroyed before the canvas can be reused.");const r=e.createResolver(e.chartOptionScopes(),n.getContext());this.platform=n._initializePlatform(o,e);const l=n.platform.acquireContext(o,r.aspectRatio),c=l&&l.canvas,h=c&&c.height,d=c&&c.width;this.id=j(),this.ctx=l,this.canvas=c,this.width=d,this.height=h,this._options=r,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this.scale=void 0,this._plugins=new Rn,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=i((()=>this.update("resize")),r.resizeDelay||0),Jn[n.id]=n,l&&c?(a.listen(n,"complete",Kn),a.listen(n,"progress",Gn),n._initialize(),n.attached&&n.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return $(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}_initialize(){const t=this;return t.notifyPlugins("beforeInit"),t.options.responsive?t.resize():ve(t,t.options.devicePixelRatio),t.bindEvents(),t.notifyPlugins("afterInit"),t}_initializePlatform(t,e){return e.platform?new e.platform:!Zn()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?new Je:new ui}clear(){return qt(this.canvas,this.ctx),this}stop(){return a.stop(this),this}resize(t,e){a.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this,n=i.options,o=i.canvas,s=n.maintainAspectRatio&&i.aspectRatio,a=i.platform.getMaximumSize(o,t,e,s),r=n.devicePixelRatio||i.platform.getDevicePixelRatio();i.width=a.width,i.height=a.height,i._aspectRatio=i.aspectRatio,ve(i,r,!0)&&(i.notifyPlugins("resize",{size:a}),Q(n.onResize,[i,a],i),i.attached&&i._doResize()&&i.render())}ensureScalesHaveIDs(){J(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this,e=t.options,i=e.scales,n=t.scales,o=Object.keys(n).reduce(((t,e)=>(t[e]=!1,t)),{});let s=[];i&&(s=s.concat(Object.keys(i).map((t=>{const e=i[t],n=Fn(t,e),o="r"===n,s="x"===n;return{options:e,dposition:o?"chartArea":s?"bottom":"left",dtype:o?"radialLinear":s?"category":"linear"}})))),J(s,(i=>{const s=i.options,a=s.id,r=Fn(a,s),l=K(s.type,i.dtype);void 0!==s.position&&Xn(s.position,r)===Xn(i.dposition)||(s.position=i.dposition),o[a]=!0;let c=null;if(a in n&&n[a].type===l)c=n[a];else{c=new(Ln.getScale(l))({id:a,type:l,ctx:t.ctx,chart:t}),n[c.id]=c}c.init(s,e)})),J(o,((t,e)=>{t||delete n[e]})),J(n,(e=>{Ze.configure(t,e,e.options),Ze.addBox(t,e)}))}_updateMetasets(){const t=this,e=t._metasets,i=t.data.datasets.length,n=e.length;if(e.sort(((t,e)=>t.index-e.index)),n>i){for(let e=i;e<n;++e)t._destroyDatasetMeta(e);e.splice(i,n-i)}t._sortedMetasets=e.slice(0).sort(qn("order","index"))}_removeUnreferencedMetasets(){const t=this,{_metasets:e,data:{datasets:i}}=t;e.length>i.length&&delete t._stacks,e.forEach(((e,n)=>{0===i.filter((t=>t===e._dataset)).length&&t._destroyDatasetMeta(n)}))}buildOrUpdateControllers(){const t=this,e=[],i=t.data.datasets;let n,o;for(t._removeUnreferencedMetasets(),n=0,o=i.length;n<o;n++){const o=i[n];let s=t.getDatasetMeta(n);const a=o.type||t.config.type;if(s.type&&s.type!==a&&(t._destroyDatasetMeta(n),s=t.getDatasetMeta(n)),s.type=a,s.indexAxis=o.indexAxis||In(a,t.options),s.order=o.order||0,s.index=n,s.label=""+o.label,s.visible=t.isDatasetVisible(n),s.controller)s.controller.updateIndex(n),s.controller.linkScales();else{const i=Ln.getController(a),{datasetElementType:o,dataElementType:r}=xt.datasets[a];Object.assign(i.prototype,{dataElementType:Ln.getElement(r),datasetElementType:o&&Ln.getElement(o)}),s.controller=new i(t,n),e.push(s.controller)}}return t._updateMetasets(),e}_resetElements(){const t=this;J(t.data.datasets,((e,i)=>{t.getDatasetMeta(i).controller.reset()}),t)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this,i=e.config;i.update(),e._options=i.createResolver(i.chartOptionScopes(),e.getContext()),J(e.scales,(t=>{Ze.removeBox(e,t)}));const n=e._animationsDisabled=!e.options.animation;e.ensureScalesHaveIDs(),e.buildOrUpdateScales();const o=new Set(Object.keys(e._listeners)),s=new Set(e.options.events);if(ut(o,s)&&!!this._responsiveListeners===e.options.responsive||(e.unbindEvents(),e.bindEvents()),e._plugins.invalidate(),!1===e.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const a=e.buildOrUpdateControllers();e.notifyPlugins("beforeElementsUpdate");let r=0;for(let t=0,i=e.data.datasets.length;t<i;t++){const{controller:i}=e.getDatasetMeta(t),o=!n&&-1===a.indexOf(i);i.buildOrUpdateElements(o),r=Math.max(+i.getMaxOverflow(),r)}e._minPadding=r,e._updateLayout(r),n||J(a,(t=>{t.reset()})),e._updateDatasets(t),e.notifyPlugins("afterUpdate",{mode:t}),e._layers.sort(qn("z","_idx")),e._lastEvent&&e._eventHandler(e._lastEvent,!0),e.render()}_updateLayout(t){const e=this;if(!1===e.notifyPlugins("beforeLayout",{cancelable:!0}))return;Ze.update(e,e.width,e.height,t);const i=e.chartArea,n=i.width<=0||i.height<=0;e._layers=[],J(e.boxes,(t=>{n&&"chartArea"===t.position||(t.configure&&t.configure(),e._layers.push(...t._layers()))}),e),e._layers.forEach(((t,e)=>{t._idx=e})),e.notifyPlugins("afterLayout")}_updateDatasets(t){const e=this,i="function"==typeof t;if(!1!==e.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let n=0,o=e.data.datasets.length;n<o;++n)e._updateDataset(n,i?t({datasetIndex:n}):t);e.notifyPlugins("afterDatasetsUpdate",{mode:t})}}_updateDataset(t,e){const i=this,n=i.getDatasetMeta(t),o={meta:n,index:t,mode:e,cancelable:!0};!1!==i.notifyPlugins("beforeDatasetUpdate",o)&&(n.controller._update(e),o.cancelable=!1,i.notifyPlugins("afterDatasetUpdate",o))}render(){const t=this;!1!==t.notifyPlugins("beforeRender",{cancelable:!0})&&(a.has(t)?t.attached&&!a.running(t)&&a.start(t):(t.draw(),Kn({chart:t})))}draw(){const t=this;let e;if(t._resizeBeforeDraw){const{width:e,height:i}=t._resizeBeforeDraw;t._resize(e,i),t._resizeBeforeDraw=null}if(t.clear(),t.width<=0||t.height<=0)return;if(!1===t.notifyPlugins("beforeDraw",{cancelable:!0}))return;const i=t._layers;for(e=0;e<i.length&&i[e].z<=0;++e)i[e].draw(t.chartArea);for(t._drawDatasets();e<i.length;++e)i[e].draw(t.chartArea);t.notifyPlugins("afterDraw")}_getSortedDatasetMetas(t){const e=this._sortedMetasets,i=[];let n,o;for(n=0,o=e.length;n<o;++n){const o=e[n];t&&!o.visible||i.push(o)}return i}getSortedVisibleDatasetMetas(){return this._getSortedDatasetMetas(!0)}_drawDatasets(){const t=this;if(!1===t.notifyPlugins("beforeDatasetsDraw",{cancelable:!0}))return;const e=t.getSortedVisibleDatasetMetas();for(let i=e.length-1;i>=0;--i)t._drawDataset(e[i]);t.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this,i=e.ctx,n=t._clip,o=!n.disabled,s=e.chartArea,a={meta:t,index:t.index,cancelable:!0};!1!==e.notifyPlugins("beforeDatasetDraw",a)&&(o&&Zt(i,{left:!1===n.left?0:s.left-n.left,right:!1===n.right?e.width:s.right+n.right,top:!1===n.top?0:s.top-n.top,bottom:!1===n.bottom?e.height:s.bottom+n.bottom}),t.controller.draw(),o&&Qt(i),a.cancelable=!1,e.notifyPlugins("afterDatasetDraw",a))}getElementsAtEventForMode(t,e,i,n){const o=Te.modes[e];return"function"==typeof o?o(this,t,i,n):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let n=i.filter((t=>t&&t._dataset===e)).pop();return n||(n={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(n)),n}getContext(){return this.$context||(this.$context={chart:this,type:"chart"})}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateDatasetVisibility(t,e){const i=this,n=e?"show":"hide",o=i.getDatasetMeta(t),s=o.controller._resolveAnimations(void 0,n);i.setDatasetVisibility(t,e),s.update(o,{visible:e}),i.update((e=>e.datasetIndex===t?n:void 0))}hide(t){this._updateDatasetVisibility(t,!1)}show(t){this._updateDatasetVisibility(t,!0)}_destroyDatasetMeta(t){const e=this,i=e._metasets&&e._metasets[t];i&&i.controller&&(i.controller._destroy(),delete e._metasets[t])}destroy(){const t=this,{canvas:e,ctx:i}=t;let n,o;for(t.stop(),a.remove(t),n=0,o=t.data.datasets.length;n<o;++n)t._destroyDatasetMeta(n);t.config.clearCache(),e&&(t.unbindEvents(),qt(e,i),t.platform.releaseContext(i),t.canvas=null,t.ctx=null),t.notifyPlugins("destroy"),delete Jn[t.id]}toBase64Image(...t){return this.canvas.toDataURL(...t)}bindEvents(){this.bindUserEvents(),this.options.responsive?this.bindResponsiveEvents():this.attached=!0}bindUserEvents(){const t=this,e=t._listeners,i=t.platform,n=function(e,i,n){e.offsetX=i,e.offsetY=n,t._eventHandler(e)};J(t.options.events,(o=>((n,o)=>{i.addEventListener(t,n,o),e[n]=o})(o,n)))}bindResponsiveEvents(){const t=this;t._responsiveListeners||(t._responsiveListeners={});const e=t._responsiveListeners,i=t.platform,n=(n,o)=>{i.addEventListener(t,n,o),e[n]=o},o=(n,o)=>{e[n]&&(i.removeEventListener(t,n,o),delete e[n])},s=(e,i)=>{t.canvas&&t.resize(e,i)};let a;const r=()=>{o("attach",r),t.attached=!0,t.resize(),n("resize",s),n("detach",a)};a=()=>{t.attached=!1,o("resize",s),n("attach",r)},i.isAttached(t.canvas)?r():a()}unbindEvents(){const t=this;J(t._listeners,((e,i)=>{t.platform.removeEventListener(t,i,e)})),t._listeners={},J(t._responsiveListeners,((e,i)=>{t.platform.removeEventListener(t,i,e)})),t._responsiveListeners=void 0}updateHoverStyle(t,e,i){const n=i?"set":"remove";let o,s,a,r;for("dataset"===e&&(o=this.getDatasetMeta(t[0].datasetIndex),o.controller["_"+n+"DatasetHoverStyle"]()),a=0,r=t.length;a<r;++a){s=t[a];const e=s&&this.getDatasetMeta(s.datasetIndex).controller;e&&e[n+"HoverStyle"](s.element,s.datasetIndex,s.index)}}getActiveElements(){return this._active||[]}setActiveElements(t){const e=this,i=e._active||[],n=t.map((({datasetIndex:t,index:i})=>{const n=e.getDatasetMeta(t);if(!n)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:n.data[i],index:i}}));!tt(n,i)&&(e._active=n,e._updateHoverStyles(n,i))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}_updateHoverStyles(t,e,i){const n=this,o=n.options.hover,s=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),a=s(e,t),r=i?t:s(t,e);a.length&&n.updateHoverStyle(a,o.mode,!1),r.length&&o.mode&&n.updateHoverStyle(r,o.mode,!0)}_eventHandler(t,e){const i=this,n={event:t,replay:e,cancelable:!0},o=e=>(e.options.events||this.options.events).includes(t.type);if(!1===i.notifyPlugins("beforeEvent",n,o))return;const s=i._handleEvent(t,e);return n.cancelable=!1,i.notifyPlugins("afterEvent",n,o),(s||n.changed)&&i.render(),i}_handleEvent(t,e){const i=this,{_active:n=[],options:o}=i,s=o.hover,a=e;let r=[],l=!1,c=null;return"mouseout"!==t.type&&(r=i.getElementsAtEventForMode(t,s.mode,s,a),c="click"===t.type?i._lastEvent:t),i._lastEvent=null,Gt(t,i.chartArea,i._minPadding)&&(Q(o.onHover,[t,r,i],i),"mouseup"!==t.type&&"click"!==t.type&&"contextmenu"!==t.type||Q(o.onClick,[t,r,i],i)),l=!tt(r,n),(l||e)&&(i._active=r,i._updateHoverStyles(r,n,e)),i._lastEvent=c,l}}const io=()=>J(eo.instances,(t=>t._plugins.invalidate())),no=!0;function oo(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}Object.defineProperties(eo,{defaults:{enumerable:no,value:xt},instances:{enumerable:no,value:Jn},overrides:{enumerable:no,value:ft},registry:{enumerable:no,value:Ln},version:{enumerable:no,value:"3.4.1"},getChart:{enumerable:no,value:to},register:{enumerable:no,value:(...t)=>{Ln.add(...t),io()}},unregister:{enumerable:no,value:(...t)=>{Ln.remove(...t),io()}}});class so{constructor(t){this.options=t||{}}formats(){return oo()}parse(t,e){return oo()}format(t,e){return oo()}add(t,e,i){return oo()}diff(t,e,i){return oo()}startOf(t,e,i){return oo()}endOf(t,e){return oo()}}so.override=function(t){Object.assign(so.prototype,t)};var ao={_date:so};function ro(t){const e=function(t){if(!t._cache.$bar){const e=t.getMatchingVisibleMetas("bar");let i=[];for(let n=0,o=e.length;n<o;n++)i=i.concat(e[n].controller.getAllParsedValues(t));t._cache.$bar=de(i.sort(((t,e)=>t-e)))}return t._cache.$bar}(t);let i,n,o,s,a=t._length;const r=()=>{32767!==o&&-32768!==o&&(ht(s)&&(a=Math.min(a,Math.abs(o-s)||a)),s=o)};for(i=0,n=e.length;i<n;++i)o=t.getPixelForValue(e[i]),r();for(s=void 0,i=0,n=t.ticks.length;i<n;++i)o=t.getPixelForTick(i),r();return a}function lo(t,e,i,n){return Y(t)?function(t,e,i,n){const o=i.parse(t[0],n),s=i.parse(t[1],n),a=Math.min(o,s),r=Math.max(o,s);let l=a,c=r;Math.abs(a)>Math.abs(r)&&(l=r,c=a),e[i.axis]=c,e._custom={barStart:l,barEnd:c,start:o,end:s,min:a,max:r}}(t,e,i,n):e[i.axis]=i.parse(t,n),e}function co(t,e,i,n){const o=t.iScale,s=t.vScale,a=o.getLabels(),r=o===s,l=[];let c,h,d,u;for(c=i,h=i+n;c<h;++c)u=e[c],d={},d[o.axis]=r||o.parse(a[c],c),l.push(lo(u,d,s,c));return l}function ho(t){return t&&void 0!==t.barStart&&void 0!==t.barEnd}class uo extends Ei{parsePrimitiveData(t,e,i,n){return co(t,e,i,n)}parseArrayData(t,e,i,n){return co(t,e,i,n)}parseObjectData(t,e,i,n){const{iScale:o,vScale:s}=t,{xAxisKey:a="x",yAxisKey:r="y"}=this._parsing,l="x"===o.axis?a:r,c="x"===s.axis?a:r,h=[];let d,u,f,g;for(d=i,u=i+n;d<u;++d)g=e[d],f={},f[o.axis]=o.parse(lt(g,l),d),h.push(lo(lt(g,c),f,s,d));return h}updateRangeFromParsed(t,e,i,n){super.updateRangeFromParsed(t,e,i,n);const o=i._custom;o&&e===this._cachedMeta.vScale&&(t.min=Math.min(t.min,o.min),t.max=Math.max(t.max,o.max))}getMaxOverflow(){return 0}getLabelAndValue(t){const e=this._cachedMeta,{iScale:i,vScale:n}=e,o=this.getParsed(t),s=o._custom,a=ho(s)?"["+s.start+", "+s.end+"]":""+n.getLabelForValue(o[n.axis]);return{label:""+i.getLabelForValue(o[i.axis]),value:a}}initialize(){const t=this;t.enableOptionSharing=!0,super.initialize();t._cachedMeta.stack=t.getDataset().stack}update(t){const e=this._cachedMeta;this.updateElements(e.data,0,e.data.length,t)}updateElements(t,e,i,n){const o=this,s="reset"===n,a=o._cachedMeta.vScale,r=a.getBasePixel(),l=a.isHorizontal(),c=o._getRuler(),h=o.resolveDataElementOptions(e,n),d=o.getSharedOptions(h),u=o.includeOptions(n,d);o.updateSharedOptions(d,n,h);for(let h=e;h<e+i;h++){const e=o.getParsed(h),i=s||$(e[a.axis])?{base:r,head:r}:o._calculateBarValuePixels(h),f=o._calculateBarIndexPixels(h,c),g=(e._stacks||{})[a.axis],p={horizontal:l,base:i.base,enableBorderRadius:!g||ho(e._custom)||o.index===g._top||o.index===g._bottom,x:l?i.head:f.center,y:l?f.center:i.head,height:l?f.size:Math.abs(i.size),width:l?Math.abs(i.size):f.size};u&&(p.options=d||o.resolveDataElementOptions(h,t[h].active?"active":n)),o.updateElement(t[h],h,p,n)}}_getStacks(t,e){const i=this._cachedMeta.iScale,n=i.getMatchingVisibleMetas(this._type),o=i.options.stacked,s=n.length,a=[];let r,l;for(r=0;r<s;++r)if(l=n[r],l.controller.options.grouped){if(void 0!==e){const t=l.controller.getParsed(e)[l.controller._cachedMeta.vScale.axis];if($(t)||isNaN(t))continue}if((!1===o||-1===a.indexOf(l.stack)||void 0===o&&void 0===l.stack)&&a.push(l.stack),l.index===t)break}return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getStackIndex(t,e,i){const n=this._getStacks(t,i),o=void 0!==e?n.indexOf(e):-1;return-1===o?n.length-1:o}_getRuler(){const t=this,e=t.options,i=t._cachedMeta,n=i.iScale,o=[];let s,a;for(s=0,a=i.data.length;s<a;++s)o.push(n.getPixelForValue(t.getParsed(s)[n.axis],s));const r=e.barThickness;return{min:r||ro(n),pixels:o,start:n._startPixel,end:n._endPixel,stackCount:t._getStackCount(),scale:n,grouped:e.grouped,ratio:r?1:e.categoryPercentage*e.barPercentage}}_calculateBarValuePixels(t){const e=this,{vScale:i,_stacked:n}=e._cachedMeta,{base:o,minBarLength:s}=e.options,a=e.getParsed(t),r=a._custom,l=ho(r);let c,h,d=a[i.axis],u=0,f=n?e.applyStack(i,a,n):d;f!==d&&(u=f-d,f=d),l&&(d=r.barStart,f=r.barEnd-r.barStart,0!==d&&Dt(d)!==Dt(r.barEnd)&&(u=0),u+=d);const g=$(o)||l?u:o;let p=i.getPixelForValue(g);c=this.chart.getDataVisibility(t)?i.getPixelForValue(u+f):p,h=c-p,void 0!==s&&Math.abs(h)<s&&(h=h<0?-s:s,0===d&&(p-=h/2),c=p+h);const m=o||0;if(p===i.getPixelForValue(m)){const t=i.getLineWidthForValue(m)/2;h>0?(p+=t,h-=t):h<0&&(p-=t,h+=t)}return{size:h,base:p,head:c,center:c+h/2}}_calculateBarIndexPixels(t,e){const i=this,n=e.scale,o=i.options,s=o.skipNull,a=K(o.maxBarThickness,1/0);let r,l;if(e.grouped){const n=s?i._getStackCount(t):e.stackCount,c="flex"===o.barThickness?function(t,e,i,n){const o=e.pixels,s=o[t];let a=t>0?o[t-1]:null,r=t<o.length-1?o[t+1]:null;const l=i.categoryPercentage;null===a&&(a=s-(null===r?e.end-e.start:r-s)),null===r&&(r=s+s-a);const c=s-(s-Math.min(a,r))/2*l;return{chunk:Math.abs(r-a)/2*l/n,ratio:i.barPercentage,start:c}}(t,e,o,n):function(t,e,i,n){const o=i.barThickness;let s,a;return $(o)?(s=e.min*i.categoryPercentage,a=i.barPercentage):(s=o*n,a=1),{chunk:s/n,ratio:a,start:e.pixels[t]-s/2}}(t,e,o,n),h=i._getStackIndex(i.index,i._cachedMeta.stack,s?t:void 0);r=c.start+c.chunk*h+c.chunk/2,l=Math.min(a,c.chunk*c.ratio)}else r=n.getPixelForValue(i.getParsed(t)[n.axis],t),l=Math.min(a,e.min*e.ratio);return{base:r-l/2,head:r+l/2,center:r,size:l}}draw(){const t=this,e=t._cachedMeta,i=e.vScale,n=e.data,o=n.length;let s=0;for(;s<o;++s)null!==t.getParsed(s)[i.axis]&&n[s].draw(t._ctx)}}uo.id="bar",uo.defaults={datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}},uo.overrides={interaction:{mode:"index"},scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}};class fo extends Ei{initialize(){this.enableOptionSharing=!0,super.initialize()}parseObjectData(t,e,i,n){const{xScale:o,yScale:s}=t,{xAxisKey:a="x",yAxisKey:r="y"}=this._parsing,l=[];let c,h,d;for(c=i,h=i+n;c<h;++c)d=e[c],l.push({x:o.parse(lt(d,a),c),y:s.parse(lt(d,r),c),_custom:d&&d.r&&+d.r});return l}getMaxOverflow(){const{data:t,_parsed:e}=this._cachedMeta;let i=0;for(let n=t.length-1;n>=0;--n)i=Math.max(i,t[n].size()/2,e[n]._custom);return i>0&&i}getLabelAndValue(t){const e=this._cachedMeta,{xScale:i,yScale:n}=e,o=this.getParsed(t),s=i.getLabelForValue(o.x),a=n.getLabelForValue(o.y),r=o._custom;return{label:e.label,value:"("+s+", "+a+(r?", "+r:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,n){const o=this,s="reset"===n,{iScale:a,vScale:r}=o._cachedMeta,l=o.resolveDataElementOptions(e,n),c=o.getSharedOptions(l),h=o.includeOptions(n,c),d=a.axis,u=r.axis;for(let l=e;l<e+i;l++){const e=t[l],i=!s&&o.getParsed(l),c={},f=c[d]=s?a.getPixelForDecimal(.5):a.getPixelForValue(i[d]),g=c[u]=s?r.getBasePixel():r.getPixelForValue(i[u]);c.skip=isNaN(f)||isNaN(g),h&&(c.options=o.resolveDataElementOptions(l,e.active?"active":n),s&&(c.options.radius=0)),o.updateElement(e,l,c,n)}o.updateSharedOptions(c,n,l)}resolveDataElementOptions(t,e){const i=this.getParsed(t);let n=super.resolveDataElementOptions(t,e);n.$shared&&(n=Object.assign({},n,{$shared:!1}));const o=n.radius;return"active"!==e&&(n.radius=0),n.radius+=K(i&&i._custom,o),n}}fo.id="bubble",fo.defaults={datasetElementType:!1,dataElementType:"point",animations:{numbers:{type:"number",properties:["x","y","borderWidth","radius"]}}},fo.overrides={scales:{x:{type:"linear"},y:{type:"linear"}},plugins:{tooltip:{callbacks:{title:()=>""}}}};class go extends Ei{constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,n=this._cachedMeta;let o,s;for(o=t,s=t+e;o<s;++o)n._parsed[o]=+i[o]}_getRotation(){return Et(this.options.rotation-90)}_getCircumference(){return Et(this.options.circumference)}_getRotationExtents(){let t=_t,e=-_t;const i=this;for(let n=0;n<i.chart.data.datasets.length;++n)if(i.chart.isDatasetVisible(n)){const o=i.chart.getDatasetMeta(n).controller,s=o._getRotation(),a=o._getCircumference();t=Math.min(t,s),e=Math.max(e,s+a)}return{rotation:t,circumference:e-t}}update(t){const e=this,i=e.chart,{chartArea:n}=i,o=e._cachedMeta,s=o.data,a=e.getMaxBorderWidth()+e.getMaxOffset(s)+e.options.spacing,r=Math.max((Math.min(n.width,n.height)-a)/2,0),l=Math.min(G(e.options.cutout,r),1),c=e._getRingWeight(e.index),{circumference:h,rotation:d}=e._getRotationExtents(),{ratioX:u,ratioY:f,offsetX:g,offsetY:p}=function(t,e,i){let n=1,o=1,s=0,a=0;if(e<_t){const r=t,l=r+e,c=Math.cos(r),h=Math.sin(r),d=Math.cos(l),u=Math.sin(l),f=(t,e,n)=>Nt(t,r,l,!0)?1:Math.max(e,e*i,n,n*i),g=(t,e,n)=>Nt(t,r,l,!0)?-1:Math.min(e,e*i,n,n*i),p=f(0,c,d),m=f(Mt,h,u),x=g(bt,c,d),b=g(bt+Mt,h,u);n=(p-x)/2,o=(m-b)/2,s=-(p+x)/2,a=-(m+b)/2}return{ratioX:n,ratioY:o,offsetX:s,offsetY:a}}(d,h,l),m=(n.width-a)/u,x=(n.height-a)/f,b=Math.max(Math.min(m,x)/2,0),_=Z(e.options.radius,b),y=(_-Math.max(_*l,0))/e._getVisibleDatasetWeightTotal();e.offsetX=g*_,e.offsetY=p*_,o.total=e.calculateTotal(),e.outerRadius=_-y*e._getRingWeightOffset(e.index),e.innerRadius=Math.max(e.outerRadius-y*c,0),e.updateElements(s,0,s.length,t)}_circumference(t,e){const i=this,n=i.options,o=i._cachedMeta,s=i._getCircumference();return e&&n.animation.animateRotate||!this.chart.getDataVisibility(t)||null===o._parsed[t]?0:i.calculateCircumference(o._parsed[t]*s/_t)}updateElements(t,e,i,n){const o=this,s="reset"===n,a=o.chart,r=a.chartArea,l=a.options.animation,c=(r.left+r.right)/2,h=(r.top+r.bottom)/2,d=s&&l.animateScale,u=d?0:o.innerRadius,f=d?0:o.outerRadius,g=o.resolveDataElementOptions(e,n),p=o.getSharedOptions(g),m=o.includeOptions(n,p);let x,b=o._getRotation();for(x=0;x<e;++x)b+=o._circumference(x,s);for(x=e;x<e+i;++x){const e=o._circumference(x,s),i=t[x],a={x:c+o.offsetX,y:h+o.offsetY,startAngle:b,endAngle:b+e,circumference:e,outerRadius:f,innerRadius:u};m&&(a.options=p||o.resolveDataElementOptions(x,i.active?"active":n)),b+=e,o.updateElement(i,x,a,n)}o.updateSharedOptions(p,n,g)}calculateTotal(){const t=this._cachedMeta,e=t.data;let i,n=0;for(i=0;i<e.length;i++){const e=t._parsed[i];null!==e&&!isNaN(e)&&this.chart.getDataVisibility(i)&&(n+=Math.abs(e))}return n}calculateCircumference(t){const e=this._cachedMeta.total;return e>0&&!isNaN(t)?_t*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,n=i.data.labels||[],o=Fi(e._parsed[t],i.options.locale);return{label:n[t]||"",value:o}}getMaxBorderWidth(t){const e=this;let i=0;const n=e.chart;let o,s,a,r,l;if(!t)for(o=0,s=n.data.datasets.length;o<s;++o)if(n.isDatasetVisible(o)){a=n.getDatasetMeta(o),t=a.data,r=a.controller,r!==e&&r.configure();break}if(!t)return 0;for(o=0,s=t.length;o<s;++o)l=r.resolveDataElementOptions(o),"inner"!==l.borderAlign&&(i=Math.max(i,l.borderWidth||0,l.hoverBorderWidth||0));return i}getMaxOffset(t){let e=0;for(let i=0,n=t.length;i<n;++i){const t=this.resolveDataElementOptions(i);e=Math.max(e,t.offset||0,t.hoverOffset||0)}return e}_getRingWeightOffset(t){let e=0;for(let i=0;i<t;++i)this.chart.isDatasetVisible(i)&&(e+=this._getRingWeight(i));return e}_getRingWeight(t){return Math.max(K(this.chart.data.datasets[t].weight,1),0)}_getVisibleDatasetWeightTotal(){return this._getRingWeightOffset(this.chart.data.datasets.length)||1}}go.id="doughnut",go.defaults={datasetElementType:!1,dataElementType:"arc",animation:{animateRotate:!0,animateScale:!1},animations:{numbers:{type:"number",properties:["circumference","endAngle","innerRadius","outerRadius","startAngle","x","y","offset","borderWidth","spacing"]}},cutout:"50%",rotation:0,circumference:360,radius:"100%",spacing:0,indexAxis:"r"},go.descriptors={_scriptable:t=>"spacing"!==t,_indexable:t=>"spacing"!==t},go.overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i}}=t.legend.options;return e.labels.map(((e,n)=>{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}},tooltip:{callbacks:{title:()=>"",label(t){let e=t.label;const i=": "+t.formattedValue;return Y(e)?(e=e.slice(),e[0]+=i):e+=i,e}}}}};class po extends Ei{initialize(){this.enableOptionSharing=!0,super.initialize()}update(t){const e=this,i=e._cachedMeta,{dataset:n,data:o=[],_dataset:s}=i,a=e.chart._animationsDisabled;let{start:r,count:l}=function(t,e,i){const n=e.length;let o=0,s=n;if(t._sorted){const{iScale:a,_parsed:r}=t,l=a.axis,{min:c,max:h,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(o=Ht(Math.min(se(r,a.axis,c).lo,i?n:se(e,l,a.getPixelForValue(c)).lo),0,n-1)),s=u?Ht(Math.max(se(r,a.axis,h).hi+1,i?0:se(e,l,a.getPixelForValue(h)).hi+1),o,n)-o:n-o}return{start:o,count:s}}(i,o,a);e._drawStart=r,e._drawCount=l,function(t){const{xScale:e,yScale:i,_scaleRanges:n}=t,o={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!n)return t._scaleRanges=o,!0;const s=n.xmin!==e.min||n.xmax!==e.max||n.ymin!==i.min||n.ymax!==i.max;return Object.assign(n,o),s}(i)&&(r=0,l=o.length),n._decimated=!!s._decimated,n.points=o;const c=e.resolveDatasetElementOptions(t);e.options.showLine||(c.borderWidth=0),c.segment=e.options.segment,e.updateElement(n,void 0,{animated:!a,options:c},t),e.updateElements(o,r,l,t)}updateElements(t,e,i,n){const o=this,s="reset"===n,{iScale:a,vScale:r,_stacked:l}=o._cachedMeta,c=o.resolveDataElementOptions(e,n),h=o.getSharedOptions(c),d=o.includeOptions(n,h),u=a.axis,f=r.axis,g=o.options.spanGaps,p=Tt(g)?g:Number.POSITIVE_INFINITY,m=o.chart._animationsDisabled||s||"none"===n;let x=e>0&&o.getParsed(e-1);for(let c=e;c<e+i;++c){const e=t[c],i=o.getParsed(c),g=m?e:{},b=$(i[f]),_=g[u]=a.getPixelForValue(i[u],c),y=g[f]=s||b?r.getBasePixel():r.getPixelForValue(l?o.applyStack(r,i,l):i[f],c);g.skip=isNaN(_)||isNaN(y)||b,g.stop=c>0&&i[u]-x[u]>p,g.parsed=i,d&&(g.options=h||o.resolveDataElementOptions(c,e.active?"active":n)),m||o.updateElement(e,c,g,n),x=i}o.updateSharedOptions(h,n,c)}getMaxOverflow(){const t=this,e=t._cachedMeta,i=e.dataset,n=i.options&&i.options.borderWidth||0,o=e.data||[];if(!o.length)return n;const s=o[0].size(t.resolveDataElementOptions(0)),a=o[o.length-1].size(t.resolveDataElementOptions(o.length-1));return Math.max(n,s,a)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}}po.id="line",po.defaults={datasetElementType:"line",dataElementType:"point",showLine:!0,spanGaps:!1},po.overrides={scales:{_index_:{type:"category"},_value_:{type:"linear"}}};class mo extends Ei{constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,n=i.data.labels||[],o=Fi(e._parsed[t].r,i.options.locale);return{label:n[t]||"",value:o}}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}_updateRadius(){const t=this,e=t.chart,i=e.chartArea,n=e.options,o=Math.min(i.right-i.left,i.bottom-i.top),s=Math.max(o/2,0),a=(s-Math.max(n.cutoutPercentage?s/100*n.cutoutPercentage:1,0))/e.getVisibleDatasetCount();t.outerRadius=s-a*t.index,t.innerRadius=t.outerRadius-a}updateElements(t,e,i,n){const o=this,s="reset"===n,a=o.chart,r=o.getDataset(),l=a.options.animation,c=o._cachedMeta.rScale,h=c.xCenter,d=c.yCenter,u=c.getIndexAngle(0)-.5*bt;let f,g=u;const p=360/o.countVisibleElements();for(f=0;f<e;++f)g+=o._computeAngle(f,n,p);for(f=e;f<e+i;f++){const e=t[f];let i=g,m=g+o._computeAngle(f,n,p),x=a.getDataVisibility(f)?c.getDistanceFromCenterForValue(r.data[f]):0;g=m,s&&(l.animateScale&&(x=0),l.animateRotate&&(i=m=u));const b={x:h,y:d,innerRadius:0,outerRadius:x,startAngle:i,endAngle:m,options:o.resolveDataElementOptions(f,e.active?"active":n)};o.updateElement(e,f,b,n)}}countVisibleElements(){const t=this.getDataset(),e=this._cachedMeta;let i=0;return e.data.forEach(((e,n)=>{!isNaN(t.data[n])&&this.chart.getDataVisibility(n)&&i++})),i}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?Et(this.resolveDataElementOptions(t,e).angle||i):0}}mo.id="polarArea",mo.defaults={dataElementType:"arc",animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:"number",properties:["x","y","startAngle","endAngle","innerRadius","outerRadius"]}},indexAxis:"r",startAngle:0},mo.overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i}}=t.legend.options;return e.labels.map(((e,n)=>{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}},tooltip:{callbacks:{title:()=>"",label:t=>t.chart.data.labels[t.dataIndex]+": "+t.formattedValue}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};class xo extends go{}xo.id="pie",xo.defaults={cutout:0,rotation:0,circumference:360,radius:"100%"};class bo extends Ei{getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}update(t){const e=this,i=e._cachedMeta,n=i.dataset,o=i.data||[],s=i.iScale.getLabels();if(n.points=o,"resize"!==t){const i=e.resolveDatasetElementOptions(t);e.options.showLine||(i.borderWidth=0);const a={_loop:!0,_fullLoop:s.length===o.length,options:i};e.updateElement(n,void 0,a,t)}e.updateElements(o,0,o.length,t)}updateElements(t,e,i,n){const o=this,s=o.getDataset(),a=o._cachedMeta.rScale,r="reset"===n;for(let l=e;l<e+i;l++){const e=t[l],i=o.resolveDataElementOptions(l,e.active?"active":n),c=a.getPointPositionForValue(l,s.data[l]),h=r?a.xCenter:c.x,d=r?a.yCenter:c.y,u={x:h,y:d,angle:c.angle,skip:isNaN(h)||isNaN(d),options:i};o.updateElement(e,l,u,n)}}}bo.id="radar",bo.defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}},bo.overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};class _o extends po{}_o.id="scatter",_o.defaults={showLine:!1,fill:!1},_o.overrides={interaction:{mode:"point"},plugins:{tooltip:{callbacks:{title:()=>"",label:t=>"("+t.label+", "+t.formattedValue+")"}}},scales:{x:{type:"linear"},y:{type:"linear"}}};var yo=Object.freeze({__proto__:null,BarController:uo,BubbleController:fo,DoughnutController:go,LineController:po,PolarAreaController:mo,PieController:xo,RadarController:bo,ScatterController:_o});function vo(t,e,i){const{startAngle:n,pixelMargin:o,x:s,y:a,outerRadius:r,innerRadius:l}=e;let c=o/r;t.beginPath(),t.arc(s,a,r,n-c,i+c),l>o?(c=o/l,t.arc(s,a,l,i+c,n-c,!0)):t.arc(s,a,o,i+Mt,n-Mt),t.closePath(),t.clip()}function wo(t,e,i,n){const o=Ee(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const s=(i-e)/2,a=Math.min(s,n*e/2),r=t=>{const e=(i-Math.min(s,t))*n/2;return Ht(t,0,Math.min(s,e))};return{outerStart:r(o.outerStart),outerEnd:r(o.outerEnd),innerStart:Ht(o.innerStart,0,a),innerEnd:Ht(o.innerEnd,0,a)}}function Mo(t,e,i,n){return{x:i+t*Math.cos(e),y:n+t*Math.sin(e)}}function ko(t,e,i,n,o){const{x:s,y:a,startAngle:r,pixelMargin:l,innerRadius:c}=e,h=Math.max(e.outerRadius+n+i-l,0),d=c>0?c+n+i+l:0;let u=0;const f=o-r;if(n){const t=((c>0?c-n:0)+(h>0?h-n:0))/2;u=(f-(0!==t?f*t/(t+n):f))/2}const g=(f-Math.max(.001,f*h-i/bt)/h)/2,p=r+g+u,m=o-g-u,{outerStart:x,outerEnd:b,innerStart:_,innerEnd:y}=wo(e,d,h,m-p),v=h-x,w=h-b,M=p+x/v,k=m-b/w,S=d+_,P=d+y,D=p+_/S,C=m-y/P;if(t.beginPath(),t.arc(s,a,h,M,k),b>0){const e=Mo(w,k,s,a);t.arc(e.x,e.y,b,k,m+Mt)}const O=Mo(P,m,s,a);if(t.lineTo(O.x,O.y),y>0){const e=Mo(P,C,s,a);t.arc(e.x,e.y,y,m+Mt,C+Math.PI)}if(t.arc(s,a,d,m-y/d,p+_/d,!0),_>0){const e=Mo(S,D,s,a);t.arc(e.x,e.y,_,D+Math.PI,p-Mt)}const T=Mo(v,p,s,a);if(t.lineTo(T.x,T.y),x>0){const e=Mo(v,M,s,a);t.arc(e.x,e.y,x,p-Mt,M)}t.closePath()}function So(t,e,i,n,o){const{options:s}=e,a="inner"===s.borderAlign;s.borderWidth&&(a?(t.lineWidth=2*s.borderWidth,t.lineJoin="round"):(t.lineWidth=s.borderWidth,t.lineJoin="bevel"),e.fullCircles&&function(t,e,i){const{x:n,y:o,startAngle:s,pixelMargin:a,fullCircles:r}=e,l=Math.max(e.outerRadius-a,0),c=e.innerRadius+a;let h;for(i&&vo(t,e,s+_t),t.beginPath(),t.arc(n,o,c,s+_t,s,!0),h=0;h<r;++h)t.stroke();for(t.beginPath(),t.arc(n,o,l,s,s+_t),h=0;h<r;++h)t.stroke()}(t,e,a),a&&vo(t,e,o),ko(t,e,i,n,o),t.stroke())}class Po extends zi{constructor(t){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,t&&Object.assign(this,t)}inRange(t,e,i){const n=this.getProps(["x","y"],i),{angle:o,distance:s}=Ft(n,{x:t,y:e}),{startAngle:a,endAngle:r,innerRadius:l,outerRadius:c,circumference:h}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],i),d=this.options.spacing/2;return(h>=_t||Nt(o,a,r))&&(s>=l+d&&s<=c+d)}getCenterPoint(t){const{x:e,y:i,startAngle:n,endAngle:o,innerRadius:s,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius","circumference"],t),{offset:r,spacing:l}=this.options,c=(n+o)/2,h=(s+a+l+r)/2;return{x:e+Math.cos(c)*h,y:i+Math.sin(c)*h}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const e=this,{options:i,circumference:n}=e,o=(i.offset||0)/2,s=(i.spacing||0)/2;if(e.pixelMargin="inner"===i.borderAlign?.33:0,e.fullCircles=n>_t?Math.floor(n/_t):0,0===n||e.innerRadius<0||e.outerRadius<0)return;t.save();let a=0;if(o){a=o/2;const i=(e.startAngle+e.endAngle)/2;t.translate(Math.cos(i)*a,Math.sin(i)*a),e.circumference>=bt&&(a=o)}t.fillStyle=i.backgroundColor,t.strokeStyle=i.borderColor;const r=function(t,e,i,n){const{fullCircles:o,startAngle:s,circumference:a}=e;let r=e.endAngle;if(o){ko(t,e,i,n,s+_t);for(let e=0;e<o;++e)t.fill();isNaN(a)||(r=s+a%_t,a%_t==0&&(r+=_t))}return ko(t,e,i,n,r),t.fill(),r}(t,e,a,s);So(t,e,a,s,r),t.restore()}}function Do(t,e,i=e){t.lineCap=K(i.borderCapStyle,e.borderCapStyle),t.setLineDash(K(i.borderDash,e.borderDash)),t.lineDashOffset=K(i.borderDashOffset,e.borderDashOffset),t.lineJoin=K(i.borderJoinStyle,e.borderJoinStyle),t.lineWidth=K(i.borderWidth,e.borderWidth),t.strokeStyle=K(i.borderColor,e.borderColor)}function Co(t,e,i){t.lineTo(i.x,i.y)}function Oo(t,e,i={}){const n=t.length,{start:o=0,end:s=n-1}=i,{start:a,end:r}=e,l=Math.max(o,a),c=Math.min(s,r),h=o<a&&s<a||o>r&&s>r;return{count:n,start:l,loop:e.loop,ilen:c<l&&!h?n+c-l:c-l}}function To(t,e,i,n){const{points:o,options:s}=e,{count:a,start:r,loop:l,ilen:c}=Oo(o,i,n),h=function(t){return t.stepped?Jt:t.tension||"monotone"===t.cubicInterpolationMode?te:Co}(s);let d,u,f,{move:g=!0,reverse:p}=n||{};for(d=0;d<=c;++d)u=o[(r+(p?c-d:d))%a],u.skip||(g?(t.moveTo(u.x,u.y),g=!1):h(t,f,u,p,s.stepped),f=u);return l&&(u=o[(r+(p?c:0))%a],h(t,f,u,p,s.stepped)),!!l}function Ao(t,e,i,n){const o=e.points,{count:s,start:a,ilen:r}=Oo(o,i,n),{move:l=!0,reverse:c}=n||{};let h,d,u,f,g,p,m=0,x=0;const b=t=>(a+(c?r-t:t))%s,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=o[b(0)],t.moveTo(d.x,d.y)),h=0;h<=r;++h){if(d=o[b(h)],d.skip)continue;const e=d.x,i=d.y,n=0|e;n===u?(i<f?f=i:i>g&&(g=i),m=(x*m+e)/++x):(_(),t.lineTo(e,i),u=n,x=0,f=g=i),p=i}_()}function Lo(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?Ao:To}Po.id="arc",Po.defaults={borderAlign:"center",borderColor:"#fff",borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:void 0},Po.defaultRoutes={backgroundColor:"backgroundColor"};const Ro="function"==typeof Path2D;function Eo(t,e,i,n){Ro&&1===e.segments.length?function(t,e,i,n){let o=e._path;o||(o=e._path=new Path2D,e.path(o,i,n)&&o.closePath()),Do(t,e.options),t.stroke(o)}(t,e,i,n):function(t,e,i,n){const{segments:o,options:s}=e,a=Lo(e);for(const r of o)Do(t,s,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+n-1})&&t.closePath(),t.stroke()}(t,e,i,n)}class zo extends zi{constructor(t){super(),this.animated=!0,this.options=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this,n=i.options;if((n.tension||"monotone"===n.cubicInterpolationMode)&&!n.stepped&&!i._pointsUpdated){const o=n.spanGaps?i._loop:i._fullLoop;pn(i._points,n,t,o,e),i._pointsUpdated=!0}}set points(t){const e=this;e._points=t,delete e._segments,delete e._path,e._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=Pn(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this,n=i.options,o=t[e],s=i.points,a=Sn(i,{property:e,start:o,end:o});if(!a.length)return;const r=[],l=function(t){return t.stepped?xn:t.tension||"monotone"===t.cubicInterpolationMode?bn:mn}(n);let c,h;for(c=0,h=a.length;c<h;++c){const{start:i,end:h}=a[c],d=s[i],u=s[h];if(d===u){r.push(d);continue}const f=l(d,u,Math.abs((o-d[e])/(u[e]-d[e])),n.stepped);f[e]=t[e],r.push(f)}return 1===r.length?r[0]:r}pathSegment(t,e,i){return Lo(this)(t,this,e,i)}path(t,e,i){const n=this,o=n.segments,s=Lo(n);let a=n._loop;e=e||0,i=i||n.points.length-e;for(const r of o)a&=s(t,n,r,{start:e,end:e+i-1});return!!a}draw(t,e,i,n){const o=this,s=o.options||{};(o.points||[]).length&&s.borderWidth&&(t.save(),Eo(t,o,i,n),t.restore(),o.animated&&(o._pointsUpdated=!1,o._path=void 0))}}function Io(t,e,i,n){const o=t.options,{[i]:s}=t.getProps([i],n);return Math.abs(e-s)<o.radius+o.hitRadius}zo.id="line",zo.defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0},zo.defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"},zo.descriptors={_scriptable:!0,_indexable:t=>"borderDash"!==t&&"fill"!==t};class Fo extends zi{constructor(t){super(),this.options=void 0,this.parsed=void 0,this.skip=void 0,this.stop=void 0,t&&Object.assign(this,t)}inRange(t,e,i){const n=this.options,{x:o,y:s}=this.getProps(["x","y"],i);return Math.pow(t-o,2)+Math.pow(e-s,2)<Math.pow(n.hitRadius+n.radius,2)}inXRange(t,e){return Io(this,t,"x",e)}inYRange(t,e){return Io(this,t,"y",e)}getCenterPoint(t){const{x:e,y:i}=this.getProps(["x","y"],t);return{x:e,y:i}}size(t){let e=(t=t||this.options||{}).radius||0;e=Math.max(e,e&&t.hoverRadius||0);return 2*(e+(e&&t.borderWidth||0))}draw(t){const e=this,i=e.options;e.skip||i.radius<.1||(t.strokeStyle=i.borderColor,t.lineWidth=i.borderWidth,t.fillStyle=i.backgroundColor,Kt(t,i,e.x,e.y))}getRange(){const t=this.options||{};return t.radius+t.hitRadius}}function Bo(t,e){const{x:i,y:n,base:o,width:s,height:a}=t.getProps(["x","y","base","width","height"],e);let r,l,c,h,d;return t.horizontal?(d=a/2,r=Math.min(i,o),l=Math.max(i,o),c=n-d,h=n+d):(d=s/2,r=i-d,l=i+d,c=Math.min(n,o),h=Math.max(n,o)),{left:r,top:c,right:l,bottom:h}}function Vo(t){let e=t.options.borderSkipped;const i={};return e?(e=t.horizontal?Wo(e,"left","right",t.base>t.x):Wo(e,"bottom","top",t.base<t.y),i[e]=!0,i):i}function Wo(t,e,i,n){var o,s,a;return n?(a=i,t=No(t=(o=t)===(s=e)?a:o===a?s:o,i,e)):t=No(t,e,i),t}function No(t,e,i){return"start"===t?e:"end"===t?i:t}function Ho(t,e,i,n){return t?0:Math.max(Math.min(e,n),i)}function jo(t){const e=Bo(t),i=e.right-e.left,n=e.bottom-e.top,o=function(t,e,i){const n=t.options.borderWidth,o=Vo(t),s=ze(n);return{t:Ho(o.top,s.top,0,i),r:Ho(o.right,s.right,0,e),b:Ho(o.bottom,s.bottom,0,i),l:Ho(o.left,s.left,0,e)}}(t,i/2,n/2),s=function(t,e,i){const{enableBorderRadius:n}=t.getProps(["enableBorderRadius"]),o=t.options.borderRadius,s=Ie(o),a=Math.min(e,i),r=Vo(t),l=n||U(o);return{topLeft:Ho(!l||r.top||r.left,s.topLeft,0,a),topRight:Ho(!l||r.top||r.right,s.topRight,0,a),bottomLeft:Ho(!l||r.bottom||r.left,s.bottomLeft,0,a),bottomRight:Ho(!l||r.bottom||r.right,s.bottomRight,0,a)}}(t,i/2,n/2);return{outer:{x:e.left,y:e.top,w:i,h:n,radius:s},inner:{x:e.left+o.l,y:e.top+o.t,w:i-o.l-o.r,h:n-o.t-o.b,radius:{topLeft:Math.max(0,s.topLeft-Math.max(o.t,o.l)),topRight:Math.max(0,s.topRight-Math.max(o.t,o.r)),bottomLeft:Math.max(0,s.bottomLeft-Math.max(o.b,o.l)),bottomRight:Math.max(0,s.bottomRight-Math.max(o.b,o.r))}}}}function $o(t,e,i,n){const o=null===e,s=null===i,a=t&&!(o&&s)&&Bo(t,n);return a&&(o||e>=a.left&&e<=a.right)&&(s||i>=a.top&&i<=a.bottom)}function Yo(t,e){t.rect(e.x,e.y,e.w,e.h)}Fo.id="point",Fo.defaults={borderWidth:1,hitRadius:1,hoverBorderWidth:1,hoverRadius:4,pointStyle:"circle",radius:3,rotation:0},Fo.defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};class Uo extends zi{constructor(t){super(),this.options=void 0,this.horizontal=void 0,this.base=void 0,this.width=void 0,this.height=void 0,t&&Object.assign(this,t)}draw(t){const e=this.options,{inner:i,outer:n}=jo(this),o=(s=n.radius).topLeft||s.topRight||s.bottomLeft||s.bottomRight?ne:Yo;var s;t.save(),n.w===i.w&&n.h===i.h||(t.beginPath(),o(t,n),t.clip(),o(t,i),t.fillStyle=e.borderColor,t.fill("evenodd")),t.beginPath(),o(t,i),t.fillStyle=e.backgroundColor,t.fill(),t.restore()}inRange(t,e,i){return $o(this,t,e,i)}inXRange(t,e){return $o(this,t,null,e)}inYRange(t,e){return $o(this,null,t,e)}getCenterPoint(t){const{x:e,y:i,base:n,horizontal:o}=this.getProps(["x","y","base","horizontal"],t);return{x:o?(e+n)/2:e,y:o?i:(i+n)/2}}getRange(t){return"x"===t?this.width/2:this.height/2}}Uo.id="bar",Uo.defaults={borderSkipped:"start",borderWidth:0,borderRadius:0,enableBorderRadius:!0,pointStyle:void 0},Uo.defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};var Xo=Object.freeze({__proto__:null,ArcElement:Po,LineElement:zo,PointElement:Fo,BarElement:Uo});function qo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{value:e})}}function Ko(t){t.data.datasets.forEach((t=>{qo(t)}))}var Go={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Ko(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:s,indexAxis:a}=e,r=t.getDatasetMeta(o),l=s||e.data;if("y"===Ve([a,t.options.indexAxis]))return;if("line"!==r.type)return;const c=t.scales[r.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let h,{start:d,count:u}=function(t,e){const i=e.length;let n,o=0;const{iScale:s}=t,{min:a,max:r,minDefined:l,maxDefined:c}=s.getUserBounds();return l&&(o=Ht(se(e,s.axis,a).lo,0,i-1)),n=c?Ht(se(e,s.axis,r).hi+1,o,i)-o:i-o,{start:o,count:n}}(r,l);if(u<=4*n)qo(e);else{switch($(s)&&(e._data=l,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":h=function(t,e,i,n,o){const s=o.samples||n;if(s>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(s-2);let l=0;const c=e+i-1;let h,d,u,f,g,p=e;for(a[l++]=t[p],h=0;h<s-2;h++){let n,o=0,s=0;const c=Math.floor((h+1)*r)+1+e,m=Math.min(Math.floor((h+2)*r)+1,i)+e,x=m-c;for(n=c;n<m;n++)o+=t[n].x,s+=t[n].y;o/=x,s/=x;const b=Math.floor(h*r)+1+e,_=Math.floor((h+1)*r)+1+e,{x:y,y:v}=t[p];for(u=f=-1,n=b;n<_;n++)f=.5*Math.abs((y-o)*(t[n].y-v)-(y-t[n].x)*(s-v)),f>u&&(u=f,d=t[n],g=n);a[l++]=d,p=g}return a[l++]=t[c],a}(l,d,u,n,i);break;case"min-max":h=function(t,e,i,n){let o,s,a,r,l,c,h,d,u,f,g=0,p=0;const m=[],x=e+i-1,b=t[e].x,_=t[x].x-b;for(o=e;o<e+i;++o){s=t[o],a=(s.x-b)/_*n,r=s.y;const e=0|a;if(e===l)r<u?(u=r,c=o):r>f&&(f=r,h=o),g=(p*g+s.x)/++p;else{const i=o-1;if(!$(c)&&!$(h)){const e=Math.min(c,h),n=Math.max(c,h);e!==d&&e!==i&&m.push({...t[e],x:g}),n!==d&&n!==i&&m.push({...t[n],x:g})}o>0&&i!==d&&m.push(t[i]),m.push(s),l=e,p=0,u=f=r,c=h=d=o}}return m}(l,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=h}}))},destroy(t){Ko(t)}};function Zo(t,e,i){const n=function(t){const e=t.options,i=e.fill;let n=K(i&&i.target,i);return void 0===n&&(n=!!e.backgroundColor),!1!==n&&null!==n&&(!0===n?"origin":n)}(t);if(U(n))return!isNaN(n.value)&&n;let o=parseFloat(n);return X(o)&&Math.floor(o)===o?("-"!==n[0]&&"+"!==n[0]||(o=e+o),!(o===e||o<0||o>=i)&&o):["origin","start","end","stack"].indexOf(n)>=0&&n}class Qo{constructor(t){this.x=t.x,this.y=t.y,this.radius=t.radius}pathSegment(t,e,i){const{x:n,y:o,radius:s}=this;return e=e||{start:0,end:_t},t.arc(n,o,s,e.end,e.start,!0),!i.bounds}interpolate(t){const{x:e,y:i,radius:n}=this,o=t.angle;return{x:e+Math.cos(o)*n,y:i+Math.sin(o)*n,angle:o}}}function Jo(t){return(t.scale||{}).getPointPositionForValue?function(t){const{scale:e,fill:i}=t,n=e.options,o=e.getLabels().length,s=[],a=n.reverse?e.max:e.min,r=n.reverse?e.min:e.max;let l,c,h;if(h="start"===i?a:"end"===i?r:U(i)?i.value:e.getBaseValue(),n.grid.circular)return c=e.getPointPositionForValue(0,a),new Qo({x:c.x,y:c.y,radius:e.getDistanceFromCenterForValue(h)});for(l=0;l<o;++l)s.push(e.getPointPositionForValue(l,h));return s}(t):function(t){const{scale:e={},fill:i}=t;let n,o=null;return"start"===i?o=e.bottom:"end"===i?o=e.top:U(i)?o=e.getPixelForValue(i.value):e.getBasePixel&&(o=e.getBasePixel()),X(o)?(n=e.isHorizontal(),{x:n?o:null,y:n?null:o}):null}(t)}function ts(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function es(t){const{chart:e,scale:i,index:n,line:o}=t,s=[],a=o.segments,r=o.points,l=function(t,e){const i=[],n=t.getSortedVisibleDatasetMetas();for(let t=0;t<n.length;t++){const o=n[t];if(o.index===e)break;is(o)&&i.unshift(o.dataset)}return i}(e,n);l.push(ss({x:null,y:i.bottom},o));for(let t=0;t<a.length;t++){const e=a[t];for(let t=e.start;t<=e.end;t++)ns(s,r[t],l)}return new zo({points:s,options:{}})}const is=t=>"line"===t.type&&!t.hidden;function ns(t,e,i){const n=[];for(let o=0;o<i.length;o++){const s=i[o],{first:a,last:r,point:l}=os(s,e,"x");if(!(!l||a&&r))if(a)n.unshift(l);else if(t.push(l),!r)break}t.push(...n)}function os(t,e,i){const n=t.interpolate(e,i);if(!n)return{};const o=n[i],s=t.segments,a=t.points;let r=!1,l=!1;for(let t=0;t<s.length;t++){const e=s[t],n=a[e.start][i],c=a[e.end][i];if(o>=n&&o<=c){r=o===n,l=o===c;break}}return{first:r,last:l,point:n}}function ss(t,e){let i=[],n=!1;return Y(t)?(n=!0,i=t):i=function(t,e){const{x:i=null,y:n=null}=t||{},o=e.points,s=[];return e.segments.forEach((({start:t,end:e})=>{e=ts(t,e,o);const a=o[t],r=o[e];null!==n?(s.push({x:a.x,y:n}),s.push({x:r.x,y:n})):null!==i&&(s.push({x:i,y:a.y}),s.push({x:i,y:r.y}))})),s}(t,e),i.length?new zo({points:i,options:{tension:0},_loop:n,_fullLoop:n}):null}function as(t,e,i){let n=t[e].fill;const o=[e];let s;if(!i)return n;for(;!1!==n&&-1===o.indexOf(n);){if(!X(n))return n;if(s=t[n],!s)return!1;if(s.visible)return n;o.push(n),n=s.fill}return!1}function rs(t,e,i){t.beginPath(),e.path(t),t.lineTo(e.last().x,i),t.lineTo(e.first().x,i),t.closePath(),t.clip()}function ls(t,e,i,n){if(n)return;let o=e[t],s=i[t];return"angle"===t&&(o=Wt(o),s=Wt(s)),{property:t,start:o,end:s}}function cs(t,e,i,n){return t&&e?n(t[i],e[i]):t?t[i]:e?e[i]:0}function hs(t,e,i){const{top:n,bottom:o}=e.chart.chartArea,{property:s,start:a,end:r}=i||{};"x"===s&&(t.beginPath(),t.rect(a,n,r-a,o-n),t.clip())}function ds(t,e,i,n){const o=e.interpolate(i,n);o&&t.lineTo(o.x,o.y)}function us(t,e){const{line:i,target:n,property:o,color:s,scale:a}=e,r=function(t,e,i){const n=t.segments,o=t.points,s=e.points,a=[];for(const t of n){let{start:n,end:r}=t;r=ts(n,r,o);const l=ls(i,o[n],o[r],t.loop);if(!e.segments){a.push({source:t,target:l,start:o[n],end:o[r]});continue}const c=Sn(e,l);for(const e of c){const n=ls(i,s[e.start],s[e.end],e.loop),r=kn(t,o,n);for(const t of r)a.push({source:t,target:e,start:{[i]:cs(l,n,"start",Math.max)},end:{[i]:cs(l,n,"end",Math.min)}})}}return a}(i,n,o);for(const{source:e,target:l,start:c,end:h}of r){const{style:{backgroundColor:r=s}={}}=e;t.save(),t.fillStyle=r,hs(t,a,ls(o,c,h)),t.beginPath();const d=!!i.pathSegment(t,e);d?t.closePath():ds(t,n,h,o);const u=!!n.pathSegment(t,l,{move:d,reverse:!0}),f=d&&u;f||ds(t,n,c,o),t.closePath(),t.fill(f?"evenodd":"nonzero"),t.restore()}}function fs(t,e,i){const n=function(t){const{chart:e,fill:i,line:n}=t;if(X(i))return function(t,e){const i=t.getDatasetMeta(e);return i&&t.isDatasetVisible(e)?i.dataset:null}(e,i);if("stack"===i)return es(t);const o=Jo(t);return o instanceof Qo?o:ss(o,n)}(e),{line:o,scale:s,axis:a}=e,r=o.options,l=r.fill,c=r.backgroundColor,{above:h=c,below:d=c}=l||{};n&&o.points.length&&(Zt(t,i),function(t,e){const{line:i,target:n,above:o,below:s,area:a,scale:r}=e,l=i._loop?"angle":e.axis;t.save(),"x"===l&&s!==o&&(rs(t,n,a.top),us(t,{line:i,target:n,color:o,scale:r,property:l}),t.restore(),t.save(),rs(t,n,a.bottom)),us(t,{line:i,target:n,color:s,scale:r,property:l}),t.restore()}(t,{line:o,target:n,above:h,below:d,area:i,scale:s,axis:a}),Qt(t))}var gs={id:"filler",afterDatasetsUpdate(t,e,i){const n=(t.data.datasets||[]).length,o=[];let s,a,r,l;for(a=0;a<n;++a)s=t.getDatasetMeta(a),r=s.dataset,l=null,r&&r.options&&r instanceof zo&&(l={visible:t.isDatasetVisible(a),index:a,fill:Zo(r,a,n),chart:t,axis:s.controller.options.indexAxis,scale:s.vScale,line:r}),s.$filler=l,o.push(l);for(a=0;a<n;++a)l=o[a],l&&!1!==l.fill&&(l.fill=as(o,a,i.propagate))},beforeDraw(t,e,i){const n="beforeDraw"===i.drawTime,o=t.getSortedVisibleDatasetMetas(),s=t.chartArea;for(let e=o.length-1;e>=0;--e){const i=o[e].$filler;i&&(i.line.updateControlPoints(s,i.axis),n&&fs(t.ctx,i,s))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const n=t.getSortedVisibleDatasetMetas();for(let e=n.length-1;e>=0;--e){const i=n[e].$filler;i&&fs(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const n=e.meta.$filler;n&&!1!==n.fill&&"beforeDatasetDraw"===i.drawTime&&fs(t.ctx,n,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const ps=(t,e)=>{let{boxHeight:i=e,boxWidth:n=e}=t;return t.usePointStyle&&(i=Math.min(i,e),n=Math.min(n,e)),{boxWidth:n,boxHeight:i,itemHeight:Math.max(e,i)}};class ms extends zi{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){const n=this;n.maxWidth=t,n.maxHeight=e,n._margins=i,n.setDimensions(),n.buildLabels(),n.fit()}setDimensions(){const t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=t._margins.left,t.right=t.width):(t.height=t.maxHeight,t.top=t._margins.top,t.bottom=t.height)}buildLabels(){const t=this,e=t.options.labels||{};let i=Q(e.generateLabels,[t.chart],t)||[];e.filter&&(i=i.filter((i=>e.filter(i,t.chart.data)))),e.sort&&(i=i.sort(((i,n)=>e.sort(i,n,t.chart.data)))),t.options.reverse&&i.reverse(),t.legendItems=i}fit(){const t=this,{options:e,ctx:i}=t;if(!e.display)return void(t.width=t.height=0);const n=e.labels,o=Be(n.font),s=o.size,a=t._computeTitleHeight(),{boxWidth:r,itemHeight:l}=ps(n,s);let c,h;i.font=o.string,t.isHorizontal()?(c=t.maxWidth,h=t._fitRows(a,s,r,l)+10):(h=t.maxHeight,c=t._fitCols(a,s,r,l)+10),t.width=Math.min(c,e.maxWidth||t.maxWidth),t.height=Math.min(h,e.maxHeight||t.maxHeight)}_fitRows(t,e,i,n){const o=this,{ctx:s,maxWidth:a,options:{labels:{padding:r}}}=o,l=o.legendHitBoxes=[],c=o.lineWidths=[0],h=n+r;let d=t;s.textAlign="left",s.textBaseline="middle";let u=-1,f=-h;return o.legendItems.forEach(((t,o)=>{const g=i+e/2+s.measureText(t.text).width;(0===o||c[c.length-1]+g+2*r>a)&&(d+=h,c[c.length-(o>0?0:1)]=0,f+=h,u++),l[o]={left:0,top:f,row:u,width:g,height:n},c[c.length-1]+=g+r})),d}_fitCols(t,e,i,n){const o=this,{ctx:s,maxHeight:a,options:{labels:{padding:r}}}=o,l=o.legendHitBoxes=[],c=o.columnSizes=[],h=a-t;let d=r,u=0,f=0,g=0,p=0;return o.legendItems.forEach(((t,o)=>{const a=i+e/2+s.measureText(t.text).width;o>0&&f+n+2*r>h&&(d+=u+r,c.push({width:u,height:f}),g+=u+r,p++,u=f=0),l[o]={left:g,top:f,col:p,width:a,height:n},u=Math.max(u,a),f+=n+r})),d+=u,c.push({width:u,height:f}),d}adjustHitBoxes(){const t=this;if(!t.options.display)return;const e=t._computeTitleHeight(),{legendHitBoxes:i,options:{align:n,labels:{padding:s},rtl:a}}=t;if(this.isHorizontal()){let r=0,l=o(n,t.left+s,t.right-t.lineWidths[r]);for(const a of i)r!==a.row&&(r=a.row,l=o(n,t.left+s,t.right-t.lineWidths[r])),a.top+=t.top+e+s,a.left=l,l+=a.width+s;if(a){const e=i.reduce(((t,e)=>(t[e.row]=t[e.row]||[],t[e.row].push(e),t)),{}),n=[];Object.keys(e).forEach((t=>{e[t].reverse(),n.push(...e[t])})),t.legendHitBoxes=n}}else{let a=0,r=o(n,t.top+e+s,t.bottom-t.columnSizes[a].height);for(const l of i)l.col!==a&&(a=l.col,r=o(n,t.top+e+s,t.bottom-t.columnSizes[a].height)),l.top=r,l.left+=t.left+s,r+=l.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){const t=this;if(t.options.display){const e=t.ctx;Zt(e,t),t._draw(),Qt(e)}}_draw(){const t=this,{options:e,columnSizes:i,lineWidths:n,ctx:a}=t,{align:r,labels:l}=e,c=xt.color,h=_n(e.rtl,t.left,t.width),d=Be(l.font),{color:u,padding:f}=l,g=d.size,p=g/2;let m;t.drawTitle(),a.textAlign=h.textAlign("left"),a.textBaseline="middle",a.lineWidth=.5,a.font=d.string;const{boxWidth:x,boxHeight:b,itemHeight:_}=ps(l,g),y=t.isHorizontal(),v=this._computeTitleHeight();m=y?{x:o(r,t.left+f,t.right-n[0]),y:t.top+f+v,line:0}:{x:t.left+f,y:o(r,t.top+v+f,t.bottom-i[0].height),line:0},yn(t.ctx,e.textDirection);const w=_+f;t.legendItems.forEach(((M,k)=>{a.strokeStyle=M.fontColor||u,a.fillStyle=M.fontColor||u;const S=a.measureText(M.text).width,P=h.textAlign(M.textAlign||(M.textAlign=l.textAlign)),D=x+p+S;let C=m.x,O=m.y;h.setWidth(t.width),y?k>0&&C+D+f>t.right&&(O=m.y+=w,m.line++,C=m.x=o(r,t.left+f,t.right-n[m.line])):k>0&&O+w>t.bottom&&(C=m.x=C+i[m.line].width+f,m.line++,O=m.y=o(r,t.top+v+f,t.bottom-i[m.line].height));!function(t,e,i){if(isNaN(x)||x<=0||isNaN(b)||b<0)return;a.save();const n=K(i.lineWidth,1);if(a.fillStyle=K(i.fillStyle,c),a.lineCap=K(i.lineCap,"butt"),a.lineDashOffset=K(i.lineDashOffset,0),a.lineJoin=K(i.lineJoin,"miter"),a.lineWidth=n,a.strokeStyle=K(i.strokeStyle,c),a.setLineDash(K(i.lineDash,[])),l.usePointStyle){const o={radius:x*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},s=h.xPlus(t,x/2);Kt(a,o,s,e+p)}else{const o=e+Math.max((g-b)/2,0),s=h.leftForLtr(t,x),r=Ie(i.borderRadius);a.beginPath(),Object.values(r).some((t=>0!==t))?ne(a,{x:s,y:o,w:x,h:b,radius:r}):a.rect(s,o,x,b),a.fill(),0!==n&&a.stroke()}a.restore()}(h.x(C),O,M),C=s(P,C+x+p,y?C+D:t.right,e.rtl),function(t,e,i){ee(a,i.text,t,e+_/2,d,{strikethrough:i.hidden,textAlign:h.textAlign(i.textAlign)})}(h.x(C),O,M),y?m.x+=D+f:m.y+=w})),vn(t.ctx,e.textDirection)}drawTitle(){const t=this,e=t.options,i=e.title,s=Be(i.font),a=Fe(i.padding);if(!i.display)return;const r=_n(e.rtl,t.left,t.width),l=t.ctx,c=i.position,h=s.size/2,d=a.top+h;let u,f=t.left,g=t.width;if(this.isHorizontal())g=Math.max(...t.lineWidths),u=t.top+d,f=o(e.align,f,t.right-g);else{const i=t.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);u=d+o(e.align,t.top,t.bottom-i-e.labels.padding-t._computeTitleHeight())}const p=o(c,f,f+g);l.textAlign=r.textAlign(n(c)),l.textBaseline="middle",l.strokeStyle=i.color,l.fillStyle=i.color,l.font=s.string,ee(l,i.text,p,u,s)}_computeTitleHeight(){const t=this.options.title,e=Be(t.font),i=Fe(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){const i=this;let n,o,s;if(t>=i.left&&t<=i.right&&e>=i.top&&e<=i.bottom)for(s=i.legendHitBoxes,n=0;n<s.length;++n)if(o=s[n],t>=o.left&&t<=o.left+o.width&&e>=o.top&&e<=o.top+o.height)return i.legendItems[n];return null}handleEvent(t){const e=this,i=e.options;if(!function(t,e){if("mousemove"===t&&(e.onHover||e.onLeave))return!0;if(e.onClick&&("click"===t||"mouseup"===t))return!0;return!1}(t.type,i))return;const n=e._getLegendItemAt(t.x,t.y);if("mousemove"===t.type){const a=e._hoveredItem,r=(s=n,null!==(o=a)&&null!==s&&o.datasetIndex===s.datasetIndex&&o.index===s.index);a&&!r&&Q(i.onLeave,[t,a,e],e),e._hoveredItem=n,n&&!r&&Q(i.onHover,[t,n,e],e)}else n&&Q(i.onClick,[t,n,e],e);var o,s}}var xs={id:"legend",_element:ms,start(t,e,i){const n=t.legend=new ms({ctx:t.ctx,options:i,chart:t});Ze.configure(t,n,i),Ze.addBox(t,n)},stop(t){Ze.removeBox(t,t.legend),delete t.legend},beforeUpdate(t,e,i){const n=t.legend;Ze.configure(t,n,i),n.options=i},afterUpdate(t){const e=t.legend;e.buildLabels(),e.adjustHitBoxes()},afterEvent(t,e){e.replay||t.legend.handleEvent(e.event)},defaults:{display:!0,position:"top",align:"center",fullSize:!0,reverse:!1,weight:1e3,onClick(t,e,i){const n=e.datasetIndex,o=i.chart;o.isDatasetVisible(n)?(o.hide(n),e.hidden=!0):(o.show(n),e.hidden=!1)},onHover:null,onLeave:null,labels:{color:t=>t.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:n,textAlign:o,color:s}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const a=t.controller.getStyle(i?0:void 0),r=Fe(a.borderWidth);return{text:e[t.index].label,fillStyle:a.backgroundColor,fontColor:s,hidden:!t.visible,lineCap:a.borderCapStyle,lineDash:a.borderDash,lineDashOffset:a.borderDashOffset,lineJoin:a.borderJoinStyle,lineWidth:(r.width+r.height)/4,strokeStyle:a.borderColor,pointStyle:n||a.pointStyle,rotation:a.rotation,textAlign:o||a.textAlign,borderRadius:0,datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class bs extends zi{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this,n=i.options;if(i.left=0,i.top=0,!n.display)return void(i.width=i.height=i.right=i.bottom=0);i.width=i.right=t,i.height=i.bottom=e;const o=Y(n.text)?n.text.length:1;i._padding=Fe(n.padding);const s=o*Be(n.font).lineHeight+i._padding.height;i.isHorizontal()?i.height=s:i.width=s}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:n,right:s,options:a}=this,r=a.align;let l,c,h,d=0;return this.isHorizontal()?(c=o(r,i,s),h=e+t,l=s-i):("left"===a.position?(c=i+t,h=o(r,n,e),d=-.5*bt):(c=s-t,h=o(r,e,n),d=.5*bt),l=n-e),{titleX:c,titleY:h,maxWidth:l,rotation:d}}draw(){const t=this,e=t.ctx,i=t.options;if(!i.display)return;const o=Be(i.font),s=o.lineHeight/2+t._padding.top,{titleX:a,titleY:r,maxWidth:l,rotation:c}=t._drawArgs(s);ee(e,i.text,0,0,o,{color:i.color,maxWidth:l,rotation:c,textAlign:n(i.align),textBaseline:"middle",translation:[a,r]})}}var _s={id:"title",_element:bs,start(t,e,i){!function(t,e){const i=new bs({ctx:t.ctx,options:e,chart:t});Ze.configure(t,i,e),Ze.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;Ze.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const n=t.titleBlock;Ze.configure(t,n,i),n.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const ys=new WeakMap;var vs={id:"subtitle",start(t,e,i){const n=new bs({ctx:t.ctx,options:i,chart:t});Ze.configure(t,n,i),Ze.addBox(t,n),ys.set(t,n)},stop(t){Ze.removeBox(t,ys.get(t)),ys.delete(t)},beforeUpdate(t,e,i){const n=ys.get(t);Ze.configure(t,n,i),n.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const ws={average(t){if(!t.length)return!1;let e,i,n=0,o=0,s=0;for(e=0,i=t.length;e<i;++e){const i=t[e].element;if(i&&i.hasValue()){const t=i.tooltipPosition();n+=t.x,o+=t.y,++s}}return{x:n/s,y:o/s}},nearest(t,e){if(!t.length)return!1;let i,n,o,s=e.x,a=e.y,r=Number.POSITIVE_INFINITY;for(i=0,n=t.length;i<n;++i){const n=t[i].element;if(n&&n.hasValue()){const t=Bt(e,n.getCenterPoint());t<r&&(r=t,o=n)}}if(o){const t=o.tooltipPosition();s=t.x,a=t.y}return{x:s,y:a}}};function Ms(t,e){return e&&(Y(e)?Array.prototype.push.apply(t,e):t.push(e)),t}function ks(t){return("string"==typeof t||t instanceof String)&&t.indexOf("\n")>-1?t.split("\n"):t}function Ss(t,e){const{element:i,datasetIndex:n,index:o}=e,s=t.getDatasetMeta(n).controller,{label:a,value:r}=s.getLabelAndValue(o);return{chart:t,label:a,parsed:s.getParsed(o),raw:t.data.datasets[n].data[o],formattedValue:r,dataset:s.getDataset(),dataIndex:o,datasetIndex:n,element:i}}function Ps(t,e){const i=t._chart.ctx,{body:n,footer:o,title:s}=t,{boxWidth:a,boxHeight:r}=e,l=Be(e.bodyFont),c=Be(e.titleFont),h=Be(e.footerFont),d=s.length,u=o.length,f=n.length,g=Fe(e.padding);let p=g.height,m=0,x=n.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(x+=t.beforeBody.length+t.afterBody.length,d&&(p+=d*c.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),x){p+=f*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(x-f)*l.lineHeight+(x-1)*e.bodySpacing}u&&(p+=e.footerMarginTop+u*h.lineHeight+(u-1)*e.footerSpacing);let b=0;const _=function(t){m=Math.max(m,i.measureText(t).width+b)};return i.save(),i.font=c.string,J(t.title,_),i.font=l.string,J(t.beforeBody.concat(t.afterBody),_),b=e.displayColors?a+2:0,J(n,(t=>{J(t.before,_),J(t.lines,_),J(t.after,_)})),b=0,i.font=h.string,J(t.footer,_),i.restore(),m+=g.width,{width:m,height:p}}function Ds(t,e,i,n){const{x:o,width:s}=i,{width:a,chartArea:{left:r,right:l}}=t;let c="center";return"center"===n?c=o<=(r+l)/2?"left":"right":o<=s/2?c="left":o>=a-s/2&&(c="right"),function(t,e,i,n){const{x:o,width:s}=n,a=i.caretSize+i.caretPadding;return"left"===t&&o+s+a>e.width||"right"===t&&o-s-a<0||void 0}(c,t,e,i)&&(c="center"),c}function Cs(t,e,i){const n=e.yAlign||function(t,e){const{y:i,height:n}=e;return i<n/2?"top":i>t.height-n/2?"bottom":"center"}(t,i);return{xAlign:e.xAlign||Ds(t,e,i,n),yAlign:n}}function Os(t,e,i,n){const{caretSize:o,caretPadding:s,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,c=o+s,h=a+s;let d=function(t,e){let{x:i,width:n}=t;return"right"===e?i-=n:"center"===e&&(i-=n/2),i}(e,r);const u=function(t,e,i){let{y:n,height:o}=t;return"top"===e?n+=i:n-="bottom"===e?o+i:o/2,n}(e,l,c);return"center"===l?"left"===r?d+=c:"right"===r&&(d-=c):"left"===r?d-=h:"right"===r&&(d+=h),{x:Ht(d,0,n.width-e.width),y:Ht(u,0,n.height-e.height)}}function Ts(t,e,i){const n=Fe(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-n.right:t.x+n.left}function As(t){return Ms([],ks(t))}function Ls(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}class Rs extends zi{constructor(t){super(),this.opacity=0,this._active=[],this._chart=t._chart,this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.options=t.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(t){this.options=t,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const t=this,e=t._cachedAnimations;if(e)return e;const i=t._chart,n=t.options.setContext(t.getContext()),o=n.enabled&&i.options.animation&&n.animations,s=new wi(t._chart,o);return o._cacheable&&(t._cachedAnimations=Object.freeze(s)),s}getContext(){const t=this;return t.$context||(t.$context=(e=t._chart.getContext(),i=t,n=t._tooltipItems,Object.assign(Object.create(e),{tooltip:i,tooltipItems:n,type:"tooltip"})));var e,i,n}getTitle(t,e){const i=this,{callbacks:n}=e,o=n.beforeTitle.apply(i,[t]),s=n.title.apply(i,[t]),a=n.afterTitle.apply(i,[t]);let r=[];return r=Ms(r,ks(o)),r=Ms(r,ks(s)),r=Ms(r,ks(a)),r}getBeforeBody(t,e){return As(e.callbacks.beforeBody.apply(this,[t]))}getBody(t,e){const i=this,{callbacks:n}=e,o=[];return J(t,(t=>{const e={before:[],lines:[],after:[]},s=Ls(n,t);Ms(e.before,ks(s.beforeLabel.call(i,t))),Ms(e.lines,s.label.call(i,t)),Ms(e.after,ks(s.afterLabel.call(i,t))),o.push(e)})),o}getAfterBody(t,e){return As(e.callbacks.afterBody.apply(this,[t]))}getFooter(t,e){const i=this,{callbacks:n}=e,o=n.beforeFooter.apply(i,[t]),s=n.footer.apply(i,[t]),a=n.afterFooter.apply(i,[t]);let r=[];return r=Ms(r,ks(o)),r=Ms(r,ks(s)),r=Ms(r,ks(a)),r}_createItems(t){const e=this,i=e._active,n=e._chart.data,o=[],s=[],a=[];let r,l,c=[];for(r=0,l=i.length;r<l;++r)c.push(Ss(e._chart,i[r]));return t.filter&&(c=c.filter(((e,i,o)=>t.filter(e,i,o,n)))),t.itemSort&&(c=c.sort(((e,i)=>t.itemSort(e,i,n)))),J(c,(i=>{const n=Ls(t.callbacks,i);o.push(n.labelColor.call(e,i)),s.push(n.labelPointStyle.call(e,i)),a.push(n.labelTextColor.call(e,i))})),e.labelColors=o,e.labelPointStyles=s,e.labelTextColors=a,e.dataPoints=c,c}update(t,e){const i=this,n=i.options.setContext(i.getContext()),o=i._active;let s,a=[];if(o.length){const t=ws[n.position].call(i,o,i._eventPosition);a=i._createItems(n),i.title=i.getTitle(a,n),i.beforeBody=i.getBeforeBody(a,n),i.body=i.getBody(a,n),i.afterBody=i.getAfterBody(a,n),i.footer=i.getFooter(a,n);const e=i._size=Ps(i,n),r=Object.assign({},t,e),l=Cs(i._chart,n,r),c=Os(n,r,l,i._chart);i.xAlign=l.xAlign,i.yAlign=l.yAlign,s={opacity:1,x:c.x,y:c.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==i.opacity&&(s={opacity:0});i._tooltipItems=a,i.$context=void 0,s&&i._resolveAnimations().update(i,s),t&&n.external&&n.external.call(i,{chart:i._chart,tooltip:i,replay:e})}drawCaret(t,e,i,n){const o=this.getCaretPosition(t,i,n);e.lineTo(o.x1,o.y1),e.lineTo(o.x2,o.y2),e.lineTo(o.x3,o.y3)}getCaretPosition(t,e,i){const{xAlign:n,yAlign:o}=this,{cornerRadius:s,caretSize:a}=i,{x:r,y:l}=t,{width:c,height:h}=e;let d,u,f,g,p,m;return"center"===o?(p=l+h/2,"left"===n?(d=r,u=d-a,g=p+a,m=p-a):(d=r+c,u=d+a,g=p-a,m=p+a),f=d):(u="left"===n?r+s+a:"right"===n?r+c-s-a:this.caretX,"top"===o?(g=l,p=g-a,d=u-a,f=u+a):(g=l+h,p=g+a,d=u+a,f=u-a),m=g),{x1:d,x2:u,x3:f,y1:g,y2:p,y3:m}}drawTitle(t,e,i){const n=this,o=n.title,s=o.length;let a,r,l;if(s){const c=_n(i.rtl,n.x,n.width);for(t.x=Ts(n,i.titleAlign,i),e.textAlign=c.textAlign(i.titleAlign),e.textBaseline="middle",a=Be(i.titleFont),r=i.titleSpacing,e.fillStyle=i.titleColor,e.font=a.string,l=0;l<s;++l)e.fillText(o[l],c.x(t.x),t.y+a.lineHeight/2),t.y+=a.lineHeight+r,l+1===s&&(t.y+=i.titleMarginBottom-r)}}_drawColorBox(t,e,i,n,o){const s=this,a=s.labelColors[i],r=s.labelPointStyles[i],{boxHeight:l,boxWidth:c}=o,h=Be(o.bodyFont),d=Ts(s,"left",o),u=n.x(d),f=l<h.lineHeight?(h.lineHeight-l)/2:0,g=e.y+f;if(o.usePointStyle){const e={radius:Math.min(c,l)/2,pointStyle:r.pointStyle,rotation:r.rotation,borderWidth:1},i=n.leftForLtr(u,c)+c/2,s=g+l/2;t.strokeStyle=o.multiKeyBackground,t.fillStyle=o.multiKeyBackground,Kt(t,e,i,s),t.strokeStyle=a.borderColor,t.fillStyle=a.backgroundColor,Kt(t,e,i,s)}else{t.lineWidth=a.borderWidth||1,t.strokeStyle=a.borderColor,t.setLineDash(a.borderDash||[]),t.lineDashOffset=a.borderDashOffset||0;const e=n.leftForLtr(u,c),i=n.leftForLtr(n.xPlus(u,1),c-2),s=Ie(a.borderRadius);Object.values(s).some((t=>0!==t))?(t.beginPath(),t.fillStyle=o.multiKeyBackground,ne(t,{x:e,y:g,w:c,h:l,radius:s}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),ne(t,{x:i,y:g+1,w:c-2,h:l-2,radius:s}),t.fill()):(t.fillStyle=o.multiKeyBackground,t.fillRect(e,g,c,l),t.strokeRect(e,g,c,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,c-2,l-2))}t.fillStyle=s.labelTextColors[i]}drawBody(t,e,i){const n=this,{body:o}=n,{bodySpacing:s,bodyAlign:a,displayColors:r,boxHeight:l,boxWidth:c}=i,h=Be(i.bodyFont);let d=h.lineHeight,u=0;const f=_n(i.rtl,n.x,n.width),g=function(i){e.fillText(i,f.x(t.x+u),t.y+d/2),t.y+=d+s},p=f.textAlign(a);let m,x,b,_,y,v,w;for(e.textAlign=a,e.textBaseline="middle",e.font=h.string,t.x=Ts(n,p,i),e.fillStyle=i.bodyColor,J(n.beforeBody,g),u=r&&"right"!==p?"center"===a?c/2+1:c+2:0,_=0,v=o.length;_<v;++_){for(m=o[_],x=n.labelTextColors[_],e.fillStyle=x,J(m.before,g),b=m.lines,r&&b.length&&(n._drawColorBox(e,t,_,f,i),d=Math.max(h.lineHeight,l)),y=0,w=b.length;y<w;++y)g(b[y]),d=h.lineHeight;J(m.after,g)}u=0,d=h.lineHeight,J(n.afterBody,g),t.y-=s}drawFooter(t,e,i){const n=this,o=n.footer,s=o.length;let a,r;if(s){const l=_n(i.rtl,n.x,n.width);for(t.x=Ts(n,i.footerAlign,i),t.y+=i.footerMarginTop,e.textAlign=l.textAlign(i.footerAlign),e.textBaseline="middle",a=Be(i.footerFont),e.fillStyle=i.footerColor,e.font=a.string,r=0;r<s;++r)e.fillText(o[r],l.x(t.x),t.y+a.lineHeight/2),t.y+=a.lineHeight+i.footerSpacing}}drawBackground(t,e,i,n){const{xAlign:o,yAlign:s}=this,{x:a,y:r}=t,{width:l,height:c}=i,h=n.cornerRadius;e.fillStyle=n.backgroundColor,e.strokeStyle=n.borderColor,e.lineWidth=n.borderWidth,e.beginPath(),e.moveTo(a+h,r),"top"===s&&this.drawCaret(t,e,i,n),e.lineTo(a+l-h,r),e.quadraticCurveTo(a+l,r,a+l,r+h),"center"===s&&"right"===o&&this.drawCaret(t,e,i,n),e.lineTo(a+l,r+c-h),e.quadraticCurveTo(a+l,r+c,a+l-h,r+c),"bottom"===s&&this.drawCaret(t,e,i,n),e.lineTo(a+h,r+c),e.quadraticCurveTo(a,r+c,a,r+c-h),"center"===s&&"left"===o&&this.drawCaret(t,e,i,n),e.lineTo(a,r+h),e.quadraticCurveTo(a,r,a+h,r),e.closePath(),e.fill(),n.borderWidth>0&&e.stroke()}_updateAnimationTarget(t){const e=this,i=e._chart,n=e.$animations,o=n&&n.x,s=n&&n.y;if(o||s){const n=ws[t.position].call(e,e._active,e._eventPosition);if(!n)return;const a=e._size=Ps(e,t),r=Object.assign({},n,e._size),l=Cs(i,t,r),c=Os(t,r,l,i);o._to===c.x&&s._to===c.y||(e.xAlign=l.xAlign,e.yAlign=l.yAlign,e.width=a.width,e.height=a.height,e.caretX=n.x,e.caretY=n.y,e._resolveAnimations().update(e,c))}}draw(t){const e=this,i=e.options.setContext(e.getContext());let n=e.opacity;if(!n)return;e._updateAnimationTarget(i);const o={width:e.width,height:e.height},s={x:e.x,y:e.y};n=Math.abs(n)<.001?0:n;const a=Fe(i.padding),r=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;i.enabled&&r&&(t.save(),t.globalAlpha=n,e.drawBackground(s,t,o,i),yn(t,i.textDirection),s.y+=a.top,e.drawTitle(s,t,i),e.drawBody(s,t,i),e.drawFooter(s,t,i),vn(t,i.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this,n=i._active,o=t.map((({datasetIndex:t,index:e})=>{const n=i._chart.getDatasetMeta(t);if(!n)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:n.data[e],index:e}})),s=!tt(n,o),a=i._positionChanged(o,e);(s||a)&&(i._active=o,i._eventPosition=e,i.update(!0))}handleEvent(t,e){const i=this,n=i.options,o=i._active||[];let s=!1,a=[];"mouseout"!==t.type&&(a=i._chart.getElementsAtEventForMode(t,n.mode,n,e),n.reverse&&a.reverse());const r=i._positionChanged(a,t);return s=e||!tt(a,o)||r,s&&(i._active=a,(n.enabled||n.external)&&(i._eventPosition={x:t.x,y:t.y},i.update(!0,e))),s}_positionChanged(t,e){const{caretX:i,caretY:n,options:o}=this,s=ws[o.position].call(this,t,e);return!1!==s&&(i!==s.x||n!==s.y)}}Rs.positioners=ws;var Es={id:"tooltip",_element:Rs,positioners:ws,afterInit(t,e,i){i&&(t.tooltip=new Rs({_chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip,i={tooltip:e};!1!==t.notifyPlugins("beforeTooltipDraw",i)&&(e&&e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i))},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:{beforeTitle:H,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,n=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(n>0&&e.dataIndex<n)return i[e.dataIndex]}return""},afterTitle:H,beforeBody:H,beforeLabel:H,label(t){if(this&&this.options&&"dataset"===this.options.mode)return t.label+": "+t.formattedValue||t.formattedValue;let e=t.dataset.label||"";e&&(e+=": ");const i=t.formattedValue;return $(i)||(e+=i),e},labelColor(t){const e=t.chart.getDatasetMeta(t.datasetIndex).controller.getStyle(t.dataIndex);return{borderColor:e.borderColor,backgroundColor:e.backgroundColor,borderWidth:e.borderWidth,borderDash:e.borderDash,borderDashOffset:e.borderDashOffset,borderRadius:0}},labelTextColor(){return this.options.bodyColor},labelPointStyle(t){const e=t.chart.getDatasetMeta(t.datasetIndex).controller.getStyle(t.dataIndex);return{pointStyle:e.pointStyle,rotation:e.rotation}},afterLabel:H,afterBody:H,beforeFooter:H,footer:H,afterFooter:H}},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]},zs=Object.freeze({__proto__:null,Decimation:Go,Filler:gs,Legend:xs,SubTitle:vs,Title:_s,Tooltip:Es});function Is(t,e,i){const n=t.indexOf(e);if(-1===n)return((t,e,i)=>"string"==typeof e?t.push(e)-1:isNaN(e)?null:i)(t,e,i);return n!==t.lastIndexOf(e)?i:n}class Fs extends qi{constructor(t){super(t),this._startValue=void 0,this._valueRange=0}parse(t,e){if($(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:Ht(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:Is(i,t,K(e,t)),i.length-1)}determineDataLimits(){const t=this,{minDefined:e,maxDefined:i}=t.getUserBounds();let{min:n,max:o}=t.getMinMax(!0);"ticks"===t.options.bounds&&(e||(n=0),i||(o=t.getLabels().length-1)),t.min=n,t.max=o}buildTicks(){const t=this,e=t.min,i=t.max,n=t.options.offset,o=[];let s=t.getLabels();s=0===e&&i===s.length-1?s:s.slice(e,i+1),t._valueRange=Math.max(s.length-(n?0:1),1),t._startValue=t.min-(n?.5:0);for(let t=e;t<=i;t++)o.push({value:t});return o}getLabelForValue(t){const e=this.getLabels();return t>=0&&t<e.length?e[t]:t}configure(){const t=this;super.configure(),t.isHorizontal()||(t._reversePixels=!t._reversePixels)}getPixelForValue(t){const e=this;return"number"!=typeof t&&(t=e.parse(t)),null===t?NaN:e.getPixelForDecimal((t-e._startValue)/e._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){const e=this;return Math.round(e._startValue+e.getDecimalForPixel(t)*e._valueRange)}getBasePixel(){return this.bottom}}function Bs(t,e,{horizontal:i,minRotation:n}){const o=Et(n),s=(i?Math.sin(o):Math.cos(o))||.001,a=.75*e*(""+t).length;return Math.min(e/s,a)}Fs.id="category",Fs.defaults={ticks:{callback:Fs.prototype.getLabelForValue}};class Vs extends qi{constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(t,e){return $(t)||("number"==typeof t||t instanceof Number)&&!isFinite(+t)?null:+t}handleTickRangeOptions(){const t=this,{beginAtZero:e}=t.options,{minDefined:i,maxDefined:n}=t.getUserBounds();let{min:o,max:s}=t;const a=t=>o=i?o:t,r=t=>s=n?s:t;if(e){const t=Dt(o),e=Dt(s);t<0&&e<0?r(0):t>0&&e>0&&a(0)}o===s&&(r(s+1),e||a(o-1)),t.min=o,t.max=s}getTickLimit(){const t=this,e=t.options.ticks;let i,{maxTicksLimit:n,stepSize:o}=e;return o?i=Math.ceil(t.max/o)-Math.floor(t.min/o)+1:(i=t.computeTickLimit(),n=n||11),n&&(i=Math.min(n,i)),i}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this,e=t.options,i=e.ticks;let n=t.getTickLimit();n=Math.max(2,n);const o=function(t,e){const i=[],{bounds:n,step:o,min:s,max:a,precision:r,count:l,maxTicks:c,maxDigits:h,includeBounds:d}=t,u=o||1,f=c-1,{min:g,max:p}=e,m=!$(s),x=!$(a),b=!$(l),_=(p-g)/(h+1);let y,v,w,M,k=Ct((p-g)/f/u)*u;if(k<1e-14&&!m&&!x)return[{value:g},{value:p}];M=Math.ceil(p/k)-Math.floor(g/k),M>f&&(k=Ct(M*k/f/u)*u),$(r)||(y=Math.pow(10,r),k=Math.ceil(k*y)/y),"ticks"===n?(v=Math.floor(g/k)*k,w=Math.ceil(p/k)*k):(v=g,w=p),m&&x&&o&&Lt((a-s)/o,k/1e3)?(M=Math.round(Math.min((a-s)/k,c)),k=(a-s)/M,v=s,w=a):b?(v=m?s:v,w=x?a:w,M=l-1,k=(w-v)/M):(M=(w-v)/k,M=At(M,Math.round(M),k/1e3)?Math.round(M):Math.ceil(M));const S=Math.max(It(k),It(v));y=Math.pow(10,$(r)?S:r),v=Math.round(v*y)/y,w=Math.round(w*y)/y;let P=0;for(m&&(d&&v!==s?(i.push({value:s}),v<s&&P++,At(Math.round((v+P*k)*y)/y,s,Bs(s,_,t))&&P++):v<s&&P++);P<M;++P)i.push({value:Math.round((v+P*k)*y)/y});return x&&d&&w!==a?At(i[i.length-1].value,a,Bs(a,_,t))?i[i.length-1].value=a:i.push({value:a}):x&&w!==a||i.push({value:w}),i}({maxTicks:n,bounds:e.bounds,min:e.min,max:e.max,precision:i.precision,step:i.stepSize,count:i.count,maxDigits:t._maxDigits(),horizontal:t.isHorizontal(),minRotation:i.minRotation||0,includeBounds:!1!==i.includeBounds},t._range||t);return"ticks"===e.bounds&&Rt(o,t,"value"),e.reverse?(o.reverse(),t.start=t.max,t.end=t.min):(t.start=t.min,t.end=t.max),o}configure(){const t=this,e=t.ticks;let i=t.min,n=t.max;if(super.configure(),t.options.offset&&e.length){const t=(n-i)/Math.max(e.length-1,1)/2;i-=t,n+=t}t._startValue=i,t._endValue=n,t._valueRange=n-i}getLabelForValue(t){return Fi(t,this.chart.options.locale)}}class Ws extends Vs{determineDataLimits(){const t=this,{min:e,max:i}=t.getMinMax(!0);t.min=X(e)?e:0,t.max=X(i)?i:1,t.handleTickRangeOptions()}computeTickLimit(){const t=this,e=t.isHorizontal(),i=e?t.width:t.height,n=Et(t.options.ticks.minRotation),o=(e?Math.sin(n):Math.cos(n))||.001,s=t._resolveTickFontOptions(0);return Math.ceil(i/Math.min(40,s.lineHeight/o))}getPixelForValue(t){return null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}function Ns(t){return 1===t/Math.pow(10,Math.floor(Pt(t)))}Ws.id="linear",Ws.defaults={ticks:{callback:Vi.formatters.numeric}};class Hs extends qi{constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=Vs.prototype.parse.apply(this,[t,e]);if(0!==i)return X(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const t=this,{min:e,max:i}=t.getMinMax(!0);t.min=X(e)?Math.max(0,e):null,t.max=X(i)?Math.max(0,i):null,t.options.beginAtZero&&(t._zero=!0),t.handleTickRangeOptions()}handleTickRangeOptions(){const t=this,{minDefined:e,maxDefined:i}=t.getUserBounds();let n=t.min,o=t.max;const s=t=>n=e?n:t,a=t=>o=i?o:t,r=(t,e)=>Math.pow(10,Math.floor(Pt(t))+e);n===o&&(n<=0?(s(1),a(10)):(s(r(n,-1)),a(r(o,1)))),n<=0&&s(r(o,-1)),o<=0&&a(r(n,1)),t._zero&&t.min!==t._suggestedMin&&n===r(t.min,0)&&s(r(n,-1)),t.min=n,t.max=o}buildTicks(){const t=this,e=t.options,i=function(t,e){const i=Math.floor(Pt(e.max)),n=Math.ceil(e.max/Math.pow(10,i)),o=[];let s=q(t.min,Math.pow(10,Math.floor(Pt(e.min)))),a=Math.floor(Pt(s)),r=Math.floor(s/Math.pow(10,a)),l=a<0?Math.pow(10,Math.abs(a)):1;do{o.push({value:s,major:Ns(s)}),++r,10===r&&(r=1,++a,l=a>=0?1:l),s=Math.round(r*Math.pow(10,a)*l)/l}while(a<i||a===i&&r<n);const c=q(t.max,s);return o.push({value:c,major:Ns(s)}),o}({min:t._userMin,max:t._userMax},t);return"ticks"===e.bounds&&Rt(i,t,"value"),e.reverse?(i.reverse(),t.start=t.max,t.end=t.min):(t.start=t.min,t.end=t.max),i}getLabelForValue(t){return void 0===t?"0":Fi(t,this.chart.options.locale)}configure(){const t=this,e=t.min;super.configure(),t._startValue=Pt(e),t._valueRange=Pt(t.max)-Pt(e)}getPixelForValue(t){const e=this;return void 0!==t&&0!==t||(t=e.min),null===t||isNaN(t)?NaN:e.getPixelForDecimal(t===e.min?0:(Pt(t)-e._startValue)/e._valueRange)}getValueForPixel(t){const e=this,i=e.getDecimalForPixel(t);return Math.pow(10,e._startValue+i*e._valueRange)}}function js(t){const e=t.ticks;if(e.display&&t.display){const t=Fe(e.backdropPadding);return K(e.font&&e.font.size,xt.font.size)+t.height}return 0}function $s(t,e,i,n,o){return t===n||t===o?{start:e-i/2,end:e+i/2}:t<n||t>o?{start:e-i,end:e}:{start:e,end:e+i}}function Ys(t){const e={l:0,r:t.width,t:0,b:t.height-t.paddingTop},i={},n=[],o=[],s=t.getLabels().length;for(let c=0;c<s;c++){const s=t.options.pointLabels.setContext(t.getContext(c));o[c]=s.padding;const h=t.getPointPosition(c,t.drawingArea+o[c]),d=Be(s.font),u=(a=t.ctx,r=d,l=Y(l=t._pointLabels[c])?l:[l],{w:Ut(a,r.string,l),h:l.length*r.lineHeight});n[c]=u;const f=t.getIndexAngle(c),g=zt(f),p=$s(g,h.x,u.w,0,180),m=$s(g,h.y,u.h,90,270);p.start<e.l&&(e.l=p.start,i.l=f),p.end>e.r&&(e.r=p.end,i.r=f),m.start<e.t&&(e.t=m.start,i.t=f),m.end>e.b&&(e.b=m.end,i.b=f)}var a,r,l;t._setReductions(t.drawingArea,e,i),t._pointLabelItems=function(t,e,i){const n=[],o=t.getLabels().length,s=t.options,a=js(s),r=t.getDistanceFromCenterForValue(s.ticks.reverse?t.min:t.max);for(let s=0;s<o;s++){const o=0===s?a/2:0,l=t.getPointPosition(s,r+o+i[s]),c=zt(t.getIndexAngle(s)),h=e[s],d=qs(l.y,h.h,c),u=Us(c),f=Xs(l.x,h.w,u);n.push({x:l.x,y:d,textAlign:u,left:f,top:d,right:f+h.w,bottom:d+h.h})}return n}(t,n,o)}function Us(t){return 0===t||180===t?"center":t<180?"left":"right"}function Xs(t,e,i){return"right"===i?t-=e:"center"===i&&(t-=e/2),t}function qs(t,e,i){return 90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e),t}function Ks(t,e,i,n){const{ctx:o}=t;if(i)o.arc(t.xCenter,t.yCenter,e,0,_t);else{let i=t.getPointPosition(0,e);o.moveTo(i.x,i.y);for(let s=1;s<n;s++)i=t.getPointPosition(s,e),o.lineTo(i.x,i.y)}}function Gs(t){return Tt(t)?t:0}Hs.id="logarithmic",Hs.defaults={ticks:{callback:Vi.formatters.logarithmic,major:{enabled:!0}}};class Zs extends Vs{constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this;t.width=t.maxWidth,t.height=t.maxHeight,t.paddingTop=js(t.options)/2,t.xCenter=Math.floor(t.width/2),t.yCenter=Math.floor((t.height-t.paddingTop)/2),t.drawingArea=Math.min(t.height-t.paddingTop,t.width)/2}determineDataLimits(){const t=this,{min:e,max:i}=t.getMinMax(!1);t.min=X(e)&&!isNaN(e)?e:0,t.max=X(i)&&!isNaN(i)?i:0,t.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/js(this.options))}generateTickLabels(t){const e=this;Vs.prototype.generateTickLabels.call(e,t),e._pointLabels=e.getLabels().map(((t,i)=>{const n=Q(e.options.pointLabels.callback,[t,i],e);return n||0===n?n:""}))}fit(){const t=this,e=t.options;e.display&&e.pointLabels.display?Ys(t):t.setCenterPoint(0,0,0,0)}_setReductions(t,e,i){const n=this;let o=e.l/Math.sin(i.l),s=Math.max(e.r-n.width,0)/Math.sin(i.r),a=-e.t/Math.cos(i.t),r=-Math.max(e.b-(n.height-n.paddingTop),0)/Math.cos(i.b);o=Gs(o),s=Gs(s),a=Gs(a),r=Gs(r),n.drawingArea=Math.max(t/2,Math.min(Math.floor(t-(o+s)/2),Math.floor(t-(a+r)/2))),n.setCenterPoint(o,s,a,r)}setCenterPoint(t,e,i,n){const o=this,s=o.width-e-o.drawingArea,a=t+o.drawingArea,r=i+o.drawingArea,l=o.height-o.paddingTop-n-o.drawingArea;o.xCenter=Math.floor((a+s)/2+o.left),o.yCenter=Math.floor((r+l)/2+o.top+o.paddingTop)}getIndexAngle(t){return Wt(t*(_t/this.getLabels().length)+Et(this.options.startAngle||0))}getDistanceFromCenterForValue(t){const e=this;if($(t))return NaN;const i=e.drawingArea/(e.max-e.min);return e.options.reverse?(e.max-t)*i:(t-e.min)*i}getValueForDistanceFromCenter(t){if($(t))return NaN;const e=this,i=t/(e.drawingArea/(e.max-e.min));return e.options.reverse?e.max-i:e.min+i}getPointPosition(t,e){const i=this,n=i.getIndexAngle(t)-Mt;return{x:Math.cos(n)*e+i.xCenter,y:Math.sin(n)*e+i.yCenter,angle:n}}getPointPositionForValue(t,e){return this.getPointPosition(t,this.getDistanceFromCenterForValue(e))}getBasePosition(t){return this.getPointPositionForValue(t||0,this.getBaseValue())}getPointLabelPosition(t){const{left:e,top:i,right:n,bottom:o}=this._pointLabelItems[t];return{left:e,top:i,right:n,bottom:o}}drawBackground(){const t=this,{backgroundColor:e,grid:{circular:i}}=t.options;if(e){const n=t.ctx;n.save(),n.beginPath(),Ks(t,t.getDistanceFromCenterForValue(t._endValue),i,t.getLabels().length),n.closePath(),n.fillStyle=e,n.fill(),n.restore()}}drawGrid(){const t=this,e=t.ctx,i=t.options,{angleLines:n,grid:o}=i,s=t.getLabels().length;let a,r,l;if(i.pointLabels.display&&function(t,e){const{ctx:i,options:{pointLabels:n}}=t;for(let o=e-1;o>=0;o--){const e=n.setContext(t.getContext(o)),s=Be(e.font),{x:a,y:r,textAlign:l,left:c,top:h,right:d,bottom:u}=t._pointLabelItems[o],{backdropColor:f}=e;if(!$(f)){const t=Fe(e.backdropPadding);i.fillStyle=f,i.fillRect(c-t.left,h-t.top,d-c+t.width,u-h+t.height)}ee(i,t._pointLabels[o],a,r+s.lineHeight/2,s,{color:e.color,textAlign:l,textBaseline:"middle"})}}(t,s),o.display&&t.ticks.forEach(((e,i)=>{if(0!==i){r=t.getDistanceFromCenterForValue(e.value);const n=o.setContext(t.getContext(i-1));!function(t,e,i,n){const o=t.ctx,s=e.circular,{color:a,lineWidth:r}=e;!s&&!n||!a||!r||i<0||(o.save(),o.strokeStyle=a,o.lineWidth=r,o.setLineDash(e.borderDash),o.lineDashOffset=e.borderDashOffset,o.beginPath(),Ks(t,i,s,n),o.closePath(),o.stroke(),o.restore())}(t,n,r,s)}})),n.display){for(e.save(),a=t.getLabels().length-1;a>=0;a--){const o=n.setContext(t.getContext(a)),{color:s,lineWidth:c}=o;c&&s&&(e.lineWidth=c,e.strokeStyle=s,e.setLineDash(o.borderDash),e.lineDashOffset=o.borderDashOffset,r=t.getDistanceFromCenterForValue(i.ticks.reverse?t.min:t.max),l=t.getPointPosition(a,r),e.beginPath(),e.moveTo(t.xCenter,t.yCenter),e.lineTo(l.x,l.y),e.stroke())}e.restore()}}drawBorder(){}drawLabels(){const t=this,e=t.ctx,i=t.options,n=i.ticks;if(!n.display)return;const o=t.getIndexAngle(0);let s,a;e.save(),e.translate(t.xCenter,t.yCenter),e.rotate(o),e.textAlign="center",e.textBaseline="middle",t.ticks.forEach(((o,r)=>{if(0===r&&!i.reverse)return;const l=n.setContext(t.getContext(r)),c=Be(l.font);if(s=t.getDistanceFromCenterForValue(t.ticks[r].value),l.showLabelBackdrop){e.font=c.string,a=e.measureText(o.label).width,e.fillStyle=l.backdropColor;const t=Fe(l.backdropPadding);e.fillRect(-a/2-t.left,-s-c.size/2-t.top,a+t.width,c.size+t.height)}ee(e,o.label,0,-s,c,{color:l.color})})),e.restore()}drawTitle(){}}Zs.id="radialLinear",Zs.defaults={display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,callback:Vi.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback:t=>t,padding:5}},Zs.defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"},Zs.descriptors={angleLines:{_fallback:"grid"}};const Qs={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Js=Object.keys(Qs);function ta(t,e){return t-e}function ea(t,e){if($(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:s}=t._parseOpts;let a=e;return"function"==typeof n&&(a=n(a)),X(a)||(a="string"==typeof n?i.parse(a,n):i.parse(a)),null===a?null:(o&&(a="week"!==o||!Tt(s)&&!0!==s?i.startOf(a,o):i.startOf(a,"isoWeek",s)),+a)}function ia(t,e,i,n){const o=Js.length;for(let s=Js.indexOf(t);s<o-1;++s){const t=Qs[Js[s]],o=t.steps?t.steps:Number.MAX_SAFE_INTEGER;if(t.common&&Math.ceil((i-e)/(o*t.size))<=n)return Js[s]}return Js[o-1]}function na(t,e,i){if(i){if(i.length){const{lo:n,hi:o}=oe(i,e);t[i[n]>=e?i[n]:i[o]]=!0}}else t[e]=!0}function oa(t,e,i){const n=[],o={},s=e.length;let a,r;for(a=0;a<s;++a)r=e[a],o[r]=a,n.push({value:r,major:!1});return 0!==s&&i?function(t,e,i,n){const o=t._adapter,s=+o.startOf(e[0].value,n),a=e[e.length-1].value;let r,l;for(r=s;r<=a;r=+o.add(r,1,n))l=i[r],l>=0&&(e[l].major=!0);return e}(t,n,o,i):n}class sa extends qi{constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e){const i=t.time||(t.time={}),n=this._adapter=new ao._date(t.adapters.date);st(i.displayFormats,n.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:ea(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this,e=t.options,i=t._adapter,n=e.time.unit||"day";let{min:o,max:s,minDefined:a,maxDefined:r}=t.getUserBounds();function l(t){a||isNaN(t.min)||(o=Math.min(o,t.min)),r||isNaN(t.max)||(s=Math.max(s,t.max))}a&&r||(l(t._getLabelBounds()),"ticks"===e.bounds&&"labels"===e.ticks.source||l(t.getMinMax(!1))),o=X(o)&&!isNaN(o)?o:+i.startOf(Date.now(),n),s=X(s)&&!isNaN(s)?s:+i.endOf(Date.now(),n)+1,t.min=Math.min(o,s-1),t.max=Math.max(o+1,s)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this,e=t.options,i=e.time,n=e.ticks,o="labels"===n.source?t.getLabelTimestamps():t._generate();"ticks"===e.bounds&&o.length&&(t.min=t._userMin||o[0],t.max=t._userMax||o[o.length-1]);const s=t.min,a=re(o,s,t.max);return t._unit=i.unit||(n.autoSkip?ia(i.minUnit,t.min,t.max,t._getLabelCapacity(s)):function(t,e,i,n,o){for(let s=Js.length-1;s>=Js.indexOf(i);s--){const i=Js[s];if(Qs[i].common&&t._adapter.diff(o,n,i)>=e-1)return i}return Js[i?Js.indexOf(i):0]}(t,a.length,i.minUnit,t.min,t.max)),t._majorUnit=n.major.enabled&&"year"!==t._unit?function(t){for(let e=Js.indexOf(t)+1,i=Js.length;e<i;++e)if(Qs[Js[e]].common)return Js[e]}(t._unit):void 0,t.initOffsets(o),e.reverse&&a.reverse(),oa(t,a,t._majorUnit)}initOffsets(t){const e=this;let i,n,o=0,s=0;e.options.offset&&t.length&&(i=e.getDecimalForValue(t[0]),o=1===t.length?1-i:(e.getDecimalForValue(t[1])-i)/2,n=e.getDecimalForValue(t[t.length-1]),s=1===t.length?n:(n-e.getDecimalForValue(t[t.length-2]))/2);const a=t.length<3?.5:.25;o=Ht(o,0,a),s=Ht(s,0,a),e._offsets={start:o,end:s,factor:1/(o+1+s)}}_generate(){const t=this,e=t._adapter,i=t.min,n=t.max,o=t.options,s=o.time,a=s.unit||ia(s.minUnit,i,n,t._getLabelCapacity(i)),r=K(s.stepSize,1),l="week"===a&&s.isoWeekday,c=Tt(l)||!0===l,h={};let d,u,f=i;if(c&&(f=+e.startOf(f,"isoWeek",l)),f=+e.startOf(f,c?"day":a),e.diff(n,i,a)>1e5*r)throw new Error(i+" and "+n+" are too far apart with stepSize of "+r+" "+a);const g="data"===o.ticks.source&&t.getDataTimestamps();for(d=f,u=0;d<n;d=+e.add(d,r,a),u++)na(h,d,g);return d!==n&&"ticks"!==o.bounds&&1!==u||na(h,d,g),Object.keys(h).sort(((t,e)=>t-e)).map((t=>+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}_tickFormatFunction(t,e,i,n){const o=this,s=o.options,a=s.time.displayFormats,r=o._unit,l=o._majorUnit,c=r&&a[r],h=l&&a[l],d=i[e],u=l&&h&&d&&d.major,f=o._adapter.format(t,n||(u?h:c)),g=s.ticks.callback;return g?Q(g,[f,e,i],o):f}generateTickLabels(t){let e,i,n;for(e=0,i=t.length;e<i;++e)n=t[e],n.label=this._tickFormatFunction(n.value,e,t)}getDecimalForValue(t){const e=this;return null===t?NaN:(t-e.min)/(e.max-e.min)}getPixelForValue(t){const e=this,i=e._offsets,n=e.getDecimalForValue(t);return e.getPixelForDecimal((i.start+n)*i.factor)}getValueForPixel(t){const e=this,i=e._offsets,n=e.getDecimalForPixel(t)/i.factor-i.end;return e.min+n*(e.max-e.min)}_getLabelSize(t){const e=this,i=e.options.ticks,n=e.ctx.measureText(t).width,o=Et(e.isHorizontal()?i.maxRotation:i.minRotation),s=Math.cos(o),a=Math.sin(o),r=e._resolveTickFontOptions(0).size;return{w:n*s+r*a,h:n*a+r*s}}_getLabelCapacity(t){const e=this,i=e.options.time,n=i.displayFormats,o=n[i.unit]||n.millisecond,s=e._tickFormatFunction(t,0,oa(e,[t],e._majorUnit),o),a=e._getLabelSize(s),r=Math.floor(e.isHorizontal()?e.width/a.w:e.height/a.h)-1;return r>0?r:1}getDataTimestamps(){const t=this;let e,i,n=t._cache.data||[];if(n.length)return n;const o=t.getMatchingVisibleMetas();if(t._normalized&&o.length)return t._cache.data=o[0].controller.getAllParsedValues(t);for(e=0,i=o.length;e<i;++e)n=n.concat(o[e].controller.getAllParsedValues(t));return t._cache.data=t.normalize(n)}getLabelTimestamps(){const t=this,e=t._cache.labels||[];let i,n;if(e.length)return e;const o=t.getLabels();for(i=0,n=o.length;i<n;++i)e.push(ea(t,o[i]));return t._cache.labels=t._normalized?e:t.normalize(e)}normalize(t){return de(t.sort(ta))}}function aa(t,e,i){let n,o,s,a,r=0,l=t.length-1;i?(e>=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=se(t,"pos",e)),({pos:n,time:s}=t[r]),({pos:o,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=se(t,"time",e)),({time:n,pos:s}=t[r]),({time:o,pos:a}=t[l]));const c=o-n;return c?s+(a-s)*(e-n)/c:s}sa.id="time",sa.defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",major:{enabled:!1}}};class ra extends sa{constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this,e=t._getTimestampsForTable(),i=t._table=t.buildLookupTable(e);t._minPos=aa(i,t.min),t._tableRange=aa(i,t.max)-t._minPos,super.initOffsets(e)}buildLookupTable(t){const{min:e,max:i}=this,n=[],o=[];let s,a,r,l,c;for(s=0,a=t.length;s<a;++s)l=t[s],l>=e&&l<=i&&n.push(l);if(n.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(s=0,a=n.length;s<a;++s)c=n[s+1],r=n[s-1],l=n[s],Math.round((c+r)/2)!==l&&o.push({time:l,pos:s/(a-1)});return o}_getTimestampsForTable(){const t=this;let e=t._cache.all||[];if(e.length)return e;const i=t.getDataTimestamps(),n=t.getLabelTimestamps();return e=i.length&&n.length?t.normalize(i.concat(n)):i.length?i:n,e=t._cache.all=e,e}getDecimalForValue(t){return(aa(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const e=this,i=e._offsets,n=e.getDecimalForPixel(t)/i.factor-i.end;return aa(e._table,n*e._tableRange+e._minPos,!0)}}ra.id="timeseries",ra.defaults=sa.defaults;var la=Object.freeze({__proto__:null,CategoryScale:Fs,LinearScale:Ws,LogarithmicScale:Hs,RadialLinearScale:Zs,TimeScale:sa,TimeSeriesScale:ra});return eo.register(yo,la,Xo,zs),eo.helpers={...Tn},eo._adapters=ao,eo.Animation=yi,eo.Animations=wi,eo.animator=a,eo.controllers=Ln.controllers.items,eo.DatasetController=Ei,eo.Element=zi,eo.elements=Xo,eo.Interaction=Te,eo.layouts=Ze,eo.platforms=fi,eo.Scale=qi,eo.Ticks=Vi,Object.assign(eo,yo,la,Xo,zs,fi),eo.Chart=eo,"undefined"!=typeof window&&(window.Chart=eo),eo}));
package-lock.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "requires": true,
3
+ "lockfileVersion": 1,
4
+ "dependencies": {
5
+ "chart.js": {
6
+ "version": "3.4.1",
7
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz",
8
+ "integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g=="
9
+ }
10
+ }
11
+ }
plugins/WordPress/Menu.php CHANGED
@@ -23,7 +23,6 @@ class Menu extends \Piwik\Plugin\Menu
23
  $menu->remove('CoreAdminHome_MenuMeasurables', 'SitesManager_MenuManage');
24
  $menu->remove('SitesManager_Sites', 'SitesManager_MenuManage');
25
  $menu->remove('CoreAdminHome_MenuSystem', 'UsersManager_MenuUsers');
26
- $menu->remove('UsersManager_MenuPersonal', 'General_Settings');
27
  $menu->remove('UsersManager_MenuPersonal', 'General_Security');
28
  $menu->remove('CoreAdminHome_MenuMeasurables', 'CoreAdminHome_TrackingCode');
29
  $menu->remove('CoreAdminHome_MenuMeasurables', 'General_Settings');
23
  $menu->remove('CoreAdminHome_MenuMeasurables', 'SitesManager_MenuManage');
24
  $menu->remove('SitesManager_Sites', 'SitesManager_MenuManage');
25
  $menu->remove('CoreAdminHome_MenuSystem', 'UsersManager_MenuUsers');
 
26
  $menu->remove('UsersManager_MenuPersonal', 'General_Security');
27
  $menu->remove('CoreAdminHome_MenuMeasurables', 'CoreAdminHome_TrackingCode');
28
  $menu->remove('CoreAdminHome_MenuMeasurables', 'General_Settings');
plugins/WordPress/WordPress.php CHANGED
@@ -53,6 +53,7 @@ class WordPress extends Plugin
53
  'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
54
  'CustomJsTracker.manipulateJsTracker' => 'updateHeatmapTrackerPath',
55
  'Visualization.beforeRender' => 'onBeforeRenderView',
 
56
  );
57
  }
58
 
@@ -331,10 +332,9 @@ class WordPress extends Plugin
331
  array('usersmanager', 'index'),
332
  array('usersmanager', ''),
333
  array('usersmanager', 'addnewtoken'),
334
- array('usersmanager', 'usersettings'),
335
  array('usersmanager', 'deletetoken'),
336
  array('usersmanager', 'usersecurity'),
337
- array('sitesmanager', ''),
338
  array('sitesmanager', 'globalsettings'),
339
  array('feedback', ''),
340
  array('feedback', 'index'),
@@ -397,4 +397,9 @@ class WordPress extends Plugin
397
  throw new \Exception('This feature is not available');
398
  }
399
 
 
 
 
 
 
400
  }
53
  'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
54
  'CustomJsTracker.manipulateJsTracker' => 'updateHeatmapTrackerPath',
55
  'Visualization.beforeRender' => 'onBeforeRenderView',
56
+ 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
57
  );
58
  }
59
 
332
  array('usersmanager', 'index'),
333
  array('usersmanager', ''),
334
  array('usersmanager', 'addnewtoken'),
 
335
  array('usersmanager', 'deletetoken'),
336
  array('usersmanager', 'usersecurity'),
337
+ array('sitesmanager', ''),
338
  array('sitesmanager', 'globalsettings'),
339
  array('feedback', ''),
340
  array('feedback', 'index'),
397
  throw new \Exception('This feature is not available');
398
  }
399
 
400
+ public function getStylesheetFiles(&$files)
401
+ {
402
+ $files[] = "../plugins/WordPress/stylesheets/user.css";
403
+ }
404
+
405
  }
plugins/WordPress/stylesheets/user.css ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ div.siteSelector, #defaultReportSiteSelector, div[name="username"],div[name="language"], div[name="timeformat"],div[name="defaultReport"], #newsletterSignup {
2
+ display: none;
3
+ }
readme.txt CHANGED
@@ -4,7 +4,7 @@ Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_i
4
  Tags: matomo,piwik,analytics,statistics,stats,tracking,ecommerce
5
  Requires at least: 4.8
6
  Tested up to: 5.8
7
- Stable tag: 4.4.1
8
  Requires PHP: 7.2.5
9
  License: GPLv3 or later
10
  License URI: https://www.gnu.org/licenses/gpl-3.0.html
4
  Tags: matomo,piwik,analytics,statistics,stats,tracking,ecommerce
5
  Requires at least: 4.8
6
  Tested up to: 5.8
7
+ Stable tag: 4.4.2
8
  Requires PHP: 7.2.5
9
  License: GPLv3 or later
10
  License URI: https://www.gnu.org/licenses/gpl-3.0.html