From ae4b03163792f0b6e933933e5d37df87dc3fd566 Mon Sep 17 00:00:00 2001 From: mattab Date: Thu, 28 Mar 2013 12:42:39 +1300 Subject: Mass conversion of all files to the newly agreed coding standard: PSR 1/2 Converting Piwik core source files, PHP, JS, TPL, CSS More info: http://piwik.org/participate/coding-standards/ --- core/API/DataTableGenericFilter.php | 270 +- core/API/DataTableManipulator.php | 281 +- core/API/DataTableManipulator/Flattener.php | 216 +- core/API/DataTableManipulator/LabelFilter.php | 292 +- core/API/DocumentationGenerator.php | 425 +- core/API/Proxy.php | 773 +- core/API/Request.php | 320 +- core/API/ResponseBuilder.php | 927 +- core/Access.php | 721 +- core/Archive.php | 884 +- core/Archive/Array.php | 261 +- core/Archive/Array/IndexedByDate.php | 222 +- core/Archive/Array/IndexedBySite.php | 504 +- core/Archive/Single.php | 1251 ++- core/ArchiveProcessing.php | 2027 ++-- core/ArchiveProcessing/Day.php | 1944 ++-- core/ArchiveProcessing/Period.php | 852 +- core/AssetManager.php | 861 +- core/Auth.php | 79 +- core/CacheFile.php | 302 +- core/Common.php | 3213 +++--- core/Config.php | 898 +- core/Config/Compat.php | 278 +- core/Controller.php | 1742 ++- core/Controller/Admin.php | 91 +- core/Cookie.php | 741 +- core/DataFiles/Countries.php | 600 +- core/DataFiles/Currencies.php | 331 +- core/DataFiles/LanguageToCountry.php | 91 +- core/DataFiles/Languages.php | 377 +- core/DataFiles/SearchEngines.php | 1655 ++- core/DataFiles/Socials.php | 389 +- core/DataTable.php | 2813 +++-- core/DataTable/Array.php | 789 +- core/DataTable/Filter.php | 107 +- .../Filter/AddColumnsProcessedMetrics.php | 222 +- .../Filter/AddColumnsProcessedMetricsGoal.php | 378 +- core/DataTable/Filter/AddConstantMetadata.php | 55 +- core/DataTable/Filter/AddSummaryRow.php | 123 +- core/DataTable/Filter/BeautifyRangeLabels.php | 261 +- core/DataTable/Filter/BeautifyTimeRangeLabels.php | 193 +- core/DataTable/Filter/CalculateEvolutionFilter.php | 238 +- core/DataTable/Filter/ColumnCallbackAddColumn.php | 141 +- .../Filter/ColumnCallbackAddColumnPercentage.php | 34 +- .../Filter/ColumnCallbackAddColumnQuotient.php | 224 +- .../DataTable/Filter/ColumnCallbackAddMetadata.php | 117 +- core/DataTable/Filter/ColumnCallbackDeleteRow.php | 81 +- core/DataTable/Filter/ColumnCallbackReplace.php | 166 +- core/DataTable/Filter/ColumnDelete.php | 211 +- core/DataTable/Filter/ExcludeLowPopulation.php | 108 +- core/DataTable/Filter/GroupBy.php | 155 +- core/DataTable/Filter/Limit.php | 101 +- .../Filter/MetadataCallbackAddMetadata.php | 113 +- core/DataTable/Filter/MetadataCallbackReplace.php | 68 +- core/DataTable/Filter/Null.php | 37 +- core/DataTable/Filter/Pattern.php | 127 +- core/DataTable/Filter/PatternRecursive.php | 121 +- core/DataTable/Filter/RangeCheck.php | 79 +- core/DataTable/Filter/ReplaceColumnNames.php | 167 +- core/DataTable/Filter/ReplaceSummaryRowLabel.php | 81 +- core/DataTable/Filter/SafeDecodeLabel.php | 115 +- core/DataTable/Filter/Sort.php | 395 +- core/DataTable/Filter/Truncate.php | 60 +- core/DataTable/Manager.php | 269 +- core/DataTable/Renderer.php | 804 +- core/DataTable/Renderer/Console.php | 299 +- core/DataTable/Renderer/Csv.php | 742 +- core/DataTable/Renderer/Html.php | 398 +- core/DataTable/Renderer/Json.php | 185 +- core/DataTable/Renderer/Php.php | 494 +- core/DataTable/Renderer/Rss.php | 340 +- core/DataTable/Renderer/Tsv.php | 42 +- core/DataTable/Renderer/Xml.php | 877 +- core/DataTable/Row.php | 1231 ++- core/DataTable/Row/DataTableSummary.php | 85 +- core/DataTable/Simple.php | 56 +- core/Date.php | 1276 ++- core/Db/Adapter.php | 174 +- core/Db/Adapter/Interface.php | 96 +- core/Db/Adapter/Mysqli.php | 294 +- core/Db/Adapter/Pdo/Mssql.php | 443 +- core/Db/Adapter/Pdo/Mysql.php | 414 +- core/Db/Adapter/Pdo/Pgsql.php | 324 +- core/Db/Schema.php | 423 +- core/Db/Schema/Interface.php | 134 +- core/Db/Schema/Myisam.php | 481 +- core/ErrorHandler.php | 127 +- core/ExceptionHandler.php | 55 +- core/FrontController.php | 672 +- core/HTMLPurifier.php | 59 +- core/Http.php | 1427 ++- core/IP.php | 1120 +- core/Loader.php | 190 +- core/Log.php | 326 +- core/Log/APICall.php | 179 +- core/Log/Error.php | 235 +- core/Log/Exception.php | 128 +- core/Log/Message.php | 115 +- core/Mail.php | 122 +- core/Menu/Abstract.php | 435 +- core/Menu/Admin.php | 115 +- core/Menu/Main.php | 149 +- core/Menu/Top.php | 131 +- core/Nonce.php | 241 +- core/Option.php | 323 +- core/Period.php | 410 +- core/Period/Day.php | 156 +- core/Period/Month.php | 110 +- core/Period/Range.php | 760 +- core/Period/Week.php | 149 +- core/Period/Year.php | 142 +- core/Piwik.php | 5096 +++++---- core/Plugin.php | 208 +- core/Plugin/Config.php | 70 +- core/PluginsFunctions/Sql.php | 806 +- core/PluginsFunctions/WidgetsList.php | 322 +- core/PluginsManager.php | 1346 ++- core/ProxyHeaders.php | 135 +- core/QuickForm2.php | 209 +- core/RankingQuery.php | 651 +- core/ReportRenderer.php | 469 +- core/ReportRenderer/Html.php | 272 +- core/ReportRenderer/Pdf.php | 972 +- core/ScheduledTask.php | 271 +- core/ScheduledTime.php | 171 +- core/ScheduledTime/Daily.php | 48 +- core/ScheduledTime/Hourly.php | 50 +- core/ScheduledTime/Monthly.php | 185 +- core/ScheduledTime/Weekly.php | 92 +- core/Segment.php | 530 +- core/SegmentExpression.php | 362 +- core/Session.php | 246 +- core/Session/Namespace.php | 31 +- core/Session/SaveHandler/DbTable.php | 246 +- core/Site.php | 692 +- core/Smarty.php | 217 +- core/SmartyPlugins/block.purify.php | 39 +- core/SmartyPlugins/function.ajaxErrorDiv.php | 19 +- core/SmartyPlugins/function.ajaxLoadingDiv.php | 27 +- .../SmartyPlugins/function.ajaxRequestErrorDiv.php | 12 +- core/SmartyPlugins/function.hiddenurl.php | 35 +- core/SmartyPlugins/function.includeAssets.php | 40 +- .../function.loadJavascriptTranslations.php | 47 +- core/SmartyPlugins/function.logoHtml.php | 36 +- core/SmartyPlugins/function.postEvent.php | 21 +- core/SmartyPlugins/function.sparkline.php | 14 +- core/SmartyPlugins/function.url.php | 10 +- core/SmartyPlugins/modifier.inlineHelp.php | 18 +- core/SmartyPlugins/modifier.money.php | 17 +- core/SmartyPlugins/modifier.stripeol.php | 4 +- core/SmartyPlugins/modifier.sumtime.php | 6 +- core/SmartyPlugins/modifier.translate.php | 33 +- core/SmartyPlugins/modifier.unescape.php | 6 +- .../SmartyPlugins/modifier.urlRewriteBasicView.php | 35 +- .../modifier.urlRewriteWithParameters.php | 10 +- core/SmartyPlugins/outputfilter.ajaxcdn.php | 38 +- core/SmartyPlugins/outputfilter.cachebuster.php | 32 +- core/TCPDF.php | 130 +- core/TablePartitioning.php | 195 +- core/TaskScheduler.php | 269 +- core/Timer.php | 91 +- core/Tracker.php | 1590 ++- core/Tracker/Action.php | 2157 ++-- core/Tracker/Cache.php | 269 +- core/Tracker/Config.php | 20 +- core/Tracker/Db.php | 398 +- core/Tracker/Db/Exception.php | 3 +- core/Tracker/Db/Mysqli.php | 499 +- core/Tracker/Db/Pdo/Mysql.php | 393 +- core/Tracker/Db/Pdo/Pgsql.php | 176 +- core/Tracker/GoalManager.php | 1619 ++- core/Tracker/IgnoreCookie.php | 103 +- core/Tracker/Visit.php | 3311 +++--- core/Translate.php | 372 +- core/TranslationWriter.php | 208 +- core/Unzip.php | 61 +- core/Unzip/Gzip.php | 129 +- core/Unzip/Interface.php | 40 +- core/Unzip/PclZip.php | 122 +- core/Unzip/Tar.php | 122 +- core/Unzip/ZipArchive.php | 210 +- core/UpdateCheck.php | 145 +- core/Updater.php | 562 +- core/Updates.php | 192 +- core/Updates/0.2.10.php | 81 +- core/Updates/0.2.12.php | 30 +- core/Updates/0.2.13.php | 22 +- core/Updates/0.2.24.php | 30 +- core/Updates/0.2.27.php | 50 +- core/Updates/0.2.32.php | 34 +- core/Updates/0.2.33.php | 40 +- core/Updates/0.2.34.php | 14 +- core/Updates/0.2.35.php | 20 +- core/Updates/0.2.37.php | 20 +- core/Updates/0.4.1.php | 24 +- core/Updates/0.4.2.php | 30 +- core/Updates/0.4.4.php | 22 +- core/Updates/0.4.php | 34 +- core/Updates/0.5.4.php | 82 +- core/Updates/0.5.5.php | 47 +- core/Updates/0.5.php | 38 +- core/Updates/0.6-rc1.php | 95 +- core/Updates/0.6.2.php | 50 +- core/Updates/0.6.3.php | 58 +- core/Updates/0.7.php | 20 +- core/Updates/0.9.1.php | 60 +- core/Updates/1.1.php | 26 +- core/Updates/1.10-b4.php | 29 +- core/Updates/1.10.1.php | 29 +- core/Updates/1.10.2-b1.php | 26 +- core/Updates/1.10.2-b2.php | 26 +- core/Updates/1.11-b1.php | 29 +- core/Updates/1.12-b1.php | 34 +- core/Updates/1.2-rc1.php | 139 +- core/Updates/1.2-rc2.php | 14 +- core/Updates/1.2.3.php | 26 +- core/Updates/1.2.5-rc1.php | 24 +- core/Updates/1.2.5-rc7.php | 20 +- core/Updates/1.4-rc1.php | 30 +- core/Updates/1.4-rc2.php | 42 +- core/Updates/1.5-b1.php | 28 +- core/Updates/1.5-b2.php | 20 +- core/Updates/1.5-b3.php | 28 +- core/Updates/1.5-b4.php | 20 +- core/Updates/1.5-b5.php | 20 +- core/Updates/1.5-rc6.php | 14 +- core/Updates/1.6-b1.php | 32 +- core/Updates/1.6-rc1.php | 14 +- core/Updates/1.7-b1.php | 30 +- core/Updates/1.7.2-rc5.php | 26 +- core/Updates/1.7.2-rc7.php | 44 +- core/Updates/1.8.3-b1.php | 136 +- core/Updates/1.8.4-b1.php | 141 +- core/Updates/1.9-b16.php | 44 +- core/Updates/1.9-b19.php | 34 +- core/Updates/1.9-b9.php | 65 +- core/Updates/1.9.1-b2.php | 30 +- core/Updates/1.9.3-b10.php | 29 +- core/Updates/1.9.3-b3.php | 18 +- core/Updates/1.9.3-b8.php | 28 +- core/Url.php | 882 +- core/Version.php | 10 +- core/View.php | 431 +- core/View/Interface.php | 16 +- core/View/OneClickDone.php | 74 +- core/View/ReportsByDimension.php | 175 +- core/ViewDataTable.php | 2899 +++-- core/ViewDataTable/Cloud.php | 210 +- core/ViewDataTable/GenerateGraphData.php | 445 +- .../GenerateGraphData/ChartEvolution.php | 615 +- core/ViewDataTable/GenerateGraphData/ChartPie.php | 50 +- .../GenerateGraphData/ChartVerticalBar.php | 46 +- core/ViewDataTable/GenerateGraphHTML.php | 357 +- .../GenerateGraphHTML/ChartEvolution.php | 358 +- core/ViewDataTable/GenerateGraphHTML/ChartPie.php | 24 +- .../GenerateGraphHTML/ChartVerticalBar.php | 24 +- core/ViewDataTable/HtmlTable.php | 425 +- core/ViewDataTable/HtmlTable/AllColumns.php | 102 +- core/ViewDataTable/HtmlTable/Goals.php | 500 +- core/ViewDataTable/Sparkline.php | 175 +- core/Visualization/Chart.php | 327 +- core/Visualization/Chart/Evolution.php | 54 +- core/Visualization/Chart/Pie.php | 72 +- core/Visualization/Chart/VerticalBar.php | 56 +- core/Visualization/Cloud.php | 174 +- core/Visualization/Sparkline.php | 148 +- core/testMinimumPhpVersion.php | 66 +- index.php | 45 +- libs/PiwikTracker/PiwikTracker.php | 1618 ++- libs/UserAgentParser/UserAgentParser.php | 1234 ++- libs/UserAgentParser/UserAgentParser.test.php | 70 +- misc/How to install Piwik.html | 10 +- misc/cron/archive.php | 1465 ++- misc/others/ExamplePiwikTracker.php | 2 +- misc/others/api_rest_call.php | 16 +- misc/others/geoipUpdateRows.php | 419 +- misc/others/iframeWidget.htm | 10 +- misc/others/iframeWidget_localhost.php | 61 +- ...t_cookies_GenerateHundredsWebsitesAndVisits.php | 7 +- misc/others/test_generateLotsVisitsWebsites.php | 205 +- misc/others/tracker_simpleImageTracker.php | 10 +- misc/others/uninstall-delete-piwik-directory.php | 24 +- misc/others/widget_example_lastvisits.html | 10 +- misc/proxy-hide-piwik-url/piwik.php | 60 +- piwik.php | 111 +- plugins/API/API.php | 3125 +++--- plugins/API/Controller.php | 168 +- plugins/API/css/styles.css | 47 +- plugins/API/templates/listAllAPI.tpl | 26 +- plugins/Actions/API.php | 991 +- plugins/Actions/Actions.php | 1099 +- plugins/Actions/Archiving.php | 950 +- plugins/Actions/ArchivingHelper.php | 939 +- plugins/Actions/Controller.php | 1007 +- plugins/Actions/templates/indexSiteSearch.tpl | 20 +- plugins/Annotations/API.php | 691 +- plugins/Annotations/AnnotationList.php | 824 +- plugins/Annotations/Annotations.php | 96 +- plugins/Annotations/Controller.php | 406 +- plugins/Annotations/templates/annotation.tpl | 83 +- .../Annotations/templates/annotationManager.tpl | 36 +- plugins/Annotations/templates/annotations.js | 1194 +-- plugins/Annotations/templates/annotations.tpl | 47 +- .../Annotations/templates/evolutionAnnotations.tpl | 18 +- plugins/Annotations/templates/styles.css | 216 +- plugins/AnonymizeIP/AnonymizeIP.php | 106 +- plugins/CoreAdminHome/API.php | 425 +- plugins/CoreAdminHome/Controller.php | 442 +- plugins/CoreAdminHome/CoreAdminHome.php | 214 +- plugins/CoreAdminHome/templates/generalSettings.js | 231 +- .../CoreAdminHome/templates/generalSettings.tpl | 456 +- plugins/CoreAdminHome/templates/header.tpl | 125 +- .../templates/jsTrackingGenerator.css | 67 +- .../CoreAdminHome/templates/jsTrackingGenerator.js | 661 +- .../templates/jsTrackingGenerator.tpl | 429 +- plugins/CoreAdminHome/templates/menu.css | 89 +- plugins/CoreAdminHome/templates/menu.tpl | 37 +- plugins/CoreAdminHome/templates/optOut.tpl | 49 +- plugins/CoreAdminHome/templates/styles.css | 165 +- plugins/CoreHome/Controller.php | 419 +- plugins/CoreHome/CoreHome.php | 162 +- .../DataTableRowAction/MultiRowEvolution.php | 134 +- .../CoreHome/DataTableRowAction/RowEvolution.php | 548 +- plugins/CoreHome/templates/autocomplete.js | 463 +- plugins/CoreHome/templates/broadcast.js | 449 +- plugins/CoreHome/templates/calendar.js | 1094 +- plugins/CoreHome/templates/cloud.css | 51 +- plugins/CoreHome/templates/cloud.tpl | 45 +- plugins/CoreHome/templates/datatable.css | 706 +- plugins/CoreHome/templates/datatable.js | 3739 ++++--- plugins/CoreHome/templates/datatable.tpl | 111 +- plugins/CoreHome/templates/datatable_actions.tpl | 101 +- .../templates/datatable_actions_recursive.tpl | 71 +- .../templates/datatable_actions_subdatable.tpl | 30 +- plugins/CoreHome/templates/datatable_cell.tpl | 23 +- plugins/CoreHome/templates/datatable_footer.tpl | 198 +- plugins/CoreHome/templates/datatable_js.tpl | 7 +- plugins/CoreHome/templates/datatable_manager.js | 356 +- plugins/CoreHome/templates/datatable_rowactions.js | 540 +- plugins/CoreHome/templates/date.js | 77 +- plugins/CoreHome/templates/donate.css | 156 +- plugins/CoreHome/templates/donate.js | 282 +- plugins/CoreHome/templates/donate.tpl | 117 +- plugins/CoreHome/templates/footer.tpl | 2 +- plugins/CoreHome/templates/graph.tpl | 74 +- plugins/CoreHome/templates/header.tpl | 40 +- plugins/CoreHome/templates/header_message.tpl | 54 +- plugins/CoreHome/templates/html_report_body.tpl | 142 +- plugins/CoreHome/templates/html_report_footer.tpl | 2 +- plugins/CoreHome/templates/html_report_header.tpl | 55 +- plugins/CoreHome/templates/iframe_buster_body.tpl | 6 +- .../CoreHome/templates/iframe_buster_header.tpl | 8 +- plugins/CoreHome/templates/index_before_menu.tpl | 6 +- plugins/CoreHome/templates/index_content.tpl | 27 +- plugins/CoreHome/templates/jqplot.css | 199 +- plugins/CoreHome/templates/jqplot.js | 2600 +++-- .../CoreHome/templates/jquery.ui.autocomplete.css | 61 +- plugins/CoreHome/templates/js_css_includes.tpl | 2 +- plugins/CoreHome/templates/js_disabled_notice.tpl | 4 +- plugins/CoreHome/templates/js_global_variables.tpl | 68 +- plugins/CoreHome/templates/logo.tpl | 15 +- plugins/CoreHome/templates/menu.js | 64 +- plugins/CoreHome/templates/menu.tpl | 26 +- plugins/CoreHome/templates/menu_init.js | 6 +- plugins/CoreHome/templates/misc.js | 319 +- plugins/CoreHome/templates/period_select.tpl | 56 +- plugins/CoreHome/templates/piwik_tag.tpl | 59 +- plugins/CoreHome/templates/popover.js | 399 +- .../templates/popover_multirowevolution.tpl | 66 +- .../CoreHome/templates/popover_rowevolution.tpl | 62 +- plugins/CoreHome/templates/promo_video.tpl | 248 +- .../CoreHome/templates/reports_by_dimension.tpl | 45 +- plugins/CoreHome/templates/sites_selection.tpl | 47 +- plugins/CoreHome/templates/sparkline.js | 104 +- plugins/CoreHome/templates/sparkline_footer.tpl | 10 +- plugins/CoreHome/templates/styles.css | 213 +- plugins/CoreHome/templates/tooltip.js | 216 +- plugins/CoreHome/templates/top_bar.tpl | 12 +- plugins/CoreHome/templates/top_bar_hello_menu.tpl | 10 +- plugins/CoreHome/templates/top_bar_top_menu.tpl | 21 +- plugins/CoreHome/templates/top_screen.tpl | 4 +- .../CoreHome/templates/warning_invalid_host.tpl | 9 +- plugins/CorePluginsAdmin/Controller.php | 129 +- plugins/CorePluginsAdmin/CorePluginsAdmin.php | 50 +- plugins/CorePluginsAdmin/templates/manage.tpl | 105 +- plugins/CoreUpdater/Controller.php | 694 +- plugins/CoreUpdater/CoreUpdater.php | 132 +- .../templates/cli_update_database_done.tpl | 80 +- .../CoreUpdater/templates/cli_update_welcome.tpl | 44 +- plugins/CoreUpdater/templates/header.tpl | 68 +- .../CoreUpdater/templates/update_database_done.tpl | 123 +- .../templates/update_new_version_available.tpl | 29 +- .../templates/update_one_click_results.tpl | 19 +- plugins/CoreUpdater/templates/update_welcome.tpl | 208 +- plugins/CustomVariables/API.php | 144 +- plugins/CustomVariables/Controller.php | 78 +- plugins/CustomVariables/CustomVariables.php | 631 +- plugins/DBStats/API.php | 584 +- plugins/DBStats/Controller.php | 717 +- plugins/DBStats/DBStats.php | 128 +- plugins/DBStats/MySQLMetadataProvider.php | 706 +- plugins/DBStats/templates/index.tpl | 231 +- plugins/Dashboard/Controller.php | 137 +- plugins/Dashboard/Dashboard.php | 285 +- plugins/Dashboard/templates/dashboard.css | 266 +- plugins/Dashboard/templates/dashboard.js | 62 +- plugins/Dashboard/templates/dashboardObject.js | 182 +- plugins/Dashboard/templates/dashboardWidget.js | 82 +- plugins/Dashboard/templates/header.tpl | 159 +- plugins/Dashboard/templates/index.tpl | 105 +- plugins/Dashboard/templates/standalone.tpl | 13 +- plugins/Dashboard/templates/widgetMenu.js | 320 +- plugins/DoNotTrack/DoNotTrack.php | 97 +- plugins/ExampleAPI/API.php | 323 +- plugins/ExampleAPI/ExampleAPI.php | 38 +- plugins/ExamplePlugin/Controller.php | 220 +- plugins/ExamplePlugin/ExamplePlugin.php | 120 +- .../ExamplePlugin/config/local.config.sample.php | 6 +- plugins/ExamplePlugin/lang/en.php | 20 +- plugins/ExamplePlugin/templates/piwikDownloads.tpl | 26 +- plugins/ExampleRssWidget/Controller.php | 7 +- plugins/ExampleRssWidget/ExampleRssWidget.php | 62 +- plugins/ExampleRssWidget/Rss.php | 23 +- plugins/ExampleRssWidget/templates/styles.css | 43 +- plugins/ExampleUI/API.php | 174 +- plugins/ExampleUI/Controller.php | 234 +- plugins/ExampleUI/ExampleUI.php | 87 +- plugins/Feedback/Controller.php | 110 +- plugins/Feedback/Feedback.php | 94 +- plugins/Feedback/templates/feedback.js | 58 +- plugins/Feedback/templates/index.tpl | 103 +- plugins/Feedback/templates/sent.tpl | 31 +- plugins/Feedback/templates/styles.css | 83 +- plugins/Goals/API.php | 1061 +- plugins/Goals/Controller.php | 1083 +- plugins/Goals/Goals.php | 1738 ++- plugins/Goals/templates/GoalForm.js | 230 +- plugins/Goals/templates/add_edit_goal.tpl | 109 +- plugins/Goals/templates/add_new_goal.tpl | 15 +- plugins/Goals/templates/form_add_goal.tpl | 180 +- plugins/Goals/templates/goals.css | 33 +- plugins/Goals/templates/list_goal_edit.tpl | 88 +- plugins/Goals/templates/list_top_dimension.tpl | 11 +- plugins/Goals/templates/overview.tpl | 73 +- plugins/Goals/templates/single_goal.tpl | 73 +- .../Goals/templates/title_and_evolution_graph.tpl | 109 +- plugins/ImageGraph/API.php | 1053 +- plugins/ImageGraph/Controller.php | 106 +- plugins/ImageGraph/ImageGraph.php | 273 +- plugins/ImageGraph/StaticGraph.php | 598 +- plugins/ImageGraph/StaticGraph/3DPie.php | 18 +- plugins/ImageGraph/StaticGraph/Evolution.php | 22 +- plugins/ImageGraph/StaticGraph/Exception.php | 86 +- plugins/ImageGraph/StaticGraph/GridGraph.php | 851 +- plugins/ImageGraph/StaticGraph/HorizontalBar.php | 346 +- plugins/ImageGraph/StaticGraph/Pie.php | 18 +- plugins/ImageGraph/StaticGraph/PieGraph.php | 217 +- plugins/ImageGraph/StaticGraph/VerticalBar.php | 32 +- .../templates/debug_graphs_all_sizes.tpl | 91 +- plugins/ImageGraph/templates/index.tpl | 4 +- plugins/Installation/Controller.php | 1913 ++-- plugins/Installation/FormDatabaseSetup.php | 528 +- plugins/Installation/FormFirstWebsiteSetup.php | 105 +- plugins/Installation/FormGeneralSetup.php | 113 +- plugins/Installation/Installation.php | 142 +- plugins/Installation/View.php | 79 +- plugins/Installation/templates/allSteps.tpl | 18 +- plugins/Installation/templates/databaseCheck.tpl | 44 +- plugins/Installation/templates/databaseSetup.tpl | 14 +- .../templates/displayJavascriptCode.tpl | 28 +- plugins/Installation/templates/finished.tpl | 2 +- .../Installation/templates/firstWebsiteSetup.tpl | 20 +- plugins/Installation/templates/generalSetup.tpl | 2 +- plugins/Installation/templates/install.css | 240 +- .../Installation/templates/integrityDetails.tpl | 75 +- plugins/Installation/templates/structure.tpl | 126 +- plugins/Installation/templates/systemCheck.tpl | 14 +- plugins/Installation/templates/systemCheckPage.css | 46 +- plugins/Installation/templates/systemCheckPage.tpl | 24 +- .../Installation/templates/systemCheckSection.tpl | 558 +- .../Installation/templates/systemCheck_legend.tpl | 20 +- plugins/Installation/templates/tablesCreation.tpl | 96 +- plugins/Installation/templates/welcome.tpl | 62 +- plugins/LanguagesManager/API.php | 322 +- plugins/LanguagesManager/Controller.php | 47 +- plugins/LanguagesManager/LanguagesManager.php | 440 +- .../LanguagesManager/templates/languageSelector.js | 82 +- plugins/LanguagesManager/templates/languages.tpl | 26 +- plugins/Live/API.php | 888 +- plugins/Live/Controller.php | 255 +- plugins/Live/Live.php | 92 +- plugins/Live/Visitor.php | 1010 +- plugins/Live/templates/index.tpl | 56 +- plugins/Live/templates/lastVisits.tpl | 145 +- plugins/Live/templates/live.css | 116 +- plugins/Live/templates/scripts/live.js | 128 +- plugins/Live/templates/simpleLastVisitCount.tpl | 232 +- plugins/Live/templates/totalVisits.tpl | 51 +- plugins/Live/templates/visitorLog.tpl | 581 +- plugins/Login/Auth.php | 177 +- plugins/Login/Controller.php | 972 +- plugins/Login/FormLogin.php | 36 +- plugins/Login/FormResetPassword.php | 32 +- plugins/Login/Login.php | 355 +- plugins/Login/templates/header.tpl | 114 +- plugins/Login/templates/login.css | 251 +- plugins/Login/templates/login.js | 191 +- plugins/Login/templates/login.tpl | 141 +- plugins/Login/templates/message.tpl | 14 +- plugins/MobileMessaging/API.php | 893 +- plugins/MobileMessaging/Controller.php | 91 +- plugins/MobileMessaging/CountryCallingCodes.php | 486 +- plugins/MobileMessaging/GSMCharset.php | 276 +- plugins/MobileMessaging/MobileMessaging.php | 621 +- .../MobileMessaging/ReportRenderer/Exception.php | 90 +- plugins/MobileMessaging/ReportRenderer/Sms.php | 198 +- plugins/MobileMessaging/SMSProvider.php | 283 +- plugins/MobileMessaging/SMSProvider/Clockwork.php | 163 +- .../SMSProvider/StubbedProvider.php | 24 +- .../scripts/MobileMessagingSettings.js | 337 +- .../MobileMessaging/templates/ReportParameters.tpl | 105 +- plugins/MobileMessaging/templates/SMSReport.tpl | 138 +- plugins/MobileMessaging/templates/Settings.tpl | 347 +- plugins/MultiSites/API.php | 930 +- plugins/MultiSites/Controller.php | 431 +- plugins/MultiSites/MultiSites.php | 157 +- plugins/MultiSites/templates/common.js | 411 +- plugins/MultiSites/templates/index.tpl | 191 +- plugins/MultiSites/templates/row.tpl | 37 +- plugins/MultiSites/templates/styles.css | 76 +- plugins/Overlay/API.php | 219 +- plugins/Overlay/Controller.php | 340 +- plugins/Overlay/Overlay.php | 46 +- plugins/Overlay/client/client.css | 161 +- plugins/Overlay/client/client.js | 518 +- plugins/Overlay/client/followingpages.js | 1018 +- plugins/Overlay/client/translations.js | 56 +- plugins/Overlay/client/urlnormalizer.js | 385 +- plugins/Overlay/templates/error_wrong_domain.tpl | 54 +- plugins/Overlay/templates/helper.js | 44 +- plugins/Overlay/templates/index.css | 131 +- plugins/Overlay/templates/index.js | 408 +- plugins/Overlay/templates/index.tpl | 60 +- plugins/Overlay/templates/index_noframe.tpl | 32 +- plugins/Overlay/templates/notify_parent_iframe.tpl | 16 +- plugins/Overlay/templates/rowaction.js | 68 +- plugins/Overlay/templates/sidebar.tpl | 34 +- plugins/PDFReports/API.php | 1430 ++- plugins/PDFReports/Controller.php | 102 +- plugins/PDFReports/PDFReports.php | 1208 +-- plugins/PDFReports/config/tcpdf_config.php | 432 +- plugins/PDFReports/templates/add.tpl | 260 +- plugins/PDFReports/templates/index.tpl | 62 +- plugins/PDFReports/templates/list.tpl | 182 +- plugins/PDFReports/templates/pdf.js | 242 +- plugins/PDFReports/templates/report_parameters.tpl | 187 +- plugins/PrivacyManager/Controller.php | 557 +- plugins/PrivacyManager/LogDataPurger.php | 615 +- plugins/PrivacyManager/PrivacyManager.php | 560 +- plugins/PrivacyManager/ReportsPurger.php | 733 +- plugins/PrivacyManager/templates/databaseSize.tpl | 4 +- .../PrivacyManager/templates/privacySettings.js | 300 +- .../PrivacyManager/templates/privacySettings.tpl | 469 +- plugins/Provider/API.php | 51 +- plugins/Provider/Controller.php | 53 +- plugins/Provider/Provider.php | 481 +- plugins/Provider/functions.php | 63 +- plugins/Proxy/Controller.php | 313 +- plugins/Proxy/Proxy.php | 38 +- plugins/Proxy/templates/exportImage.tpl | 19 +- plugins/Referers/API.php | 969 +- plugins/Referers/Controller.php | 1349 ++- plugins/Referers/Referers.php | 1153 +- plugins/Referers/functions.php | 262 +- .../Referers/templates/Websites_SocialNetworks.tpl | 8 +- plugins/Referers/templates/index.tpl | 128 +- .../Referers/templates/searchEngines_Keywords.tpl | 8 +- plugins/SEO/API.php | 153 +- plugins/SEO/Controller.php | 55 +- plugins/SEO/MajesticClient.php | 154 +- plugins/SEO/RankChecker.php | 379 +- plugins/SEO/SEO.php | 44 +- plugins/SEO/templates/index.tpl | 78 +- plugins/SEO/templates/rank.js | 2 +- plugins/SecurityInfo/Controller.php | 42 +- plugins/SecurityInfo/PhpSecInfo/PhpSecInfo.php | 976 +- .../PhpSecInfo/Test/Application/php.php | 77 +- .../PhpSecInfo/Test/Application/piwik.php | 55 +- .../PhpSecInfo/Test/CGI/force_redirect.php | 157 +- .../PhpSecInfo/Test/Core/allow_url_fopen.php | 100 +- .../PhpSecInfo/Test/Core/allow_url_include.php | 107 +- .../PhpSecInfo/Test/Core/display_errors.php | 85 +- .../PhpSecInfo/Test/Core/expose_php.php | 85 +- .../PhpSecInfo/Test/Core/file_uploads.php | 86 +- plugins/SecurityInfo/PhpSecInfo/Test/Core/gid.php | 112 +- .../PhpSecInfo/Test/Core/magic_quotes_gpc.php | 83 +- .../PhpSecInfo/Test/Core/memory_limit.php | 87 +- .../PhpSecInfo/Test/Core/open_basedir.php | 81 +- .../PhpSecInfo/Test/Core/post_max_size.php | 68 +- .../PhpSecInfo/Test/Core/register_globals.php | 83 +- plugins/SecurityInfo/PhpSecInfo/Test/Core/uid.php | 114 +- .../PhpSecInfo/Test/Core/upload_max_filesize.php | 84 +- .../PhpSecInfo/Test/Core/upload_tmp_dir.php | 133 +- .../PhpSecInfo/Test/Curl/file_support.php | 76 +- .../PhpSecInfo/Test/Session/save_path.php | 165 +- .../PhpSecInfo/Test/Session/use_trans_sid.php | 63 +- .../PhpSecInfo/Test/Suhosin/extension.php | 39 +- .../SecurityInfo/PhpSecInfo/Test/Suhosin/patch.php | 67 +- plugins/SecurityInfo/PhpSecInfo/Test/Test.php | 1087 +- .../PhpSecInfo/Test/Test_Application.php | 71 +- plugins/SecurityInfo/PhpSecInfo/Test/Test_Cgi.php | 78 +- plugins/SecurityInfo/PhpSecInfo/Test/Test_Core.php | 50 +- plugins/SecurityInfo/PhpSecInfo/Test/Test_Curl.php | 80 +- .../SecurityInfo/PhpSecInfo/Test/Test_Session.php | 52 +- .../SecurityInfo/PhpSecInfo/Test/Test_Suhosin.php | 65 +- plugins/SecurityInfo/SecurityInfo.php | 56 +- plugins/SecurityInfo/templates/index.tpl | 41 +- plugins/SitesManager/API.php | 2655 +++-- plugins/SitesManager/Controller.php | 359 +- plugins/SitesManager/SitesManager.php | 408 +- .../templates/DisplayAlternativeTags.tpl | 203 +- .../templates/DisplayJavascriptCode.tpl | 109 +- plugins/SitesManager/templates/SitesManager.js | 551 +- plugins/SitesManager/templates/SitesManager.tpl | 648 +- plugins/Transitions/API.php | 582 +- plugins/Transitions/Controller.php | 150 +- plugins/Transitions/Transitions.php | 691 +- plugins/Transitions/templates/transitions.css | 197 +- plugins/Transitions/templates/transitions.js | 2380 ++--- plugins/Transitions/templates/transitions.tpl | 91 +- plugins/UserCountry/API.php | 381 +- plugins/UserCountry/Controller.php | 957 +- plugins/UserCountry/GeoIPAutoUpdater.php | 1124 +- plugins/UserCountry/LocationProvider.php | 883 +- plugins/UserCountry/LocationProvider/Default.php | 182 +- plugins/UserCountry/LocationProvider/GeoIp.php | 495 +- .../UserCountry/LocationProvider/GeoIp/Pecl.php | 641 +- plugins/UserCountry/LocationProvider/GeoIp/Php.php | 651 +- .../LocationProvider/GeoIp/ServerBased.php | 547 +- plugins/UserCountry/UserCountry.php | 1012 +- plugins/UserCountry/functions.php | 180 +- plugins/UserCountry/templates/admin.js | 298 +- plugins/UserCountry/templates/adminIndex.tpl | 199 +- plugins/UserCountry/templates/index.tpl | 29 +- plugins/UserCountry/templates/styles.css | 59 +- plugins/UserCountry/templates/updaterSetup.tpl | 5 +- plugins/UserCountryMap/Controller.php | 392 +- plugins/UserCountryMap/UserCountryMap.php | 42 +- plugins/UserCountryMap/css/map.css | 7 +- plugins/UserCountryMap/css/qtip.css | 218 +- plugins/UserCountryMap/css/realtime-map.css | 45 +- plugins/UserCountryMap/css/visitor-map.css | 31 +- plugins/UserCountryMap/js/realtime-map.js | 132 +- plugins/UserCountryMap/js/vendor/chroma.min.js | 680 +- .../UserCountryMap/js/vendor/jquery.qtip.min.js | 364 +- plugins/UserCountryMap/js/vendor/kartograph.js | 10638 ++++++++++--------- plugins/UserCountryMap/js/vendor/kartograph.min.js | 1674 ++- plugins/UserCountryMap/js/vendor/kmeans.js | 256 +- plugins/UserCountryMap/js/vendor/raphael-min.js | 2215 +++- plugins/UserCountryMap/js/visitor-map.js | 759 +- plugins/UserCountryMap/templates/realtime-map.tpl | 60 +- plugins/UserCountryMap/templates/visitor-map.tpl | 77 +- plugins/UserSettings/API.php | 372 +- plugins/UserSettings/Controller.php | 360 +- plugins/UserSettings/UserSettings.php | 859 +- plugins/UserSettings/functions.php | 314 +- plugins/UserSettings/templates/index.tpl | 42 +- plugins/UsersManager/API.php | 1307 ++- plugins/UsersManager/Controller.php | 675 +- plugins/UsersManager/UsersManager.php | 245 +- plugins/UsersManager/templates/UsersManager.js | 315 +- plugins/UsersManager/templates/UsersManager.tpl | 251 +- plugins/UsersManager/templates/userSettings.js | 99 +- plugins/UsersManager/templates/userSettings.tpl | 225 +- plugins/VisitFrequency/API.php | 258 +- plugins/VisitFrequency/Controller.php | 170 +- plugins/VisitFrequency/VisitFrequency.php | 234 +- plugins/VisitFrequency/templates/index.tpl | 4 +- plugins/VisitFrequency/templates/sparklines.tpl | 13 +- plugins/VisitTime/API.php | 285 +- plugins/VisitTime/Controller.php | 181 +- plugins/VisitTime/VisitTime.php | 416 +- plugins/VisitTime/templates/index.tpl | 8 +- plugins/VisitorGenerator/Controller.php | 278 +- plugins/VisitorGenerator/VisitorGenerator.php | 50 +- plugins/VisitorGenerator/templates/generate.tpl | 19 +- plugins/VisitorGenerator/templates/index.tpl | 58 +- plugins/VisitorInterest/API.php | 208 +- plugins/VisitorInterest/Controller.php | 196 +- plugins/VisitorInterest/VisitorInterest.php | 633 +- plugins/VisitorInterest/templates/index.tpl | 1 - plugins/VisitsSummary/API.php | 274 +- plugins/VisitsSummary/Controller.php | 301 +- plugins/VisitsSummary/VisitsSummary.php | 112 +- plugins/VisitsSummary/templates/sparklines.tpl | 102 +- plugins/Widgetize/Controller.php | 120 +- plugins/Widgetize/Widgetize.php | 112 +- plugins/Widgetize/templates/iframe.tpl | 24 +- plugins/Widgetize/templates/index.tpl | 165 +- plugins/Widgetize/templates/js.tpl | 36 +- plugins/Widgetize/templates/test_jsinclude.tpl | 7 +- plugins/Widgetize/templates/widgetize.js | 148 +- tests/LocalTracker.php | 212 +- tests/PHPUnit/BaseFixture.php | 447 +- tests/PHPUnit/BenchmarkTestCase.php | 178 +- .../Benchmarks/ArchivingProcessBenchmark.php | 22 +- .../OneSiteTwelveThousandVisitsOneYear.php | 104 +- tests/PHPUnit/Benchmarks/Fixtures/SqlDump.php | 107 +- .../ThousandSitesTwelveVisitsEachOneDay.php | 121 +- tests/PHPUnit/Benchmarks/TrackerBenchmark.php | 115 +- tests/PHPUnit/Core/API/ResponseBuilderTest.php | 48 +- tests/PHPUnit/Core/AccessTest.php | 66 +- tests/PHPUnit/Core/ArchiveProcessing/DayTest.php | 194 +- tests/PHPUnit/Core/ArchiveProcessingTest.php | 227 +- tests/PHPUnit/Core/CommonTest.php | 435 +- tests/PHPUnit/Core/ConfigTest.php | 17 +- tests/PHPUnit/Core/CookieTest.php | 62 +- tests/PHPUnit/Core/DataTable/ArrayTest.php | 8 +- .../Core/DataTable/Filter/AddSummaryRowTest.php | 105 +- .../DataTable/Filter/ExcludeLowPopulationTest.php | 83 +- tests/PHPUnit/Core/DataTable/Filter/LimitTest.php | 224 +- .../Core/DataTable/Filter/PatternRecursiveTest.php | 26 +- .../PHPUnit/Core/DataTable/Filter/PatternTest.php | 28 +- .../Core/DataTable/Filter/RangeCheckTest.php | 28 +- tests/PHPUnit/Core/DataTable/Filter/SortTest.php | 138 +- .../PHPUnit/Core/DataTable/Filter/TruncateTest.php | 90 +- tests/PHPUnit/Core/DataTable/Renderer/CSVTest.php | 202 +- .../Core/DataTable/Renderer/ConsoleTest.php | 126 +- tests/PHPUnit/Core/DataTable/Renderer/JSONTest.php | 206 +- tests/PHPUnit/Core/DataTable/Renderer/PHPTest.php | 438 +- tests/PHPUnit/Core/DataTable/Renderer/XMLTest.php | 184 +- tests/PHPUnit/Core/DataTable/RowTest.php | 274 +- tests/PHPUnit/Core/DataTableTest.php | 847 +- tests/PHPUnit/Core/DateTest.php | 57 +- tests/PHPUnit/Core/HttpTest.php | 98 +- tests/PHPUnit/Core/IPTest.php | 349 +- tests/PHPUnit/Core/JsProxyTest.php | 4 +- tests/PHPUnit/Core/NonceTest.php | 10 +- tests/PHPUnit/Core/OptionTest.php | 14 +- tests/PHPUnit/Core/Period/DayTest.php | 96 +- tests/PHPUnit/Core/Period/MonthTest.php | 40 +- tests/PHPUnit/Core/Period/RangeTest.php | 1048 +- tests/PHPUnit/Core/Period/WeekTest.php | 44 +- tests/PHPUnit/Core/Period/YearTest.php | 20 +- tests/PHPUnit/Core/PeriodTest.php | 34 +- tests/PHPUnit/Core/PiwikTest.php | 148 +- tests/PHPUnit/Core/Plugin/ConfigTest.php | 3 +- .../Core/PluginsFunctions/WidgetsListTest.php | 30 +- tests/PHPUnit/Core/RankingQueryTest.php | 128 +- tests/PHPUnit/Core/ReleaseCheckListTest.php | 82 +- tests/PHPUnit/Core/ScheduledTaskTest.php | 60 +- tests/PHPUnit/Core/ScheduledTime/DailyTest.php | 35 +- tests/PHPUnit/Core/ScheduledTime/HourlyTest.php | 18 +- tests/PHPUnit/Core/ScheduledTime/MonthlyTest.php | 50 +- tests/PHPUnit/Core/ScheduledTime/WeeklyTest.php | 40 +- tests/PHPUnit/Core/SegmentExpressionTest.php | 4 +- tests/PHPUnit/Core/SegmentTest.php | 189 +- tests/PHPUnit/Core/ServeStaticFileTest.php | 4 +- tests/PHPUnit/Core/SqlTest.php | 52 +- tests/PHPUnit/Core/TablePartitioningTest.php | 40 +- tests/PHPUnit/Core/TaskSchedulerTest.php | 611 +- tests/PHPUnit/Core/Tracker/ActionTest.php | 219 +- tests/PHPUnit/Core/Tracker/VisitTest.php | 157 +- tests/PHPUnit/Core/TranslationWriterTest.php | 17 +- tests/PHPUnit/Core/UnzipTest.php | 103 +- tests/PHPUnit/Core/UpdaterTest.php | 12 +- tests/PHPUnit/Core/UrlTest.php | 40 +- tests/PHPUnit/DatabaseTestCase.php | 19 +- tests/PHPUnit/FakeAccess.php | 128 +- .../PHPUnit/Fixtures/FewVisitsWithSetVisitorId.php | 20 +- tests/PHPUnit/Fixtures/InvalidVisits.php | 39 +- tests/PHPUnit/Fixtures/ManySitesImportedLogs.php | 289 +- tests/PHPUnit/Fixtures/ManyVisitsWithGeoIP.php | 334 +- .../ManyVisitsWithMockLocationProvider.php | 374 +- .../ManyVisitsWithSubDirReferrersAndCustomVars.php | 23 +- .../PHPUnit/Fixtures/OneVisitSeveralPageViews.php | 26 +- .../Fixtures/OneVisitWithAbnormalPageviewUrls.php | 18 +- tests/PHPUnit/Fixtures/OneVisitorTwoVisits.php | 127 +- .../PHPUnit/Fixtures/SomeVisitsAllConversions.php | 26 +- ...VisitsCustomVariablesCampaignsNotHeuristics.php | 44 +- .../SomeVisitsManyPageviewsWithTransitions.php | 150 +- tests/PHPUnit/Fixtures/SomeVisitsWithLongUrls.php | 16 +- .../SomeVisitsWithNonUnicodePageTitles.php | 148 +- tests/PHPUnit/Fixtures/ThreeGoalsOnePageview.php | 48 +- .../ThreeSitesWithManyVisitsWithSiteSearch.php | 318 +- .../Fixtures/TwoSitesEcommerceOrderWithItems.php | 30 +- ...itsOverSeveralDaysWithSearchEngineReferrers.php | 63 +- .../Fixtures/TwoSitesTwoVisitorsDifferentDays.php | 54 +- tests/PHPUnit/Fixtures/TwoSitesVisitsInPast.php | 18 +- tests/PHPUnit/Fixtures/TwoSitesWithAnnotations.php | 113 +- .../PHPUnit/Fixtures/TwoVisitsNoKeywordWithBot.php | 16 +- .../Fixtures/TwoVisitsWithCustomVariables.php | 49 +- tests/PHPUnit/Fixtures/VisitsOverSeveralDays.php | 58 +- tests/PHPUnit/Integration/AnnotationsTest.php | 662 +- .../Integration/ApiGetReportMetadataTest.php | 46 +- .../Integration/ApiGetReportMetadata_yearTest.php | 8 +- .../PHPUnit/Integration/BlobReportLimitingTest.php | 155 +- tests/PHPUnit/Integration/CsvExportTest.php | 10 +- .../Integration/EcommerceOrderWithItemsTest.php | 264 +- tests/PHPUnit/Integration/FlattenReportsTest.php | 56 +- tests/PHPUnit/Integration/ImportLogsTest.php | 70 +- tests/PHPUnit/Integration/LabelFilterTest.php | 124 +- .../Integration/ManyVisitorsOneWebsiteTest.php | 108 +- tests/PHPUnit/Integration/NoVisitTest.php | 6 +- tests/PHPUnit/Integration/NonUnicodeTest.php | 68 +- ...eVisitorOneWebsite_SeveralDaysDateRangeTest.php | 90 +- ...ite_SeveralDaysDateRange_ArchivingTestsTest.php | 20 +- .../Integration/OneVisitorTwoVisitsTest.php | 225 +- .../OneVisitorTwoVisits_withCookieSupportTest.php | 2 +- .../OneVisitor_LongUrlsTruncatedTest.php | 2 +- .../OneVisitor_NoKeywordSpecifiedTest.php | 8 +- ...sRange_DateIsLastN_MetadataAndNormalAPITest.php | 52 +- tests/PHPUnit/Integration/RowEvolutionTest.php | 148 +- tests/PHPUnit/Integration/SiteSearchTest.php | 40 +- ...ampaigns_ForceUsingVisitIdNotHeuristicsTest.php | 4 +- ...kGoals_AllowMultipleConversionsPerVisitTest.php | 6 +- .../Integration/TrackingAPI_SetVisitorIdTest.php | 8 +- tests/PHPUnit/Integration/TransitionsTest.php | 58 +- .../TwoVisitors_TwoWebsites_DifferentDaysTest.php | 44 +- ...ebsites_DifferentDays_ArchivingDisabledTest.php | 8 +- ...s_TwoWebsites_DifferentDays_ConversionsTest.php | 34 +- .../TwoVisitsWithCustomVariablesTest.php | 26 +- ...sitsWithCustomVariables_SegmentContainsTest.php | 20 +- ...tomVariables_SegmentMatchALL_NoGoalDataTest.php | 6 +- ...itsWithCustomVariables_SegmentMatchNONETest.php | 8 +- ...CustomVariables_SegmentMatchVisitorTypeTest.php | 8 +- tests/PHPUnit/Integration/UrlNormalizationTest.php | 20 +- .../VisitsInPast_InvalidateOldReportsTest.php | 32 +- ...firstSite_lastN__API.getProcessedReport_day.xml | 4 +- ...__PDFReports.generateReport_month.original.html | 165 +- ...y__PDFReports.generateReport_week.original.html | 180 +- tests/PHPUnit/IntegrationTestCase.php | 1092 +- tests/PHPUnit/MockEventDispatcher.php | 19 +- tests/PHPUnit/MockLocationProvider.php | 81 +- tests/PHPUnit/MockPiwikOption.php | 25 +- tests/PHPUnit/Plugins/ActionsTest.php | 84 +- tests/PHPUnit/Plugins/AnonymizeIPTest.php | 45 +- tests/PHPUnit/Plugins/LanguagesManagerTest.php | 56 +- tests/PHPUnit/Plugins/LoginTest.php | 29 +- tests/PHPUnit/Plugins/MobileMessagingTest.php | 384 +- tests/PHPUnit/Plugins/MultiSitesTest.php | 64 +- tests/PHPUnit/Plugins/PDFReportsTest.php | 927 +- tests/PHPUnit/Plugins/PrivacyManagerTest.php | 466 +- tests/PHPUnit/Plugins/ReferersTest.php | 40 +- tests/PHPUnit/Plugins/SEOTest.php | 7 +- tests/PHPUnit/Plugins/SitesManagerTest.php | 579 +- tests/PHPUnit/Plugins/UserCountryTest.php | 133 +- tests/PHPUnit/Plugins/UserSettingsTest.php | 7 +- tests/PHPUnit/Plugins/UsersManagerTest.php | 651 +- tests/PHPUnit/bootstrap.php | 80 +- tests/PHPUnit/config.ini.travis.php | 20 +- tests/PHPUnit/proxy/index.php | 35 +- tests/PHPUnit/proxy/piwik.php | 12 +- tests/index.php | 27 +- 854 files changed, 121350 insertions(+), 121446 deletions(-) diff --git a/core/API/DataTableGenericFilter.php b/core/API/DataTableGenericFilter.php index ffdd4de044..02e2b31a24 100644 --- a/core/API/DataTableGenericFilter.php +++ b/core/API/DataTableGenericFilter.php @@ -1,10 +1,10 @@ request = $request; - } + /** + * Constructor + * + * @param $request + */ + function __construct($request) + { + $this->request = $request; + } - /** - * Filters the given data table - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - $this->applyGenericFilters($table); - } - - /** - * Returns an array containing the information of the generic Piwik_DataTable_Filter - * to be applied automatically to the data resulting from the API calls. - * - * Order to apply the filters: - * 1 - Filter that remove filtered rows - * 2 - Filter that sort the remaining rows - * 3 - Filter that keep only a subset of the results - * 4 - Presentation filters - * - * @return array See the code for spec - */ - public static function getGenericFiltersInformation() - { - if (is_null(self::$genericFiltersInfo)) - { - self::$genericFiltersInfo = array( - 'Pattern' => array( - 'filter_column' => array('string', 'label'), - 'filter_pattern' => array('string'), - ), - 'PatternRecursive' => array( - 'filter_column_recursive' => array('string', 'label'), - 'filter_pattern_recursive' => array('string'), - ), - 'ExcludeLowPopulation' => array( - 'filter_excludelowpop' => array('string'), - 'filter_excludelowpop_value'=> array('float', '0'), - ), - 'AddColumnsProcessedMetrics' => array( - 'filter_add_columns_when_show_all_columns' => array('integer') - ), - 'AddColumnsProcessedMetricsGoal' => array( - 'filter_update_columns_when_show_all_goals' => array('integer'), - 'idGoal' => array('string', Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal::GOALS_OVERVIEW), - ), - 'Sort' => array( - 'filter_sort_column' => array('string'), - 'filter_sort_order' => array('string', 'desc'), - ), - 'Truncate' => array( - 'filter_truncate' => array('integer'), - ), - 'Limit' => array( - 'filter_offset' => array('integer', '0'), - 'filter_limit' => array('integer'), - 'keep_summary_row' => array('integer', '0'), - ), - ); - } + /** + * Filters the given data table + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + $this->applyGenericFilters($table); + } - return self::$genericFiltersInfo; - } - - /** - * Apply generic filters to the DataTable object resulting from the API Call. - * Disable this feature by setting the parameter disable_generic_filters to 1 in the API call request. - * - * @param Piwik_DataTable $datatable - * @return bool - */ - protected function applyGenericFilters($datatable) - { - if($datatable instanceof Piwik_DataTable_Array ) - { - $tables = $datatable->getArray(); - $filterWasApplied = false; - foreach($tables as $table) - { - $filterWasApplied = $this->applyGenericFilters($table); - } - return; - } - - $genericFilters = self::getGenericFiltersInformation(); - - $filterApplied = false; - foreach($genericFilters as $filterName => $parameters) - { - $filterParameters = array(); - $exceptionRaised = false; - foreach($parameters as $name => $info) - { - // parameter type to cast to - $type = $info[0]; - - // default value if specified, when the parameter doesn't have a value - $defaultValue = null; - if(isset($info[1])) - { - $defaultValue = $info[1]; - } - - // third element in the array, if it exists, overrides the name of the request variable - $varName = $name; - if(isset($info[2])) - { - $varName = $info[2]; - } - - try { - $value = Piwik_Common::getRequestVar($name, $defaultValue, $type, $this->request); - settype($value, $type); - $filterParameters[] = $value; - } - catch(Exception $e) - { - $exceptionRaised = true; - break; - } - } + /** + * Returns an array containing the information of the generic Piwik_DataTable_Filter + * to be applied automatically to the data resulting from the API calls. + * + * Order to apply the filters: + * 1 - Filter that remove filtered rows + * 2 - Filter that sort the remaining rows + * 3 - Filter that keep only a subset of the results + * 4 - Presentation filters + * + * @return array See the code for spec + */ + public static function getGenericFiltersInformation() + { + if (is_null(self::$genericFiltersInfo)) { + self::$genericFiltersInfo = array( + 'Pattern' => array( + 'filter_column' => array('string', 'label'), + 'filter_pattern' => array('string'), + ), + 'PatternRecursive' => array( + 'filter_column_recursive' => array('string', 'label'), + 'filter_pattern_recursive' => array('string'), + ), + 'ExcludeLowPopulation' => array( + 'filter_excludelowpop' => array('string'), + 'filter_excludelowpop_value' => array('float', '0'), + ), + 'AddColumnsProcessedMetrics' => array( + 'filter_add_columns_when_show_all_columns' => array('integer') + ), + 'AddColumnsProcessedMetricsGoal' => array( + 'filter_update_columns_when_show_all_goals' => array('integer'), + 'idGoal' => array('string', Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal::GOALS_OVERVIEW), + ), + 'Sort' => array( + 'filter_sort_column' => array('string'), + 'filter_sort_order' => array('string', 'desc'), + ), + 'Truncate' => array( + 'filter_truncate' => array('integer'), + ), + 'Limit' => array( + 'filter_offset' => array('integer', '0'), + 'filter_limit' => array('integer'), + 'keep_summary_row' => array('integer', '0'), + ), + ); + } - if(!$exceptionRaised) - { - $datatable->filter($filterName, $filterParameters); - $filterApplied = true; - } - } - return $filterApplied; - } + return self::$genericFiltersInfo; + } + + /** + * Apply generic filters to the DataTable object resulting from the API Call. + * Disable this feature by setting the parameter disable_generic_filters to 1 in the API call request. + * + * @param Piwik_DataTable $datatable + * @return bool + */ + protected function applyGenericFilters($datatable) + { + if ($datatable instanceof Piwik_DataTable_Array) { + $tables = $datatable->getArray(); + $filterWasApplied = false; + foreach ($tables as $table) { + $filterWasApplied = $this->applyGenericFilters($table); + } + return; + } + + $genericFilters = self::getGenericFiltersInformation(); + + $filterApplied = false; + foreach ($genericFilters as $filterName => $parameters) { + $filterParameters = array(); + $exceptionRaised = false; + foreach ($parameters as $name => $info) { + // parameter type to cast to + $type = $info[0]; + + // default value if specified, when the parameter doesn't have a value + $defaultValue = null; + if (isset($info[1])) { + $defaultValue = $info[1]; + } + + // third element in the array, if it exists, overrides the name of the request variable + $varName = $name; + if (isset($info[2])) { + $varName = $info[2]; + } + + try { + $value = Piwik_Common::getRequestVar($name, $defaultValue, $type, $this->request); + settype($value, $type); + $filterParameters[] = $value; + } catch (Exception $e) { + $exceptionRaised = true; + break; + } + } + + if (!$exceptionRaised) { + $datatable->filter($filterName, $filterParameters); + $filterApplied = true; + } + } + return $filterApplied; + } } diff --git a/core/API/DataTableManipulator.php b/core/API/DataTableManipulator.php index b7f1f79a2e..dee8d80316 100644 --- a/core/API/DataTableManipulator.php +++ b/core/API/DataTableManipulator.php @@ -1,10 +1,10 @@ apiModule = $apiModule; - $this->apiMethod = $apiMethod; - $this->request = $request; - } - - /** - * This method can be used by subclasses to iterate over data tables that might be - * data table arrays. It calls back the template method self::doManipulate for each table. - * This way, data table arrays can be handled in a transparent fashion. - * - * @param Piwik_DataTable_Array|Piwik_DataTable $dataTable - * @throws Exception - * @return Piwik_DataTable_Array|Piwik_DataTable - */ - protected function manipulate($dataTable) - { - if ($dataTable instanceof Piwik_DataTable_Array) - { - return $this->manipulateDataTableArray($dataTable); - } - else if ($dataTable instanceof Piwik_DataTable) - { - return $this->manipulateDataTable($dataTable); - } - else - { - return $dataTable; - } - } - - /** - * Manipulates child DataTables of a DataTable_Array. See @manipulate for more info. - */ - protected function manipulateDataTableArray( $dataTable ) - { - $result = $dataTable->getEmptyClone(); - foreach ($dataTable->getArray() as $tableLabel => $childTable) - { - $newTable = $this->manipulate($childTable); - $result->addTable($newTable, $tableLabel); - } - return $result; - } - - /** - * Manipulates a single Piwik_DataTable instance. Derived classes must define - * this function. - */ - protected abstract function manipulateDataTable( $dataTable ); - - /** - * Load the subtable for a row. - * Returns null if none is found. - * - * @param Piwik_Datatable_Row $row - * @throws Exception - * @return Piwik_DataTable - */ - protected function loadSubtable($dataTable, $row) { - if (!($this->apiModule && $this->apiMethod && count($this->request))) { - return null; - } - - $request = $this->request; - + protected $apiModule; + protected $apiMethod; + protected $request; + + private $apiMethodForSubtable; + + /** + * Constructor + * + * @param bool $apiModule + * @param bool $apiMethod + * @param array $request + */ + public function __construct($apiModule = false, $apiMethod = false, $request = array()) + { + $this->apiModule = $apiModule; + $this->apiMethod = $apiMethod; + $this->request = $request; + } + + /** + * This method can be used by subclasses to iterate over data tables that might be + * data table arrays. It calls back the template method self::doManipulate for each table. + * This way, data table arrays can be handled in a transparent fashion. + * + * @param Piwik_DataTable_Array|Piwik_DataTable $dataTable + * @throws Exception + * @return Piwik_DataTable_Array|Piwik_DataTable + */ + protected function manipulate($dataTable) + { + if ($dataTable instanceof Piwik_DataTable_Array) { + return $this->manipulateDataTableArray($dataTable); + } else if ($dataTable instanceof Piwik_DataTable) { + return $this->manipulateDataTable($dataTable); + } else { + return $dataTable; + } + } + + /** + * Manipulates child DataTables of a DataTable_Array. See @manipulate for more info. + */ + protected function manipulateDataTableArray($dataTable) + { + $result = $dataTable->getEmptyClone(); + foreach ($dataTable->getArray() as $tableLabel => $childTable) { + $newTable = $this->manipulate($childTable); + $result->addTable($newTable, $tableLabel); + } + return $result; + } + + /** + * Manipulates a single Piwik_DataTable instance. Derived classes must define + * this function. + */ + protected abstract function manipulateDataTable($dataTable); + + /** + * Load the subtable for a row. + * Returns null if none is found. + * + * @param Piwik_Datatable_Row $row + * @throws Exception + * @return Piwik_DataTable + */ + protected function loadSubtable($dataTable, $row) + { + if (!($this->apiModule && $this->apiMethod && count($this->request))) { + return null; + } + + $request = $this->request; + $idSubTable = $row->getIdSubDataTable(); - if ($idSubTable === null) - { - return null; - } - - $request['idSubtable'] = $idSubTable; - if ($dataTable) - { - $request['date'] = $dataTable->metadata['period']->getDateStart()->toString(); - } - - $class = 'Piwik_'.$this->apiModule.'_API'; + if ($idSubTable === null) { + return null; + } + + $request['idSubtable'] = $idSubTable; + if ($dataTable) { + $request['date'] = $dataTable->metadata['period']->getDateStart()->toString(); + } + + $class = 'Piwik_' . $this->apiModule . '_API'; $method = $this->getApiMethodForSubtable(); - + $this->manipulateSubtableRequest($request); $request['serialize'] = 0; - $request['expanded'] = 0; - - // don't want to run recursive filters on the subtables as they are loaded, - // otherwise the result will be empty in places (or everywhere). instead we - // run it on the flattened table. - unset($request['filter_pattern_recursive']); - - $dataTable = Piwik_API_Proxy::getInstance()->call($class, $method, $request); - $response = new Piwik_API_ResponseBuilder($format = 'original', $request); - $dataTable = $response->getResponse($dataTable); - if (method_exists($dataTable, 'applyQueuedFilters')) - { - $dataTable->applyQueuedFilters(); - } - - return $dataTable; - } - - /** - * In this method, subclasses can clean up the request array for loading subtables - * in order to make Piwik_API_ResponseBuilder behave correctly (e.g. not trigger the - * manipulator again). - * - * @param $request - * @return - */ - protected abstract function manipulateSubtableRequest(&$request); - - /** - * Extract the API method for loading subtables from the meta data - * - * @return string - */ - private function getApiMethodForSubtable() - { - if (!$this->apiMethodForSubtable) - { - $meta = Piwik_API_API::getInstance()->getMetadata('all', $this->apiModule, $this->apiMethod); - if (isset($meta[0]['actionToLoadSubTables'])) - { - $this->apiMethodForSubtable = $meta[0]['actionToLoadSubTables']; - } - else - { - $this->apiMethodForSubtable = $this->apiMethod; - } - } - return $this->apiMethodForSubtable; - } - + $request['expanded'] = 0; + + // don't want to run recursive filters on the subtables as they are loaded, + // otherwise the result will be empty in places (or everywhere). instead we + // run it on the flattened table. + unset($request['filter_pattern_recursive']); + + $dataTable = Piwik_API_Proxy::getInstance()->call($class, $method, $request); + $response = new Piwik_API_ResponseBuilder($format = 'original', $request); + $dataTable = $response->getResponse($dataTable); + if (method_exists($dataTable, 'applyQueuedFilters')) { + $dataTable->applyQueuedFilters(); + } + + return $dataTable; + } + + /** + * In this method, subclasses can clean up the request array for loading subtables + * in order to make Piwik_API_ResponseBuilder behave correctly (e.g. not trigger the + * manipulator again). + * + * @param $request + * @return + */ + protected abstract function manipulateSubtableRequest(&$request); + + /** + * Extract the API method for loading subtables from the meta data + * + * @return string + */ + private function getApiMethodForSubtable() + { + if (!$this->apiMethodForSubtable) { + $meta = Piwik_API_API::getInstance()->getMetadata('all', $this->apiModule, $this->apiMethod); + if (isset($meta[0]['actionToLoadSubTables'])) { + $this->apiMethodForSubtable = $meta[0]['actionToLoadSubTables']; + } else { + $this->apiMethodForSubtable = $this->apiMethod; + } + } + return $this->apiMethodForSubtable; + } + } diff --git a/core/API/DataTableManipulator/Flattener.php b/core/API/DataTableManipulator/Flattener.php index e6cd3c0991..a3360d5618 100644 --- a/core/API/DataTableManipulator/Flattener.php +++ b/core/API/DataTableManipulator/Flattener.php @@ -1,136 +1,126 @@ includeAggregateRows = true; - } - - /** - * Separator for building recursive labels (or paths) - * @var string - */ - public $recursiveLabelSeparator = ' - '; - /** - * @param Piwik_DataTable $dataTable - * @return Piwik_DataTable|Piwik_DataTable_Array - */ - public function flatten($dataTable) - { - if ($this->apiModule == 'Actions' || $this->apiMethod == 'getWebsites') - { - $this->recursiveLabelSeparator = '/'; - } - - return $this->manipulate($dataTable); - } + private $includeAggregateRows = false; - /** - * Template method called from self::manipulate. - * Flatten each data table. - * - * @param Piwik_DataTable $dataTable - * @return Piwik_DataTable - */ - protected function manipulateDataTable($dataTable) - { - $newDataTable = $dataTable->getEmptyClone(); - foreach ($dataTable->getRows() as $row) - { - $this->flattenRow($row, $newDataTable); - } - return $newDataTable; - } + /** + * If the flattener is used after calling this method, aggregate rows will + * be included in the result. This can be useful when they contain data that + * the leafs don't have (e.g. conversion stats in some cases). + */ + public function includeAggregateRows() + { + $this->includeAggregateRows = true; + } - /** - * @param Piwik_DataTable_Row $row - * @param Piwik_DataTable $dataTable - * @param string $labelPrefix - * @param bool $parentLogo - */ - private function flattenRow(Piwik_DataTable_Row $row, Piwik_DataTable $dataTable, - $labelPrefix = '', $parentLogo = false) { - - $label = $row->getColumn('label'); - if ($label !== false) - { - $label = trim($label); - if (substr($label, 0, 1) == '/' && $this->recursiveLabelSeparator == '/') - { - $label = substr($label, 1); - } - $label = $labelPrefix . $label; - $row->setColumn('label', $label); - } - - $logo = $row->getMetadata('logo'); - if ($logo === false && $parentLogo !== false) - { - $logo = $parentLogo; - $row->setMetadata('logo', $logo); - } - - $subTable = $this->loadSubtable($dataTable, $row); - $row->removeSubtable(); - - if ($subTable === null) - { - if ($this->includeAggregateRows) - { - $row->setMetadata('is_aggregate', 0); - } - $dataTable->addRow($row); - } - else - { - if ($this->includeAggregateRows) - { - $row->setMetadata('is_aggregate', 1); - $dataTable->addRow($row); - } - $prefix = $label . $this->recursiveLabelSeparator; - foreach ($subTable->getRows() as $row) - { - $this->flattenRow($row, $dataTable, $prefix, $logo); - } - } - } + /** + * Separator for building recursive labels (or paths) + * @var string + */ + public $recursiveLabelSeparator = ' - '; - /** - * Remove the flat parameter from the subtable request - * - * @param array $request - */ - protected function manipulateSubtableRequest(&$request) - { - unset($request['flat']); - } + /** + * @param Piwik_DataTable $dataTable + * @return Piwik_DataTable|Piwik_DataTable_Array + */ + public function flatten($dataTable) + { + if ($this->apiModule == 'Actions' || $this->apiMethod == 'getWebsites') { + $this->recursiveLabelSeparator = '/'; + } + + return $this->manipulate($dataTable); + } + + /** + * Template method called from self::manipulate. + * Flatten each data table. + * + * @param Piwik_DataTable $dataTable + * @return Piwik_DataTable + */ + protected function manipulateDataTable($dataTable) + { + $newDataTable = $dataTable->getEmptyClone(); + foreach ($dataTable->getRows() as $row) { + $this->flattenRow($row, $newDataTable); + } + return $newDataTable; + } + + /** + * @param Piwik_DataTable_Row $row + * @param Piwik_DataTable $dataTable + * @param string $labelPrefix + * @param bool $parentLogo + */ + private function flattenRow(Piwik_DataTable_Row $row, Piwik_DataTable $dataTable, + $labelPrefix = '', $parentLogo = false) + { + + $label = $row->getColumn('label'); + if ($label !== false) { + $label = trim($label); + if (substr($label, 0, 1) == '/' && $this->recursiveLabelSeparator == '/') { + $label = substr($label, 1); + } + $label = $labelPrefix . $label; + $row->setColumn('label', $label); + } + + $logo = $row->getMetadata('logo'); + if ($logo === false && $parentLogo !== false) { + $logo = $parentLogo; + $row->setMetadata('logo', $logo); + } + + $subTable = $this->loadSubtable($dataTable, $row); + $row->removeSubtable(); + + if ($subTable === null) { + if ($this->includeAggregateRows) { + $row->setMetadata('is_aggregate', 0); + } + $dataTable->addRow($row); + } else { + if ($this->includeAggregateRows) { + $row->setMetadata('is_aggregate', 1); + $dataTable->addRow($row); + } + $prefix = $label . $this->recursiveLabelSeparator; + foreach ($subTable->getRows() as $row) { + $this->flattenRow($row, $dataTable, $prefix, $logo); + } + } + } + + /** + * Remove the flat parameter from the subtable request + * + * @param array $request + */ + protected function manipulateSubtableRequest(&$request) + { + unset($request['flat']); + } } diff --git a/core/API/DataTableManipulator/LabelFilter.php b/core/API/DataTableManipulator/LabelFilter.php index 246a4b6ebc..f2aa0020da 100644 --- a/core/API/DataTableManipulator/LabelFilter.php +++ b/core/API/DataTableManipulator/LabelFilter.php @@ -1,10 +1,10 @@ to join them. - * + * * This filter does not work when expanded=1 is set because it is designed to load * only the subtables on the path, not all existing subtables (which would happen with * expanded=1). Also, the aim of this filter is to return only the row matching the * label. With expanded=1, the subtables of the matching row would be returned as well. - * + * * @package Piwik * @subpackage Piwik_API */ class Piwik_API_DataTableManipulator_LabelFilter extends Piwik_API_DataTableManipulator { - const SEPARATOR_RECURSIVE_LABEL = '>'; - - private $labels; - private $addEmptyRows; - - /** - * Filter a data table by label. - * The filtered table is returned, which might be a new instance. - * - * $apiModule, $apiMethod and $request are needed load sub-datatables - * for the recursive search. If the label is not recursive, these parameters - * are not needed. - * - * @param string $labels the labels to search for - * @param Piwik_DataTable $dataTable the data table to be filtered - * @param bool $addEmptyRows Whether to add empty rows when a row isn't found - * for a label, or not. - * @return Piwik_DataTable - */ - public function filter($labels, $dataTable, $addEmptyRows = false) - { - if (!is_array($labels)) - { - $labels = array($labels); - } - - $this->labels = $labels; - $this->addEmptyRows = (bool)$addEmptyRows; - return $this->manipulate($dataTable); - } - - /** - * Method for the recursive descend - * - * @param array $labelParts - * @param Piwik_DataTable $dataTable - * @return Piwik_DataTable_Row|false - */ - private function doFilterRecursiveDescend($labelParts, $dataTable) - { - // search for the first part of the tree search + const SEPARATOR_RECURSIVE_LABEL = '>'; + + private $labels; + private $addEmptyRows; + + /** + * Filter a data table by label. + * The filtered table is returned, which might be a new instance. + * + * $apiModule, $apiMethod and $request are needed load sub-datatables + * for the recursive search. If the label is not recursive, these parameters + * are not needed. + * + * @param string $labels the labels to search for + * @param Piwik_DataTable $dataTable the data table to be filtered + * @param bool $addEmptyRows Whether to add empty rows when a row isn't found + * for a label, or not. + * @return Piwik_DataTable + */ + public function filter($labels, $dataTable, $addEmptyRows = false) + { + if (!is_array($labels)) { + $labels = array($labels); + } + + $this->labels = $labels; + $this->addEmptyRows = (bool)$addEmptyRows; + return $this->manipulate($dataTable); + } + + /** + * Method for the recursive descend + * + * @param array $labelParts + * @param Piwik_DataTable $dataTable + * @return Piwik_DataTable_Row|false + */ + private function doFilterRecursiveDescend($labelParts, $dataTable) + { + // search for the first part of the tree search $labelPart = array_shift($labelParts); - - foreach ($this->getLabelVariations($labelPart) as $labelPart) - { - $row = $dataTable->getRowFromLabel($labelPart); - if ($row !== false) - { - break; - } - } - - if ($row === false) - { - // not found - return false; - } - - // end of tree search reached - if (count($labelParts) == 0) - { - return $row; - } - - $subTable = $this->loadSubtable($dataTable, $row); - if ($subTable === null) - { - // no more subtables but label parts left => no match found - return false; - } - - return $this->doFilterRecursiveDescend($labelParts, $subTable); - } - - /** - * Clean up request for Piwik_API_ResponseBuilder to behave correctly - * - * @param $request - */ - protected function manipulateSubtableRequest(&$request) - { - unset($request['label']); - } - - /** - * Use variations of the label to make it easier to specify the desired label - * - * Note: The HTML Encoded version must be tried first, since in Piwik_API_ResponseBuilder the $label is unsanitized - * via Piwik_Common::unsanitizeInputValue. - * - * @param string $label - * @return array - */ - private function getLabelVariations($label) - { - $variations = array(); - $label = trim($label); - - $sanitizedLabel = Piwik_Common::sanitizeInputValue($label); - $variations[] = $sanitizedLabel; - - if ($this->apiModule == 'Actions' - && $this->apiMethod == 'getPageTitles') - { - // special case: the Actions.getPageTitles report prefixes some labels with a blank. - // the blank might be passed by the user but is removed in Piwik_API_Request::getRequestArrayFromString. - $variations[] = ' '.$sanitizedLabel; - $variations[] = ' '.$label; - } - $variations[] = $label; - - return $variations; - } - - /** - * Filter a Piwik_DataTable instance. See @filter for more info. - */ - protected function manipulateDataTable( $dataTable ) - { - $result = $dataTable->getEmptyClone(); - foreach ($this->labels as $labelIdx => $label) - { - $row = null; - foreach ($this->getLabelVariations($label) as $labelVariation) - { - $labelVariation = explode(self::SEPARATOR_RECURSIVE_LABEL, $labelVariation); - $labelVariation = array_map('urldecode', $labelVariation); - - $row = $this->doFilterRecursiveDescend($labelVariation, $dataTable); - if ($row) - { - $result->addRow($row); - break; - } - } - - if (empty($row) - && $this->addEmptyRows) // if no row has been found, add an empty one - { - $row = new Piwik_DataTable_Row(); - $row->setColumn('label', $label); - $result->addRow($row); - } - } - return $result; - } + + foreach ($this->getLabelVariations($labelPart) as $labelPart) { + $row = $dataTable->getRowFromLabel($labelPart); + if ($row !== false) { + break; + } + } + + if ($row === false) { + // not found + return false; + } + + // end of tree search reached + if (count($labelParts) == 0) { + return $row; + } + + $subTable = $this->loadSubtable($dataTable, $row); + if ($subTable === null) { + // no more subtables but label parts left => no match found + return false; + } + + return $this->doFilterRecursiveDescend($labelParts, $subTable); + } + + /** + * Clean up request for Piwik_API_ResponseBuilder to behave correctly + * + * @param $request + */ + protected function manipulateSubtableRequest(&$request) + { + unset($request['label']); + } + + /** + * Use variations of the label to make it easier to specify the desired label + * + * Note: The HTML Encoded version must be tried first, since in Piwik_API_ResponseBuilder the $label is unsanitized + * via Piwik_Common::unsanitizeInputValue. + * + * @param string $label + * @return array + */ + private function getLabelVariations($label) + { + $variations = array(); + $label = trim($label); + + $sanitizedLabel = Piwik_Common::sanitizeInputValue($label); + $variations[] = $sanitizedLabel; + + if ($this->apiModule == 'Actions' + && $this->apiMethod == 'getPageTitles' + ) { + // special case: the Actions.getPageTitles report prefixes some labels with a blank. + // the blank might be passed by the user but is removed in Piwik_API_Request::getRequestArrayFromString. + $variations[] = ' ' . $sanitizedLabel; + $variations[] = ' ' . $label; + } + $variations[] = $label; + + return $variations; + } + + /** + * Filter a Piwik_DataTable instance. See @filter for more info. + */ + protected function manipulateDataTable($dataTable) + { + $result = $dataTable->getEmptyClone(); + foreach ($this->labels as $labelIdx => $label) { + $row = null; + foreach ($this->getLabelVariations($label) as $labelVariation) { + $labelVariation = explode(self::SEPARATOR_RECURSIVE_LABEL, $labelVariation); + $labelVariation = array_map('urldecode', $labelVariation); + + $row = $this->doFilterRecursiveDescend($labelVariation, $dataTable); + if ($row) { + $result->addRow($row); + break; + } + } + + if (empty($row) + && $this->addEmptyRows + ) // if no row has been found, add an empty one + { + $row = new Piwik_DataTable_Row(); + $row->setColumn('label', $label); + $result->addRow($row); + } + } + return $result; + } } diff --git a/core/API/DocumentationGenerator.php b/core/API/DocumentationGenerator.php index 505f35ec04..1c2d9a62a4 100644 --- a/core/API/DocumentationGenerator.php +++ b/core/API/DocumentationGenerator.php @@ -1,10 +1,10 @@ getLoadedPluginsName(); - foreach( $plugins as $plugin ) - { - $plugin = Piwik::unprefixClass($plugin); - try { - Piwik_API_Proxy::getInstance()->registerClass('Piwik_'.$plugin.'_API'); - } - catch(Exception $e){ - } - } - } + /** + * trigger loading all plugins with an API.php file in the Proxy + */ + public function __construct() + { + $plugins = Piwik_PluginsManager::getInstance()->getLoadedPluginsName(); + foreach ($plugins as $plugin) { + $plugin = Piwik::unprefixClass($plugin); + try { + Piwik_API_Proxy::getInstance()->registerClass('Piwik_' . $plugin . '_API'); + } catch (Exception $e) { + } + } + } - /** - * Returns a HTML page containing help for all the successfully loaded APIs. - * For each module it will return a mini help with the method names, parameters to give, - * links to get the result in Xml/Csv/etc - * - * @param bool $outputExampleUrls - * @param string $prefixUrls - * @return string - */ - public function getAllInterfaceString( $outputExampleUrls = true, $prefixUrls = '' ) - { - if(!empty($prefixUrls)) { - $prefixUrls = 'http://demo.piwik.org/'; - } - $str = $toc = ''; - $token_auth = "&token_auth=" . Piwik::getCurrentUserTokenAuth(); - $parametersToSet = array( - 'idSite' => Piwik_Common::getRequestVar('idSite', 1, 'int'), - 'period' => Piwik_Common::getRequestVar('period', 'day', 'string'), - 'date' => Piwik_Common::getRequestVar('date', 'today', 'string') - ); - - foreach(Piwik_API_Proxy::getInstance()->getMetadata() as $class => $info) - { - $moduleName = Piwik_API_Proxy::getInstance()->getModuleNameFromClassName($class); - if(in_array($moduleName, $this->modulesToHide)) - { - continue; - } - $toc .= "$moduleName
"; - $str .= "\n

Module ".$moduleName."

"; - $str .= "
".$info['__documentation'] . "
"; - foreach($info as $methodName => $infoMethod) - { - if($methodName=='__documentation') - { - continue; - } - $params = $this->getParametersString($class, $methodName); - $str .= "\n
- $moduleName.$methodName " . $params . ""; - $str .= ''; - - if($outputExampleUrls) - { - // we prefix all URLs with $prefixUrls - // used when we include this output in the Piwik official documentation for example - $str .= ""; - $exampleUrl = $this->getExampleUrl($class, $methodName, $parametersToSet); - if($exampleUrl !== false) - { - $lastNUrls = ''; - if( preg_match('/(&period)|(&date)/',$exampleUrl)) - { - $exampleUrlRss1 = $prefixUrls . $this->getExampleUrl($class, $methodName, array('date' => 'last10', 'period' => 'day') + $parametersToSet) ; - $exampleUrlRss2 = $prefixUrls . $this->getExampleUrl($class, $methodName, array('date' => 'last5','period' => 'week',) + $parametersToSet ); - $lastNUrls = ", RSS of the last 10 days"; - } - $exampleUrl = $prefixUrls . $exampleUrl ; - $str .= " [ Example in + /** + * Returns a HTML page containing help for all the successfully loaded APIs. + * For each module it will return a mini help with the method names, parameters to give, + * links to get the result in Xml/Csv/etc + * + * @param bool $outputExampleUrls + * @param string $prefixUrls + * @return string + */ + public function getAllInterfaceString($outputExampleUrls = true, $prefixUrls = '') + { + if (!empty($prefixUrls)) { + $prefixUrls = 'http://demo.piwik.org/'; + } + $str = $toc = ''; + $token_auth = "&token_auth=" . Piwik::getCurrentUserTokenAuth(); + $parametersToSet = array( + 'idSite' => Piwik_Common::getRequestVar('idSite', 1, 'int'), + 'period' => Piwik_Common::getRequestVar('period', 'day', 'string'), + 'date' => Piwik_Common::getRequestVar('date', 'today', 'string') + ); + + foreach (Piwik_API_Proxy::getInstance()->getMetadata() as $class => $info) { + $moduleName = Piwik_API_Proxy::getInstance()->getModuleNameFromClassName($class); + if (in_array($moduleName, $this->modulesToHide)) { + continue; + } + $toc .= "$moduleName
"; + $str .= "\n

Module " . $moduleName . "

"; + $str .= "
" . $info['__documentation'] . "
"; + foreach ($info as $methodName => $infoMethod) { + if ($methodName == '__documentation') { + continue; + } + $params = $this->getParametersString($class, $methodName); + $str .= "\n
- $moduleName.$methodName " . $params . ""; + $str .= ''; + + if ($outputExampleUrls) { + // we prefix all URLs with $prefixUrls + // used when we include this output in the Piwik official documentation for example + $str .= ""; + $exampleUrl = $this->getExampleUrl($class, $methodName, $parametersToSet); + if ($exampleUrl !== false) { + $lastNUrls = ''; + if (preg_match('/(&period)|(&date)/', $exampleUrl)) { + $exampleUrlRss1 = $prefixUrls . $this->getExampleUrl($class, $methodName, array('date' => 'last10', 'period' => 'day') + $parametersToSet); + $exampleUrlRss2 = $prefixUrls . $this->getExampleUrl($class, $methodName, array('date' => 'last5', 'period' => 'week',) + $parametersToSet); + $lastNUrls = ", RSS of the last 10 days"; + } + $exampleUrl = $prefixUrls . $exampleUrl; + $str .= " [ Example in XML, Json, Tsv (Excel) $lastNUrls ]"; - } - else - { - $str .= " [ No example available ]"; - } - $str .= ""; - } - $str .= ''; - $str .= "
\n"; - } - $str .= '
↑ Back to top
'; - } - - $str = "

Quick access to APIs

+ } else { + $str .= " [ No example available ]"; + } + $str .= "
"; + } + $str .= '
'; + $str .= "
\n"; + } + $str .= '
↑ Back to top
'; + } + + $str = "

Quick access to APIs

$toc $str"; - return $str; - } + return $str; + } + + /** + * Returns a string containing links to examples on how to call a given method on a given API + * It will export links to XML, CSV, HTML, JSON, PHP, etc. + * It will not export links for methods such as deleteSite or deleteUser + * + * @param string $class the class + * @param string $methodName the method + * @param array $parametersToSet parameters to set + * @return string|false when not possible + */ + public function getExampleUrl($class, $methodName, $parametersToSet = array()) + { + $knowExampleDefaultParametersValues = array( + 'access' => 'view', + 'userLogin' => 'test', + 'passwordMd5ied' => 'passwordExample', + 'email' => 'test@example.org', + + 'languageCode' => 'fr', + 'url' => 'http://forum.piwik.org/', + 'apiModule' => 'UserCountry', + 'apiAction' => 'getCountry', + 'lastMinutes' => '30', + 'abandonedCarts' => '0', + 'ip' => '194.57.91.215', + ); + + foreach ($parametersToSet as $name => $value) { + $knowExampleDefaultParametersValues[$name] = $value; + } - /** - * Returns a string containing links to examples on how to call a given method on a given API - * It will export links to XML, CSV, HTML, JSON, PHP, etc. - * It will not export links for methods such as deleteSite or deleteUser - * - * @param string $class the class - * @param string $methodName the method - * @param array $parametersToSet parameters to set - * @return string|false when not possible - */ - public function getExampleUrl($class, $methodName, $parametersToSet = array()) - { - $knowExampleDefaultParametersValues = array( - 'access' => 'view', - 'userLogin' => 'test', - 'passwordMd5ied' => 'passwordExample', - 'email' => 'test@example.org', - - 'languageCode' => 'fr', - 'url' => 'http://forum.piwik.org/', - 'apiModule' => 'UserCountry', - 'apiAction' => 'getCountry', - 'lastMinutes' => '30', - 'abandonedCarts' => '0', - 'ip' => '194.57.91.215', - ); - - foreach($parametersToSet as $name => $value) - { - $knowExampleDefaultParametersValues[$name] = $value; - } - - // no links for these method names - $doNotPrintExampleForTheseMethods = array( - //Sites - 'deleteSite', - 'addSite', - 'updateSite', - 'addSiteAliasUrls', - //Users - 'deleteUser', - 'addUser', - 'updateUser', - 'setUserAccess', - //Goals - 'addGoal', - 'updateGoal', - 'deleteGoal', - ); - - if(in_array($methodName,$doNotPrintExampleForTheseMethods)) - { - return false; - } - - // we try to give an URL example to call the API - $aParameters = Piwik_API_Proxy::getInstance()->getParametersList($class, $methodName); - // Kindly force some known generic parameters to appear in the final list - // the parameter 'format' can be set to all API methods (used in tests) - // the parameter 'hideIdSubDatable' is used for integration tests only - // the parameter 'serialize' sets php outputs human readable, used in integration tests and debug - // the parameter 'language' sets the language for the response (eg. country names) - // the parameter 'flat' reduces a hierarchical table to a single level by concatenating labels - // the parameter 'include_aggregate_rows' can be set to include inner nodes in flat reports - // the parameter 'translateColumnNames' can be set to translate metric names in csv/tsv exports - $aParameters['format'] = false; - $aParameters['hideIdSubDatable'] = false; - $aParameters['serialize'] = false; - $aParameters['language'] = false; - $aParameters['translateColumnNames'] = false; + // no links for these method names + $doNotPrintExampleForTheseMethods = array( + //Sites + 'deleteSite', + 'addSite', + 'updateSite', + 'addSiteAliasUrls', + //Users + 'deleteUser', + 'addUser', + 'updateUser', + 'setUserAccess', + //Goals + 'addGoal', + 'updateGoal', + 'deleteGoal', + ); + + if (in_array($methodName, $doNotPrintExampleForTheseMethods)) { + return false; + } + + // we try to give an URL example to call the API + $aParameters = Piwik_API_Proxy::getInstance()->getParametersList($class, $methodName); + // Kindly force some known generic parameters to appear in the final list + // the parameter 'format' can be set to all API methods (used in tests) + // the parameter 'hideIdSubDatable' is used for integration tests only + // the parameter 'serialize' sets php outputs human readable, used in integration tests and debug + // the parameter 'language' sets the language for the response (eg. country names) + // the parameter 'flat' reduces a hierarchical table to a single level by concatenating labels + // the parameter 'include_aggregate_rows' can be set to include inner nodes in flat reports + // the parameter 'translateColumnNames' can be set to translate metric names in csv/tsv exports + $aParameters['format'] = false; + $aParameters['hideIdSubDatable'] = false; + $aParameters['serialize'] = false; + $aParameters['language'] = false; + $aParameters['translateColumnNames'] = false; $aParameters['label'] = false; - $aParameters['flat'] = false; - $aParameters['include_aggregate_rows'] = false; + $aParameters['flat'] = false; + $aParameters['include_aggregate_rows'] = false; $aParameters['filter_limit'] = false; //@review without adding this, I can not set filter_limit in $otherRequestParameters integration tests $aParameters['filter_sort_column'] = false; //@review without adding this, I can not set filter_sort_column in $otherRequestParameters integration tests $aParameters['filter_truncate'] = false; $aParameters['hideColumns'] = false; $aParameters['showColumns'] = false; $aParameters['filter_pattern_recursive'] = false; - - $moduleName = Piwik_API_Proxy::getInstance()->getModuleNameFromClassName($class); - $aParameters = array_merge(array('module' => 'API', 'method' => $moduleName.'.'.$methodName), $aParameters); - - foreach($aParameters as $nameVariable => &$defaultValue) - { - if(isset($knowExampleDefaultParametersValues[$nameVariable])) - { - $defaultValue = $knowExampleDefaultParametersValues[$nameVariable]; - } - // if there isn't a default value for a given parameter, - // we need a 'know default value' or we can't generate the link - elseif($defaultValue instanceof Piwik_API_Proxy_NoDefaultValue) - { - return false; - } - } - return '?'.Piwik_Url::getQueryStringFromParameters($aParameters); - } - - - /** - * Returns the methods $class.$name parameters (and default value if provided) as a string. - * - * @param string $class The class name - * @param string $name The method name - * @return string For example "(idSite, period, date = 'today')" - */ - public function getParametersString($class, $name) - { - $aParameters = Piwik_API_Proxy::getInstance()->getParametersList($class, $name); - $asParameters = array(); - foreach($aParameters as $nameVariable=> $defaultValue) - { - // Do not show API parameters starting with _ - // They are supposed to be used only in internal API calls - if(strpos($nameVariable, '_') === 0) - { - continue; - } - $str = $nameVariable; - if(!($defaultValue instanceof Piwik_API_Proxy_NoDefaultValue)) - { - if (is_array($defaultValue)) - { - $str .= " = 'Array'"; - } - else - { - $str .= " = '$defaultValue'"; - } - } - $asParameters[] = $str; - } - $sParameters = implode(", ", $asParameters); - return "($sParameters)"; - } + + $moduleName = Piwik_API_Proxy::getInstance()->getModuleNameFromClassName($class); + $aParameters = array_merge(array('module' => 'API', 'method' => $moduleName . '.' . $methodName), $aParameters); + + foreach ($aParameters as $nameVariable => &$defaultValue) { + if (isset($knowExampleDefaultParametersValues[$nameVariable])) { + $defaultValue = $knowExampleDefaultParametersValues[$nameVariable]; + } // if there isn't a default value for a given parameter, + // we need a 'know default value' or we can't generate the link + elseif ($defaultValue instanceof Piwik_API_Proxy_NoDefaultValue) { + return false; + } + } + return '?' . Piwik_Url::getQueryStringFromParameters($aParameters); + } + + + /** + * Returns the methods $class.$name parameters (and default value if provided) as a string. + * + * @param string $class The class name + * @param string $name The method name + * @return string For example "(idSite, period, date = 'today')" + */ + public function getParametersString($class, $name) + { + $aParameters = Piwik_API_Proxy::getInstance()->getParametersList($class, $name); + $asParameters = array(); + foreach ($aParameters as $nameVariable => $defaultValue) { + // Do not show API parameters starting with _ + // They are supposed to be used only in internal API calls + if (strpos($nameVariable, '_') === 0) { + continue; + } + $str = $nameVariable; + if (!($defaultValue instanceof Piwik_API_Proxy_NoDefaultValue)) { + if (is_array($defaultValue)) { + $str .= " = 'Array'"; + } else { + $str .= " = '$defaultValue'"; + } + } + $asParameters[] = $str; + } + $sParameters = implode(", ", $asParameters); + return "($sParameters)"; + } } diff --git a/core/API/Proxy.php b/core/API/Proxy.php index 3619e0fc87..727d291b29 100644 --- a/core/API/Proxy.php +++ b/core/API/Proxy.php @@ -1,419 +1,402 @@ noDefaultValue = new Piwik_API_Proxy_NoDefaultValue(); - } - - /** - * Singleton, returns instance - * - * @return Piwik_API_Proxy - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - /** - * Returns array containing reflection meta data for all the loaded classes - * eg. number of parameters, method names, etc. - * - * @return array - */ - public function getMetadata() - { - ksort($this->metadataArray); - return $this->metadataArray; - } - - /** - * Registers the API information of a given module. - * - * The module to be registered must be - * - a singleton (providing a getInstance() method) - * - the API file must be located in plugins/ModuleName/API.php - * for example plugins/Referers/API.php - * - * The method will introspect the methods, their parameters, etc. - * - * @param string $className ModuleName eg. "Piwik_UserSettings_API" - */ - public function registerClass( $className ) - { - if(isset($this->alreadyRegistered[$className])) - { - return; - } - $this->includeApiFile( $className ); - $this->checkClassIsSingleton($className); - - $rClass = new ReflectionClass($className); - foreach($rClass->getMethods() as $method) - { - $this->loadMethodMetadata($className, $method); - } - - $this->setDocumentation($rClass, $className); - $this->alreadyRegistered[$className] = true; - } - - /** - * Will be displayed in the API page - * - * @param ReflectionClass $rClass Instance of ReflectionClass - * @param string $className Name of the class - */ - private function setDocumentation($rClass, $className) - { - // Doc comment - $doc = $rClass->getDocComment(); - $doc = str_replace(" * ".PHP_EOL, "
", $doc); - - // boldify the first line only if there is more than one line, otherwise too much bold - if(substr_count($doc, '
') > 1) - { - $firstLineBreak = strpos($doc, "
"); - $doc = "
".substr($doc, 0, $firstLineBreak)."
".substr($doc,$firstLineBreak+strlen("
")); - } - $doc = preg_replace("/(@package)[a-z _A-Z]*/", "", $doc); - $doc = str_replace(array("\t","\n", "/**", "*/", " * "," *"," ", "\t*", " * @package"), " ", $doc); - $this->metadataArray[$className]['__documentation'] = $doc; - } - - /** - * Returns number of classes already loaded - * @return int - */ - public function getCountRegisteredClasses() - { - return count($this->alreadyRegistered); - } - - /** - * Will execute $className->$methodName($parametersValues) - * If any error is detected (wrong number of parameters, method not found, class not found, etc.) - * it will throw an exception - * - * It also logs the API calls, with the parameters values, the returned value, the performance, etc. - * You can enable logging in config/global.ini.php (log_api_call) - * - * @param string $className The class name (eg. Piwik_Referers_API) - * @param string $methodName The method name - * @param array $parametersRequest The parameters pairs (name=>value) - * - * @return mixed|null - * @throws Exception|Piwik_Access_NoAccessException - */ - public function call($className, $methodName, $parametersRequest ) - { - $returnedValue = null; - - // Temporarily sets the Request array to this API call context - $saveGET = $_GET; - foreach($parametersRequest as $param => $value) { - $_GET[$param] = $value; - } - - try { - $this->registerClass($className); - - // instanciate the object - $object = call_user_func(array($className, "getInstance")); - - // check method exists - $this->checkMethodExists($className, $methodName); - - // get the list of parameters required by the method - $parameterNamesDefaultValues = $this->getParametersList($className, $methodName); - - // load parameters in the right order, etc. - $finalParameters = $this->getRequestParametersArray( $parameterNamesDefaultValues, $parametersRequest ); - - // start the timer - $timer = new Piwik_Timer(); - - // call the method - $returnedValue = call_user_func_array(array($object, $methodName), $finalParameters); - - // allow plugins to manipulate the value - if (substr($className, 0, 6) == 'Piwik_' && substr($className, -4) == '_API') - { - $pluginName = substr($className, 6, -4); - Piwik_PostEvent('API.Proxy.processReturnValue', $returnedValue, array( - 'className' => $className, - 'module' => $pluginName, - 'action' => $methodName, - 'parameters' => &$parametersRequest - )); - } - - // Restore the request - $_GET = $saveGET; - - // log the API Call - try { - Zend_Registry::get('logger_api_call')->logEvent( - $className, - $methodName, - $parameterNamesDefaultValues, - $finalParameters, - $timer->getTimeMs(), - $returnedValue - ); - } catch (Exception $e) { - // logger can fail (eg. Tracker request) - } - } catch (Exception $e) { - $_GET = $saveGET; - throw $e; - } - - return $returnedValue; - } - - /** - * Returns the parameters names and default values for the method $name - * of the class $class - * - * @param string $class The class name - * @param string $name The method name - * @return array Format array( - * 'testParameter' => null, // no default value - * 'life' => 42, // default value = 42 - * 'date' => 'yesterday', - * ); - */ - public function getParametersList($class, $name) - { - return $this->metadataArray[$class][$name]['parameters']; - } - - /** - * Returns the 'moduleName' part of 'Piwik_moduleName_API' classname - * - * @param string $className "Piwik_Referers_API" - * @return string "Referers" - */ - public function getModuleNameFromClassName( $className ) - { - return str_replace(array('Piwik_', '_API'), '', $className); - } - - /** - * Sets whether to hide '@ignore'd functions from method metadata or not. - * - * @param bool $hideIgnoredFunctions - */ - public function setHideIgnoredFunctions( $hideIgnoredFunctions ) - { - $this->hideIgnoredFunctions = $hideIgnoredFunctions; - - // make sure metadata gets reloaded - $this->alreadyRegistered = array(); - $this->metadataArray = array(); - } - - /** - * Returns an array containing the values of the parameters to pass to the method to call - * - * @param array $requiredParameters array of (parameter name, default value) - * @param array $parametersRequest - * @throws Exception - * @return array values to pass to the function call - */ - private function getRequestParametersArray( $requiredParameters, $parametersRequest ) - { - $finalParameters = array(); - foreach($requiredParameters as $name => $defaultValue) - { - try{ - if($defaultValue instanceof Piwik_API_Proxy_NoDefaultValue) - { - $requestValue = Piwik_Common::getRequestVar($name, null, null, $parametersRequest); - } - else - { - try{ - $requestValue = Piwik_Common::getRequestVar($name, $defaultValue, null, $parametersRequest); - } catch(Exception $e) { - // Special case: empty parameter in the URL, should return the empty string - if(isset($parametersRequest[$name]) - && $parametersRequest[$name] === '') - { - $requestValue = ''; - } - else - { - $requestValue = $defaultValue; - } - } - } - } catch(Exception $e) { - throw new Exception(Piwik_TranslateException('General_PleaseSpecifyValue', array($name))); - } - $finalParameters[] = $requestValue; - } - return $finalParameters; - } - - /** - * Includes the class Piwik_UserSettings_API by looking up plugins/UserSettings/API.php - * - * @param string $fileName api class name eg. "Piwik_UserSettings_API" - * @throws Exception - */ - private function includeApiFile($fileName) - { - $module = self::getModuleNameFromClassName($fileName); - $path = PIWIK_INCLUDE_PATH . '/plugins/' . $module . '/API.php'; - - if(is_readable($path)) - { - require_once $path; // prefixed by PIWIK_INCLUDE_PATH - } - else - { - throw new Exception("API module $module not found."); - } - } - - /** - * @param string $class name of a class - * @param ReflectionMethod $method instance of ReflectionMethod - */ - private function loadMethodMetadata($class, $method) - { - if($method->isPublic() - && !$method->isConstructor() - && $method->getName() != 'getInstance' - && false === strstr($method->getDocComment(), '@deprecated') - && (!$this->hideIgnoredFunctions || false === strstr($method->getDocComment(), '@ignore')) - ) - { - $name = $method->getName(); - $parameters = $method->getParameters(); - - $aParameters = array(); - foreach($parameters as $parameter) - { - $nameVariable = $parameter->getName(); - - $defaultValue = $this->noDefaultValue; - if($parameter->isDefaultValueAvailable()) - { - $defaultValue = $parameter->getDefaultValue(); - } - - $aParameters[$nameVariable] = $defaultValue; - } - $this->metadataArray[$class][$name]['parameters'] = $aParameters; - $this->metadataArray[$class][$name]['numberOfRequiredParameters'] = $method->getNumberOfRequiredParameters(); - } - } - - /** - * Checks that the method exists in the class - * - * @param string $className The class name - * @param string $methodName The method name - * @throws Exception If the method is not found - */ - private function checkMethodExists($className, $methodName) - { - if(!$this->isMethodAvailable($className, $methodName)) - { - throw new Exception(Piwik_TranslateException('General_ExceptionMethodNotFound', array($methodName,$className))); - } - } - - /** - * Returns the number of required parameters (parameters without default values). - * - * @param string $class The class name - * @param string $name The method name - * @return int The number of required parameters - */ - private function getNumberOfRequiredParameters($class, $name) - { - return $this->metadataArray[$class][$name]['numberOfRequiredParameters']; - } - - /** - * Returns true if the method is found in the API of the given class name. - * - * @param string $className The class name - * @param string $methodName The method name - * @return bool - */ - private function isMethodAvailable( $className, $methodName) - { - return isset($this->metadataArray[$className][$methodName]); - } - - /** - * Checks that the class is a Singleton (presence of the getInstance() method) - * - * @param string $className The class name - * @throws Exception If the class is not a Singleton - */ - private function checkClassIsSingleton($className) - { - if(!method_exists($className, "getInstance")) - { - throw new Exception("Objects that provide an API must be Singleton and have a 'static public function getInstance()' method."); - } - } + // array of already registered plugins names + protected $alreadyRegistered = array(); + + private $metadataArray = array(); + private $hideIgnoredFunctions = true; + + // when a parameter doesn't have a default value we use this + private $noDefaultValue; + + /** + * Singleton instance + * @var self|null + */ + static private $instance = null; + + /** + * protected constructor + */ + protected function __construct() + { + $this->noDefaultValue = new Piwik_API_Proxy_NoDefaultValue(); + } + + /** + * Singleton, returns instance + * + * @return Piwik_API_Proxy + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + /** + * Returns array containing reflection meta data for all the loaded classes + * eg. number of parameters, method names, etc. + * + * @return array + */ + public function getMetadata() + { + ksort($this->metadataArray); + return $this->metadataArray; + } + + /** + * Registers the API information of a given module. + * + * The module to be registered must be + * - a singleton (providing a getInstance() method) + * - the API file must be located in plugins/ModuleName/API.php + * for example plugins/Referers/API.php + * + * The method will introspect the methods, their parameters, etc. + * + * @param string $className ModuleName eg. "Piwik_UserSettings_API" + */ + public function registerClass($className) + { + if (isset($this->alreadyRegistered[$className])) { + return; + } + $this->includeApiFile($className); + $this->checkClassIsSingleton($className); + + $rClass = new ReflectionClass($className); + foreach ($rClass->getMethods() as $method) { + $this->loadMethodMetadata($className, $method); + } + + $this->setDocumentation($rClass, $className); + $this->alreadyRegistered[$className] = true; + } + + /** + * Will be displayed in the API page + * + * @param ReflectionClass $rClass Instance of ReflectionClass + * @param string $className Name of the class + */ + private function setDocumentation($rClass, $className) + { + // Doc comment + $doc = $rClass->getDocComment(); + $doc = str_replace(" * " . PHP_EOL, "
", $doc); + + // boldify the first line only if there is more than one line, otherwise too much bold + if (substr_count($doc, '
') > 1) { + $firstLineBreak = strpos($doc, "
"); + $doc = "
" . substr($doc, 0, $firstLineBreak) . "
" . substr($doc, $firstLineBreak + strlen("
")); + } + $doc = preg_replace("/(@package)[a-z _A-Z]*/", "", $doc); + $doc = str_replace(array("\t", "\n", "/**", "*/", " * ", " *", " ", "\t*", " * @package"), " ", $doc); + $this->metadataArray[$className]['__documentation'] = $doc; + } + + /** + * Returns number of classes already loaded + * @return int + */ + public function getCountRegisteredClasses() + { + return count($this->alreadyRegistered); + } + + /** + * Will execute $className->$methodName($parametersValues) + * If any error is detected (wrong number of parameters, method not found, class not found, etc.) + * it will throw an exception + * + * It also logs the API calls, with the parameters values, the returned value, the performance, etc. + * You can enable logging in config/global.ini.php (log_api_call) + * + * @param string $className The class name (eg. Piwik_Referers_API) + * @param string $methodName The method name + * @param array $parametersRequest The parameters pairs (name=>value) + * + * @return mixed|null + * @throws Exception|Piwik_Access_NoAccessException + */ + public function call($className, $methodName, $parametersRequest) + { + $returnedValue = null; + + // Temporarily sets the Request array to this API call context + $saveGET = $_GET; + foreach ($parametersRequest as $param => $value) { + $_GET[$param] = $value; + } + + try { + $this->registerClass($className); + + // instanciate the object + $object = call_user_func(array($className, "getInstance")); + + // check method exists + $this->checkMethodExists($className, $methodName); + + // get the list of parameters required by the method + $parameterNamesDefaultValues = $this->getParametersList($className, $methodName); + + // load parameters in the right order, etc. + $finalParameters = $this->getRequestParametersArray($parameterNamesDefaultValues, $parametersRequest); + + // start the timer + $timer = new Piwik_Timer(); + + // call the method + $returnedValue = call_user_func_array(array($object, $methodName), $finalParameters); + + // allow plugins to manipulate the value + if (substr($className, 0, 6) == 'Piwik_' && substr($className, -4) == '_API') { + $pluginName = substr($className, 6, -4); + Piwik_PostEvent('API.Proxy.processReturnValue', $returnedValue, array( + 'className' => $className, + 'module' => $pluginName, + 'action' => $methodName, + 'parameters' => &$parametersRequest + )); + } + + // Restore the request + $_GET = $saveGET; + + // log the API Call + try { + Zend_Registry::get('logger_api_call')->logEvent( + $className, + $methodName, + $parameterNamesDefaultValues, + $finalParameters, + $timer->getTimeMs(), + $returnedValue + ); + } catch (Exception $e) { + // logger can fail (eg. Tracker request) + } + } catch (Exception $e) { + $_GET = $saveGET; + throw $e; + } + + return $returnedValue; + } + + /** + * Returns the parameters names and default values for the method $name + * of the class $class + * + * @param string $class The class name + * @param string $name The method name + * @return array Format array( + * 'testParameter' => null, // no default value + * 'life' => 42, // default value = 42 + * 'date' => 'yesterday', + * ); + */ + public function getParametersList($class, $name) + { + return $this->metadataArray[$class][$name]['parameters']; + } + + /** + * Returns the 'moduleName' part of 'Piwik_moduleName_API' classname + * + * @param string $className "Piwik_Referers_API" + * @return string "Referers" + */ + public function getModuleNameFromClassName($className) + { + return str_replace(array('Piwik_', '_API'), '', $className); + } + + /** + * Sets whether to hide '@ignore'd functions from method metadata or not. + * + * @param bool $hideIgnoredFunctions + */ + public function setHideIgnoredFunctions($hideIgnoredFunctions) + { + $this->hideIgnoredFunctions = $hideIgnoredFunctions; + + // make sure metadata gets reloaded + $this->alreadyRegistered = array(); + $this->metadataArray = array(); + } + + /** + * Returns an array containing the values of the parameters to pass to the method to call + * + * @param array $requiredParameters array of (parameter name, default value) + * @param array $parametersRequest + * @throws Exception + * @return array values to pass to the function call + */ + private function getRequestParametersArray($requiredParameters, $parametersRequest) + { + $finalParameters = array(); + foreach ($requiredParameters as $name => $defaultValue) { + try { + if ($defaultValue instanceof Piwik_API_Proxy_NoDefaultValue) { + $requestValue = Piwik_Common::getRequestVar($name, null, null, $parametersRequest); + } else { + try { + $requestValue = Piwik_Common::getRequestVar($name, $defaultValue, null, $parametersRequest); + } catch (Exception $e) { + // Special case: empty parameter in the URL, should return the empty string + if (isset($parametersRequest[$name]) + && $parametersRequest[$name] === '' + ) { + $requestValue = ''; + } else { + $requestValue = $defaultValue; + } + } + } + } catch (Exception $e) { + throw new Exception(Piwik_TranslateException('General_PleaseSpecifyValue', array($name))); + } + $finalParameters[] = $requestValue; + } + return $finalParameters; + } + + /** + * Includes the class Piwik_UserSettings_API by looking up plugins/UserSettings/API.php + * + * @param string $fileName api class name eg. "Piwik_UserSettings_API" + * @throws Exception + */ + private function includeApiFile($fileName) + { + $module = self::getModuleNameFromClassName($fileName); + $path = PIWIK_INCLUDE_PATH . '/plugins/' . $module . '/API.php'; + + if (is_readable($path)) { + require_once $path; // prefixed by PIWIK_INCLUDE_PATH + } else { + throw new Exception("API module $module not found."); + } + } + + /** + * @param string $class name of a class + * @param ReflectionMethod $method instance of ReflectionMethod + */ + private function loadMethodMetadata($class, $method) + { + if ($method->isPublic() + && !$method->isConstructor() + && $method->getName() != 'getInstance' + && false === strstr($method->getDocComment(), '@deprecated') + && (!$this->hideIgnoredFunctions || false === strstr($method->getDocComment(), '@ignore')) + ) { + $name = $method->getName(); + $parameters = $method->getParameters(); + + $aParameters = array(); + foreach ($parameters as $parameter) { + $nameVariable = $parameter->getName(); + + $defaultValue = $this->noDefaultValue; + if ($parameter->isDefaultValueAvailable()) { + $defaultValue = $parameter->getDefaultValue(); + } + + $aParameters[$nameVariable] = $defaultValue; + } + $this->metadataArray[$class][$name]['parameters'] = $aParameters; + $this->metadataArray[$class][$name]['numberOfRequiredParameters'] = $method->getNumberOfRequiredParameters(); + } + } + + /** + * Checks that the method exists in the class + * + * @param string $className The class name + * @param string $methodName The method name + * @throws Exception If the method is not found + */ + private function checkMethodExists($className, $methodName) + { + if (!$this->isMethodAvailable($className, $methodName)) { + throw new Exception(Piwik_TranslateException('General_ExceptionMethodNotFound', array($methodName, $className))); + } + } + + /** + * Returns the number of required parameters (parameters without default values). + * + * @param string $class The class name + * @param string $name The method name + * @return int The number of required parameters + */ + private function getNumberOfRequiredParameters($class, $name) + { + return $this->metadataArray[$class][$name]['numberOfRequiredParameters']; + } + + /** + * Returns true if the method is found in the API of the given class name. + * + * @param string $className The class name + * @param string $methodName The method name + * @return bool + */ + private function isMethodAvailable($className, $methodName) + { + return isset($this->metadataArray[$className][$methodName]); + } + + /** + * Checks that the class is a Singleton (presence of the getInstance() method) + * + * @param string $className The class name + * @throws Exception If the class is not a Singleton + */ + private function checkClassIsSingleton($className) + { + if (!method_exists($className, "getInstance")) { + throw new Exception("Objects that provide an API must be Singleton and have a 'static public function getInstance()' method."); + } + } } diff --git a/core/API/Request.php b/core/API/Request.php index 1c0b970cf3..2abf779c52 100644 --- a/core/API/Request.php +++ b/core/API/Request.php @@ -1,10 +1,10 @@ process(); + * method=UserSettings.getWideScreen + * &idSite=1 + * &date=yesterday + * &period=week + * &format=xml + * &filter_limit=5 + * &filter_offset=0 + * '); + * $result = $request->process(); * echo $result; - * + * * @see http://piwik.org/docs/analytics-api * @package Piwik * @subpackage Piwik_API */ class Piwik_API_Request -{ - protected $request = null; - - /** - * Returns the request array as string - * - * @param string|array $request - * @return array|null - */ - static public function getRequestArrayFromString($request) - { - $defaultRequest = $_GET + $_POST; - $requestArray = $defaultRequest; - - if(!is_null($request)) - { - if(is_array($request)) - { +{ + protected $request = null; + + /** + * Returns the request array as string + * + * @param string|array $request + * @return array|null + */ + static public function getRequestArrayFromString($request) + { + $defaultRequest = $_GET + $_POST; + $requestArray = $defaultRequest; + + if (!is_null($request)) { + if (is_array($request)) { $url = array(); - foreach ($request as $key => $value) - { + foreach ($request as $key => $value) { $url[] = $key . "=" . $value; } $request = implode("&", $url); } - $request = trim($request); - $request = str_replace(array("\n","\t"),'', $request); - parse_str($request, $requestArray); - - $requestArray = $requestArray + $defaultRequest; - } - - foreach($requestArray as &$element) - { - if(!is_array($element)) - { - $element = trim($element); - } - } - return $requestArray; - } - - /** - * Constructs the request to the API, given the request url - * - * @param string $request GET request that defines the API call (must at least contain a "method" parameter) - * Example: method=UserSettings.getWideScreen&idSite=1&date=yesterday&period=week&format=xml - * If a request is not provided, then we use the $_GET and $_POST superglobal and fetch - * the values directly from the HTTP GET query. - */ - function __construct($request = null) - { - $this->request = self::getRequestArrayFromString($request); - } - - /** - * Handles the request to the API. - * It first checks that the method called (parameter 'method') is available in the module (it means that the method exists and is public) - * It then reads the parameters from the request string and throws an exception if there are missing parameters. - * It then calls the API Proxy which will call the requested method. - * - * @throws Piwik_FrontController_PluginDeactivatedException - * @return mixed The data resulting from the API call - */ - public function process() - { - // read the format requested for the output data - $outputFormat = strtolower(Piwik_Common::getRequestVar('format', 'xml', 'string', $this->request)); - - // create the response - $response = new Piwik_API_ResponseBuilder($outputFormat, $this->request); - - try { - // read parameters - $moduleMethod = Piwik_Common::getRequestVar('method', null, 'string', $this->request); - - list($module, $method) = $this->extractModuleAndMethod($moduleMethod); - - if(!Piwik_PluginsManager::getInstance()->isPluginActivated($module)) - { - throw new Piwik_FrontController_PluginDeactivatedException($module); - } - $moduleClass = "Piwik_" . $module . "_API"; - - self::reloadAuthUsingTokenAuth($this->request); - - // call the method - $returnedValue = Piwik_API_Proxy::getInstance()->call($moduleClass, $method, $this->request); - - $toReturn = $response->getResponse($returnedValue, $module, $method); - } catch(Exception $e ) { - $toReturn = $response->getResponseException( $e ); - } - return $toReturn; - } - - /** - * If the token_auth is found in the $request parameter, - * the current session will be authenticated using this token_auth. - * It will overwrite the previous Auth object. - * - * @param array $request If null, uses the default request ($_GET) - * @return void - */ - static public function reloadAuthUsingTokenAuth($request = null) - { - // if a token_auth is specified in the API request, we load the right permissions - $token_auth = Piwik_Common::getRequestVar('token_auth', '', 'string', $request); - if($token_auth) - { - Piwik_PostEvent('API.Request.authenticate', $token_auth); - Zend_Registry::get('access')->reloadAccess(); - Piwik::raiseMemoryLimitIfNecessary(); - } - } - - /** - * Returns array( $class, $method) from the given string $class.$method - * - * @param string $parameter - * @throws Exception - * @return array - */ - private function extractModuleAndMethod($parameter) - { - $a = explode('.',$parameter); - if(count($a) != 2) - { - throw new Exception("The method name is invalid. Expected 'module.methodName'"); - } - return $a; - } - - /** - * Helper method to process an API request using the variables in $_GET and $_POST. - * - * @param string $method The API method to call, ie, Actions.getPageTitles - * @param array $paramOverride The parameter name-value pairs to use instead of what's - * in $_GET & $_POST. - * @param mixed The result of the API request. - */ - public static function processRequest( $method, $paramOverride = array() ) - { - // set up request params - $params = $_GET + $_POST; - $params['format'] = 'original'; - $params['module'] = 'API'; - $params['method'] = $method; - $params = $paramOverride + $params; - - // process request - $request = new Piwik_API_Request($params); - return $request->process(); - } + $request = trim($request); + $request = str_replace(array("\n", "\t"), '', $request); + parse_str($request, $requestArray); + + $requestArray = $requestArray + $defaultRequest; + } + + foreach ($requestArray as &$element) { + if (!is_array($element)) { + $element = trim($element); + } + } + return $requestArray; + } + + /** + * Constructs the request to the API, given the request url + * + * @param string $request GET request that defines the API call (must at least contain a "method" parameter) + * Example: method=UserSettings.getWideScreen&idSite=1&date=yesterday&period=week&format=xml + * If a request is not provided, then we use the $_GET and $_POST superglobal and fetch + * the values directly from the HTTP GET query. + */ + function __construct($request = null) + { + $this->request = self::getRequestArrayFromString($request); + } + + /** + * Handles the request to the API. + * It first checks that the method called (parameter 'method') is available in the module (it means that the method exists and is public) + * It then reads the parameters from the request string and throws an exception if there are missing parameters. + * It then calls the API Proxy which will call the requested method. + * + * @throws Piwik_FrontController_PluginDeactivatedException + * @return mixed The data resulting from the API call + */ + public function process() + { + // read the format requested for the output data + $outputFormat = strtolower(Piwik_Common::getRequestVar('format', 'xml', 'string', $this->request)); + + // create the response + $response = new Piwik_API_ResponseBuilder($outputFormat, $this->request); + + try { + // read parameters + $moduleMethod = Piwik_Common::getRequestVar('method', null, 'string', $this->request); + + list($module, $method) = $this->extractModuleAndMethod($moduleMethod); + + if (!Piwik_PluginsManager::getInstance()->isPluginActivated($module)) { + throw new Piwik_FrontController_PluginDeactivatedException($module); + } + $moduleClass = "Piwik_" . $module . "_API"; + + self::reloadAuthUsingTokenAuth($this->request); + + // call the method + $returnedValue = Piwik_API_Proxy::getInstance()->call($moduleClass, $method, $this->request); + + $toReturn = $response->getResponse($returnedValue, $module, $method); + } catch (Exception $e) { + $toReturn = $response->getResponseException($e); + } + return $toReturn; + } + + /** + * If the token_auth is found in the $request parameter, + * the current session will be authenticated using this token_auth. + * It will overwrite the previous Auth object. + * + * @param array $request If null, uses the default request ($_GET) + * @return void + */ + static public function reloadAuthUsingTokenAuth($request = null) + { + // if a token_auth is specified in the API request, we load the right permissions + $token_auth = Piwik_Common::getRequestVar('token_auth', '', 'string', $request); + if ($token_auth) { + Piwik_PostEvent('API.Request.authenticate', $token_auth); + Zend_Registry::get('access')->reloadAccess(); + Piwik::raiseMemoryLimitIfNecessary(); + } + } + + /** + * Returns array( $class, $method) from the given string $class.$method + * + * @param string $parameter + * @throws Exception + * @return array + */ + private function extractModuleAndMethod($parameter) + { + $a = explode('.', $parameter); + if (count($a) != 2) { + throw new Exception("The method name is invalid. Expected 'module.methodName'"); + } + return $a; + } + + /** + * Helper method to process an API request using the variables in $_GET and $_POST. + * + * @param string $method The API method to call, ie, Actions.getPageTitles + * @param array $paramOverride The parameter name-value pairs to use instead of what's + * in $_GET & $_POST. + * @param mixed The result of the API request. + */ + public static function processRequest($method, $paramOverride = array()) + { + // set up request params + $params = $_GET + $_POST; + $params['format'] = 'original'; + $params['module'] = 'API'; + $params['method'] = $method; + $params = $paramOverride + $params; + + // process request + $request = new Piwik_API_Request($params); + return $request->process(); + } } diff --git a/core/API/ResponseBuilder.php b/core/API/ResponseBuilder.php index 07df53ce7e..6830e34c0e 100644 --- a/core/API/ResponseBuilder.php +++ b/core/API/ResponseBuilder.php @@ -1,10 +1,10 @@ request = $request; - $this->outputFormat = $outputFormat; - } - - /** - * This method processes the data resulting from the API call. - * - * - If the data resulted from the API call is a Piwik_DataTable then - * - we apply the standard filters if the parameters have been found - * in the URL. For example to offset,limit the Table you can add the following parameters to any API - * call that returns a DataTable: filter_limit=10&filter_offset=20 - * - we apply the filters that have been previously queued on the DataTable - * @see Piwik_DataTable::queueFilter() - * - we apply the renderer that generate the DataTable in a given format (XML, PHP, HTML, JSON, etc.) - * the format can be changed using the 'format' parameter in the request. - * Example: format=xml - * - * - If there is nothing returned (void) we display a standard success message - * - * - If there is a PHP array returned, we try to convert it to a dataTable - * It is then possible to convert this datatable to any requested format (xml/etc) - * - * - If a bool is returned we convert to a string (true is displayed as 'true' false as 'false') - * - * - If an integer / float is returned, we simply return it - * - * @param mixed $value The initial returned value, before post process. If set to null, success response is returned. - * @param bool|string $apiModule The API module that was called - * @param bool|string $apiMethod The API method that was called - * @return mixed Usually a string, but can still be a PHP data structure if the format requested is 'original' - */ - public function getResponse($value = null, $apiModule = false, $apiMethod = false) - { + /** + * @param string $outputFormat + * @param array $request + */ + public function __construct($outputFormat, $request = array()) + { + $this->request = $request; + $this->outputFormat = $outputFormat; + } + + /** + * This method processes the data resulting from the API call. + * + * - If the data resulted from the API call is a Piwik_DataTable then + * - we apply the standard filters if the parameters have been found + * in the URL. For example to offset,limit the Table you can add the following parameters to any API + * call that returns a DataTable: filter_limit=10&filter_offset=20 + * - we apply the filters that have been previously queued on the DataTable + * @see Piwik_DataTable::queueFilter() + * - we apply the renderer that generate the DataTable in a given format (XML, PHP, HTML, JSON, etc.) + * the format can be changed using the 'format' parameter in the request. + * Example: format=xml + * + * - If there is nothing returned (void) we display a standard success message + * + * - If there is a PHP array returned, we try to convert it to a dataTable + * It is then possible to convert this datatable to any requested format (xml/etc) + * + * - If a bool is returned we convert to a string (true is displayed as 'true' false as 'false') + * + * - If an integer / float is returned, we simply return it + * + * @param mixed $value The initial returned value, before post process. If set to null, success response is returned. + * @param bool|string $apiModule The API module that was called + * @param bool|string $apiMethod The API method that was called + * @return mixed Usually a string, but can still be a PHP data structure if the format requested is 'original' + */ + public function getResponse($value = null, $apiModule = false, $apiMethod = false) + { $this->apiModule = $apiModule; $this->apiMethod = $apiMethod; - - // when null or void is returned from the api call, we handle it as a successful operation - if(!isset($value)) - { - return $this->handleSuccess(); - } - - // If the returned value is an object DataTable we - // apply the set of generic filters if asked in the URL - // and we render the DataTable according to the format specified in the URL - if($value instanceof Piwik_DataTable - || $value instanceof Piwik_DataTable_Array) - { - return $this->handleDataTable($value); - } - - // Case an array is returned from the API call, we convert it to the requested format - // - if calling from inside the application (format = original) - // => the data stays unchanged (ie. a standard php array or whatever data structure) - // - if any other format is requested, we have to convert this data structure (which we assume - // to be an array) to a DataTable in order to apply the requested DataTable_Renderer (for example XML) - if(is_array($value)) - { - return $this->handleArray($value); - } - - // original data structure requested, we return without process - if( $this->outputFormat == 'original' ) - { - return $value; - } - - if( is_object($value) - || is_resource($value)) - { - return $this->getResponseException(new Exception('The API cannot handle this data structure.')); - } - - // bool // integer // float // serialized object - return $this->handleScalar($value); - } - - /** - * Returns an error $message in the requested $format - * - * @param Exception $e - * @throws Exception - * @return string - */ - public function getResponseException(Exception $e) - { - $format = strtolower($this->outputFormat); - - if( $format == 'original' ) - { - throw $e; - } - - try { - $renderer = Piwik_DataTable_Renderer::factory($format); - } catch (Exception $exceptionRenderer) { - return "Error: " . $e->getMessage() . " and: " . $exceptionRenderer->getMessage(); - } - - $renderer->setException($e); - - if($format == 'php') - { - $renderer->setSerialize($this->caseRendererPHPSerialize()); - } - - return $renderer->renderException(); - } - - /** - * Returns true if the user requested to serialize the output data (&serialize=1 in the request) - * - * @param mixed $defaultSerializeValue Default value in case the user hasn't specified a value - * @return bool - */ - protected function caseRendererPHPSerialize($defaultSerializeValue = 1) - { - $serialize = Piwik_Common::getRequestVar('serialize', $defaultSerializeValue, 'int', $this->request); - if($serialize) - { - return true; - } - return false; - } - - /** - * Apply the specified renderer to the DataTable - * - * @param Piwik_DataTable|array $dataTable - * @return string - */ - protected function getRenderedDataTable($dataTable) - { - $format = strtolower($this->outputFormat); - - // if asked for original dataStructure - if($format == 'original') - { - // by default "original" data is not serialized - if($this->caseRendererPHPSerialize( $defaultSerialize = 0)) - { - $dataTable = serialize($dataTable); - } - return $dataTable; - } - - $method = Piwik_Common::getRequestVar('method', '', 'string', $this->request); - - $renderer = Piwik_DataTable_Renderer::factory($format); - $renderer->setTable($dataTable); - $renderer->setRenderSubTables(Piwik_Common::getRequestVar('expanded', false, 'int', $this->request)); - $renderer->setHideIdSubDatableFromResponse(Piwik_Common::getRequestVar('hideIdSubDatable', false, 'int', $this->request)); - - if($format == 'php') - { - $renderer->setSerialize($this->caseRendererPHPSerialize()); - $renderer->setPrettyDisplay(Piwik_Common::getRequestVar('prettyDisplay', false, 'int', $this->request)); - } - else if($format == 'html') - { - $renderer->setTableId($this->request['method']); - } - else if($format == 'csv' || $format == 'tsv') - { - $renderer->setConvertToUnicode(Piwik_Common::getRequestVar('convertToUnicode', true, 'int', $this->request)); - } - - // prepare translation of column names - if ($format == 'html' || $format == 'csv' || $format == 'tsv' || $format = 'rss') - { - $renderer->setApiMethod($method); - $renderer->setIdSite(Piwik_Common::getRequestVar('idSite', false, 'int', $this->request)); - $renderer->setTranslateColumnNames(Piwik_Common::getRequestVar('translateColumnNames', false, 'int', $this->request)); - } - - return $renderer->render(); - } - - /** - * Returns a success $message in the requested $format - * - * @param string $message - * @return string - */ - protected function handleSuccess( $message = 'ok' ) - { - // return a success message only if no content has already been buffered, useful when APIs return raw text or html content to the browser - if(!ob_get_contents()) - { - switch($this->outputFormat) - { - case 'xml': - @header("Content-Type: text/xml;charset=utf-8"); - $return = - "\n" . - "\n". - "\t\n". - ""; - break; - case 'json': - @header( "Content-Type: application/json" ); - $return = '{"result":"success", "message":"'.$message.'"}'; - break; - case 'php': - $return = array('result' => 'success', 'message' => $message); - if($this->caseRendererPHPSerialize()) - { - $return = serialize($return); - } - break; - - case 'csv': - @header("Content-Type: application/vnd.ms-excel"); - @header("Content-Disposition: attachment; filename=piwik-report-export.csv"); - $return = "message\n".$message; - break; - - default: - $return = 'Success:'.$message; - break; - } - return $return; - } - } - - /** - * Converts the given scalar to an data table - * - * @param mixed $scalar - * @return string - */ - protected function handleScalar($scalar) - { - $dataTable = new Piwik_DataTable_Simple(); - $dataTable->addRowsFromArray( array($scalar) ); - return $this->getRenderedDataTable($dataTable); - } - - /** - * Handles the given data table - * - * @param Piwik_DataTable $datatable - * @return string - */ - protected function handleDataTable($datatable) - { - // if requested, flatten nested tables - if (Piwik_Common::getRequestVar('flat', '0', 'string', $this->request) == '1') - { - $flattener = new Piwik_API_DataTableManipulator_Flattener($this->apiModule, $this->apiMethod, $this->request); - if (Piwik_Common::getRequestVar('include_aggregate_rows', '0', 'string', $this->request) == '1') - { - $flattener->includeAggregateRows(); - } - $datatable = $flattener->flatten($datatable); - } - - // if the flag disable_generic_filters is defined we skip the generic filters - if(0 == Piwik_Common::getRequestVar('disable_generic_filters', '0', 'string', $this->request)) - { - $genericFilter = new Piwik_API_DataTableGenericFilter($this->request); - $genericFilter->filter($datatable); - } - - // we automatically safe decode all datatable labels (against xss) - $datatable->queueFilter('SafeDecodeLabel'); - - // if the flag disable_queued_filters is defined we skip the filters that were queued - if(Piwik_Common::getRequestVar('disable_queued_filters', 'false', 'string', $this->request) == 'false') - { - $datatable->applyQueuedFilters(); - } - - // use the ColumnDelete filter if hideColumns/showColumns is provided (must be done - // after queued filters are run so processed metrics can be removed, too) - $hideColumns = Piwik_Common::getRequestVar('hideColumns', '', 'string', $this->request); - $showColumns = Piwik_Common::getRequestVar('showColumns', '', 'string', $this->request); - if ($hideColumns !== '' || $showColumns !== '') - { - $datatable->filter('ColumnDelete', array($hideColumns, $showColumns)); - } - - // apply label filter: only return rows matching the label parameter (more than one if more than one label) - $label = $this->getLabelQueryParam(); - if (!empty($label)) - { - $label = Piwik_Common::unsanitizeInputValues($label); - $addEmptyRows = Piwik_Common::getRequestVar('labelFilterAddEmptyRows', 0, 'int', $this->request) == 1; - - $filter = new Piwik_API_DataTableManipulator_LabelFilter($this->apiModule, $this->apiMethod, $this->request); - $datatable = $filter->filter($label, $datatable, $addEmptyRows); - } - return $this->getRenderedDataTable($datatable); - } - - /** - * Converts the given simple array to a data table - * - * @param array $array - * @return string - */ - protected function handleArray($array) - { - if($this->outputFormat == 'original') - { - // we handle the serialization. Because some php array have a very special structure that - // couldn't be converted with the automatic DataTable->addRowsFromSimpleArray - // the user may want to request the original PHP data structure serialized by the API - // in case he has to setup serialize=1 in the URL - if($this->caseRendererPHPSerialize( $defaultSerialize = 0)) - { - return serialize($array); - } - return $array; - } - $multiDimensional = $this->handleMultiDimensionalArray($array); - if($multiDimensional !== false) - { - return $multiDimensional; - } - - return $this->getRenderedDataTable($array); - } - - /** - * Is this a multi dimensional array? - * Multi dim arrays are not supported by the Datatable renderer. - * We manually render these. - * - * array( - * array( - * 1, - * 2 => array( 1, - * 2 - * ) - * ), - * array( 2, - * 3 - * ) - * ); - * - * @param array $array - * @return string|bool false if it isn't a multidim array - */ - protected function handleMultiDimensionalArray($array) - { - $first = reset($array); - foreach($array as $first) - { - if(is_array($first)) - { - foreach($first as $key => $value) - { - // Yes, this is a multi dim array - if(is_array($value)) - { - switch($this->outputFormat) - { - case 'json': - @header( "Content-Type: application/json" ); - return self::convertMultiDimensionalArrayToJson($array); - break; - - case 'php': - if($this->caseRendererPHPSerialize( $defaultSerialize = 0)) - { - return serialize($array); - } - return $array; - - case 'xml': - @header("Content-Type: text/xml;charset=utf-8"); - return $this->getRenderedDataTable($array); - default: - break; - } - } - } - } - } - return false; - } - - /** - * Render a multidimensional array to Json - * Handle Piwik_DataTable|Piwik_DataTable_Array elements in the first dimension only, following case does not work: - * array( - * array( - * Piwik_DataTable, - * 2 => array( - * 1, - * 2 - * ), - * ), - * ); - * - * @param array $array can contain scalar, arrays, Piwik_DataTable and Piwik_DataTable_Array - * @return string - */ - public static function convertMultiDimensionalArrayToJson($array) - { - $isAssociative = Piwik::isAssociativeArray($array); - - if($isAssociative) - { - $json = "{"; - } - else - { - $json = "["; - } - - foreach ($array as $key=>$value) - { - if($isAssociative) - { - $json .= "\"".$key."\":"; - } - - switch(true) - { - // Case dimension is a PHP array - case (is_array($value)): - - $json .= Piwik_Common::json_encode($value); - break; - - // Case dimension is a Piwik_DataTable_Array or a Piwik_DataTable - case ($value instanceof Piwik_DataTable_Array || $value instanceof Piwik_DataTable): - - $XMLRenderer = new Piwik_DataTable_Renderer_Json(); - $XMLRenderer->setTable($value); - $renderedReport = $XMLRenderer->render(); - $json .= $renderedReport; - break; - - // Case scalar - default: - - $json .= Piwik_Common::json_encode($value); - break; - } - - $json .= ","; - } - - // Remove trailing "," - $json = substr ($json, 0, strlen($json) - 1); - - if($isAssociative) - { - $json .= "}"; - } - else - { - $json .= "]"; - } - return $json; - } - - /** - * Returns the value for the label query parameter which can be either a string - * (ie, label=...) or array (ie, label[]=...). - * - * @return array - */ - private function getLabelQueryParam() - { + + // when null or void is returned from the api call, we handle it as a successful operation + if (!isset($value)) { + return $this->handleSuccess(); + } + + // If the returned value is an object DataTable we + // apply the set of generic filters if asked in the URL + // and we render the DataTable according to the format specified in the URL + if ($value instanceof Piwik_DataTable + || $value instanceof Piwik_DataTable_Array + ) { + return $this->handleDataTable($value); + } + + // Case an array is returned from the API call, we convert it to the requested format + // - if calling from inside the application (format = original) + // => the data stays unchanged (ie. a standard php array or whatever data structure) + // - if any other format is requested, we have to convert this data structure (which we assume + // to be an array) to a DataTable in order to apply the requested DataTable_Renderer (for example XML) + if (is_array($value)) { + return $this->handleArray($value); + } + + // original data structure requested, we return without process + if ($this->outputFormat == 'original') { + return $value; + } + + if (is_object($value) + || is_resource($value) + ) { + return $this->getResponseException(new Exception('The API cannot handle this data structure.')); + } + + // bool // integer // float // serialized object + return $this->handleScalar($value); + } + + /** + * Returns an error $message in the requested $format + * + * @param Exception $e + * @throws Exception + * @return string + */ + public function getResponseException(Exception $e) + { + $format = strtolower($this->outputFormat); + + if ($format == 'original') { + throw $e; + } + + try { + $renderer = Piwik_DataTable_Renderer::factory($format); + } catch (Exception $exceptionRenderer) { + return "Error: " . $e->getMessage() . " and: " . $exceptionRenderer->getMessage(); + } + + $renderer->setException($e); + + if ($format == 'php') { + $renderer->setSerialize($this->caseRendererPHPSerialize()); + } + + return $renderer->renderException(); + } + + /** + * Returns true if the user requested to serialize the output data (&serialize=1 in the request) + * + * @param mixed $defaultSerializeValue Default value in case the user hasn't specified a value + * @return bool + */ + protected function caseRendererPHPSerialize($defaultSerializeValue = 1) + { + $serialize = Piwik_Common::getRequestVar('serialize', $defaultSerializeValue, 'int', $this->request); + if ($serialize) { + return true; + } + return false; + } + + /** + * Apply the specified renderer to the DataTable + * + * @param Piwik_DataTable|array $dataTable + * @return string + */ + protected function getRenderedDataTable($dataTable) + { + $format = strtolower($this->outputFormat); + + // if asked for original dataStructure + if ($format == 'original') { + // by default "original" data is not serialized + if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) { + $dataTable = serialize($dataTable); + } + return $dataTable; + } + + $method = Piwik_Common::getRequestVar('method', '', 'string', $this->request); + + $renderer = Piwik_DataTable_Renderer::factory($format); + $renderer->setTable($dataTable); + $renderer->setRenderSubTables(Piwik_Common::getRequestVar('expanded', false, 'int', $this->request)); + $renderer->setHideIdSubDatableFromResponse(Piwik_Common::getRequestVar('hideIdSubDatable', false, 'int', $this->request)); + + if ($format == 'php') { + $renderer->setSerialize($this->caseRendererPHPSerialize()); + $renderer->setPrettyDisplay(Piwik_Common::getRequestVar('prettyDisplay', false, 'int', $this->request)); + } else if ($format == 'html') { + $renderer->setTableId($this->request['method']); + } else if ($format == 'csv' || $format == 'tsv') { + $renderer->setConvertToUnicode(Piwik_Common::getRequestVar('convertToUnicode', true, 'int', $this->request)); + } + + // prepare translation of column names + if ($format == 'html' || $format == 'csv' || $format == 'tsv' || $format = 'rss') { + $renderer->setApiMethod($method); + $renderer->setIdSite(Piwik_Common::getRequestVar('idSite', false, 'int', $this->request)); + $renderer->setTranslateColumnNames(Piwik_Common::getRequestVar('translateColumnNames', false, 'int', $this->request)); + } + + return $renderer->render(); + } + + /** + * Returns a success $message in the requested $format + * + * @param string $message + * @return string + */ + protected function handleSuccess($message = 'ok') + { + // return a success message only if no content has already been buffered, useful when APIs return raw text or html content to the browser + if (!ob_get_contents()) { + switch ($this->outputFormat) { + case 'xml': + @header("Content-Type: text/xml;charset=utf-8"); + $return = + "\n" . + "\n" . + "\t\n" . + ""; + break; + case 'json': + @header("Content-Type: application/json"); + $return = '{"result":"success", "message":"' . $message . '"}'; + break; + case 'php': + $return = array('result' => 'success', 'message' => $message); + if ($this->caseRendererPHPSerialize()) { + $return = serialize($return); + } + break; + + case 'csv': + @header("Content-Type: application/vnd.ms-excel"); + @header("Content-Disposition: attachment; filename=piwik-report-export.csv"); + $return = "message\n" . $message; + break; + + default: + $return = 'Success:' . $message; + break; + } + return $return; + } + } + + /** + * Converts the given scalar to an data table + * + * @param mixed $scalar + * @return string + */ + protected function handleScalar($scalar) + { + $dataTable = new Piwik_DataTable_Simple(); + $dataTable->addRowsFromArray(array($scalar)); + return $this->getRenderedDataTable($dataTable); + } + + /** + * Handles the given data table + * + * @param Piwik_DataTable $datatable + * @return string + */ + protected function handleDataTable($datatable) + { + // if requested, flatten nested tables + if (Piwik_Common::getRequestVar('flat', '0', 'string', $this->request) == '1') { + $flattener = new Piwik_API_DataTableManipulator_Flattener($this->apiModule, $this->apiMethod, $this->request); + if (Piwik_Common::getRequestVar('include_aggregate_rows', '0', 'string', $this->request) == '1') { + $flattener->includeAggregateRows(); + } + $datatable = $flattener->flatten($datatable); + } + + // if the flag disable_generic_filters is defined we skip the generic filters + if (0 == Piwik_Common::getRequestVar('disable_generic_filters', '0', 'string', $this->request)) { + $genericFilter = new Piwik_API_DataTableGenericFilter($this->request); + $genericFilter->filter($datatable); + } + + // we automatically safe decode all datatable labels (against xss) + $datatable->queueFilter('SafeDecodeLabel'); + + // if the flag disable_queued_filters is defined we skip the filters that were queued + if (Piwik_Common::getRequestVar('disable_queued_filters', 'false', 'string', $this->request) == 'false') { + $datatable->applyQueuedFilters(); + } + + // use the ColumnDelete filter if hideColumns/showColumns is provided (must be done + // after queued filters are run so processed metrics can be removed, too) + $hideColumns = Piwik_Common::getRequestVar('hideColumns', '', 'string', $this->request); + $showColumns = Piwik_Common::getRequestVar('showColumns', '', 'string', $this->request); + if ($hideColumns !== '' || $showColumns !== '') { + $datatable->filter('ColumnDelete', array($hideColumns, $showColumns)); + } + + // apply label filter: only return rows matching the label parameter (more than one if more than one label) + $label = $this->getLabelQueryParam(); + if (!empty($label)) { + $label = Piwik_Common::unsanitizeInputValues($label); + $addEmptyRows = Piwik_Common::getRequestVar('labelFilterAddEmptyRows', 0, 'int', $this->request) == 1; + + $filter = new Piwik_API_DataTableManipulator_LabelFilter($this->apiModule, $this->apiMethod, $this->request); + $datatable = $filter->filter($label, $datatable, $addEmptyRows); + } + return $this->getRenderedDataTable($datatable); + } + + /** + * Converts the given simple array to a data table + * + * @param array $array + * @return string + */ + protected function handleArray($array) + { + if ($this->outputFormat == 'original') { + // we handle the serialization. Because some php array have a very special structure that + // couldn't be converted with the automatic DataTable->addRowsFromSimpleArray + // the user may want to request the original PHP data structure serialized by the API + // in case he has to setup serialize=1 in the URL + if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) { + return serialize($array); + } + return $array; + } + $multiDimensional = $this->handleMultiDimensionalArray($array); + if ($multiDimensional !== false) { + return $multiDimensional; + } + + return $this->getRenderedDataTable($array); + } + + /** + * Is this a multi dimensional array? + * Multi dim arrays are not supported by the Datatable renderer. + * We manually render these. + * + * array( + * array( + * 1, + * 2 => array( 1, + * 2 + * ) + * ), + * array( 2, + * 3 + * ) + * ); + * + * @param array $array + * @return string|bool false if it isn't a multidim array + */ + protected function handleMultiDimensionalArray($array) + { + $first = reset($array); + foreach ($array as $first) { + if (is_array($first)) { + foreach ($first as $key => $value) { + // Yes, this is a multi dim array + if (is_array($value)) { + switch ($this->outputFormat) { + case 'json': + @header("Content-Type: application/json"); + return self::convertMultiDimensionalArrayToJson($array); + break; + + case 'php': + if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) { + return serialize($array); + } + return $array; + + case 'xml': + @header("Content-Type: text/xml;charset=utf-8"); + return $this->getRenderedDataTable($array); + default: + break; + } + } + } + } + } + return false; + } + + /** + * Render a multidimensional array to Json + * Handle Piwik_DataTable|Piwik_DataTable_Array elements in the first dimension only, following case does not work: + * array( + * array( + * Piwik_DataTable, + * 2 => array( + * 1, + * 2 + * ), + * ), + * ); + * + * @param array $array can contain scalar, arrays, Piwik_DataTable and Piwik_DataTable_Array + * @return string + */ + public static function convertMultiDimensionalArrayToJson($array) + { + $isAssociative = Piwik::isAssociativeArray($array); + + if ($isAssociative) { + $json = "{"; + } else { + $json = "["; + } + + foreach ($array as $key => $value) { + if ($isAssociative) { + $json .= "\"" . $key . "\":"; + } + + switch (true) { + // Case dimension is a PHP array + case (is_array($value)): + + $json .= Piwik_Common::json_encode($value); + break; + + // Case dimension is a Piwik_DataTable_Array or a Piwik_DataTable + case ($value instanceof Piwik_DataTable_Array || $value instanceof Piwik_DataTable): + + $XMLRenderer = new Piwik_DataTable_Renderer_Json(); + $XMLRenderer->setTable($value); + $renderedReport = $XMLRenderer->render(); + $json .= $renderedReport; + break; + + // Case scalar + default: + + $json .= Piwik_Common::json_encode($value); + break; + } + + $json .= ","; + } + + // Remove trailing "," + $json = substr($json, 0, strlen($json) - 1); + + if ($isAssociative) { + $json .= "}"; + } else { + $json .= "]"; + } + return $json; + } + + /** + * Returns the value for the label query parameter which can be either a string + * (ie, label=...) or array (ie, label[]=...). + * + * @return array + */ + private function getLabelQueryParam() + { $label = Piwik_Common::getRequestVar('label', array(), 'array', $this->request); - if (empty($label)) - { - $label = Piwik_Common::getRequestVar('label', '', 'string', $this->request); - if (!empty($label)) - { - $label = array($label); - } + if (empty($label)) { + $label = Piwik_Common::getRequestVar('label', '', 'string', $this->request); + if (!empty($label)) { + $label = array($label); + } } return $label; - } + } } diff --git a/core/Access.php b/core/Access.php index ed7b2e386e..0e3bc42054 100644 --- a/core/Access.php +++ b/core/Access.php @@ -11,9 +11,9 @@ /** * Class to handle User Access: - * - loads user access from the Piwik_Auth_Result object + * - loads user access from the Piwik_Auth_Result object * - provides easy to use API to check the permissions for the current (check* methods) - * + * * In Piwik there are mainly 4 access levels * - no access * - VIEW access @@ -24,389 +24,368 @@ * A given user has a given access level for a given website. * For example: * User Noemie has - * - VIEW access on the website 1, + * - VIEW access on the website 1, * - ADMIN on the website 2 and 4, and * - NO access on the website 3 and 5 * - * There is only one Super User. He has ADMIN access to all the websites + * There is only one Super User. He has ADMIN access to all the websites * and he only can change the main configuration settings. * * @package Piwik * @subpackage Piwik_Access */ class Piwik_Access -{ - /** - * Array of idsites available to the current user, indexed by permission level - * @see getSitesIdWith*() - * - * @var array - */ - protected $idsitesByAccess = null; - - /** - * Login of the current user - * - * @var string - */ - protected $login = null; - - /** - * token_auth of the current user - * - * @var string - */ - protected $token_auth = null; - - /** - * Defines if the current user is the super user - * @see isSuperUser() - * - * @var bool - */ - protected $isSuperUser = false; - - /** - * List of available permissions in Piwik - * - * @var array - */ - static private $availableAccess = array('noaccess', 'view', 'admin', 'superuser'); - - /** - * Authentification object (see Piwik_Auth) - * - * @var Piwik_Auth - */ - private $auth = null; - - /** - * Returns the list of the existing Access level. - * Useful when a given API method requests a given acccess Level. - * We first check that the required access level exists. - * - * @return array - */ - static public function getListAccess() - { - return self::$availableAccess; - } - - /** - * Constructor - */ - function __construct() - { - $this->idsitesByAccess = array( - 'view' => array(), - 'admin' => array(), - 'superuser' => array() - ); - } - - /** - * Loads the access levels for the current user. - * - * Calls the authentication method to try to log the user in the system. - * If the user credentials are not correct we don't load anything. - * If the login/password is correct the user is either the SuperUser or a normal user. - * We load the access levels for this user for all the websites. - * - * @param null|Piwik_Auth $auth Auth adapter - * @return bool true on success, false if reloading access failed (when auth object wasn't specified and user is not enforced to be Super User) - */ - public function reloadAccess(Piwik_Auth $auth = null) - { - if(!is_null($auth)) - { - $this->auth = $auth; - } - - // if the Piwik_Auth wasn't set, we may be in the special case of setSuperUser(), otherwise we fail - if(is_null($this->auth)) - { - if($this->isSuperUser()) - { - return $this->reloadAccessSuperUser(); - } - return false; - } - - // access = array ( idsite => accessIdSite, idsite2 => accessIdSite2) - $result = $this->auth->authenticate(); - - if(!$result->isValid()) - { - return false; - } - $this->login = $result->getIdentity(); - $this->token_auth = $result->getTokenAuth(); - - // case the superUser is logged in - if($result->getCode() == Piwik_Auth_Result::SUCCESS_SUPERUSER_AUTH_CODE) - { - return $this->reloadAccessSuperUser(); - } - // in case multiple calls to API using different tokens, we ensure we reset it as not SU - $this->setSuperUser(false); - - // we join with site in case there are rows in access for an idsite that doesn't exist anymore - // (backward compatibility ; before we deleted the site without deleting rows in _access table) - $accessRaw = self::getRawSitesWithSomeViewAccess($this->login); - foreach($accessRaw as $access) - { - $this->idsitesByAccess[$access['access']][] = $access['idsite']; - } - return true; - } - - static public function getRawSitesWithSomeViewAccess($login) - { - return Piwik_FetchAll(self::getSqlAccessSite("access, t2.idsite"), $login); - } - - /** - * Returns the SQL query joining sites and access table for a given login - * - * @param string $select Columns or expression to SELECT FROM table, eg. "MIN(ts_created)" - * @return string SQL query - */ - static public function getSqlAccessSite($select) - { - return "SELECT ". $select ." - FROM ".Piwik_Common::prefixTable('access'). " as t1 - JOIN ".Piwik_Common::prefixTable('site')." as t2 USING (idsite) ". - " WHERE login = ?"; - } - - /** - * Reload super user access - * - * @return bool - */ - protected function reloadAccessSuperUser() - { - $this->isSuperUser = true; - $this->idsitesByAccess['superuser'] = Piwik_SitesManager_API::getInstance()->getAllSitesId(); - $this->login = Piwik_Config::getInstance()->superuser['login']; - return true; - } - - /** - * We bypass the normal auth method and give the current user Super User rights. - * This should be very carefully used. - * - * @param bool $bool - */ - public function setSuperUser($bool = true) - { - if($bool) - { - $this->reloadAccessSuperUser(); - } - else - { - $this->isSuperUser = false; - $this->idsitesByAccess['superuser'] = array(); - } - } - - /** - * Returns true if the current user is logged in as the super user - * - * @return bool - */ - public function isSuperUser() - { - return $this->isSuperUser; - } - - /** - * Returns the current user login - * - * @return string|null - */ - public function getLogin() - { - return $this->login; - } - - /** - * Returns the token_auth used to authenticate this user in the API - * - * @return string|null - */ - public function getTokenAuth() - { - return $this->token_auth; - } - - /** - * Returns an array of ID sites for which the user has at least a VIEW access. - * Which means VIEW or ADMIN or SUPERUSER. - * - * @return array Example if the user is ADMIN for 4 - * and has VIEW access for 1 and 7, it returns array(1, 4, 7); - */ - public function getSitesIdWithAtLeastViewAccess() - { - return array_unique(array_merge( - $this->idsitesByAccess['view'], - $this->idsitesByAccess['admin'], - $this->idsitesByAccess['superuser']) - ); - } - - /** - * Returns an array of ID sites for which the user has an ADMIN access. - * - * @return array Example if the user is ADMIN for 4 and 8 - * and has VIEW access for 1 and 7, it returns array(4, 8); - */ - public function getSitesIdWithAdminAccess() - { - return array_unique(array_merge( - $this->idsitesByAccess['admin'], - $this->idsitesByAccess['superuser']) - ); - } - - - /** - * Returns an array of ID sites for which the user has a VIEW access only. - * - * @return array Example if the user is ADMIN for 4 - * and has VIEW access for 1 and 7, it returns array(1, 7); - * @see getSitesIdWithAtLeastViewAccess() - */ - public function getSitesIdWithViewAccess() - { - return $this->idsitesByAccess['view']; - } - - /** - * Throws an exception if the user is not the SuperUser - * - * @throws Piwik_Access_NoAccessException - */ - public function checkUserIsSuperUser() - { - if(!$this->isSuperUser()) - { - throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilege', array("'superuser'"))); - } - } - - /** - * If the user doesn't have an ADMIN access for at least one website, throws an exception - * - * @throws Piwik_Access_NoAccessException - */ - public function checkUserHasSomeAdminAccess() - { - if($this->isSuperUser()) - { - return; - } - $idSitesAccessible = $this->getSitesIdWithAdminAccess(); - if(count($idSitesAccessible) == 0) - { - throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAtLeastOneWebsite', array('admin'))); - } - } - - /** - * If the user doesn't have any view permission, throw exception - * - * @throws Piwik_Access_NoAccessException - */ - public function checkUserHasSomeViewAccess() - { - if($this->isSuperUser()) - { - return; - } - $idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess(); - if(count($idSitesAccessible) == 0) - { - throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAtLeastOneWebsite', array('view'))); - } - } - - /** - * This method checks that the user has ADMIN access for the given list of websites. - * If the user doesn't have ADMIN access for at least one website of the list, we throw an exception. - * - * @param int|array $idSites List of ID sites to check - * @throws Piwik_Access_NoAccessException If for any of the websites the user doesn't have an ADMIN access - */ - public function checkUserHasAdminAccess( $idSites ) - { - if($this->isSuperUser()) - { - return; - } - $idSites = $this->getIdSites($idSites); - $idSitesAccessible = $this->getSitesIdWithAdminAccess(); - foreach($idSites as $idsite) - { - if(!in_array($idsite, $idSitesAccessible)) - { - throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAccessWebsite', array("'admin'", $idsite))); - } - } - } - - /** - * This method checks that the user has VIEW or ADMIN access for the given list of websites. - * If the user doesn't have VIEW or ADMIN access for at least one website of the list, we throw an exception. - * - * @param int|array|string $idSites List of ID sites to check (integer, array of integers, string comma separated list of integers) - * @throws Piwik_Access_NoAccessException If for any of the websites the user doesn't have an VIEW or ADMIN access - */ - public function checkUserHasViewAccess( $idSites ) - { - if($this->isSuperUser()) - { - return; - } - $idSites = $this->getIdSites($idSites); - $idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess(); - foreach($idSites as $idsite) - { - if(!in_array($idsite, $idSitesAccessible)) - { - throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAccessWebsite', array("'view'", $idsite))); - } - } - } - - /** - * @param int|array|string $idSites - * @return array - * @throws Piwik_Access_NoAccessException - */ - protected function getIdSites($idSites) - { - if($idSites === 'all') - { - $idSites = $this->getSitesIdWithAtLeastViewAccess(); - } - - $idSites = Piwik_Site::getIdSitesFromIdSitesString($idSites); - if(empty($idSites)) - { - throw new Piwik_Access_NoAccessException("The parameter 'idSite=' is missing from the request."); - } - return $idSites; - } +{ + /** + * Array of idsites available to the current user, indexed by permission level + * @see getSitesIdWith*() + * + * @var array + */ + protected $idsitesByAccess = null; + + /** + * Login of the current user + * + * @var string + */ + protected $login = null; + + /** + * token_auth of the current user + * + * @var string + */ + protected $token_auth = null; + + /** + * Defines if the current user is the super user + * @see isSuperUser() + * + * @var bool + */ + protected $isSuperUser = false; + + /** + * List of available permissions in Piwik + * + * @var array + */ + static private $availableAccess = array('noaccess', 'view', 'admin', 'superuser'); + + /** + * Authentification object (see Piwik_Auth) + * + * @var Piwik_Auth + */ + private $auth = null; + + /** + * Returns the list of the existing Access level. + * Useful when a given API method requests a given acccess Level. + * We first check that the required access level exists. + * + * @return array + */ + static public function getListAccess() + { + return self::$availableAccess; + } + + /** + * Constructor + */ + function __construct() + { + $this->idsitesByAccess = array( + 'view' => array(), + 'admin' => array(), + 'superuser' => array() + ); + } + + /** + * Loads the access levels for the current user. + * + * Calls the authentication method to try to log the user in the system. + * If the user credentials are not correct we don't load anything. + * If the login/password is correct the user is either the SuperUser or a normal user. + * We load the access levels for this user for all the websites. + * + * @param null|Piwik_Auth $auth Auth adapter + * @return bool true on success, false if reloading access failed (when auth object wasn't specified and user is not enforced to be Super User) + */ + public function reloadAccess(Piwik_Auth $auth = null) + { + if (!is_null($auth)) { + $this->auth = $auth; + } + + // if the Piwik_Auth wasn't set, we may be in the special case of setSuperUser(), otherwise we fail + if (is_null($this->auth)) { + if ($this->isSuperUser()) { + return $this->reloadAccessSuperUser(); + } + return false; + } + + // access = array ( idsite => accessIdSite, idsite2 => accessIdSite2) + $result = $this->auth->authenticate(); + + if (!$result->isValid()) { + return false; + } + $this->login = $result->getIdentity(); + $this->token_auth = $result->getTokenAuth(); + + // case the superUser is logged in + if ($result->getCode() == Piwik_Auth_Result::SUCCESS_SUPERUSER_AUTH_CODE) { + return $this->reloadAccessSuperUser(); + } + // in case multiple calls to API using different tokens, we ensure we reset it as not SU + $this->setSuperUser(false); + + // we join with site in case there are rows in access for an idsite that doesn't exist anymore + // (backward compatibility ; before we deleted the site without deleting rows in _access table) + $accessRaw = self::getRawSitesWithSomeViewAccess($this->login); + foreach ($accessRaw as $access) { + $this->idsitesByAccess[$access['access']][] = $access['idsite']; + } + return true; + } + + static public function getRawSitesWithSomeViewAccess($login) + { + return Piwik_FetchAll(self::getSqlAccessSite("access, t2.idsite"), $login); + } + + /** + * Returns the SQL query joining sites and access table for a given login + * + * @param string $select Columns or expression to SELECT FROM table, eg. "MIN(ts_created)" + * @return string SQL query + */ + static public function getSqlAccessSite($select) + { + return "SELECT " . $select . " + FROM " . Piwik_Common::prefixTable('access') . " as t1 + JOIN " . Piwik_Common::prefixTable('site') . " as t2 USING (idsite) " . + " WHERE login = ?"; + } + + /** + * Reload super user access + * + * @return bool + */ + protected function reloadAccessSuperUser() + { + $this->isSuperUser = true; + $this->idsitesByAccess['superuser'] = Piwik_SitesManager_API::getInstance()->getAllSitesId(); + $this->login = Piwik_Config::getInstance()->superuser['login']; + return true; + } + + /** + * We bypass the normal auth method and give the current user Super User rights. + * This should be very carefully used. + * + * @param bool $bool + */ + public function setSuperUser($bool = true) + { + if ($bool) { + $this->reloadAccessSuperUser(); + } else { + $this->isSuperUser = false; + $this->idsitesByAccess['superuser'] = array(); + } + } + + /** + * Returns true if the current user is logged in as the super user + * + * @return bool + */ + public function isSuperUser() + { + return $this->isSuperUser; + } + + /** + * Returns the current user login + * + * @return string|null + */ + public function getLogin() + { + return $this->login; + } + + /** + * Returns the token_auth used to authenticate this user in the API + * + * @return string|null + */ + public function getTokenAuth() + { + return $this->token_auth; + } + + /** + * Returns an array of ID sites for which the user has at least a VIEW access. + * Which means VIEW or ADMIN or SUPERUSER. + * + * @return array Example if the user is ADMIN for 4 + * and has VIEW access for 1 and 7, it returns array(1, 4, 7); + */ + public function getSitesIdWithAtLeastViewAccess() + { + return array_unique(array_merge( + $this->idsitesByAccess['view'], + $this->idsitesByAccess['admin'], + $this->idsitesByAccess['superuser']) + ); + } + + /** + * Returns an array of ID sites for which the user has an ADMIN access. + * + * @return array Example if the user is ADMIN for 4 and 8 + * and has VIEW access for 1 and 7, it returns array(4, 8); + */ + public function getSitesIdWithAdminAccess() + { + return array_unique(array_merge( + $this->idsitesByAccess['admin'], + $this->idsitesByAccess['superuser']) + ); + } + + + /** + * Returns an array of ID sites for which the user has a VIEW access only. + * + * @return array Example if the user is ADMIN for 4 + * and has VIEW access for 1 and 7, it returns array(1, 7); + * @see getSitesIdWithAtLeastViewAccess() + */ + public function getSitesIdWithViewAccess() + { + return $this->idsitesByAccess['view']; + } + + /** + * Throws an exception if the user is not the SuperUser + * + * @throws Piwik_Access_NoAccessException + */ + public function checkUserIsSuperUser() + { + if (!$this->isSuperUser()) { + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilege', array("'superuser'"))); + } + } + + /** + * If the user doesn't have an ADMIN access for at least one website, throws an exception + * + * @throws Piwik_Access_NoAccessException + */ + public function checkUserHasSomeAdminAccess() + { + if ($this->isSuperUser()) { + return; + } + $idSitesAccessible = $this->getSitesIdWithAdminAccess(); + if (count($idSitesAccessible) == 0) { + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAtLeastOneWebsite', array('admin'))); + } + } + + /** + * If the user doesn't have any view permission, throw exception + * + * @throws Piwik_Access_NoAccessException + */ + public function checkUserHasSomeViewAccess() + { + if ($this->isSuperUser()) { + return; + } + $idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess(); + if (count($idSitesAccessible) == 0) { + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAtLeastOneWebsite', array('view'))); + } + } + + /** + * This method checks that the user has ADMIN access for the given list of websites. + * If the user doesn't have ADMIN access for at least one website of the list, we throw an exception. + * + * @param int|array $idSites List of ID sites to check + * @throws Piwik_Access_NoAccessException If for any of the websites the user doesn't have an ADMIN access + */ + public function checkUserHasAdminAccess($idSites) + { + if ($this->isSuperUser()) { + return; + } + $idSites = $this->getIdSites($idSites); + $idSitesAccessible = $this->getSitesIdWithAdminAccess(); + foreach ($idSites as $idsite) { + if (!in_array($idsite, $idSitesAccessible)) { + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAccessWebsite', array("'admin'", $idsite))); + } + } + } + + /** + * This method checks that the user has VIEW or ADMIN access for the given list of websites. + * If the user doesn't have VIEW or ADMIN access for at least one website of the list, we throw an exception. + * + * @param int|array|string $idSites List of ID sites to check (integer, array of integers, string comma separated list of integers) + * @throws Piwik_Access_NoAccessException If for any of the websites the user doesn't have an VIEW or ADMIN access + */ + public function checkUserHasViewAccess($idSites) + { + if ($this->isSuperUser()) { + return; + } + $idSites = $this->getIdSites($idSites); + $idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess(); + foreach ($idSites as $idsite) { + if (!in_array($idsite, $idSitesAccessible)) { + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAccessWebsite', array("'view'", $idsite))); + } + } + } + + /** + * @param int|array|string $idSites + * @return array + * @throws Piwik_Access_NoAccessException + */ + protected function getIdSites($idSites) + { + if ($idSites === 'all') { + $idSites = $this->getSitesIdWithAtLeastViewAccess(); + } + + $idSites = Piwik_Site::getIdSitesFromIdSitesString($idSites); + if (empty($idSites)) { + throw new Piwik_Access_NoAccessException("The parameter 'idSite=' is missing from the request."); + } + return $idSites; + } } /** * Exception thrown when a user doesn't have sufficient access. - * + * * @package Piwik * @subpackage Piwik_Access */ class Piwik_Access_NoAccessException extends Exception -{} +{ +} diff --git a/core/Archive.php b/core/Archive.php index 1a18efd53a..530694f7a6 100644 --- a/core/Archive.php +++ b/core/Archive.php @@ -1,478 +1,456 @@ - * $archive = Piwik_Archive::build($idSite = 1, $period = 'week', '2008-03-08' ); - * $dataTable = $archive->getDataTable('Provider_hostnameExt'); - * $dataTable->queueFilter('ReplaceColumnNames'); - * return $dataTable; + * $archive = Piwik_Archive::build($idSite = 1, $period = 'week', '2008-03-08' ); + * $dataTable = $archive->getDataTable('Provider_hostnameExt'); + * $dataTable->queueFilter('ReplaceColumnNames'); + * return $dataTable; * - * + * * Example bis: *
- * 		$archive = Piwik_Archive::build($idSite = 3, $period = 'day', $date = 'today' );
- * 		$nbVisits = $archive->getNumeric('nb_visits');
- * 		return $nbVisits;		
+ *        $archive = Piwik_Archive::build($idSite = 3, $period = 'day', $date = 'today' );
+ *        $nbVisits = $archive->getNumeric('nb_visits');
+ *        return $nbVisits;
  * 
- * + * * If the requested statistics are not yet processed, Archive uses ArchiveProcessing to archive the statistics. - * + * * @package Piwik * @subpackage Piwik_Archive */ abstract class Piwik_Archive { - /** - * When saving DataTables in the DB, we sometimes replace the columns name by these IDs so we save up lots of bytes - * Eg. INDEX_NB_UNIQ_VISITORS is an integer: 4 bytes, but 'nb_uniq_visitors' is 16 bytes at least - * (in php it's actually even much more) - * - */ - const INDEX_NB_UNIQ_VISITORS = 1; - const INDEX_NB_VISITS = 2; - const INDEX_NB_ACTIONS = 3; - const INDEX_MAX_ACTIONS = 4; - const INDEX_SUM_VISIT_LENGTH = 5; - const INDEX_BOUNCE_COUNT = 6; - const INDEX_NB_VISITS_CONVERTED = 7; - const INDEX_NB_CONVERSIONS = 8; - const INDEX_REVENUE = 9; - const INDEX_GOALS = 10; - const INDEX_SUM_DAILY_NB_UNIQ_VISITORS = 11; - - // Specific to the Actions reports - const INDEX_PAGE_NB_HITS = 12; - const INDEX_PAGE_SUM_TIME_SPENT = 13; - - const INDEX_PAGE_EXIT_NB_UNIQ_VISITORS = 14; - const INDEX_PAGE_EXIT_NB_VISITS = 15; - const INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS = 16; - - const INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS = 17; - const INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS = 18; - const INDEX_PAGE_ENTRY_NB_VISITS = 19; - const INDEX_PAGE_ENTRY_NB_ACTIONS = 20; - const INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH = 21; - const INDEX_PAGE_ENTRY_BOUNCE_COUNT = 22; - - // Ecommerce Items reports - const INDEX_ECOMMERCE_ITEM_REVENUE = 23; - const INDEX_ECOMMERCE_ITEM_QUANTITY = 24; - const INDEX_ECOMMERCE_ITEM_PRICE = 25; - const INDEX_ECOMMERCE_ORDERS = 26; - const INDEX_ECOMMERCE_ITEM_PRICE_VIEWED = 27; - - // Site Search - const INDEX_SITE_SEARCH_HAS_NO_RESULT = 28; - const INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS = 29; - - // Performance Analytics - const INDEX_PAGE_SUM_TIME_GENERATION = 30; - const INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION = 31; - - // Goal reports - const INDEX_GOAL_NB_CONVERSIONS = 1; - const INDEX_GOAL_REVENUE = 2; - const INDEX_GOAL_NB_VISITS_CONVERTED = 3; - - const INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL = 4; - const INDEX_GOAL_ECOMMERCE_REVENUE_TAX = 5; - const INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING = 6; - const INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT = 7; - const INDEX_GOAL_ECOMMERCE_ITEMS = 8; - - public static $mappingFromIdToName = array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 'nb_uniq_visitors', - Piwik_Archive::INDEX_NB_VISITS => 'nb_visits', - Piwik_Archive::INDEX_NB_ACTIONS => 'nb_actions', - Piwik_Archive::INDEX_MAX_ACTIONS => 'max_actions', - Piwik_Archive::INDEX_SUM_VISIT_LENGTH => 'sum_visit_length', - Piwik_Archive::INDEX_BOUNCE_COUNT => 'bounce_count', - Piwik_Archive::INDEX_NB_VISITS_CONVERTED => 'nb_visits_converted', - Piwik_Archive::INDEX_NB_CONVERSIONS => 'nb_conversions', - Piwik_Archive::INDEX_REVENUE => 'revenue', - Piwik_Archive::INDEX_GOALS => 'goals', - Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_nb_uniq_visitors', - - // Actions metrics - Piwik_Archive::INDEX_PAGE_NB_HITS => 'nb_hits', - Piwik_Archive::INDEX_PAGE_SUM_TIME_SPENT => 'sum_time_spent', - Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION => 'sum_time_generation', - Piwik_Archive::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION => 'nb_hits_with_time_generation', - - Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS => 'exit_nb_uniq_visitors', - Piwik_Archive::INDEX_PAGE_EXIT_NB_VISITS => 'exit_nb_visits', - Piwik_Archive::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_exit_nb_uniq_visitors', - - Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS => 'entry_nb_uniq_visitors', - Piwik_Archive::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_entry_nb_uniq_visitors', - Piwik_Archive::INDEX_PAGE_ENTRY_NB_VISITS => 'entry_nb_visits', - Piwik_Archive::INDEX_PAGE_ENTRY_NB_ACTIONS => 'entry_nb_actions', - Piwik_Archive::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH => 'entry_sum_visit_length', - Piwik_Archive::INDEX_PAGE_ENTRY_BOUNCE_COUNT => 'entry_bounce_count', - Piwik_Archive::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS => 'nb_hits_following_search', - - // Items reports metrics - Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE => 'revenue', - Piwik_Archive::INDEX_ECOMMERCE_ITEM_QUANTITY => 'quantity', - Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE => 'price', - Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED => 'price_viewed', - Piwik_Archive::INDEX_ECOMMERCE_ORDERS => 'orders', - ); - - public static $mappingFromIdToNameGoal = array( - Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 'nb_conversions', - Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 'nb_visits_converted', - Piwik_Archive::INDEX_GOAL_REVENUE => 'revenue', - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => 'revenue_subtotal', - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => 'revenue_tax', - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => 'revenue_shipping', - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => 'revenue_discount', - Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 'items', - ); - - /** - * string indexed column name => Integer indexed column name + /** + * When saving DataTables in the DB, we sometimes replace the columns name by these IDs so we save up lots of bytes + * Eg. INDEX_NB_UNIQ_VISITORS is an integer: 4 bytes, but 'nb_uniq_visitors' is 16 bytes at least + * (in php it's actually even much more) + * + */ + const INDEX_NB_UNIQ_VISITORS = 1; + const INDEX_NB_VISITS = 2; + const INDEX_NB_ACTIONS = 3; + const INDEX_MAX_ACTIONS = 4; + const INDEX_SUM_VISIT_LENGTH = 5; + const INDEX_BOUNCE_COUNT = 6; + const INDEX_NB_VISITS_CONVERTED = 7; + const INDEX_NB_CONVERSIONS = 8; + const INDEX_REVENUE = 9; + const INDEX_GOALS = 10; + const INDEX_SUM_DAILY_NB_UNIQ_VISITORS = 11; + + // Specific to the Actions reports + const INDEX_PAGE_NB_HITS = 12; + const INDEX_PAGE_SUM_TIME_SPENT = 13; + + const INDEX_PAGE_EXIT_NB_UNIQ_VISITORS = 14; + const INDEX_PAGE_EXIT_NB_VISITS = 15; + const INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS = 16; + + const INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS = 17; + const INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS = 18; + const INDEX_PAGE_ENTRY_NB_VISITS = 19; + const INDEX_PAGE_ENTRY_NB_ACTIONS = 20; + const INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH = 21; + const INDEX_PAGE_ENTRY_BOUNCE_COUNT = 22; + + // Ecommerce Items reports + const INDEX_ECOMMERCE_ITEM_REVENUE = 23; + const INDEX_ECOMMERCE_ITEM_QUANTITY = 24; + const INDEX_ECOMMERCE_ITEM_PRICE = 25; + const INDEX_ECOMMERCE_ORDERS = 26; + const INDEX_ECOMMERCE_ITEM_PRICE_VIEWED = 27; + + // Site Search + const INDEX_SITE_SEARCH_HAS_NO_RESULT = 28; + const INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS = 29; + + // Performance Analytics + const INDEX_PAGE_SUM_TIME_GENERATION = 30; + const INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION = 31; + + // Goal reports + const INDEX_GOAL_NB_CONVERSIONS = 1; + const INDEX_GOAL_REVENUE = 2; + const INDEX_GOAL_NB_VISITS_CONVERTED = 3; + + const INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL = 4; + const INDEX_GOAL_ECOMMERCE_REVENUE_TAX = 5; + const INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING = 6; + const INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT = 7; + const INDEX_GOAL_ECOMMERCE_ITEMS = 8; + + public static $mappingFromIdToName = array( + Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 'nb_uniq_visitors', + Piwik_Archive::INDEX_NB_VISITS => 'nb_visits', + Piwik_Archive::INDEX_NB_ACTIONS => 'nb_actions', + Piwik_Archive::INDEX_MAX_ACTIONS => 'max_actions', + Piwik_Archive::INDEX_SUM_VISIT_LENGTH => 'sum_visit_length', + Piwik_Archive::INDEX_BOUNCE_COUNT => 'bounce_count', + Piwik_Archive::INDEX_NB_VISITS_CONVERTED => 'nb_visits_converted', + Piwik_Archive::INDEX_NB_CONVERSIONS => 'nb_conversions', + Piwik_Archive::INDEX_REVENUE => 'revenue', + Piwik_Archive::INDEX_GOALS => 'goals', + Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_nb_uniq_visitors', + + // Actions metrics + Piwik_Archive::INDEX_PAGE_NB_HITS => 'nb_hits', + Piwik_Archive::INDEX_PAGE_SUM_TIME_SPENT => 'sum_time_spent', + Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION => 'sum_time_generation', + Piwik_Archive::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION => 'nb_hits_with_time_generation', + + Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS => 'exit_nb_uniq_visitors', + Piwik_Archive::INDEX_PAGE_EXIT_NB_VISITS => 'exit_nb_visits', + Piwik_Archive::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_exit_nb_uniq_visitors', + + Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS => 'entry_nb_uniq_visitors', + Piwik_Archive::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_entry_nb_uniq_visitors', + Piwik_Archive::INDEX_PAGE_ENTRY_NB_VISITS => 'entry_nb_visits', + Piwik_Archive::INDEX_PAGE_ENTRY_NB_ACTIONS => 'entry_nb_actions', + Piwik_Archive::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH => 'entry_sum_visit_length', + Piwik_Archive::INDEX_PAGE_ENTRY_BOUNCE_COUNT => 'entry_bounce_count', + Piwik_Archive::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS => 'nb_hits_following_search', + + // Items reports metrics + Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE => 'revenue', + Piwik_Archive::INDEX_ECOMMERCE_ITEM_QUANTITY => 'quantity', + Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE => 'price', + Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED => 'price_viewed', + Piwik_Archive::INDEX_ECOMMERCE_ORDERS => 'orders', + ); + + public static $mappingFromIdToNameGoal = array( + Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 'nb_conversions', + Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 'nb_visits_converted', + Piwik_Archive::INDEX_GOAL_REVENUE => 'revenue', + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => 'revenue_subtotal', + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => 'revenue_tax', + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => 'revenue_shipping', + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => 'revenue_discount', + Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 'items', + ); + + /** + * string indexed column name => Integer indexed column name * @var array - */ - public static $mappingFromNameToId = array( - 'nb_uniq_visitors' => Piwik_Archive::INDEX_NB_UNIQ_VISITORS, - 'nb_visits' => Piwik_Archive::INDEX_NB_VISITS, - 'nb_actions' => Piwik_Archive::INDEX_NB_ACTIONS, - 'max_actions' => Piwik_Archive::INDEX_MAX_ACTIONS, - 'sum_visit_length' => Piwik_Archive::INDEX_SUM_VISIT_LENGTH, - 'bounce_count' => Piwik_Archive::INDEX_BOUNCE_COUNT, - 'nb_visits_converted' => Piwik_Archive::INDEX_NB_VISITS_CONVERTED, - 'nb_conversions' => Piwik_Archive::INDEX_NB_CONVERSIONS, - 'revenue' => Piwik_Archive::INDEX_REVENUE, - 'goals' => Piwik_Archive::INDEX_GOALS, - 'sum_daily_nb_uniq_visitors' => Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS, - ); - - /** - * Metrics calculated and archived by the Actions plugin. - * - * @var array - */ - public static $actionsMetrics = array( - 'nb_pageviews', - 'nb_uniq_pageviews', - 'nb_downloads', - 'nb_uniq_downloads', - 'nb_outlinks', - 'nb_uniq_outlinks', - 'nb_searches', - 'nb_keywords', - 'nb_hits', - 'nb_hits_following_search', - ); - - const LABEL_ECOMMERCE_CART = 'ecommerceAbandonedCart'; - const LABEL_ECOMMERCE_ORDER = 'ecommerceOrder'; - - /** - * Website Piwik_Site - * - * @var Piwik_Site - */ - protected $site = null; - - /** - * Segment applied to the visits set - * @var Piwik_Segment - */ - protected $segment = false; - - /** - * Builds an Archive object or returns the same archive if previously built. - * - * @param int|string $idSite integer, or comma separated list of integer - * @param string $period 'week' 'day' etc. - * @param Piwik_Date|string $strDate 'YYYY-MM-DD' or magic keywords 'today' @see Piwik_Date::factory() - * @param bool|string $segment Segment definition - defaults to false for Backward Compatibility - * @param bool|string $_restrictSitesToLogin Used only when running as a scheduled task - * @return Piwik_Archive - */ - static public function build($idSite, $period, $strDate, $segment = false, $_restrictSitesToLogin = false ) - { - if($idSite === 'all') - { - $sites = Piwik_SitesManager_API::getInstance()->getSitesIdWithAtLeastViewAccess($_restrictSitesToLogin); - } - else - { - $sites = Piwik_Site::getIdSitesFromIdSitesString($idSite); - } - - if (!($segment instanceof Piwik_Segment)) - { - $segment = new Piwik_Segment($segment, $idSite); - } - - // idSite=1,3 or idSite=all - if( $idSite === 'all' - || is_array($idSite) - || count($sites) > 1 ) - { - $archive = new Piwik_Archive_Array_IndexedBySite($sites, $period, $strDate, $segment, $_restrictSitesToLogin); - } - // if a period date string is detected: either 'last30', 'previous10' or 'YYYY-MM-DD,YYYY-MM-DD' - elseif(is_string($strDate) && self::isMultiplePeriod($strDate, $period)) - { - $oSite = new Piwik_Site($idSite); - $archive = new Piwik_Archive_Array_IndexedByDate($oSite, $period, $strDate, $segment); - } - // case we request a single archive - else - { - $oSite = new Piwik_Site($idSite); - $oPeriod = Piwik_Archive::makePeriodFromQueryParams($oSite, $period, $strDate); - - $archive = new Piwik_Archive_Single(); - $archive->setPeriod($oPeriod); - $archive->setSite($oSite); - $archive->setSegment($segment); - } - return $archive; - } - - /** - * Creates a period instance using a Piwik_Site instance and two strings describing - * the period & date. - * - * @param Piwik_Site $site - * @param string $strPeriod The period string: day, week, month, year, range - * @param string $strDate The date or date range string. - * @return Piwik_Period - */ - static public function makePeriodFromQueryParams( $site, $strPeriod, $strDate ) - { - $tz = $site->getTimezone(); - - if($strPeriod == 'range') - { - $oPeriod = new Piwik_Period_Range('range', $strDate, $tz, Piwik_Date::factory('today', $tz)); - } - else - { - $oDate = $strDate; - if(!($strDate instanceof Piwik_Date)) - { - if($strDate == 'now' || $strDate == 'today') - { - $strDate = date('Y-m-d', Piwik_Date::factory('now', $tz)->getTimestamp()); - } - elseif($strDate == 'yesterday' || $strDate == 'yesterdaySameTime') - { - $strDate = date('Y-m-d', Piwik_Date::factory('now', $tz)->subDay(1)->getTimestamp()); - } - $oDate = Piwik_Date::factory($strDate); - } - $date = $oDate->toString(); - $oPeriod = Piwik_Period::factory($strPeriod, $oDate); - } - - return $oPeriod; - } - - abstract public function prepareArchive(); - - /** - * Returns the value of the element $name from the current archive - * The value to be returned is a numeric value and is stored in the archive_numeric_* tables - * - * @param string $name For example Referers_distinctKeywords - * @return float|int|false False if no value with the given name - */ - abstract public function getNumeric( $name ); - - /** - * Returns the value of the element $name from the current archive - * - * The value to be returned is a blob value and is stored in the archive_numeric_* tables - * - * It can return anything from strings, to serialized PHP arrays or PHP objects, etc. - * - * @param string $name For example Referers_distinctKeywords - * @return mixed False if no value with the given name - */ - abstract public function getBlob( $name ); - - /** - * - * @param $fields - * @return Piwik_DataTable - */ - abstract public function getDataTableFromNumeric( $fields ); - - /** - * This method will build a dataTable from the blob value $name in the current archive. - * - * For example $name = 'Referers_searchEngineByKeyword' will return a Piwik_DataTable containing all the keywords - * If a idSubTable is given, the method will return the subTable of $name - * - * @param string $name - * @param int $idSubTable or null if requesting the parent table - * @return Piwik_DataTable - * @throws exception If the value cannot be found - */ - abstract public function getDataTable( $name, $idSubTable = null ); - - /** - * Same as getDataTable() except that it will also load in memory - * all the subtables for the DataTable $name. - * You can then access the subtables by using the Piwik_DataTable_Manager getTable() - * - * @param string $name - * @param int|null $idSubTable null if requesting the parent table - * @return Piwik_DataTable - */ - abstract public function getDataTableExpanded($name, $idSubTable = null); - - - /** - * Helper - Loads a DataTable from the Archive. - * Optionally loads the table recursively, - * or optionally fetches a given subtable with $idSubtable - * - * @param string $name - * @param int $idSite - * @param string $period - * @param Piwik_Date $date - * @param string $segment - * @param bool $expanded - * @param null $idSubtable - * @return Piwik_DataTable|Piwik_DataTable_Array - */ - static public function getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded, $idSubtable = null ) - { - Piwik::checkUserHasViewAccess( $idSite ); - $archive = Piwik_Archive::build($idSite, $period, $date, $segment ); - if($idSubtable === false) - { - $idSubtable = null; - } - - if($expanded) - { - $dataTable = $archive->getDataTableExpanded($name, $idSubtable); - } - else - { - $dataTable = $archive->getDataTable($name, $idSubtable); - } - - $dataTable->queueFilter('ReplaceSummaryRowLabel'); - - return $dataTable; - } - - protected function formatNumericValue($value) - { - // If there is no dot, we return as is - // Note: this could be an integer bigger than 32 bits - if(strpos($value, '.') === false) - { - if($value === false) - { - return 0; - } - return (float)$value; - } - - // Round up the value with 2 decimals - // we cast the result as float because returns false when no visitors - $value = round((float)$value, 2); - return $value; - } - - public function getSegment() - { - return $this->segment; - } - - public function setSegment(Piwik_Segment $segment) - { - $this->segment = $segment; - } - - /** - * Sets the site - * - * @param Piwik_Site $site - */ - public function setSite( Piwik_Site $site ) - { - $this->site = $site; - } - - /** - * Gets the site - * - * @return Piwik_Site - */ - public function getSite() - { - return $this->site; - } - - /** - * Returns the Id site associated with this archive - * - * @return int - */ - public function getIdSite() - { - return $this->site->getId(); - } - - /** - * Returns true if Segmentation is allowed for this user - * - * @return bool - */ - static public function isSegmentationEnabled() - { - return !Piwik::isUserIsAnonymous() - || Piwik_Config::getInstance()->General['anonymous_user_enable_use_segments_API'] - ; - } - - /** - * Indicate if $dateString and $period correspond to multiple periods - * - * @static - * @param $dateString - * @param $period - * @return boolean - */ - static public function isMultiplePeriod($dateString, $period) - { - return (preg_match('/^(last|previous){1}([0-9]*)$/D', $dateString, $regs) - || Piwik_Period_Range::parseDateRange($dateString)) - && $period != 'range'; - } - - /** - * Indicate if $idSiteString corresponds to multiple sites. - * - * @param string $idSiteString - * @return bool - */ - static public function isMultipleSites( $idSiteString ) - { - return $idSiteString == 'all' || strpos($idSiteString, ',') !== false; - } + */ + public static $mappingFromNameToId = array( + 'nb_uniq_visitors' => Piwik_Archive::INDEX_NB_UNIQ_VISITORS, + 'nb_visits' => Piwik_Archive::INDEX_NB_VISITS, + 'nb_actions' => Piwik_Archive::INDEX_NB_ACTIONS, + 'max_actions' => Piwik_Archive::INDEX_MAX_ACTIONS, + 'sum_visit_length' => Piwik_Archive::INDEX_SUM_VISIT_LENGTH, + 'bounce_count' => Piwik_Archive::INDEX_BOUNCE_COUNT, + 'nb_visits_converted' => Piwik_Archive::INDEX_NB_VISITS_CONVERTED, + 'nb_conversions' => Piwik_Archive::INDEX_NB_CONVERSIONS, + 'revenue' => Piwik_Archive::INDEX_REVENUE, + 'goals' => Piwik_Archive::INDEX_GOALS, + 'sum_daily_nb_uniq_visitors' => Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS, + ); + + /** + * Metrics calculated and archived by the Actions plugin. + * + * @var array + */ + public static $actionsMetrics = array( + 'nb_pageviews', + 'nb_uniq_pageviews', + 'nb_downloads', + 'nb_uniq_downloads', + 'nb_outlinks', + 'nb_uniq_outlinks', + 'nb_searches', + 'nb_keywords', + 'nb_hits', + 'nb_hits_following_search', + ); + + const LABEL_ECOMMERCE_CART = 'ecommerceAbandonedCart'; + const LABEL_ECOMMERCE_ORDER = 'ecommerceOrder'; + + /** + * Website Piwik_Site + * + * @var Piwik_Site + */ + protected $site = null; + + /** + * Segment applied to the visits set + * @var Piwik_Segment + */ + protected $segment = false; + + /** + * Builds an Archive object or returns the same archive if previously built. + * + * @param int|string $idSite integer, or comma separated list of integer + * @param string $period 'week' 'day' etc. + * @param Piwik_Date|string $strDate 'YYYY-MM-DD' or magic keywords 'today' @see Piwik_Date::factory() + * @param bool|string $segment Segment definition - defaults to false for Backward Compatibility + * @param bool|string $_restrictSitesToLogin Used only when running as a scheduled task + * @return Piwik_Archive + */ + static public function build($idSite, $period, $strDate, $segment = false, $_restrictSitesToLogin = false) + { + if ($idSite === 'all') { + $sites = Piwik_SitesManager_API::getInstance()->getSitesIdWithAtLeastViewAccess($_restrictSitesToLogin); + } else { + $sites = Piwik_Site::getIdSitesFromIdSitesString($idSite); + } + + if (!($segment instanceof Piwik_Segment)) { + $segment = new Piwik_Segment($segment, $idSite); + } + + // idSite=1,3 or idSite=all + if ($idSite === 'all' + || is_array($idSite) + || count($sites) > 1 + ) { + $archive = new Piwik_Archive_Array_IndexedBySite($sites, $period, $strDate, $segment, $_restrictSitesToLogin); + } // if a period date string is detected: either 'last30', 'previous10' or 'YYYY-MM-DD,YYYY-MM-DD' + elseif (is_string($strDate) && self::isMultiplePeriod($strDate, $period)) { + $oSite = new Piwik_Site($idSite); + $archive = new Piwik_Archive_Array_IndexedByDate($oSite, $period, $strDate, $segment); + } // case we request a single archive + else { + $oSite = new Piwik_Site($idSite); + $oPeriod = Piwik_Archive::makePeriodFromQueryParams($oSite, $period, $strDate); + + $archive = new Piwik_Archive_Single(); + $archive->setPeriod($oPeriod); + $archive->setSite($oSite); + $archive->setSegment($segment); + } + return $archive; + } + + /** + * Creates a period instance using a Piwik_Site instance and two strings describing + * the period & date. + * + * @param Piwik_Site $site + * @param string $strPeriod The period string: day, week, month, year, range + * @param string $strDate The date or date range string. + * @return Piwik_Period + */ + static public function makePeriodFromQueryParams($site, $strPeriod, $strDate) + { + $tz = $site->getTimezone(); + + if ($strPeriod == 'range') { + $oPeriod = new Piwik_Period_Range('range', $strDate, $tz, Piwik_Date::factory('today', $tz)); + } else { + $oDate = $strDate; + if (!($strDate instanceof Piwik_Date)) { + if ($strDate == 'now' || $strDate == 'today') { + $strDate = date('Y-m-d', Piwik_Date::factory('now', $tz)->getTimestamp()); + } elseif ($strDate == 'yesterday' || $strDate == 'yesterdaySameTime') { + $strDate = date('Y-m-d', Piwik_Date::factory('now', $tz)->subDay(1)->getTimestamp()); + } + $oDate = Piwik_Date::factory($strDate); + } + $date = $oDate->toString(); + $oPeriod = Piwik_Period::factory($strPeriod, $oDate); + } + + return $oPeriod; + } + + abstract public function prepareArchive(); + + /** + * Returns the value of the element $name from the current archive + * The value to be returned is a numeric value and is stored in the archive_numeric_* tables + * + * @param string $name For example Referers_distinctKeywords + * @return float|int|false False if no value with the given name + */ + abstract public function getNumeric($name); + + /** + * Returns the value of the element $name from the current archive + * + * The value to be returned is a blob value and is stored in the archive_numeric_* tables + * + * It can return anything from strings, to serialized PHP arrays or PHP objects, etc. + * + * @param string $name For example Referers_distinctKeywords + * @return mixed False if no value with the given name + */ + abstract public function getBlob($name); + + /** + * + * @param $fields + * @return Piwik_DataTable + */ + abstract public function getDataTableFromNumeric($fields); + + /** + * This method will build a dataTable from the blob value $name in the current archive. + * + * For example $name = 'Referers_searchEngineByKeyword' will return a Piwik_DataTable containing all the keywords + * If a idSubTable is given, the method will return the subTable of $name + * + * @param string $name + * @param int $idSubTable or null if requesting the parent table + * @return Piwik_DataTable + * @throws exception If the value cannot be found + */ + abstract public function getDataTable($name, $idSubTable = null); + + /** + * Same as getDataTable() except that it will also load in memory + * all the subtables for the DataTable $name. + * You can then access the subtables by using the Piwik_DataTable_Manager getTable() + * + * @param string $name + * @param int|null $idSubTable null if requesting the parent table + * @return Piwik_DataTable + */ + abstract public function getDataTableExpanded($name, $idSubTable = null); + + + /** + * Helper - Loads a DataTable from the Archive. + * Optionally loads the table recursively, + * or optionally fetches a given subtable with $idSubtable + * + * @param string $name + * @param int $idSite + * @param string $period + * @param Piwik_Date $date + * @param string $segment + * @param bool $expanded + * @param null $idSubtable + * @return Piwik_DataTable|Piwik_DataTable_Array + */ + static public function getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded, $idSubtable = null) + { + Piwik::checkUserHasViewAccess($idSite); + $archive = Piwik_Archive::build($idSite, $period, $date, $segment); + if ($idSubtable === false) { + $idSubtable = null; + } + + if ($expanded) { + $dataTable = $archive->getDataTableExpanded($name, $idSubtable); + } else { + $dataTable = $archive->getDataTable($name, $idSubtable); + } + + $dataTable->queueFilter('ReplaceSummaryRowLabel'); + + return $dataTable; + } + + protected function formatNumericValue($value) + { + // If there is no dot, we return as is + // Note: this could be an integer bigger than 32 bits + if (strpos($value, '.') === false) { + if ($value === false) { + return 0; + } + return (float)$value; + } + + // Round up the value with 2 decimals + // we cast the result as float because returns false when no visitors + $value = round((float)$value, 2); + return $value; + } + + public function getSegment() + { + return $this->segment; + } + + public function setSegment(Piwik_Segment $segment) + { + $this->segment = $segment; + } + + /** + * Sets the site + * + * @param Piwik_Site $site + */ + public function setSite(Piwik_Site $site) + { + $this->site = $site; + } + + /** + * Gets the site + * + * @return Piwik_Site + */ + public function getSite() + { + return $this->site; + } + + /** + * Returns the Id site associated with this archive + * + * @return int + */ + public function getIdSite() + { + return $this->site->getId(); + } + + /** + * Returns true if Segmentation is allowed for this user + * + * @return bool + */ + static public function isSegmentationEnabled() + { + return !Piwik::isUserIsAnonymous() + || Piwik_Config::getInstance()->General['anonymous_user_enable_use_segments_API']; + } + + /** + * Indicate if $dateString and $period correspond to multiple periods + * + * @static + * @param $dateString + * @param $period + * @return boolean + */ + static public function isMultiplePeriod($dateString, $period) + { + return (preg_match('/^(last|previous){1}([0-9]*)$/D', $dateString, $regs) + || Piwik_Period_Range::parseDateRange($dateString)) + && $period != 'range'; + } + + /** + * Indicate if $idSiteString corresponds to multiple sites. + * + * @param string $idSiteString + * @return bool + */ + static public function isMultipleSites($idSiteString) + { + return $idSiteString == 'all' || strpos($idSiteString, ',') !== false; + } } diff --git a/core/Archive/Array.php b/core/Archive/Array.php index cd9472a245..896cc1e17c 100644 --- a/core/Archive/Array.php +++ b/core/Archive/Array.php @@ -1,152 +1,147 @@ archives as $archive) { + destroy($archive); + } + $this->archives = array(); + } + + /** + * Prepares each archive in the array + */ + public function prepareArchive() + { + foreach ($this->archives as $archive) { + $archive->prepareArchive(); + } + } + + /** + * Returns a newly created Piwik_DataTable_Array. + * + * @return Piwik_DataTable_Array + */ + protected function getNewDataTableArray() + { + $table = new Piwik_DataTable_Array(); + $table->setKeyName($this->getIndexName()); + return $table; + } + + /** + * Returns a DataTable_Array containing numeric values + * of the element $name from the archives in this Archive_Array. + * + * @param string $name Name of the mysql table field to load eg. Referers_distinctKeywords + * @return Piwik_DataTable_Array containing the requested numeric value for each Archive + */ + public function getNumeric($name) + { + $table = $this->getNewDataTableArray(); + + foreach ($this->archives as $archive) { + $numeric = $archive->getNumeric($name); + $subTable = $archive->makeDataTable($isSimple = true); + $subTable->addRowsFromArray(array($numeric)); + $table->addTable($subTable, $this->getDataTableLabelValue($archive)); + } + + return $table; + } + + /** + * Returns a DataTable_Array containing values + * of the element $name from the archives in this Archive_Array. + * + * The value to be returned are blob values (stored in the archive_numeric_* tables in the DB). * + * It can return anything from strings, to serialized PHP arrays or PHP objects, etc. + * + * @param string $name Name of the mysql table field to load eg. Referers_keywordBySearchEngine + * @return Piwik_DataTable_Array containing the requested blob values for each Archive + */ + public function getBlob($name) + { + $table = $this->getNewDataTableArray(); + + foreach ($this->archives as $archive) { + $blob = $archive->getBlob($name); + $subTable = $archive->makeNewDataTable($isSimple = true); + $subTable->addRowsFromArray(array('blob' => $blob)); + $table->addTable($subTable, $this->getDataTableLabelValue($archive)); + } + return $table; + } + + /** + * Given a BLOB field name (eg. 'Referers_searchEngineByKeyword'), it will return a Piwik_DataTable_Array + * which is an array of Piwik_DataTable, ordered by chronological order + * + * @param string $name Name of the mysql table field to load + * @param int $idSubTable optional idSubDataTable + * @return Piwik_DataTable_Array + * @throws Exception If the value cannot be found + */ + public function getDataTable($name, $idSubTable = null) + { + $table = $this->getNewDataTableArray(); + foreach ($this->archives as $archive) { + $subTable = $archive->getDataTable($name, $idSubTable); + $table->addTable($subTable, $this->getDataTableLabelValue($archive)); + } + return $table; + } - /** - * Destructor - */ - public function __destruct() - { - foreach($this->archives as $archive) - { - destroy($archive); - } - $this->archives = array(); - } - /** - * Prepares each archive in the array - */ - public function prepareArchive() - { - foreach($this->archives as $archive) - { - $archive->prepareArchive(); - } - } - - /** - * Returns a newly created Piwik_DataTable_Array. - * - * @return Piwik_DataTable_Array - */ - protected function getNewDataTableArray() - { - $table = new Piwik_DataTable_Array(); - $table->setKeyName($this->getIndexName()); - return $table; - } - - /** - * Returns a DataTable_Array containing numeric values - * of the element $name from the archives in this Archive_Array. - * - * @param string $name Name of the mysql table field to load eg. Referers_distinctKeywords - * @return Piwik_DataTable_Array containing the requested numeric value for each Archive - */ - public function getNumeric( $name ) - { - $table = $this->getNewDataTableArray(); - - foreach($this->archives as $archive) - { - $numeric = $archive->getNumeric( $name ) ; - $subTable = $archive->makeDataTable($isSimple = true); - $subTable->addRowsFromArray( array( $numeric ) ); - $table->addTable($subTable, $this->getDataTableLabelValue($archive)); - } - - return $table; - } - - /** - * Returns a DataTable_Array containing values - * of the element $name from the archives in this Archive_Array. - * - * The value to be returned are blob values (stored in the archive_numeric_* tables in the DB). * - * It can return anything from strings, to serialized PHP arrays or PHP objects, etc. - * - * @param string $name Name of the mysql table field to load eg. Referers_keywordBySearchEngine - * @return Piwik_DataTable_Array containing the requested blob values for each Archive - */ - public function getBlob( $name ) - { - $table = $this->getNewDataTableArray(); - - foreach($this->archives as $archive) - { - $blob = $archive->getBlob( $name ) ; - $subTable = $archive->makeNewDataTable($isSimple = true); - $subTable->addRowsFromArray( array('blob' => $blob)); - $table->addTable($subTable, $this->getDataTableLabelValue($archive)); - } - return $table; - } - - /** - * Given a BLOB field name (eg. 'Referers_searchEngineByKeyword'), it will return a Piwik_DataTable_Array - * which is an array of Piwik_DataTable, ordered by chronological order - * - * @param string $name Name of the mysql table field to load - * @param int $idSubTable optional idSubDataTable - * @return Piwik_DataTable_Array - * @throws Exception If the value cannot be found - */ - public function getDataTable( $name, $idSubTable = null ) - { - $table = $this->getNewDataTableArray(); - foreach($this->archives as $archive) - { - $subTable = $archive->getDataTable( $name, $idSubTable ) ; - $table->addTable($subTable, $this->getDataTableLabelValue($archive)); - } - return $table; - } - - - /** - * Same as getDataTable() except that it will also load in memory - * all the subtables for the DataTable $name. - * You can then access the subtables by using the Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); - * - * @param string $name Name of the mysql table field to load - * @param int $idSubTable optional idSubDataTable - * @return Piwik_DataTable_Array - */ - public function getDataTableExpanded($name, $idSubTable = null) - { - $table = $this->getNewDataTableArray(); - foreach($this->archives as $archive) - { - $subTable = $archive->getDataTableExpanded( $name, $idSubTable ) ; - $table->addTable($subTable, $this->getDataTableLabelValue($archive)); - } - return $table; - } + /** + * Same as getDataTable() except that it will also load in memory + * all the subtables for the DataTable $name. + * You can then access the subtables by using the Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); + * + * @param string $name Name of the mysql table field to load + * @param int $idSubTable optional idSubDataTable + * @return Piwik_DataTable_Array + */ + public function getDataTableExpanded($name, $idSubTable = null) + { + $table = $this->getNewDataTableArray(); + foreach ($this->archives as $archive) { + $subTable = $archive->getDataTableExpanded($name, $idSubTable); + $table->addTable($subTable, $this->getDataTableLabelValue($archive)); + } + return $table; + } } diff --git a/core/Archive/Array/IndexedByDate.php b/core/Archive/Array/IndexedByDate.php index 91bbab2a78..9a59712d4a 100644 --- a/core/Archive/Array/IndexedByDate.php +++ b/core/Archive/Array/IndexedByDate.php @@ -1,10 +1,10 @@ getTimezone()); - foreach($rangePeriod->getSubperiods() as $subPeriod) - { - $startDate = $subPeriod->getDateStart(); - $archive = Piwik_Archive::build($oSite->getId(), $strPeriod, $startDate, $segment->getString() ); - $archive->setSegment($segment); - $this->archives[] = $archive; - } - $this->setSite($oSite); - } + /** + * Builds an array of Piwik_Archive of a given date range + * + * @param Piwik_Site $oSite + * @param string $strPeriod eg. 'day' 'week' etc. + * @param string $strDate A date range, eg. 'last10', 'previous5' or 'YYYY-MM-DD,YYYY-MM-DD' + * @param Piwik_Segment $segment + */ + public function __construct(Piwik_Site $oSite, $strPeriod, $strDate, Piwik_Segment $segment) + { + $rangePeriod = new Piwik_Period_Range($strPeriod, $strDate, $oSite->getTimezone()); + foreach ($rangePeriod->getSubperiods() as $subPeriod) { + $startDate = $subPeriod->getDateStart(); + $archive = Piwik_Archive::build($oSite->getId(), $strPeriod, $startDate, $segment->getString()); + $archive->setSegment($segment); + $this->archives[] = $archive; + } + $this->setSite($oSite); + } - /** - * @return string - */ - protected function getIndexName() - { - return 'date'; - } + /** + * @return string + */ + protected function getIndexName() + { + return 'date'; + } - /** - * @param Piwik_Archive $archive - * @return mixed - */ - protected function getDataTableLabelValue( $archive ) - { - return $archive->getPrettyDate(); - } - - /** - * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Array - * which is an array of Piwik_DataTable_Simple, ordered by chronological order - * - * @param array|string $fields array( fieldName1, fieldName2, ...) Names of the mysql table fields to load - * @return Piwik_DataTable_Array - */ - public function getDataTableFromNumeric( $fields ) - { - $inNames = Piwik_Common::getSqlStringFieldsArray($fields); - - // we select in different shots - // one per distinct table (case we select last 300 days, maybe we will select from 10 different tables) - $queries = array(); - foreach($this->archives as $archive) - { - $archive->setRequestedReport( is_string($fields) ? $fields : current($fields) ); - $archive->prepareArchive(); - if(!$archive->isThereSomeVisits) - { - continue; - } - - $table = $archive->archiveProcessing->getTableArchiveNumericName(); + /** + * @param Piwik_Archive $archive + * @return mixed + */ + protected function getDataTableLabelValue($archive) + { + return $archive->getPrettyDate(); + } - // for every query store IDs - $queries[$table][] = $archive->getIdArchive(); - } - // we select the requested value - $db = Zend_Registry::get('db'); - - // date => array( 'field1' =>X, 'field2'=>Y) - // date2 => array( 'field1' =>X2, 'field2'=>Y2) - - $arrayValues = array(); - foreach($queries as $table => $aIds) - { - $inIds = implode(', ', array_filter($aIds)); - if(empty($inIds)) - { - // Probable timezone configuration error, i.e., mismatch between PHP and MySQL server. - continue; - } + /** + * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Array + * which is an array of Piwik_DataTable_Simple, ordered by chronological order + * + * @param array|string $fields array( fieldName1, fieldName2, ...) Names of the mysql table fields to load + * @return Piwik_DataTable_Array + */ + public function getDataTableFromNumeric($fields) + { + $inNames = Piwik_Common::getSqlStringFieldsArray($fields); - $sql = "SELECT value, name, date1 as startDate + // we select in different shots + // one per distinct table (case we select last 300 days, maybe we will select from 10 different tables) + $queries = array(); + foreach ($this->archives as $archive) { + $archive->setRequestedReport(is_string($fields) ? $fields : current($fields)); + $archive->prepareArchive(); + if (!$archive->isThereSomeVisits) { + continue; + } + + $table = $archive->archiveProcessing->getTableArchiveNumericName(); + + // for every query store IDs + $queries[$table][] = $archive->getIdArchive(); + } + // we select the requested value + $db = Zend_Registry::get('db'); + + // date => array( 'field1' =>X, 'field2'=>Y) + // date2 => array( 'field1' =>X2, 'field2'=>Y2) + + $arrayValues = array(); + foreach ($queries as $table => $aIds) { + $inIds = implode(', ', array_filter($aIds)); + if (empty($inIds)) { + // Probable timezone configuration error, i.e., mismatch between PHP and MySQL server. + continue; + } + + $sql = "SELECT value, name, date1 as startDate FROM $table WHERE idarchive IN ( $inIds ) AND name IN ( $inNames ) ORDER BY date1, name"; - $values = $db->fetchAll($sql, $fields); - foreach($values as $value) - { - $timestamp = Piwik_Date::factory($value['startDate'])->getTimestamp(); - $arrayValues[$timestamp][$value['name']] = $this->formatNumericValue($value['value']); - } - } - - $contentArray = array(); - // we add empty tables so that every requested date has an entry, even if there is nothing - // example: - $archiveByTimestamp = array(); - foreach($this->archives as $archive) - { - $timestamp = $archive->getTimestampStartDate(); - $archiveByTimestamp[$timestamp] = $archive; - $contentArray[$timestamp]['table'] = $archive->makeDataTable($simple = true); - $contentArray[$timestamp]['prettyDate'] = $archive->getPrettyDate(); - } + $values = $db->fetchAll($sql, $fields); + foreach ($values as $value) { + $timestamp = Piwik_Date::factory($value['startDate'])->getTimestamp(); + $arrayValues[$timestamp][$value['name']] = $this->formatNumericValue($value['value']); + } + } + + $contentArray = array(); + // we add empty tables so that every requested date has an entry, even if there is nothing + // example: + $archiveByTimestamp = array(); + foreach ($this->archives as $archive) { + $timestamp = $archive->getTimestampStartDate(); + $archiveByTimestamp[$timestamp] = $archive; + $contentArray[$timestamp]['table'] = $archive->makeDataTable($simple = true); + $contentArray[$timestamp]['prettyDate'] = $archive->getPrettyDate(); + } - foreach($arrayValues as $timestamp => $aNameValues) - { - // undefined in some edge/unknown cases see http://dev.piwik.org/trac/ticket/2578 - if(isset($contentArray[$timestamp]['table'])) - { - $contentArray[$timestamp]['table']->addRowsFromArray($aNameValues); - } - } + foreach ($arrayValues as $timestamp => $aNameValues) { + // undefined in some edge/unknown cases see http://dev.piwik.org/trac/ticket/2578 + if (isset($contentArray[$timestamp]['table'])) { + $contentArray[$timestamp]['table']->addRowsFromArray($aNameValues); + } + } - $tableArray = $this->getNewDataTableArray(); - foreach($contentArray as $timestamp => $aData) - { - $tableArray->addTable($aData['table'], $aData['prettyDate']); - } - return $tableArray; - } + $tableArray = $this->getNewDataTableArray(); + foreach ($contentArray as $timestamp => $aData) { + $tableArray->addTable($aData['table'], $aData['prettyDate']); + } + return $tableArray; + } } diff --git a/core/Archive/Array/IndexedBySite.php b/core/Archive/Array/IndexedBySite.php index c6fee07bcc..1ac9ff682a 100644 --- a/core/Archive/Array/IndexedBySite.php +++ b/core/Archive/Array/IndexedBySite.php @@ -1,10 +1,10 @@ archives[$idSite] = $archive; - } - ksort( $this->archives ); - } - - /** - * @return string - */ - protected function getIndexName() - { - return 'idSite'; - } - - /** - * @param Piwik_Archive $archive - * @return mixed - */ - protected function getDataTableLabelValue( $archive ) - { - return $archive->getIdSite(); - } - - /** - * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Array - * ordered by idsite - * - * @param array|string $fields array( fieldName1, fieldName2, ...) Names of the mysql table fields to load - * @return Piwik_DataTable_Array - */ - public function getDataTableFromNumeric( $fields ) - { - $tableArray = $this->getNewDataTableArray(); - if ($this->getFirstArchive() instanceof Piwik_Archive_Single) - { - $values = $this->getValues($fields); - foreach($this->archives as $idSite => $archive) - { - $table = $archive->makeDataTable($isSimple = true); - if (array_key_exists($idSite, $values)) - { - $table->addRowsFromArray($values[$idSite]); - } - $tableArray->addTable($table, $idSite); - } - } - elseif ($this->getFirstArchive() instanceof Piwik_Archive_Array) - { - foreach($this->archives as $idSite => $archive) - { - $tableArray->addTable($archive->getDataTableFromNumeric($fields), $idSite); - } - } - - return $tableArray; - } - - /** - * Returns the values of the requested fields - * - * @param array $fields - * @return array - */ - private function getValues($fields) - { - // Creating the default array, to ensure consistent order - $defaultValues = array(); - foreach($fields as $field) { $defaultValues[$field] = null; } - - $arrayValues = array(); - foreach($this->loadValuesFromDB($fields) as $value) - { - if(!isset($arrayValues[$value['idsite']])) - { - $arrayValues[$value['idsite']] = $defaultValues; - } - $arrayValues[$value['idsite']][$value['name']] = $this->formatNumericValue( $value['value'] ); - } - return $arrayValues; - } - - /** - * @param $fields - * @return array|array (one row in the array per row fetched in the DB) - */ - private function loadValuesFromDB($fields) - { - $requestedMetrics = is_string($fields) ? array($fields) : $fields; - $inNames = Piwik_Common::getSqlStringFieldsArray($fields); - - // get the archive ids - if (!$this->getFirstArchive()->isArchivingDisabled()) - { - $archiveIds = $this->getArchiveIdsAfterLaunching($requestedMetrics); - } - else - { - $archiveIds = $this->getArchiveIdsWithoutLaunching($requestedMetrics); - } - - $archiveIds = implode(', ', array_filter($archiveIds)); - - // if no archive ids are found, avoid executing any SQL queries - if(empty($archiveIds)) - { - return array(); - } - - // select archive data - $sql = "SELECT value, name, idarchive, idsite + /** + * Used to cache the name of the table that holds the data this archive. + * + * This will only be used if the archives held by this instance are instances of + * Piwik_Archive_Single. + */ + private $tableName = null; + + /** + * @param array $sites array of siteIds + * @param string $strPeriod eg. 'day' 'week' etc. + * @param string $strDate A date range, eg. 'last10', 'previous5' or 'YYYY-MM-DD,YYYY-MM-DD' + * @param Piwik_Segment $segment + * @param string $_restrictSitesToLogin + */ + function __construct($sites, $strPeriod, $strDate, Piwik_Segment $segment, $_restrictSitesToLogin) + { + foreach ($sites as $idSite) { + $archive = Piwik_Archive::build($idSite, $strPeriod, $strDate, $segment, $_restrictSitesToLogin); + $this->archives[$idSite] = $archive; + } + ksort($this->archives); + } + + /** + * @return string + */ + protected function getIndexName() + { + return 'idSite'; + } + + /** + * @param Piwik_Archive $archive + * @return mixed + */ + protected function getDataTableLabelValue($archive) + { + return $archive->getIdSite(); + } + + /** + * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Array + * ordered by idsite + * + * @param array|string $fields array( fieldName1, fieldName2, ...) Names of the mysql table fields to load + * @return Piwik_DataTable_Array + */ + public function getDataTableFromNumeric($fields) + { + $tableArray = $this->getNewDataTableArray(); + if ($this->getFirstArchive() instanceof Piwik_Archive_Single) { + $values = $this->getValues($fields); + foreach ($this->archives as $idSite => $archive) { + $table = $archive->makeDataTable($isSimple = true); + if (array_key_exists($idSite, $values)) { + $table->addRowsFromArray($values[$idSite]); + } + $tableArray->addTable($table, $idSite); + } + } elseif ($this->getFirstArchive() instanceof Piwik_Archive_Array) { + foreach ($this->archives as $idSite => $archive) { + $tableArray->addTable($archive->getDataTableFromNumeric($fields), $idSite); + } + } + + return $tableArray; + } + + /** + * Returns the values of the requested fields + * + * @param array $fields + * @return array + */ + private function getValues($fields) + { + // Creating the default array, to ensure consistent order + $defaultValues = array(); + foreach ($fields as $field) { + $defaultValues[$field] = null; + } + + $arrayValues = array(); + foreach ($this->loadValuesFromDB($fields) as $value) { + if (!isset($arrayValues[$value['idsite']])) { + $arrayValues[$value['idsite']] = $defaultValues; + } + $arrayValues[$value['idsite']][$value['name']] = $this->formatNumericValue($value['value']); + } + return $arrayValues; + } + + /** + * @param $fields + * @return array|array (one row in the array per row fetched in the DB) + */ + private function loadValuesFromDB($fields) + { + $requestedMetrics = is_string($fields) ? array($fields) : $fields; + $inNames = Piwik_Common::getSqlStringFieldsArray($fields); + + // get the archive ids + if (!$this->getFirstArchive()->isArchivingDisabled()) { + $archiveIds = $this->getArchiveIdsAfterLaunching($requestedMetrics); + } else { + $archiveIds = $this->getArchiveIdsWithoutLaunching($requestedMetrics); + } + + $archiveIds = implode(', ', array_filter($archiveIds)); + + // if no archive ids are found, avoid executing any SQL queries + if (empty($archiveIds)) { + return array(); + } + + // select archive data + $sql = "SELECT value, name, idarchive, idsite FROM {$this->getNumericTableName()} WHERE idarchive IN ( $archiveIds ) AND name IN ( $inNames )"; - - return Piwik_FetchAll($sql, $fields); - } - - /** - * Returns the first archive in the list - * - * @return Piwik_Archive - */ - private function getFirstArchive() - { - return reset($this->archives); - } - - /** - * Gets the archive id of every Single archive this archive holds. This method - * will launch the archiving process if appropriate. - * - * @param array $metrics The requested archive metrics. - * @throws Exception - * @return array - */ - private function getArchiveIdsAfterLaunching( $metrics ) - { - // collect the individual report names for the requested metrics - $reports = array(); - foreach($metrics as $metric) - { - $report = Piwik_Archive_Single::getRequestedReportFor($metric); - $reports[$report] = $metric; - } - - // process archives for each individual report - $archiveIds = array(); - foreach($reports as $report => $metric) - { - // prepare archives (this will launch archiving when appropriate) - foreach($this->archives as $archive) - { - // NOTE: Piwik_Archive_Single expects a metric here, not a report - $archive->setRequestedReport( $metric ); - $archive->prepareArchive(); - } - - // collect archive ids for archives that have visits - foreach($this->archives as $archive) - { - if( !$archive->isThereSomeVisits ) - { - continue; - } - - $archiveIds[] = $archive->getIdArchive(); - - if( $this->getNumericTableName() != $archive->archiveProcessing->getTableArchiveNumericName()) - { - throw new Exception("Piwik_Archive_Array_IndexedBySite::getDataTableFromNumeric() algorithm won't work if data is stored in different tables"); - } - } - } - - return $archiveIds; - } - - /** - * Gets the archive id of every Single archive this archive holds. This method - * will not launch the archiving process. - * - * @param array $metrics The requested archive metrics. - * @return array - */ - private function getArchiveIdsWithoutLaunching( $metrics ) - { - $firstArchive = $this->getFirstArchive(); - $segment = $firstArchive->getSegment(); - $period = $firstArchive->getPeriod(); - - // the flags used to tell how the archiving process for a specific archive was completed, - // if it was completed - $doneFlags = array(); - foreach ($metrics as $metric) - { - $done = Piwik_ArchiveProcessing::getDoneStringFlagFor($segment, $period, $metric); - $donePlugins = Piwik_ArchiveProcessing::getDoneStringFlagFor($segment, $period, $metric, true); - - $doneFlags[$done] = $done; - $doneFlags[$donePlugins] = $donePlugins; - } - - $allDoneFlags = "'".implode("','", $doneFlags)."'"; - - // create the SQL to query every archive ID - $nameCondition = "(name IN ($allDoneFlags)) AND - (value = '".Piwik_ArchiveProcessing::DONE_OK."' OR - value = '".Piwik_ArchiveProcessing::DONE_OK_TEMPORARY."')"; - - $sql = "SELECT idsite, + + return Piwik_FetchAll($sql, $fields); + } + + /** + * Returns the first archive in the list + * + * @return Piwik_Archive + */ + private function getFirstArchive() + { + return reset($this->archives); + } + + /** + * Gets the archive id of every Single archive this archive holds. This method + * will launch the archiving process if appropriate. + * + * @param array $metrics The requested archive metrics. + * @throws Exception + * @return array + */ + private function getArchiveIdsAfterLaunching($metrics) + { + // collect the individual report names for the requested metrics + $reports = array(); + foreach ($metrics as $metric) { + $report = Piwik_Archive_Single::getRequestedReportFor($metric); + $reports[$report] = $metric; + } + + // process archives for each individual report + $archiveIds = array(); + foreach ($reports as $report => $metric) { + // prepare archives (this will launch archiving when appropriate) + foreach ($this->archives as $archive) { + // NOTE: Piwik_Archive_Single expects a metric here, not a report + $archive->setRequestedReport($metric); + $archive->prepareArchive(); + } + + // collect archive ids for archives that have visits + foreach ($this->archives as $archive) { + if (!$archive->isThereSomeVisits) { + continue; + } + + $archiveIds[] = $archive->getIdArchive(); + + if ($this->getNumericTableName() != $archive->archiveProcessing->getTableArchiveNumericName()) { + throw new Exception("Piwik_Archive_Array_IndexedBySite::getDataTableFromNumeric() algorithm won't work if data is stored in different tables"); + } + } + } + + return $archiveIds; + } + + /** + * Gets the archive id of every Single archive this archive holds. This method + * will not launch the archiving process. + * + * @param array $metrics The requested archive metrics. + * @return array + */ + private function getArchiveIdsWithoutLaunching($metrics) + { + $firstArchive = $this->getFirstArchive(); + $segment = $firstArchive->getSegment(); + $period = $firstArchive->getPeriod(); + + // the flags used to tell how the archiving process for a specific archive was completed, + // if it was completed + $doneFlags = array(); + foreach ($metrics as $metric) { + $done = Piwik_ArchiveProcessing::getDoneStringFlagFor($segment, $period, $metric); + $donePlugins = Piwik_ArchiveProcessing::getDoneStringFlagFor($segment, $period, $metric, true); + + $doneFlags[$done] = $done; + $doneFlags[$donePlugins] = $donePlugins; + } + + $allDoneFlags = "'" . implode("','", $doneFlags) . "'"; + + // create the SQL to query every archive ID + $nameCondition = "(name IN ($allDoneFlags)) AND + (value = '" . Piwik_ArchiveProcessing::DONE_OK . "' OR + value = '" . Piwik_ArchiveProcessing::DONE_OK_TEMPORARY . "')"; + + $sql = "SELECT idsite, MAX(idarchive) AS idarchive - FROM ".$this->getNumericTableName()." + FROM " . $this->getNumericTableName() . " WHERE date1 = ? AND date2 = ? AND period = ? AND $nameCondition - AND idsite IN (".implode(',', array_keys($this->archives)).") + AND idsite IN (" . implode(',', array_keys($this->archives)) . ") GROUP BY idsite"; - - $bind = array($period->getDateStart()->toString('Y-m-d'), - $period->getDateEnd()->toString('Y-m-d'), - $period->getId()); - - // execute the query and process the results. - $archiveIds = array(); - foreach (Piwik_FetchAll($sql, $bind) as $row) - { - $archiveIds[] = $row['idarchive']; - } - - return $archiveIds; - } - - /** - * Gets the name of the database table that holds the numeric archive data for - * this archive. - * - * @return string - */ - private function getNumericTableName() - { - if (is_null($this->tableName)) - { - $table = Piwik_ArchiveProcessing::makeNumericArchiveTable($this->getFirstArchive()->getPeriod()); - $this->tableName = $table->getTableName(); - } - - return $this->tableName; - } + + $bind = array($period->getDateStart()->toString('Y-m-d'), + $period->getDateEnd()->toString('Y-m-d'), + $period->getId()); + + // execute the query and process the results. + $archiveIds = array(); + foreach (Piwik_FetchAll($sql, $bind) as $row) { + $archiveIds[] = $row['idarchive']; + } + + return $archiveIds; + } + + /** + * Gets the name of the database table that holds the numeric archive data for + * this archive. + * + * @return string + */ + private function getNumericTableName() + { + if (is_null($this->tableName)) { + $table = Piwik_ArchiveProcessing::makeNumericArchiveTable($this->getFirstArchive()->getPeriod()); + $this->tableName = $table->getTableName(); + } + + return $this->tableName; + } } diff --git a/core/Archive/Single.php b/core/Archive/Single.php index adb73095e5..2f4d5e283e 100644 --- a/core/Archive/Single.php +++ b/core/Archive/Single.php @@ -1,668 +1,631 @@ blobCached as $name => $blob) - { - $this->freeBlob($name); - } - $this->blobCached = array(); - } - - public function __destruct() - { - $this->clearCache(); - } - - /** - * Returns the blob cache. For testing. - * - * @return array - */ - public function getBlobCache() - { - return $this->blobCached; - } - - /** - * Returns the pretty date of this Archive, eg. 'Thursday 20th March 2008' - * - * @return string - */ - public function getPrettyDate() - { - return $this->period->getPrettyString(); - } - - /** - * Returns the idarchive of this Archive used to index this archive in the DB - * - * @throws Exception - * @return int - */ - public function getIdArchive() - { - if(is_null($this->idArchive)) - { - throw new Exception("idArchive is null"); - } - return $this->idArchive; - } - - /** - * Set the period - * - * @param Piwik_Period $period - */ - public function setPeriod( Piwik_Period $period ) - { - $this->period = $period; - } - - public function getPeriod() - { - return $this->period; - } - - /** - * Returns the timestamp of the first date in the period for this Archive. - * This is used to sort archives by date when working on a Archive_Array - * - * @return int Unix timestamp - */ - public function getTimestampStartDate() - { - if(!is_null($this->archiveProcessing)) - { - $timestamp = $this->archiveProcessing->getTimestampStartDate(); - if(!empty($timestamp)) - { - return $timestamp; - } - } - return $this->period->getDateStart()->getTimestamp(); - } - - /** - * Prepares the archive. Gets the idarchive from the ArchiveProcessing. - * - * This will possibly launch the archiving process if the archive was not available. - * @return bool - */ - public function prepareArchive() - { - $archiveJustProcessed = false; - - - $periodString = $this->period->getLabel(); - $plugin = Piwik_ArchiveProcessing::getPluginBeingProcessed($this->getRequestedReport()); - - $cacheKey = 'all'; - if($periodString == 'range') - { - $cacheKey = $plugin; - } - if(!isset($this->alreadyChecked[$cacheKey])) - { - $this->isThereSomeVisits = false; - $this->alreadyChecked[$cacheKey] = true; - $dayString = $this->period->getPrettyString(); - $logMessage = sprintf("%s (%s), plugin %s", $periodString, $dayString, $plugin); - // if the END of the period is BEFORE the website creation date - // we already know there are no stats for this period - // we add one day to make sure we don't miss the day of the website creation - if( $this->period->getDateEnd()->addDay(2)->isEarlier( $this->site->getCreationDate() ) ) - { - Piwik::log(sprintf("Archive %s skipped, archive is before the website was created.", $logMessage)); - return; - } - - // if the starting date is in the future we know there is no visit - if( $this->period->getDateStart()->subDay(2)->isLater( Piwik_Date::today() ) ) - { - Piwik::log(sprintf("Archive %s skipped, archive is after today.", $logMessage)); - return; - } - - // we make sure the archive is available for the given date - $periodLabel = $this->period->getLabel(); - $this->archiveProcessing = Piwik_ArchiveProcessing::factory($periodLabel); - $this->archiveProcessing->setSite($this->site); - $this->archiveProcessing->setPeriod($this->period); - $this->archiveProcessing->setSegment($this->segment); - - $this->archiveProcessing->init(); - - $this->archiveProcessing->setRequestedReport( $this->getRequestedReport() ); - - $archivingDisabledArchiveNotProcessed = false; - $idArchive = $this->archiveProcessing->loadArchive(); - if(empty($idArchive)) - { - if($this->archiveProcessing->isArchivingDisabled()) - { - $archivingDisabledArchiveNotProcessed = true; - $logMessage = sprintf("Archiving disabled, for %s", $logMessage); - } - else - { - Piwik::log(sprintf("Processing %s, not archived yet...", $logMessage)); - $archiveJustProcessed = true; - - // Process the reports - $this->archiveProcessing->launchArchiving(); - - $idArchive = $this->archiveProcessing->getIdArchive(); - $logMessage = sprintf("Processed %d, for %s", $idArchive, $logMessage); - } - } - else - { - $logMessage = sprintf("Already processed, fetching idArchive = %d (idSite=%d), for %s", $idArchive, $this->site->getId(), $logMessage); - } - Piwik::log(sprintf("%s, Visits = %d", $logMessage, $this->archiveProcessing->getNumberOfVisits())); - $this->isThereSomeVisits = !$archivingDisabledArchiveNotProcessed - && $this->archiveProcessing->isThereSomeVisits(); - $this->idArchive = $idArchive; - } - return $archiveJustProcessed; - } - - /** - * Returns a value from the current archive with the name = $name - * Method used by getNumeric or getBlob - * - * @param string $name - * @param string $typeValue numeric|blob - * @param string|bool $archivedDate Value to store date of archive info in. If false, not stored. - * @return mixed|bool false if no result - */ - protected function get( $name, $typeValue = 'numeric', &$archivedDate = false ) - { - $this->setRequestedReport($name); - $this->prepareArchive(); - - // values previously "get" and now cached - if($typeValue == 'numeric' - && $this->cacheEnabledForNumeric - && isset($this->numericCached[$name]) - ) - { - return $this->numericCached[$name]; - } - - // During archiving we prefetch the blobs recursively - // and we get them faster from memory after - if($typeValue == 'blob' - && isset($this->blobCached[$name])) - { - return $this->blobCached[$name]; - } - - if($name == 'idarchive') - { - return $this->idArchive; - } - - if(!$this->isThereSomeVisits) - { - return false; - } - - // select the table to use depending on the type of the data requested - switch($typeValue) - { - case 'blob': - $table = $this->archiveProcessing->getTableArchiveBlobName(); - break; - - case 'numeric': - default: - $table = $this->archiveProcessing->getTableArchiveNumericName(); - break; - } - - $db = Zend_Registry::get('db'); - $row = $db->fetchRow("SELECT value, ts_archived + /** + * The Piwik_ArchiveProcessing object used to check that the archive is available + * and launch the processing if the archive was not yet processed + * + * @var Piwik_ArchiveProcessing + */ + public $archiveProcessing = null; + + /** + * @var bool Set to true if the archive has at least 1 visit + */ + public $isThereSomeVisits = null; + + /** + * Period of this Archive + * + * @var Piwik_Period + */ + protected $period = null; + + /** + * Set to true will activate numeric value caching for this archive. + * + * @var bool + */ + protected $cacheEnabledForNumeric = true; + + /** + * Array of cached numeric values, used to make requests faster + * when requesting the same value again and again + * + * @var array of numeric + */ + protected $numericCached = array(); + + /** + * Array of cached blob, used to make requests faster when requesting the same blob again and again + * + * @var array of mixed + */ + protected $blobCached = array(); + + /** + * idarchive of this Archive in the database + * + * @var int + */ + protected $idArchive = null; + + /** + * name of requested report + * + * @var string + */ + protected $requestedReport = null; + + /** + * Flag set to true once the archive has been checked (when we make sure it is archived) + * + * @var bool + */ + protected $alreadyChecked = array(); + + protected function clearCache() + { + foreach ($this->blobCached as $name => $blob) { + $this->freeBlob($name); + } + $this->blobCached = array(); + } + + public function __destruct() + { + $this->clearCache(); + } + + /** + * Returns the blob cache. For testing. + * + * @return array + */ + public function getBlobCache() + { + return $this->blobCached; + } + + /** + * Returns the pretty date of this Archive, eg. 'Thursday 20th March 2008' + * + * @return string + */ + public function getPrettyDate() + { + return $this->period->getPrettyString(); + } + + /** + * Returns the idarchive of this Archive used to index this archive in the DB + * + * @throws Exception + * @return int + */ + public function getIdArchive() + { + if (is_null($this->idArchive)) { + throw new Exception("idArchive is null"); + } + return $this->idArchive; + } + + /** + * Set the period + * + * @param Piwik_Period $period + */ + public function setPeriod(Piwik_Period $period) + { + $this->period = $period; + } + + public function getPeriod() + { + return $this->period; + } + + /** + * Returns the timestamp of the first date in the period for this Archive. + * This is used to sort archives by date when working on a Archive_Array + * + * @return int Unix timestamp + */ + public function getTimestampStartDate() + { + if (!is_null($this->archiveProcessing)) { + $timestamp = $this->archiveProcessing->getTimestampStartDate(); + if (!empty($timestamp)) { + return $timestamp; + } + } + return $this->period->getDateStart()->getTimestamp(); + } + + /** + * Prepares the archive. Gets the idarchive from the ArchiveProcessing. + * + * This will possibly launch the archiving process if the archive was not available. + * @return bool + */ + public function prepareArchive() + { + $archiveJustProcessed = false; + + + $periodString = $this->period->getLabel(); + $plugin = Piwik_ArchiveProcessing::getPluginBeingProcessed($this->getRequestedReport()); + + $cacheKey = 'all'; + if ($periodString == 'range') { + $cacheKey = $plugin; + } + if (!isset($this->alreadyChecked[$cacheKey])) { + $this->isThereSomeVisits = false; + $this->alreadyChecked[$cacheKey] = true; + $dayString = $this->period->getPrettyString(); + $logMessage = sprintf("%s (%s), plugin %s", $periodString, $dayString, $plugin); + // if the END of the period is BEFORE the website creation date + // we already know there are no stats for this period + // we add one day to make sure we don't miss the day of the website creation + if ($this->period->getDateEnd()->addDay(2)->isEarlier($this->site->getCreationDate())) { + Piwik::log(sprintf("Archive %s skipped, archive is before the website was created.", $logMessage)); + return; + } + + // if the starting date is in the future we know there is no visit + if ($this->period->getDateStart()->subDay(2)->isLater(Piwik_Date::today())) { + Piwik::log(sprintf("Archive %s skipped, archive is after today.", $logMessage)); + return; + } + + // we make sure the archive is available for the given date + $periodLabel = $this->period->getLabel(); + $this->archiveProcessing = Piwik_ArchiveProcessing::factory($periodLabel); + $this->archiveProcessing->setSite($this->site); + $this->archiveProcessing->setPeriod($this->period); + $this->archiveProcessing->setSegment($this->segment); + + $this->archiveProcessing->init(); + + $this->archiveProcessing->setRequestedReport($this->getRequestedReport()); + + $archivingDisabledArchiveNotProcessed = false; + $idArchive = $this->archiveProcessing->loadArchive(); + if (empty($idArchive)) { + if ($this->archiveProcessing->isArchivingDisabled()) { + $archivingDisabledArchiveNotProcessed = true; + $logMessage = sprintf("Archiving disabled, for %s", $logMessage); + } else { + Piwik::log(sprintf("Processing %s, not archived yet...", $logMessage)); + $archiveJustProcessed = true; + + // Process the reports + $this->archiveProcessing->launchArchiving(); + + $idArchive = $this->archiveProcessing->getIdArchive(); + $logMessage = sprintf("Processed %d, for %s", $idArchive, $logMessage); + } + } else { + $logMessage = sprintf("Already processed, fetching idArchive = %d (idSite=%d), for %s", $idArchive, $this->site->getId(), $logMessage); + } + Piwik::log(sprintf("%s, Visits = %d", $logMessage, $this->archiveProcessing->getNumberOfVisits())); + $this->isThereSomeVisits = !$archivingDisabledArchiveNotProcessed + && $this->archiveProcessing->isThereSomeVisits(); + $this->idArchive = $idArchive; + } + return $archiveJustProcessed; + } + + /** + * Returns a value from the current archive with the name = $name + * Method used by getNumeric or getBlob + * + * @param string $name + * @param string $typeValue numeric|blob + * @param string|bool $archivedDate Value to store date of archive info in. If false, not stored. + * @return mixed|bool false if no result + */ + protected function get($name, $typeValue = 'numeric', &$archivedDate = false) + { + $this->setRequestedReport($name); + $this->prepareArchive(); + + // values previously "get" and now cached + if ($typeValue == 'numeric' + && $this->cacheEnabledForNumeric + && isset($this->numericCached[$name]) + ) { + return $this->numericCached[$name]; + } + + // During archiving we prefetch the blobs recursively + // and we get them faster from memory after + if ($typeValue == 'blob' + && isset($this->blobCached[$name]) + ) { + return $this->blobCached[$name]; + } + + if ($name == 'idarchive') { + return $this->idArchive; + } + + if (!$this->isThereSomeVisits) { + return false; + } + + // select the table to use depending on the type of the data requested + switch ($typeValue) { + case 'blob': + $table = $this->archiveProcessing->getTableArchiveBlobName(); + break; + + case 'numeric': + default: + $table = $this->archiveProcessing->getTableArchiveNumericName(); + break; + } + + $db = Zend_Registry::get('db'); + $row = $db->fetchRow("SELECT value, ts_archived FROM $table WHERE idarchive = ? AND name = ?", - array( $this->idArchive , $name) - ); - - $value = $tsArchived = false; - if (is_array($row)) - { - $value = $row['value']; - $tsArchived = $row['ts_archived']; - } - - if ($archivedDate !== false) - { - $archivedDate = $tsArchived; - } - - if($value === false) - { - if($typeValue == 'numeric' - && $this->cacheEnabledForNumeric) - { - $this->numericCached[$name] = false; - } - return $value; - } - - // uncompress when selecting from the BLOB table - if($typeValue == 'blob' && $db->hasBlobDataType()) - { - $value = $this->uncompress($value); - } - - if($typeValue == 'numeric' - && $this->cacheEnabledForNumeric) - { - $this->numericCached[$name] = $value; - } - return $value; - } - - - /** - * This method loads in memory all the subtables for the main table called $name. - * You have to give it the parent table $dataTableToLoad so we can lookup the sub tables ids to load. - * - * If $addMetadataSubtableId set to true, it will add for each row a 'metadata' called 'databaseSubtableId' - * containing the child ID of the subtable associated to this row. - * - * @param string $name - * @param Piwik_DataTable $dataTableToLoad - * @param bool $addMetadataSubtableId - */ - public function loadSubDataTables($name, Piwik_DataTable $dataTableToLoad, $addMetadataSubtableId = false) - { - // we have to recursively load all the subtables associated to this table's rows - // and update the subtableID so that it matches the newly instanciated table - foreach($dataTableToLoad->getRows() as $row) - { - $subTableID = $row->getIdSubDataTable(); - - if($subTableID !== null) - { - $subDataTableLoaded = $this->getDataTable($name, $subTableID); - - $row->setSubtable( $subDataTableLoaded ); - - $this->loadSubDataTables($name, $subDataTableLoaded, $addMetadataSubtableId); - - // we edit the subtable ID so that it matches the newly table created in memory - // NB: we dont overwrite the datatableid in the case we are displaying the table expanded. - if($addMetadataSubtableId) - { - // this will be written back to the column 'idsubdatatable' just before rendering, see Renderer/Php.php - $row->addMetadata('idsubdatatable_in_db', $row->getIdSubDataTable()); - } - } - } - } - - - /** - * Free the blob cache memory array - * @param $name - */ - public function freeBlob( $name ) - { - unset($this->blobCached[$name]); - $this->blobCached[$name] = null; - } - - protected function uncompress($data) - { - return @gzuncompress($data); - } - - /** - * Fetches all blob fields name_* at once for the current archive for performance reasons. - * - * @param $name - * @return - */ - public function preFetchBlob( $name ) - { - $this->setRequestedReport($name); - $this->prepareArchive(); - if(!$this->isThereSomeVisits) { return; } - - $tableBlob = $this->archiveProcessing->getTableArchiveBlobName(); - - $db = Zend_Registry::get('db'); - $hasBlobs = $db->hasBlobDataType(); - - // select blobs w/ name like "$name_[0-9]+" w/o using RLIKE - $nameEnd = strlen($name) + 2; - $query = $db->query("SELECT value, name + array($this->idArchive, $name) + ); + + $value = $tsArchived = false; + if (is_array($row)) { + $value = $row['value']; + $tsArchived = $row['ts_archived']; + } + + if ($archivedDate !== false) { + $archivedDate = $tsArchived; + } + + if ($value === false) { + if ($typeValue == 'numeric' + && $this->cacheEnabledForNumeric + ) { + $this->numericCached[$name] = false; + } + return $value; + } + + // uncompress when selecting from the BLOB table + if ($typeValue == 'blob' && $db->hasBlobDataType()) { + $value = $this->uncompress($value); + } + + if ($typeValue == 'numeric' + && $this->cacheEnabledForNumeric + ) { + $this->numericCached[$name] = $value; + } + return $value; + } + + + /** + * This method loads in memory all the subtables for the main table called $name. + * You have to give it the parent table $dataTableToLoad so we can lookup the sub tables ids to load. + * + * If $addMetadataSubtableId set to true, it will add for each row a 'metadata' called 'databaseSubtableId' + * containing the child ID of the subtable associated to this row. + * + * @param string $name + * @param Piwik_DataTable $dataTableToLoad + * @param bool $addMetadataSubtableId + */ + public function loadSubDataTables($name, Piwik_DataTable $dataTableToLoad, $addMetadataSubtableId = false) + { + // we have to recursively load all the subtables associated to this table's rows + // and update the subtableID so that it matches the newly instanciated table + foreach ($dataTableToLoad->getRows() as $row) { + $subTableID = $row->getIdSubDataTable(); + + if ($subTableID !== null) { + $subDataTableLoaded = $this->getDataTable($name, $subTableID); + + $row->setSubtable($subDataTableLoaded); + + $this->loadSubDataTables($name, $subDataTableLoaded, $addMetadataSubtableId); + + // we edit the subtable ID so that it matches the newly table created in memory + // NB: we dont overwrite the datatableid in the case we are displaying the table expanded. + if ($addMetadataSubtableId) { + // this will be written back to the column 'idsubdatatable' just before rendering, see Renderer/Php.php + $row->addMetadata('idsubdatatable_in_db', $row->getIdSubDataTable()); + } + } + } + } + + + /** + * Free the blob cache memory array + * @param $name + */ + public function freeBlob($name) + { + unset($this->blobCached[$name]); + $this->blobCached[$name] = null; + } + + protected function uncompress($data) + { + return @gzuncompress($data); + } + + /** + * Fetches all blob fields name_* at once for the current archive for performance reasons. + * + * @param $name + * @return + */ + public function preFetchBlob($name) + { + $this->setRequestedReport($name); + $this->prepareArchive(); + if (!$this->isThereSomeVisits) { + return; + } + + $tableBlob = $this->archiveProcessing->getTableArchiveBlobName(); + + $db = Zend_Registry::get('db'); + $hasBlobs = $db->hasBlobDataType(); + + // select blobs w/ name like "$name_[0-9]+" w/o using RLIKE + $nameEnd = strlen($name) + 2; + $query = $db->query("SELECT value, name FROM $tableBlob WHERE idarchive = ? AND (name = ? OR (name LIKE ? AND SUBSTRING(name, $nameEnd, 1) >= '0' AND SUBSTRING(name, $nameEnd, 1) <= '9') )", - array( $this->idArchive, $name, $name.'%' ) - ); - - while($row = $query->fetch()) - { - $value = $row['value']; - $name = $row['name']; - - if($hasBlobs) - { - $this->blobCached[$name] = $this->uncompress($value); - if($this->blobCached[$name] === false) - { - //throw new Exception("Error gzuncompress $name "); - } - } - else - { - $this->blobCached[$name] = $value; - } - } - } - - /** - * Returns a numeric value from this Archive, with the name '$name' - * - * @param string $name - * @return int|float - */ - public function getNumeric( $name ) - { - return $this->formatNumericValue( $this->get($name, 'numeric') ); - } - - - /** - * Returns a blob value from this Archive, with the name '$name' - * Blob values are all values except int and float. - * - * @param string $name - * @return mixed - */ - public function getBlob( $name ) - { - return $this->get($name, 'blob'); - } - - /** - * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Simple - * containing one row per field name. - * - * For example $fields = array( 'max_actions', - * 'nb_uniq_visitors', - * 'nb_visits', - * 'nb_actions', - * 'sum_visit_length', - * 'bounce_count', - * 'nb_visits_converted' - * ); - * - * @param string|array $fields Name or array of names of Archive fields - * - * @return Piwik_DataTable_Simple - */ - public function getDataTableFromNumeric( $fields ) - { - if(!is_array($fields)) - { - $fields = array($fields); - } - - $values = array(); - foreach($fields as $field) - { - $values[$field] = $this->getNumeric($field); - } - - $table = new Piwik_DataTable_Simple(); - $table->addRowsFromArray($values); - return $table; - } - - /** - * Returns a DataTable that has the name '$name' from the current Archive. - * If $idSubTable is specified, returns the subDataTable called '$name_$idSubTable' - * - * @param string $name - * @param int $idSubTable optional id SubDataTable - * @return Piwik_DataTable - */ - public function getDataTable( $name, $idSubTable = null ) - { - if(!is_null($idSubTable)) - { - $name .= sprintf("_%s", $idSubTable); - } - - $this->setRequestedReport($name); - - $data = $this->get($name, 'blob', $tsArchived); - - $table = $this->makeDataTable(); - - if($data !== false) - { - $table->addRowsFromSerializedArray($data); - $table->setMetadata(Piwik_DataTable::ARCHIVED_DATE_METADATA_NAME, $tsArchived); - } - if($data === false - && $idSubTable !== null) - { - // This is not expected, but somehow happens in some unknown cases and very rarely. - // Do not throw error in this case - //throw new Exception("not expected"); - return new Piwik_DataTable(); - } - - return $table; - } - - /** - * Creates a new DataTable with some metadata set. Sets the following metadata: - * - 'site' => Piwik_Site instance for this archive - * - 'period' => Piwik_Period instance for this archive - * - 'timestamp' => the timestamp of the first date in this archive - * - * @param bool $isSimple Whether the DataTable should be a DataTable_Simple - * instance or not. - * @return Piwik_DataTable - */ - public function makeDataTable( $isSimple = false ) - { - if ($isSimple) - { - $result = new Piwik_DataTable_Simple(); - } - else - { - $result = new Piwik_DataTable(); - } - - $result->setMetadata('site', $this->getSite()); - $result->setMetadata('period', $this->getPeriod()); - $result->setMetadata('timestamp', $this->getTimestampStartDate()); - - return $result; - } - - public function setRequestedReport($requestedReport ) - { - $this->requestedReport = $requestedReport; - } - - /** - * Returns the report (the named collection of metrics) this Archive instance is - * currently going to query/process. - * - * @return string - */ - protected function getRequestedReport() - { - return self::getRequestedReportFor($this->requestedReport); - } - - /** - * Returns the name of the report (the named collection of metrics) that contains the - * specified metric. - * - * @param string $metric The metric whose report is being requested. If this does - * not belong to a known report, its assumed to be the report - * itself. - * @return string - */ - public static function getRequestedReportFor($metric) - { - // Core metrics are always processed in Core, for the requested date/period/segment - if(in_array($metric, Piwik_ArchiveProcessing::getCoreMetrics()) - || $metric == 'max_actions') - { - return 'VisitsSummary_CoreMetrics'; - } - // VisitFrequency metrics don't follow the same naming convention (HACK) - if(strpos($metric, '_returning') > 0 - // ignore Goal_visitor_returning_1_1_nb_conversions - && strpos($metric, 'Goal_') === false) - { - return 'VisitFrequency_Metrics'; - } - // Goal_* metrics are processed by the Goals plugin (HACK) - if(strpos($metric, 'Goal_') === 0) - { - return 'Goals_Metrics'; - } - // Actions metrics are processed by the Actions plugin (HACK) (3RD HACK IN FACT) (YES, THIS IS TOO MUCH HACKING) - // (FIXME PLEASE). - if (in_array($metric, Piwik_Archive::$actionsMetrics)) - { - return 'Actions_Metrics'; - } - return $metric; - } - - /** - * Returns a DataTable that has the name '$name' from the current Archive. - * Also loads in memory all subDataTable for this DataTable. - * - * For example, if $name = 'Referers_keywordBySearchEngine' it will load all DataTable - * named 'Referers_keywordBySearchEngine_*' and they will be set as subDataTable to the - * rows. You can then go through the rows - * $rows = DataTable->getRows(); - * and for each row request the subDataTable (in this case the DataTable of the keywords for each search engines) - * $idSubTable = $row->getIdSubDataTable(); - * $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); - * - * @param string $name - * @param int $idSubTable Optional subDataTable to load instead of loading the parent DataTable - * @return Piwik_DataTable - */ - public function getDataTableExpanded($name, $idSubTable = null) - { - $this->preFetchBlob($name); - $dataTableToLoad = $this->getDataTable($name, $idSubTable); - $this->loadSubDataTables($name, $dataTableToLoad, $addMetadataSubtableId = true); - $dataTableToLoad->enableRecursiveFilters(); - $this->freeBlob($name); - return $dataTableToLoad; - } - - /** - * Returns true if Piwik can launch the archiving process for this archive, - * false if otherwise. - * - * @return bool - */ - public function isArchivingDisabled() - { - return Piwik_ArchiveProcessing::isArchivingDisabledFor($this->segment, $this->period); - } + array($this->idArchive, $name, $name . '%') + ); + + while ($row = $query->fetch()) { + $value = $row['value']; + $name = $row['name']; + + if ($hasBlobs) { + $this->blobCached[$name] = $this->uncompress($value); + if ($this->blobCached[$name] === false) { + //throw new Exception("Error gzuncompress $name "); + } + } else { + $this->blobCached[$name] = $value; + } + } + } + + /** + * Returns a numeric value from this Archive, with the name '$name' + * + * @param string $name + * @return int|float + */ + public function getNumeric($name) + { + return $this->formatNumericValue($this->get($name, 'numeric')); + } + + + /** + * Returns a blob value from this Archive, with the name '$name' + * Blob values are all values except int and float. + * + * @param string $name + * @return mixed + */ + public function getBlob($name) + { + return $this->get($name, 'blob'); + } + + /** + * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Simple + * containing one row per field name. + * + * For example $fields = array( 'max_actions', + * 'nb_uniq_visitors', + * 'nb_visits', + * 'nb_actions', + * 'sum_visit_length', + * 'bounce_count', + * 'nb_visits_converted' + * ); + * + * @param string|array $fields Name or array of names of Archive fields + * + * @return Piwik_DataTable_Simple + */ + public function getDataTableFromNumeric($fields) + { + if (!is_array($fields)) { + $fields = array($fields); + } + + $values = array(); + foreach ($fields as $field) { + $values[$field] = $this->getNumeric($field); + } + + $table = new Piwik_DataTable_Simple(); + $table->addRowsFromArray($values); + return $table; + } + + /** + * Returns a DataTable that has the name '$name' from the current Archive. + * If $idSubTable is specified, returns the subDataTable called '$name_$idSubTable' + * + * @param string $name + * @param int $idSubTable optional id SubDataTable + * @return Piwik_DataTable + */ + public function getDataTable($name, $idSubTable = null) + { + if (!is_null($idSubTable)) { + $name .= sprintf("_%s", $idSubTable); + } + + $this->setRequestedReport($name); + + $data = $this->get($name, 'blob', $tsArchived); + + $table = $this->makeDataTable(); + + if ($data !== false) { + $table->addRowsFromSerializedArray($data); + $table->setMetadata(Piwik_DataTable::ARCHIVED_DATE_METADATA_NAME, $tsArchived); + } + if ($data === false + && $idSubTable !== null + ) { + // This is not expected, but somehow happens in some unknown cases and very rarely. + // Do not throw error in this case + //throw new Exception("not expected"); + return new Piwik_DataTable(); + } + + return $table; + } + + /** + * Creates a new DataTable with some metadata set. Sets the following metadata: + * - 'site' => Piwik_Site instance for this archive + * - 'period' => Piwik_Period instance for this archive + * - 'timestamp' => the timestamp of the first date in this archive + * + * @param bool $isSimple Whether the DataTable should be a DataTable_Simple + * instance or not. + * @return Piwik_DataTable + */ + public function makeDataTable($isSimple = false) + { + if ($isSimple) { + $result = new Piwik_DataTable_Simple(); + } else { + $result = new Piwik_DataTable(); + } + + $result->setMetadata('site', $this->getSite()); + $result->setMetadata('period', $this->getPeriod()); + $result->setMetadata('timestamp', $this->getTimestampStartDate()); + + return $result; + } + + public function setRequestedReport($requestedReport) + { + $this->requestedReport = $requestedReport; + } + + /** + * Returns the report (the named collection of metrics) this Archive instance is + * currently going to query/process. + * + * @return string + */ + protected function getRequestedReport() + { + return self::getRequestedReportFor($this->requestedReport); + } + + /** + * Returns the name of the report (the named collection of metrics) that contains the + * specified metric. + * + * @param string $metric The metric whose report is being requested. If this does + * not belong to a known report, its assumed to be the report + * itself. + * @return string + */ + public static function getRequestedReportFor($metric) + { + // Core metrics are always processed in Core, for the requested date/period/segment + if (in_array($metric, Piwik_ArchiveProcessing::getCoreMetrics()) + || $metric == 'max_actions' + ) { + return 'VisitsSummary_CoreMetrics'; + } + // VisitFrequency metrics don't follow the same naming convention (HACK) + if (strpos($metric, '_returning') > 0 + // ignore Goal_visitor_returning_1_1_nb_conversions + && strpos($metric, 'Goal_') === false + ) { + return 'VisitFrequency_Metrics'; + } + // Goal_* metrics are processed by the Goals plugin (HACK) + if (strpos($metric, 'Goal_') === 0) { + return 'Goals_Metrics'; + } + // Actions metrics are processed by the Actions plugin (HACK) (3RD HACK IN FACT) (YES, THIS IS TOO MUCH HACKING) + // (FIXME PLEASE). + if (in_array($metric, Piwik_Archive::$actionsMetrics)) { + return 'Actions_Metrics'; + } + return $metric; + } + + /** + * Returns a DataTable that has the name '$name' from the current Archive. + * Also loads in memory all subDataTable for this DataTable. + * + * For example, if $name = 'Referers_keywordBySearchEngine' it will load all DataTable + * named 'Referers_keywordBySearchEngine_*' and they will be set as subDataTable to the + * rows. You can then go through the rows + * $rows = DataTable->getRows(); + * and for each row request the subDataTable (in this case the DataTable of the keywords for each search engines) + * $idSubTable = $row->getIdSubDataTable(); + * $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); + * + * @param string $name + * @param int $idSubTable Optional subDataTable to load instead of loading the parent DataTable + * @return Piwik_DataTable + */ + public function getDataTableExpanded($name, $idSubTable = null) + { + $this->preFetchBlob($name); + $dataTableToLoad = $this->getDataTable($name, $idSubTable); + $this->loadSubDataTables($name, $dataTableToLoad, $addMetadataSubtableId = true); + $dataTableToLoad->enableRecursiveFilters(); + $this->freeBlob($name); + return $dataTableToLoad; + } + + /** + * Returns true if Piwik can launch the archiving process for this archive, + * false if otherwise. + * + * @return bool + */ + public function isArchivingDisabled() + { + return Piwik_ArchiveProcessing::isArchivingDisabledFor($this->segment, $this->period); + } } diff --git a/core/ArchiveProcessing.php b/core/ArchiveProcessing.php index 17d2a32246..7ffa394ce9 100644 --- a/core/ArchiveProcessing.php +++ b/core/ArchiveProcessing.php @@ -1,10 +1,10 @@ time = time(); - } - - /** - * Returns the Piwik_ArchiveProcessing_Day or Piwik_ArchiveProcessing_Period object - * depending on $name period string - * - * @param string $name day|week|month|year - * @throws Exception - * @return Piwik_ArchiveProcessing Piwik_ArchiveProcessing_Day|Piwik_ArchiveProcessing_Period - */ - static function factory($name) - { - switch($name) - { - case 'day': - $process = new Piwik_ArchiveProcessing_Day(); - $process->debugAlwaysArchive = Piwik_Config::getInstance()->Debug['always_archive_data_day']; - break; - - case 'week': - case 'month': - case 'year': - $process = new Piwik_ArchiveProcessing_Period(); - $process->debugAlwaysArchive = Piwik_Config::getInstance()->Debug['always_archive_data_period']; - break; - - case 'range': - $process = new Piwik_ArchiveProcessing_Period(); - $process->debugAlwaysArchive = Piwik_Config::getInstance()->Debug['always_archive_data_range']; - break; - - default: - throw new Exception("Unknown Archiving period specified '$name'"); - break; - } - return $process; - } - - const OPTION_TODAY_ARCHIVE_TTL = 'todayArchiveTimeToLive'; - const OPTION_BROWSER_TRIGGER_ARCHIVING = 'enableBrowserTriggerArchiving'; - - static public function getCoreMetrics() - { - return array( - 'nb_uniq_visitors', - 'nb_visits', - 'nb_actions', - 'sum_visit_length', - 'bounce_count', - 'nb_visits_converted', - ); - } - - static public function setTodayArchiveTimeToLive($timeToLiveSeconds) - { - $timeToLiveSeconds = (int)$timeToLiveSeconds; - if($timeToLiveSeconds <= 0) - { - throw new Exception(Piwik_TranslateException('General_ExceptionInvalidArchiveTimeToLive')); - } - Piwik_SetOption(self::OPTION_TODAY_ARCHIVE_TTL, $timeToLiveSeconds, $autoload = true); - } - - static public function getTodayArchiveTimeToLive() - { - $timeToLive = Piwik_GetOption(self::OPTION_TODAY_ARCHIVE_TTL); - if($timeToLive !== false) - { - return $timeToLive; - } - return Piwik_Config::getInstance()->General['time_before_today_archive_considered_outdated']; - } - - static public function setBrowserTriggerArchiving($enabled) - { - if(!is_bool($enabled)) - { - throw new Exception('Browser trigger archiving must be set to true or false.'); - } - Piwik_SetOption(self::OPTION_BROWSER_TRIGGER_ARCHIVING, (int)$enabled, $autoload = true); - Piwik_Tracker_Cache::clearCacheGeneral(); - } - - static public function isBrowserTriggerArchivingEnabled() - { - $browserArchivingEnabled = Piwik_GetOption(self::OPTION_BROWSER_TRIGGER_ARCHIVING); - if($browserArchivingEnabled !== false) - { - return (bool)$browserArchivingEnabled; - } - return (bool)Piwik_Config::getInstance()->General['enable_browser_archiving_triggering']; - } - - public function getIdArchive() - { - return $this->idArchive; - } - - /** - * Sets object attributes that will be used throughout the process - */ - public function init() - { - $this->idsite = $this->site->getId(); - $this->periodId = $this->period->getId(); - - $this->initDates(); - - $this->tableArchiveNumeric = self::makeNumericArchiveTable($this->period); - $this->tableArchiveBlob = self::makeBlobArchiveTable($this->period); - - $this->minDatetimeArchiveProcessedUTC = $this->getMinTimeArchivedProcessed(); - $db = Zend_Registry::get('db'); - $this->compressBlob = $db->hasBlobDataType(); - } - - /** - * The archive processing classes have features that might be useful for live querying; - * In particular, Piwik_ArchiveProcessing_Day::query*. In order to reuse those methods - * outside the actual archiving or to reuse archiving code for live querying, an instance - * of archive processing has to be faked. - * - * For example, this code can be used in an API method: - * $archiveProcessing = new Piwik_ArchiveProcessing_Day(); - * $archiveProcessing->setSite(new Piwik_Site($idSite)); - * $archiveProcessing->setPeriod(Piwik_Period::advancedFactory($period, $date)); - * $archiveProcessing->setSegment(new Piwik_Segment($segment, $idSite)); - * $archiveProcessing->initForLiveUsage(); - * Then, either use $archiveProcessing->query* or pass the instance to the archiving - * code of the plugin. Note that even though we use Piwik_ArchiveProcessing_Day, this - * works for any $period and $date that has been passed to the API. - */ - public function initForLiveUsage() { - $this->idsite = $this->site->getId(); - $this->initDates(); - } - - private function initDates() { - $dateStartLocalTimezone = $this->period->getDateStart(); - $dateEndLocalTimezone = $this->period->getDateEnd(); - - $dateStartUTC = $dateStartLocalTimezone->setTimezone($this->site->getTimezone()); - $dateEndUTC = $dateEndLocalTimezone->setTimezone($this->site->getTimezone()); - $this->startDatetimeUTC = $dateStartUTC->getDateStartUTC(); - $this->endDatetimeUTC = $dateEndUTC->getDateEndUTC(); - $this->startTimestampUTC = $dateStartUTC->getTimestamp(); - $this->endTimestampUTC = strtotime($this->endDatetimeUTC); - } - - /** - * Utility function which creates a TablePartitioning instance for the numeric - * archive data of a given period. - * - * @param Piwik_Period $period The time period of the archive data. - * @return Piwik_TablePartitioning_Monthly - */ - public static function makeNumericArchiveTable($period) - { - $result = new Piwik_TablePartitioning_Monthly('archive_numeric'); - $result->setTimestamp($period->getDateStart()->getTimestamp()); - return $result; - } - - /** - * Utility function which creates a TablePartitioning instance for the blob - * archive data of a given period. - * - * @param Piwik_Period $period The time period of the archive data. - * @return Piwik_TablePartitioning_Monthly - */ - public static function makeBlobArchiveTable($period) - { - $result = new Piwik_TablePartitioning_Monthly('archive_blob'); - $result->setTimestamp($period->getDateStart()->getTimestamp()); - return $result; - } - - public function getStartDatetimeUTC() - { - return $this->startDatetimeUTC; - } - - public function getEndDatetimeUTC() - { - return $this->endDatetimeUTC; - } - - public function isArchiveTemporary() - { - return $this->temporaryArchive; - } - - /** - * Returns the minimum archive processed datetime to look at - * - * @return string Datetime string, or false if must look at any archive available - */ - public function getMinTimeArchivedProcessed() - { - $this->temporaryArchive = false; - // if the current archive is a DAY and if it's today, - // we set this minDatetimeArchiveProcessedUTC that defines the lifetime value of today's archive - if( $this->period->getNumberOfSubperiods() == 0 - && ($this->startTimestampUTC <= $this->time && $this->endTimestampUTC > $this->time) - ) - { - $this->temporaryArchive = true; - $minDatetimeArchiveProcessedUTC = $this->time - self::getTodayArchiveTimeToLive(); - // see #1150; if new archives are not triggered from the browser, - // we still want to try and return the latest archive available for today (rather than return nothing) - if($this->isArchivingDisabled()) - { - return false; - } - } - // - if the period we are looking for is finished, we look for a ts_archived that - // is greater than the last day of the archive - elseif($this->endTimestampUTC <= $this->time) - { - $minDatetimeArchiveProcessedUTC = $this->endTimestampUTC; - } - // - if the period we're looking for is not finished, we look for a recent enough archive - else - { - $this->temporaryArchive = true; - - // We choose to only look at archives that are newer than the specified timeout - $minDatetimeArchiveProcessedUTC = $this->time - self::getTodayArchiveTimeToLive(); - - // However, if archiving is disabled for this request, we shall - // accept any archive that was processed today after 00:00:01 this morning - if($this->isArchivingDisabled()) - { - $timezone = $this->site->getTimezone(); - $minDatetimeArchiveProcessedUTC = Piwik_Date::factory(Piwik_Date::factory('now', $timezone)->getDateStartUTC())->setTimezone($timezone)->getTimestamp(); - } - } - return $minDatetimeArchiveProcessedUTC; - } - - /** - * This method returns the idArchive ; if necessary, it triggers the archiving process. - * - * If the archive was not processed yet, it will launch the archiving process. - * If the current archive needs sub-archives (eg. a month archive needs all the days archive) - * it will recursively launch the archiving (using this loadArchive() on the sub-periods) - * - * @return int|false The idarchive of the archive, false if the archive is not archived yet - */ - public function loadArchive() - { - $this->init(); - if($this->debugAlwaysArchive) - { - return false; - } - $this->idArchive = $this->isArchived(); - - if($this->idArchive === false) - { - return false; - } - return $this->idArchive; - } - - /** - * @see loadArchive() - */ - public function launchArchiving() - { - if (!Piwik::getArchiveProcessingLock($this->idsite, $this->period, $this->segment)) - { - Piwik::log('Unable to get lock for idSite = ' . $this->idsite - . ', period = ' . $this->period->getLabel() - . ', UTC datetime [' . $this->startDatetimeUTC . ' -> ' . $this->endDatetimeUTC . ' ]...'); - return; - } - - $this->initCompute(); - $this->compute(); - $this->postCompute(); - // we execute again the isArchived that does some initialization work - $this->idArchive = $this->isArchived(); - Piwik::releaseArchiveProcessingLock($this->idsite, $this->period, $this->segment); - } - - /** - * This methods reads the subperiods if necessary, - * and computes the archive of the current period. - */ - abstract protected function compute(); - - abstract public function isThereSomeVisits(); - - /** - * Returns the name of the archive field used to tell the status of an archive, (ie, - * whether the archive was created successfully or not). - * - * @param bool $flagArchiveAsAllPlugins - * @return string - */ - public function getDoneStringFlag($flagArchiveAsAllPlugins = false) - { - return self::getDoneStringFlagFor( - $this->getSegment(), $this->period, $this->getRequestedReport(), $flagArchiveAsAllPlugins); - } - - /** - * Returns the name of the archive field used to tell the status of an archive, (ie, - * whether the archive was created successfully or not). - * - * @param Piwik_Segment $segment - * @param Piwik_Period $period - * @param string $requestedReport - * @param bool $flagArchiveAsAllPlugins - * @return string - */ - public static function getDoneStringFlagFor($segment, $period, $requestedReport, $flagArchiveAsAllPlugins = false) - { - $segmentHash = $segment->getHash(); - if(!self::shouldProcessReportsAllPluginsFor($segment, $period)) - { - $pluginProcessed = self::getPluginBeingProcessed($requestedReport); - if(!Piwik_PluginsManager::getInstance()->isPluginLoaded($pluginProcessed) - || $flagArchiveAsAllPlugins - ) - { - $pluginProcessed = 'all'; - } - $segmentHash .= '.'.$pluginProcessed; - } - return 'done' . $segmentHash; - } - - /** - * Init the object before launching the real archive processing - */ - protected function initCompute() - { - $this->loadNextIdarchive(); - $done = $this->getDoneStringFlag(); - $this->insertNumericRecord($done, Piwik_ArchiveProcessing::DONE_ERROR); - - // Can be removed when GeoIp is in core - $this->logTable = Piwik_Common::prefixTable('log_visit'); - - $temporary = 'definitive archive'; - if($this->isArchiveTemporary()) - { - $temporary = 'temporary archive'; - } + /** + * Flag stored at the end of the archiving + * + * @var int + */ + const DONE_OK = 1; + + /** + * Flag stored at the start of the archiving + * When requesting an Archive, we make sure that non-finished archive are not considered valid + * + * @var int + */ + const DONE_ERROR = 2; + + /** + * Flag indicates the archive is over a period that is not finished, eg. the current day, current week, etc. + * Archives flagged will be regularly purged from the DB. + * + * @var int + */ + const DONE_OK_TEMPORARY = 3; + + /** + * A row is created to lock an idarchive for the current archive being processed + * @var string + */ + const PREFIX_SQL_LOCK = "locked_"; + + /** + * Idarchive in the DB for the requested archive + * + * @var int + */ + protected $idArchive; + + /** + * Period id @see Piwik_Period::getId() + * + * @var int + */ + protected $periodId; + + /** + * Timestamp for the first date of the period + * + * @var int unix timestamp + */ + protected $timestampDateStart = null; + + /** + * Starting date of the archive + * + * @var Piwik_Date + */ + protected $dateStart; + + /** + * Ending date of the archive + * + * @var Piwik_Date + */ + protected $dateEnd; + + /** + * Object used to generate (depending on the $dateStart) the name of the DB table to use to store numeric values + * + * @var Piwik_TablePartitioning + */ + protected $tableArchiveNumeric; + + /** + * Object used to generate (depending on the $dateStart) the name of the DB table to use to store numeric values + * + * @var Piwik_TablePartitioning + */ + protected $tableArchiveBlob; + + /** + * Minimum timestamp looked at for processed archives + * + * @var int + */ + protected $minDatetimeArchiveProcessedUTC = false; + + /** + * Compress blobs + * + * @var bool + */ + protected $compressBlob; + + /** + * Is the current archive temporary. ie. + * - today + * - current week / month / year + */ + protected $temporaryArchive; + + /** + * Id of the current site + * Can be accessed by plugins (that is why it's public) + * + * @var int + */ + public $idsite = null; + + /** + * Period of the current archive + * Can be accessed by plugins (that is why it's public) + * + * @var Piwik_Period + */ + public $period = null; + + /** + * Site of the current archive + * Can be accessed by plugins (that is why it's public) + * + * @var Piwik_Site + */ + public $site = null; + + /** + * @var Piwik_Segment + */ + protected $segment = null; + + /** + * Current time. + * This value is cached. + * + * @var int + */ + public $time = null; + + /** + * Starting datetime in UTC + * + * @var string + */ + public $startDatetimeUTC; + + /** + * Ending date in UTC + * + * @var string + */ + public $strDateEnd; + + /** + * Name of the DB table _log_visit + * + * @var string + */ + public $logTable; + + /** + * When set to true, we always archive, even if the archive is already available. + * You can change this settings automatically in the config/global.ini.php always_archive_data under the [Debug] section + * + * @var bool + */ + public $debugAlwaysArchive = false; + + /** + * If the archive has at least 1 visit, this is set to true. + * + * @var bool + */ + public $isThereSomeVisits = null; + + protected $startTimestampUTC; + protected $endTimestampUTC; + + /** + * Flag that will forcefully disable the archiving process. Only set by the tests. + */ + public static $forceDisableArchiving = false; + + /** + * Constructor + */ + public function __construct() + { + $this->time = time(); + } + + /** + * Returns the Piwik_ArchiveProcessing_Day or Piwik_ArchiveProcessing_Period object + * depending on $name period string + * + * @param string $name day|week|month|year + * @throws Exception + * @return Piwik_ArchiveProcessing Piwik_ArchiveProcessing_Day|Piwik_ArchiveProcessing_Period + */ + static function factory($name) + { + switch ($name) { + case 'day': + $process = new Piwik_ArchiveProcessing_Day(); + $process->debugAlwaysArchive = Piwik_Config::getInstance()->Debug['always_archive_data_day']; + break; + + case 'week': + case 'month': + case 'year': + $process = new Piwik_ArchiveProcessing_Period(); + $process->debugAlwaysArchive = Piwik_Config::getInstance()->Debug['always_archive_data_period']; + break; + + case 'range': + $process = new Piwik_ArchiveProcessing_Period(); + $process->debugAlwaysArchive = Piwik_Config::getInstance()->Debug['always_archive_data_range']; + break; + + default: + throw new Exception("Unknown Archiving period specified '$name'"); + break; + } + return $process; + } + + const OPTION_TODAY_ARCHIVE_TTL = 'todayArchiveTimeToLive'; + const OPTION_BROWSER_TRIGGER_ARCHIVING = 'enableBrowserTriggerArchiving'; + + static public function getCoreMetrics() + { + return array( + 'nb_uniq_visitors', + 'nb_visits', + 'nb_actions', + 'sum_visit_length', + 'bounce_count', + 'nb_visits_converted', + ); + } + + static public function setTodayArchiveTimeToLive($timeToLiveSeconds) + { + $timeToLiveSeconds = (int)$timeToLiveSeconds; + if ($timeToLiveSeconds <= 0) { + throw new Exception(Piwik_TranslateException('General_ExceptionInvalidArchiveTimeToLive')); + } + Piwik_SetOption(self::OPTION_TODAY_ARCHIVE_TTL, $timeToLiveSeconds, $autoload = true); + } + + static public function getTodayArchiveTimeToLive() + { + $timeToLive = Piwik_GetOption(self::OPTION_TODAY_ARCHIVE_TTL); + if ($timeToLive !== false) { + return $timeToLive; + } + return Piwik_Config::getInstance()->General['time_before_today_archive_considered_outdated']; + } + + static public function setBrowserTriggerArchiving($enabled) + { + if (!is_bool($enabled)) { + throw new Exception('Browser trigger archiving must be set to true or false.'); + } + Piwik_SetOption(self::OPTION_BROWSER_TRIGGER_ARCHIVING, (int)$enabled, $autoload = true); + Piwik_Tracker_Cache::clearCacheGeneral(); + } + + static public function isBrowserTriggerArchivingEnabled() + { + $browserArchivingEnabled = Piwik_GetOption(self::OPTION_BROWSER_TRIGGER_ARCHIVING); + if ($browserArchivingEnabled !== false) { + return (bool)$browserArchivingEnabled; + } + return (bool)Piwik_Config::getInstance()->General['enable_browser_archiving_triggering']; + } + + public function getIdArchive() + { + return $this->idArchive; + } + + /** + * Sets object attributes that will be used throughout the process + */ + public function init() + { + $this->idsite = $this->site->getId(); + $this->periodId = $this->period->getId(); + + $this->initDates(); + + $this->tableArchiveNumeric = self::makeNumericArchiveTable($this->period); + $this->tableArchiveBlob = self::makeBlobArchiveTable($this->period); + + $this->minDatetimeArchiveProcessedUTC = $this->getMinTimeArchivedProcessed(); + $db = Zend_Registry::get('db'); + $this->compressBlob = $db->hasBlobDataType(); + } + + /** + * The archive processing classes have features that might be useful for live querying; + * In particular, Piwik_ArchiveProcessing_Day::query*. In order to reuse those methods + * outside the actual archiving or to reuse archiving code for live querying, an instance + * of archive processing has to be faked. + * + * For example, this code can be used in an API method: + * $archiveProcessing = new Piwik_ArchiveProcessing_Day(); + * $archiveProcessing->setSite(new Piwik_Site($idSite)); + * $archiveProcessing->setPeriod(Piwik_Period::advancedFactory($period, $date)); + * $archiveProcessing->setSegment(new Piwik_Segment($segment, $idSite)); + * $archiveProcessing->initForLiveUsage(); + * Then, either use $archiveProcessing->query* or pass the instance to the archiving + * code of the plugin. Note that even though we use Piwik_ArchiveProcessing_Day, this + * works for any $period and $date that has been passed to the API. + */ + public function initForLiveUsage() + { + $this->idsite = $this->site->getId(); + $this->initDates(); + } + + private function initDates() + { + $dateStartLocalTimezone = $this->period->getDateStart(); + $dateEndLocalTimezone = $this->period->getDateEnd(); + + $dateStartUTC = $dateStartLocalTimezone->setTimezone($this->site->getTimezone()); + $dateEndUTC = $dateEndLocalTimezone->setTimezone($this->site->getTimezone()); + $this->startDatetimeUTC = $dateStartUTC->getDateStartUTC(); + $this->endDatetimeUTC = $dateEndUTC->getDateEndUTC(); + $this->startTimestampUTC = $dateStartUTC->getTimestamp(); + $this->endTimestampUTC = strtotime($this->endDatetimeUTC); + } + + /** + * Utility function which creates a TablePartitioning instance for the numeric + * archive data of a given period. + * + * @param Piwik_Period $period The time period of the archive data. + * @return Piwik_TablePartitioning_Monthly + */ + public static function makeNumericArchiveTable($period) + { + $result = new Piwik_TablePartitioning_Monthly('archive_numeric'); + $result->setTimestamp($period->getDateStart()->getTimestamp()); + return $result; + } + + /** + * Utility function which creates a TablePartitioning instance for the blob + * archive data of a given period. + * + * @param Piwik_Period $period The time period of the archive data. + * @return Piwik_TablePartitioning_Monthly + */ + public static function makeBlobArchiveTable($period) + { + $result = new Piwik_TablePartitioning_Monthly('archive_blob'); + $result->setTimestamp($period->getDateStart()->getTimestamp()); + return $result; + } + + public function getStartDatetimeUTC() + { + return $this->startDatetimeUTC; + } + + public function getEndDatetimeUTC() + { + return $this->endDatetimeUTC; + } + + public function isArchiveTemporary() + { + return $this->temporaryArchive; + } + + /** + * Returns the minimum archive processed datetime to look at + * + * @return string Datetime string, or false if must look at any archive available + */ + public function getMinTimeArchivedProcessed() + { + $this->temporaryArchive = false; + // if the current archive is a DAY and if it's today, + // we set this minDatetimeArchiveProcessedUTC that defines the lifetime value of today's archive + if ($this->period->getNumberOfSubperiods() == 0 + && ($this->startTimestampUTC <= $this->time && $this->endTimestampUTC > $this->time) + ) { + $this->temporaryArchive = true; + $minDatetimeArchiveProcessedUTC = $this->time - self::getTodayArchiveTimeToLive(); + // see #1150; if new archives are not triggered from the browser, + // we still want to try and return the latest archive available for today (rather than return nothing) + if ($this->isArchivingDisabled()) { + return false; + } + } // - if the period we are looking for is finished, we look for a ts_archived that + // is greater than the last day of the archive + elseif ($this->endTimestampUTC <= $this->time) { + $minDatetimeArchiveProcessedUTC = $this->endTimestampUTC; + } // - if the period we're looking for is not finished, we look for a recent enough archive + else { + $this->temporaryArchive = true; + + // We choose to only look at archives that are newer than the specified timeout + $minDatetimeArchiveProcessedUTC = $this->time - self::getTodayArchiveTimeToLive(); + + // However, if archiving is disabled for this request, we shall + // accept any archive that was processed today after 00:00:01 this morning + if ($this->isArchivingDisabled()) { + $timezone = $this->site->getTimezone(); + $minDatetimeArchiveProcessedUTC = Piwik_Date::factory(Piwik_Date::factory('now', $timezone)->getDateStartUTC())->setTimezone($timezone)->getTimestamp(); + } + } + return $minDatetimeArchiveProcessedUTC; + } + + /** + * This method returns the idArchive ; if necessary, it triggers the archiving process. + * + * If the archive was not processed yet, it will launch the archiving process. + * If the current archive needs sub-archives (eg. a month archive needs all the days archive) + * it will recursively launch the archiving (using this loadArchive() on the sub-periods) + * + * @return int|false The idarchive of the archive, false if the archive is not archived yet + */ + public function loadArchive() + { + $this->init(); + if ($this->debugAlwaysArchive) { + return false; + } + $this->idArchive = $this->isArchived(); + + if ($this->idArchive === false) { + return false; + } + return $this->idArchive; + } + + /** + * @see loadArchive() + */ + public function launchArchiving() + { + if (!Piwik::getArchiveProcessingLock($this->idsite, $this->period, $this->segment)) { + Piwik::log('Unable to get lock for idSite = ' . $this->idsite + . ', period = ' . $this->period->getLabel() + . ', UTC datetime [' . $this->startDatetimeUTC . ' -> ' . $this->endDatetimeUTC . ' ]...'); + return; + } + + $this->initCompute(); + $this->compute(); + $this->postCompute(); + // we execute again the isArchived that does some initialization work + $this->idArchive = $this->isArchived(); + Piwik::releaseArchiveProcessingLock($this->idsite, $this->period, $this->segment); + } + + /** + * This methods reads the subperiods if necessary, + * and computes the archive of the current period. + */ + abstract protected function compute(); + + abstract public function isThereSomeVisits(); + + /** + * Returns the name of the archive field used to tell the status of an archive, (ie, + * whether the archive was created successfully or not). + * + * @param bool $flagArchiveAsAllPlugins + * @return string + */ + public function getDoneStringFlag($flagArchiveAsAllPlugins = false) + { + return self::getDoneStringFlagFor( + $this->getSegment(), $this->period, $this->getRequestedReport(), $flagArchiveAsAllPlugins); + } + + /** + * Returns the name of the archive field used to tell the status of an archive, (ie, + * whether the archive was created successfully or not). + * + * @param Piwik_Segment $segment + * @param Piwik_Period $period + * @param string $requestedReport + * @param bool $flagArchiveAsAllPlugins + * @return string + */ + public static function getDoneStringFlagFor($segment, $period, $requestedReport, $flagArchiveAsAllPlugins = false) + { + $segmentHash = $segment->getHash(); + if (!self::shouldProcessReportsAllPluginsFor($segment, $period)) { + $pluginProcessed = self::getPluginBeingProcessed($requestedReport); + if (!Piwik_PluginsManager::getInstance()->isPluginLoaded($pluginProcessed) + || $flagArchiveAsAllPlugins + ) { + $pluginProcessed = 'all'; + } + $segmentHash .= '.' . $pluginProcessed; + } + return 'done' . $segmentHash; + } + + /** + * Init the object before launching the real archive processing + */ + protected function initCompute() + { + $this->loadNextIdarchive(); + $done = $this->getDoneStringFlag(); + $this->insertNumericRecord($done, Piwik_ArchiveProcessing::DONE_ERROR); + + // Can be removed when GeoIp is in core + $this->logTable = Piwik_Common::prefixTable('log_visit'); + + $temporary = 'definitive archive'; + if ($this->isArchiveTemporary()) { + $temporary = 'temporary archive'; + } Piwik::log(sprintf("'%s, idSite = %d (%s), segment '%s', report = '%s', UTC datetime [%s -> %s]", $this->period->getLabel(), $this->idsite, @@ -576,518 +560,491 @@ abstract class Piwik_ArchiveProcessing $this->startDatetimeUTC, $this->endTimestampUTC )); - } - - /** - * Post processing called at the end of the main archive processing. - * Makes sure the new archive is marked as "successful" in the DB - * - * We also try to delete some stuff from memory but really there is still a lot... - */ - protected function postCompute() - { - // delete the first done = ERROR - $done = $this->getDoneStringFlag(); - Piwik_Query("DELETE FROM ".$this->tableArchiveNumeric->getTableName()." - WHERE idarchive = ? AND (name = '".$done."' OR name LIKE '".self::PREFIX_SQL_LOCK."%')", - array($this->idArchive) - ); - - $flag = Piwik_ArchiveProcessing::DONE_OK; - if($this->isArchiveTemporary()) - { - $flag = Piwik_ArchiveProcessing::DONE_OK_TEMPORARY; - } - $this->insertNumericRecord($done, $flag); - } - - /** - * Returns the name of the numeric table where the archive numeric values are stored - * - * @return string - */ - public function getTableArchiveNumericName() - { - return $this->tableArchiveNumeric->getTableName(); - } - - /** - * Returns the name of the blob table where the archive blob values are stored - * - * @return string - */ - public function getTableArchiveBlobName() - { - return $this->tableArchiveBlob->getTableName(); - } - - /** - * Set the period - * - * @param Piwik_Period $period - */ - public function setPeriod( Piwik_Period $period ) - { - $this->period = $period; - } - - public function setSegment( Piwik_Segment $segment) - { - $this->segment = $segment; - } - - public function getSegment() - { - return $this->segment; - } - /** - * Set the site - * - * @param Piwik_Site $site - */ - public function setSite( Piwik_Site $site ) - { - $this->site = $site; - } - - public function setRequestedReport($requestedReport) - { - $this->requestedReport = $requestedReport; - } - - protected function getRequestedReport() - { - return $this->requestedReport; - } - - static public function getPluginBeingProcessed( $requestedReport ) - { - $plugin = substr($requestedReport, 0, strpos($requestedReport, '_')); - if(empty($plugin) - || !Piwik_PluginsManager::getInstance()->isPluginActivated($plugin)) - { - $pluginStr = empty($plugin) ? '' : "($plugin)"; - throw new Exception("Error: The report '$requestedReport' was requested but it is not available at this stage. You may also disable the related plugin $pluginStr to avoid this error."); - } - return $plugin; - } - - /** - * Returns the timestamp of the first date of the period - * - * @return int - */ - public function getTimestampStartDate() - { - return $this->timestampDateStart; - } - - // exposing the number of visits publicly (number used to compute conversions rates) - protected $nb_visits = null; - protected $nb_visits_converted = null; - - protected function setNumberOfVisits($nb_visits) - { - $this->nb_visits = $nb_visits; - } - public function getNumberOfVisits() - { - return $this->nb_visits; - } - protected function setNumberOfVisitsConverted($nb_visits_converted) - { - $this->nb_visits_converted = $nb_visits_converted; - } - public function getNumberOfVisitsConverted() - { - return $this->nb_visits_converted; - } - - - /** - * Returns the idArchive we will use for the current archive - * - * @return int IdArchive to use when saving the current Archive - */ - protected function loadNextIdarchive() - { - $table = $this->tableArchiveNumeric->getTableName(); - $dbLockName = "loadNextIdArchive.$table"; - - $db = Zend_Registry::get('db'); - $locked = self::PREFIX_SQL_LOCK . Piwik_Common::generateUniqId(); - $date = date("Y-m-d H:i:s"); - - if (Piwik_GetDbLock($dbLockName, $maxRetries = 30) === false) - { - throw new Exception("loadNextIdArchive: Cannot get named lock for table $table."); - } - $db->exec("INSERT INTO $table " - ." SELECT ifnull(max(idarchive),0)+1, - '".$locked."', - ".(int)$this->idsite.", - '".$date."', - '".$date."', + } + + /** + * Post processing called at the end of the main archive processing. + * Makes sure the new archive is marked as "successful" in the DB + * + * We also try to delete some stuff from memory but really there is still a lot... + */ + protected function postCompute() + { + // delete the first done = ERROR + $done = $this->getDoneStringFlag(); + Piwik_Query("DELETE FROM " . $this->tableArchiveNumeric->getTableName() . " + WHERE idarchive = ? AND (name = '" . $done . "' OR name LIKE '" . self::PREFIX_SQL_LOCK . "%')", + array($this->idArchive) + ); + + $flag = Piwik_ArchiveProcessing::DONE_OK; + if ($this->isArchiveTemporary()) { + $flag = Piwik_ArchiveProcessing::DONE_OK_TEMPORARY; + } + $this->insertNumericRecord($done, $flag); + } + + /** + * Returns the name of the numeric table where the archive numeric values are stored + * + * @return string + */ + public function getTableArchiveNumericName() + { + return $this->tableArchiveNumeric->getTableName(); + } + + /** + * Returns the name of the blob table where the archive blob values are stored + * + * @return string + */ + public function getTableArchiveBlobName() + { + return $this->tableArchiveBlob->getTableName(); + } + + /** + * Set the period + * + * @param Piwik_Period $period + */ + public function setPeriod(Piwik_Period $period) + { + $this->period = $period; + } + + public function setSegment(Piwik_Segment $segment) + { + $this->segment = $segment; + } + + public function getSegment() + { + return $this->segment; + } + + /** + * Set the site + * + * @param Piwik_Site $site + */ + public function setSite(Piwik_Site $site) + { + $this->site = $site; + } + + public function setRequestedReport($requestedReport) + { + $this->requestedReport = $requestedReport; + } + + protected function getRequestedReport() + { + return $this->requestedReport; + } + + static public function getPluginBeingProcessed($requestedReport) + { + $plugin = substr($requestedReport, 0, strpos($requestedReport, '_')); + if (empty($plugin) + || !Piwik_PluginsManager::getInstance()->isPluginActivated($plugin) + ) { + $pluginStr = empty($plugin) ? '' : "($plugin)"; + throw new Exception("Error: The report '$requestedReport' was requested but it is not available at this stage. You may also disable the related plugin $pluginStr to avoid this error."); + } + return $plugin; + } + + /** + * Returns the timestamp of the first date of the period + * + * @return int + */ + public function getTimestampStartDate() + { + return $this->timestampDateStart; + } + + // exposing the number of visits publicly (number used to compute conversions rates) + protected $nb_visits = null; + protected $nb_visits_converted = null; + + protected function setNumberOfVisits($nb_visits) + { + $this->nb_visits = $nb_visits; + } + + public function getNumberOfVisits() + { + return $this->nb_visits; + } + + protected function setNumberOfVisitsConverted($nb_visits_converted) + { + $this->nb_visits_converted = $nb_visits_converted; + } + + public function getNumberOfVisitsConverted() + { + return $this->nb_visits_converted; + } + + + /** + * Returns the idArchive we will use for the current archive + * + * @return int IdArchive to use when saving the current Archive + */ + protected function loadNextIdarchive() + { + $table = $this->tableArchiveNumeric->getTableName(); + $dbLockName = "loadNextIdArchive.$table"; + + $db = Zend_Registry::get('db'); + $locked = self::PREFIX_SQL_LOCK . Piwik_Common::generateUniqId(); + $date = date("Y-m-d H:i:s"); + + if (Piwik_GetDbLock($dbLockName, $maxRetries = 30) === false) { + throw new Exception("loadNextIdArchive: Cannot get named lock for table $table."); + } + $db->exec("INSERT INTO $table " + . " SELECT ifnull(max(idarchive),0)+1, + '" . $locked . "', + " . (int)$this->idsite . ", + '" . $date . "', + '" . $date . "', 0, - '".$date."', + '" . $date . "', 0 " - ." FROM $table as tb1"); - Piwik_ReleaseDbLock($dbLockName); + . " FROM $table as tb1"); + Piwik_ReleaseDbLock($dbLockName); $id = $db->fetchOne("SELECT idarchive FROM $table WHERE name = ? LIMIT 1", $locked); - $this->idArchive = $id; - } - - /** - * @param string $name - * @param int|float $value - */ - public function insertNumericRecord($name, $value) - { - $value = round($value, 2); - return $this->insertRecord($name, $value); - } - - /** - * @param string $name - * @param string|array $values - * @return bool|array - */ - public function insertBlobRecord($name, $values) - { - if(is_array($values)) - { - $clean = array(); - foreach($values as $id => $value) - { - // for the parent Table we keep the name - // for example for the Table of searchEngines we keep the name 'referer_search_engine' - // but for the child table of 'Google' which has the ID = 9 the name would be 'referer_search_engine_9' - $newName = $name; - if($id != 0) - { - $newName = $name . '_' . $id; - } - - if($this->compressBlob) - { - $value = $this->compress($value); - } - $clean[] = array($newName, $value); - } - return $this->insertBulkRecords($clean); - } - - if($this->compressBlob) - { - $values = $this->compress($values); - } - - $this->insertRecord($name, $values); - return array($name => $values); - } - - protected function compress($data) - { - return gzcompress($data); - } - - protected function insertBulkRecords($records) - { - // Using standard plain INSERT if there is only one record to insert - if($DEBUG_DO_NOT_USE_BULK_INSERT = false - || count($records) == 1) - { - foreach($records as $record) - { - $this->insertRecord($record[0], $record[1]); - } - return ; - } - $bindSql = $this->getBindArray(); - $values = array(); - - foreach($records as $record) - { - // don't record zero - if(empty($record[1])) continue; - - $bind = $bindSql; - $bind[] = $record[0]; // name - $bind[] = $record[1]; // value - $values[] = $bind; - - } - if(empty($values)) return ; - - if(is_numeric($record[1])) - { - $table = $this->tableArchiveNumeric; - } - else - { - $table = $this->tableArchiveBlob; - } - - Piwik::tableInsertBatch($table->getTableName(), $this->getInsertFields(), $values); - return true; - } - - protected function getBindArray() - { - return array( $this->idArchive, - $this->idsite, - $this->period->getDateStart()->toString('Y-m-d'), - $this->period->getDateEnd()->toString('Y-m-d'), - $this->periodId, - date("Y-m-d H:i:s")); - } - - protected function getInsertFields() - { - return array('idarchive', 'idsite', 'date1', 'date2', 'period', 'ts_archived', 'name', 'value'); - } - - /** - * Inserts a record in the right table (either NUMERIC or BLOB) - * @param $name - * @param $value - * @return - */ - protected function insertRecord($name, $value) - { - // table to use to save the data - if(is_numeric($value)) - { - // We choose not to record records with a value of 0 - if($value == 0) - { - return; - } - $table = $this->tableArchiveNumeric; - } - else - { - $table = $this->tableArchiveBlob; - } - - // duplicate idarchives are Ignored, see http://dev.piwik.org/trac/ticket/987 - - $query = "INSERT IGNORE INTO ".$table->getTableName()." - (". implode(", ", $this->getInsertFields()).") + $this->idArchive = $id; + } + + /** + * @param string $name + * @param int|float $value + */ + public function insertNumericRecord($name, $value) + { + $value = round($value, 2); + return $this->insertRecord($name, $value); + } + + /** + * @param string $name + * @param string|array $values + * @return bool|array + */ + public function insertBlobRecord($name, $values) + { + if (is_array($values)) { + $clean = array(); + foreach ($values as $id => $value) { + // for the parent Table we keep the name + // for example for the Table of searchEngines we keep the name 'referer_search_engine' + // but for the child table of 'Google' which has the ID = 9 the name would be 'referer_search_engine_9' + $newName = $name; + if ($id != 0) { + $newName = $name . '_' . $id; + } + + if ($this->compressBlob) { + $value = $this->compress($value); + } + $clean[] = array($newName, $value); + } + return $this->insertBulkRecords($clean); + } + + if ($this->compressBlob) { + $values = $this->compress($values); + } + + $this->insertRecord($name, $values); + return array($name => $values); + } + + protected function compress($data) + { + return gzcompress($data); + } + + protected function insertBulkRecords($records) + { + // Using standard plain INSERT if there is only one record to insert + if ($DEBUG_DO_NOT_USE_BULK_INSERT = false + || count($records) == 1 + ) { + foreach ($records as $record) { + $this->insertRecord($record[0], $record[1]); + } + return; + } + $bindSql = $this->getBindArray(); + $values = array(); + + foreach ($records as $record) { + // don't record zero + if (empty($record[1])) continue; + + $bind = $bindSql; + $bind[] = $record[0]; // name + $bind[] = $record[1]; // value + $values[] = $bind; + + } + if (empty($values)) return; + + if (is_numeric($record[1])) { + $table = $this->tableArchiveNumeric; + } else { + $table = $this->tableArchiveBlob; + } + + Piwik::tableInsertBatch($table->getTableName(), $this->getInsertFields(), $values); + return true; + } + + protected function getBindArray() + { + return array($this->idArchive, + $this->idsite, + $this->period->getDateStart()->toString('Y-m-d'), + $this->period->getDateEnd()->toString('Y-m-d'), + $this->periodId, + date("Y-m-d H:i:s")); + } + + protected function getInsertFields() + { + return array('idarchive', 'idsite', 'date1', 'date2', 'period', 'ts_archived', 'name', 'value'); + } + + /** + * Inserts a record in the right table (either NUMERIC or BLOB) + * @param $name + * @param $value + * @return + */ + protected function insertRecord($name, $value) + { + // table to use to save the data + if (is_numeric($value)) { + // We choose not to record records with a value of 0 + if ($value == 0) { + return; + } + $table = $this->tableArchiveNumeric; + } else { + $table = $this->tableArchiveBlob; + } + + // duplicate idarchives are Ignored, see http://dev.piwik.org/trac/ticket/987 + + $query = "INSERT IGNORE INTO " . $table->getTableName() . " + (" . implode(", ", $this->getInsertFields()) . ") VALUES (?,?,?,?,?,?,?,?)"; - $bindSql = $this->getBindArray(); - $bindSql[] = $name; - $bindSql[] = $value; - Piwik_Query($query, $bindSql); - } - - /** - * Returns the idArchive if the archive is available in the database. - * Returns false if the archive needs to be computed. - * - * An archive is available if - * - for today, the archive was computed less than minDatetimeArchiveProcessedUTC seconds ago - * - for any other day, if the archive was computed once this day was finished - * - for other periods, if the archive was computed once the period was finished - * - * @return int|false - */ - protected function isArchived() - { - $bindSQL = array( $this->idsite, - $this->period->getDateStart()->toString('Y-m-d'), - $this->period->getDateEnd()->toString('Y-m-d'), - $this->periodId, - ); - - $timeStampWhere = ''; - - if($this->minDatetimeArchiveProcessedUTC) - { - $timeStampWhere = " AND ts_archived >= ? "; - $bindSQL[] = Piwik_Date::factory($this->minDatetimeArchiveProcessedUTC)->getDatetime(); - } - - // When a Segment is specified, we try and only process the requested report in the archive - // As a limitation, we don't know all the time which plugin should process which report - // There is a catch all flag 'all' appended to archives containing all reports already - // We look for this 'done.ABCDEFG.all', or for an archive that contains only our plugin data 'done.ABDCDEFG.Referers' - $done = $this->getDoneStringFlag(); - $doneAllPluginsProcessed = $this->getDoneStringFlag($flagArchiveAsAllPlugins = true); - - $sqlSegmentsFindArchiveAllPlugins = ''; - - if($done != $doneAllPluginsProcessed) - { - $sqlSegmentsFindArchiveAllPlugins = "OR (name = '".$doneAllPluginsProcessed."' AND value = ".Piwik_ArchiveProcessing::DONE_OK.") - OR (name = '".$doneAllPluginsProcessed."' AND value = ".Piwik_ArchiveProcessing::DONE_OK_TEMPORARY.")"; - } - $sqlQuery = " SELECT idarchive, value, name, date1 as startDate - FROM ".$this->tableArchiveNumeric->getTableName()." + $bindSql = $this->getBindArray(); + $bindSql[] = $name; + $bindSql[] = $value; + Piwik_Query($query, $bindSql); + } + + /** + * Returns the idArchive if the archive is available in the database. + * Returns false if the archive needs to be computed. + * + * An archive is available if + * - for today, the archive was computed less than minDatetimeArchiveProcessedUTC seconds ago + * - for any other day, if the archive was computed once this day was finished + * - for other periods, if the archive was computed once the period was finished + * + * @return int|false + */ + protected function isArchived() + { + $bindSQL = array($this->idsite, + $this->period->getDateStart()->toString('Y-m-d'), + $this->period->getDateEnd()->toString('Y-m-d'), + $this->periodId, + ); + + $timeStampWhere = ''; + + if ($this->minDatetimeArchiveProcessedUTC) { + $timeStampWhere = " AND ts_archived >= ? "; + $bindSQL[] = Piwik_Date::factory($this->minDatetimeArchiveProcessedUTC)->getDatetime(); + } + + // When a Segment is specified, we try and only process the requested report in the archive + // As a limitation, we don't know all the time which plugin should process which report + // There is a catch all flag 'all' appended to archives containing all reports already + // We look for this 'done.ABCDEFG.all', or for an archive that contains only our plugin data 'done.ABDCDEFG.Referers' + $done = $this->getDoneStringFlag(); + $doneAllPluginsProcessed = $this->getDoneStringFlag($flagArchiveAsAllPlugins = true); + + $sqlSegmentsFindArchiveAllPlugins = ''; + + if ($done != $doneAllPluginsProcessed) { + $sqlSegmentsFindArchiveAllPlugins = "OR (name = '" . $doneAllPluginsProcessed . "' AND value = " . Piwik_ArchiveProcessing::DONE_OK . ") + OR (name = '" . $doneAllPluginsProcessed . "' AND value = " . Piwik_ArchiveProcessing::DONE_OK_TEMPORARY . ")"; + } + $sqlQuery = " SELECT idarchive, value, name, date1 as startDate + FROM " . $this->tableArchiveNumeric->getTableName() . " WHERE idsite = ? AND date1 = ? AND date2 = ? AND period = ? - AND ( (name = '".$done."' AND value = ".Piwik_ArchiveProcessing::DONE_OK.") - OR (name = '".$done."' AND value = ".Piwik_ArchiveProcessing::DONE_OK_TEMPORARY.") + AND ( (name = '" . $done . "' AND value = " . Piwik_ArchiveProcessing::DONE_OK . ") + OR (name = '" . $done . "' AND value = " . Piwik_ArchiveProcessing::DONE_OK_TEMPORARY . ") $sqlSegmentsFindArchiveAllPlugins OR name = 'nb_visits') $timeStampWhere ORDER BY idarchive DESC"; - $results = Piwik_FetchAll($sqlQuery, $bindSQL ); - if(empty($results)) - { - return false; - } - - $idarchive = false; - // we look for the more recent idarchive - foreach($results as $result) - { - if($result['name'] == $done - || $result['name'] == $doneAllPluginsProcessed) - { - $idarchive = $result['idarchive']; - $this->timestampDateStart = Piwik_Date::factory($result['startDate'])->getTimestamp(); - break; - } - } - - // case when we have a nb_visits entry in the archive, but the process is not finished yet or failed to finish - // therefore we don't have the done=OK - if($idarchive === false) - { - return false; - } - - if($this->getPluginBeingProcessed($this->getRequestedReport()) == 'VisitsSummary') - { - $this->isThereSomeVisits = false; - } - - // we look for the nb_visits result for this most recent archive - foreach($results as $result) - { - if($result['name'] == 'nb_visits' - && $result['idarchive'] == $idarchive) - { - $this->isThereSomeVisits = ($result['value'] > 0); - $this->setNumberOfVisits($result['value']); - break; - } - } - return $idarchive; - } - - /** - * Returns true if, for some reasons, triggering the archiving is disabled. - * Note that when a segment is passed to the function, archiving will always occur - * (since segments are by default not pre-processed) - * - * @return bool - */ - public function isArchivingDisabled() - { - return self::isArchivingDisabledFor($this->getSegment(), $this->period); - } - - public static function isArchivingDisabledFor($segment, $period) - { - if($period->getLabel() == 'range') { + $results = Piwik_FetchAll($sqlQuery, $bindSQL); + if (empty($results)) { + return false; + } + + $idarchive = false; + // we look for the more recent idarchive + foreach ($results as $result) { + if ($result['name'] == $done + || $result['name'] == $doneAllPluginsProcessed + ) { + $idarchive = $result['idarchive']; + $this->timestampDateStart = Piwik_Date::factory($result['startDate'])->getTimestamp(); + break; + } + } + + // case when we have a nb_visits entry in the archive, but the process is not finished yet or failed to finish + // therefore we don't have the done=OK + if ($idarchive === false) { + return false; + } + + if ($this->getPluginBeingProcessed($this->getRequestedReport()) == 'VisitsSummary') { + $this->isThereSomeVisits = false; + } + + // we look for the nb_visits result for this most recent archive + foreach ($results as $result) { + if ($result['name'] == 'nb_visits' + && $result['idarchive'] == $idarchive + ) { + $this->isThereSomeVisits = ($result['value'] > 0); + $this->setNumberOfVisits($result['value']); + break; + } + } + return $idarchive; + } + + /** + * Returns true if, for some reasons, triggering the archiving is disabled. + * Note that when a segment is passed to the function, archiving will always occur + * (since segments are by default not pre-processed) + * + * @return bool + */ + public function isArchivingDisabled() + { + return self::isArchivingDisabledFor($this->getSegment(), $this->period); + } + + public static function isArchivingDisabledFor($segment, $period) + { + if ($period->getLabel() == 'range') { return false; } $processOneReportOnly = !self::shouldProcessReportsAllPluginsFor($segment, $period); $isArchivingDisabled = !self::isRequestAuthorizedToArchive(); - if($processOneReportOnly) - { + if ($processOneReportOnly) { // When there is a segment, archiving is not necessary allowed // If browser archiving is allowed, then archiving is enabled // if browser archiving is not allowed, then archiving is disabled - if(!$segment->isEmpty() + if (!$segment->isEmpty() && $isArchivingDisabled && Piwik_Config::getInstance()->General['browser_archiving_disabled_enforce'] - ) - { + ) { Piwik::log("Archiving is disabled because of config setting browser_archiving_disabled_enforce=1"); return true; } return false; } - return $isArchivingDisabled; - } - - protected static function isRequestAuthorizedToArchive() - { - return !self::$forceDisableArchiving && - (self::isBrowserTriggerArchivingEnabled() - || Piwik_Common::isPhpCliMode() - || (Piwik::isUserIsSuperUser() - && Piwik_Common::isArchivePhpTriggered())) - ; - } - - /** - * Returns true when - * - there is no segment and period is not range - * - there is a segment that is part of the preprocessed [Segments] list - * @param Piwik_Segment $segment - * @param Piwik_Period $period - * @return bool - */ - protected function shouldProcessReportsAllPlugins($segment, $period) - { - return self::shouldProcessReportsAllPluginsFor($segment, $period); - } - - /** - * @param Piwik_Segment $segment - * @param Piwik_Period $period - * @return bool - */ - protected static function shouldProcessReportsAllPluginsFor($segment, $period) - { - if($segment->isEmpty() && $period->getLabel() != 'range') - { - return true; - } - - $segmentsToProcess = Piwik::getKnownSegmentsToArchive(); - if(!empty($segmentsToProcess)) - { - // If the requested segment is one of the segments to pre-process - // we ensure that any call to the API will trigger archiving of all reports for this segment - $segment = $segment->getString(); - if(in_array($segment, $segmentsToProcess)) - { - return true; - } - } - return false; - } - - /** - * When a segment is set, we shall only process the requested report (no more). - * The requested data set will return a lot faster if we only process these reports rather than all plugins. - * Similarly, when a period=range is requested, we shall only process the requested report for the range itself. - * - * @param string $pluginName - * @return bool - */ - public function shouldProcessReportsForPlugin($pluginName) - { - if($this->shouldProcessReportsAllPlugins($this->getSegment(), $this->period)) - { - return true; - } - - // If any other segment, only process if the requested report belong to this plugin - // or process all plugins if the requested report plugin couldn't be guessed - $pluginBeingProcessed = self::getPluginBeingProcessed($this->getRequestedReport()); - return $pluginBeingProcessed == $pluginName - || !Piwik_PluginsManager::getInstance()->isPluginLoaded($pluginBeingProcessed) - ; - } - + return $isArchivingDisabled; + } + + protected static function isRequestAuthorizedToArchive() + { + return !self::$forceDisableArchiving && + (self::isBrowserTriggerArchivingEnabled() + || Piwik_Common::isPhpCliMode() + || (Piwik::isUserIsSuperUser() + && Piwik_Common::isArchivePhpTriggered())); + } + + /** + * Returns true when + * - there is no segment and period is not range + * - there is a segment that is part of the preprocessed [Segments] list + * @param Piwik_Segment $segment + * @param Piwik_Period $period + * @return bool + */ + protected function shouldProcessReportsAllPlugins($segment, $period) + { + return self::shouldProcessReportsAllPluginsFor($segment, $period); + } + + /** + * @param Piwik_Segment $segment + * @param Piwik_Period $period + * @return bool + */ + protected static function shouldProcessReportsAllPluginsFor($segment, $period) + { + if ($segment->isEmpty() && $period->getLabel() != 'range') { + return true; + } + + $segmentsToProcess = Piwik::getKnownSegmentsToArchive(); + if (!empty($segmentsToProcess)) { + // If the requested segment is one of the segments to pre-process + // we ensure that any call to the API will trigger archiving of all reports for this segment + $segment = $segment->getString(); + if (in_array($segment, $segmentsToProcess)) { + return true; + } + } + return false; + } + + /** + * When a segment is set, we shall only process the requested report (no more). + * The requested data set will return a lot faster if we only process these reports rather than all plugins. + * Similarly, when a period=range is requested, we shall only process the requested report for the range itself. + * + * @param string $pluginName + * @return bool + */ + public function shouldProcessReportsForPlugin($pluginName) + { + if ($this->shouldProcessReportsAllPlugins($this->getSegment(), $this->period)) { + return true; + } + + // If any other segment, only process if the requested report belong to this plugin + // or process all plugins if the requested report plugin couldn't be guessed + $pluginBeingProcessed = self::getPluginBeingProcessed($this->getRequestedReport()); + return $pluginBeingProcessed == $pluginName + || !Piwik_PluginsManager::getInstance()->isPluginLoaded($pluginBeingProcessed); + } + } diff --git a/core/ArchiveProcessing/Day.php b/core/ArchiveProcessing/Day.php index 0e4198a36f..07921e9a50 100644 --- a/core/ArchiveProcessing/Day.php +++ b/core/ArchiveProcessing/Day.php @@ -21,65 +21,62 @@ */ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing { - /** - * Constructor - */ - function __construct() - { - parent::__construct(); - $this->db = Zend_Registry::get('db'); - } - - /** - * Main method to process logs for a day. The only logic done here is computing the number of visits, actions, etc. - * All the other reports are computed inside plugins listening to the event 'ArchiveProcessing_Day.compute'. - * See some of the plugins for an example eg. 'Provider' - */ - protected function compute() - { - if(!$this->isThereSomeVisits()) - { - return; - } - Piwik_PostEvent('ArchiveProcessing_Day.compute', $this); - } - - /** - * Returns true if there are logs for the current archive. - * - * If the current archive is for a specific plugin (for example, Referers), - * (for example when a Segment is defined and the Keywords report is requested) - * Then the function will create the Archive for the Core metrics 'VisitsSummary' which will in turn process the number of visits - * - * If there is no specified segment, the SQL query will always run. - * - * @return bool|null - */ - public function isThereSomeVisits() - { - if (!is_null($this->isThereSomeVisits)) - { - if ($this->isThereSomeVisits && is_null($this->nb_visits)) - { - debug_print_backtrace(); - exit; - } - return $this->isThereSomeVisits; - } - - // prepare segmentation - $segment = $this->getSegment(); - - // We check if there is visits for the requested date / site / segment - // If no specified Segment - // Or if a segment is passed and we specifically process VisitsSummary - // Then we check the logs. This is to ensure that this query is ran only once for this day/site/segment (rather than running it for every plugin) - $reportType = self::getPluginBeingProcessed($this->getRequestedReport()); - if ($this->shouldProcessReportsAllPlugins($this->getSegment(), $this->period) - || ($reportType == 'VisitsSummary')) - { - // build query parts - $select = "count(distinct log_visit.idvisitor) as nb_uniq_visitors, + /** + * Constructor + */ + function __construct() + { + parent::__construct(); + $this->db = Zend_Registry::get('db'); + } + + /** + * Main method to process logs for a day. The only logic done here is computing the number of visits, actions, etc. + * All the other reports are computed inside plugins listening to the event 'ArchiveProcessing_Day.compute'. + * See some of the plugins for an example eg. 'Provider' + */ + protected function compute() + { + if (!$this->isThereSomeVisits()) { + return; + } + Piwik_PostEvent('ArchiveProcessing_Day.compute', $this); + } + + /** + * Returns true if there are logs for the current archive. + * + * If the current archive is for a specific plugin (for example, Referers), + * (for example when a Segment is defined and the Keywords report is requested) + * Then the function will create the Archive for the Core metrics 'VisitsSummary' which will in turn process the number of visits + * + * If there is no specified segment, the SQL query will always run. + * + * @return bool|null + */ + public function isThereSomeVisits() + { + if (!is_null($this->isThereSomeVisits)) { + if ($this->isThereSomeVisits && is_null($this->nb_visits)) { + debug_print_backtrace(); + exit; + } + return $this->isThereSomeVisits; + } + + // prepare segmentation + $segment = $this->getSegment(); + + // We check if there is visits for the requested date / site / segment + // If no specified Segment + // Or if a segment is passed and we specifically process VisitsSummary + // Then we check the logs. This is to ensure that this query is ran only once for this day/site/segment (rather than running it for every plugin) + $reportType = self::getPluginBeingProcessed($this->getRequestedReport()); + if ($this->shouldProcessReportsAllPlugins($this->getSegment(), $this->period) + || ($reportType == 'VisitsSummary') + ) { + // build query parts + $select = "count(distinct log_visit.idvisitor) as nb_uniq_visitors, count(*) as nb_visits, sum(log_visit.visit_total_actions) as nb_actions, max(log_visit.visit_total_actions) as max_actions, @@ -87,545 +84,503 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing sum(case log_visit.visit_total_actions when 1 then 1 when 0 then 1 else 0 end) as bounce_count, sum(case log_visit.visit_goal_converted when 1 then 1 else 0 end) as nb_visits_converted "; - $from = "log_visit"; - $where = "log_visit.visit_last_action_time >= ? + $from = "log_visit"; + $where = "log_visit.visit_last_action_time >= ? AND log_visit.visit_last_action_time <= ? AND log_visit.idsite = ? "; - - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); - $query = $segment->getSelectQuery($select, $from, $where, $bind); - - $bind = $query['bind']; - $sql = $query['sql']; - - $data = $this->db->fetchRow($sql, $bind); - - // no visits found - if (!is_array($data) || $data['nb_visits'] == 0) - { - return $this->isThereSomeVisits = false; - } - - // visits found: set attribtues - foreach ($data as $name => $value) - { - $this->insertNumericRecord($name, $value); - } - - $this->setNumberOfVisits($data['nb_visits']); - $this->setNumberOfVisitsConverted($data['nb_visits_converted']); - - return $this->isThereSomeVisits = true; - } - - return $this->redirectRequestToVisitsSummary(); - } - - /** - * If a segment is specified but a plugin other than 'VisitsSummary' is being requested, - * we create an archive for processing VisitsSummary Core Metrics, which will in turn - * execute the query above (in isThereSomeVisits) - * - * @return bool|null - */ - private function redirectRequestToVisitsSummary() - { - $archive = new Piwik_Archive_Single(); - $archive->setSite($this->site); - $archive->setPeriod($this->period); - $archive->setSegment($this->getSegment()); - $archive->setRequestedReport('VisitsSummary'); - - $nbVisits = $archive->getNumeric('nb_visits'); - $this->isThereSomeVisits = $nbVisits > 0; - - if ($this->isThereSomeVisits) - { - $nbVisitsConverted = $archive->getNumeric('nb_visits_converted'); - $this->setNumberOfVisits($nbVisits); - $this->setNumberOfVisitsConverted($nbVisitsConverted); - } - - return $this->isThereSomeVisits; - } - - /** - * Creates and returns an array of SQL SELECT expressions that will summarize - * the data in a column of a specified table, over a set of ranges. - * - * The SELECT expressions will count the number of column values that are - * within each range. - * - * @param string $column The column of the log_conversion table to reduce. - * @param array $ranges The ranges to reduce data over. - * @param string $table The table the SELECTs should use. - * @param string $selectColumnPrefix The prefix when specifying what a SELECT - * expression will be selected AS. - * @param bool|string $extraCondition An extra condition to be appended to 'case when' - * expressions. Must start with the logical operator, - * ie (AND, OR, etc.). - * @return array An array of SQL SELECT expressions. - */ - public static function buildReduceByRangeSelect( - $column, $ranges, $table, $selectColumnPrefix = '', $extraCondition = false) - { - $selects = array(); - - foreach($ranges as $gap) - { - if (count($gap) == 2) - { - $lowerBound = $gap[0]; - $upperBound = $gap[1]; - - $selectAs = "$selectColumnPrefix$lowerBound-$upperBound"; - - $selects[] = "sum(case when $table.$column between $lowerBound and $upperBound $extraCondition". - " then 1 else 0 end) as `$selectAs`"; - } - else - { - $lowerBound = $gap[0]; - - $selectAs = $selectColumnPrefix.($lowerBound + 1).urlencode('+'); - - $selects[] = "sum(case when $table.$column > $lowerBound $extraCondition then 1 else 0 end) as `$selectAs`"; - } - } - - return $selects; - } - - /** - * Converts a database SELECT result into a whole DataTable with two columns and as many - * rows as elements in $row. - * - * The key of each element in $row is used as the value of the first column, and the - * value of each element is used as the second column. - * - * NOTE: $selectAsPrefix can be used to make sure only SOME of the data in $row is used. - * - * @param array $row The database row to convert. - * @param mixed $labelCount The label to use for the second column of the DataTable result. - * @param string $selectAsPrefix A string that identifies which elements of $row to use - * in the result. Every key of $row that starts with this - * value is used. - * @return Piwik_DataTable - */ - public function getSimpleDataTableFromRow($row, $labelCount, $selectAsPrefix = '') - { - // the labels in $row can have prefixes that need to be removed before creating a table - $cleanRow = array(); - - foreach($row as $label => $count) - { - if (empty($selectAsPrefix) || strpos($label, $selectAsPrefix) === 0) - { - $cleanLabel = substr($label, strlen($selectAsPrefix)); - - $cleanRow[$cleanLabel] = array($labelCount => $count); - } - } - - $table = new Piwik_DataTable(); - $table->addRowsFromArrayWithIndexLabel($cleanRow); - return $table; - } - - /** - * Performs a simple query on the log_visit table within the time range this archive - * represents. - * - * @param string $select The SELECT clause. - * @param string|bool $orderBy The ORDER BY clause (without the 'ORDER BY' part). Set to - * false to specify no ORDER BY. - * @param array|bool $groupByCols An array of column names to group by. Set to false to - * specify no GROUP BY. - * @param bool $oneResultRow Whether only one row is expected or not. If set to true, - * this function returns one row, if false, an array of rows. - * @return array - */ - public function queryVisitsSimple($select, $orderBy = false, $groupByCols = false, $oneResultRow = true) - { - $from = "log_visit"; - $where = "log_visit.visit_last_action_time >= ? + + $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); + $query = $segment->getSelectQuery($select, $from, $where, $bind); + + $bind = $query['bind']; + $sql = $query['sql']; + + $data = $this->db->fetchRow($sql, $bind); + + // no visits found + if (!is_array($data) || $data['nb_visits'] == 0) { + return $this->isThereSomeVisits = false; + } + + // visits found: set attribtues + foreach ($data as $name => $value) { + $this->insertNumericRecord($name, $value); + } + + $this->setNumberOfVisits($data['nb_visits']); + $this->setNumberOfVisitsConverted($data['nb_visits_converted']); + + return $this->isThereSomeVisits = true; + } + + return $this->redirectRequestToVisitsSummary(); + } + + /** + * If a segment is specified but a plugin other than 'VisitsSummary' is being requested, + * we create an archive for processing VisitsSummary Core Metrics, which will in turn + * execute the query above (in isThereSomeVisits) + * + * @return bool|null + */ + private function redirectRequestToVisitsSummary() + { + $archive = new Piwik_Archive_Single(); + $archive->setSite($this->site); + $archive->setPeriod($this->period); + $archive->setSegment($this->getSegment()); + $archive->setRequestedReport('VisitsSummary'); + + $nbVisits = $archive->getNumeric('nb_visits'); + $this->isThereSomeVisits = $nbVisits > 0; + + if ($this->isThereSomeVisits) { + $nbVisitsConverted = $archive->getNumeric('nb_visits_converted'); + $this->setNumberOfVisits($nbVisits); + $this->setNumberOfVisitsConverted($nbVisitsConverted); + } + + return $this->isThereSomeVisits; + } + + /** + * Creates and returns an array of SQL SELECT expressions that will summarize + * the data in a column of a specified table, over a set of ranges. + * + * The SELECT expressions will count the number of column values that are + * within each range. + * + * @param string $column The column of the log_conversion table to reduce. + * @param array $ranges The ranges to reduce data over. + * @param string $table The table the SELECTs should use. + * @param string $selectColumnPrefix The prefix when specifying what a SELECT + * expression will be selected AS. + * @param bool|string $extraCondition An extra condition to be appended to 'case when' + * expressions. Must start with the logical operator, + * ie (AND, OR, etc.). + * @return array An array of SQL SELECT expressions. + */ + public static function buildReduceByRangeSelect( + $column, $ranges, $table, $selectColumnPrefix = '', $extraCondition = false) + { + $selects = array(); + + foreach ($ranges as $gap) { + if (count($gap) == 2) { + $lowerBound = $gap[0]; + $upperBound = $gap[1]; + + $selectAs = "$selectColumnPrefix$lowerBound-$upperBound"; + + $selects[] = "sum(case when $table.$column between $lowerBound and $upperBound $extraCondition" . + " then 1 else 0 end) as `$selectAs`"; + } else { + $lowerBound = $gap[0]; + + $selectAs = $selectColumnPrefix . ($lowerBound + 1) . urlencode('+'); + + $selects[] = "sum(case when $table.$column > $lowerBound $extraCondition then 1 else 0 end) as `$selectAs`"; + } + } + + return $selects; + } + + /** + * Converts a database SELECT result into a whole DataTable with two columns and as many + * rows as elements in $row. + * + * The key of each element in $row is used as the value of the first column, and the + * value of each element is used as the second column. + * + * NOTE: $selectAsPrefix can be used to make sure only SOME of the data in $row is used. + * + * @param array $row The database row to convert. + * @param mixed $labelCount The label to use for the second column of the DataTable result. + * @param string $selectAsPrefix A string that identifies which elements of $row to use + * in the result. Every key of $row that starts with this + * value is used. + * @return Piwik_DataTable + */ + public function getSimpleDataTableFromRow($row, $labelCount, $selectAsPrefix = '') + { + // the labels in $row can have prefixes that need to be removed before creating a table + $cleanRow = array(); + + foreach ($row as $label => $count) { + if (empty($selectAsPrefix) || strpos($label, $selectAsPrefix) === 0) { + $cleanLabel = substr($label, strlen($selectAsPrefix)); + + $cleanRow[$cleanLabel] = array($labelCount => $count); + } + } + + $table = new Piwik_DataTable(); + $table->addRowsFromArrayWithIndexLabel($cleanRow); + return $table; + } + + /** + * Performs a simple query on the log_visit table within the time range this archive + * represents. + * + * @param string $select The SELECT clause. + * @param string|bool $orderBy The ORDER BY clause (without the 'ORDER BY' part). Set to + * false to specify no ORDER BY. + * @param array|bool $groupByCols An array of column names to group by. Set to false to + * specify no GROUP BY. + * @param bool $oneResultRow Whether only one row is expected or not. If set to true, + * this function returns one row, if false, an array of rows. + * @return array + */ + public function queryVisitsSimple($select, $orderBy = false, $groupByCols = false, $oneResultRow = true) + { + $from = "log_visit"; + $where = "log_visit.visit_last_action_time >= ? AND log_visit.visit_last_action_time <= ? AND log_visit.idsite = ?"; - - $groupBy = false; - if ($groupByCols and !empty($groupByCols)) - { - $groupBy = implode(',', $groupByCols); - } - - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); - - $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); - - if ($oneResultRow) - { - return $this->db->fetchRow($query['sql'], $query['bind']); - } - else - { - return $this->db->fetchAll($query['sql'], $query['bind']); - } - } - - /** - * Helper function that returns a DataTable containing the $select fields / value pairs. - * IMPORTANT: The $select must return only one row!! - * - * Example $select = "count(distinct( config_os )) as countDistinctOs, - * sum( config_flash ) / count(distinct(idvisit)) as percentFlash " - * $labelCount = "test_column_name" - * will return a dataTable that looks like - * label test_column_name - * CountDistinctOs 9 - * PercentFlash 0.5676 - * - * - * @param string $select - * @param string $labelCount - * @return Piwik_DataTable - */ - public function getSimpleDataTableFromSelect($select, $labelCount) - { - $data = $this->queryVisitsSimple($select); - return $this->getSimpleDataTableFromRow($data, $labelCount); - } - - /** - * Returns the actions by the given dimension - * - * - The basic use case is to use $label and optionally $where. - * - If you want to apply a limit and group the others, use $orderBy to sort the way you - * want the limit to be applied and pass a pre-configured instance of Piwik_RankingQuery. - * The ranking query instance has to have a limit and at least one label column. - * See Piwik_RankingQuery::setLimit() and Piwik_RankingQuery::addLabelColumn(). - * If $rankingQuery is set, the return value is the array returned by - * Piwik_RankingQuery::execute(). - * - By default, the method only queries log_link_visit_action. If you need data from - * log_action (e.g. to partition the result from the ranking query into the different - * action types), use $joinLogActionOnColumn and $addSelect to join log_action and select - * the column you need from log_action. - * - * - * @param array|string $label the dimensions(s) you're interested in - * @param string $where where clause - * @param bool|array $metrics Set this if you want to limit the columns that are returned. - * The possible values in the array are Piwik_Archive::INDEX_*. - * @param bool|string $orderBy order by clause - * @param Piwik_RankingQuery $rankingQuery pre-configured ranking query instance - * @param bool|string $joinLogActionOnColumn column from log_link_visit_action that - * log_action should be joined on. - * can be an array to join multiple times. - * @param bool|string $addSelect additional select clause - * @return mixed - */ - public function queryActionsByDimension($label, $where = '', $metrics = false, $orderBy = false, - $rankingQuery = null, $joinLogActionOnColumn = false, $addSelect = false) - { - if(is_array($label)) - { - $label2 = $label; - foreach($label2 as &$field) { $field = 'log_link_visit_action.'. $field; } - $groupBy = implode(", ", $label2); - foreach($label2 as $id => &$field) { $field = "$field AS ".$label[$id]; } - $select = implode(", ", $label2); - - // IF we query Custom Variables scope "page" either: Product SKU, Product Name, - // then we also query the "Product page view" price which was possibly recorded. - if(in_array(reset($label), array('custom_var_k3','custom_var_k4','custom_var_k5'))) - { - $select .= ", ".self::getSqlRevenue("AVG(log_link_visit_action.custom_var_v2)")." as `". Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED ."`"; - } - } - else - { - $select = $label . " AS label "; - $groupBy = 'label'; - } - - if(!empty($where)) - { - $where = sprintf($where, "log_link_visit_action", "log_link_visit_action"); - $where = ' AND '.$where; - } - - $pre = ", \n\t\t\t"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_VISITS, $metrics)) - $select .= $pre . "count(distinct log_link_visit_action.idvisit) as `". Piwik_Archive::INDEX_NB_VISITS ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_UNIQ_VISITORS, $metrics)) - $select .= $pre . "count(distinct log_link_visit_action.idvisitor) as `". Piwik_Archive::INDEX_NB_UNIQ_VISITORS ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_ACTIONS, $metrics)) - $select .= $pre . "count(*) as `". Piwik_Archive::INDEX_NB_ACTIONS ."`"; - - $from = "log_link_visit_action"; - - if ($joinLogActionOnColumn !== false) - { - $multiJoin = is_array($joinLogActionOnColumn); - if (!$multiJoin) - { - $joinLogActionOnColumn = array($joinLogActionOnColumn); - } - - $from = array($from); - - foreach ($joinLogActionOnColumn as $i => $joinColumn) - { - $tableAlias = 'log_action'.($multiJoin ? $i + 1 : ''); - if (strpos($joinColumn, ' ') === false) { - $joinOn = $tableAlias.'.idaction = log_link_visit_action.'.$joinColumn; - } else { - // more complex join column like IF(...) - $joinOn = $tableAlias.'.idaction = '.$joinColumn; - } - $from[] = array( - 'table' => 'log_action', - 'tableAlias' => $tableAlias, - 'joinOn' => $joinOn - ); - } - } - - if ($addSelect !== false) - { - $select .= ', '.$addSelect; - } - - $where = "log_link_visit_action.server_time >= ? + + $groupBy = false; + if ($groupByCols and !empty($groupByCols)) { + $groupBy = implode(',', $groupByCols); + } + + $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); + + $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); + + if ($oneResultRow) { + return $this->db->fetchRow($query['sql'], $query['bind']); + } else { + return $this->db->fetchAll($query['sql'], $query['bind']); + } + } + + /** + * Helper function that returns a DataTable containing the $select fields / value pairs. + * IMPORTANT: The $select must return only one row!! + * + * Example $select = "count(distinct( config_os )) as countDistinctOs, + * sum( config_flash ) / count(distinct(idvisit)) as percentFlash " + * $labelCount = "test_column_name" + * will return a dataTable that looks like + * label test_column_name + * CountDistinctOs 9 + * PercentFlash 0.5676 + * + * + * @param string $select + * @param string $labelCount + * @return Piwik_DataTable + */ + public function getSimpleDataTableFromSelect($select, $labelCount) + { + $data = $this->queryVisitsSimple($select); + return $this->getSimpleDataTableFromRow($data, $labelCount); + } + + /** + * Returns the actions by the given dimension + * + * - The basic use case is to use $label and optionally $where. + * - If you want to apply a limit and group the others, use $orderBy to sort the way you + * want the limit to be applied and pass a pre-configured instance of Piwik_RankingQuery. + * The ranking query instance has to have a limit and at least one label column. + * See Piwik_RankingQuery::setLimit() and Piwik_RankingQuery::addLabelColumn(). + * If $rankingQuery is set, the return value is the array returned by + * Piwik_RankingQuery::execute(). + * - By default, the method only queries log_link_visit_action. If you need data from + * log_action (e.g. to partition the result from the ranking query into the different + * action types), use $joinLogActionOnColumn and $addSelect to join log_action and select + * the column you need from log_action. + * + * + * @param array|string $label the dimensions(s) you're interested in + * @param string $where where clause + * @param bool|array $metrics Set this if you want to limit the columns that are returned. + * The possible values in the array are Piwik_Archive::INDEX_*. + * @param bool|string $orderBy order by clause + * @param Piwik_RankingQuery $rankingQuery pre-configured ranking query instance + * @param bool|string $joinLogActionOnColumn column from log_link_visit_action that + * log_action should be joined on. + * can be an array to join multiple times. + * @param bool|string $addSelect additional select clause + * @return mixed + */ + public function queryActionsByDimension($label, $where = '', $metrics = false, $orderBy = false, + $rankingQuery = null, $joinLogActionOnColumn = false, $addSelect = false) + { + if (is_array($label)) { + $label2 = $label; + foreach ($label2 as &$field) { + $field = 'log_link_visit_action.' . $field; + } + $groupBy = implode(", ", $label2); + foreach ($label2 as $id => &$field) { + $field = "$field AS " . $label[$id]; + } + $select = implode(", ", $label2); + + // IF we query Custom Variables scope "page" either: Product SKU, Product Name, + // then we also query the "Product page view" price which was possibly recorded. + if (in_array(reset($label), array('custom_var_k3', 'custom_var_k4', 'custom_var_k5'))) { + $select .= ", " . self::getSqlRevenue("AVG(log_link_visit_action.custom_var_v2)") . " as `" . Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED . "`"; + } + } else { + $select = $label . " AS label "; + $groupBy = 'label'; + } + + if (!empty($where)) { + $where = sprintf($where, "log_link_visit_action", "log_link_visit_action"); + $where = ' AND ' . $where; + } + + $pre = ", \n\t\t\t"; + if (!$metrics || in_array(Piwik_Archive::INDEX_NB_VISITS, $metrics)) + $select .= $pre . "count(distinct log_link_visit_action.idvisit) as `" . Piwik_Archive::INDEX_NB_VISITS . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_NB_UNIQ_VISITORS, $metrics)) + $select .= $pre . "count(distinct log_link_visit_action.idvisitor) as `" . Piwik_Archive::INDEX_NB_UNIQ_VISITORS . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_NB_ACTIONS, $metrics)) + $select .= $pre . "count(*) as `" . Piwik_Archive::INDEX_NB_ACTIONS . "`"; + + $from = "log_link_visit_action"; + + if ($joinLogActionOnColumn !== false) { + $multiJoin = is_array($joinLogActionOnColumn); + if (!$multiJoin) { + $joinLogActionOnColumn = array($joinLogActionOnColumn); + } + + $from = array($from); + + foreach ($joinLogActionOnColumn as $i => $joinColumn) { + $tableAlias = 'log_action' . ($multiJoin ? $i + 1 : ''); + if (strpos($joinColumn, ' ') === false) { + $joinOn = $tableAlias . '.idaction = log_link_visit_action.' . $joinColumn; + } else { + // more complex join column like IF(...) + $joinOn = $tableAlias . '.idaction = ' . $joinColumn; + } + $from[] = array( + 'table' => 'log_action', + 'tableAlias' => $tableAlias, + 'joinOn' => $joinOn + ); + } + } + + if ($addSelect !== false) { + $select .= ', ' . $addSelect; + } + + $where = "log_link_visit_action.server_time >= ? AND log_link_visit_action.server_time <= ? AND log_link_visit_action.idsite = ? $where"; - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); - - $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); - - if ($rankingQuery !== null) - { - $sumColumns = array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS, - Piwik_Archive::INDEX_NB_VISITS, - Piwik_Archive::INDEX_NB_ACTIONS - ); - if ($metrics) - { - foreach ($sumColumns as $i => $column) - { - if (!in_array($column, $metrics)) - { - unset($sumColumns[$i]); - } - } - $sumColumns = array_values($sumColumns); - } - $rankingQuery->addColumn($sumColumns, 'sum'); - return $rankingQuery->execute($query['sql'], $query['bind']); - } - - return $this->db->query($query['sql'], $query['bind']); - } - - /** - * Query visits by dimension - * - * @param array|string $label Can be a string, eg. "referer_name", will be aliased as 'label' in the returned rows - * Can also be an array of strings, when the dimension spans multiple fields, - * eg. array("referer_name", "referer_keyword") - * @param string $where Additional condition for WHERE clause - * @param bool|array $metrics Set this if you want to limit the columns that are returned. - * The possible values in the array are Piwik_Archive::INDEX_*. - * @param bool|string $orderBy ORDER BY clause. This is needed in combination with $rankingQuery. - * @param Piwik_RankingQuery $rankingQuery - * A pre-configured ranking query instance that is used to limit the result. - * If set, the return value is the array returned by Piwik_RankingQuery::execute(). - * @param string $addSelect Additional SELECT clause - * @param bool $addSelectGeneratesLabelColumn - * Set to true if the $label column is generated in $addSelect. - * @return mixed - */ - public function queryVisitsByDimension($label, $where = '', $metrics = false, $orderBy = false, - $rankingQuery = null, $addSelect = false, $addSelectGeneratesLabelColumn = false) - { - if(is_array($label)) - { - $groupBy = "log_visit.".implode(", log_visit.", $label); - foreach($label as &$field) - { - $field = 'log_visit.'.$field.' AS '.$field; - } - $select = implode(", ", $label); - } - else if ($addSelectGeneratesLabelColumn) - { - $select = $addSelect; - $groupBy = $label; - } - else - { - $select = $label . " AS label "; - $groupBy = 'label'; - } - - if(!empty($where)) - { - $where = sprintf($where, "log_visit", "log_visit"); - $where = ' AND '.$where; - } - - $pre = ", \n\t\t\t"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_UNIQ_VISITORS, $metrics)) - $select .= $pre . "count(distinct log_visit.idvisitor) as `". Piwik_Archive::INDEX_NB_UNIQ_VISITORS ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_VISITS, $metrics)) - $select .= $pre . "count(*) as `". Piwik_Archive::INDEX_NB_VISITS ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_ACTIONS, $metrics)) - $select .= $pre . "sum(log_visit.visit_total_actions) as `". Piwik_Archive::INDEX_NB_ACTIONS ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_MAX_ACTIONS, $metrics)) - $select .= $pre . "max(log_visit.visit_total_actions) as `". Piwik_Archive::INDEX_MAX_ACTIONS ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_SUM_VISIT_LENGTH, $metrics)) - $select .= $pre . "sum(log_visit.visit_total_time) as `". Piwik_Archive::INDEX_SUM_VISIT_LENGTH ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_BOUNCE_COUNT, $metrics)) - $select .= $pre . "sum(case log_visit.visit_total_actions when 1 then 1 when 0 then 1 else 0 end) as `". Piwik_Archive::INDEX_BOUNCE_COUNT ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_VISITS_CONVERTED, $metrics)) - $select .= $pre . "sum(case log_visit.visit_goal_converted when 1 then 1 else 0 end) as `". Piwik_Archive::INDEX_NB_VISITS_CONVERTED ."`"; - - if ($addSelect && !$addSelectGeneratesLabelColumn) { - $select .= ', '.$addSelect; - } - - $from = "log_visit"; - - $where = "log_visit.visit_last_action_time >= ? + $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); + + $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); + + if ($rankingQuery !== null) { + $sumColumns = array( + Piwik_Archive::INDEX_NB_UNIQ_VISITORS, + Piwik_Archive::INDEX_NB_VISITS, + Piwik_Archive::INDEX_NB_ACTIONS + ); + if ($metrics) { + foreach ($sumColumns as $i => $column) { + if (!in_array($column, $metrics)) { + unset($sumColumns[$i]); + } + } + $sumColumns = array_values($sumColumns); + } + $rankingQuery->addColumn($sumColumns, 'sum'); + return $rankingQuery->execute($query['sql'], $query['bind']); + } + + return $this->db->query($query['sql'], $query['bind']); + } + + /** + * Query visits by dimension + * + * @param array|string $label Can be a string, eg. "referer_name", will be aliased as 'label' in the returned rows + * Can also be an array of strings, when the dimension spans multiple fields, + * eg. array("referer_name", "referer_keyword") + * @param string $where Additional condition for WHERE clause + * @param bool|array $metrics Set this if you want to limit the columns that are returned. + * The possible values in the array are Piwik_Archive::INDEX_*. + * @param bool|string $orderBy ORDER BY clause. This is needed in combination with $rankingQuery. + * @param Piwik_RankingQuery $rankingQuery + * A pre-configured ranking query instance that is used to limit the result. + * If set, the return value is the array returned by Piwik_RankingQuery::execute(). + * @param string $addSelect Additional SELECT clause + * @param bool $addSelectGeneratesLabelColumn + * Set to true if the $label column is generated in $addSelect. + * @return mixed + */ + public function queryVisitsByDimension($label, $where = '', $metrics = false, $orderBy = false, + $rankingQuery = null, $addSelect = false, $addSelectGeneratesLabelColumn = false) + { + if (is_array($label)) { + $groupBy = "log_visit." . implode(", log_visit.", $label); + foreach ($label as &$field) { + $field = 'log_visit.' . $field . ' AS ' . $field; + } + $select = implode(", ", $label); + } else if ($addSelectGeneratesLabelColumn) { + $select = $addSelect; + $groupBy = $label; + } else { + $select = $label . " AS label "; + $groupBy = 'label'; + } + + if (!empty($where)) { + $where = sprintf($where, "log_visit", "log_visit"); + $where = ' AND ' . $where; + } + + $pre = ", \n\t\t\t"; + if (!$metrics || in_array(Piwik_Archive::INDEX_NB_UNIQ_VISITORS, $metrics)) + $select .= $pre . "count(distinct log_visit.idvisitor) as `" . Piwik_Archive::INDEX_NB_UNIQ_VISITORS . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_NB_VISITS, $metrics)) + $select .= $pre . "count(*) as `" . Piwik_Archive::INDEX_NB_VISITS . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_NB_ACTIONS, $metrics)) + $select .= $pre . "sum(log_visit.visit_total_actions) as `" . Piwik_Archive::INDEX_NB_ACTIONS . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_MAX_ACTIONS, $metrics)) + $select .= $pre . "max(log_visit.visit_total_actions) as `" . Piwik_Archive::INDEX_MAX_ACTIONS . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_SUM_VISIT_LENGTH, $metrics)) + $select .= $pre . "sum(log_visit.visit_total_time) as `" . Piwik_Archive::INDEX_SUM_VISIT_LENGTH . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_BOUNCE_COUNT, $metrics)) + $select .= $pre . "sum(case log_visit.visit_total_actions when 1 then 1 when 0 then 1 else 0 end) as `" . Piwik_Archive::INDEX_BOUNCE_COUNT . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_NB_VISITS_CONVERTED, $metrics)) + $select .= $pre . "sum(case log_visit.visit_goal_converted when 1 then 1 else 0 end) as `" . Piwik_Archive::INDEX_NB_VISITS_CONVERTED . "`"; + + if ($addSelect && !$addSelectGeneratesLabelColumn) { + $select .= ', ' . $addSelect; + } + + $from = "log_visit"; + + $where = "log_visit.visit_last_action_time >= ? AND log_visit.visit_last_action_time <= ? AND log_visit.idsite = ? $where"; - - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); - - $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); - - if ($rankingQuery !== null) - { - $sumColumns = array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS, Piwik_Archive::INDEX_NB_VISITS, - Piwik_Archive::INDEX_NB_ACTIONS, Piwik_Archive::INDEX_SUM_VISIT_LENGTH, - Piwik_Archive::INDEX_BOUNCE_COUNT, Piwik_Archive::INDEX_NB_VISITS_CONVERTED - ); - if ($metrics) - { - foreach ($sumColumns as $i => $column) - { - if (!in_array($column, $metrics)) - { - unset($sumColumns[$i]); - } - } - $sumColumns = array_values($sumColumns); - } - $rankingQuery->addColumn($sumColumns, 'sum'); - if (!$metrics || in_array(Piwik_Archive::INDEX_MAX_ACTIONS, $metrics)) - { - $rankingQuery->addColumn(Piwik_Archive::INDEX_MAX_ACTIONS, 'max'); - } - return $rankingQuery->execute($query['sql'], $query['bind']); - } - - return $this->db->query($query['sql'], $query['bind']); - } - - /** - * @see queryVisitsByDimension() Similar to this function, - * but queries metrics for the requested dimensions, - * for each Goal conversion - * - * @param string|array $label - * @param string $where - * @param array $aggregateLabels - * @return - */ - public function queryConversionsByDimension($label, $where = '', $aggregateLabels = array()) - { - if(empty($label)) - { - $select = ""; - $groupBy = ""; - } - elseif(is_array($label)) - { - $groupBy = "log_conversion.".implode(", log_conversion.", $label); - foreach($label as &$field) - { - $field = 'log_conversion.'.$field.' AS '.$field ; - } - $select = implode(", ", $label) . ", "; - } - else - { - $select = $label . " AS label, "; - $groupBy = 'label'; - } - if(!empty($aggregateLabels)) - { - $select .= implode(", ", $aggregateLabels) . ", "; - } - if(!empty($where)) - { - $where = sprintf($where, "log_conversion", "log_conversion"); - $where = ' AND '.$where; - } - - $select .= self::getSqlRevenue('SUM(log_conversion.revenue_subtotal)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL ."`,". - self::getSqlRevenue('SUM(log_conversion.revenue_tax)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX ."`,". - self::getSqlRevenue('SUM(log_conversion.revenue_shipping)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING ."`,". - self::getSqlRevenue('SUM(log_conversion.revenue_discount)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT ."`,". - "SUM(log_conversion.items) as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS ."`, "; - - $groupBy = !empty($groupBy) ? ", $groupBy" : ''; - - $select = "$select + + $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); + + $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); + + if ($rankingQuery !== null) { + $sumColumns = array( + Piwik_Archive::INDEX_NB_UNIQ_VISITORS, Piwik_Archive::INDEX_NB_VISITS, + Piwik_Archive::INDEX_NB_ACTIONS, Piwik_Archive::INDEX_SUM_VISIT_LENGTH, + Piwik_Archive::INDEX_BOUNCE_COUNT, Piwik_Archive::INDEX_NB_VISITS_CONVERTED + ); + if ($metrics) { + foreach ($sumColumns as $i => $column) { + if (!in_array($column, $metrics)) { + unset($sumColumns[$i]); + } + } + $sumColumns = array_values($sumColumns); + } + $rankingQuery->addColumn($sumColumns, 'sum'); + if (!$metrics || in_array(Piwik_Archive::INDEX_MAX_ACTIONS, $metrics)) { + $rankingQuery->addColumn(Piwik_Archive::INDEX_MAX_ACTIONS, 'max'); + } + return $rankingQuery->execute($query['sql'], $query['bind']); + } + + return $this->db->query($query['sql'], $query['bind']); + } + + /** + * @see queryVisitsByDimension() Similar to this function, + * but queries metrics for the requested dimensions, + * for each Goal conversion + * + * @param string|array $label + * @param string $where + * @param array $aggregateLabels + * @return + */ + public function queryConversionsByDimension($label, $where = '', $aggregateLabels = array()) + { + if (empty($label)) { + $select = ""; + $groupBy = ""; + } elseif (is_array($label)) { + $groupBy = "log_conversion." . implode(", log_conversion.", $label); + foreach ($label as &$field) { + $field = 'log_conversion.' . $field . ' AS ' . $field; + } + $select = implode(", ", $label) . ", "; + } else { + $select = $label . " AS label, "; + $groupBy = 'label'; + } + if (!empty($aggregateLabels)) { + $select .= implode(", ", $aggregateLabels) . ", "; + } + if (!empty($where)) { + $where = sprintf($where, "log_conversion", "log_conversion"); + $where = ' AND ' . $where; + } + + $select .= self::getSqlRevenue('SUM(log_conversion.revenue_subtotal)') . " as `" . Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL . "`," . + self::getSqlRevenue('SUM(log_conversion.revenue_tax)') . " as `" . Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX . "`," . + self::getSqlRevenue('SUM(log_conversion.revenue_shipping)') . " as `" . Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING . "`," . + self::getSqlRevenue('SUM(log_conversion.revenue_discount)') . " as `" . Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT . "`," . + "SUM(log_conversion.items) as `" . Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS . "`, "; + + $groupBy = !empty($groupBy) ? ", $groupBy" : ''; + + $select = "$select log_conversion.idgoal, - count(*) as `". Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS ."`, - ".self::getSqlRevenue('SUM(log_conversion.revenue)')." as `". Piwik_Archive::INDEX_GOAL_REVENUE ."`, - count(distinct log_conversion.idvisit) as `". Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED."`"; - - $from = "log_conversion"; - - $where = "log_conversion.server_time >= ? + count(*) as `" . Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS . "`, + " . self::getSqlRevenue('SUM(log_conversion.revenue)') . " as `" . Piwik_Archive::INDEX_GOAL_REVENUE . "`, + count(distinct log_conversion.idvisit) as `" . Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED . "`"; + + $from = "log_conversion"; + + $where = "log_conversion.server_time >= ? AND log_conversion.server_time <= ? AND log_conversion.idsite = ? $where"; - - $groupBy = "log_conversion.idgoal $groupBy"; - - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); - - $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy=false, $groupBy); - + + $groupBy = "log_conversion.idgoal $groupBy"; + + $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); + + $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy = false, $groupBy); + return $this->db->query($query['sql'], $query['bind']); - } - - /** - * Returns the ecommerce items - * - * @param string $field - * @return string - */ - public function queryEcommerceItems($field) - { - $query = "SELECT + } + + /** + * Returns the ecommerce items + * + * @param string $field + * @return string + */ + public function queryEcommerceItems($field) + { + $query = "SELECT name as label, - ".self::getSqlRevenue('SUM(quantity * price)')." as `". Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE ."`, - ".self::getSqlRevenue('SUM(quantity)')." as `". Piwik_Archive::INDEX_ECOMMERCE_ITEM_QUANTITY ."`, - ".self::getSqlRevenue('SUM(price)')." as `". Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE ."`, - count(distinct idorder) as `". Piwik_Archive::INDEX_ECOMMERCE_ORDERS."`, - count(idvisit) as `". Piwik_Archive::INDEX_NB_VISITS."`, - case idorder when '0' then ".Piwik_Tracker_GoalManager::IDGOAL_CART." else ".Piwik_Tracker_GoalManager::IDGOAL_ORDER." end as ecommerceType - FROM ".Piwik_Common::prefixTable('log_conversion_item')." - LEFT JOIN ".Piwik_Common::prefixTable('log_action')." + " . self::getSqlRevenue('SUM(quantity * price)') . " as `" . Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE . "`, + " . self::getSqlRevenue('SUM(quantity)') . " as `" . Piwik_Archive::INDEX_ECOMMERCE_ITEM_QUANTITY . "`, + " . self::getSqlRevenue('SUM(price)') . " as `" . Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE . "`, + count(distinct idorder) as `" . Piwik_Archive::INDEX_ECOMMERCE_ORDERS . "`, + count(idvisit) as `" . Piwik_Archive::INDEX_NB_VISITS . "`, + case idorder when '0' then " . Piwik_Tracker_GoalManager::IDGOAL_CART . " else " . Piwik_Tracker_GoalManager::IDGOAL_ORDER . " end as ecommerceType + FROM " . Piwik_Common::prefixTable('log_conversion_item') . " + LEFT JOIN " . Piwik_Common::prefixTable('log_action') . " ON $field = idaction WHERE server_time >= ? AND server_time <= ? @@ -633,433 +588,406 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing AND deleted = 0 GROUP BY ecommerceType, $field ORDER BY NULL"; - - $bind = array( $this->getStartDatetimeUTC(), - $this->getEndDatetimeUTC(), - $this->idsite + + $bind = array($this->getStartDatetimeUTC(), + $this->getEndDatetimeUTC(), + $this->idsite + ); + $query = $this->db->query($query, $bind); + return $query; + } + + /** + * @param string $field + * @return string + */ + static public function getSqlRevenue($field) + { + return "ROUND(" . $field . "," . Piwik_Tracker_GoalManager::REVENUE_PRECISION . ")"; + } + + /** + * Converts the given array to a datatable + * @param array $array + * @return Piwik_DataTable + */ + static public function getDataTableFromArray($array) + { + $table = new Piwik_DataTable(); + $table->addRowsFromArrayWithIndexLabel($array); + return $table; + } + + /** + * Output: + * array( + * LABEL => array( + * Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, + * Piwik_Archive::INDEX_NB_VISITS => 0 + * ), + * LABEL2 => array( + * [...] + * ) + * ) + * + * Helper function that returns an array with common statistics for a given database field distinct values. + * + * The statistics returned are: + * - number of unique visitors + * - number of visits + * - number of actions + * - maximum number of action for a visit + * - sum of the visits' length in sec + * - count of bouncing visits (visits with one page view) + * + * For example if $label = 'config_os' it will return the statistics for every distinct Operating systems + * The returned array will have a row per distinct operating systems, + * and a column per stat (nb of visits, max actions, etc) + * + * 'label' Piwik_Archive::INDEX_NB_UNIQ_VISITORS Piwik_Archive::INDEX_NB_VISITS etc. + * Linux 27 66 ... + * Windows XP 12 ... + * Mac OS 15 36 ... + * + * @param string $label Table log_visit field name to be use to compute common stats + * @return array + */ + public function getArrayInterestForLabel($label) + { + $query = $this->queryVisitsByDimension($label); + $interest = array(); + while ($row = $query->fetch()) { + if (!isset($interest[$row['label']])) $interest[$row['label']] = $this->getNewInterestRow(); + $this->updateInterestStats($row, $interest[$row['label']]); + } + return $interest; + } + + /** + * Generates a dataTable given a multidimensional PHP array that associates LABELS to Piwik_DataTableRows + * This is used for the "Actions" DataTable, where a line is the aggregate of all the subtables + * Example: the category /blog has 3 visits because it has /blog/index (2 visits) + /blog/about (1 visit) + * + * @param array $table + * @param array $parents + * @return Piwik_DataTable + */ + static public function generateDataTable($table, $parents = array()) + { + $dataTableToReturn = new Piwik_DataTable(); + foreach ($table as $label => $maybeDatatableRow) { + // case the aInfo is a subtable-like array + // it means that we have to go recursively and process it + // then we build the row that is an aggregate of all the children + // and we associate this row to the subtable + if (!($maybeDatatableRow instanceof Piwik_DataTable_Row)) { + array_push($parents, array($dataTableToReturn->getId(), $label)); + + $subTable = self::generateDataTable($maybeDatatableRow, $parents); + $subTable->setParents($parents); + $row = new Piwik_DataTable_Row_DataTableSummary($subTable); + $row->setColumns(array('label' => $label) + $row->getColumns()); + $row->addSubtable($subTable); + + array_pop($parents); + } // if aInfo is a simple Row we build it + else { + $row = $maybeDatatableRow; + } + + if ($row->getMetadata('issummaryrow') == true) { + $row->deleteMetadata('issummaryrow'); + $dataTableToReturn->addSummaryRow($row); + } else { + $dataTableToReturn->addRow($row); + } + } + return $dataTableToReturn; + } + + /** + * Helper function that returns the serialized DataTable of the given PHP array. + * The array must have the format of Piwik_DataTable::addRowsFromArrayWithIndexLabel() + * Example: array ( + * LABEL => array(col1 => X, col2 => Y), + * LABEL2 => array(col1 => X, col2 => Y), + * ) + * + * @param array $array at the given format + * @return array Array with one element: the serialized data table string + */ + public function getDataTableSerialized($array) + { + $table = new Piwik_DataTable(); + $table->addRowsFromArrayWithIndexLabel($array); + $toReturn = $table->getSerialized(); + return $toReturn; + } + + + /** + * Helper function that returns the multiple serialized DataTable of the given PHP array. + * The DataTable here associates a subtable to every row of the level 0 array. + * This is used for example for search engines. + * Every search engine (level 0) has a subtable containing the keywords. + * + * The $arrayLevel0 must have the format + * Example: array ( + * // Yahoo.com => array( kwd1 => stats, kwd2 => stats ) + * LABEL => array(col1 => X, col2 => Y), + * LABEL2 => array(col1 => X, col2 => Y), + * ) + * + * The $subArrayLevel1ByKey must have the format + * Example: array( + * // Yahoo.com => array( stats ) + * LABEL => #Piwik_DataTable_ForLABEL, + * LABEL2 => #Piwik_DataTable_ForLABEL2, + * ) + * + * + * @param array $arrayLevel0 + * @param array $subArrayLevel1ByKey Array of Piwik_DataTable + * @return array Array with N elements: the strings of the datatable serialized + */ + public function getDataTableWithSubtablesFromArraysIndexedByLabel($arrayLevel0, $subArrayLevel1ByKey) + { + $parentTableLevel0 = new Piwik_DataTable(); + + $tablesByLabel = array(); + foreach ($arrayLevel0 as $label => $aAllRowsForThisLabel) { + $table = new Piwik_DataTable(); + $table->addRowsFromArrayWithIndexLabel($aAllRowsForThisLabel); + $tablesByLabel[$label] = $table; + } + $parentTableLevel0->addRowsFromArrayWithIndexLabel($subArrayLevel1ByKey, $tablesByLabel); + + return $parentTableLevel0; + } + + /** + * Returns an empty row containing default values for the common stat + * + * @param bool $onlyMetricsAvailableInActionsTable + * @param bool $doNotSumVisits + * @return array + */ + public function getNewInterestRow($onlyMetricsAvailableInActionsTable = false, $doNotSumVisits = false) + { + if ($onlyMetricsAvailableInActionsTable) { + if ($doNotSumVisits) { + return array(Piwik_Archive::INDEX_NB_ACTIONS => 0); + } + return array( + Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, + Piwik_Archive::INDEX_NB_VISITS => 0, + Piwik_Archive::INDEX_NB_ACTIONS => 0); + } + return array(Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, + Piwik_Archive::INDEX_NB_VISITS => 0, + Piwik_Archive::INDEX_NB_ACTIONS => 0, + Piwik_Archive::INDEX_MAX_ACTIONS => 0, + Piwik_Archive::INDEX_SUM_VISIT_LENGTH => 0, + Piwik_Archive::INDEX_BOUNCE_COUNT => 0, + Piwik_Archive::INDEX_NB_VISITS_CONVERTED => 0, + ); + } + + + /** + * Returns a Piwik_DataTable_Row containing default values for common stat, + * plus a column 'label' with the value $label + * + * @param string $label + * @return Piwik_DataTable_Row + */ + public function getNewInterestRowLabeled($label) + { + return new Piwik_DataTable_Row( + array( + Piwik_DataTable_Row::COLUMNS => array('label' => $label) + + $this->getNewInterestRow() + ) + ); + } + + /** + * Adds the given row $newRowToAdd to the existing $oldRowToUpdate passed by reference + * + * The rows are php arrays Name => value + * + * @param array $newRowToAdd + * @param array $oldRowToUpdate + * @param bool $onlyMetricsAvailableInActionsTable + * @param bool $doNotSumVisits + * @return + */ + public function updateInterestStats($newRowToAdd, &$oldRowToUpdate, $onlyMetricsAvailableInActionsTable = false, $doNotSumVisits = false) + { + // Pre 1.2 format: string indexed rows are returned from the DB + // Left here for Backward compatibility with plugins doing custom SQL queries using these metrics as string + if (!isset($newRowToAdd[Piwik_Archive::INDEX_NB_VISITS])) { + if (!$doNotSumVisits) { + $oldRowToUpdate[Piwik_Archive::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd['nb_uniq_visitors']; + $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS] += $newRowToAdd['nb_visits']; + } + $oldRowToUpdate[Piwik_Archive::INDEX_NB_ACTIONS] += $newRowToAdd['nb_actions']; + if ($onlyMetricsAvailableInActionsTable) { + return; + } + $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd['max_actions'], $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS]); + $oldRowToUpdate[Piwik_Archive::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd['sum_visit_length']; + $oldRowToUpdate[Piwik_Archive::INDEX_BOUNCE_COUNT] += $newRowToAdd['bounce_count']; + $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS_CONVERTED] += $newRowToAdd['nb_visits_converted']; + return; + } + if (!$doNotSumVisits) { + $oldRowToUpdate[Piwik_Archive::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd[Piwik_Archive::INDEX_NB_UNIQ_VISITORS]; + $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS] += $newRowToAdd[Piwik_Archive::INDEX_NB_VISITS]; + } + $oldRowToUpdate[Piwik_Archive::INDEX_NB_ACTIONS] += $newRowToAdd[Piwik_Archive::INDEX_NB_ACTIONS]; + + // Hack for Price tracking on Ecommerce product/category pages + // The price is not summed, but AVG is taken in the SQL query + $index = Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED; + if (!empty($newRowToAdd[$index])) { + $oldRowToUpdate[$index] = (float)$newRowToAdd[$index]; + } + + if ($onlyMetricsAvailableInActionsTable) { + return; + } + $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd[Piwik_Archive::INDEX_MAX_ACTIONS], $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS]); + $oldRowToUpdate[Piwik_Archive::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd[Piwik_Archive::INDEX_SUM_VISIT_LENGTH]; + $oldRowToUpdate[Piwik_Archive::INDEX_BOUNCE_COUNT] += $newRowToAdd[Piwik_Archive::INDEX_BOUNCE_COUNT]; + $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS_CONVERTED] += $newRowToAdd[Piwik_Archive::INDEX_NB_VISITS_CONVERTED]; + + } + + /** + * Given an array of stats, it will process the sum of goal conversions + * and sum of revenue and add it in the stats array in two new fields. + * + * @param array $interestByLabel Passed by reference, it will be modified as follows: + * Input: + * array( + * LABEL => array( Piwik_Archive::INDEX_NB_VISITS => X, + * Piwik_Archive::INDEX_GOALS => array( + * idgoal1 => array( [...] ), + * idgoal2 => array( [...] ), + * ), + * [...] ), + * LABEL2 => array( Piwik_Archive::INDEX_NB_VISITS => Y, [...] ) + * ); + * + * + * Output: + * array( + * LABEL => array( Piwik_Archive::INDEX_NB_VISITS => X, + * Piwik_Archive::INDEX_NB_CONVERSIONS => Y, // sum of all conversions + * Piwik_Archive::INDEX_REVENUE => Z, // sum of all revenue + * Piwik_Archive::INDEX_GOALS => array( + * idgoal1 => array( [...] ), + * idgoal2 => array( [...] ), + * ), + * [...] ), + * LABEL2 => array( Piwik_Archive::INDEX_NB_VISITS => Y, [...] ) + * ); + * ) + * + * @param array $interestByLabel Passed by reference, will be modified + */ + function enrichConversionsByLabelArray(&$interestByLabel) + { + foreach ($interestByLabel as $label => &$values) { + if (isset($values[Piwik_Archive::INDEX_GOALS])) { + // When per goal metrics are processed, general 'visits converted' is not meaningful because + // it could differ from the sum of each goal conversions + unset($values[Piwik_Archive::INDEX_NB_VISITS_CONVERTED]); + $revenue = $conversions = 0; + foreach ($values[Piwik_Archive::INDEX_GOALS] as $idgoal => $goalValues) { + // Do not sum Cart revenue since it is a lost revenue + if ($idgoal >= Piwik_Tracker_GoalManager::IDGOAL_ORDER) { + $revenue += $goalValues[Piwik_Archive::INDEX_GOAL_REVENUE]; + $conversions += $goalValues[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; + } + } + $values[Piwik_Archive::INDEX_NB_CONVERSIONS] = $conversions; + + // 25.00 recorded as 25 + if (round($revenue) == $revenue) { + $revenue = round($revenue); + } + $values[Piwik_Archive::INDEX_REVENUE] = $revenue; + } + } + } + + /** + * + * @param array $interestByLabelAndSubLabel Passed by reference, will be modified + */ + function enrichConversionsByLabelArrayHasTwoLevels(&$interestByLabelAndSubLabel) + { + foreach ($interestByLabelAndSubLabel as $mainLabel => &$interestBySubLabel) { + $this->enrichConversionsByLabelArray($interestBySubLabel); + } + } + + /** + * + * @param $newRowToAdd + * @param $oldRowToUpdate + */ + function updateGoalStats($newRowToAdd, &$oldRowToUpdate) + { + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED]; + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_REVENUE] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_REVENUE]; + + // Cart & Order + if (isset($oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS])) { + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS]; + + // Order only + if (isset($oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL])) { + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL]; + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX]; + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING]; + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT]; + } + } + } + + /** + * + * @param $idGoal + * @return array + */ + function getNewGoalRow($idGoal) + { + if ($idGoal > Piwik_Tracker_GoalManager::IDGOAL_ORDER) { + return array(Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, + Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0, + Piwik_Archive::INDEX_GOAL_REVENUE => 0, + ); + } + if ($idGoal == Piwik_Tracker_GoalManager::IDGOAL_ORDER) { + return array(Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, + Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0, + Piwik_Archive::INDEX_GOAL_REVENUE => 0, + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => 0, + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => 0, + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => 0, + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => 0, + Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 0, + ); + } + // $row['idgoal'] == Piwik_Tracker_GoalManager::IDGOAL_CART + return array(Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, + Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0, + Piwik_Archive::INDEX_GOAL_REVENUE => 0, + Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 0, ); - $query = $this->db->query($query, $bind); - return $query; - } - - /** - * @param string $field - * @return string - */ - static public function getSqlRevenue($field) - { - return "ROUND(".$field.",".Piwik_Tracker_GoalManager::REVENUE_PRECISION.")"; - } - - /** - * Converts the given array to a datatable - * @param array $array - * @return Piwik_DataTable - */ - static public function getDataTableFromArray( $array ) - { - $table = new Piwik_DataTable(); - $table->addRowsFromArrayWithIndexLabel($array); - return $table; - } - - /** - * Output: - * array( - * LABEL => array( - * Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, - * Piwik_Archive::INDEX_NB_VISITS => 0 - * ), - * LABEL2 => array( - * [...] - * ) - * ) - * - * Helper function that returns an array with common statistics for a given database field distinct values. - * - * The statistics returned are: - * - number of unique visitors - * - number of visits - * - number of actions - * - maximum number of action for a visit - * - sum of the visits' length in sec - * - count of bouncing visits (visits with one page view) - * - * For example if $label = 'config_os' it will return the statistics for every distinct Operating systems - * The returned array will have a row per distinct operating systems, - * and a column per stat (nb of visits, max actions, etc) - * - * 'label' Piwik_Archive::INDEX_NB_UNIQ_VISITORS Piwik_Archive::INDEX_NB_VISITS etc. - * Linux 27 66 ... - * Windows XP 12 ... - * Mac OS 15 36 ... - * - * @param string $label Table log_visit field name to be use to compute common stats - * @return array - */ - public function getArrayInterestForLabel($label) - { - $query = $this->queryVisitsByDimension($label); - $interest = array(); - while($row = $query->fetch()) - { - if(!isset($interest[$row['label']])) $interest[$row['label']]= $this->getNewInterestRow(); - $this->updateInterestStats( $row, $interest[$row['label']]); - } - return $interest; - } - - /** - * Generates a dataTable given a multidimensional PHP array that associates LABELS to Piwik_DataTableRows - * This is used for the "Actions" DataTable, where a line is the aggregate of all the subtables - * Example: the category /blog has 3 visits because it has /blog/index (2 visits) + /blog/about (1 visit) - * - * @param array $table - * @param array $parents - * @return Piwik_DataTable - */ - static public function generateDataTable( $table, $parents=array() ) - { - $dataTableToReturn = new Piwik_DataTable(); - foreach($table as $label => $maybeDatatableRow) - { - // case the aInfo is a subtable-like array - // it means that we have to go recursively and process it - // then we build the row that is an aggregate of all the children - // and we associate this row to the subtable - if( !($maybeDatatableRow instanceof Piwik_DataTable_Row) ) - { - array_push($parents, array($dataTableToReturn->getId(), $label)); - - $subTable = self::generateDataTable($maybeDatatableRow, $parents); - $subTable->setParents($parents); - $row = new Piwik_DataTable_Row_DataTableSummary( $subTable ); - $row->setColumns( array('label' => $label) + $row->getColumns()); - $row->addSubtable($subTable); - - array_pop($parents); - } - // if aInfo is a simple Row we build it - else - { - $row = $maybeDatatableRow; - } - - if ($row->getMetadata('issummaryrow') == true) - { - $row->deleteMetadata('issummaryrow'); - $dataTableToReturn->addSummaryRow($row); - } - else - { - $dataTableToReturn->addRow($row); - } - } - return $dataTableToReturn; - } - - /** - * Helper function that returns the serialized DataTable of the given PHP array. - * The array must have the format of Piwik_DataTable::addRowsFromArrayWithIndexLabel() - * Example: array ( - * LABEL => array(col1 => X, col2 => Y), - * LABEL2 => array(col1 => X, col2 => Y), - * ) - * - * @param array $array at the given format - * @return array Array with one element: the serialized data table string - */ - public function getDataTableSerialized( $array ) - { - $table = new Piwik_DataTable(); - $table->addRowsFromArrayWithIndexLabel($array ); - $toReturn = $table->getSerialized(); - return $toReturn; - } - - - /** - * Helper function that returns the multiple serialized DataTable of the given PHP array. - * The DataTable here associates a subtable to every row of the level 0 array. - * This is used for example for search engines. - * Every search engine (level 0) has a subtable containing the keywords. - * - * The $arrayLevel0 must have the format - * Example: array ( - * // Yahoo.com => array( kwd1 => stats, kwd2 => stats ) - * LABEL => array(col1 => X, col2 => Y), - * LABEL2 => array(col1 => X, col2 => Y), - * ) - * - * The $subArrayLevel1ByKey must have the format - * Example: array( - * // Yahoo.com => array( stats ) - * LABEL => #Piwik_DataTable_ForLABEL, - * LABEL2 => #Piwik_DataTable_ForLABEL2, - * ) - * - * - * @param array $arrayLevel0 - * @param array $subArrayLevel1ByKey Array of Piwik_DataTable - * @return array Array with N elements: the strings of the datatable serialized - */ - public function getDataTableWithSubtablesFromArraysIndexedByLabel( $arrayLevel0, $subArrayLevel1ByKey ) - { - $parentTableLevel0 = new Piwik_DataTable(); - - $tablesByLabel = array(); - foreach($arrayLevel0 as $label => $aAllRowsForThisLabel) - { - $table = new Piwik_DataTable(); - $table->addRowsFromArrayWithIndexLabel($aAllRowsForThisLabel); - $tablesByLabel[$label] = $table; - } - $parentTableLevel0->addRowsFromArrayWithIndexLabel($subArrayLevel1ByKey, $tablesByLabel); - - return $parentTableLevel0; - } - - /** - * Returns an empty row containing default values for the common stat - * - * @param bool $onlyMetricsAvailableInActionsTable - * @param bool $doNotSumVisits - * @return array - */ - public function getNewInterestRow($onlyMetricsAvailableInActionsTable = false, $doNotSumVisits = false) - { - if($onlyMetricsAvailableInActionsTable) - { - if($doNotSumVisits) - { - return array(Piwik_Archive::INDEX_NB_ACTIONS => 0 ); - } - return array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, - Piwik_Archive::INDEX_NB_VISITS => 0, - Piwik_Archive::INDEX_NB_ACTIONS => 0 ); - } - return array( Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, - Piwik_Archive::INDEX_NB_VISITS => 0, - Piwik_Archive::INDEX_NB_ACTIONS => 0, - Piwik_Archive::INDEX_MAX_ACTIONS => 0, - Piwik_Archive::INDEX_SUM_VISIT_LENGTH => 0, - Piwik_Archive::INDEX_BOUNCE_COUNT => 0, - Piwik_Archive::INDEX_NB_VISITS_CONVERTED=> 0, - ); - } - - - /** - * Returns a Piwik_DataTable_Row containing default values for common stat, - * plus a column 'label' with the value $label - * - * @param string $label - * @return Piwik_DataTable_Row - */ - public function getNewInterestRowLabeled( $label ) - { - return new Piwik_DataTable_Row( - array( - Piwik_DataTable_Row::COLUMNS => array( 'label' => $label) - + $this->getNewInterestRow() - ) - ); - } - - /** - * Adds the given row $newRowToAdd to the existing $oldRowToUpdate passed by reference - * - * The rows are php arrays Name => value - * - * @param array $newRowToAdd - * @param array $oldRowToUpdate - * @param bool $onlyMetricsAvailableInActionsTable - * @param bool $doNotSumVisits - * @return - */ - public function updateInterestStats( $newRowToAdd, &$oldRowToUpdate, $onlyMetricsAvailableInActionsTable = false, $doNotSumVisits = false) - { - // Pre 1.2 format: string indexed rows are returned from the DB - // Left here for Backward compatibility with plugins doing custom SQL queries using these metrics as string - if(!isset($newRowToAdd[Piwik_Archive::INDEX_NB_VISITS])) - { - if(!$doNotSumVisits) - { - $oldRowToUpdate[Piwik_Archive::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd['nb_uniq_visitors']; - $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS] += $newRowToAdd['nb_visits']; - } - $oldRowToUpdate[Piwik_Archive::INDEX_NB_ACTIONS] += $newRowToAdd['nb_actions']; - if($onlyMetricsAvailableInActionsTable) - { - return; - } - $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd['max_actions'], $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS]); - $oldRowToUpdate[Piwik_Archive::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd['sum_visit_length']; - $oldRowToUpdate[Piwik_Archive::INDEX_BOUNCE_COUNT] += $newRowToAdd['bounce_count']; - $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS_CONVERTED] += $newRowToAdd['nb_visits_converted']; - return; - } - if(!$doNotSumVisits) - { - $oldRowToUpdate[Piwik_Archive::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd[Piwik_Archive::INDEX_NB_UNIQ_VISITORS]; - $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS] += $newRowToAdd[Piwik_Archive::INDEX_NB_VISITS]; - } - $oldRowToUpdate[Piwik_Archive::INDEX_NB_ACTIONS] += $newRowToAdd[Piwik_Archive::INDEX_NB_ACTIONS]; - - // Hack for Price tracking on Ecommerce product/category pages - // The price is not summed, but AVG is taken in the SQL query - $index = Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED; - if(!empty($newRowToAdd[$index])) - { - $oldRowToUpdate[$index] = (float)$newRowToAdd[$index]; - } - - if($onlyMetricsAvailableInActionsTable) - { - return; - } - $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd[Piwik_Archive::INDEX_MAX_ACTIONS], $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS]); - $oldRowToUpdate[Piwik_Archive::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd[Piwik_Archive::INDEX_SUM_VISIT_LENGTH]; - $oldRowToUpdate[Piwik_Archive::INDEX_BOUNCE_COUNT] += $newRowToAdd[Piwik_Archive::INDEX_BOUNCE_COUNT]; - $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS_CONVERTED] += $newRowToAdd[Piwik_Archive::INDEX_NB_VISITS_CONVERTED]; - - } - - /** - * Given an array of stats, it will process the sum of goal conversions - * and sum of revenue and add it in the stats array in two new fields. - * - * @param array $interestByLabel Passed by reference, it will be modified as follows: - * Input: - * array( - * LABEL => array( Piwik_Archive::INDEX_NB_VISITS => X, - * Piwik_Archive::INDEX_GOALS => array( - * idgoal1 => array( [...] ), - * idgoal2 => array( [...] ), - * ), - * [...] ), - * LABEL2 => array( Piwik_Archive::INDEX_NB_VISITS => Y, [...] ) - * ); - * - * - * Output: - * array( - * LABEL => array( Piwik_Archive::INDEX_NB_VISITS => X, - * Piwik_Archive::INDEX_NB_CONVERSIONS => Y, // sum of all conversions - * Piwik_Archive::INDEX_REVENUE => Z, // sum of all revenue - * Piwik_Archive::INDEX_GOALS => array( - * idgoal1 => array( [...] ), - * idgoal2 => array( [...] ), - * ), - * [...] ), - * LABEL2 => array( Piwik_Archive::INDEX_NB_VISITS => Y, [...] ) - * ); - * ) - * - * @param array $interestByLabel Passed by reference, will be modified - */ - function enrichConversionsByLabelArray(&$interestByLabel) - { - foreach($interestByLabel as $label => &$values) - { - if(isset($values[Piwik_Archive::INDEX_GOALS])) - { - // When per goal metrics are processed, general 'visits converted' is not meaningful because - // it could differ from the sum of each goal conversions - unset($values[Piwik_Archive::INDEX_NB_VISITS_CONVERTED]); - $revenue = $conversions = 0; - foreach($values[Piwik_Archive::INDEX_GOALS] as $idgoal => $goalValues) - { - // Do not sum Cart revenue since it is a lost revenue - if($idgoal >= Piwik_Tracker_GoalManager::IDGOAL_ORDER) - { - $revenue += $goalValues[Piwik_Archive::INDEX_GOAL_REVENUE]; - $conversions += $goalValues[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; - } - } - $values[Piwik_Archive::INDEX_NB_CONVERSIONS] = $conversions; - - // 25.00 recorded as 25 - if(round($revenue) == $revenue) - { - $revenue = round($revenue); - } - $values[Piwik_Archive::INDEX_REVENUE] = $revenue; - } - } - } - - /** - * - * @param array $interestByLabelAndSubLabel Passed by reference, will be modified - */ - function enrichConversionsByLabelArrayHasTwoLevels(&$interestByLabelAndSubLabel) - { - foreach($interestByLabelAndSubLabel as $mainLabel => &$interestBySubLabel) - { - $this->enrichConversionsByLabelArray($interestBySubLabel); - } - } - - /** - * - * @param $newRowToAdd - * @param $oldRowToUpdate - */ - function updateGoalStats($newRowToAdd, &$oldRowToUpdate) - { - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED]; - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_REVENUE] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_REVENUE]; - - // Cart & Order - if(isset($oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS])) - { - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS]; - - // Order only - if(isset($oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL])) - { - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL]; - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX]; - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING]; - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT]; - } - } - } - - /** - * - * @param $idGoal - * @return array - */ - function getNewGoalRow($idGoal) - { - if($idGoal > Piwik_Tracker_GoalManager::IDGOAL_ORDER) - { - return array( Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, - Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0, - Piwik_Archive::INDEX_GOAL_REVENUE => 0, - ); - } - if($idGoal == Piwik_Tracker_GoalManager::IDGOAL_ORDER) - { - return array( Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, - Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0, - Piwik_Archive::INDEX_GOAL_REVENUE => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 0, - ); - } - // $row['idgoal'] == Piwik_Tracker_GoalManager::IDGOAL_CART - return array( Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, - Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0, - Piwik_Archive::INDEX_GOAL_REVENUE => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 0, - ); - } + } } diff --git a/core/ArchiveProcessing/Period.php b/core/ArchiveProcessing/Period.php index c6d38a512c..0ca9ce171b 100644 --- a/core/ArchiveProcessing/Period.php +++ b/core/ArchiveProcessing/Period.php @@ -1,472 +1,444 @@ column name renamed) of the columns for which sum operation is invalid. - * The summed value is not accurate and these columns will be renamed accordingly. + /** + * Array of (column name before => column name renamed) of the columns for which sum operation is invalid. + * The summed value is not accurate and these columns will be renamed accordingly. * @var array - */ - static public $invalidSummedColumnNameToRenamedName = array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS => Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS - ); - - /** - * @var Piwik_Archive_Single[] - */ - public $archives = array(); - - /** - * Sums all values for the given field names $aNames over the period - * See @archiveNumericValuesGeneral for more information - * - * @param string|array $aNames - * @return array - */ - public function archiveNumericValuesSum( $aNames ) - { - return $this->archiveNumericValuesGeneral($aNames, 'sum'); - } - - /** - * Get the maximum value for all values for the given field names $aNames over the period - * See @archiveNumericValuesGeneral for more information - * - * @param string|array $aNames - * @return array - */ - public function archiveNumericValuesMax( $aNames ) - { - return $this->archiveNumericValuesGeneral($aNames, 'max'); - } - - /** - * Given a list of fields names, the method will fetch all their values over the period, and archive them using the given operation. - * - * For example if $operationToApply = 'sum' and $aNames = array('nb_visits', 'sum_time_visit') - * it will sum all values of nb_visits for the period (for example give the number of visits for the month by summing the visits of every day) - * - * @param array|string $aNames Array of strings or string containg the field names to select - * @param string $operationToApply Available operations = sum, max, min - * @throws Exception - * @return array - */ - private function archiveNumericValuesGeneral($aNames, $operationToApply) - { - $this->loadSubPeriods(); - if(!is_array($aNames)) - { - $aNames = array($aNames); - } - - // fetch the numeric values and apply the operation on them - $results = array(); - foreach($this->archives as $id => $archive) - { - foreach($aNames as $name) - { - if(!isset($results[$name])) - { - $results[$name] = 0; - } - if($name == 'nb_uniq_visitors') continue; - - $valueToSum = $archive->getNumeric($name); - - if($valueToSum !== false) - { - switch ($operationToApply) { - case 'sum': - $results[$name] += $valueToSum; - break; - case 'max': - $results[$name] = max($results[$name], $valueToSum); - break; - case 'min': - $results[$name] = min($results[$name], $valueToSum); - break; - default: - throw new Exception("Operation not applicable."); - break; - } - } - } - } - - if(!Piwik::isUniqueVisitorsEnabled($this->period->getLabel())) - { - unset($results['nb_uniq_visitors']); - } - - foreach($results as $name => $value) - { - if($name == 'nb_uniq_visitors') - { - $value = (float) $this->computeNbUniqVisitors(); - } - $this->insertRecord($name, $value); - } - - // if asked for only one field to sum - if(count($results) == 1) - { - return $results[$name]; - } - - // returns the array of records once summed - return $results; - } - - /** - * This method will compute the sum of DataTables over the period for the given fields $aRecordName. - * The resulting DataTable will be then added to queue of data to be recorded in the database. - * It will usually be called in a plugin that listens to the hook 'ArchiveProcessing_Period.compute' - * - * For example if $aRecordName = 'UserCountry_country' the method will select all UserCountry_country DataTable for the period - * (eg. the 31 dataTable of the last month), sum them, then record it in the DB - * - * - * This method works on recursive dataTable. For example for the 'Actions' it will select all subtables of all dataTable of all the sub periods - * and get the sum. - * - * It returns an array that gives information about the "final" DataTable. The array gives for every field name, the number of rows in the - * final DataTable (ie. the number of distinct LABEL over the period) (eg. the number of distinct keywords over the last month) - * - * @param string|array $aRecordName Field name(s) of DataTable to select so we can get the sum - * @param array $invalidSummedColumnNameToRenamedName (current_column_name => new_column_name) for columns that must change names when summed - * (eg. unique visitors go from nb_uniq_visitors to sum_daily_nb_uniq_visitors) - * @param int $maximumRowsInDataTableLevelZero Max row count of parent datatable to archive - * @param int $maximumRowsInSubDataTable Max row count of children datatable(s) to archive - * @param string $columnToSortByBeforeTruncation Column name to sort by, before truncating rows (ie. if there are more rows than the specified max row count) - * - * @return array array ( - * nameTable1 => number of rows, - * nameTable2 => number of rows, - * ) - */ - public function archiveDataTable( $aRecordName, - $invalidSummedColumnNameToRenamedName = null, - $maximumRowsInDataTableLevelZero = null, - $maximumRowsInSubDataTable = null, - $columnToSortByBeforeTruncation = null ) - { - // We clean up below all tables created during this function call (and recursive calls) - $latestUsedTableId = Piwik_DataTable_Manager::getInstance()->getMostRecentTableId(); - - $this->loadSubPeriods(); - if(!is_array($aRecordName)) - { - $aRecordName = array($aRecordName); - } - - $nameToCount = array(); - foreach($aRecordName as $recordName) - { - $table = $this->getRecordDataTableSum($recordName, $invalidSummedColumnNameToRenamedName); - - $nameToCount[$recordName]['level0'] = $table->getRowsCount(); - $nameToCount[$recordName]['recursive'] = $table->getRowsCountRecursive(); - - $blob = $table->getSerialized( $maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation ); - destroy($table); - $this->insertBlobRecord($recordName, $blob); - } - Piwik_DataTable_Manager::getInstance()->deleteAll( $latestUsedTableId ); - - return $nameToCount; - } - - /** - * This method selects all DataTables that have the name $name over the period. - * It calls the appropriate methods that sum all these tables together. - * The resulting DataTable is returned. - * - * @param string $name - * @param array $invalidSummedColumnNameToRenamedName columns in the array (old name, new name) to be renamed as the sum operation is not valid on them (eg. nb_uniq_visitors->sum_daily_nb_uniq_visitors) - * @return Piwik_DataTable - */ - protected function getRecordDataTableSum( $name, $invalidSummedColumnNameToRenamedName ) - { - $table = new Piwik_DataTable(); - foreach($this->archives as $archive) - { - $archive->preFetchBlob($name); - $datatableToSum = $archive->getDataTable($name); - $archive->loadSubDataTables($name, $datatableToSum); - $table->addDataTable($datatableToSum); - $archive->freeBlob($name); - } - - if(is_null($invalidSummedColumnNameToRenamedName)) - { - $invalidSummedColumnNameToRenamedName = self::$invalidSummedColumnNameToRenamedName; - } - foreach($invalidSummedColumnNameToRenamedName as $oldName => $newName) - { - $table->renameColumn($oldName, $newName); - } - return $table; - } - - protected function initCompute() - { - parent::initCompute(); - } - - /** - * Returns the ID of the archived subperiods. - * - * @return array Array of the idArchive of the subperiods - */ - protected function loadSubperiodsArchive() - { - $periods = array(); - - // we first compute every subperiod of the archive - foreach($this->period->getSubperiods() as $period) - { - $archivePeriod = new Piwik_Archive_Single(); - $archivePeriod->setSite( $this->site ); - $archivePeriod->setPeriod( $period ); - $archivePeriod->setSegment( $this->getSegment() ); - $archivePeriod->setRequestedReport($this->getRequestedReport()); - - $periods[] = $archivePeriod; - } - return $periods; - } - - /** - * Main method to process logs for a period. - * The only logic done here is computing the number of visits, actions, etc. - * - * All the other reports are computed inside plugins listening to the event 'ArchiveProcessing_Period.compute'. - * See some of the plugins for an example. - */ - protected function compute() - { - if(!$this->isThereSomeVisits()) - { - return; - } - Piwik_PostEvent('ArchiveProcessing_Period.compute', $this); - } - - protected function loadSubPeriods() - { - if(empty($this->archives)) - { - $this->archives = $this->loadSubperiodsArchive(); - } - } - - /** - * - * @see Piwik_ArchiveProcessing_Day::isThereSomeVisits() - * @return bool|null - */ - public function isThereSomeVisits() - { - if(!is_null($this->isThereSomeVisits)) - { - return $this->isThereSomeVisits; - } - - $this->loadSubPeriods(); - if(self::getPluginBeingProcessed($this->getRequestedReport()) == 'VisitsSummary' - || $this->shouldProcessReportsAllPlugins($this->getSegment(), $this->period) - ) - { - $toSum = self::getCoreMetrics(); - $record = $this->archiveNumericValuesSum($toSum); - $this->archiveNumericValuesMax( 'max_actions' ); - - $nbVisitsConverted = $record['nb_visits_converted']; - $nbVisits = $record['nb_visits']; - } - else - { - $archive = new Piwik_Archive_Single(); - $archive->setSite( $this->site ); - $archive->setPeriod( $this->period ); - $archive->setSegment( $this->getSegment() ); - - $nbVisits = $archive->getNumeric('nb_visits'); - $nbVisitsConverted = 0; - if($nbVisits > 0) - { - $nbVisitsConverted = $archive->getNumeric('nb_visits_converted'); - } - } - - $this->setNumberOfVisits($nbVisits); - $this->setNumberOfVisitsConverted($nbVisitsConverted); - $this->isThereSomeVisits = ($nbVisits > 0); - return $this->isThereSomeVisits; - } - - /** - * Processes number of unique visitors for the given period - * - * This is the only metric we process from the logs directly, - * since unique visitors cannot be summed like other metrics. - * - * @return int - */ - protected function computeNbUniqVisitors() - { - $select = "count(distinct log_visit.idvisitor) as nb_uniq_visitors"; - $from = "log_visit"; - $where = "log_visit.visit_last_action_time >= ? + */ + static public $invalidSummedColumnNameToRenamedName = array( + Piwik_Archive::INDEX_NB_UNIQ_VISITORS => Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS + ); + + /** + * @var Piwik_Archive_Single[] + */ + public $archives = array(); + + /** + * Sums all values for the given field names $aNames over the period + * See @archiveNumericValuesGeneral for more information + * + * @param string|array $aNames + * @return array + */ + public function archiveNumericValuesSum($aNames) + { + return $this->archiveNumericValuesGeneral($aNames, 'sum'); + } + + /** + * Get the maximum value for all values for the given field names $aNames over the period + * See @archiveNumericValuesGeneral for more information + * + * @param string|array $aNames + * @return array + */ + public function archiveNumericValuesMax($aNames) + { + return $this->archiveNumericValuesGeneral($aNames, 'max'); + } + + /** + * Given a list of fields names, the method will fetch all their values over the period, and archive them using the given operation. + * + * For example if $operationToApply = 'sum' and $aNames = array('nb_visits', 'sum_time_visit') + * it will sum all values of nb_visits for the period (for example give the number of visits for the month by summing the visits of every day) + * + * @param array|string $aNames Array of strings or string containg the field names to select + * @param string $operationToApply Available operations = sum, max, min + * @throws Exception + * @return array + */ + private function archiveNumericValuesGeneral($aNames, $operationToApply) + { + $this->loadSubPeriods(); + if (!is_array($aNames)) { + $aNames = array($aNames); + } + + // fetch the numeric values and apply the operation on them + $results = array(); + foreach ($this->archives as $id => $archive) { + foreach ($aNames as $name) { + if (!isset($results[$name])) { + $results[$name] = 0; + } + if ($name == 'nb_uniq_visitors') continue; + + $valueToSum = $archive->getNumeric($name); + + if ($valueToSum !== false) { + switch ($operationToApply) { + case 'sum': + $results[$name] += $valueToSum; + break; + case 'max': + $results[$name] = max($results[$name], $valueToSum); + break; + case 'min': + $results[$name] = min($results[$name], $valueToSum); + break; + default: + throw new Exception("Operation not applicable."); + break; + } + } + } + } + + if (!Piwik::isUniqueVisitorsEnabled($this->period->getLabel())) { + unset($results['nb_uniq_visitors']); + } + + foreach ($results as $name => $value) { + if ($name == 'nb_uniq_visitors') { + $value = (float)$this->computeNbUniqVisitors(); + } + $this->insertRecord($name, $value); + } + + // if asked for only one field to sum + if (count($results) == 1) { + return $results[$name]; + } + + // returns the array of records once summed + return $results; + } + + /** + * This method will compute the sum of DataTables over the period for the given fields $aRecordName. + * The resulting DataTable will be then added to queue of data to be recorded in the database. + * It will usually be called in a plugin that listens to the hook 'ArchiveProcessing_Period.compute' + * + * For example if $aRecordName = 'UserCountry_country' the method will select all UserCountry_country DataTable for the period + * (eg. the 31 dataTable of the last month), sum them, then record it in the DB + * + * + * This method works on recursive dataTable. For example for the 'Actions' it will select all subtables of all dataTable of all the sub periods + * and get the sum. + * + * It returns an array that gives information about the "final" DataTable. The array gives for every field name, the number of rows in the + * final DataTable (ie. the number of distinct LABEL over the period) (eg. the number of distinct keywords over the last month) + * + * @param string|array $aRecordName Field name(s) of DataTable to select so we can get the sum + * @param array $invalidSummedColumnNameToRenamedName (current_column_name => new_column_name) for columns that must change names when summed + * (eg. unique visitors go from nb_uniq_visitors to sum_daily_nb_uniq_visitors) + * @param int $maximumRowsInDataTableLevelZero Max row count of parent datatable to archive + * @param int $maximumRowsInSubDataTable Max row count of children datatable(s) to archive + * @param string $columnToSortByBeforeTruncation Column name to sort by, before truncating rows (ie. if there are more rows than the specified max row count) + * + * @return array array ( + * nameTable1 => number of rows, + * nameTable2 => number of rows, + * ) + */ + public function archiveDataTable($aRecordName, + $invalidSummedColumnNameToRenamedName = null, + $maximumRowsInDataTableLevelZero = null, + $maximumRowsInSubDataTable = null, + $columnToSortByBeforeTruncation = null) + { + // We clean up below all tables created during this function call (and recursive calls) + $latestUsedTableId = Piwik_DataTable_Manager::getInstance()->getMostRecentTableId(); + + $this->loadSubPeriods(); + if (!is_array($aRecordName)) { + $aRecordName = array($aRecordName); + } + + $nameToCount = array(); + foreach ($aRecordName as $recordName) { + $table = $this->getRecordDataTableSum($recordName, $invalidSummedColumnNameToRenamedName); + + $nameToCount[$recordName]['level0'] = $table->getRowsCount(); + $nameToCount[$recordName]['recursive'] = $table->getRowsCountRecursive(); + + $blob = $table->getSerialized($maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation); + destroy($table); + $this->insertBlobRecord($recordName, $blob); + } + Piwik_DataTable_Manager::getInstance()->deleteAll($latestUsedTableId); + + return $nameToCount; + } + + /** + * This method selects all DataTables that have the name $name over the period. + * It calls the appropriate methods that sum all these tables together. + * The resulting DataTable is returned. + * + * @param string $name + * @param array $invalidSummedColumnNameToRenamedName columns in the array (old name, new name) to be renamed as the sum operation is not valid on them (eg. nb_uniq_visitors->sum_daily_nb_uniq_visitors) + * @return Piwik_DataTable + */ + protected function getRecordDataTableSum($name, $invalidSummedColumnNameToRenamedName) + { + $table = new Piwik_DataTable(); + foreach ($this->archives as $archive) { + $archive->preFetchBlob($name); + $datatableToSum = $archive->getDataTable($name); + $archive->loadSubDataTables($name, $datatableToSum); + $table->addDataTable($datatableToSum); + $archive->freeBlob($name); + } + + if (is_null($invalidSummedColumnNameToRenamedName)) { + $invalidSummedColumnNameToRenamedName = self::$invalidSummedColumnNameToRenamedName; + } + foreach ($invalidSummedColumnNameToRenamedName as $oldName => $newName) { + $table->renameColumn($oldName, $newName); + } + return $table; + } + + protected function initCompute() + { + parent::initCompute(); + } + + /** + * Returns the ID of the archived subperiods. + * + * @return array Array of the idArchive of the subperiods + */ + protected function loadSubperiodsArchive() + { + $periods = array(); + + // we first compute every subperiod of the archive + foreach ($this->period->getSubperiods() as $period) { + $archivePeriod = new Piwik_Archive_Single(); + $archivePeriod->setSite($this->site); + $archivePeriod->setPeriod($period); + $archivePeriod->setSegment($this->getSegment()); + $archivePeriod->setRequestedReport($this->getRequestedReport()); + + $periods[] = $archivePeriod; + } + return $periods; + } + + /** + * Main method to process logs for a period. + * The only logic done here is computing the number of visits, actions, etc. + * + * All the other reports are computed inside plugins listening to the event 'ArchiveProcessing_Period.compute'. + * See some of the plugins for an example. + */ + protected function compute() + { + if (!$this->isThereSomeVisits()) { + return; + } + Piwik_PostEvent('ArchiveProcessing_Period.compute', $this); + } + + protected function loadSubPeriods() + { + if (empty($this->archives)) { + $this->archives = $this->loadSubperiodsArchive(); + } + } + + /** + * + * @see Piwik_ArchiveProcessing_Day::isThereSomeVisits() + * @return bool|null + */ + public function isThereSomeVisits() + { + if (!is_null($this->isThereSomeVisits)) { + return $this->isThereSomeVisits; + } + + $this->loadSubPeriods(); + if (self::getPluginBeingProcessed($this->getRequestedReport()) == 'VisitsSummary' + || $this->shouldProcessReportsAllPlugins($this->getSegment(), $this->period) + ) { + $toSum = self::getCoreMetrics(); + $record = $this->archiveNumericValuesSum($toSum); + $this->archiveNumericValuesMax('max_actions'); + + $nbVisitsConverted = $record['nb_visits_converted']; + $nbVisits = $record['nb_visits']; + } else { + $archive = new Piwik_Archive_Single(); + $archive->setSite($this->site); + $archive->setPeriod($this->period); + $archive->setSegment($this->getSegment()); + + $nbVisits = $archive->getNumeric('nb_visits'); + $nbVisitsConverted = 0; + if ($nbVisits > 0) { + $nbVisitsConverted = $archive->getNumeric('nb_visits_converted'); + } + } + + $this->setNumberOfVisits($nbVisits); + $this->setNumberOfVisitsConverted($nbVisitsConverted); + $this->isThereSomeVisits = ($nbVisits > 0); + return $this->isThereSomeVisits; + } + + /** + * Processes number of unique visitors for the given period + * + * This is the only metric we process from the logs directly, + * since unique visitors cannot be summed like other metrics. + * + * @return int + */ + protected function computeNbUniqVisitors() + { + $select = "count(distinct log_visit.idvisitor) as nb_uniq_visitors"; + $from = "log_visit"; + $where = "log_visit.visit_last_action_time >= ? AND log_visit.visit_last_action_time <= ? AND log_visit.idsite = ?"; - - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); - - $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind); - - return Zend_Registry::get('db')->fetchOne($query['sql'], $query['bind']); - } - - /** - * Called at the end of the archiving process. - * Does some cleaning job in the database. - */ - protected function postCompute() - { - parent::postCompute(); - - $numericTable = $this->tableArchiveNumeric->getTableName(); - self::doPurgeOutdatedArchives($numericTable, $this->isArchiveTemporary()); - - if(!isset($this->archives)) - { - return; - } - foreach($this->archives as $archive) - { - destroy($archive); - } - $this->archives = array(); - } - - const FLAG_TABLE_PURGED = 'lastPurge_'; - - // Used to disable Purge Outdated reports during test data setup - static public $enablePurgeOutdated = true; - - /** - * Given a monthly archive table, will delete all reports that are now outdated, - * or reports that ended with an error - */ - static public function doPurgeOutdatedArchives($numericTable) - { - if(!self::$enablePurgeOutdated) { - return; - } - $blobTable = str_replace("numeric", "blob", $numericTable); - $key = self::FLAG_TABLE_PURGED . $blobTable; - $timestamp = Piwik_GetOption($key); - - // we shall purge temporary archives after their timeout is finished, plus an extra 6 hours - // in case archiving is disabled or run once a day, we give it this extra time to run - // and re-process more recent records... - // TODO: Instead of hardcoding 6 we should put the actual number of hours between 2 archiving runs - $temporaryArchivingTimeout = self::getTodayArchiveTimeToLive(); - $purgeEveryNSeconds = max($temporaryArchivingTimeout, 6 * 3600); - - // we only delete archives if we are able to process them, otherwise, the browser might process reports - // when &segment= is specified (or custom date range) and would below, delete temporary archives that the - // browser is not able to process until next cron run (which could be more than 1 hour away) - if(self::isRequestAuthorizedToArchive() - && (!$timestamp - || $timestamp < time() - $purgeEveryNSeconds)) - { - Piwik_SetOption($key, time()); - - // If Browser Archiving is enabled, it is likely there are many more temporary archives - // We delete more often which is safe, since reports are re-processed on demand - if(self::isBrowserTriggerArchivingEnabled()) - { - $purgeArchivesOlderThan = Piwik_Date::factory(time() - 2 * $temporaryArchivingTimeout)->getDateTime(); - } - // If archive.php via Cron is building the reports, we should keep all temporary reports from today - else - { - $purgeArchivesOlderThan = Piwik_Date::factory('today')->getDateTime(); - } - $result = Piwik_FetchAll(" + + $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); + + $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind); + + return Zend_Registry::get('db')->fetchOne($query['sql'], $query['bind']); + } + + /** + * Called at the end of the archiving process. + * Does some cleaning job in the database. + */ + protected function postCompute() + { + parent::postCompute(); + + $numericTable = $this->tableArchiveNumeric->getTableName(); + self::doPurgeOutdatedArchives($numericTable, $this->isArchiveTemporary()); + + if (!isset($this->archives)) { + return; + } + foreach ($this->archives as $archive) { + destroy($archive); + } + $this->archives = array(); + } + + const FLAG_TABLE_PURGED = 'lastPurge_'; + + // Used to disable Purge Outdated reports during test data setup + static public $enablePurgeOutdated = true; + + /** + * Given a monthly archive table, will delete all reports that are now outdated, + * or reports that ended with an error + */ + static public function doPurgeOutdatedArchives($numericTable) + { + if (!self::$enablePurgeOutdated) { + return; + } + $blobTable = str_replace("numeric", "blob", $numericTable); + $key = self::FLAG_TABLE_PURGED . $blobTable; + $timestamp = Piwik_GetOption($key); + + // we shall purge temporary archives after their timeout is finished, plus an extra 6 hours + // in case archiving is disabled or run once a day, we give it this extra time to run + // and re-process more recent records... + // TODO: Instead of hardcoding 6 we should put the actual number of hours between 2 archiving runs + $temporaryArchivingTimeout = self::getTodayArchiveTimeToLive(); + $purgeEveryNSeconds = max($temporaryArchivingTimeout, 6 * 3600); + + // we only delete archives if we are able to process them, otherwise, the browser might process reports + // when &segment= is specified (or custom date range) and would below, delete temporary archives that the + // browser is not able to process until next cron run (which could be more than 1 hour away) + if (self::isRequestAuthorizedToArchive() + && (!$timestamp + || $timestamp < time() - $purgeEveryNSeconds) + ) { + Piwik_SetOption($key, time()); + + // If Browser Archiving is enabled, it is likely there are many more temporary archives + // We delete more often which is safe, since reports are re-processed on demand + if (self::isBrowserTriggerArchivingEnabled()) { + $purgeArchivesOlderThan = Piwik_Date::factory(time() - 2 * $temporaryArchivingTimeout)->getDateTime(); + } // If archive.php via Cron is building the reports, we should keep all temporary reports from today + else { + $purgeArchivesOlderThan = Piwik_Date::factory('today')->getDateTime(); + } + $result = Piwik_FetchAll(" SELECT idarchive FROM $numericTable WHERE name LIKE 'done%' - AND (( value = ". Piwik_ArchiveProcessing::DONE_OK_TEMPORARY ." + AND (( value = " . Piwik_ArchiveProcessing::DONE_OK_TEMPORARY . " AND ts_archived < ?) - OR value = ". Piwik_ArchiveProcessing::DONE_ERROR .")", - array($purgeArchivesOlderThan) - ); - - $idArchivesToDelete = array(); - if(!empty($result)) - { - foreach($result as $row) { - $idArchivesToDelete[] = $row['idarchive']; - } - $query = "DELETE + OR value = " . Piwik_ArchiveProcessing::DONE_ERROR . ")", + array($purgeArchivesOlderThan) + ); + + $idArchivesToDelete = array(); + if (!empty($result)) { + foreach ($result as $row) { + $idArchivesToDelete[] = $row['idarchive']; + } + $query = "DELETE FROM %s - WHERE idarchive IN (".implode(',',$idArchivesToDelete).") + WHERE idarchive IN (" . implode(',', $idArchivesToDelete) . ") "; - - Piwik_Query(sprintf($query, $numericTable)); - - // Individual blob tables could be missing - try { - Piwik_Query(sprintf($query, $blobTable)); - } catch(Exception $e) { } - } - Piwik::log("Purging temporary archives: done [ purged archives older than $purgeArchivesOlderThan from $blobTable and $numericTable ] [Deleted IDs: ". implode(',',$idArchivesToDelete)."]"); - - // Deleting "Custom Date Range" reports after 1 day, since they can be re-processed - // and would take up unecessary space - $yesterday = Piwik_Date::factory('yesterday')->getDateTime(); - $query = "DELETE + + Piwik_Query(sprintf($query, $numericTable)); + + // Individual blob tables could be missing + try { + Piwik_Query(sprintf($query, $blobTable)); + } catch (Exception $e) { + } + } + Piwik::log("Purging temporary archives: done [ purged archives older than $purgeArchivesOlderThan from $blobTable and $numericTable ] [Deleted IDs: " . implode(',', $idArchivesToDelete) . "]"); + + // Deleting "Custom Date Range" reports after 1 day, since they can be re-processed + // and would take up unecessary space + $yesterday = Piwik_Date::factory('yesterday')->getDateTime(); + $query = "DELETE FROM %s WHERE period = ? AND ts_archived < ?"; - $bind = array(Piwik::$idPeriods['range'], $yesterday); - Piwik::log("Purging Custom Range archives: done [ purged archives older than $yesterday from $blobTable and $numericTable ]"); - - Piwik_Query(sprintf($query, $numericTable), $bind); - - // Individual blob tables could be missing - try { - Piwik_Query(sprintf($query, $blobTable), $bind); - } catch(Exception $e) { } - - // these tables will be OPTIMIZEd daily in a scheduled task, to claim lost space - } - else - { - Piwik::log("Purging temporary archives: skipped."); - } - } + $bind = array(Piwik::$idPeriods['range'], $yesterday); + Piwik::log("Purging Custom Range archives: done [ purged archives older than $yesterday from $blobTable and $numericTable ]"); + + Piwik_Query(sprintf($query, $numericTable), $bind); + + // Individual blob tables could be missing + try { + Piwik_Query(sprintf($query, $blobTable), $bind); + } catch (Exception $e) { + } + + // these tables will be OPTIMIZEd daily in a scheduled task, to claim lost space + } else { + Piwik::log("Purging temporary archives: skipped."); + } + } } \ No newline at end of file diff --git a/core/AssetManager.php b/core/AssetManager.php index 3ef46bf0d3..1091e518f0 100644 --- a/core/AssetManager.php +++ b/core/AssetManager.php @@ -24,7 +24,7 @@ require_once PIWIK_INCLUDE_PATH . '/libs/jsmin/jsmin.php'; * JavaScript and CSS files. * * It performs the following actions: - * - Identifies required assets + * - Identifies required assets * - Includes assets in the rendered HTML page * - Manages asset merging and minifying * - Manages server-side cache @@ -39,92 +39,89 @@ require_once PIWIK_INCLUDE_PATH . '/libs/jsmin/jsmin.php'; */ class Piwik_AssetManager { - const MERGED_CSS_FILE = "asset_manager_global_css.css"; - const MERGED_JS_FILE = "asset_manager_global_js.js"; - const CSS_IMPORT_EVENT = "AssetManager.getCssFiles"; - const JS_IMPORT_EVENT = "AssetManager.getJsFiles"; - const MERGED_FILE_DIR = "tmp/assets/"; - const CSS_IMPORT_DIRECTIVE = "\n"; - const JS_IMPORT_DIRECTIVE = "\n"; - const GET_CSS_MODULE_ACTION = "index.php?module=Proxy&action=getCss"; - const GET_JS_MODULE_ACTION = "index.php?module=Proxy&action=getJs"; - const MINIFIED_JS_RATIO = 100; - - /** - * Returns CSS file inclusion directive(s) using the markup - * - * @return string - */ - public static function getCssAssets() - { - if ( self::getDisableMergedAssets() ) - { - // Individual includes mode - self::removeMergedAsset(self::MERGED_CSS_FILE); - return self::getIndividualCssIncludes(); - } - return sprintf ( self::CSS_IMPORT_DIRECTIVE, self::GET_CSS_MODULE_ACTION ); - } - - /** - * Returns JS file inclusion directive(s) using the markup \n"; + const GET_CSS_MODULE_ACTION = "index.php?module=Proxy&action=getCss"; + const GET_JS_MODULE_ACTION = "index.php?module=Proxy&action=getJs"; + const MINIFIED_JS_RATIO = 100; + + /** + * Returns CSS file inclusion directive(s) using the markup + * + * @return string + */ + public static function getCssAssets() + { + if (self::getDisableMergedAssets()) { + // Individual includes mode + self::removeMergedAsset(self::MERGED_CSS_FILE); + return self::getIndividualCssIncludes(); + } + return sprintf(self::CSS_IMPORT_DIRECTIVE, self::GET_CSS_MODULE_ACTION); + } + + /** + * Returns JS file inclusion directive(s) using the markup '; - } - return $jsCode; + static $pluginTranslationsAlreadyLoaded = array(); + if (!isset($params['plugins'])) { + throw new Exception("The smarty function loadJavascriptTranslations needs a 'plugins' parameter."); + } + if (in_array($params['plugins'], $pluginTranslationsAlreadyLoaded)) { + return; + } + $pluginTranslationsAlreadyLoaded[] = $params['plugins']; + $jsTranslations = Piwik_Translate::getInstance()->getJavascriptTranslations(explode(' ', $params['plugins'])); + $jsCode = ''; + if (isset($params['disableOutputScriptTag'])) { + $jsCode .= $jsTranslations; + } else { + $jsCode .= ''; + } + return $jsCode; } diff --git a/core/SmartyPlugins/function.logoHtml.php b/core/SmartyPlugins/function.logoHtml.php index acff49b34c..5de794330c 100644 --- a/core/SmartyPlugins/function.logoHtml.php +++ b/core/SmartyPlugins/function.logoHtml.php @@ -1,10 +1,10 @@ "; + if (!isset($params['metadata']['logo'])) { + return; + } + $width = $height = $alt = ''; + if (isset($params['metadata']['logoWidth'])) { + $width = "width=" . $params['metadata']['logoWidth']; + } + if (isset($params['metadata']['logoHeight'])) { + $height = "height=" . $params['metadata']['logoHeight']; + } + if (isset($params['alt'])) { + $alt = "title='" . $params['alt'] . "' alt='" . $params['alt'] . "'"; + } + return " "; } diff --git a/core/SmartyPlugins/function.postEvent.php b/core/SmartyPlugins/function.postEvent.php index ff43fd91fe..079822e530 100644 --- a/core/SmartyPlugins/function.postEvent.php +++ b/core/SmartyPlugins/function.postEvent.php @@ -1,10 +1,10 @@ getWidth(); - $height = $graph->getHeight(); - return "\"\""; + $src = $params['src']; + $graph = new Piwik_Visualization_Sparkline(); + $width = $graph->getWidth(); + $height = $graph->getHeight(); + return "\"\""; } diff --git a/core/SmartyPlugins/function.url.php b/core/SmartyPlugins/function.url.php index 9e20e93d1e..ef8ca0bf6c 100644 --- a/core/SmartyPlugins/function.url.php +++ b/core/SmartyPlugins/function.url.php @@ -1,10 +1,10 @@ - * + * * @see Piwik_Url::getCurrentQueryStringWithParametersModified() * * @param array $params $name=>$value pairs of the parameters to modify in the generated URL * @param Smarty &smarty Smarty object - * @return string Something like index.php?module=X&action=Y + * @return string Something like index.php?module=X&action=Y */ function smarty_function_url($params, &$smarty) { - return Piwik_Common::sanitizeInputValue('index.php' . Piwik_Url::getCurrentQueryStringWithParametersModified( $params )); + return Piwik_Common::sanitizeInputValue('index.php' . Piwik_Url::getCurrentQueryStringWithParametersModified($params)); } diff --git a/core/SmartyPlugins/modifier.inlineHelp.php b/core/SmartyPlugins/modifier.inlineHelp.php index 7cc7531f4a..a0ad93bed4 100644 --- a/core/SmartyPlugins/modifier.inlineHelp.php +++ b/core/SmartyPlugins/modifier.inlineHelp.php @@ -1,10 +1,10 @@ '. - '
'. - ''. - $text. - '
'. - ''; + return + '
' . + '
' . + '' . + $text . + '
' . + '
'; } diff --git a/core/SmartyPlugins/modifier.money.php b/core/SmartyPlugins/modifier.money.php index 7d9eaa7afe..29bd853566 100644 --- a/core/SmartyPlugins/modifier.money.php +++ b/core/SmartyPlugins/modifier.money.php @@ -1,10 +1,10 @@ General['jquery_version']; - $jqueryui_version = Piwik_Config::getInstance()->General['jqueryui_version']; + $jquery_version = Piwik_Config::getInstance()->General['jquery_version']; + $jqueryui_version = Piwik_Config::getInstance()->General['jqueryui_version']; - $pattern = array( - '~~', - '~ - - - - - - + + + + + + + + + + isAvailable()) -{ - if ($displayNotes) - { - Piwik::log("[note] The GeoIP PECL extension is not installed."); - } - - $provider = null; -} -else -{ - $workingOrError = $provider->isWorking(); - if ($workingOrError !== true) - { - if ($displayNotes) - { - Piwik::log("[note] The GeoIP PECL extension is broken: $workingOrError"); - } - if (Piwik_Common::isPhpCliMode()) - { - Piwik::log("[note] Make sure your command line PHP is configured to use the PECL extension."); - } - $provider = null; - } +if (!$provider->isAvailable()) { + if ($displayNotes) { + Piwik::log("[note] The GeoIP PECL extension is not installed."); + } + + $provider = null; +} else { + $workingOrError = $provider->isWorking(); + if ($workingOrError !== true) { + if ($displayNotes) { + Piwik::log("[note] The GeoIP PECL extension is broken: $workingOrError"); + } + if (Piwik_Common::isPhpCliMode()) { + Piwik::log("[note] Make sure your command line PHP is configured to use the PECL extension."); + } + $provider = null; + } } // use php api if pecl extension cannot be used -if (is_null($provider)) -{ - if ($displayNotes) - { - Piwik::log("[note] Falling back to PHP API. This may become too slow for you. If so, you can read this link on how to install the PECL extension: http://piwik.org/faq/how-to/#faq_164"); - } - - $provider = new Piwik_UserCountry_LocationProvider_GeoIp_Php(); - if (!$provider->isAvailable()) - { - if ($displayNotes) - { - Piwik::log("[note] The GeoIP PHP API is not available. This means you do not have a GeoIP location database in your ./misc directory. The database must be named either GeoIP.dat or GeoIPCity.dat based on the type of database it is."); - } - $provider = null; - } - else - { - $workingOrError = $provider->isWorking(); - if ($workingOrError !== true) - { - if ($displayNotes) - { - Piwik::log("[note] The GeoIP PHP API is broken: $workingOrError"); - } - $provider = null; - } - } +if (is_null($provider)) { + if ($displayNotes) { + Piwik::log("[note] Falling back to PHP API. This may become too slow for you. If so, you can read this link on how to install the PECL extension: http://piwik.org/faq/how-to/#faq_164"); + } + + $provider = new Piwik_UserCountry_LocationProvider_GeoIp_Php(); + if (!$provider->isAvailable()) { + if ($displayNotes) { + Piwik::log("[note] The GeoIP PHP API is not available. This means you do not have a GeoIP location database in your ./misc directory. The database must be named either GeoIP.dat or GeoIPCity.dat based on the type of database it is."); + } + $provider = null; + } else { + $workingOrError = $provider->isWorking(); + if ($workingOrError !== true) { + if ($displayNotes) { + Piwik::log("[note] The GeoIP PHP API is broken: $workingOrError"); + } + $provider = null; + } + } } -if (is_null($provider)) -{ - geoipUpdateError("\n[error] There is no location provider that can be used with this script. Only the GeoIP PECL module or the GeoIP PHP API can be used at present. Please install and configure one of these first."); +if (is_null($provider)) { + geoipUpdateError("\n[error] There is no location provider that can be used with this script. Only the GeoIP PECL module or the GeoIP PHP API can be used at present. Please install and configure one of these first."); } $info = $provider->getInfo(); -if ($displayNotes) -{ - Piwik::log("[note] Found working provider: {$info['id']}"); +if ($displayNotes) { + Piwik::log("[note] Found working provider: {$info['id']}"); } // perform update $logVisitFieldsToUpdate = array('location_country' => Piwik_UserCountry_LocationProvider::COUNTRY_CODE_KEY, - 'location_region' => Piwik_UserCountry_LocationProvider::REGION_CODE_KEY, - 'location_city' => Piwik_UserCountry_LocationProvider::CITY_NAME_KEY, - 'location_latitude' => Piwik_UserCountry_LocationProvider::LATITUDE_KEY, - 'location_longitude' => Piwik_UserCountry_LocationProvider::LONGITUDE_KEY); - -if ($displayNotes) -{ - Piwik::log("\n$count rows to process in ".Piwik_Common::prefixTable('log_visit') - . " and ".Piwik_Common::prefixTable('log_conversion')."..."); + 'location_region' => Piwik_UserCountry_LocationProvider::REGION_CODE_KEY, + 'location_city' => Piwik_UserCountry_LocationProvider::CITY_NAME_KEY, + 'location_latitude' => Piwik_UserCountry_LocationProvider::LATITUDE_KEY, + 'location_longitude' => Piwik_UserCountry_LocationProvider::LONGITUDE_KEY); + +if ($displayNotes) { + Piwik::log("\n$count rows to process in " . Piwik_Common::prefixTable('log_visit') + . " and " . Piwik_Common::prefixTable('log_conversion') . "..."); } flush(); -for (; $start < $end; $start += $limit) -{ - $rows = Piwik_FetchAll("SELECT idvisit, location_ip, ".implode(',', array_keys($logVisitFieldsToUpdate))." - FROM ".Piwik_Common::prefixTable('log_visit')." +for (; $start < $end; $start += $limit) { + $rows = Piwik_FetchAll("SELECT idvisit, location_ip, " . implode(',', array_keys($logVisitFieldsToUpdate)) . " + FROM " . Piwik_Common::prefixTable('log_visit') . " LIMIT $start, $limit"); - if(!count($rows)) - { - continue; - } - - foreach ( $rows as $i => $row ) - { - $fieldsToSet = array(); - foreach ($logVisitFieldsToUpdate as $field => $ignore) - { - if (empty($fieldsToSet[$field])) - { - $fieldsToSet[] = $field; - } - } - - // skip if it already has a location - if (empty($fieldsToSet)) - { - continue; - } - - $ip = Piwik_IP::N2P($row['location_ip']); - $location = $provider->getLocation(array('ip' => $ip)); - - if (!empty($location[Piwik_UserCountry_LocationProvider::COUNTRY_CODE_KEY])) - { - $location[Piwik_UserCountry_LocationProvider::COUNTRY_CODE_KEY] = - strtolower($location[Piwik_UserCountry_LocationProvider::COUNTRY_CODE_KEY]); - } - $row['location_country'] = strtolower($row['location_country']); - - $columnsToSet = array(); - $bind = array(); - foreach ($logVisitFieldsToUpdate as $column => $locationKey) - { - if (!empty($location[$locationKey]) - && $location[$locationKey] != $row[$column]) - { - $columnsToSet[] = $column.' = ?'; - $bind[] = $location[$locationKey]; - } - } - - if (empty($columnsToSet)) - { - continue; - } - - $bind[] = $row['idvisit']; - - // update log_visit - $sql = "UPDATE ".Piwik_Common::prefixTable('log_visit')." - SET ".implode(', ', $columnsToSet)." + if (!count($rows)) { + continue; + } + + foreach ($rows as $i => $row) { + $fieldsToSet = array(); + foreach ($logVisitFieldsToUpdate as $field => $ignore) { + if (empty($fieldsToSet[$field])) { + $fieldsToSet[] = $field; + } + } + + // skip if it already has a location + if (empty($fieldsToSet)) { + continue; + } + + $ip = Piwik_IP::N2P($row['location_ip']); + $location = $provider->getLocation(array('ip' => $ip)); + + if (!empty($location[Piwik_UserCountry_LocationProvider::COUNTRY_CODE_KEY])) { + $location[Piwik_UserCountry_LocationProvider::COUNTRY_CODE_KEY] = + strtolower($location[Piwik_UserCountry_LocationProvider::COUNTRY_CODE_KEY]); + } + $row['location_country'] = strtolower($row['location_country']); + + $columnsToSet = array(); + $bind = array(); + foreach ($logVisitFieldsToUpdate as $column => $locationKey) { + if (!empty($location[$locationKey]) + && $location[$locationKey] != $row[$column] + ) { + $columnsToSet[] = $column . ' = ?'; + $bind[] = $location[$locationKey]; + } + } + + if (empty($columnsToSet)) { + continue; + } + + $bind[] = $row['idvisit']; + + // update log_visit + $sql = "UPDATE " . Piwik_Common::prefixTable('log_visit') . " + SET " . implode(', ', $columnsToSet) . " WHERE idvisit = ?"; - Piwik_Query($sql, $bind); - - // update log_conversion - $sql = "UPDATE ".Piwik_Common::prefixTable('log_conversion')." - SET ".implode(', ', $columnsToSet)." + Piwik_Query($sql, $bind); + + // update log_conversion + $sql = "UPDATE " . Piwik_Common::prefixTable('log_conversion') . " + SET " . implode(', ', $columnsToSet) . " WHERE idvisit = ?"; - Piwik_Query($sql, $bind); - } - Piwik::log(round($start * 100 / $count) . "% done..."); - flush(); + Piwik_Query($sql, $bind); + } + Piwik::log(round($start * 100 / $count) . "% done..."); + flush(); } -if ($start >= $count) -{ - Piwik::log("100% done!"); - Piwik::log(""); - Piwik::log("[note] Now that you've geolocated your old visits, you need to force your reports to be re-processed. See this FAQ entry: http://piwik.org/faq/how-to/#faq_59"); +if ($start >= $count) { + Piwik::log("100% done!"); + Piwik::log(""); + Piwik::log("[note] Now that you've geolocated your old visits, you need to force your reports to be re-processed. See this FAQ entry: http://piwik.org/faq/how-to/#faq_59"); } diff --git a/misc/others/iframeWidget.htm b/misc/others/iframeWidget.htm index f26a8ba463..9b7c4261f1 100644 --- a/misc/others/iframeWidget.htm +++ b/misc/others/iframeWidget.htm @@ -2,6 +2,12 @@

Embedding the Piwik Country widget in an Iframe

-
- +
+ +
+ + + diff --git a/misc/others/iframeWidget_localhost.php b/misc/others/iframeWidget_localhost.php index ad105a7c9c..d01ba89227 100644 --- a/misc/others/iframeWidget_localhost.php +++ b/misc/others/iframeWidget_localhost.php @@ -9,9 +9,13 @@ $url = "http://localhost/trunk/index.php?token_auth=0b809661490d605bfd77f57ed11f

Embedding the Piwik Country widget in an Iframe

-

Loads a widget from localhost/trunk/ with login=root, pwd=test. Widget URL

-
+ +

Loads a widget from localhost/trunk/ with login=root, pwd=test. Widget URL

+ +
+ +

@@ -26,32 +30,29 @@ require_once PIWIK_INCLUDE_PATH . "/core/API/Request.php"; Piwik_FrontController::getInstance()->init(); $widgets = Piwik_GetWidgetsList(); -foreach($widgets as $category => $widgetsInCategory) -{ - echo '

'.$category . '

'; - foreach($widgetsInCategory as $widget) - { - echo '

'.$widget['name'].'

'; - $widgetUrl = Piwik_Common::getArrayFromQueryString($url); - $widgetUrl['moduleToWidgetize'] = $widget['parameters']['module']; - $widgetUrl['actionToWidgetize'] = $widget['parameters']['action']; - $parameters = $widget['parameters']; - unset($parameters['module']); - unset($parameters['action']); - foreach($parameters as $name => $value) - { - if(is_array($value)) - { - $value = current($value); - } - $widgetUrl[$name] = $value; - } - $widgetUrl = Piwik_Url::getQueryStringFromParameters($widgetUrl); - - echo '
'; - - } +foreach ($widgets as $category => $widgetsInCategory) { + echo '

' . $category . '

'; + foreach ($widgetsInCategory as $widget) { + echo '

' . $widget['name'] . '

'; + $widgetUrl = Piwik_Common::getArrayFromQueryString($url); + $widgetUrl['moduleToWidgetize'] = $widget['parameters']['module']; + $widgetUrl['actionToWidgetize'] = $widget['parameters']['action']; + $parameters = $widget['parameters']; + unset($parameters['module']); + unset($parameters['action']); + foreach ($parameters as $name => $value) { + if (is_array($value)) { + $value = current($value); + } + $widgetUrl[$name] = $value; + } + $widgetUrl = Piwik_Url::getQueryStringFromParameters($widgetUrl); + + echo '
'; + + } } ?> - + + diff --git a/misc/others/test_cookies_GenerateHundredsWebsitesAndVisits.php b/misc/others/test_cookies_GenerateHundredsWebsitesAndVisits.php index 8ce69cdf5b..4f51391b96 100644 --- a/misc/others/test_cookies_GenerateHundredsWebsitesAndVisits.php +++ b/misc/others/test_cookies_GenerateHundredsWebsitesAndVisits.php @@ -14,10 +14,9 @@ require_once PIWIK_INCLUDE_PATH . "/libs/PiwikTracker/PiwikTracker.php"; Piwik_FrontController::getInstance()->init(); Piwik::setUserIsSuperUser(); $count = 100; -for($i = 0; $i <= $count; $i++) -{ - $id = Piwik_SitesManager_API::getInstance()->addSite(Piwik_Common::getRandomString(), 'http://piwik.org'); +for ($i = 0; $i <= $count; $i++) { + $id = Piwik_SitesManager_API::getInstance()->addSite(Piwik_Common::getRandomString(), 'http://piwik.org'); $t = new PiwikTracker($id, 'http://localhost/trunk/piwik.php'); - echo $id . "
"; + echo $id . "
"; } diff --git a/misc/others/test_generateLotsVisitsWebsites.php b/misc/others/test_generateLotsVisitsWebsites.php index e8819bb9c8..eeb5b22e5a 100644 --- a/misc/others/test_generateLotsVisitsWebsites.php +++ b/misc/others/test_generateLotsVisitsWebsites.php @@ -1,5 +1,5 @@ init(); // SECURITY: DO NOT DELETE THIS LINE! -if(!Piwik_Common::isPhpCliMode()) { die("ERROR: Must be executed in CLI"); } +if (!Piwik_Common::isPhpCliMode()) { + die("ERROR: Must be executed in CLI"); +} $process = new Piwik_StressTests_CopyLogs; $process->init(); @@ -19,118 +21,121 @@ $process->run(); class Piwik_StressTests_CopyLogs { - function init() - { - $config = Piwik_Config::getInstance(); - $config->log['log_only_when_debug_parameter'] = 0; - $config->log['logger_message'] = array("logger_message" => "screen"); - Piwik::createLogObject(); - } - - function run() - { - // Copy all visits in date range into TODAY + function init() + { + $config = Piwik_Config::getInstance(); + $config->log['log_only_when_debug_parameter'] = 0; + $config->log['logger_message'] = array("logger_message" => "screen"); + Piwik::createLogObject(); + } + + function run() + { + // Copy all visits in date range into TODAY $startDate = '2011-08-12'; $endDate = '2011-08-12'; - + $this->log("Starting..."); - $db = Zend_Registry::get('db'); - - $initial = $this->getVisitsToday(); - $this->log(" Visits today so far: " . $initial); - $initialActions = $this->getActionsToday(); - $this->log(" Actions today: " . $initialActions); - $initialPurchasedItems = $this->getConversionItemsToday(); - $this->log(" Purchased items today: " . $initialPurchasedItems); - $initialConversions = $this->getConversionsToday(); - $this->log(" Conversions today: " . $initialConversions); - - $this->log(" Now copying visits between '$startDate' and '$endDate'..."); - $sql = "INSERT INTO ". Piwik_Common::prefixTable('log_visit')." (`idsite`, `idvisitor`, `visitor_localtime`, `visitor_returning`, `visitor_count_visits`, `visit_first_action_time`, `visit_last_action_time`, `visit_exit_idaction_url`, `visit_exit_idaction_name`, `visit_entry_idaction_url`, `visit_entry_idaction_name`, `visit_total_actions`, `visit_total_time`, `visit_goal_converted`, `visit_goal_buyer`, `referer_type`, `referer_name`, `referer_url`, `referer_keyword`, `config_id`, `config_os`, `config_browser_name`, `config_browser_version`, `config_resolution`, `config_pdf`, `config_flash`, `config_java`, `config_director`, `config_quicktime`, `config_realplayer`, `config_windowsmedia`, `config_gears`, `config_silverlight`, `config_cookie`, `location_ip`, `location_browser_lang`, `location_country`, `location_provider`, `custom_var_k1`, `custom_var_v1`, `custom_var_k2`, `custom_var_v2`, `custom_var_k3`, `custom_var_v3`, `custom_var_k4`, `custom_var_v4`, `custom_var_k5`, `custom_var_v5`, `visitor_days_since_last`, `visitor_days_since_order`, `visitor_days_since_first`) + $db = Zend_Registry::get('db'); + + $initial = $this->getVisitsToday(); + $this->log(" Visits today so far: " . $initial); + $initialActions = $this->getActionsToday(); + $this->log(" Actions today: " . $initialActions); + $initialPurchasedItems = $this->getConversionItemsToday(); + $this->log(" Purchased items today: " . $initialPurchasedItems); + $initialConversions = $this->getConversionsToday(); + $this->log(" Conversions today: " . $initialConversions); + + $this->log(" Now copying visits between '$startDate' and '$endDate'..."); + $sql = "INSERT INTO " . Piwik_Common::prefixTable('log_visit') . " (`idsite`, `idvisitor`, `visitor_localtime`, `visitor_returning`, `visitor_count_visits`, `visit_first_action_time`, `visit_last_action_time`, `visit_exit_idaction_url`, `visit_exit_idaction_name`, `visit_entry_idaction_url`, `visit_entry_idaction_name`, `visit_total_actions`, `visit_total_time`, `visit_goal_converted`, `visit_goal_buyer`, `referer_type`, `referer_name`, `referer_url`, `referer_keyword`, `config_id`, `config_os`, `config_browser_name`, `config_browser_version`, `config_resolution`, `config_pdf`, `config_flash`, `config_java`, `config_director`, `config_quicktime`, `config_realplayer`, `config_windowsmedia`, `config_gears`, `config_silverlight`, `config_cookie`, `location_ip`, `location_browser_lang`, `location_country`, `location_provider`, `custom_var_k1`, `custom_var_v1`, `custom_var_k2`, `custom_var_v2`, `custom_var_k3`, `custom_var_v3`, `custom_var_k4`, `custom_var_v4`, `custom_var_k5`, `custom_var_v5`, `visitor_days_since_last`, `visitor_days_since_order`, `visitor_days_since_first`) SELECT `idsite`, `idvisitor`, `visitor_localtime`, `visitor_returning`, `visitor_count_visits`, CONCAT(CURRENT_DATE() , \" \", FLOOR(RAND()*24) , \":\",FLOOR(RAND()*60),\":\",FLOOR(RAND()*60)), CONCAT(CURRENT_DATE() , \" \", FLOOR(RAND()*24) , \":\",FLOOR(RAND()*60),\":\",FLOOR(RAND()*60)), `visit_exit_idaction_url`, `visit_exit_idaction_name`, `visit_entry_idaction_url`, `visit_entry_idaction_name`, `visit_total_actions`, `visit_total_time`, `visit_goal_converted`, `visit_goal_buyer`, `referer_type`, `referer_name`, `referer_url`, `referer_keyword`, `config_id`, `config_os`, `config_browser_name`, `config_browser_version`, `config_resolution`, `config_pdf`, `config_flash`, `config_java`, `config_director`, `config_quicktime`, `config_realplayer`, `config_windowsmedia`, `config_gears`, `config_silverlight`, `config_cookie`, `location_ip`, `location_browser_lang`, `location_country`, `location_provider`, `custom_var_k1`, `custom_var_v1`, `custom_var_k2`, `custom_var_v2`, `custom_var_k3`, `custom_var_v3`, `custom_var_k4`, `custom_var_v4`, `custom_var_k5`, `custom_var_v5`, `visitor_days_since_last`, `visitor_days_since_order`, `visitor_days_since_first` - FROM `". Piwik_Common::prefixTable('log_visit')."` + FROM `" . Piwik_Common::prefixTable('log_visit') . "` WHERE idsite >= 1 AND date(visit_last_action_time) between '$startDate' and '$endDate' ;"; - $result = $db->query($sql); - - $this->log(" Copying actions..."); - $sql = "INSERT INTO ". Piwik_Common::prefixTable('log_link_visit_action')." (`idsite`, `idvisitor`, `server_time`, `idvisit`, `idaction_url`, `idaction_url_ref`, `idaction_name`, `idaction_name_ref`, `time_spent_ref_action`, `custom_var_k1`, `custom_var_v1`, `custom_var_k2`, `custom_var_v2`, `custom_var_k3`, `custom_var_v3`, `custom_var_k4`, `custom_var_v4`, `custom_var_k5`, `custom_var_v5`) + $result = $db->query($sql); + + $this->log(" Copying actions..."); + $sql = "INSERT INTO " . Piwik_Common::prefixTable('log_link_visit_action') . " (`idsite`, `idvisitor`, `server_time`, `idvisit`, `idaction_url`, `idaction_url_ref`, `idaction_name`, `idaction_name_ref`, `time_spent_ref_action`, `custom_var_k1`, `custom_var_v1`, `custom_var_k2`, `custom_var_v2`, `custom_var_k3`, `custom_var_v3`, `custom_var_k4`, `custom_var_v4`, `custom_var_k5`, `custom_var_v5`) SELECT `idsite`, `idvisitor`, CONCAT(CURRENT_DATE() , \" \", FLOOR(RAND()*24) , \":\",FLOOR(RAND()*60),\":\",FLOOR(RAND()*60)), `idvisit`, `idaction_url`, `idaction_url_ref`, `idaction_name`, `idaction_name_ref`, `time_spent_ref_action`, `custom_var_k1`, `custom_var_v1`, `custom_var_k2`, `custom_var_v2`, `custom_var_k3`, `custom_var_v3`, `custom_var_k4`, `custom_var_v4`, `custom_var_k5`, `custom_var_v5` - FROM `". Piwik_Common::prefixTable('log_link_visit_action')."` + FROM `" . Piwik_Common::prefixTable('log_link_visit_action') . "` WHERE idsite >= 1 AND date(server_time) between '$startDate' and '$endDate' ;"; // LIMIT 1000000 - $result = $db->query($sql); - - $this->log(" Copying conversions..."); - $sql = "INSERT IGNORE INTO `". Piwik_Common::prefixTable('log_conversion')."` (`idvisit`, `idsite`, `visitor_days_since_first`, `visitor_days_since_order`, `visitor_count_visits`, `idvisitor`, `server_time`, `idaction_url`, `idlink_va`, `referer_visit_server_date`, `referer_type`, `referer_name`, `referer_keyword`, `visitor_returning`, `location_country`, `url`, `idgoal`, `revenue`, `buster`, `idorder`, `custom_var_k1`, `custom_var_v1`, `custom_var_k2`, `custom_var_v2`, `custom_var_k3`, `custom_var_v3`, `custom_var_k4`, `custom_var_v4`, `custom_var_k5`, `custom_var_v5`, `items`, `revenue_subtotal`, `revenue_tax`, `revenue_shipping`, `revenue_discount`) + $result = $db->query($sql); + + $this->log(" Copying conversions..."); + $sql = "INSERT IGNORE INTO `" . Piwik_Common::prefixTable('log_conversion') . "` (`idvisit`, `idsite`, `visitor_days_since_first`, `visitor_days_since_order`, `visitor_count_visits`, `idvisitor`, `server_time`, `idaction_url`, `idlink_va`, `referer_visit_server_date`, `referer_type`, `referer_name`, `referer_keyword`, `visitor_returning`, `location_country`, `url`, `idgoal`, `revenue`, `buster`, `idorder`, `custom_var_k1`, `custom_var_v1`, `custom_var_k2`, `custom_var_v2`, `custom_var_k3`, `custom_var_v3`, `custom_var_k4`, `custom_var_v4`, `custom_var_k5`, `custom_var_v5`, `items`, `revenue_subtotal`, `revenue_tax`, `revenue_shipping`, `revenue_discount`) SELECT `idvisit`, `idsite`, `visitor_days_since_first`, `visitor_days_since_order`, `visitor_count_visits`, `idvisitor`, CONCAT(CURRENT_DATE() , \" \", FLOOR(RAND()*24) , \":\",FLOOR(RAND()*60),\":\",FLOOR(RAND()*60)), `idaction_url`, `idlink_va`, `referer_visit_server_date`, `referer_type`, `referer_name`, `referer_keyword`, `visitor_returning`, `location_country`, `url`, `idgoal`, `revenue`, FLOOR(`buster` * RAND()), CONCAT(`idorder`,SUBSTRING(MD5(RAND()) FROM 1 FOR 9)) , `custom_var_k1`, `custom_var_v1`, `custom_var_k2`, `custom_var_v2`, `custom_var_k3`, `custom_var_v3`, `custom_var_k4`, `custom_var_v4`, `custom_var_k5`, `custom_var_v5`, `items`, `revenue_subtotal`, `revenue_tax`, `revenue_shipping`, `revenue_discount` - FROM `". Piwik_Common::prefixTable('log_conversion')."` + FROM `" . Piwik_Common::prefixTable('log_conversion') . "` WHERE idsite >= 1 AND date(server_time) between '$startDate' and '$endDate' ;"; - $result = $db->query($sql); - - $this->log(" Copying purchased items..."); - $sql = "INSERT INTO `". Piwik_Common::prefixTable('log_conversion_item')."` (`idsite`, `idvisitor`, `server_time`, `idvisit`, `idorder`, `idaction_sku`, `idaction_name`, `idaction_category`, `price`, `quantity`, `deleted`) + $result = $db->query($sql); + + $this->log(" Copying purchased items..."); + $sql = "INSERT INTO `" . Piwik_Common::prefixTable('log_conversion_item') . "` (`idsite`, `idvisitor`, `server_time`, `idvisit`, `idorder`, `idaction_sku`, `idaction_name`, `idaction_category`, `price`, `quantity`, `deleted`) SELECT `idsite`, `idvisitor`, CONCAT(CURRENT_DATE() , \" \", TIME(`server_time`)), `idvisit`, CONCAT(`idorder`,SUBSTRING(MD5(RAND()) FROM 1 FOR 9)) , `idaction_sku`, `idaction_name`, `idaction_category`, `price`, `quantity`, `deleted` - FROM `". Piwik_Common::prefixTable('log_conversion_item')."` + FROM `" . Piwik_Common::prefixTable('log_conversion_item') . "` WHERE idsite >= 1 AND date(server_time) between '$startDate' and '$endDate' ;"; - $result = $db->query($sql); - - $now = $this->getVisitsToday(); - $actions = $this->getActionsToday(); - $purchasedItems = $this->getConversionItemsToday(); - $conversions = $this->getConversionsToday(); - - $this->log(" -------------------------------------"); - $this->log(" Today visits after import: " . $now); - $this->log(" Actions: " . $actions); - $this->log(" Purchased items: " . $purchasedItems); - $this->log(" Conversions: " . $conversions); - $this->log(" - New visits created: " . ($now-$initial)); - $this->log(" - Actions created: " . ($actions-$initialActions)); - $this->log(" - New conversions created: " . ($conversions-$initialConversions)); - $this->log(" - New purchased items created: " . ($purchasedItems-$initialPurchasedItems)); + $result = $db->query($sql); + + $now = $this->getVisitsToday(); + $actions = $this->getActionsToday(); + $purchasedItems = $this->getConversionItemsToday(); + $conversions = $this->getConversionsToday(); + + $this->log(" -------------------------------------"); + $this->log(" Today visits after import: " . $now); + $this->log(" Actions: " . $actions); + $this->log(" Purchased items: " . $purchasedItems); + $this->log(" Conversions: " . $conversions); + $this->log(" - New visits created: " . ($now - $initial)); + $this->log(" - Actions created: " . ($actions - $initialActions)); + $this->log(" - New conversions created: " . ($conversions - $initialConversions)); + $this->log(" - New purchased items created: " . ($purchasedItems - $initialPurchasedItems)); $this->log("done"); - } - - function delete() - { - $this->log("Deleting logs for today..."); - $db = Zend_Registry::get('db'); - $sql = "DELETE FROM ". Piwik_Common::prefixTable('log_visit')." + } + + function delete() + { + $this->log("Deleting logs for today..."); + $db = Zend_Registry::get('db'); + $sql = "DELETE FROM " . Piwik_Common::prefixTable('log_visit') . " WHERE date(visit_last_action_time) = CURRENT_DATE();"; - $db->query($sql); - foreach(array('log_link_visit_action', 'log_conversion', 'log_conversion_item') as $table) - { - $sql = "DELETE FROM ".Piwik_Common::prefixTable($table)." + $db->query($sql); + foreach (array('log_link_visit_action', 'log_conversion', 'log_conversion_item') as $table) { + $sql = "DELETE FROM " . Piwik_Common::prefixTable($table) . " WHERE date(server_time) = CURRENT_DATE();"; - $db->query($sql); - } - $sql = "OPTIMIZE TABLE ". Piwik_Common::prefixTable('log_link_visit_action').", ". Piwik_Common::prefixTable('log_conversion').", ". Piwik_Common::prefixTable('log_conversion_item').", ". Piwik_Common::prefixTable('log_visit').""; - $db->query($sql); - $this->log("done"); - } - - function log($m) - { - Piwik::log($m); - } - function getVisitsToday() - { - $sql = "SELECT count(*) FROM `". Piwik_Common::prefixTable('log_visit')."` WHERE idsite >= 1 AND DATE(`visit_last_action_time`) = CURRENT_DATE;"; - return Zend_Registry::get('db')->fetchOne($sql); - } - function getConversionItemsToday($table = 'log_conversion_item') - { - $sql = "SELECT count(*) FROM `".Piwik_Common::prefixTable($table)."` WHERE idsite >= 1 AND DATE(`server_time`) = CURRENT_DATE;"; - return Zend_Registry::get('db')->fetchOne($sql); - } - function getConversionsToday() - { - return $this->getConversionItemsToday($table = "log_conversion"); - } - function getActionsToday() - { - $sql = "SELECT count(*) FROM `". Piwik_Common::prefixTable('log_link_visit_action')."` WHERE idsite >= 1 AND DATE(`server_time`) = CURRENT_DATE;"; - return Zend_Registry::get('db')->fetchOne($sql); - } + $db->query($sql); + } + $sql = "OPTIMIZE TABLE " . Piwik_Common::prefixTable('log_link_visit_action') . ", " . Piwik_Common::prefixTable('log_conversion') . ", " . Piwik_Common::prefixTable('log_conversion_item') . ", " . Piwik_Common::prefixTable('log_visit') . ""; + $db->query($sql); + $this->log("done"); + } + + function log($m) + { + Piwik::log($m); + } + + function getVisitsToday() + { + $sql = "SELECT count(*) FROM `" . Piwik_Common::prefixTable('log_visit') . "` WHERE idsite >= 1 AND DATE(`visit_last_action_time`) = CURRENT_DATE;"; + return Zend_Registry::get('db')->fetchOne($sql); + } + + function getConversionItemsToday($table = 'log_conversion_item') + { + $sql = "SELECT count(*) FROM `" . Piwik_Common::prefixTable($table) . "` WHERE idsite >= 1 AND DATE(`server_time`) = CURRENT_DATE;"; + return Zend_Registry::get('db')->fetchOne($sql); + } + + function getConversionsToday() + { + return $this->getConversionItemsToday($table = "log_conversion"); + } + + function getActionsToday() + { + $sql = "SELECT count(*) FROM `" . Piwik_Common::prefixTable('log_link_visit_action') . "` WHERE idsite >= 1 AND DATE(`server_time`) = CURRENT_DATE;"; + return Zend_Registry::get('db')->fetchOne($sql); + } } diff --git a/misc/others/tracker_simpleImageTracker.php b/misc/others/tracker_simpleImageTracker.php index ae7502874a..373ed31474 100644 --- a/misc/others/tracker_simpleImageTracker.php +++ b/misc/others/tracker_simpleImageTracker.php @@ -1,4 +1,5 @@ - + + This page loads a Simple Tracker request to Piwik website id=1 @@ -7,7 +8,8 @@ This page loads a Simple Tracker request to Piwik website id=1 require_once "../libs/PiwikTracker/PiwikTracker.php"; PiwikTracker::$URL = 'http://example.org/piwik/'; // Example 1: Tracks a pageview for Website id = {$IDSITE} -$trackingURL = Piwik_getUrlTrackPageView( $idSite = 1, $customTitle = 'This title will appear in the report Actions > Page titles'); -echo ''; +$trackingURL = Piwik_getUrlTrackPageView($idSite = 1, $customTitle = 'This title will appear in the report Actions > Page titles'); +echo ''; ?> - \ No newline at end of file + + \ No newline at end of file diff --git a/misc/others/uninstall-delete-piwik-directory.php b/misc/others/uninstall-delete-piwik-directory.php index b51ae63577..e05f70259c 100644 --- a/misc/others/uninstall-delete-piwik-directory.php +++ b/misc/others/uninstall-delete-piwik-directory.php @@ -9,18 +9,18 @@ // please let us know at hello@piwik.org - we are interested by your experience function unlinkRecursive($dir) { - if(!$dh = @opendir($dir)) return "Warning: folder $dir couldn't be read by PHP"; - while (false !== ($obj = readdir($dh))) { - if($obj == '.' || $obj == '..') { - continue; - } - if (!@unlink($dir . '/' . $obj)) { - unlinkRecursive($dir.'/'.$obj, true); - } - } - closedir($dh); - @rmdir($dir); - return "Folder $dir deleted!"; + if (!$dh = @opendir($dir)) return "Warning: folder $dir couldn't be read by PHP"; + while (false !== ($obj = readdir($dh))) { + if ($obj == '.' || $obj == '..') { + continue; + } + if (!@unlink($dir . '/' . $obj)) { + unlinkRecursive($dir . '/' . $obj, true); + } + } + closedir($dh); + @rmdir($dir); + return "Folder $dir deleted!"; } echo unlinkRecursive('piwik/'); diff --git a/misc/others/widget_example_lastvisits.html b/misc/others/widget_example_lastvisits.html index 403a3d4f88..a515255d03 100644 --- a/misc/others/widget_example_lastvisits.html +++ b/misc/others/widget_example_lastvisits.html @@ -1,5 +1,11 @@ - + +

Number of visits per week for the last 52 weeks

-
+ +
+ +
diff --git a/misc/proxy-hide-piwik-url/piwik.php b/misc/proxy-hide-piwik-url/piwik.php index 9bce43bb2b..eabac8c097 100644 --- a/misc/proxy-hide-piwik-url/piwik.php +++ b/misc/proxy-hide-piwik-url/piwik.php @@ -29,45 +29,45 @@ $timeout = 5; // --------------------------- // 1) PIWIK.JS PROXY: No _GET parameter, we serve the JS file if (empty($_GET)) { - $modifiedSince = false; - if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { - $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE']; - // strip any trailing data appended to header - if (false !== ($semicolon = strpos($modifiedSince, ';'))) { - $modifiedSince = strtotime(substr($modifiedSince, 0, $semicolon)); - } - } - // Re-download the piwik.js once a day maximum - $lastModified = time()-86400; + $modifiedSince = false; + if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { + $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE']; + // strip any trailing data appended to header + if (false !== ($semicolon = strpos($modifiedSince, ';'))) { + $modifiedSince = strtotime(substr($modifiedSince, 0, $semicolon)); + } + } + // Re-download the piwik.js once a day maximum + $lastModified = time() - 86400; - // set HTTP response headers - header('Vary: Accept-Encoding'); + // set HTTP response headers + header('Vary: Accept-Encoding'); - // Returns 304 if not modified since - if (!empty($modifiedSince) && $modifiedSince < $lastModified) { - header(sprintf("%s 304 Not Modified", $_SERVER['SERVER_PROTOCOL'])); - } else { - header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - @header('Content-Type: application/javascript; charset=UTF-8'); - if ($piwikJs = file_get_contents($PIWIK_URL.'piwik.js')) { - echo $piwikJs; - } else { - header($_SERVER['SERVER_PROTOCOL'] . '505 Internal server error'); - } - } - exit; + // Returns 304 if not modified since + if (!empty($modifiedSince) && $modifiedSince < $lastModified) { + header(sprintf("%s 304 Not Modified", $_SERVER['SERVER_PROTOCOL'])); + } else { + header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + @header('Content-Type: application/javascript; charset=UTF-8'); + if ($piwikJs = file_get_contents($PIWIK_URL . 'piwik.js')) { + echo $piwikJs; + } else { + header($_SERVER['SERVER_PROTOCOL'] . '505 Internal server error'); + } + } + exit; } // 2) PIWIK.PHP PROXY: GET parameters found, this is a tracking request, we redirect it to Piwik $url = sprintf("%spiwik.php?cip=%s&token_auth=%s&", $PIWIK_URL, @$_SERVER['REMOTE_ADDR'], $TOKEN_AUTH); -foreach($_GET as $key=>$value) { - $url .= $key .'='.urlencode($value).'&'; +foreach ($_GET as $key => $value) { + $url .= $key . '=' . urlencode($value) . '&'; } header("Content-Type: image/gif"); $stream_options = array('http' => array( - 'user_agent' => @$_SERVER['HTTP_USER_AGENT'], - 'header' => sprintf("Accept-Language: %s%s\r\n", @str_replace(array("\n","\t","\r"), "", $_SERVER['HTTP_ACCEPT_LANGUAGE'])), - 'timeout' => $timeout + 'user_agent' => @$_SERVER['HTTP_USER_AGENT'], + 'header' => sprintf("Accept-Language: %s%s\r\n", @str_replace(array("\n", "\t", "\r"), "", $_SERVER['HTTP_ACCEPT_LANGUAGE'])), + 'timeout' => $timeout )); $ctx = stream_context_create($stream_options); echo file_get_contents($url, 0, $ctx); diff --git a/piwik.php b/piwik.php index 14e5e5d518..6633313410 100644 --- a/piwik.php +++ b/piwik.php @@ -11,81 +11,74 @@ $GLOBALS['PIWIK_TRACKER_DEBUG'] = false; $GLOBALS['PIWIK_TRACKER_DEBUG_FORCE_SCHEDULED_TASKS'] = false; define('PIWIK_ENABLE_TRACKING', true); -define('PIWIK_DOCUMENT_ROOT', dirname(__FILE__)=='/'?'':dirname(__FILE__)); -if(file_exists(PIWIK_DOCUMENT_ROOT . '/bootstrap.php')) -{ - require_once PIWIK_DOCUMENT_ROOT . '/bootstrap.php'; +define('PIWIK_DOCUMENT_ROOT', dirname(__FILE__) == '/' ? '' : dirname(__FILE__)); +if (file_exists(PIWIK_DOCUMENT_ROOT . '/bootstrap.php')) { + require_once PIWIK_DOCUMENT_ROOT . '/bootstrap.php'; } $GLOBALS['PIWIK_TRACKER_MODE'] = true; -error_reporting(E_ALL|E_NOTICE); +error_reporting(E_ALL | E_NOTICE); @ini_set('xdebug.show_exception_trace', 0); @ini_set('magic_quotes_runtime', 0); -if(!defined('PIWIK_USER_PATH')) -{ - define('PIWIK_USER_PATH', PIWIK_DOCUMENT_ROOT); +if (!defined('PIWIK_USER_PATH')) { + define('PIWIK_USER_PATH', PIWIK_DOCUMENT_ROOT); } -if(!defined('PIWIK_INCLUDE_PATH')) -{ - define('PIWIK_INCLUDE_PATH', PIWIK_DOCUMENT_ROOT); +if (!defined('PIWIK_INCLUDE_PATH')) { + define('PIWIK_INCLUDE_PATH', PIWIK_DOCUMENT_ROOT); } @ignore_user_abort(true); -require_once PIWIK_INCLUDE_PATH .'/libs/upgradephp/upgrade.php'; -require_once PIWIK_INCLUDE_PATH .'/libs/Event/Dispatcher.php'; -require_once PIWIK_INCLUDE_PATH .'/libs/Event/Notification.php'; -require_once PIWIK_INCLUDE_PATH .'/core/PluginsManager.php'; -require_once PIWIK_INCLUDE_PATH .'/core/Plugin.php'; -require_once PIWIK_INCLUDE_PATH .'/core/Common.php'; -require_once PIWIK_INCLUDE_PATH .'/core/IP.php'; -require_once PIWIK_INCLUDE_PATH .'/core/Tracker.php'; -require_once PIWIK_INCLUDE_PATH .'/core/Config.php'; -require_once PIWIK_INCLUDE_PATH .'/core/Translate.php'; -require_once PIWIK_INCLUDE_PATH .'/core/Tracker/Cache.php'; -require_once PIWIK_INCLUDE_PATH .'/core/Tracker/Db.php'; -require_once PIWIK_INCLUDE_PATH .'/core/Tracker/Db/Exception.php'; -require_once PIWIK_INCLUDE_PATH .'/core/Tracker/IgnoreCookie.php'; -require_once PIWIK_INCLUDE_PATH .'/core/Tracker/Visit.php'; -require_once PIWIK_INCLUDE_PATH .'/core/Tracker/GoalManager.php'; -require_once PIWIK_INCLUDE_PATH .'/core/Tracker/Action.php'; -require_once PIWIK_INCLUDE_PATH .'/core/CacheFile.php'; -require_once PIWIK_INCLUDE_PATH .'/core/Cookie.php'; +require_once PIWIK_INCLUDE_PATH . '/libs/upgradephp/upgrade.php'; +require_once PIWIK_INCLUDE_PATH . '/libs/Event/Dispatcher.php'; +require_once PIWIK_INCLUDE_PATH . '/libs/Event/Notification.php'; +require_once PIWIK_INCLUDE_PATH . '/core/PluginsManager.php'; +require_once PIWIK_INCLUDE_PATH . '/core/Plugin.php'; +require_once PIWIK_INCLUDE_PATH . '/core/Common.php'; +require_once PIWIK_INCLUDE_PATH . '/core/IP.php'; +require_once PIWIK_INCLUDE_PATH . '/core/Tracker.php'; +require_once PIWIK_INCLUDE_PATH . '/core/Config.php'; +require_once PIWIK_INCLUDE_PATH . '/core/Translate.php'; +require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Cache.php'; +require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db.php'; +require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Exception.php'; +require_once PIWIK_INCLUDE_PATH . '/core/Tracker/IgnoreCookie.php'; +require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Visit.php'; +require_once PIWIK_INCLUDE_PATH . '/core/Tracker/GoalManager.php'; +require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Action.php'; +require_once PIWIK_INCLUDE_PATH . '/core/CacheFile.php'; +require_once PIWIK_INCLUDE_PATH . '/core/Cookie.php'; session_cache_limiter('nocache'); @date_default_timezone_set('UTC'); -if(!defined('PIWIK_ENABLE_TRACKING') || PIWIK_ENABLE_TRACKING) -{ - ob_start(); +if (!defined('PIWIK_ENABLE_TRACKING') || PIWIK_ENABLE_TRACKING) { + ob_start(); } -if($GLOBALS['PIWIK_TRACKER_DEBUG'] === true) -{ - require_once PIWIK_INCLUDE_PATH .'/core/Loader.php'; - require_once PIWIK_INCLUDE_PATH .'/core/ErrorHandler.php'; - require_once PIWIK_INCLUDE_PATH .'/core/ExceptionHandler.php'; - $timer = new Piwik_Timer(); - set_error_handler('Piwik_ErrorHandler'); - set_exception_handler('Piwik_ExceptionHandler'); - printDebug("Debug enabled - Input parameters:
" . var_export($_GET, true)); - Piwik_Tracker_Db::enableProfiling(); - Piwik::createConfigObject(); - Piwik::createLogObject(); +if ($GLOBALS['PIWIK_TRACKER_DEBUG'] === true) { + require_once PIWIK_INCLUDE_PATH . '/core/Loader.php'; + require_once PIWIK_INCLUDE_PATH . '/core/ErrorHandler.php'; + require_once PIWIK_INCLUDE_PATH . '/core/ExceptionHandler.php'; + $timer = new Piwik_Timer(); + set_error_handler('Piwik_ErrorHandler'); + set_exception_handler('Piwik_ExceptionHandler'); + printDebug("Debug enabled - Input parameters:
" . var_export($_GET, true)); + Piwik_Tracker_Db::enableProfiling(); + Piwik::createConfigObject(); + Piwik::createLogObject(); } -if(!defined('PIWIK_ENABLE_TRACKING') || PIWIK_ENABLE_TRACKING) -{ - $process = new Piwik_Tracker(); - try { - $process->main(); - } catch(Exception $e) { - echo "Error:" . $e->getMessage(); - } - ob_end_flush(); - if($GLOBALS['PIWIK_TRACKER_DEBUG'] === true) - { - printDebug($_COOKIE); - printDebug($timer); - } +if (!defined('PIWIK_ENABLE_TRACKING') || PIWIK_ENABLE_TRACKING) { + $process = new Piwik_Tracker(); + try { + $process->main(); + } catch (Exception $e) { + echo "Error:" . $e->getMessage(); + } + ob_end_flush(); + if ($GLOBALS['PIWIK_TRACKER_DEBUG'] === true) { + printDebug($_COOKIE); + printDebug($timer); + } } diff --git a/plugins/API/API.php b/plugins/API/API.php index 0181bd6996..211122da30 100644 --- a/plugins/API/API.php +++ b/plugins/API/API.php @@ -13,56 +13,58 @@ /** * @package Piwik_API */ -class Piwik_API extends Piwik_Plugin { - - public function getInformation() - { - return array( - 'description' => Piwik_Translate('API_PluginDescription'), - 'author' => 'Piwik', - 'author_homepage' => 'http://piwik.org/', - 'version' => Piwik_Version::VERSION, - ); - } - - public function getListHooksRegistered() - { - return array( - 'AssetManager.getCssFiles' => 'getCssFiles', - 'TopMenu.add' => 'addTopMenu', - ); - } - - public function addTopMenu() - { - $apiUrlParams = array('module' => 'API', 'action' => 'listAllAPI'); - $tooltip = Piwik_Translate('API_TopLinkTooltip'); - - Piwik_AddTopMenu('General_API', $apiUrlParams, true, 7, $isHTML = false, $tooltip); - - $this->addTopMenuMobileApp(); - } - - protected function addTopMenuMobileApp() - { - if (empty($_SERVER['HTTP_USER_AGENT'])) { - return; - } - require_once PIWIK_INCLUDE_PATH . '/libs/UserAgentParser/UserAgentParser.php'; - $os = UserAgentParser::getOperatingSystem($_SERVER['HTTP_USER_AGENT']); - if ($os && in_array($os['id'], array('AND', 'IPD', 'IPA', 'IPH'))) { - Piwik_AddTopMenu('Piwik Mobile App', array('module' => 'Proxy', 'action' => 'redirect', 'url' => 'http://piwik.org/mobile/'), true, 4); - } - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - public function getCssFiles($notification) { - $cssFiles = &$notification->getNotificationObject(); - - $cssFiles[] = "plugins/API/css/styles.css"; - } +class Piwik_API extends Piwik_Plugin +{ + + public function getInformation() + { + return array( + 'description' => Piwik_Translate('API_PluginDescription'), + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'version' => Piwik_Version::VERSION, + ); + } + + public function getListHooksRegistered() + { + return array( + 'AssetManager.getCssFiles' => 'getCssFiles', + 'TopMenu.add' => 'addTopMenu', + ); + } + + public function addTopMenu() + { + $apiUrlParams = array('module' => 'API', 'action' => 'listAllAPI'); + $tooltip = Piwik_Translate('API_TopLinkTooltip'); + + Piwik_AddTopMenu('General_API', $apiUrlParams, true, 7, $isHTML = false, $tooltip); + + $this->addTopMenuMobileApp(); + } + + protected function addTopMenuMobileApp() + { + if (empty($_SERVER['HTTP_USER_AGENT'])) { + return; + } + require_once PIWIK_INCLUDE_PATH . '/libs/UserAgentParser/UserAgentParser.php'; + $os = UserAgentParser::getOperatingSystem($_SERVER['HTTP_USER_AGENT']); + if ($os && in_array($os['id'], array('AND', 'IPD', 'IPA', 'IPH'))) { + Piwik_AddTopMenu('Piwik Mobile App', array('module' => 'Proxy', 'action' => 'redirect', 'url' => 'http://piwik.org/mobile/'), true, 4); + } + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + public function getCssFiles($notification) + { + $cssFiles = & $notification->getNotificationObject(); + + $cssFiles[] = "plugins/API/css/styles.css"; + } } @@ -85,1619 +87,1488 @@ class Piwik_API extends Piwik_Plugin { */ class Piwik_API_API { - static private $instance = null; - - /** - * @return Piwik_API_API - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - /** - * Get Piwik version - * @return string - */ - public function getPiwikVersion() - { - Piwik::checkUserHasSomeViewAccess(); - return Piwik_Version::VERSION; - } - - /** - * Returns the section [APISettings] if defined in config.ini.php - * @return array - */ - public function getSettings() - { - return Piwik_Config::getInstance()->APISettings; - } - - /** - * Derive the unit name from a column name - * @param $column - * @param $idSite - * @return string - * @ignore - */ - static public function getUnit($column, $idSite) - { - $nameToUnit = array( - '_rate' => '%', - 'revenue' => Piwik::getCurrency($idSite), - '_time_' => 's' - ); - - foreach ($nameToUnit as $pattern => $type) - { - if (strpos($column, $pattern) !== false) - { - return $type; - } - } - - return ''; - } - - /** - * Is a lower value for a given column better? - * @param $column - * @return bool - * - * @ignore - */ - static public function isLowerValueBetter($column) - { - $lowerIsBetterPatterns = array( - 'bounce', 'exit' - ); - - foreach ($lowerIsBetterPatterns as $pattern) - { - if (strpos($column, $pattern) !== false) - { - return true; - } - } - - return false; - } - - /** - * Default translations for many core metrics. - * This is used for exports with translated labels. The exports contain columns that - * are not visible in the UI and not present in the API meta data. These columns are - * translated here. - * @return array - */ - static public function getDefaultMetricTranslations() - { - $trans = array( - 'label' => 'General_ColumnLabel', - 'date' => 'General_Date', - 'avg_time_on_page' => 'General_ColumnAverageTimeOnPage', - 'sum_time_spent' => 'General_ColumnSumVisitLength', - 'sum_visit_length' => 'General_ColumnSumVisitLength', - 'bounce_count' => 'General_ColumnBounces', - 'bounce_count_returning' => 'VisitFrequency_ColumnBounceCountForReturningVisits', - 'max_actions' => 'General_ColumnMaxActions', - 'max_actions_returning' => 'VisitFrequency_ColumnMaxActionsInReturningVisit', - 'nb_visits_converted_returning' => 'VisitFrequency_ColumnNbReturningVisitsConverted', - 'sum_visit_length_returning' => 'VisitFrequency_ColumnSumVisitLengthReturning', - 'nb_visits_converted' => 'General_ColumnVisitsWithConversions', - 'nb_conversions' => 'Goals_ColumnConversions', - 'revenue' => 'Goals_ColumnRevenue', - 'nb_hits' => 'General_ColumnPageviews', - 'entry_nb_visits' => 'General_ColumnEntrances', - 'entry_nb_uniq_visitors' => 'General_ColumnUniqueEntrances', - 'exit_nb_visits' => 'General_ColumnExits', - 'exit_nb_uniq_visitors' => 'General_ColumnUniqueExits', - 'entry_bounce_count' => 'General_ColumnBounces', - 'exit_bounce_count' => 'General_ColumnBounces', - 'exit_rate' => 'General_ColumnExitRate' - ); - - $trans = array_map('Piwik_Translate', $trans); - - $dailySum = ' ('.Piwik_Translate('General_DailySum').')'; - $afterEntry = ' '.Piwik_Translate('General_AfterEntry'); - - $trans['sum_daily_nb_uniq_visitors'] = Piwik_Translate('General_ColumnNbUniqVisitors').$dailySum; - $trans['sum_daily_entry_nb_uniq_visitors'] = Piwik_Translate('General_ColumnUniqueEntrances').$dailySum; - $trans['sum_daily_exit_nb_uniq_visitors'] = Piwik_Translate('General_ColumnUniqueExits').$dailySum; - $trans['entry_nb_actions'] = Piwik_Translate('General_ColumnNbActions').$afterEntry; - $trans['entry_sum_visit_length'] = Piwik_Translate('General_ColumnSumVisitLength').$afterEntry; - - $api = self::getInstance(); - $trans = array_merge($api->getDefaultMetrics(), $api->getDefaultProcessedMetrics(), $trans); - - return $trans; - } - - public function getDefaultMetrics() - { - $translations = array( - // Standard metrics - 'nb_visits' => 'General_ColumnNbVisits', - 'nb_uniq_visitors' => 'General_ColumnNbUniqVisitors', - 'nb_actions' => 'General_ColumnNbActions', + static private $instance = null; + + /** + * @return Piwik_API_API + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + /** + * Get Piwik version + * @return string + */ + public function getPiwikVersion() + { + Piwik::checkUserHasSomeViewAccess(); + return Piwik_Version::VERSION; + } + + /** + * Returns the section [APISettings] if defined in config.ini.php + * @return array + */ + public function getSettings() + { + return Piwik_Config::getInstance()->APISettings; + } + + /** + * Derive the unit name from a column name + * @param $column + * @param $idSite + * @return string + * @ignore + */ + static public function getUnit($column, $idSite) + { + $nameToUnit = array( + '_rate' => '%', + 'revenue' => Piwik::getCurrency($idSite), + '_time_' => 's' + ); + + foreach ($nameToUnit as $pattern => $type) { + if (strpos($column, $pattern) !== false) { + return $type; + } + } + + return ''; + } + + /** + * Is a lower value for a given column better? + * @param $column + * @return bool + * + * @ignore + */ + static public function isLowerValueBetter($column) + { + $lowerIsBetterPatterns = array( + 'bounce', 'exit' + ); + + foreach ($lowerIsBetterPatterns as $pattern) { + if (strpos($column, $pattern) !== false) { + return true; + } + } + + return false; + } + + /** + * Default translations for many core metrics. + * This is used for exports with translated labels. The exports contain columns that + * are not visible in the UI and not present in the API meta data. These columns are + * translated here. + * @return array + */ + static public function getDefaultMetricTranslations() + { + $trans = array( + 'label' => 'General_ColumnLabel', + 'date' => 'General_Date', + 'avg_time_on_page' => 'General_ColumnAverageTimeOnPage', + 'sum_time_spent' => 'General_ColumnSumVisitLength', + 'sum_visit_length' => 'General_ColumnSumVisitLength', + 'bounce_count' => 'General_ColumnBounces', + 'bounce_count_returning' => 'VisitFrequency_ColumnBounceCountForReturningVisits', + 'max_actions' => 'General_ColumnMaxActions', + 'max_actions_returning' => 'VisitFrequency_ColumnMaxActionsInReturningVisit', + 'nb_visits_converted_returning' => 'VisitFrequency_ColumnNbReturningVisitsConverted', + 'sum_visit_length_returning' => 'VisitFrequency_ColumnSumVisitLengthReturning', + 'nb_visits_converted' => 'General_ColumnVisitsWithConversions', + 'nb_conversions' => 'Goals_ColumnConversions', + 'revenue' => 'Goals_ColumnRevenue', + 'nb_hits' => 'General_ColumnPageviews', + 'entry_nb_visits' => 'General_ColumnEntrances', + 'entry_nb_uniq_visitors' => 'General_ColumnUniqueEntrances', + 'exit_nb_visits' => 'General_ColumnExits', + 'exit_nb_uniq_visitors' => 'General_ColumnUniqueExits', + 'entry_bounce_count' => 'General_ColumnBounces', + 'exit_bounce_count' => 'General_ColumnBounces', + 'exit_rate' => 'General_ColumnExitRate' + ); + + $trans = array_map('Piwik_Translate', $trans); + + $dailySum = ' (' . Piwik_Translate('General_DailySum') . ')'; + $afterEntry = ' ' . Piwik_Translate('General_AfterEntry'); + + $trans['sum_daily_nb_uniq_visitors'] = Piwik_Translate('General_ColumnNbUniqVisitors') . $dailySum; + $trans['sum_daily_entry_nb_uniq_visitors'] = Piwik_Translate('General_ColumnUniqueEntrances') . $dailySum; + $trans['sum_daily_exit_nb_uniq_visitors'] = Piwik_Translate('General_ColumnUniqueExits') . $dailySum; + $trans['entry_nb_actions'] = Piwik_Translate('General_ColumnNbActions') . $afterEntry; + $trans['entry_sum_visit_length'] = Piwik_Translate('General_ColumnSumVisitLength') . $afterEntry; + + $api = self::getInstance(); + $trans = array_merge($api->getDefaultMetrics(), $api->getDefaultProcessedMetrics(), $trans); + + return $trans; + } + + public function getDefaultMetrics() + { + $translations = array( + // Standard metrics + 'nb_visits' => 'General_ColumnNbVisits', + 'nb_uniq_visitors' => 'General_ColumnNbUniqVisitors', + 'nb_actions' => 'General_ColumnNbActions', // Do not display these in reports, as they are not so relevant // They are used to process metrics below // 'nb_visits_converted' => 'General_ColumnVisitsWithConversions', // 'max_actions' => 'General_ColumnMaxActions', // 'sum_visit_length' => 'General_ColumnSumVisitLength', // 'bounce_count' - ); - $translations = array_map('Piwik_Translate', $translations); - return $translations; - } - - public function getDefaultProcessedMetrics() - { - $translations = array( - // Processed in AddColumnsProcessedMetrics - 'nb_actions_per_visit' => 'General_ColumnActionsPerVisit', - 'avg_time_on_site' => 'General_ColumnAvgTimeOnSite', - 'bounce_rate' => 'General_ColumnBounceRate', - 'conversion_rate' => 'General_ColumnConversionRate', - ); - return array_map('Piwik_Translate', $translations); - } - - public function getDefaultMetricsDocumentation() - { - $documentation = array( - 'nb_visits' => 'General_ColumnNbVisitsDocumentation', - 'nb_uniq_visitors' => 'General_ColumnNbUniqVisitorsDocumentation', - 'nb_actions' => 'General_ColumnNbActionsDocumentation', - 'nb_actions_per_visit' => 'General_ColumnActionsPerVisitDocumentation', - 'avg_time_on_site' => 'General_ColumnAvgTimeOnSiteDocumentation', - 'bounce_rate' => 'General_ColumnBounceRateDocumentation', - 'conversion_rate' => 'General_ColumnConversionRateDocumentation', - 'avg_time_on_page' => 'General_ColumnAverageTimeOnPageDocumentation', - 'nb_hits' => 'General_ColumnPageviewsDocumentation', - 'exit_rate' => 'General_ColumnExitRateDocumentation' - ); - return array_map('Piwik_Translate', $documentation); - } - - public function getSegmentsMetadata($idSites = array(), $_hideImplementationData = true) - { - $segments = array(); - Piwik_PostEvent('API.getSegmentsMetadata', $segments, $idSites); - - $segments[] = array( - 'type' => 'dimension', - 'category' => 'Visit', - 'name' => 'General_VisitorIP', - 'segment' => 'visitIp', - 'acceptedValues' => '13.54.122.1, etc.', - 'sqlSegment' => 'log_visit.location_ip', - 'sqlFilter' => array('Piwik_IP', 'P2N'), - 'permission' => Piwik::isUserHasAdminAccess($idSites), - ); - $segments[] = array( - 'type' => 'dimension', - 'category' => 'Visit', - 'name' => 'General_VisitorID', - 'segment' => 'visitorId', - 'acceptedValues' => '34c31e04394bdc63 - any 16 Hexadecimal chars ID, which can be fetched using the Tracking API function getVisitorId()', - 'sqlSegment' => 'log_visit.idvisitor', - 'sqlFilter' => array('Piwik_Common', 'convertVisitorIdToBin'), - ); - $segments[] = array( - 'type' => 'metric', - 'category' => 'Visit', - 'name' => 'General_NbActions', - 'segment' => 'actions', - 'sqlSegment' => 'log_visit.visit_total_actions', - ); - $segments[] = array( - 'type' => 'metric', - 'category' => 'Visit', - 'name' => 'General_NbSearches', - 'segment' => 'searches', - 'sqlSegment' => 'log_visit.visit_total_searches', - 'acceptedValues' => 'To select all visits who used internal Site Search, use: &segment=searches>0', - ); - $segments[] = array( - 'type' => 'metric', - 'category' => 'Visit', - 'name' => 'General_ColumnVisitDuration', - 'segment' => 'visitDuration', - 'sqlSegment' => 'log_visit.visit_total_time', - ); - $segments[] = array( - 'type' => 'dimension', - 'category' => 'Visit', - 'name' => Piwik_Translate('General_VisitType') . ". ".Piwik_Translate('General_VisitTypeExample', '"&segment=visitorType==returning,visitorType==returningCustomer"'), - 'segment' => 'visitorType', - 'acceptedValues' => 'new, returning, returningCustomer', - 'sqlSegment' => 'log_visit.visitor_returning', - 'sqlFilter' => create_function('$type', 'return $type == "new" ? 0 : ($type == "returning" ? 1 : 2);'), - ); - $segments[] = array( - 'type' => 'metric', - 'category' => 'Visit', - 'name' => 'General_DaysSinceLastVisit', - 'segment' => 'daysSinceLastVisit', - 'sqlSegment' => 'log_visit.visitor_days_since_last', - ); - $segments[] = array( - 'type' => 'metric', - 'category' => 'Visit', - 'name' => 'General_DaysSinceFirstVisit', - 'segment' => 'daysSinceFirstVisit', - 'sqlSegment' => 'log_visit.visitor_days_since_first', - ); - $segments[] = array( - 'type' => 'metric', - 'category' => 'Visit', - 'name' => 'General_NumberOfVisits', - 'segment' => 'visitCount', - 'sqlSegment' => 'log_visit.visitor_count_visits', - ); - - $segments[] = array( - 'type' => 'dimension', - 'category' => 'Visit', - 'name' => 'General_VisitConvertedGoal', - 'segment' => 'visitConverted', - 'acceptedValues' => '0, 1', - 'sqlSegment' => 'log_visit.visit_goal_converted', - ); - - $segments[] = array( - 'type' => 'dimension', - 'category' => 'Visit', - 'name' => Piwik_Translate('General_EcommerceVisitStatus', '"&segment=visitEcommerceStatus==ordered,visitEcommerceStatus==orderedThenAbandonedCart"'), - 'segment' => 'visitEcommerceStatus', - 'acceptedValues' => implode(", ", self::$visitEcommerceStatus), - 'sqlSegment' => 'log_visit.visit_goal_buyer', - 'sqlFilter' => array('Piwik_API_API', 'getVisitEcommerceStatus'), - ); - - $segments[] = array( - 'type' => 'metric', - 'category' => 'Visit', - 'name' => 'General_DaysSinceLastEcommerceOrder', - 'segment' => 'daysSinceLastEcommerceOrder', - 'sqlSegment' => 'log_visit.visitor_days_since_order', - ); - - foreach ($segments as &$segment) - { - $segment['name'] = Piwik_Translate($segment['name']); - $segment['category'] = Piwik_Translate($segment['category']); - - if($_hideImplementationData) - { - unset($segment['sqlFilter']); - unset($segment['sqlSegment']); - } - } - - usort($segments, array($this, 'sortSegments')); - return $segments; - } - - static protected $visitEcommerceStatus = array( - Piwik_Tracker_GoalManager::TYPE_BUYER_NONE => 'none', - Piwik_Tracker_GoalManager::TYPE_BUYER_ORDERED => 'ordered', - Piwik_Tracker_GoalManager::TYPE_BUYER_OPEN_CART => 'abandonedCart', - Piwik_Tracker_GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART => 'orderedThenAbandonedCart', - ); - - /** - * @ignore - */ - static public function getVisitEcommerceStatusFromId($id) - { - if(!isset(self::$visitEcommerceStatus[$id])) - { - throw new Exception("Unexpected ECommerce status value "); - } - return self::$visitEcommerceStatus[$id]; - } - - /** - * @ignore - */ - static public function getVisitEcommerceStatus($status) - { - $id = array_search($status, self::$visitEcommerceStatus); - if($id === false) - { - throw new Exception("Invalid 'visitEcommerceStatus' segment value"); - } - return $id; - } - - private function sortSegments($row1, $row2) - { - $columns = array('type', 'category', 'name', 'segment'); - foreach($columns as $column) - { - // Keep segments ordered alphabetically inside categories.. - $type = -1; - if($column == 'name') $type = 1; - $compare = $type * strcmp($row1[$column], $row2[$column]); - - // hack so that custom variables "page" are grouped together in the doc - if($row1['category'] == Piwik_Translate('CustomVariables_CustomVariables') - && $row1['category'] == $row2['category']) { - $compare = strcmp($row1['segment'], $row2['segment']); - return $compare; - } - if($compare != 0){ - return $compare; - } - } - return $compare; - } - - /** - * Returns the url to application logo (~280x110px) - * - * @param bool $pathOnly If true, returns path relative to doc root. Otherwise, returns a URL. - * @return string - */ - public function getLogoUrl($pathOnly=false) - { - $logo = 'themes/default/images/logo.png'; - if(Piwik_Config::getInstance()->branding['use_custom_logo'] == 1 - && file_exists(Piwik_Common::getPathToPiwikRoot() .'/themes/logo.png')) - { - $logo = 'themes/logo.png'; - } - if(!$pathOnly) { - return Piwik::getPiwikUrl() . $logo; - } - return Piwik_Common::getPathToPiwikRoot() .'/'. $logo; - } - - /** - * Returns the url to header logo (~127x50px) - * - * @param bool $pathOnly If true, returns path relative to doc root. Otherwise, returns a URL. - * @return string - */ - public function getHeaderLogoUrl($pathOnly=false) - { - $logo = 'themes/default/images/logo-header.png'; - if(Piwik_Config::getInstance()->branding['use_custom_logo'] == 1 - && file_exists(Piwik_Common::getPathToPiwikRoot() .'/themes/logo-header.png')) - { - $logo = 'themes/logo-header.png'; - } - if(!$pathOnly) { - return Piwik::getPiwikUrl() . $logo; - } - return Piwik_Common::getPathToPiwikRoot() .'/'. $logo; - } - - /** - * Returns the URL to application SVG Logo - * - * @param bool $pathOnly If true, returns path relative to doc root. Otherwise, returns a URL. - * @return string - */ - public function getSVGLogoUrl($pathOnly=false) - { - $logo = 'themes/default/images/logo.svg'; - if(Piwik_Config::getInstance()->branding['use_custom_logo'] == 1 - && file_exists(Piwik_Common::getPathToPiwikRoot() .'/themes/logo.svg')) - { - $logo = 'themes/logo.svg'; - } - if(!$pathOnly) { - return Piwik::getPiwikUrl() . $logo; - } - return Piwik_Common::getPathToPiwikRoot() .'/'. $logo; - } - - /** - * Returns whether there is an SVG Logo available. - * - * @return bool - */ - public function hasSVGLogo() { - if (Piwik_Config::getInstance()->branding['use_custom_logo'] == 0) { - /* We always have our application logo */ - return true; - } else if(Piwik_Config::getInstance()->branding['use_custom_logo'] == 1 - && file_exists(Piwik_Common::getPathToPiwikRoot() .'/themes/logo.svg')) - { - return true; - } - - return false; - } - - /** - * Loads reports metadata, then return the requested one, - * matching optional API parameters. - */ - public function getMetadata($idSite, $apiModule, $apiAction, $apiParameters = array(), $language = false, - $period = false, $date = false, $hideMetricsDoc = false, $showSubtableReports = false) - { - Piwik_Translate::getInstance()->reloadLanguage($language); - $reportsMetadata = $this->getReportMetadata($idSite, $period, $date, $hideMetricsDoc, $showSubtableReports); - - foreach($reportsMetadata as $report) - { - // See ArchiveProcessing/Period.php - unique visitors are not processed for period != day - if(($period && $period != 'day') && !($apiModule == 'VisitsSummary' && $apiAction == 'get')) - { - unset($report['metrics']['nb_uniq_visitors']); - } - if($report['module'] == $apiModule - && $report['action'] == $apiAction) - { - // No custom parameters - if(empty($apiParameters) - && empty($report['parameters'])) - { - return array($report); - } - if(empty($report['parameters'])) - { - continue; - } - $diff = array_diff($report['parameters'], $apiParameters); - if(empty($diff)) - { - return array($report); - } - } - } - return false; - } - - /** - * Triggers a hook to ask plugins for available Reports. - * Returns metadata information about each report (category, name, dimension, metrics, etc.) - * - * @param string $idSites Comma separated list of website Ids - * @return array - */ - public function getReportMetadata($idSites = '', $period = false, $date = false, $hideMetricsDoc = false, - $showSubtableReports = false) - { - $idSites = Piwik_Site::getIdSitesFromIdSitesString($idSites); - if(!empty($idSites)) - { - Piwik::checkUserHasViewAccess($idSites); - } - - $parameters = array( 'idSites' => $idSites, 'period' => $period, 'date' => $date); - - $availableReports = array(); - Piwik_PostEvent('API.getReportMetadata', $availableReports, $parameters); - foreach ($availableReports as &$availableReport) { - if (!isset($availableReport['metrics'])) { - $availableReport['metrics'] = $this->getDefaultMetrics(); - } - if (!isset($availableReport['processedMetrics'])) { - $availableReport['processedMetrics'] = $this->getDefaultProcessedMetrics(); - } - - if ($hideMetricsDoc) // remove metric documentation if it's not wanted - { - unset($availableReport['metricsDocumentation']); - } - else if (!isset($availableReport['metricsDocumentation'])) - { - // set metric documentation to default if it's not set - $availableReport['metricsDocumentation'] = $this->getDefaultMetricsDocumentation(); - } - - // if hide/show columns specified, hide/show metrics & docs - $availableReport['metrics'] = $this->hideShowMetrics($availableReport['metrics']); - $availableReport['processedMetrics'] = $this->hideShowMetrics($availableReport['processedMetrics']); - if (isset($availableReport['metricsDocumentation'])) - { - $availableReport['metricsDocumentation'] = - $this->hideShowMetrics($availableReport['metricsDocumentation']); - } - } - - // Some plugins need to add custom metrics after all plugins hooked in - Piwik_PostEvent('API.getReportMetadata.end', $availableReports, $parameters); - // Oh this is not pretty! Until we have event listeners order parameter... - Piwik_PostEvent('API.getReportMetadata.end.end', $availableReports, $parameters); - - // Sort results to ensure consistent order - usort($availableReports, array($this, 'sort')); - - // Add the magic API.get report metadata aggregating all plugins API.get API calls automatically - $this->addApiGetMetdata($availableReports); - - $knownMetrics = array_merge( $this->getDefaultMetrics(), $this->getDefaultProcessedMetrics() ); - foreach($availableReports as &$availableReport) - { - // Ensure all metrics have a translation - $metrics = $availableReport['metrics']; - $cleanedMetrics = array(); - foreach($metrics as $metricId => $metricTranslation) - { - // When simply the column name was given, ie 'metric' => array( 'nb_visits' ) - // $metricTranslation is in this case nb_visits. We look for a known translation. - if(is_numeric($metricId) - && isset($knownMetrics[$metricTranslation])) - { - $metricId = $metricTranslation; - $metricTranslation = $knownMetrics[$metricTranslation]; - } - $cleanedMetrics[$metricId] = $metricTranslation; - } - $availableReport['metrics'] = $cleanedMetrics; - - // Remove array elements that are false (to clean up API output) - foreach($availableReport as $attributeName => $attributeValue) - { - if(empty($attributeValue)) - { - unset($availableReport[$attributeName]); - } - } - // when there are per goal metrics, don't display conversion_rate since it can differ from per goal sum - if(isset($availableReport['metricsGoal'])) - { - unset($availableReport['processedMetrics']['conversion_rate']); - unset($availableReport['metricsGoal']['conversion_rate']); - } - - // Processing a uniqueId for each report, - // can be used by UIs as a key to match a given report - $uniqueId = $availableReport['module'] . '_' . $availableReport['action']; - if(!empty($availableReport['parameters'])) - { - foreach($availableReport['parameters'] as $key => $value) - { - $uniqueId .= '_' . $key . '--' . $value; - } - } - $availableReport['uniqueId'] = $uniqueId; - - // Order is used to order reports internally, but not meant to be used outside - unset($availableReport['order']); - } - - // remove subtable reports - if (!$showSubtableReports) - { - foreach ($availableReports as $idx => $report) - { - if (isset($report['isSubtableReport']) && $report['isSubtableReport']) - { - unset($availableReports[$idx]); - } - } - } - - return array_values($availableReports); // make sure array has contiguous key values - } - - - /** - * Add the metadata for the API.get report - * In other plugins, this would hook on 'API.getReportMetadata' - */ - private function addApiGetMetdata(&$availableReports) - { - $metadata = array( - 'category' => Piwik_Translate('General_API'), - 'name' => Piwik_Translate('General_MainMetrics'), - 'module' => 'API', - 'action' => 'get', - 'metrics' => array(), - 'processedMetrics' => array(), - 'metricsDocumentation' => array(), - 'order' => 1 - ); - - $indexesToMerge = array('metrics', 'processedMetrics', 'metricsDocumentation'); - - foreach ($availableReports as $report) - { - if ($report['action'] == 'get') - { - foreach ($indexesToMerge as $index) - { - if (isset($report[$index]) - && is_array($report[$index])) - { - $metadata[$index] = array_merge($metadata[$index], $report[$index]); - } - } - } - } - - $availableReports[] = $metadata; - } - - public function getProcessedReport( $idSite, $period, $date, $apiModule, $apiAction, $segment = false, - $apiParameters = false, $idGoal = false, $language = false, - $showTimer = true, $hideMetricsDoc = false, $idSubtable = false, $showRawMetrics = false) + ); + $translations = array_map('Piwik_Translate', $translations); + return $translations; + } + + public function getDefaultProcessedMetrics() + { + $translations = array( + // Processed in AddColumnsProcessedMetrics + 'nb_actions_per_visit' => 'General_ColumnActionsPerVisit', + 'avg_time_on_site' => 'General_ColumnAvgTimeOnSite', + 'bounce_rate' => 'General_ColumnBounceRate', + 'conversion_rate' => 'General_ColumnConversionRate', + ); + return array_map('Piwik_Translate', $translations); + } + + public function getDefaultMetricsDocumentation() + { + $documentation = array( + 'nb_visits' => 'General_ColumnNbVisitsDocumentation', + 'nb_uniq_visitors' => 'General_ColumnNbUniqVisitorsDocumentation', + 'nb_actions' => 'General_ColumnNbActionsDocumentation', + 'nb_actions_per_visit' => 'General_ColumnActionsPerVisitDocumentation', + 'avg_time_on_site' => 'General_ColumnAvgTimeOnSiteDocumentation', + 'bounce_rate' => 'General_ColumnBounceRateDocumentation', + 'conversion_rate' => 'General_ColumnConversionRateDocumentation', + 'avg_time_on_page' => 'General_ColumnAverageTimeOnPageDocumentation', + 'nb_hits' => 'General_ColumnPageviewsDocumentation', + 'exit_rate' => 'General_ColumnExitRateDocumentation' + ); + return array_map('Piwik_Translate', $documentation); + } + + public function getSegmentsMetadata($idSites = array(), $_hideImplementationData = true) { - $timer = new Piwik_Timer(); - if($apiParameters === false) - { - $apiParameters = array(); - } - if(!empty($idGoal) - && empty($apiParameters['idGoal'])) - { - $apiParameters['idGoal'] = $idGoal; - } + $segments = array(); + Piwik_PostEvent('API.getSegmentsMetadata', $segments, $idSites); + + $segments[] = array( + 'type' => 'dimension', + 'category' => 'Visit', + 'name' => 'General_VisitorIP', + 'segment' => 'visitIp', + 'acceptedValues' => '13.54.122.1, etc.', + 'sqlSegment' => 'log_visit.location_ip', + 'sqlFilter' => array('Piwik_IP', 'P2N'), + 'permission' => Piwik::isUserHasAdminAccess($idSites), + ); + $segments[] = array( + 'type' => 'dimension', + 'category' => 'Visit', + 'name' => 'General_VisitorID', + 'segment' => 'visitorId', + 'acceptedValues' => '34c31e04394bdc63 - any 16 Hexadecimal chars ID, which can be fetched using the Tracking API function getVisitorId()', + 'sqlSegment' => 'log_visit.idvisitor', + 'sqlFilter' => array('Piwik_Common', 'convertVisitorIdToBin'), + ); + $segments[] = array( + 'type' => 'metric', + 'category' => 'Visit', + 'name' => 'General_NbActions', + 'segment' => 'actions', + 'sqlSegment' => 'log_visit.visit_total_actions', + ); + $segments[] = array( + 'type' => 'metric', + 'category' => 'Visit', + 'name' => 'General_NbSearches', + 'segment' => 'searches', + 'sqlSegment' => 'log_visit.visit_total_searches', + 'acceptedValues' => 'To select all visits who used internal Site Search, use: &segment=searches>0', + ); + $segments[] = array( + 'type' => 'metric', + 'category' => 'Visit', + 'name' => 'General_ColumnVisitDuration', + 'segment' => 'visitDuration', + 'sqlSegment' => 'log_visit.visit_total_time', + ); + $segments[] = array( + 'type' => 'dimension', + 'category' => 'Visit', + 'name' => Piwik_Translate('General_VisitType') . ". " . Piwik_Translate('General_VisitTypeExample', '"&segment=visitorType==returning,visitorType==returningCustomer"'), + 'segment' => 'visitorType', + 'acceptedValues' => 'new, returning, returningCustomer', + 'sqlSegment' => 'log_visit.visitor_returning', + 'sqlFilter' => create_function('$type', 'return $type == "new" ? 0 : ($type == "returning" ? 1 : 2);'), + ); + $segments[] = array( + 'type' => 'metric', + 'category' => 'Visit', + 'name' => 'General_DaysSinceLastVisit', + 'segment' => 'daysSinceLastVisit', + 'sqlSegment' => 'log_visit.visitor_days_since_last', + ); + $segments[] = array( + 'type' => 'metric', + 'category' => 'Visit', + 'name' => 'General_DaysSinceFirstVisit', + 'segment' => 'daysSinceFirstVisit', + 'sqlSegment' => 'log_visit.visitor_days_since_first', + ); + $segments[] = array( + 'type' => 'metric', + 'category' => 'Visit', + 'name' => 'General_NumberOfVisits', + 'segment' => 'visitCount', + 'sqlSegment' => 'log_visit.visitor_count_visits', + ); + + $segments[] = array( + 'type' => 'dimension', + 'category' => 'Visit', + 'name' => 'General_VisitConvertedGoal', + 'segment' => 'visitConverted', + 'acceptedValues' => '0, 1', + 'sqlSegment' => 'log_visit.visit_goal_converted', + ); + + $segments[] = array( + 'type' => 'dimension', + 'category' => 'Visit', + 'name' => Piwik_Translate('General_EcommerceVisitStatus', '"&segment=visitEcommerceStatus==ordered,visitEcommerceStatus==orderedThenAbandonedCart"'), + 'segment' => 'visitEcommerceStatus', + 'acceptedValues' => implode(", ", self::$visitEcommerceStatus), + 'sqlSegment' => 'log_visit.visit_goal_buyer', + 'sqlFilter' => array('Piwik_API_API', 'getVisitEcommerceStatus'), + ); + + $segments[] = array( + 'type' => 'metric', + 'category' => 'Visit', + 'name' => 'General_DaysSinceLastEcommerceOrder', + 'segment' => 'daysSinceLastEcommerceOrder', + 'sqlSegment' => 'log_visit.visitor_days_since_order', + ); + + foreach ($segments as &$segment) { + $segment['name'] = Piwik_Translate($segment['name']); + $segment['category'] = Piwik_Translate($segment['category']); + + if ($_hideImplementationData) { + unset($segment['sqlFilter']); + unset($segment['sqlSegment']); + } + } + + usort($segments, array($this, 'sortSegments')); + return $segments; + } + + static protected $visitEcommerceStatus = array( + Piwik_Tracker_GoalManager::TYPE_BUYER_NONE => 'none', + Piwik_Tracker_GoalManager::TYPE_BUYER_ORDERED => 'ordered', + Piwik_Tracker_GoalManager::TYPE_BUYER_OPEN_CART => 'abandonedCart', + Piwik_Tracker_GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART => 'orderedThenAbandonedCart', + ); + + /** + * @ignore + */ + static public function getVisitEcommerceStatusFromId($id) + { + if (!isset(self::$visitEcommerceStatus[$id])) { + throw new Exception("Unexpected ECommerce status value "); + } + return self::$visitEcommerceStatus[$id]; + } + + /** + * @ignore + */ + static public function getVisitEcommerceStatus($status) + { + $id = array_search($status, self::$visitEcommerceStatus); + if ($id === false) { + throw new Exception("Invalid 'visitEcommerceStatus' segment value"); + } + return $id; + } + + private function sortSegments($row1, $row2) + { + $columns = array('type', 'category', 'name', 'segment'); + foreach ($columns as $column) { + // Keep segments ordered alphabetically inside categories.. + $type = -1; + if ($column == 'name') $type = 1; + $compare = $type * strcmp($row1[$column], $row2[$column]); + + // hack so that custom variables "page" are grouped together in the doc + if ($row1['category'] == Piwik_Translate('CustomVariables_CustomVariables') + && $row1['category'] == $row2['category'] + ) { + $compare = strcmp($row1['segment'], $row2['segment']); + return $compare; + } + if ($compare != 0) { + return $compare; + } + } + return $compare; + } + + /** + * Returns the url to application logo (~280x110px) + * + * @param bool $pathOnly If true, returns path relative to doc root. Otherwise, returns a URL. + * @return string + */ + public function getLogoUrl($pathOnly = false) + { + $logo = 'themes/default/images/logo.png'; + if (Piwik_Config::getInstance()->branding['use_custom_logo'] == 1 + && file_exists(Piwik_Common::getPathToPiwikRoot() . '/themes/logo.png') + ) { + $logo = 'themes/logo.png'; + } + if (!$pathOnly) { + return Piwik::getPiwikUrl() . $logo; + } + return Piwik_Common::getPathToPiwikRoot() . '/' . $logo; + } + + /** + * Returns the url to header logo (~127x50px) + * + * @param bool $pathOnly If true, returns path relative to doc root. Otherwise, returns a URL. + * @return string + */ + public function getHeaderLogoUrl($pathOnly = false) + { + $logo = 'themes/default/images/logo-header.png'; + if (Piwik_Config::getInstance()->branding['use_custom_logo'] == 1 + && file_exists(Piwik_Common::getPathToPiwikRoot() . '/themes/logo-header.png') + ) { + $logo = 'themes/logo-header.png'; + } + if (!$pathOnly) { + return Piwik::getPiwikUrl() . $logo; + } + return Piwik_Common::getPathToPiwikRoot() . '/' . $logo; + } + + /** + * Returns the URL to application SVG Logo + * + * @param bool $pathOnly If true, returns path relative to doc root. Otherwise, returns a URL. + * @return string + */ + public function getSVGLogoUrl($pathOnly = false) + { + $logo = 'themes/default/images/logo.svg'; + if (Piwik_Config::getInstance()->branding['use_custom_logo'] == 1 + && file_exists(Piwik_Common::getPathToPiwikRoot() . '/themes/logo.svg') + ) { + $logo = 'themes/logo.svg'; + } + if (!$pathOnly) { + return Piwik::getPiwikUrl() . $logo; + } + return Piwik_Common::getPathToPiwikRoot() . '/' . $logo; + } + + /** + * Returns whether there is an SVG Logo available. + * + * @return bool + */ + public function hasSVGLogo() + { + if (Piwik_Config::getInstance()->branding['use_custom_logo'] == 0) { + /* We always have our application logo */ + return true; + } else if (Piwik_Config::getInstance()->branding['use_custom_logo'] == 1 + && file_exists(Piwik_Common::getPathToPiwikRoot() . '/themes/logo.svg') + ) { + return true; + } + + return false; + } + + /** + * Loads reports metadata, then return the requested one, + * matching optional API parameters. + */ + public function getMetadata($idSite, $apiModule, $apiAction, $apiParameters = array(), $language = false, + $period = false, $date = false, $hideMetricsDoc = false, $showSubtableReports = false) + { + Piwik_Translate::getInstance()->reloadLanguage($language); + $reportsMetadata = $this->getReportMetadata($idSite, $period, $date, $hideMetricsDoc, $showSubtableReports); + + foreach ($reportsMetadata as $report) { + // See ArchiveProcessing/Period.php - unique visitors are not processed for period != day + if (($period && $period != 'day') && !($apiModule == 'VisitsSummary' && $apiAction == 'get')) { + unset($report['metrics']['nb_uniq_visitors']); + } + if ($report['module'] == $apiModule + && $report['action'] == $apiAction + ) { + // No custom parameters + if (empty($apiParameters) + && empty($report['parameters']) + ) { + return array($report); + } + if (empty($report['parameters'])) { + continue; + } + $diff = array_diff($report['parameters'], $apiParameters); + if (empty($diff)) { + return array($report); + } + } + } + return false; + } + + /** + * Triggers a hook to ask plugins for available Reports. + * Returns metadata information about each report (category, name, dimension, metrics, etc.) + * + * @param string $idSites Comma separated list of website Ids + * @return array + */ + public function getReportMetadata($idSites = '', $period = false, $date = false, $hideMetricsDoc = false, + $showSubtableReports = false) + { + $idSites = Piwik_Site::getIdSitesFromIdSitesString($idSites); + if (!empty($idSites)) { + Piwik::checkUserHasViewAccess($idSites); + } + + $parameters = array('idSites' => $idSites, 'period' => $period, 'date' => $date); + + $availableReports = array(); + Piwik_PostEvent('API.getReportMetadata', $availableReports, $parameters); + foreach ($availableReports as &$availableReport) { + if (!isset($availableReport['metrics'])) { + $availableReport['metrics'] = $this->getDefaultMetrics(); + } + if (!isset($availableReport['processedMetrics'])) { + $availableReport['processedMetrics'] = $this->getDefaultProcessedMetrics(); + } + + if ($hideMetricsDoc) // remove metric documentation if it's not wanted + { + unset($availableReport['metricsDocumentation']); + } else if (!isset($availableReport['metricsDocumentation'])) { + // set metric documentation to default if it's not set + $availableReport['metricsDocumentation'] = $this->getDefaultMetricsDocumentation(); + } + + // if hide/show columns specified, hide/show metrics & docs + $availableReport['metrics'] = $this->hideShowMetrics($availableReport['metrics']); + $availableReport['processedMetrics'] = $this->hideShowMetrics($availableReport['processedMetrics']); + if (isset($availableReport['metricsDocumentation'])) { + $availableReport['metricsDocumentation'] = + $this->hideShowMetrics($availableReport['metricsDocumentation']); + } + } + + // Some plugins need to add custom metrics after all plugins hooked in + Piwik_PostEvent('API.getReportMetadata.end', $availableReports, $parameters); + // Oh this is not pretty! Until we have event listeners order parameter... + Piwik_PostEvent('API.getReportMetadata.end.end', $availableReports, $parameters); + + // Sort results to ensure consistent order + usort($availableReports, array($this, 'sort')); + + // Add the magic API.get report metadata aggregating all plugins API.get API calls automatically + $this->addApiGetMetdata($availableReports); + + $knownMetrics = array_merge($this->getDefaultMetrics(), $this->getDefaultProcessedMetrics()); + foreach ($availableReports as &$availableReport) { + // Ensure all metrics have a translation + $metrics = $availableReport['metrics']; + $cleanedMetrics = array(); + foreach ($metrics as $metricId => $metricTranslation) { + // When simply the column name was given, ie 'metric' => array( 'nb_visits' ) + // $metricTranslation is in this case nb_visits. We look for a known translation. + if (is_numeric($metricId) + && isset($knownMetrics[$metricTranslation]) + ) { + $metricId = $metricTranslation; + $metricTranslation = $knownMetrics[$metricTranslation]; + } + $cleanedMetrics[$metricId] = $metricTranslation; + } + $availableReport['metrics'] = $cleanedMetrics; + + // Remove array elements that are false (to clean up API output) + foreach ($availableReport as $attributeName => $attributeValue) { + if (empty($attributeValue)) { + unset($availableReport[$attributeName]); + } + } + // when there are per goal metrics, don't display conversion_rate since it can differ from per goal sum + if (isset($availableReport['metricsGoal'])) { + unset($availableReport['processedMetrics']['conversion_rate']); + unset($availableReport['metricsGoal']['conversion_rate']); + } + + // Processing a uniqueId for each report, + // can be used by UIs as a key to match a given report + $uniqueId = $availableReport['module'] . '_' . $availableReport['action']; + if (!empty($availableReport['parameters'])) { + foreach ($availableReport['parameters'] as $key => $value) { + $uniqueId .= '_' . $key . '--' . $value; + } + } + $availableReport['uniqueId'] = $uniqueId; + + // Order is used to order reports internally, but not meant to be used outside + unset($availableReport['order']); + } + + // remove subtable reports + if (!$showSubtableReports) { + foreach ($availableReports as $idx => $report) { + if (isset($report['isSubtableReport']) && $report['isSubtableReport']) { + unset($availableReports[$idx]); + } + } + } + + return array_values($availableReports); // make sure array has contiguous key values + } + + + /** + * Add the metadata for the API.get report + * In other plugins, this would hook on 'API.getReportMetadata' + */ + private function addApiGetMetdata(&$availableReports) + { + $metadata = array( + 'category' => Piwik_Translate('General_API'), + 'name' => Piwik_Translate('General_MainMetrics'), + 'module' => 'API', + 'action' => 'get', + 'metrics' => array(), + 'processedMetrics' => array(), + 'metricsDocumentation' => array(), + 'order' => 1 + ); + + $indexesToMerge = array('metrics', 'processedMetrics', 'metricsDocumentation'); + + foreach ($availableReports as $report) { + if ($report['action'] == 'get') { + foreach ($indexesToMerge as $index) { + if (isset($report[$index]) + && is_array($report[$index]) + ) { + $metadata[$index] = array_merge($metadata[$index], $report[$index]); + } + } + } + } + + $availableReports[] = $metadata; + } + + public function getProcessedReport($idSite, $period, $date, $apiModule, $apiAction, $segment = false, + $apiParameters = false, $idGoal = false, $language = false, + $showTimer = true, $hideMetricsDoc = false, $idSubtable = false, $showRawMetrics = false) + { + $timer = new Piwik_Timer(); + if ($apiParameters === false) { + $apiParameters = array(); + } + if (!empty($idGoal) + && empty($apiParameters['idGoal']) + ) { + $apiParameters['idGoal'] = $idGoal; + } // Is this report found in the Metadata available reports? $reportMetadata = $this->getMetadata($idSite, $apiModule, $apiAction, $apiParameters, $language, - $period, $date, $hideMetricsDoc, $showSubtableReports = true); - if(empty($reportMetadata)) - { - throw new Exception("Requested report $apiModule.$apiAction for Website id=$idSite not found in the list of available reports. \n"); + $period, $date, $hideMetricsDoc, $showSubtableReports = true); + if (empty($reportMetadata)) { + throw new Exception("Requested report $apiModule.$apiAction for Website id=$idSite not found in the list of available reports. \n"); } $reportMetadata = reset($reportMetadata); - // Generate Api call URL passing custom parameters - $parameters = array_merge( $apiParameters, array( - 'method' => $apiModule.'.'.$apiAction, - 'idSite' => $idSite, - 'period' => $period, - 'date' => $date, - 'format' => 'original', - 'serialize' => '0', - 'language' => $language, - 'idSubtable' => $idSubtable, - )); - if(!empty($segment)) $parameters['segment'] = $segment; - - $url = Piwik_Url::getQueryStringFromParameters($parameters); + // Generate Api call URL passing custom parameters + $parameters = array_merge($apiParameters, array( + 'method' => $apiModule . '.' . $apiAction, + 'idSite' => $idSite, + 'period' => $period, + 'date' => $date, + 'format' => 'original', + 'serialize' => '0', + 'language' => $language, + 'idSubtable' => $idSubtable, + )); + if (!empty($segment)) $parameters['segment'] = $segment; + + $url = Piwik_Url::getQueryStringFromParameters($parameters); $request = new Piwik_API_Request($url); try { - /** @var Piwik_DataTable */ - $dataTable = $request->process(); - } catch(Exception $e) { - throw new Exception("API returned an error: ".$e->getMessage()."\n"); - } - - list($newReport, $columns, $rowsMetadata) = $this->handleTableReport($idSite, $dataTable, $reportMetadata, isset($reportMetadata['dimension']), $showRawMetrics); - foreach($columns as $columnId => &$name) - { - $name = ucfirst($name); - } - $website = new Piwik_Site($idSite); + /** @var Piwik_DataTable */ + $dataTable = $request->process(); + } catch (Exception $e) { + throw new Exception("API returned an error: " . $e->getMessage() . "\n"); + } + + list($newReport, $columns, $rowsMetadata) = $this->handleTableReport($idSite, $dataTable, $reportMetadata, isset($reportMetadata['dimension']), $showRawMetrics); + foreach ($columns as $columnId => &$name) { + $name = ucfirst($name); + } + $website = new Piwik_Site($idSite); // $segment = new Piwik_Segment($segment, $idSite); - $period = Piwik_Period::advancedFactory($period, $date); - $period = $period->getLocalizedLongString(); + $period = Piwik_Period::advancedFactory($period, $date); + $period = $period->getLocalizedLongString(); - $return = array( - 'website' => $website->getName(), - 'prettyDate' => $period, + $return = array( + 'website' => $website->getName(), + 'prettyDate' => $period, // 'prettySegment' => $segment->getPrettyString(), - 'metadata' => $reportMetadata, - 'columns' => $columns, - 'reportData' => $newReport, - 'reportMetadata' => $rowsMetadata, - ); - if($showTimer) - { - $return['timerMillis'] = $timer->getTimeMs(0); - } - return $return; + 'metadata' => $reportMetadata, + 'columns' => $columns, + 'reportData' => $newReport, + 'reportMetadata' => $rowsMetadata, + ); + if ($showTimer) { + $return['timerMillis'] = $timer->getTimeMs(0); + } + return $return; } - /** - * Enhance a $dataTable using metadata : - * - * - remove metrics based on $reportMetadata['metrics'] - * - add 0 valued metrics if $dataTable doesn't provide all $reportMetadata['metrics'] - * - format metric values to a 'human readable' format - * - extract row metadata to a separate Piwik_DataTable_Simple|Piwik_DataTable_Array : $rowsMetadata - * - translate metric names to a separate array : $columns - * - * @param int $idSite enables monetary value formatting based on site currency - * @param Piwik_DataTable|Piwik_DataTable_Array $dataTable - * @param array $reportMetadata - * @param boolean $hasDimension - * @return array Piwik_DataTable_Simple|Piwik_DataTable_Array $newReport with human readable format & array $columns list of translated column names & Piwik_DataTable_Simple|Piwik_DataTable_Array $rowsMetadata - **/ + /** + * Enhance a $dataTable using metadata : + * + * - remove metrics based on $reportMetadata['metrics'] + * - add 0 valued metrics if $dataTable doesn't provide all $reportMetadata['metrics'] + * - format metric values to a 'human readable' format + * - extract row metadata to a separate Piwik_DataTable_Simple|Piwik_DataTable_Array : $rowsMetadata + * - translate metric names to a separate array : $columns + * + * @param int $idSite enables monetary value formatting based on site currency + * @param Piwik_DataTable|Piwik_DataTable_Array $dataTable + * @param array $reportMetadata + * @param boolean $hasDimension + * @return array Piwik_DataTable_Simple|Piwik_DataTable_Array $newReport with human readable format & array $columns list of translated column names & Piwik_DataTable_Simple|Piwik_DataTable_Array $rowsMetadata + **/ private function handleTableReport($idSite, $dataTable, &$reportMetadata, $hasDimension, $showRawMetrics = false) { - $columns = $reportMetadata['metrics']; - - if($hasDimension) - { - $columns = array_merge( - array('label' => $reportMetadata['dimension'] ), - $columns - ); - - if(isset($reportMetadata['processedMetrics'])) - { - $processedMetricsAdded = $this->getDefaultProcessedMetrics(); - foreach($processedMetricsAdded as $processedMetricId => $processedMetricTranslation) - { - // this processed metric can be displayed for this report - if(isset($reportMetadata['processedMetrics'][$processedMetricId])) - { - $columns[$processedMetricId] = $processedMetricTranslation; - } - } - } - - // Display the global Goal metrics - if(isset($reportMetadata['metricsGoal'])) - { - $metricsGoalDisplay = array('revenue'); - // Add processed metrics to be displayed for this report - foreach($metricsGoalDisplay as $goalMetricId) - { - if(isset($reportMetadata['metricsGoal'][$goalMetricId])) - { - $columns[$goalMetricId] = $reportMetadata['metricsGoal'][$goalMetricId]; - } - } - } - - if(isset($reportMetadata['processedMetrics'])) - { - // Add processed metrics - $dataTable->filter('AddColumnsProcessedMetrics', array($deleteRowsWithNoVisit = false)); - } - } - - $columns = $this->hideShowMetrics($columns); - - // $dataTable is an instance of Piwik_DataTable_Array when multiple periods requested - if ($dataTable instanceof Piwik_DataTable_Array) - { - // Need a new Piwik_DataTable_Array to store the 'human readable' values - $newReport = new Piwik_DataTable_Array(); - $newReport->setKeyName("prettyDate"); - - // Need a new Piwik_DataTable_Array to store report metadata - $rowsMetadata = new Piwik_DataTable_Array(); - $rowsMetadata->setKeyName("prettyDate"); - - // Process each Piwik_DataTable_Simple entry - foreach($dataTable->getArray() as $label => $simpleDataTable) - { - $this->removeEmptyColumns($columns, $reportMetadata, $simpleDataTable); - - list($enhancedSimpleDataTable, $rowMetadata) = $this->handleSimpleDataTable($idSite, $simpleDataTable, $columns, $hasDimension, $showRawMetrics); - $enhancedSimpleDataTable->metadata = $simpleDataTable->metadata; - - $period = $simpleDataTable->metadata['period']->getLocalizedLongString(); - $newReport->addTable($enhancedSimpleDataTable, $period); - $rowsMetadata->addTable($rowMetadata, $period); - } - } - else - { - $this->removeEmptyColumns($columns, $reportMetadata, $dataTable); - list($newReport, $rowsMetadata) = $this->handleSimpleDataTable($idSite, $dataTable, $columns, $hasDimension, $showRawMetrics); - } - - return array( - $newReport, - $columns, - $rowsMetadata - ); + $columns = $reportMetadata['metrics']; + + if ($hasDimension) { + $columns = array_merge( + array('label' => $reportMetadata['dimension']), + $columns + ); + + if (isset($reportMetadata['processedMetrics'])) { + $processedMetricsAdded = $this->getDefaultProcessedMetrics(); + foreach ($processedMetricsAdded as $processedMetricId => $processedMetricTranslation) { + // this processed metric can be displayed for this report + if (isset($reportMetadata['processedMetrics'][$processedMetricId])) { + $columns[$processedMetricId] = $processedMetricTranslation; + } + } + } + + // Display the global Goal metrics + if (isset($reportMetadata['metricsGoal'])) { + $metricsGoalDisplay = array('revenue'); + // Add processed metrics to be displayed for this report + foreach ($metricsGoalDisplay as $goalMetricId) { + if (isset($reportMetadata['metricsGoal'][$goalMetricId])) { + $columns[$goalMetricId] = $reportMetadata['metricsGoal'][$goalMetricId]; + } + } + } + + if (isset($reportMetadata['processedMetrics'])) { + // Add processed metrics + $dataTable->filter('AddColumnsProcessedMetrics', array($deleteRowsWithNoVisit = false)); + } + } + + $columns = $this->hideShowMetrics($columns); + + // $dataTable is an instance of Piwik_DataTable_Array when multiple periods requested + if ($dataTable instanceof Piwik_DataTable_Array) { + // Need a new Piwik_DataTable_Array to store the 'human readable' values + $newReport = new Piwik_DataTable_Array(); + $newReport->setKeyName("prettyDate"); + + // Need a new Piwik_DataTable_Array to store report metadata + $rowsMetadata = new Piwik_DataTable_Array(); + $rowsMetadata->setKeyName("prettyDate"); + + // Process each Piwik_DataTable_Simple entry + foreach ($dataTable->getArray() as $label => $simpleDataTable) { + $this->removeEmptyColumns($columns, $reportMetadata, $simpleDataTable); + + list($enhancedSimpleDataTable, $rowMetadata) = $this->handleSimpleDataTable($idSite, $simpleDataTable, $columns, $hasDimension, $showRawMetrics); + $enhancedSimpleDataTable->metadata = $simpleDataTable->metadata; + + $period = $simpleDataTable->metadata['period']->getLocalizedLongString(); + $newReport->addTable($enhancedSimpleDataTable, $period); + $rowsMetadata->addTable($rowMetadata, $period); + } + } else { + $this->removeEmptyColumns($columns, $reportMetadata, $dataTable); + list($newReport, $rowsMetadata) = $this->handleSimpleDataTable($idSite, $dataTable, $columns, $hasDimension, $showRawMetrics); + } + + return array( + $newReport, + $columns, + $rowsMetadata + ); + } + + /** + * Removes metrics from the list of columns and the report meta data if they are marked empty + * in the data table meta data. + */ + private function removeEmptyColumns(&$columns, &$reportMetadata, $dataTable) + { + $emptyColumns = $dataTable->getMetadata(Piwik_DataTable::EMPTY_COLUMNS_METADATA_NAME); + + if (!is_array($emptyColumns)) { + return; + } + + $columns = $this->hideShowMetrics($columns, $emptyColumns); + + if (isset($reportMetadata['metrics'])) { + $reportMetadata['metrics'] = $this->hideShowMetrics($reportMetadata['metrics'], $emptyColumns); + } + + if (isset($reportMetadata['metricsDocumentation'])) { + $reportMetadata['metricsDocumentation'] = $this->hideShowMetrics($reportMetadata['metricsDocumentation'], $emptyColumns); + } } - /** - * Removes metrics from the list of columns and the report meta data if they are marked empty - * in the data table meta data. - */ - private function removeEmptyColumns( &$columns, &$reportMetadata, $dataTable ) - { - $emptyColumns = $dataTable->getMetadata(Piwik_DataTable::EMPTY_COLUMNS_METADATA_NAME); - - if (!is_array($emptyColumns)) - { - return; - } - - $columns = $this->hideShowMetrics($columns, $emptyColumns); - - if (isset($reportMetadata['metrics'])) - { - $reportMetadata['metrics'] = $this->hideShowMetrics($reportMetadata['metrics'], $emptyColumns); - } - - if (isset($reportMetadata['metricsDocumentation'])) - { - $reportMetadata['metricsDocumentation'] = $this->hideShowMetrics($reportMetadata['metricsDocumentation'], $emptyColumns); - } - } - - /** + /** * Removes column names from an array based on the values in the hideColumns, * showColumns query parameters. This is a hack that provides the ColumnDelete * filter functionality in processed reports. * * @param array $columns List of metrics shown in a processed report. - * @param array $emptyColumns Empty columns from the data table meta data. + * @param array $emptyColumns Empty columns from the data table meta data. * @return array Filtered list of metrics. */ - private function hideShowMetrics( $columns, $emptyColumns = array() ) + private function hideShowMetrics($columns, $emptyColumns = array()) + { + if (!is_array($columns)) { + return $columns; + } + + // remove columns if hideColumns query parameters exist + $columnsToRemove = Piwik_Common::getRequestVar('hideColumns', ''); + if ($columnsToRemove != '') { + $columnsToRemove = explode(',', $columnsToRemove); + foreach ($columnsToRemove as $name) { + // if a column to remove is in the column list, remove it + if (isset($columns[$name])) { + unset($columns[$name]); + } + } + } + + // remove columns if showColumns query parameters exist + $columnsToKeep = Piwik_Common::getRequestVar('showColumns', ''); + if ($columnsToKeep != '') { + $columnsToKeep = explode(',', $columnsToKeep); + $columnsToKeep[] = 'label'; + + foreach ($columns as $name => $ignore) { + // if the current column should not be kept, remove it + $idx = array_search($name, $columnsToKeep); + if ($idx === FALSE) // if $name is not in $columnsToKeep + { + unset($columns[$name]); + } + } + } + + // remove empty columns + if (is_array($emptyColumns)) { + foreach ($emptyColumns as $column) { + if (isset($columns[$column])) { + unset($columns[$column]); + } + } + } + + return $columns; + } + + /** + * Enhance $simpleDataTable using metadata : + * + * - remove metrics based on $reportMetadata['metrics'] + * - add 0 valued metrics if $simpleDataTable doesn't provide all $reportMetadata['metrics'] + * - format metric values to a 'human readable' format + * - extract row metadata to a separate Piwik_DataTable_Simple $rowsMetadata + * + * @param int $idSite enables monetary value formatting based on site currency + * @param Piwik_DataTable_Simple $simpleDataTable + * @param array $metadataColumns + * @param boolean $hasDimension + * @param bool $returnRawMetrics If set to true, the original metrics will be returned + * + * @return array Piwik_DataTable $enhancedDataTable filtered metrics with human readable format & Piwik_DataTable_Simple $rowsMetadata + */ + private function handleSimpleDataTable($idSite, $simpleDataTable, $metadataColumns, $hasDimension, $returnRawMetrics = false) + { + // new DataTable to store metadata + $rowsMetadata = new Piwik_DataTable(); + + // new DataTable to store 'human readable' values + if ($hasDimension) { + $enhancedDataTable = new Piwik_DataTable(); + } else { + $enhancedDataTable = new Piwik_DataTable_Simple(); + } + + // add missing metrics + foreach ($simpleDataTable->getRows() as $row) { + $rowMetrics = $row->getColumns(); + foreach ($metadataColumns as $id => $name) { + if (!isset($rowMetrics[$id])) { + $row->addColumn($id, 0); + } + } + } + + foreach ($simpleDataTable->getRows() as $row) { + $enhancedRow = new Piwik_DataTable_Row(); + $enhancedDataTable->addRow($enhancedRow); + $rowMetrics = $row->getColumns(); + foreach ($rowMetrics as $columnName => $columnValue) { + // filter metrics according to metadata definition + if (isset($metadataColumns[$columnName])) { + // generate 'human readable' metric values + $prettyValue = Piwik::getPrettyValue($idSite, $columnName, $columnValue, $htmlAllowed = false, $timeAsSentence = false); + $enhancedRow->addColumn($columnName, $prettyValue); + } // For example the Maps Widget requires the raw metrics to do advanced datavis + elseif ($returnRawMetrics) { + $enhancedRow->addColumn($columnName, $columnValue); + } + } + + // If report has a dimension, extract metadata into a distinct DataTable + if ($hasDimension) { + $rowMetadata = $row->getMetadata(); + $idSubDataTable = $row->getIdSubDataTable(); + + // Create a row metadata only if there are metadata to insert + if (count($rowMetadata) > 0 || !is_null($idSubDataTable)) { + $metadataRow = new Piwik_DataTable_Row(); + $rowsMetadata->addRow($metadataRow); + + foreach ($rowMetadata as $metadataKey => $metadataValue) { + $metadataRow->addColumn($metadataKey, $metadataValue); + } + + if (!is_null($idSubDataTable)) { + $metadataRow->addColumn('idsubdatatable', $idSubDataTable); + } + } + } + } + + return array( + $enhancedDataTable, + $rowsMetadata + ); + } + + /** + * API metadata are sorted by category/name, + * with a little tweak to replicate the standard Piwik category ordering + * + * @param string $a + * @param string $b + * @return int + */ + private function sort($a, $b) { - if (!is_array($columns)) - { - return $columns; - } - - // remove columns if hideColumns query parameters exist - $columnsToRemove = Piwik_Common::getRequestVar('hideColumns', ''); - if ($columnsToRemove != '') - { - $columnsToRemove = explode(',', $columnsToRemove); - foreach ($columnsToRemove as $name) - { - // if a column to remove is in the column list, remove it - if (isset($columns[$name])) - { - unset($columns[$name]); - } - } - } - - // remove columns if showColumns query parameters exist - $columnsToKeep = Piwik_Common::getRequestVar('showColumns', ''); - if ($columnsToKeep != '') - { - $columnsToKeep = explode(',', $columnsToKeep); - $columnsToKeep[] = 'label'; - - foreach ($columns as $name => $ignore) - { - // if the current column should not be kept, remove it - $idx = array_search($name, $columnsToKeep); - if ($idx === FALSE) // if $name is not in $columnsToKeep - { - unset($columns[$name]); - } - } - } - - // remove empty columns - if (is_array($emptyColumns)) - { - foreach ($emptyColumns as $column) - { - if (isset($columns[$column])) - { - unset($columns[$column]); - } - } - } - - return $columns; + static $order = null; + if (is_null($order)) { + $order = array( + Piwik_Translate('General_MultiSitesSummary'), + Piwik_Translate('VisitsSummary_VisitsSummary'), + Piwik_Translate('Goals_Ecommerce'), + Piwik_Translate('Actions_Actions'), + Piwik_Translate('Actions_SubmenuSitesearch'), + Piwik_Translate('Referers_Referers'), + Piwik_Translate('Goals_Goals'), + Piwik_Translate('General_Visitors'), + Piwik_Translate('UserSettings_VisitorSettings'), + ); + } + return ($category = strcmp(array_search($a['category'], $order), array_search($b['category'], $order))) == 0 + ? (@$a['order'] < @$b['order'] ? -1 : 1) + : $category; + } + + /** + * Get a combined report of the *.get API methods. + */ + public function get($idSite, $period, $date, $segment = false, $columns = false) + { + $columns = Piwik::getArrayFromApiParameter($columns); + + // build columns map for faster checks later on + $columnsMap = array(); + foreach ($columns as $column) { + $columnsMap[$column] = true; + } + + // find out which columns belong to which plugin + $columnsByPlugin = array(); + $meta = Piwik_API_API::getInstance()->getReportMetadata($idSite, $period, $date); + foreach ($meta as $reportMeta) { + // scan all *.get reports + if ($reportMeta['action'] == 'get' && !isset($reportMeta['parameters']) + && $reportMeta['module'] != 'API' + ) { + $plugin = $reportMeta['module']; + foreach ($reportMeta['metrics'] as $column => $columnTranslation) { + // a metric from this report has been requested + if (isset($columnsMap[$column]) + // or by default, return all metrics + || empty($columnsMap) + ) { + $columnsByPlugin[$plugin][] = $column; + } + } + } + } + krsort($columnsByPlugin); + + $mergedDataTable = false; + $params = compact('idSite', 'period', 'date', 'segment', 'idGoal'); + foreach ($columnsByPlugin as $plugin => $columns) { + // load the data + $className = 'Piwik_' . $plugin . '_API'; + $params['columns'] = implode(',', $columns); + $dataTable = Piwik_API_Proxy::getInstance()->call($className, 'get', $params); + // make sure the table has all columns + $array = ($dataTable instanceof Piwik_DataTable_Array ? $dataTable->getArray() : array($dataTable)); + foreach ($array as $table) { + // we don't support idSites=all&date=DATE1,DATE2 + if ($table instanceof Piwik_DataTable) { + $firstRow = $table->getFirstRow(); + if (!$firstRow) { + $firstRow = new Piwik_DataTable_Row; + $table->addRow($firstRow); + } + foreach ($columns as $column) { + if ($firstRow->getColumn($column) === false) { + $firstRow->setColumn($column, 0); + } + } + } + } + + // merge reports + if ($mergedDataTable === false) { + $mergedDataTable = $dataTable; + } else { + $this->mergeDataTables($mergedDataTable, $dataTable); + } + } + return $mergedDataTable; + } + + + /** + * Merge the columns of two data tables. + * Manipulates the first table. + */ + private function mergeDataTables($table1, $table2) + { + // handle table arrays + if ($table1 instanceof Piwik_DataTable_Array && $table2 instanceof Piwik_DataTable_Array) { + $subTables2 = $table2->getArray(); + foreach ($table1->getArray() as $index => $subTable1) { + $subTable2 = $subTables2[$index]; + $this->mergeDataTables($subTable1, $subTable2); + } + return; + } + + $firstRow1 = $table1->getFirstRow(); + $firstRow2 = $table2->getFirstRow(); + if ($firstRow2 instanceof Piwik_DataTable_Row) { + foreach ($firstRow2->getColumns() as $metric => $value) { + $firstRow1->setColumn($metric, $value); + } + } + } + + + /** + * Given an API report to query (eg. "Referers.getKeywords", and a Label (eg. "free%20software"), + * this function will query the API for the previous days/weeks/etc. and will return + * a ready to use data structure containing the metrics for the requested Label, along with enriched information (min/max values, etc.) + * + * @return array + */ + public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true) + { + // validation of requested $period & $date + if ($period == 'range') { + // load days in the range + $period = 'day'; + } + + if (!Piwik_Archive::isMultiplePeriod($date, $period)) { + throw new Exception("Row evolutions can not be processed with this combination of \'date\' and \'period\' parameters."); + } + + // this is needed because Piwik_API_Proxy uses Piwik_Common::getRequestVar which in turn + // uses Piwik_Common::sanitizeInputValue. This causes the > that separates recursive labels + // to become > and we need to undo that here. + $label = Piwik_Common::unsanitizeInputValue($label); + + if ($label) { + $labels = explode(',', $label); + $labels = array_unique($labels); + } else { + $labels = array(); + } + + $dataTable = $this->loadRowEvolutionDataFromAPI($idSite, $period, $date, $apiModule, $apiAction, $labels, $segment, $idGoal); + + if (empty($labels)) { + // if no labels specified, use all possible labels as list + foreach ($dataTable->getArray() as $table) { + $labels = array_merge($labels, $table->getColumn('label')); + } + $labels = array_unique($labels); + } + + if (count($labels) > 1) { + $data = $this->getMultiRowEvolution( + $dataTable, + $idSite, + $period, + $date, + $apiModule, + $apiAction, + $labels, + $segment, + $column, + $language, + $idGoal, + $legendAppendMetric, + $labelUseAbsoluteUrl + ); + } else { + $data = $this->getSingleRowEvolution( + $dataTable, + $idSite, + $period, + $date, + $apiModule, + $apiAction, + $labels[0], + $segment, + $language, + $idGoal, + $labelUseAbsoluteUrl + ); + } + return $data; } - /** - * Enhance $simpleDataTable using metadata : - * - * - remove metrics based on $reportMetadata['metrics'] - * - add 0 valued metrics if $simpleDataTable doesn't provide all $reportMetadata['metrics'] - * - format metric values to a 'human readable' format - * - extract row metadata to a separate Piwik_DataTable_Simple $rowsMetadata - * - * @param int $idSite enables monetary value formatting based on site currency - * @param Piwik_DataTable_Simple $simpleDataTable - * @param array $metadataColumns - * @param boolean $hasDimension - * @param bool $returnRawMetrics If set to true, the original metrics will be returned - * - * @return array Piwik_DataTable $enhancedDataTable filtered metrics with human readable format & Piwik_DataTable_Simple $rowsMetadata - */ - private function handleSimpleDataTable($idSite, $simpleDataTable, $metadataColumns, $hasDimension, $returnRawMetrics = false) - { - // new DataTable to store metadata - $rowsMetadata = new Piwik_DataTable(); - - // new DataTable to store 'human readable' values - if($hasDimension) - { - $enhancedDataTable = new Piwik_DataTable(); - } - else - { - $enhancedDataTable = new Piwik_DataTable_Simple(); - } - - // add missing metrics - foreach($simpleDataTable->getRows() as $row) - { - $rowMetrics = $row->getColumns(); - foreach($metadataColumns as $id => $name) - { - if(!isset($rowMetrics[$id])) - { - $row->addColumn($id, 0); - } - } - } - - foreach($simpleDataTable->getRows() as $row) - { - $enhancedRow = new Piwik_DataTable_Row(); - $enhancedDataTable->addRow($enhancedRow); - $rowMetrics = $row->getColumns(); - foreach($rowMetrics as $columnName => $columnValue) - { - // filter metrics according to metadata definition - if(isset($metadataColumns[$columnName])) - { - // generate 'human readable' metric values - $prettyValue = Piwik::getPrettyValue($idSite, $columnName, $columnValue, $htmlAllowed = false, $timeAsSentence = false); - $enhancedRow->addColumn($columnName, $prettyValue); - } - // For example the Maps Widget requires the raw metrics to do advanced datavis - elseif($returnRawMetrics) - { - $enhancedRow->addColumn($columnName, $columnValue); - } - } - - // If report has a dimension, extract metadata into a distinct DataTable - if($hasDimension) - { - $rowMetadata = $row->getMetadata(); - $idSubDataTable = $row->getIdSubDataTable(); - - // Create a row metadata only if there are metadata to insert - if(count($rowMetadata) > 0 || !is_null($idSubDataTable)) - { - $metadataRow = new Piwik_DataTable_Row(); - $rowsMetadata->addRow($metadataRow); - - foreach($rowMetadata as $metadataKey => $metadataValue) - { - $metadataRow->addColumn($metadataKey, $metadataValue); - } - - if(!is_null($idSubDataTable)) - { - $metadataRow->addColumn('idsubdatatable', $idSubDataTable); - } - } - } - } - - return array( - $enhancedDataTable, - $rowsMetadata - ); - } - - /** - * API metadata are sorted by category/name, - * with a little tweak to replicate the standard Piwik category ordering - * - * @param string $a - * @param string $b - * @return int - */ - private function sort($a, $b) - { - static $order = null; - if(is_null($order)) - { - $order = array( - Piwik_Translate('General_MultiSitesSummary'), - Piwik_Translate('VisitsSummary_VisitsSummary'), - Piwik_Translate('Goals_Ecommerce'), - Piwik_Translate('Actions_Actions'), - Piwik_Translate('Actions_SubmenuSitesearch'), - Piwik_Translate('Referers_Referers'), - Piwik_Translate('Goals_Goals'), - Piwik_Translate('General_Visitors'), - Piwik_Translate('UserSettings_VisitorSettings'), - ); - } - return ($category = strcmp(array_search($a['category'], $order), array_search($b['category'], $order))) == 0 - ? (@$a['order'] < @$b['order'] ? -1 : 1) - : $category; - } - - /** - * Get a combined report of the *.get API methods. - */ - public function get( $idSite, $period, $date, $segment = false, $columns = false) - { - $columns = Piwik::getArrayFromApiParameter($columns); - - // build columns map for faster checks later on - $columnsMap = array(); - foreach ($columns as $column) { - $columnsMap[$column] = true; - } - - // find out which columns belong to which plugin - $columnsByPlugin = array(); - $meta = Piwik_API_API::getInstance()->getReportMetadata($idSite, $period, $date); - foreach ($meta as $reportMeta) - { - // scan all *.get reports - if ($reportMeta['action'] == 'get' && !isset($reportMeta['parameters']) - && $reportMeta['module'] != 'API') - { - $plugin = $reportMeta['module']; - foreach ($reportMeta['metrics'] as $column => $columnTranslation) - { - // a metric from this report has been requested - if (isset($columnsMap[$column]) - // or by default, return all metrics - || empty($columnsMap)) - { - $columnsByPlugin[$plugin][] = $column; - } - } - } - } - krsort($columnsByPlugin); - - $mergedDataTable = false; - $params = compact('idSite', 'period', 'date', 'segment', 'idGoal'); - foreach ($columnsByPlugin as $plugin => $columns) - { - // load the data - $className = 'Piwik_'.$plugin.'_API'; - $params['columns'] = implode(',', $columns); - $dataTable = Piwik_API_Proxy::getInstance()->call($className, 'get', $params); - // make sure the table has all columns - $array = ($dataTable instanceof Piwik_DataTable_Array ? $dataTable->getArray() : array($dataTable)); - foreach ($array as $table) - { - // we don't support idSites=all&date=DATE1,DATE2 - if($table instanceof Piwik_DataTable) - { - $firstRow = $table->getFirstRow(); - if(!$firstRow) - { - $firstRow = new Piwik_DataTable_Row; - $table->addRow($firstRow); - } - foreach ($columns as $column) - { - if ($firstRow->getColumn($column) === false) - { - $firstRow->setColumn($column, 0); - } - } - } - } - - // merge reports - if($mergedDataTable === false) - { - $mergedDataTable = $dataTable; - } - else - { - $this->mergeDataTables($mergedDataTable, $dataTable); - } - } - return $mergedDataTable; - } - - - /** - * Merge the columns of two data tables. - * Manipulates the first table. - */ - private function mergeDataTables($table1, $table2) - { - // handle table arrays - if ($table1 instanceof Piwik_DataTable_Array && $table2 instanceof Piwik_DataTable_Array) - { - $subTables2 = $table2->getArray(); - foreach ($table1->getArray() as $index => $subTable1) - { - $subTable2 = $subTables2[$index]; - $this->mergeDataTables($subTable1, $subTable2); - } - return; - } - - $firstRow1 = $table1->getFirstRow(); - $firstRow2 = $table2->getFirstRow(); - if($firstRow2 instanceof Piwik_DataTable_Row) - { - foreach ($firstRow2->getColumns() as $metric => $value) - { - $firstRow1->setColumn($metric, $value); - } - } - } - - - /** - * Given an API report to query (eg. "Referers.getKeywords", and a Label (eg. "free%20software"), - * this function will query the API for the previous days/weeks/etc. and will return - * a ready to use data structure containing the metrics for the requested Label, along with enriched information (min/max values, etc.) - * - * @return array - */ - public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true) + /** + * Get row evolution for a single label + * @return array containing report data, metadata, label, logo + */ + private function getSingleRowEvolution($dataTable, $idSite, $period, $date, $apiModule, $apiAction, $label, $segment, $language = false, $idGoal = false, $labelUseAbsoluteUrl = true) { - // validation of requested $period & $date - if ($period == 'range') - { - // load days in the range - $period = 'day'; - } - - if(!Piwik_Archive::isMultiplePeriod($date, $period)) - { - throw new Exception("Row evolutions can not be processed with this combination of \'date\' and \'period\' parameters."); - } - - // this is needed because Piwik_API_Proxy uses Piwik_Common::getRequestVar which in turn - // uses Piwik_Common::sanitizeInputValue. This causes the > that separates recursive labels - // to become > and we need to undo that here. - $label = Piwik_Common::unsanitizeInputValue($label); - - if($label) - { - $labels = explode(',', $label); - $labels = array_unique($labels); - } - else - { - $labels = array(); - } - - $dataTable = $this->loadRowEvolutionDataFromAPI($idSite, $period, $date, $apiModule, $apiAction, $labels, $segment, $idGoal); - - if (empty($labels)) - { - // if no labels specified, use all possible labels as list - foreach ($dataTable->getArray() as $table) - { - $labels = array_merge($labels, $table->getColumn('label')); - } - $labels = array_unique($labels); - } - - if (count($labels) > 1) - { - $data = $this->getMultiRowEvolution( - $dataTable, - $idSite, - $period, - $date, - $apiModule, - $apiAction, - $labels, - $segment, - $column, - $language, - $idGoal, - $legendAppendMetric, - $labelUseAbsoluteUrl - ); - } - else - { - $data = $this->getSingleRowEvolution( - $dataTable, - $idSite, - $period, - $date, - $apiModule, - $apiAction, - $labels[0], - $segment, - $language, - $idGoal, - $labelUseAbsoluteUrl - ); - } - return $data; - } - - /** - * Get row evolution for a single label - * @return array containing report data, metadata, label, logo - */ - private function getSingleRowEvolution($dataTable, $idSite, $period, $date, $apiModule, $apiAction, $label, $segment, $language=false, $idGoal = false, $labelUseAbsoluteUrl = true) - { - $metadata = $this->getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal); - $metricNames = array_keys($metadata['metrics']); - - $logo = $actualLabel = false; - $urlFound = false; - foreach ($dataTable->getArray() as $date => $subTable) - { - /** @var $subTable Piwik_DataTable */ - $subTable->applyQueuedFilters(); - if ($subTable->getRowsCount() > 0) - { - /** @var $row Piwik_DataTable_Row */ - $row = $subTable->getFirstRow(); - - if (!$actualLabel) - { - $logo = $row->getMetadata('logo'); - - $actualLabel = $this->getRowUrlForEvolutionLabel($row, $apiModule, $apiAction, $labelUseAbsoluteUrl); - $urlFound = $actualLabel !== false; - if(empty($actualLabel)) - { - $actualLabel = $row->getColumn('label'); - } - - } - - // remove all columns that are not in the available metrics. - // this removes the label as well (which is desired for two reasons: (1) it was passed - // in the request, (2) it would cause the evolution graph to show the label in the legend). - foreach ($row->getColumns() as $column => $value) - { - if (!in_array($column, $metricNames)) - { - $row->deleteColumn($column); - } - } - - $row->deleteMetadata(); - } - } - - $this->enhanceRowEvolutionMetaData($metadata, $dataTable); - - // if we have a recursive label and no url, use the path - if (!$urlFound) - { - $actualLabel = str_replace(Piwik_API_DataTableManipulator_LabelFilter::SEPARATOR_RECURSIVE_LABEL, ' - ', $label); - } - - $return = array( - 'label' => Piwik_DataTable_Filter_SafeDecodeLabel::safeDecodeLabel($actualLabel), - 'reportData' => $dataTable, - 'metadata' => $metadata - ); - if(!empty($logo)){ - $return['logo'] = $logo; - } - return $return; - } - - private function getRowUrlForEvolutionLabel($row, $apiModule, $apiAction, $labelUseAbsoluteUrl) - { - $url = $row->getMetadata('url'); - if ($url - && ($apiModule == 'Actions' - || ($apiModule == 'Referers' - && $apiAction == 'getWebsites')) - && $labelUseAbsoluteUrl - ) { - $actualLabel = preg_replace(';^http(s)?://(www.)?;i', '', $url); - return $actualLabel; - } - return false; - } - - /** - * @param $idSite - * @param $period - * @param $date - * @param $apiModule - * @param $apiAction - * @param $label - * @param $segment - * @param $idGoal - * @throws Exception - * @return Piwik_DataTable_Array|Piwik_DataTable - */ - private function loadRowEvolutionDataFromAPI($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $idGoal = false) - { - if (!is_array($label)) - { - $label = array($label); - } - $label = array_map('rawurlencode', $label); - - $parameters = array( - 'method' => $apiModule.'.'.$apiAction, - 'label' => $label, - 'idSite' => $idSite, - 'period' => $period, - 'date' => $date, - 'format' => 'original', - 'serialize' => '0', - 'segment' => $segment, - 'idGoal' => $idGoal, - - // if more than one label is used, we add empty rows for labels we can't - // find to ensure we know the order of the rows in the returned data table - 'labelFilterAddEmptyRows' => count($label) > 1 ? 1 : 0, - ); - - // add "processed metrics" like actions per visit or bounce rate - // note: some reports should not be filtered with AddColumnProcessedMetrics - // specifically, reports without the Piwik_Archive::INDEX_NB_VISITS metric such as Goals.getVisitsUntilConversion & Goal.getDaysToConversion - // this is because the AddColumnProcessedMetrics filter removes all datable rows lacking this metric - if - ( - $apiModule != 'Actions' - && - ($apiModule != 'Goals' || ($apiAction != 'getVisitsUntilConversion' && $apiAction != 'getDaysToConversion')) - && $label // do not request processed metrics when retrieving top n labels - ) - { - $parameters['filter_add_columns_when_show_all_columns'] = '1'; - } - - $url = Piwik_Url::getQueryStringFromParameters($parameters); - + $metadata = $this->getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal); + $metricNames = array_keys($metadata['metrics']); + + $logo = $actualLabel = false; + $urlFound = false; + foreach ($dataTable->getArray() as $date => $subTable) { + /** @var $subTable Piwik_DataTable */ + $subTable->applyQueuedFilters(); + if ($subTable->getRowsCount() > 0) { + /** @var $row Piwik_DataTable_Row */ + $row = $subTable->getFirstRow(); + + if (!$actualLabel) { + $logo = $row->getMetadata('logo'); + + $actualLabel = $this->getRowUrlForEvolutionLabel($row, $apiModule, $apiAction, $labelUseAbsoluteUrl); + $urlFound = $actualLabel !== false; + if (empty($actualLabel)) { + $actualLabel = $row->getColumn('label'); + } + + } + + // remove all columns that are not in the available metrics. + // this removes the label as well (which is desired for two reasons: (1) it was passed + // in the request, (2) it would cause the evolution graph to show the label in the legend). + foreach ($row->getColumns() as $column => $value) { + if (!in_array($column, $metricNames)) { + $row->deleteColumn($column); + } + } + + $row->deleteMetadata(); + } + } + + $this->enhanceRowEvolutionMetaData($metadata, $dataTable); + + // if we have a recursive label and no url, use the path + if (!$urlFound) { + $actualLabel = str_replace(Piwik_API_DataTableManipulator_LabelFilter::SEPARATOR_RECURSIVE_LABEL, ' - ', $label); + } + + $return = array( + 'label' => Piwik_DataTable_Filter_SafeDecodeLabel::safeDecodeLabel($actualLabel), + 'reportData' => $dataTable, + 'metadata' => $metadata + ); + if (!empty($logo)) { + $return['logo'] = $logo; + } + return $return; + } + + private function getRowUrlForEvolutionLabel($row, $apiModule, $apiAction, $labelUseAbsoluteUrl) + { + $url = $row->getMetadata('url'); + if ($url + && ($apiModule == 'Actions' + || ($apiModule == 'Referers' + && $apiAction == 'getWebsites')) + && $labelUseAbsoluteUrl + ) { + $actualLabel = preg_replace(';^http(s)?://(www.)?;i', '', $url); + return $actualLabel; + } + return false; + } + + /** + * @param $idSite + * @param $period + * @param $date + * @param $apiModule + * @param $apiAction + * @param $label + * @param $segment + * @param $idGoal + * @throws Exception + * @return Piwik_DataTable_Array|Piwik_DataTable + */ + private function loadRowEvolutionDataFromAPI($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $idGoal = false) + { + if (!is_array($label)) { + $label = array($label); + } + $label = array_map('rawurlencode', $label); + + $parameters = array( + 'method' => $apiModule . '.' . $apiAction, + 'label' => $label, + 'idSite' => $idSite, + 'period' => $period, + 'date' => $date, + 'format' => 'original', + 'serialize' => '0', + 'segment' => $segment, + 'idGoal' => $idGoal, + + // if more than one label is used, we add empty rows for labels we can't + // find to ensure we know the order of the rows in the returned data table + 'labelFilterAddEmptyRows' => count($label) > 1 ? 1 : 0, + ); + + // add "processed metrics" like actions per visit or bounce rate + // note: some reports should not be filtered with AddColumnProcessedMetrics + // specifically, reports without the Piwik_Archive::INDEX_NB_VISITS metric such as Goals.getVisitsUntilConversion & Goal.getDaysToConversion + // this is because the AddColumnProcessedMetrics filter removes all datable rows lacking this metric + if + ( + $apiModule != 'Actions' + && + ($apiModule != 'Goals' || ($apiAction != 'getVisitsUntilConversion' && $apiAction != 'getDaysToConversion')) + && $label // do not request processed metrics when retrieving top n labels + ) { + $parameters['filter_add_columns_when_show_all_columns'] = '1'; + } + + $url = Piwik_Url::getQueryStringFromParameters($parameters); + $request = new Piwik_API_Request($url); - try { - $dataTable = $request->process(); - } catch (Exception $e) { - throw new Exception("API returned an error: ".$e->getMessage()."\n"); - } - - return $dataTable; - } - - /** - * For a given API report, returns a simpler version - * of the metadata (will return only the metrics and the dimension name) - * @param $idSite - * @param $period - * @param $date - * @param $apiModule - * @param $apiAction - * @param $language - * @param $idGoal - * @throws Exception - * @return array - */ - private function getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal = false) - { - $apiParameters = array(); - if(!empty($idGoal) && $idGoal > 0 ) { - $apiParameters = array( 'idGoal' => $idGoal); - } - $reportMetadata = $this->getMetadata($idSite, $apiModule, $apiAction, $apiParameters, $language, $period, $date, $hideMetricsDoc = false, $showSubtableReports = true); - - if (empty($reportMetadata)) - { - throw new Exception("Requested report $apiModule.$apiAction for Website id=$idSite " - . "not found in the list of available reports. \n"); - } - - $reportMetadata = reset($reportMetadata); - - $metrics = $reportMetadata['metrics']; - if (isset($reportMetadata['processedMetrics']) && is_array($reportMetadata['processedMetrics'])) - { - $metrics = $metrics + $reportMetadata['processedMetrics']; - } - - $dimension = $reportMetadata['dimension']; - - return compact('metrics', 'dimension'); - } - - /** - * Given the Row evolution dataTable, and the associated metadata, - * enriches the metadata with min/max values, and % change between the first period and the last one - * @param array $metadata - * @param Piwik_DataTable_Array $dataTable - */ - private function enhanceRowEvolutionMetaData(&$metadata, $dataTable) - { - // prepare result array for metrics - $metricsResult = array(); - foreach ($metadata['metrics'] as $metric => $name) - { - $metricsResult[$metric] = array('name' => $name); - - if(!empty($metadata['logos'][$metric])) { - $metricsResult[$metric]['logo'] = $metadata['logos'][$metric]; - } - } - unset($metadata['logos']); - - $subDataTables = $dataTable->getArray(); - $firstDataTable = reset($subDataTables); - $firstDataTableRow = $firstDataTable->getFirstRow(); - $lastDataTable = end($subDataTables); - $lastDataTableRow = $lastDataTable->getFirstRow(); - - // Process min/max values - $firstNonZeroFound = array(); - foreach ($subDataTables as $subDataTable) - { - // $subDataTable is the report for one period, it has only one row - $firstRow = $subDataTable->getFirstRow(); - foreach ($metadata['metrics'] as $metric => $label) - { - $value = $firstRow ? floatval($firstRow->getColumn($metric)) : 0; - if ($value > 0) - { - $firstNonZeroFound[$metric] = true; - } - else if (!isset($firstNonZeroFound[$metric])) - { - continue; - } - if (!isset($metricsResult[$metric]['min']) - || $metricsResult[$metric]['min'] > $value) - { - $metricsResult[$metric]['min'] = $value; - } - if (!isset($metricsResult[$metric]['max']) - || $metricsResult[$metric]['max'] < $value) - { - $metricsResult[$metric]['max'] = $value; - } - } - } - - // Process % change between first/last values - foreach ($metadata['metrics'] as $metric => $label) - { - $first = $firstDataTableRow ? floatval($firstDataTableRow->getColumn($metric)) : 0; - $last = $lastDataTableRow ? floatval($lastDataTableRow->getColumn($metric)) : 0; - - // do not calculate evolution if the first value is 0 (to avoid divide-by-zero) - if ($first == 0) - { - continue; - } - - $change = Piwik_DataTable_Filter_CalculateEvolutionFilter::calculate($last, $first, $quotientPrecision = 0); + try { + $dataTable = $request->process(); + } catch (Exception $e) { + throw new Exception("API returned an error: " . $e->getMessage() . "\n"); + } + + return $dataTable; + } + + /** + * For a given API report, returns a simpler version + * of the metadata (will return only the metrics and the dimension name) + * @param $idSite + * @param $period + * @param $date + * @param $apiModule + * @param $apiAction + * @param $language + * @param $idGoal + * @throws Exception + * @return array + */ + private function getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal = false) + { + $apiParameters = array(); + if (!empty($idGoal) && $idGoal > 0) { + $apiParameters = array('idGoal' => $idGoal); + } + $reportMetadata = $this->getMetadata($idSite, $apiModule, $apiAction, $apiParameters, $language, $period, $date, $hideMetricsDoc = false, $showSubtableReports = true); + + if (empty($reportMetadata)) { + throw new Exception("Requested report $apiModule.$apiAction for Website id=$idSite " + . "not found in the list of available reports. \n"); + } + + $reportMetadata = reset($reportMetadata); + + $metrics = $reportMetadata['metrics']; + if (isset($reportMetadata['processedMetrics']) && is_array($reportMetadata['processedMetrics'])) { + $metrics = $metrics + $reportMetadata['processedMetrics']; + } + + $dimension = $reportMetadata['dimension']; + + return compact('metrics', 'dimension'); + } + + /** + * Given the Row evolution dataTable, and the associated metadata, + * enriches the metadata with min/max values, and % change between the first period and the last one + * @param array $metadata + * @param Piwik_DataTable_Array $dataTable + */ + private function enhanceRowEvolutionMetaData(&$metadata, $dataTable) + { + // prepare result array for metrics + $metricsResult = array(); + foreach ($metadata['metrics'] as $metric => $name) { + $metricsResult[$metric] = array('name' => $name); + + if (!empty($metadata['logos'][$metric])) { + $metricsResult[$metric]['logo'] = $metadata['logos'][$metric]; + } + } + unset($metadata['logos']); + + $subDataTables = $dataTable->getArray(); + $firstDataTable = reset($subDataTables); + $firstDataTableRow = $firstDataTable->getFirstRow(); + $lastDataTable = end($subDataTables); + $lastDataTableRow = $lastDataTable->getFirstRow(); + + // Process min/max values + $firstNonZeroFound = array(); + foreach ($subDataTables as $subDataTable) { + // $subDataTable is the report for one period, it has only one row + $firstRow = $subDataTable->getFirstRow(); + foreach ($metadata['metrics'] as $metric => $label) { + $value = $firstRow ? floatval($firstRow->getColumn($metric)) : 0; + if ($value > 0) { + $firstNonZeroFound[$metric] = true; + } else if (!isset($firstNonZeroFound[$metric])) { + continue; + } + if (!isset($metricsResult[$metric]['min']) + || $metricsResult[$metric]['min'] > $value + ) { + $metricsResult[$metric]['min'] = $value; + } + if (!isset($metricsResult[$metric]['max']) + || $metricsResult[$metric]['max'] < $value + ) { + $metricsResult[$metric]['max'] = $value; + } + } + } + + // Process % change between first/last values + foreach ($metadata['metrics'] as $metric => $label) { + $first = $firstDataTableRow ? floatval($firstDataTableRow->getColumn($metric)) : 0; + $last = $lastDataTableRow ? floatval($lastDataTableRow->getColumn($metric)) : 0; + + // do not calculate evolution if the first value is 0 (to avoid divide-by-zero) + if ($first == 0) { + continue; + } + + $change = Piwik_DataTable_Filter_CalculateEvolutionFilter::calculate($last, $first, $quotientPrecision = 0); $change = Piwik_DataTable_Filter_CalculateEvolutionFilter::prependPlusSignToNumber($change); - $metricsResult[$metric]['change'] = $change; - } - - $metadata['metrics'] = $metricsResult; - } - - /** Get row evolution for a multiple labels */ - private function getMultiRowEvolution($dataTable, $idSite, $period, $date, $apiModule, $apiAction, $labels, $segment, $column, $language=false, $idGoal=false, $legendAppendMetric=true, $labelUseAbsoluteUrl=true) - { - $actualLabels = $logos = array(); - - $metadata = $this->getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal); - - if (!isset($metadata['metrics'][$column])) - { - // invalid column => use the first one that's available - $metrics = array_keys($metadata['metrics']); - $column = reset($metrics); - } - - // get the processed label and logo (if any) for every requested label - $actualLabels = $logos = array(); - foreach ($labels as $labelIdx => $label) - { - foreach ($dataTable->getArray() as $table) - { - // find row for this label. LabelFilter will add empty rows and - // keep them ordered in the same way the labels array is, so we - // assume the $labelIdx is also the row Id - $labelRow = $table->getRowFromId($labelIdx); - - if ($labelRow) - { - $actualLabels[$labelIdx] = $this->getRowUrlForEvolutionLabel( - $labelRow, $apiModule, $apiAction, $labelUseAbsoluteUrl); - - $logos[$labelIdx] = $labelRow->getMetadata('logo'); - - if (!empty($actualLabels[$labelIdx])) - { - break; - } - } - } - - if (empty($actualLabels[$labelIdx])) - { - $actualLabels[$labelIdx] = $this->cleanOriginalLabel($label); - } - } - - // convert rows to be array($column.'_'.$labelIdx => $value) as opposed to - // array('label' => $label, 'column' => $value). - $dataTableMulti = $dataTable->getEmptyClone(); - foreach ($dataTable->getArray() as $tableLabel => $table) - { - $newRow = new Piwik_DataTable_Row(); - - foreach ($table->getRows() as $rowId => $row) - { - $value = $row->getColumn($column); - $value = floatVal(str_replace(',', '.', $value)); - if ($value == '') - { - $value = 0; - } - - $newLabel = $column.'_'.$rowId; // $rowId corresponds to the label index - - $newRow->addColumn($newLabel, $value); - } - - $newTable = $table->getEmptyClone(); - $newTable->addRow($newRow); - $dataTableMulti->addTable($newTable, $tableLabel); - } - - // the available metrics for the report are returned as metadata / columns - $metadata['columns'] = $metadata['metrics']; - - // metadata / metrics should document the rows that are compared - // this way, UI code can be reused - $metadata['metrics'] = array(); - foreach ($actualLabels as $labelIndex => $label) - { - if($legendAppendMetric) - { - $label .= ' ('.$metadata['columns'][$column].')'; - } - $metricName = $column.'_'.$labelIndex; - $metadata['metrics'][$metricName] = Piwik_DataTable_Filter_SafeDecodeLabel::safeDecodeLabel($label); - - if(!empty($logos[$labelIndex])) - { - $metadata['logos'][$metricName] = $logos[$labelIndex]; - } - } - - $this->enhanceRowEvolutionMetaData($metadata, $dataTableMulti); - - return array( - 'column' => $column, - 'reportData' => $dataTableMulti, - 'metadata' => $metadata - ); - } - - /** - * Returns a prettier, more comprehensible version of a row evolution label - * for display. - */ - private function cleanOriginalLabel( $label ) - { - return str_replace(Piwik_API_DataTableManipulator_LabelFilter::SEPARATOR_RECURSIVE_LABEL, ' - ', $label); - } - - /** - * Performs multiple API requests at once and returns every result. - * - * @param array $urls The array of API requests. - */ - public function getBulkRequest( $urls ) - { - if (empty($urls)) - { - return array(); - } - - $urls = Piwik_Common::unsanitizeInputValues($urls); - - $result = array(); - foreach ($urls as $url) - { - $req = new Piwik_API_Request($url); - $result[] = $req->process(); - } - return $result; - } + $metricsResult[$metric]['change'] = $change; + } + + $metadata['metrics'] = $metricsResult; + } + + /** Get row evolution for a multiple labels */ + private function getMultiRowEvolution($dataTable, $idSite, $period, $date, $apiModule, $apiAction, $labels, $segment, $column, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true) + { + $actualLabels = $logos = array(); + + $metadata = $this->getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal); + + if (!isset($metadata['metrics'][$column])) { + // invalid column => use the first one that's available + $metrics = array_keys($metadata['metrics']); + $column = reset($metrics); + } + + // get the processed label and logo (if any) for every requested label + $actualLabels = $logos = array(); + foreach ($labels as $labelIdx => $label) { + foreach ($dataTable->getArray() as $table) { + // find row for this label. LabelFilter will add empty rows and + // keep them ordered in the same way the labels array is, so we + // assume the $labelIdx is also the row Id + $labelRow = $table->getRowFromId($labelIdx); + + if ($labelRow) { + $actualLabels[$labelIdx] = $this->getRowUrlForEvolutionLabel( + $labelRow, $apiModule, $apiAction, $labelUseAbsoluteUrl); + + $logos[$labelIdx] = $labelRow->getMetadata('logo'); + + if (!empty($actualLabels[$labelIdx])) { + break; + } + } + } + + if (empty($actualLabels[$labelIdx])) { + $actualLabels[$labelIdx] = $this->cleanOriginalLabel($label); + } + } + + // convert rows to be array($column.'_'.$labelIdx => $value) as opposed to + // array('label' => $label, 'column' => $value). + $dataTableMulti = $dataTable->getEmptyClone(); + foreach ($dataTable->getArray() as $tableLabel => $table) { + $newRow = new Piwik_DataTable_Row(); + + foreach ($table->getRows() as $rowId => $row) { + $value = $row->getColumn($column); + $value = floatVal(str_replace(',', '.', $value)); + if ($value == '') { + $value = 0; + } + + $newLabel = $column . '_' . $rowId; // $rowId corresponds to the label index + + $newRow->addColumn($newLabel, $value); + } + + $newTable = $table->getEmptyClone(); + $newTable->addRow($newRow); + $dataTableMulti->addTable($newTable, $tableLabel); + } + + // the available metrics for the report are returned as metadata / columns + $metadata['columns'] = $metadata['metrics']; + + // metadata / metrics should document the rows that are compared + // this way, UI code can be reused + $metadata['metrics'] = array(); + foreach ($actualLabels as $labelIndex => $label) { + if ($legendAppendMetric) { + $label .= ' (' . $metadata['columns'][$column] . ')'; + } + $metricName = $column . '_' . $labelIndex; + $metadata['metrics'][$metricName] = Piwik_DataTable_Filter_SafeDecodeLabel::safeDecodeLabel($label); + + if (!empty($logos[$labelIndex])) { + $metadata['logos'][$metricName] = $logos[$labelIndex]; + } + } + + $this->enhanceRowEvolutionMetaData($metadata, $dataTableMulti); + + return array( + 'column' => $column, + 'reportData' => $dataTableMulti, + 'metadata' => $metadata + ); + } + + /** + * Returns a prettier, more comprehensible version of a row evolution label + * for display. + */ + private function cleanOriginalLabel($label) + { + return str_replace(Piwik_API_DataTableManipulator_LabelFilter::SEPARATOR_RECURSIVE_LABEL, ' - ', $label); + } + + /** + * Performs multiple API requests at once and returns every result. + * + * @param array $urls The array of API requests. + */ + public function getBulkRequest($urls) + { + if (empty($urls)) { + return array(); + } + + $urls = Piwik_Common::unsanitizeInputValues($urls); + + $result = array(); + foreach ($urls as $url) { + $req = new Piwik_API_Request($url); + $result[] = $req->process(); + } + return $result; + } } diff --git a/plugins/API/Controller.php b/plugins/API/Controller.php index 134b055715..6c3a5424fa 100644 --- a/plugins/API/Controller.php +++ b/plugins/API/Controller.php @@ -1,109 +1,105 @@ General['API_datatable_default_limit']; - } - $request = new Piwik_API_Request('token_auth='.Piwik_Common::getRequestVar('token_auth', 'anonymous', 'string')); - echo $request->process(); - } + function index() + { + // when calling the API through http, we limit the number of returned results + if (!isset($_GET['filter_limit'])) { + $_GET['filter_limit'] = Piwik_Config::getInstance()->General['API_datatable_default_limit']; + } + $request = new Piwik_API_Request('token_auth=' . Piwik_Common::getRequestVar('token_auth', 'anonymous', 'string')); + echo $request->process(); + } - public function listAllMethods() - { - $ApiDocumentation = new Piwik_API_DocumentationGenerator(); - echo $ApiDocumentation->getAllInterfaceString( $outputExampleUrls = true, $prefixUrls = Piwik_Common::getRequestVar('prefixUrl', '') ); - } - - public function listAllAPI() - { - $view = Piwik_View::factory("listAllAPI"); - $this->setGeneralVariablesView($view); - - $ApiDocumentation = new Piwik_API_DocumentationGenerator(); - $view->countLoadedAPI = Piwik_API_Proxy::getInstance()->getCountRegisteredClasses(); - $view->list_api_methods_with_links = $ApiDocumentation->getAllInterfaceString(); - echo $view->render(); - } - - public function listSegments() - { - $segments = Piwik_API_API::getInstance()->getSegmentsMetadata($this->idSite); - - $tableDimensions = $tableMetrics = ''; - $customVariables=0; - $lastCategory=array(); - foreach($segments as $segment) - { - $onlyDisplay = array('customVariableName1', 'customVariableName2', 'customVariableValue1', 'customVariableValue2', 'customVariablePageName1', 'customVariablePageValue1'); - $customVariableWillBeDisplayed = in_array($segment['segment'], $onlyDisplay); - // Don't display more than 4 custom variables name/value rows - if($segment['category'] == 'Custom Variables' - && !$customVariableWillBeDisplayed) - { - continue; - } - - $thisCategory = $segment['category']; - $output = ''; - if(empty($lastCategory[$segment['type']]) - || $lastCategory[$segment['type']] != $thisCategory) - { - $output .= ''.$thisCategory.''; - } - - $lastCategory[$segment['type']] = $thisCategory; - - $exampleValues = isset($segment['acceptedValues']) - ? 'Example values: '.$segment['acceptedValues'].'' - : ''; - $restrictedToAdmin = isset($segment['permission']) ? '
Note: This segment can only be used by an Admin user' : ''; - $output .= ' - '.$segment['segment'].' - '.$segment['name'] .$restrictedToAdmin.'
'.$exampleValues.' + public function listAllMethods() + { + $ApiDocumentation = new Piwik_API_DocumentationGenerator(); + echo $ApiDocumentation->getAllInterfaceString($outputExampleUrls = true, $prefixUrls = Piwik_Common::getRequestVar('prefixUrl', '')); + } + + public function listAllAPI() + { + $view = Piwik_View::factory("listAllAPI"); + $this->setGeneralVariablesView($view); + + $ApiDocumentation = new Piwik_API_DocumentationGenerator(); + $view->countLoadedAPI = Piwik_API_Proxy::getInstance()->getCountRegisteredClasses(); + $view->list_api_methods_with_links = $ApiDocumentation->getAllInterfaceString(); + echo $view->render(); + } + + public function listSegments() + { + $segments = Piwik_API_API::getInstance()->getSegmentsMetadata($this->idSite); + + $tableDimensions = $tableMetrics = ''; + $customVariables = 0; + $lastCategory = array(); + foreach ($segments as $segment) { + $onlyDisplay = array('customVariableName1', 'customVariableName2', 'customVariableValue1', 'customVariableValue2', 'customVariablePageName1', 'customVariablePageValue1'); + $customVariableWillBeDisplayed = in_array($segment['segment'], $onlyDisplay); + // Don't display more than 4 custom variables name/value rows + if ($segment['category'] == 'Custom Variables' + && !$customVariableWillBeDisplayed + ) { + continue; + } + + $thisCategory = $segment['category']; + $output = ''; + if (empty($lastCategory[$segment['type']]) + || $lastCategory[$segment['type']] != $thisCategory + ) { + $output .= '' . $thisCategory . ''; + } + + $lastCategory[$segment['type']] = $thisCategory; + + $exampleValues = isset($segment['acceptedValues']) + ? 'Example values: ' . $segment['acceptedValues'] . '' + : ''; + $restrictedToAdmin = isset($segment['permission']) ? '
Note: This segment can only be used by an Admin user' : ''; + $output .= ' + ' . $segment['segment'] . ' + ' . $segment['name'] . $restrictedToAdmin . '
' . $exampleValues . ' '; - - // Show only 2 custom variables and display message for rest - if($customVariableWillBeDisplayed) - { - $customVariables++; - if($customVariables == count($onlyDisplay)) - { - $output .= ' There are 5 custom variables available, so you can segment across any segment name and value range. + + // Show only 2 custom variables and display message for rest + if ($customVariableWillBeDisplayed) { + $customVariables++; + if ($customVariables == count($onlyDisplay)) { + $output .= ' There are 5 custom variables available, so you can segment across any segment name and value range.
For example, customVariableName1==Type;customVariableValue1==Customer
Returns all visitors that have the Custom Variable "Type" set to "Customer".
Custom Variables of scope "page" can be queried separately. For example, to query the Custom Variable of scope "page",
stored in index 1, you would use the segment customVariablePageName1==ArticleLanguage;customVariablePageValue1==FR '; - } - } - - - if($segment['type'] == 'dimension') { - $tableDimensions .= $output; - } else { - $tableMetrics .= $output; - } - } - - echo " + } + } + + + if ($segment['type'] == 'dimension') { + $tableDimensions .= $output; + } else { + $tableMetrics .= $output; + } + } + + echo " Dimensions $tableDimensions @@ -114,5 +110,5 @@ class Piwik_API_Controller extends Piwik_Controller $tableMetrics
"; - } + } } diff --git a/plugins/API/css/styles.css b/plugins/API/css/styles.css index 2b2f547da6..06a3c9f435 100644 --- a/plugins/API/css/styles.css +++ b/plugins/API/css/styles.css @@ -1,45 +1,48 @@ - #token_auth { - background-color:#E8FFE9; - border:1px solid #00CC3A; - margin: 0 0 16px 8px; - padding: 12px; - line-height:4em; + background-color: #E8FFE9; + border: 1px solid #00CC3A; + margin: 0 0 16px 8px; + padding: 12px; + line-height: 4em; } + .example, .example A { - color:#9E9E9E; + color: #9E9E9E; } -.page_api{ - padding:0 15px 0 15px; - font-size:13px; +.page_api { + padding: 0 15px 0 15px; + font-size: 13px; } .page_api h2 { - border-bottom:1px solid #DADADA; - margin:10px -15px 15px 0; - padding:0 0 5px 0; - font-size:24px; + border-bottom: 1px solid #DADADA; + margin: 10px -15px 15px 0; + padding: 0 0 5px 0; + font-size: 24px; } .page_api p { - line-height:140%; - padding-bottom:20px; + line-height: 140%; + padding-bottom: 20px; } .apiFirstLine { - font-weight:bold; - padding-bottom:10px; + font-weight: bold; + padding-bottom: 10px; } + .page_api ul { list-style: disc outside none; margin-left: 25px; } + .apiDescription { - line-height:1.5em; - padding-bottom:1em; + line-height: 1.5em; + padding-bottom: 1em; } + .apiMethod { - margin-bottom:5px; - margin-left:20px; + margin-bottom: 5px; + margin-left: 20px; } \ No newline at end of file diff --git a/plugins/API/templates/listAllAPI.tpl b/plugins/API/templates/listAllAPI.tpl index d59d651500..70f02ef4a9 100644 --- a/plugins/API/templates/listAllAPI.tpl +++ b/plugins/API/templates/listAllAPI.tpl @@ -7,24 +7,28 @@
{include file="CoreHome/templates/period_select.tpl"}
- +

{'API_QuickDocumentationTitle'|translate}

+

{'API_PluginDescription'|translate}

- + {if $isSuperUser}

{'API_GenerateVisits'|translate:'VisitorGenerator':'VisitorGenerator'}

{/if} - -

{'API_MoreInformation'|translate:"":"":"":""}

- + +

+ {'API_MoreInformation'|translate:"":"":"":""} +

+

{'API_UserAuthentication'|translate}

+

- {'API_UsingTokenAuth'|translate:'':'':""}
- &token_auth={$token_auth}
- {'API_KeepTokenSecret'|translate:'':''} - - {$list_api_methods_with_links} -
+ {'API_UsingTokenAuth'|translate:'':'':""}
+ &token_auth={$token_auth}
+ {'API_KeepTokenSecret'|translate:'':''} + + {$list_api_methods_with_links} +
{include file="CoreHome/templates/footer.tpl"} diff --git a/plugins/Actions/API.php b/plugins/Actions/API.php index bb0367c742..060912f19f 100644 --- a/plugins/Actions/API.php +++ b/plugins/Actions/API.php @@ -1,10 +1,10 @@ Actions metrics for each row. - * - * It is also possible to request data for a specific Page Title with "getPageTitle" - * and setting the parameter pageName to the page title you wish to request. - * Similarly, you can request metrics for a given Page URL via "getPageUrl", a Download file via "getDownload" + * + * It is also possible to request data for a specific Page Title with "getPageTitle" + * and setting the parameter pageName to the page title you wish to request. + * Similarly, you can request metrics for a given Page URL via "getPageUrl", a Download file via "getDownload" * and an outlink via "getOutlink". - * + * * Note: pageName, pageUrl, outlinkUrl, downloadUrl parameters must be URL encoded before you call the API. * @package Piwik_Actions */ class Piwik_Actions_API { - static private $instance = null; - - /** - * @return Piwik_Actions_API - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - - /** - * Backward compatibility. Fallsback to getPageTitles() instead. - * @deprecated Deprecated since Piwik 0.5 - * @ignore - * - * @param int $idSite - * @param string $period - * @param $date - * @param bool $segment - * @param bool $expanded - * @param bool|int $idSubtable - * @return Piwik_DataTable|Piwik_DataTable_Array - */ - public function getActions( $idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false ) - { - return $this->getPageTitles( $idSite, $period, $date, $segment, $expanded, $idSubtable ); - } - - /** - * Returns the list of metrics (pages, downloads, outlinks) - * - * @param int $idSite - * @param string $period - * @param string $date - * @param bool|string $segment - * @param bool|array $columns - * @return Piwik_DataTable - */ - public function get( $idSite, $period, $date, $segment = false, $columns = false) - { - Piwik::checkUserHasViewAccess( $idSite ); - $archive = Piwik_Archive::build( $idSite, $period, $date, $segment ); - - $metrics = array( - 'Actions_nb_pageviews' => 'nb_pageviews', - 'Actions_nb_uniq_pageviews' => 'nb_uniq_pageviews', - 'Actions_nb_downloads' => 'nb_downloads', - 'Actions_nb_uniq_downloads' => 'nb_uniq_downloads', - 'Actions_nb_outlinks' => 'nb_outlinks', - 'Actions_nb_uniq_outlinks' => 'nb_uniq_outlinks', - 'Actions_nb_searches' => 'nb_searches', - 'Actions_nb_keywords' => 'nb_keywords', - ); - - // get requested columns - $columns = Piwik::getArrayFromApiParameter($columns); - if(!empty($columns)) - { - // get the columns that are available and requested - $columns = array_intersect($columns, array_values($metrics)); - $columns = array_values($columns); // make sure indexes are right - $nameReplace = array(); - foreach ($columns as $i => $column) - { - $fullColumn = array_search($column, $metrics); - $columns[$i] = $fullColumn; - $nameReplace[$fullColumn] = $column; - } - } - else - { - // get all columns - $columns = array_keys($metrics); - $nameReplace = &$metrics; - } - - $table = $archive->getDataTableFromNumeric($columns); - - // replace labels (remove Actions_) - $table->filter('ReplaceColumnNames', array($nameReplace)); - - return $table; - } + static private $instance = null; /** - * @param int $idSite - * @param string $period - * @param Piwik_Date $date - * @param bool $segment - * @param bool $expanded - * @param bool $idSubtable + * @return Piwik_Actions_API + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + + /** + * Backward compatibility. Fallsback to getPageTitles() instead. + * @deprecated Deprecated since Piwik 0.5 + * @ignore * + * @param int $idSite + * @param string $period + * @param $date + * @param bool $segment + * @param bool $expanded + * @param bool|int $idSubtable * @return Piwik_DataTable|Piwik_DataTable_Array */ - public function getPageUrls( $idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false ) - { - $dataTable = Piwik_Archive::getDataTableFromArchive('Actions_actions_url', $idSite, $period, $date, $segment, $expanded, $idSubtable ); - $this->filterPageDatatable($dataTable); - $this->filterActionsDataTable($dataTable, $expanded); - return $dataTable; - } + public function getActions($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false) + { + return $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable); + } /** - * @param int $idSite - * @param string $period - * @param Piwik_Date $date - * @param bool $segment - * @param bool $expanded - * @param bool $idSubtable + * Returns the list of metrics (pages, downloads, outlinks) + * + * @param int $idSite + * @param string $period + * @param string $date + * @param bool|string $segment + * @param bool|array $columns + * @return Piwik_DataTable + */ + public function get($idSite, $period, $date, $segment = false, $columns = false) + { + Piwik::checkUserHasViewAccess($idSite); + $archive = Piwik_Archive::build($idSite, $period, $date, $segment); + + $metrics = array( + 'Actions_nb_pageviews' => 'nb_pageviews', + 'Actions_nb_uniq_pageviews' => 'nb_uniq_pageviews', + 'Actions_nb_downloads' => 'nb_downloads', + 'Actions_nb_uniq_downloads' => 'nb_uniq_downloads', + 'Actions_nb_outlinks' => 'nb_outlinks', + 'Actions_nb_uniq_outlinks' => 'nb_uniq_outlinks', + 'Actions_nb_searches' => 'nb_searches', + 'Actions_nb_keywords' => 'nb_keywords', + ); + + // get requested columns + $columns = Piwik::getArrayFromApiParameter($columns); + if (!empty($columns)) { + // get the columns that are available and requested + $columns = array_intersect($columns, array_values($metrics)); + $columns = array_values($columns); // make sure indexes are right + $nameReplace = array(); + foreach ($columns as $i => $column) { + $fullColumn = array_search($column, $metrics); + $columns[$i] = $fullColumn; + $nameReplace[$fullColumn] = $column; + } + } else { + // get all columns + $columns = array_keys($metrics); + $nameReplace = & $metrics; + } + + $table = $archive->getDataTableFromNumeric($columns); + + // replace labels (remove Actions_) + $table->filter('ReplaceColumnNames', array($nameReplace)); + + return $table; + } + + /** + * @param int $idSite + * @param string $period + * @param Piwik_Date $date + * @param bool $segment + * @param bool $expanded + * @param bool $idSubtable + * + * @return Piwik_DataTable|Piwik_DataTable_Array + */ + public function getPageUrls($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false) + { + $dataTable = Piwik_Archive::getDataTableFromArchive('Actions_actions_url', $idSite, $period, $date, $segment, $expanded, $idSubtable); + $this->filterPageDatatable($dataTable); + $this->filterActionsDataTable($dataTable, $expanded); + return $dataTable; + } + + /** + * @param int $idSite + * @param string $period + * @param Piwik_Date $date + * @param bool $segment + * @param bool $expanded + * @param bool $idSubtable * * @return Piwik_DataTable|Piwik_DataTable_Array */ - public function getPageUrlsFollowingSiteSearch( $idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false ) - { - $dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable); - $this->keepPagesFollowingSearch($dataTable); - return $dataTable; - } + public function getPageUrlsFollowingSiteSearch($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false) + { + $dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable); + $this->keepPagesFollowingSearch($dataTable); + return $dataTable; + } /** - * @param int $idSite - * @param string $period - * @param Piwik_Date $date - * @param bool $segment - * @param bool $expanded - * @param bool $idSubtable + * @param int $idSite + * @param string $period + * @param Piwik_Date $date + * @param bool $segment + * @param bool $expanded + * @param bool $idSubtable * * @return Piwik_DataTable|Piwik_DataTable_Array */ - public function getPageTitlesFollowingSiteSearch( $idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false ) - { - $dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable); - $this->keepPagesFollowingSearch($dataTable); - return $dataTable; - } + public function getPageTitlesFollowingSiteSearch($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false) + { + $dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable); + $this->keepPagesFollowingSearch($dataTable); + return $dataTable; + } /** * @param Piwik_DataTable $dataTable */ protected function keepPagesFollowingSearch($dataTable) - { - // Keep only pages which are following site search - $dataTable->filter('ColumnCallbackDeleteRow', array( - 'nb_hits_following_search', - create_function('$value', 'return $value > 0;') - )); - } - - /** - * Returns a DataTable with analytics information for every unique entry page URL, for - * the specified site, period & segment. - */ - public function getEntryPageUrls( $idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false ) - { - $dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable); - $this->filterNonEntryActions($dataTable); - return $dataTable; - } - - /** - * Returns a DataTable with analytics information for every unique exit page URL, for - * the specified site, period & segment. - */ - public function getExitPageUrls( $idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false ) - { - $dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable); - $this->filterNonExitActions($dataTable); - return $dataTable; - } - - public function getPageUrl( $pageUrl, $idSite, $period, $date, $segment = false) - { - $callBackParameters = array('Actions_actions_url', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false ); - $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageUrl, Piwik_Tracker_Action::TYPE_ACTION_URL); - $this->filterPageDatatable($dataTable); - $this->filterActionsDataTable($dataTable); - return $dataTable; - } - - public function getPageTitles( $idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false) - { - $dataTable = Piwik_Archive::getDataTableFromArchive('Actions_actions', $idSite, $period, $date, $segment, $expanded, $idSubtable); - $this->filterPageDatatable($dataTable); - $this->filterActionsDataTable($dataTable, $expanded); - return $dataTable; - } - - /** - * Returns a Piwik_DataTable with analytics information for every unique entry page title - * for the given site, time period & segment. - */ - public function getEntryPageTitles( $idSite, $period, $date, $segment = false, $expanded = false, - $idSubtable = false ) - { - $dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable); - $this->filterNonEntryActions($dataTable); - return $dataTable; - } - - /** - * Returns a Piwik_DataTable with analytics information for every unique exit page title - * for the given site, time period & segment. - */ - public function getExitPageTitles( $idSite, $period, $date, $segment = false, $expanded = false, - $idSubtable = false ) - { - $dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable); - $this->filterNonExitActions($dataTable); - return $dataTable; - } - - public function getPageTitle( $pageName, $idSite, $period, $date, $segment = false) - { - $callBackParameters = array('Actions_actions', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false ); - $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageName, Piwik_Tracker_Action::TYPE_ACTION_NAME); - $this->filterPageDatatable($dataTable); - $this->filterActionsDataTable($dataTable); - return $dataTable; - } - - public function getDownloads( $idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false ) - { - $dataTable = Piwik_Archive::getDataTableFromArchive('Actions_downloads', $idSite, $period, $date, $segment, $expanded, $idSubtable ); - $this->filterActionsDataTable($dataTable, $expanded); - return $dataTable; - } - - public function getDownload( $downloadUrl, $idSite, $period, $date, $segment = false) - { - $callBackParameters = array('Actions_downloads', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false ); - $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $downloadUrl, Piwik_Tracker_Action::TYPE_DOWNLOAD); - $this->filterActionsDataTable($dataTable); - return $dataTable; - } - - public function getOutlinks( $idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false ) - { - $dataTable = Piwik_Archive::getDataTableFromArchive('Actions_outlink', $idSite, $period, $date, $segment, $expanded, $idSubtable ); - $this->filterActionsDataTable($dataTable, $expanded); - return $dataTable; - } - - public function getOutlink( $outlinkUrl, $idSite, $period, $date, $segment = false) - { - $callBackParameters = array('Actions_outlink', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false ); - $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $outlinkUrl, Piwik_Tracker_Action::TYPE_OUTLINK); - $this->filterActionsDataTable($dataTable); - return $dataTable; - } - - public function getSiteSearchKeywords( $idSite, $period, $date, $segment = false ) - { - $dataTable = $this->getSiteSearchKeywordsRaw($idSite, $period, $date, $segment); - $dataTable->deleteColumn(Piwik_Archive::INDEX_SITE_SEARCH_HAS_NO_RESULT); - $this->filterPageDatatable($dataTable); - $this->filterActionsDataTable($dataTable); - $this->addPagesPerSearchColumn($dataTable); - return $dataTable; - } - - //Visitors can search, and then click "next" to view more results. This is the average number of search results pages viewed for this keyword. - public function addPagesPerSearchColumn($dataTable, $columnToRead = 'nb_hits') - { - $dataTable->filter('ColumnCallbackAddColumnQuotient', array('nb_pages_per_search', $columnToRead, 'nb_visits', $precision = 1)); - } - - protected function getSiteSearchKeywordsRaw($idSite, $period, $date, $segment) - { - $dataTable = Piwik_Archive::getDataTableFromArchive('Actions_sitesearch', $idSite, $period, $date, $segment, $expanded = false); - return $dataTable; - } - - public function getSiteSearchNoResultKeywords( $idSite, $period, $date, $segment = false ) - { - $dataTable = $this->getSiteSearchKeywordsRaw($idSite, $period, $date, $segment); - // Delete all rows that have some results - $dataTable->filter('ColumnCallbackDeleteRow', - array( - Piwik_Archive::INDEX_SITE_SEARCH_HAS_NO_RESULT, - create_function ( '$value', 'return $value >= 1;') - )); - $dataTable->deleteRow(Piwik_DataTable::ID_SUMMARY_ROW); - $dataTable->deleteColumn(Piwik_Archive::INDEX_SITE_SEARCH_HAS_NO_RESULT); - $this->filterPageDatatable($dataTable); - $this->filterActionsDataTable($dataTable); - $this->addPagesPerSearchColumn($dataTable); - return $dataTable; - } + { + // Keep only pages which are following site search + $dataTable->filter('ColumnCallbackDeleteRow', array( + 'nb_hits_following_search', + create_function('$value', 'return $value > 0;') + )); + } + + /** + * Returns a DataTable with analytics information for every unique entry page URL, for + * the specified site, period & segment. + */ + public function getEntryPageUrls($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false) + { + $dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable); + $this->filterNonEntryActions($dataTable); + return $dataTable; + } /** - * @param int $idSite - * @param string $period - * @param Piwik_Date $date - * @param bool $segment + * Returns a DataTable with analytics information for every unique exit page URL, for + * the specified site, period & segment. + */ + public function getExitPageUrls($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false) + { + $dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable); + $this->filterNonExitActions($dataTable); + return $dataTable; + } + + public function getPageUrl($pageUrl, $idSite, $period, $date, $segment = false) + { + $callBackParameters = array('Actions_actions_url', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false); + $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageUrl, Piwik_Tracker_Action::TYPE_ACTION_URL); + $this->filterPageDatatable($dataTable); + $this->filterActionsDataTable($dataTable); + return $dataTable; + } + + public function getPageTitles($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false) + { + $dataTable = Piwik_Archive::getDataTableFromArchive('Actions_actions', $idSite, $period, $date, $segment, $expanded, $idSubtable); + $this->filterPageDatatable($dataTable); + $this->filterActionsDataTable($dataTable, $expanded); + return $dataTable; + } + + /** + * Returns a Piwik_DataTable with analytics information for every unique entry page title + * for the given site, time period & segment. + */ + public function getEntryPageTitles($idSite, $period, $date, $segment = false, $expanded = false, + $idSubtable = false) + { + $dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable); + $this->filterNonEntryActions($dataTable); + return $dataTable; + } + + /** + * Returns a Piwik_DataTable with analytics information for every unique exit page title + * for the given site, time period & segment. + */ + public function getExitPageTitles($idSite, $period, $date, $segment = false, $expanded = false, + $idSubtable = false) + { + $dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable); + $this->filterNonExitActions($dataTable); + return $dataTable; + } + + public function getPageTitle($pageName, $idSite, $period, $date, $segment = false) + { + $callBackParameters = array('Actions_actions', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false); + $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageName, Piwik_Tracker_Action::TYPE_ACTION_NAME); + $this->filterPageDatatable($dataTable); + $this->filterActionsDataTable($dataTable); + return $dataTable; + } + + public function getDownloads($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false) + { + $dataTable = Piwik_Archive::getDataTableFromArchive('Actions_downloads', $idSite, $period, $date, $segment, $expanded, $idSubtable); + $this->filterActionsDataTable($dataTable, $expanded); + return $dataTable; + } + + public function getDownload($downloadUrl, $idSite, $period, $date, $segment = false) + { + $callBackParameters = array('Actions_downloads', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false); + $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $downloadUrl, Piwik_Tracker_Action::TYPE_DOWNLOAD); + $this->filterActionsDataTable($dataTable); + return $dataTable; + } + + public function getOutlinks($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false) + { + $dataTable = Piwik_Archive::getDataTableFromArchive('Actions_outlink', $idSite, $period, $date, $segment, $expanded, $idSubtable); + $this->filterActionsDataTable($dataTable, $expanded); + return $dataTable; + } + + public function getOutlink($outlinkUrl, $idSite, $period, $date, $segment = false) + { + $callBackParameters = array('Actions_outlink', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false); + $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $outlinkUrl, Piwik_Tracker_Action::TYPE_OUTLINK); + $this->filterActionsDataTable($dataTable); + return $dataTable; + } + + public function getSiteSearchKeywords($idSite, $period, $date, $segment = false) + { + $dataTable = $this->getSiteSearchKeywordsRaw($idSite, $period, $date, $segment); + $dataTable->deleteColumn(Piwik_Archive::INDEX_SITE_SEARCH_HAS_NO_RESULT); + $this->filterPageDatatable($dataTable); + $this->filterActionsDataTable($dataTable); + $this->addPagesPerSearchColumn($dataTable); + return $dataTable; + } + + //Visitors can search, and then click "next" to view more results. This is the average number of search results pages viewed for this keyword. + public function addPagesPerSearchColumn($dataTable, $columnToRead = 'nb_hits') + { + $dataTable->filter('ColumnCallbackAddColumnQuotient', array('nb_pages_per_search', $columnToRead, 'nb_visits', $precision = 1)); + } + + protected function getSiteSearchKeywordsRaw($idSite, $period, $date, $segment) + { + $dataTable = Piwik_Archive::getDataTableFromArchive('Actions_sitesearch', $idSite, $period, $date, $segment, $expanded = false); + return $dataTable; + } + + public function getSiteSearchNoResultKeywords($idSite, $period, $date, $segment = false) + { + $dataTable = $this->getSiteSearchKeywordsRaw($idSite, $period, $date, $segment); + // Delete all rows that have some results + $dataTable->filter('ColumnCallbackDeleteRow', + array( + Piwik_Archive::INDEX_SITE_SEARCH_HAS_NO_RESULT, + create_function('$value', 'return $value >= 1;') + )); + $dataTable->deleteRow(Piwik_DataTable::ID_SUMMARY_ROW); + $dataTable->deleteColumn(Piwik_Archive::INDEX_SITE_SEARCH_HAS_NO_RESULT); + $this->filterPageDatatable($dataTable); + $this->filterActionsDataTable($dataTable); + $this->addPagesPerSearchColumn($dataTable); + return $dataTable; + } + + /** + * @param int $idSite + * @param string $period + * @param Piwik_Date $date + * @param bool $segment * * @return Piwik_DataTable|Piwik_DataTable_Array */ - public function getSiteSearchCategories( $idSite, $period, $date, $segment = false ) - { - Piwik_Actions::checkCustomVariablesPluginEnabled(); - $customVariables = Piwik_CustomVariables_API::getInstance()->getCustomVariables($idSite, $period, $date, $segment, $expanded = false, $_leavePiwikCoreVariables = true); - - $customVarNameToLookFor = Piwik_Tracker_Action::CVAR_KEY_SEARCH_CATEGORY; - - $dataTable = new Piwik_DataTable(); - // Handle case where date=last30&period=day - // TODO: this logic should really be refactored somewhere, this is ugly! - if($customVariables instanceof Piwik_DataTable_Array) - { - $dataTable = $customVariables->getEmptyClone(); - - $customVariableDatatables = $customVariables->getArray(); - $dataTables = $dataTable->getArray(); - foreach($customVariableDatatables as $key => $customVariableTableForDate) - { - // we do not enter the IF, in the case idSite=1,3 AND period=day&date=datefrom,dateto, - if(isset($customVariableTableForDate->metadata['period'])) - { - $row = $customVariableTableForDate->getRowFromLabel($customVarNameToLookFor); - if($row) - { - $dateRewrite = $customVariableTableForDate->metadata['period']->getDateStart()->toString(); - $idSubtable = $row->getIdSubDataTable(); - $categories = Piwik_CustomVariables_API::getInstance()->getCustomVariablesValuesFromNameId($idSite, $period, $dateRewrite, $idSubtable, $segment); - $dataTable->addTable($categories, $key); - } - } - } - } - elseif($customVariables instanceof Piwik_DataTable) - { - $row = $customVariables->getRowFromLabel($customVarNameToLookFor); - if($row) - { - $idSubtable = $row->getIdSubDataTable(); - $dataTable = Piwik_CustomVariables_API::getInstance()->getCustomVariablesValuesFromNameId($idSite, $period, $date, $idSubtable, $segment); - } - } - $this->filterActionsDataTable($dataTable); - $this->addPagesPerSearchColumn($dataTable, $columnToRead = 'nb_actions'); - return $dataTable; - } - - /** - * Will search in the DataTable for a Label matching the searched string - * and return only the matching row, or an empty datatable - */ - protected function getFilterPageDatatableSearch($callBackParameters, $search, $actionType, $table = false, - $searchTree = false) - { - if ($searchTree === false) - { - // build the query parts that are searched inside the tree - if($actionType == Piwik_Tracker_Action::TYPE_ACTION_NAME) - { - $searchedString = Piwik_Common::unsanitizeInputValue($search); - } - else - { - $idSite = $callBackParameters[1]; - try { - $searchedString = Piwik_Tracker_Action::excludeQueryParametersFromUrl($search, $idSite); - } catch(Exception $e) { - $searchedString = $search; - } - } - Piwik_Actions_ArchivingHelper::reloadConfig(); - $searchTree = Piwik_Actions_ArchivingHelper::getActionExplodedNames($searchedString, $actionType); - } - - if ($table === false) - { - // fetch the data table - $table = call_user_func_array(array('Piwik_Archive', 'getDataTableFromArchive'), $callBackParameters); - - if ($table instanceof Piwik_DataTable_Array) - { - // search an array of tables, e.g. when using date=last30 - // note that if the root is an array, we filter all children - // if an array occurs inside the nested table, we only look for the first match (see below) - $newTableArray = $table->getEmptyClone(); - - foreach ($table->getArray() as $label => $subTable) - { - $newSubTable = $this->doFilterPageDatatableSearch($callBackParameters, $subTable, $searchTree); - - $newTableArray->addTable($newSubTable, $label); - } - - return $newTableArray; - } - - } - - return $this->doFilterPageDatatableSearch($callBackParameters, $table, $searchTree); - } - - /** - * This looks very similar to LabelFilter.php should it be refactored somehow? FIXME - */ - protected function doFilterPageDatatableSearch($callBackParameters, $table, $searchTree) - { - // filter a data table array - if ($table instanceof Piwik_DataTable_Array) - { - foreach ($table->getArray() as $subTable) - { - $filteredSubTable = $this->doFilterPageDatatableSearch($callBackParameters, $subTable, $searchTree); - - if ($filteredSubTable->getRowsCount() > 0) - { - // match found in a sub table, return and stop searching the others - return $filteredSubTable; - } - } - - // nothing found in all sub tables - return new Piwik_DataTable; - } - - // filter regular data table - if ($table instanceof Piwik_DataTable) - { - // search for the first part of the tree search - $search = array_shift($searchTree); - $row = $table->getRowFromLabel($search); - if ($row === false) - { - // not found - $result = new Piwik_DataTable; - $result->metadata = $table->metadata; - return $result; - } - - // end of tree search reached - if (count($searchTree) == 0) - { - $result = new Piwik_DataTable(); - $result->addRow($row); - $result->metadata = $table->metadata; - return $result; - } - - // match found on this level and more levels remaining: go deeper - $idSubTable = $row->getIdSubDataTable(); - $callBackParameters[6] = $idSubTable; - $table = call_user_func_array(array('Piwik_Archive', 'getDataTableFromArchive'), $callBackParameters); - return $this->doFilterPageDatatableSearch($callBackParameters, $table, $searchTree); - } - - throw new Exception("For this API function, DataTable ".get_class($table)." is not supported"); - } - - /** - * Common filters for Page URLs and Page Titles - */ - protected function filterPageDatatable($dataTable) - { - // Average time on page = total time on page / number visits on that page - $dataTable->queueFilter('ColumnCallbackAddColumnQuotient', array('avg_time_on_page', 'sum_time_spent', 'nb_visits', 0)); - - // Bounce rate = single page visits on this page / visits started on this page - $dataTable->queueFilter('ColumnCallbackAddColumnPercentage', array('bounce_rate', 'entry_bounce_count', 'entry_nb_visits', 0)); - - // % Exit = Number of visits that finished on this page / visits on this page - $dataTable->queueFilter('ColumnCallbackAddColumnPercentage', array('exit_rate', 'exit_nb_visits', 'nb_visits', 0)); - - // Handle performance analytics - $hasTimeGeneration = (array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION)) > 0); - if ($hasTimeGeneration) { - // Average generation time = total generation time / number of pageviews - $dataTable->queueFilter('ColumnCallbackAddColumnQuotient', array('avg_time_generation', 'sum_time_generation', 'nb_hits_with_time_generation', 3)); - } else { - // No generation time: remove it from the API output and add it to empty_columns metadata, so that - // the columns can also be removed from the view - $dataTable->filter('ColumnDelete', array(array(Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION, Piwik_Archive::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION))); - if ($dataTable instanceof Piwik_DataTable) { - $emptyColumns = $dataTable->getMetadata(Piwik_DataTable::EMPTY_COLUMNS_METADATA_NAME); - if (!is_array($emptyColumns)) { - $emptyColumns = array(); - } - $emptyColumns[] = 'sum_time_generation'; - $emptyColumns[] = 'avg_time_generation'; - $dataTable->setMetadata(Piwik_DataTable::EMPTY_COLUMNS_METADATA_NAME, $emptyColumns); - } - } - } - - /** - * Common filters for all Actions API getters - */ - protected function filterActionsDataTable($dataTable, $expanded = false) - { - // Must be applied before Sort in this case, since the DataTable can contain both int and strings indexes - // (in the transition period between pre 1.2 and post 1.2 datatable structure) - $dataTable->filter('ReplaceColumnNames'); - $dataTable->filter('Sort', array('nb_visits', 'desc', $naturalSort = false, $expanded)); - - $dataTable->queueFilter('ReplaceSummaryRowLabel'); - } - - /** - * Removes DataTable rows referencing actions that were never the first action of a visit. - * - * @param Piwik_DataTable $dataTable - */ - private function filterNonEntryActions( $dataTable ) - { - $dataTable->filter('ColumnCallbackDeleteRow', array('entry_nb_visits', 'strlen')); - } - - /** - * Removes DataTable rows referencing actions that were never the last action of a visit. - * - * @param Piwik_DataTable $dataTable - */ - private function filterNonExitActions( $dataTable ) - { - $dataTable->filter('ColumnCallbackDeleteRow', array('exit_nb_visits', 'strlen')); - } + public function getSiteSearchCategories($idSite, $period, $date, $segment = false) + { + Piwik_Actions::checkCustomVariablesPluginEnabled(); + $customVariables = Piwik_CustomVariables_API::getInstance()->getCustomVariables($idSite, $period, $date, $segment, $expanded = false, $_leavePiwikCoreVariables = true); + + $customVarNameToLookFor = Piwik_Tracker_Action::CVAR_KEY_SEARCH_CATEGORY; + + $dataTable = new Piwik_DataTable(); + // Handle case where date=last30&period=day + // TODO: this logic should really be refactored somewhere, this is ugly! + if ($customVariables instanceof Piwik_DataTable_Array) { + $dataTable = $customVariables->getEmptyClone(); + + $customVariableDatatables = $customVariables->getArray(); + $dataTables = $dataTable->getArray(); + foreach ($customVariableDatatables as $key => $customVariableTableForDate) { + // we do not enter the IF, in the case idSite=1,3 AND period=day&date=datefrom,dateto, + if (isset($customVariableTableForDate->metadata['period'])) { + $row = $customVariableTableForDate->getRowFromLabel($customVarNameToLookFor); + if ($row) { + $dateRewrite = $customVariableTableForDate->metadata['period']->getDateStart()->toString(); + $idSubtable = $row->getIdSubDataTable(); + $categories = Piwik_CustomVariables_API::getInstance()->getCustomVariablesValuesFromNameId($idSite, $period, $dateRewrite, $idSubtable, $segment); + $dataTable->addTable($categories, $key); + } + } + } + } elseif ($customVariables instanceof Piwik_DataTable) { + $row = $customVariables->getRowFromLabel($customVarNameToLookFor); + if ($row) { + $idSubtable = $row->getIdSubDataTable(); + $dataTable = Piwik_CustomVariables_API::getInstance()->getCustomVariablesValuesFromNameId($idSite, $period, $date, $idSubtable, $segment); + } + } + $this->filterActionsDataTable($dataTable); + $this->addPagesPerSearchColumn($dataTable, $columnToRead = 'nb_actions'); + return $dataTable; + } + + /** + * Will search in the DataTable for a Label matching the searched string + * and return only the matching row, or an empty datatable + */ + protected function getFilterPageDatatableSearch($callBackParameters, $search, $actionType, $table = false, + $searchTree = false) + { + if ($searchTree === false) { + // build the query parts that are searched inside the tree + if ($actionType == Piwik_Tracker_Action::TYPE_ACTION_NAME) { + $searchedString = Piwik_Common::unsanitizeInputValue($search); + } else { + $idSite = $callBackParameters[1]; + try { + $searchedString = Piwik_Tracker_Action::excludeQueryParametersFromUrl($search, $idSite); + } catch (Exception $e) { + $searchedString = $search; + } + } + Piwik_Actions_ArchivingHelper::reloadConfig(); + $searchTree = Piwik_Actions_ArchivingHelper::getActionExplodedNames($searchedString, $actionType); + } + + if ($table === false) { + // fetch the data table + $table = call_user_func_array(array('Piwik_Archive', 'getDataTableFromArchive'), $callBackParameters); + + if ($table instanceof Piwik_DataTable_Array) { + // search an array of tables, e.g. when using date=last30 + // note that if the root is an array, we filter all children + // if an array occurs inside the nested table, we only look for the first match (see below) + $newTableArray = $table->getEmptyClone(); + + foreach ($table->getArray() as $label => $subTable) { + $newSubTable = $this->doFilterPageDatatableSearch($callBackParameters, $subTable, $searchTree); + + $newTableArray->addTable($newSubTable, $label); + } + + return $newTableArray; + } + + } + + return $this->doFilterPageDatatableSearch($callBackParameters, $table, $searchTree); + } + + /** + * This looks very similar to LabelFilter.php should it be refactored somehow? FIXME + */ + protected function doFilterPageDatatableSearch($callBackParameters, $table, $searchTree) + { + // filter a data table array + if ($table instanceof Piwik_DataTable_Array) { + foreach ($table->getArray() as $subTable) { + $filteredSubTable = $this->doFilterPageDatatableSearch($callBackParameters, $subTable, $searchTree); + + if ($filteredSubTable->getRowsCount() > 0) { + // match found in a sub table, return and stop searching the others + return $filteredSubTable; + } + } + + // nothing found in all sub tables + return new Piwik_DataTable; + } + + // filter regular data table + if ($table instanceof Piwik_DataTable) { + // search for the first part of the tree search + $search = array_shift($searchTree); + $row = $table->getRowFromLabel($search); + if ($row === false) { + // not found + $result = new Piwik_DataTable; + $result->metadata = $table->metadata; + return $result; + } + + // end of tree search reached + if (count($searchTree) == 0) { + $result = new Piwik_DataTable(); + $result->addRow($row); + $result->metadata = $table->metadata; + return $result; + } + + // match found on this level and more levels remaining: go deeper + $idSubTable = $row->getIdSubDataTable(); + $callBackParameters[6] = $idSubTable; + $table = call_user_func_array(array('Piwik_Archive', 'getDataTableFromArchive'), $callBackParameters); + return $this->doFilterPageDatatableSearch($callBackParameters, $table, $searchTree); + } + + throw new Exception("For this API function, DataTable " . get_class($table) . " is not supported"); + } + + /** + * Common filters for Page URLs and Page Titles + */ + protected function filterPageDatatable($dataTable) + { + // Average time on page = total time on page / number visits on that page + $dataTable->queueFilter('ColumnCallbackAddColumnQuotient', array('avg_time_on_page', 'sum_time_spent', 'nb_visits', 0)); + + // Bounce rate = single page visits on this page / visits started on this page + $dataTable->queueFilter('ColumnCallbackAddColumnPercentage', array('bounce_rate', 'entry_bounce_count', 'entry_nb_visits', 0)); + + // % Exit = Number of visits that finished on this page / visits on this page + $dataTable->queueFilter('ColumnCallbackAddColumnPercentage', array('exit_rate', 'exit_nb_visits', 'nb_visits', 0)); + + // Handle performance analytics + $hasTimeGeneration = (array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION)) > 0); + if ($hasTimeGeneration) { + // Average generation time = total generation time / number of pageviews + $dataTable->queueFilter('ColumnCallbackAddColumnQuotient', array('avg_time_generation', 'sum_time_generation', 'nb_hits_with_time_generation', 3)); + } else { + // No generation time: remove it from the API output and add it to empty_columns metadata, so that + // the columns can also be removed from the view + $dataTable->filter('ColumnDelete', array(array(Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION, Piwik_Archive::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION))); + if ($dataTable instanceof Piwik_DataTable) { + $emptyColumns = $dataTable->getMetadata(Piwik_DataTable::EMPTY_COLUMNS_METADATA_NAME); + if (!is_array($emptyColumns)) { + $emptyColumns = array(); + } + $emptyColumns[] = 'sum_time_generation'; + $emptyColumns[] = 'avg_time_generation'; + $dataTable->setMetadata(Piwik_DataTable::EMPTY_COLUMNS_METADATA_NAME, $emptyColumns); + } + } + } + + /** + * Common filters for all Actions API getters + */ + protected function filterActionsDataTable($dataTable, $expanded = false) + { + // Must be applied before Sort in this case, since the DataTable can contain both int and strings indexes + // (in the transition period between pre 1.2 and post 1.2 datatable structure) + $dataTable->filter('ReplaceColumnNames'); + $dataTable->filter('Sort', array('nb_visits', 'desc', $naturalSort = false, $expanded)); + + $dataTable->queueFilter('ReplaceSummaryRowLabel'); + } + + /** + * Removes DataTable rows referencing actions that were never the first action of a visit. + * + * @param Piwik_DataTable $dataTable + */ + private function filterNonEntryActions($dataTable) + { + $dataTable->filter('ColumnCallbackDeleteRow', array('entry_nb_visits', 'strlen')); + } + + /** + * Removes DataTable rows referencing actions that were never the last action of a visit. + * + * @param Piwik_DataTable $dataTable + */ + private function filterNonExitActions($dataTable) + { + $dataTable->filter('ColumnCallbackDeleteRow', array('exit_nb_visits', 'strlen')); + } } diff --git a/plugins/Actions/Actions.php b/plugins/Actions/Actions.php index 230968e66f..635b0d95c4 100644 --- a/plugins/Actions/Actions.php +++ b/plugins/Actions/Actions.php @@ -8,7 +8,7 @@ * @category Piwik_Plugins * @package Piwik_Actions */ - + /** * Actions plugin * @@ -18,592 +18,585 @@ */ class Piwik_Actions extends Piwik_Plugin { - public function getInformation() - { - $info = array( - 'description' => Piwik_Translate('Actions_PluginDescription'), - 'author' => 'Piwik', - 'author_homepage' => 'http://piwik.org/', - 'version' => Piwik_Version::VERSION, - ); - return $info; - } - - - public function getListHooksRegistered() - { - $hooks = array( - 'ArchiveProcessing_Day.compute' => 'archiveDay', - 'ArchiveProcessing_Period.compute' => 'archivePeriod', - 'WidgetsList.add' => 'addWidgets', - 'Menu.add' => 'addMenus', - 'API.getReportMetadata' => 'getReportMetadata', - 'API.getSegmentsMetadata' => 'getSegmentsMetadata', - ); - return $hooks; - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - public function getSegmentsMetadata($notification) - { - $segments =& $notification->getNotificationObject(); - $sqlFilter = array($this, 'getIdActionFromSegment'); - - // entry and exit pages of visit - $segments[] = array( - 'type' => 'dimension', - 'category' => 'Actions_Actions', - 'name' => 'Actions_ColumnEntryPageURL', - 'segment' => 'entryPageUrl', - 'sqlSegment' => 'log_visit.visit_entry_idaction_url', - 'sqlFilter' => $sqlFilter, + public function getInformation() + { + $info = array( + 'description' => Piwik_Translate('Actions_PluginDescription'), + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'version' => Piwik_Version::VERSION, + ); + return $info; + } + + + public function getListHooksRegistered() + { + $hooks = array( + 'ArchiveProcessing_Day.compute' => 'archiveDay', + 'ArchiveProcessing_Period.compute' => 'archivePeriod', + 'WidgetsList.add' => 'addWidgets', + 'Menu.add' => 'addMenus', + 'API.getReportMetadata' => 'getReportMetadata', + 'API.getSegmentsMetadata' => 'getSegmentsMetadata', + ); + return $hooks; + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + public function getSegmentsMetadata($notification) + { + $segments =& $notification->getNotificationObject(); + $sqlFilter = array($this, 'getIdActionFromSegment'); + + // entry and exit pages of visit + $segments[] = array( + 'type' => 'dimension', + 'category' => 'Actions_Actions', + 'name' => 'Actions_ColumnEntryPageURL', + 'segment' => 'entryPageUrl', + 'sqlSegment' => 'log_visit.visit_entry_idaction_url', + 'sqlFilter' => $sqlFilter, ); $segments[] = array( - 'type' => 'dimension', - 'category' => 'Actions_Actions', - 'name' => 'Actions_ColumnEntryPageTitle', - 'segment' => 'entryPageTitle', - 'sqlSegment' => 'log_visit.visit_entry_idaction_name', - 'sqlFilter' => $sqlFilter, + 'type' => 'dimension', + 'category' => 'Actions_Actions', + 'name' => 'Actions_ColumnEntryPageTitle', + 'segment' => 'entryPageTitle', + 'sqlSegment' => 'log_visit.visit_entry_idaction_name', + 'sqlFilter' => $sqlFilter, ); $segments[] = array( - 'type' => 'dimension', - 'category' => 'Actions_Actions', - 'name' => 'Actions_ColumnExitPageURL', - 'segment' => 'exitPageUrl', - 'sqlSegment' => 'log_visit.visit_exit_idaction_url', - 'sqlFilter' => $sqlFilter, + 'type' => 'dimension', + 'category' => 'Actions_Actions', + 'name' => 'Actions_ColumnExitPageURL', + 'segment' => 'exitPageUrl', + 'sqlSegment' => 'log_visit.visit_exit_idaction_url', + 'sqlFilter' => $sqlFilter, ); $segments[] = array( - 'type' => 'dimension', - 'category' => 'Actions_Actions', - 'name' => 'Actions_ColumnExitPageTitle', - 'segment' => 'exitPageTitle', - 'sqlSegment' => 'log_visit.visit_exit_idaction_name', - 'sqlFilter' => $sqlFilter, + 'type' => 'dimension', + 'category' => 'Actions_Actions', + 'name' => 'Actions_ColumnExitPageTitle', + 'segment' => 'exitPageTitle', + 'sqlSegment' => 'log_visit.visit_exit_idaction_name', + 'sqlFilter' => $sqlFilter, ); - + // single pages $segments[] = array( - 'type' => 'dimension', - 'category' => 'Actions_Actions', - 'name' => 'Actions_ColumnPageURL', - 'segment' => 'pageUrl', - 'sqlSegment' => 'log_link_visit_action.idaction_url', - 'sqlFilter' => $sqlFilter, - 'acceptedValues' => "All these segments must be URL encoded, for example: ".urlencode('http://example.com/path/page?query'), + 'type' => 'dimension', + 'category' => 'Actions_Actions', + 'name' => 'Actions_ColumnPageURL', + 'segment' => 'pageUrl', + 'sqlSegment' => 'log_link_visit_action.idaction_url', + 'sqlFilter' => $sqlFilter, + 'acceptedValues' => "All these segments must be URL encoded, for example: " . urlencode('http://example.com/path/page?query'), ); - $segments[] = array( - 'type' => 'dimension', - 'category' => 'Actions_Actions', - 'name' => 'Actions_ColumnPageName', - 'segment' => 'pageTitle', - 'sqlSegment' => 'log_link_visit_action.idaction_name', - 'sqlFilter' => $sqlFilter, - ); - // TODO here could add keyword segment and hack $sqlFilter to make it select the right idaction - } - - /** - * Convert segment expression to an action ID or an SQL expression. - * - * This method is used as a sqlFilter-callback for the segments of this plugin. - * Usually, these callbacks only return a value that should be compared to the - * column in the database. In this case, that doesn't work since multiple IDs - * can match an expression (e.g. "pageUrl=@foo"). - * @param string $string - * @param string $sqlField - * @param string $matchType - * @throws Exception - * @return array|int|string - */ - public function getIdActionFromSegment($string, $sqlField, $matchType='==') - { - // Field is visit_*_idaction_url or visit_*_idaction_name - $actionType = strpos($sqlField, '_name') === false - ? Piwik_Tracker_Action::TYPE_ACTION_URL - : Piwik_Tracker_Action::TYPE_ACTION_NAME; - - if ($actionType == Piwik_Tracker_Action::TYPE_ACTION_URL) - { - // for urls trim protocol and www because it is not recorded in the db - $string = preg_replace('@^http[s]?://(www\.)?@i', '', $string); - } - + $segments[] = array( + 'type' => 'dimension', + 'category' => 'Actions_Actions', + 'name' => 'Actions_ColumnPageName', + 'segment' => 'pageTitle', + 'sqlSegment' => 'log_link_visit_action.idaction_name', + 'sqlFilter' => $sqlFilter, + ); + // TODO here could add keyword segment and hack $sqlFilter to make it select the right idaction + } + + /** + * Convert segment expression to an action ID or an SQL expression. + * + * This method is used as a sqlFilter-callback for the segments of this plugin. + * Usually, these callbacks only return a value that should be compared to the + * column in the database. In this case, that doesn't work since multiple IDs + * can match an expression (e.g. "pageUrl=@foo"). + * @param string $string + * @param string $sqlField + * @param string $matchType + * @throws Exception + * @return array|int|string + */ + public function getIdActionFromSegment($string, $sqlField, $matchType = '==') + { + // Field is visit_*_idaction_url or visit_*_idaction_name + $actionType = strpos($sqlField, '_name') === false + ? Piwik_Tracker_Action::TYPE_ACTION_URL + : Piwik_Tracker_Action::TYPE_ACTION_NAME; + + if ($actionType == Piwik_Tracker_Action::TYPE_ACTION_URL) { + // for urls trim protocol and www because it is not recorded in the db + $string = preg_replace('@^http[s]?://(www\.)?@i', '', $string); + } + // exact matches work by returning the id directly - if ($matchType == Piwik_SegmentExpression::MATCH_EQUAL - || $matchType == Piwik_SegmentExpression::MATCH_NOT_EQUAL) - { + if ($matchType == Piwik_SegmentExpression::MATCH_EQUAL + || $matchType == Piwik_SegmentExpression::MATCH_NOT_EQUAL + ) { $sql = Piwik_Tracker_Action::getSqlSelectActionId(); $bind = array($string, $string, $actionType); $idAction = Piwik_FetchOne($sql, $bind); // if the action is not found, we hack -100 to ensure it tries to match against an integer // otherwise binding idaction_name to "false" returns some rows for some reasons (in case &segment=pageTitle==Větrnásssssss) - if(empty($idAction)) - { + if (empty($idAction)) { $idAction = -100; } return $idAction; } - + // now, we handle the cases =@ (contains) and !@ (does not contain) - + // build the expression based on the match type - $sql = 'SELECT idaction FROM '.Piwik_Common::prefixTable('log_action').' WHERE '; - switch ($matchType) - { + $sql = 'SELECT idaction FROM ' . Piwik_Common::prefixTable('log_action') . ' WHERE '; + switch ($matchType) { case '=@': // use concat to make sure, no %s occurs because some plugins use %s in their sql - $sql .= '( name LIKE CONCAT("%", ?, "%") AND type = '.$actionType.' )'; + $sql .= '( name LIKE CONCAT("%", ?, "%") AND type = ' . $actionType . ' )'; break; case '!@': - $sql .= '( name NOT LIKE CONCAT("%", ?, "%") AND type = '.$actionType.' )'; + $sql .= '( name NOT LIKE CONCAT("%", ?, "%") AND type = ' . $actionType . ' )'; break; default: throw new Exception("This match type is not available for action-segments."); break; } - + return array( // mark that the returned value is an sql-expression instead of a literal value - 'SQL' => $sql, - 'bind' => $string + 'SQL' => $sql, + 'bind' => $string ); - } - - /** - * Returns metadata for available reports - * - * @param Piwik_Event_Notification $notification notification object - */ - public function getReportMetadata($notification) - { - $reports = &$notification->getNotificationObject(); - - $reports[] = array( - 'category' => Piwik_Translate('Actions_Actions'), - 'name' => Piwik_Translate('Actions_Actions') . ' - ' . Piwik_Translate('General_MainMetrics'), - 'module' => 'Actions', - 'action' => 'get', - 'metrics' => array( - 'nb_pageviews' => Piwik_Translate('General_ColumnPageviews'), - 'nb_uniq_pageviews' => Piwik_Translate('General_ColumnUniquePageviews'), - 'nb_downloads' => Piwik_Translate('Actions_ColumnDownloads'), - 'nb_uniq_downloads' => Piwik_Translate('Actions_ColumnUniqueDownloads'), - 'nb_outlinks' => Piwik_Translate('Actions_ColumnOutlinks'), - 'nb_uniq_outlinks' => Piwik_Translate('Actions_ColumnUniqueOutlinks'), - 'nb_searches' => Piwik_Translate('Actions_ColumnSearches'), - 'nb_keywords' => Piwik_Translate('Actions_ColumnSiteSearchKeywords'), - ), - 'metricsDocumentation' => array( - 'nb_pageviews' => Piwik_Translate('General_ColumnPageviewsDocumentation'), - 'nb_uniq_pageviews' => Piwik_Translate('General_ColumnUniquePageviewsDocumentation'), - 'nb_downloads' => Piwik_Translate('Actions_ColumnClicksDocumentation'), - 'nb_uniq_downloads' => Piwik_Translate('Actions_ColumnUniqueClicksDocumentation'), - 'nb_outlinks' => Piwik_Translate('Actions_ColumnClicksDocumentation'), - 'nb_uniq_outlinks' => Piwik_Translate('Actions_ColumnUniqueClicksDocumentation'), - 'nb_searches' => Piwik_Translate('Actions_ColumnSearchesDocumentation'), + } + + /** + * Returns metadata for available reports + * + * @param Piwik_Event_Notification $notification notification object + */ + public function getReportMetadata($notification) + { + $reports = & $notification->getNotificationObject(); + + $reports[] = array( + 'category' => Piwik_Translate('Actions_Actions'), + 'name' => Piwik_Translate('Actions_Actions') . ' - ' . Piwik_Translate('General_MainMetrics'), + 'module' => 'Actions', + 'action' => 'get', + 'metrics' => array( + 'nb_pageviews' => Piwik_Translate('General_ColumnPageviews'), + 'nb_uniq_pageviews' => Piwik_Translate('General_ColumnUniquePageviews'), + 'nb_downloads' => Piwik_Translate('Actions_ColumnDownloads'), + 'nb_uniq_downloads' => Piwik_Translate('Actions_ColumnUniqueDownloads'), + 'nb_outlinks' => Piwik_Translate('Actions_ColumnOutlinks'), + 'nb_uniq_outlinks' => Piwik_Translate('Actions_ColumnUniqueOutlinks'), + 'nb_searches' => Piwik_Translate('Actions_ColumnSearches'), + 'nb_keywords' => Piwik_Translate('Actions_ColumnSiteSearchKeywords'), + ), + 'metricsDocumentation' => array( + 'nb_pageviews' => Piwik_Translate('General_ColumnPageviewsDocumentation'), + 'nb_uniq_pageviews' => Piwik_Translate('General_ColumnUniquePageviewsDocumentation'), + 'nb_downloads' => Piwik_Translate('Actions_ColumnClicksDocumentation'), + 'nb_uniq_downloads' => Piwik_Translate('Actions_ColumnUniqueClicksDocumentation'), + 'nb_outlinks' => Piwik_Translate('Actions_ColumnClicksDocumentation'), + 'nb_uniq_outlinks' => Piwik_Translate('Actions_ColumnUniqueClicksDocumentation'), + 'nb_searches' => Piwik_Translate('Actions_ColumnSearchesDocumentation'), // 'nb_keywords' => Piwik_Translate('Actions_ColumnSiteSearchKeywords'), - ), - 'processedMetrics' => false, - 'order' => 1 - ); - - $metrics = array( - 'nb_hits' => Piwik_Translate('General_ColumnPageviews'), - 'nb_visits' => Piwik_Translate('General_ColumnUniquePageviews'), - 'bounce_rate' => Piwik_Translate('General_ColumnBounceRate'), - 'avg_time_on_page' => Piwik_Translate('General_ColumnAverageTimeOnPage'), - 'exit_rate' => Piwik_Translate('General_ColumnExitRate'), - 'avg_time_generation' => Piwik_Translate('General_ColumnAverageGenerationTime') - ); - - $documentation = array( - 'nb_hits' => Piwik_Translate('General_ColumnPageviewsDocumentation'), - 'nb_visits' => Piwik_Translate('General_ColumnUniquePageviewsDocumentation'), - 'bounce_rate' => Piwik_Translate('General_ColumnPageBounceRateDocumentation'), - 'avg_time_on_page' => Piwik_Translate('General_ColumnAverageTimeOnPageDocumentation'), - 'exit_rate' => Piwik_Translate('General_ColumnExitRateDocumentation'), - 'avg_time_generation' => Piwik_Translate('General_ColumnAverageGenerationTimeDocumentation'), - ); - - // pages report - $reports[] = array( - 'category' => Piwik_Translate('Actions_Actions'), - 'name' => Piwik_Translate('Actions_PageUrls'), - 'module' => 'Actions', - 'action' => 'getPageUrls', - 'dimension' => Piwik_Translate('Actions_ColumnPageURL'), - 'metrics' => $metrics, - 'metricsDocumentation' => $documentation, - 'documentation' => Piwik_Translate('Actions_PagesReportDocumentation', '
') - .'
'.Piwik_Translate('General_UsePlusMinusIconsDocumentation'), - 'processedMetrics' => false, - 'actionToLoadSubTables' => 'getPageUrls', - 'order' => 2 - ); - - // entry pages report - $reports[] = array( - 'category' => Piwik_Translate('Actions_Actions'), - 'name' => Piwik_Translate('Actions_SubmenuPagesEntry'), - 'module' => 'Actions', - 'action' => 'getEntryPageUrls', - 'dimension' => Piwik_Translate('Actions_ColumnPageURL'), - 'metrics' => array( - 'entry_nb_visits' => Piwik_Translate('General_ColumnEntrances'), - 'entry_bounce_count' => Piwik_Translate('General_ColumnBounces'), - 'bounce_rate' => Piwik_Translate('General_ColumnBounceRate'), - ), - 'metricsDocumentation' => array( - 'entry_nb_visits' => Piwik_Translate('General_ColumnEntrancesDocumentation'), - 'entry_bounce_count' => Piwik_Translate('General_ColumnBouncesDocumentation'), - 'bounce_rate' => Piwik_Translate('General_ColumnBounceRateForPageDocumentation') - ), - 'documentation' => Piwik_Translate('Actions_EntryPagesReportDocumentation', '
') - .' '.Piwik_Translate('General_UsePlusMinusIconsDocumentation'), - 'processedMetrics' => false, - 'actionToLoadSubTables' => 'getEntryPageUrls', - 'order' => 3 - ); - - // exit pages report - $reports[] = array( - 'category' => Piwik_Translate('Actions_Actions'), - 'name' => Piwik_Translate('Actions_SubmenuPagesExit'), - 'module' => 'Actions', - 'action' => 'getExitPageUrls', - 'dimension' => Piwik_Translate('Actions_ColumnPageURL'), - 'metrics' => array( - 'exit_nb_visits' => Piwik_Translate('General_ColumnExits'), - 'nb_visits' => Piwik_Translate('General_ColumnUniquePageviews'), - 'exit_rate' => Piwik_Translate('General_ColumnExitRate') - ), - 'metricsDocumentation' => array( - 'exit_nb_visits' => Piwik_Translate('General_ColumnExitsDocumentation'), - 'nb_visits' => Piwik_Translate('General_ColumnUniquePageviewsDocumentation'), - 'exit_rate' => Piwik_Translate('General_ColumnExitRateDocumentation') - ), - 'documentation' => Piwik_Translate('Actions_ExitPagesReportDocumentation', '
') - .' '.Piwik_Translate('General_UsePlusMinusIconsDocumentation'), - 'processedMetrics' => false, - 'actionToLoadSubTables' => 'getExitPageUrls', - 'order' => 4 - ); - - // page titles report - $reports[] = array( - 'category' => Piwik_Translate('Actions_Actions'), - 'name' => Piwik_Translate('Actions_SubmenuPageTitles'), - 'module' => 'Actions', - 'action' => 'getPageTitles', - 'dimension' => Piwik_Translate('Actions_ColumnPageName'), - 'metrics' => $metrics, - 'metricsDocumentation' => $documentation, - 'documentation' => Piwik_Translate('Actions_PageTitlesReportDocumentation', array('
', htmlentities(''))), - 'processedMetrics' => false, - 'actionToLoadSubTables' => 'getPageTitles', - 'order' => 5, - - ); - - // entry page titles report - $reports[] = array( - 'category' => Piwik_Translate('Actions_Actions'), - 'name' => Piwik_Translate('Actions_EntryPageTitles'), - 'module' => 'Actions', - 'action' => 'getEntryPageTitles', - 'dimension' => Piwik_Translate('Actions_ColumnPageName'), - 'metrics' => array( - 'entry_nb_visits' => Piwik_Translate('General_ColumnEntrances'), - 'entry_bounce_count' => Piwik_Translate('General_ColumnBounces'), - 'bounce_rate' => Piwik_Translate('General_ColumnBounceRate'), - ), - 'metricsDocumentation' => array( - 'entry_nb_visits' => Piwik_Translate('General_ColumnEntrancesDocumentation'), - 'entry_bounce_count' => Piwik_Translate('General_ColumnBouncesDocumentation'), - 'bounce_rate' => Piwik_Translate('General_ColumnBounceRateForPageDocumentation') - ), - 'documentation' => Piwik_Translate('Actions_ExitPageTitlesReportDocumentation', '<br />') - .' '.Piwik_Translate('General_UsePlusMinusIconsDocumentation'), - 'processedMetrics' => false, - 'actionToLoadSubTables' => 'getEntryPageTitles', - 'order' => 6 - ); - - // exit page titles report - $reports[] = array( - 'category' => Piwik_Translate('Actions_Actions'), - 'name' => Piwik_Translate('Actions_ExitPageTitles'), - 'module' => 'Actions', - 'action' => 'getExitPageTitles', - 'dimension' => Piwik_Translate('Actions_ColumnPageName'), - 'metrics' => array( - 'exit_nb_visits' => Piwik_Translate('General_ColumnExits'), - 'nb_visits' => Piwik_Translate('General_ColumnUniquePageviews'), - 'exit_rate' => Piwik_Translate('General_ColumnExitRate') - ), - 'metricsDocumentation' => array( - 'exit_nb_visits' => Piwik_Translate('General_ColumnExitsDocumentation'), - 'nb_visits' => Piwik_Translate('General_ColumnUniquePageviewsDocumentation'), - 'exit_rate' => Piwik_Translate('General_ColumnExitRateDocumentation') - ), - 'documentation' => Piwik_Translate('Actions_EntryPageTitlesReportDocumentation', '<br />') - .' '.Piwik_Translate('General_UsePlusMinusIconsDocumentation'), - 'processedMetrics' => false, - 'actionToLoadSubTables' => 'getExitPageTitles', - 'order' => 7 - ); - - $documentation = array( - 'nb_visits' => Piwik_Translate('Actions_ColumnUniqueClicksDocumentation'), - 'nb_hits' => Piwik_Translate('Actions_ColumnClicksDocumentation') - ); - - // outlinks report - $reports[] = array( - 'category' => Piwik_Translate('Actions_Actions'), - 'name' => Piwik_Translate('Actions_SubmenuOutlinks'), - 'module' => 'Actions', - 'action' => 'getOutlinks', - 'dimension' => Piwik_Translate('Actions_ColumnClickedURL'), - 'metrics' => array( - 'nb_visits' => Piwik_Translate('Actions_ColumnUniqueClicks'), - 'nb_hits' => Piwik_Translate('Actions_ColumnClicks') - ), - 'metricsDocumentation' => $documentation, - 'documentation' => Piwik_Translate('Actions_OutlinksReportDocumentation').' ' - .Piwik_Translate('Actions_OutlinkDocumentation').'<br />' - .Piwik_Translate('General_UsePlusMinusIconsDocumentation'), - 'processedMetrics' => false, - 'actionToLoadSubTables' => 'getOutlinks', - 'order' => 8, - ); - - // downloads report - $reports[] = array( - 'category' => Piwik_Translate('Actions_Actions'), - 'name' => Piwik_Translate('Actions_SubmenuDownloads'), - 'module' => 'Actions', - 'action' => 'getDownloads', - 'dimension' => Piwik_Translate('Actions_ColumnDownloadURL'), - 'metrics' => array( - 'nb_visits' => Piwik_Translate('Actions_ColumnUniqueDownloads'), - 'nb_hits' => Piwik_Translate('Actions_ColumnDownloads') - ), - 'metricsDocumentation' => $documentation, - 'documentation' => Piwik_Translate('Actions_DownloadsReportDocumentation', '<br />'), - 'processedMetrics' => false, - 'actionToLoadSubTables' => 'getDownloads', - 'order' => 9, - ); - - if($this->isSiteSearchEnabled()) - { - // Search Keywords - $reports[] = array( - 'category' => Piwik_Translate('Actions_SubmenuSitesearch'), - 'name' => Piwik_Translate('Actions_WidgetSearchKeywords'), - 'module' => 'Actions', - 'action' => 'getSiteSearchKeywords', - 'dimension' => Piwik_Translate('Actions_ColumnSearchKeyword'), - 'metrics' => array( - 'nb_visits' => Piwik_Translate('Actions_ColumnSearches'), - 'nb_pages_per_search' => Piwik_Translate('Actions_ColumnPagesPerSearch'), - 'exit_rate' => Piwik_Translate('Actions_ColumnSearchExits'), - ), - 'metricsDocumentation' => array( - 'nb_visits' => Piwik_Translate('Actions_ColumnSearchesDocumentation'), - 'nb_pages_per_search' => Piwik_Translate('Actions_ColumnPagesPerSearchDocumentation'), - 'exit_rate' => Piwik_Translate('Actions_ColumnSearchExitsDocumentation'), - ), - 'documentation' => Piwik_Translate('Actions_SiteSearchKeywordsDocumentation') . '<br/><br/>' . Piwik_Translate('Actions_SiteSearchIntro') . '<br/><br/>' - . '<a href="http://piwik.org/docs/site-search/" target="_blank">'. Piwik_Translate('Actions_LearnMoreAboutSiteSearchLink') . '</a>', - 'processedMetrics' => false, - 'order' => 15 - ); - // No Result Search Keywords - $reports[] = array( - 'category' => Piwik_Translate('Actions_SubmenuSitesearch'), - 'name' => Piwik_Translate('Actions_WidgetSearchNoResultKeywords'), - 'module' => 'Actions', - 'action' => 'getSiteSearchNoResultKeywords', - 'dimension' => Piwik_Translate('Actions_ColumnNoResultKeyword'), - 'metrics' => array( - 'nb_visits' => Piwik_Translate('Actions_ColumnSearches'), - 'exit_rate' => Piwik_Translate('Actions_ColumnSearchExits'), - ), - 'metricsDocumentation' => array( - 'nb_visits' => Piwik_Translate('Actions_ColumnSearchesDocumentation'), - 'exit_rate' => Piwik_Translate('Actions_ColumnSearchExitsDocumentation'), - ), - 'documentation' => Piwik_Translate('Actions_SiteSearchIntro'). '<br /><br />' . Piwik_Translate('Actions_SiteSearchKeywordsNoResultDocumentation'), - 'processedMetrics' => false, - 'order' => 16 - ); - - if(self::isCustomVariablesPluginsEnabled()) { - // Search Categories - $reports[] = array( - 'category' => Piwik_Translate('Actions_SubmenuSitesearch'), - 'name' => Piwik_Translate('Actions_WidgetSearchCategories'), - 'module' => 'Actions', - 'action' => 'getSiteSearchCategories', - 'dimension' => Piwik_Translate('Actions_ColumnSearchCategory'), - 'metrics' => array( - 'nb_visits' => Piwik_Translate('Actions_ColumnSearches'), - 'nb_pages_per_search' => Piwik_Translate('Actions_ColumnPagesPerSearch'), - 'exit_rate' => Piwik_Translate('Actions_ColumnSearchExits'), - ), - 'metricsDocumentation' => array( - 'nb_visits' => Piwik_Translate('Actions_ColumnSearchesDocumentation'), - 'nb_pages_per_search' => Piwik_Translate('Actions_ColumnPagesPerSearchDocumentation'), - 'exit_rate' => Piwik_Translate('Actions_ColumnSearchExitsDocumentation'), - ), - 'documentation' => Piwik_Translate('Actions_SiteSearchCategories1') . '<br/>' . Piwik_Translate('Actions_SiteSearchCategories2'), - 'processedMetrics' => false, - 'order' => 17 - ); - } - - $documentation = Piwik_Translate('Actions_SiteSearchFollowingPagesDoc') .'<br/>'.Piwik_Translate('General_UsePlusMinusIconsDocumentation'); - // Pages URLs following Search - $reports[] = array( - 'category' => Piwik_Translate('Actions_SubmenuSitesearch'), - 'name' => Piwik_Translate('Actions_WidgetPageUrlsFollowingSearch'), - 'module' => 'Actions', - 'action' => 'getPageUrlsFollowingSiteSearch', - 'dimension' => Piwik_Translate('General_ColumnDestinationPage'), - 'metrics' => array( - 'nb_hits_following_search' => Piwik_Translate('General_ColumnViewedAfterSearch'), - 'nb_hits' => Piwik_Translate('General_ColumnTotalPageviews'), - ), - 'metricsDocumentation' => array( - 'nb_hits_following_search' => Piwik_Translate('General_ColumnViewedAfterSearchDocumentation'), - 'nb_hits' => Piwik_Translate('General_ColumnPageviewsDocumentation'), - ), - 'documentation' => $documentation, - 'processedMetrics' => false, - 'order' => 18 - ); - // Pages Titles following Search - $reports[] = array( - 'category' => Piwik_Translate('Actions_SubmenuSitesearch'), - 'name' => Piwik_Translate('Actions_WidgetPageTitlesFollowingSearch'), - 'module' => 'Actions', - 'action' => 'getPageTitlesFollowingSiteSearch', - 'dimension' => Piwik_Translate('General_ColumnDestinationPage'), - 'metrics' => array( - 'nb_hits_following_search' => Piwik_Translate('General_ColumnViewedAfterSearch'), - 'nb_hits' => Piwik_Translate('General_ColumnTotalPageviews'), - ), - 'metricsDocumentation' => array( - 'nb_hits_following_search' => Piwik_Translate('General_ColumnViewedAfterSearchDocumentation'), - 'nb_hits' => Piwik_Translate('General_ColumnPageviewsDocumentation'), - ), - 'documentation' => $documentation, - 'processedMetrics' => false, - 'order' => 19 - ); - } - } - - function addWidgets() - { - Piwik_AddWidget( 'Actions_Actions', 'Actions_SubmenuPages', 'Actions', 'getPageUrls'); - Piwik_AddWidget( 'Actions_Actions', 'Actions_WidgetPageTitles', 'Actions', 'getPageTitles'); - Piwik_AddWidget( 'Actions_Actions', 'Actions_SubmenuOutlinks', 'Actions', 'getOutlinks'); - Piwik_AddWidget( 'Actions_Actions', 'Actions_SubmenuDownloads', 'Actions', 'getDownloads'); - Piwik_AddWidget( 'Actions_Actions', 'Actions_WidgetPagesEntry', 'Actions', 'getEntryPageUrls'); - Piwik_AddWidget( 'Actions_Actions', 'Actions_WidgetPagesExit', 'Actions', 'getExitPageUrls'); - Piwik_AddWidget( 'Actions_Actions', 'Actions_WidgetEntryPageTitles', 'Actions', 'getEntryPageTitles' ); - Piwik_AddWidget( 'Actions_Actions', 'Actions_WidgetExitPageTitles', 'Actions', 'getExitPageTitles' ); - - if($this->isSiteSearchEnabled()) - { - Piwik_AddWidget( 'Actions_SubmenuSitesearch', 'Actions_WidgetSearchKeywords', 'Actions', 'getSiteSearchKeywords'); - - if(self::isCustomVariablesPluginsEnabled()) { - Piwik_AddWidget( 'Actions_SubmenuSitesearch', 'Actions_WidgetSearchCategories', 'Actions', 'getSiteSearchCategories'); - } - Piwik_AddWidget( 'Actions_SubmenuSitesearch', 'Actions_WidgetSearchNoResultKeywords', 'Actions', 'getSiteSearchNoResultKeywords'); - Piwik_AddWidget( 'Actions_SubmenuSitesearch', 'Actions_WidgetPageUrlsFollowingSearch', 'Actions', 'getPageUrlsFollowingSiteSearch'); - Piwik_AddWidget( 'Actions_SubmenuSitesearch', 'Actions_WidgetPageTitlesFollowingSearch', 'Actions', 'getPageTitlesFollowingSiteSearch'); - } - } - - function addMenus() - { - Piwik_AddMenu('Actions_Actions', '', array('module' => 'Actions', 'action' => 'indexPageUrls'), true, 15); - Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuPages', array('module' => 'Actions', 'action' => 'indexPageUrls'), true, 1); - Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuPagesEntry', array('module' => 'Actions', 'action' => 'indexEntryPageUrls'), true, 2); - Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuPagesExit', array('module' => 'Actions', 'action' => 'indexExitPageUrls'), true, 3); - Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuPageTitles', array('module' => 'Actions', 'action' => 'indexPageTitles'), true, 4); - Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuOutlinks', array('module' => 'Actions', 'action' => 'indexOutlinks'), true, 6); - Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuDownloads', array('module' => 'Actions', 'action' => 'indexDownloads'), true, 7); - - if($this->isSiteSearchEnabled()) - { - Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuSitesearch', array('module' => 'Actions', 'action' => 'indexSiteSearch'), true, 5); - } - } - - protected function isSiteSearchEnabled() - { - $idSite = Piwik_Common::getRequestVar('idSite', 0, 'int'); - if($idSite == 0 ) { - return false; - } - return Piwik_Site::isSiteSearchEnabledFor($idSite); - } - - - /** - * @param Piwik_Event_Notification $notification notification object - * @return mixed - */ - function archivePeriod( $notification ) - { - $archiveProcessing = $notification->getNotificationObject(); - - if(!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; - - $actionsArchiving = new Piwik_Actions_Archiving($archiveProcessing->idsite); - return $actionsArchiving->archivePeriod($archiveProcessing); - } - - /** - * Compute all the actions along with their hierarchies. - * - * For each action we process the "interest statistics" : - * visits, unique visitors, bounce count, sum visit length. - * - * @param Piwik_Event_Notification $notification notification object - */ - public function archiveDay( $notification ) - { - /* @var $archiveProcessing Piwik_ArchiveProcessing_Day */ - $archiveProcessing = $notification->getNotificationObject(); - - if(!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; - - $actionsArchiving = new Piwik_Actions_Archiving($archiveProcessing->idsite); - return $actionsArchiving->archiveDay($archiveProcessing); - } - - static public function checkCustomVariablesPluginEnabled() - { - if(!self::isCustomVariablesPluginsEnabled()) - { - throw new Exception("To Track Site Search Categories, please ask the Piwik Administrator to enable the 'Custom Variables' plugin in Settings > Plugins."); - } - } - - static protected function isCustomVariablesPluginsEnabled() - { - return Piwik_PluginsManager::getInstance()->isPluginActivated('CustomVariables'); - } + ), + 'processedMetrics' => false, + 'order' => 1 + ); + + $metrics = array( + 'nb_hits' => Piwik_Translate('General_ColumnPageviews'), + 'nb_visits' => Piwik_Translate('General_ColumnUniquePageviews'), + 'bounce_rate' => Piwik_Translate('General_ColumnBounceRate'), + 'avg_time_on_page' => Piwik_Translate('General_ColumnAverageTimeOnPage'), + 'exit_rate' => Piwik_Translate('General_ColumnExitRate'), + 'avg_time_generation' => Piwik_Translate('General_ColumnAverageGenerationTime') + ); + + $documentation = array( + 'nb_hits' => Piwik_Translate('General_ColumnPageviewsDocumentation'), + 'nb_visits' => Piwik_Translate('General_ColumnUniquePageviewsDocumentation'), + 'bounce_rate' => Piwik_Translate('General_ColumnPageBounceRateDocumentation'), + 'avg_time_on_page' => Piwik_Translate('General_ColumnAverageTimeOnPageDocumentation'), + 'exit_rate' => Piwik_Translate('General_ColumnExitRateDocumentation'), + 'avg_time_generation' => Piwik_Translate('General_ColumnAverageGenerationTimeDocumentation'), + ); + + // pages report + $reports[] = array( + 'category' => Piwik_Translate('Actions_Actions'), + 'name' => Piwik_Translate('Actions_PageUrls'), + 'module' => 'Actions', + 'action' => 'getPageUrls', + 'dimension' => Piwik_Translate('Actions_ColumnPageURL'), + 'metrics' => $metrics, + 'metricsDocumentation' => $documentation, + 'documentation' => Piwik_Translate('Actions_PagesReportDocumentation', '<br />') + . '<br />' . Piwik_Translate('General_UsePlusMinusIconsDocumentation'), + 'processedMetrics' => false, + 'actionToLoadSubTables' => 'getPageUrls', + 'order' => 2 + ); + + // entry pages report + $reports[] = array( + 'category' => Piwik_Translate('Actions_Actions'), + 'name' => Piwik_Translate('Actions_SubmenuPagesEntry'), + 'module' => 'Actions', + 'action' => 'getEntryPageUrls', + 'dimension' => Piwik_Translate('Actions_ColumnPageURL'), + 'metrics' => array( + 'entry_nb_visits' => Piwik_Translate('General_ColumnEntrances'), + 'entry_bounce_count' => Piwik_Translate('General_ColumnBounces'), + 'bounce_rate' => Piwik_Translate('General_ColumnBounceRate'), + ), + 'metricsDocumentation' => array( + 'entry_nb_visits' => Piwik_Translate('General_ColumnEntrancesDocumentation'), + 'entry_bounce_count' => Piwik_Translate('General_ColumnBouncesDocumentation'), + 'bounce_rate' => Piwik_Translate('General_ColumnBounceRateForPageDocumentation') + ), + 'documentation' => Piwik_Translate('Actions_EntryPagesReportDocumentation', '<br />') + . ' ' . Piwik_Translate('General_UsePlusMinusIconsDocumentation'), + 'processedMetrics' => false, + 'actionToLoadSubTables' => 'getEntryPageUrls', + 'order' => 3 + ); + + // exit pages report + $reports[] = array( + 'category' => Piwik_Translate('Actions_Actions'), + 'name' => Piwik_Translate('Actions_SubmenuPagesExit'), + 'module' => 'Actions', + 'action' => 'getExitPageUrls', + 'dimension' => Piwik_Translate('Actions_ColumnPageURL'), + 'metrics' => array( + 'exit_nb_visits' => Piwik_Translate('General_ColumnExits'), + 'nb_visits' => Piwik_Translate('General_ColumnUniquePageviews'), + 'exit_rate' => Piwik_Translate('General_ColumnExitRate') + ), + 'metricsDocumentation' => array( + 'exit_nb_visits' => Piwik_Translate('General_ColumnExitsDocumentation'), + 'nb_visits' => Piwik_Translate('General_ColumnUniquePageviewsDocumentation'), + 'exit_rate' => Piwik_Translate('General_ColumnExitRateDocumentation') + ), + 'documentation' => Piwik_Translate('Actions_ExitPagesReportDocumentation', '<br />') + . ' ' . Piwik_Translate('General_UsePlusMinusIconsDocumentation'), + 'processedMetrics' => false, + 'actionToLoadSubTables' => 'getExitPageUrls', + 'order' => 4 + ); + + // page titles report + $reports[] = array( + 'category' => Piwik_Translate('Actions_Actions'), + 'name' => Piwik_Translate('Actions_SubmenuPageTitles'), + 'module' => 'Actions', + 'action' => 'getPageTitles', + 'dimension' => Piwik_Translate('Actions_ColumnPageName'), + 'metrics' => $metrics, + 'metricsDocumentation' => $documentation, + 'documentation' => Piwik_Translate('Actions_PageTitlesReportDocumentation', array('<br />', htmlentities('<title>'))), + 'processedMetrics' => false, + 'actionToLoadSubTables' => 'getPageTitles', + 'order' => 5, + + ); + + // entry page titles report + $reports[] = array( + 'category' => Piwik_Translate('Actions_Actions'), + 'name' => Piwik_Translate('Actions_EntryPageTitles'), + 'module' => 'Actions', + 'action' => 'getEntryPageTitles', + 'dimension' => Piwik_Translate('Actions_ColumnPageName'), + 'metrics' => array( + 'entry_nb_visits' => Piwik_Translate('General_ColumnEntrances'), + 'entry_bounce_count' => Piwik_Translate('General_ColumnBounces'), + 'bounce_rate' => Piwik_Translate('General_ColumnBounceRate'), + ), + 'metricsDocumentation' => array( + 'entry_nb_visits' => Piwik_Translate('General_ColumnEntrancesDocumentation'), + 'entry_bounce_count' => Piwik_Translate('General_ColumnBouncesDocumentation'), + 'bounce_rate' => Piwik_Translate('General_ColumnBounceRateForPageDocumentation') + ), + 'documentation' => Piwik_Translate('Actions_ExitPageTitlesReportDocumentation', '<br />') + . ' ' . Piwik_Translate('General_UsePlusMinusIconsDocumentation'), + 'processedMetrics' => false, + 'actionToLoadSubTables' => 'getEntryPageTitles', + 'order' => 6 + ); + + // exit page titles report + $reports[] = array( + 'category' => Piwik_Translate('Actions_Actions'), + 'name' => Piwik_Translate('Actions_ExitPageTitles'), + 'module' => 'Actions', + 'action' => 'getExitPageTitles', + 'dimension' => Piwik_Translate('Actions_ColumnPageName'), + 'metrics' => array( + 'exit_nb_visits' => Piwik_Translate('General_ColumnExits'), + 'nb_visits' => Piwik_Translate('General_ColumnUniquePageviews'), + 'exit_rate' => Piwik_Translate('General_ColumnExitRate') + ), + 'metricsDocumentation' => array( + 'exit_nb_visits' => Piwik_Translate('General_ColumnExitsDocumentation'), + 'nb_visits' => Piwik_Translate('General_ColumnUniquePageviewsDocumentation'), + 'exit_rate' => Piwik_Translate('General_ColumnExitRateDocumentation') + ), + 'documentation' => Piwik_Translate('Actions_EntryPageTitlesReportDocumentation', '<br />') + . ' ' . Piwik_Translate('General_UsePlusMinusIconsDocumentation'), + 'processedMetrics' => false, + 'actionToLoadSubTables' => 'getExitPageTitles', + 'order' => 7 + ); + + $documentation = array( + 'nb_visits' => Piwik_Translate('Actions_ColumnUniqueClicksDocumentation'), + 'nb_hits' => Piwik_Translate('Actions_ColumnClicksDocumentation') + ); + + // outlinks report + $reports[] = array( + 'category' => Piwik_Translate('Actions_Actions'), + 'name' => Piwik_Translate('Actions_SubmenuOutlinks'), + 'module' => 'Actions', + 'action' => 'getOutlinks', + 'dimension' => Piwik_Translate('Actions_ColumnClickedURL'), + 'metrics' => array( + 'nb_visits' => Piwik_Translate('Actions_ColumnUniqueClicks'), + 'nb_hits' => Piwik_Translate('Actions_ColumnClicks') + ), + 'metricsDocumentation' => $documentation, + 'documentation' => Piwik_Translate('Actions_OutlinksReportDocumentation') . ' ' + . Piwik_Translate('Actions_OutlinkDocumentation') . '<br />' + . Piwik_Translate('General_UsePlusMinusIconsDocumentation'), + 'processedMetrics' => false, + 'actionToLoadSubTables' => 'getOutlinks', + 'order' => 8, + ); + + // downloads report + $reports[] = array( + 'category' => Piwik_Translate('Actions_Actions'), + 'name' => Piwik_Translate('Actions_SubmenuDownloads'), + 'module' => 'Actions', + 'action' => 'getDownloads', + 'dimension' => Piwik_Translate('Actions_ColumnDownloadURL'), + 'metrics' => array( + 'nb_visits' => Piwik_Translate('Actions_ColumnUniqueDownloads'), + 'nb_hits' => Piwik_Translate('Actions_ColumnDownloads') + ), + 'metricsDocumentation' => $documentation, + 'documentation' => Piwik_Translate('Actions_DownloadsReportDocumentation', '<br />'), + 'processedMetrics' => false, + 'actionToLoadSubTables' => 'getDownloads', + 'order' => 9, + ); + + if ($this->isSiteSearchEnabled()) { + // Search Keywords + $reports[] = array( + 'category' => Piwik_Translate('Actions_SubmenuSitesearch'), + 'name' => Piwik_Translate('Actions_WidgetSearchKeywords'), + 'module' => 'Actions', + 'action' => 'getSiteSearchKeywords', + 'dimension' => Piwik_Translate('Actions_ColumnSearchKeyword'), + 'metrics' => array( + 'nb_visits' => Piwik_Translate('Actions_ColumnSearches'), + 'nb_pages_per_search' => Piwik_Translate('Actions_ColumnPagesPerSearch'), + 'exit_rate' => Piwik_Translate('Actions_ColumnSearchExits'), + ), + 'metricsDocumentation' => array( + 'nb_visits' => Piwik_Translate('Actions_ColumnSearchesDocumentation'), + 'nb_pages_per_search' => Piwik_Translate('Actions_ColumnPagesPerSearchDocumentation'), + 'exit_rate' => Piwik_Translate('Actions_ColumnSearchExitsDocumentation'), + ), + 'documentation' => Piwik_Translate('Actions_SiteSearchKeywordsDocumentation') . '<br/><br/>' . Piwik_Translate('Actions_SiteSearchIntro') . '<br/><br/>' + . '<a href="http://piwik.org/docs/site-search/" target="_blank">' . Piwik_Translate('Actions_LearnMoreAboutSiteSearchLink') . '</a>', + 'processedMetrics' => false, + 'order' => 15 + ); + // No Result Search Keywords + $reports[] = array( + 'category' => Piwik_Translate('Actions_SubmenuSitesearch'), + 'name' => Piwik_Translate('Actions_WidgetSearchNoResultKeywords'), + 'module' => 'Actions', + 'action' => 'getSiteSearchNoResultKeywords', + 'dimension' => Piwik_Translate('Actions_ColumnNoResultKeyword'), + 'metrics' => array( + 'nb_visits' => Piwik_Translate('Actions_ColumnSearches'), + 'exit_rate' => Piwik_Translate('Actions_ColumnSearchExits'), + ), + 'metricsDocumentation' => array( + 'nb_visits' => Piwik_Translate('Actions_ColumnSearchesDocumentation'), + 'exit_rate' => Piwik_Translate('Actions_ColumnSearchExitsDocumentation'), + ), + 'documentation' => Piwik_Translate('Actions_SiteSearchIntro') . '<br /><br />' . Piwik_Translate('Actions_SiteSearchKeywordsNoResultDocumentation'), + 'processedMetrics' => false, + 'order' => 16 + ); + + if (self::isCustomVariablesPluginsEnabled()) { + // Search Categories + $reports[] = array( + 'category' => Piwik_Translate('Actions_SubmenuSitesearch'), + 'name' => Piwik_Translate('Actions_WidgetSearchCategories'), + 'module' => 'Actions', + 'action' => 'getSiteSearchCategories', + 'dimension' => Piwik_Translate('Actions_ColumnSearchCategory'), + 'metrics' => array( + 'nb_visits' => Piwik_Translate('Actions_ColumnSearches'), + 'nb_pages_per_search' => Piwik_Translate('Actions_ColumnPagesPerSearch'), + 'exit_rate' => Piwik_Translate('Actions_ColumnSearchExits'), + ), + 'metricsDocumentation' => array( + 'nb_visits' => Piwik_Translate('Actions_ColumnSearchesDocumentation'), + 'nb_pages_per_search' => Piwik_Translate('Actions_ColumnPagesPerSearchDocumentation'), + 'exit_rate' => Piwik_Translate('Actions_ColumnSearchExitsDocumentation'), + ), + 'documentation' => Piwik_Translate('Actions_SiteSearchCategories1') . '<br/>' . Piwik_Translate('Actions_SiteSearchCategories2'), + 'processedMetrics' => false, + 'order' => 17 + ); + } + + $documentation = Piwik_Translate('Actions_SiteSearchFollowingPagesDoc') . '<br/>' . Piwik_Translate('General_UsePlusMinusIconsDocumentation'); + // Pages URLs following Search + $reports[] = array( + 'category' => Piwik_Translate('Actions_SubmenuSitesearch'), + 'name' => Piwik_Translate('Actions_WidgetPageUrlsFollowingSearch'), + 'module' => 'Actions', + 'action' => 'getPageUrlsFollowingSiteSearch', + 'dimension' => Piwik_Translate('General_ColumnDestinationPage'), + 'metrics' => array( + 'nb_hits_following_search' => Piwik_Translate('General_ColumnViewedAfterSearch'), + 'nb_hits' => Piwik_Translate('General_ColumnTotalPageviews'), + ), + 'metricsDocumentation' => array( + 'nb_hits_following_search' => Piwik_Translate('General_ColumnViewedAfterSearchDocumentation'), + 'nb_hits' => Piwik_Translate('General_ColumnPageviewsDocumentation'), + ), + 'documentation' => $documentation, + 'processedMetrics' => false, + 'order' => 18 + ); + // Pages Titles following Search + $reports[] = array( + 'category' => Piwik_Translate('Actions_SubmenuSitesearch'), + 'name' => Piwik_Translate('Actions_WidgetPageTitlesFollowingSearch'), + 'module' => 'Actions', + 'action' => 'getPageTitlesFollowingSiteSearch', + 'dimension' => Piwik_Translate('General_ColumnDestinationPage'), + 'metrics' => array( + 'nb_hits_following_search' => Piwik_Translate('General_ColumnViewedAfterSearch'), + 'nb_hits' => Piwik_Translate('General_ColumnTotalPageviews'), + ), + 'metricsDocumentation' => array( + 'nb_hits_following_search' => Piwik_Translate('General_ColumnViewedAfterSearchDocumentation'), + 'nb_hits' => Piwik_Translate('General_ColumnPageviewsDocumentation'), + ), + 'documentation' => $documentation, + 'processedMetrics' => false, + 'order' => 19 + ); + } + } + + function addWidgets() + { + Piwik_AddWidget('Actions_Actions', 'Actions_SubmenuPages', 'Actions', 'getPageUrls'); + Piwik_AddWidget('Actions_Actions', 'Actions_WidgetPageTitles', 'Actions', 'getPageTitles'); + Piwik_AddWidget('Actions_Actions', 'Actions_SubmenuOutlinks', 'Actions', 'getOutlinks'); + Piwik_AddWidget('Actions_Actions', 'Actions_SubmenuDownloads', 'Actions', 'getDownloads'); + Piwik_AddWidget('Actions_Actions', 'Actions_WidgetPagesEntry', 'Actions', 'getEntryPageUrls'); + Piwik_AddWidget('Actions_Actions', 'Actions_WidgetPagesExit', 'Actions', 'getExitPageUrls'); + Piwik_AddWidget('Actions_Actions', 'Actions_WidgetEntryPageTitles', 'Actions', 'getEntryPageTitles'); + Piwik_AddWidget('Actions_Actions', 'Actions_WidgetExitPageTitles', 'Actions', 'getExitPageTitles'); + + if ($this->isSiteSearchEnabled()) { + Piwik_AddWidget('Actions_SubmenuSitesearch', 'Actions_WidgetSearchKeywords', 'Actions', 'getSiteSearchKeywords'); + + if (self::isCustomVariablesPluginsEnabled()) { + Piwik_AddWidget('Actions_SubmenuSitesearch', 'Actions_WidgetSearchCategories', 'Actions', 'getSiteSearchCategories'); + } + Piwik_AddWidget('Actions_SubmenuSitesearch', 'Actions_WidgetSearchNoResultKeywords', 'Actions', 'getSiteSearchNoResultKeywords'); + Piwik_AddWidget('Actions_SubmenuSitesearch', 'Actions_WidgetPageUrlsFollowingSearch', 'Actions', 'getPageUrlsFollowingSiteSearch'); + Piwik_AddWidget('Actions_SubmenuSitesearch', 'Actions_WidgetPageTitlesFollowingSearch', 'Actions', 'getPageTitlesFollowingSiteSearch'); + } + } + + function addMenus() + { + Piwik_AddMenu('Actions_Actions', '', array('module' => 'Actions', 'action' => 'indexPageUrls'), true, 15); + Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuPages', array('module' => 'Actions', 'action' => 'indexPageUrls'), true, 1); + Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuPagesEntry', array('module' => 'Actions', 'action' => 'indexEntryPageUrls'), true, 2); + Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuPagesExit', array('module' => 'Actions', 'action' => 'indexExitPageUrls'), true, 3); + Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuPageTitles', array('module' => 'Actions', 'action' => 'indexPageTitles'), true, 4); + Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuOutlinks', array('module' => 'Actions', 'action' => 'indexOutlinks'), true, 6); + Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuDownloads', array('module' => 'Actions', 'action' => 'indexDownloads'), true, 7); + + if ($this->isSiteSearchEnabled()) { + Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuSitesearch', array('module' => 'Actions', 'action' => 'indexSiteSearch'), true, 5); + } + } + + protected function isSiteSearchEnabled() + { + $idSite = Piwik_Common::getRequestVar('idSite', 0, 'int'); + if ($idSite == 0) { + return false; + } + return Piwik_Site::isSiteSearchEnabledFor($idSite); + } + + + /** + * @param Piwik_Event_Notification $notification notification object + * @return mixed + */ + function archivePeriod($notification) + { + $archiveProcessing = $notification->getNotificationObject(); + + if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; + + $actionsArchiving = new Piwik_Actions_Archiving($archiveProcessing->idsite); + return $actionsArchiving->archivePeriod($archiveProcessing); + } + + /** + * Compute all the actions along with their hierarchies. + * + * For each action we process the "interest statistics" : + * visits, unique visitors, bounce count, sum visit length. + * + * @param Piwik_Event_Notification $notification notification object + */ + public function archiveDay($notification) + { + /* @var $archiveProcessing Piwik_ArchiveProcessing_Day */ + $archiveProcessing = $notification->getNotificationObject(); + + if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; + + $actionsArchiving = new Piwik_Actions_Archiving($archiveProcessing->idsite); + return $actionsArchiving->archiveDay($archiveProcessing); + } + + static public function checkCustomVariablesPluginEnabled() + { + if (!self::isCustomVariablesPluginsEnabled()) { + throw new Exception("To Track Site Search Categories, please ask the Piwik Administrator to enable the 'Custom Variables' plugin in Settings > Plugins."); + } + } + + static protected function isCustomVariablesPluginsEnabled() + { + return Piwik_PluginsManager::getInstance()->isPluginActivated('CustomVariables'); + } } diff --git a/plugins/Actions/Archiving.php b/plugins/Actions/Archiving.php index c0d36d1083..db71faf788 100644 --- a/plugins/Actions/Archiving.php +++ b/plugins/Actions/Archiving.php @@ -16,100 +16,100 @@ */ class Piwik_Actions_Archiving { - protected $actionsTablesByType = null; - - public static $actionTypes = array( - Piwik_Tracker_Action::TYPE_ACTION_URL, - Piwik_Tracker_Action::TYPE_OUTLINK, - Piwik_Tracker_Action::TYPE_DOWNLOAD, - Piwik_Tracker_Action::TYPE_ACTION_NAME, - Piwik_Tracker_Action::TYPE_SITE_SEARCH, - ); - - static protected $invalidSummedColumnNameToRenamedNameFromPeriodArchive = array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS => Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS, - Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS => Piwik_Archive::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS, - Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS => Piwik_Archive::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS, - ); - - static protected $invalidSummedColumnNameToDeleteFromDayArchive = array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS, - Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS, - Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS, - ); - - protected $isSiteSearchEnabled = false; - - function __construct($idSite) - { - $this->isSiteSearchEnabled = Piwik_Site::isSiteSearchEnabledFor($idSite); - } - - /** - * Archives Actions reports for a Period - * @param Piwik_ArchiveProcessing $archiveProcessing - * @return bool - */ - public function archivePeriod(Piwik_ArchiveProcessing $archiveProcessing) - { - Piwik_Actions_ArchivingHelper::reloadConfig(); - $dataTableToSum = array( - 'Actions_actions', - 'Actions_downloads', - 'Actions_outlink', - 'Actions_actions_url', - 'Actions_sitesearch', - ); - $nameToCount = $archiveProcessing->archiveDataTable($dataTableToSum, - self::$invalidSummedColumnNameToRenamedNameFromPeriodArchive, - Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, - Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, - Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation); - - $archiveProcessing->archiveNumericValuesSum(array( - 'Actions_nb_pageviews', - 'Actions_nb_uniq_pageviews', - 'Actions_nb_downloads', - 'Actions_nb_uniq_downloads', - 'Actions_nb_outlinks', - 'Actions_nb_uniq_outlinks', - 'Actions_nb_searches', - )); - - // Unique Keywords can't be summed, instead we take the RowsCount() of the keyword table - $archiveProcessing->insertNumericRecord('Actions_nb_keywords', $nameToCount['Actions_sitesearch']['level0']); - return true; - } - - /** - * Archives Actions reports for a Day - * - * @param Piwik_ArchiveProcessing $archiveProcessing - * @return bool - */ - public function archiveDay(Piwik_ArchiveProcessing $archiveProcessing) - { - $rankingQueryLimit = self::getRankingQueryLimit(); - Piwik_Actions_ArchivingHelper::reloadConfig(); - - $this->initActionsTables(); - $this->archiveDayActions($archiveProcessing, $rankingQueryLimit); - $this->archiveDayEntryActions($archiveProcessing, $rankingQueryLimit); - $this->archiveDayExitActions($archiveProcessing, $rankingQueryLimit); - $this->archiveDayActionsTime($archiveProcessing, $rankingQueryLimit); - - // Record the final datasets - $this->archiveDayRecordInDatabase($archiveProcessing); - - return true; - } - - /* - * Page URLs and Page names, general stats - */ - protected function archiveDayActions($archiveProcessing, $rankingQueryLimit) - { - $select = "log_action.name, + protected $actionsTablesByType = null; + + public static $actionTypes = array( + Piwik_Tracker_Action::TYPE_ACTION_URL, + Piwik_Tracker_Action::TYPE_OUTLINK, + Piwik_Tracker_Action::TYPE_DOWNLOAD, + Piwik_Tracker_Action::TYPE_ACTION_NAME, + Piwik_Tracker_Action::TYPE_SITE_SEARCH, + ); + + static protected $invalidSummedColumnNameToRenamedNameFromPeriodArchive = array( + Piwik_Archive::INDEX_NB_UNIQ_VISITORS => Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS, + Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS => Piwik_Archive::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS, + Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS => Piwik_Archive::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS, + ); + + static protected $invalidSummedColumnNameToDeleteFromDayArchive = array( + Piwik_Archive::INDEX_NB_UNIQ_VISITORS, + Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS, + Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS, + ); + + protected $isSiteSearchEnabled = false; + + function __construct($idSite) + { + $this->isSiteSearchEnabled = Piwik_Site::isSiteSearchEnabledFor($idSite); + } + + /** + * Archives Actions reports for a Period + * @param Piwik_ArchiveProcessing $archiveProcessing + * @return bool + */ + public function archivePeriod(Piwik_ArchiveProcessing $archiveProcessing) + { + Piwik_Actions_ArchivingHelper::reloadConfig(); + $dataTableToSum = array( + 'Actions_actions', + 'Actions_downloads', + 'Actions_outlink', + 'Actions_actions_url', + 'Actions_sitesearch', + ); + $nameToCount = $archiveProcessing->archiveDataTable($dataTableToSum, + self::$invalidSummedColumnNameToRenamedNameFromPeriodArchive, + Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, + Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, + Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation); + + $archiveProcessing->archiveNumericValuesSum(array( + 'Actions_nb_pageviews', + 'Actions_nb_uniq_pageviews', + 'Actions_nb_downloads', + 'Actions_nb_uniq_downloads', + 'Actions_nb_outlinks', + 'Actions_nb_uniq_outlinks', + 'Actions_nb_searches', + )); + + // Unique Keywords can't be summed, instead we take the RowsCount() of the keyword table + $archiveProcessing->insertNumericRecord('Actions_nb_keywords', $nameToCount['Actions_sitesearch']['level0']); + return true; + } + + /** + * Archives Actions reports for a Day + * + * @param Piwik_ArchiveProcessing $archiveProcessing + * @return bool + */ + public function archiveDay(Piwik_ArchiveProcessing $archiveProcessing) + { + $rankingQueryLimit = self::getRankingQueryLimit(); + Piwik_Actions_ArchivingHelper::reloadConfig(); + + $this->initActionsTables(); + $this->archiveDayActions($archiveProcessing, $rankingQueryLimit); + $this->archiveDayEntryActions($archiveProcessing, $rankingQueryLimit); + $this->archiveDayExitActions($archiveProcessing, $rankingQueryLimit); + $this->archiveDayActionsTime($archiveProcessing, $rankingQueryLimit); + + // Record the final datasets + $this->archiveDayRecordInDatabase($archiveProcessing); + + return true; + } + + /* + * Page URLs and Page names, general stats + */ + protected function archiveDayActions($archiveProcessing, $rankingQueryLimit) + { + $select = "log_action.name, log_action.type, log_action.idaction, log_action.url_prefix, @@ -117,432 +117,422 @@ class Piwik_Actions_Archiving count(distinct log_link_visit_action.idvisitor) as `" . Piwik_Archive::INDEX_NB_UNIQ_VISITORS . "`, count(*) as `" . Piwik_Archive::INDEX_PAGE_NB_HITS . "`, sum( - case when " . Piwik_Tracker_Action::DB_COLUMN_TIME_GENERATION ." is null + case when " . Piwik_Tracker_Action::DB_COLUMN_TIME_GENERATION . " is null then 0 - else " . Piwik_Tracker_Action::DB_COLUMN_TIME_GENERATION ." + else " . Piwik_Tracker_Action::DB_COLUMN_TIME_GENERATION . " end ) / 1000 as `" . Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION . "`, sum( - case when " . Piwik_Tracker_Action::DB_COLUMN_TIME_GENERATION ." is null + case when " . Piwik_Tracker_Action::DB_COLUMN_TIME_GENERATION . " is null then 0 else 1 end ) as `" . Piwik_Archive::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION . "`"; - $from = array( - "log_link_visit_action", - array( - "table" => "log_action", - "joinOn" => "log_link_visit_action.%s = log_action.idaction" - ) - ); + $from = array( + "log_link_visit_action", + array( + "table" => "log_action", + "joinOn" => "log_link_visit_action.%s = log_action.idaction" + ) + ); - $where = "log_link_visit_action.server_time >= ? + $where = "log_link_visit_action.server_time >= ? AND log_link_visit_action.server_time <= ? AND log_link_visit_action.idsite = ? AND log_link_visit_action.%s IS NOT NULL"; - $groupBy = "log_action.idaction"; - $orderBy = "`" . Piwik_Archive::INDEX_PAGE_NB_HITS . "` DESC, name ASC"; - - $rankingQuery = false; - if ($rankingQueryLimit > 0) - { - $rankingQuery = new Piwik_RankingQuery($rankingQueryLimit); - $rankingQuery->setOthersLabel(Piwik_DataTable::LABEL_SUMMARY_ROW); - $rankingQuery->addLabelColumn(array('idaction', 'name')); - $rankingQuery->addColumn(array('url_prefix', Piwik_Archive::INDEX_NB_UNIQ_VISITORS)); - $rankingQuery->addColumn(array(Piwik_Archive::INDEX_PAGE_NB_HITS, Piwik_Archive::INDEX_NB_VISITS), 'sum'); - if ($this->isSiteSearchEnabled()) - { - $rankingQuery->addColumn(Piwik_Archive::INDEX_SITE_SEARCH_HAS_NO_RESULT, 'min'); - $rankingQuery->addColumn(Piwik_Archive::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS, 'sum'); - } - $rankingQuery->addColumn(Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION, 'sum'); - $rankingQuery->addColumn(Piwik_Archive::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION, 'sum'); - $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($this->actionsTablesByType)); - } - - // Special Magic to get - // 1) No result Keywords - // 2) For each page view, count number of times the referrer page was a Site Search - if($this->isSiteSearchEnabled()) - { - $selectFlagNoResultKeywords = ", - CASE WHEN (MAX(log_link_visit_action.custom_var_v". Piwik_Tracker_Action::CVAR_INDEX_SEARCH_COUNT.") = 0 AND log_link_visit_action.custom_var_k". Piwik_Tracker_Action::CVAR_INDEX_SEARCH_COUNT." = '". Piwik_Tracker_Action::CVAR_KEY_SEARCH_COUNT. "') THEN 1 ELSE 0 END AS `" . Piwik_Archive::INDEX_SITE_SEARCH_HAS_NO_RESULT . "`"; - - //we need an extra JOIN to know whether the referrer "idaction_name_ref" was a Site Search request - $from[] = array( - "table" => "log_action", - "tableAlias" => "log_action_name_ref", - "joinOn" => "log_link_visit_action.idaction_name_ref = log_action_name_ref.idaction" - ); - - $selectSiteSearchFollowingPages = ", - SUM(CASE WHEN log_action_name_ref.type = " . Piwik_Tracker_Action::TYPE_SITE_SEARCH . " THEN 1 ELSE 0 END) AS `". Piwik_Archive::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS."`"; - - $select .= $selectFlagNoResultKeywords - . $selectSiteSearchFollowingPages; - // Not working yet + $groupBy = "log_action.idaction"; + $orderBy = "`" . Piwik_Archive::INDEX_PAGE_NB_HITS . "` DESC, name ASC"; + + $rankingQuery = false; + if ($rankingQueryLimit > 0) { + $rankingQuery = new Piwik_RankingQuery($rankingQueryLimit); + $rankingQuery->setOthersLabel(Piwik_DataTable::LABEL_SUMMARY_ROW); + $rankingQuery->addLabelColumn(array('idaction', 'name')); + $rankingQuery->addColumn(array('url_prefix', Piwik_Archive::INDEX_NB_UNIQ_VISITORS)); + $rankingQuery->addColumn(array(Piwik_Archive::INDEX_PAGE_NB_HITS, Piwik_Archive::INDEX_NB_VISITS), 'sum'); + if ($this->isSiteSearchEnabled()) { + $rankingQuery->addColumn(Piwik_Archive::INDEX_SITE_SEARCH_HAS_NO_RESULT, 'min'); + $rankingQuery->addColumn(Piwik_Archive::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS, 'sum'); + } + $rankingQuery->addColumn(Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION, 'sum'); + $rankingQuery->addColumn(Piwik_Archive::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION, 'sum'); + $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($this->actionsTablesByType)); + } + + // Special Magic to get + // 1) No result Keywords + // 2) For each page view, count number of times the referrer page was a Site Search + if ($this->isSiteSearchEnabled()) { + $selectFlagNoResultKeywords = ", + CASE WHEN (MAX(log_link_visit_action.custom_var_v" . Piwik_Tracker_Action::CVAR_INDEX_SEARCH_COUNT . ") = 0 AND log_link_visit_action.custom_var_k" . Piwik_Tracker_Action::CVAR_INDEX_SEARCH_COUNT . " = '" . Piwik_Tracker_Action::CVAR_KEY_SEARCH_COUNT . "') THEN 1 ELSE 0 END AS `" . Piwik_Archive::INDEX_SITE_SEARCH_HAS_NO_RESULT . "`"; + + //we need an extra JOIN to know whether the referrer "idaction_name_ref" was a Site Search request + $from[] = array( + "table" => "log_action", + "tableAlias" => "log_action_name_ref", + "joinOn" => "log_link_visit_action.idaction_name_ref = log_action_name_ref.idaction" + ); + + $selectSiteSearchFollowingPages = ", + SUM(CASE WHEN log_action_name_ref.type = " . Piwik_Tracker_Action::TYPE_SITE_SEARCH . " THEN 1 ELSE 0 END) AS `" . Piwik_Archive::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS . "`"; + + $select .= $selectFlagNoResultKeywords + . $selectSiteSearchFollowingPages; + // Not working yet // $selectRefPageIsStartingSiteSearch = ", // SUM(CASE WHEN log_action_name_ref.type = " . Piwik_Tracker_Action::TYPE_ACTION_NAME . " THEN 1 ELSE 0 END) AS `". Piwik_Archive::INDEX_PAGE_STARTING_SITE_SEARCH_NB_HITS."`"; // . $selectRefPageIsStartingSiteSearch // . ", idaction_url_ref, idaction_name_ref" - } - - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, - "idaction_name", $archiveProcessing, $rankingQuery); - - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, - "idaction_url", $archiveProcessing, $rankingQuery); - } - - /** - * Entry actions for Page URLs and Page names - */ - protected function archiveDayEntryActions($archiveProcessing, $rankingQueryLimit) - { - $rankingQuery = false; - if ($rankingQueryLimit > 0) { - $rankingQuery = new Piwik_RankingQuery($rankingQueryLimit); - $rankingQuery->setOthersLabel(Piwik_DataTable::LABEL_SUMMARY_ROW); - $rankingQuery->addLabelColumn('idaction'); - $rankingQuery->addColumn(Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS); - $rankingQuery->addColumn(array(Piwik_Archive::INDEX_PAGE_ENTRY_NB_VISITS, - Piwik_Archive::INDEX_PAGE_ENTRY_NB_ACTIONS, - Piwik_Archive::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH, - Piwik_Archive::INDEX_PAGE_ENTRY_BOUNCE_COUNT), 'sum'); - $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($this->actionsTablesByType)); - - $extraSelects = 'log_action.type, log_action.name,'; - $from = array( - "log_visit", - array( - "table" => "log_action", - "joinOn" => "log_visit.%s = log_action.idaction" - ) - ); - $orderBy = "`" . Piwik_Archive::INDEX_PAGE_ENTRY_NB_ACTIONS . "` DESC, log_action.name ASC"; - } else { - $extraSelects = false; - $from = "log_visit"; - $orderBy = false; - } - - $select = "log_visit.%s as idaction, $extraSelects + } + + $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, + "idaction_name", $archiveProcessing, $rankingQuery); + + $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, + "idaction_url", $archiveProcessing, $rankingQuery); + } + + /** + * Entry actions for Page URLs and Page names + */ + protected function archiveDayEntryActions($archiveProcessing, $rankingQueryLimit) + { + $rankingQuery = false; + if ($rankingQueryLimit > 0) { + $rankingQuery = new Piwik_RankingQuery($rankingQueryLimit); + $rankingQuery->setOthersLabel(Piwik_DataTable::LABEL_SUMMARY_ROW); + $rankingQuery->addLabelColumn('idaction'); + $rankingQuery->addColumn(Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS); + $rankingQuery->addColumn(array(Piwik_Archive::INDEX_PAGE_ENTRY_NB_VISITS, + Piwik_Archive::INDEX_PAGE_ENTRY_NB_ACTIONS, + Piwik_Archive::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH, + Piwik_Archive::INDEX_PAGE_ENTRY_BOUNCE_COUNT), 'sum'); + $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($this->actionsTablesByType)); + + $extraSelects = 'log_action.type, log_action.name,'; + $from = array( + "log_visit", + array( + "table" => "log_action", + "joinOn" => "log_visit.%s = log_action.idaction" + ) + ); + $orderBy = "`" . Piwik_Archive::INDEX_PAGE_ENTRY_NB_ACTIONS . "` DESC, log_action.name ASC"; + } else { + $extraSelects = false; + $from = "log_visit"; + $orderBy = false; + } + + $select = "log_visit.%s as idaction, $extraSelects count(distinct log_visit.idvisitor) as `" . Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS . "`, count(*) as `" . Piwik_Archive::INDEX_PAGE_ENTRY_NB_VISITS . "`, sum(log_visit.visit_total_actions) as `" . Piwik_Archive::INDEX_PAGE_ENTRY_NB_ACTIONS . "`, sum(log_visit.visit_total_time) as `" . Piwik_Archive::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH . "`, sum(case log_visit.visit_total_actions when 1 then 1 when 0 then 1 else 0 end) as `" . Piwik_Archive::INDEX_PAGE_ENTRY_BOUNCE_COUNT . "`"; - $where = "log_visit.visit_last_action_time >= ? + $where = "log_visit.visit_last_action_time >= ? AND log_visit.visit_last_action_time <= ? AND log_visit.idsite = ? AND log_visit.%s > 0"; - $groupBy = "log_visit.%s, idaction"; - - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, - "visit_entry_idaction_url", $archiveProcessing, $rankingQuery); - - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, - "visit_entry_idaction_name", $archiveProcessing, $rankingQuery); - } - - - /** - * Time per action - */ - protected function archiveDayActionsTime($archiveProcessing, $rankingQueryLimit) - { - $rankingQuery = false; - if ($rankingQueryLimit > 0) - { - $rankingQuery = new Piwik_RankingQuery($rankingQueryLimit); - $rankingQuery->setOthersLabel(Piwik_DataTable::LABEL_SUMMARY_ROW); - $rankingQuery->addLabelColumn('idaction'); - $rankingQuery->addColumn(Piwik_Archive::INDEX_PAGE_SUM_TIME_SPENT, 'sum'); - $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($this->actionsTablesByType)); - - $extraSelects = "log_action.type, log_action.name, count(*) as `" . Piwik_Archive::INDEX_PAGE_NB_HITS . "`,"; - $from = array( - "log_link_visit_action", - array( - "table" => "log_action", - "joinOn" => "log_link_visit_action.%s = log_action.idaction" - ) - ); - $orderBy = "`" . Piwik_Archive::INDEX_PAGE_NB_HITS . "` DESC, log_action.name ASC"; - } else { - $extraSelects = false; - $from = "log_link_visit_action"; - $orderBy = false; - } - - $select = "log_link_visit_action.%s as idaction, $extraSelects + $groupBy = "log_visit.%s, idaction"; + + $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, + "visit_entry_idaction_url", $archiveProcessing, $rankingQuery); + + $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, + "visit_entry_idaction_name", $archiveProcessing, $rankingQuery); + } + + + /** + * Time per action + */ + protected function archiveDayActionsTime($archiveProcessing, $rankingQueryLimit) + { + $rankingQuery = false; + if ($rankingQueryLimit > 0) { + $rankingQuery = new Piwik_RankingQuery($rankingQueryLimit); + $rankingQuery->setOthersLabel(Piwik_DataTable::LABEL_SUMMARY_ROW); + $rankingQuery->addLabelColumn('idaction'); + $rankingQuery->addColumn(Piwik_Archive::INDEX_PAGE_SUM_TIME_SPENT, 'sum'); + $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($this->actionsTablesByType)); + + $extraSelects = "log_action.type, log_action.name, count(*) as `" . Piwik_Archive::INDEX_PAGE_NB_HITS . "`,"; + $from = array( + "log_link_visit_action", + array( + "table" => "log_action", + "joinOn" => "log_link_visit_action.%s = log_action.idaction" + ) + ); + $orderBy = "`" . Piwik_Archive::INDEX_PAGE_NB_HITS . "` DESC, log_action.name ASC"; + } else { + $extraSelects = false; + $from = "log_link_visit_action"; + $orderBy = false; + } + + $select = "log_link_visit_action.%s as idaction, $extraSelects sum(log_link_visit_action.time_spent_ref_action) as `" . Piwik_Archive::INDEX_PAGE_SUM_TIME_SPENT . "`"; - $where = "log_link_visit_action.server_time >= ? + $where = "log_link_visit_action.server_time >= ? AND log_link_visit_action.server_time <= ? AND log_link_visit_action.idsite = ? AND log_link_visit_action.time_spent_ref_action > 0 AND log_link_visit_action.%s > 0"; - $groupBy = "log_link_visit_action.%s, idaction"; - - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, - "idaction_url_ref", $archiveProcessing, $rankingQuery); - - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, - "idaction_name_ref", $archiveProcessing, $rankingQuery); - } - - /** - * Exit actions - */ - public function archiveDayExitActions($archiveProcessing, $rankingQueryLimit) - { - $rankingQuery = false; - if ($rankingQueryLimit > 0) { - $rankingQuery = new Piwik_RankingQuery($rankingQueryLimit); - $rankingQuery->setOthersLabel(Piwik_DataTable::LABEL_SUMMARY_ROW); - $rankingQuery->addLabelColumn('idaction'); - $rankingQuery->addColumn(Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS); - $rankingQuery->addColumn(Piwik_Archive::INDEX_PAGE_EXIT_NB_VISITS, 'sum'); - $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($this->actionsTablesByType)); - - $extraSelects = 'log_action.type, log_action.name,'; - $from = array( - "log_visit", - array( - "table" => "log_action", - "joinOn" => "log_visit.%s = log_action.idaction" - ) - ); - $orderBy = "`" . Piwik_Archive::INDEX_PAGE_EXIT_NB_VISITS . "` DESC, log_action.name ASC"; - } else { - $extraSelects = false; - $from = "log_visit"; - $orderBy = false; - } - - $select = "log_visit.%s as idaction, $extraSelects + $groupBy = "log_link_visit_action.%s, idaction"; + + $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, + "idaction_url_ref", $archiveProcessing, $rankingQuery); + + $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, + "idaction_name_ref", $archiveProcessing, $rankingQuery); + } + + /** + * Exit actions + */ + public function archiveDayExitActions($archiveProcessing, $rankingQueryLimit) + { + $rankingQuery = false; + if ($rankingQueryLimit > 0) { + $rankingQuery = new Piwik_RankingQuery($rankingQueryLimit); + $rankingQuery->setOthersLabel(Piwik_DataTable::LABEL_SUMMARY_ROW); + $rankingQuery->addLabelColumn('idaction'); + $rankingQuery->addColumn(Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS); + $rankingQuery->addColumn(Piwik_Archive::INDEX_PAGE_EXIT_NB_VISITS, 'sum'); + $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($this->actionsTablesByType)); + + $extraSelects = 'log_action.type, log_action.name,'; + $from = array( + "log_visit", + array( + "table" => "log_action", + "joinOn" => "log_visit.%s = log_action.idaction" + ) + ); + $orderBy = "`" . Piwik_Archive::INDEX_PAGE_EXIT_NB_VISITS . "` DESC, log_action.name ASC"; + } else { + $extraSelects = false; + $from = "log_visit"; + $orderBy = false; + } + + $select = "log_visit.%s as idaction, $extraSelects count(distinct log_visit.idvisitor) as `" . Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS . "`, count(*) as `" . Piwik_Archive::INDEX_PAGE_EXIT_NB_VISITS . "`"; - $where = "log_visit.visit_last_action_time >= ? + $where = "log_visit.visit_last_action_time >= ? AND log_visit.visit_last_action_time <= ? AND log_visit.idsite = ? AND log_visit.%s > 0"; - $groupBy = "log_visit.%s, idaction"; - - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, - "visit_exit_idaction_url", $archiveProcessing, $rankingQuery); - - $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, - "visit_exit_idaction_name", $archiveProcessing, $rankingQuery); - return array($rankingQuery, $extraSelects, $from, $orderBy, $select, $where, $groupBy); - } - - - /** - * Records in the DB the archived reports for Page views, Downloads, Outlinks, and Page titles - * - * @param $archiveProcessing - */ - protected function archiveDayRecordInDatabase($archiveProcessing) - { - Piwik_Actions_ArchivingHelper::clearActionsCache(); - - /** @var Piwik_DataTable $dataTable */ - $dataTable = $this->actionsTablesByType[Piwik_Tracker_Action::TYPE_ACTION_URL]; - self::deleteInvalidSummedColumnsFromDataTable($dataTable); - $s = $dataTable->getSerialized( Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation ); - $archiveProcessing->insertBlobRecord('Actions_actions_url', $s); - $archiveProcessing->insertNumericRecord('Actions_nb_pageviews', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS))); - $archiveProcessing->insertNumericRecord('Actions_nb_uniq_pageviews', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS))); - destroy($dataTable); - - $dataTable = $this->actionsTablesByType[Piwik_Tracker_Action::TYPE_DOWNLOAD]; - self::deleteInvalidSummedColumnsFromDataTable($dataTable); - $s = $dataTable->getSerialized(Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation ); - $archiveProcessing->insertBlobRecord('Actions_downloads', $s); - $archiveProcessing->insertNumericRecord('Actions_nb_downloads', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS))); - $archiveProcessing->insertNumericRecord('Actions_nb_uniq_downloads', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS))); - destroy($dataTable); - - $dataTable = $this->actionsTablesByType[Piwik_Tracker_Action::TYPE_OUTLINK]; - self::deleteInvalidSummedColumnsFromDataTable($dataTable); - $s = $dataTable->getSerialized( Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation ); - $archiveProcessing->insertBlobRecord('Actions_outlink', $s); - $archiveProcessing->insertNumericRecord('Actions_nb_outlinks', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS))); - $archiveProcessing->insertNumericRecord('Actions_nb_uniq_outlinks', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS))); - destroy($dataTable); - - $dataTable = $this->actionsTablesByType[Piwik_Tracker_Action::TYPE_ACTION_NAME]; - self::deleteInvalidSummedColumnsFromDataTable($dataTable); - $s = $dataTable->getSerialized( Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation ); - $archiveProcessing->insertBlobRecord('Actions_actions', $s); - destroy($dataTable); - - $dataTable = $this->actionsTablesByType[Piwik_Tracker_Action::TYPE_SITE_SEARCH]; - self::deleteInvalidSummedColumnsFromDataTable($dataTable); - $this->deleteUnusedColumnsFromKeywordsDataTable($dataTable); - $s = $dataTable->getSerialized( Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation ); - $archiveProcessing->insertBlobRecord('Actions_sitesearch', $s); - $archiveProcessing->insertNumericRecord('Actions_nb_searches', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS))); - $archiveProcessing->insertNumericRecord('Actions_nb_keywords', $dataTable->getRowsCount()); - destroy($dataTable); - - destroy($this->actionsTablesByType); - } - - protected function deleteUnusedColumnsFromKeywordsDataTable($dataTable) - { - $columnsToDelete = array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS, - Piwik_Archive::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS, - Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS, - Piwik_Archive::INDEX_PAGE_ENTRY_NB_ACTIONS, - Piwik_Archive::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH, - Piwik_Archive::INDEX_PAGE_ENTRY_NB_VISITS, - Piwik_Archive::INDEX_PAGE_ENTRY_BOUNCE_COUNT, - Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS, - ); - $dataTable->deleteColumns($columnsToDelete); - } - - static protected function removeEmptyColumns($dataTable) - { - // Delete all columns that have a value of zero - $dataTable->filter('ColumnDelete', array( - $columnsToRemove = array(Piwik_Archive::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS), - $columnsToKeep = array(), - $deleteIfZeroOnly = true - )); - } - - - /** - * Returns the limit to use with RankingQuery for this plugin. - * - * @return int - */ - private static function getRankingQueryLimit() - { - $configGeneral = Piwik_Config::getInstance()->General; - $configLimit = $configGeneral['archiving_ranking_query_row_limit']; - return $configLimit == 0 ? 0 : max( - $configLimit, - $configGeneral['datatable_archiving_maximum_rows_actions'], - $configGeneral['datatable_archiving_maximum_rows_subtable_actions'] - ); - } - - - /** - * @param $select - * @param $from - * @param $where - * @param $orderBy - * @param $groupBy - * @param $sprintfField - * @param Piwik_ArchiveProcessing $archiveProcessing - * @param Piwik_RankingQuery|false $rankingQuery - * @return int - */ - protected function archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, - $sprintfField, $archiveProcessing, $rankingQuery = false) - { - // idaction field needs to be set in select clause before calling getSelectQuery(). - // if a complex segmentation join is needed, the field needs to be propagated - // to the outer select. therefore, $segment needs to know about it. - $select = sprintf($select, $sprintfField); - - $bind = array(); - - // get query with segmentation - $query = $archiveProcessing->getSegment()->getSelectQuery( - $select, $from, $where, $bind, $orderBy, $groupBy); - - // extend bindings - $bind = array_merge(array( $archiveProcessing->getStartDatetimeUTC(), - $archiveProcessing->getEndDatetimeUTC(), - $archiveProcessing->idsite - ), - $query['bind'] - ); - - // replace the rest of the %s - $querySql = str_replace("%s", $sprintfField, $query['sql']); - - // apply ranking query - if ($rankingQuery) - { - $querySql = $rankingQuery->generateQuery($querySql); - } - - // get result - $resultSet = $archiveProcessing->db->query($querySql, $bind); - $modified = Piwik_Actions_ArchivingHelper::updateActionsTableWithRowQuery($resultSet, $sprintfField, $this->actionsTablesByType); - return $modified; - } - - - /** - * For rows which have subtables (eg. directories with sub pages), - * deletes columns which don't make sense when all values of sub pages are summed. - * - * @param $dataTable Piwik_DataTable - */ - static public function deleteInvalidSummedColumnsFromDataTable($dataTable) - { - foreach($dataTable->getRows() as $id => $row) - { - if(($idSubtable = $row->getIdSubDataTable()) !== null - || $id === Piwik_DataTable::ID_SUMMARY_ROW) - { - if ($idSubtable !== null) - { - $subtable = Piwik_DataTable_Manager::getInstance()->getTable($idSubtable); - self::deleteInvalidSummedColumnsFromDataTable($subtable); - } - - if ($row instanceof Piwik_DataTable_Row_DataTableSummary) - { - $row->recalculate(); - } - - foreach(self::$invalidSummedColumnNameToDeleteFromDayArchive as $name) - { - $row->deleteColumn($name); - } - } - } - - // And this as well - self::removeEmptyColumns($dataTable); - } - - /** - * Initializes the DataTables created by the archiveDay function. - */ - private function initActionsTables() - { - $this->actionsTablesByType = array(); - foreach (self::$actionTypes as $type) - { - $dataTable = new Piwik_DataTable(); - $dataTable->setMaximumAllowedRows(Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero); - - $this->actionsTablesByType[$type] = $dataTable; - } - } - - protected function isSiteSearchEnabled() - { - return $this->isSiteSearchEnabled; - } + $groupBy = "log_visit.%s, idaction"; + + $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, + "visit_exit_idaction_url", $archiveProcessing, $rankingQuery); + + $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, + "visit_exit_idaction_name", $archiveProcessing, $rankingQuery); + return array($rankingQuery, $extraSelects, $from, $orderBy, $select, $where, $groupBy); + } + + + /** + * Records in the DB the archived reports for Page views, Downloads, Outlinks, and Page titles + * + * @param $archiveProcessing + */ + protected function archiveDayRecordInDatabase($archiveProcessing) + { + Piwik_Actions_ArchivingHelper::clearActionsCache(); + + /** @var Piwik_DataTable $dataTable */ + $dataTable = $this->actionsTablesByType[Piwik_Tracker_Action::TYPE_ACTION_URL]; + self::deleteInvalidSummedColumnsFromDataTable($dataTable); + $s = $dataTable->getSerialized(Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation); + $archiveProcessing->insertBlobRecord('Actions_actions_url', $s); + $archiveProcessing->insertNumericRecord('Actions_nb_pageviews', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS))); + $archiveProcessing->insertNumericRecord('Actions_nb_uniq_pageviews', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS))); + destroy($dataTable); + + $dataTable = $this->actionsTablesByType[Piwik_Tracker_Action::TYPE_DOWNLOAD]; + self::deleteInvalidSummedColumnsFromDataTable($dataTable); + $s = $dataTable->getSerialized(Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation); + $archiveProcessing->insertBlobRecord('Actions_downloads', $s); + $archiveProcessing->insertNumericRecord('Actions_nb_downloads', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS))); + $archiveProcessing->insertNumericRecord('Actions_nb_uniq_downloads', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS))); + destroy($dataTable); + + $dataTable = $this->actionsTablesByType[Piwik_Tracker_Action::TYPE_OUTLINK]; + self::deleteInvalidSummedColumnsFromDataTable($dataTable); + $s = $dataTable->getSerialized(Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation); + $archiveProcessing->insertBlobRecord('Actions_outlink', $s); + $archiveProcessing->insertNumericRecord('Actions_nb_outlinks', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS))); + $archiveProcessing->insertNumericRecord('Actions_nb_uniq_outlinks', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS))); + destroy($dataTable); + + $dataTable = $this->actionsTablesByType[Piwik_Tracker_Action::TYPE_ACTION_NAME]; + self::deleteInvalidSummedColumnsFromDataTable($dataTable); + $s = $dataTable->getSerialized(Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation); + $archiveProcessing->insertBlobRecord('Actions_actions', $s); + destroy($dataTable); + + $dataTable = $this->actionsTablesByType[Piwik_Tracker_Action::TYPE_SITE_SEARCH]; + self::deleteInvalidSummedColumnsFromDataTable($dataTable); + $this->deleteUnusedColumnsFromKeywordsDataTable($dataTable); + $s = $dataTable->getSerialized(Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation); + $archiveProcessing->insertBlobRecord('Actions_sitesearch', $s); + $archiveProcessing->insertNumericRecord('Actions_nb_searches', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS))); + $archiveProcessing->insertNumericRecord('Actions_nb_keywords', $dataTable->getRowsCount()); + destroy($dataTable); + + destroy($this->actionsTablesByType); + } + + protected function deleteUnusedColumnsFromKeywordsDataTable($dataTable) + { + $columnsToDelete = array( + Piwik_Archive::INDEX_NB_UNIQ_VISITORS, + Piwik_Archive::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS, + Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS, + Piwik_Archive::INDEX_PAGE_ENTRY_NB_ACTIONS, + Piwik_Archive::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH, + Piwik_Archive::INDEX_PAGE_ENTRY_NB_VISITS, + Piwik_Archive::INDEX_PAGE_ENTRY_BOUNCE_COUNT, + Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS, + ); + $dataTable->deleteColumns($columnsToDelete); + } + + static protected function removeEmptyColumns($dataTable) + { + // Delete all columns that have a value of zero + $dataTable->filter('ColumnDelete', array( + $columnsToRemove = array(Piwik_Archive::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS), + $columnsToKeep = array(), + $deleteIfZeroOnly = true + )); + } + + + /** + * Returns the limit to use with RankingQuery for this plugin. + * + * @return int + */ + private static function getRankingQueryLimit() + { + $configGeneral = Piwik_Config::getInstance()->General; + $configLimit = $configGeneral['archiving_ranking_query_row_limit']; + return $configLimit == 0 ? 0 : max( + $configLimit, + $configGeneral['datatable_archiving_maximum_rows_actions'], + $configGeneral['datatable_archiving_maximum_rows_subtable_actions'] + ); + } + + + /** + * @param $select + * @param $from + * @param $where + * @param $orderBy + * @param $groupBy + * @param $sprintfField + * @param Piwik_ArchiveProcessing $archiveProcessing + * @param Piwik_RankingQuery|false $rankingQuery + * @return int + */ + protected function archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, + $sprintfField, $archiveProcessing, $rankingQuery = false) + { + // idaction field needs to be set in select clause before calling getSelectQuery(). + // if a complex segmentation join is needed, the field needs to be propagated + // to the outer select. therefore, $segment needs to know about it. + $select = sprintf($select, $sprintfField); + + $bind = array(); + + // get query with segmentation + $query = $archiveProcessing->getSegment()->getSelectQuery( + $select, $from, $where, $bind, $orderBy, $groupBy); + + // extend bindings + $bind = array_merge(array($archiveProcessing->getStartDatetimeUTC(), + $archiveProcessing->getEndDatetimeUTC(), + $archiveProcessing->idsite + ), + $query['bind'] + ); + + // replace the rest of the %s + $querySql = str_replace("%s", $sprintfField, $query['sql']); + + // apply ranking query + if ($rankingQuery) { + $querySql = $rankingQuery->generateQuery($querySql); + } + + // get result + $resultSet = $archiveProcessing->db->query($querySql, $bind); + $modified = Piwik_Actions_ArchivingHelper::updateActionsTableWithRowQuery($resultSet, $sprintfField, $this->actionsTablesByType); + return $modified; + } + + + /** + * For rows which have subtables (eg. directories with sub pages), + * deletes columns which don't make sense when all values of sub pages are summed. + * + * @param $dataTable Piwik_DataTable + */ + static public function deleteInvalidSummedColumnsFromDataTable($dataTable) + { + foreach ($dataTable->getRows() as $id => $row) { + if (($idSubtable = $row->getIdSubDataTable()) !== null + || $id === Piwik_DataTable::ID_SUMMARY_ROW + ) { + if ($idSubtable !== null) { + $subtable = Piwik_DataTable_Manager::getInstance()->getTable($idSubtable); + self::deleteInvalidSummedColumnsFromDataTable($subtable); + } + + if ($row instanceof Piwik_DataTable_Row_DataTableSummary) { + $row->recalculate(); + } + + foreach (self::$invalidSummedColumnNameToDeleteFromDayArchive as $name) { + $row->deleteColumn($name); + } + } + } + + // And this as well + self::removeEmptyColumns($dataTable); + } + + /** + * Initializes the DataTables created by the archiveDay function. + */ + private function initActionsTables() + { + $this->actionsTablesByType = array(); + foreach (self::$actionTypes as $type) { + $dataTable = new Piwik_DataTable(); + $dataTable->setMaximumAllowedRows(Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero); + + $this->actionsTablesByType[$type] = $dataTable; + } + } + + protected function isSiteSearchEnabled() + { + return $this->isSiteSearchEnabled; + } } diff --git a/plugins/Actions/ArchivingHelper.php b/plugins/Actions/ArchivingHelper.php index 30fb58b555..739e8a2fef 100644 --- a/plugins/Actions/ArchivingHelper.php +++ b/plugins/Actions/ArchivingHelper.php @@ -19,495 +19,452 @@ class Piwik_Actions_ArchivingHelper { - const OTHERS_ROW_KEY = ''; - - /** - * @param Zend_Db_Statement|PDOStatement $query - * @param string|bool $fieldQueried - * @param array $actionsTablesByType - * @return int - */ - static public function updateActionsTableWithRowQuery($query, $fieldQueried, & $actionsTablesByType) - { - $rowsProcessed = 0; - while( $row = $query->fetch() ) - { - if(empty($row['idaction'])) - { - $row['type'] = ($fieldQueried == 'idaction_url' ? Piwik_Tracker_Action::TYPE_ACTION_URL : Piwik_Tracker_Action::TYPE_ACTION_NAME); - // This will be replaced with 'X not defined' later - $row['name'] = ''; - // Yes, this is kind of a hack, so we don't mix 'page url not defined' with 'page title not defined' etc. - $row['idaction'] = -$row['type']; - } - - if($row['type'] != Piwik_Tracker_Action::TYPE_SITE_SEARCH) - { - unset($row[Piwik_Archive::INDEX_SITE_SEARCH_HAS_NO_RESULT]); - } - - // This will appear as <url /> in the API, which is actually very important to keep - // eg. When there's at least one row in a report that does not have a URL, not having this <url/> would break HTML/PDF reports. - $url = ''; - if($row['type'] == Piwik_Tracker_Action::TYPE_SITE_SEARCH - || $row['type'] == Piwik_Tracker_Action::TYPE_ACTION_NAME) - { - $url = null; - } - elseif(!empty($row['name']) - && $row['name'] != Piwik_DataTable::LABEL_SUMMARY_ROW) - { - $url = Piwik_Tracker_Action::reconstructNormalizedUrl((string)$row['name'], $row['url_prefix']); - } - - if(isset($row['name']) - && isset($row['type'])) - { - $actionName = $row['name']; - $actionType = $row['type']; - $urlPrefix = $row['url_prefix']; - $idaction = $row['idaction']; - - // in some unknown case, the type field is NULL, as reported in #1082 - we ignore this page view - if(empty($actionType)) - { - if ($idaction != Piwik_DataTable::LABEL_SUMMARY_ROW) - { - self::setCachedActionRow($idaction, $actionType, false); - } - continue; - } - - $actionRow = self::getActionRow($actionName, $actionType, $urlPrefix, $actionsTablesByType); - - self::setCachedActionRow($idaction, $actionType, $actionRow); - } - else - { - $actionRow = self::getCachedActionRow($row['idaction'], $row['type']); - - // Action processed as "to skip" for some reasons - if($actionRow === false) - { - continue; - } - } - - - if (is_null($actionRow)) - { - continue; - } - - // Here we do ensure that, the Metadata URL set for a given row, is the one from the Pageview with the most hits. - // This is to ensure that when, different URLs are loaded with the same page name. - // For example http://piwik.org and http://id.piwik.org are reported in Piwik > Actions > Pages with /index - // But, we must make sure http://piwik.org is used to link & for transitions - // Note: this code is partly duplicated from Piwik_DataTable_Row->sumRowMetadata() - if( !is_null($url) - && !$actionRow->isSummaryRow()) - { - if(($existingUrl = $actionRow->getMetadata('url')) !== false) - { - if( !empty($row[Piwik_Archive::INDEX_PAGE_NB_HITS]) - && $row[Piwik_Archive::INDEX_PAGE_NB_HITS] > $actionRow->maxVisitsSummed) - { - $actionRow->setMetadata('url', $url); - $actionRow->maxVisitsSummed = $row[Piwik_Archive::INDEX_PAGE_NB_HITS]; - } - } - else - { - $actionRow->setMetadata('url', $url); - $actionRow->maxVisitsSummed = !empty($row[Piwik_Archive::INDEX_PAGE_NB_HITS]) ? $row[Piwik_Archive::INDEX_PAGE_NB_HITS] : 0; - } - } - - if ($row['type'] != Piwik_Tracker_Action::TYPE_ACTION_URL - && $row['type'] != Piwik_Tracker_Action::TYPE_ACTION_NAME) { - // only keep performance metrics when they're used (i.e. for URLs and page titles) - if (array_key_exists(Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION, $row)) { - unset($row[Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION]); - } - if (array_key_exists(Piwik_Archive::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION, $row)) { - unset($row[Piwik_Archive::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION]); - } - } - - unset($row['name']); - unset($row['type']); - unset($row['idaction']); - unset($row['url_prefix']); - - foreach($row as $name => $value) - { - // in some edge cases, we have twice the same action name with 2 different idaction - // - this happens when 2 visitors visit the same new page at the same time, and 2 actions get recorded for the same name - // - this could also happen when 2 URLs end up having the same label (eg. 2 subdomains get aggregated to the "/index" page name) - if(($alreadyValue = $actionRow->getColumn($name)) !== false) - { - $actionRow->setColumn($name, $alreadyValue+$value); - } - else - { - $actionRow->addColumn($name, $value); - } - } - - // if the exit_action was not recorded properly in the log_link_visit_action - // there would be an error message when getting the nb_hits column - // we must fake the record and add the columns - if($actionRow->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS) === false) - { - // to test this code: delete the entries in log_link_action_visit for - // a given exit_idaction_url - foreach(self::getDefaultRow()->getColumns() as $name => $value) - { - $actionRow->addColumn($name, $value); - } - } - $rowsProcessed++; - } - - // just to make sure php copies the last $actionRow in the $parentTable array - $actionRow =& $actionsTablesByType; - return $rowsProcessed; - } - - static public $maximumRowsInDataTableLevelZero; - static public $maximumRowsInSubDataTable; - static public $columnToSortByBeforeTruncation; - - static protected $actionUrlCategoryDelimiter = null; - static protected $actionTitleCategoryDelimiter = null; - static protected $defaultActionName = null; - static protected $defaultActionNameWhenNotDefined = null; - static protected $defaultActionUrlWhenNotDefined = null; - - static public function reloadConfig() - { - // for BC, we read the old style delimiter first (see #1067)Row - $actionDelimiter = @Piwik_Config::getInstance()->General['action_category_delimiter']; - if(empty($actionDelimiter)) - { - self::$actionUrlCategoryDelimiter = Piwik_Config::getInstance()->General['action_url_category_delimiter']; - self::$actionTitleCategoryDelimiter = Piwik_Config::getInstance()->General['action_title_category_delimiter']; - } - else - { - self::$actionUrlCategoryDelimiter = self::$actionTitleCategoryDelimiter = $actionDelimiter; - } - - self::$defaultActionName = Piwik_Config::getInstance()->General['action_default_name']; - self::$columnToSortByBeforeTruncation = Piwik_Archive::INDEX_NB_VISITS; - self::$maximumRowsInDataTableLevelZero = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_actions']; - self::$maximumRowsInSubDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_subtable_actions']; - - Piwik_DataTable::setMaximumDepthLevelAllowedAtLeast(self::getSubCategoryLevelLimit() + 1); - } - - - /** - * The default row is used when archiving, if data is inconsistent in the DB, - * there could be pages that have exit/entry hits, but don't yet - * have a record in the table (or the record was truncated). - * - * @return Piwik_DataTable_Row - */ - static private function getDefaultRow() - { - static $row = false; - if($row === false) { - // This row is used in the case where an action is know as an exit_action - // but this action was not properly recorded when it was hit in the first place - // so we add this fake row information to make sure there is a nb_hits, etc. column for every action - $row = new Piwik_DataTable_Row(array( - Piwik_DataTable_Row::COLUMNS => array( - Piwik_Archive::INDEX_NB_VISITS => 1, - Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 1, - Piwik_Archive::INDEX_PAGE_NB_HITS => 1, - ))); - } - return $row; - } - - /** - * Given a page name and type, builds a recursive datatable where - * each level of the tree is a category, based on the page name split by a delimiter (slash / by default) - * - * @param string $actionName - * @param int $actionType - * @param int $urlPrefix - * @param array $actionsTablesByType - * @return Piwik_DataTable - */ - protected static function getActionRow( $actionName, $actionType, $urlPrefix=null, &$actionsTablesByType ) - { - // we work on the root table of the given TYPE (either ACTION_URL or DOWNLOAD or OUTLINK etc.) - /* @var Piwik_DataTable $currentTable */ - $currentTable =& $actionsTablesByType[$actionType]; - - // check for ranking query cut-off - if ($actionName == Piwik_DataTable::LABEL_SUMMARY_ROW) - { - $summaryRow = $currentTable->getRowFromId(Piwik_DataTable::ID_SUMMARY_ROW); - if ($summaryRow === false) - { - $summaryRow = $currentTable->addSummaryRow(self::createSummaryRow()); - } - return $summaryRow; - } - - // go to the level of the subcategory - $actionExplodedNames = self::getActionExplodedNames($actionName, $actionType, $urlPrefix); - list($row, $level) = $currentTable->walkPath( - $actionExplodedNames, self::getDefaultRowColumns(), self::$maximumRowsInSubDataTable); - - return $row; - } - - /** - * Explodes action name into an array of elements. - * - * NOTE: before calling this function make sure Piwik_Actions_ArchivingHelper::reloadConfig(); is called - * - * for downloads: - * we explode link http://piwik.org/some/path/piwik.zip into an array( 'piwik.org', '/some/path/piwik.zip' ); - * - * for outlinks: - * we explode link http://dev.piwik.org/some/path into an array( 'dev.piwik.org', '/some/path' ); - * - * for action urls: - * we explode link http://piwik.org/some/path into an array( 'some', 'path' ); - * - * for action names: - * we explode name 'Piwik / Category 1 / Category 2' into an array('Piwik', 'Category 1', 'Category 2'); - * - * @param string action name - * @param int action type - * @param int url prefix (only used for TYPE_ACTION_URL) - * @return array of exploded elements from $name - */ - static public function getActionExplodedNames($name, $type, $urlPrefix=null) - { - // Site Search does not split Search keywords - if($type == Piwik_Tracker_Action::TYPE_SITE_SEARCH) - { - return array($name); - } - - $matches = array(); - $isUrl = false; - $name = str_replace("\n", "", $name); - - $urlRegexAfterDomain = '([^/]+)[/]?([^#]*)[#]?(.*)'; - if ($urlPrefix === null) - { - // match url with protocol (used for outlinks / downloads) - $urlRegex = '@^http[s]?://'.$urlRegexAfterDomain.'$@i'; - } - else - { - // the name is a url that does not contain protocol and www anymore - // we know that normalization has been done on db level because $urlPrefix is set - $urlRegex = '@^'.$urlRegexAfterDomain.'$@i'; - } - - preg_match($urlRegex, $name, $matches); - if( count($matches) ) - { - $isUrl = true; - $urlHost = $matches[1]; - $urlPath = $matches[2]; - $urlFragment = $matches[3]; - } - - if($type == Piwik_Tracker_Action::TYPE_DOWNLOAD - || $type == Piwik_Tracker_Action::TYPE_OUTLINK) - { - if( $isUrl ) - { - return array(trim($urlHost), '/' . trim($urlPath)); - } - } - - if( $isUrl ) - { - $name = $urlPath; - - if( $name === '' || substr($name, -1) == '/' ) - { - $name .= self::$defaultActionName; - } - } - - if($type == Piwik_Tracker_Action::TYPE_ACTION_NAME) - { - $categoryDelimiter = self::$actionTitleCategoryDelimiter; - } - else - { - $categoryDelimiter = self::$actionUrlCategoryDelimiter; - } - - - if( $isUrl ) - { - $urlFragment = Piwik_Tracker_Action::processUrlFragment($urlFragment); - if(!empty($urlFragment)) { - $name .= '#' . $urlFragment; - } - } - - if(empty($categoryDelimiter)) - { - return array( trim($name) ); - } - - $split = explode($categoryDelimiter, $name, self::getSubCategoryLevelLimit()); - - // trim every category and remove empty categories - $split = array_map('trim', $split); - $split = array_filter($split, 'strlen'); - - // forces array key to start at 0 - $split = array_values($split); - - if( empty($split) ) - { - $defaultName = self::getUnknownActionName($type); - return array( trim($defaultName) ); - } - - $lastPageName = end($split); - // we are careful to prefix the page URL / name with some value - // so that if a page has the same name as a category - // we don't merge both entries - if($type != Piwik_Tracker_Action::TYPE_ACTION_NAME ) - { - $lastPageName = '/' . $lastPageName; - } - else - { - $lastPageName = ' ' . $lastPageName; - } - $split[count($split)-1] = $lastPageName; - return array_values( $split ); - } - - /** - * Gets the key for the cache of action rows from an action ID and type. - * - * @param int $idAction - * @param int $actionType - * @return string|int - */ - private static function getCachedActionRowKey( $idAction, $actionType ) - { - return $idAction == Piwik_DataTable::LABEL_SUMMARY_ROW - ? $actionType.'_others' - : $idAction; - } - - /** - * Returns the configured sub-category level limit. - * - * @return int - */ - public static function getSubCategoryLevelLimit() - { - return Piwik_Config::getInstance()->General['action_category_level_limit']; - } - - /** - * Returns default label for the action type - * - * @param $type - * @return string - */ - static public function getUnknownActionName($type) - { - if(empty(self::$defaultActionNameWhenNotDefined)) - { - self::$defaultActionNameWhenNotDefined = Piwik_Translate('General_NotDefined', Piwik_Translate('Actions_ColumnPageName')); - self::$defaultActionUrlWhenNotDefined = Piwik_Translate('General_NotDefined', Piwik_Translate('Actions_ColumnPageURL')); - } - if($type == Piwik_Tracker_Action::TYPE_ACTION_NAME) - { - return self::$defaultActionNameWhenNotDefined; - } - return self::$defaultActionUrlWhenNotDefined; - } - - /** - * Static cache to store Rows during processing - */ - static protected $cacheParsedAction = array(); - - public static function clearActionsCache() - { - self::$cacheParsedAction = array(); - } - - /** - * Get cached action row by id & type. If $idAction is set to -1, the 'Others' row - * for the specific action type will be returned. - * - * @param int $idAction - * @param int $actionType - * @return Piwik_DataTable_Row|false - */ - private static function getCachedActionRow( $idAction, $actionType ) - { - $cacheLabel = self::getCachedActionRowKey($idAction, $actionType); - - if (!isset(self::$cacheParsedAction[$cacheLabel])) - { - // This can happen when - // - We select an entry page ID that was only seen yesterday, so wasn't selected in the first query - // - We count time spent on a page, when this page was only seen yesterday - return false; - } - - return self::$cacheParsedAction[$cacheLabel]; - } - - /** - * Set cached action row for an id & type. - * - * @param int $idAction - * @param int $actionType - * @param Piwik_DataTable_Row - */ - private static function setCachedActionRow( $idAction, $actionType, $actionRow ) - { - $cacheLabel = self::getCachedActionRowKey($idAction, $actionType); - self::$cacheParsedAction[$cacheLabel] = $actionRow; - } - - /** - * Returns the default columns for a row in an Actions DataTable. - * - * @return array - */ - private static function getDefaultRowColumns() - { - return array(Piwik_Archive::INDEX_NB_VISITS => 0, - Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, - Piwik_Archive::INDEX_PAGE_NB_HITS => 0, - Piwik_Archive::INDEX_PAGE_SUM_TIME_SPENT => 0); - } - - /** - * Creates a summary row for an Actions DataTable. - * - * @return Piwik_DataTable_Row - */ - private static function createSummaryRow() - { - return new Piwik_DataTable_Row(array( - Piwik_DataTable_Row::COLUMNS => - array('label' => Piwik_DataTable::LABEL_SUMMARY_ROW) + self::getDefaultRowColumns() - )); - } + const OTHERS_ROW_KEY = ''; + + /** + * @param Zend_Db_Statement|PDOStatement $query + * @param string|bool $fieldQueried + * @param array $actionsTablesByType + * @return int + */ + static public function updateActionsTableWithRowQuery($query, $fieldQueried, & $actionsTablesByType) + { + $rowsProcessed = 0; + while ($row = $query->fetch()) { + if (empty($row['idaction'])) { + $row['type'] = ($fieldQueried == 'idaction_url' ? Piwik_Tracker_Action::TYPE_ACTION_URL : Piwik_Tracker_Action::TYPE_ACTION_NAME); + // This will be replaced with 'X not defined' later + $row['name'] = ''; + // Yes, this is kind of a hack, so we don't mix 'page url not defined' with 'page title not defined' etc. + $row['idaction'] = -$row['type']; + } + + if ($row['type'] != Piwik_Tracker_Action::TYPE_SITE_SEARCH) { + unset($row[Piwik_Archive::INDEX_SITE_SEARCH_HAS_NO_RESULT]); + } + + // This will appear as <url /> in the API, which is actually very important to keep + // eg. When there's at least one row in a report that does not have a URL, not having this <url/> would break HTML/PDF reports. + $url = ''; + if ($row['type'] == Piwik_Tracker_Action::TYPE_SITE_SEARCH + || $row['type'] == Piwik_Tracker_Action::TYPE_ACTION_NAME + ) { + $url = null; + } elseif (!empty($row['name']) + && $row['name'] != Piwik_DataTable::LABEL_SUMMARY_ROW + ) { + $url = Piwik_Tracker_Action::reconstructNormalizedUrl((string)$row['name'], $row['url_prefix']); + } + + if (isset($row['name']) + && isset($row['type']) + ) { + $actionName = $row['name']; + $actionType = $row['type']; + $urlPrefix = $row['url_prefix']; + $idaction = $row['idaction']; + + // in some unknown case, the type field is NULL, as reported in #1082 - we ignore this page view + if (empty($actionType)) { + if ($idaction != Piwik_DataTable::LABEL_SUMMARY_ROW) { + self::setCachedActionRow($idaction, $actionType, false); + } + continue; + } + + $actionRow = self::getActionRow($actionName, $actionType, $urlPrefix, $actionsTablesByType); + + self::setCachedActionRow($idaction, $actionType, $actionRow); + } else { + $actionRow = self::getCachedActionRow($row['idaction'], $row['type']); + + // Action processed as "to skip" for some reasons + if ($actionRow === false) { + continue; + } + } + + + if (is_null($actionRow)) { + continue; + } + + // Here we do ensure that, the Metadata URL set for a given row, is the one from the Pageview with the most hits. + // This is to ensure that when, different URLs are loaded with the same page name. + // For example http://piwik.org and http://id.piwik.org are reported in Piwik > Actions > Pages with /index + // But, we must make sure http://piwik.org is used to link & for transitions + // Note: this code is partly duplicated from Piwik_DataTable_Row->sumRowMetadata() + if (!is_null($url) + && !$actionRow->isSummaryRow() + ) { + if (($existingUrl = $actionRow->getMetadata('url')) !== false) { + if (!empty($row[Piwik_Archive::INDEX_PAGE_NB_HITS]) + && $row[Piwik_Archive::INDEX_PAGE_NB_HITS] > $actionRow->maxVisitsSummed + ) { + $actionRow->setMetadata('url', $url); + $actionRow->maxVisitsSummed = $row[Piwik_Archive::INDEX_PAGE_NB_HITS]; + } + } else { + $actionRow->setMetadata('url', $url); + $actionRow->maxVisitsSummed = !empty($row[Piwik_Archive::INDEX_PAGE_NB_HITS]) ? $row[Piwik_Archive::INDEX_PAGE_NB_HITS] : 0; + } + } + + if ($row['type'] != Piwik_Tracker_Action::TYPE_ACTION_URL + && $row['type'] != Piwik_Tracker_Action::TYPE_ACTION_NAME + ) { + // only keep performance metrics when they're used (i.e. for URLs and page titles) + if (array_key_exists(Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION, $row)) { + unset($row[Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION]); + } + if (array_key_exists(Piwik_Archive::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION, $row)) { + unset($row[Piwik_Archive::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION]); + } + } + + unset($row['name']); + unset($row['type']); + unset($row['idaction']); + unset($row['url_prefix']); + + foreach ($row as $name => $value) { + // in some edge cases, we have twice the same action name with 2 different idaction + // - this happens when 2 visitors visit the same new page at the same time, and 2 actions get recorded for the same name + // - this could also happen when 2 URLs end up having the same label (eg. 2 subdomains get aggregated to the "/index" page name) + if (($alreadyValue = $actionRow->getColumn($name)) !== false) { + $actionRow->setColumn($name, $alreadyValue + $value); + } else { + $actionRow->addColumn($name, $value); + } + } + + // if the exit_action was not recorded properly in the log_link_visit_action + // there would be an error message when getting the nb_hits column + // we must fake the record and add the columns + if ($actionRow->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS) === false) { + // to test this code: delete the entries in log_link_action_visit for + // a given exit_idaction_url + foreach (self::getDefaultRow()->getColumns() as $name => $value) { + $actionRow->addColumn($name, $value); + } + } + $rowsProcessed++; + } + + // just to make sure php copies the last $actionRow in the $parentTable array + $actionRow =& $actionsTablesByType; + return $rowsProcessed; + } + + static public $maximumRowsInDataTableLevelZero; + static public $maximumRowsInSubDataTable; + static public $columnToSortByBeforeTruncation; + + static protected $actionUrlCategoryDelimiter = null; + static protected $actionTitleCategoryDelimiter = null; + static protected $defaultActionName = null; + static protected $defaultActionNameWhenNotDefined = null; + static protected $defaultActionUrlWhenNotDefined = null; + + static public function reloadConfig() + { + // for BC, we read the old style delimiter first (see #1067)Row + $actionDelimiter = @Piwik_Config::getInstance()->General['action_category_delimiter']; + if (empty($actionDelimiter)) { + self::$actionUrlCategoryDelimiter = Piwik_Config::getInstance()->General['action_url_category_delimiter']; + self::$actionTitleCategoryDelimiter = Piwik_Config::getInstance()->General['action_title_category_delimiter']; + } else { + self::$actionUrlCategoryDelimiter = self::$actionTitleCategoryDelimiter = $actionDelimiter; + } + + self::$defaultActionName = Piwik_Config::getInstance()->General['action_default_name']; + self::$columnToSortByBeforeTruncation = Piwik_Archive::INDEX_NB_VISITS; + self::$maximumRowsInDataTableLevelZero = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_actions']; + self::$maximumRowsInSubDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_subtable_actions']; + + Piwik_DataTable::setMaximumDepthLevelAllowedAtLeast(self::getSubCategoryLevelLimit() + 1); + } + + + /** + * The default row is used when archiving, if data is inconsistent in the DB, + * there could be pages that have exit/entry hits, but don't yet + * have a record in the table (or the record was truncated). + * + * @return Piwik_DataTable_Row + */ + static private function getDefaultRow() + { + static $row = false; + if ($row === false) { + // This row is used in the case where an action is know as an exit_action + // but this action was not properly recorded when it was hit in the first place + // so we add this fake row information to make sure there is a nb_hits, etc. column for every action + $row = new Piwik_DataTable_Row(array( + Piwik_DataTable_Row::COLUMNS => array( + Piwik_Archive::INDEX_NB_VISITS => 1, + Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 1, + Piwik_Archive::INDEX_PAGE_NB_HITS => 1, + ))); + } + return $row; + } + + /** + * Given a page name and type, builds a recursive datatable where + * each level of the tree is a category, based on the page name split by a delimiter (slash / by default) + * + * @param string $actionName + * @param int $actionType + * @param int $urlPrefix + * @param array $actionsTablesByType + * @return Piwik_DataTable + */ + protected static function getActionRow($actionName, $actionType, $urlPrefix = null, &$actionsTablesByType) + { + // we work on the root table of the given TYPE (either ACTION_URL or DOWNLOAD or OUTLINK etc.) + /* @var Piwik_DataTable $currentTable */ + $currentTable =& $actionsTablesByType[$actionType]; + + // check for ranking query cut-off + if ($actionName == Piwik_DataTable::LABEL_SUMMARY_ROW) { + $summaryRow = $currentTable->getRowFromId(Piwik_DataTable::ID_SUMMARY_ROW); + if ($summaryRow === false) { + $summaryRow = $currentTable->addSummaryRow(self::createSummaryRow()); + } + return $summaryRow; + } + + // go to the level of the subcategory + $actionExplodedNames = self::getActionExplodedNames($actionName, $actionType, $urlPrefix); + list($row, $level) = $currentTable->walkPath( + $actionExplodedNames, self::getDefaultRowColumns(), self::$maximumRowsInSubDataTable); + + return $row; + } + + /** + * Explodes action name into an array of elements. + * + * NOTE: before calling this function make sure Piwik_Actions_ArchivingHelper::reloadConfig(); is called + * + * for downloads: + * we explode link http://piwik.org/some/path/piwik.zip into an array( 'piwik.org', '/some/path/piwik.zip' ); + * + * for outlinks: + * we explode link http://dev.piwik.org/some/path into an array( 'dev.piwik.org', '/some/path' ); + * + * for action urls: + * we explode link http://piwik.org/some/path into an array( 'some', 'path' ); + * + * for action names: + * we explode name 'Piwik / Category 1 / Category 2' into an array('Piwik', 'Category 1', 'Category 2'); + * + * @param string action name + * @param int action type + * @param int url prefix (only used for TYPE_ACTION_URL) + * @return array of exploded elements from $name + */ + static public function getActionExplodedNames($name, $type, $urlPrefix = null) + { + // Site Search does not split Search keywords + if ($type == Piwik_Tracker_Action::TYPE_SITE_SEARCH) { + return array($name); + } + + $matches = array(); + $isUrl = false; + $name = str_replace("\n", "", $name); + + $urlRegexAfterDomain = '([^/]+)[/]?([^#]*)[#]?(.*)'; + if ($urlPrefix === null) { + // match url with protocol (used for outlinks / downloads) + $urlRegex = '@^http[s]?://' . $urlRegexAfterDomain . '$@i'; + } else { + // the name is a url that does not contain protocol and www anymore + // we know that normalization has been done on db level because $urlPrefix is set + $urlRegex = '@^' . $urlRegexAfterDomain . '$@i'; + } + + preg_match($urlRegex, $name, $matches); + if (count($matches)) { + $isUrl = true; + $urlHost = $matches[1]; + $urlPath = $matches[2]; + $urlFragment = $matches[3]; + } + + if ($type == Piwik_Tracker_Action::TYPE_DOWNLOAD + || $type == Piwik_Tracker_Action::TYPE_OUTLINK + ) { + if ($isUrl) { + return array(trim($urlHost), '/' . trim($urlPath)); + } + } + + if ($isUrl) { + $name = $urlPath; + + if ($name === '' || substr($name, -1) == '/') { + $name .= self::$defaultActionName; + } + } + + if ($type == Piwik_Tracker_Action::TYPE_ACTION_NAME) { + $categoryDelimiter = self::$actionTitleCategoryDelimiter; + } else { + $categoryDelimiter = self::$actionUrlCategoryDelimiter; + } + + + if ($isUrl) { + $urlFragment = Piwik_Tracker_Action::processUrlFragment($urlFragment); + if (!empty($urlFragment)) { + $name .= '#' . $urlFragment; + } + } + + if (empty($categoryDelimiter)) { + return array(trim($name)); + } + + $split = explode($categoryDelimiter, $name, self::getSubCategoryLevelLimit()); + + // trim every category and remove empty categories + $split = array_map('trim', $split); + $split = array_filter($split, 'strlen'); + + // forces array key to start at 0 + $split = array_values($split); + + if (empty($split)) { + $defaultName = self::getUnknownActionName($type); + return array(trim($defaultName)); + } + + $lastPageName = end($split); + // we are careful to prefix the page URL / name with some value + // so that if a page has the same name as a category + // we don't merge both entries + if ($type != Piwik_Tracker_Action::TYPE_ACTION_NAME) { + $lastPageName = '/' . $lastPageName; + } else { + $lastPageName = ' ' . $lastPageName; + } + $split[count($split) - 1] = $lastPageName; + return array_values($split); + } + + /** + * Gets the key for the cache of action rows from an action ID and type. + * + * @param int $idAction + * @param int $actionType + * @return string|int + */ + private static function getCachedActionRowKey($idAction, $actionType) + { + return $idAction == Piwik_DataTable::LABEL_SUMMARY_ROW + ? $actionType . '_others' + : $idAction; + } + + /** + * Returns the configured sub-category level limit. + * + * @return int + */ + public static function getSubCategoryLevelLimit() + { + return Piwik_Config::getInstance()->General['action_category_level_limit']; + } + + /** + * Returns default label for the action type + * + * @param $type + * @return string + */ + static public function getUnknownActionName($type) + { + if (empty(self::$defaultActionNameWhenNotDefined)) { + self::$defaultActionNameWhenNotDefined = Piwik_Translate('General_NotDefined', Piwik_Translate('Actions_ColumnPageName')); + self::$defaultActionUrlWhenNotDefined = Piwik_Translate('General_NotDefined', Piwik_Translate('Actions_ColumnPageURL')); + } + if ($type == Piwik_Tracker_Action::TYPE_ACTION_NAME) { + return self::$defaultActionNameWhenNotDefined; + } + return self::$defaultActionUrlWhenNotDefined; + } + + /** + * Static cache to store Rows during processing + */ + static protected $cacheParsedAction = array(); + + public static function clearActionsCache() + { + self::$cacheParsedAction = array(); + } + + /** + * Get cached action row by id & type. If $idAction is set to -1, the 'Others' row + * for the specific action type will be returned. + * + * @param int $idAction + * @param int $actionType + * @return Piwik_DataTable_Row|false + */ + private static function getCachedActionRow($idAction, $actionType) + { + $cacheLabel = self::getCachedActionRowKey($idAction, $actionType); + + if (!isset(self::$cacheParsedAction[$cacheLabel])) { + // This can happen when + // - We select an entry page ID that was only seen yesterday, so wasn't selected in the first query + // - We count time spent on a page, when this page was only seen yesterday + return false; + } + + return self::$cacheParsedAction[$cacheLabel]; + } + + /** + * Set cached action row for an id & type. + * + * @param int $idAction + * @param int $actionType + * @param Piwik_DataTable_Row + */ + private static function setCachedActionRow($idAction, $actionType, $actionRow) + { + $cacheLabel = self::getCachedActionRowKey($idAction, $actionType); + self::$cacheParsedAction[$cacheLabel] = $actionRow; + } + + /** + * Returns the default columns for a row in an Actions DataTable. + * + * @return array + */ + private static function getDefaultRowColumns() + { + return array(Piwik_Archive::INDEX_NB_VISITS => 0, + Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, + Piwik_Archive::INDEX_PAGE_NB_HITS => 0, + Piwik_Archive::INDEX_PAGE_SUM_TIME_SPENT => 0); + } + + /** + * Creates a summary row for an Actions DataTable. + * + * @return Piwik_DataTable_Row + */ + private static function createSummaryRow() + { + return new Piwik_DataTable_Row(array( + Piwik_DataTable_Row::COLUMNS => + array('label' => Piwik_DataTable::LABEL_SUMMARY_ROW) + self::getDefaultRowColumns() + )); + } } diff --git a/plugins/Actions/Controller.php b/plugins/Actions/Controller.php index 00eb49a804..aed8efd683 100644 --- a/plugins/Actions/Controller.php +++ b/plugins/Actions/Controller.php @@ -16,513 +16,502 @@ */ class Piwik_Actions_Controller extends Piwik_Controller { - const ACTIONS_REPORT_ROWS_DISPLAY = 100; - - protected function getPageUrlsView($currentAction, $controllerActionSubtable, $apiAction) - { - $view = Piwik_ViewDataTable::factory(); - $view->init($this->pluginName, $currentAction, $apiAction, $controllerActionSubtable); - $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnPageURL')); - return $view; - } - - /** - * PAGES - * @param bool $fetch - * @return string - */ - - public function indexPageUrls($fetch = false) - { - return Piwik_View::singleReport( - Piwik_Translate('Actions_SubmenuPages'), - $this->getPageUrls(true), $fetch); - } - - public function getPageUrls($fetch = false) - { - $view = $this->getPageUrlsView(__FUNCTION__, 'getPageUrls', 'Actions.getPageUrls'); - $this->configureViewPages($view); - $this->configureViewActions($view); - return $this->renderView($view, $fetch); - } - - protected function configureViewPages($view) - { - $view->setColumnsToDisplay( array('label','nb_hits','nb_visits', 'bounce_rate', 'avg_time_on_page', 'exit_rate', 'avg_time_generation') ); - } - - /** - * ENTRY PAGES - * @param bool $fetch - * @return string|void - */ - public function indexEntryPageUrls($fetch = false) - { - return Piwik_View::singleReport( - Piwik_Translate('Actions_SubmenuPagesEntry'), - $this->getEntryPageUrls(true), $fetch); - } - - public function getEntryPageUrls($fetch = false) - { - $view = $this->getPageUrlsView(__FUNCTION__, 'getEntryPageUrls', 'Actions.getEntryPageUrls'); - $this->configureViewEntryPageUrls($view); - $this->configureViewActions($view); - return $this->renderView($view, $fetch); - } - - protected function configureViewEntryPageUrls($view) - { - $view->setSortedColumn('entry_nb_visits'); - $view->setColumnsToDisplay( array('label','entry_nb_visits', 'entry_bounce_count', 'bounce_rate') ); - $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnEntryPageURL')); - $view->setColumnTranslation('entry_bounce_count', Piwik_Translate('General_ColumnBounces')); - $view->setColumnTranslation('entry_nb_visits', Piwik_Translate('General_ColumnEntrances')); - $view->addRelatedReports(Piwik_Translate('Actions_SubmenuPagesEntry'), array( - 'Actions.getEntryPageTitles' => Piwik_Translate('Actions_EntryPageTitles') - )); - $view->setReportUrl('Actions', $this->getEntryPageUrlActionForLink()); - } - - /* - * EXIT PAGES - */ - public function indexExitPageUrls($fetch = false) - { - return Piwik_View::singleReport( - Piwik_Translate('Actions_SubmenuPagesExit'), - $this->getExitPageUrls(true), $fetch); - } - - public function getExitPageUrls($fetch = false) - { - $view = $this->getPageUrlsView(__FUNCTION__, 'getExitPageUrls', 'Actions.getExitPageUrls'); - $this->configureViewExitPageUrls($view); - $this->configureViewActions($view); - return $this->renderView($view, $fetch); - } - - protected function configureViewExitPageUrls($view) - { - $view->setSortedColumn('exit_nb_visits'); - $view->setColumnsToDisplay( array('label', 'exit_nb_visits', 'nb_visits', 'exit_rate') ); - $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnExitPageURL')); - $view->setColumnTranslation('exit_nb_visits', Piwik_Translate('General_ColumnExits')); - $view->addRelatedReports(Piwik_Translate('Actions_SubmenuPagesExit'), array( - 'Actions.getExitPageTitles' => Piwik_Translate('Actions_ExitPageTitles') - )); - $view->setReportUrl('Actions', $this->getExitPageUrlActionForLink()); - } - - /* - * SITE SEARCH - */ - public function indexSiteSearch() - { - $view = Piwik_View::factory('indexSiteSearch'); - - $view->keywords = $this->getSiteSearchKeywords( true ); - $view->noResultKeywords = $this->getSiteSearchNoResultKeywords( true ); - $view->pagesUrlsFollowingSiteSearch = $this->getPageUrlsFollowingSiteSearch( true ); - - $categoryTrackingEnabled = Piwik_PluginsManager::getInstance()->isPluginActivated('CustomVariables'); - if($categoryTrackingEnabled) - { - $view->categories = $this->getSiteSearchCategories( true ); - } - - echo $view->render(); - } - - public function getSiteSearchKeywords($fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, __FUNCTION__, 'Actions.getSiteSearchKeywords' ); - $this->configureViewSiteSearchKeywords($view); - return $this->renderView($view, $fetch); - } - - public function getSiteSearchNoResultKeywords($fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, __FUNCTION__, 'Actions.getSiteSearchNoResultKeywords' ); - $this->configureViewSiteSearchKeywords($view); - $view->setColumnsToDisplay(array('label', 'nb_visits', 'exit_rate')); - $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnNoResultKeyword')); - return $this->renderView($view, $fetch); - } - - public function configureViewSiteSearchKeywords(Piwik_ViewDataTable $view) - { - $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnSearchKeyword')); - $view->setColumnsToDisplay(array('label', 'nb_visits', 'nb_pages_per_search', 'exit_rate')); - $view->setColumnTranslation('nb_visits', Piwik_Translate('Actions_ColumnSearches')); - $view->setColumnTranslation('exit_rate', str_replace("% ", "% ", Piwik_Translate('Actions_ColumnSearchExits'))); - $view->setColumnTranslation('nb_pages_per_search', Piwik_Translate('Actions_ColumnPagesPerSearch')); - $view->disableShowBarChart(); - $view->disableShowAllColumns(); - } - - public function getSiteSearchCategories($fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, __FUNCTION__, 'Actions.getSiteSearchCategories' ); - $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnSearchCategory')); - $view->setColumnTranslation('nb_visits', Piwik_Translate('Actions_ColumnSearches')); - $view->setColumnsToDisplay( array('label','nb_visits', 'nb_pages_per_search') ); - $view->setColumnTranslation('nb_pages_per_search', Piwik_Translate('Actions_ColumnPagesPerSearch')); - $view->disableShowAllColumns(); - $view->disableShowBarChart(); - $view->disableRowEvolution(); - return $this->renderView($view, $fetch); - } - - - public function getPageUrlsFollowingSiteSearch($fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, __FUNCTION__, 'Actions.getPageUrlsFollowingSiteSearch', 'getPageUrlsFollowingSiteSearch' ); - $view->addRelatedReports(Piwik_Translate('Actions_WidgetPageUrlsFollowingSearch'), array( - 'Actions.getPageTitlesFollowingSiteSearch' => Piwik_Translate('Actions_WidgetPageTitlesFollowingSearch'), - )); - $view = $this->configureViewPagesFollowingSiteSearch($view); - return $this->renderView($view, $fetch); - } - - public function getPageTitlesFollowingSiteSearch($fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, __FUNCTION__, 'Actions.getPageTitlesFollowingSiteSearch', 'getPageTitlesFollowingSiteSearch' ); - $view->addRelatedReports(Piwik_Translate('Actions_WidgetPageTitlesFollowingSearch'), array( - 'Actions.getPageUrlsFollowingSiteSearch' => Piwik_Translate('Actions_WidgetPageUrlsFollowingSearch'), - )); - $view = $this->configureViewPagesFollowingSiteSearch($view); - return $this->renderView($view, $fetch); - } - - public function configureViewPagesFollowingSiteSearch($view) - { - $view->setColumnsToDisplay(array('label', 'nb_hits_following_search', 'nb_hits')); - $view->setColumnTranslation('nb_hits_following_search', Piwik_Translate('General_ColumnViewedAfterSearch')); - $view->setColumnTranslation('label', Piwik_Translate('General_ColumnDestinationPage')); - $view->setSortedColumn('nb_hits_following_search'); - $view->setColumnTranslation('nb_hits', Piwik_Translate('General_ColumnTotalPageviews')); - $view->disableExcludeLowPopulation(); - $view = $this->configureViewActions($view, $doSetTranslations = false); - return $view; - } - - /* - * PAGE TITLES - */ - public function indexPageTitles($fetch = false) - { - return Piwik_View::singleReport( - Piwik_Translate('Actions_SubmenuPageTitles'), - $this->getPageTitles(true), $fetch); - } - - public function getPageTitles($fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, - __FUNCTION__, - 'Actions.getPageTitles', - 'getPageTitlesSubDataTable' ); - $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnPageName')); - $view->addRelatedReports(Piwik_Translate('Actions_SubmenuPageTitles'), array( - 'Actions.getEntryPageTitles' => Piwik_Translate('Actions_EntryPageTitles'), - 'Actions.getExitPageTitles' => Piwik_Translate('Actions_ExitPageTitles'), - )); - $view->setReportUrl('Actions', $this->getPageTitlesActionForLink()); - $this->configureViewPages($view); - $this->configureViewActions($view); - return $this->renderView($view, $fetch); - } - - public function getPageTitlesSubDataTable($fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, - __FUNCTION__, - 'Actions.getPageTitles', - 'getPageTitlesSubDataTable' ); - $this->configureViewPages($view); - $this->configureViewActions($view); - return $this->renderView($view, $fetch); - } - - /** - * Echos or returns a report displaying analytics data for every unique entry - * page title. - * - * @param bool $fetch True to return the view as a string, false to echo it. - * @return string - */ - public function getEntryPageTitles( $fetch = false ) - { - $view = Piwik_ViewDataTable::factory(); - $view->init($this->pluginName, __FUNCTION__, 'Actions.getEntryPageTitles', __FUNCTION__); - $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnEntryPageTitle')); - $view->setColumnTranslation('entry_bounce_count', Piwik_Translate('General_ColumnBounces')); - $view->setColumnTranslation('entry_nb_visits', Piwik_Translate('General_ColumnEntrances')); - $view->setColumnsToDisplay( array('label','entry_nb_visits', 'entry_bounce_count', 'bounce_rate') ); - - $entryPageUrlAction = $this->getEntryPageUrlActionForLink(); - $view->addRelatedReports(Piwik_Translate('Actions_EntryPageTitles'), array( - 'Actions.getPageTitles' => Piwik_Translate('Actions_SubmenuPageTitles'), - "Actions.$entryPageUrlAction" => Piwik_Translate('Actions_SubmenuPagesEntry'), - )); - - $this->configureViewActions($view); - - return $this->renderView($view, $fetch); - } - - /** - * Echos or returns a report displaying analytics data for every unique exit - * page title. - * - * @param bool $fetch True to return the view as a string, false to echo it. - * @return string - */ - public function getExitPageTitles( $fetch = false ) - { - $view = Piwik_ViewDataTable::factory(); - $view->init($this->pluginName, __FUNCTION__, 'Actions.getExitPageTitles', __FUNCTION__); - $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnExitPageTitle')); - $view->setColumnTranslation('exit_nb_visits', Piwik_Translate('General_ColumnExits')); - $view->setColumnsToDisplay( array('label', 'exit_nb_visits', 'nb_visits', 'exit_rate') ); - - $exitPageUrlAction = $this->getExitPageUrlActionForLink(); - $view->addRelatedReports(Piwik_Translate('Actions_ExitPageTitles'), array( - 'Actions.getPageTitles' => Piwik_Translate('Actions_SubmenuPageTitles'), - "Actions.$exitPageUrlAction" => Piwik_Translate('Actions_SubmenuPagesExit'), - )); - - $this->configureViewActions($view); - - return $this->renderView($view, $fetch); - } - - /* - * DOWNLOADS - */ - - public function indexDownloads($fetch = false) - { - return Piwik_View::singleReport( - Piwik_Translate('Actions_SubmenuDownloads'), - $this->getDownloads(true), $fetch); - } - - public function getDownloads($fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, - __FUNCTION__, - 'Actions.getDownloads', - 'getDownloadsSubDataTable' ); - - $this->configureViewDownloads($view); - return $this->renderView($view, $fetch); - } - - public function getDownloadsSubDataTable($fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, - __FUNCTION__, - 'Actions.getDownloads', - 'getDownloadsSubDataTable'); - $this->configureViewDownloads($view); - return $this->renderView($view, $fetch); - } - - - /* - * OUTLINKS - */ - - public function indexOutlinks($fetch = false) - { - return Piwik_View::singleReport( - Piwik_Translate('Actions_SubmenuOutlinks'), - $this->getOutlinks(true), $fetch); - } - - public function getOutlinks($fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, - __FUNCTION__, - 'Actions.getOutlinks', - 'getOutlinksSubDataTable' ); - $this->configureViewOutlinks($view); - return $this->renderView($view, $fetch); - } - - public function getOutlinksSubDataTable($fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, - __FUNCTION__, - 'Actions.getOutlinks', - 'getOutlinksSubDataTable'); - $this->configureViewOutlinks($view); - return $this->renderView($view, $fetch); - } - - /* - * Page titles & Page URLs reports - */ - protected function configureViewActions($view, $doSetTranslations = true) - { - if($doSetTranslations) - { - $view->setColumnTranslation('nb_hits', Piwik_Translate('General_ColumnPageviews')); - $view->setColumnTranslation('nb_visits', Piwik_Translate('General_ColumnUniquePageviews')); - $view->setColumnTranslation('avg_time_on_page', Piwik_Translate('General_ColumnAverageTimeOnPage')); - $view->setColumnTranslation('bounce_rate', Piwik_Translate('General_ColumnBounceRate')); - $view->setColumnTranslation('exit_rate', Piwik_Translate('General_ColumnExitRate')); - $view->setColumnTranslation('avg_time_generation', Piwik_Translate('General_ColumnAverageGenerationTime')); - $view->queueFilter('ColumnCallbackReplace', array('avg_time_on_page', array('Piwik', 'getPrettyTimeFromSeconds'))); - $view->queueFilter('ColumnCallbackReplace', array('avg_time_generation', - create_function('$averageTimeOnSite', 'return $averageTimeOnSite ? Piwik::getPrettyTimeFromSeconds($averageTimeOnSite, true, true, false) : "-";'))); - } - - if(Piwik_Common::getRequestVar('enable_filter_excludelowpop', '0', 'string' ) != '0') - { - // computing minimum value to exclude - $visitsInfo = Piwik_VisitsSummary_Controller::getVisitsSummary(); - $visitsInfo = $visitsInfo->getFirstRow(); - $nbActions = $visitsInfo->getColumn('nb_actions'); - $nbActionsLowPopulationThreshold = floor(0.02 * $nbActions); // 2 percent of the total number of actions - // we remove 1 to make sure some actions/downloads are displayed in the case we have a very few of them - // and each of them has 1 or 2 hits... - $nbActionsLowPopulationThreshold = min($visitsInfo->getColumn('max_actions')-1, $nbActionsLowPopulationThreshold-1); - - $view->setExcludeLowPopulation( 'nb_hits', $nbActionsLowPopulationThreshold ); - } - - $this->configureGenericViewActions($view); - return $view; - } - - /* - * Downloads report - */ - protected function configureViewDownloads($view) - { - $view->setColumnsToDisplay( array('label','nb_visits','nb_hits') ); - $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnDownloadURL')); - $view->setColumnTranslation('nb_visits', Piwik_Translate('Actions_ColumnUniqueDownloads')); - $view->setColumnTranslation('nb_hits', Piwik_Translate('Actions_ColumnDownloads')); - $view->disableExcludeLowPopulation(); - $this->configureGenericViewActions($view); - } - - /* - * Outlinks report - */ - protected function configureViewOutlinks($view) - { - $view->setColumnsToDisplay( array('label','nb_visits','nb_hits') ); - $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnClickedURL')); - $view->setColumnTranslation('nb_visits', Piwik_Translate('Actions_ColumnUniqueClicks')); - $view->setColumnTranslation('nb_hits', Piwik_Translate('Actions_ColumnClicks')); - $view->disableExcludeLowPopulation(); - $this->configureGenericViewActions($view); - } - - /* - * Common to all Actions reports, how to use the custom Actions Datatable html - */ - protected function configureGenericViewActions($view) - { - $view->setTemplate('CoreHome/templates/datatable_actions.tpl'); - if(Piwik_Common::getRequestVar('idSubtable', -1) != -1) - { - $view->setTemplate('CoreHome/templates/datatable_actions_subdatable.tpl'); - } - $currentlySearching = $view->setSearchRecursive(); - if($currentlySearching) - { - $view->setTemplate('CoreHome/templates/datatable_actions_recursive.tpl'); - } - // disable Footer icons - $view->disableShowAllViewsIcons(); - $view->disableShowAllColumns(); - - $view->setLimit( self::ACTIONS_REPORT_ROWS_DISPLAY ); - - // if the flat parameter is not provided, make sure it is set to 0 in the URL, - // so users can see that they can set it to 1 (see #3365) - if (Piwik_Common::getRequestVar('flat', false) === false) - { - $view->setCustomParameter('flat', 0); - } - - $view->main(); - // we need to rewrite the phpArray so it contains all the recursive arrays - if($currentlySearching) - { - $phpArrayRecursive = $this->getArrayFromRecursiveDataTable($view->getDataTable()); - $view->getView()->arrayDataTable = $phpArrayRecursive; - } - } - - protected function getArrayFromRecursiveDataTable( $dataTable, $depth = 0 ) - { - $table = array(); - foreach($dataTable->getRows() as $row) - { - $phpArray = array(); - if(($idSubtable = $row->getIdSubDataTable()) !== null) - { - $subTable = Piwik_DataTable_Manager::getInstance()->getTable( $idSubtable ); - - if($subTable->getRowsCount() > 0) - { - $phpArray = $this->getArrayFromRecursiveDataTable( $subTable, $depth + 1 ); - } - } - - $newRow = array( - 'level' => $depth, - 'columns' => $row->getColumns(), - 'metadata' => $row->getMetadata(), - 'idsubdatatable' => $row->getIdSubDataTable() - ); - $table[] = $newRow; - if(count($phpArray) > 0) - { - $table = array_merge( $table, $phpArray); - } - } - return $table; - } - - /** Returns action to use when linking to the exit page URLs report. */ - private function getExitPageUrlActionForLink() - { - // link to the page not, just the report, but only if not a widget - return Piwik_Common::getRequestVar('widget', 0) == 0 ? 'indexExitPageUrls' : 'getExitPageUrls'; - } - - - /** Returns action to use when linking to the entry page URLs report. */ - private function getEntryPageUrlActionForLink() - { - // link to the page not, just the report, but only if not a widget - return Piwik_Common::getRequestVar('widget', 0) == 0 ? 'indexEntryPageUrls' : 'getEntryPageUrls'; - } - - /** Returns action to use when linking to the page titles report. */ - private function getPageTitlesActionForLink() - { - // link to the page not, just the report, but only if not a widget - return Piwik_Common::getRequestVar('widget', 0) == 0 ? 'indexPageTitles' : 'getPageTitles'; - } + const ACTIONS_REPORT_ROWS_DISPLAY = 100; + + protected function getPageUrlsView($currentAction, $controllerActionSubtable, $apiAction) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, $currentAction, $apiAction, $controllerActionSubtable); + $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnPageURL')); + return $view; + } + + /** + * PAGES + * @param bool $fetch + * @return string + */ + + public function indexPageUrls($fetch = false) + { + return Piwik_View::singleReport( + Piwik_Translate('Actions_SubmenuPages'), + $this->getPageUrls(true), $fetch); + } + + public function getPageUrls($fetch = false) + { + $view = $this->getPageUrlsView(__FUNCTION__, 'getPageUrls', 'Actions.getPageUrls'); + $this->configureViewPages($view); + $this->configureViewActions($view); + return $this->renderView($view, $fetch); + } + + protected function configureViewPages($view) + { + $view->setColumnsToDisplay(array('label', 'nb_hits', 'nb_visits', 'bounce_rate', 'avg_time_on_page', 'exit_rate', 'avg_time_generation')); + } + + /** + * ENTRY PAGES + * @param bool $fetch + * @return string|void + */ + public function indexEntryPageUrls($fetch = false) + { + return Piwik_View::singleReport( + Piwik_Translate('Actions_SubmenuPagesEntry'), + $this->getEntryPageUrls(true), $fetch); + } + + public function getEntryPageUrls($fetch = false) + { + $view = $this->getPageUrlsView(__FUNCTION__, 'getEntryPageUrls', 'Actions.getEntryPageUrls'); + $this->configureViewEntryPageUrls($view); + $this->configureViewActions($view); + return $this->renderView($view, $fetch); + } + + protected function configureViewEntryPageUrls($view) + { + $view->setSortedColumn('entry_nb_visits'); + $view->setColumnsToDisplay(array('label', 'entry_nb_visits', 'entry_bounce_count', 'bounce_rate')); + $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnEntryPageURL')); + $view->setColumnTranslation('entry_bounce_count', Piwik_Translate('General_ColumnBounces')); + $view->setColumnTranslation('entry_nb_visits', Piwik_Translate('General_ColumnEntrances')); + $view->addRelatedReports(Piwik_Translate('Actions_SubmenuPagesEntry'), array( + 'Actions.getEntryPageTitles' => Piwik_Translate('Actions_EntryPageTitles') + )); + $view->setReportUrl('Actions', $this->getEntryPageUrlActionForLink()); + } + + /* + * EXIT PAGES + */ + public function indexExitPageUrls($fetch = false) + { + return Piwik_View::singleReport( + Piwik_Translate('Actions_SubmenuPagesExit'), + $this->getExitPageUrls(true), $fetch); + } + + public function getExitPageUrls($fetch = false) + { + $view = $this->getPageUrlsView(__FUNCTION__, 'getExitPageUrls', 'Actions.getExitPageUrls'); + $this->configureViewExitPageUrls($view); + $this->configureViewActions($view); + return $this->renderView($view, $fetch); + } + + protected function configureViewExitPageUrls($view) + { + $view->setSortedColumn('exit_nb_visits'); + $view->setColumnsToDisplay(array('label', 'exit_nb_visits', 'nb_visits', 'exit_rate')); + $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnExitPageURL')); + $view->setColumnTranslation('exit_nb_visits', Piwik_Translate('General_ColumnExits')); + $view->addRelatedReports(Piwik_Translate('Actions_SubmenuPagesExit'), array( + 'Actions.getExitPageTitles' => Piwik_Translate('Actions_ExitPageTitles') + )); + $view->setReportUrl('Actions', $this->getExitPageUrlActionForLink()); + } + + /* + * SITE SEARCH + */ + public function indexSiteSearch() + { + $view = Piwik_View::factory('indexSiteSearch'); + + $view->keywords = $this->getSiteSearchKeywords(true); + $view->noResultKeywords = $this->getSiteSearchNoResultKeywords(true); + $view->pagesUrlsFollowingSiteSearch = $this->getPageUrlsFollowingSiteSearch(true); + + $categoryTrackingEnabled = Piwik_PluginsManager::getInstance()->isPluginActivated('CustomVariables'); + if ($categoryTrackingEnabled) { + $view->categories = $this->getSiteSearchCategories(true); + } + + echo $view->render(); + } + + public function getSiteSearchKeywords($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, 'Actions.getSiteSearchKeywords'); + $this->configureViewSiteSearchKeywords($view); + return $this->renderView($view, $fetch); + } + + public function getSiteSearchNoResultKeywords($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, 'Actions.getSiteSearchNoResultKeywords'); + $this->configureViewSiteSearchKeywords($view); + $view->setColumnsToDisplay(array('label', 'nb_visits', 'exit_rate')); + $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnNoResultKeyword')); + return $this->renderView($view, $fetch); + } + + public function configureViewSiteSearchKeywords(Piwik_ViewDataTable $view) + { + $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnSearchKeyword')); + $view->setColumnsToDisplay(array('label', 'nb_visits', 'nb_pages_per_search', 'exit_rate')); + $view->setColumnTranslation('nb_visits', Piwik_Translate('Actions_ColumnSearches')); + $view->setColumnTranslation('exit_rate', str_replace("% ", "% ", Piwik_Translate('Actions_ColumnSearchExits'))); + $view->setColumnTranslation('nb_pages_per_search', Piwik_Translate('Actions_ColumnPagesPerSearch')); + $view->disableShowBarChart(); + $view->disableShowAllColumns(); + } + + public function getSiteSearchCategories($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, 'Actions.getSiteSearchCategories'); + $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnSearchCategory')); + $view->setColumnTranslation('nb_visits', Piwik_Translate('Actions_ColumnSearches')); + $view->setColumnsToDisplay(array('label', 'nb_visits', 'nb_pages_per_search')); + $view->setColumnTranslation('nb_pages_per_search', Piwik_Translate('Actions_ColumnPagesPerSearch')); + $view->disableShowAllColumns(); + $view->disableShowBarChart(); + $view->disableRowEvolution(); + return $this->renderView($view, $fetch); + } + + + public function getPageUrlsFollowingSiteSearch($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, 'Actions.getPageUrlsFollowingSiteSearch', 'getPageUrlsFollowingSiteSearch'); + $view->addRelatedReports(Piwik_Translate('Actions_WidgetPageUrlsFollowingSearch'), array( + 'Actions.getPageTitlesFollowingSiteSearch' => Piwik_Translate('Actions_WidgetPageTitlesFollowingSearch'), + )); + $view = $this->configureViewPagesFollowingSiteSearch($view); + return $this->renderView($view, $fetch); + } + + public function getPageTitlesFollowingSiteSearch($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, 'Actions.getPageTitlesFollowingSiteSearch', 'getPageTitlesFollowingSiteSearch'); + $view->addRelatedReports(Piwik_Translate('Actions_WidgetPageTitlesFollowingSearch'), array( + 'Actions.getPageUrlsFollowingSiteSearch' => Piwik_Translate('Actions_WidgetPageUrlsFollowingSearch'), + )); + $view = $this->configureViewPagesFollowingSiteSearch($view); + return $this->renderView($view, $fetch); + } + + public function configureViewPagesFollowingSiteSearch($view) + { + $view->setColumnsToDisplay(array('label', 'nb_hits_following_search', 'nb_hits')); + $view->setColumnTranslation('nb_hits_following_search', Piwik_Translate('General_ColumnViewedAfterSearch')); + $view->setColumnTranslation('label', Piwik_Translate('General_ColumnDestinationPage')); + $view->setSortedColumn('nb_hits_following_search'); + $view->setColumnTranslation('nb_hits', Piwik_Translate('General_ColumnTotalPageviews')); + $view->disableExcludeLowPopulation(); + $view = $this->configureViewActions($view, $doSetTranslations = false); + return $view; + } + + /* + * PAGE TITLES + */ + public function indexPageTitles($fetch = false) + { + return Piwik_View::singleReport( + Piwik_Translate('Actions_SubmenuPageTitles'), + $this->getPageTitles(true), $fetch); + } + + public function getPageTitles($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, + __FUNCTION__, + 'Actions.getPageTitles', + 'getPageTitlesSubDataTable'); + $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnPageName')); + $view->addRelatedReports(Piwik_Translate('Actions_SubmenuPageTitles'), array( + 'Actions.getEntryPageTitles' => Piwik_Translate('Actions_EntryPageTitles'), + 'Actions.getExitPageTitles' => Piwik_Translate('Actions_ExitPageTitles'), + )); + $view->setReportUrl('Actions', $this->getPageTitlesActionForLink()); + $this->configureViewPages($view); + $this->configureViewActions($view); + return $this->renderView($view, $fetch); + } + + public function getPageTitlesSubDataTable($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, + __FUNCTION__, + 'Actions.getPageTitles', + 'getPageTitlesSubDataTable'); + $this->configureViewPages($view); + $this->configureViewActions($view); + return $this->renderView($view, $fetch); + } + + /** + * Echos or returns a report displaying analytics data for every unique entry + * page title. + * + * @param bool $fetch True to return the view as a string, false to echo it. + * @return string + */ + public function getEntryPageTitles($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, 'Actions.getEntryPageTitles', __FUNCTION__); + $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnEntryPageTitle')); + $view->setColumnTranslation('entry_bounce_count', Piwik_Translate('General_ColumnBounces')); + $view->setColumnTranslation('entry_nb_visits', Piwik_Translate('General_ColumnEntrances')); + $view->setColumnsToDisplay(array('label', 'entry_nb_visits', 'entry_bounce_count', 'bounce_rate')); + + $entryPageUrlAction = $this->getEntryPageUrlActionForLink(); + $view->addRelatedReports(Piwik_Translate('Actions_EntryPageTitles'), array( + 'Actions.getPageTitles' => Piwik_Translate('Actions_SubmenuPageTitles'), + "Actions.$entryPageUrlAction" => Piwik_Translate('Actions_SubmenuPagesEntry'), + )); + + $this->configureViewActions($view); + + return $this->renderView($view, $fetch); + } + + /** + * Echos or returns a report displaying analytics data for every unique exit + * page title. + * + * @param bool $fetch True to return the view as a string, false to echo it. + * @return string + */ + public function getExitPageTitles($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, 'Actions.getExitPageTitles', __FUNCTION__); + $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnExitPageTitle')); + $view->setColumnTranslation('exit_nb_visits', Piwik_Translate('General_ColumnExits')); + $view->setColumnsToDisplay(array('label', 'exit_nb_visits', 'nb_visits', 'exit_rate')); + + $exitPageUrlAction = $this->getExitPageUrlActionForLink(); + $view->addRelatedReports(Piwik_Translate('Actions_ExitPageTitles'), array( + 'Actions.getPageTitles' => Piwik_Translate('Actions_SubmenuPageTitles'), + "Actions.$exitPageUrlAction" => Piwik_Translate('Actions_SubmenuPagesExit'), + )); + + $this->configureViewActions($view); + + return $this->renderView($view, $fetch); + } + + /* + * DOWNLOADS + */ + + public function indexDownloads($fetch = false) + { + return Piwik_View::singleReport( + Piwik_Translate('Actions_SubmenuDownloads'), + $this->getDownloads(true), $fetch); + } + + public function getDownloads($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, + __FUNCTION__, + 'Actions.getDownloads', + 'getDownloadsSubDataTable'); + + $this->configureViewDownloads($view); + return $this->renderView($view, $fetch); + } + + public function getDownloadsSubDataTable($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, + __FUNCTION__, + 'Actions.getDownloads', + 'getDownloadsSubDataTable'); + $this->configureViewDownloads($view); + return $this->renderView($view, $fetch); + } + + + /* + * OUTLINKS + */ + + public function indexOutlinks($fetch = false) + { + return Piwik_View::singleReport( + Piwik_Translate('Actions_SubmenuOutlinks'), + $this->getOutlinks(true), $fetch); + } + + public function getOutlinks($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, + __FUNCTION__, + 'Actions.getOutlinks', + 'getOutlinksSubDataTable'); + $this->configureViewOutlinks($view); + return $this->renderView($view, $fetch); + } + + public function getOutlinksSubDataTable($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, + __FUNCTION__, + 'Actions.getOutlinks', + 'getOutlinksSubDataTable'); + $this->configureViewOutlinks($view); + return $this->renderView($view, $fetch); + } + + /* + * Page titles & Page URLs reports + */ + protected function configureViewActions($view, $doSetTranslations = true) + { + if ($doSetTranslations) { + $view->setColumnTranslation('nb_hits', Piwik_Translate('General_ColumnPageviews')); + $view->setColumnTranslation('nb_visits', Piwik_Translate('General_ColumnUniquePageviews')); + $view->setColumnTranslation('avg_time_on_page', Piwik_Translate('General_ColumnAverageTimeOnPage')); + $view->setColumnTranslation('bounce_rate', Piwik_Translate('General_ColumnBounceRate')); + $view->setColumnTranslation('exit_rate', Piwik_Translate('General_ColumnExitRate')); + $view->setColumnTranslation('avg_time_generation', Piwik_Translate('General_ColumnAverageGenerationTime')); + $view->queueFilter('ColumnCallbackReplace', array('avg_time_on_page', array('Piwik', 'getPrettyTimeFromSeconds'))); + $view->queueFilter('ColumnCallbackReplace', array('avg_time_generation', + create_function('$averageTimeOnSite', 'return $averageTimeOnSite ? Piwik::getPrettyTimeFromSeconds($averageTimeOnSite, true, true, false) : "-";'))); + } + + if (Piwik_Common::getRequestVar('enable_filter_excludelowpop', '0', 'string') != '0') { + // computing minimum value to exclude + $visitsInfo = Piwik_VisitsSummary_Controller::getVisitsSummary(); + $visitsInfo = $visitsInfo->getFirstRow(); + $nbActions = $visitsInfo->getColumn('nb_actions'); + $nbActionsLowPopulationThreshold = floor(0.02 * $nbActions); // 2 percent of the total number of actions + // we remove 1 to make sure some actions/downloads are displayed in the case we have a very few of them + // and each of them has 1 or 2 hits... + $nbActionsLowPopulationThreshold = min($visitsInfo->getColumn('max_actions') - 1, $nbActionsLowPopulationThreshold - 1); + + $view->setExcludeLowPopulation('nb_hits', $nbActionsLowPopulationThreshold); + } + + $this->configureGenericViewActions($view); + return $view; + } + + /* + * Downloads report + */ + protected function configureViewDownloads($view) + { + $view->setColumnsToDisplay(array('label', 'nb_visits', 'nb_hits')); + $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnDownloadURL')); + $view->setColumnTranslation('nb_visits', Piwik_Translate('Actions_ColumnUniqueDownloads')); + $view->setColumnTranslation('nb_hits', Piwik_Translate('Actions_ColumnDownloads')); + $view->disableExcludeLowPopulation(); + $this->configureGenericViewActions($view); + } + + /* + * Outlinks report + */ + protected function configureViewOutlinks($view) + { + $view->setColumnsToDisplay(array('label', 'nb_visits', 'nb_hits')); + $view->setColumnTranslation('label', Piwik_Translate('Actions_ColumnClickedURL')); + $view->setColumnTranslation('nb_visits', Piwik_Translate('Actions_ColumnUniqueClicks')); + $view->setColumnTranslation('nb_hits', Piwik_Translate('Actions_ColumnClicks')); + $view->disableExcludeLowPopulation(); + $this->configureGenericViewActions($view); + } + + /* + * Common to all Actions reports, how to use the custom Actions Datatable html + */ + protected function configureGenericViewActions($view) + { + $view->setTemplate('CoreHome/templates/datatable_actions.tpl'); + if (Piwik_Common::getRequestVar('idSubtable', -1) != -1) { + $view->setTemplate('CoreHome/templates/datatable_actions_subdatable.tpl'); + } + $currentlySearching = $view->setSearchRecursive(); + if ($currentlySearching) { + $view->setTemplate('CoreHome/templates/datatable_actions_recursive.tpl'); + } + // disable Footer icons + $view->disableShowAllViewsIcons(); + $view->disableShowAllColumns(); + + $view->setLimit(self::ACTIONS_REPORT_ROWS_DISPLAY); + + // if the flat parameter is not provided, make sure it is set to 0 in the URL, + // so users can see that they can set it to 1 (see #3365) + if (Piwik_Common::getRequestVar('flat', false) === false) { + $view->setCustomParameter('flat', 0); + } + + $view->main(); + // we need to rewrite the phpArray so it contains all the recursive arrays + if ($currentlySearching) { + $phpArrayRecursive = $this->getArrayFromRecursiveDataTable($view->getDataTable()); + $view->getView()->arrayDataTable = $phpArrayRecursive; + } + } + + protected function getArrayFromRecursiveDataTable($dataTable, $depth = 0) + { + $table = array(); + foreach ($dataTable->getRows() as $row) { + $phpArray = array(); + if (($idSubtable = $row->getIdSubDataTable()) !== null) { + $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubtable); + + if ($subTable->getRowsCount() > 0) { + $phpArray = $this->getArrayFromRecursiveDataTable($subTable, $depth + 1); + } + } + + $newRow = array( + 'level' => $depth, + 'columns' => $row->getColumns(), + 'metadata' => $row->getMetadata(), + 'idsubdatatable' => $row->getIdSubDataTable() + ); + $table[] = $newRow; + if (count($phpArray) > 0) { + $table = array_merge($table, $phpArray); + } + } + return $table; + } + + /** Returns action to use when linking to the exit page URLs report. */ + private function getExitPageUrlActionForLink() + { + // link to the page not, just the report, but only if not a widget + return Piwik_Common::getRequestVar('widget', 0) == 0 ? 'indexExitPageUrls' : 'getExitPageUrls'; + } + + + /** Returns action to use when linking to the entry page URLs report. */ + private function getEntryPageUrlActionForLink() + { + // link to the page not, just the report, but only if not a widget + return Piwik_Common::getRequestVar('widget', 0) == 0 ? 'indexEntryPageUrls' : 'getEntryPageUrls'; + } + + /** Returns action to use when linking to the page titles report. */ + private function getPageTitlesActionForLink() + { + // link to the page not, just the report, but only if not a widget + return Piwik_Common::getRequestVar('widget', 0) == 0 ? 'indexPageTitles' : 'getPageTitles'; + } } diff --git a/plugins/Actions/templates/indexSiteSearch.tpl b/plugins/Actions/templates/indexSiteSearch.tpl index 3af0d5c0a4..a692fdd697 100644 --- a/plugins/Actions/templates/indexSiteSearch.tpl +++ b/plugins/Actions/templates/indexSiteSearch.tpl @@ -1,19 +1,19 @@ <div id='leftcolumn'> - <h2>{'Actions_WidgetSearchKeywords'|translate}</h2> -{$keywords} + <h2>{'Actions_WidgetSearchKeywords'|translate}</h2> + {$keywords} - <h2>{'Actions_WidgetSearchNoResultKeywords'|translate}</h2> -{$noResultKeywords} + <h2>{'Actions_WidgetSearchNoResultKeywords'|translate}</h2> + {$noResultKeywords} -{if isset($categories)} - <h2>{'Actions_WidgetSearchCategories'|translate}</h2> -{$categories} -{/if} + {if isset($categories)} + <h2>{'Actions_WidgetSearchCategories'|translate}</h2> + {$categories} + {/if} </div> <div id='rightcolumn'> - <h2>{'Actions_WidgetPageUrlsFollowingSearch'|translate}</h2> -{$pagesUrlsFollowingSiteSearch} + <h2>{'Actions_WidgetPageUrlsFollowingSearch'|translate}</h2> + {$pagesUrlsFollowingSiteSearch} </div> diff --git a/plugins/Annotations/API.php b/plugins/Annotations/API.php index e03f5f5dba..7750c816e3 100755 --- a/plugins/Annotations/API.php +++ b/plugins/Annotations/API.php @@ -12,365 +12,350 @@ /** * @see plugins/Annotations/AnnotationList.php */ -require_once PIWIK_INCLUDE_PATH.'/plugins/Annotations/AnnotationList.php'; +require_once PIWIK_INCLUDE_PATH . '/plugins/Annotations/AnnotationList.php'; /** * API for annotations plugin. Provides methods to create, modify, delete & query * annotations. - * + * * @package Piwik_Annotations */ class Piwik_Annotations_API { - static private $instance = null; - - /** - * Returns this API's singleton instance. - * - * @return Piwik_Annotations_API - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - /** - * Create a new annotation for a site. - * - * @param string $idSite The site ID to add the annotation to. - * @param string $date The date the annotation is attached to. - * @param string $note The text of the annotation. - * @param string $starred Either 0 or 1. Whether the annotation should be starred. - * @return array Returns an array of two elements. The first element (indexed by - * 'annotation') is the new annotation. The second element (indexed - * by 'idNote' is the new note's ID). - */ - public function add( $idSite, $date, $note, $starred = 0 ) - { - $this->checkSingleIdSite($idSite, $extraMessage = "Note: Cannot add one note to multiple sites."); - $this->checkDateIsValid($date); - $this->checkUserCanAddNotesFor($idSite); - - // add, save & return a new annotation - $annotations = new Piwik_Annotations_AnnotationList($idSite); - - $newAnnotation = $annotations->add($idSite, $date, $note, $starred); - $annotations->save($idSite); - - return $newAnnotation; - } - - /** - * Modifies an annotation for a site and returns the modified annotation - * and its ID. - * - * If the current user is not allowed to modify an annotation, an exception - * will be thrown. A user can modify a note if: - * - the user has admin access for the site, OR - * - the user has view access, is not the anonymous user and is the user that - * created the note - * - * @param string $idSite The site ID to add the annotation to. - * @param string $idNote The ID of the note. - * @param string|null $date The date the annotation is attached to. If null, the annotation's - * date is not modified. - * @param string|null $note The text of the annotation. If null, the annotation's text - * is not modified. - * @param string|null $starred Either 0 or 1. Whether the annotation should be starred. - * If null, the annotation is not starred/un-starred. - * @return array Returns an array of two elements. The first element (indexed by - * 'annotation') is the new annotation. The second element (indexed - * by 'idNote' is the new note's ID). - */ - public function save( $idSite, $idNote, $date = null, $note = null, $starred = null ) - { - $this->checkSingleIdSite($idSite, $extraMessage = "Note: Cannot modify more than one note at a time."); - $this->checkDateIsValid($date, $canBeNull = true); - - // get the annotations for the site - $annotations = new Piwik_Annotations_AnnotationList($idSite); - - // check permissions - $this->checkUserCanModifyOrDelete($idSite, $annotations->get($idSite, $idNote)); - - // modify the annotation, and save the whole list - $annotations->update($idSite, $idNote, $date, $note, $starred); - $annotations->save($idSite); - - return $annotations->get($idSite, $idNote); - } - - /** - * Removes an annotation from a site's list of annotations. - * - * If the current user is not allowed to delete the annotation, an exception - * will be thrown. A user can delete a note if: - * - the user has admin access for the site, OR - * - the user has view access, is not the anonymous user and is the user that - * created the note - * - * @param string $idSite The site ID to add the annotation to. - * @param string $idNote The ID of the note to delete. - */ - public function delete( $idSite, $idNote ) - { - $this->checkSingleIdSite($idSite, $extraMessage = "Note: Cannot delete multiple notes."); - - $annotations = new Piwik_Annotations_AnnotationList($idSite); - - // check permissions - $this->checkUserCanModifyOrDelete($idSite, $annotations->get($idSite, $idNote)); - - // remove the note & save the list - $annotations->remove($idSite, $idNote); - $annotations->save($idSite); - } - - /** - * Returns a single note for one site. - * - * @param string $idSite The site ID to add the annotation to. - * @param string $idNote The ID of the note to get. - * @return array The annotation. It will contain the following properties: - * - date: The date the annotation was recorded for. - * - note: The note text. - * - starred: Whether the note is starred or not. - * - user: The user that created the note. - * - canEditOrDelete: Whether the user that called this method can edit or - * delete the annotation returned. - */ - public function get( $idSite, $idNote ) - { - $this->checkSingleIdSite($idSite, $extraMessage = "Note: Specify only one site ID when getting ONE note."); - Piwik::checkUserHasViewAccess($idSite); - - // get single annotation - $annotations = new Piwik_Annotations_AnnotationList($idSite); - return $annotations->get($idSite, $idNote); - } - - /** - * Returns every annotation for a specific site within a specific date range. - * The date range is specified by a date, the period type (day/week/month/year) - * and an optional number of N periods in the past to include. - * - * @param string $idSite The site ID to add the annotation to. Can be one ID or - * a list of site IDs. - * @param string|false $date The date of the period. - * @param string $period The period type. - * @param int|false $lastN Whether to include the last N number of periods in the - * date range or not. - * @return array An array that indexes arrays of annotations by site ID. ie, - * array( - * 5 => array( - * array(...), // annotation #1 - * array(...), // annotation #2 - * ), - * 8 => array(...) - * ) - */ - public function getAll( $idSite, $date = false, $period = 'day', $lastN = false ) - { - Piwik::checkUserHasViewAccess($idSite); - - $annotations = new Piwik_Annotations_AnnotationList($idSite); - - // if date/period are supplied, determine start/end date for search - list($startDate, $endDate) = self::getDateRangeForPeriod($date, $period, $lastN); - - return $annotations->search($startDate, $endDate); - } - - /** - * Returns the count of annotations for a list of periods, including the count of - * starred annotations. - * - * @param string $idSite The site ID to add the annotation to. - * @param string|false $date The date of the period. - * @param string $period The period type. - * @param int|false $lastN Whether to get counts for the last N number of periods or not. - * @return array An array mapping site IDs to arrays holding dates & the count of - * annotations made for those dates. eg, - * array( - * 5 => array( - * array('2012-01-02', array('count' => 4, 'starred' => 2)), - * array('2012-01-03', array('count' => 0, 'starred' => 0)), - * array('2012-01-04', array('count' => 2, 'starred' => 0)), - * ), - * 6 => array( - * array('2012-01-02', array('count' => 1, 'starred' => 0)), - * array('2012-01-03', array('count' => 4, 'starred' => 3)), - * array('2012-01-04', array('count' => 2, 'starred' => 0)), - * ), - * ... - * ) - */ - public function getAnnotationCountForDates( $idSite, $date, $period, $lastN = false, $getAnnotationText = false ) - { - Piwik::checkUserHasViewAccess($idSite); - - // get start & end date for request. lastN is ignored if $period == 'range' - list($startDate, $endDate) = self::getDateRangeForPeriod($date, $period, $lastN); - if ($period == 'range') - { - $period = 'day'; - } - - // create list of dates - $dates = array(); - for (; $startDate->getTimestamp() <= $endDate->getTimestamp(); $startDate = $startDate->addPeriod(1, $period)) - { - $dates[] = $startDate; - } - // we add one for the end of the last period (used in for loop below to bound annotation dates) - $dates[] = $startDate; - - // get annotations for the site - $annotations = new Piwik_Annotations_AnnotationList($idSite); - - // create result w/ 0-counts - $result = array(); - for ($i = 0; $i != count($dates) - 1; ++$i) - { - $date = $dates[$i]; - $nextDate = $dates[$i + 1]; - $strDate = $date->toString(); - - foreach ($annotations->getIdSites() as $idSite) - { - $result[$idSite][$strDate] = $annotations->count($idSite, $date, $nextDate); - - // if only one annotation, return the one annotation's text w/ the counts - if ($getAnnotationText - && $result[$idSite][$strDate]['count'] == 1) - { - $annotationsForSite = $annotations->search( - $date, Piwik_Date::factory($nextDate->getTimestamp() - 1), $idSite); - $annotation = reset($annotationsForSite[$idSite]); - - $result[$idSite][$strDate]['note'] = $annotation['note']; - } - } - } - - // convert associative array into array of pairs (so it can be traversed by index) - $pairResult = array(); - foreach ($result as $idSite => $counts) - { - foreach ($counts as $date => $count) - { - $pairResult[$idSite][] = array($date, $count); - } - } - return $pairResult; - } - - /** - * Throws if the current user is not allowed to modify or delete an annotation. - * - * @param int $idSite The site ID the annotation belongs to. - * @param array $annotation The annotation. - * @throws Exception if the current user is not allowed to modify/delete $annotation. - */ - private function checkUserCanModifyOrDelete( $idSite, $annotation ) - { - if (!$annotation['canEditOrDelete']) - { - throw new Exception(Piwik_Translate('Annotations_YouCannotModifyThisNote')); - } - } - - /** - * Throws if the current user is not allowed to create annotations for a site. - * - * @param int $idSite The site ID. - * @throws Exception if the current user is anonymous or does not have view access - * for site w/ id=$idSite. - */ - private static function checkUserCanAddNotesFor( $idSite ) - { - if (!Piwik_Annotations_AnnotationList::canUserAddNotesFor($idSite)) - { - throw new Exception("The current user is not allowed to add notes for site #$idSite."); - } - } - - /** - * Returns start & end dates for the range described by a period and optional lastN - * argument. - * - * @param string $date|false The start date of the period (or the date range of a range - * period). - * @param string $period The period type ('day', 'week', 'month', 'year' or 'range'). - * @param int|false $lastN Whether to include the last N periods in the range or not. - * Ignored if period == range. - * - * @ignore - */ - public static function getDateRangeForPeriod( $date, $period, $lastN = false ) - { - if ($date === false) - { - return array(false, false); - } - - // if the range is just a normal period (or the period is a range in which case lastN is ignored) - if ($lastN === false - || $period == 'range') - { - if ($period == 'range') - { - $oPeriod = new Piwik_Period_Range('day', $date); - } - else - { - $oPeriod = Piwik_Period::factory($period, Piwik_Date::factory($date)); - } - - $startDate = $oPeriod->getDateStart(); - $endDate = $oPeriod->getDateEnd(); - } - else // if the range includes the last N periods - { - list($date, $lastN) = - Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution::getDateRangeAndLastN($period, $date, $lastN); - list($startDate, $endDate) = explode(',', $date); - - $startDate = Piwik_Date::factory($startDate); - $endDate = Piwik_Date::factory($endDate); - } - return array($startDate, $endDate); - } - - /** - * Utility function, makes sure idSite string has only one site ID and throws if - * otherwise. - */ - private function checkSingleIdSite( $idSite, $extraMessage ) - { - // can only add a note to one site - if (!is_numeric($idSite)) - { - throw new Exception("Invalid idSite: '$idSite'. $extraMessage"); - } - } - - /** - * Utility function, makes sure date string is valid date, and throws if - * otherwise. - */ - private function checkDateIsValid( $date, $canBeNull = false ) - { - if ($date === null - && $canBeNull) - { - return; - } - - Piwik_Date::factory($date); - } + static private $instance = null; + + /** + * Returns this API's singleton instance. + * + * @return Piwik_Annotations_API + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + /** + * Create a new annotation for a site. + * + * @param string $idSite The site ID to add the annotation to. + * @param string $date The date the annotation is attached to. + * @param string $note The text of the annotation. + * @param string $starred Either 0 or 1. Whether the annotation should be starred. + * @return array Returns an array of two elements. The first element (indexed by + * 'annotation') is the new annotation. The second element (indexed + * by 'idNote' is the new note's ID). + */ + public function add($idSite, $date, $note, $starred = 0) + { + $this->checkSingleIdSite($idSite, $extraMessage = "Note: Cannot add one note to multiple sites."); + $this->checkDateIsValid($date); + $this->checkUserCanAddNotesFor($idSite); + + // add, save & return a new annotation + $annotations = new Piwik_Annotations_AnnotationList($idSite); + + $newAnnotation = $annotations->add($idSite, $date, $note, $starred); + $annotations->save($idSite); + + return $newAnnotation; + } + + /** + * Modifies an annotation for a site and returns the modified annotation + * and its ID. + * + * If the current user is not allowed to modify an annotation, an exception + * will be thrown. A user can modify a note if: + * - the user has admin access for the site, OR + * - the user has view access, is not the anonymous user and is the user that + * created the note + * + * @param string $idSite The site ID to add the annotation to. + * @param string $idNote The ID of the note. + * @param string|null $date The date the annotation is attached to. If null, the annotation's + * date is not modified. + * @param string|null $note The text of the annotation. If null, the annotation's text + * is not modified. + * @param string|null $starred Either 0 or 1. Whether the annotation should be starred. + * If null, the annotation is not starred/un-starred. + * @return array Returns an array of two elements. The first element (indexed by + * 'annotation') is the new annotation. The second element (indexed + * by 'idNote' is the new note's ID). + */ + public function save($idSite, $idNote, $date = null, $note = null, $starred = null) + { + $this->checkSingleIdSite($idSite, $extraMessage = "Note: Cannot modify more than one note at a time."); + $this->checkDateIsValid($date, $canBeNull = true); + + // get the annotations for the site + $annotations = new Piwik_Annotations_AnnotationList($idSite); + + // check permissions + $this->checkUserCanModifyOrDelete($idSite, $annotations->get($idSite, $idNote)); + + // modify the annotation, and save the whole list + $annotations->update($idSite, $idNote, $date, $note, $starred); + $annotations->save($idSite); + + return $annotations->get($idSite, $idNote); + } + + /** + * Removes an annotation from a site's list of annotations. + * + * If the current user is not allowed to delete the annotation, an exception + * will be thrown. A user can delete a note if: + * - the user has admin access for the site, OR + * - the user has view access, is not the anonymous user and is the user that + * created the note + * + * @param string $idSite The site ID to add the annotation to. + * @param string $idNote The ID of the note to delete. + */ + public function delete($idSite, $idNote) + { + $this->checkSingleIdSite($idSite, $extraMessage = "Note: Cannot delete multiple notes."); + + $annotations = new Piwik_Annotations_AnnotationList($idSite); + + // check permissions + $this->checkUserCanModifyOrDelete($idSite, $annotations->get($idSite, $idNote)); + + // remove the note & save the list + $annotations->remove($idSite, $idNote); + $annotations->save($idSite); + } + + /** + * Returns a single note for one site. + * + * @param string $idSite The site ID to add the annotation to. + * @param string $idNote The ID of the note to get. + * @return array The annotation. It will contain the following properties: + * - date: The date the annotation was recorded for. + * - note: The note text. + * - starred: Whether the note is starred or not. + * - user: The user that created the note. + * - canEditOrDelete: Whether the user that called this method can edit or + * delete the annotation returned. + */ + public function get($idSite, $idNote) + { + $this->checkSingleIdSite($idSite, $extraMessage = "Note: Specify only one site ID when getting ONE note."); + Piwik::checkUserHasViewAccess($idSite); + + // get single annotation + $annotations = new Piwik_Annotations_AnnotationList($idSite); + return $annotations->get($idSite, $idNote); + } + + /** + * Returns every annotation for a specific site within a specific date range. + * The date range is specified by a date, the period type (day/week/month/year) + * and an optional number of N periods in the past to include. + * + * @param string $idSite The site ID to add the annotation to. Can be one ID or + * a list of site IDs. + * @param string|false $date The date of the period. + * @param string $period The period type. + * @param int|false $lastN Whether to include the last N number of periods in the + * date range or not. + * @return array An array that indexes arrays of annotations by site ID. ie, + * array( + * 5 => array( + * array(...), // annotation #1 + * array(...), // annotation #2 + * ), + * 8 => array(...) + * ) + */ + public function getAll($idSite, $date = false, $period = 'day', $lastN = false) + { + Piwik::checkUserHasViewAccess($idSite); + + $annotations = new Piwik_Annotations_AnnotationList($idSite); + + // if date/period are supplied, determine start/end date for search + list($startDate, $endDate) = self::getDateRangeForPeriod($date, $period, $lastN); + + return $annotations->search($startDate, $endDate); + } + + /** + * Returns the count of annotations for a list of periods, including the count of + * starred annotations. + * + * @param string $idSite The site ID to add the annotation to. + * @param string|false $date The date of the period. + * @param string $period The period type. + * @param int|false $lastN Whether to get counts for the last N number of periods or not. + * @return array An array mapping site IDs to arrays holding dates & the count of + * annotations made for those dates. eg, + * array( + * 5 => array( + * array('2012-01-02', array('count' => 4, 'starred' => 2)), + * array('2012-01-03', array('count' => 0, 'starred' => 0)), + * array('2012-01-04', array('count' => 2, 'starred' => 0)), + * ), + * 6 => array( + * array('2012-01-02', array('count' => 1, 'starred' => 0)), + * array('2012-01-03', array('count' => 4, 'starred' => 3)), + * array('2012-01-04', array('count' => 2, 'starred' => 0)), + * ), + * ... + * ) + */ + public function getAnnotationCountForDates($idSite, $date, $period, $lastN = false, $getAnnotationText = false) + { + Piwik::checkUserHasViewAccess($idSite); + + // get start & end date for request. lastN is ignored if $period == 'range' + list($startDate, $endDate) = self::getDateRangeForPeriod($date, $period, $lastN); + if ($period == 'range') { + $period = 'day'; + } + + // create list of dates + $dates = array(); + for (; $startDate->getTimestamp() <= $endDate->getTimestamp(); $startDate = $startDate->addPeriod(1, $period)) { + $dates[] = $startDate; + } + // we add one for the end of the last period (used in for loop below to bound annotation dates) + $dates[] = $startDate; + + // get annotations for the site + $annotations = new Piwik_Annotations_AnnotationList($idSite); + + // create result w/ 0-counts + $result = array(); + for ($i = 0; $i != count($dates) - 1; ++$i) { + $date = $dates[$i]; + $nextDate = $dates[$i + 1]; + $strDate = $date->toString(); + + foreach ($annotations->getIdSites() as $idSite) { + $result[$idSite][$strDate] = $annotations->count($idSite, $date, $nextDate); + + // if only one annotation, return the one annotation's text w/ the counts + if ($getAnnotationText + && $result[$idSite][$strDate]['count'] == 1 + ) { + $annotationsForSite = $annotations->search( + $date, Piwik_Date::factory($nextDate->getTimestamp() - 1), $idSite); + $annotation = reset($annotationsForSite[$idSite]); + + $result[$idSite][$strDate]['note'] = $annotation['note']; + } + } + } + + // convert associative array into array of pairs (so it can be traversed by index) + $pairResult = array(); + foreach ($result as $idSite => $counts) { + foreach ($counts as $date => $count) { + $pairResult[$idSite][] = array($date, $count); + } + } + return $pairResult; + } + + /** + * Throws if the current user is not allowed to modify or delete an annotation. + * + * @param int $idSite The site ID the annotation belongs to. + * @param array $annotation The annotation. + * @throws Exception if the current user is not allowed to modify/delete $annotation. + */ + private function checkUserCanModifyOrDelete($idSite, $annotation) + { + if (!$annotation['canEditOrDelete']) { + throw new Exception(Piwik_Translate('Annotations_YouCannotModifyThisNote')); + } + } + + /** + * Throws if the current user is not allowed to create annotations for a site. + * + * @param int $idSite The site ID. + * @throws Exception if the current user is anonymous or does not have view access + * for site w/ id=$idSite. + */ + private static function checkUserCanAddNotesFor($idSite) + { + if (!Piwik_Annotations_AnnotationList::canUserAddNotesFor($idSite)) { + throw new Exception("The current user is not allowed to add notes for site #$idSite."); + } + } + + /** + * Returns start & end dates for the range described by a period and optional lastN + * argument. + * + * @param string $date|false The start date of the period (or the date range of a range + * period). + * @param string $period The period type ('day', 'week', 'month', 'year' or 'range'). + * @param int|false $lastN Whether to include the last N periods in the range or not. + * Ignored if period == range. + * + * @ignore + */ + public static function getDateRangeForPeriod($date, $period, $lastN = false) + { + if ($date === false) { + return array(false, false); + } + + // if the range is just a normal period (or the period is a range in which case lastN is ignored) + if ($lastN === false + || $period == 'range' + ) { + if ($period == 'range') { + $oPeriod = new Piwik_Period_Range('day', $date); + } else { + $oPeriod = Piwik_Period::factory($period, Piwik_Date::factory($date)); + } + + $startDate = $oPeriod->getDateStart(); + $endDate = $oPeriod->getDateEnd(); + } else // if the range includes the last N periods + { + list($date, $lastN) = + Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution::getDateRangeAndLastN($period, $date, $lastN); + list($startDate, $endDate) = explode(',', $date); + + $startDate = Piwik_Date::factory($startDate); + $endDate = Piwik_Date::factory($endDate); + } + return array($startDate, $endDate); + } + + /** + * Utility function, makes sure idSite string has only one site ID and throws if + * otherwise. + */ + private function checkSingleIdSite($idSite, $extraMessage) + { + // can only add a note to one site + if (!is_numeric($idSite)) { + throw new Exception("Invalid idSite: '$idSite'. $extraMessage"); + } + } + + /** + * Utility function, makes sure date string is valid date, and throws if + * otherwise. + */ + private function checkDateIsValid($date, $canBeNull = false) + { + if ($date === null + && $canBeNull + ) { + return; + } + + Piwik_Date::factory($date); + } } diff --git a/plugins/Annotations/AnnotationList.php b/plugins/Annotations/AnnotationList.php index 47d897f2dc..daed59a554 100755 --- a/plugins/Annotations/AnnotationList.php +++ b/plugins/Annotations/AnnotationList.php @@ -12,440 +12,418 @@ /** * This class can be used to query & modify annotations for multiple sites * at once. - * + * * Example use: * $annotations = new Piwik_Annotations_AnnotationList($idSites = "1,2,5"); * $annotation = $annotations->get($idSite = 1, $idNote = 4); * // do stuff w/ annotation * $annotations->update($idSite = 2, $idNote = 4, $note = "This is the new text."); * $annotations->save($idSite); - * + * * Note: There is a concurrency issue w/ this code. If two users try to save * an annotation for the same site, it's possible one of their changes will * never get made (as it will be overwritten by the other's). - * + * * @package Piwik_Annotations */ class Piwik_Annotations_AnnotationList { - const ANNOTATION_COLLECTION_OPTION_SUFFIX = '_annotations'; - - /** - * List of site IDs this instance holds annotations for. - * - * @var array - */ - private $idSites; - - /** - * Array that associates lists of annotations with site IDs. - * - * @var array - */ - private $annotations; - - /** - * Constructor. Loads annotations from the database. - * - * @param string|int $idSites The list of site IDs to load annotations for. - */ - public function __construct( $idSites ) - { - $this->idSites = Piwik_Site::getIdSitesFromIdSitesString($idSites); - $this->annotations = $this->getAnnotationsForSite(); - } - - /** - * Returns the list of site IDs this list contains annotations for. - * - * @return array - */ - public function getIdSites() - { - return $this->idSites; - } - - /** - * Creates a new annotation for a site. This method does not perist the result. - * To save the new annotation in the database, call $this->save. - * - * @param int $idSite The ID of the site to add an annotation to. - * @param string $date The date the annotation is in reference to. - * @param string $note The text of the new annotation. - * @param int $starred Either 1 or 0. If 1, the new annotation has been starred, - * otherwise it will start out unstarred. - * @return array The added annotation. - * @throws Exception if $idSite is not an ID that was supplied upon construction. - */ - public function add($idSite, $date, $note, $starred = 0) - { - $this->checkIdSiteIsLoaded($idSite); - - $this->annotations[$idSite][] = self::makeAnnotation($date, $note, $starred); - - // get the id of the new annotation - end($this->annotations[$idSite]); - $newNoteId = key($this->annotations[$idSite]); - - return $this->get($idSite, $newNoteId); - } - - /** - * Persists the annotations list for a site, overwriting whatever exists. - * - * @param int $idSite The ID of the site to save annotations for. - * @throws Exception if $idSite is not an ID that was supplied upon construction. - */ - public function save($idSite) - { - $this->checkIdSiteIsLoaded($idSite); - - $optionName = self::getAnnotationCollectionOptionName($idSite); - Piwik_SetOption($optionName, serialize($this->annotations[$idSite])); - } - - /** - * Modifies an annotation in this instance's collection of annotations. - * - * Note: This method does not perist the change in the DB. The save method must - * be called for that. - * - * @param int $idSite The ID of the site whose annotation will be updated. - * @param int $idNote The ID of the note. - * @param string|null $date The new date of the annotation, eg '2012-01-01'. If - * null, no change is made. - * @param string|null $note The new text of the annotation. If null, no change - * is made. - * @param int|null $starred Either 1 or 0, whether the annotation should be - * starred or not. If null, no change is made. - * @throws Exception if $idSite is not an ID that was supplied upon construction. - * @throws Exception if $idNote does not refer to valid note for the site. - */ - public function update( $idSite, $idNote, $date = null, $note = null, $starred = null ) - { - $this->checkIdSiteIsLoaded($idSite); - $this->checkNoteExists($idSite, $idNote); - - $annotation =& $this->annotations[$idSite][$idNote]; - if ($date !== null) - { - $annotation['date'] = $date; - } - if ($note !== null) - { - $annotation['note'] = $note; - } - if ($starred !== null) - { - $annotation['starred'] = $starred; - } - } - - /** - * Removes a note from a site's collection of annotations. - * - * Note: This method does not perist the change in the DB. The save method must - * be called for that. - * - * @param int $idSite The ID of the site whose annotation will be updated. - * @param int $idNote The ID of the note. - * @throws Exception if $idSite is not an ID that was supplied upon construction. - * @throws Exception if $idNote does not refer to valid note for the site. - */ - public function remove( $idSite, $idNote ) - { - $this->checkIdSiteIsLoaded($idSite); - $this->checkNoteExists($idSite, $idNote); - - unset($this->annotations[$idSite][$idNote]); - } - - /** - * Retrieves an annotation by ID. - * - * This function returns an array with the following elements: - * - idNote: The ID of the annotation. - * - date: The date of the annotation. - * - note: The text of the annotation. - * - starred: 1 or 0, whether the annotation is stared; - * - user: (unless current user is anonymous) The user that created the annotation. - * - canEditOrDelete: True if the user can edit/delete the annotation. - * - * @param int $idSite The ID of the site to get an annotation for. - * @param int $idNote The ID of the note to get. - * @param array The annotation. - * @throws Exception if $idSite is not an ID that was supplied upon construction. - * @throws Exception if $idNote does not refer to valid note for the site. - */ - public function get( $idSite, $idNote ) - { - $this->checkIdSiteIsLoaded($idSite); - $this->checkNoteExists($idSite, $idNote); - - $annotation = $this->annotations[$idSite][$idNote]; - $this->augmentAnnotationData($idSite, $idNote, $annotation); - return $annotation; - } - - /** - * Returns all annotations within a specific date range. The result is - * an array that maps site IDs with arrays of annotations within the range. - * - * Note: The date range is inclusive. - * - * @see self::get for info on what attributes stored within annotations. - * - * @param Piwik_Date|false $startDate The start of the date range. - * @param Piwik_Date|false $endDate The end of the date range. - * @param string|int|array|false $idSite IDs of the sites whose annotations to - * search through. - * @return array Array mapping site IDs with arrays of annotations, eg: - * array( - * '5' => array( - * array(...), // annotation - * array(...), // annotation - * ... - * ), - * '6' => array( - * array(...), // annotation - * array(...), // annotation - * ... - * ), - * ) - */ - public function search( $startDate, $endDate, $idSite = false ) - { - if ($idSite) - { - $idSites = Piwik_Site::getIdSitesFromIdSitesString($idSite); - } - else - { - $idSites = array_keys($this->annotations); - } - - // collect annotations that are within the right date range & belong to the right - // site - $result = array(); - foreach ($idSites as $idSite) - { - if (!isset($this->annotations[$idSite])) - { - continue; - } - - foreach ($this->annotations[$idSite] as $idNote => $annotation) - { - if ($startDate !== false) - { - $annotationDate = Piwik_Date::factory($annotation['date']); - if ($annotationDate->getTimestamp() < $startDate->getTimestamp() - || $annotationDate->getTimestamp() > $endDate->getTimestamp()) - { - continue; - } - } - - $this->augmentAnnotationData($idSite, $idNote, $annotation); - $result[$idSite][] = $annotation; - } - - // sort by annotation date - if (!empty($result[$idSite])) - { - uasort($result[$idSite], array($this, 'compareAnnotationDate')); - } - } - return $result; - } - - /** - * Counts annotations & starred annotations within a date range and returns - * the counts. The date range includes the start date, but not the end date. - * - * @param int $idSite The ID of the site to count annotations for. - * @param string|false $startDate The start date of the range or false if no - * range check is desired. - * @param string|false $endDate The end date of the range or false if no - * range check is desired. - * @return array eg, array('count' => 5, 'starred' => 2) - */ - public function count( $idSite, $startDate, $endDate ) - { - $this->checkIdSiteIsLoaded($idSite); - - // search includes end date, and count should not, so subtract one from the timestamp - $annotations = $this->search($startDate, Piwik_Date::factory($endDate->getTimestamp() - 1)); - - // count the annotations - $count = $starred = 0; - if (!empty($annotations[$idSite])) - { - $count = count($annotations[$idSite]); - foreach ($annotations[$idSite] as $annotation) - { - if ($annotation['starred']) - { - ++$starred; - } - } - } - - return array('count' => $count, 'starred' => $starred); - } - - /** - * Utility function. Creates a new annotation. - * - * @param string $date - * @param string $note - * @param int $starred - */ - private function makeAnnotation( $date, $note, $starred = 0 ) - { - return array('date' => $date, - 'note' => $note, - 'starred' => (int)$starred, - 'user' => Piwik::getCurrentUserLogin()); - } - - /** - * Retrieves annotations from the database for the sites supplied to the - * constructor. - * - * @return array Lists of annotations mapped by site ID. - */ - private function getAnnotationsForSite() - { - $result = array(); - foreach ($this->idSites as $id) - { - $optionName = self::getAnnotationCollectionOptionName($id); - $serialized = Piwik_GetOption($optionName); - - if ($serialized !== false) - { - $result[$id] = unserialize($serialized); - } - else - { - $result[$id] = array(); - } - } - return $result; - } - - /** - * Utility function that checks if a site ID was supplied and if not, - * throws an exception. - * - * We can only modify/read annotations for sites that we've actually - * loaded the annotations for. - * - * @param int $idSite - * @throws Exception - */ - private function checkIdSiteIsLoaded( $idSite ) - { - if (!in_array($idSite, $this->idSites)) - { - throw new Exception("This AnnotationList was not initialized with idSite '$idSite'."); - } - } - - /** - * Utility function that checks if a note exists for a site, and if not, - * throws an exception. - * - * @param int $idSite - * @param int $idNote - * @throws Exception - */ - private function checkNoteExists( $idSite, $idNote ) - { - if (empty($this->annotations[$idSite][$idNote])) - { - throw new Exception("There is no note with id '$idNote' for site with id '$idSite'."); - } - } - - /** - * Returns true if the current user can modify or delete a specific annotation. - * - * A user can modify/delete a note if the user has admin access for the site OR - * the user has view access, is not the anonymous user and is the user that - * created the note in question. - * - * @param int $idSite The site ID the annotation belongs to. - * @param array $annotation The annotation. - * @return bool - */ - public static function canUserModifyOrDelete( $idSite, $annotation ) - { - // user can save if user is admin or if has view access, is not anonymous & is user who wrote note - $canEdit = Piwik::isUserHasAdminAccess($idSite) - || (!Piwik::isUserIsAnonymous() - && Piwik::getCurrentUserLogin() == $annotation['user']); - return $canEdit; - } - - /** - * Adds extra data to an annotation, including the annotation's ID and whether - * the current user can edit or delete it. - * - * Also, if the current user is anonymous, the user attribute is removed. - * - * @param int $idSite - * @param int $idNote - * @param array $annotation - */ - private function augmentAnnotationData( $idSite, $idNote, &$annotation ) - { - $annotation['idNote'] = $idNote; - $annotation['canEditOrDelete'] = self::canUserModifyOrDelete($idSite, $annotation); - - // we don't supply user info if the current user is anonymous - if (Piwik::isUserIsAnonymous()) - { - unset($annotation['user']); - } - } - - /** - * Utility function that compares two annotations. - * - * @param array $lhs An annotation. - * @param array $rhs An annotation. - * @return int -1, 0 or 1 - */ - public function compareAnnotationDate( $lhs, $rhs ) - { - if ($lhs['date'] == $rhs['date']) - { - return $lhs['idNote'] <= $rhs['idNote'] ? -1 : 1; - } - - return $lhs['date'] < $rhs['date'] ? -1 : 1; // string comparison works because date format should be YYYY-MM-DD - } - - /** - * Returns true if the current user can add notes for a specific site. - * - * @param int $idSite The site to add notes to. - */ - public static function canUserAddNotesFor( $idSite ) - { - return Piwik::isUserHasViewAccess($idSite) - && !Piwik::isUserIsAnonymous($idSite); - } - - /** - * Returns the option name used to store annotations for a site. - * - * @param int $idSite The site ID. - */ - public static function getAnnotationCollectionOptionName( $idSite ) - { - return $idSite.self::ANNOTATION_COLLECTION_OPTION_SUFFIX; - } + const ANNOTATION_COLLECTION_OPTION_SUFFIX = '_annotations'; + + /** + * List of site IDs this instance holds annotations for. + * + * @var array + */ + private $idSites; + + /** + * Array that associates lists of annotations with site IDs. + * + * @var array + */ + private $annotations; + + /** + * Constructor. Loads annotations from the database. + * + * @param string|int $idSites The list of site IDs to load annotations for. + */ + public function __construct($idSites) + { + $this->idSites = Piwik_Site::getIdSitesFromIdSitesString($idSites); + $this->annotations = $this->getAnnotationsForSite(); + } + + /** + * Returns the list of site IDs this list contains annotations for. + * + * @return array + */ + public function getIdSites() + { + return $this->idSites; + } + + /** + * Creates a new annotation for a site. This method does not perist the result. + * To save the new annotation in the database, call $this->save. + * + * @param int $idSite The ID of the site to add an annotation to. + * @param string $date The date the annotation is in reference to. + * @param string $note The text of the new annotation. + * @param int $starred Either 1 or 0. If 1, the new annotation has been starred, + * otherwise it will start out unstarred. + * @return array The added annotation. + * @throws Exception if $idSite is not an ID that was supplied upon construction. + */ + public function add($idSite, $date, $note, $starred = 0) + { + $this->checkIdSiteIsLoaded($idSite); + + $this->annotations[$idSite][] = self::makeAnnotation($date, $note, $starred); + + // get the id of the new annotation + end($this->annotations[$idSite]); + $newNoteId = key($this->annotations[$idSite]); + + return $this->get($idSite, $newNoteId); + } + + /** + * Persists the annotations list for a site, overwriting whatever exists. + * + * @param int $idSite The ID of the site to save annotations for. + * @throws Exception if $idSite is not an ID that was supplied upon construction. + */ + public function save($idSite) + { + $this->checkIdSiteIsLoaded($idSite); + + $optionName = self::getAnnotationCollectionOptionName($idSite); + Piwik_SetOption($optionName, serialize($this->annotations[$idSite])); + } + + /** + * Modifies an annotation in this instance's collection of annotations. + * + * Note: This method does not perist the change in the DB. The save method must + * be called for that. + * + * @param int $idSite The ID of the site whose annotation will be updated. + * @param int $idNote The ID of the note. + * @param string|null $date The new date of the annotation, eg '2012-01-01'. If + * null, no change is made. + * @param string|null $note The new text of the annotation. If null, no change + * is made. + * @param int|null $starred Either 1 or 0, whether the annotation should be + * starred or not. If null, no change is made. + * @throws Exception if $idSite is not an ID that was supplied upon construction. + * @throws Exception if $idNote does not refer to valid note for the site. + */ + public function update($idSite, $idNote, $date = null, $note = null, $starred = null) + { + $this->checkIdSiteIsLoaded($idSite); + $this->checkNoteExists($idSite, $idNote); + + $annotation =& $this->annotations[$idSite][$idNote]; + if ($date !== null) { + $annotation['date'] = $date; + } + if ($note !== null) { + $annotation['note'] = $note; + } + if ($starred !== null) { + $annotation['starred'] = $starred; + } + } + + /** + * Removes a note from a site's collection of annotations. + * + * Note: This method does not perist the change in the DB. The save method must + * be called for that. + * + * @param int $idSite The ID of the site whose annotation will be updated. + * @param int $idNote The ID of the note. + * @throws Exception if $idSite is not an ID that was supplied upon construction. + * @throws Exception if $idNote does not refer to valid note for the site. + */ + public function remove($idSite, $idNote) + { + $this->checkIdSiteIsLoaded($idSite); + $this->checkNoteExists($idSite, $idNote); + + unset($this->annotations[$idSite][$idNote]); + } + + /** + * Retrieves an annotation by ID. + * + * This function returns an array with the following elements: + * - idNote: The ID of the annotation. + * - date: The date of the annotation. + * - note: The text of the annotation. + * - starred: 1 or 0, whether the annotation is stared; + * - user: (unless current user is anonymous) The user that created the annotation. + * - canEditOrDelete: True if the user can edit/delete the annotation. + * + * @param int $idSite The ID of the site to get an annotation for. + * @param int $idNote The ID of the note to get. + * @param array The annotation. + * @throws Exception if $idSite is not an ID that was supplied upon construction. + * @throws Exception if $idNote does not refer to valid note for the site. + */ + public function get($idSite, $idNote) + { + $this->checkIdSiteIsLoaded($idSite); + $this->checkNoteExists($idSite, $idNote); + + $annotation = $this->annotations[$idSite][$idNote]; + $this->augmentAnnotationData($idSite, $idNote, $annotation); + return $annotation; + } + + /** + * Returns all annotations within a specific date range. The result is + * an array that maps site IDs with arrays of annotations within the range. + * + * Note: The date range is inclusive. + * + * @see self::get for info on what attributes stored within annotations. + * + * @param Piwik_Date|false $startDate The start of the date range. + * @param Piwik_Date|false $endDate The end of the date range. + * @param string|int|array|false $idSite IDs of the sites whose annotations to + * search through. + * @return array Array mapping site IDs with arrays of annotations, eg: + * array( + * '5' => array( + * array(...), // annotation + * array(...), // annotation + * ... + * ), + * '6' => array( + * array(...), // annotation + * array(...), // annotation + * ... + * ), + * ) + */ + public function search($startDate, $endDate, $idSite = false) + { + if ($idSite) { + $idSites = Piwik_Site::getIdSitesFromIdSitesString($idSite); + } else { + $idSites = array_keys($this->annotations); + } + + // collect annotations that are within the right date range & belong to the right + // site + $result = array(); + foreach ($idSites as $idSite) { + if (!isset($this->annotations[$idSite])) { + continue; + } + + foreach ($this->annotations[$idSite] as $idNote => $annotation) { + if ($startDate !== false) { + $annotationDate = Piwik_Date::factory($annotation['date']); + if ($annotationDate->getTimestamp() < $startDate->getTimestamp() + || $annotationDate->getTimestamp() > $endDate->getTimestamp() + ) { + continue; + } + } + + $this->augmentAnnotationData($idSite, $idNote, $annotation); + $result[$idSite][] = $annotation; + } + + // sort by annotation date + if (!empty($result[$idSite])) { + uasort($result[$idSite], array($this, 'compareAnnotationDate')); + } + } + return $result; + } + + /** + * Counts annotations & starred annotations within a date range and returns + * the counts. The date range includes the start date, but not the end date. + * + * @param int $idSite The ID of the site to count annotations for. + * @param string|false $startDate The start date of the range or false if no + * range check is desired. + * @param string|false $endDate The end date of the range or false if no + * range check is desired. + * @return array eg, array('count' => 5, 'starred' => 2) + */ + public function count($idSite, $startDate, $endDate) + { + $this->checkIdSiteIsLoaded($idSite); + + // search includes end date, and count should not, so subtract one from the timestamp + $annotations = $this->search($startDate, Piwik_Date::factory($endDate->getTimestamp() - 1)); + + // count the annotations + $count = $starred = 0; + if (!empty($annotations[$idSite])) { + $count = count($annotations[$idSite]); + foreach ($annotations[$idSite] as $annotation) { + if ($annotation['starred']) { + ++$starred; + } + } + } + + return array('count' => $count, 'starred' => $starred); + } + + /** + * Utility function. Creates a new annotation. + * + * @param string $date + * @param string $note + * @param int $starred + */ + private function makeAnnotation($date, $note, $starred = 0) + { + return array('date' => $date, + 'note' => $note, + 'starred' => (int)$starred, + 'user' => Piwik::getCurrentUserLogin()); + } + + /** + * Retrieves annotations from the database for the sites supplied to the + * constructor. + * + * @return array Lists of annotations mapped by site ID. + */ + private function getAnnotationsForSite() + { + $result = array(); + foreach ($this->idSites as $id) { + $optionName = self::getAnnotationCollectionOptionName($id); + $serialized = Piwik_GetOption($optionName); + + if ($serialized !== false) { + $result[$id] = unserialize($serialized); + } else { + $result[$id] = array(); + } + } + return $result; + } + + /** + * Utility function that checks if a site ID was supplied and if not, + * throws an exception. + * + * We can only modify/read annotations for sites that we've actually + * loaded the annotations for. + * + * @param int $idSite + * @throws Exception + */ + private function checkIdSiteIsLoaded($idSite) + { + if (!in_array($idSite, $this->idSites)) { + throw new Exception("This AnnotationList was not initialized with idSite '$idSite'."); + } + } + + /** + * Utility function that checks if a note exists for a site, and if not, + * throws an exception. + * + * @param int $idSite + * @param int $idNote + * @throws Exception + */ + private function checkNoteExists($idSite, $idNote) + { + if (empty($this->annotations[$idSite][$idNote])) { + throw new Exception("There is no note with id '$idNote' for site with id '$idSite'."); + } + } + + /** + * Returns true if the current user can modify or delete a specific annotation. + * + * A user can modify/delete a note if the user has admin access for the site OR + * the user has view access, is not the anonymous user and is the user that + * created the note in question. + * + * @param int $idSite The site ID the annotation belongs to. + * @param array $annotation The annotation. + * @return bool + */ + public static function canUserModifyOrDelete($idSite, $annotation) + { + // user can save if user is admin or if has view access, is not anonymous & is user who wrote note + $canEdit = Piwik::isUserHasAdminAccess($idSite) + || (!Piwik::isUserIsAnonymous() + && Piwik::getCurrentUserLogin() == $annotation['user']); + return $canEdit; + } + + /** + * Adds extra data to an annotation, including the annotation's ID and whether + * the current user can edit or delete it. + * + * Also, if the current user is anonymous, the user attribute is removed. + * + * @param int $idSite + * @param int $idNote + * @param array $annotation + */ + private function augmentAnnotationData($idSite, $idNote, &$annotation) + { + $annotation['idNote'] = $idNote; + $annotation['canEditOrDelete'] = self::canUserModifyOrDelete($idSite, $annotation); + + // we don't supply user info if the current user is anonymous + if (Piwik::isUserIsAnonymous()) { + unset($annotation['user']); + } + } + + /** + * Utility function that compares two annotations. + * + * @param array $lhs An annotation. + * @param array $rhs An annotation. + * @return int -1, 0 or 1 + */ + public function compareAnnotationDate($lhs, $rhs) + { + if ($lhs['date'] == $rhs['date']) { + return $lhs['idNote'] <= $rhs['idNote'] ? -1 : 1; + } + + return $lhs['date'] < $rhs['date'] ? -1 : 1; // string comparison works because date format should be YYYY-MM-DD + } + + /** + * Returns true if the current user can add notes for a specific site. + * + * @param int $idSite The site to add notes to. + */ + public static function canUserAddNotesFor($idSite) + { + return Piwik::isUserHasViewAccess($idSite) + && !Piwik::isUserIsAnonymous($idSite); + } + + /** + * Returns the option name used to store annotations for a site. + * + * @param int $idSite The site ID. + */ + public static function getAnnotationCollectionOptionName($idSite) + { + return $idSite . self::ANNOTATION_COLLECTION_OPTION_SUFFIX; + } } diff --git a/plugins/Annotations/Annotations.php b/plugins/Annotations/Annotations.php index 1b0503862a..937500053a 100755 --- a/plugins/Annotations/Annotations.php +++ b/plugins/Annotations/Annotations.php @@ -12,58 +12,58 @@ /** * Annotations plugins. Provides the ability to attach text notes to * dates for each sites. Notes can be viewed, modified, deleted or starred. - * + * * @package Piwik_Annotations */ class Piwik_Annotations extends Piwik_Plugin { - /** - * Returns information about this plugin. - * - * @return array - */ - public function getInformation() - { - return array( - 'description' => Piwik_Translate('Annotations_PluginDescription'), - 'author' => 'Piwik', - 'author_homepage' => 'http://piwik.org/', - 'version' => Piwik_Version::VERSION, - ); - } - - /** - * Returns list of event hooks. - * - * @return array - */ - public function getListHooksRegistered() - { - return array( - 'AssetManager.getCssFiles' => 'getCssFiles', - 'AssetManager.getJsFiles' => 'getJsFiles' - ); - } + /** + * Returns information about this plugin. + * + * @return array + */ + public function getInformation() + { + return array( + 'description' => Piwik_Translate('Annotations_PluginDescription'), + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'version' => Piwik_Version::VERSION, + ); + } + + /** + * Returns list of event hooks. + * + * @return array + */ + public function getListHooksRegistered() + { + return array( + 'AssetManager.getCssFiles' => 'getCssFiles', + 'AssetManager.getJsFiles' => 'getJsFiles' + ); + } - /** - * Adds css files for this plugin to the list in the event notification. - * - * @param Piwik_Event_Notification $notification notification object - */ - function getCssFiles( $notification ) - { - $cssFiles = &$notification->getNotificationObject(); - $cssFiles[] = "plugins/Annotations/templates/styles.css"; - } + /** + * Adds css files for this plugin to the list in the event notification. + * + * @param Piwik_Event_Notification $notification notification object + */ + function getCssFiles($notification) + { + $cssFiles = & $notification->getNotificationObject(); + $cssFiles[] = "plugins/Annotations/templates/styles.css"; + } - /** - * Adds js files for this plugin to the list in the event notification. - * - * @param Piwik_Event_Notification $notification notification object - */ - function getJsFiles( $notification ) - { - $jsFiles = &$notification->getNotificationObject(); - $jsFiles[] = "plugins/Annotations/templates/annotations.js"; - } + /** + * Adds js files for this plugin to the list in the event notification. + * + * @param Piwik_Event_Notification $notification notification object + */ + function getJsFiles($notification) + { + $jsFiles = & $notification->getNotificationObject(); + $jsFiles[] = "plugins/Annotations/templates/annotations.js"; + } } diff --git a/plugins/Annotations/Controller.php b/plugins/Annotations/Controller.php index 0a235a2200..9b9f9e352c 100755 --- a/plugins/Annotations/Controller.php +++ b/plugins/Annotations/Controller.php @@ -11,216 +11,206 @@ /** * Controller for the Annotations plugin. - * + * * @package Piwik_Annotations */ class Piwik_Annotations_Controller extends Piwik_Controller { - /** - * Controller action that returns HTML displaying annotations for a site and - * specific date range. - * - * Query Param Input: - * - idSite: The ID of the site to get annotations for. Only one allowed. - * - date: The date to get annotations for. If lastN is not supplied, this is the start date, - * otherwise the start date in the last period. - * - period: The period type. - * - lastN: If supplied, the last N # of periods will be included w/ the range specified - * by date + period. - * - * Output: - * - HTML displaying annotations for a specific range. - * - * @param bool $fetch True if the annotation manager should be returned as a string, - * false if it should be echo-ed. - * @param string $date Override for 'date' query parameter. - * @param string $period Override for 'period' query parameter. - * @param string $lastN Override for 'lastN' query parameter. - * @return string|void - */ - public function getAnnotationManager( $fetch = false, $date = false, $period = false, $lastN = false ) - { - $idSite = Piwik_Common::getRequestVar('idSite'); - - if ($date === false) - { - $date = Piwik_Common::getRequestVar('date', false); - } - - if ($period === false) - { - $period = Piwik_Common::getRequestVar('period', 'day'); - } - - if ($lastN === false) - { - $lastN = Piwik_Common::getRequestVar('lastN', false); - } - - // create & render the view - $view = Piwik_View::factory('annotationManager'); - - $allAnnotations = Piwik_API_Request::processRequest( - 'Annotations.getAll', array('date' => $date, 'period' => $period, 'lastN' => $lastN)); - $view->annotations = empty($allAnnotations[$idSite]) ? array() : $allAnnotations[$idSite]; - - $view->period = $period; - $view->lastN = $lastN; - - list($startDate, $endDate) = Piwik_Annotations_API::getDateRangeForPeriod($date, $period, $lastN); - $view->startDate = $startDate->toString(); - $view->endDate = $endDate->toString(); - - $dateFormat = Piwik_Translate('CoreHome_ShortDateFormatWithYear'); - $view->startDatePretty = $startDate->getLocalized($dateFormat); - $view->endDatePretty = $endDate->getLocalized($dateFormat); - - $view->canUserAddNotes = Piwik_Annotations_AnnotationList::canUserAddNotesFor($idSite); - - if ($fetch) - { - return $view->render(); - } - else - { - echo $view->render(); - } - } - - /** - * Controller action that modifies an annotation and returns HTML displaying - * the modified annotation. - * - * Query Param Input: - * - idSite: The ID of the site the annotation belongs to. Only one ID is allowed. - * - idNote: The ID of the annotation. - * - date: The new date value for the annotation. (optional) - * - note: The new text for the annotation. (optional) - * - starred: Either 1 or 0. Whether the note should be starred or not. (optional) - * - * Output: - * - HTML displaying modified annotation. - * - * If an optional query param is not supplied, that part of the annotation is - * not modified. - */ - public function saveAnnotation() - { - if ($_SERVER["REQUEST_METHOD"] == "POST") - { - $this->checkTokenInUrl(); - - $view = Piwik_View::factory('annotation'); - - // NOTE: permissions checked in API method - // save the annotation - $view->annotation = Piwik_API_Request::processRequest("Annotations.save"); - - echo $view->render(); - } - } - - /** - * Controller action that adds a new annotation for a site and returns new - * annotation manager HTML for the site and date range. - * - * Query Param Input: - * - idSite: The ID of the site to add an annotation to. - * - date: The date for the new annotation. - * - note: The text of the annotation. - * - starred: Either 1 or 0, whether the annotation should be starred or not. - * Defaults to 0. - * - managerDate: The date for the annotation manager. If a range is given, the start - * date is used for the new annotation. - * - managerPeriod: For rendering the annotation manager. @see self::getAnnotationManager - * for more info. - * - lastN: For rendering the annotation manager. @see self::getAnnotationManager - * for more info. - * Output: - * - @see self::getAnnotationManager - */ - public function addAnnotation() - { - if ($_SERVER["REQUEST_METHOD"] == "POST") - { - $this->checkTokenInUrl(); - - // the date used is for the annotation manager HTML that gets echo'd. we - // use this date for the new annotation, unless it is a date range, in - // which case we use the first date of the range. - $date = Piwik_Common::getRequestVar('date'); - if (strpos($date, ',') !== false) - { - $date = reset(explode(',', $date)); - } - - // add the annotation. NOTE: permissions checked in API method - Piwik_API_Request::processRequest("Annotations.add", array('date' => $date)); - - $managerDate = Piwik_Common::getRequestVar('managerDate', false); - $managerPeriod = Piwik_Common::getRequestVar('managerPeriod', false); - echo $this->getAnnotationManager($fetch = true, $managerDate, $managerPeriod); - } - } - - /** - * Controller action that deletes an annotation and returns new annotation - * manager HTML for the site & date range. - * - * Query Param Input: - * - idSite: The ID of the site this annotation belongs to. - * - idNote: The ID of the annotation to delete. - * - date: For rendering the annotation manager. @see self::getAnnotationManager - * for more info. - * - period: For rendering the annotation manager. @see self::getAnnotationManager - * for more info. - * - lastN: For rendering the annotation manager. @see self::getAnnotationManager - * for more info. - * - * Output: - * - @see self::getAnnotationManager - */ - public function deleteAnnotation() - { - if ($_SERVER["REQUEST_METHOD"] == "POST") - { - $this->checkTokenInUrl(); - - // delete annotation. NOTE: permissions checked in API method - Piwik_API_Request::processRequest("Annotations.delete"); - - echo $this->getAnnotationManager($fetch = true); - } - } - - /** - * Controller action that echo's HTML that displays marker icons for an - * evolution graph's x-axis. The marker icons still need to be positioned - * by the JavaScript. - * - * Query Param Input: - * - idSite: The ID of the site this annotation belongs to. Only one is allowed. - * - date: The date to check for annotations. If lastN is not supplied, this is - * the start of the date range used to check for annotations. If supplied, - * this is the start of the last period in the date range. - * - period: The period type. - * - lastN: If supplied, the last N # of periods are included in the date range - * used to check for annotations. - * - * Output: - * - HTML that displays marker icons for an evolution graph based on the - * number of annotations & starred annotations in the graph's date range. - */ - public function getEvolutionIcons() - { - // get annotation the count - $annotationCounts = Piwik_API_Request::processRequest( - "Annotations.getAnnotationCountForDates", array('getAnnotationText' => 1)); - - // create & render the view - $view = Piwik_View::factory('evolutionAnnotations'); - $view->annotationCounts = reset($annotationCounts); // only one idSite allowed for this action - - echo $view->render(); - } + /** + * Controller action that returns HTML displaying annotations for a site and + * specific date range. + * + * Query Param Input: + * - idSite: The ID of the site to get annotations for. Only one allowed. + * - date: The date to get annotations for. If lastN is not supplied, this is the start date, + * otherwise the start date in the last period. + * - period: The period type. + * - lastN: If supplied, the last N # of periods will be included w/ the range specified + * by date + period. + * + * Output: + * - HTML displaying annotations for a specific range. + * + * @param bool $fetch True if the annotation manager should be returned as a string, + * false if it should be echo-ed. + * @param string $date Override for 'date' query parameter. + * @param string $period Override for 'period' query parameter. + * @param string $lastN Override for 'lastN' query parameter. + * @return string|void + */ + public function getAnnotationManager($fetch = false, $date = false, $period = false, $lastN = false) + { + $idSite = Piwik_Common::getRequestVar('idSite'); + + if ($date === false) { + $date = Piwik_Common::getRequestVar('date', false); + } + + if ($period === false) { + $period = Piwik_Common::getRequestVar('period', 'day'); + } + + if ($lastN === false) { + $lastN = Piwik_Common::getRequestVar('lastN', false); + } + + // create & render the view + $view = Piwik_View::factory('annotationManager'); + + $allAnnotations = Piwik_API_Request::processRequest( + 'Annotations.getAll', array('date' => $date, 'period' => $period, 'lastN' => $lastN)); + $view->annotations = empty($allAnnotations[$idSite]) ? array() : $allAnnotations[$idSite]; + + $view->period = $period; + $view->lastN = $lastN; + + list($startDate, $endDate) = Piwik_Annotations_API::getDateRangeForPeriod($date, $period, $lastN); + $view->startDate = $startDate->toString(); + $view->endDate = $endDate->toString(); + + $dateFormat = Piwik_Translate('CoreHome_ShortDateFormatWithYear'); + $view->startDatePretty = $startDate->getLocalized($dateFormat); + $view->endDatePretty = $endDate->getLocalized($dateFormat); + + $view->canUserAddNotes = Piwik_Annotations_AnnotationList::canUserAddNotesFor($idSite); + + if ($fetch) { + return $view->render(); + } else { + echo $view->render(); + } + } + + /** + * Controller action that modifies an annotation and returns HTML displaying + * the modified annotation. + * + * Query Param Input: + * - idSite: The ID of the site the annotation belongs to. Only one ID is allowed. + * - idNote: The ID of the annotation. + * - date: The new date value for the annotation. (optional) + * - note: The new text for the annotation. (optional) + * - starred: Either 1 or 0. Whether the note should be starred or not. (optional) + * + * Output: + * - HTML displaying modified annotation. + * + * If an optional query param is not supplied, that part of the annotation is + * not modified. + */ + public function saveAnnotation() + { + if ($_SERVER["REQUEST_METHOD"] == "POST") { + $this->checkTokenInUrl(); + + $view = Piwik_View::factory('annotation'); + + // NOTE: permissions checked in API method + // save the annotation + $view->annotation = Piwik_API_Request::processRequest("Annotations.save"); + + echo $view->render(); + } + } + + /** + * Controller action that adds a new annotation for a site and returns new + * annotation manager HTML for the site and date range. + * + * Query Param Input: + * - idSite: The ID of the site to add an annotation to. + * - date: The date for the new annotation. + * - note: The text of the annotation. + * - starred: Either 1 or 0, whether the annotation should be starred or not. + * Defaults to 0. + * - managerDate: The date for the annotation manager. If a range is given, the start + * date is used for the new annotation. + * - managerPeriod: For rendering the annotation manager. @see self::getAnnotationManager + * for more info. + * - lastN: For rendering the annotation manager. @see self::getAnnotationManager + * for more info. + * Output: + * - @see self::getAnnotationManager + */ + public function addAnnotation() + { + if ($_SERVER["REQUEST_METHOD"] == "POST") { + $this->checkTokenInUrl(); + + // the date used is for the annotation manager HTML that gets echo'd. we + // use this date for the new annotation, unless it is a date range, in + // which case we use the first date of the range. + $date = Piwik_Common::getRequestVar('date'); + if (strpos($date, ',') !== false) { + $date = reset(explode(',', $date)); + } + + // add the annotation. NOTE: permissions checked in API method + Piwik_API_Request::processRequest("Annotations.add", array('date' => $date)); + + $managerDate = Piwik_Common::getRequestVar('managerDate', false); + $managerPeriod = Piwik_Common::getRequestVar('managerPeriod', false); + echo $this->getAnnotationManager($fetch = true, $managerDate, $managerPeriod); + } + } + + /** + * Controller action that deletes an annotation and returns new annotation + * manager HTML for the site & date range. + * + * Query Param Input: + * - idSite: The ID of the site this annotation belongs to. + * - idNote: The ID of the annotation to delete. + * - date: For rendering the annotation manager. @see self::getAnnotationManager + * for more info. + * - period: For rendering the annotation manager. @see self::getAnnotationManager + * for more info. + * - lastN: For rendering the annotation manager. @see self::getAnnotationManager + * for more info. + * + * Output: + * - @see self::getAnnotationManager + */ + public function deleteAnnotation() + { + if ($_SERVER["REQUEST_METHOD"] == "POST") { + $this->checkTokenInUrl(); + + // delete annotation. NOTE: permissions checked in API method + Piwik_API_Request::processRequest("Annotations.delete"); + + echo $this->getAnnotationManager($fetch = true); + } + } + + /** + * Controller action that echo's HTML that displays marker icons for an + * evolution graph's x-axis. The marker icons still need to be positioned + * by the JavaScript. + * + * Query Param Input: + * - idSite: The ID of the site this annotation belongs to. Only one is allowed. + * - date: The date to check for annotations. If lastN is not supplied, this is + * the start of the date range used to check for annotations. If supplied, + * this is the start of the last period in the date range. + * - period: The period type. + * - lastN: If supplied, the last N # of periods are included in the date range + * used to check for annotations. + * + * Output: + * - HTML that displays marker icons for an evolution graph based on the + * number of annotations & starred annotations in the graph's date range. + */ + public function getEvolutionIcons() + { + // get annotation the count + $annotationCounts = Piwik_API_Request::processRequest( + "Annotations.getAnnotationCountForDates", array('getAnnotationText' => 1)); + + // create & render the view + $view = Piwik_View::factory('evolutionAnnotations'); + $view->annotationCounts = reset($annotationCounts); // only one idSite allowed for this action + + echo $view->render(); + } } diff --git a/plugins/Annotations/templates/annotation.tpl b/plugins/Annotations/templates/annotation.tpl index e1cd5f6013..cc0d909a3b 100755 --- a/plugins/Annotations/templates/annotation.tpl +++ b/plugins/Annotations/templates/annotation.tpl @@ -1,43 +1,46 @@ <tr class="annotation" data-id="{$annotation.idNote}" data-date="{$annotation.date}"> - <td class="annotation-meta"> - <div class="annotation-star{if $annotation.canEditOrDelete} annotation-star-changeable{/if}" data-starred="{$annotation.starred}" {if $annotation.canEditOrDelete}title="{'Annotations_ClickToStarOrUnstar'|translate}"{/if}> - {if $annotation.starred} - <img src="themes/default/images/star.png"/> - {else} - <img src="themes/default/images/star_empty.png"/> - {/if} - </div> - <div class="annotation-period {if $annotation.canEditOrDelete}annotation-enter-edit-mode{/if}">({$annotation.date})</div> - {if $annotation.canEditOrDelete} - <div class="annotation-period-edit" style="display:none"> - <a href="#">{$annotation.date}</a> - <div class="datepicker" style="display:none"/> - </div> - {/if} - </td> - <td class="annotation-value"> - <div class="annotation-view-mode"> - <span {if $annotation.canEditOrDelete}title="{'Annotations_ClickToEdit'|translate}" class="annotation-enter-edit-mode"{/if}>{$annotation.note|unescape|escape:'html'}</span> - {if $annotation.canEditOrDelete} - <a href="#" class="edit-annotation annotation-enter-edit-mode" title="{'Annotations_ClickToEdit'|translate}">{'General_Edit'|translate}...</a> - {/if} - </div> - {if $annotation.canEditOrDelete} - <div class="annotation-edit-mode" style="display:none"> - <input class="annotation-edit" type="text" value="{$annotation.note|unescape|escape:'html'}"/> - <br/> - <input class="annotation-save submit" type="button" value="{'General_Save'|translate}"/> - <input class="annotation-cancel submit" type="button" value="{'General_Cancel'|translate}"/> - </div> - {/if} - </td> - {if isset($annotation.user) && $userLogin != 'anonymous'} - <td class="annotation-user-cell"> - <span class="annotation-user">{$annotation.user|unescape|escape:'html'}</span><br/> - {if $annotation.canEditOrDelete} - <a href="#" class="delete-annotation" style="display:none" title="{'Annotations_ClickToDelete'|translate}">{'General_Delete'|translate}</a> - {/if} - </td> - {/if} + <td class="annotation-meta"> + <div class="annotation-star{if $annotation.canEditOrDelete} annotation-star-changeable{/if}" data-starred="{$annotation.starred}" + {if $annotation.canEditOrDelete}title="{'Annotations_ClickToStarOrUnstar'|translate}"{/if}> + {if $annotation.starred} + <img src="themes/default/images/star.png"/> + {else} + <img src="themes/default/images/star_empty.png"/> + {/if} + </div> + <div class="annotation-period {if $annotation.canEditOrDelete}annotation-enter-edit-mode{/if}">({$annotation.date})</div> + {if $annotation.canEditOrDelete} + <div class="annotation-period-edit" style="display:none"> + <a href="#">{$annotation.date}</a> + + <div class="datepicker" style="display:none"/> + </div> + {/if} + </td> + <td class="annotation-value"> + <div class="annotation-view-mode"> + <span {if $annotation.canEditOrDelete}title="{'Annotations_ClickToEdit'|translate}" + class="annotation-enter-edit-mode"{/if}>{$annotation.note|unescape|escape:'html'}</span> + {if $annotation.canEditOrDelete} + <a href="#" class="edit-annotation annotation-enter-edit-mode" title="{'Annotations_ClickToEdit'|translate}">{'General_Edit'|translate}...</a> + {/if} + </div> + {if $annotation.canEditOrDelete} + <div class="annotation-edit-mode" style="display:none"> + <input class="annotation-edit" type="text" value="{$annotation.note|unescape|escape:'html'}"/> + <br/> + <input class="annotation-save submit" type="button" value="{'General_Save'|translate}"/> + <input class="annotation-cancel submit" type="button" value="{'General_Cancel'|translate}"/> + </div> + {/if} + </td> + {if isset($annotation.user) && $userLogin != 'anonymous'} + <td class="annotation-user-cell"> + <span class="annotation-user">{$annotation.user|unescape|escape:'html'}</span><br/> + {if $annotation.canEditOrDelete} + <a href="#" class="delete-annotation" style="display:none" title="{'Annotations_ClickToDelete'|translate}">{'General_Delete'|translate}</a> + {/if} + </td> + {/if} </tr> diff --git a/plugins/Annotations/templates/annotationManager.tpl b/plugins/Annotations/templates/annotationManager.tpl index 852c09bbc4..401c2458c9 100755 --- a/plugins/Annotations/templates/annotationManager.tpl +++ b/plugins/Annotations/templates/annotationManager.tpl @@ -1,27 +1,27 @@ <div class="annotation-manager" - {if $startDate neq $endDate}data-date="{$startDate},{$endDate}" data-period="range" - {else}data-date="{$startDate}" data-period="{$period}" - {/if}> + {if $startDate neq $endDate}data-date="{$startDate},{$endDate}" data-period="range" + {else}data-date="{$startDate}" data-period="{$period}" + {/if}> -<div class="annotations-header"> - <span>{'Annotations_Annotations'|translate}</span> -</div> + <div class="annotations-header"> + <span>{'Annotations_Annotations'|translate}</span> + </div> -<div class="annotation-list-range">{$startDatePretty}{if $startDate neq $endDate} — {$endDatePretty}{/if}</div> + <div class="annotation-list-range">{$startDatePretty}{if $startDate neq $endDate} — {$endDatePretty}{/if}</div> -<div class="annotation-list"> -{include file="Annotations/templates/annotations.tpl"} + <div class="annotation-list"> + {include file="Annotations/templates/annotations.tpl"} -<span class="loadingPiwik" style="display:none"><img src="themes/default/images/loading-blue.gif"/>{'General_Loading_js'|translate}</span> + <span class="loadingPiwik" style="display:none"><img src="themes/default/images/loading-blue.gif"/>{'General_Loading_js'|translate}</span> -</div> + </div> -<div class="annotation-controls"> - {if $canUserAddNotes} - <a href="#" class="add-annotation" title="{'Annotations_CreateNewAnnotation'|translate}">{'Annotations_CreateNewAnnotation'|translate}</a> - {elseif $userLogin eq 'anonymous'} - <a href="index.php?module=Login">{'Annotations_LoginToAnnotate'|translate}</a> - {/if} -</div> + <div class="annotation-controls"> + {if $canUserAddNotes} + <a href="#" class="add-annotation" title="{'Annotations_CreateNewAnnotation'|translate}">{'Annotations_CreateNewAnnotation'|translate}</a> + {elseif $userLogin eq 'anonymous'} + <a href="index.php?module=Login">{'Annotations_LoginToAnnotate'|translate}</a> + {/if} + </div> </div> diff --git a/plugins/Annotations/templates/annotations.js b/plugins/Annotations/templates/annotations.js index 62f63a59aa..85097ad1d6 100755 --- a/plugins/Annotations/templates/annotations.js +++ b/plugins/Annotations/templates/annotations.js @@ -5,628 +5,586 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ -(function($, piwik) { - -var annotationsApi = { - - // calls Annotations.getAnnotationManager - getAnnotationManager: function(idSite, date, period, lastN, callback) - { - var ajaxParams = - { - module: 'Annotations', - action: 'getAnnotationManager', - idSite: idSite, - date: date, - period: period - }; - if (lastN) - { - ajaxParams.lastN = lastN; - } - - var ajaxRequest = new ajaxHelper(); - ajaxRequest.addParams(ajaxParams, 'get'); - ajaxRequest.setCallback(callback); - ajaxRequest.setFormat('html'); - ajaxRequest.send(false); - }, - - // calls Annotations.addAnnotation - addAnnotation: function(idSite, managerDate, managerPeriod, date, note, callback) - { - var ajaxParams = - { - module: 'Annotations', - action: 'addAnnotation', - idSite: idSite, - date: date, - managerDate: managerDate, - managerPeriod: managerPeriod, - note: note - }; - - var ajaxRequest = new ajaxHelper(); - ajaxRequest.addParams(ajaxParams, 'get'); - ajaxRequest.addParams({token_auth: piwik.token_auth}, 'post'); - ajaxRequest.setCallback(callback); - ajaxRequest.setFormat('html'); - ajaxRequest.send(false); - }, - - // calls Annotations.saveAnnotation - saveAnnotation: function(idSite, idNote, date, noteData, callback) - { - var ajaxParams = - { - module: 'Annotations', - action: 'saveAnnotation', - idSite: idSite, - idNote: idNote, - date: date - }; - - for (var key in noteData) - { - ajaxParams[key] = noteData[key]; - } - - var ajaxRequest = new ajaxHelper(); - ajaxRequest.addParams(ajaxParams, 'get'); - ajaxRequest.addParams({token_auth: piwik.token_auth}, 'post'); - ajaxRequest.setCallback(callback); - ajaxRequest.setFormat('html'); - ajaxRequest.send(false); - }, - - // calls Annotations.deleteAnnotation - deleteAnnotation: function(idSite, idNote, managerDate, managerPeriod, callback) - { - var ajaxParams = - { - module: 'Annotations', - action: 'deleteAnnotation', - idSite: idSite, - idNote: idNote, - date: managerDate, - period: managerPeriod - }; - - var ajaxRequest = new ajaxHelper(); - ajaxRequest.addParams(ajaxParams, 'get'); - ajaxRequest.addParams({token_auth: piwik.token_auth}, 'post'); - ajaxRequest.setCallback(callback); - ajaxRequest.setFormat('html'); - ajaxRequest.send(false); - }, - - // calls Annotations.getEvolutionIcons - getEvolutionIcons: function(idSite, date, period, lastN, callback) - { - var ajaxParams = - { - module: 'Annotations', - action: 'getEvolutionIcons', - idSite: idSite, - date: date, - period: period - }; - if (lastN) - { - ajaxParams.lastN = lastN; - } - - var ajaxRequest = new ajaxHelper(); - ajaxRequest.addParams(ajaxParams, 'get'); - ajaxRequest.setFormat('html'); - ajaxRequest.setCallback(callback); - ajaxRequest.send(false); - } -}; - -var today = new Date(); - -/** - * Returns options to configure an annotation's datepicker shown in edit mode. - * - * @param {Element} annotation The annotation element. - */ -var getDatePickerOptions = function(annotation) -{ - var annotationDateStr = annotation.attr('data-date'), - parts = annotationDateStr.split('-'), - annotationDate = new Date(parts[0], parts[1] - 1, parts[2]); - - var result = piwik.getBaseDatePickerOptions(annotationDate); - - // make sure days before site start & after today cannot be selected - var piwikMinDate = result.minDate; - result.beforeShowDay = function (date) - { - var valid = true; - - // if date is after today or before date of site creation, it cannot be selected - if (date > today - || date < piwikMinDate) - { - valid = false; - } - - return [valid, '']; - }; - - // on select a date, change the text of the edit date link - result.onSelect = function (dateText) - { - $('.annotation-period-edit>a', annotation).text(dateText); - $('.datepicker', annotation).hide(); - }; - - return result; -}; - -/** - * Switches the current mode of an annotation between the view/edit modes. - * - * @param {Element} inAnnotationElement An element within the annotation to toggle the mode of. - * Should be two levels nested in the .annotation-value - * element. - * @return {Element} The .annotation-value element. - */ -var toggleAnnotationMode = function(inAnnotationElement) -{ - var annotation = $(inAnnotationElement).closest('.annotation'); - $('.annotation-period,.annotation-period-edit,.delete-annotation,' + - '.annotation-edit-mode,.annotation-view-mode', annotation).toggle(); - - return $(inAnnotationElement).find('.annotation-value'); -}; - -/** - * Creates the datepicker for an annotation element. - * - * @param {Element} annotation The annotation element. - */ -var createDatePicker = function ( annotation ) -{ - $('.datepicker', annotation).datepicker(getDatePickerOptions(annotation)).hide(); -}; - -/** - * Creates datepickers for every period edit in an annotation manager. - * - * @param {Element} manager The annotation manager element. - */ -var createDatePickers = function ( manager ) -{ - $('.annotation-period-edit', manager).each(function() { - createDatePicker($(this).parent().parent()); - }); -} - -/** - * Replaces the HTML of an annotation manager element, and resets date/period - * attributes. - * - * @param {Element} manager The annotation manager. - * @param {string} tml The HTML of the new annotation manager. - */ -var replaceAnnotationManager = function(manager, html) -{ - var newManager = $(html); - manager.html(newManager.html()) - .attr('data-date', newManager.attr('data-date')) - .attr('data-period', newManager.attr('data-period')); - createDatePickers(manager); -}; - -/** - * Returns true if an annotation element is starred, false if otherwise. - * - * @param {Element} annotation The annotation element. - * @return {bool} - */ -var isAnnotationStarred = function(annotation) -{ - return +$('.annotation-star', annotation).attr('data-starred') == 1 ? true : false; -}; - -/** - * Replaces the HTML of an annotation element with HTML returned from Piwik, and - * makes sure the data attributes are correct. - * - * @param {Element} annotation The annotation element. - * @param {string} html The replacement HTML (or alternatively, the replacement - * element/jQuery object). - */ -var replaceAnnotationHtml = function ( annotation, html ) -{ - var newHtml = $(html); - annotation.html(newHtml.html()).attr('data-date', newHtml.attr('data-date')); - createDatePicker(annotation); -} - -/** - * Binds events to an annotation manager element. - * - * @param {Element} manager The annotation manager. - * @param {int} idSite The site ID the manager is showing annotations for. - * @param {function} onAnnotationCountChange Callback that is called when there is a change - * in the number of annotations and/or starred annotations, - * eg, when a user adds a new one or deletes an existing one. - */ -var bindAnnotationManagerEvents = function(manager, idSite, onAnnotationCountChange) -{ - if (!onAnnotationCountChange) - { - onAnnotationCountChange = function() {}; - } - - // show new annotation row if create new annotation link is clicked - manager.on('click', '.add-annotation', function(e) { - e.preventDefault(); - - $('.new-annotation-row', manager).show(); - $(this).hide(); - - return false; - }); - - // hide new annotation row if cancel button clicked - manager.on('click', '.new-annotation-cancel', function() { - var newAnnotationRow = $(this).parent().parent(); - newAnnotationRow.hide(); - - $('.add-annotation', newAnnotationRow.closest('.annotation-manager')).show(); - }); - - // save new annotation when new annotation row save is clicked - manager.on('click', '.new-annotation-save', function() { - var addRow = $(this).parent().parent(), - addNoteInput = addRow.find('.new-annotation-edit'), - noteDate = addRow.find('.annotation-period-edit>a').text(); - - // do nothing if input is empty - if (!addNoteInput.val()) - { - return; - } - - // disable input & link - addNoteInput.attr('disabled', 'disabled'); - $(this).attr('disabled', 'disabled'); - - // add a new annotation for the site, date & period - annotationsApi.addAnnotation( - idSite, - manager.attr('data-date'), - manager.attr('data-period'), - noteDate, - addNoteInput.val(), - function(response) { - replaceAnnotationManager(manager, response); - - // increment annotation count for this date - onAnnotationCountChange(noteDate, 1, 0); - } - ); - }); - - // add new annotation when enter key pressed on new annotation input - manager.on('keypress', '.new-annotation-edit', function(e) { - if (e.which == 13) - { - $(this).parent().find('.new-annotation-save').click(); - } - }); - - // show annotation editor if edit link, annotation text or period text is clicked - manager.on('click', '.annotation-enter-edit-mode', function(e) { - e.preventDefault(); - - var annotationContent = toggleAnnotationMode(this); - annotationContent.find('.annotation-edit').focus(); - - return false; - }); - - // hide annotation editor if cancel button is clicked - manager.on('click', '.annotation-cancel', function() { - toggleAnnotationMode(this); - }); - - // save annotation if save button clicked - manager.on('click', '.annotation-edit-mode .annotation-save', function() { - var annotation = $(this).parent().parent().parent(), - input = $('.annotation-edit', annotation), - dateEditText = $('.annotation-period-edit>a', annotation).text(); - - // if annotation value/date has not changed, just show the view mode instead of edit - if (input[0].defaultValue == input.val() - && dateEditText == annotation.attr('data-date')) - { - toggleAnnotationMode(this); - return; - } - - // disable input while ajax is happening - input.attr('disabled', 'disabled'); - $(this).attr('disabled', 'disabled'); - - // save the note w/ the new note text & date - annotationsApi.saveAnnotation( - idSite, - annotation.attr('data-id'), - dateEditText, - { - note: input.val() - }, - function(response) { - response = $(response); - - var newDate = response.attr('data-date'), - isStarred = isAnnotationStarred(response), - originalDate = annotation.attr('data-date'); - - replaceAnnotationHtml(annotation, response); - - // if the date has been changed, update the evolution icon counts to reflect the change - if (originalDate != newDate) - { - // reduce count for original date - onAnnotationCountChange(originalDate, -1, isStarred ? -1 : 0); - - // increase count for new date - onAnnotationCountChange(newDate, 1, isStarred ? 1 : 0); - } - } - ); - }); - - // save annotation if 'enter' pressed on input - manager.on('keypress', '.annotation-value input', function(e) { - if (e.which == 13) - { - $(this).parent().find('.annotation-save').click(); - } - }); - - // delete annotation if delete link clicked - manager.on('click', '.delete-annotation', function(e) { - e.preventDefault(); - - var annotation = $(this).parent().parent(); - $(this).attr('disabled', 'disabled'); - - // delete annotation by ajax - annotationsApi.deleteAnnotation( - idSite, - annotation.attr('data-id'), - manager.attr('data-date'), - manager.attr('data-period'), - function (response) { - replaceAnnotationManager(manager, response); - - // update evolution icons - var isStarred = isAnnotationStarred(annotation); - onAnnotationCountChange(annotation.attr('data-date'), -1, isStarred ? -1 : 0); - } - ); - - return false; - }); - - // star/unstar annotation if star clicked - manager.on('click', '.annotation-star-changeable', function(e) { - var annotation = $(this).parent().parent(), - newStarredVal = $(this).attr('data-starred') == 0 ? 1 : 0 // flip existing 'starred' value - ; - - // perform ajax request to star annotation - annotationsApi.saveAnnotation( - idSite, - annotation.attr('data-id'), - annotation.attr('data-date'), - { - starred: newStarredVal - }, - function (response) { - replaceAnnotationHtml(annotation, response); - - // change starred count for this annotation in evolution graph based on what we're - // changing the starred value to - onAnnotationCountChange(annotation.attr('data-date'), 0, newStarredVal == 0 ? -1 : 1); - } - ); - }); - - // when period edit is clicked, show datepicker - manager.on('click', '.annotation-period-edit>a', function(e) { - e.preventDefault(); - $('.datepicker', $(this).parent()).toggle(); - return false; - }); - - // make sure datepicker popups are closed if someone clicks elsewhere - $('body').on('mouseup', function(e) { - var container = $('.annotation-period-edit>.datepicker:visible').parent(); - - if (!container.has(e.target).length) - { - container.find('.datepicker').hide(); - } - }); -}; +(function ($, piwik) { + + var annotationsApi = { + + // calls Annotations.getAnnotationManager + getAnnotationManager: function (idSite, date, period, lastN, callback) { + var ajaxParams = + { + module: 'Annotations', + action: 'getAnnotationManager', + idSite: idSite, + date: date, + period: period + }; + if (lastN) { + ajaxParams.lastN = lastN; + } + + var ajaxRequest = new ajaxHelper(); + ajaxRequest.addParams(ajaxParams, 'get'); + ajaxRequest.setCallback(callback); + ajaxRequest.setFormat('html'); + ajaxRequest.send(false); + }, + + // calls Annotations.addAnnotation + addAnnotation: function (idSite, managerDate, managerPeriod, date, note, callback) { + var ajaxParams = + { + module: 'Annotations', + action: 'addAnnotation', + idSite: idSite, + date: date, + managerDate: managerDate, + managerPeriod: managerPeriod, + note: note + }; + + var ajaxRequest = new ajaxHelper(); + ajaxRequest.addParams(ajaxParams, 'get'); + ajaxRequest.addParams({token_auth: piwik.token_auth}, 'post'); + ajaxRequest.setCallback(callback); + ajaxRequest.setFormat('html'); + ajaxRequest.send(false); + }, + + // calls Annotations.saveAnnotation + saveAnnotation: function (idSite, idNote, date, noteData, callback) { + var ajaxParams = + { + module: 'Annotations', + action: 'saveAnnotation', + idSite: idSite, + idNote: idNote, + date: date + }; + + for (var key in noteData) { + ajaxParams[key] = noteData[key]; + } + + var ajaxRequest = new ajaxHelper(); + ajaxRequest.addParams(ajaxParams, 'get'); + ajaxRequest.addParams({token_auth: piwik.token_auth}, 'post'); + ajaxRequest.setCallback(callback); + ajaxRequest.setFormat('html'); + ajaxRequest.send(false); + }, + + // calls Annotations.deleteAnnotation + deleteAnnotation: function (idSite, idNote, managerDate, managerPeriod, callback) { + var ajaxParams = + { + module: 'Annotations', + action: 'deleteAnnotation', + idSite: idSite, + idNote: idNote, + date: managerDate, + period: managerPeriod + }; + + var ajaxRequest = new ajaxHelper(); + ajaxRequest.addParams(ajaxParams, 'get'); + ajaxRequest.addParams({token_auth: piwik.token_auth}, 'post'); + ajaxRequest.setCallback(callback); + ajaxRequest.setFormat('html'); + ajaxRequest.send(false); + }, + + // calls Annotations.getEvolutionIcons + getEvolutionIcons: function (idSite, date, period, lastN, callback) { + var ajaxParams = + { + module: 'Annotations', + action: 'getEvolutionIcons', + idSite: idSite, + date: date, + period: period + }; + if (lastN) { + ajaxParams.lastN = lastN; + } + + var ajaxRequest = new ajaxHelper(); + ajaxRequest.addParams(ajaxParams, 'get'); + ajaxRequest.setFormat('html'); + ajaxRequest.setCallback(callback); + ajaxRequest.send(false); + } + }; + + var today = new Date(); + + /** + * Returns options to configure an annotation's datepicker shown in edit mode. + * + * @param {Element} annotation The annotation element. + */ + var getDatePickerOptions = function (annotation) { + var annotationDateStr = annotation.attr('data-date'), + parts = annotationDateStr.split('-'), + annotationDate = new Date(parts[0], parts[1] - 1, parts[2]); + + var result = piwik.getBaseDatePickerOptions(annotationDate); + + // make sure days before site start & after today cannot be selected + var piwikMinDate = result.minDate; + result.beforeShowDay = function (date) { + var valid = true; + + // if date is after today or before date of site creation, it cannot be selected + if (date > today + || date < piwikMinDate) { + valid = false; + } + + return [valid, '']; + }; + + // on select a date, change the text of the edit date link + result.onSelect = function (dateText) { + $('.annotation-period-edit>a', annotation).text(dateText); + $('.datepicker', annotation).hide(); + }; + + return result; + }; + + /** + * Switches the current mode of an annotation between the view/edit modes. + * + * @param {Element} inAnnotationElement An element within the annotation to toggle the mode of. + * Should be two levels nested in the .annotation-value + * element. + * @return {Element} The .annotation-value element. + */ + var toggleAnnotationMode = function (inAnnotationElement) { + var annotation = $(inAnnotationElement).closest('.annotation'); + $('.annotation-period,.annotation-period-edit,.delete-annotation,' + + '.annotation-edit-mode,.annotation-view-mode', annotation).toggle(); + + return $(inAnnotationElement).find('.annotation-value'); + }; + + /** + * Creates the datepicker for an annotation element. + * + * @param {Element} annotation The annotation element. + */ + var createDatePicker = function (annotation) { + $('.datepicker', annotation).datepicker(getDatePickerOptions(annotation)).hide(); + }; + + /** + * Creates datepickers for every period edit in an annotation manager. + * + * @param {Element} manager The annotation manager element. + */ + var createDatePickers = function (manager) { + $('.annotation-period-edit', manager).each(function () { + createDatePicker($(this).parent().parent()); + }); + } + + /** + * Replaces the HTML of an annotation manager element, and resets date/period + * attributes. + * + * @param {Element} manager The annotation manager. + * @param {string} tml The HTML of the new annotation manager. + */ + var replaceAnnotationManager = function (manager, html) { + var newManager = $(html); + manager.html(newManager.html()) + .attr('data-date', newManager.attr('data-date')) + .attr('data-period', newManager.attr('data-period')); + createDatePickers(manager); + }; + + /** + * Returns true if an annotation element is starred, false if otherwise. + * + * @param {Element} annotation The annotation element. + * @return {bool} + */ + var isAnnotationStarred = function (annotation) { + return +$('.annotation-star', annotation).attr('data-starred') == 1 ? true : false; + }; + + /** + * Replaces the HTML of an annotation element with HTML returned from Piwik, and + * makes sure the data attributes are correct. + * + * @param {Element} annotation The annotation element. + * @param {string} html The replacement HTML (or alternatively, the replacement + * element/jQuery object). + */ + var replaceAnnotationHtml = function (annotation, html) { + var newHtml = $(html); + annotation.html(newHtml.html()).attr('data-date', newHtml.attr('data-date')); + createDatePicker(annotation); + } + + /** + * Binds events to an annotation manager element. + * + * @param {Element} manager The annotation manager. + * @param {int} idSite The site ID the manager is showing annotations for. + * @param {function} onAnnotationCountChange Callback that is called when there is a change + * in the number of annotations and/or starred annotations, + * eg, when a user adds a new one or deletes an existing one. + */ + var bindAnnotationManagerEvents = function (manager, idSite, onAnnotationCountChange) { + if (!onAnnotationCountChange) { + onAnnotationCountChange = function () {}; + } + + // show new annotation row if create new annotation link is clicked + manager.on('click', '.add-annotation', function (e) { + e.preventDefault(); + + $('.new-annotation-row', manager).show(); + $(this).hide(); + + return false; + }); + + // hide new annotation row if cancel button clicked + manager.on('click', '.new-annotation-cancel', function () { + var newAnnotationRow = $(this).parent().parent(); + newAnnotationRow.hide(); + + $('.add-annotation', newAnnotationRow.closest('.annotation-manager')).show(); + }); + + // save new annotation when new annotation row save is clicked + manager.on('click', '.new-annotation-save', function () { + var addRow = $(this).parent().parent(), + addNoteInput = addRow.find('.new-annotation-edit'), + noteDate = addRow.find('.annotation-period-edit>a').text(); + + // do nothing if input is empty + if (!addNoteInput.val()) { + return; + } + + // disable input & link + addNoteInput.attr('disabled', 'disabled'); + $(this).attr('disabled', 'disabled'); + + // add a new annotation for the site, date & period + annotationsApi.addAnnotation( + idSite, + manager.attr('data-date'), + manager.attr('data-period'), + noteDate, + addNoteInput.val(), + function (response) { + replaceAnnotationManager(manager, response); + + // increment annotation count for this date + onAnnotationCountChange(noteDate, 1, 0); + } + ); + }); + + // add new annotation when enter key pressed on new annotation input + manager.on('keypress', '.new-annotation-edit', function (e) { + if (e.which == 13) { + $(this).parent().find('.new-annotation-save').click(); + } + }); + + // show annotation editor if edit link, annotation text or period text is clicked + manager.on('click', '.annotation-enter-edit-mode', function (e) { + e.preventDefault(); + + var annotationContent = toggleAnnotationMode(this); + annotationContent.find('.annotation-edit').focus(); + + return false; + }); + + // hide annotation editor if cancel button is clicked + manager.on('click', '.annotation-cancel', function () { + toggleAnnotationMode(this); + }); + + // save annotation if save button clicked + manager.on('click', '.annotation-edit-mode .annotation-save', function () { + var annotation = $(this).parent().parent().parent(), + input = $('.annotation-edit', annotation), + dateEditText = $('.annotation-period-edit>a', annotation).text(); + + // if annotation value/date has not changed, just show the view mode instead of edit + if (input[0].defaultValue == input.val() + && dateEditText == annotation.attr('data-date')) { + toggleAnnotationMode(this); + return; + } + + // disable input while ajax is happening + input.attr('disabled', 'disabled'); + $(this).attr('disabled', 'disabled'); + + // save the note w/ the new note text & date + annotationsApi.saveAnnotation( + idSite, + annotation.attr('data-id'), + dateEditText, + { + note: input.val() + }, + function (response) { + response = $(response); + + var newDate = response.attr('data-date'), + isStarred = isAnnotationStarred(response), + originalDate = annotation.attr('data-date'); + + replaceAnnotationHtml(annotation, response); + + // if the date has been changed, update the evolution icon counts to reflect the change + if (originalDate != newDate) { + // reduce count for original date + onAnnotationCountChange(originalDate, -1, isStarred ? -1 : 0); + + // increase count for new date + onAnnotationCountChange(newDate, 1, isStarred ? 1 : 0); + } + } + ); + }); + + // save annotation if 'enter' pressed on input + manager.on('keypress', '.annotation-value input', function (e) { + if (e.which == 13) { + $(this).parent().find('.annotation-save').click(); + } + }); + + // delete annotation if delete link clicked + manager.on('click', '.delete-annotation', function (e) { + e.preventDefault(); + + var annotation = $(this).parent().parent(); + $(this).attr('disabled', 'disabled'); + + // delete annotation by ajax + annotationsApi.deleteAnnotation( + idSite, + annotation.attr('data-id'), + manager.attr('data-date'), + manager.attr('data-period'), + function (response) { + replaceAnnotationManager(manager, response); + + // update evolution icons + var isStarred = isAnnotationStarred(annotation); + onAnnotationCountChange(annotation.attr('data-date'), -1, isStarred ? -1 : 0); + } + ); + + return false; + }); + + // star/unstar annotation if star clicked + manager.on('click', '.annotation-star-changeable', function (e) { + var annotation = $(this).parent().parent(), + newStarredVal = $(this).attr('data-starred') == 0 ? 1 : 0 // flip existing 'starred' value + ; + + // perform ajax request to star annotation + annotationsApi.saveAnnotation( + idSite, + annotation.attr('data-id'), + annotation.attr('data-date'), + { + starred: newStarredVal + }, + function (response) { + replaceAnnotationHtml(annotation, response); + + // change starred count for this annotation in evolution graph based on what we're + // changing the starred value to + onAnnotationCountChange(annotation.attr('data-date'), 0, newStarredVal == 0 ? -1 : 1); + } + ); + }); + + // when period edit is clicked, show datepicker + manager.on('click', '.annotation-period-edit>a', function (e) { + e.preventDefault(); + $('.datepicker', $(this).parent()).toggle(); + return false; + }); + + // make sure datepicker popups are closed if someone clicks elsewhere + $('body').on('mouseup', function (e) { + var container = $('.annotation-period-edit>.datepicker:visible').parent(); + + if (!container.has(e.target).length) { + container.find('.datepicker').hide(); + } + }); + }; // used in below function -var loadingAnnotationManager = false; - -/** - * Shows an annotation manager under a report for a specific site & date range. - * - * @param {Element} domElem The element of the report to show the annotation manger - * under. - * @param {int} idSite The ID of the site to show the annotations of. - * @param {string} date The start date of the period. - * @param {string} period The period type. - * @param {int} Whether to include the last N periods in the date range or not. Can - * be undefined. - */ -var showAnnotationViewer = function(domElem, idSite, date, period, lastN, callback) -{ - var addToAnnotationCount = function(date, amt, starAmt) - { - if (date.indexOf(',') != -1) - { - date = date.split(',')[0]; - } - - $('.evolution-annotations>span', domElem).each(function() { - if ($(this).attr('data-date') == date) - { - // get counts from attributes (and convert them to ints) - var starredCount = +$(this).attr('data-starred'), - annotationCount = +$(this).attr('data-count'); - - // modify the starred count & make sure the correct image is used - var newStarCount = starredCount + starAmt; - if(newStarCount > 0) { - var newImg = 'themes/default/images/yellow_marker.png'; - } else { - var newImg = 'themes/default/images/grey_marker.png'; + var loadingAnnotationManager = false; + + /** + * Shows an annotation manager under a report for a specific site & date range. + * + * @param {Element} domElem The element of the report to show the annotation manger + * under. + * @param {int} idSite The ID of the site to show the annotations of. + * @param {string} date The start date of the period. + * @param {string} period The period type. + * @param {int} Whether to include the last N periods in the date range or not. Can + * be undefined. + */ + var showAnnotationViewer = function (domElem, idSite, date, period, lastN, callback) { + var addToAnnotationCount = function (date, amt, starAmt) { + if (date.indexOf(',') != -1) { + date = date.split(',')[0]; + } + + $('.evolution-annotations>span', domElem).each(function () { + if ($(this).attr('data-date') == date) { + // get counts from attributes (and convert them to ints) + var starredCount = +$(this).attr('data-starred'), + annotationCount = +$(this).attr('data-count'); + + // modify the starred count & make sure the correct image is used + var newStarCount = starredCount + starAmt; + if (newStarCount > 0) { + var newImg = 'themes/default/images/yellow_marker.png'; + } else { + var newImg = 'themes/default/images/grey_marker.png'; + } + $(this).attr('data-starred', newStarCount).find('img').attr('src', newImg); + + // modify the annotation count & hide/show based on new count + var newCount = annotationCount + amt; + $(this).attr('data-count', newCount).css('opacity', newCount > 0 ? 1 : 0); + + return false; + } + }); + }; + + var manager = $('.annotation-manager', domElem); + if (manager.length) { + // if annotations for the requested date + period are already loaded, then just toggle the + // visibility of the annotation viewer. otherwise, we reload the annotations. + if (manager.attr('data-date') == date + && manager.attr('data-period') == period) { + // toggle manager view + if (manager.is(':hidden')) { + manager.slideDown('slow', function () { if (callback) callback(manager) }); + } + else { + manager.slideUp('slow', function () { if (callback) callback(manager) }); + } + } + else { + // show nothing but the loading gif + $('.annotations', manager).html(''); + $('.loadingPiwik', manager).show(); + + // reload annotation manager for new date/period + annotationsApi.getAnnotationManager(idSite, date, period, lastN, function (response) { + replaceAnnotationManager(manager, response); + + createDatePickers(manager); + + // show if hidden + if (manager.is(':hidden')) { + manager.slideDown('slow', function () { if (callback) callback(manager) }); + } + else { + if (callback) { + callback(manager); + } + } + }); + } } - $(this).attr('data-starred', newStarCount).find('img').attr('src', newImg); - - // modify the annotation count & hide/show based on new count - var newCount = annotationCount + amt; - $(this).attr('data-count', newCount).css('opacity', newCount > 0 ? 1 : 0); - - return false; - } - }); - }; - - var manager = $('.annotation-manager', domElem); - if (manager.length) - { - // if annotations for the requested date + period are already loaded, then just toggle the - // visibility of the annotation viewer. otherwise, we reload the annotations. - if (manager.attr('data-date') == date - && manager.attr('data-period') == period) - { - // toggle manager view - if (manager.is(':hidden')) - { - manager.slideDown('slow', function () { if (callback) callback(manager) }); - } - else - { - manager.slideUp('slow', function () { if (callback) callback(manager) }); - } - } - else - { - // show nothing but the loading gif - $('.annotations', manager).html(''); - $('.loadingPiwik', manager).show(); - - // reload annotation manager for new date/period - annotationsApi.getAnnotationManager(idSite, date, period, lastN, function(response) { - replaceAnnotationManager(manager, response); - - createDatePickers(manager); - - // show if hidden - if (manager.is(':hidden')) - { - manager.slideDown('slow', function () { if (callback) callback(manager) }); - } - else - { - if (callback) - { - callback(manager); - } - } - }); - } - } - else - { - // if we are already loading the annotation manager, don't load it again - if (loadingAnnotationManager) - { - return; - } - - loadingAnnotationManager = true; - - var loading = $('.loadingPiwikBelow', domElem).css({display: 'block'}); - - // the annotations for this report have not been retrieved yet, so do an ajax request - // & show the result - annotationsApi.getAnnotationManager(idSite, date, period, lastN, function(response) { - var manager = $(response).hide(); - - // if an error occurred (and response does not contain the annotation manager), do nothing - if (!manager.hasClass('annotation-manager')) - { - return; - } - - // create datepickers for each shown annotation - createDatePickers(manager); - - bindAnnotationManagerEvents(manager, idSite, addToAnnotationCount); - - loading.css('visibility', 'hidden'); - - // add & show annotation manager - $('.dataTableFeatures', domElem).append(manager); - manager.slideDown('slow', function() { - loading.hide().css('visibility', 'visible'); - loadingAnnotationManager = false; - - if (callback) callback(manager) - }); - }); - } -}; - -/** - * Determines the x-coordinates of a set of evolution annotation icons. - * - * @param {Element} annotations The '.evolution-annotations' element. - * @param {Element} graphElem The evolution graph's datatable element. - */ -var placeEvolutionIcons = function (annotations, graphElem) -{ - var canvases = $('.piwik-graph .jqplot-xaxis canvas', graphElem), - noteSize = 16; - - // if no graph available, hide all icons - if (!canvases || canvases.length == 0) { - $('span', annotations).hide(); - return true; - } - - // set position of each individual icon - $('span', annotations).each(function(i) { - var canvas = $(canvases[i]), - canvasCenterX = canvas.position().left + (canvas.width() / 2); - $(this).css({ - left: canvasCenterX - noteSize / 2, - // show if there are annotations for this x-axis tick - opacity: +$(this).attr('data-count') > 0 ? 1 : 0 - }); - }); -}; + else { + // if we are already loading the annotation manager, don't load it again + if (loadingAnnotationManager) { + return; + } + + loadingAnnotationManager = true; + + var loading = $('.loadingPiwikBelow', domElem).css({display: 'block'}); + + // the annotations for this report have not been retrieved yet, so do an ajax request + // & show the result + annotationsApi.getAnnotationManager(idSite, date, period, lastN, function (response) { + var manager = $(response).hide(); + + // if an error occurred (and response does not contain the annotation manager), do nothing + if (!manager.hasClass('annotation-manager')) { + return; + } + + // create datepickers for each shown annotation + createDatePickers(manager); + + bindAnnotationManagerEvents(manager, idSite, addToAnnotationCount); + + loading.css('visibility', 'hidden'); + + // add & show annotation manager + $('.dataTableFeatures', domElem).append(manager); + manager.slideDown('slow', function () { + loading.hide().css('visibility', 'visible'); + loadingAnnotationManager = false; + + if (callback) callback(manager) + }); + }); + } + }; + + /** + * Determines the x-coordinates of a set of evolution annotation icons. + * + * @param {Element} annotations The '.evolution-annotations' element. + * @param {Element} graphElem The evolution graph's datatable element. + */ + var placeEvolutionIcons = function (annotations, graphElem) { + var canvases = $('.piwik-graph .jqplot-xaxis canvas', graphElem), + noteSize = 16; + + // if no graph available, hide all icons + if (!canvases || canvases.length == 0) { + $('span', annotations).hide(); + return true; + } + + // set position of each individual icon + $('span', annotations).each(function (i) { + var canvas = $(canvases[i]), + canvasCenterX = canvas.position().left + (canvas.width() / 2); + $(this).css({ + left: canvasCenterX - noteSize / 2, + // show if there are annotations for this x-axis tick + opacity: +$(this).attr('data-count') > 0 ? 1 : 0 + }); + }); + }; // make showAnnotationViewer, placeEvolutionIcons & annotationsApi globally accessible -piwik.annotations = { - showAnnotationViewer: showAnnotationViewer, - placeEvolutionIcons: placeEvolutionIcons, - api: annotationsApi -}; + piwik.annotations = { + showAnnotationViewer: showAnnotationViewer, + placeEvolutionIcons: placeEvolutionIcons, + api: annotationsApi + }; }(jQuery, piwik)); diff --git a/plugins/Annotations/templates/annotations.tpl b/plugins/Annotations/templates/annotations.tpl index f15750e548..26c1f708f1 100755 --- a/plugins/Annotations/templates/annotations.tpl +++ b/plugins/Annotations/templates/annotations.tpl @@ -1,30 +1,29 @@ <div class="annotations"> -{if empty($annotations)} + {if empty($annotations)} + <div class="empty-annotation-list">{'Annotations_NoAnnotations'|translate}</div> + {/if} -<div class="empty-annotation-list">{'Annotations_NoAnnotations'|translate}</div> + <table> + {foreach from=$annotations item=annotation} + {include file="Annotations/templates/annotation.tpl"} + {/foreach} + <tr class="new-annotation-row" style="display:none" data-date="{$startDate}"> + <td class="annotation-meta"> + <div class="annotation-star"> </div> + <div class="annotation-period-edit"> + <a href="#">{$startDate}</a> -{/if} - -<table> -{foreach from=$annotations item=annotation} -{include file="Annotations/templates/annotation.tpl"} -{/foreach} -<tr class="new-annotation-row" style="display:none" data-date="{$startDate}"> - <td class="annotation-meta"> - <div class="annotation-star"> </div> - <div class="annotation-period-edit"> - <a href="#">{$startDate}</a> - <div class="datepicker" style="display:none"/> - </div> - </td> - <td class="annotation-value"> - <input type="text" value="" class="new-annotation-edit" placeholder="{'Annotations_EnterAnnotationText'|translate}"/><br/> - <input type="button" class="submit new-annotation-save" value="{'General_Save'|translate}"/> - <input type="button" class="submit new-annotation-cancel" value="{'General_Cancel'|translate}"/> - </td> - <td class="annotation-user-cell"><span class="annotation-user">{$userLogin}</span></td> -</tr> -</table> + <div class="datepicker" style="display:none"/> + </div> + </td> + <td class="annotation-value"> + <input type="text" value="" class="new-annotation-edit" placeholder="{'Annotations_EnterAnnotationText'|translate}"/><br/> + <input type="button" class="submit new-annotation-save" value="{'General_Save'|translate}"/> + <input type="button" class="submit new-annotation-cancel" value="{'General_Cancel'|translate}"/> + </td> + <td class="annotation-user-cell"><span class="annotation-user">{$userLogin}</span></td> + </tr> + </table> </div> diff --git a/plugins/Annotations/templates/evolutionAnnotations.tpl b/plugins/Annotations/templates/evolutionAnnotations.tpl index 8da13d35f0..eaf6f9d9d3 100755 --- a/plugins/Annotations/templates/evolutionAnnotations.tpl +++ b/plugins/Annotations/templates/evolutionAnnotations.tpl @@ -1,15 +1,15 @@ <div class="evolution-annotations"> -{foreach from=$annotationCounts item=dateCountPair} - {assign var=date value=$dateCountPair[0]} - {assign var=counts value=$dateCountPair[1]} - <span data-date="{$date}" data-count="{$counts.count}" data-starred="{$counts.starred}" - {if $counts.count eq 0}title="{'Annotations_AddAnnotationsFor_js'|translate:$date}" - {elseif $counts.count eq 1}title="{'Annotations_AnnotationOnDate'|translate:$date:$counts.note} + {foreach from=$annotationCounts item=dateCountPair} + {assign var=date value=$dateCountPair[0]} + {assign var=counts value=$dateCountPair[1]} + <span data-date="{$date}" data-count="{$counts.count}" data-starred="{$counts.starred}" + {if $counts.count eq 0}title="{'Annotations_AddAnnotationsFor_js'|translate:$date}" + {elseif $counts.count eq 1}title="{'Annotations_AnnotationOnDate'|translate:$date:$counts.note} {'Annotations_ClickToEditOrAdd'|translate}" - {else}title="{'Annotations_ViewAndAddAnnotations_js'|translate:$date}" - {/if}> + {else}title="{'Annotations_ViewAndAddAnnotations_js'|translate:$date}" + {/if}> <img src="themes/default/images/{if $counts.starred > 0}yellow_marker.png{else}grey_marker.png{/if}" width="16" height="16"/> </span> -{/foreach} + {/foreach} </div> diff --git a/plugins/Annotations/templates/styles.css b/plugins/Annotations/templates/styles.css index 4831bafcb1..089a8ef6c3 100755 --- a/plugins/Annotations/templates/styles.css +++ b/plugins/Annotations/templates/styles.css @@ -1,194 +1,198 @@ .evolution-annotations { - position: relative; - height: 16px; - width: 100%; - margin-top: 12px; - margin-bottom: -28px; - cursor: pointer; + position: relative; + height: 16px; + width: 100%; + margin-top: 12px; + margin-bottom: -28px; + cursor: pointer; } .evolution-annotations > span { - position: absolute; + position: absolute; } .annotation-manager { - text-align: left; - margin-top: -18px; + text-align: left; + margin-top: -18px; } .annotations-header { - display: inline-block; - width: 128px; - text-align: right; - font-size: 12px; - font-style: italic; - margin-bottom: 8px; - vertical-align: top; - color: #666; + display: inline-block; + width: 128px; + text-align: right; + font-size: 12px; + font-style: italic; + margin-bottom: 8px; + vertical-align: top; + color: #666; } .annotation-controls { - display:inline-block; - margin-left: 132px; + display: inline-block; + margin-left: 132px; } .annotation-controls>a { - font-size: 11px; - font-style: italic; - color: #666; - cursor:pointer; - padding:3px 0 6px 0; - display:inline-block; + font-size: 11px; + font-style: italic; + color: #666; + cursor: pointer; + padding: 3px 0 6px 0; + display: inline-block; } .annotation-controls>a:hover { - text-decoration:none; + text-decoration: none; } .annotation-list { - margin-left: 8px; + margin-left: 8px; } .annotation-list table { - width: 100%; + width: 100%; } .annotation-list-range { - display: inline-block; - font-size: 12px; - font-style: italic; - color: #666; - vertical-align: top; - margin: 0 0 8px 8px; + display: inline-block; + font-size: 12px; + font-style: italic; + color: #666; + vertical-align: top; + margin: 0 0 8px 8px; } -.empty-annotation-list,.annotation-list .loadingPiwik { - display: block; - - font-style: italic; - color: #666; - margin: 0 0 12px 140px; +.empty-annotation-list, .annotation-list .loadingPiwik { + display: block; + + font-style: italic; + color: #666; + margin: 0 0 12px 140px; } .annotation-meta { - width: 128px; - text-align: right; - vertical-align: top; - font-size:14px; + width: 128px; + text-align: right; + vertical-align: top; + font-size: 14px; } .annotation-user { - font-style: italic; - font-size: 11px; - color:#444; + font-style: italic; + font-size: 11px; + color: #444; } .annotation-user-cell { - vertical-align: top; - width: 92px; + vertical-align: top; + width: 92px; } .annotation-period { - display:inline-block; - font-style: italic; - margin: 0 8px 8px 8px; - vertical-align: top; + display: inline-block; + font-style: italic; + margin: 0 8px 8px 8px; + vertical-align: top; } .annotation-value { - margin: 0 12px 12px 8px; - vertical-align: top; - position: relative; - font-size:14px; + margin: 0 12px 12px 8px; + vertical-align: top; + position: relative; + font-size: 14px; } .annotation-enter-edit-mode { - cursor: pointer; + cursor: pointer; } -.annotation-edit,.new-annotation-edit { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - width:98%; +.annotation-edit, .new-annotation-edit { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 98%; } .annotation-star { - display: inline-block; - margin: 0 8px 8px 0; - width: 16px; + display: inline-block; + margin: 0 8px 8px 0; + width: 16px; } .annotation-star-changeable { - cursor: pointer; + cursor: pointer; } .delete-annotation { - font-size:12px; - font-style: italic; - color: red; - text-decoration: none; - display: inline-block; + font-size: 12px; + font-style: italic; + color: red; + text-decoration: none; + display: inline-block; } .delete-annotation:hover { - text-decoration: underline; + text-decoration: underline; } .annotation-manager .submit { - float:none; + float: none; } .edit-annotation { - font-size:10px; - color:#666; - font-style:italic; + font-size: 10px; + color: #666; + font-style: italic; } + .edit-annotation:hover { - text-decoration:none; + text-decoration: none; } .annotationView { - float: right; + float: right; margin-left: 5px; position: relative; - cursor:pointer; + cursor: pointer; } .annotationView > span { - font-style: italic; - display: inline-block; - margin: 4px 4px 0 4px; + font-style: italic; + display: inline-block; + margin: 4px 4px 0 4px; } .annotation-period-edit { - display:inline-block; - background:white; - color:#444; - font-size:12px; - border: 1px solid #e4e5e4; - padding:5px 5px 6px 3px; - border-radius:4px; - -moz-border-radius:4px; - -webkit-border-radius:4px; + display: inline-block; + background: white; + color: #444; + font-size: 12px; + border: 1px solid #e4e5e4; + padding: 5px 5px 6px 3px; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; } + .annotation-period-edit:hover { - background:#f1f0eb; - border-color:#a9a399; + background: #f1f0eb; + border-color: #a9a399; } + .annotation-period-edit>a { - text-decoration:none; - cursor:pointer; - display:block; + text-decoration: none; + cursor: pointer; + display: block; } + .annotation-period-edit>.datepicker { - position:absolute; - margin-top:6px; - margin-left:-5px; - z-index:15; - background:white; - border: 1px solid #e4e5e4; - border-radius:4px; - -moz-border-radius:4px; - -webkit-border-radius:4px; + position: absolute; + margin-top: 6px; + margin-left: -5px; + z-index: 15; + background: white; + border: 1px solid #e4e5e4; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; } diff --git a/plugins/AnonymizeIP/AnonymizeIP.php b/plugins/AnonymizeIP/AnonymizeIP.php index ebb0c29180..e7b311fc26 100644 --- a/plugins/AnonymizeIP/AnonymizeIP.php +++ b/plugins/AnonymizeIP/AnonymizeIP.php @@ -16,63 +16,61 @@ */ class Piwik_AnonymizeIP extends Piwik_Plugin { - /** - * Get plugin information - * @return array - */ - public function getInformation() - { - return array( - 'description' => Piwik_Translate('AnonymizeIP_PluginDescription'), - 'author' => 'Piwik', - 'author_homepage' => 'http://piwik.org/', - 'version' => Piwik_Version::VERSION, - 'TrackerPlugin' => true, - ); - } + /** + * Get plugin information + * @return array + */ + public function getInformation() + { + return array( + 'description' => Piwik_Translate('AnonymizeIP_PluginDescription'), + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'version' => Piwik_Version::VERSION, + 'TrackerPlugin' => true, + ); + } - /** - * Get list of hooks to register - * @return array - */ - public function getListHooksRegistered() - { - return array( - 'Tracker.Visit.setVisitorIp' => 'setVisitorIpAddress', - ); - } + /** + * Get list of hooks to register + * @return array + */ + public function getListHooksRegistered() + { + return array( + 'Tracker.Visit.setVisitorIp' => 'setVisitorIpAddress', + ); + } - /** - * Internal function to mask portions of the visitor IP address - * - * @param string $ip IP address in network address format - * @param int $maskLength Number of octets to reset - * @return string - */ - static public function applyIPMask($ip, $maskLength) - { - $i = Piwik_Common::strlen($ip); - if($maskLength > $i) - { - $maskLength = $i; - } + /** + * Internal function to mask portions of the visitor IP address + * + * @param string $ip IP address in network address format + * @param int $maskLength Number of octets to reset + * @return string + */ + static public function applyIPMask($ip, $maskLength) + { + $i = Piwik_Common::strlen($ip); + if ($maskLength > $i) { + $maskLength = $i; + } - while($maskLength-- > 0) - { - $ip[--$i] = chr(0); - } + while ($maskLength-- > 0) { + $ip[--$i] = chr(0); + } - return $ip; - } + return $ip; + } - /** - * Hook on Tracker.Visit.setVisitorIp to anonymize visitor IP addresses - * - * @param Piwik_Event_Notification $notification notification object - */ - function setVisitorIpAddress($notification) - { - $ip =& $notification->getNotificationObject(); - $ip = self::applyIPMask($ip, Piwik_Config::getInstance()->Tracker['ip_address_mask_length']); - } + /** + * Hook on Tracker.Visit.setVisitorIp to anonymize visitor IP addresses + * + * @param Piwik_Event_Notification $notification notification object + */ + function setVisitorIpAddress($notification) + { + $ip =& $notification->getNotificationObject(); + $ip = self::applyIPMask($ip, Piwik_Config::getInstance()->Tracker['ip_address_mask_length']); + } } diff --git a/plugins/CoreAdminHome/API.php b/plugins/CoreAdminHome/API.php index 97842dd943..eded6d451f 100644 --- a/plugins/CoreAdminHome/API.php +++ b/plugins/CoreAdminHome/API.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik_Plugins * @package Piwik_CoreAdminHome */ @@ -12,222 +12,211 @@ /** * @package Piwik_CoreAdminHome */ -class Piwik_CoreAdminHome_API +class Piwik_CoreAdminHome_API { - static private $instance = null; - /** - * @return Piwik_CoreAdminHome_API - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - /** - * Will run all scheduled tasks due to run at this time. - * - * @return array - */ - public function runScheduledTasks() - { - Piwik::checkUserIsSuperUser(); - return Piwik_TaskScheduler::runTasks(); - } - - public function getKnownSegmentsToArchive() - { - Piwik::checkUserIsSuperUser(); - return Piwik::getKnownSegmentsToArchive(); - } - - /* - * stores the list of websites IDs to re-reprocess in archive.php - */ - const OPTION_INVALIDATED_IDSITES = 'InvalidatedOldReports_WebsiteIds'; - - /** - * When tracking data in the past (using Tracking API), this function - * can be used to invalidate reports for the idSites and dates where new data - * was added. - * DEV: If you call this API, the UI should display the data correctly, but will process - * in real time, which could be very slow after large data imports. - * After calling this function via REST, you can manually force all data - * to be reprocessed by visiting the script as the Super User: - * http://example.net/piwik/misc/cron/archive.php?token_auth=$SUPER_USER_TOKEN_AUTH_HERE - * REQUIREMENTS: On large piwik setups, you will need in PHP configuration: max_execution_time = 0 - * We recommend to use an hourly schedule of the script at misc/cron/archive.php - * More information: http://piwik.org/setup-auto-archiving/ - * - * @param string $idSites Comma separated list of idSite that have had data imported for the specified dates - * @param string $dates Comma separated list of dates to invalidate for all these websites - * @return array - */ - public function invalidateArchivedReports($idSites, $dates) - { - $idSites = Piwik_Site::getIdSitesFromIdSitesString($idSites); - if(empty($idSites)) { - throw new Exception("Specify a value for &idSites= as a comma separated list of website IDs, for which your token_auth has 'admin' permission"); - } - Piwik::checkUserHasAdminAccess($idSites); - - // Ensure the specified dates are valid - $toInvalidate = $invalidDates = array(); - $dates = explode(',', $dates); - $dates = array_unique($dates); - foreach($dates as $theDate) - { - try { - $date = Piwik_Date::factory($theDate); - } catch(Exception $e) { - $invalidDates[] = $theDate; - continue; - } - if($date->toString() == $theDate) - { - $toInvalidate[] = $date; - } - else - { - $invalidDates[] = $theDate; - } - } - - // Lookup archive tables - $tables = Piwik::getTablesInstalled(); - $archiveTables = Piwik::getTablesArchivesInstalled(); - - // If using the feature "Delete logs older than N days"... - $logsAreDeletedBeforeThisDate = Piwik_Config::getInstance()->Deletelogs['delete_logs_schedule_lowest_interval']; - $logsDeleteEnabled = Piwik_Config::getInstance()->Deletelogs['delete_logs_enable']; - $minimumDateWithLogs = false; - if($logsDeleteEnabled - && $logsAreDeletedBeforeThisDate) - { - $minimumDateWithLogs = Piwik_Date::factory('today')->subDay($logsAreDeletedBeforeThisDate); - } - - // Given the list of dates, process which tables they should be deleted from - $minDate = false; - $warningDates = $processedDates = array(); - /* @var $date Piwik_Date */ - foreach($toInvalidate as $date) - { - // we should only delete reports for dates that are more recent than N days - if($minimumDateWithLogs - && $date->isEarlier($minimumDateWithLogs)) - { - $warningDates[] = $date->toString(); - } - else - { - $processedDates[] = $date->toString(); - } - - $month = $date->toString('Y_m'); - // For a given date, we must invalidate in the monthly archive table - $datesByMonth[$month][] = $date->toString(); - - // But also the year stored in January - $year = $date->toString('Y_01'); - $datesByMonth[$year][] = $date->toString(); - - // but also weeks overlapping several months stored in the month where the week is starting - /* @var $week Piwik_Period_Week */ - $week = Piwik_Period::factory('week', $date); - $week = $week->getDateStart()->toString('Y_m'); - $datesByMonth[$week][] = $date->toString(); - - // Keep track of the minimum date for each website - if($minDate === false - || $date->isEarlier($minDate)) - { - $minDate = $date; - } - } - - // In each table, invalidate day/week/month/year containing this date - $sqlIdSites = implode(",", $idSites); - foreach($archiveTables as $table) - { - // Extract Y_m from table name - $suffix = str_replace(array('archive_numeric_','archive_blob_'), '', Piwik_Common::unprefixTable($table)); - - if(!isset($datesByMonth[$suffix])) - { - continue; - } - // Dates which are to be deleted from this table - $datesToDeleteInTable = $datesByMonth[$suffix]; - - // Build one statement to delete all dates from the given table - $sql = $bind = array(); - $datesToDeleteInTable = array_unique($datesToDeleteInTable); - foreach($datesToDeleteInTable as $dateToDelete) - { - $sql[] = '(date1 <= ? AND ? <= date2)'; - $bind[] = $dateToDelete; - $bind[] = $dateToDelete; - } - $sql = implode(" OR ", $sql); - - $query = "DELETE FROM $table ". - " WHERE ( $sql ) ". - " AND idsite IN (". $sqlIdSites .")"; - Piwik_Query($query, $bind); - } - - // Update piwik_site.ts_created - $query = "UPDATE " . Piwik_Common::prefixTable("site") . - " SET ts_created = ?". - " WHERE idsite IN ( $sqlIdSites ) + static private $instance = null; + + /** + * @return Piwik_CoreAdminHome_API + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + /** + * Will run all scheduled tasks due to run at this time. + * + * @return array + */ + public function runScheduledTasks() + { + Piwik::checkUserIsSuperUser(); + return Piwik_TaskScheduler::runTasks(); + } + + public function getKnownSegmentsToArchive() + { + Piwik::checkUserIsSuperUser(); + return Piwik::getKnownSegmentsToArchive(); + } + + /* + * stores the list of websites IDs to re-reprocess in archive.php + */ + const OPTION_INVALIDATED_IDSITES = 'InvalidatedOldReports_WebsiteIds'; + + /** + * When tracking data in the past (using Tracking API), this function + * can be used to invalidate reports for the idSites and dates where new data + * was added. + * DEV: If you call this API, the UI should display the data correctly, but will process + * in real time, which could be very slow after large data imports. + * After calling this function via REST, you can manually force all data + * to be reprocessed by visiting the script as the Super User: + * http://example.net/piwik/misc/cron/archive.php?token_auth=$SUPER_USER_TOKEN_AUTH_HERE + * REQUIREMENTS: On large piwik setups, you will need in PHP configuration: max_execution_time = 0 + * We recommend to use an hourly schedule of the script at misc/cron/archive.php + * More information: http://piwik.org/setup-auto-archiving/ + * + * @param string $idSites Comma separated list of idSite that have had data imported for the specified dates + * @param string $dates Comma separated list of dates to invalidate for all these websites + * @return array + */ + public function invalidateArchivedReports($idSites, $dates) + { + $idSites = Piwik_Site::getIdSitesFromIdSitesString($idSites); + if (empty($idSites)) { + throw new Exception("Specify a value for &idSites= as a comma separated list of website IDs, for which your token_auth has 'admin' permission"); + } + Piwik::checkUserHasAdminAccess($idSites); + + // Ensure the specified dates are valid + $toInvalidate = $invalidDates = array(); + $dates = explode(',', $dates); + $dates = array_unique($dates); + foreach ($dates as $theDate) { + try { + $date = Piwik_Date::factory($theDate); + } catch (Exception $e) { + $invalidDates[] = $theDate; + continue; + } + if ($date->toString() == $theDate) { + $toInvalidate[] = $date; + } else { + $invalidDates[] = $theDate; + } + } + + // Lookup archive tables + $tables = Piwik::getTablesInstalled(); + $archiveTables = Piwik::getTablesArchivesInstalled(); + + // If using the feature "Delete logs older than N days"... + $logsAreDeletedBeforeThisDate = Piwik_Config::getInstance()->Deletelogs['delete_logs_schedule_lowest_interval']; + $logsDeleteEnabled = Piwik_Config::getInstance()->Deletelogs['delete_logs_enable']; + $minimumDateWithLogs = false; + if ($logsDeleteEnabled + && $logsAreDeletedBeforeThisDate + ) { + $minimumDateWithLogs = Piwik_Date::factory('today')->subDay($logsAreDeletedBeforeThisDate); + } + + // Given the list of dates, process which tables they should be deleted from + $minDate = false; + $warningDates = $processedDates = array(); + /* @var $date Piwik_Date */ + foreach ($toInvalidate as $date) { + // we should only delete reports for dates that are more recent than N days + if ($minimumDateWithLogs + && $date->isEarlier($minimumDateWithLogs) + ) { + $warningDates[] = $date->toString(); + } else { + $processedDates[] = $date->toString(); + } + + $month = $date->toString('Y_m'); + // For a given date, we must invalidate in the monthly archive table + $datesByMonth[$month][] = $date->toString(); + + // But also the year stored in January + $year = $date->toString('Y_01'); + $datesByMonth[$year][] = $date->toString(); + + // but also weeks overlapping several months stored in the month where the week is starting + /* @var $week Piwik_Period_Week */ + $week = Piwik_Period::factory('week', $date); + $week = $week->getDateStart()->toString('Y_m'); + $datesByMonth[$week][] = $date->toString(); + + // Keep track of the minimum date for each website + if ($minDate === false + || $date->isEarlier($minDate) + ) { + $minDate = $date; + } + } + + // In each table, invalidate day/week/month/year containing this date + $sqlIdSites = implode(",", $idSites); + foreach ($archiveTables as $table) { + // Extract Y_m from table name + $suffix = str_replace(array('archive_numeric_', 'archive_blob_'), '', Piwik_Common::unprefixTable($table)); + + if (!isset($datesByMonth[$suffix])) { + continue; + } + // Dates which are to be deleted from this table + $datesToDeleteInTable = $datesByMonth[$suffix]; + + // Build one statement to delete all dates from the given table + $sql = $bind = array(); + $datesToDeleteInTable = array_unique($datesToDeleteInTable); + foreach ($datesToDeleteInTable as $dateToDelete) { + $sql[] = '(date1 <= ? AND ? <= date2)'; + $bind[] = $dateToDelete; + $bind[] = $dateToDelete; + } + $sql = implode(" OR ", $sql); + + $query = "DELETE FROM $table " . + " WHERE ( $sql ) " . + " AND idsite IN (" . $sqlIdSites . ")"; + Piwik_Query($query, $bind); + } + + // Update piwik_site.ts_created + $query = "UPDATE " . Piwik_Common::prefixTable("site") . + " SET ts_created = ?" . + " WHERE idsite IN ( $sqlIdSites ) AND ts_created > ?"; - $minDateSql = $minDate->subDay(1)->getDatetime(); - $bind = array($minDateSql,$minDateSql); - Piwik_Query($query, $bind); - - // Force to re-process data for these websites in the next archive.php cron run - $invalidatedIdSites = Piwik_CoreAdminHome_API::getWebsiteIdsToInvalidate(); - $invalidatedIdSites = array_merge($invalidatedIdSites, $idSites); - $invalidatedIdSites = array_unique($invalidatedIdSites); - $invalidatedIdSites = array_values($invalidatedIdSites); - Piwik_SetOption(self::OPTION_INVALIDATED_IDSITES, serialize($invalidatedIdSites)); - - Piwik_Site::clearCache(); - - $output = array(); - // output logs - if($warningDates) - { - $output[] = 'Warning: the following Dates have not been invalidated, because they are earlier than your Log Deletion limit: '. - implode(", ", $warningDates). - "\n The last day with logs is " . $minimumDateWithLogs. ". ". - "\n Please disable 'Delete old Logs' or set it to a higher deletion threshold (eg. 180 days or 365 years).'."; - } - $output[] = "Success. The following dates were invalidated successfully: ". - implode(", ", $processedDates); - return $output; - } - - /** - * Returns array of idSites to force re-process next time archive.php runs - * - * @ignore - * @return mixed - */ - static public function getWebsiteIdsToInvalidate() - { - Piwik::checkUserHasSomeAdminAccess(); - $invalidatedIdSites = Piwik_GetOption(self::OPTION_INVALIDATED_IDSITES); - if($invalidatedIdSites - && ($invalidatedIdSites = unserialize($invalidatedIdSites)) - && count($invalidatedIdSites)) - { - return $invalidatedIdSites; - } - return array(); - } + $minDateSql = $minDate->subDay(1)->getDatetime(); + $bind = array($minDateSql, $minDateSql); + Piwik_Query($query, $bind); + + // Force to re-process data for these websites in the next archive.php cron run + $invalidatedIdSites = Piwik_CoreAdminHome_API::getWebsiteIdsToInvalidate(); + $invalidatedIdSites = array_merge($invalidatedIdSites, $idSites); + $invalidatedIdSites = array_unique($invalidatedIdSites); + $invalidatedIdSites = array_values($invalidatedIdSites); + Piwik_SetOption(self::OPTION_INVALIDATED_IDSITES, serialize($invalidatedIdSites)); + + Piwik_Site::clearCache(); + + $output = array(); + // output logs + if ($warningDates) { + $output[] = 'Warning: the following Dates have not been invalidated, because they are earlier than your Log Deletion limit: ' . + implode(", ", $warningDates) . + "\n The last day with logs is " . $minimumDateWithLogs . ". " . + "\n Please disable 'Delete old Logs' or set it to a higher deletion threshold (eg. 180 days or 365 years).'."; + } + $output[] = "Success. The following dates were invalidated successfully: " . + implode(", ", $processedDates); + return $output; + } + + /** + * Returns array of idSites to force re-process next time archive.php runs + * + * @ignore + * @return mixed + */ + static public function getWebsiteIdsToInvalidate() + { + Piwik::checkUserHasSomeAdminAccess(); + $invalidatedIdSites = Piwik_GetOption(self::OPTION_INVALIDATED_IDSITES); + if ($invalidatedIdSites + && ($invalidatedIdSites = unserialize($invalidatedIdSites)) + && count($invalidatedIdSites) + ) { + return $invalidatedIdSites; + } + return array(); + } } diff --git a/plugins/CoreAdminHome/Controller.php b/plugins/CoreAdminHome/Controller.php index f5e5d83a9c..c915205bc2 100644 --- a/plugins/CoreAdminHome/Controller.php +++ b/plugins/CoreAdminHome/Controller.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik_Plugins * @package Piwik_CoreAdminHome */ @@ -15,228 +15,218 @@ */ class Piwik_CoreAdminHome_Controller extends Piwik_Controller_Admin { - const LOGO_HEIGHT = 300; - const LOGO_SMALL_HEIGHT = 100; - - public function index() - { - return $this->redirectToIndex('UsersManager', 'userSettings'); - } - - public function generalSettings() - { - Piwik::checkUserHasSomeAdminAccess(); - $view = Piwik_View::factory('generalSettings'); - - if(Piwik::isUserIsSuperUser()) - { - $enableBrowserTriggerArchiving = Piwik_ArchiveProcessing::isBrowserTriggerArchivingEnabled(); - $todayArchiveTimeToLive = Piwik_ArchiveProcessing::getTodayArchiveTimeToLive(); - $showWarningCron = false; - if(!$enableBrowserTriggerArchiving - && $todayArchiveTimeToLive < 3600) - { - $showWarningCron = true; - } - $view->showWarningCron = $showWarningCron; - $view->todayArchiveTimeToLive = $todayArchiveTimeToLive; - $view->enableBrowserTriggerArchiving = $enableBrowserTriggerArchiving; - - $config = Piwik_Config::getInstance(); - - if(!$config->isFileWritable()) - { - $view->configFileNotWritable = true; - } - - $debug = $config->Debug; - $view->enableBetaReleaseCheck = $debug['allow_upgrades_to_beta']; - - $view->mail = $config->mail; - - $view->branding = $config->branding; - - $directoryWritable = is_writable(PIWIK_DOCUMENT_ROOT.'/themes/'); - $logoFilesWriteable = is_writeable(PIWIK_DOCUMENT_ROOT.'/themes/logo.png') && is_writeable(PIWIK_DOCUMENT_ROOT.'/themes/logo-header.png'); - $view->logosWriteable = ($logoFilesWriteable || $directoryWritable) && ini_get('file_uploads') == 1; - - $trustedHosts = array(); - if (isset($config->General['trusted_hosts'])) - { - $trustedHosts = $config->General['trusted_hosts']; - } - $view->trustedHosts = $trustedHosts; - } - - $view->language = Piwik_LanguagesManager::getLanguageCodeForCurrentUser(); - $this->setBasicVariablesView($view); - $view->topMenu = Piwik_GetTopMenu(); - $view->menu = Piwik_GetAdminMenu(); - echo $view->render(); - } - - public function setGeneralSettings() - { - Piwik::checkUserIsSuperUser(); - $response = new Piwik_API_ResponseBuilder(Piwik_Common::getRequestVar('format')); - try { - $this->checkTokenInUrl(); - $enableBrowserTriggerArchiving = Piwik_Common::getRequestVar('enableBrowserTriggerArchiving'); - $todayArchiveTimeToLive = Piwik_Common::getRequestVar('todayArchiveTimeToLive'); - - Piwik_ArchiveProcessing::setBrowserTriggerArchiving((bool)$enableBrowserTriggerArchiving); - Piwik_ArchiveProcessing::setTodayArchiveTimeToLive($todayArchiveTimeToLive); - - // Update email settings - $mail = array(); - $mail['transport'] = (Piwik_Common::getRequestVar('mailUseSmtp') == '1') ? 'smtp' : ''; - $mail['port'] = Piwik_Common::getRequestVar('mailPort', ''); - $mail['host'] = Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('mailHost', '')); - $mail['type'] = Piwik_Common::getRequestVar('mailType', ''); - $mail['username'] = Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('mailUsername', '')); - $mail['password'] = Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('mailPassword', '')); - $mail['encryption'] = Piwik_Common::getRequestVar('mailEncryption', ''); - - $config = Piwik_Config::getInstance(); - $config->mail = $mail; - - // update branding settings - $branding = $config->branding; - $branding['use_custom_logo'] = Piwik_Common::getRequestVar('useCustomLogo', '0'); - $config->branding = $branding; - - // update beta channel setting - $debug = $config->Debug; - $debug['allow_upgrades_to_beta'] = Piwik_Common::getRequestVar('enableBetaReleaseCheck', '0', 'int'); - $config->Debug = $debug; - // update trusted host settings - $trustedHosts = Piwik_Common::getRequestVar('trustedHosts', false, 'json'); - if ($trustedHosts !== false) - { - Piwik_Url::saveTrustedHostnameInConfig($trustedHosts); - } - - $config->forceSave(); - - $toReturn = $response->getResponse(); - } catch(Exception $e ) { - $toReturn = $response->getResponseException( $e ); - } - echo $toReturn; - } - - /** - * Renders and echo's an admin page that lets users generate custom JavaScript - * tracking code and custom image tracker links. - */ - public function trackingCodeGenerator() - { - $view = Piwik_View::factory('jsTrackingGenerator'); - $this->setBasicVariablesView($view); - $view->topMenu = Piwik_GetTopMenu(); - $view->menu = Piwik_GetAdminMenu(); - - $viewableIdSites = Piwik_SitesManager_API::getInstance()->getSitesIdWithAtLeastViewAccess(); - - $defaultIdSite = reset($viewableIdSites); - $view->idSite = Piwik_Common::getRequestVar('idSite', $defaultIdSite, 'int'); - - $view->defaultReportSiteName = Piwik_Site::getNameFor($view->idSite); - $view->defaultSiteRevenue = Piwik::getCurrency($view->idSite); - - $allUrls = Piwik_SitesManager_API::getInstance()->getSiteUrlsFromId($view->idSite); - if (isset($allUrls[1])) - { - $aliasUrl = $allUrls[1]; - } - else - { - $aliasUrl = 'x.domain.com'; - } - $view->defaultReportSiteAlias = $aliasUrl; - - $mainUrl = Piwik_Site::getMainUrlFor($view->idSite); - $view->defaultReportSiteDomain = @parse_url($mainUrl, PHP_URL_HOST); - - // get currencies for each viewable site - $view->currencySymbols = Piwik_SitesManager_API::getInstance()->getCurrencySymbols(); - - $view->serverSideDoNotTrackEnabled = Piwik_PrivacyManager_Controller::isDntSupported(); - - echo $view->render(); - } - - /** - * Shows the "Track Visits" checkbox. - */ - public function optOut() - { - $trackVisits = !Piwik_Tracker_IgnoreCookie::isIgnoreCookieFound(); - - $nonce = Piwik_Common::getRequestVar('nonce', false); - $language = Piwik_Common::getRequestVar('language', ''); - if($nonce !== false && Piwik_Nonce::verifyNonce('Piwik_OptOut', $nonce)) - { - Piwik_Nonce::discardNonce('Piwik_OptOut'); - Piwik_Tracker_IgnoreCookie::setIgnoreCookie(); - $trackVisits = !$trackVisits; - } - - $view = Piwik_View::factory('optOut'); - $view->trackVisits = $trackVisits; - $view->nonce = Piwik_Nonce::getNonce('Piwik_OptOut', 3600); - $view->language = Piwik_LanguagesManager_API::getInstance()->isLanguageAvailable($language) - ? $language - : Piwik_LanguagesManager::getLanguageCodeForCurrentUser(); - echo $view->render(); - } - - public function uploadCustomLogo() - { - Piwik::checkUserIsSuperUser(); - if(empty($_FILES['customLogo']) - || !empty($_FILES['customLogo']['error']) - ) - { - echo '0'; - return; - } - - $file = $_FILES['customLogo']['tmp_name']; - if(!file_exists($file)) - { - echo '0'; - return; - } - $error = false; - - list($width, $height) = getimagesize($file); - switch($_FILES['customLogo']['type']) { - case 'image/jpeg': - $image = imagecreatefromjpeg($file); - break; - case 'image/png': - $image = imagecreatefrompng($file); - break; - case 'image/gif': - $image = imagecreatefromgif($file); - break; - default: - echo '0'; - return; - } - - $widthExpected = round($width * self::LOGO_HEIGHT / $height); - $smallWidthExpected = round($width * self::LOGO_SMALL_HEIGHT / $height); - - $logo = imagecreatetruecolor($widthExpected, self::LOGO_HEIGHT); - $logoSmall = imagecreatetruecolor($smallWidthExpected, self::LOGO_SMALL_HEIGHT); - imagecopyresized($logo, $image, 0, 0, 0, 0, $widthExpected, self::LOGO_HEIGHT, $width, $height); - imagecopyresized($logoSmall, $image, 0, 0, 0, 0, $smallWidthExpected, self::LOGO_SMALL_HEIGHT, $width, $height); - - imagepng($logo, PIWIK_DOCUMENT_ROOT.'/themes/logo.png', 3); - imagepng($logoSmall, PIWIK_DOCUMENT_ROOT.'/themes/logo-header.png', 3); - echo '1'; - return; - } + const LOGO_HEIGHT = 300; + const LOGO_SMALL_HEIGHT = 100; + + public function index() + { + return $this->redirectToIndex('UsersManager', 'userSettings'); + } + + public function generalSettings() + { + Piwik::checkUserHasSomeAdminAccess(); + $view = Piwik_View::factory('generalSettings'); + + if (Piwik::isUserIsSuperUser()) { + $enableBrowserTriggerArchiving = Piwik_ArchiveProcessing::isBrowserTriggerArchivingEnabled(); + $todayArchiveTimeToLive = Piwik_ArchiveProcessing::getTodayArchiveTimeToLive(); + $showWarningCron = false; + if (!$enableBrowserTriggerArchiving + && $todayArchiveTimeToLive < 3600 + ) { + $showWarningCron = true; + } + $view->showWarningCron = $showWarningCron; + $view->todayArchiveTimeToLive = $todayArchiveTimeToLive; + $view->enableBrowserTriggerArchiving = $enableBrowserTriggerArchiving; + + $config = Piwik_Config::getInstance(); + + if (!$config->isFileWritable()) { + $view->configFileNotWritable = true; + } + + $debug = $config->Debug; + $view->enableBetaReleaseCheck = $debug['allow_upgrades_to_beta']; + + $view->mail = $config->mail; + + $view->branding = $config->branding; + + $directoryWritable = is_writable(PIWIK_DOCUMENT_ROOT . '/themes/'); + $logoFilesWriteable = is_writeable(PIWIK_DOCUMENT_ROOT . '/themes/logo.png') && is_writeable(PIWIK_DOCUMENT_ROOT . '/themes/logo-header.png'); + $view->logosWriteable = ($logoFilesWriteable || $directoryWritable) && ini_get('file_uploads') == 1; + + $trustedHosts = array(); + if (isset($config->General['trusted_hosts'])) { + $trustedHosts = $config->General['trusted_hosts']; + } + $view->trustedHosts = $trustedHosts; + } + + $view->language = Piwik_LanguagesManager::getLanguageCodeForCurrentUser(); + $this->setBasicVariablesView($view); + $view->topMenu = Piwik_GetTopMenu(); + $view->menu = Piwik_GetAdminMenu(); + echo $view->render(); + } + + public function setGeneralSettings() + { + Piwik::checkUserIsSuperUser(); + $response = new Piwik_API_ResponseBuilder(Piwik_Common::getRequestVar('format')); + try { + $this->checkTokenInUrl(); + $enableBrowserTriggerArchiving = Piwik_Common::getRequestVar('enableBrowserTriggerArchiving'); + $todayArchiveTimeToLive = Piwik_Common::getRequestVar('todayArchiveTimeToLive'); + + Piwik_ArchiveProcessing::setBrowserTriggerArchiving((bool)$enableBrowserTriggerArchiving); + Piwik_ArchiveProcessing::setTodayArchiveTimeToLive($todayArchiveTimeToLive); + + // Update email settings + $mail = array(); + $mail['transport'] = (Piwik_Common::getRequestVar('mailUseSmtp') == '1') ? 'smtp' : ''; + $mail['port'] = Piwik_Common::getRequestVar('mailPort', ''); + $mail['host'] = Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('mailHost', '')); + $mail['type'] = Piwik_Common::getRequestVar('mailType', ''); + $mail['username'] = Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('mailUsername', '')); + $mail['password'] = Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('mailPassword', '')); + $mail['encryption'] = Piwik_Common::getRequestVar('mailEncryption', ''); + + $config = Piwik_Config::getInstance(); + $config->mail = $mail; + + // update branding settings + $branding = $config->branding; + $branding['use_custom_logo'] = Piwik_Common::getRequestVar('useCustomLogo', '0'); + $config->branding = $branding; + + // update beta channel setting + $debug = $config->Debug; + $debug['allow_upgrades_to_beta'] = Piwik_Common::getRequestVar('enableBetaReleaseCheck', '0', 'int'); + $config->Debug = $debug; + // update trusted host settings + $trustedHosts = Piwik_Common::getRequestVar('trustedHosts', false, 'json'); + if ($trustedHosts !== false) { + Piwik_Url::saveTrustedHostnameInConfig($trustedHosts); + } + + $config->forceSave(); + + $toReturn = $response->getResponse(); + } catch (Exception $e) { + $toReturn = $response->getResponseException($e); + } + echo $toReturn; + } + + /** + * Renders and echo's an admin page that lets users generate custom JavaScript + * tracking code and custom image tracker links. + */ + public function trackingCodeGenerator() + { + $view = Piwik_View::factory('jsTrackingGenerator'); + $this->setBasicVariablesView($view); + $view->topMenu = Piwik_GetTopMenu(); + $view->menu = Piwik_GetAdminMenu(); + + $viewableIdSites = Piwik_SitesManager_API::getInstance()->getSitesIdWithAtLeastViewAccess(); + + $defaultIdSite = reset($viewableIdSites); + $view->idSite = Piwik_Common::getRequestVar('idSite', $defaultIdSite, 'int'); + + $view->defaultReportSiteName = Piwik_Site::getNameFor($view->idSite); + $view->defaultSiteRevenue = Piwik::getCurrency($view->idSite); + + $allUrls = Piwik_SitesManager_API::getInstance()->getSiteUrlsFromId($view->idSite); + if (isset($allUrls[1])) { + $aliasUrl = $allUrls[1]; + } else { + $aliasUrl = 'x.domain.com'; + } + $view->defaultReportSiteAlias = $aliasUrl; + + $mainUrl = Piwik_Site::getMainUrlFor($view->idSite); + $view->defaultReportSiteDomain = @parse_url($mainUrl, PHP_URL_HOST); + + // get currencies for each viewable site + $view->currencySymbols = Piwik_SitesManager_API::getInstance()->getCurrencySymbols(); + + $view->serverSideDoNotTrackEnabled = Piwik_PrivacyManager_Controller::isDntSupported(); + + echo $view->render(); + } + + /** + * Shows the "Track Visits" checkbox. + */ + public function optOut() + { + $trackVisits = !Piwik_Tracker_IgnoreCookie::isIgnoreCookieFound(); + + $nonce = Piwik_Common::getRequestVar('nonce', false); + $language = Piwik_Common::getRequestVar('language', ''); + if ($nonce !== false && Piwik_Nonce::verifyNonce('Piwik_OptOut', $nonce)) { + Piwik_Nonce::discardNonce('Piwik_OptOut'); + Piwik_Tracker_IgnoreCookie::setIgnoreCookie(); + $trackVisits = !$trackVisits; + } + + $view = Piwik_View::factory('optOut'); + $view->trackVisits = $trackVisits; + $view->nonce = Piwik_Nonce::getNonce('Piwik_OptOut', 3600); + $view->language = Piwik_LanguagesManager_API::getInstance()->isLanguageAvailable($language) + ? $language + : Piwik_LanguagesManager::getLanguageCodeForCurrentUser(); + echo $view->render(); + } + + public function uploadCustomLogo() + { + Piwik::checkUserIsSuperUser(); + if (empty($_FILES['customLogo']) + || !empty($_FILES['customLogo']['error']) + ) { + echo '0'; + return; + } + + $file = $_FILES['customLogo']['tmp_name']; + if (!file_exists($file)) { + echo '0'; + return; + } + $error = false; + + list($width, $height) = getimagesize($file); + switch ($_FILES['customLogo']['type']) { + case 'image/jpeg': + $image = imagecreatefromjpeg($file); + break; + case 'image/png': + $image = imagecreatefrompng($file); + break; + case 'image/gif': + $image = imagecreatefromgif($file); + break; + default: + echo '0'; + return; + } + + $widthExpected = round($width * self::LOGO_HEIGHT / $height); + $smallWidthExpected = round($width * self::LOGO_SMALL_HEIGHT / $height); + + $logo = imagecreatetruecolor($widthExpected, self::LOGO_HEIGHT); + $logoSmall = imagecreatetruecolor($smallWidthExpected, self::LOGO_SMALL_HEIGHT); + imagecopyresized($logo, $image, 0, 0, 0, 0, $widthExpected, self::LOGO_HEIGHT, $width, $height); + imagecopyresized($logoSmall, $image, 0, 0, 0, 0, $smallWidthExpected, self::LOGO_SMALL_HEIGHT, $width, $height); + + imagepng($logo, PIWIK_DOCUMENT_ROOT . '/themes/logo.png', 3); + imagepng($logoSmall, PIWIK_DOCUMENT_ROOT . '/themes/logo-header.png', 3); + echo '1'; + return; + } } diff --git a/plugins/CoreAdminHome/CoreAdminHome.php b/plugins/CoreAdminHome/CoreAdminHome.php index 7dbf1b6ee9..3164811cca 100644 --- a/plugins/CoreAdminHome/CoreAdminHome.php +++ b/plugins/CoreAdminHome/CoreAdminHome.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik_Plugins * @package Piwik_CoreAdminHome */ @@ -15,114 +15,112 @@ */ class Piwik_CoreAdminHome extends Piwik_Plugin { - public function getInformation() - { - return array( - 'description' => Piwik_Translate('CoreAdminHome_PluginDescription'), - 'author' => 'Piwik', - 'author_homepage' => 'http://piwik.org/', - 'version' => Piwik_Version::VERSION, - ); - } + public function getInformation() + { + return array( + 'description' => Piwik_Translate('CoreAdminHome_PluginDescription'), + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'version' => Piwik_Version::VERSION, + ); + } + + public function getListHooksRegistered() + { + return array( + 'AssetManager.getCssFiles' => 'getCssFiles', + 'AssetManager.getJsFiles' => 'getJsFiles', + 'AdminMenu.add' => 'addMenu', + 'TaskScheduler.getScheduledTasks' => 'getScheduledTasks', + ); + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + function getScheduledTasks($notification) + { + $tasks = & $notification->getNotificationObject(); + + // general data purge on older archive tables, executed daily + $purgeArchiveTablesTask = new Piwik_ScheduledTask ($this, + 'purgeOutdatedArchives', + null, + new Piwik_ScheduledTime_Daily(), + Piwik_ScheduledTask::HIGH_PRIORITY); + $tasks[] = $purgeArchiveTablesTask; + + // lowest priority since tables should be optimized after they are modified + $optimizeArchiveTableTask = new Piwik_ScheduledTask ($this, + 'optimizeArchiveTable', + null, + new Piwik_ScheduledTime_Daily(), + Piwik_ScheduledTask::LOWEST_PRIORITY); + $tasks[] = $optimizeArchiveTableTask; + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + function getCssFiles($notification) + { + $cssFiles = & $notification->getNotificationObject(); + + $cssFiles[] = "libs/jquery/themes/base/jquery-ui.css"; + $cssFiles[] = "plugins/CoreAdminHome/templates/menu.css"; + $cssFiles[] = "themes/default/common.css"; + $cssFiles[] = "plugins/CoreAdminHome/templates/styles.css"; + $cssFiles[] = "plugins/CoreHome/templates/donate.css"; + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + function getJsFiles($notification) + { + $jsFiles = & $notification->getNotificationObject(); + + $jsFiles[] = "libs/jquery/jquery.js"; + $jsFiles[] = "libs/jquery/jquery-ui.js"; + $jsFiles[] = "libs/javascript/sprintf.js"; + $jsFiles[] = "themes/default/common.js"; + $jsFiles[] = "themes/default/ajaxHelper.js"; + $jsFiles[] = "libs/jquery/jquery.history.js"; + $jsFiles[] = "plugins/CoreHome/templates/broadcast.js"; + $jsFiles[] = "plugins/CoreAdminHome/templates/generalSettings.js"; + $jsFiles[] = "plugins/CoreHome/templates/donate.js"; + } - public function getListHooksRegistered() - { - return array( - 'AssetManager.getCssFiles' => 'getCssFiles', - 'AssetManager.getJsFiles' => 'getJsFiles', - 'AdminMenu.add' => 'addMenu', - 'TaskScheduler.getScheduledTasks' => 'getScheduledTasks', - ); - } + function addMenu() + { + Piwik_AddAdminSubMenu('CoreAdminHome_MenuManage', NULL, "", Piwik::isUserHasSomeAdminAccess(), $order = 1); + Piwik_AddAdminSubMenu('CoreAdminHome_MenuCommunity', NULL, "", Piwik::isUserHasSomeAdminAccess(), $order = 3); + Piwik_AddAdminSubMenu('CoreAdminHome_MenuDiagnostic', NULL, "", Piwik::isUserHasSomeAdminAccess(), $order = 20); + Piwik_AddAdminSubMenu('General_Settings', NULL, "", Piwik::isUserHasSomeAdminAccess(), $order = 5); + Piwik_AddAdminSubMenu('General_Settings', 'CoreAdminHome_MenuGeneralSettings', + array('module' => 'CoreAdminHome', 'action' => 'generalSettings'), + Piwik::isUserHasSomeAdminAccess(), + $order = 6); + Piwik_AddAdminSubMenu('CoreAdminHome_MenuManage', 'CoreAdminHome_TrackingCode', + array('module' => 'CoreAdminHome', 'action' => 'trackingCodeGenerator'), + Piwik::isUserHasSomeAdminAccess(), + $order = 4); - /** - * @param Piwik_Event_Notification $notification notification object - */ - function getScheduledTasks ( $notification ) - { - $tasks = &$notification->getNotificationObject(); - - // general data purge on older archive tables, executed daily - $purgeArchiveTablesTask = new Piwik_ScheduledTask ( $this, - 'purgeOutdatedArchives', - null, - new Piwik_ScheduledTime_Daily(), - Piwik_ScheduledTask::HIGH_PRIORITY); - $tasks[] = $purgeArchiveTablesTask; - - // lowest priority since tables should be optimized after they are modified - $optimizeArchiveTableTask = new Piwik_ScheduledTask ( $this, - 'optimizeArchiveTable', - null, - new Piwik_ScheduledTime_Daily(), - Piwik_ScheduledTask::LOWEST_PRIORITY); - $tasks[] = $optimizeArchiveTableTask; - } + } - /** - * @param Piwik_Event_Notification $notification notification object - */ - function getCssFiles( $notification ) - { - $cssFiles = &$notification->getNotificationObject(); - - $cssFiles[] = "libs/jquery/themes/base/jquery-ui.css"; - $cssFiles[] = "plugins/CoreAdminHome/templates/menu.css"; - $cssFiles[] = "themes/default/common.css"; - $cssFiles[] = "plugins/CoreAdminHome/templates/styles.css"; - $cssFiles[] = "plugins/CoreHome/templates/donate.css"; - } + function purgeOutdatedArchives() + { + $archiveTables = Piwik::getTablesArchivesInstalled(); + foreach ($archiveTables as $table) { + if (strpos($table, 'numeric') !== false) { + Piwik_ArchiveProcessing_Period::doPurgeOutdatedArchives($table); + } + } + } - /** - * @param Piwik_Event_Notification $notification notification object - */ - function getJsFiles ( $notification ) - { - $jsFiles = &$notification->getNotificationObject(); - - $jsFiles[] = "libs/jquery/jquery.js"; - $jsFiles[] = "libs/jquery/jquery-ui.js"; - $jsFiles[] = "libs/javascript/sprintf.js"; - $jsFiles[] = "themes/default/common.js"; - $jsFiles[] = "themes/default/ajaxHelper.js"; - $jsFiles[] = "libs/jquery/jquery.history.js"; - $jsFiles[] = "plugins/CoreHome/templates/broadcast.js"; - $jsFiles[] = "plugins/CoreAdminHome/templates/generalSettings.js"; - $jsFiles[] = "plugins/CoreHome/templates/donate.js"; - } - - function addMenu() - { - Piwik_AddAdminSubMenu('CoreAdminHome_MenuManage', NULL, "", Piwik::isUserHasSomeAdminAccess(), $order = 1); - Piwik_AddAdminSubMenu('CoreAdminHome_MenuCommunity', NULL, "", Piwik::isUserHasSomeAdminAccess(), $order = 3); - Piwik_AddAdminSubMenu('CoreAdminHome_MenuDiagnostic', NULL, "", Piwik::isUserHasSomeAdminAccess(), $order = 20); - Piwik_AddAdminSubMenu('General_Settings', NULL, "", Piwik::isUserHasSomeAdminAccess(), $order = 5); - Piwik_AddAdminSubMenu('General_Settings', 'CoreAdminHome_MenuGeneralSettings', - array('module' => 'CoreAdminHome', 'action' => 'generalSettings'), - Piwik::isUserHasSomeAdminAccess(), - $order = 6); - Piwik_AddAdminSubMenu('CoreAdminHome_MenuManage', 'CoreAdminHome_TrackingCode', - array('module' => 'CoreAdminHome', 'action' => 'trackingCodeGenerator'), - Piwik::isUserHasSomeAdminAccess(), - $order = 4); - - } - - function purgeOutdatedArchives() - { - $archiveTables = Piwik::getTablesArchivesInstalled(); - foreach($archiveTables as $table) - { - if(strpos($table, 'numeric') !== false) - { - Piwik_ArchiveProcessing_Period::doPurgeOutdatedArchives($table); - } - } - } - - function optimizeArchiveTable() - { - $archiveTables = Piwik::getTablesArchivesInstalled(); - Piwik_OptimizeTables($archiveTables); - } + function optimizeArchiveTable() + { + $archiveTables = Piwik::getTablesArchivesInstalled(); + Piwik_OptimizeTables($archiveTables); + } } diff --git a/plugins/CoreAdminHome/templates/generalSettings.js b/plugins/CoreAdminHome/templates/generalSettings.js index 8adcad271d..6a493626c7 100644 --- a/plugins/CoreAdminHome/templates/generalSettings.js +++ b/plugins/CoreAdminHome/templates/generalSettings.js @@ -5,33 +5,32 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ -function sendGeneralSettingsAJAX() -{ - var enableBrowserTriggerArchiving = $('input[name=enableBrowserTriggerArchiving]:checked').val(); - var enableBetaReleaseCheck = $('input[name=enableBetaReleaseCheck]:checked').val(); - var todayArchiveTimeToLive = $('#todayArchiveTimeToLive').val(); +function sendGeneralSettingsAJAX() { + var enableBrowserTriggerArchiving = $('input[name=enableBrowserTriggerArchiving]:checked').val(); + var enableBetaReleaseCheck = $('input[name=enableBetaReleaseCheck]:checked').val(); + var todayArchiveTimeToLive = $('#todayArchiveTimeToLive').val(); - var trustedHosts = []; - $('input[name=trusted_host]').each(function () { - trustedHosts.push($(this).val()); - }); + var trustedHosts = []; + $('input[name=trusted_host]').each(function () { + trustedHosts.push($(this).val()); + }); var ajaxHandler = new ajaxHelper(); ajaxHandler.setLoadingElement(); ajaxHandler.addParams({ - format: 'json', - enableBrowserTriggerArchiving: enableBrowserTriggerArchiving, - enableBetaReleaseCheck: enableBetaReleaseCheck, - todayArchiveTimeToLive: todayArchiveTimeToLive, - mailUseSmtp: isSmtpEnabled(), - mailPort: $('#mailPort').val(), - mailHost: $('#mailHost').val(), - mailType: $('#mailType').val(), - mailUsername: $('#mailUsername').val(), - mailPassword: $('#mailPassword').val(), - mailEncryption: $('#mailEncryption').val(), - useCustomLogo: isCustomLogoEnabled(), - trustedHosts: JSON.stringify(trustedHosts) + format: 'json', + enableBrowserTriggerArchiving: enableBrowserTriggerArchiving, + enableBetaReleaseCheck: enableBetaReleaseCheck, + todayArchiveTimeToLive: todayArchiveTimeToLive, + mailUseSmtp: isSmtpEnabled(), + mailPort: $('#mailPort').val(), + mailHost: $('#mailHost').val(), + mailType: $('#mailType').val(), + mailUsername: $('#mailUsername').val(), + mailPassword: $('#mailPassword').val(), + mailEncryption: $('#mailEncryption').val(), + useCustomLogo: isCustomLogoEnabled(), + trustedHosts: JSON.stringify(trustedHosts) }, 'POST'); ajaxHandler.addParams({ module: 'CoreAdminHome', @@ -40,111 +39,103 @@ function sendGeneralSettingsAJAX() ajaxHandler.redirectOnSuccess(); ajaxHandler.send(true); } -function showSmtpSettings(value) -{ - $('#smtpSettings').toggle(value==1); +function showSmtpSettings(value) { + $('#smtpSettings').toggle(value == 1); } -function isSmtpEnabled() -{ - return $('input[name="mailUseSmtp"]:checked').val(); +function isSmtpEnabled() { + return $('input[name="mailUseSmtp"]:checked').val(); } -function showCustomLogoSettings(value) -{ - $('#logoSettings').toggle(value==1); +function showCustomLogoSettings(value) { + $('#logoSettings').toggle(value == 1); } -function isCustomLogoEnabled() -{ - return $('input[name="useCustomLogo"]:checked').val(); +function isCustomLogoEnabled() { + return $('input[name="useCustomLogo"]:checked').val(); } function refreshCustomLogo() { - var imageDiv = $("#currentLogo"); - if(imageDiv && imageDiv.attr("src")) { - var logoUrl = imageDiv.attr("src").split("?")[0]; - imageDiv.attr("src", logoUrl+"?"+ (new Date()).getTime()); - } + var imageDiv = $("#currentLogo"); + if (imageDiv && imageDiv.attr("src")) { + var logoUrl = imageDiv.attr("src").split("?")[0]; + imageDiv.attr("src", logoUrl + "?" + (new Date()).getTime()); + } } -$(document).ready( function() { - var originalTrustedHostCount = $('input[name=trusted_host]').length; - - showSmtpSettings(isSmtpEnabled()); - showCustomLogoSettings(isCustomLogoEnabled()); - $('#generalSettingsSubmit').click( function() { - var doSubmit = function() - { - sendGeneralSettingsAJAX(); - }; - - var hasTrustedHostsChanged = false, - hosts = $('input[name=trusted_host]'); - if (hosts.length != originalTrustedHostCount) - { - hasTrustedHostsChanged = true; - } - else - { - hosts.each(function() { - hasTrustedHostsChanged |= this.defaultValue != this.value; - }); - } - - // if trusted hosts have changed, make sure to ask for confirmation - if (hasTrustedHostsChanged) - { - piwikHelper.modalConfirm('#confirmTrustedHostChange', {yes: doSubmit}); - } - else - { - doSubmit(); - } - }); +$(document).ready(function () { + var originalTrustedHostCount = $('input[name=trusted_host]').length; - $('input[name=mailUseSmtp]').click(function(){ - showSmtpSettings($(this).val()); - }); - $('input[name=useCustomLogo]').click(function(){ - refreshCustomLogo(); - showCustomLogoSettings($(this).val()); - }); - $('input').keypress( function(e) { - var key=e.keyCode || e.which; - if (key==13) { - $('#generalSettingsSubmit').click(); - } - } - ); - - $("#logoUploadForm").submit( function(data) { - var submittingForm = $( this ); - var frameName = "upload"+(new Date()).getTime(); - var uploadFrame = $("<iframe name=\""+frameName+"\" />"); - uploadFrame.css("display", "none"); - uploadFrame.load(function(data){ - setTimeout(function(){ - refreshCustomLogo(); - uploadFrame.remove();},1000); - }); - $("body:first").append(uploadFrame); - submittingForm.attr("target", frameName); - }); - - $('#customLogo').change(function(){$("#logoUploadForm").submit()}); - - // trusted hosts event handling - $('#trustedHostSettings .adminTable').on('click', '.remove-trusted-host', function(e) { - e.preventDefault(); - $(this).parent().parent().remove(); - return false; - }); - $('#trustedHostSettings .add-trusted-host').click(function(e) { - e.preventDefault(); - - // append new row to the table - $('#trustedHostSettings tbody').append('<tr>' - + '<td><input name="trusted_host" type="text" value=""/></td>' - + '<td><a href="#" class="remove-trusted-host">x</a></td>' - + '</tr>'); - return false; - }); + showSmtpSettings(isSmtpEnabled()); + showCustomLogoSettings(isCustomLogoEnabled()); + $('#generalSettingsSubmit').click(function () { + var doSubmit = function () { + sendGeneralSettingsAJAX(); + }; + + var hasTrustedHostsChanged = false, + hosts = $('input[name=trusted_host]'); + if (hosts.length != originalTrustedHostCount) { + hasTrustedHostsChanged = true; + } + else { + hosts.each(function () { + hasTrustedHostsChanged |= this.defaultValue != this.value; + }); + } + + // if trusted hosts have changed, make sure to ask for confirmation + if (hasTrustedHostsChanged) { + piwikHelper.modalConfirm('#confirmTrustedHostChange', {yes: doSubmit}); + } + else { + doSubmit(); + } + }); + + $('input[name=mailUseSmtp]').click(function () { + showSmtpSettings($(this).val()); + }); + $('input[name=useCustomLogo]').click(function () { + refreshCustomLogo(); + showCustomLogoSettings($(this).val()); + }); + $('input').keypress(function (e) { + var key = e.keyCode || e.which; + if (key == 13) { + $('#generalSettingsSubmit').click(); + } + } + ); + + $("#logoUploadForm").submit(function (data) { + var submittingForm = $(this); + var frameName = "upload" + (new Date()).getTime(); + var uploadFrame = $("<iframe name=\"" + frameName + "\" />"); + uploadFrame.css("display", "none"); + uploadFrame.load(function (data) { + setTimeout(function () { + refreshCustomLogo(); + uploadFrame.remove(); + }, 1000); + }); + $("body:first").append(uploadFrame); + submittingForm.attr("target", frameName); + }); + + $('#customLogo').change(function () {$("#logoUploadForm").submit()}); + + // trusted hosts event handling + $('#trustedHostSettings .adminTable').on('click', '.remove-trusted-host', function (e) { + e.preventDefault(); + $(this).parent().parent().remove(); + return false; + }); + $('#trustedHostSettings .add-trusted-host').click(function (e) { + e.preventDefault(); + + // append new row to the table + $('#trustedHostSettings tbody').append('<tr>' + + '<td><input name="trusted_host" type="text" value=""/></td>' + + '<td><a href="#" class="remove-trusted-host">x</a></td>' + + '</tr>'); + return false; + }); }); diff --git a/plugins/CoreAdminHome/templates/generalSettings.tpl b/plugins/CoreAdminHome/templates/generalSettings.tpl index b4aa4c28d4..5a97d6fe91 100644 --- a/plugins/CoreAdminHome/templates/generalSettings.tpl +++ b/plugins/CoreAdminHome/templates/generalSettings.tpl @@ -2,245 +2,247 @@ {loadJavascriptTranslations plugins='UsersManager'} {if $isSuperUser} -<h2>{'General_GeneralSettings'|translate}</h2> - -{ajaxErrorDiv id=ajaxError} -{ajaxLoadingDiv id=ajaxLoading} - -<table class="adminTable" style='width:900px;'> -<tr> - <td style='width:400px'>{'General_AllowPiwikArchivingToTriggerBrowser'|translate}</td> - <td style='width:220px'> - <fieldset> - <label><input type="radio" value="1" name="enableBrowserTriggerArchiving"{if $enableBrowserTriggerArchiving==1} checked="checked"{/if} /> - {'General_Yes'|translate} <br /> - <span class="form-description">{'General_Default'|translate}</span> - </label><br /><br /> - - <label><input type="radio" value="0" name="enableBrowserTriggerArchiving"{if $enableBrowserTriggerArchiving==0} checked="checked"{/if} /> - {'General_No'|translate} <br /> - <span class="form-description">{'General_ArchivingTriggerDescription'|translate:"<a href='?module=Proxy&action=redirect&url=http://piwik.org/docs/setup-auto-archiving/' target='_blank'>":"</a>"}</span> - </label> - </fieldset> - <td> - {capture assign=browserArchivingHelp} - {'General_ArchivingInlineHelp'|translate}<br /> - {'General_SeeTheOfficialDocumentationForMoreInformation'|translate:"<a href='?module=Proxy&action=redirect&url=http://piwik.org/docs/setup-auto-archiving/' target='_blank'>":"</a>"} - {/capture} - {$browserArchivingHelp|inlineHelp} - </td> -</tr> -<tr> - <td><label for="todayArchiveTimeToLive">{'General_ReportsContainingTodayWillBeProcessedAtMostEvery'|translate}</label></td> - <td> - {'General_NSeconds'|translate:"<input size='3' value='$todayArchiveTimeToLive' id='todayArchiveTimeToLive' />"} - </td> - <td width='450px'> - {capture assign=archiveTodayTTLHelp} - {if $showWarningCron} - <strong> - {'General_NewReportsWillBeProcessedByCron'|translate}<br/> - {'General_ReportsWillBeProcessedAtMostEveryHour'|translate} - {'General_IfArchivingIsFastYouCanSetupCronRunMoreOften'|translate}<br/> - </strong> - {/if} - {'General_SmallTrafficYouCanLeaveDefault'|translate:10}<br /> - {'General_MediumToHighTrafficItIsRecommendedTo'|translate:1800:3600} - {/capture} - {$archiveTodayTTLHelp|inlineHelp} - </td> -</tr> -<tr> - <td style='width:400px'>{'CoreAdminHome_CheckReleaseGetVersion'|translate}</td> - <td style='width:220px'> - <fieldset> - <label><input type="radio" value="0" name="enableBetaReleaseCheck"{if $enableBetaReleaseCheck==0} checked="checked"{/if} /> - {'CoreAdminHome_LatestStableRelease'|translate} <br /> - <span class="form-description">{'General_Recommended'|translate}</span> - </label><br /><br /> - - <label><input type="radio" value="1" name="enableBetaReleaseCheck"{if $enableBetaReleaseCheck==1} checked="checked"{/if} /> - {'CoreAdminHome_LatestBetaRelease'|translate} <br /> - <span class="form-description">{'CoreAdminHome_ForBetaTestersOnly'|translate}</span> - </label> - </fieldset> - <td> - {capture assign=checkReleaseHelp} - {'CoreAdminHome_DevelopmentProcess'|translate:"<a href='?module=Proxy&action=redirect&url=http://piwik.org/participate/development-process/' target='_blank'>":"</a>"}<br /> - {'CoreAdminHome_StableReleases'|translate:"<a href='?module=Proxy&action=redirect&url=http://piwik.org/participate/user-feedback/' target='_blank'>":"</a>"} - {/capture} - {$checkReleaseHelp|inlineHelp} - </td> -</tr> -</table> + <h2>{'General_GeneralSettings'|translate}</h2> + {ajaxErrorDiv id=ajaxError} + {ajaxLoadingDiv id=ajaxLoading} + <table class="adminTable" style='width:900px;'> + <tr> + <td style='width:400px'>{'General_AllowPiwikArchivingToTriggerBrowser'|translate}</td> + <td style='width:220px'> + <fieldset> + <label><input type="radio" value="1" name="enableBrowserTriggerArchiving"{if $enableBrowserTriggerArchiving==1} checked="checked"{/if} /> + {'General_Yes'|translate} <br/> + <span class="form-description">{'General_Default'|translate}</span> + </label><br/><br/> -<h2>{'CoreAdminHome_EmailServerSettings'|translate}</h2> -<div id='emailSettings'> -<table class="adminTable" style='width:600px;'> - <tr> - <td>{'General_UseSMTPServerForEmail'|translate}<br /> - <span class="form-description">{'General_SelectYesIfYouWantToSendEmailsViaServer'|translate}</span> - </td> - <td style='width:200px'> - <label><input type="radio" name="mailUseSmtp" value="1" {if $mail.transport eq 'smtp'} checked {/if}/> {'General_Yes'|translate}</label> - <label><input type="radio" name="mailUseSmtp" value="0" style ="margin-left:20px;" {if $mail.transport eq ''} checked {/if}/> {'General_No'|translate}</label> - </td> - </tr> -</table> -</div> + <label><input type="radio" value="0" name="enableBrowserTriggerArchiving"{if $enableBrowserTriggerArchiving==0} checked="checked"{/if} /> + {'General_No'|translate} <br/> + <span class="form-description">{'General_ArchivingTriggerDescription'|translate:"<a href='?module=Proxy&action=redirect&url=http://piwik.org/docs/setup-auto-archiving/' target='_blank'>":"</a>"}</span> + </label> + </fieldset> + <td> + {capture assign=browserArchivingHelp} + {'General_ArchivingInlineHelp'|translate} + <br/> + {'General_SeeTheOfficialDocumentationForMoreInformation'|translate:"<a href='?module=Proxy&action=redirect&url=http://piwik.org/docs/setup-auto-archiving/' target='_blank'>":"</a>"} + {/capture} + {$browserArchivingHelp|inlineHelp} + </td> + </tr> + <tr> + <td><label for="todayArchiveTimeToLive">{'General_ReportsContainingTodayWillBeProcessedAtMostEvery'|translate}</label></td> + <td> + {'General_NSeconds'|translate:"<input size='3' value='$todayArchiveTimeToLive' id='todayArchiveTimeToLive' />"} + </td> + <td width='450px'> + {capture assign=archiveTodayTTLHelp} + {if $showWarningCron} + <strong> + {'General_NewReportsWillBeProcessedByCron'|translate}<br/> + {'General_ReportsWillBeProcessedAtMostEveryHour'|translate} + {'General_IfArchivingIsFastYouCanSetupCronRunMoreOften'|translate}<br/> + </strong> + {/if} + {'General_SmallTrafficYouCanLeaveDefault'|translate:10} + <br/> + {'General_MediumToHighTrafficItIsRecommendedTo'|translate:1800:3600} + {/capture} + {$archiveTodayTTLHelp|inlineHelp} + </td> + </tr> + <tr> + <td style='width:400px'>{'CoreAdminHome_CheckReleaseGetVersion'|translate}</td> + <td style='width:220px'> + <fieldset> + <label><input type="radio" value="0" name="enableBetaReleaseCheck"{if $enableBetaReleaseCheck==0} checked="checked"{/if} /> + {'CoreAdminHome_LatestStableRelease'|translate} <br/> + <span class="form-description">{'General_Recommended'|translate}</span> + </label><br/><br/> -<div id='smtpSettings'> - <table class="adminTable" style='width:550px;'> - <tr> - <td><label for="mailHost">{'General_SmtpServerAddress'|translate}</label></td> - <td style='width:200px'><input type="text" id="mailHost" value="{$mail.host|escape}"></td> - </tr> - <tr> - <td><label for="mailPort">{'General_SmtpPort'|translate}</label><br /> - <span class="form-description">{'General_OptionalSmtpPort'|translate}</span></td> - <td><input type="text" id="mailPort" value="{$mail.port}"></td> - </tr> - <tr> - <td><label for="mailType">{'General_AuthenticationMethodSmtp'|translate}</label><br /> - <span class="form-description">{'General_OnlyUsedIfUserPwdIsSet'|translate}</span> - </td> - <td> - <select id="mailType"> - <option value="" {if $mail.type eq ''} selected="selected" {/if}></option> - <option id="plain" {if $mail.type eq 'Plain'} selected="selected" {/if} value="Plain">Plain</option> - <option id="login" {if $mail.type eq 'Login'} selected="selected" {/if} value="Login"> Login</option> - <option id="cram-md5" {if $mail.type eq 'Crammd5'} selected="selected" {/if} value="Crammd5"> Crammd5</option> - </select> - </td> - </tr> - <tr> - <td><label for="mailUsername">{'General_SmtpUsername'|translate}</label><br /> - <span class="form-description">{'General_OnlyEnterIfRequired'|translate}</span></td> - <td> - <input type="text" id="mailUsername" value = "{$mail.username|escape}" /> - </td> - </tr> - <tr> - <td><label for="mailPassword">{'General_SmtpPassword'|translate}</label><br /> + <label><input type="radio" value="1" name="enableBetaReleaseCheck"{if $enableBetaReleaseCheck==1} checked="checked"{/if} /> + {'CoreAdminHome_LatestBetaRelease'|translate} <br/> + <span class="form-description">{'CoreAdminHome_ForBetaTestersOnly'|translate}</span> + </label> + </fieldset> + <td> + {capture assign=checkReleaseHelp} + {'CoreAdminHome_DevelopmentProcess'|translate:"<a href='?module=Proxy&action=redirect&url=http://piwik.org/participate/development-process/' target='_blank'>":"</a>"} + <br/> + {'CoreAdminHome_StableReleases'|translate:"<a href='?module=Proxy&action=redirect&url=http://piwik.org/participate/user-feedback/' target='_blank'>":"</a>"} + {/capture} + {$checkReleaseHelp|inlineHelp} + </td> + </tr> + </table> + <h2>{'CoreAdminHome_EmailServerSettings'|translate}</h2> + <div id='emailSettings'> + <table class="adminTable" style='width:600px;'> + <tr> + <td>{'General_UseSMTPServerForEmail'|translate}<br/> + <span class="form-description">{'General_SelectYesIfYouWantToSendEmailsViaServer'|translate}</span> + </td> + <td style='width:200px'> + <label><input type="radio" name="mailUseSmtp" value="1" {if $mail.transport eq 'smtp'} checked {/if}/> {'General_Yes'|translate}</label> + <label><input type="radio" name="mailUseSmtp" value="0" + style="margin-left:20px;" {if $mail.transport eq ''} checked {/if}/> {'General_No'|translate}</label> + </td> + </tr> + </table> + </div> + <div id='smtpSettings'> + <table class="adminTable" style='width:550px;'> + <tr> + <td><label for="mailHost">{'General_SmtpServerAddress'|translate}</label></td> + <td style='width:200px'><input type="text" id="mailHost" value="{$mail.host|escape}"></td> + </tr> + <tr> + <td><label for="mailPort">{'General_SmtpPort'|translate}</label><br/> + <span class="form-description">{'General_OptionalSmtpPort'|translate}</span></td> + <td><input type="text" id="mailPort" value="{$mail.port}"></td> + </tr> + <tr> + <td><label for="mailType">{'General_AuthenticationMethodSmtp'|translate}</label><br/> + <span class="form-description">{'General_OnlyUsedIfUserPwdIsSet'|translate}</span> + </td> + <td> + <select id="mailType"> + <option value="" {if $mail.type eq ''} selected="selected" {/if}></option> + <option id="plain" {if $mail.type eq 'Plain'} selected="selected" {/if} value="Plain">Plain</option> + <option id="login" {if $mail.type eq 'Login'} selected="selected" {/if} value="Login"> Login</option> + <option id="cram-md5" {if $mail.type eq 'Crammd5'} selected="selected" {/if} value="Crammd5"> Crammd5</option> + </select> + </td> + </tr> + <tr> + <td><label for="mailUsername">{'General_SmtpUsername'|translate}</label><br/> + <span class="form-description">{'General_OnlyEnterIfRequired'|translate}</span></td> + <td> + <input type="text" id="mailUsername" value="{$mail.username|escape}"/> + </td> + </tr> + <tr> + <td><label for="mailPassword">{'General_SmtpPassword'|translate}</label><br/> <span class="form-description">{'General_OnlyEnterIfRequiredPassword'|translate}<br/> - {'General_WarningPasswordStored'|translate:"<strong>":"</strong>"}</span> - </td> - <td> - <input type="password" id="mailPassword" value = "{$mail.password|escape}" /> - </td> - </tr> - <tr> - <td><label for="mailEncryption">{'General_SmtpEncryption'|translate}</label><br /> - <span class="form-description">{'General_EncryptedSmtpTransport'|translate}</span></td> - <td> - <select id="mailEncryption"> - <option value="" {if $mail.encryption eq ''} selected="selected" {/if}></option> - <option id="ssl" {if $mail.encryption eq 'ssl'} selected="selected" {/if} value="ssl">SSL</option> - <option id="tls" {if $mail.encryption eq 'tls'} selected="selected" {/if} value="tls">TLS</option> - </select> - </td> - </tr> - </table> -</div> - -<div class="ui-confirm" id="confirmTrustedHostChange"> - <h2>{'CoreAdminHome_TrustedHostConfirm'|translate}</h2> - <input role="yes" type="button" value="{'General_Yes'|translate}" /> - <input role="no" type="button" value="{'General_No'|translate}" /> -</div> - -<h2 id="trustedHostsSection">{'CoreAdminHome_TrustedHostSettings'|translate}</h2> -<div id='trustedHostSettings'> -{* untrusted host warning (display again) *} - {if isset($isValidHost) && isset($invalidHostMessage) && !$isValidHost} - <div class="ajaxSuccess"> - <a style="float:right" href="http://piwik.org/faq/troubleshooting/#faq_171" target="_blank"><img src="themes/default/images/help_grey.png" /></a> - <strong>{'General_Warning'|translate}: </strong>{$invalidHostMessage} - </div> - {/if} - {if count($trustedHosts) eq 1 && (!isset($isValidHost) || $isValidHost)} - {'CoreAdminHome_PiwikIsInstalledAt'|translate}:  <input name="trusted_host" type="text" value="{$trustedHosts[0]}"/> - {else} - <p>{'CoreAdminHome_PiwikIsInstalledAt'|translate}:</p> - <table class="adminTable"> + {'General_WarningPasswordStored'|translate:"<strong>":"</strong>"}</span> + </td> + <td> + <input type="password" id="mailPassword" value="{$mail.password|escape}"/> + </td> + </tr> <tr> - <th style="width:250px">{'CoreAdminHome_ValidPiwikHostname'|translate}</th> - <th style="width:10px"> </th> + <td><label for="mailEncryption">{'General_SmtpEncryption'|translate}</label><br/> + <span class="form-description">{'General_EncryptedSmtpTransport'|translate}</span></td> + <td> + <select id="mailEncryption"> + <option value="" {if $mail.encryption eq ''} selected="selected" {/if}></option> + <option id="ssl" {if $mail.encryption eq 'ssl'} selected="selected" {/if} value="ssl">SSL</option> + <option id="tls" {if $mail.encryption eq 'tls'} selected="selected" {/if} value="tls">TLS</option> + </select> + </td> </tr> - {foreach from=$trustedHosts item=host key=hostIdx} + </table> + </div> + <div class="ui-confirm" id="confirmTrustedHostChange"> + <h2>{'CoreAdminHome_TrustedHostConfirm'|translate}</h2> + <input role="yes" type="button" value="{'General_Yes'|translate}"/> + <input role="no" type="button" value="{'General_No'|translate}"/> + </div> + <h2 id="trustedHostsSection">{'CoreAdminHome_TrustedHostSettings'|translate}</h2> + <div id='trustedHostSettings'> + {* untrusted host warning (display again) *} + {if isset($isValidHost) && isset($invalidHostMessage) && !$isValidHost} + <div class="ajaxSuccess"> + <a style="float:right" href="http://piwik.org/faq/troubleshooting/#faq_171" target="_blank"><img src="themes/default/images/help_grey.png"/></a> + <strong>{'General_Warning'|translate}: </strong>{$invalidHostMessage} + </div> + {/if} + {if count($trustedHosts) eq 1 && (!isset($isValidHost) || $isValidHost)} + {'CoreAdminHome_PiwikIsInstalledAt'|translate}:   + <input name="trusted_host" type="text" value="{$trustedHosts[0]}"/> + {else} + <p>{'CoreAdminHome_PiwikIsInstalledAt'|translate}:</p> + <table class="adminTable"> <tr> - <td><input name="trusted_host" type="text" value="{$host}"/></td> - <td> - <a href="#" class="remove-trusted-host">x</a> - </td> + <th style="width:250px">{'CoreAdminHome_ValidPiwikHostname'|translate}</th> + <th style="width:10px"> </th> </tr> - {/foreach} + {foreach from=$trustedHosts item=host key=hostIdx} + <tr> + <td><input name="trusted_host" type="text" value="{$host}"/></td> + <td> + <a href="#" class="remove-trusted-host">x</a> + </td> + </tr> + {/foreach} + </table> + <div class="adminTable add-trusted-host-container"> + <a href="#" class="add-trusted-host"><em>{'General_Add'|translate}</em></a> + </div> + {/if} + </div> + <h2>{'CoreAdminHome_BrandingSettings'|translate}</h2> + <div id='brandSettings'> + {'CoreAdminHome_CustomLogoHelpText'|translate} + <table class="adminTable" style='width:600px;'> + <tr> + <td> {'CoreAdminHome_UseCustomLogo'|translate}</td> + <td style='width:200px'> + <label><input type="radio" name="useCustomLogo" value="1" {if $branding.use_custom_logo == 1} checked {/if}/> {'General_Yes'|translate} + </label> + <label><input type="radio" name="useCustomLogo" value="0" + style="margin-left:20px;" {if $branding.use_custom_logo == 0} checked {/if}/> {'General_No'|translate}</label> + </td> + </tr> </table> - <div class="adminTable add-trusted-host-container"> - <a href="#" class="add-trusted-host"><em>{'General_Add'|translate}</em></a> - </div> - {/if} -</div> - -<h2>{'CoreAdminHome_BrandingSettings'|translate}</h2> -<div id='brandSettings'> -{'CoreAdminHome_CustomLogoHelpText'|translate} -<table class="adminTable" style='width:600px;'> - <tr> - <td> {'CoreAdminHome_UseCustomLogo'|translate}</td> - <td style='width:200px'> - <label><input type="radio" name="useCustomLogo" value="1" {if $branding.use_custom_logo == 1} checked {/if}/> {'General_Yes'|translate}</label> - <label><input type="radio" name="useCustomLogo" value="0" style ="margin-left:20px;" {if $branding.use_custom_logo == 0} checked {/if}/> {'General_No'|translate}</label> - </td> - </tr> -</table> -</div> - -<div id='logoSettings'> - {capture assign=giveUsFeedbackText}"{'General_GiveUsYourFeedback'|translate}"{/capture} - {capture assign=customLogoHelp} - {'CoreAdminHome_CustomLogoFeedbackInfo'|translate:$giveUsFeedbackText:"<a href='?module=CorePluginsAdmin&action=index' target='_blank'>":"</a>"} - {/capture} - {$customLogoHelp|inlineHelp} - <form id="logoUploadForm" method="post" enctype="multipart/form-data" action="index.php?module=CoreAdminHome&format=json&action=uploadCustomLogo"> - <table class="adminTable" style='width:550px;'> - <tr> - {if $logosWriteable} - <td><label for="customLogo">{'CoreAdminHome_LogoUpload'|translate}:<br /> - <span class="form-description">{'CoreAdminHome_LogoUploadDescription'|translate:"JPG / PNG / GIF":110}</span></label></td> - <td style='width:200px'> - <input name="customLogo" type="file" id="customLogo" /><img src="themes/logo.png?r={math equation='rand(10,1000)'}" id="currentLogo" height="150"/> - </td> - {else} - <td><span class="ajaxSuccess">{'CoreAdminHome_LogoNotWriteable'|translate:"<ul style='list-style: disc inside;'><li>/themes/</li><li>/themes/logo.png</li><li>/themes/logo-header.png</li></ul>"}</span></td> - {/if} - </tr> - </table> - </form> -</div> - -<input type="submit" value="{'General_Save'|translate}" id="generalSettingsSubmit" class="submit" /> -<br /><br /> - -{capture assign=clickDeleteLogSettings}{'PrivacyManager_DeleteDataSettings'|translate}{/capture} -<h2>{'PrivacyManager_DeleteDataSettings'|translate}</h2> -<p> - {'PrivacyManager_DeleteDataDescription'|translate} {'PrivacyManager_DeleteDataDescription2'|translate} -<br/> - <a href='{url module="PrivacyManager" action="privacySettings"}#deleteLogsAnchor'> - {'PrivacyManager_ClickHereSettings'|translate:"'$clickDeleteLogSettings'"} - </a> -</p> + </div> + <div id='logoSettings'> + {capture assign=giveUsFeedbackText}"{'General_GiveUsYourFeedback'|translate}"{/capture} + {capture assign=customLogoHelp} + {'CoreAdminHome_CustomLogoFeedbackInfo'|translate:$giveUsFeedbackText:"<a href='?module=CorePluginsAdmin&action=index' target='_blank'>":"</a>"} + {/capture} + {$customLogoHelp|inlineHelp} + <form id="logoUploadForm" method="post" enctype="multipart/form-data" action="index.php?module=CoreAdminHome&format=json&action=uploadCustomLogo"> + <table class="adminTable" style='width:550px;'> + <tr> + {if $logosWriteable} + <td><label for="customLogo">{'CoreAdminHome_LogoUpload'|translate}:<br/> + <span class="form-description">{'CoreAdminHome_LogoUploadDescription'|translate:"JPG / PNG / GIF":110}</span></label></td> + <td style='width:200px'> + <input name="customLogo" type="file" id="customLogo"/><img src="themes/logo.png?r={math equation='rand(10,1000)'}" id="currentLogo" + height="150"/> + </td> + {else} + <td> + <span class="ajaxSuccess">{'CoreAdminHome_LogoNotWriteable'|translate:"<ul style='list-style: disc inside;'><li>/themes/</li><li>/themes/logo.png</li><li>/themes/logo-header.png</li></ul>"}</span> + </td> + {/if} + </tr> + </table> + </form> + </div> + <input type="submit" value="{'General_Save'|translate}" id="generalSettingsSubmit" class="submit"/> + <br/> + <br/> + {capture assign=clickDeleteLogSettings}{'PrivacyManager_DeleteDataSettings'|translate}{/capture} + <h2>{'PrivacyManager_DeleteDataSettings'|translate}</h2> + <p> + {'PrivacyManager_DeleteDataDescription'|translate} {'PrivacyManager_DeleteDataDescription2'|translate} + <br/> + <a href='{url module="PrivacyManager" action="privacySettings"}#deleteLogsAnchor'> + {'PrivacyManager_ClickHereSettings'|translate:"'$clickDeleteLogSettings'"} + </a> + </p> {/if} <h2>{'CoreAdminHome_OptOutForYourVisitors'|translate}</h2> <p>{'CoreAdminHome_OptOutExplanation'|translate} -{capture name=optOutUrl}{$piwikUrl}index.php?module=CoreAdminHome&action=optOut&language={$language}{/capture} -{assign var=optOutUrl value=$smarty.capture.optOutUrl} -{capture name=iframeOptOut}<iframe frameborder="no" width="600px" height="200px" src="{$smarty.capture.optOutUrl}"></iframe>{/capture} -<code>{$smarty.capture.iframeOptOut|escape:'html'}</code> -<br/> -{'CoreAdminHome_OptOutExplanationBis'|translate:"<a href='$optOutUrl' target='_blank'>":"</a>"} + {capture name=optOutUrl}{$piwikUrl}index.php?module=CoreAdminHome&action=optOut&language={$language}{/capture} + {assign var=optOutUrl value=$smarty.capture.optOutUrl} + {capture name=iframeOptOut} + <iframe frameborder="no" width="600px" height="200px" src="{$smarty.capture.optOutUrl}"></iframe>{/capture} + <code>{$smarty.capture.iframeOptOut|escape:'html'}</code> + <br/> + {'CoreAdminHome_OptOutExplanationBis'|translate:"<a href='$optOutUrl' target='_blank'>":"</a>"} </p> {include file="CoreAdminHome/templates/footer.tpl"} diff --git a/plugins/CoreAdminHome/templates/header.tpl b/plugins/CoreAdminHome/templates/header.tpl index acfa47e3d4..84a8407b0a 100644 --- a/plugins/CoreAdminHome/templates/header.tpl +++ b/plugins/CoreAdminHome/templates/header.tpl @@ -1,80 +1,83 @@ <!DOCTYPE html> -<!--[if lt IE 9 ]><html class="old-ie"> <![endif]--> -<!--[if (gte IE 9)|!(IE)]><!--><html><!--<![endif]--> +<!--[if lt IE 9 ]> +<html class="old-ie"> <![endif]--> +<!--[if (gte IE 9)|!(IE)]><!--> +<html><!--<![endif]--> <head> -<title>{if !$isCustomLogo}Piwik › {/if}{'CoreAdminHome_Administration'|translate} - - - + {if !$isCustomLogo}Piwik › {/if}{'CoreAdminHome_Administration'|translate} + + + -{loadJavascriptTranslations plugins='CoreAdminHome CoreHome'} + {loadJavascriptTranslations plugins='CoreAdminHome CoreHome'} -{include file="CoreHome/templates/js_global_variables.tpl"} -{include file="CoreHome/templates/js_css_includes.tpl"} - -{include file="CoreHome/templates/iframe_buster_header.tpl"} + {include file="CoreHome/templates/js_global_variables.tpl"} + {include file="CoreHome/templates/js_css_includes.tpl"} + + {include file="CoreHome/templates/iframe_buster_header.tpl"} {include file="CoreHome/templates/iframe_buster_body.tpl"}

-{if !isset($showTopMenu) || $showTopMenu} -{assign var=showSitesSelection value=false} -{assign var=showPeriodSelection value=false} -{include file="CoreHome/templates/top_bar.tpl"} -{/if} + {if !isset($showTopMenu) || $showTopMenu} + {assign var=showSitesSelection value=false} + {assign var=showPeriodSelection value=false} + {include file="CoreHome/templates/top_bar.tpl"} + {/if} - + -{ajaxRequestErrorDiv} -
-{if !isset($showMenu) || $showMenu} - {include file="CoreAdminHome/templates/menu.tpl"} -{/if} + {ajaxRequestErrorDiv} +
+ {if !isset($showMenu) || $showMenu} + {include file="CoreAdminHome/templates/menu.tpl"} + {/if} -
+
-{include file="CoreHome/templates/header_message.tpl"} + {include file="CoreHome/templates/header_message.tpl"} -{if !empty($configFileNotWritable)} -
- {'General_ConfigFileIsNotWritable'|translate:"(config/config.ini.php)":"
"} -
-{elseif preg_match('/updated=[1-9]/', $url)} -
- {'General_YourChangesHaveBeenSaved'|translate} -
-{/if} + {if !empty($configFileNotWritable)} +
+ {'General_ConfigFileIsNotWritable'|translate:"(config/config.ini.php)":"
"} +
+ {elseif preg_match('/updated=[1-9]/', $url)} +
+ {'General_YourChangesHaveBeenSaved'|translate} +
+ {/if} -{if !empty($statisticsNotRecorded)} -
- {'General_StatisticsAreNotRecorded'|translate} -
-{/if} + {if !empty($statisticsNotRecorded)} +
+ {'General_StatisticsAreNotRecorded'|translate} +
+ {/if} -
-

- -
+
+

+ +
-{include file="CoreHome/templates/warning_invalid_host.tpl"} + {include file="CoreHome/templates/warning_invalid_host.tpl"} -{* missing plugins warning *} -{if $isSuperUser && !empty($missingPluginsWarning)} -
- {'General_Warning'|translate}: {$missingPluginsWarning} -
-{/if} + {* missing plugins warning *} + {if $isSuperUser && !empty($missingPluginsWarning)} +
+ {'General_Warning'|translate}: {$missingPluginsWarning} +
+ {/if} -{* old GeoIP plugin warning *} -{if $isSuperUser && !empty($usingOldGeoIPPlugin)} -
- {'General_Warning'|translate}: {'UserCountry_OldGeoIPWarning'|translate:'':'':'':'':'':'':'':''} -
-{/if} + {* old GeoIP plugin warning *} + {if $isSuperUser && !empty($usingOldGeoIPPlugin)} +
+ {'General_Warning'|translate} + : {'UserCountry_OldGeoIPWarning'|translate:'':'':'':'':'':'':'':''} +
+ {/if} diff --git a/plugins/CoreAdminHome/templates/jsTrackingGenerator.css b/plugins/CoreAdminHome/templates/jsTrackingGenerator.css index 7f242ddcc1..2aeafbbe6b 100644 --- a/plugins/CoreAdminHome/templates/jsTrackingGenerator.css +++ b/plugins/CoreAdminHome/templates/jsTrackingGenerator.css @@ -1,8 +1,8 @@ -#javascript-output-section textarea,#image-link-output-section textarea { - width:100%; - display: block; - color: #111; - font-family: "Courier New", Courier, monospace; +#javascript-output-section textarea, #image-link-output-section textarea { + width: 100%; + display: block; + color: #111; + font-family: "Courier New", Courier, monospace; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; @@ -10,67 +10,70 @@ } #javascript-output-section textarea { - height: 256px; + height: 256px; } #image-link-output-section textarea { - height: 128px; + height: 128px; } label { - margin-right: .35em; - display:inline-block; + margin-right: .35em; + display: inline-block; } -#optional-js-tracking-options>tbody>tr>td,#image-tracking-section>tbody>tr>td { - width:488px; - max-width:488px; +#optional-js-tracking-options>tbody>tr>td, #image-tracking-section>tbody>tr>td { + width: 488px; + max-width: 488px; } -.custom-variable-name,.custom-variable-value { - width:100px; +.custom-variable-name, .custom-variable-value { + width: 100px; } h3 { - margin-top:0; + margin-top: 0; } .small-form-description { - color:#666; - font-size:1em; - font-style:italic; - margin-left:4em; + color: #666; + font-size: 1em; + font-style: italic; + margin-left: 4em; } .tracking-option-section { - margin-bottom:1.5em; + margin-bottom: 1.5em; } -#javascript-output-section,#image-link-output-section { - padding-top:1em; +#javascript-output-section, #image-link-output-section { + padding-top: 1em; } -#optional-js-tracking-options th,#image-tracking-section th { - text-align:left; +#optional-js-tracking-options th, #image-tracking-section th { + text-align: left; } -#js-visitor-cv-extra,#js-page-cv-extra,#js-campaign-query-param-extra { - margin-left:1em; +#js-visitor-cv-extra, #js-page-cv-extra, #js-campaign-query-param-extra { + margin-left: 1em; } -#js-visitor-cv-extra td,#js-page-cv-extra td,#js-campaign-query-param-extra td { - vertical-align:middle; + +#js-visitor-cv-extra td, #js-page-cv-extra td, #js-campaign-query-param-extra td { + vertical-align: middle; } .revenue { - width:32px; + width: 32px; } + .goal-picker { - height:1.2em; + height: 1.2em; } + .goal-picker select { - width:128px; + width: 128px; } #js-campaign-query-param-extra input { - width:72px; + width: 72px; } diff --git a/plugins/CoreAdminHome/templates/jsTrackingGenerator.js b/plugins/CoreAdminHome/templates/jsTrackingGenerator.js index 10080fd814..d94a037356 100644 --- a/plugins/CoreAdminHome/templates/jsTrackingGenerator.js +++ b/plugins/CoreAdminHome/templates/jsTrackingGenerator.js @@ -5,228 +5,204 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ -(function($) { - -$(document).ready(function() { - - var piwikHost = window.location.host, - piwikPath = location.pathname.substring(0, location.pathname.lastIndexOf('/')); - - // - // utility methods - // - - // returns JavaScript code for tracking custom variables based on an array of - // custom variable name-value pairs (so an array of 2-element arrays) and - // a scope (either 'visit' or 'page') - var getCustomVariableJS = function(customVariables, scope) - { - var result = ''; - for (var i = 0; i != 5; ++i) - { - if (customVariables[i]) - { - var key = customVariables[i][0], - value = customVariables[i][1]; - result += ' _paq.push(["setCustomVariable", ' + (i + 1) + ', ' + JSON.stringify(key) + ', ' - + JSON.stringify(value) + ', ' + JSON.stringify(scope) + ']);\n'; - } - } - return result; - }; - - // gets the list of custom variables entered by the user in a custom variable - // section - var getCustomVariables = function (sectionId) - { - var customVariableNames = $('.custom-variable-name', '#' + sectionId), - customVariableValues = $('.custom-variable-value', '#' + sectionId); - - var result = []; - if ($('.section-toggler-link', '#' + sectionId).is(':checked')) - { - for (var i = 0; i != customVariableNames.length; ++i) - { - var name = $(customVariableNames[i]).val(); - - result[i] = null; - if (name) - { - result[i] = [name, $(customVariableValues[i]).val()]; - } - } - } - return result; - }; - - // quickly gets the host + port from a url - var getHostNameFromUrl = function(url) - { - var element = $('')[0]; - element.href = url; - return element.hostname; - }; - - // get preloaded server-side data necessary for code generation - var dataElement = $('#js-tracking-generator-data'), - currencySymbols = JSON.parse(dataElement.attr('data-currencies')), - siteUrls = {}, - siteCurrencies = {}, - allGoals = {}, - noneText = $('#image-tracker-goal>option').text(); - - // queries Piwik for needed site info for one site - var getSiteData = function (idSite, sectionSelect, callback) - { - // if data is already loaded, don't do an AJAX request - if (siteUrls[idSite] - && siteCurrencies[idSite] - && typeof allGoals[idSite] !== 'undefined') - { - callback(); - return; - } - - // disable section - $(sectionSelect).find('input,select,textarea').attr('disabled', 'disabled'); - - var ajaxRequest = new ajaxHelper(); - ajaxRequest.setBulkRequests( - // get site info (for currency) - { - module: 'API', - method: 'SitesManager.getSiteFromId', - idSite: idSite - }, - - // get site urls - { - module: 'API', - method: 'SitesManager.getSiteUrlsFromId', - idSite: idSite - }, - - // get site goals - { - module: 'API', - method: 'Goals.getGoals', - idSite: idSite - } - ); - ajaxRequest.setCallback(function (data) { - for (var i = 0; i != data.length; ++i) - { - data[i] = JSON.parse(data[i]); - } - - // set data - var currency = data[0][0].currency || ''; - siteCurrencies[idSite] = currencySymbols[currency.toUpperCase()]; - siteUrls[idSite] = data[1] || []; - allGoals[idSite] = data[2] || []; - - // re-enable controls - $(sectionSelect).find('input,select,textarea').removeAttr('disabled'); - - callback(); - }); - ajaxRequest.setFormat('json'); - ajaxRequest.send(false); - }; - - // resets the select options of a goal select using a site ID - var resetGoalSelectItems = function (idsite, id) - { - var selectElement = $('#' + id).html(''); - - selectElement.append($('').text(noneText)); - - var goals = allGoals[idsite] || []; - for (var key in goals) - { - var goal = goals[key]; - selectElement.append($('').text(noneText)); + + var goals = allGoals[idsite] || []; + for (var key in goals) { + var goal = goals[key]; + selectElement.append($('
+ + {/foreach} diff --git a/plugins/Live/templates/live.css b/plugins/Live/templates/live.css index e4fab30625..43ef43e2bd 100644 --- a/plugins/Live/templates/live.css +++ b/plugins/Live/templates/live.css @@ -1,99 +1,123 @@ #visitsLive { - text-align:left; - font-size:90%; - color:#444; + text-align: left; + font-size: 90%; + color: #444; } + #visitsLive .datetime, #visitsLive .country, #visitsLive .referer, #visitsLive .settings, #visitsLive .returning { - border-bottom: 1px solid #d3d1c5; - border-right:1px solid #d3d1c5; - padding:5px 5px 5px 12px; + border-bottom: 1px solid #d3d1c5; + border-right: 1px solid #d3d1c5; + padding: 5px 5px 5px 12px; } + #visitsLive .datetime { - background:#E4E2D7; - border-top:1px solid #d3d1c5; - margin:0; - text-align:left; + background: #E4E2D7; + border-top: 1px solid #d3d1c5; + margin: 0; + text-align: left; } + #visitsLive .country { - background:#FFF url(../../../plugins/CoreHome/templates/images/bullet1.gif) no-repeat scroll 0 0; + background: #FFF url(../../../plugins/CoreHome/templates/images/bullet1.gif) no-repeat scroll 0 0; } + #visitsLive .referer { - background:#F9FAFA none repeat scroll 0 0; + background: #F9FAFA none repeat scroll 0 0; } + #visitsLive .referer:hover { - background:#FFFFF7; + background: #FFFFF7; } + #visitsLive .pagesTitle { - display:block; - float:left; + display: block; + float: left; } + #visitsLive .settings { - background:#FFF none repeat scroll 0 0; + background: #FFF none repeat scroll 0 0; } -#visitsLive .settings a{ - text-decoration:none; + +#visitsLive .settings a { + text-decoration: none; } + #visitsLive .returning { - background:#F9FAFA none repeat scroll 0 0; + background: #F9FAFA none repeat scroll 0 0; } + .visitsLiveFooter img { - vertical-align:middle; + vertical-align: middle; } + .visitsLiveFooter { - line-height:2.5em; + line-height: 2.5em; } + .visitorLog table img { - margin:0 3px 0 0; + margin: 0 3px 0 0; } -.visitsLiveFooter a.rightLink{ - float:right; - padding-right:20px; + +.visitsLiveFooter a.rightLink { + float: right; + padding-right: 20px; } -#visitsLive .datetime a{ + +#visitsLive .datetime a { text-decoration: none; } + table.dataTable td.highlightField { - background-color:#FFFFCB !important; + background-color: #FFFFCB !important; } + ol.visitorLog { - list-style:decimal inside none; + list-style: decimal inside none; } + ol.visitorLog li { - margin-bottom:4px; + margin-bottom: 4px; } + #visitsLive img { - vertical-align:middle; + vertical-align: middle; } + .visitorRank img { - vertical-align:text-bottom; + vertical-align: text-bottom; } + .iconPadding { - margin-left:4px; - margin-right:4px; + margin-left: 4px; + margin-right: 4px; } + .visitorRank { - margin-left:15px; - border:1px solid #D8D8D8; - color:#474747; - border-radius:3px; - -moz-border-radius:3px; - -webkit-border-radius:3px; + margin-left: 15px; + border: 1px solid #D8D8D8; + color: #474747; + border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; padding: 3px 5px; } + #visitsLive .visitorRank { - padding:2px; - border:none; - margin-left:5px; + padding: 2px; + border: none; + margin-left: 5px; } + .hash { color: #BBB; font-size: 9pt; - margin-right:2px; + margin-right: 2px; } -.repeat { - font-weight: bold; + +.repeat { + font-weight: bold; border: 1px solid #444; - border-radius: 3px 3px 3px 3px; + border-radius: 3px 3px 3px 3px; -moz-border-radius: 3px 3px 3px 3px; -webkit-border-radius: 3px 3px 3px 3px; padding: 2px; diff --git a/plugins/Live/templates/scripts/live.js b/plugins/Live/templates/scripts/live.js index 13d34cce2f..bad5c82ee5 100644 --- a/plugins/Live/templates/scripts/live.js +++ b/plugins/Live/templates/scripts/live.js @@ -9,95 +9,95 @@ * jQuery Plugin for Live visitors widget */ -(function($) { +(function ($) { $.extend({ - liveWidget: new function() { - + liveWidget: new function () { + /** * Default settings for widgetPreview */ var settings = { - // Maximum numbers of rows to display in widget - maxRows: 10, - // minimal time in microseconds to wait between updates - interval: 3000, - // maximum time to wait between requests - maxInterval: 300000, - // url params to use for data request - dataUrlParams: null, - // callback triggered on a successfull update (content of widget changed) - onUpdate: null, - // speed for fade animation - fadeInSpeed: 'slow' + // Maximum numbers of rows to display in widget + maxRows: 10, + // minimal time in microseconds to wait between updates + interval: 3000, + // maximum time to wait between requests + maxInterval: 300000, + // url params to use for data request + dataUrlParams: null, + // callback triggered on a successfull update (content of widget changed) + onUpdate: null, + // speed for fade animation + fadeInSpeed: 'slow' }; - + var currentInterval, updated, updateInterval, liveWidget; var isStarted = true; - + /** * Update the widget */ function update() { - + // is content updated (eg new visits/views) updated = false; var ajaxRequest = new ajaxHelper(); ajaxRequest.addParams(settings.dataUrlParams, 'GET'); ajaxRequest.setFormat('html'); - ajaxRequest.setCallback( function(r) { + ajaxRequest.setCallback(function (r) { parseResponse(r); - + // add default interval to last interval if not updated or reset to default if so - if(!updated) { + if (!updated) { currentInterval += settings.interval; } else { currentInterval = settings.interval; - if(settings.onUpdate) settings.onUpdate(); + if (settings.onUpdate) settings.onUpdate(); } - + // check new interval doesn't reach the defined maximum - if(settings.maxInterval < currentInterval) { + if (settings.maxInterval < currentInterval) { currentInterval = settings.maxInterval; } - - if(isStarted) { + + if (isStarted) { window.clearTimeout(updateInterval); - if($(liveWidget).closest('body').length) { + if ($(liveWidget).closest('body').length) { updateInterval = window.setTimeout(update, currentInterval); } } }); ajaxRequest.send(false); }; - + /** * Parses the given response and updates the widget if newer content is available */ function parseResponse(data) { - if(!data || !data.length) { + if (!data || !data.length) { updated = false; return; } - + var items = $('li', $(data)); - for(var i=items.length;i--;){ + for (var i = items.length; i--;) { parseItem(items[i]); } }; - + /** * Parses the given item and updates or adds an entry to the list - * + * * @param item to parse */ function parseItem(item) { var visitId = $(item).attr('id'); - if($('#'+visitId, liveWidget).length) { - if($('#'+visitId, liveWidget).html() != $(item).html()) { + if ($('#' + visitId, liveWidget).length) { + if ($('#' + visitId, liveWidget).html() != $(item).html()) { updated = true; } - $('#'+visitId, liveWidget).remove(); + $('#' + visitId, liveWidget).remove(); $(liveWidget).prepend(item); } else { updated = true; @@ -106,65 +106,65 @@ $(item).fadeIn(settings.fadeInSpeed); } // remove rows if there are more than the maximum - $('li:gt('+(settings.maxRows-1)+')', liveWidget).remove(); + $('li:gt(' + (settings.maxRows - 1) + ')', liveWidget).remove(); }; - + /** * Constructor - * + * * @param object userSettings Settings to be used * @return void */ - this.construct = function(userSettings) { + this.construct = function (userSettings) { settings = jQuery.extend(settings, userSettings); - - if(!settings.dataUrlParams) { + + if (!settings.dataUrlParams) { console && console.error('liveWidget error: dataUrlParams needs to be defined in settings.'); return; } - + liveWidget = this; - + currentInterval = settings.interval; - + updateInterval = window.setTimeout(update, currentInterval); }; - + /** * Triggers an update for the widget - * + * * @return void */ - this.update = function() { + this.update = function () { update(); }; - + /** * Starts the automatic update cycle */ - this.start = function() { + this.start = function () { isStarted = true; currentInterval = 0; update(); }; - + /** * Stops the automatic update cycle */ - this.stop = function() { + this.stop = function () { isStarted = false; window.clearTimeout(updateInterval); }; - + /** * Set the interval for refresh */ - this.setInterval = function(interval) { + this.setInterval = function (interval) { currentInterval = interval; }; } }); - + /** * Makes liveWidget available with $().liveWidget() */ @@ -178,15 +178,13 @@ var pauseImage = "plugins/Live/templates/images/pause.gif"; var pauseDisabledImage = "plugins/Live/templates/images/pause_disabled.gif"; var playImage = "plugins/Live/templates/images/play.gif"; var playDisabledImage = "plugins/Live/templates/images/play_disabled.gif"; -function onClickPause() -{ - $('#pauseImage').attr('src', pauseImage); - $('#playImage').attr('src', playDisabledImage); - return $.liveWidget.stop(); +function onClickPause() { + $('#pauseImage').attr('src', pauseImage); + $('#playImage').attr('src', playDisabledImage); + return $.liveWidget.stop(); } -function onClickPlay() -{ - $('#playImage').attr('src', playImage); - $('#pauseImage').attr('src', pauseDisabledImage); - return $.liveWidget.start(); +function onClickPlay() { + $('#playImage').attr('src', playImage); + $('#pauseImage').attr('src', pauseDisabledImage); + return $.liveWidget.start(); } diff --git a/plugins/Live/templates/simpleLastVisitCount.tpl b/plugins/Live/templates/simpleLastVisitCount.tpl index bc893a93b0..65e2ae731e 100644 --- a/plugins/Live/templates/simpleLastVisitCount.tpl +++ b/plugins/Live/templates/simpleLastVisitCount.tpl @@ -1,121 +1,123 @@ {literal} - + {/literal}
-
-
{$visitors}
-
-
-
- {capture assign="visitsMessage"}{if $visits eq 1}{'General_OneVisit'|translate}{else}{'General_NVisits'|translate:$visits}{/if}{/capture} - {capture assign="actionsMessage"}{if $actions eq 1}{'General_OneAction'|translate}{else}{'VisitsSummary_NbActionsDescription'|translate:$actions}{/if}{/capture} - {capture assign="minutesMessage"}{if $lastMinutes eq 1}{'General_OneMinute'|translate}{else}{'General_NMinutes'|translate:$lastMinutes}{/if}{/capture} - - {'Live_SimpleRealTimeWidget_Message'|translate:$visitsMessage:$actionsMessage:$minutesMessage} -
+
+
{$visitors}
+
+
+ +
+ {capture assign="visitsMessage"}{if $visits eq 1}{'General_OneVisit'|translate}{else}{'General_NVisits'|translate:$visits}{/if}{/capture} + {capture assign="actionsMessage"}{if $actions eq 1}{'General_OneAction'|translate}{else}{'VisitsSummary_NbActionsDescription'|translate:$actions}{/if}{/capture} + {capture assign="minutesMessage"}{if $lastMinutes eq 1}{'General_OneMinute'|translate}{else}{'General_NMinutes'|translate:$lastMinutes}{/if}{/capture} + + {'Live_SimpleRealTimeWidget_Message'|translate:$visitsMessage:$actionsMessage:$minutesMessage} +
diff --git a/plugins/Live/templates/totalVisits.tpl b/plugins/Live/templates/totalVisits.tpl index 36d8558eb3..27c12873a0 100644 --- a/plugins/Live/templates/totalVisits.tpl +++ b/plugins/Live/templates/totalVisits.tpl @@ -1,26 +1,29 @@
- - - - - - - - - - - - - - - - - - - - -
-
{'General_Date'|translate}
-
{'General_ColumnNbVisits'|translate}
-
{'General_ColumnPageviews'|translate}
{'Live_LastHours'|translate:24}{$visitorsCountToday}{$pisToday}
{'Live_LastMinutes'|translate:30}{$visitorsCountHalfHour}{$pisHalfhour}
+ + + + + + + + + + + + + + + + + + + + +
+
{'General_Date'|translate}
+
+
{'General_ColumnNbVisits'|translate}
+
+
{'General_ColumnPageviews'|translate}
+
{'Live_LastHours'|translate:24}{$visitorsCountToday}{$pisToday}
{'Live_LastMinutes'|translate:30}{$visitorsCountHalfHour}{$pisHalfhour}
diff --git a/plugins/Live/templates/visitorLog.tpl b/plugins/Live/templates/visitorLog.tpl index f8b9183738..ff39c60179 100644 --- a/plugins/Live/templates/visitorLog.tpl +++ b/plugins/Live/templates/visitorLog.tpl @@ -1,316 +1,347 @@
{if !$isWidget} -

{if $javascriptVariablesToSet.filterEcommerce}{'Goals_EcommerceLog'|translate}{else}{'Live_VisitorLog'|translate}{/if}

- - {if !empty($reportDocumentation)} -

{$reportDocumentation}

- {/if} +

{if $javascriptVariablesToSet.filterEcommerce}{'Goals_EcommerceLog'|translate}{else}{'Live_VisitorLog'|translate}{/if}

+ {if !empty($reportDocumentation)} +

{$reportDocumentation}

+ {/if} {/if} {capture assign='displayVisitorsInOwnColumn'}{if $isWidget}0{else}1{/if}{/capture} {if isset($arrayDataTable.result) and $arrayDataTable.result == 'error'} - {$arrayDataTable.message} - {else} - {if count($arrayDataTable) == 0} - -
{'CoreHome_ThereIsNoDataForThisReport'|translate}
- {else} - + {$arrayDataTable.message} +{else} +{if count($arrayDataTable) == 0} + +
{'CoreHome_ThereIsNoDataForThisReport'|translate}
+{else} + + + + + + + {if $displayVisitorsInOwnColumn} + + {/if} + + + + + -
+
{'General_Date'|translate} +
+
+
{'General_Visitors'|translate} +
+
+
{'Live_Referrer_URL'|translate} +
+
+
{'General_ColumnNbActions'|translate} +
+
- - - - - {if $displayVisitorsInOwnColumn} - - {/if} - - - - - + {foreach from=$arrayDataTable item=visitor} -{foreach from=$arrayDataTable item=visitor} + {capture assign='visitorColumnContent'} +   + +   + +   + + {if $visitor.columns.visitorTypeIcon} + {if !empty($visitor.columns.visitorId)} + + {/if} +  - + + {if !empty($visitor.columns.visitorId)}{/if} + {/if} + + {if !$displayVisitorsInOwnColumn}

{/if} - {capture assign='visitorColumnContent'} -   -   -   - {if $visitor.columns.visitorTypeIcon} - {if !empty($visitor.columns.visitorId)} - +  {if $visitor.columns.visitConverted} + + + # + {$visitor.columns.goalConversions} + {if $visitor.columns.visitEcommerceStatusIcon} +  - + {/if} -  - - {if !empty($visitor.columns.visitorId)}{/if} - {/if} - - {if !$displayVisitorsInOwnColumn}

{/if} - -  {if $visitor.columns.visitConverted} - - - #{$visitor.columns.goalConversions} - {if $visitor.columns.visitEcommerceStatusIcon} -  - - {/if} - {/if} -
- {if $displayVisitorsInOwnColumn} - {if count($visitor.columns.pluginsIcons) > 0} -
- {'UserSettings_Plugins'|translate}: - {foreach from=$visitor.columns.pluginsIcons item=pluginIcon name=plugins} - {$pluginIcon.pluginName|capitalize:true} - {/foreach} - {/if} - {/if} - {/capture} - - {capture assign='visitorRow'} - - - + + + + {if $displayVisitorsInOwnColumn} + + {/if} - GPS (lat/long): {$visitor.columns.latitude|escape:'html'},{$visitor.columns.longitude|escape:'html'}{/if}">IP: {$visitor.columns.visitIp}{/if} - - {if (isset($visitor.columns.provider)&&$visitor.columns.provider!='IP')} -
- {'Provider_ColumnProvider'|translate}: - - {$visitor.columns.provider} - - {/if} - {if !empty($visitor.columns.customVariables)} -
- {foreach from=$visitor.columns.customVariables item=customVariable key=id} - {capture assign=name}customVariableName{$id}{/capture} - {capture assign=value}customVariableValue{$id}{/capture} -
{$customVariable.$name|truncate:30:"...":true|escape:'html'}{if strlen($customVariable.$value)>0}: {$customVariable.$value|truncate:50:"...":true|escape:'html'}{/if} - {/foreach} - {/if} - {if !$displayVisitorsInOwnColumn} -
- {$visitorColumnContent} - {/if} - - - {if $displayVisitorsInOwnColumn} - - {/if} - - - + - - {/capture} - - {if !$javascriptVariablesToSet.filterEcommerce - || !empty($visitorHasSomeEcommerceActivity)} - {$visitorRow} - {/if} -{/foreach} + {elseif empty($action.goalName)} + {* Page view / Download / Outlink *} + {if !empty($action.pageTitle)} + {if $action.type == 'search'}{/if} + {$action.pageTitle|unescape|urldecode|escape:'html'|truncate:80:"...":true} + {/if} + {if !empty($action.url)} + {if $action.type == 'action' && !empty($action.pageTitle)}
{/if} + {if $action.type == 'download' + || $action.type == 'outlink'} + + {/if} + {$action.url|escape:'html'|truncate:80:"...":true} + {elseif $action.type!='search'} +
+ {$javascriptVariablesToSet.pageUrlNotDefined} + {/if} + {else} + {* Goal conversion *} + + {$action.goalName|escape:'html'} + {if $action.revenue > 0}, {'Live_GoalRevenue'|translate}: + {$action.revenue|money:$javascriptVariablesToSet.idSite}{/if} + {/if} + + {/if} + {/foreach} + + + + {/capture} - -
-
{'General_Date'|translate}
-
{'General_Visitors'|translate}
-
{'Live_Referrer_URL'|translate}
-
{'General_ColumnNbActions'|translate}
- - {$visitor.columns.serverDatePrettyFirstAction} - {if $isWidget}
{else}-{/if} {$visitor.columns.serverTimePrettyFirstAction}
- {if !empty($visitor.columns.visitIp)}
+ {/foreach} + {/if} + {/if} + {/capture} + + {capture assign='visitorRow'} +
+ + {$visitor.columns.serverDatePrettyFirstAction} + {if $isWidget}
{else}-{/if} {$visitor.columns.serverTimePrettyFirstAction}
+ {if !empty($visitor.columns.visitIp)} +
+ + IP: {$visitor.columns.visitIp}{/if} + + {if (isset($visitor.columns.provider)&&$visitor.columns.provider!='IP')} +
+ {'Provider_ColumnProvider'|translate}: + + {$visitor.columns.provider} + + {/if} + {if !empty($visitor.columns.customVariables)} +
+ {foreach from=$visitor.columns.customVariables item=customVariable key=id} + {capture assign=name}customVariableName{$id}{/capture} + {capture assign=value}customVariableValue{$id}{/capture} +
+ {$customVariable.$name|truncate:30:"...":true|escape:'html'}{if strlen($customVariable.$value)>0}: {$customVariable.$value|truncate:50:"...":true|escape:'html'}{/if} + {/foreach} + {/if} + {if !$displayVisitorsInOwnColumn} +
+ {$visitorColumnContent} + {/if} +
+ {$visitorColumnContent} + - {$visitorColumnContent} - -
- {if $visitor.columns.referrerType == 'website'} - {'Referers_ColumnWebsite'|translate}: - - {$visitor.columns.referrerName|escape:'html'} - - {/if} - {if $visitor.columns.referrerType == 'campaign'} - {'Referers_ColumnCampaign'|translate} -
- {$visitor.columns.referrerName|escape:'html'} - {if !empty($visitor.columns.referrerKeyword)} - {$visitor.columns.referrerKeyword|escape:'html'}{/if} - {/if} - {if $visitor.columns.referrerType == 'search'} - {if !empty($visitor.columns.searchEngineIcon)} - {$visitor.columns.referrerName|escape:'html'} - {/if} - {$visitor.columns.referrerName|escape:'html'} - {if !empty($visitor.columns.referrerKeyword)}{'Referers_Keywords'|translate}: -
- - "{$visitor.columns.referrerKeyword|escape:'html'}" - {/if} - {capture assign='keyword'}{$visitor.columns.referrerKeyword|escape:'html'}{/capture} - {capture assign='searchName'}{$visitor.columns.referrerName|escape:"html"}{/capture} - {capture assign='position'}#{$visitor.columns.referrerKeywordPosition}{/capture} - {if !empty($visitor.columns.referrerKeywordPosition)}#{$visitor.columns.referrerKeywordPosition}{/if} - {/if} - {if $visitor.columns.referrerType == 'direct'}{'Referers_DirectEntry'|translate}{/if} -
-
- - {$visitor.columns.actionDetails|@count} - {if $visitor.columns.actionDetails|@count <= 1} - {'Live_Action'|translate} - {else} - {'Live_Actions'|translate} - {/if} - {if $visitor.columns.visitDuration > 0}- {$visitor.columns.visitDurationPretty}{/if} - -
-
    - {capture assign='visitorHasSomeEcommerceActivity'}0{/capture} - {foreach from=$visitor.columns.actionDetails item=action} - {capture assign='customVariablesTooltip'}{if !empty($action.customVariables)}{'CustomVariables_CustomVariables'|translate} - {foreach from=$action.customVariables item=customVariable key=id}{capture assign=name}customVariableName{$id}{/capture}{capture assign=value}customVariableValue{$id}{/capture} +
+
+ {if $visitor.columns.referrerType == 'website'} + {'Referers_ColumnWebsite'|translate}: + + {$visitor.columns.referrerName|escape:'html'} + + {/if} + {if $visitor.columns.referrerType == 'campaign'} + {'Referers_ColumnCampaign'|translate} +
+ {$visitor.columns.referrerName|escape:'html'} + {if !empty($visitor.columns.referrerKeyword)} - {$visitor.columns.referrerKeyword|escape:'html'}{/if} + {/if} + {if $visitor.columns.referrerType == 'search'} + {if !empty($visitor.columns.searchEngineIcon)} + {$visitor.columns.referrerName|escape:'html'} + {/if} + {$visitor.columns.referrerName|escape:'html'} + {if !empty($visitor.columns.referrerKeyword)}{'Referers_Keywords'|translate}: +
+ + "{$visitor.columns.referrerKeyword|escape:'html'}" + {/if} + {capture assign='keyword'}{$visitor.columns.referrerKeyword|escape:'html'}{/capture} + {capture assign='searchName'}{$visitor.columns.referrerName|escape:"html"}{/capture} + {capture assign='position'}#{$visitor.columns.referrerKeywordPosition}{/capture} + {if !empty($visitor.columns.referrerKeywordPosition)} + # + {$visitor.columns.referrerKeywordPosition}{/if} + {/if} + {if $visitor.columns.referrerType == 'direct'}{'Referers_DirectEntry'|translate}{/if} +
+
+ + {$visitor.columns.actionDetails|@count} + {if $visitor.columns.actionDetails|@count <= 1} + {'Live_Action'|translate} + {else} + {'Live_Actions'|translate} + {/if} + {if $visitor.columns.visitDuration > 0}- {$visitor.columns.visitDurationPretty}{/if} + +
+
    + {capture assign='visitorHasSomeEcommerceActivity'}0{/capture} + {foreach from=$visitor.columns.actionDetails item=action} + {capture assign='customVariablesTooltip'}{if !empty($action.customVariables)}{'CustomVariables_CustomVariables'|translate} + {foreach from=$action.customVariables item=customVariable key=id}{capture assign=name}customVariableName{$id}{/capture}{capture assign=value}customVariableValue{$id}{/capture} -- {$customVariable.$name|escape:'html'} {if strlen($customVariable.$value) > 0} = {$customVariable.$value|escape:'html'}{/if} - {/foreach}{/if} - {/capture} - {if !$javascriptVariablesToSet.filterEcommerce - || $action.type == 'ecommerceOrder' - || $action.type == 'ecommerceAbandonedCart'} -
  1. - {if $action.type == 'ecommerceOrder' || $action.type == 'ecommerceAbandonedCart'} - {* Ecommerce Abandoned Cart / Ecommerce Order *} - - - {if $action.type == 'ecommerceOrder'} - {capture assign='visitorHasSomeEcommerceActivity'}1{/capture} - {'Goals_EcommerceOrder'|translate} ({$action.orderId}) - {else}{'Goals_AbandonedCart'|translate} - - {* TODO: would be nice to have the icons Orders / Cart in the ecommerce log footer *} - {if $javascriptVariablesToSet.filterEcommerce == 2}{capture assign='visitorHasSomeEcommerceActivity'}1{/capture}{/if} - - {/if}
    - + {if $action.type == 'ecommerceOrder' || $action.type == 'ecommerceAbandonedCart'} + {* Ecommerce Abandoned Cart / Ecommerce Order *} + + {if $action.type == 'ecommerceOrder'} + {capture assign='visitorHasSomeEcommerceActivity'}1{/capture} + {'Goals_EcommerceOrder'|translate} + ({$action.orderId}) + {else}{'Goals_AbandonedCart'|translate} + + {* TODO: would be nice to have the icons Orders / Cart in the ecommerce log footer *} + {if $javascriptVariablesToSet.filterEcommerce == 2}{capture assign='visitorHasSomeEcommerceActivity'}1{/capture}{/if} + + {/if} +
    + {if $action.type == 'ecommerceOrder'} - {'Live_GoalRevenue'|translate}: - {else} - {capture assign='revenueLeft'}{'Live_GoalRevenue'|translate}{/capture}{'Goals_LeftInCart'|translate:$revenueLeft}: - {/if} - {$action.revenue|money:$javascriptVariablesToSet.idSite}{if $action.type == 'ecommerceOrder'}{/if}, - {'General_Quantity'|translate}: {$action.items} - - {* Ecommerce items in Cart/Order *} - {if !empty($action.itemDetails)} -
      - {foreach from=$action.itemDetails item=product} -
    • {$product.itemSKU|escape}{if !empty($product.itemName)}: {$product.itemName|escape}{/if}{if !empty($product.itemCategory)} ({$product.itemCategory|escape}){/if}, - {'General_Quantity'|translate}: {$product.quantity}, - {'General_Price'|translate}: {$product.price|money:$javascriptVariablesToSet.idSite} -
    • - {/foreach} -
    - {/if} + {else} + {capture assign='revenueLeft'}{'Live_GoalRevenue'|translate}{/capture}{'Goals_LeftInCart'|translate:$revenueLeft} + : + {/if} + {$action.revenue|money:$javascriptVariablesToSet.idSite}{if $action.type == 'ecommerceOrder'} + {/if}, + {'General_Quantity'|translate}: {$action.items} + + {* Ecommerce items in Cart/Order *} + {if !empty($action.itemDetails)} +
      + {foreach from=$action.itemDetails item=product} +
    • {$product.itemSKU|escape}{if !empty($product.itemName)}: {$product.itemName|escape}{/if}{if !empty($product.itemCategory)} ({$product.itemCategory|escape}){/if} + , + {'General_Quantity'|translate}: {$product.quantity}, + {'General_Price'|translate}: {$product.price|money:$javascriptVariablesToSet.idSite} +
    • + {/foreach} +
    + {/if}
    - - {elseif empty($action.goalName)} - {* Page view / Download / Outlink *} - {if !empty($action.pageTitle)} - {if $action.type == 'search'}{/if} - {$action.pageTitle|unescape|urldecode|escape:'html'|truncate:80:"...":true} - {/if} - {if !empty($action.url)} - {if $action.type == 'action' && !empty($action.pageTitle)}
    {/if} - {if $action.type == 'download' - || $action.type == 'outlink'} - - {/if} - {$action.url|escape:'html'|truncate:80:"...":true} - {elseif $action.type!='search'} -
    - {$javascriptVariablesToSet.pageUrlNotDefined} - {/if} - {else} - {* Goal conversion *} - - {$action.goalName|escape:'html'} - {if $action.revenue > 0}, {'Live_GoalRevenue'|translate}: {$action.revenue|money:$javascriptVariablesToSet.idSite}{/if} - {/if} -
  2. - {/if} - {/foreach} -
-
-{/if} + {if !$javascriptVariablesToSet.filterEcommerce + || !empty($visitorHasSomeEcommerceActivity)} + {$visitorRow} + {/if} + {/foreach} -{if $properties.show_footer} - {include file="CoreHome/templates/datatable_footer.tpl"} + + {/if} -{include file="CoreHome/templates/datatable_js.tpl"} - + {/literal} + {/if} {literal} - + {/literal}
diff --git a/plugins/Login/Auth.php b/plugins/Login/Auth.php index c8c58e7aaf..a7a8a2e490 100644 --- a/plugins/Login/Auth.php +++ b/plugins/Login/Auth.php @@ -15,106 +15,101 @@ */ class Piwik_Login_Auth implements Piwik_Auth { - protected $login = null; - protected $token_auth = null; + protected $login = null; + protected $token_auth = null; - /** - * Authentication module's name, e.g., "Login" - * - * @return string - */ - public function getName() - { - return 'Login'; - } + /** + * Authentication module's name, e.g., "Login" + * + * @return string + */ + public function getName() + { + return 'Login'; + } - /** - * Authenticates user - * - * @return Piwik_Auth_Result - */ - public function authenticate() - { - $rootLogin = Piwik_Config::getInstance()->superuser['login']; - $rootPassword = Piwik_Config::getInstance()->superuser['password']; - $rootToken = Piwik_UsersManager_API::getInstance()->getTokenAuth($rootLogin, $rootPassword); + /** + * Authenticates user + * + * @return Piwik_Auth_Result + */ + public function authenticate() + { + $rootLogin = Piwik_Config::getInstance()->superuser['login']; + $rootPassword = Piwik_Config::getInstance()->superuser['password']; + $rootToken = Piwik_UsersManager_API::getInstance()->getTokenAuth($rootLogin, $rootPassword); - if(is_null($this->login)) - { - if($this->token_auth === $rootToken) - { - return new Piwik_Auth_Result(Piwik_Auth_Result::SUCCESS_SUPERUSER_AUTH_CODE, $rootLogin, $this->token_auth ); - } + if (is_null($this->login)) { + if ($this->token_auth === $rootToken) { + return new Piwik_Auth_Result(Piwik_Auth_Result::SUCCESS_SUPERUSER_AUTH_CODE, $rootLogin, $this->token_auth); + } - $login = Piwik_FetchOne( - 'SELECT login - FROM '.Piwik_Common::prefixTable('user').' + $login = Piwik_FetchOne( + 'SELECT login + FROM ' . Piwik_Common::prefixTable('user') . ' WHERE token_auth = ?', - array($this->token_auth) - ); - if(!empty($login)) - { - return new Piwik_Auth_Result(Piwik_Auth_Result::SUCCESS, $login, $this->token_auth ); - } - } - else if(!empty($this->login)) - { - if($this->login === $rootLogin - && ($this->getHashTokenAuth($rootLogin, $rootToken) === $this->token_auth) - || $rootToken === $this->token_auth) - { - $this->setTokenAuth($rootToken); - return new Piwik_Auth_Result(Piwik_Auth_Result::SUCCESS_SUPERUSER_AUTH_CODE, $rootLogin, $this->token_auth ); - } + array($this->token_auth) + ); + if (!empty($login)) { + return new Piwik_Auth_Result(Piwik_Auth_Result::SUCCESS, $login, $this->token_auth); + } + } else if (!empty($this->login)) { + if ($this->login === $rootLogin + && ($this->getHashTokenAuth($rootLogin, $rootToken) === $this->token_auth) + || $rootToken === $this->token_auth + ) { + $this->setTokenAuth($rootToken); + return new Piwik_Auth_Result(Piwik_Auth_Result::SUCCESS_SUPERUSER_AUTH_CODE, $rootLogin, $this->token_auth); + } - $login = $this->login; - $userToken = Piwik_FetchOne( - 'SELECT token_auth - FROM '.Piwik_Common::prefixTable('user').' + $login = $this->login; + $userToken = Piwik_FetchOne( + 'SELECT token_auth + FROM ' . Piwik_Common::prefixTable('user') . ' WHERE login = ?', - array($login) - ); - if(!empty($userToken) - && (($this->getHashTokenAuth($login, $userToken) === $this->token_auth) - || $userToken === $this->token_auth)) - { - $this->setTokenAuth($userToken); - return new Piwik_Auth_Result(Piwik_Auth_Result::SUCCESS, $login, $userToken ); - } - } + array($login) + ); + if (!empty($userToken) + && (($this->getHashTokenAuth($login, $userToken) === $this->token_auth) + || $userToken === $this->token_auth) + ) { + $this->setTokenAuth($userToken); + return new Piwik_Auth_Result(Piwik_Auth_Result::SUCCESS, $login, $userToken); + } + } - return new Piwik_Auth_Result( Piwik_Auth_Result::FAILURE, $this->login, $this->token_auth ); - } + return new Piwik_Auth_Result(Piwik_Auth_Result::FAILURE, $this->login, $this->token_auth); + } - /** - * Accessor to set login name - * - * @param string $login user login - */ - public function setLogin($login) - { - $this->login = $login; - } + /** + * Accessor to set login name + * + * @param string $login user login + */ + public function setLogin($login) + { + $this->login = $login; + } - /** - * Accessor to set authentication token - * - * @param string $token_auth authentication token - */ - public function setTokenAuth($token_auth) - { - $this->token_auth = $token_auth; - } + /** + * Accessor to set authentication token + * + * @param string $token_auth authentication token + */ + public function setTokenAuth($token_auth) + { + $this->token_auth = $token_auth; + } - /** - * Accessor to compute the hashed authentication token - * - * @param string $login user login - * @param string $token_auth authentication token - * @return string hashed authentication token - */ - public function getHashTokenAuth($login, $token_auth) - { - return md5($login . $token_auth); - } + /** + * Accessor to compute the hashed authentication token + * + * @param string $login user login + * @param string $token_auth authentication token + * @return string hashed authentication token + */ + public function getHashTokenAuth($login, $token_auth) + { + return md5($login . $token_auth); + } } diff --git a/plugins/Login/Controller.php b/plugins/Login/Controller.php index 7031ca4307..912c5f441d 100644 --- a/plugins/Login/Controller.php +++ b/plugins/Login/Controller.php @@ -16,510 +16,470 @@ */ class Piwik_Login_Controller extends Piwik_Controller { - /** - * Generate hash on user info and password - * - * @param string $userinfo User name, email, etc - * @param string $password - * @return string - */ - private function generateHash($userInfo, $password) - { - // mitigate rainbow table attack - $passwordLen = strlen($password) / 2; - $hash = Piwik_Common::hash( - $userInfo . substr($password, 0, $passwordLen) - . Piwik_Common::getSalt() . substr($password, $passwordLen) - ); - return $hash; - } - - /** - * Default action - * - * @param none - * @return void - */ - function index() - { - $this->login(); - } - - /** - * Login form - * - * @param string $messageNoAccess Access error message - * @param string $currentUrl Current URL - * @return void - */ - function login($messageNoAccess = null, $infoMessage = false) - { - self::checkForceSslLogin(); - - $form = new Piwik_Login_FormLogin(); - if($form->validate()) - { - $nonce = $form->getSubmitValue('form_nonce'); - if(Piwik_Nonce::verifyNonce('Piwik_Login.login', $nonce)) - { - $login = $form->getSubmitValue('form_login'); - $password = $form->getSubmitValue('form_password'); - $rememberMe = $form->getSubmitValue('form_rememberme') == '1'; - $md5Password = md5($password); - try { - $this->authenticateAndRedirect($login, $md5Password, $rememberMe); - } catch(Exception $e) { - $messageNoAccess = $e->getMessage(); - } - } - else - { - $messageNoAccess = $this->getMessageExceptionNoAccess(); - } - } - - $view = Piwik_View::factory('login'); - $view->AccessErrorString = $messageNoAccess; - $view->infoMessage = nl2br($infoMessage); - $view->addForm( $form ); - $this->configureView($view); - self::setHostValidationVariablesView($view); - echo $view->render(); - } - - /** - * Configure common view properties - * - * @param Piwik_View $view - */ - private function configureView($view) - { - $this->setBasicVariablesView($view); - - $view->linkTitle = Piwik::getRandomTitle(); - - $view->forceSslLogin = Piwik_Config::getInstance()->General['force_ssl_login']; - - // crsf token: don't trust the submitted value; generate/fetch it from session data - $view->nonce = Piwik_Nonce::getNonce('Piwik_Login.login'); - } - - /** - * Form-less login - * @see how to use it on http://piwik.org/faq/how-to/#faq_30 - * @throws Exception - * @return void - */ - function logme() - { - self::checkForceSslLogin(); - - $password = Piwik_Common::getRequestVar('password', null, 'string'); - if(strlen($password) != 32) - { - throw new Exception(Piwik_TranslateException('Login_ExceptionPasswordMD5HashExpected')); - } - - $login = Piwik_Common::getRequestVar('login', null, 'string'); - if($login == Piwik_Config::getInstance()->superuser['login']) - { - throw new Exception(Piwik_TranslateException('Login_ExceptionInvalidSuperUserAuthenticationMethod', array("logme"))); - } - - $currentUrl = 'index.php'; - - if(($idSite = Piwik_Common::getRequestVar('idSite', false, 'int')) !== false) - { - $currentUrl .= '?idSite='.$idSite; - } - - $urlToRedirect = Piwik_Common::getRequestVar('url', $currentUrl, 'string'); - $urlToRedirect = Piwik_Common::unsanitizeInputValue($urlToRedirect); - - $this->authenticateAndRedirect($login, $password, false, $urlToRedirect); - } - - /** - * Authenticate user and password. Redirect if successful. - * - * @param string $login user name - * @param string $md5Password md5 hash of password - * @param bool $rememberMe Remember me? - * @param string $urlToRedirect URL to redirect to, if successfully authenticated - * @return string failure message if unable to authenticate - */ - protected function authenticateAndRedirect($login, $md5Password, $rememberMe, $urlToRedirect = 'index.php') - { - $info = array( 'login' => $login, - 'md5Password' => $md5Password, - 'rememberMe' => $rememberMe, - ); - Piwik_Nonce::discardNonce('Piwik_Login.login'); - Piwik_PostEvent('Login.initSession', $info); - Piwik_Url::redirectToUrl($urlToRedirect); - } - - protected function getMessageExceptionNoAccess() - { - $message = Piwik_Translate('Login_InvalidNonceOrHeadersOrReferer', array('', '')); - // Should mention trusted_hosts or link to FAQ - return $message; - } - - /** - * Reset password action. Stores new password as hash and sends email - * to confirm use. - * - * @param none - * @return void - */ - function resetPassword() - { - self::checkForceSslLogin(); - - $infoMessage = null; - $formErrors = null; - - $form = new Piwik_Login_FormResetPassword(); - if($form->validate()) - { - $nonce = $form->getSubmitValue('form_nonce'); - if(Piwik_Nonce::verifyNonce('Piwik_Login.login', $nonce)) - { - $formErrors = $this->resetPasswordFirstStep($form); - if (empty($formErrors)) - { - $infoMessage = Piwik_Translate('Login_ConfirmationLinkSent'); - } - } - else - { - $formErrors = array($this->getMessageExceptionNoAccess()); - } - } - else - { - // if invalid, display error - $formData = $form->getFormData(); - $formErrors = $formData['errors']; - } - - $view = Piwik_View::factory('message'); - $view->infoMessage = $infoMessage; - $view->formErrors = $formErrors; - echo $view->render(); - } - - /** - * Saves password reset info and sends confirmation email. - * - * @return array Error message(s) if an error occurs. - */ - private function resetPasswordFirstStep( $form ) - { - $loginMail = $form->getSubmitValue('form_login'); - $token = $form->getSubmitValue('form_token'); - $password = $form->getSubmitValue('form_password'); - - // check the password - try - { - Piwik_UsersManager::checkPassword($password); - } - catch (Exception $ex) - { - return array($ex->getMessage()); - } - - // get the user's login - if ($loginMail === 'anonymous') - { - return array(Piwik_Translate('Login_InvalidUsernameEmail')); - } - - $user = self::getUserInformation($loginMail); - if ($user === null) - { - return array(Piwik_Translate('Login_InvalidUsernameEmail')); - } - - $login = $user['login']; - - // if valid, store password information in options table, then... - Piwik_Login::savePasswordResetInfo($login, $password); - - // ... send email with confirmation link - try - { - $this->sendEmailConfirmationLink($user); - } - catch (Exception $ex) - { - // remove password reset info - Piwik_Login::removePasswordResetInfo($login); - - return array($ex->getMessage().'
'.Piwik_Translate('Login_ContactAdmin')); - } - - return null; - } - - /** - * Sends email confirmation link for a password reset request. - * - * @param array $user User info for the requested password reset. - */ - private function sendEmailConfirmationLink( $user ) - { - $login = $user['login']; - $email = $user['email']; - - // construct a password reset token from user information - $resetToken = self::generatePasswordResetToken($user); - - $ip = Piwik_IP::getIpFromHeader(); - $url = Piwik_Url::getCurrentUrlWithoutQueryString() - . "?module=Login&action=confirmResetPassword&login=".urlencode($login) - . "&resetToken=".urlencode($resetToken); - - // send email with new password - $mail = new Piwik_Mail(); - $mail->addTo($email, $login); - $mail->setSubject(Piwik_Translate('Login_MailTopicPasswordChange')); - $bodyText = str_replace( - '\n', - "\n", - sprintf(Piwik_Translate('Login_MailPasswordChangeBody'), $login, $ip, $url) - ) . "\n"; - $mail->setBodyText($bodyText); - - $fromEmailName = Piwik_Config::getInstance()->General['login_password_recovery_email_name']; - $fromEmailAddress = Piwik_Config::getInstance()->General['login_password_recovery_email_address']; - $mail->setFrom($fromEmailAddress, $fromEmailName); - @$mail->send(); - } - - /** - * Password reset confirmation action. Finishes the password reset process. - * Users visit this action from a link supplied in an email. - */ - public function confirmResetPassword() - { - $errorMessage = null; - - $login = Piwik_Common::getRequestVar('login', ''); - $resetToken = Piwik_Common::getRequestVar('resetToken', ''); - - try - { - // get password reset info & user info - $user = self::getUserInformation($login); - if ($user === null) - { - throw new Exception(Piwik_Translate('Login_InvalidUsernameEmail')); - } - - // check that the reset token is valid - $resetPassword = Piwik_Login::getPasswordToResetTo($login); - if ($resetPassword === false || !self::isValidToken($resetToken, $user)) - { - throw new Exception(Piwik_Translate('Login_InvalidOrExpiredToken')); - } - - // reset password of user - $this->setNewUserPassword($user, $resetPassword); - } - catch (Exception $ex) - { - $errorMessage = $ex->getMessage(); - } - - if (is_null($errorMessage)) // if success, show login w/ success message - { - $this->redirectToIndex('Login', 'resetPasswordSuccess'); - } - else - { - // show login page w/ error. this will keep the token in the URL - return $this->login($errorMessage); - } - } - - /** - * Sets the password for a user. - * - * @param array $user User info. - * @param string $passwordHash The hashed password to use. - */ - private function setNewUserPassword( $user, $passwordHash ) - { - if (strlen($passwordHash) !== 32) // sanity check - { - throw new Exception( - "setNewUserPassword called w/ incorrect password hash. Something has gone terribly wrong."); - } - - if( $user['email'] == Piwik::getSuperUserEmail() ) - { - if(!Piwik_Config::getInstance()->isFileWritable()) - { - throw new Exception(Piwik_Translate('General_ConfigFileIsNotWritable', array("(config/config.ini.php)","
"))); - } - - $user['password'] = $passwordHash; - Piwik_Config::getInstance()->superuser = $user; - Piwik_Config::getInstance()->forceSave(); - } - else - { - Piwik_UsersManager_API::getInstance()->updateUser( - $user['login'], $passwordHash, $email = false, $alias = false, $isPasswordHashed = true); - } - } - - /** - * The action used after a password is successfully reset. Displays the login - * screen with an extra message. A separate action is used instead of returning - * the HTML in confirmResetPassword so the resetToken won't be in the URL. - */ - public function resetPasswordSuccess() - { - return $this->login($errorMessage = null, $infoMessage = Piwik_Translate('Login_PasswordChanged')); - } - - /** - * Get user information - * - * @param string $loginMail user login or email address - * @return array ("login" => '...', "email" => '...', "password" => '...') or null, if user not found - */ - protected function getUserInformation($loginMail) - { - Piwik::setUserIsSuperUser(); - - $user = null; - if( $loginMail == Piwik::getSuperUserEmail() - || $loginMail == Piwik_Config::getInstance()->superuser['login'] ) - { - $user = array( - 'login' => Piwik_Config::getInstance()->superuser['login'], - 'email' => Piwik::getSuperUserEmail(), - 'password' => Piwik_Config::getInstance()->superuser['password'], - ); - } - else if( Piwik_UsersManager_API::getInstance()->userExists($loginMail) ) - { - $user = Piwik_UsersManager_API::getInstance()->getUser($loginMail); - } - else if( Piwik_UsersManager_API::getInstance()->userEmailExists($loginMail) ) - { - $user = Piwik_UsersManager_API::getInstance()->getUserByEmail($loginMail); - } - - return $user; - } - - /** - * Generate a password reset token. Expires in (roughly) 24 hours. - * - * @param array user information - * @param int $timestamp Unix timestamp - * @return string generated token - */ - protected function generatePasswordResetToken($user, $timestamp = null) - { - /* - * Piwik does not store the generated password reset token. - * This avoids a database schema change and SQL queries to store, retrieve, and purge (expired) tokens. - */ - if(!$timestamp) - { - $timestamp = time() + 24*60*60; /* +24 hrs */ - } - - $expiry = strftime('%Y%m%d%H', $timestamp); - $token = $this->generateHash( - $expiry . $user['login'] . $user['email'], - $user['password'] - ); - return $token; - } - - /** - * Validate token. - * - * @param string $token - * @param array $user user information - * @return bool true if valid, false otherwise - */ - protected function isValidToken($token, $user) - { - $now = time(); - - // token valid for 24 hrs (give or take, due to the coarse granularity in our strftime format string) - for($i = 0; $i <= 24; $i++) - { - $generatedToken = self::generatePasswordResetToken($user, $now + $i*60*60); - if($generatedToken === $token) - { - return true; - } - } - - // fails if token is invalid, expired, password already changed, other user information has changed, ... - return false; - } - - /** - * Clear session information - * - * @param none - * @return void - */ - static public function clearSession() - { - $authCookieName = Piwik_Config::getInstance()->General['login_cookie_name']; - $cookie = new Piwik_Cookie($authCookieName); - $cookie->delete(); - - Piwik_Session::expireSessionCookie(); - } - - /** - * Logout current user - * - * @param none - * @return void - */ - public function logout() - { - self::clearSession(); - - $logoutUrl = @Piwik_Config::getInstance()->General['login_logout_url']; - if(empty($logoutUrl)) { - Piwik::redirectToModule('CoreHome'); - } else { - Piwik_Url::redirectToUrl($logoutUrl); - } - } - - /** - * Check force_ssl_login and redirect if connection isn't secure and not using a reverse proxy - * - * @param none - * @return void - */ - protected function checkForceSslLogin() - { - $forceSslLogin = Piwik_Config::getInstance()->General['force_ssl_login']; - if($forceSslLogin - && !Piwik::isHttps()) - { - $url = 'https://' - . Piwik_Url::getCurrentHost() - . Piwik_Url::getCurrentScriptName() - . Piwik_Url::getCurrentQueryString(); - Piwik_Url::redirectToUrl($url); - } - } + /** + * Generate hash on user info and password + * + * @param string $userinfo User name, email, etc + * @param string $password + * @return string + */ + private function generateHash($userInfo, $password) + { + // mitigate rainbow table attack + $passwordLen = strlen($password) / 2; + $hash = Piwik_Common::hash( + $userInfo . substr($password, 0, $passwordLen) + . Piwik_Common::getSalt() . substr($password, $passwordLen) + ); + return $hash; + } + + /** + * Default action + * + * @param none + * @return void + */ + function index() + { + $this->login(); + } + + /** + * Login form + * + * @param string $messageNoAccess Access error message + * @param string $currentUrl Current URL + * @return void + */ + function login($messageNoAccess = null, $infoMessage = false) + { + self::checkForceSslLogin(); + + $form = new Piwik_Login_FormLogin(); + if ($form->validate()) { + $nonce = $form->getSubmitValue('form_nonce'); + if (Piwik_Nonce::verifyNonce('Piwik_Login.login', $nonce)) { + $login = $form->getSubmitValue('form_login'); + $password = $form->getSubmitValue('form_password'); + $rememberMe = $form->getSubmitValue('form_rememberme') == '1'; + $md5Password = md5($password); + try { + $this->authenticateAndRedirect($login, $md5Password, $rememberMe); + } catch (Exception $e) { + $messageNoAccess = $e->getMessage(); + } + } else { + $messageNoAccess = $this->getMessageExceptionNoAccess(); + } + } + + $view = Piwik_View::factory('login'); + $view->AccessErrorString = $messageNoAccess; + $view->infoMessage = nl2br($infoMessage); + $view->addForm($form); + $this->configureView($view); + self::setHostValidationVariablesView($view); + echo $view->render(); + } + + /** + * Configure common view properties + * + * @param Piwik_View $view + */ + private function configureView($view) + { + $this->setBasicVariablesView($view); + + $view->linkTitle = Piwik::getRandomTitle(); + + $view->forceSslLogin = Piwik_Config::getInstance()->General['force_ssl_login']; + + // crsf token: don't trust the submitted value; generate/fetch it from session data + $view->nonce = Piwik_Nonce::getNonce('Piwik_Login.login'); + } + + /** + * Form-less login + * @see how to use it on http://piwik.org/faq/how-to/#faq_30 + * @throws Exception + * @return void + */ + function logme() + { + self::checkForceSslLogin(); + + $password = Piwik_Common::getRequestVar('password', null, 'string'); + if (strlen($password) != 32) { + throw new Exception(Piwik_TranslateException('Login_ExceptionPasswordMD5HashExpected')); + } + + $login = Piwik_Common::getRequestVar('login', null, 'string'); + if ($login == Piwik_Config::getInstance()->superuser['login']) { + throw new Exception(Piwik_TranslateException('Login_ExceptionInvalidSuperUserAuthenticationMethod', array("logme"))); + } + + $currentUrl = 'index.php'; + + if (($idSite = Piwik_Common::getRequestVar('idSite', false, 'int')) !== false) { + $currentUrl .= '?idSite=' . $idSite; + } + + $urlToRedirect = Piwik_Common::getRequestVar('url', $currentUrl, 'string'); + $urlToRedirect = Piwik_Common::unsanitizeInputValue($urlToRedirect); + + $this->authenticateAndRedirect($login, $password, false, $urlToRedirect); + } + + /** + * Authenticate user and password. Redirect if successful. + * + * @param string $login user name + * @param string $md5Password md5 hash of password + * @param bool $rememberMe Remember me? + * @param string $urlToRedirect URL to redirect to, if successfully authenticated + * @return string failure message if unable to authenticate + */ + protected function authenticateAndRedirect($login, $md5Password, $rememberMe, $urlToRedirect = 'index.php') + { + $info = array('login' => $login, + 'md5Password' => $md5Password, + 'rememberMe' => $rememberMe, + ); + Piwik_Nonce::discardNonce('Piwik_Login.login'); + Piwik_PostEvent('Login.initSession', $info); + Piwik_Url::redirectToUrl($urlToRedirect); + } + + protected function getMessageExceptionNoAccess() + { + $message = Piwik_Translate('Login_InvalidNonceOrHeadersOrReferer', array('', '')); + // Should mention trusted_hosts or link to FAQ + return $message; + } + + /** + * Reset password action. Stores new password as hash and sends email + * to confirm use. + * + * @param none + * @return void + */ + function resetPassword() + { + self::checkForceSslLogin(); + + $infoMessage = null; + $formErrors = null; + + $form = new Piwik_Login_FormResetPassword(); + if ($form->validate()) { + $nonce = $form->getSubmitValue('form_nonce'); + if (Piwik_Nonce::verifyNonce('Piwik_Login.login', $nonce)) { + $formErrors = $this->resetPasswordFirstStep($form); + if (empty($formErrors)) { + $infoMessage = Piwik_Translate('Login_ConfirmationLinkSent'); + } + } else { + $formErrors = array($this->getMessageExceptionNoAccess()); + } + } else { + // if invalid, display error + $formData = $form->getFormData(); + $formErrors = $formData['errors']; + } + + $view = Piwik_View::factory('message'); + $view->infoMessage = $infoMessage; + $view->formErrors = $formErrors; + echo $view->render(); + } + + /** + * Saves password reset info and sends confirmation email. + * + * @return array Error message(s) if an error occurs. + */ + private function resetPasswordFirstStep($form) + { + $loginMail = $form->getSubmitValue('form_login'); + $token = $form->getSubmitValue('form_token'); + $password = $form->getSubmitValue('form_password'); + + // check the password + try { + Piwik_UsersManager::checkPassword($password); + } catch (Exception $ex) { + return array($ex->getMessage()); + } + + // get the user's login + if ($loginMail === 'anonymous') { + return array(Piwik_Translate('Login_InvalidUsernameEmail')); + } + + $user = self::getUserInformation($loginMail); + if ($user === null) { + return array(Piwik_Translate('Login_InvalidUsernameEmail')); + } + + $login = $user['login']; + + // if valid, store password information in options table, then... + Piwik_Login::savePasswordResetInfo($login, $password); + + // ... send email with confirmation link + try { + $this->sendEmailConfirmationLink($user); + } catch (Exception $ex) { + // remove password reset info + Piwik_Login::removePasswordResetInfo($login); + + return array($ex->getMessage() . '
' . Piwik_Translate('Login_ContactAdmin')); + } + + return null; + } + + /** + * Sends email confirmation link for a password reset request. + * + * @param array $user User info for the requested password reset. + */ + private function sendEmailConfirmationLink($user) + { + $login = $user['login']; + $email = $user['email']; + + // construct a password reset token from user information + $resetToken = self::generatePasswordResetToken($user); + + $ip = Piwik_IP::getIpFromHeader(); + $url = Piwik_Url::getCurrentUrlWithoutQueryString() + . "?module=Login&action=confirmResetPassword&login=" . urlencode($login) + . "&resetToken=" . urlencode($resetToken); + + // send email with new password + $mail = new Piwik_Mail(); + $mail->addTo($email, $login); + $mail->setSubject(Piwik_Translate('Login_MailTopicPasswordChange')); + $bodyText = str_replace( + '\n', + "\n", + sprintf(Piwik_Translate('Login_MailPasswordChangeBody'), $login, $ip, $url) + ) . "\n"; + $mail->setBodyText($bodyText); + + $fromEmailName = Piwik_Config::getInstance()->General['login_password_recovery_email_name']; + $fromEmailAddress = Piwik_Config::getInstance()->General['login_password_recovery_email_address']; + $mail->setFrom($fromEmailAddress, $fromEmailName); + @$mail->send(); + } + + /** + * Password reset confirmation action. Finishes the password reset process. + * Users visit this action from a link supplied in an email. + */ + public function confirmResetPassword() + { + $errorMessage = null; + + $login = Piwik_Common::getRequestVar('login', ''); + $resetToken = Piwik_Common::getRequestVar('resetToken', ''); + + try { + // get password reset info & user info + $user = self::getUserInformation($login); + if ($user === null) { + throw new Exception(Piwik_Translate('Login_InvalidUsernameEmail')); + } + + // check that the reset token is valid + $resetPassword = Piwik_Login::getPasswordToResetTo($login); + if ($resetPassword === false || !self::isValidToken($resetToken, $user)) { + throw new Exception(Piwik_Translate('Login_InvalidOrExpiredToken')); + } + + // reset password of user + $this->setNewUserPassword($user, $resetPassword); + } catch (Exception $ex) { + $errorMessage = $ex->getMessage(); + } + + if (is_null($errorMessage)) // if success, show login w/ success message + { + $this->redirectToIndex('Login', 'resetPasswordSuccess'); + } else { + // show login page w/ error. this will keep the token in the URL + return $this->login($errorMessage); + } + } + + /** + * Sets the password for a user. + * + * @param array $user User info. + * @param string $passwordHash The hashed password to use. + */ + private function setNewUserPassword($user, $passwordHash) + { + if (strlen($passwordHash) !== 32) // sanity check + { + throw new Exception( + "setNewUserPassword called w/ incorrect password hash. Something has gone terribly wrong."); + } + + if ($user['email'] == Piwik::getSuperUserEmail()) { + if (!Piwik_Config::getInstance()->isFileWritable()) { + throw new Exception(Piwik_Translate('General_ConfigFileIsNotWritable', array("(config/config.ini.php)", "
"))); + } + + $user['password'] = $passwordHash; + Piwik_Config::getInstance()->superuser = $user; + Piwik_Config::getInstance()->forceSave(); + } else { + Piwik_UsersManager_API::getInstance()->updateUser( + $user['login'], $passwordHash, $email = false, $alias = false, $isPasswordHashed = true); + } + } + + /** + * The action used after a password is successfully reset. Displays the login + * screen with an extra message. A separate action is used instead of returning + * the HTML in confirmResetPassword so the resetToken won't be in the URL. + */ + public function resetPasswordSuccess() + { + return $this->login($errorMessage = null, $infoMessage = Piwik_Translate('Login_PasswordChanged')); + } + + /** + * Get user information + * + * @param string $loginMail user login or email address + * @return array ("login" => '...', "email" => '...', "password" => '...') or null, if user not found + */ + protected function getUserInformation($loginMail) + { + Piwik::setUserIsSuperUser(); + + $user = null; + if ($loginMail == Piwik::getSuperUserEmail() + || $loginMail == Piwik_Config::getInstance()->superuser['login'] + ) { + $user = array( + 'login' => Piwik_Config::getInstance()->superuser['login'], + 'email' => Piwik::getSuperUserEmail(), + 'password' => Piwik_Config::getInstance()->superuser['password'], + ); + } else if (Piwik_UsersManager_API::getInstance()->userExists($loginMail)) { + $user = Piwik_UsersManager_API::getInstance()->getUser($loginMail); + } else if (Piwik_UsersManager_API::getInstance()->userEmailExists($loginMail)) { + $user = Piwik_UsersManager_API::getInstance()->getUserByEmail($loginMail); + } + + return $user; + } + + /** + * Generate a password reset token. Expires in (roughly) 24 hours. + * + * @param array user information + * @param int $timestamp Unix timestamp + * @return string generated token + */ + protected function generatePasswordResetToken($user, $timestamp = null) + { + /* + * Piwik does not store the generated password reset token. + * This avoids a database schema change and SQL queries to store, retrieve, and purge (expired) tokens. + */ + if (!$timestamp) { + $timestamp = time() + 24 * 60 * 60; /* +24 hrs */ + } + + $expiry = strftime('%Y%m%d%H', $timestamp); + $token = $this->generateHash( + $expiry . $user['login'] . $user['email'], + $user['password'] + ); + return $token; + } + + /** + * Validate token. + * + * @param string $token + * @param array $user user information + * @return bool true if valid, false otherwise + */ + protected function isValidToken($token, $user) + { + $now = time(); + + // token valid for 24 hrs (give or take, due to the coarse granularity in our strftime format string) + for ($i = 0; $i <= 24; $i++) { + $generatedToken = self::generatePasswordResetToken($user, $now + $i * 60 * 60); + if ($generatedToken === $token) { + return true; + } + } + + // fails if token is invalid, expired, password already changed, other user information has changed, ... + return false; + } + + /** + * Clear session information + * + * @param none + * @return void + */ + static public function clearSession() + { + $authCookieName = Piwik_Config::getInstance()->General['login_cookie_name']; + $cookie = new Piwik_Cookie($authCookieName); + $cookie->delete(); + + Piwik_Session::expireSessionCookie(); + } + + /** + * Logout current user + * + * @param none + * @return void + */ + public function logout() + { + self::clearSession(); + + $logoutUrl = @Piwik_Config::getInstance()->General['login_logout_url']; + if (empty($logoutUrl)) { + Piwik::redirectToModule('CoreHome'); + } else { + Piwik_Url::redirectToUrl($logoutUrl); + } + } + + /** + * Check force_ssl_login and redirect if connection isn't secure and not using a reverse proxy + * + * @param none + * @return void + */ + protected function checkForceSslLogin() + { + $forceSslLogin = Piwik_Config::getInstance()->General['force_ssl_login']; + if ($forceSslLogin + && !Piwik::isHttps() + ) { + $url = 'https://' + . Piwik_Url::getCurrentHost() + . Piwik_Url::getCurrentScriptName() + . Piwik_Url::getCurrentQueryString(); + Piwik_Url::redirectToUrl($url); + } + } } diff --git a/plugins/Login/FormLogin.php b/plugins/Login/FormLogin.php index 46c02f2f91..a90146f47e 100644 --- a/plugins/Login/FormLogin.php +++ b/plugins/Login/FormLogin.php @@ -15,28 +15,28 @@ */ class Piwik_Login_FormLogin extends Piwik_QuickForm2 { - function __construct( $id = 'login_form', $method = 'post', $attributes = null, $trackSubmit = false) - { - parent::__construct($id, $method, $attributes, $trackSubmit); - } + function __construct($id = 'login_form', $method = 'post', $attributes = null, $trackSubmit = false) + { + parent::__construct($id, $method, $attributes, $trackSubmit); + } - function init() - { - $this->addElement('text', 'form_login') - ->addRule('required', Piwik_Translate('General_Required', Piwik_Translate('General_Username'))); + function init() + { + $this->addElement('text', 'form_login') + ->addRule('required', Piwik_Translate('General_Required', Piwik_Translate('General_Username'))); - $this->addElement('password', 'form_password') - ->addRule('required', Piwik_Translate('General_Required', Piwik_Translate('Login_Password'))); + $this->addElement('password', 'form_password') + ->addRule('required', Piwik_Translate('General_Required', Piwik_Translate('Login_Password'))); - $this->addElement('hidden', 'form_nonce'); + $this->addElement('hidden', 'form_nonce'); - $this->addElement('checkbox', 'form_rememberme'); + $this->addElement('checkbox', 'form_rememberme'); - $this->addElement('submit', 'submit'); + $this->addElement('submit', 'submit'); - // default values - $this->addDataSource(new HTML_QuickForm2_DataSource_Array(array( - 'form_rememberme' => 0, - ))); - } + // default values + $this->addDataSource(new HTML_QuickForm2_DataSource_Array(array( + 'form_rememberme' => 0, + ))); + } } diff --git a/plugins/Login/FormResetPassword.php b/plugins/Login/FormResetPassword.php index 88791060d4..0411beddbb 100644 --- a/plugins/Login/FormResetPassword.php +++ b/plugins/Login/FormResetPassword.php @@ -15,25 +15,25 @@ */ class Piwik_Login_FormResetPassword extends Piwik_QuickForm2 { - function __construct( $id = 'resetpasswordform', $method = 'post', $attributes = null, $trackSubmit = false) - { - parent::__construct($id, $method, $attributes, $trackSubmit); - } + function __construct($id = 'resetpasswordform', $method = 'post', $attributes = null, $trackSubmit = false) + { + parent::__construct($id, $method, $attributes, $trackSubmit); + } - function init() - { - $this->addElement('text', 'form_login') - ->addRule('required', Piwik_Translate('General_Required', Piwik_Translate('General_Username'))); + function init() + { + $this->addElement('text', 'form_login') + ->addRule('required', Piwik_Translate('General_Required', Piwik_Translate('General_Username'))); - $password = $this->addElement('password', 'form_password'); - $password->addRule('required', Piwik_Translate('General_Required', Piwik_Translate('Login_Password'))); + $password = $this->addElement('password', 'form_password'); + $password->addRule('required', Piwik_Translate('General_Required', Piwik_Translate('Login_Password'))); - $passwordBis = $this->addElement('password', 'form_password_bis'); - $passwordBis->addRule('required', Piwik_Translate('General_Required', Piwik_Translate('Login_PasswordRepeat'))); - $passwordBis->addRule('eq', Piwik_Translate( 'Login_PasswordsDoNotMatch'), $password); + $passwordBis = $this->addElement('password', 'form_password_bis'); + $passwordBis->addRule('required', Piwik_Translate('General_Required', Piwik_Translate('Login_PasswordRepeat'))); + $passwordBis->addRule('eq', Piwik_Translate('Login_PasswordsDoNotMatch'), $password); - $this->addElement('hidden', 'form_nonce'); + $this->addElement('hidden', 'form_nonce'); - $this->addElement('submit', 'submit'); - } + $this->addElement('submit', 'submit'); + } } diff --git a/plugins/Login/Login.php b/plugins/Login/Login.php index e56c4560e2..5b92eaca87 100644 --- a/plugins/Login/Login.php +++ b/plugins/Login/Login.php @@ -15,183 +15,180 @@ */ class Piwik_Login extends Piwik_Plugin { - public function getInformation() - { - $info = array( - 'description' => Piwik_Translate('Login_PluginDescription'), - 'author' => 'Piwik', - 'author_homepage' => 'http://piwik.org/', - 'version' => Piwik_Version::VERSION, - ); - return $info; - } - - function getListHooksRegistered() - { - $hooks = array( - 'FrontController.initAuthenticationObject' => 'initAuthenticationObject', - 'FrontController.NoAccessException' => 'noAccess', - 'API.Request.authenticate' => 'ApiRequestAuthenticate', - 'Login.initSession' => 'initSession', - ); - return $hooks; - } - - /** - * Redirects to Login form with error message. - * Listens to FrontController.NoAccessException hook. - * - * @param Piwik_Event_Notification $notification notification object - */ - function noAccess( $notification ) - { - /* @var Exception $exception */ - $exception = $notification->getNotificationObject(); - $exceptionMessage = $exception->getMessage(); - - $controller = new Piwik_Login_Controller(); - $controller->login($exceptionMessage, '' /* $exception->getTraceAsString() */ ); - } - - /** - * Set login name and autehntication token for authentication request. - * Listens to API.Request.authenticate hook. - * - * @param Piwik_Event_Notification $notification notification object - */ - function ApiRequestAuthenticate($notification) - { - $tokenAuth = $notification->getNotificationObject(); - Zend_Registry::get('auth')->setLogin($login = null); - Zend_Registry::get('auth')->setTokenAuth($tokenAuth); - } - - /** - * Initializes the authentication object. - * Listens to FrontController.initAuthenticationObject hook. - * - * @param Piwik_Event_Notification $notification notification object - */ - function initAuthenticationObject($notification) - { - $auth = new Piwik_Login_Auth(); - Zend_Registry::set('auth', $auth); - - $allowCookieAuthentication = $notification->getNotificationInfo(); - - $action = Piwik::getAction(); - if (Piwik::getModule() === 'API' - && (empty($action) || $action == 'index') - && $allowCookieAuthentication !== true) - { - return; - } - - $authCookieName = Piwik_Config::getInstance()->General['login_cookie_name']; - $authCookieExpiry = 0; - $authCookiePath = Piwik_Config::getInstance()->General['login_cookie_path']; - $authCookie = new Piwik_Cookie($authCookieName, $authCookieExpiry, $authCookiePath); - $defaultLogin = 'anonymous'; - $defaultTokenAuth = 'anonymous'; - if($authCookie->isCookieFound()) - { - $defaultLogin = $authCookie->get('login'); - $defaultTokenAuth = $authCookie->get('token_auth'); - } - $auth->setLogin($defaultLogin); - $auth->setTokenAuth($defaultTokenAuth); - } - - /** - * Authenticate user and initializes the session. - * Listens to Login.initSession hook. - * - * @param Piwik_Event_Notification $notification notification object - * @throws Exception - */ - function initSession($notification) - { - $info = $notification->getNotificationObject(); - $login = $info['login']; - $md5Password = $info['md5Password']; - $rememberMe = $info['rememberMe']; - - $tokenAuth = Piwik_UsersManager_API::getInstance()->getTokenAuth($login, $md5Password); - - $auth = Zend_Registry::get('auth'); - $auth->setLogin($login); - $auth->setTokenAuth($tokenAuth); - $authResult = $auth->authenticate(); - - $authCookieName = Piwik_Config::getInstance()->General['login_cookie_name']; - $authCookieExpiry = $rememberMe ? time() + Piwik_Config::getInstance()->General['login_cookie_expire'] : 0; - $authCookiePath = Piwik_Config::getInstance()->General['login_cookie_path']; - $cookie = new Piwik_Cookie($authCookieName, $authCookieExpiry, $authCookiePath); - if(!$authResult->isValid()) - - { - $cookie->delete(); - throw new Exception(Piwik_Translate('Login_LoginPasswordNotCorrect')); - } - - $cookie->set('login', $login); - $cookie->set('token_auth', $auth->getHashTokenAuth($login, $authResult->getTokenAuth())); - $cookie->setSecure(Piwik::isHttps()); - $cookie->setHttpOnly(true); - $cookie->save(); - - @Piwik_Session::regenerateId(); - - // remove password reset entry if it exists - self::removePasswordResetInfo($login); - } - - /** - * Stores password reset info for a specific login. - * - * @param string $login The user login for whom a password change was requested. - * @param string $password The new password to set. - */ - public static function savePasswordResetInfo( $login, $password ) - { - $optionName = self::getPasswordResetInfoOptionName($login); - $optionData = Piwik_UsersManager::getPasswordHash($password); - - Piwik_SetOption($optionName, $optionData); - } - - /** - * Removes stored password reset info if it exists. - * - * @param string $login The user login to check for. - */ - public static function removePasswordResetInfo( $login ) - { - $optionName = self::getPasswordResetInfoOptionName($login); - Piwik_Option::getInstance()->delete($optionName); - } - - /** - * Gets password hash stored in password reset info. - * - * @param string $login The user login to check for. - * @return string|false The hashed password or false if no reset info exists. - */ - public static function getPasswordToResetTo( $login ) - { - $optionName = self::getPasswordResetInfoOptionName($login); - return Piwik_GetOption($optionName); - } - - /** - * Gets the option name for the option that will store a user's password change - * request. - * - * @param string $login The user login for whom a password change was requested. - * @return string - */ - public static function getPasswordResetInfoOptionName( $login ) - { - return $login.'_reset_password_info'; - } + public function getInformation() + { + $info = array( + 'description' => Piwik_Translate('Login_PluginDescription'), + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'version' => Piwik_Version::VERSION, + ); + return $info; + } + + function getListHooksRegistered() + { + $hooks = array( + 'FrontController.initAuthenticationObject' => 'initAuthenticationObject', + 'FrontController.NoAccessException' => 'noAccess', + 'API.Request.authenticate' => 'ApiRequestAuthenticate', + 'Login.initSession' => 'initSession', + ); + return $hooks; + } + + /** + * Redirects to Login form with error message. + * Listens to FrontController.NoAccessException hook. + * + * @param Piwik_Event_Notification $notification notification object + */ + function noAccess($notification) + { + /* @var Exception $exception */ + $exception = $notification->getNotificationObject(); + $exceptionMessage = $exception->getMessage(); + + $controller = new Piwik_Login_Controller(); + $controller->login($exceptionMessage, '' /* $exception->getTraceAsString() */); + } + + /** + * Set login name and autehntication token for authentication request. + * Listens to API.Request.authenticate hook. + * + * @param Piwik_Event_Notification $notification notification object + */ + function ApiRequestAuthenticate($notification) + { + $tokenAuth = $notification->getNotificationObject(); + Zend_Registry::get('auth')->setLogin($login = null); + Zend_Registry::get('auth')->setTokenAuth($tokenAuth); + } + + /** + * Initializes the authentication object. + * Listens to FrontController.initAuthenticationObject hook. + * + * @param Piwik_Event_Notification $notification notification object + */ + function initAuthenticationObject($notification) + { + $auth = new Piwik_Login_Auth(); + Zend_Registry::set('auth', $auth); + + $allowCookieAuthentication = $notification->getNotificationInfo(); + + $action = Piwik::getAction(); + if (Piwik::getModule() === 'API' + && (empty($action) || $action == 'index') + && $allowCookieAuthentication !== true + ) { + return; + } + + $authCookieName = Piwik_Config::getInstance()->General['login_cookie_name']; + $authCookieExpiry = 0; + $authCookiePath = Piwik_Config::getInstance()->General['login_cookie_path']; + $authCookie = new Piwik_Cookie($authCookieName, $authCookieExpiry, $authCookiePath); + $defaultLogin = 'anonymous'; + $defaultTokenAuth = 'anonymous'; + if ($authCookie->isCookieFound()) { + $defaultLogin = $authCookie->get('login'); + $defaultTokenAuth = $authCookie->get('token_auth'); + } + $auth->setLogin($defaultLogin); + $auth->setTokenAuth($defaultTokenAuth); + } + + /** + * Authenticate user and initializes the session. + * Listens to Login.initSession hook. + * + * @param Piwik_Event_Notification $notification notification object + * @throws Exception + */ + function initSession($notification) + { + $info = $notification->getNotificationObject(); + $login = $info['login']; + $md5Password = $info['md5Password']; + $rememberMe = $info['rememberMe']; + + $tokenAuth = Piwik_UsersManager_API::getInstance()->getTokenAuth($login, $md5Password); + + $auth = Zend_Registry::get('auth'); + $auth->setLogin($login); + $auth->setTokenAuth($tokenAuth); + $authResult = $auth->authenticate(); + + $authCookieName = Piwik_Config::getInstance()->General['login_cookie_name']; + $authCookieExpiry = $rememberMe ? time() + Piwik_Config::getInstance()->General['login_cookie_expire'] : 0; + $authCookiePath = Piwik_Config::getInstance()->General['login_cookie_path']; + $cookie = new Piwik_Cookie($authCookieName, $authCookieExpiry, $authCookiePath); + if (!$authResult->isValid()) { + $cookie->delete(); + throw new Exception(Piwik_Translate('Login_LoginPasswordNotCorrect')); + } + + $cookie->set('login', $login); + $cookie->set('token_auth', $auth->getHashTokenAuth($login, $authResult->getTokenAuth())); + $cookie->setSecure(Piwik::isHttps()); + $cookie->setHttpOnly(true); + $cookie->save(); + + @Piwik_Session::regenerateId(); + + // remove password reset entry if it exists + self::removePasswordResetInfo($login); + } + + /** + * Stores password reset info for a specific login. + * + * @param string $login The user login for whom a password change was requested. + * @param string $password The new password to set. + */ + public static function savePasswordResetInfo($login, $password) + { + $optionName = self::getPasswordResetInfoOptionName($login); + $optionData = Piwik_UsersManager::getPasswordHash($password); + + Piwik_SetOption($optionName, $optionData); + } + + /** + * Removes stored password reset info if it exists. + * + * @param string $login The user login to check for. + */ + public static function removePasswordResetInfo($login) + { + $optionName = self::getPasswordResetInfoOptionName($login); + Piwik_Option::getInstance()->delete($optionName); + } + + /** + * Gets password hash stored in password reset info. + * + * @param string $login The user login to check for. + * @return string|false The hashed password or false if no reset info exists. + */ + public static function getPasswordToResetTo($login) + { + $optionName = self::getPasswordResetInfoOptionName($login); + return Piwik_GetOption($optionName); + } + + /** + * Gets the option name for the option that will store a user's password change + * request. + * + * @param string $login The user login for whom a password change was requested. + * @return string + */ + public static function getPasswordResetInfoOptionName($login) + { + return $login . '_reset_password_info'; + } } diff --git a/plugins/Login/templates/header.tpl b/plugins/Login/templates/header.tpl index 6b27f66710..e4e1919633 100644 --- a/plugins/Login/templates/header.tpl +++ b/plugins/Login/templates/header.tpl @@ -1,60 +1,64 @@ - - + + + - {if !$isCustomLogo}Piwik › {/if}{'Login_LogIn'|translate} - - - - - - - -{if isset($forceSslLogin) && $forceSslLogin} -{literal} - -{/literal} -{/if} -{literal} - -{/literal} - -{if 'General_LayoutDirection'|translate =='rtl'} - -{/if} -{include file="CoreHome/templates/iframe_buster_header.tpl"} + {if !$isCustomLogo}Piwik › {/if}{'Login_LogIn'|translate} + + + + + + + + {if isset($forceSslLogin) && $forceSslLogin} + {literal} + + {/literal} + {/if} + {literal} + + {/literal} + + {if 'General_LayoutDirection'|translate =='rtl'} + + {/if} + {include file="CoreHome/templates/iframe_buster_header.tpl"} {include file="CoreHome/templates/iframe_buster_body.tpl"} - + diff --git a/plugins/Login/templates/login.css b/plugins/Login/templates/login.css index d8c3f3d87f..7d62cea47a 100644 --- a/plugins/Login/templates/login.css +++ b/plugins/Login/templates/login.css @@ -1,227 +1,224 @@ /* shamelessly taken from wordpress 2.5 - thank you guys!!! */ * { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } body { - font: 12px "Lucida Grande", "Lucida Sans Unicode", Tahoma, Verdana, sans-serif; + font: 12px "Lucida Grande", "Lucida Sans Unicode", Tahoma, Verdana, sans-serif; } form { - padding: 16px 16px 16px 16px; - border-radius: 5px; + padding: 16px 16px 16px 16px; + border-radius: 5px; } #login form input.submit { - font-family: "Lucida Grande", "Lucida Sans Unicode", Tahoma, Verdana, sans-serif; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - float: right; - height: 35px; - padding: 0 20px; - cursor: pointer; - font: bold 15px Arial, Helvetica; + font-family: "Lucida Grande", "Lucida Sans Unicode", Tahoma, Verdana, sans-serif; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + float: right; + height: 35px; + padding: 0 20px; + cursor: pointer; + font: bold 15px Arial, Helvetica; } #login form div { - margin-bottom: 24px; + margin-bottom: 24px; } -.updated,.login #login_error,.login .message { - background-color: #ffffe0; - border-color: #e6db55; - margin: 0 auto; - width: 330px; +.updated, .login #login_error, .login .message { + background-color: #ffffe0; + border-color: #e6db55; + margin: 0 auto; + width: 330px; } #login fieldset { - border: 0; + border: 0; } #login fieldset.actions { - line-height: 35px; - width: 315px; - margin-top: 10px; -} - -#login h1 -{ - text-align: center; - color: #666; - margin: 0 0 30px 0; - font: normal 26px/1 Verdana, Helvetica; - position: relative; -} - -#login -{ - background-color: #fafafa; - width: 360px; - padding: 30px; - margin: 50px auto 0 auto; - z-index: 0; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; - -webkit-box-shadow: - 0 0 2px rgba(0, 0, 0, 0.2), - 0 1px 1px rgba(0, 0, 0, .2); - -moz-box-shadow: - 0 0 2px rgba(0, 0, 0, 0.2), - 1px 1px 0 rgba(0, 0, 0, .1); - box-shadow: - 0 0 2px rgba(0, 0, 0, 0.2), - 0 1px 1px rgba(0, 0, 0, .2); -} - -#login form { margin: 0 5px; position: relative; } + line-height: 35px; + width: 315px; + margin-top: 10px; +} + +#login h1 { + text-align: center; + color: #666; + margin: 0 0 30px 0; + font: normal 26px/1 Verdana, Helvetica; + position: relative; +} + +#login { + background-color: #fafafa; + width: 360px; + padding: 30px; + margin: 50px auto 0 auto; + z-index: 0; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: 0 0 2px rgba(0, 0, 0, 0.2), 0 1px 1px rgba(0, 0, 0, .2); + -moz-box-shadow: 0 0 2px rgba(0, 0, 0, 0.2), 1px 1px 0 rgba(0, 0, 0, .1); + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2), 0 1px 1px rgba(0, 0, 0, .2); +} + +#login form { + margin: 0 5px; + position: relative; +} + #login form input[type="text"], #login form input[type="password"] { - padding: 10px 15px 10px 45px; - margin: 0 0 15px 0; - width: 253px; /* 258 + 2 + 55 = 315 */ - border: 1px solid #ccc; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - border-radius: 5px; - -moz-box-shadow: 0 1px 1px #ccc inset, 0 1px 0 #fff; - -webkit-box-shadow: 0 1px 1px #ccc inset, 0 1px 0 #fff; - box-shadow: 0 1px 1px #ccc inset, 0 1px 0 #fff; + padding: 10px 15px 10px 45px; + margin: 0 0 15px 0; + width: 253px; /* 258 + 2 + 55 = 315 */ + border: 1px solid #ccc; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; + -moz-box-shadow: 0 1px 1px #ccc inset, 0 1px 0 #fff; + -webkit-box-shadow: 0 1px 1px #ccc inset, 0 1px 0 #fff; + box-shadow: 0 1px 1px #ccc inset, 0 1px 0 #fff; } #login_form_rememberme { - vertical-align: middle; + vertical-align: middle; } -#login_error,.message { - margin: 0 0 16px 8px; - border: 1px solid; - padding: 12px; +#login_error, .message { + margin: 0 0 16px 8px; + border: 1px solid; + padding: 12px; } #nav, #piwik { - margin: 0 0 0 8px; - padding: 16px; + margin: 0 0 0 8px; + padding: 16px; } #nav { - text-align: center; + text-align: center; } #nav a:hover { - text-decoration: underline; + text-decoration: underline; } -#login_form_password, #reset_form_password, +#login_form_password, #reset_form_password, #reset_form_password_bis, #login_form_login, #reset_form_login { - background: #fff url(../../../themes/default/images/login-sprite.png) no-repeat; + background: #fff url(../../../themes/default/images/login-sprite.png) no-repeat; } #login_form_password, #reset_form_password, #reset_form_password_bis { - background-position: 10px -51px !important; + background-position: 10px -51px !important; } #login_form_login, #reset_form_login { - background-position: 10px 11px !important; + background-position: 10px 11px !important; } -#login_form_password,#reset_form_password,#reset_form_password_bis, -#login_form_login,#reset_form_login { - font-size: 20px; - width: 97%; - padding: 3px; - margin-right: 6px; +#login_form_password, #reset_form_password, #reset_form_password_bis, +#login_form_login, #reset_form_login { + font-size: 20px; + width: 97%; + padding: 3px; + margin-right: 6px; } #login #login_error { - background-color: #ffebe8; - border-color: #c00; + background-color: #ffebe8; + border-color: #c00; } #login form input.submit { - background-color: #e5e5e5; + background-color: #e5e5e5; } #login form input.submit:hover { - background-color: #eee; + background-color: #eee; } .login #login_error { - background-color: #ffffe0; - border-color: #e6db55; + background-color: #ffffe0; + border-color: #e6db55; } .login #nav a { - color: #777; + color: #777; } + .login #piwik a { - color: #CDCDCD; + color: #CDCDCD; } body.login { - border-top-color: #464646; + border-top-color: #464646; } #login form input { - color: #555; + color: #555; } a { - text-decoration: none; + text-decoration: none; } #logo { - margin: 100px auto 0 auto; - width: 240px; - position: relative; + margin: 100px auto 0 auto; + width: 240px; + position: relative; } #logo .description a { - font:16px/16px 'Patrick Hand'; - color:#666666; - right: auto; - text-decoration: none; + font: 16px/16px 'Patrick Hand'; + color: #666666; + right: auto; + text-decoration: none; } #logo .description { - position: absolute; - left: -40px !important; - top: -30px !important; - -webkit-transform:rotate(-6deg); - -moz-transform:rotate(-6deg); - -ms-transform:rotate(-6deg); - -o-transform:rotate(-6deg); + position: absolute; + left: -40px !important; + top: -30px !important; + -webkit-transform: rotate(-6deg); + -moz-transform: rotate(-6deg); + -ms-transform: rotate(-6deg); + -o-transform: rotate(-6deg); } #logo .description .arrow { - background:url(../../../themes/default/images/affix-arrow.png); - width:50px; - height:68px; - position:absolute; - left:-35px; + background: url(../../../themes/default/images/affix-arrow.png); + width: 50px; + height: 68px; + position: absolute; + left: -35px; } #logo img { - border:0; - vertical-align: bottom; - width: 260px; + border: 0; + vertical-align: bottom; + width: 260px; } #logo .h1 { - font-family: Georgia, "Times New Roman", Times, serif; - font-weight: normal; - color: #136F8B; - font-size: 45pt; - text-transform: none; + font-family: Georgia, "Times New Roman", Times, serif; + font-weight: normal; + color: #136F8B; + font-size: 45pt; + text-transform: none; } .loadingPiwik { - float: left; - margin-left: 16px; + float: left; + margin-left: 16px; } /* IE < 9 will use this */ -html.old-ie .ie-hide { - display: none; +html.old-ie .ie-hide { + display: none; } diff --git a/plugins/Login/templates/login.js b/plugins/Login/templates/login.js index 28171b47ab..5458942fef 100755 --- a/plugins/Login/templates/login.js +++ b/plugins/Login/templates/login.js @@ -4,104 +4,97 @@ * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ -(function($) { - -$(document).ready(function() { - var switchForm = function(fromFormId, toFormId, message, callback) { - var fromLoginInputId = '#'+fromFormId+'_login', - toLoginInputId = '#'+toFormId+'_login', - toPasswordInputId = '#'+toFormId+'_password', - fromLoginNavId = '#'+fromFormId+'_nav', - toLoginNavId = '#'+toFormId+'_nav'; - - if ($(toLoginInputId).val() === '') - { - $(toLoginInputId).val($(fromLoginInputId).val()); - } - - // hide the bottom portion of the login screen & show the password reset bits - $('#'+fromFormId+',#message_container').fadeOut(500, function() { - // show lost password instructions - $('#message_container').html(message); - - $(fromLoginNavId).hide(); - $(toLoginNavId).show(); - $('#'+toFormId+',#message_container').fadeIn(500, function() { - // focus on login or password control based on whether a login exists - if ($(toLoginInputId).val() === '') - { - $(toLoginInputId).focus(); - } - else - { - $(toPasswordInputId).focus(); - } - - if (callback) - { - callback(); - } - }); - }); - }; - - // 'lost your password?' on click - $('#login_form_nav').click(function(e) { - e.preventDefault(); - switchForm('login_form', 'reset_form', $('#lost_password_instructions').html()); - return false; - }); - - // 'cancel' on click - $('#reset_form_nav,#alternate_reset_nav').click(function(e) { - e.preventDefault(); - $('#alternate_reset_nav').hide(); - switchForm('reset_form', 'login_form', ''); - return false; - }); - - // password reset on submit - $('#reset_form_submit').click(function(e) { - e.preventDefault(); - - var ajaxDone = function(response) - { - $('.loadingPiwik').hide(); - - var isSuccess = response.indexOf('id="login_error"') === -1, - fadeOutIds = '#message_container'; - if (isSuccess) - { - fadeOutIds += ',#reset_form,#reset_form_nav'; - } - - $(fadeOutIds).fadeOut(300, function() { - if (isSuccess) - { - $('#alternate_reset_nav').show(); - } - - $('#message_container').html(response).fadeIn(300); - }); - }; - - $('.loadingPiwik').show(); - - // perform reset password request - $.ajax({ - type: 'POST', - url: 'index.php', - dataType: 'html', - async: true, - error: function() { ajaxDone('
HTTP Error
'); }, - success: ajaxDone, // Callback when the request succeeds - data: $('#reset_form').serialize() - }); - - return false; - }); - - $('#login_form_login').focus(); -}); +(function ($) { + + $(document).ready(function () { + var switchForm = function (fromFormId, toFormId, message, callback) { + var fromLoginInputId = '#' + fromFormId + '_login', + toLoginInputId = '#' + toFormId + '_login', + toPasswordInputId = '#' + toFormId + '_password', + fromLoginNavId = '#' + fromFormId + '_nav', + toLoginNavId = '#' + toFormId + '_nav'; + + if ($(toLoginInputId).val() === '') { + $(toLoginInputId).val($(fromLoginInputId).val()); + } + + // hide the bottom portion of the login screen & show the password reset bits + $('#' + fromFormId + ',#message_container').fadeOut(500, function () { + // show lost password instructions + $('#message_container').html(message); + + $(fromLoginNavId).hide(); + $(toLoginNavId).show(); + $('#' + toFormId + ',#message_container').fadeIn(500, function () { + // focus on login or password control based on whether a login exists + if ($(toLoginInputId).val() === '') { + $(toLoginInputId).focus(); + } + else { + $(toPasswordInputId).focus(); + } + + if (callback) { + callback(); + } + }); + }); + }; + + // 'lost your password?' on click + $('#login_form_nav').click(function (e) { + e.preventDefault(); + switchForm('login_form', 'reset_form', $('#lost_password_instructions').html()); + return false; + }); + + // 'cancel' on click + $('#reset_form_nav,#alternate_reset_nav').click(function (e) { + e.preventDefault(); + $('#alternate_reset_nav').hide(); + switchForm('reset_form', 'login_form', ''); + return false; + }); + + // password reset on submit + $('#reset_form_submit').click(function (e) { + e.preventDefault(); + + var ajaxDone = function (response) { + $('.loadingPiwik').hide(); + + var isSuccess = response.indexOf('id="login_error"') === -1, + fadeOutIds = '#message_container'; + if (isSuccess) { + fadeOutIds += ',#reset_form,#reset_form_nav'; + } + + $(fadeOutIds).fadeOut(300, function () { + if (isSuccess) { + $('#alternate_reset_nav').show(); + } + + $('#message_container').html(response).fadeIn(300); + }); + }; + + $('.loadingPiwik').show(); + + // perform reset password request + $.ajax({ + type: 'POST', + url: 'index.php', + dataType: 'html', + async: true, + error: function () { ajaxDone('
HTTP Error
'); }, + success: ajaxDone, // Callback when the request succeeds + data: $('#reset_form').serialize() + }); + + return false; + }); + + $('#login_form_login').focus(); + }); }(jQuery)); diff --git a/plugins/Login/templates/login.tpl b/plugins/Login/templates/login.tpl index 956f8d4bc8..ad85960ec5 100644 --- a/plugins/Login/templates/login.tpl +++ b/plugins/Login/templates/login.tpl @@ -2,84 +2,89 @@
-{* untrusted host warning *} -{if isset($isValidHost) && isset($invalidHostMessage) && !$isValidHost} -
- {'General_Warning'|translate}: {$invalidHostMessage} + {* untrusted host warning *} + {if isset($isValidHost) && isset($invalidHostMessage) && !$isValidHost} +
+ {'General_Warning'|translate}: {$invalidHostMessage} -

{$invalidHostMessageHowToFix} -

{'General_Help'|translate}
+

{$invalidHostMessageHowToFix} +

{'General_Help'|translate}
-
-{else} -
- {if $form_data.errors} -
- {foreach from=$form_data.errors item=data} - {'General_Error'|translate}: {$data}
- {/foreach} -
- {/if} +
+ {else} +
+ {if $form_data.errors} +
+ {foreach from=$form_data.errors item=data} + {'General_Error'|translate} + : {$data} +
+ {/foreach} +
+ {/if} - {if $AccessErrorString} -
{'General_Error'|translate}: {$AccessErrorString}
- {/if} + {if $AccessErrorString} +
{'General_Error'|translate}: {$AccessErrorString}
+ {/if} - {if $infoMessage} -

{$infoMessage}

- {/if} -
+ {if $infoMessage} +

{$infoMessage}

+ {/if} +
+
+

{'Login_LogIn'|translate}

+
+ + + +
- -

{'Login_LogIn'|translate}

-
- - - -
+
+ + + +
+
+ - -
- - -
- - - - - - -{if isset($smarty.capture.poweredByPiwik)} -

- {$smarty.capture.poweredByPiwik} -

-{/if} - - -{/if} + + + + + {if isset($smarty.capture.poweredByPiwik)} +

+ {$smarty.capture.poweredByPiwik} +

+ {/if} + + {/if}
diff --git a/plugins/Login/templates/message.tpl b/plugins/Login/templates/message.tpl index 7b85147d8f..ff2e9d21e3 100755 --- a/plugins/Login/templates/message.tpl +++ b/plugins/Login/templates/message.tpl @@ -1,11 +1,13 @@ {if isset($infoMessage)} -

{$infoMessage}

+

{$infoMessage}

{/if} {if isset($formErrors)} -

- {foreach from=$formErrors item=data} - {'General_Error'|translate}: {$data}
- {/foreach} -

+

+ {foreach from=$formErrors item=data} + {'General_Error'|translate} + : {$data} +
+ {/foreach} +

{/if} diff --git a/plugins/MobileMessaging/API.php b/plugins/MobileMessaging/API.php index df212a662f..ae80979245 100644 --- a/plugins/MobileMessaging/API.php +++ b/plugins/MobileMessaging/API.php @@ -19,459 +19,442 @@ */ class Piwik_MobileMessaging_API { - const VERIFICATION_CODE_LENGTH = 5; - const SMS_FROM = 'Piwik'; - - static private $instance = null; - - /** - * @return Piwik_MobileMessaging_API - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - /** - * @return Piwik_MobileMessaging_SMSProvider - */ - static private function getSMSProviderInstance($provider) - { - return Piwik_MobileMessaging_SMSProvider::factory($provider); - } - - /** - * determine if SMS API credential are available for the current user - * - * @return bool true if SMS API credential are available for the current user - */ - public function areSMSAPICredentialProvided() - { - Piwik::checkUserHasSomeViewAccess(); - - $credential = $this->getSMSAPICredential(); - return isset($credential[Piwik_MobileMessaging::API_KEY_OPTION]); - } - - private function getSMSAPICredential() - { - $settings = $this->getCredentialManagerSettings(); - return array( - Piwik_MobileMessaging::PROVIDER_OPTION => - isset($settings[Piwik_MobileMessaging::PROVIDER_OPTION]) ? $settings[Piwik_MobileMessaging::PROVIDER_OPTION] : null, - Piwik_MobileMessaging::API_KEY_OPTION => - isset($settings[Piwik_MobileMessaging::API_KEY_OPTION]) ? $settings[Piwik_MobileMessaging::API_KEY_OPTION] : null, - ); - } - - /** - * return the SMS API Provider for the current user - * - * @return string SMS API Provider - */ - public function getSMSProvider() - { - $this->checkCredentialManagementRights(); - $credential = $this->getSMSAPICredential(); - return $credential[Piwik_MobileMessaging::PROVIDER_OPTION]; - } - - /** - * set the SMS API credential - * - * @param string $provider SMS API provider - * @param string $apiKey API Key - * - * @return bool true if SMS API credential were validated and saved, false otherwise - */ - public function setSMSAPICredential($provider, $apiKey) - { - $this->checkCredentialManagementRights(); - - $smsProviderInstance = self::getSMSProviderInstance($provider); - $smsProviderInstance->verifyCredential($apiKey); - - $settings = $this->getCredentialManagerSettings(); - - $settings[Piwik_MobileMessaging::PROVIDER_OPTION] = $provider; - $settings[Piwik_MobileMessaging::API_KEY_OPTION] = $apiKey; - - $this->setCredentialManagerSettings($settings); - - return true; - } - - /** - * add phone number - * - * @param string $phoneNumber - * - * @return bool true - */ - public function addPhoneNumber($phoneNumber) - { - Piwik::checkUserIsNotAnonymous(); - - $phoneNumber = self::sanitizePhoneNumber($phoneNumber); - - $verificationCode = ""; - for($i = 0; $i < self::VERIFICATION_CODE_LENGTH; $i++) - { - $verificationCode .= mt_rand(0,9); - } - - $smsText = Piwik_Translate( - 'MobileMessaging_VerificationText', - array( - $verificationCode, - Piwik_Translate('UserSettings_SubmenuSettings'), - Piwik_Translate('MobileMessaging_SettingsMenu') - ) - ); - - $this->sendSMS($smsText, $phoneNumber, self::SMS_FROM); - - $phoneNumbers = $this->retrievePhoneNumbers(); - $phoneNumbers[$phoneNumber] = $verificationCode; - $this->savePhoneNumbers($phoneNumbers); - - $this->increaseCount(Piwik_MobileMessaging::PHONE_NUMBER_VALIDATION_REQUEST_COUNT_OPTION, $phoneNumber); - - return true; - } - - /** - * sanitize phone number - * - * @param string $phoneNumber - * - * @return string sanitized phone number - */ - public static function sanitizePhoneNumber($phoneNumber) - { - return str_replace(' ', '', $phoneNumber); - } - - /** - * send a SMS - * - * @param string $phoneNumber - * @return bool true - * @ignore - */ - public function sendSMS($content, $phoneNumber, $from) - { - Piwik::checkUserIsNotAnonymous(); - - $credential = $this->getSMSAPICredential(); - $SMSProvider = self::getSMSProviderInstance($credential[Piwik_MobileMessaging::PROVIDER_OPTION]); - $SMSProvider->sendSMS( - $credential[Piwik_MobileMessaging::API_KEY_OPTION], - $content, - $phoneNumber, - $from - ); - - $this->increaseCount(Piwik_MobileMessaging::SMS_SENT_COUNT_OPTION, $phoneNumber); - - return true; - } - - /** - * get remaining credit - * - * @return string remaining credit - */ - public function getCreditLeft() - { - $this->checkCredentialManagementRights(); - - $credential = $this->getSMSAPICredential(); - $SMSProvider = self::getSMSProviderInstance($credential[Piwik_MobileMessaging::PROVIDER_OPTION]); - return $SMSProvider->getCreditLeft( - $credential[Piwik_MobileMessaging::API_KEY_OPTION] - ); - } - - /** - * remove phone number - * - * @param string $phoneNumber - * - * @return bool true - */ - public function removePhoneNumber($phoneNumber) - { - Piwik::checkUserIsNotAnonymous(); - - $phoneNumbers = $this->retrievePhoneNumbers(); - unset($phoneNumbers[$phoneNumber]); - $this->savePhoneNumbers($phoneNumbers); - - // remove phone number from reports - $pdfReportsAPIInstance = Piwik_PDFReports_API::getInstance(); - $reports = $pdfReportsAPIInstance->getReports( - $idSite = false, - $period = false, - $idReport = false, - $ifSuperUserReturnOnlySuperUserReports = $this->getDelegatedManagement() - ); - - foreach($reports as $report) - { - if ($report['type'] == Piwik_MobileMessaging::MOBILE_TYPE) - { - $reportParameters = $report['parameters']; - $reportPhoneNumbers = $reportParameters[Piwik_MobileMessaging::PHONE_NUMBERS_PARAMETER]; - $updatedPhoneNumbers = array(); - foreach($reportPhoneNumbers as $reportPhoneNumber) - { - if($reportPhoneNumber != $phoneNumber) - { - $updatedPhoneNumbers[] = $reportPhoneNumber; - } - } - - if(count($updatedPhoneNumbers) != count($reportPhoneNumbers)) - { - $reportParameters[Piwik_MobileMessaging::PHONE_NUMBERS_PARAMETER] = $updatedPhoneNumbers; - - // note: reports can end up without any recipients - $pdfReportsAPIInstance->updateReport( - $report['idreport'], - $report['idsite'], - $report['description'], - $report['period'], - $report['type'], - $report['format'], - $report['reports'], - $reportParameters - ); - } - } - } - - return true; - } - - private function retrievePhoneNumbers() - { - $settings = $this->getCurrentUserSettings(); - - $phoneNumbers = array(); - if(isset($settings[Piwik_MobileMessaging::PHONE_NUMBERS_OPTION])) - { - $phoneNumbers = $settings[Piwik_MobileMessaging::PHONE_NUMBERS_OPTION]; - } - - return $phoneNumbers; - } - - private function savePhoneNumbers($phoneNumbers) - { - $settings = $this->getCurrentUserSettings(); - - $settings[Piwik_MobileMessaging::PHONE_NUMBERS_OPTION] = $phoneNumbers; - - $this->setCurrentUserSettings($settings); - } - - private function increaseCount($option, $phoneNumber) - { - $settings = $this->getCurrentUserSettings(); - - $counts = array(); - if(isset($settings[$option])) - { - $counts = $settings[$option]; - } - - $countToUpdate = 0; - if(isset($counts[$phoneNumber])) - { - $countToUpdate = $counts[$phoneNumber]; - } - - $counts[$phoneNumber] = $countToUpdate + 1; - - $settings[$option] = $counts; - - $this->setCurrentUserSettings($settings); - } - - /** - * validate phone number - * - * @param string $phoneNumber - * @param string $verificationCode - * - * @return bool true if validation code is correct, false otherwise - */ - public function validatePhoneNumber($phoneNumber, $verificationCode) - { - Piwik::checkUserIsNotAnonymous(); - - $phoneNumbers = $this->retrievePhoneNumbers(); - - if(isset($phoneNumbers[$phoneNumber])) - { - if($verificationCode == $phoneNumbers[$phoneNumber]) { - - $phoneNumbers[$phoneNumber] = null; - $this->savePhoneNumbers($phoneNumbers); - return true; - } - } - - return false; - } - - /** - * get phone number list - * - * @return array $phoneNumber => $isValidated - * @ignore - */ - public function getPhoneNumbers() - { - Piwik::checkUserIsNotAnonymous(); - - $rawPhoneNumbers = $this->retrievePhoneNumbers(); - - $phoneNumbers = array(); - foreach($rawPhoneNumbers as $phoneNumber => $verificationCode) - { - $phoneNumbers[$phoneNumber] = self::isActivated($verificationCode); - } - - return $phoneNumbers; - } - - /** - * get activated phone number list - * - * @return array $phoneNumber - * @ignore - */ - public function getActivatedPhoneNumbers() - { - Piwik::checkUserIsNotAnonymous(); - - $phoneNumbers = $this->retrievePhoneNumbers(); - - $activatedPhoneNumbers = array(); - foreach($phoneNumbers as $phoneNumber => $verificationCode) - { - if(self::isActivated($verificationCode)) - { - $activatedPhoneNumbers[] = $phoneNumber; - } - } - - return $activatedPhoneNumbers; - } - - private static function isActivated($verificationCode) - { - return $verificationCode === null; - } - - /** - * delete the SMS API credential - * - * @return bool true - */ - public function deleteSMSAPICredential() - { - $this->checkCredentialManagementRights(); - - $settings = $this->getCredentialManagerSettings(); - - $settings[Piwik_MobileMessaging::API_KEY_OPTION] = null; - - $this->setCredentialManagerSettings($settings); - - return true; - } - - private function checkCredentialManagementRights() - { - $this->getDelegatedManagement() ? Piwik::checkUserIsNotAnonymous() : Piwik::checkUserIsSuperUser(); - } - - private function setUserSettings($user, $settings) - { - Piwik_SetOption( - $user . Piwik_MobileMessaging::USER_SETTINGS_POSTFIX_OPTION, - Piwik_Common::json_encode($settings) - ); - } - - private function setCurrentUserSettings($settings) - { - $this->setUserSettings(Piwik::getCurrentUserLogin(), $settings); - } - - private function setCredentialManagerSettings($settings) - { - $this->setUserSettings($this->getCredentialManagerLogin(), $settings); - } - - private function getCredentialManagerLogin() - { - return $this->getDelegatedManagement() ? Piwik::getCurrentUserLogin() : Piwik::getSuperUserLogin(); - } - - private function getUserSettings($user) - { - $optionIndex = $user . Piwik_MobileMessaging::USER_SETTINGS_POSTFIX_OPTION; - $userSettings = Piwik_GetOption($optionIndex); - - if(empty($userSettings)) - { - $userSettings = array(); - } - else - { - $userSettings = Piwik_Common::json_decode($userSettings, true); - } - - return $userSettings; - } - - private function getCredentialManagerSettings() - { - return $this->getUserSettings($this->getCredentialManagerLogin()); - } - - private function getCurrentUserSettings() - { - return $this->getUserSettings(Piwik::getCurrentUserLogin()); - } - - /** - * Specify if normal users can manage their own SMS API credential - * - * @param bool $delegatedManagement false if SMS API credential only manageable by super admin, true otherwise - */ - public function setDelegatedManagement($delegatedManagement) - { - Piwik::checkUserIsSuperUser(); - Piwik_SetOption(Piwik_MobileMessaging::DELEGATED_MANAGEMENT_OPTION, $delegatedManagement); - } - - /** - * Determine if normal users can manage their own SMS API credential - * - * @return bool false if SMS API credential only manageable by super admin, true otherwise - */ - public function getDelegatedManagement() - { - Piwik::checkUserHasSomeViewAccess(); - return Piwik_GetOption(Piwik_MobileMessaging::DELEGATED_MANAGEMENT_OPTION) == 'true'; - } + const VERIFICATION_CODE_LENGTH = 5; + const SMS_FROM = 'Piwik'; + + static private $instance = null; + + /** + * @return Piwik_MobileMessaging_API + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + /** + * @return Piwik_MobileMessaging_SMSProvider + */ + static private function getSMSProviderInstance($provider) + { + return Piwik_MobileMessaging_SMSProvider::factory($provider); + } + + /** + * determine if SMS API credential are available for the current user + * + * @return bool true if SMS API credential are available for the current user + */ + public function areSMSAPICredentialProvided() + { + Piwik::checkUserHasSomeViewAccess(); + + $credential = $this->getSMSAPICredential(); + return isset($credential[Piwik_MobileMessaging::API_KEY_OPTION]); + } + + private function getSMSAPICredential() + { + $settings = $this->getCredentialManagerSettings(); + return array( + Piwik_MobileMessaging::PROVIDER_OPTION => + isset($settings[Piwik_MobileMessaging::PROVIDER_OPTION]) ? $settings[Piwik_MobileMessaging::PROVIDER_OPTION] : null, + Piwik_MobileMessaging::API_KEY_OPTION => + isset($settings[Piwik_MobileMessaging::API_KEY_OPTION]) ? $settings[Piwik_MobileMessaging::API_KEY_OPTION] : null, + ); + } + + /** + * return the SMS API Provider for the current user + * + * @return string SMS API Provider + */ + public function getSMSProvider() + { + $this->checkCredentialManagementRights(); + $credential = $this->getSMSAPICredential(); + return $credential[Piwik_MobileMessaging::PROVIDER_OPTION]; + } + + /** + * set the SMS API credential + * + * @param string $provider SMS API provider + * @param string $apiKey API Key + * + * @return bool true if SMS API credential were validated and saved, false otherwise + */ + public function setSMSAPICredential($provider, $apiKey) + { + $this->checkCredentialManagementRights(); + + $smsProviderInstance = self::getSMSProviderInstance($provider); + $smsProviderInstance->verifyCredential($apiKey); + + $settings = $this->getCredentialManagerSettings(); + + $settings[Piwik_MobileMessaging::PROVIDER_OPTION] = $provider; + $settings[Piwik_MobileMessaging::API_KEY_OPTION] = $apiKey; + + $this->setCredentialManagerSettings($settings); + + return true; + } + + /** + * add phone number + * + * @param string $phoneNumber + * + * @return bool true + */ + public function addPhoneNumber($phoneNumber) + { + Piwik::checkUserIsNotAnonymous(); + + $phoneNumber = self::sanitizePhoneNumber($phoneNumber); + + $verificationCode = ""; + for ($i = 0; $i < self::VERIFICATION_CODE_LENGTH; $i++) { + $verificationCode .= mt_rand(0, 9); + } + + $smsText = Piwik_Translate( + 'MobileMessaging_VerificationText', + array( + $verificationCode, + Piwik_Translate('UserSettings_SubmenuSettings'), + Piwik_Translate('MobileMessaging_SettingsMenu') + ) + ); + + $this->sendSMS($smsText, $phoneNumber, self::SMS_FROM); + + $phoneNumbers = $this->retrievePhoneNumbers(); + $phoneNumbers[$phoneNumber] = $verificationCode; + $this->savePhoneNumbers($phoneNumbers); + + $this->increaseCount(Piwik_MobileMessaging::PHONE_NUMBER_VALIDATION_REQUEST_COUNT_OPTION, $phoneNumber); + + return true; + } + + /** + * sanitize phone number + * + * @param string $phoneNumber + * + * @return string sanitized phone number + */ + public static function sanitizePhoneNumber($phoneNumber) + { + return str_replace(' ', '', $phoneNumber); + } + + /** + * send a SMS + * + * @param string $phoneNumber + * @return bool true + * @ignore + */ + public function sendSMS($content, $phoneNumber, $from) + { + Piwik::checkUserIsNotAnonymous(); + + $credential = $this->getSMSAPICredential(); + $SMSProvider = self::getSMSProviderInstance($credential[Piwik_MobileMessaging::PROVIDER_OPTION]); + $SMSProvider->sendSMS( + $credential[Piwik_MobileMessaging::API_KEY_OPTION], + $content, + $phoneNumber, + $from + ); + + $this->increaseCount(Piwik_MobileMessaging::SMS_SENT_COUNT_OPTION, $phoneNumber); + + return true; + } + + /** + * get remaining credit + * + * @return string remaining credit + */ + public function getCreditLeft() + { + $this->checkCredentialManagementRights(); + + $credential = $this->getSMSAPICredential(); + $SMSProvider = self::getSMSProviderInstance($credential[Piwik_MobileMessaging::PROVIDER_OPTION]); + return $SMSProvider->getCreditLeft( + $credential[Piwik_MobileMessaging::API_KEY_OPTION] + ); + } + + /** + * remove phone number + * + * @param string $phoneNumber + * + * @return bool true + */ + public function removePhoneNumber($phoneNumber) + { + Piwik::checkUserIsNotAnonymous(); + + $phoneNumbers = $this->retrievePhoneNumbers(); + unset($phoneNumbers[$phoneNumber]); + $this->savePhoneNumbers($phoneNumbers); + + // remove phone number from reports + $pdfReportsAPIInstance = Piwik_PDFReports_API::getInstance(); + $reports = $pdfReportsAPIInstance->getReports( + $idSite = false, + $period = false, + $idReport = false, + $ifSuperUserReturnOnlySuperUserReports = $this->getDelegatedManagement() + ); + + foreach ($reports as $report) { + if ($report['type'] == Piwik_MobileMessaging::MOBILE_TYPE) { + $reportParameters = $report['parameters']; + $reportPhoneNumbers = $reportParameters[Piwik_MobileMessaging::PHONE_NUMBERS_PARAMETER]; + $updatedPhoneNumbers = array(); + foreach ($reportPhoneNumbers as $reportPhoneNumber) { + if ($reportPhoneNumber != $phoneNumber) { + $updatedPhoneNumbers[] = $reportPhoneNumber; + } + } + + if (count($updatedPhoneNumbers) != count($reportPhoneNumbers)) { + $reportParameters[Piwik_MobileMessaging::PHONE_NUMBERS_PARAMETER] = $updatedPhoneNumbers; + + // note: reports can end up without any recipients + $pdfReportsAPIInstance->updateReport( + $report['idreport'], + $report['idsite'], + $report['description'], + $report['period'], + $report['type'], + $report['format'], + $report['reports'], + $reportParameters + ); + } + } + } + + return true; + } + + private function retrievePhoneNumbers() + { + $settings = $this->getCurrentUserSettings(); + + $phoneNumbers = array(); + if (isset($settings[Piwik_MobileMessaging::PHONE_NUMBERS_OPTION])) { + $phoneNumbers = $settings[Piwik_MobileMessaging::PHONE_NUMBERS_OPTION]; + } + + return $phoneNumbers; + } + + private function savePhoneNumbers($phoneNumbers) + { + $settings = $this->getCurrentUserSettings(); + + $settings[Piwik_MobileMessaging::PHONE_NUMBERS_OPTION] = $phoneNumbers; + + $this->setCurrentUserSettings($settings); + } + + private function increaseCount($option, $phoneNumber) + { + $settings = $this->getCurrentUserSettings(); + + $counts = array(); + if (isset($settings[$option])) { + $counts = $settings[$option]; + } + + $countToUpdate = 0; + if (isset($counts[$phoneNumber])) { + $countToUpdate = $counts[$phoneNumber]; + } + + $counts[$phoneNumber] = $countToUpdate + 1; + + $settings[$option] = $counts; + + $this->setCurrentUserSettings($settings); + } + + /** + * validate phone number + * + * @param string $phoneNumber + * @param string $verificationCode + * + * @return bool true if validation code is correct, false otherwise + */ + public function validatePhoneNumber($phoneNumber, $verificationCode) + { + Piwik::checkUserIsNotAnonymous(); + + $phoneNumbers = $this->retrievePhoneNumbers(); + + if (isset($phoneNumbers[$phoneNumber])) { + if ($verificationCode == $phoneNumbers[$phoneNumber]) { + + $phoneNumbers[$phoneNumber] = null; + $this->savePhoneNumbers($phoneNumbers); + return true; + } + } + + return false; + } + + /** + * get phone number list + * + * @return array $phoneNumber => $isValidated + * @ignore + */ + public function getPhoneNumbers() + { + Piwik::checkUserIsNotAnonymous(); + + $rawPhoneNumbers = $this->retrievePhoneNumbers(); + + $phoneNumbers = array(); + foreach ($rawPhoneNumbers as $phoneNumber => $verificationCode) { + $phoneNumbers[$phoneNumber] = self::isActivated($verificationCode); + } + + return $phoneNumbers; + } + + /** + * get activated phone number list + * + * @return array $phoneNumber + * @ignore + */ + public function getActivatedPhoneNumbers() + { + Piwik::checkUserIsNotAnonymous(); + + $phoneNumbers = $this->retrievePhoneNumbers(); + + $activatedPhoneNumbers = array(); + foreach ($phoneNumbers as $phoneNumber => $verificationCode) { + if (self::isActivated($verificationCode)) { + $activatedPhoneNumbers[] = $phoneNumber; + } + } + + return $activatedPhoneNumbers; + } + + private static function isActivated($verificationCode) + { + return $verificationCode === null; + } + + /** + * delete the SMS API credential + * + * @return bool true + */ + public function deleteSMSAPICredential() + { + $this->checkCredentialManagementRights(); + + $settings = $this->getCredentialManagerSettings(); + + $settings[Piwik_MobileMessaging::API_KEY_OPTION] = null; + + $this->setCredentialManagerSettings($settings); + + return true; + } + + private function checkCredentialManagementRights() + { + $this->getDelegatedManagement() ? Piwik::checkUserIsNotAnonymous() : Piwik::checkUserIsSuperUser(); + } + + private function setUserSettings($user, $settings) + { + Piwik_SetOption( + $user . Piwik_MobileMessaging::USER_SETTINGS_POSTFIX_OPTION, + Piwik_Common::json_encode($settings) + ); + } + + private function setCurrentUserSettings($settings) + { + $this->setUserSettings(Piwik::getCurrentUserLogin(), $settings); + } + + private function setCredentialManagerSettings($settings) + { + $this->setUserSettings($this->getCredentialManagerLogin(), $settings); + } + + private function getCredentialManagerLogin() + { + return $this->getDelegatedManagement() ? Piwik::getCurrentUserLogin() : Piwik::getSuperUserLogin(); + } + + private function getUserSettings($user) + { + $optionIndex = $user . Piwik_MobileMessaging::USER_SETTINGS_POSTFIX_OPTION; + $userSettings = Piwik_GetOption($optionIndex); + + if (empty($userSettings)) { + $userSettings = array(); + } else { + $userSettings = Piwik_Common::json_decode($userSettings, true); + } + + return $userSettings; + } + + private function getCredentialManagerSettings() + { + return $this->getUserSettings($this->getCredentialManagerLogin()); + } + + private function getCurrentUserSettings() + { + return $this->getUserSettings(Piwik::getCurrentUserLogin()); + } + + /** + * Specify if normal users can manage their own SMS API credential + * + * @param bool $delegatedManagement false if SMS API credential only manageable by super admin, true otherwise + */ + public function setDelegatedManagement($delegatedManagement) + { + Piwik::checkUserIsSuperUser(); + Piwik_SetOption(Piwik_MobileMessaging::DELEGATED_MANAGEMENT_OPTION, $delegatedManagement); + } + + /** + * Determine if normal users can manage their own SMS API credential + * + * @return bool false if SMS API credential only manageable by super admin, true otherwise + */ + public function getDelegatedManagement() + { + Piwik::checkUserHasSomeViewAccess(); + return Piwik_GetOption(Piwik_MobileMessaging::DELEGATED_MANAGEMENT_OPTION) == 'true'; + } } diff --git a/plugins/MobileMessaging/Controller.php b/plugins/MobileMessaging/Controller.php index 11efa0817c..ee027c4505 100644 --- a/plugins/MobileMessaging/Controller.php +++ b/plugins/MobileMessaging/Controller.php @@ -17,60 +17,57 @@ require_once PIWIK_INCLUDE_PATH . '/plugins/UserCountry/functions.php'; */ class Piwik_MobileMessaging_Controller extends Piwik_Controller_Admin { - /* - * Mobile Messaging Settings tab : - * - set delegated management - * - provide & validate SMS API credential - * - add & activate phone numbers - * - check remaining credits - */ - function index() - { - Piwik::checkUserIsNotAnonymous(); + /* + * Mobile Messaging Settings tab : + * - set delegated management + * - provide & validate SMS API credential + * - add & activate phone numbers + * - check remaining credits + */ + function index() + { + Piwik::checkUserIsNotAnonymous(); - $view = Piwik_View::factory('Settings'); + $view = Piwik_View::factory('Settings'); - $view->isSuperUser = Piwik::isUserIsSuperUser(); + $view->isSuperUser = Piwik::isUserIsSuperUser(); - $mobileMessagingAPI = Piwik_MobileMessaging_API::getInstance(); - $view->delegatedManagement = $mobileMessagingAPI->getDelegatedManagement(); - $view->credentialSupplied = $mobileMessagingAPI->areSMSAPICredentialProvided(); - $view->accountManagedByCurrentUser = $view->isSuperUser || $view->delegatedManagement; - $view->strHelpAddPhone = Piwik_Translate('MobileMessaging_Settings_PhoneNumbers_HelpAdd', array( Piwik_Translate('UserSettings_SubmenuSettings'), Piwik_Translate('MobileMessaging_SettingsMenu') ) ); - if($view->credentialSupplied && $view->accountManagedByCurrentUser) - { - $view->provider = $mobileMessagingAPI->getSMSProvider(); - $view->creditLeft = $mobileMessagingAPI->getCreditLeft(); - } + $mobileMessagingAPI = Piwik_MobileMessaging_API::getInstance(); + $view->delegatedManagement = $mobileMessagingAPI->getDelegatedManagement(); + $view->credentialSupplied = $mobileMessagingAPI->areSMSAPICredentialProvided(); + $view->accountManagedByCurrentUser = $view->isSuperUser || $view->delegatedManagement; + $view->strHelpAddPhone = Piwik_Translate('MobileMessaging_Settings_PhoneNumbers_HelpAdd', array(Piwik_Translate('UserSettings_SubmenuSettings'), Piwik_Translate('MobileMessaging_SettingsMenu'))); + if ($view->credentialSupplied && $view->accountManagedByCurrentUser) { + $view->provider = $mobileMessagingAPI->getSMSProvider(); + $view->creditLeft = $mobileMessagingAPI->getCreditLeft(); + } - $view->smsProviders = Piwik_MobileMessaging_SMSProvider::$availableSMSProviders; + $view->smsProviders = Piwik_MobileMessaging_SMSProvider::$availableSMSProviders; - // construct the list of countries from the lang files - $countries = array(); - foreach(Piwik_Common::getCountriesList() as $countryCode => $continentCode) - { - if(isset(Piwik_MobileMessaging_CountryCallingCodes::$countryCallingCodes[$countryCode])) - { - $countries[$countryCode] = - array( - 'countryName' => Piwik_CountryTranslate($countryCode), - 'countryCallingCode' => Piwik_MobileMessaging_CountryCallingCodes::$countryCallingCodes[$countryCode], - ); - } - } - $view->countries = $countries; + // construct the list of countries from the lang files + $countries = array(); + foreach (Piwik_Common::getCountriesList() as $countryCode => $continentCode) { + if (isset(Piwik_MobileMessaging_CountryCallingCodes::$countryCallingCodes[$countryCode])) { + $countries[$countryCode] = + array( + 'countryName' => Piwik_CountryTranslate($countryCode), + 'countryCallingCode' => Piwik_MobileMessaging_CountryCallingCodes::$countryCallingCodes[$countryCode], + ); + } + } + $view->countries = $countries; - $view->defaultCountry = Piwik_Common::getCountry( - Piwik_LanguagesManager::getLanguageCodeForCurrentUser(), - true, - Piwik_IP::getIpFromHeader() - ); + $view->defaultCountry = Piwik_Common::getCountry( + Piwik_LanguagesManager::getLanguageCodeForCurrentUser(), + true, + Piwik_IP::getIpFromHeader() + ); - $view->phoneNumbers = $mobileMessagingAPI->getPhoneNumbers(); + $view->phoneNumbers = $mobileMessagingAPI->getPhoneNumbers(); - $this->setBasicVariablesView($view); + $this->setBasicVariablesView($view); - $view->menu = Piwik_GetAdminMenu(); - echo $view->render(); - } + $view->menu = Piwik_GetAdminMenu(); + echo $view->render(); + } } diff --git a/plugins/MobileMessaging/CountryCallingCodes.php b/plugins/MobileMessaging/CountryCallingCodes.php index a6c72bc86f..b1097220f8 100644 --- a/plugins/MobileMessaging/CountryCallingCodes.php +++ b/plugins/MobileMessaging/CountryCallingCodes.php @@ -15,257 +15,257 @@ */ class Piwik_MobileMessaging_CountryCallingCodes { - // list taken from core/DataFiles/Countries.php - public static $countryCallingCodes = array( - 'ad' => '376', - 'ae' => '971', - 'af' => '93', - 'ag' => '1268', // @wikipedia original value: 1 268 - 'ai' => '1264', // @wikipedia original value: 1 264 - 'al' => '355', - 'am' => '374', - 'ao' => '244', + // list taken from core/DataFiles/Countries.php + public static $countryCallingCodes = array( + 'ad' => '376', + 'ae' => '971', + 'af' => '93', + 'ag' => '1268', // @wikipedia original value: 1 268 + 'ai' => '1264', // @wikipedia original value: 1 264 + 'al' => '355', + 'am' => '374', + 'ao' => '244', // 'aq' => 'MISSING CODE', // @wikipedia In Antarctica dialing is dependent on the parent country of each base - 'ar' => '54', - 'as' => '1684', // @wikipedia original value: 1 684 - 'at' => '43', - 'au' => '61', - 'aw' => '297', - 'ax' => '358', - 'az' => '994', - 'ba' => '387', - 'bb' => '1246', // @wikipedia original value: 1 246 - 'bd' => '880', - 'be' => '32', - 'bf' => '226', - 'bg' => '359', - 'bh' => '973', - 'bi' => '257', - 'bj' => '229', - 'bl' => '590', - 'bm' => '1441', // @wikipedia original value: 1 441 - 'bn' => '673', - 'bo' => '591', - 'bq' => '5997', // @wikipedia original value: 599 7 - 'br' => '55', - 'bs' => '1242', // @wikipedia original value: 1 242 - 'bt' => '975', + 'ar' => '54', + 'as' => '1684', // @wikipedia original value: 1 684 + 'at' => '43', + 'au' => '61', + 'aw' => '297', + 'ax' => '358', + 'az' => '994', + 'ba' => '387', + 'bb' => '1246', // @wikipedia original value: 1 246 + 'bd' => '880', + 'be' => '32', + 'bf' => '226', + 'bg' => '359', + 'bh' => '973', + 'bi' => '257', + 'bj' => '229', + 'bl' => '590', + 'bm' => '1441', // @wikipedia original value: 1 441 + 'bn' => '673', + 'bo' => '591', + 'bq' => '5997', // @wikipedia original value: 599 7 + 'br' => '55', + 'bs' => '1242', // @wikipedia original value: 1 242 + 'bt' => '975', // 'bv' => 'MISSING CODE', - 'bw' => '267', - 'by' => '375', - 'bz' => '501', - 'ca' => '1', - 'cc' => '61', - 'cd' => '243', - 'cf' => '236', - 'cg' => '242', - 'ch' => '41', - 'ci' => '225', - 'ck' => '682', - 'cl' => '56', - 'cm' => '237', - 'cn' => '86', - 'co' => '57', - 'cr' => '506', - 'cu' => '53', - 'cv' => '238', - 'cw' => '5999', // @wikipedia original value: 599 9 - 'cx' => '61', - 'cy' => '357', - 'cz' => '420', - 'de' => '49', - 'dj' => '253', - 'dk' => '45', - 'dm' => '1767', // @wikipedia original value: 1 767 + 'bw' => '267', + 'by' => '375', + 'bz' => '501', + 'ca' => '1', + 'cc' => '61', + 'cd' => '243', + 'cf' => '236', + 'cg' => '242', + 'ch' => '41', + 'ci' => '225', + 'ck' => '682', + 'cl' => '56', + 'cm' => '237', + 'cn' => '86', + 'co' => '57', + 'cr' => '506', + 'cu' => '53', + 'cv' => '238', + 'cw' => '5999', // @wikipedia original value: 599 9 + 'cx' => '61', + 'cy' => '357', + 'cz' => '420', + 'de' => '49', + 'dj' => '253', + 'dk' => '45', + 'dm' => '1767', // @wikipedia original value: 1 767 // 'do' => 'MISSING CODE', // @wikipedia original values: 1 809, 1 829, 1 849 - 'dz' => '213', - 'ec' => '593', - 'ee' => '372', - 'eg' => '20', - 'eh' => '212', - 'er' => '291', - 'es' => '34', - 'et' => '251', - 'fi' => '358', - 'fj' => '679', - 'fk' => '500', - 'fm' => '691', - 'fo' => '298', - 'fr' => '33', - 'ga' => '241', - 'gb' => '44', - 'gd' => '1473', // @wikipedia original value: 1 473 - 'ge' => '995', - 'gf' => '594', - 'gg' => '44', - 'gh' => '233', - 'gi' => '350', - 'gl' => '299', - 'gm' => '220', - 'gn' => '224', - 'gp' => '590', - 'gq' => '240', - 'gr' => '30', - 'gs' => '500', - 'gt' => '502', - 'gu' => '1671', // @wikipedia original value: 1 671 - 'gw' => '245', - 'gy' => '592', - 'hk' => '852', + 'dz' => '213', + 'ec' => '593', + 'ee' => '372', + 'eg' => '20', + 'eh' => '212', + 'er' => '291', + 'es' => '34', + 'et' => '251', + 'fi' => '358', + 'fj' => '679', + 'fk' => '500', + 'fm' => '691', + 'fo' => '298', + 'fr' => '33', + 'ga' => '241', + 'gb' => '44', + 'gd' => '1473', // @wikipedia original value: 1 473 + 'ge' => '995', + 'gf' => '594', + 'gg' => '44', + 'gh' => '233', + 'gi' => '350', + 'gl' => '299', + 'gm' => '220', + 'gn' => '224', + 'gp' => '590', + 'gq' => '240', + 'gr' => '30', + 'gs' => '500', + 'gt' => '502', + 'gu' => '1671', // @wikipedia original value: 1 671 + 'gw' => '245', + 'gy' => '592', + 'hk' => '852', // 'hm' => 'MISSING CODE', - 'hn' => '504', - 'hr' => '385', - 'ht' => '509', - 'hu' => '36', - 'id' => '62', - 'ie' => '353', - 'il' => '972', - 'im' => '44', - 'in' => '91', - 'io' => '246', - 'iq' => '964', - 'ir' => '98', - 'is' => '354', - 'it' => '39', - 'je' => '44', - 'jm' => '1876', // @wikipedia original value: 1 876 - 'jo' => '962', - 'jp' => '81', - 'ke' => '254', - 'kg' => '996', - 'kh' => '855', - 'ki' => '686', - 'km' => '269', - 'kn' => '1869', // @wikipedia original value: 1 869 - 'kp' => '850', - 'kr' => '82', - 'kw' => '965', - 'ky' => '1345', // @wikipedia original value: 1 345 + 'hn' => '504', + 'hr' => '385', + 'ht' => '509', + 'hu' => '36', + 'id' => '62', + 'ie' => '353', + 'il' => '972', + 'im' => '44', + 'in' => '91', + 'io' => '246', + 'iq' => '964', + 'ir' => '98', + 'is' => '354', + 'it' => '39', + 'je' => '44', + 'jm' => '1876', // @wikipedia original value: 1 876 + 'jo' => '962', + 'jp' => '81', + 'ke' => '254', + 'kg' => '996', + 'kh' => '855', + 'ki' => '686', + 'km' => '269', + 'kn' => '1869', // @wikipedia original value: 1 869 + 'kp' => '850', + 'kr' => '82', + 'kw' => '965', + 'ky' => '1345', // @wikipedia original value: 1 345 // 'kz' => 'MISSING CODE', // @wikipedia original values: 7 6, 7 7 - 'la' => '856', - 'lb' => '961', - 'lc' => '1758', // @wikipedia original value: 1 758 - 'li' => '423', - 'lk' => '94', - 'lr' => '231', - 'ls' => '266', - 'lt' => '370', - 'lu' => '352', - 'lv' => '371', - 'ly' => '218', - 'ma' => '212', - 'mc' => '377', - 'md' => '373', - 'me' => '382', - 'mf' => '590', - 'mg' => '261', - 'mh' => '692', - 'mk' => '389', - 'ml' => '223', - 'mm' => '95', - 'mn' => '976', - 'mo' => '853', - 'mp' => '1670', // @wikipedia original value: 1 670 - 'mq' => '596', - 'mr' => '222', - 'ms' => '1664', // @wikipedia original value: 1 664 - 'mt' => '356', - 'mu' => '230', - 'mv' => '960', - 'mw' => '265', - 'mx' => '52', - 'my' => '60', - 'mz' => '258', - 'na' => '264', - 'nc' => '687', - 'ne' => '227', - 'nf' => '672', - 'ng' => '234', - 'ni' => '505', - 'nl' => '31', - 'no' => '47', - 'np' => '977', - 'nr' => '674', - 'nu' => '683', - 'nz' => '64', - 'om' => '968', - 'pa' => '507', - 'pe' => '51', - 'pf' => '689', - 'pg' => '675', - 'ph' => '63', - 'pk' => '92', - 'pl' => '48', - 'pm' => '508', - 'pn' => '672', + 'la' => '856', + 'lb' => '961', + 'lc' => '1758', // @wikipedia original value: 1 758 + 'li' => '423', + 'lk' => '94', + 'lr' => '231', + 'ls' => '266', + 'lt' => '370', + 'lu' => '352', + 'lv' => '371', + 'ly' => '218', + 'ma' => '212', + 'mc' => '377', + 'md' => '373', + 'me' => '382', + 'mf' => '590', + 'mg' => '261', + 'mh' => '692', + 'mk' => '389', + 'ml' => '223', + 'mm' => '95', + 'mn' => '976', + 'mo' => '853', + 'mp' => '1670', // @wikipedia original value: 1 670 + 'mq' => '596', + 'mr' => '222', + 'ms' => '1664', // @wikipedia original value: 1 664 + 'mt' => '356', + 'mu' => '230', + 'mv' => '960', + 'mw' => '265', + 'mx' => '52', + 'my' => '60', + 'mz' => '258', + 'na' => '264', + 'nc' => '687', + 'ne' => '227', + 'nf' => '672', + 'ng' => '234', + 'ni' => '505', + 'nl' => '31', + 'no' => '47', + 'np' => '977', + 'nr' => '674', + 'nu' => '683', + 'nz' => '64', + 'om' => '968', + 'pa' => '507', + 'pe' => '51', + 'pf' => '689', + 'pg' => '675', + 'ph' => '63', + 'pk' => '92', + 'pl' => '48', + 'pm' => '508', + 'pn' => '672', // 'pr' => 'MISSING CODE', // @wikipedia original values: 1 787, 1 939 - 'ps' => '970', - 'pt' => '351', - 'pw' => '680', - 'py' => '595', - 'qa' => '974', - 're' => '262', - 'ro' => '40', - 'rs' => '381', - 'ru' => '7', - 'rw' => '250', - 'sa' => '966', - 'sb' => '677', - 'sc' => '248', - 'sd' => '249', - 'se' => '46', - 'sg' => '65', - 'sh' => '290', - 'si' => '386', - 'sj' => '47', - 'sk' => '421', - 'sl' => '232', - 'sm' => '378', - 'sn' => '221', - 'so' => '252', - 'sr' => '597', - 'ss' => '211', - 'st' => '239', - 'sv' => '503', - 'sx' => '1721', //@wikipedia original value: 1 721 - 'sy' => '963', - 'sz' => '268', - 'tc' => '1649', // @wikipedia original value: 1 649 - 'td' => '235', + 'ps' => '970', + 'pt' => '351', + 'pw' => '680', + 'py' => '595', + 'qa' => '974', + 're' => '262', + 'ro' => '40', + 'rs' => '381', + 'ru' => '7', + 'rw' => '250', + 'sa' => '966', + 'sb' => '677', + 'sc' => '248', + 'sd' => '249', + 'se' => '46', + 'sg' => '65', + 'sh' => '290', + 'si' => '386', + 'sj' => '47', + 'sk' => '421', + 'sl' => '232', + 'sm' => '378', + 'sn' => '221', + 'so' => '252', + 'sr' => '597', + 'ss' => '211', + 'st' => '239', + 'sv' => '503', + 'sx' => '1721', //@wikipedia original value: 1 721 + 'sy' => '963', + 'sz' => '268', + 'tc' => '1649', // @wikipedia original value: 1 649 + 'td' => '235', // 'tf' => 'MISSING CODE', - 'tg' => '228', - 'th' => '66', + 'tg' => '228', + 'th' => '66', // 'ti' => 'MISSING CODE', - 'tj' => '992', - 'tk' => '690', - 'tl' => '670', - 'tm' => '993', - 'tn' => '216', - 'to' => '676', - 'tr' => '90', - 'tt' => '1868', // @wikipedia original value: 1 868 - 'tv' => '688', - 'tw' => '886', - 'tz' => '255', - 'ua' => '380', - 'ug' => '256', + 'tj' => '992', + 'tk' => '690', + 'tl' => '670', + 'tm' => '993', + 'tn' => '216', + 'to' => '676', + 'tr' => '90', + 'tt' => '1868', // @wikipedia original value: 1 868 + 'tv' => '688', + 'tw' => '886', + 'tz' => '255', + 'ua' => '380', + 'ug' => '256', // 'um' => 'MISSING CODE', - 'us' => '1', - 'uy' => '598', - 'uz' => '998', + 'us' => '1', + 'uy' => '598', + 'uz' => '998', // 'va' => 'MISSING CODE', // @wikipedia original values: 39 066, assigned 379 - 'vc' => '1784', // @wikipedia original value: 1 784 - 've' => '58', - 'vg' => '1284', // @wikipedia original value: 1 284 - 'vi' => '1340', // @wikipedia original value: 1 340 - 'vn' => '84', - 'vu' => '678', - 'wf' => '681', - 'ws' => '685', - 'ye' => '967', - 'yt' => '262', - 'za' => '27', - 'zm' => '260', - 'zw' => '263' - ); + 'vc' => '1784', // @wikipedia original value: 1 784 + 've' => '58', + 'vg' => '1284', // @wikipedia original value: 1 284 + 'vi' => '1340', // @wikipedia original value: 1 340 + 'vn' => '84', + 'vu' => '678', + 'wf' => '681', + 'ws' => '685', + 'ye' => '967', + 'yt' => '262', + 'za' => '27', + 'zm' => '260', + 'zw' => '263' + ); } diff --git a/plugins/MobileMessaging/GSMCharset.php b/plugins/MobileMessaging/GSMCharset.php index 22278facb2..4d58c96580 100644 --- a/plugins/MobileMessaging/GSMCharset.php +++ b/plugins/MobileMessaging/GSMCharset.php @@ -16,144 +16,144 @@ */ class Piwik_MobileMessaging_GSMCharset { - public static $GSMCharset = array( + public static $GSMCharset = array( - // Standard GSM Characters, weight = 1 - '@' => 1, - '£' => 1, - '$' => 1, - '¥' => 1, - 'è' => 1, - 'é' => 1, - 'ù' => 1, - 'ì' => 1, - 'ò' => 1, - 'Ç' => 1, - 'Ø' => 1, - 'ø' => 1, - 'Å' => 1, - 'å' => 1, - '∆' => 1, - '_' => 1, - 'Φ' => 1, - 'Γ' => 1, - 'Λ' => 1, - 'Ω' => 1, - 'Π' => 1, - 'Ψ' => 1, - 'Σ' => 1, - 'Θ' => 1, - 'Ξ' => 1, - 'Æ' => 1, - 'æ' => 1, - 'ß' => 1, - 'É' => 1, - ' ' => 1, - '!' => 1, - '"' => 1, - '#' => 1, - '¤' => 1, - '%' => 1, - '&' => 1, - '\'' => 1, - '(' => 1, - ')' => 1, - '*' => 1, - '+' => 1, - ',' => 1, - '-' => 1, - '.' => 1, - '/' => 1, - '0' => 1, - '1' => 1, - '2' => 1, - '3' => 1, - '4' => 1, - '5' => 1, - '6' => 1, - '7' => 1, - '8' => 1, - '9' => 1, - ':' => 1, - ';' => 1, - '<' => 1, - '=' => 1, - '>' => 1, - '?' => 1, - '¡' => 1, - 'A' => 1, - 'B' => 1, - 'C' => 1, - 'D' => 1, - 'E' => 1, - 'F' => 1, - 'G' => 1, - 'H' => 1, - 'I' => 1, - 'J' => 1, - 'K' => 1, - 'L' => 1, - 'M' => 1, - 'N' => 1, - 'O' => 1, - 'P' => 1, - 'Q' => 1, - 'R' => 1, - 'S' => 1, - 'T' => 1, - 'U' => 1, - 'V' => 1, - 'W' => 1, - 'X' => 1, - 'Y' => 1, - 'Z' => 1, - 'Ä' => 1, - 'Ö' => 1, - 'Ñ' => 1, - 'Ü' => 1, - '§' => 1, - '¿' => 1, - 'a' => 1, - 'b' => 1, - 'c' => 1, - 'd' => 1, - 'e' => 1, - 'f' => 1, - 'g' => 1, - 'h' => 1, - 'i' => 1, - 'j' => 1, - 'k' => 1, - 'l' => 1, - 'm' => 1, - 'n' => 1, - 'o' => 1, - 'p' => 1, - 'q' => 1, - 'r' => 1, - 's' => 1, - 't' => 1, - 'u' => 1, - 'v' => 1, - 'w' => 1, - 'x' => 1, - 'y' => 1, - 'z' => 1, - 'ä' => 1, - 'ö' => 1, - 'ñ' => 1, - 'ü' => 1, - 'à' => 1, + // Standard GSM Characters, weight = 1 + '@' => 1, + '£' => 1, + '$' => 1, + '¥' => 1, + 'è' => 1, + 'é' => 1, + 'ù' => 1, + 'ì' => 1, + 'ò' => 1, + 'Ç' => 1, + 'Ø' => 1, + 'ø' => 1, + 'Å' => 1, + 'å' => 1, + '∆' => 1, + '_' => 1, + 'Φ' => 1, + 'Γ' => 1, + 'Λ' => 1, + 'Ω' => 1, + 'Π' => 1, + 'Ψ' => 1, + 'Σ' => 1, + 'Θ' => 1, + 'Ξ' => 1, + 'Æ' => 1, + 'æ' => 1, + 'ß' => 1, + 'É' => 1, + ' ' => 1, + '!' => 1, + '"' => 1, + '#' => 1, + '¤' => 1, + '%' => 1, + '&' => 1, + '\'' => 1, + '(' => 1, + ')' => 1, + '*' => 1, + '+' => 1, + ',' => 1, + '-' => 1, + '.' => 1, + '/' => 1, + '0' => 1, + '1' => 1, + '2' => 1, + '3' => 1, + '4' => 1, + '5' => 1, + '6' => 1, + '7' => 1, + '8' => 1, + '9' => 1, + ':' => 1, + ';' => 1, + '<' => 1, + '=' => 1, + '>' => 1, + '?' => 1, + '¡' => 1, + 'A' => 1, + 'B' => 1, + 'C' => 1, + 'D' => 1, + 'E' => 1, + 'F' => 1, + 'G' => 1, + 'H' => 1, + 'I' => 1, + 'J' => 1, + 'K' => 1, + 'L' => 1, + 'M' => 1, + 'N' => 1, + 'O' => 1, + 'P' => 1, + 'Q' => 1, + 'R' => 1, + 'S' => 1, + 'T' => 1, + 'U' => 1, + 'V' => 1, + 'W' => 1, + 'X' => 1, + 'Y' => 1, + 'Z' => 1, + 'Ä' => 1, + 'Ö' => 1, + 'Ñ' => 1, + 'Ü' => 1, + '§' => 1, + '¿' => 1, + 'a' => 1, + 'b' => 1, + 'c' => 1, + 'd' => 1, + 'e' => 1, + 'f' => 1, + 'g' => 1, + 'h' => 1, + 'i' => 1, + 'j' => 1, + 'k' => 1, + 'l' => 1, + 'm' => 1, + 'n' => 1, + 'o' => 1, + 'p' => 1, + 'q' => 1, + 'r' => 1, + 's' => 1, + 't' => 1, + 'u' => 1, + 'v' => 1, + 'w' => 1, + 'x' => 1, + 'y' => 1, + 'z' => 1, + 'ä' => 1, + 'ö' => 1, + 'ñ' => 1, + 'ü' => 1, + 'à' => 1, - // Extended GSM Characters, weight = 2 - '^' => 2, - '{' => 2, - '}' => 2, - '\\' => 2, - '[' => 2, - '~' => 2, - ']' => 2, - '|' => 2, - '€' => 2, - ); + // Extended GSM Characters, weight = 2 + '^' => 2, + '{' => 2, + '}' => 2, + '\\' => 2, + '[' => 2, + '~' => 2, + ']' => 2, + '|' => 2, + '€' => 2, + ); } diff --git a/plugins/MobileMessaging/MobileMessaging.php b/plugins/MobileMessaging/MobileMessaging.php index e090411562..62028ba3e4 100644 --- a/plugins/MobileMessaging/MobileMessaging.php +++ b/plugins/MobileMessaging/MobileMessaging.php @@ -1,10 +1,10 @@ true, - ); - - static private $managedReportTypes = array( - self::MOBILE_TYPE => 'plugins/MobileMessaging/images/phone.png' - ); - - static private $managedReportFormats = array( - self::SMS_FORMAT => 'plugins/MobileMessaging/images/phone.png' - ); - - static private $availableReports = array( - array( - 'module' => 'MultiSites', - 'action' => 'getAll', - ), - array( - 'module' => 'MultiSites', - 'action' => 'getOne', - ), - ); - - /** - * Return information about this plugin. - * - * @see Piwik_Plugin - * - * @return array - */ - public function getInformation() - { - return array( - 'name' => 'Mobile Messaging Plugin', - 'description' => Piwik_Translate('MobileMessaging_PluginDescription'), - 'homepage' => 'http://piwik.org/', - 'author' => 'Piwik', - 'author_homepage' => 'http://piwik.org/', - 'license' => 'GPL v3 or later', - 'license_homepage' => 'http://www.gnu.org/licenses/gpl.html', - 'version' => Piwik_Version::VERSION, - ); - } - - function getListHooksRegistered() - { - return array( - 'AdminMenu.add' => 'addMenu', - 'AssetManager.getJsFiles' => 'getJsFiles', - 'PDFReports.getReportParameters' => 'getReportParameters', - 'PDFReports.validateReportParameters' => 'validateReportParameters', - 'PDFReports.getReportMetadata' => 'getReportMetadata', - 'PDFReports.getReportTypes' => 'getReportTypes', - 'PDFReports.getReportFormats' => 'getReportFormats', - 'PDFReports.getRendererInstance' => 'getRendererInstance', - 'PDFReports.getReportRecipients' => 'getReportRecipients', - 'PDFReports.allowMultipleReports' => 'allowMultipleReports', - 'PDFReports.sendReport' => 'sendReport', - 'template_reportParametersPDFReports' => 'template_reportParametersPDFReports', - ); - } - - function addMenu() - { - Piwik_AddAdminMenu( - 'MobileMessaging_SettingsMenu', - array('module' => 'MobileMessaging', 'action' => 'index'), - true - ); - } - - /** - * Get JavaScript files - * - * @param Piwik_Event_Notification $notification notification object - */ - function getJsFiles( $notification ) - { - $jsFiles = &$notification->getNotificationObject(); - - $jsFiles[] = "plugins/MobileMessaging/scripts/MobileMessagingSettings.js"; - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - function validateReportParameters( $notification ) - { - if(self::manageEvent($notification)) - { - $parameters = &$notification->getNotificationObject(); - - // phone number validation - $availablePhoneNumbers = Piwik_MobileMessaging_API::getInstance()->getActivatedPhoneNumbers(); - - $phoneNumbers = $parameters[self::PHONE_NUMBERS_PARAMETER]; - foreach($phoneNumbers as $key => $phoneNumber) - { - //when a wrong phone number is supplied we silently discard it - if(!in_array($phoneNumber, $availablePhoneNumbers)) - { - unset($phoneNumbers[$key]); - } - } - - // 'unset' seems to transform the array to an associative array - $parameters[self::PHONE_NUMBERS_PARAMETER] = array_values($phoneNumbers); - } - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - function getReportMetadata( $notification ) - { - if(self::manageEvent($notification)) - { - $availableReportMetadata = &$notification->getNotificationObject(); - - $notificationInfo = $notification->getNotificationInfo(); - $idSite = $notificationInfo[Piwik_PDFReports_API::ID_SITE_INFO_KEY]; - - foreach(self::$availableReports as $availableReport) - { - $reportMetadata = Piwik_API_API::getInstance()->getMetadata( - $idSite, - $availableReport['module'], - $availableReport['action'] - ); - - if($reportMetadata != null) - { - $reportMetadata = reset($reportMetadata); - $availableReportMetadata[] = $reportMetadata; - } - } - } - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - function getReportTypes( $notification ) - { - $reportTypes = &$notification->getNotificationObject(); - $reportTypes = array_merge($reportTypes, self::$managedReportTypes); - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - function getReportFormats( $notification ) - { - if(self::manageEvent($notification)) - { - $reportFormats = &$notification->getNotificationObject(); - $reportFormats = array_merge($reportFormats, self::$managedReportFormats); - } - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - function getReportParameters( $notification ) - { - if(self::manageEvent($notification)) - { - $availableParameters = &$notification->getNotificationObject(); - $availableParameters = self::$availableParameters; - } - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - function getRendererInstance( $notification ) - { - if(self::manageEvent($notification)) - { - $reportRenderer = &$notification->getNotificationObject(); - - if(Piwik_PluginsManager::getInstance()->isPluginActivated('MultiSites')) - { - $reportRenderer = new Piwik_MobileMessaging_ReportRenderer_Sms(); - } - else - { - $reportRenderer = new Piwik_MobileMessaging_ReportRenderer_Exception( - Piwik_Translate('MobileMessaging_MultiSites_Must_Be_Activated') - ); - } - } - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - function allowMultipleReports( $notification ) - { - if(self::manageEvent($notification)) - { - $allowMultipleReports = &$notification->getNotificationObject(); - $allowMultipleReports = false; - } - } - - function getReportRecipients( $notification ) - { - if(self::manageEvent($notification)) - { - $recipients = &$notification->getNotificationObject(); - $notificationInfo = $notification->getNotificationInfo(); - - $report = $notificationInfo[Piwik_PDFReports_API::REPORT_KEY]; - $recipients = $report['parameters'][self::PHONE_NUMBERS_PARAMETER]; - } - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - function sendReport( $notification ) - { - if(self::manageEvent($notification)) - { - $notificationInfo = $notification->getNotificationInfo(); - $report = $notificationInfo[Piwik_PDFReports_API::REPORT_KEY]; - $contents = $notificationInfo[Piwik_PDFReports_API::REPORT_CONTENT_KEY]; - - $parameters = $report['parameters']; - $phoneNumbers = $parameters[self::PHONE_NUMBERS_PARAMETER]; - - if(in_array('MultiSites_getAll', $report['reports'])) - { - $from = Piwik_Translate('General_Reports'); - } - else - { - $from = $notificationInfo[Piwik_PDFReports_API::WEBSITE_NAME_KEY]; - } - - $mobileMessagingAPI = Piwik_MobileMessaging_API::getInstance(); - foreach($phoneNumbers as $phoneNumber) - { - $mobileMessagingAPI->sendSMS( - $contents, - $phoneNumber, - $from - ); - } - } - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - static public function template_reportParametersPDFReports($notification) - { - if(Piwik::isUserIsAnonymous()) - { - return; - } - - $out =& $notification->getNotificationObject(); - $view = Piwik_View::factory('ReportParameters'); - $view->reportType = self::MOBILE_TYPE; - $view->phoneNumbers = Piwik_MobileMessaging_API::getInstance()->getActivatedPhoneNumbers(); - $out .= $view->render(); - } - - private static function manageEvent($notification) - { - $notificationInfo = $notification->getNotificationInfo(); - return in_array($notificationInfo[Piwik_PDFReports_API::REPORT_TYPE_INFO_KEY], array_keys(self::$managedReportTypes)); - } - - function install() - { - $delegatedManagement = Piwik_GetOption(self::DELEGATED_MANAGEMENT_OPTION); - if (empty($delegatedManagement)) - { - Piwik_SetOption(self::DELEGATED_MANAGEMENT_OPTION, self::DELEGATED_MANAGEMENT_OPTION_DEFAULT); - } - } - - function deactivate() - { - // delete all mobile reports - $pdfReportsAPIInstance = Piwik_PDFReports_API::getInstance(); - $reports = $pdfReportsAPIInstance->getReports(); - - foreach($reports as $report) - { - if ($report['type'] == Piwik_MobileMessaging::MOBILE_TYPE) - { - $pdfReportsAPIInstance->deleteReport($report['idreport']); - } - } - } - - public function uninstall() - { - // currently the UI does not allow to delete a plugin - // when it becomes available, all the MobileMessaging settings (API credentials, phone numbers, etc..) should be removed from the option table - return; - } + const DELEGATED_MANAGEMENT_OPTION = 'MobileMessaging_DelegatedManagement'; + const PROVIDER_OPTION = 'Provider'; + const API_KEY_OPTION = 'APIKey'; + const PHONE_NUMBERS_OPTION = 'PhoneNumbers'; + const PHONE_NUMBER_VALIDATION_REQUEST_COUNT_OPTION = 'PhoneNumberValidationRequestCount'; + const SMS_SENT_COUNT_OPTION = 'SMSSentCount'; + const DELEGATED_MANAGEMENT_OPTION_DEFAULT = 'false'; + const USER_SETTINGS_POSTFIX_OPTION = '_MobileMessagingSettings'; + + const PHONE_NUMBERS_PARAMETER = 'phoneNumbers'; + + const MOBILE_TYPE = 'mobile'; + const SMS_FORMAT = 'sms'; + + static private $availableParameters = array( + self::PHONE_NUMBERS_PARAMETER => true, + ); + + static private $managedReportTypes = array( + self::MOBILE_TYPE => 'plugins/MobileMessaging/images/phone.png' + ); + + static private $managedReportFormats = array( + self::SMS_FORMAT => 'plugins/MobileMessaging/images/phone.png' + ); + + static private $availableReports = array( + array( + 'module' => 'MultiSites', + 'action' => 'getAll', + ), + array( + 'module' => 'MultiSites', + 'action' => 'getOne', + ), + ); + + /** + * Return information about this plugin. + * + * @see Piwik_Plugin + * + * @return array + */ + public function getInformation() + { + return array( + 'name' => 'Mobile Messaging Plugin', + 'description' => Piwik_Translate('MobileMessaging_PluginDescription'), + 'homepage' => 'http://piwik.org/', + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'license' => 'GPL v3 or later', + 'license_homepage' => 'http://www.gnu.org/licenses/gpl.html', + 'version' => Piwik_Version::VERSION, + ); + } + + function getListHooksRegistered() + { + return array( + 'AdminMenu.add' => 'addMenu', + 'AssetManager.getJsFiles' => 'getJsFiles', + 'PDFReports.getReportParameters' => 'getReportParameters', + 'PDFReports.validateReportParameters' => 'validateReportParameters', + 'PDFReports.getReportMetadata' => 'getReportMetadata', + 'PDFReports.getReportTypes' => 'getReportTypes', + 'PDFReports.getReportFormats' => 'getReportFormats', + 'PDFReports.getRendererInstance' => 'getRendererInstance', + 'PDFReports.getReportRecipients' => 'getReportRecipients', + 'PDFReports.allowMultipleReports' => 'allowMultipleReports', + 'PDFReports.sendReport' => 'sendReport', + 'template_reportParametersPDFReports' => 'template_reportParametersPDFReports', + ); + } + + function addMenu() + { + Piwik_AddAdminMenu( + 'MobileMessaging_SettingsMenu', + array('module' => 'MobileMessaging', 'action' => 'index'), + true + ); + } + + /** + * Get JavaScript files + * + * @param Piwik_Event_Notification $notification notification object + */ + function getJsFiles($notification) + { + $jsFiles = & $notification->getNotificationObject(); + + $jsFiles[] = "plugins/MobileMessaging/scripts/MobileMessagingSettings.js"; + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + function validateReportParameters($notification) + { + if (self::manageEvent($notification)) { + $parameters = & $notification->getNotificationObject(); + + // phone number validation + $availablePhoneNumbers = Piwik_MobileMessaging_API::getInstance()->getActivatedPhoneNumbers(); + + $phoneNumbers = $parameters[self::PHONE_NUMBERS_PARAMETER]; + foreach ($phoneNumbers as $key => $phoneNumber) { + //when a wrong phone number is supplied we silently discard it + if (!in_array($phoneNumber, $availablePhoneNumbers)) { + unset($phoneNumbers[$key]); + } + } + + // 'unset' seems to transform the array to an associative array + $parameters[self::PHONE_NUMBERS_PARAMETER] = array_values($phoneNumbers); + } + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + function getReportMetadata($notification) + { + if (self::manageEvent($notification)) { + $availableReportMetadata = & $notification->getNotificationObject(); + + $notificationInfo = $notification->getNotificationInfo(); + $idSite = $notificationInfo[Piwik_PDFReports_API::ID_SITE_INFO_KEY]; + + foreach (self::$availableReports as $availableReport) { + $reportMetadata = Piwik_API_API::getInstance()->getMetadata( + $idSite, + $availableReport['module'], + $availableReport['action'] + ); + + if ($reportMetadata != null) { + $reportMetadata = reset($reportMetadata); + $availableReportMetadata[] = $reportMetadata; + } + } + } + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + function getReportTypes($notification) + { + $reportTypes = & $notification->getNotificationObject(); + $reportTypes = array_merge($reportTypes, self::$managedReportTypes); + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + function getReportFormats($notification) + { + if (self::manageEvent($notification)) { + $reportFormats = & $notification->getNotificationObject(); + $reportFormats = array_merge($reportFormats, self::$managedReportFormats); + } + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + function getReportParameters($notification) + { + if (self::manageEvent($notification)) { + $availableParameters = & $notification->getNotificationObject(); + $availableParameters = self::$availableParameters; + } + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + function getRendererInstance($notification) + { + if (self::manageEvent($notification)) { + $reportRenderer = & $notification->getNotificationObject(); + + if (Piwik_PluginsManager::getInstance()->isPluginActivated('MultiSites')) { + $reportRenderer = new Piwik_MobileMessaging_ReportRenderer_Sms(); + } else { + $reportRenderer = new Piwik_MobileMessaging_ReportRenderer_Exception( + Piwik_Translate('MobileMessaging_MultiSites_Must_Be_Activated') + ); + } + } + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + function allowMultipleReports($notification) + { + if (self::manageEvent($notification)) { + $allowMultipleReports = & $notification->getNotificationObject(); + $allowMultipleReports = false; + } + } + + function getReportRecipients($notification) + { + if (self::manageEvent($notification)) { + $recipients = & $notification->getNotificationObject(); + $notificationInfo = $notification->getNotificationInfo(); + + $report = $notificationInfo[Piwik_PDFReports_API::REPORT_KEY]; + $recipients = $report['parameters'][self::PHONE_NUMBERS_PARAMETER]; + } + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + function sendReport($notification) + { + if (self::manageEvent($notification)) { + $notificationInfo = $notification->getNotificationInfo(); + $report = $notificationInfo[Piwik_PDFReports_API::REPORT_KEY]; + $contents = $notificationInfo[Piwik_PDFReports_API::REPORT_CONTENT_KEY]; + + $parameters = $report['parameters']; + $phoneNumbers = $parameters[self::PHONE_NUMBERS_PARAMETER]; + + if (in_array('MultiSites_getAll', $report['reports'])) { + $from = Piwik_Translate('General_Reports'); + } else { + $from = $notificationInfo[Piwik_PDFReports_API::WEBSITE_NAME_KEY]; + } + + $mobileMessagingAPI = Piwik_MobileMessaging_API::getInstance(); + foreach ($phoneNumbers as $phoneNumber) { + $mobileMessagingAPI->sendSMS( + $contents, + $phoneNumber, + $from + ); + } + } + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + static public function template_reportParametersPDFReports($notification) + { + if (Piwik::isUserIsAnonymous()) { + return; + } + + $out =& $notification->getNotificationObject(); + $view = Piwik_View::factory('ReportParameters'); + $view->reportType = self::MOBILE_TYPE; + $view->phoneNumbers = Piwik_MobileMessaging_API::getInstance()->getActivatedPhoneNumbers(); + $out .= $view->render(); + } + + private static function manageEvent($notification) + { + $notificationInfo = $notification->getNotificationInfo(); + return in_array($notificationInfo[Piwik_PDFReports_API::REPORT_TYPE_INFO_KEY], array_keys(self::$managedReportTypes)); + } + + function install() + { + $delegatedManagement = Piwik_GetOption(self::DELEGATED_MANAGEMENT_OPTION); + if (empty($delegatedManagement)) { + Piwik_SetOption(self::DELEGATED_MANAGEMENT_OPTION, self::DELEGATED_MANAGEMENT_OPTION_DEFAULT); + } + } + + function deactivate() + { + // delete all mobile reports + $pdfReportsAPIInstance = Piwik_PDFReports_API::getInstance(); + $reports = $pdfReportsAPIInstance->getReports(); + + foreach ($reports as $report) { + if ($report['type'] == Piwik_MobileMessaging::MOBILE_TYPE) { + $pdfReportsAPIInstance->deleteReport($report['idreport']); + } + } + } + + public function uninstall() + { + // currently the UI does not allow to delete a plugin + // when it becomes available, all the MobileMessaging settings (API credentials, phone numbers, etc..) should be removed from the option table + return; + } } diff --git a/plugins/MobileMessaging/ReportRenderer/Exception.php b/plugins/MobileMessaging/ReportRenderer/Exception.php index af2352150a..a1eac80d8f 100644 --- a/plugins/MobileMessaging/ReportRenderer/Exception.php +++ b/plugins/MobileMessaging/ReportRenderer/Exception.php @@ -15,57 +15,57 @@ */ class Piwik_MobileMessaging_ReportRenderer_Exception extends Piwik_ReportRenderer { - private $rendering = ""; + private $rendering = ""; - function __construct($exception) - { - $this->rendering = $exception; - } + function __construct($exception) + { + $this->rendering = $exception; + } - public function setLocale($locale) - { - // nothing to do - } + public function setLocale($locale) + { + // nothing to do + } - public function sendToDisk($filename) - { - return Piwik_ReportRenderer::writeFile( - $filename, - Piwik_MobileMessaging_ReportRenderer_Sms::SMS_FILE_EXTENSION, - $this->rendering - ); - } + public function sendToDisk($filename) + { + return Piwik_ReportRenderer::writeFile( + $filename, + Piwik_MobileMessaging_ReportRenderer_Sms::SMS_FILE_EXTENSION, + $this->rendering + ); + } - public function sendToBrowserDownload($filename) - { - Piwik_ReportRenderer::sendToBrowser( - $filename, - Piwik_MobileMessaging_ReportRenderer_Sms::SMS_FILE_EXTENSION, - Piwik_MobileMessaging_ReportRenderer_Sms::SMS_CONTENT_TYPE, - $this->rendering - ); - } + public function sendToBrowserDownload($filename) + { + Piwik_ReportRenderer::sendToBrowser( + $filename, + Piwik_MobileMessaging_ReportRenderer_Sms::SMS_FILE_EXTENSION, + Piwik_MobileMessaging_ReportRenderer_Sms::SMS_CONTENT_TYPE, + $this->rendering + ); + } - public function sendToBrowserInline($filename) - { - Piwik_ReportRenderer::inlineToBrowser( - Piwik_MobileMessaging_ReportRenderer_Sms::SMS_CONTENT_TYPE, - $this->rendering - ); - } + public function sendToBrowserInline($filename) + { + Piwik_ReportRenderer::inlineToBrowser( + Piwik_MobileMessaging_ReportRenderer_Sms::SMS_CONTENT_TYPE, + $this->rendering + ); + } - public function getRenderedReport() - { - return $this->rendering; - } + public function getRenderedReport() + { + return $this->rendering; + } - public function renderFrontPage($websiteName, $prettyDate, $description, $reportMetadata) - { - // nothing to do - } + public function renderFrontPage($websiteName, $prettyDate, $description, $reportMetadata) + { + // nothing to do + } - public function renderReport($processedReport) - { - // nothing to do - } + public function renderReport($processedReport) + { + // nothing to do + } } diff --git a/plugins/MobileMessaging/ReportRenderer/Sms.php b/plugins/MobileMessaging/ReportRenderer/Sms.php index 47186f3ff6..96f2999d9f 100644 --- a/plugins/MobileMessaging/ReportRenderer/Sms.php +++ b/plugins/MobileMessaging/ReportRenderer/Sms.php @@ -16,65 +16,64 @@ */ class Piwik_MobileMessaging_ReportRenderer_Sms extends Piwik_ReportRenderer { - const FLOAT_REGEXP = '/[-+]?[0-9]*[\.,]?[0-9]+/'; - const SMS_CONTENT_TYPE = 'text/plain'; - const SMS_FILE_EXTENSION = 'sms'; - - private $rendering = ""; - - public function setLocale($locale) - { - // nothing to do - } - - public function sendToDisk($filename) - { - return Piwik_ReportRenderer::writeFile($filename, self::SMS_FILE_EXTENSION, $this->rendering); - } - - public function sendToBrowserDownload($filename) - { - Piwik_ReportRenderer::sendToBrowser($filename, self::SMS_FILE_EXTENSION, self::SMS_CONTENT_TYPE, $this->rendering); - } - - public function sendToBrowserInline($filename) - { - Piwik_ReportRenderer::inlineToBrowser(self::SMS_CONTENT_TYPE, $this->rendering); - } - - public function getRenderedReport() - { - return $this->rendering; - } - - public function renderFrontPage($websiteName, $prettyDate, $description, $reportMetadata) - { - // nothing to do - } - - public function renderReport($processedReport) - { - $isGoalPluginEnabled = Piwik_Common::isGoalPluginEnabled(); - $prettyDate = $processedReport['prettyDate']; - $reportData = $processedReport['reportData']; - - $evolutionMetrics = array(); - $multiSitesAPIMetrics = Piwik_MultiSites_API::getApiMetrics($enhanced = true); - foreach($multiSitesAPIMetrics as $metricSettings) - { - $evolutionMetrics[] = $metricSettings[Piwik_MultiSites_API::METRIC_EVOLUTION_COL_NAME_KEY]; - } - - // no decimal for all metrics to shorten SMS content (keeps the monetary sign for revenue metrics) - $reportData->filter( - 'ColumnCallbackReplace', - array( - array_merge(array_keys($multiSitesAPIMetrics),$evolutionMetrics), - create_function ( - '$value', - ' - return preg_replace_callback ( - "'.self::FLOAT_REGEXP.'", + const FLOAT_REGEXP = '/[-+]?[0-9]*[\.,]?[0-9]+/'; + const SMS_CONTENT_TYPE = 'text/plain'; + const SMS_FILE_EXTENSION = 'sms'; + + private $rendering = ""; + + public function setLocale($locale) + { + // nothing to do + } + + public function sendToDisk($filename) + { + return Piwik_ReportRenderer::writeFile($filename, self::SMS_FILE_EXTENSION, $this->rendering); + } + + public function sendToBrowserDownload($filename) + { + Piwik_ReportRenderer::sendToBrowser($filename, self::SMS_FILE_EXTENSION, self::SMS_CONTENT_TYPE, $this->rendering); + } + + public function sendToBrowserInline($filename) + { + Piwik_ReportRenderer::inlineToBrowser(self::SMS_CONTENT_TYPE, $this->rendering); + } + + public function getRenderedReport() + { + return $this->rendering; + } + + public function renderFrontPage($websiteName, $prettyDate, $description, $reportMetadata) + { + // nothing to do + } + + public function renderReport($processedReport) + { + $isGoalPluginEnabled = Piwik_Common::isGoalPluginEnabled(); + $prettyDate = $processedReport['prettyDate']; + $reportData = $processedReport['reportData']; + + $evolutionMetrics = array(); + $multiSitesAPIMetrics = Piwik_MultiSites_API::getApiMetrics($enhanced = true); + foreach ($multiSitesAPIMetrics as $metricSettings) { + $evolutionMetrics[] = $metricSettings[Piwik_MultiSites_API::METRIC_EVOLUTION_COL_NAME_KEY]; + } + + // no decimal for all metrics to shorten SMS content (keeps the monetary sign for revenue metrics) + $reportData->filter( + 'ColumnCallbackReplace', + array( + array_merge(array_keys($multiSitesAPIMetrics), $evolutionMetrics), + create_function( + '$value', + ' + return preg_replace_callback ( + "' . self::FLOAT_REGEXP . '", create_function ( \'$matches\', \'return round($matches[0]);\' @@ -82,47 +81,46 @@ class Piwik_MobileMessaging_ReportRenderer_Sms extends Piwik_ReportRenderer $value ); ' - ) - ) - ); - - // evolution metrics formatting : - // - remove monetary, percentage and white spaces to shorten SMS content - // (this is also needed to be able to test $value != 0 and see if there is an evolution at all in SMSReport.tpl) - // - adds a plus sign - $reportData->filter( - 'ColumnCallbackReplace', - array( - $evolutionMetrics, - create_function ( - '$value', - ' - $matched = preg_match("'.self::FLOAT_REGEXP.'", $value, $matches); + ) + ) + ); + + // evolution metrics formatting : + // - remove monetary, percentage and white spaces to shorten SMS content + // (this is also needed to be able to test $value != 0 and see if there is an evolution at all in SMSReport.tpl) + // - adds a plus sign + $reportData->filter( + 'ColumnCallbackReplace', + array( + $evolutionMetrics, + create_function( + '$value', + ' + $matched = preg_match("' . self::FLOAT_REGEXP . '", $value, $matches); return $matched ? sprintf("%+d",$matches[0]) : $value; ' - ) - ) - ); - - $dataRows = $reportData->getRows(); - $reportMetadata = $processedReport['reportMetadata']; - $reportRowsMetadata = $reportMetadata->getRows(); - - $siteHasECommerce = array(); - foreach($reportRowsMetadata as $rowMetadata) - { - $idSite = $rowMetadata->getColumn('idsite'); - $siteHasECommerce[$idSite] = Piwik_Site::isEcommerceEnabledFor($idSite); - } - - $smarty = new Piwik_Smarty(); - $smarty->assign("isGoalPluginEnabled", $isGoalPluginEnabled); - $smarty->assign("reportRows", $dataRows); - $smarty->assign("reportRowsMetadata", $reportRowsMetadata); - $smarty->assign("prettyDate", $prettyDate); - $smarty->assign("siteHasECommerce", $siteHasECommerce); - $smarty->assign("displaySiteName", $processedReport['metadata']['action'] == 'getAll'); - - $this->rendering .= $smarty->fetch(PIWIK_USER_PATH . '/plugins/MobileMessaging/templates/SMSReport.tpl'); - } + ) + ) + ); + + $dataRows = $reportData->getRows(); + $reportMetadata = $processedReport['reportMetadata']; + $reportRowsMetadata = $reportMetadata->getRows(); + + $siteHasECommerce = array(); + foreach ($reportRowsMetadata as $rowMetadata) { + $idSite = $rowMetadata->getColumn('idsite'); + $siteHasECommerce[$idSite] = Piwik_Site::isEcommerceEnabledFor($idSite); + } + + $smarty = new Piwik_Smarty(); + $smarty->assign("isGoalPluginEnabled", $isGoalPluginEnabled); + $smarty->assign("reportRows", $dataRows); + $smarty->assign("reportRowsMetadata", $reportRowsMetadata); + $smarty->assign("prettyDate", $prettyDate); + $smarty->assign("siteHasECommerce", $siteHasECommerce); + $smarty->assign("displaySiteName", $processedReport['metadata']['action'] == 'getAll'); + + $this->rendering .= $smarty->fetch(PIWIK_USER_PATH . '/plugins/MobileMessaging/templates/SMSReport.tpl'); + } } diff --git a/plugins/MobileMessaging/SMSProvider.php b/plugins/MobileMessaging/SMSProvider.php index 12bd7f0933..9f17311349 100644 --- a/plugins/MobileMessaging/SMSProvider.php +++ b/plugins/MobileMessaging/SMSProvider.php @@ -17,13 +17,13 @@ */ abstract class Piwik_MobileMessaging_SMSProvider { - const MAX_GSM_CHARS_IN_ONE_UNIQUE_SMS = 160; - const MAX_GSM_CHARS_IN_ONE_CONCATENATED_SMS = 153; - const MAX_UCS2_CHARS_IN_ONE_UNIQUE_SMS = 70; - const MAX_UCS2_CHARS_IN_ONE_CONCATENATED_SMS = 67; + const MAX_GSM_CHARS_IN_ONE_UNIQUE_SMS = 160; + const MAX_GSM_CHARS_IN_ONE_CONCATENATED_SMS = 153; + const MAX_UCS2_CHARS_IN_ONE_UNIQUE_SMS = 70; + const MAX_UCS2_CHARS_IN_ONE_CONCATENATED_SMS = 67; - static public $availableSMSProviders = array( - 'Clockwork' => 'You can use to send SMS Reports from Piwik.
+ static public $availableSMSProviders = array( + 'Clockwork' => 'You can use to send SMS Reports from Piwik.
  • First, get an API Key from Clockwork (Signup is free!)
  • Enter your Clockwork API Key on this page.
  • @@ -35,141 +35,138 @@ abstract class Piwik_MobileMessaging_SMSProvider
', - ); - - /** - * Return the SMSProvider associated to the provider name $providerName - * - * @throws exception If the provider is unknown - * @param string $providerName - * @return Piwik_MobileMessaging_SMSProvider - */ - static public function factory($providerName) - { - $className = 'Piwik_MobileMessaging_SMSProvider_' . $providerName; - - try { - Piwik_Loader::loadClass($className); - return new $className; - } catch(Exception $e) { - throw new Exception( - Piwik_TranslateException( - 'MobileMessaging_Exception_UnknownProvider', - array($providerName, implode(', ', array_keys(self::$availableSMSProviders))) - ) - ); - } - } - - /** - * Assert whether a given String contains UCS2 characters - * - * @param string $string - * @return bool true if $string contains UCS2 characters - */ - static public function containsUCS2Characters($string) - { - $GSMCharsetAsString = implode(array_keys(Piwik_MobileMessaging_GSMCharset::$GSMCharset)); - - foreach(self::mb_str_split($string) as $char) - { - if(mb_strpos($GSMCharsetAsString, $char) === false) { - return true; - } - } - - return false; - } - - /** - * Truncate $string and append $appendedString at the end if $string can not fit the - * the $maximumNumberOfConcatenatedSMS. - * - * @param string $string String to truncate - * @param int $maximumNumberOfConcatenatedSMS - * @param string $appendedString - * @return string original $string or truncated $string appended with $appendedString - */ - static public function truncate($string, $maximumNumberOfConcatenatedSMS, $appendedString = 'MobileMessaging_SMS_Content_Too_Long') - { - $appendedString = Piwik_Translate($appendedString); - - $smsContentContainsUCS2Chars = self::containsUCS2Characters($string); - $maxCharsAllowed = self::maxCharsAllowed($maximumNumberOfConcatenatedSMS, $smsContentContainsUCS2Chars); - $sizeOfSMSContent = self::sizeOfSMSContent($string, $smsContentContainsUCS2Chars); - - if($sizeOfSMSContent <= $maxCharsAllowed) return $string; - - $smsContentContainsUCS2Chars = $smsContentContainsUCS2Chars || self::containsUCS2Characters($appendedString); - $maxCharsAllowed = self::maxCharsAllowed($maximumNumberOfConcatenatedSMS, $smsContentContainsUCS2Chars); - $sizeOfSMSContent = self::sizeOfSMSContent($string . $appendedString, $smsContentContainsUCS2Chars); - - $sizeToTruncate = $sizeOfSMSContent - $maxCharsAllowed; - - $subStrToTruncate = ''; - $subStrSize = 0; - $reversedStringChars = array_reverse(self::mb_str_split($string)); - for($i = 0; $subStrSize < $sizeToTruncate; $i++) - { - $subStrToTruncate = $reversedStringChars[$i] . $subStrToTruncate; - $subStrSize = self::sizeOfSMSContent($subStrToTruncate, $smsContentContainsUCS2Chars); - } - - return preg_replace('/' . preg_quote($subStrToTruncate) . '$/', $appendedString, $string); - } - - static private function mb_str_split($string) - { - return preg_split('//u',$string, -1, PREG_SPLIT_NO_EMPTY); - } - - static private function sizeOfSMSContent($smsContent, $containsUCS2Chars) - { - if($containsUCS2Chars) return mb_strlen($smsContent, 'UTF-8'); - - $sizeOfSMSContent = 0; - foreach(self::mb_str_split($smsContent) as $char) - { - $sizeOfSMSContent += Piwik_MobileMessaging_GSMCharset::$GSMCharset[$char]; - } - return $sizeOfSMSContent; - } - - static private function maxCharsAllowed($maximumNumberOfConcatenatedSMS, $containsUCS2Chars) - { - $maxCharsInOneUniqueSMS = $containsUCS2Chars ? self::MAX_UCS2_CHARS_IN_ONE_UNIQUE_SMS : self::MAX_GSM_CHARS_IN_ONE_UNIQUE_SMS; - $maxCharsInOneConcatenatedSMS = $containsUCS2Chars ? self::MAX_UCS2_CHARS_IN_ONE_CONCATENATED_SMS : self::MAX_GSM_CHARS_IN_ONE_CONCATENATED_SMS; - - $uniqueSMS = $maximumNumberOfConcatenatedSMS == 1; - - return $uniqueSMS ? - $maxCharsInOneUniqueSMS : - $maxCharsInOneConcatenatedSMS * $maximumNumberOfConcatenatedSMS; - } - - /** - * verify the SMS API credential - * - * @param string $apiKey API Key - * @return bool true if SMS API credential are valid, false otherwise - */ - abstract public function verifyCredential($apiKey); - - /** - * get remaining credits - * - * @param string $apiKey API Key - * @return string remaining credits - */ - abstract public function getCreditLeft($apiKey); - - /** - * send SMS - * - * @param string $apiKey - * @param string $smsText - * @param string $phoneNumber - * @return bool true - */ - abstract public function sendSMS($apiKey, $smsText, $phoneNumber, $from); + ); + + /** + * Return the SMSProvider associated to the provider name $providerName + * + * @throws exception If the provider is unknown + * @param string $providerName + * @return Piwik_MobileMessaging_SMSProvider + */ + static public function factory($providerName) + { + $className = 'Piwik_MobileMessaging_SMSProvider_' . $providerName; + + try { + Piwik_Loader::loadClass($className); + return new $className; + } catch (Exception $e) { + throw new Exception( + Piwik_TranslateException( + 'MobileMessaging_Exception_UnknownProvider', + array($providerName, implode(', ', array_keys(self::$availableSMSProviders))) + ) + ); + } + } + + /** + * Assert whether a given String contains UCS2 characters + * + * @param string $string + * @return bool true if $string contains UCS2 characters + */ + static public function containsUCS2Characters($string) + { + $GSMCharsetAsString = implode(array_keys(Piwik_MobileMessaging_GSMCharset::$GSMCharset)); + + foreach (self::mb_str_split($string) as $char) { + if (mb_strpos($GSMCharsetAsString, $char) === false) { + return true; + } + } + + return false; + } + + /** + * Truncate $string and append $appendedString at the end if $string can not fit the + * the $maximumNumberOfConcatenatedSMS. + * + * @param string $string String to truncate + * @param int $maximumNumberOfConcatenatedSMS + * @param string $appendedString + * @return string original $string or truncated $string appended with $appendedString + */ + static public function truncate($string, $maximumNumberOfConcatenatedSMS, $appendedString = 'MobileMessaging_SMS_Content_Too_Long') + { + $appendedString = Piwik_Translate($appendedString); + + $smsContentContainsUCS2Chars = self::containsUCS2Characters($string); + $maxCharsAllowed = self::maxCharsAllowed($maximumNumberOfConcatenatedSMS, $smsContentContainsUCS2Chars); + $sizeOfSMSContent = self::sizeOfSMSContent($string, $smsContentContainsUCS2Chars); + + if ($sizeOfSMSContent <= $maxCharsAllowed) return $string; + + $smsContentContainsUCS2Chars = $smsContentContainsUCS2Chars || self::containsUCS2Characters($appendedString); + $maxCharsAllowed = self::maxCharsAllowed($maximumNumberOfConcatenatedSMS, $smsContentContainsUCS2Chars); + $sizeOfSMSContent = self::sizeOfSMSContent($string . $appendedString, $smsContentContainsUCS2Chars); + + $sizeToTruncate = $sizeOfSMSContent - $maxCharsAllowed; + + $subStrToTruncate = ''; + $subStrSize = 0; + $reversedStringChars = array_reverse(self::mb_str_split($string)); + for ($i = 0; $subStrSize < $sizeToTruncate; $i++) { + $subStrToTruncate = $reversedStringChars[$i] . $subStrToTruncate; + $subStrSize = self::sizeOfSMSContent($subStrToTruncate, $smsContentContainsUCS2Chars); + } + + return preg_replace('/' . preg_quote($subStrToTruncate) . '$/', $appendedString, $string); + } + + static private function mb_str_split($string) + { + return preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY); + } + + static private function sizeOfSMSContent($smsContent, $containsUCS2Chars) + { + if ($containsUCS2Chars) return mb_strlen($smsContent, 'UTF-8'); + + $sizeOfSMSContent = 0; + foreach (self::mb_str_split($smsContent) as $char) { + $sizeOfSMSContent += Piwik_MobileMessaging_GSMCharset::$GSMCharset[$char]; + } + return $sizeOfSMSContent; + } + + static private function maxCharsAllowed($maximumNumberOfConcatenatedSMS, $containsUCS2Chars) + { + $maxCharsInOneUniqueSMS = $containsUCS2Chars ? self::MAX_UCS2_CHARS_IN_ONE_UNIQUE_SMS : self::MAX_GSM_CHARS_IN_ONE_UNIQUE_SMS; + $maxCharsInOneConcatenatedSMS = $containsUCS2Chars ? self::MAX_UCS2_CHARS_IN_ONE_CONCATENATED_SMS : self::MAX_GSM_CHARS_IN_ONE_CONCATENATED_SMS; + + $uniqueSMS = $maximumNumberOfConcatenatedSMS == 1; + + return $uniqueSMS ? + $maxCharsInOneUniqueSMS : + $maxCharsInOneConcatenatedSMS * $maximumNumberOfConcatenatedSMS; + } + + /** + * verify the SMS API credential + * + * @param string $apiKey API Key + * @return bool true if SMS API credential are valid, false otherwise + */ + abstract public function verifyCredential($apiKey); + + /** + * get remaining credits + * + * @param string $apiKey API Key + * @return string remaining credits + */ + abstract public function getCreditLeft($apiKey); + + /** + * send SMS + * + * @param string $apiKey + * @param string $smsText + * @param string $phoneNumber + * @return bool true + */ + abstract public function sendSMS($apiKey, $smsText, $phoneNumber, $from); } diff --git a/plugins/MobileMessaging/SMSProvider/Clockwork.php b/plugins/MobileMessaging/SMSProvider/Clockwork.php index dfb7854fd3..b7a50dfe02 100644 --- a/plugins/MobileMessaging/SMSProvider/Clockwork.php +++ b/plugins/MobileMessaging/SMSProvider/Clockwork.php @@ -16,86 +16,85 @@ require_once PIWIK_INCLUDE_PATH . "/plugins/MobileMessaging/APIException.php"; */ class Piwik_MobileMessaging_SMSProvider_Clockwork extends Piwik_MobileMessaging_SMSProvider { - const SOCKET_TIMEOUT = 15; - - const BASE_API_URL = 'https://api.mediaburst.co.uk/http'; - const CHECK_CREDIT_RESOURCE = '/credit.aspx'; - const SEND_SMS_RESOURCE = '/send.aspx'; - - const ERROR_STRING = 'Error'; - - const MAXIMUM_FROM_LENGTH = 11; - const MAXIMUM_CONCATENATED_SMS = 3; - - public function verifyCredential($apiKey) - { - $this->getCreditLeft($apiKey); - - return true; - } - - public function sendSMS($apiKey, $smsText, $phoneNumber, $from) - { - $from = substr($from, 0, self::MAXIMUM_FROM_LENGTH); - - $smsText = self::truncate($smsText, self::MAXIMUM_CONCATENATED_SMS); - - $additionalParameters = array( - 'To' => str_replace('+','', $phoneNumber), - 'Content' => $smsText, - 'From' => $from, - 'Long' => 1, - 'MsgType' => self::containsUCS2Characters($smsText) ? 'UCS2' : 'TEXT', - ); - - $this->issueApiCall( - $apiKey, - self::SEND_SMS_RESOURCE, - $additionalParameters - ); - } - - private function issueApiCall($apiKey, $resource, $additionalParameters = array()) - { - $accountParameters = array( - 'Key' => $apiKey, - ); - - $parameters = array_merge($accountParameters, $additionalParameters); - - $url = self::BASE_API_URL - . $resource - . '?' . http_build_query($parameters, '', '&'); - - $timeout = self::SOCKET_TIMEOUT; - - $result = Piwik_Http::sendHttpRequestBy( - Piwik_Http::getTransportMethod(), - $url, - $timeout, - $userAgent = null, - $destinationPath = null, - $file = null, - $followDepth = 0, - $acceptLanguage = false, - $acceptInvalidSslCertificate = true - ); - - if(strpos($result, self::ERROR_STRING) !== false) - { - throw new Piwik_MobileMessaging_APIException( - 'Clockwork API returned the following error message : ' . $result - ); - } - - return $result; - } - - public function getCreditLeft($apiKey) - { - return $this->issueApiCall( - $apiKey, - self::CHECK_CREDIT_RESOURCE - ); - } + const SOCKET_TIMEOUT = 15; + + const BASE_API_URL = 'https://api.mediaburst.co.uk/http'; + const CHECK_CREDIT_RESOURCE = '/credit.aspx'; + const SEND_SMS_RESOURCE = '/send.aspx'; + + const ERROR_STRING = 'Error'; + + const MAXIMUM_FROM_LENGTH = 11; + const MAXIMUM_CONCATENATED_SMS = 3; + + public function verifyCredential($apiKey) + { + $this->getCreditLeft($apiKey); + + return true; + } + + public function sendSMS($apiKey, $smsText, $phoneNumber, $from) + { + $from = substr($from, 0, self::MAXIMUM_FROM_LENGTH); + + $smsText = self::truncate($smsText, self::MAXIMUM_CONCATENATED_SMS); + + $additionalParameters = array( + 'To' => str_replace('+', '', $phoneNumber), + 'Content' => $smsText, + 'From' => $from, + 'Long' => 1, + 'MsgType' => self::containsUCS2Characters($smsText) ? 'UCS2' : 'TEXT', + ); + + $this->issueApiCall( + $apiKey, + self::SEND_SMS_RESOURCE, + $additionalParameters + ); + } + + private function issueApiCall($apiKey, $resource, $additionalParameters = array()) + { + $accountParameters = array( + 'Key' => $apiKey, + ); + + $parameters = array_merge($accountParameters, $additionalParameters); + + $url = self::BASE_API_URL + . $resource + . '?' . http_build_query($parameters, '', '&'); + + $timeout = self::SOCKET_TIMEOUT; + + $result = Piwik_Http::sendHttpRequestBy( + Piwik_Http::getTransportMethod(), + $url, + $timeout, + $userAgent = null, + $destinationPath = null, + $file = null, + $followDepth = 0, + $acceptLanguage = false, + $acceptInvalidSslCertificate = true + ); + + if (strpos($result, self::ERROR_STRING) !== false) { + throw new Piwik_MobileMessaging_APIException( + 'Clockwork API returned the following error message : ' . $result + ); + } + + return $result; + } + + public function getCreditLeft($apiKey) + { + return $this->issueApiCall( + $apiKey, + self::CHECK_CREDIT_RESOURCE + ); + } } diff --git a/plugins/MobileMessaging/SMSProvider/StubbedProvider.php b/plugins/MobileMessaging/SMSProvider/StubbedProvider.php index 57f89e73d2..8402eefc51 100644 --- a/plugins/MobileMessaging/SMSProvider/StubbedProvider.php +++ b/plugins/MobileMessaging/SMSProvider/StubbedProvider.php @@ -17,18 +17,18 @@ class Piwik_MobileMessaging_SMSProvider_StubbedProvider extends Piwik_MobileMessaging_SMSProvider { - public function verifyCredential($apiKey) - { - return true; - } + public function verifyCredential($apiKey) + { + return true; + } - public function sendSMS($apiKey, $smsText, $phoneNumber, $from) - { - // nothing to do - } + public function sendSMS($apiKey, $smsText, $phoneNumber, $from) + { + // nothing to do + } - public function getCreditLeft($apiKey) - { - return 1; - } + public function getCreditLeft($apiKey) + { + return 1; + } } \ No newline at end of file diff --git a/plugins/MobileMessaging/scripts/MobileMessagingSettings.js b/plugins/MobileMessaging/scripts/MobileMessagingSettings.js index 840578a1c1..207d4c725d 100644 --- a/plugins/MobileMessaging/scripts/MobileMessagingSettings.js +++ b/plugins/MobileMessaging/scripts/MobileMessagingSettings.js @@ -8,103 +8,93 @@ // TODO when UI stabilized, factorize ajax boiler plate accros MobileMessagingSettings javascript functions var MobileMessagingSettings = MobileMessagingSettings || (function () { - /************************************************************ - * Private data - ************************************************************/ - var - delegatedManagementSelector = 'input[name=delegatedManagement]', - apiAccountSubmitSelector = '#apiAccountSubmit', - addPhoneNumberSubmitSelector = '#addPhoneNumberSubmit', - providersSelector = '#smsProviders', - providerDescriptionsSelector = '.providerDescription', - apiKeySelector = '#apiKey', - countriesSelector = '#countries', - countryCallingCodeSelector = '#countryCallingCode', - newPhoneNumberSelector = '#newPhoneNumber', - suspiciousPhoneNumberSelector = '#suspiciousPhoneNumber', - validatePhoneNumberSubmitSelector = '.validatePhoneNumberSubmit', - formDescriptionSelector = '.form-description', - removePhoneNumberSubmitSelector = '.removePhoneNumberSubmit', - verificationCodeSelector = '.verificationCode', - phoneNumberSelector = '.phoneNumber', - deleteAcountSelector = '#deleteAccount', - confirmDeleteAccountSelector = '#confirmDeleteAccount', - accountFormSelector = '#accountForm', - displayAccountFormSelector = '#displayAccountForm', - phoneNumberActivatedSelector = '#phoneNumberActivated', - invalidActivationCodeMsgSelector = '#invalidActivationCode', - ajaxErrorsSelector = '#ajaxErrorMobileMessagingSettings', - invalidVerificationCodeAjaxErrorSelector = '#invalidVerificationCodeAjaxError', - ajaxLoadingSelector = '#ajaxLoadingMobileMessagingSettings'; - - /************************************************************ - * Private methods - ************************************************************/ - - function initUIEvents() { - - $(delegatedManagementSelector).change(updateDelegatedManagement); - $(apiAccountSubmitSelector).click(updateApiAccount); - $(deleteAcountSelector).click(confirmDeleteApiAccount); - $(displayAccountFormSelector).click(displayAccountForm); - $(addPhoneNumberSubmitSelector).click(addPhoneNumber); - $(newPhoneNumberSelector).keyup(updateSuspiciousPhoneNumberMessage); - $(validatePhoneNumberSubmitSelector).click(validatePhoneNumber); - $(removePhoneNumberSubmitSelector).click(removePhoneNumber); - $(countryCallingCodeSelector).keyup(updateCountry); - $(countriesSelector).change(updateCountryCallingCode); - updateCountryCallingCode(); - $(providersSelector).change(updateProviderDescription); - updateProviderDescription(); - } - - function updateCountry() - { - var countryCallingCode = $(countryCallingCodeSelector).val(); - if(countryCallingCode != null && countryCallingCode != '') - { - var countryToSelect = $(countriesSelector + ' option[value='+countryCallingCode+']'); - if(countryToSelect.size() > 0) - { - countryToSelect.attr('selected', 'selected'); - } - else - { - $(countriesSelector + ' option:selected').removeAttr('selected'); - } - } - } - - function displayAccountForm() - { - $(accountFormSelector).show(); - } - - function validatePhoneNumber(event) - { - var phoneNumberContainer = $(event.target).parent(); - var verificationCodeContainer = phoneNumberContainer.find(verificationCodeSelector); - var verificationCode = verificationCodeContainer.val(); - var phoneNumber = phoneNumberContainer.find(phoneNumberSelector).html(); - - if(verificationCode != null && verificationCode != '') - { - var success = - function(response) - { - $(phoneNumberActivatedSelector).hide(); - if(!response.value) - { - $(invalidVerificationCodeAjaxErrorSelector).html($(invalidActivationCodeMsgSelector).html()).fadeIn(); - } - else - { - $(phoneNumberActivatedSelector).show(); - $(verificationCodeContainer).remove(); - $(phoneNumberContainer).find(validatePhoneNumberSubmitSelector).remove(); - $(phoneNumberContainer).find(formDescriptionSelector).remove(); - } - }; + /************************************************************ + * Private data + ************************************************************/ + var + delegatedManagementSelector = 'input[name=delegatedManagement]', + apiAccountSubmitSelector = '#apiAccountSubmit', + addPhoneNumberSubmitSelector = '#addPhoneNumberSubmit', + providersSelector = '#smsProviders', + providerDescriptionsSelector = '.providerDescription', + apiKeySelector = '#apiKey', + countriesSelector = '#countries', + countryCallingCodeSelector = '#countryCallingCode', + newPhoneNumberSelector = '#newPhoneNumber', + suspiciousPhoneNumberSelector = '#suspiciousPhoneNumber', + validatePhoneNumberSubmitSelector = '.validatePhoneNumberSubmit', + formDescriptionSelector = '.form-description', + removePhoneNumberSubmitSelector = '.removePhoneNumberSubmit', + verificationCodeSelector = '.verificationCode', + phoneNumberSelector = '.phoneNumber', + deleteAcountSelector = '#deleteAccount', + confirmDeleteAccountSelector = '#confirmDeleteAccount', + accountFormSelector = '#accountForm', + displayAccountFormSelector = '#displayAccountForm', + phoneNumberActivatedSelector = '#phoneNumberActivated', + invalidActivationCodeMsgSelector = '#invalidActivationCode', + ajaxErrorsSelector = '#ajaxErrorMobileMessagingSettings', + invalidVerificationCodeAjaxErrorSelector = '#invalidVerificationCodeAjaxError', + ajaxLoadingSelector = '#ajaxLoadingMobileMessagingSettings'; + + /************************************************************ + * Private methods + ************************************************************/ + + function initUIEvents() { + + $(delegatedManagementSelector).change(updateDelegatedManagement); + $(apiAccountSubmitSelector).click(updateApiAccount); + $(deleteAcountSelector).click(confirmDeleteApiAccount); + $(displayAccountFormSelector).click(displayAccountForm); + $(addPhoneNumberSubmitSelector).click(addPhoneNumber); + $(newPhoneNumberSelector).keyup(updateSuspiciousPhoneNumberMessage); + $(validatePhoneNumberSubmitSelector).click(validatePhoneNumber); + $(removePhoneNumberSubmitSelector).click(removePhoneNumber); + $(countryCallingCodeSelector).keyup(updateCountry); + $(countriesSelector).change(updateCountryCallingCode); + updateCountryCallingCode(); + $(providersSelector).change(updateProviderDescription); + updateProviderDescription(); + } + + function updateCountry() { + var countryCallingCode = $(countryCallingCodeSelector).val(); + if (countryCallingCode != null && countryCallingCode != '') { + var countryToSelect = $(countriesSelector + ' option[value=' + countryCallingCode + ']'); + if (countryToSelect.size() > 0) { + countryToSelect.attr('selected', 'selected'); + } + else { + $(countriesSelector + ' option:selected').removeAttr('selected'); + } + } + } + + function displayAccountForm() { + $(accountFormSelector).show(); + } + + function validatePhoneNumber(event) { + var phoneNumberContainer = $(event.target).parent(); + var verificationCodeContainer = phoneNumberContainer.find(verificationCodeSelector); + var verificationCode = verificationCodeContainer.val(); + var phoneNumber = phoneNumberContainer.find(phoneNumberSelector).html(); + + if (verificationCode != null && verificationCode != '') { + var success = + function (response) { + $(phoneNumberActivatedSelector).hide(); + if (!response.value) { + $(invalidVerificationCodeAjaxErrorSelector).html($(invalidActivationCodeMsgSelector).html()).fadeIn(); + } + else { + $(phoneNumberActivatedSelector).show(); + $(verificationCodeContainer).remove(); + $(phoneNumberContainer).find(validatePhoneNumberSubmitSelector).remove(); + $(phoneNumberContainer).find(formDescriptionSelector).remove(); + } + }; var ajaxHandler = new ajaxHelper(); ajaxHandler.addParams({ @@ -117,13 +107,12 @@ var MobileMessagingSettings = MobileMessagingSettings || (function () { ajaxHandler.setLoadingElement(ajaxLoadingSelector); ajaxHandler.setErrorElement(invalidVerificationCodeAjaxErrorSelector); ajaxHandler.send(true); - } - } + } + } - function removePhoneNumber(event) - { - var phoneNumberContainer = $(event.target).parent(); - var phoneNumber = phoneNumberContainer.find(phoneNumberSelector).html(); + function removePhoneNumber(event) { + var phoneNumberContainer = $(event.target).parent(); + var phoneNumber = phoneNumberContainer.find(phoneNumberSelector).html(); var ajaxHandler = new ajaxHelper(); ajaxHandler.addParams({ @@ -136,32 +125,27 @@ var MobileMessagingSettings = MobileMessagingSettings || (function () { ajaxHandler.setLoadingElement(ajaxLoadingSelector); ajaxHandler.setErrorElement(ajaxErrorsSelector); ajaxHandler.send(true); - } - - function updateSuspiciousPhoneNumberMessage() - { - var newPhoneNumber = $(newPhoneNumberSelector).val(); - - // check if number starts with 0 - if($.trim(newPhoneNumber).lastIndexOf('0', 0) === 0) - { - $(suspiciousPhoneNumberSelector).show(); - } - else - { - $(suspiciousPhoneNumberSelector).hide(); - } - } - - function addPhoneNumber() - { - var newPhoneNumber = $(newPhoneNumberSelector).val(); - var countryCallingCode = $(countryCallingCodeSelector).val(); - - var phoneNumber = '+' + countryCallingCode + newPhoneNumber; - - if(newPhoneNumber != null && newPhoneNumber != '') - { + } + + function updateSuspiciousPhoneNumberMessage() { + var newPhoneNumber = $(newPhoneNumberSelector).val(); + + // check if number starts with 0 + if ($.trim(newPhoneNumber).lastIndexOf('0', 0) === 0) { + $(suspiciousPhoneNumberSelector).show(); + } + else { + $(suspiciousPhoneNumberSelector).hide(); + } + } + + function addPhoneNumber() { + var newPhoneNumber = $(newPhoneNumberSelector).val(); + var countryCallingCode = $(countryCallingCodeSelector).val(); + + var phoneNumber = '+' + countryCallingCode + newPhoneNumber; + + if (newPhoneNumber != null && newPhoneNumber != '') { var ajaxHandler = new ajaxHelper(); ajaxHandler.addParams({ module: 'API', @@ -173,30 +157,27 @@ var MobileMessagingSettings = MobileMessagingSettings || (function () { ajaxHandler.setLoadingElement(ajaxLoadingSelector); ajaxHandler.setErrorElement(ajaxErrorsSelector); ajaxHandler.send(true); - } - } - - function updateCountryCallingCode() - { - $(countryCallingCodeSelector).val($(countriesSelector + ' option:selected').val()); - } - - function updateProviderDescription() - { - $(providerDescriptionsSelector).hide(); - $('#' + $(providersSelector + ' option:selected').val() + providerDescriptionsSelector).show(); - } - - function updateDelegatedManagement() { - setDelegatedManagement(getDelegatedManagement()); - } - - function confirmDeleteApiAccount() - { - piwikHelper.modalConfirm(confirmDeleteAccountSelector, {yes: deleteApiAccount}); - } - - function deleteApiAccount() { + } + } + + function updateCountryCallingCode() { + $(countryCallingCodeSelector).val($(countriesSelector + ' option:selected').val()); + } + + function updateProviderDescription() { + $(providerDescriptionsSelector).hide(); + $('#' + $(providersSelector + ' option:selected').val() + providerDescriptionsSelector).show(); + } + + function updateDelegatedManagement() { + setDelegatedManagement(getDelegatedManagement()); + } + + function confirmDeleteApiAccount() { + piwikHelper.modalConfirm(confirmDeleteAccountSelector, {yes: deleteApiAccount}); + } + + function deleteApiAccount() { var ajaxHandler = new ajaxHelper(); ajaxHandler.addParams({ module: 'API', @@ -207,14 +188,14 @@ var MobileMessagingSettings = MobileMessagingSettings || (function () { ajaxHandler.setLoadingElement(ajaxLoadingSelector); ajaxHandler.setErrorElement(ajaxErrorsSelector); ajaxHandler.send(true); - } + } - function updateApiAccount() { + function updateApiAccount() { - var provider = $(providersSelector + ' option:selected').val(); - var apiKey = $(apiKeySelector).val(); + var provider = $(providersSelector + ' option:selected').val(); + var apiKey = $(apiKeySelector).val(); - if(apiKey != '') { + if (apiKey != '') { var ajaxHandler = new ajaxHelper(); ajaxHandler.addParams({ module: 'API', @@ -226,10 +207,10 @@ var MobileMessagingSettings = MobileMessagingSettings || (function () { ajaxHandler.setLoadingElement(ajaxLoadingSelector); ajaxHandler.setErrorElement(ajaxErrorsSelector); ajaxHandler.send(true); - } - } + } + } - function setDelegatedManagement(delegatedManagement) { + function setDelegatedManagement(delegatedManagement) { var ajaxHandler = new ajaxHelper(); ajaxHandler.addParams({ module: 'API', @@ -241,28 +222,28 @@ var MobileMessagingSettings = MobileMessagingSettings || (function () { ajaxHandler.setLoadingElement(ajaxLoadingSelector); ajaxHandler.setErrorElement(ajaxErrorsSelector); ajaxHandler.send(true); - } + } - function getDelegatedManagement() { - return $(delegatedManagementSelector + ':checked').val(); - } + function getDelegatedManagement() { + return $(delegatedManagementSelector + ':checked').val(); + } - /************************************************************ - * Public data and methods - ************************************************************/ + /************************************************************ + * Public data and methods + ************************************************************/ - return { + return { - /* - * Initialize UI events - */ - initUIEvents: function () { - initUIEvents(); - } - }; + /* + * Initialize UI events + */ + initUIEvents: function () { + initUIEvents(); + } + }; }()); -$(document).ready( function() { - MobileMessagingSettings.initUIEvents(); +$(document).ready(function () { + MobileMessagingSettings.initUIEvents(); }); diff --git a/plugins/MobileMessaging/templates/ReportParameters.tpl b/plugins/MobileMessaging/templates/ReportParameters.tpl index dceecaf490..24a6154667 100644 --- a/plugins/MobileMessaging/templates/ReportParameters.tpl +++ b/plugins/MobileMessaging/templates/ReportParameters.tpl @@ -1,64 +1,71 @@ - - {'MobileMessaging_MobileReport_PhoneNumbers'|translate} - - - {if $phoneNumbers|@count eq 0} -
- {'MobileMessaging_MobileReport_NoPhoneNumbers'|translate} - {else} - {foreach from=$phoneNumbers item=phoneNumber} -
- {/foreach} -
- {'MobileMessaging_MobileReport_AdditionalPhoneNumbers'|translate} - {/if} - {'MobileMessaging_MobileReport_MobileMessagingSettingsLink'|translate} -
- + + {'MobileMessaging_MobileReport_PhoneNumbers'|translate} + + + {if $phoneNumbers|@count eq 0} +
+ {'MobileMessaging_MobileReport_NoPhoneNumbers'|translate} + {else} + {foreach from=$phoneNumbers item=phoneNumber} + +
+ {/foreach} +
+ {'MobileMessaging_MobileReport_AdditionalPhoneNumbers'|translate} + {/if} + {'MobileMessaging_MobileReport_MobileMessagingSettingsLink'|translate} +
+ diff --git a/plugins/MobileMessaging/templates/SMSReport.tpl b/plugins/MobileMessaging/templates/SMSReport.tpl index 2084ce426f..c74ea165c4 100644 --- a/plugins/MobileMessaging/templates/SMSReport.tpl +++ b/plugins/MobileMessaging/templates/SMSReport.tpl @@ -1,71 +1,71 @@ {strip} - {$prettyDate}.{literal} {/literal} - - {if empty($reportRows)} - {'CoreHome_ThereIsNoDataForThisReport'|translate} - {/if} - - {foreach name=reportRows from=$reportRows item=row key=rowId} - - {assign var=rowMetrics value=$row->getColumns()} - {assign var=rowMetadata value=$reportRowsMetadata[$rowId]->getColumns()} - - {if $displaySiteName} - {$rowMetrics.label}:{literal} {/literal} - {/if} - - {*visits*} - {$rowMetrics.nb_visits} {'General_ColumnNbVisits'|translate} - {if $rowMetrics.visits_evolution != 0} - {literal} {/literal}({$rowMetrics.visits_evolution}%) - {/if} - - {if $rowMetrics.nb_visits != 0} - - {*actions*} - ,{literal} {/literal} - {$rowMetrics.nb_actions} {'General_ColumnNbActions'|translate} - {if $rowMetrics.actions_evolution != 0} - {literal} {/literal}({$rowMetrics.actions_evolution}%) - {/if} - - {if $isGoalPluginEnabled} - - {*goal metrics*} - {if $rowMetrics.nb_conversions != 0} - - ,{literal} {/literal} - {'Goals_ColumnRevenue'|translate}:{literal} {/literal}{$rowMetrics.revenue} - {if $rowMetrics.revenue_evolution != 0} - {literal} {/literal}({$rowMetrics.revenue_evolution}%) - {/if} - - ,{literal} {/literal} - {$rowMetrics.nb_conversions} {'Goals_GoalConversions'|translate} - {if $rowMetrics.nb_conversions_evolution != 0} - {literal} {/literal}({$rowMetrics.nb_conversions_evolution}%) - {/if} - {/if} - - {*eCommerce metrics*} - {if $siteHasECommerce[$rowMetadata.idsite]} - - ,{literal} {/literal} - {'General_ProductRevenue'|translate}:{literal} {/literal}{$rowMetrics.ecommerce_revenue} - {if $rowMetrics.ecommerce_revenue_evolution != 0} - {literal} {/literal}({$rowMetrics.ecommerce_revenue_evolution}%) - {/if} - - ,{literal} {/literal} - {$rowMetrics.orders} {'General_EcommerceOrders'|translate} - {if $rowMetrics.orders_evolution != 0} - {literal} {/literal}({$rowMetrics.orders_evolution}%) - {/if} - {/if} - {/if} - - {/if} - - {if !$smarty.foreach.reportRows.last}.{literal} {/literal}{/if} - {/foreach} + {$prettyDate}.{literal} {/literal} + + {if empty($reportRows)} + {'CoreHome_ThereIsNoDataForThisReport'|translate} + {/if} + + {foreach name=reportRows from=$reportRows item=row key=rowId} + + {assign var=rowMetrics value=$row->getColumns()} + {assign var=rowMetadata value=$reportRowsMetadata[$rowId]->getColumns()} + + {if $displaySiteName} + {$rowMetrics.label}:{literal} {/literal} + {/if} + + {*visits*} + {$rowMetrics.nb_visits} {'General_ColumnNbVisits'|translate} + {if $rowMetrics.visits_evolution != 0} + {literal} {/literal}({$rowMetrics.visits_evolution}%) + {/if} + + {if $rowMetrics.nb_visits != 0} + + {*actions*} + ,{literal} {/literal} + {$rowMetrics.nb_actions} {'General_ColumnNbActions'|translate} + {if $rowMetrics.actions_evolution != 0} + {literal} {/literal}({$rowMetrics.actions_evolution}%) + {/if} + + {if $isGoalPluginEnabled} + + {*goal metrics*} + {if $rowMetrics.nb_conversions != 0} + + ,{literal} {/literal} + {'Goals_ColumnRevenue'|translate}:{literal} {/literal}{$rowMetrics.revenue} + {if $rowMetrics.revenue_evolution != 0} + {literal} {/literal}({$rowMetrics.revenue_evolution}%) + {/if} + + ,{literal} {/literal} + {$rowMetrics.nb_conversions} {'Goals_GoalConversions'|translate} + {if $rowMetrics.nb_conversions_evolution != 0} + {literal} {/literal}({$rowMetrics.nb_conversions_evolution}%) + {/if} + {/if} + + {*eCommerce metrics*} + {if $siteHasECommerce[$rowMetadata.idsite]} + + ,{literal} {/literal} + {'General_ProductRevenue'|translate}:{literal} {/literal}{$rowMetrics.ecommerce_revenue} + {if $rowMetrics.ecommerce_revenue_evolution != 0} + {literal} {/literal}({$rowMetrics.ecommerce_revenue_evolution}%) + {/if} + + ,{literal} {/literal} + {$rowMetrics.orders} {'General_EcommerceOrders'|translate} + {if $rowMetrics.orders_evolution != 0} + {literal} {/literal}({$rowMetrics.orders_evolution}%) + {/if} + {/if} + {/if} + + {/if} + + {if !$smarty.foreach.reportRows.last}.{literal} {/literal}{/if} + {/foreach} {/strip} diff --git a/plugins/MobileMessaging/templates/Settings.tpl b/plugins/MobileMessaging/templates/Settings.tpl index 77809a0186..547e637eb8 100644 --- a/plugins/MobileMessaging/templates/Settings.tpl +++ b/plugins/MobileMessaging/templates/Settings.tpl @@ -2,194 +2,201 @@ {loadJavascriptTranslations plugins='MobileMessaging'} {literal} - + {/literal} {if $accountManagedByCurrentUser} -

{'MobileMessaging_Settings_SMSAPIAccount'|translate}

- {if $credentialSupplied} - {'MobileMessaging_Settings_CredentialProvided'|translate:$provider} - {$creditLeft} -
- {'MobileMessaging_Settings_UpdateOrDeleteAccount'|translate:"":"":"":""} - {else} - {'MobileMessaging_Settings_PleaseSignUp'|translate} - {/if} - -
-
- {'MobileMessaging_Settings_SMSProvider'|translate} - - - {'MobileMessaging_Settings_APIKey'|translate} - - - - - {foreach from=$smsProviders key=smsProvider item=description} -
- {$description} -
- {/foreach} - -
+

{'MobileMessaging_Settings_SMSAPIAccount'|translate}

+ {if $credentialSupplied} + {'MobileMessaging_Settings_CredentialProvided'|translate:$provider} + {$creditLeft} +
+ {'MobileMessaging_Settings_UpdateOrDeleteAccount'|translate:"":"":"":""} + {else} + {'MobileMessaging_Settings_PleaseSignUp'|translate} + {/if} +
+
+ {'MobileMessaging_Settings_SMSProvider'|translate} + + + {'MobileMessaging_Settings_APIKey'|translate} + + + + + {foreach from=$smsProviders key=smsProvider item=description} +
+ {$description} +
+ {/foreach} + +
{/if} {ajaxErrorDiv id=ajaxErrorMobileMessagingSettings}

{'MobileMessaging_Settings_PhoneNumbers'|translate}

{if !$credentialSupplied} - {if $accountManagedByCurrentUser} - {'MobileMessaging_Settings_CredentialNotProvided'|translate} - {else} - {'MobileMessaging_Settings_CredentialNotProvidedByAdmin'|translate} - {/if} + {if $accountManagedByCurrentUser} + {'MobileMessaging_Settings_CredentialNotProvided'|translate} + {else} + {'MobileMessaging_Settings_CredentialNotProvidedByAdmin'|translate} + {/if} {else} - {'MobileMessaging_Settings_PhoneNumbers_Help'|translate}

- - - - + + + + + + +
- {'MobileMessaging_Settings_PhoneNumbers_Add'|translate}

+ {'MobileMessaging_Settings_PhoneNumbers_Help'|translate} +
+
+ + + + - - - -
+ {'MobileMessaging_Settings_PhoneNumbers_Add'|translate}

- - +   - - - -
+ + +   + + + +
{'MobileMessaging_Settings_CountryCode'|translate} {'MobileMessaging_Settings_PhoneNumber'|translate} -

- - {'MobileMessaging_Settings_PhoneNumbers_CountryCode_Help'|translate} - - - -
- {$strHelpAddPhone|inlineHelp} - -
- - {if $phoneNumbers|@count gt 0} -
-
- {'MobileMessaging_Settings_ManagePhoneNumbers'|translate}

- {/if} - - {ajaxErrorDiv id=invalidVerificationCodeAjaxError} - - - - - -
    - {foreach from=$phoneNumbers key=phoneNumber item=validated} -
  • - {$phoneNumber} - {if !$validated} - - - {/if} - - {if !$validated} -
    - {'MobileMessaging_Settings_VerificationCodeJustSent'|translate} - {/if} -
    -
    -
  • - {/foreach} -
- -
+

+ + {'MobileMessaging_Settings_PhoneNumbers_CountryCode_Help'|translate} + + + +
+ {$strHelpAddPhone|inlineHelp} + +
+ + {if $phoneNumbers|@count gt 0} +
+
+ {'MobileMessaging_Settings_ManagePhoneNumbers'|translate} +
+
+ {/if} + + {ajaxErrorDiv id=invalidVerificationCodeAjaxError} + + + + + +
    + {foreach from=$phoneNumbers key=phoneNumber item=validated} +
  • + {$phoneNumber} + {if !$validated} + + + {/if} + + {if !$validated} +
    + {'MobileMessaging_Settings_VerificationCodeJustSent'|translate} + {/if} +
    +
    +
  • + {/foreach} +
+ +
{/if} {if $isSuperUser} -

{'MobileMessaging_Settings_SuperAdmin'|translate}

- - - - - -
{'MobileMessaging_Settings_LetUsersManageAPICredential'|translate} -
- -
-
- - -
-
+

{'MobileMessaging_Settings_SuperAdmin'|translate}

+ + + + +
{'MobileMessaging_Settings_LetUsersManageAPICredential'|translate} +
+ +
+
+ + +
+
{/if} {ajaxLoadingDiv id=ajaxLoadingMobileMessagingSettings} @@ -197,8 +204,8 @@ {include file='CoreAdminHome/templates/footer.tpl'}
-

{'MobileMessaging_Settings_DeleteAccountConfirm'|translate}

- - +

{'MobileMessaging_Settings_DeleteAccountConfirm'|translate}

+ +
diff --git a/plugins/MultiSites/API.php b/plugins/MultiSites/API.php index 3f4176bbc0..2277291598 100755 --- a/plugins/MultiSites/API.php +++ b/plugins/MultiSites/API.php @@ -1,10 +1,10 @@ array ( - self::METRIC_TRANSLATION_KEY => 'General_ColumnNbVisits', - self::METRIC_EVOLUTION_COL_NAME_KEY => 'visits_evolution', - self::METRIC_RECORD_NAME_KEY => self::NB_VISITS_METRIC, - self::METRIC_IS_ECOMMERCE_KEY => false, - ), - self::NB_ACTIONS_METRIC => array ( - self::METRIC_TRANSLATION_KEY => 'General_ColumnNbActions', - self::METRIC_EVOLUTION_COL_NAME_KEY => 'actions_evolution', - self::METRIC_RECORD_NAME_KEY => self::NB_ACTIONS_METRIC, - self::METRIC_IS_ECOMMERCE_KEY => false, - ), - self::NB_PAGEVIEWS_LABEL => array ( - self::METRIC_TRANSLATION_KEY => 'General_ColumnPageviews', - self::METRIC_EVOLUTION_COL_NAME_KEY => 'pageviews_evolution', - self::METRIC_RECORD_NAME_KEY => self::NB_PAGEVIEWS_METRIC, - self::METRIC_IS_ECOMMERCE_KEY => false, - ) - ); - - /** - * The singleton instance of this class. - */ - static private $instance = null; - - /** - * Returns the singleton instance of this class. The instance is created - * if it hasn't been already. - * - * @return Piwik_MultiSites_API - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - - return self::$instance; - } - - /** - * Returns a report displaying the total visits, actions and revenue, as - * well as the evolution of these values, of all existing sites over a - * specified period of time. - * - * If the specified period is not a 'range', this function will calculcate - * evolution metrics. Evolution metrics are metrics that display the - * percent increase/decrease of another metric since the last period. - * - * This function will merge the result of the archive query so each - * row in the result DataTable will correspond to the metrics of a single - * site. If a date range is specified, the result will be a - * DataTable_Array, but it will still be merged. - * - * @param string $period The period type to get data for. - * @param string $date The date(s) to get data for. - * @param bool|string $segment The segments to get data for. - * @param bool|string $_restrictSitesToLogin Hack used to enforce we restrict the returned data to the specified username - * Only used when a scheduled task is running - * @param bool|string $enhanced When true, return additional goal & ecommerce metrics - * @param bool|string $pattern If specified, only the website which names (or site ID) match the pattern will be returned using SitesManager.getPatternMatchSites - * @return Piwik_DataTable - */ - public function getAll($period, $date, $segment = false, $_restrictSitesToLogin = false, $enhanced = false, $pattern = false) - { - Piwik::checkUserHasSomeViewAccess(); - - $idSites = $this->getSitesIdFromPattern($pattern); - - return $this->buildDataTable( - $idSites, - $period, - $date, - $segment, - $_restrictSitesToLogin, - $enhanced, - $multipleWebsitesRequested = true - ); - } - - /** - * Fetches the list of sites which names match the string pattern - * - * @param $pattern - * @return array|string - */ - private function getSitesIdFromPattern($pattern) - { - $idSites = 'all'; - if (!empty($pattern)) { - $sites = Piwik_API_Request::processRequest('SitesManager.getPatternMatchSites', - array('pattern' => $pattern, - // added because caller could overwrite these - 'serialize' => 0, - 'format' => 'original')); - if (!empty($sites)) { - $idSites = array(); - foreach ($sites as $site) { - $idSites[] = $site['idsite']; - } - } - } - return $idSites; - } - - /** - * Same as getAll but for a unique Piwik site - * @see Piwik_MultiSites_API::getAll() - * - * @param int $idSite Id of the Piwik site - * @param string $period The period type to get data for. - * @param string $date The date(s) to get data for. - * @param bool|string $segment The segments to get data for. - * @param bool|string $_restrictSitesToLogin Hack used to enforce we restrict the returned data to the specified username - * Only used when a scheduled task is running - * @param bool|string $enhanced When true, return additional goal & ecommerce metrics - * @return Piwik_DataTable - */ - public function getOne($idSite, $period, $date, $segment = false, $_restrictSitesToLogin = false, $enhanced = false) - { - Piwik::checkUserHasViewAccess($idSite); - return $this->buildDataTable( - $idSite, - $period, - $date, - $segment, - $_restrictSitesToLogin, - $enhanced, - $multipleWebsitesRequested = false - ); - } - - private function buildDataTable($sites, $period, $date, $segment, $_restrictSitesToLogin, $enhanced, $multipleWebsitesRequested) - { - $allWebsitesRequested = ($sites == 'all'); - if($allWebsitesRequested) - { - if (Piwik::isUserIsSuperUser() - // Hack: when this API function is called as a Scheduled Task, Super User status is enforced. - // This means this function would return ALL websites in all cases. - // Instead, we make sure that only the right set of data is returned - && !Piwik_TaskScheduler::isTaskBeingExecuted()) - { - Piwik_Site::setSites( - Piwik_SitesManager_API::getInstance()->getAllSites() - ); - } - else - { - Piwik_Site::setSitesFromArray( - Piwik_SitesManager_API::getInstance()->getSitesWithAtLeastViewAccess($limit = false, $_restrictSitesToLogin) - ); - } - } - - // build the archive type used to query archive data - $archive = Piwik_Archive::build( - $sites, - $period, - $date, - $segment, - $_restrictSitesToLogin - ); - - // determine what data will be displayed - $fieldsToGet = array(); - $columnNameRewrites = array(); - $apiECommerceMetrics = array(); - $apiMetrics = Piwik_MultiSites_API::getApiMetrics($enhanced); - foreach($apiMetrics as $metricName => $metricSettings) - { - $fieldsToGet[] = $metricSettings[self::METRIC_RECORD_NAME_KEY]; - $columnNameRewrites[$metricSettings[self::METRIC_RECORD_NAME_KEY]] = $metricName; - - if($metricSettings[self::METRIC_IS_ECOMMERCE_KEY]) - { - $apiECommerceMetrics[$metricName] = $metricSettings; - } - } - - // get the data - // $dataTable instanceOf Piwik_DataTable_Array - $dataTable = $archive->getDataTableFromNumeric($fieldsToGet); - - // get rid of the DataTable_Array that is created by the IndexedBySite archive type - if($dataTable instanceof Piwik_DataTable_Array - && $multipleWebsitesRequested) - { - $dataTable = $dataTable->mergeChildren(); - } - else - { - if(!$dataTable instanceof Piwik_DataTable_Array) - { - $firstDataTableRow = $dataTable->getFirstRow(); - $firstDataTableRow->setColumn('label', $sites); - } - } - - // calculate total visits/actions/revenue - $this->setMetricsTotalsMetadata($dataTable, $apiMetrics); - - // if the period isn't a range & a lastN/previousN date isn't used, we get the same - // data for the last period to show the evolution of visits/actions/revenue - list($strLastDate, $lastPeriod) = Piwik_Period_Range::getLastDate($date, $period); - if ($strLastDate !== false) - { - if ($lastPeriod !== false) - { - // NOTE: no easy way to set last period date metadata when range of dates is requested. - // will be easier if DataTable_Array::metadata is removed, and metadata that is - // put there is put directly in Piwik_DataTable::metadata. - $dataTable->setMetadata(self::getLastPeriodMetadataName('date'), $lastPeriod); - } - - $pastArchive = Piwik_Archive::build('all', $period, $strLastDate, $segment, $_restrictSitesToLogin); - $pastData = $pastArchive->getDataTableFromNumeric($fieldsToGet); - - $pastData = $pastData->mergeChildren(); - - // use past data to calculate evolution percentages - $this->calculateEvolutionPercentages($dataTable, $pastData, $apiMetrics); - - $this->setPastDataMetadata($dataTable, $pastData, $apiMetrics); - } - - // remove eCommerce related metrics on non eCommerce Piwik sites - // note: this is not optimal in terms of performance: those metrics should not be retrieved in the first place - if($enhanced) - { - // $dataTableRows instanceOf Piwik_DataTable_Row[] - $dataTableRows = $dataTable->getRows(); - - foreach($dataTableRows as $dataTableRow) - { - $siteId = $dataTableRow->getColumn('label'); - if(!Piwik_Site::isEcommerceEnabledFor($siteId)) - { - foreach($apiECommerceMetrics as $metricSettings) - { - $dataTableRow->deleteColumn($metricSettings[self::METRIC_RECORD_NAME_KEY]); - $dataTableRow->deleteColumn($metricSettings[self::METRIC_EVOLUTION_COL_NAME_KEY]); - } - } - } - } - - // move the site id to a metadata column - $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'idsite')); - - // set the label of each row to the site name - if($multipleWebsitesRequested) - { - $getNameFor = array('Piwik_Site', 'getNameFor'); - $dataTable->filter('ColumnCallbackReplace', array('label', $getNameFor)); - } - else - { - $dataTable->filter('ColumnDelete', array('label')); - } - - // replace record names with user friendly metric names - $dataTable->filter('ReplaceColumnNames', array($columnNameRewrites)); - - // Ensures data set sorted, for Metadata output - $dataTable->filter('Sort', array(self::NB_VISITS_METRIC, 'desc', $naturalSort = false)); - - // filter rows without visits - // note: if only one website is queried and there are no visits, we can not remove the row otherwise Piwik_API_ResponseBuilder throws 'Call to a member function getColumns() on a non-object' - if($multipleWebsitesRequested) - { - $dataTable->filter( - 'ColumnCallbackDeleteRow', - array( - self::NB_VISITS_METRIC, - create_function ( '$value', 'return $value != 0;') - ) - ); - } - - return $dataTable; - } - - /** - * Performs a binary filter of two - * DataTables in order to correctly calculate evolution metrics. - * - * @param Piwik_DataTable|Piwik_DataTable_Array $currentData - * @param Piwik_DataTable|Piwik_DataTable_Array $pastData - * @param array $fields The array of string fields to calculate evolution - * metrics for. - */ - private function calculateEvolutionPercentages($currentData, $pastData, $apiMetrics) - { - if ($currentData instanceof Piwik_DataTable_Array) - { - $pastArray = $pastData->getArray(); - foreach ($currentData->getArray() as $subTable) - { - $this->calculateEvolutionPercentages($subTable, current($pastArray), $apiMetrics); - next($pastArray); - } - } - else - { - foreach ($apiMetrics as $metricSettings) - { - $currentData->filter( - 'CalculateEvolutionFilter', - array( - $pastData, - $metricSettings[self::METRIC_EVOLUTION_COL_NAME_KEY], - $metricSettings[self::METRIC_RECORD_NAME_KEY], - $quotientPrecision = 1) - ); - } - } - } - - /** - * Sets the total visits, actions & revenue for a DataTable returned by - * $this->buildDataTable. - * - * @param Piwik_DataTable $dataTable - * @param array $apiMetrics Metrics info. - * @return array Array of three values: total visits, total actions, total revenue - */ - private function setMetricsTotalsMetadata( $dataTable, $apiMetrics ) - { - if ($dataTable instanceof Piwik_DataTable_Array) - { - foreach ($dataTable->getArray() as $table) - { - $this->setMetricsTotalsMetadata($table, $apiMetrics); - } - } - else - { - $revenueMetric = ''; - if (Piwik_Common::isGoalPluginEnabled()) - { - $revenueMetric = Piwik_Goals::getRecordName(self::GOAL_REVENUE_METRIC); - } - - $totals = array(); - foreach ($apiMetrics as $label => $metricInfo) - { - $totalMetadataName = self::getTotalMetadataName($label); - $totals[$totalMetadataName] = 0; - } - - foreach ($dataTable->getRows() as $row) - { - foreach ($apiMetrics as $label => $metricInfo) - { - $totalMetadataName = self::getTotalMetadataName($label); - $totals[$totalMetadataName] += $row->getColumn($metricInfo[self::METRIC_RECORD_NAME_KEY]); - } - } - - foreach ($totals as $name => $value) - { - $dataTable->setMetadata($name, $value); - } - } - } - - /** - * Sets the total evolution metadata for a datatable returned by $this->buildDataTable - * given data for the last period. - * - * @param Piwik_DataTable $dataTable - * @param Piwik_DataTable $pastData - * @param array $apiMetrics Metrics info. - */ - private function setPastDataMetadata( $dataTable, $pastData, $apiMetrics ) - { - if ($dataTable instanceof Piwik_DataTable_Array) - { - $pastArray = $pastData->getArray(); - foreach ($dataTable->getArray() as $subTable) - { - $this->setPastDataMetadata($subTable, current($pastArray), $apiMetrics); - next($pastArray); - } - } - else - { - // calculate total visits/actions/revenue for past data - $this->setMetricsTotalsMetadata($pastData, $apiMetrics); - - foreach ($apiMetrics as $label => $metricInfo) - { - // get the names of metadata to set - $totalMetadataName = self::getTotalMetadataName($label); - $lastPeriodTotalMetadataName = self::getLastPeriodMetadataName($totalMetadataName); - $totalEvolutionMetadataName = - self::getTotalMetadataName($metricInfo[self::METRIC_EVOLUTION_COL_NAME_KEY]); - - // set last period total - $pastTotal = $pastData->getMetadata($totalMetadataName); - $dataTable->setMetadata($lastPeriodTotalMetadataName, $pastTotal); - - // calculate & set evolution - $currentTotal = $dataTable->getMetadata($totalMetadataName); - $evolution = Piwik_DataTable_Filter_CalculateEvolutionFilter::calculate($currentTotal, $pastTotal); - $dataTable->setMetadata($totalEvolutionMetadataName, $evolution); - } - } - } - - /** - * @ignore - */ - public static function getApiMetrics($enhanced) - { - $metrics = self::$baseMetrics; - if (Piwik_Common::isGoalPluginEnabled()) - { - // goal revenue metric - $metrics[self::GOAL_REVENUE_METRIC] = array( - self::METRIC_TRANSLATION_KEY => 'Goals_ColumnRevenue', - self::METRIC_EVOLUTION_COL_NAME_KEY => self::GOAL_REVENUE_METRIC . '_evolution', - self::METRIC_RECORD_NAME_KEY => Piwik_Goals::getRecordName(self::GOAL_REVENUE_METRIC), - self::METRIC_IS_ECOMMERCE_KEY => false, - ); - - if($enhanced) - { - // number of goal conversions metric - $metrics[self::GOAL_CONVERSION_METRIC] = array( - self::METRIC_TRANSLATION_KEY => 'Goals_ColumnConversions', - self::METRIC_EVOLUTION_COL_NAME_KEY => self::GOAL_CONVERSION_METRIC . '_evolution', - self::METRIC_RECORD_NAME_KEY => Piwik_Goals::getRecordName(self::GOAL_CONVERSION_METRIC), - self::METRIC_IS_ECOMMERCE_KEY => false, - ); - - // number of orders - $metrics[self::ECOMMERCE_ORDERS_METRIC] = array( - self::METRIC_TRANSLATION_KEY => 'General_EcommerceOrders', - self::METRIC_EVOLUTION_COL_NAME_KEY => self::ECOMMERCE_ORDERS_METRIC . '_evolution', - self::METRIC_RECORD_NAME_KEY => Piwik_Goals::getRecordName(self::GOAL_CONVERSION_METRIC ,0), - self::METRIC_IS_ECOMMERCE_KEY => true, - ); - - // eCommerce revenue - $metrics[self::ECOMMERCE_REVENUE_METRIC] = array( - self::METRIC_TRANSLATION_KEY => 'General_ProductRevenue', - self::METRIC_EVOLUTION_COL_NAME_KEY => self::ECOMMERCE_REVENUE_METRIC . '_evolution', - self::METRIC_RECORD_NAME_KEY => Piwik_Goals::getRecordName(self::GOAL_REVENUE_METRIC ,0), - self::METRIC_IS_ECOMMERCE_KEY => true, - ); - } - } - - return $metrics; - } - - private static function getTotalMetadataName( $name ) - { - return 'total_'.$name; - } - - private static function getLastPeriodMetadataName( $name ) - { - return 'last_period_'.$name; - } + const METRIC_TRANSLATION_KEY = 'translation'; + const METRIC_EVOLUTION_COL_NAME_KEY = 'evolution_column_name'; + const METRIC_RECORD_NAME_KEY = 'record_name'; + const METRIC_IS_ECOMMERCE_KEY = 'is_ecommerce'; + + const NB_VISITS_METRIC = 'nb_visits'; + const NB_ACTIONS_METRIC = 'nb_actions'; + const NB_PAGEVIEWS_LABEL = 'nb_pageviews'; + const NB_PAGEVIEWS_METRIC = 'Actions_nb_pageviews'; + const GOAL_REVENUE_METRIC = 'revenue'; + const GOAL_CONVERSION_METRIC = 'nb_conversions'; + const ECOMMERCE_ORDERS_METRIC = 'orders'; + const ECOMMERCE_REVENUE_METRIC = 'ecommerce_revenue'; + + static private $baseMetrics = array( + self::NB_VISITS_METRIC => array( + self::METRIC_TRANSLATION_KEY => 'General_ColumnNbVisits', + self::METRIC_EVOLUTION_COL_NAME_KEY => 'visits_evolution', + self::METRIC_RECORD_NAME_KEY => self::NB_VISITS_METRIC, + self::METRIC_IS_ECOMMERCE_KEY => false, + ), + self::NB_ACTIONS_METRIC => array( + self::METRIC_TRANSLATION_KEY => 'General_ColumnNbActions', + self::METRIC_EVOLUTION_COL_NAME_KEY => 'actions_evolution', + self::METRIC_RECORD_NAME_KEY => self::NB_ACTIONS_METRIC, + self::METRIC_IS_ECOMMERCE_KEY => false, + ), + self::NB_PAGEVIEWS_LABEL => array( + self::METRIC_TRANSLATION_KEY => 'General_ColumnPageviews', + self::METRIC_EVOLUTION_COL_NAME_KEY => 'pageviews_evolution', + self::METRIC_RECORD_NAME_KEY => self::NB_PAGEVIEWS_METRIC, + self::METRIC_IS_ECOMMERCE_KEY => false, + ) + ); + + /** + * The singleton instance of this class. + */ + static private $instance = null; + + /** + * Returns the singleton instance of this class. The instance is created + * if it hasn't been already. + * + * @return Piwik_MultiSites_API + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + + return self::$instance; + } + + /** + * Returns a report displaying the total visits, actions and revenue, as + * well as the evolution of these values, of all existing sites over a + * specified period of time. + * + * If the specified period is not a 'range', this function will calculcate + * evolution metrics. Evolution metrics are metrics that display the + * percent increase/decrease of another metric since the last period. + * + * This function will merge the result of the archive query so each + * row in the result DataTable will correspond to the metrics of a single + * site. If a date range is specified, the result will be a + * DataTable_Array, but it will still be merged. + * + * @param string $period The period type to get data for. + * @param string $date The date(s) to get data for. + * @param bool|string $segment The segments to get data for. + * @param bool|string $_restrictSitesToLogin Hack used to enforce we restrict the returned data to the specified username + * Only used when a scheduled task is running + * @param bool|string $enhanced When true, return additional goal & ecommerce metrics + * @param bool|string $pattern If specified, only the website which names (or site ID) match the pattern will be returned using SitesManager.getPatternMatchSites + * @return Piwik_DataTable + */ + public function getAll($period, $date, $segment = false, $_restrictSitesToLogin = false, $enhanced = false, $pattern = false) + { + Piwik::checkUserHasSomeViewAccess(); + + $idSites = $this->getSitesIdFromPattern($pattern); + + return $this->buildDataTable( + $idSites, + $period, + $date, + $segment, + $_restrictSitesToLogin, + $enhanced, + $multipleWebsitesRequested = true + ); + } + + /** + * Fetches the list of sites which names match the string pattern + * + * @param $pattern + * @return array|string + */ + private function getSitesIdFromPattern($pattern) + { + $idSites = 'all'; + if (!empty($pattern)) { + $sites = Piwik_API_Request::processRequest('SitesManager.getPatternMatchSites', + array('pattern' => $pattern, + // added because caller could overwrite these + 'serialize' => 0, + 'format' => 'original')); + if (!empty($sites)) { + $idSites = array(); + foreach ($sites as $site) { + $idSites[] = $site['idsite']; + } + } + } + return $idSites; + } + + /** + * Same as getAll but for a unique Piwik site + * @see Piwik_MultiSites_API::getAll() + * + * @param int $idSite Id of the Piwik site + * @param string $period The period type to get data for. + * @param string $date The date(s) to get data for. + * @param bool|string $segment The segments to get data for. + * @param bool|string $_restrictSitesToLogin Hack used to enforce we restrict the returned data to the specified username + * Only used when a scheduled task is running + * @param bool|string $enhanced When true, return additional goal & ecommerce metrics + * @return Piwik_DataTable + */ + public function getOne($idSite, $period, $date, $segment = false, $_restrictSitesToLogin = false, $enhanced = false) + { + Piwik::checkUserHasViewAccess($idSite); + return $this->buildDataTable( + $idSite, + $period, + $date, + $segment, + $_restrictSitesToLogin, + $enhanced, + $multipleWebsitesRequested = false + ); + } + + private function buildDataTable($sites, $period, $date, $segment, $_restrictSitesToLogin, $enhanced, $multipleWebsitesRequested) + { + $allWebsitesRequested = ($sites == 'all'); + if ($allWebsitesRequested) { + if (Piwik::isUserIsSuperUser() + // Hack: when this API function is called as a Scheduled Task, Super User status is enforced. + // This means this function would return ALL websites in all cases. + // Instead, we make sure that only the right set of data is returned + && !Piwik_TaskScheduler::isTaskBeingExecuted() + ) { + Piwik_Site::setSites( + Piwik_SitesManager_API::getInstance()->getAllSites() + ); + } else { + Piwik_Site::setSitesFromArray( + Piwik_SitesManager_API::getInstance()->getSitesWithAtLeastViewAccess($limit = false, $_restrictSitesToLogin) + ); + } + } + + // build the archive type used to query archive data + $archive = Piwik_Archive::build( + $sites, + $period, + $date, + $segment, + $_restrictSitesToLogin + ); + + // determine what data will be displayed + $fieldsToGet = array(); + $columnNameRewrites = array(); + $apiECommerceMetrics = array(); + $apiMetrics = Piwik_MultiSites_API::getApiMetrics($enhanced); + foreach ($apiMetrics as $metricName => $metricSettings) { + $fieldsToGet[] = $metricSettings[self::METRIC_RECORD_NAME_KEY]; + $columnNameRewrites[$metricSettings[self::METRIC_RECORD_NAME_KEY]] = $metricName; + + if ($metricSettings[self::METRIC_IS_ECOMMERCE_KEY]) { + $apiECommerceMetrics[$metricName] = $metricSettings; + } + } + + // get the data + // $dataTable instanceOf Piwik_DataTable_Array + $dataTable = $archive->getDataTableFromNumeric($fieldsToGet); + + // get rid of the DataTable_Array that is created by the IndexedBySite archive type + if ($dataTable instanceof Piwik_DataTable_Array + && $multipleWebsitesRequested + ) { + $dataTable = $dataTable->mergeChildren(); + } else { + if (!$dataTable instanceof Piwik_DataTable_Array) { + $firstDataTableRow = $dataTable->getFirstRow(); + $firstDataTableRow->setColumn('label', $sites); + } + } + + // calculate total visits/actions/revenue + $this->setMetricsTotalsMetadata($dataTable, $apiMetrics); + + // if the period isn't a range & a lastN/previousN date isn't used, we get the same + // data for the last period to show the evolution of visits/actions/revenue + list($strLastDate, $lastPeriod) = Piwik_Period_Range::getLastDate($date, $period); + if ($strLastDate !== false) { + if ($lastPeriod !== false) { + // NOTE: no easy way to set last period date metadata when range of dates is requested. + // will be easier if DataTable_Array::metadata is removed, and metadata that is + // put there is put directly in Piwik_DataTable::metadata. + $dataTable->setMetadata(self::getLastPeriodMetadataName('date'), $lastPeriod); + } + + $pastArchive = Piwik_Archive::build('all', $period, $strLastDate, $segment, $_restrictSitesToLogin); + $pastData = $pastArchive->getDataTableFromNumeric($fieldsToGet); + + $pastData = $pastData->mergeChildren(); + + // use past data to calculate evolution percentages + $this->calculateEvolutionPercentages($dataTable, $pastData, $apiMetrics); + + $this->setPastDataMetadata($dataTable, $pastData, $apiMetrics); + } + + // remove eCommerce related metrics on non eCommerce Piwik sites + // note: this is not optimal in terms of performance: those metrics should not be retrieved in the first place + if ($enhanced) { + // $dataTableRows instanceOf Piwik_DataTable_Row[] + $dataTableRows = $dataTable->getRows(); + + foreach ($dataTableRows as $dataTableRow) { + $siteId = $dataTableRow->getColumn('label'); + if (!Piwik_Site::isEcommerceEnabledFor($siteId)) { + foreach ($apiECommerceMetrics as $metricSettings) { + $dataTableRow->deleteColumn($metricSettings[self::METRIC_RECORD_NAME_KEY]); + $dataTableRow->deleteColumn($metricSettings[self::METRIC_EVOLUTION_COL_NAME_KEY]); + } + } + } + } + + // move the site id to a metadata column + $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'idsite')); + + // set the label of each row to the site name + if ($multipleWebsitesRequested) { + $getNameFor = array('Piwik_Site', 'getNameFor'); + $dataTable->filter('ColumnCallbackReplace', array('label', $getNameFor)); + } else { + $dataTable->filter('ColumnDelete', array('label')); + } + + // replace record names with user friendly metric names + $dataTable->filter('ReplaceColumnNames', array($columnNameRewrites)); + + // Ensures data set sorted, for Metadata output + $dataTable->filter('Sort', array(self::NB_VISITS_METRIC, 'desc', $naturalSort = false)); + + // filter rows without visits + // note: if only one website is queried and there are no visits, we can not remove the row otherwise Piwik_API_ResponseBuilder throws 'Call to a member function getColumns() on a non-object' + if ($multipleWebsitesRequested) { + $dataTable->filter( + 'ColumnCallbackDeleteRow', + array( + self::NB_VISITS_METRIC, + create_function('$value', 'return $value != 0;') + ) + ); + } + + return $dataTable; + } + + /** + * Performs a binary filter of two + * DataTables in order to correctly calculate evolution metrics. + * + * @param Piwik_DataTable|Piwik_DataTable_Array $currentData + * @param Piwik_DataTable|Piwik_DataTable_Array $pastData + * @param array $fields The array of string fields to calculate evolution + * metrics for. + */ + private function calculateEvolutionPercentages($currentData, $pastData, $apiMetrics) + { + if ($currentData instanceof Piwik_DataTable_Array) { + $pastArray = $pastData->getArray(); + foreach ($currentData->getArray() as $subTable) { + $this->calculateEvolutionPercentages($subTable, current($pastArray), $apiMetrics); + next($pastArray); + } + } else { + foreach ($apiMetrics as $metricSettings) { + $currentData->filter( + 'CalculateEvolutionFilter', + array( + $pastData, + $metricSettings[self::METRIC_EVOLUTION_COL_NAME_KEY], + $metricSettings[self::METRIC_RECORD_NAME_KEY], + $quotientPrecision = 1) + ); + } + } + } + + /** + * Sets the total visits, actions & revenue for a DataTable returned by + * $this->buildDataTable. + * + * @param Piwik_DataTable $dataTable + * @param array $apiMetrics Metrics info. + * @return array Array of three values: total visits, total actions, total revenue + */ + private function setMetricsTotalsMetadata($dataTable, $apiMetrics) + { + if ($dataTable instanceof Piwik_DataTable_Array) { + foreach ($dataTable->getArray() as $table) { + $this->setMetricsTotalsMetadata($table, $apiMetrics); + } + } else { + $revenueMetric = ''; + if (Piwik_Common::isGoalPluginEnabled()) { + $revenueMetric = Piwik_Goals::getRecordName(self::GOAL_REVENUE_METRIC); + } + + $totals = array(); + foreach ($apiMetrics as $label => $metricInfo) { + $totalMetadataName = self::getTotalMetadataName($label); + $totals[$totalMetadataName] = 0; + } + + foreach ($dataTable->getRows() as $row) { + foreach ($apiMetrics as $label => $metricInfo) { + $totalMetadataName = self::getTotalMetadataName($label); + $totals[$totalMetadataName] += $row->getColumn($metricInfo[self::METRIC_RECORD_NAME_KEY]); + } + } + + foreach ($totals as $name => $value) { + $dataTable->setMetadata($name, $value); + } + } + } + + /** + * Sets the total evolution metadata for a datatable returned by $this->buildDataTable + * given data for the last period. + * + * @param Piwik_DataTable $dataTable + * @param Piwik_DataTable $pastData + * @param array $apiMetrics Metrics info. + */ + private function setPastDataMetadata($dataTable, $pastData, $apiMetrics) + { + if ($dataTable instanceof Piwik_DataTable_Array) { + $pastArray = $pastData->getArray(); + foreach ($dataTable->getArray() as $subTable) { + $this->setPastDataMetadata($subTable, current($pastArray), $apiMetrics); + next($pastArray); + } + } else { + // calculate total visits/actions/revenue for past data + $this->setMetricsTotalsMetadata($pastData, $apiMetrics); + + foreach ($apiMetrics as $label => $metricInfo) { + // get the names of metadata to set + $totalMetadataName = self::getTotalMetadataName($label); + $lastPeriodTotalMetadataName = self::getLastPeriodMetadataName($totalMetadataName); + $totalEvolutionMetadataName = + self::getTotalMetadataName($metricInfo[self::METRIC_EVOLUTION_COL_NAME_KEY]); + + // set last period total + $pastTotal = $pastData->getMetadata($totalMetadataName); + $dataTable->setMetadata($lastPeriodTotalMetadataName, $pastTotal); + + // calculate & set evolution + $currentTotal = $dataTable->getMetadata($totalMetadataName); + $evolution = Piwik_DataTable_Filter_CalculateEvolutionFilter::calculate($currentTotal, $pastTotal); + $dataTable->setMetadata($totalEvolutionMetadataName, $evolution); + } + } + } + + /** + * @ignore + */ + public static function getApiMetrics($enhanced) + { + $metrics = self::$baseMetrics; + if (Piwik_Common::isGoalPluginEnabled()) { + // goal revenue metric + $metrics[self::GOAL_REVENUE_METRIC] = array( + self::METRIC_TRANSLATION_KEY => 'Goals_ColumnRevenue', + self::METRIC_EVOLUTION_COL_NAME_KEY => self::GOAL_REVENUE_METRIC . '_evolution', + self::METRIC_RECORD_NAME_KEY => Piwik_Goals::getRecordName(self::GOAL_REVENUE_METRIC), + self::METRIC_IS_ECOMMERCE_KEY => false, + ); + + if ($enhanced) { + // number of goal conversions metric + $metrics[self::GOAL_CONVERSION_METRIC] = array( + self::METRIC_TRANSLATION_KEY => 'Goals_ColumnConversions', + self::METRIC_EVOLUTION_COL_NAME_KEY => self::GOAL_CONVERSION_METRIC . '_evolution', + self::METRIC_RECORD_NAME_KEY => Piwik_Goals::getRecordName(self::GOAL_CONVERSION_METRIC), + self::METRIC_IS_ECOMMERCE_KEY => false, + ); + + // number of orders + $metrics[self::ECOMMERCE_ORDERS_METRIC] = array( + self::METRIC_TRANSLATION_KEY => 'General_EcommerceOrders', + self::METRIC_EVOLUTION_COL_NAME_KEY => self::ECOMMERCE_ORDERS_METRIC . '_evolution', + self::METRIC_RECORD_NAME_KEY => Piwik_Goals::getRecordName(self::GOAL_CONVERSION_METRIC, 0), + self::METRIC_IS_ECOMMERCE_KEY => true, + ); + + // eCommerce revenue + $metrics[self::ECOMMERCE_REVENUE_METRIC] = array( + self::METRIC_TRANSLATION_KEY => 'General_ProductRevenue', + self::METRIC_EVOLUTION_COL_NAME_KEY => self::ECOMMERCE_REVENUE_METRIC . '_evolution', + self::METRIC_RECORD_NAME_KEY => Piwik_Goals::getRecordName(self::GOAL_REVENUE_METRIC, 0), + self::METRIC_IS_ECOMMERCE_KEY => true, + ); + } + } + + return $metrics; + } + + private static function getTotalMetadataName($name) + { + return 'total_' . $name; + } + + private static function getLastPeriodMetadataName($name) + { + return 'last_period_' . $name; + } } diff --git a/plugins/MultiSites/Controller.php b/plugins/MultiSites/Controller.php index 46ae9c73d9..cb63dc5746 100644 --- a/plugins/MultiSites/Controller.php +++ b/plugins/MultiSites/Controller.php @@ -1,10 +1,10 @@ limit = Piwik_Config::getInstance()->General['all_websites_website_per_page']; - } - - function index() - { - $this->getSitesInfo($isWidgetized = false); - } - - function standalone() - { - $this->getSitesInfo($isWidgetized = true); - } - - - public function getSitesInfo($isWidgetized) - { - Piwik::checkUserHasSomeViewAccess(); - $displayRevenueColumn = Piwik_Common::isGoalPluginEnabled(); - - $date = Piwik_Common::getRequestVar('date', 'today'); - $period = Piwik_Common::getRequestVar('period', 'day'); - $siteIds = Piwik_SitesManager_API::getInstance()->getSitesIdWithAtLeastViewAccess(); - list($minDate, $maxDate) = $this->getMinMaxDateAcrossWebsites($siteIds); - - // overwrites the default Date set in the parent controller - // Instead of the default current website's local date, - // we set "today" or "yesterday" based on the default Piwik timezone - $piwikDefaultTimezone = Piwik_SitesManager_API::getInstance()->getDefaultTimezone(); - if($period != 'range') - { - $date = $this->getDateParameterInTimezone($date, $piwikDefaultTimezone); - $this->setDate($date); - $date = $date->toString(); - } - $dataTable = Piwik_MultiSites_API::getInstance()->getAll($period, $date, $segment = false); - - - // put data into a form the template will understand better - $digestableData = array(); - foreach($siteIds as $idSite) - { - $isEcommerceEnabled = Piwik_Site::isEcommerceEnabledFor($idSite); - - $digestableData[$idSite] = array( - 'idsite' => $idSite, - 'main_url' => Piwik_Site::getMainUrlFor($idSite), - 'name' => Piwik_Site::getNameFor($idSite), - 'visits' => 0, - 'pageviews' => 0 - ); - - if ($period != 'range') - { - $digestableData[$idSite]['visits_evolution'] = 0; - $digestableData[$idSite]['pageviews_evolution'] = 0; - } - - if ($displayRevenueColumn) - { - $revenueDefault = $isEcommerceEnabled ? 0 : "'-'"; - - if ($period != 'range') - { - $digestableData[$idSite]['revenue_evolution'] = $revenueDefault; - } - } - } - - foreach($dataTable->getRows() as $row) - { - $idsite = (int)$row->getMetadata('idsite'); - - $site = &$digestableData[$idsite]; - - $site['visits'] = (int)$row->getColumn('nb_visits'); - $site['pageviews'] = (int)$row->getColumn('nb_pageviews'); - - if ($displayRevenueColumn) - { - if ($row->getColumn('revenue') !== false) - { - $site['revenue'] = $row->getColumn('revenue'); - } - } - - if ($period != 'range') - { - $site['visits_evolution'] = $row->getColumn('visits_evolution'); - $site['pageviews_evolution'] = $row->getColumn('pageviews_evolution'); - - if ($displayRevenueColumn) - { - $site['revenue_evolution'] = $row->getColumn('revenue_evolution'); - } - } - } - - $this->applyPrettyMoney($digestableData); - - $view = new Piwik_View("MultiSites/templates/index.tpl"); - $view->isWidgetized = $isWidgetized; - $view->sitesData = array_values($digestableData); - $view->evolutionBy = $this->evolutionBy; - $view->period = $period; - $view->page = $this->page; - $view->limit = $this->limit; - $view->orderBy = $this->orderBy; - $view->order = $this->order; - $view->totalVisits = $dataTable->getMetadata('total_nb_visits'); - $view->totalRevenue = $dataTable->getMetadata('total_revenue'); - - $view->displayRevenueColumn = $displayRevenueColumn; - $view->totalPageviews = $dataTable->getMetadata('total_nb_pageviews'); - $view->pastTotalVisits = $dataTable->getMetadata('last_period_total_nb_visits'); - $view->totalVisitsEvolution = $dataTable->getMetadata('total_visits_evolution'); - if ($view->totalVisitsEvolution > 0) - { - $view->totalVisitsEvolution = '+'.$view->totalVisitsEvolution; - } - - if ($period != 'range') - { - $lastPeriod = Piwik_Period::factory($period, $dataTable->getMetadata('last_period_date')); - $view->pastPeriodPretty = self::getCalendarPrettyDate($lastPeriod); - } - - $params = $this->getGraphParamsModified(); - $view->dateSparkline = $period == 'range' ? $date : $params['date']; - - $view->autoRefreshTodayReport = false; - // if the current date is today, or yesterday, - // in case the website is set to UTC-12), or today in UTC+14, we refresh the page every 5min - if(in_array($date, array( 'today', date('Y-m-d'), - 'yesterday', Piwik_Date::factory('yesterday')->toString('Y-m-d'), - Piwik_Date::factory('now', 'UTC+14')->toString('Y-m-d')))) - { - - $view->autoRefreshTodayReport = Piwik_Config::getInstance()->General['multisites_refresh_after_seconds']; - } - $this->setGeneralVariablesView($view); - $this->setMinDateView($minDate, $view); - $this->setMaxDateView($maxDate, $view); - $view->show_sparklines = Piwik_Config::getInstance()->General['show_multisites_sparklines']; - - echo $view->render(); - } - - /** - * The Multisites reports displays the first calendar date as the earliest day available for all websites. - * Also, today is the later "today" available across all timezones. - * @param array $siteIds Array of IDs for each site being displayed. - * @return array of two Piwik_Date instances. First is the min-date & the second - * is the max date. - */ - private function getMinMaxDateAcrossWebsites($siteIds) - { - $now = Piwik_Date::now(); - - $minDate = null; - $maxDate = $now->subDay(1)->getTimestamp(); - foreach($siteIds as $idsite) - { - // look for 'now' in the website's timezone - $timezone = Piwik_Site::getTimezoneFor($idsite); - $date = Piwik_Date::adjustForTimezone($now->getTimestamp(), $timezone); - if($date > $maxDate) - { - $maxDate = $date; - } - - // look for the absolute minimum date - $creationDate = Piwik_Site::getCreationDateFor($idsite); - $date = Piwik_Date::adjustForTimezone(strtotime($creationDate), $timezone); - if(is_null($minDate) || $date < $minDate) - { - $minDate = $date; - } - } - - return array(Piwik_Date::factory($minDate), Piwik_Date::factory($maxDate)); - } - - protected function applyPrettyMoney(&$sites) - { - foreach($sites as $idsite => &$site) - { - $revenue = "-"; - if(!empty($site['revenue'])) - { - $revenue = Piwik::getPrettyMoney($site['revenue'], $site['idsite'], $htmlAllowed = false); - } - $site['revenue'] = '"'. $revenue . '"'; - } - } - - public function getEvolutionGraph( $fetch = false, $columns = false) - { - if(empty($columns)) - { - $columns = Piwik_Common::getRequestVar('columns'); - } - $api = "API.get"; - - if($columns == 'revenue') - { - $api = "Goals.get"; - } - $view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, $api); - $view->setColumnsToDisplay($columns); - return $this->renderView($view, $fetch); - } + protected $orderBy = 'visits'; + protected $order = 'desc'; + protected $evolutionBy = 'visits'; + protected $page = 1; + protected $limit = 0; + protected $period; + protected $date; + + function __construct() + { + parent::__construct(); + + $this->limit = Piwik_Config::getInstance()->General['all_websites_website_per_page']; + } + + function index() + { + $this->getSitesInfo($isWidgetized = false); + } + + function standalone() + { + $this->getSitesInfo($isWidgetized = true); + } + + + public function getSitesInfo($isWidgetized) + { + Piwik::checkUserHasSomeViewAccess(); + $displayRevenueColumn = Piwik_Common::isGoalPluginEnabled(); + + $date = Piwik_Common::getRequestVar('date', 'today'); + $period = Piwik_Common::getRequestVar('period', 'day'); + $siteIds = Piwik_SitesManager_API::getInstance()->getSitesIdWithAtLeastViewAccess(); + list($minDate, $maxDate) = $this->getMinMaxDateAcrossWebsites($siteIds); + + // overwrites the default Date set in the parent controller + // Instead of the default current website's local date, + // we set "today" or "yesterday" based on the default Piwik timezone + $piwikDefaultTimezone = Piwik_SitesManager_API::getInstance()->getDefaultTimezone(); + if ($period != 'range') { + $date = $this->getDateParameterInTimezone($date, $piwikDefaultTimezone); + $this->setDate($date); + $date = $date->toString(); + } + $dataTable = Piwik_MultiSites_API::getInstance()->getAll($period, $date, $segment = false); + + + // put data into a form the template will understand better + $digestableData = array(); + foreach ($siteIds as $idSite) { + $isEcommerceEnabled = Piwik_Site::isEcommerceEnabledFor($idSite); + + $digestableData[$idSite] = array( + 'idsite' => $idSite, + 'main_url' => Piwik_Site::getMainUrlFor($idSite), + 'name' => Piwik_Site::getNameFor($idSite), + 'visits' => 0, + 'pageviews' => 0 + ); + + if ($period != 'range') { + $digestableData[$idSite]['visits_evolution'] = 0; + $digestableData[$idSite]['pageviews_evolution'] = 0; + } + + if ($displayRevenueColumn) { + $revenueDefault = $isEcommerceEnabled ? 0 : "'-'"; + + if ($period != 'range') { + $digestableData[$idSite]['revenue_evolution'] = $revenueDefault; + } + } + } + + foreach ($dataTable->getRows() as $row) { + $idsite = (int)$row->getMetadata('idsite'); + + $site = & $digestableData[$idsite]; + + $site['visits'] = (int)$row->getColumn('nb_visits'); + $site['pageviews'] = (int)$row->getColumn('nb_pageviews'); + + if ($displayRevenueColumn) { + if ($row->getColumn('revenue') !== false) { + $site['revenue'] = $row->getColumn('revenue'); + } + } + + if ($period != 'range') { + $site['visits_evolution'] = $row->getColumn('visits_evolution'); + $site['pageviews_evolution'] = $row->getColumn('pageviews_evolution'); + + if ($displayRevenueColumn) { + $site['revenue_evolution'] = $row->getColumn('revenue_evolution'); + } + } + } + + $this->applyPrettyMoney($digestableData); + + $view = new Piwik_View("MultiSites/templates/index.tpl"); + $view->isWidgetized = $isWidgetized; + $view->sitesData = array_values($digestableData); + $view->evolutionBy = $this->evolutionBy; + $view->period = $period; + $view->page = $this->page; + $view->limit = $this->limit; + $view->orderBy = $this->orderBy; + $view->order = $this->order; + $view->totalVisits = $dataTable->getMetadata('total_nb_visits'); + $view->totalRevenue = $dataTable->getMetadata('total_revenue'); + + $view->displayRevenueColumn = $displayRevenueColumn; + $view->totalPageviews = $dataTable->getMetadata('total_nb_pageviews'); + $view->pastTotalVisits = $dataTable->getMetadata('last_period_total_nb_visits'); + $view->totalVisitsEvolution = $dataTable->getMetadata('total_visits_evolution'); + if ($view->totalVisitsEvolution > 0) { + $view->totalVisitsEvolution = '+' . $view->totalVisitsEvolution; + } + + if ($period != 'range') { + $lastPeriod = Piwik_Period::factory($period, $dataTable->getMetadata('last_period_date')); + $view->pastPeriodPretty = self::getCalendarPrettyDate($lastPeriod); + } + + $params = $this->getGraphParamsModified(); + $view->dateSparkline = $period == 'range' ? $date : $params['date']; + + $view->autoRefreshTodayReport = false; + // if the current date is today, or yesterday, + // in case the website is set to UTC-12), or today in UTC+14, we refresh the page every 5min + if (in_array($date, array('today', date('Y-m-d'), + 'yesterday', Piwik_Date::factory('yesterday')->toString('Y-m-d'), + Piwik_Date::factory('now', 'UTC+14')->toString('Y-m-d'))) + ) { + + $view->autoRefreshTodayReport = Piwik_Config::getInstance()->General['multisites_refresh_after_seconds']; + } + $this->setGeneralVariablesView($view); + $this->setMinDateView($minDate, $view); + $this->setMaxDateView($maxDate, $view); + $view->show_sparklines = Piwik_Config::getInstance()->General['show_multisites_sparklines']; + + echo $view->render(); + } + + /** + * The Multisites reports displays the first calendar date as the earliest day available for all websites. + * Also, today is the later "today" available across all timezones. + * @param array $siteIds Array of IDs for each site being displayed. + * @return array of two Piwik_Date instances. First is the min-date & the second + * is the max date. + */ + private function getMinMaxDateAcrossWebsites($siteIds) + { + $now = Piwik_Date::now(); + + $minDate = null; + $maxDate = $now->subDay(1)->getTimestamp(); + foreach ($siteIds as $idsite) { + // look for 'now' in the website's timezone + $timezone = Piwik_Site::getTimezoneFor($idsite); + $date = Piwik_Date::adjustForTimezone($now->getTimestamp(), $timezone); + if ($date > $maxDate) { + $maxDate = $date; + } + + // look for the absolute minimum date + $creationDate = Piwik_Site::getCreationDateFor($idsite); + $date = Piwik_Date::adjustForTimezone(strtotime($creationDate), $timezone); + if (is_null($minDate) || $date < $minDate) { + $minDate = $date; + } + } + + return array(Piwik_Date::factory($minDate), Piwik_Date::factory($maxDate)); + } + + protected function applyPrettyMoney(&$sites) + { + foreach ($sites as $idsite => &$site) { + $revenue = "-"; + if (!empty($site['revenue'])) { + $revenue = Piwik::getPrettyMoney($site['revenue'], $site['idsite'], $htmlAllowed = false); + } + $site['revenue'] = '"' . $revenue . '"'; + } + } + + public function getEvolutionGraph($fetch = false, $columns = false) + { + if (empty($columns)) { + $columns = Piwik_Common::getRequestVar('columns'); + } + $api = "API.get"; + + if ($columns == 'revenue') { + $api = "Goals.get"; + } + $view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, $api); + $view->setColumnsToDisplay($columns); + return $this->renderView($view, $fetch); + } } diff --git a/plugins/MultiSites/MultiSites.php b/plugins/MultiSites/MultiSites.php index 967300f9a4..43797132c7 100644 --- a/plugins/MultiSites/MultiSites.php +++ b/plugins/MultiSites/MultiSites.php @@ -15,91 +15,90 @@ */ class Piwik_MultiSites extends Piwik_Plugin { - public function getInformation() - { - return array( - 'description' => Piwik_Translate('MultiSites_PluginDescription'), - 'author' => 'ClearCode.cc', - 'author_homepage' => "http://clearcode.cc/", - 'version' => Piwik_Version::VERSION, - ); - } + public function getInformation() + { + return array( + 'description' => Piwik_Translate('MultiSites_PluginDescription'), + 'author' => 'ClearCode.cc', + 'author_homepage' => "http://clearcode.cc/", + 'version' => Piwik_Version::VERSION, + ); + } - public function getListHooksRegistered() - { - return array( - 'AssetManager.getCssFiles' => 'getCssFiles', - 'AssetManager.getJsFiles' => 'getJsFiles', - 'TopMenu.add' => 'addTopMenu', - 'API.getReportMetadata' => 'getReportMetadata', - ); - } + public function getListHooksRegistered() + { + return array( + 'AssetManager.getCssFiles' => 'getCssFiles', + 'AssetManager.getJsFiles' => 'getJsFiles', + 'TopMenu.add' => 'addTopMenu', + 'API.getReportMetadata' => 'getReportMetadata', + ); + } - /** - * @param Piwik_Event_Notification $notification notification object - */ - public function getReportMetadata($notification) - { - $metadataMetrics = array(); - foreach(Piwik_MultiSites_API::getApiMetrics($enhanced = true) as $metricName => $metricSettings) - { - $metadataMetrics[$metricName] = - Piwik_Translate($metricSettings[Piwik_MultiSites_API::METRIC_TRANSLATION_KEY]); - $metadataMetrics[$metricSettings[Piwik_MultiSites_API::METRIC_EVOLUTION_COL_NAME_KEY]] = - Piwik_Translate($metricSettings[Piwik_MultiSites_API::METRIC_TRANSLATION_KEY]) . " " . Piwik_Translate('MultiSites_Evolution'); - } + /** + * @param Piwik_Event_Notification $notification notification object + */ + public function getReportMetadata($notification) + { + $metadataMetrics = array(); + foreach (Piwik_MultiSites_API::getApiMetrics($enhanced = true) as $metricName => $metricSettings) { + $metadataMetrics[$metricName] = + Piwik_Translate($metricSettings[Piwik_MultiSites_API::METRIC_TRANSLATION_KEY]); + $metadataMetrics[$metricSettings[Piwik_MultiSites_API::METRIC_EVOLUTION_COL_NAME_KEY]] = + Piwik_Translate($metricSettings[Piwik_MultiSites_API::METRIC_TRANSLATION_KEY]) . " " . Piwik_Translate('MultiSites_Evolution'); + } - $reports = &$notification->getNotificationObject(); + $reports = & $notification->getNotificationObject(); - $reports[] = array( - 'category' => Piwik_Translate('General_MultiSitesSummary'), - 'name' => Piwik_Translate('General_AllWebsitesDashboard'), - 'module' => 'MultiSites', - 'action' => 'getAll', - 'dimension' => Piwik_Translate('General_Website'), // re-using translation - 'metrics' => $metadataMetrics, - 'processedMetrics' => false, - 'constantRowsCount' => false, - 'order' => 5 - ); + $reports[] = array( + 'category' => Piwik_Translate('General_MultiSitesSummary'), + 'name' => Piwik_Translate('General_AllWebsitesDashboard'), + 'module' => 'MultiSites', + 'action' => 'getAll', + 'dimension' => Piwik_Translate('General_Website'), // re-using translation + 'metrics' => $metadataMetrics, + 'processedMetrics' => false, + 'constantRowsCount' => false, + 'order' => 5 + ); - $reports[] = array( - 'category' => Piwik_Translate('General_MultiSitesSummary'), - 'name' => Piwik_Translate('General_SingleWebsitesDashboard'), - 'module' => 'MultiSites', - 'action' => 'getOne', - 'dimension' => Piwik_Translate('General_Website'), // re-using translation - 'metrics' => $metadataMetrics, - 'processedMetrics' => false, - 'constantRowsCount' => false, - 'order' => 5 - ); - } + $reports[] = array( + 'category' => Piwik_Translate('General_MultiSitesSummary'), + 'name' => Piwik_Translate('General_SingleWebsitesDashboard'), + 'module' => 'MultiSites', + 'action' => 'getOne', + 'dimension' => Piwik_Translate('General_Website'), // re-using translation + 'metrics' => $metadataMetrics, + 'processedMetrics' => false, + 'constantRowsCount' => false, + 'order' => 5 + ); + } - public function addTopMenu() - { - $urlParams = array('module' => 'MultiSites', 'action' => 'index'); - $tooltip = Piwik_Translate('MultiSites_TopLinkTooltip'); - Piwik_AddTopMenu('General_MultiSitesSummary', $urlParams, true, 3, $isHTML = false, $tooltip); - } + public function addTopMenu() + { + $urlParams = array('module' => 'MultiSites', 'action' => 'index'); + $tooltip = Piwik_Translate('MultiSites_TopLinkTooltip'); + Piwik_AddTopMenu('General_MultiSitesSummary', $urlParams, true, 3, $isHTML = false, $tooltip); + } - /** - * @param Piwik_Event_Notification $notification notification object - */ - function getJsFiles( $notification ) - { - $jsFiles = &$notification->getNotificationObject(); - - $jsFiles[] = "plugins/MultiSites/templates/common.js"; - } + /** + * @param Piwik_Event_Notification $notification notification object + */ + function getJsFiles($notification) + { + $jsFiles = & $notification->getNotificationObject(); - /** - * @param Piwik_Event_Notification $notification notification object - */ - function getCssFiles( $notification ) - { - $cssFiles = &$notification->getNotificationObject(); - - $cssFiles[] = "plugins/MultiSites/templates/styles.css"; - } + $jsFiles[] = "plugins/MultiSites/templates/common.js"; + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + function getCssFiles($notification) + { + $cssFiles = & $notification->getNotificationObject(); + + $cssFiles[] = "plugins/MultiSites/templates/styles.css"; + } } diff --git a/plugins/MultiSites/templates/common.js b/plugins/MultiSites/templates/common.js index 4ef9c82002..29439401d0 100644 --- a/plugins/MultiSites/templates/common.js +++ b/plugins/MultiSites/templates/common.js @@ -5,248 +5,217 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ -function setRowData (idsite, visits, pageviews, revenue, name, url, visitsSummaryValue, pageviewsSummaryValue, revenueSummaryValue) -{ - this.idsite = idsite; - this.visits = visits; - this.revenue = revenue; - this.name = name; - this.url = url; - this.pageviews = pageviews; - this.visitsSummaryValue = parseFloat(visitsSummaryValue); - this.pageviewsSummaryValue = parseFloat(pageviewsSummaryValue); - this.revenueSummaryValue = parseFloat(revenueSummaryValue) || 0; +function setRowData(idsite, visits, pageviews, revenue, name, url, visitsSummaryValue, pageviewsSummaryValue, revenueSummaryValue) { + this.idsite = idsite; + this.visits = visits; + this.revenue = revenue; + this.name = name; + this.url = url; + this.pageviews = pageviews; + this.visitsSummaryValue = parseFloat(visitsSummaryValue); + this.pageviewsSummaryValue = parseFloat(pageviewsSummaryValue); + this.revenueSummaryValue = parseFloat(revenueSummaryValue) || 0; } -function setOrderBy(self, allSites, params, mOrderBy) -{ - if(params['mOrderBy'] == mOrderBy) { - if(params['order'] == 'desc') { - params['order'] = 'asc'; - } else { - params['order'] = 'desc'; - } - } - params['mOrderBy'] = mOrderBy; - prepareRows(allSites, params); - - $('.arrow').removeClass('multisites_desc multisites_asc'); - if($(self).attr('class') == 'evolution') - { - mOrderBy = 'evolution'; - } - $('#' + mOrderBy + ' .arrow').addClass('multisites_' + params['order']); - - return params; +function setOrderBy(self, allSites, params, mOrderBy) { + if (params['mOrderBy'] == mOrderBy) { + if (params['order'] == 'desc') { + params['order'] = 'asc'; + } else { + params['order'] = 'desc'; + } + } + params['mOrderBy'] = mOrderBy; + prepareRows(allSites, params); + + $('.arrow').removeClass('multisites_desc multisites_asc'); + if ($(self).attr('class') == 'evolution') { + mOrderBy = 'evolution'; + } + $('#' + mOrderBy + ' .arrow').addClass('multisites_' + params['order']); + + return params; } -function prepareRows(allUnsortedSites, params) -{ - var allSites; - $("#tb").find("tr").remove(); - $("#next").html(''); - $("#prev").html(''); - var mOrderBy = params['mOrderBy']; - - allSites = orderBy(allUnsortedSites, params); - - if(allSites.length > params['limit']) - { - allSites = limitBy(allSites, params); - } - - displayRows(allSites, params); - - showPagination(allUnsortedSites, params); - params['sitesVisible'] = allSites; +function prepareRows(allUnsortedSites, params) { + var allSites; + $("#tb").find("tr").remove(); + $("#next").html(''); + $("#prev").html(''); + var mOrderBy = params['mOrderBy']; + + allSites = orderBy(allUnsortedSites, params); + + if (allSites.length > params['limit']) { + allSites = limitBy(allSites, params); + } + + displayRows(allSites, params); + + showPagination(allUnsortedSites, params); + params['sitesVisible'] = allSites; } -function orderBy(allSites, params) -{ - if(params['mOrderBy'] == 'names') - { - allSites.sort(function(a,b) { - if (a['name'].toLowerCase() == b['name'].toLowerCase()) - { - return 0; - } - return (a['name'].toLowerCase() < b['name'].toLowerCase()) ? -1 : 1; - }); - } - else if(params['mOrderBy'] == 'visits') - { - allSites.sort(function(a,b) { - if (a['visits'] == b['visits']) { - return 0; - } - return (a['visits'] < b['visits']) ? -1 : 1; - }); - } - else if(params['mOrderBy'] == 'pageviews') - { - allSites.sort(function (a,b) { - if (a['pageviews'] == b['pageviews']) { - return 0; - } - return (a['pageviews'] < b['pageviews']) ? -1 : 1; - }); - } - else if(params['mOrderBy'] == 'revenue') - { - allSites.sort(function (a,b) { - var lhs = parseFloat(a['revenue'].replace(/[^0-9\.]+/g,"")) || 0, - rhs = parseFloat(b['revenue'].replace(/[^0-9\.]+/g,"")) || 0; - - return lhs === rhs ? 0 : ((lhs < rhs) ? -1 : 1); - }); - } - else if(params['mOrderBy'] == 'revenueSummary') - { - allSites.sort(function (a,b) { - if (a['revenueSummaryValue'] == b['revenueSummaryValue']) { - return 0; - } - return (a['revenueSummaryValue'] - b['revenueSummaryValue'] <= 0.01) ? -1 : 1; - }); - } - else if(params['mOrderBy'] == 'pageviewsSummary') - { - allSites.sort(function (a,b) { - if (a['pageviewsSummaryValue'] == b['pageviewsSummaryValue']) { - return 0; - } - return (a['pageviewsSummaryValue'] - b['pageviewsSummaryValue'] <= 0.01) ? -1 : 1; - }); - } - else if(params['mOrderBy'] == 'visitsSummary') - { - allSites.sort(function (a,b) { - if (a['visitsSummaryValue'] == b['visitsSummaryValue']) { - return 0; - } - return (a['visitsSummaryValue'] - b['visitsSummaryValue'] <= 0.01) ? -1 : 1; - }); - } - - if(params['order'] == 'desc') - { - allSites.reverse(); - } - return allSites; +function orderBy(allSites, params) { + if (params['mOrderBy'] == 'names') { + allSites.sort(function (a, b) { + if (a['name'].toLowerCase() == b['name'].toLowerCase()) { + return 0; + } + return (a['name'].toLowerCase() < b['name'].toLowerCase()) ? -1 : 1; + }); + } + else if (params['mOrderBy'] == 'visits') { + allSites.sort(function (a, b) { + if (a['visits'] == b['visits']) { + return 0; + } + return (a['visits'] < b['visits']) ? -1 : 1; + }); + } + else if (params['mOrderBy'] == 'pageviews') { + allSites.sort(function (a, b) { + if (a['pageviews'] == b['pageviews']) { + return 0; + } + return (a['pageviews'] < b['pageviews']) ? -1 : 1; + }); + } + else if (params['mOrderBy'] == 'revenue') { + allSites.sort(function (a, b) { + var lhs = parseFloat(a['revenue'].replace(/[^0-9\.]+/g, "")) || 0, + rhs = parseFloat(b['revenue'].replace(/[^0-9\.]+/g, "")) || 0; + + return lhs === rhs ? 0 : ((lhs < rhs) ? -1 : 1); + }); + } + else if (params['mOrderBy'] == 'revenueSummary') { + allSites.sort(function (a, b) { + if (a['revenueSummaryValue'] == b['revenueSummaryValue']) { + return 0; + } + return (a['revenueSummaryValue'] - b['revenueSummaryValue'] <= 0.01) ? -1 : 1; + }); + } + else if (params['mOrderBy'] == 'pageviewsSummary') { + allSites.sort(function (a, b) { + if (a['pageviewsSummaryValue'] == b['pageviewsSummaryValue']) { + return 0; + } + return (a['pageviewsSummaryValue'] - b['pageviewsSummaryValue'] <= 0.01) ? -1 : 1; + }); + } + else if (params['mOrderBy'] == 'visitsSummary') { + allSites.sort(function (a, b) { + if (a['visitsSummaryValue'] == b['visitsSummaryValue']) { + return 0; + } + return (a['visitsSummaryValue'] - b['visitsSummaryValue'] <= 0.01) ? -1 : 1; + }); + } + + if (params['order'] == 'desc') { + allSites.reverse(); + } + return allSites; } -function limitBy(allSites, params) -{ - var begin = (params['page'] - 1) * params['limit']; - var end = (params['page'] * params['limit']); - return allSites.slice(begin, end); +function limitBy(allSites, params) { + var begin = (params['page'] - 1) * params['limit']; + var end = (params['page'] * params['limit']); + return allSites.slice(begin, end); } -function switchEvolution(params) -{ - $('.pageviews').hide(); - $('.revenue').hide(); - $('.visits').hide(); - $('.' + params['evolutionBy']).show(); - sitesVisible = params['sitesVisible']; - for(i = 0; i < allSites.length; i++) - { - $('#sparkline_' + allSites[i].idsite).html(getSparklineImg(allSites[i].idsite, params['evolutionBy'], params)); - } +function switchEvolution(params) { + $('.pageviews').hide(); + $('.revenue').hide(); + $('.visits').hide(); + $('.' + params['evolutionBy']).show(); + sitesVisible = params['sitesVisible']; + for (i = 0; i < allSites.length; i++) { + $('#sparkline_' + allSites[i].idsite).html(getSparklineImg(allSites[i].idsite, params['evolutionBy'], params)); + } } -function displayRows(allSites, params) -{ - for(var i = 0; i < allSites.length; i++) - { - var str = params['row']; - str = str.replace(/%revenueSummary%/g, getImageForSummary(allSites[i].revenueSummaryValue)); - str = str.replace(/%pageviewsSummary%/g, getImageForSummary(allSites[i].pageviewsSummaryValue)); - str = str.replace(/%visitsSummary%/g, getImageForSummary(allSites[i].visitsSummaryValue)); - str = str.replace(/%sparkline%/g, getSparklineImg(allSites[i].idsite, params['evolutionBy'], params)); - str = str.replace(/%pageviews%/g, allSites[i].pageviews); - str = str.replace(/%idsite%/g, allSites[i].idsite); - str = str.replace(/%visits%/g, allSites[i].visits); - str = str.replace(/%name%/g, allSites[i].name); - str = str.replace(/%revenue%/g, allSites[i].revenue); - str = str.replace(/%main_url%/g, allSites[i].url); - str = str.replace(/%date%/g, params['date'] || params['dateSparkline']); // For period=range, dateSparkline only is set - str = str.replace(/%period%/g, params['period']); - - $('#tb').append('' + str + ''); - } - - $(".table_row").show(); - $('.pageviews').hide(); - $('.revenue').hide(); - $('.visits').hide(); - $('#main_indicator').hide(); - $('.' + params['evolutionBy']).show(); - $("#main_indicator").hide(); +function displayRows(allSites, params) { + for (var i = 0; i < allSites.length; i++) { + var str = params['row']; + str = str.replace(/%revenueSummary%/g, getImageForSummary(allSites[i].revenueSummaryValue)); + str = str.replace(/%pageviewsSummary%/g, getImageForSummary(allSites[i].pageviewsSummaryValue)); + str = str.replace(/%visitsSummary%/g, getImageForSummary(allSites[i].visitsSummaryValue)); + str = str.replace(/%sparkline%/g, getSparklineImg(allSites[i].idsite, params['evolutionBy'], params)); + str = str.replace(/%pageviews%/g, allSites[i].pageviews); + str = str.replace(/%idsite%/g, allSites[i].idsite); + str = str.replace(/%visits%/g, allSites[i].visits); + str = str.replace(/%name%/g, allSites[i].name); + str = str.replace(/%revenue%/g, allSites[i].revenue); + str = str.replace(/%main_url%/g, allSites[i].url); + str = str.replace(/%date%/g, params['date'] || params['dateSparkline']); // For period=range, dateSparkline only is set + str = str.replace(/%period%/g, params['period']); + + $('#tb').append('' + str + ''); + } + + $(".table_row").show(); + $('.pageviews').hide(); + $('.revenue').hide(); + $('.visits').hide(); + $('#main_indicator').hide(); + $('.' + params['evolutionBy']).show(); + $("#main_indicator").hide(); } -function getSparklineImg(id, column, params) -{ - if(column != 'revenue') { - column = 'nb_' + column; - } - var append = ''; - var token_auth = broadcast.getValueFromUrl('token_auth'); - if(token_auth.length) { - append = '&token_auth=' + token_auth; - } - return ''; +function getSparklineImg(id, column, params) { + if (column != 'revenue') { + column = 'nb_' + column; + } + var append = ''; + var token_auth = broadcast.getValueFromUrl('token_auth'); + if (token_auth.length) { + append = '&token_auth=' + token_auth; + } + return ''; } -function showPagination(allSites, params) -{ - if ((params['page'] * params['limit']) < allSites.length) - { - var html = '' + params['next'] + ' »'; - $("#next").html(html); - } - if(params['page'] > 1) - { - html = '« ' + params['prev'] + '' - $("#prev").html(html); - } - var start = (params['page'] - 1) * params['limit'] + 1; - var count = allSites.length; - var end = parseInt(start) + parseInt(params['limit']) - 1; - if(end > count) end = count; - html = '' + (start ) + ' - ' + end + ' of ' + count + ''; - $("#counter").html(html); +function showPagination(allSites, params) { + if ((params['page'] * params['limit']) < allSites.length) { + var html = '' + params['next'] + ' »'; + $("#next").html(html); + } + if (params['page'] > 1) { + html = '« ' + params['prev'] + '' + $("#prev").html(html); + } + var start = (params['page'] - 1) * params['limit'] + 1; + var count = allSites.length; + var end = parseInt(start) + parseInt(params['limit']) - 1; + if (end > count) end = count; + html = '' + (start ) + ' - ' + end + ' of ' + count + ''; + $("#counter").html(html); } -function changePage(allSites, params, kind) -{ - if(kind == 'next') - { - params['page']++; - } - else - { - params['page']--; - } - prepareRows(allSites, params); - return params; +function changePage(allSites, params, kind) { + if (kind == 'next') { + params['page']++; + } + else { + params['page']--; + } + prepareRows(allSites, params); + return params; } -function getImageForSummary(value) -{ - if(value > 0) - { - return ' ' + value + ' %'; - } - else if(value == 0) - { - return ' ' + value + '%'; - } - else - { - return ' ' + value +' %'; - } +function getImageForSummary(value) { + if (value > 0) { + return ' ' + value + ' %'; + } + else if (value == 0) { + return ' ' + value + '%'; + } + else { + return ' ' + value + ' %'; + } } diff --git a/plugins/MultiSites/templates/index.tpl b/plugins/MultiSites/templates/index.tpl index 8bb928fabb..f130594d6e 100644 --- a/plugins/MultiSites/templates/index.tpl +++ b/plugins/MultiSites/templates/index.tpl @@ -1,114 +1,121 @@ {assign var=showSitesSelection value=false} {if !$isWidgetized} -{include file="CoreHome/templates/header.tpl"} + {include file="CoreHome/templates/header.tpl"} {/if}
-
-{include file="MultiSites/templates/row.tpl" assign="row"} - +
+ {include file="MultiSites/templates/row.tpl" assign="row"} + -{postEvent name="template_headerMultiSites"} + {postEvent name="template_headerMultiSites"} -{if !$isWidgetized} -
- {include file="CoreHome/templates/period_select.tpl"} - {include file="CoreHome/templates/header_message.tpl"} -
-{/if} + {if !$isWidgetized} +
+ {include file="CoreHome/templates/period_select.tpl"} + {include file="CoreHome/templates/header_message.tpl"} +
+ {/if} -
+
-

{'General_AllWebsitesDashboard'|translate} - {capture assign=nVisits}{'General_NVisits'|translate:$totalVisits}{/capture} - {capture assign=nVisitsLast}{'General_NVisits'|translate:$pastTotalVisits}{/capture} - +

{'General_AllWebsitesDashboard'|translate} + {capture assign=nVisits}{'General_NVisits'|translate:$totalVisits}{/capture} + {capture assign=nVisitsLast}{'General_NVisits'|translate:$pastTotalVisits}{/capture} + {'General_TotalVisitsPageviewsRevenue'|translate:"$totalVisits":"$totalPageviews":"$totalRevenue"} -

+

- - - - - - - {if $displayRevenueColumn} - - {/if} - - - +
- {'General_Website'|translate} - - - {'General_ColumnNbVisits'|translate} - - - {'General_ColumnPageviews'|translate} - - - {'Goals_ColumnRevenue'|translate} - - - - {'MultiSites_Evolution'|translate} - -
+ + + + + + {if $displayRevenueColumn} + + {/if} + + + - - + + - - {if $isSuperUser} - - - - {/if} - - + {if $isSuperUser} + + + + {/if} + + - - -
+ {'General_Website'|translate} + + + {'General_ColumnNbVisits'|translate} + + + {'General_ColumnPageviews'|translate} + + + {'Goals_ColumnRevenue'|translate} + + + + {'MultiSites_Evolution'|translate} + +
- {'SitesManager_AddSite'|translate} -
- +
+ {'SitesManager_AddSite'|translate} +
+ - -
-
- -
+ {if $autoRefreshTodayReport} + piwikHelper.refreshAfter({$autoRefreshTodayReport} * 1000 + ) + ; + {/if} + +
{include file="CoreHome/templates/footer.tpl"} diff --git a/plugins/MultiSites/templates/row.tpl b/plugins/MultiSites/templates/row.tpl index f651af317e..b49457a5f7 100644 --- a/plugins/MultiSites/templates/row.tpl +++ b/plugins/MultiSites/templates/row.tpl @@ -1,8 +1,8 @@ - + %name% - + @@ -12,22 +12,23 @@ %pageviews% {if $displayRevenueColumn} - - %revenue% - + + %revenue% + {/if} {if $period!='range'} - - - - {if $displayRevenueColumn} - - {/if} -{/if} -{if $show_sparklines} - - - + + + + {if $displayRevenueColumn} + + {/if} + {/if} + {if $show_sparklines} + + + {/if} diff --git a/plugins/MultiSites/templates/styles.css b/plugins/MultiSites/templates/styles.css index e217f0b468..796a2b091a 100644 --- a/plugins/MultiSites/templates/styles.css +++ b/plugins/MultiSites/templates/styles.css @@ -1,67 +1,77 @@ -#multisites { - border:0; - padding:0 15px; - font-size:14px; +#multisites { + border: 0; + padding: 0 15px; + font-size: 14px; } -#multisites .top_controls_inner { - height:10px; +#multisites .top_controls_inner { + height: 10px; } + .smallTitle { - font-size:15px; + font-size: 15px; } + .indicator { - background: url(../images/loading-blue.gif) no-repeat center; - height: 20px; - width: 60px; - margin: auto; - border: 0 !important; + background: url(../images/loading-blue.gif) no-repeat center; + height: 20px; + width: 60px; + margin: auto; + border: 0 !important; } + .clean { border: 0 !important; } + #multisites th { - cursor:pointer; + cursor: pointer; } + #multisites td, #multisites tr, #multisites .sparkline { text-align: center; vertical-align: middle; padding: 1px; margin: 0; } + #multisites td.multisites-label { - padding-left: 15px ; - text-align:left; + padding-left: 15px; + text-align: left; width: 250px; } + #multisites td.multisites-label a:hover { text-decoration: underline; } -#multisites td.multisites-column, #multisites th.multisites-column { - width:70px; - white-space:nowrap; + +#multisites td.multisites-column, #multisites th.multisites-column { + width: 70px; + white-space: nowrap; } -#multisites td.multisites-column-evolution, #multisites th.multisites-column-evolution { - width:70px; + +#multisites td.multisites-column-evolution, #multisites th.multisites-column-evolution { + width: 70px; } -.multisites_desc -{ - width: 16px; - height: 13px; - display: inline-block; - background-image: url(../../../themes/default/images/sortdesc.png); + +.multisites_desc { + width: 16px; + height: 13px; + display: inline-block; + background-image: url(../../../themes/default/images/sortdesc.png); } -.multisites_asc -{ - width: 16px; - height: 13px; - display: inline-block; - background-image: url(../../../themes/default/images/sortasc.png); + +.multisites_asc { + width: 16px; + height: 13px; + display: inline-block; + background-image: url(../../../themes/default/images/sortasc.png); } #mt thead { - line-height:2.5em; + line-height: 2.5em; } + #mt thead :first-child { -moz-border-radius-topleft: 7px; -webkit-border-top-left-radius: 7px; diff --git a/plugins/Overlay/API.php b/plugins/Overlay/API.php index 391b54082f..413406fe43 100644 --- a/plugins/Overlay/API.php +++ b/plugins/Overlay/API.php @@ -11,118 +11,111 @@ class Piwik_Overlay_API { - - private static $instance = null; - - /** - * Get Singleton instance - * @return Piwik_Overlay_API - */ - public static function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - /** - * Get translation strings - */ - public function getTranslations($idSite) - { - $this->authenticate($idSite); - - $translations = array( - 'oneClick' => 'Overlay_OneClick', - 'clicks' => 'Overlay_Clicks', - 'clicksFromXLinks' => 'Overlay_ClicksFromXLinks', - 'link' => 'Overlay_Link' - ); - - return array_map('Piwik_Translate', $translations); - } - - /** - * Get excluded query parameters for a site. - * This information is used for client side url normalization. - */ - public function getExcludedQueryParameters($idSite) - { - $this->authenticate($idSite); - - $sitesManager = Piwik_SitesManager_API::getInstance(); - $site = $sitesManager->getSiteFromId($idSite); - - try { - return Piwik_SitesManager::getTrackerExcludedQueryParameters($site); - } catch(Exception $e) { - // an exception is thrown when the user has no view access. - // do not throw the exception to the outside. - return array(); - } - } - - /** - * Get following pages of a url. - * This is done on the logs - not the archives! - * - * Note: if you use this method via the regular API, the number of results will be limited. - * Make sure, you set filter_limit=-1 in the request. - */ - public function getFollowingPages($url, $idSite, $period, $date, $segment = false) - { - $this->authenticate($idSite); - - $url = Piwik_Tracker_Action::excludeQueryParametersFromUrl($url, $idSite); - // we don't unsanitize $url here. it will be done in the Transitions plugin. - - $resultDataTable = new Piwik_DataTable; - - try - { - $limitBeforeGrouping = Piwik_Config::getInstance()->General['overlay_following_pages_limit']; - $transitionsReport = Piwik_Transitions_API::getInstance()->getTransitionsForAction( - $url, $type = 'url', $idSite, $period, $date, $segment, $limitBeforeGrouping, - $part = 'followingActions', $returnNormalizedUrls = true); - } - catch(Exception $e) - { - return $resultDataTable; - } - - $reports = array('followingPages', 'outlinks', 'downloads'); - foreach ($reports as $reportName) - { - if (!isset($transitionsReport[$reportName])) - { - continue; - } - foreach ($transitionsReport[$reportName]->getRows() as $row) - { - // don't touch the row at all for performance reasons - $resultDataTable->addRow($row); - } - } - - return $resultDataTable; - } - - /** Do cookie authentication. This way, the token can remain secret. */ - private function authenticate($idSite) - { - $notification = null; - Piwik_PostEvent('FrontController.initAuthenticationObject', $notification, $allowCookieAuthentication = true); - - $auth = Zend_Registry::get('auth'); - $success = Zend_Registry::get('access')->reloadAccess($auth); - - if (!$success) { - throw new Exception('Authentication failed'); - } - - Piwik::checkUserHasViewAccess($idSite); - } + + private static $instance = null; + + /** + * Get Singleton instance + * @return Piwik_Overlay_API + */ + public static function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + /** + * Get translation strings + */ + public function getTranslations($idSite) + { + $this->authenticate($idSite); + + $translations = array( + 'oneClick' => 'Overlay_OneClick', + 'clicks' => 'Overlay_Clicks', + 'clicksFromXLinks' => 'Overlay_ClicksFromXLinks', + 'link' => 'Overlay_Link' + ); + + return array_map('Piwik_Translate', $translations); + } + + /** + * Get excluded query parameters for a site. + * This information is used for client side url normalization. + */ + public function getExcludedQueryParameters($idSite) + { + $this->authenticate($idSite); + + $sitesManager = Piwik_SitesManager_API::getInstance(); + $site = $sitesManager->getSiteFromId($idSite); + + try { + return Piwik_SitesManager::getTrackerExcludedQueryParameters($site); + } catch (Exception $e) { + // an exception is thrown when the user has no view access. + // do not throw the exception to the outside. + return array(); + } + } + + /** + * Get following pages of a url. + * This is done on the logs - not the archives! + * + * Note: if you use this method via the regular API, the number of results will be limited. + * Make sure, you set filter_limit=-1 in the request. + */ + public function getFollowingPages($url, $idSite, $period, $date, $segment = false) + { + $this->authenticate($idSite); + + $url = Piwik_Tracker_Action::excludeQueryParametersFromUrl($url, $idSite); + // we don't unsanitize $url here. it will be done in the Transitions plugin. + + $resultDataTable = new Piwik_DataTable; + + try { + $limitBeforeGrouping = Piwik_Config::getInstance()->General['overlay_following_pages_limit']; + $transitionsReport = Piwik_Transitions_API::getInstance()->getTransitionsForAction( + $url, $type = 'url', $idSite, $period, $date, $segment, $limitBeforeGrouping, + $part = 'followingActions', $returnNormalizedUrls = true); + } catch (Exception $e) { + return $resultDataTable; + } + + $reports = array('followingPages', 'outlinks', 'downloads'); + foreach ($reports as $reportName) { + if (!isset($transitionsReport[$reportName])) { + continue; + } + foreach ($transitionsReport[$reportName]->getRows() as $row) { + // don't touch the row at all for performance reasons + $resultDataTable->addRow($row); + } + } + + return $resultDataTable; + } + + /** Do cookie authentication. This way, the token can remain secret. */ + private function authenticate($idSite) + { + $notification = null; + Piwik_PostEvent('FrontController.initAuthenticationObject', $notification, $allowCookieAuthentication = true); + + $auth = Zend_Registry::get('auth'); + $success = Zend_Registry::get('access')->reloadAccess($auth); + + if (!$success) { + throw new Exception('Authentication failed'); + } + + Piwik::checkUserHasViewAccess($idSite); + } } diff --git a/plugins/Overlay/Controller.php b/plugins/Overlay/Controller.php index 6b938b9443..636683fb36 100644 --- a/plugins/Overlay/Controller.php +++ b/plugins/Overlay/Controller.php @@ -11,129 +11,123 @@ class Piwik_Overlay_Controller extends Piwik_Controller { - - /** The index of the plugin */ - public function index() - { - Piwik::checkUserHasViewAccess($this->idSite); - - $template = 'index'; - if (Piwik_Config::getInstance()->General['overlay_disable_framed_mode']) { - $template = 'index_noframe'; - } - - $view = Piwik_View::factory($template); - - $this->setGeneralVariablesView($view); - $view->showTopMenu = false; - $view->showSitesSelection = false; - $view->addToHead = '' - . ''; - - $view->idSite = $this->idSite; - $view->date = Piwik_Common::getRequestVar('date', 'today'); - $view->period = Piwik_Common::getRequestVar('period', 'day'); - - $view->ssl = Piwik::isHttps(); - - echo $view->render(); - } - - /** Render the area left of the iframe */ - public function renderSidebar() - { - $idSite = Piwik_Common::getRequestVar('idSite'); - $period = Piwik_Common::getRequestVar('period'); - $date = Piwik_Common::getRequestVar('date'); - $currentUrl = Piwik_Common::getRequestVar('currentUrl'); - $currentUrl = Piwik_Common::unsanitizeInputValue($currentUrl); - - $normalizedCurrentUrl = Piwik_Tracker_Action::excludeQueryParametersFromUrl($currentUrl, $idSite); - $normalizedCurrentUrl = Piwik_Common::unsanitizeInputValue($normalizedCurrentUrl); - - // load the appropriate row of the page urls report using the label filter - Piwik_Actions_ArchivingHelper::reloadConfig(); - $path = Piwik_Actions_ArchivingHelper::getActionExplodedNames($normalizedCurrentUrl, Piwik_Tracker_Action::TYPE_ACTION_URL); - $path = array_map('urlencode', $path); - $label = implode('>', $path); - $request = new Piwik_API_Request( - 'method=Actions.getPageUrls' - .'&idSite='.urlencode($idSite) - .'&date='.urlencode($date) - .'&period='.urlencode($period) - .'&label='.urlencode($label) - .'&format=original' - ); - $dataTable = $request->process(); - - $data = array(); - if ($dataTable->getRowsCount() > 0) - { - $row = $dataTable->getFirstRow(); - - $translations = Piwik_API_API::getDefaultMetricTranslations(); - $showMetrics = array('nb_hits', 'nb_visits', 'nb_uniq_visitors', - 'bounce_rate', 'exit_rate', 'avg_time_on_page'); - - - foreach ($showMetrics as $metric) - { - $value = $row->getColumn($metric); - if ($value === false) - { - // skip unique visitors for period != day - continue; - } - if ($metric == 'avg_time_on_page') - { - $value = Piwik::getPrettyTimeFromSeconds($value); - } - $data[] = array( - 'name' => $translations[$metric], - 'value' => $value - ); - } - } - - // generate page url string - foreach ($path as &$part) - { - $part = preg_replace(';^/;', '', urldecode($part)); - } - $page = '/'.implode('/', $path); - $page = preg_replace(';/index$;', '/', $page); - if ($page == '/') - { - $page = '/index'; - } - - // render template - $view = Piwik_View::factory('sidebar'); - $view->data = $data; - $view->location = $page; - $view->normalizedUrl = $normalizedCurrentUrl; - $view->label = $label; - $view->idSite = $idSite; - $view->period = $period; - $view->date = $date; - echo $view->render(); - } - - /** - * Start an Overlay session: Redirect to the tracked website. The Piwik - * tracker will recognize this referrer and start the session. - */ - public function startOverlaySession() - { - $idSite = Piwik_Common::getRequestVar('idsite', 0, 'int'); - Piwik::checkUserHasViewAccess($idSite); - - $sitesManager = Piwik_SitesManager_API::getInstance(); - $site = $sitesManager->getSiteFromId($idSite); - $urls = $sitesManager->getSiteUrlsFromId($idSite); - - @header('Content-Type: text/html; charset=UTF-8'); - echo ' + + /** The index of the plugin */ + public function index() + { + Piwik::checkUserHasViewAccess($this->idSite); + + $template = 'index'; + if (Piwik_Config::getInstance()->General['overlay_disable_framed_mode']) { + $template = 'index_noframe'; + } + + $view = Piwik_View::factory($template); + + $this->setGeneralVariablesView($view); + $view->showTopMenu = false; + $view->showSitesSelection = false; + $view->addToHead = '' + . ''; + + $view->idSite = $this->idSite; + $view->date = Piwik_Common::getRequestVar('date', 'today'); + $view->period = Piwik_Common::getRequestVar('period', 'day'); + + $view->ssl = Piwik::isHttps(); + + echo $view->render(); + } + + /** Render the area left of the iframe */ + public function renderSidebar() + { + $idSite = Piwik_Common::getRequestVar('idSite'); + $period = Piwik_Common::getRequestVar('period'); + $date = Piwik_Common::getRequestVar('date'); + $currentUrl = Piwik_Common::getRequestVar('currentUrl'); + $currentUrl = Piwik_Common::unsanitizeInputValue($currentUrl); + + $normalizedCurrentUrl = Piwik_Tracker_Action::excludeQueryParametersFromUrl($currentUrl, $idSite); + $normalizedCurrentUrl = Piwik_Common::unsanitizeInputValue($normalizedCurrentUrl); + + // load the appropriate row of the page urls report using the label filter + Piwik_Actions_ArchivingHelper::reloadConfig(); + $path = Piwik_Actions_ArchivingHelper::getActionExplodedNames($normalizedCurrentUrl, Piwik_Tracker_Action::TYPE_ACTION_URL); + $path = array_map('urlencode', $path); + $label = implode('>', $path); + $request = new Piwik_API_Request( + 'method=Actions.getPageUrls' + . '&idSite=' . urlencode($idSite) + . '&date=' . urlencode($date) + . '&period=' . urlencode($period) + . '&label=' . urlencode($label) + . '&format=original' + ); + $dataTable = $request->process(); + + $data = array(); + if ($dataTable->getRowsCount() > 0) { + $row = $dataTable->getFirstRow(); + + $translations = Piwik_API_API::getDefaultMetricTranslations(); + $showMetrics = array('nb_hits', 'nb_visits', 'nb_uniq_visitors', + 'bounce_rate', 'exit_rate', 'avg_time_on_page'); + + + foreach ($showMetrics as $metric) { + $value = $row->getColumn($metric); + if ($value === false) { + // skip unique visitors for period != day + continue; + } + if ($metric == 'avg_time_on_page') { + $value = Piwik::getPrettyTimeFromSeconds($value); + } + $data[] = array( + 'name' => $translations[$metric], + 'value' => $value + ); + } + } + + // generate page url string + foreach ($path as &$part) { + $part = preg_replace(';^/;', '', urldecode($part)); + } + $page = '/' . implode('/', $path); + $page = preg_replace(';/index$;', '/', $page); + if ($page == '/') { + $page = '/index'; + } + + // render template + $view = Piwik_View::factory('sidebar'); + $view->data = $data; + $view->location = $page; + $view->normalizedUrl = $normalizedCurrentUrl; + $view->label = $label; + $view->idSite = $idSite; + $view->period = $period; + $view->date = $date; + echo $view->render(); + } + + /** + * Start an Overlay session: Redirect to the tracked website. The Piwik + * tracker will recognize this referrer and start the session. + */ + public function startOverlaySession() + { + $idSite = Piwik_Common::getRequestVar('idsite', 0, 'int'); + Piwik::checkUserHasViewAccess($idSite); + + $sitesManager = Piwik_SitesManager_API::getInstance(); + $site = $sitesManager->getSiteFromId($idSite); + $urls = $sitesManager->getSiteUrlsFromId($idSite); + + @header('Content-Type: text/html; charset=UTF-8'); + echo ' '; - } - - /** - * This method is called when the JS from startOverlaySession() detects that the target domain - * is not configured for the current site. - */ - public function showErrorWrongDomain() - { - $idSite = Piwik_Common::getRequestVar('idSite', 0, 'int'); - Piwik::checkUserHasViewAccess($idSite); - - $url = Piwik_Common::getRequestVar('url', ''); - $url = Piwik_Common::unsanitizeInputValue($url); - - $message = Piwik_Translate('Overlay_RedirectUrlError', array($url, "\n")); - $message = nl2br(htmlentities($message)); - - $view = Piwik_View::factory('error_wrong_domain'); - $view->message = $message; - - if (Piwik::isUserHasAdminAccess($idSite)) { - // TODO use $idSite to link to the correct row. This is tricky because the #rowX ids don't match - // the site ids when sites have been deleted. - $url = 'index.php?module=SitesManager&action=index'; - $troubleshoot = htmlentities(Piwik_Translate('Overlay_RedirectUrlErrorAdmin')); - $troubleshoot = sprintf($troubleshoot, '', ''); - $view->troubleshoot = $troubleshoot; - } else { - $view->troubleshoot = htmlentities(Piwik_Translate('Overlay_RedirectUrlErrorUser')); - } - - echo $view->render(); - } - - /** - * This method is used to pass information from the iframe back to Piwik. - * Due to the same origin policy, we can't do that directly, so we inject - * an additional iframe in the Overlay session that calls this controller - * method. - * The rendered iframe is from the same origin as the Piwik window so we - * can bypass the same origin policy and call the parent. - */ - public function notifyParentIframe() - { - $view = Piwik_View::factory('notify_parent_iframe'); - echo $view->render(); - } - + } + + /** + * This method is called when the JS from startOverlaySession() detects that the target domain + * is not configured for the current site. + */ + public function showErrorWrongDomain() + { + $idSite = Piwik_Common::getRequestVar('idSite', 0, 'int'); + Piwik::checkUserHasViewAccess($idSite); + + $url = Piwik_Common::getRequestVar('url', ''); + $url = Piwik_Common::unsanitizeInputValue($url); + + $message = Piwik_Translate('Overlay_RedirectUrlError', array($url, "\n")); + $message = nl2br(htmlentities($message)); + + $view = Piwik_View::factory('error_wrong_domain'); + $view->message = $message; + + if (Piwik::isUserHasAdminAccess($idSite)) { + // TODO use $idSite to link to the correct row. This is tricky because the #rowX ids don't match + // the site ids when sites have been deleted. + $url = 'index.php?module=SitesManager&action=index'; + $troubleshoot = htmlentities(Piwik_Translate('Overlay_RedirectUrlErrorAdmin')); + $troubleshoot = sprintf($troubleshoot, '', ''); + $view->troubleshoot = $troubleshoot; + } else { + $view->troubleshoot = htmlentities(Piwik_Translate('Overlay_RedirectUrlErrorUser')); + } + + echo $view->render(); + } + + /** + * This method is used to pass information from the iframe back to Piwik. + * Due to the same origin policy, we can't do that directly, so we inject + * an additional iframe in the Overlay session that calls this controller + * method. + * The rendered iframe is from the same origin as the Piwik window so we + * can bypass the same origin policy and call the parent. + */ + public function notifyParentIframe() + { + $view = Piwik_View::factory('notify_parent_iframe'); + echo $view->render(); + } + } diff --git a/plugins/Overlay/Overlay.php b/plugins/Overlay/Overlay.php index 7e800884f3..d7b950491e 100644 --- a/plugins/Overlay/Overlay.php +++ b/plugins/Overlay/Overlay.php @@ -11,28 +11,28 @@ class Piwik_Overlay extends Piwik_Plugin { - public function getInformation() - { - return array( - 'description' => Piwik_Translate('Overlay_PluginDescription'), - 'author' => 'Piwik', - 'author_homepage' => 'http://piwik.org/', - 'version' => Piwik_Version::VERSION, - ); - } - - function getListHooksRegistered() - { - return array( - 'AssetManager.getJsFiles' => 'getJsFiles' - ); - } - - public function getJsFiles($notification) - { - $jsFiles = &$notification->getNotificationObject(); - $jsFiles[] = 'plugins/Overlay/templates/rowaction.js'; - $jsFiles[] = 'plugins/Overlay/templates/helper.js'; - } + public function getInformation() + { + return array( + 'description' => Piwik_Translate('Overlay_PluginDescription'), + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'version' => Piwik_Version::VERSION, + ); + } + + function getListHooksRegistered() + { + return array( + 'AssetManager.getJsFiles' => 'getJsFiles' + ); + } + + public function getJsFiles($notification) + { + $jsFiles = & $notification->getNotificationObject(); + $jsFiles[] = 'plugins/Overlay/templates/rowaction.js'; + $jsFiles[] = 'plugins/Overlay/templates/helper.js'; + } } diff --git a/plugins/Overlay/client/client.css b/plugins/Overlay/client/client.css index 05e2e87169..ec00ea71da 100644 --- a/plugins/Overlay/client/client.css +++ b/plugins/Overlay/client/client.css @@ -1,4 +1,3 @@ - /** * Reset styles */ @@ -11,84 +10,80 @@ .PIS_LinkHighlightBoxRight, .PIS_LinkHighlightBoxLeft, .PIS_LinkHighlightBoxText { - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-weight: normal; - font-style: normal; - font-size: 11px; - font-family: Arial, Helvetica, sans-serif; - vertical-align: baseline; - line-height: 1.4em; - text-indent: 0; - text-decoration: none; - text-transform: none; - cursor: default; - text-align: left; - float: none; - color: #333; + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-weight: normal; + font-style: normal; + font-size: 11px; + font-family: Arial, Helvetica, sans-serif; + vertical-align: baseline; + line-height: 1.4em; + text-indent: 0; + text-decoration: none; + text-transform: none; + cursor: default; + text-align: left; + float: none; + color: #333; } - - /** * Link Tags */ .PIS_LinkTag { - position: absolute; - z-index: 9999; - width: 36px; - height: 21px; - text-align: left; - background: url(./linktags_lessshadow.png) no-repeat 0 -21px; - overflow: hidden; + position: absolute; + z-index: 9999; + width: 36px; + height: 21px; + text-align: left; + background: url(./linktags_lessshadow.png) no-repeat 0 -21px; + overflow: hidden; } .PIS_LinkTag span { - position: absolute; - width: 31px; - height: 14px; - font-size: 10px; - text-align: center; - font-weight: bold; - line-height: 14px; - margin-left: 1px; + position: absolute; + width: 31px; + height: 14px; + font-size: 10px; + text-align: center; + font-weight: bold; + line-height: 14px; + margin-left: 1px; } .PIS_LinkTag.PIS_Highlighted { - z-index: 10002; + z-index: 10002; } .PIS_LinkTag.PIS_Highlighted span { - color: #E87500; + color: #E87500; } .PIS_LinkTag.PIS_Right { - background-position: -36px -21px; + background-position: -36px -21px; } .PIS_LinkTag.PIS_Right span, .PIS_LinkTag.PIS_BottomRight span { - margin-left: 5px; + margin-left: 5px; } .PIS_LinkTag.PIS_Bottom { - background-position: 0 0; + background-position: 0 0; } .PIS_LinkTag.PIS_Bottom span, .PIS_LinkTag.PIS_BottomRight span { - margin-top: 4px; + margin-top: 4px; } .PIS_LinkTag.PIS_BottomRight { - background-position: -36px 0; + background-position: -36px 0; } - - /** * Link Highlights */ @@ -97,60 +92,58 @@ .PIS_LinkHighlightBoxRight, .PIS_LinkHighlightBoxText, .PIS_LinkHighlightBoxLeft { - position: absolute; - z-index: 10001; - overflow: hidden; - width: 2px; - height: 2px; - background: #E87500; + position: absolute; + z-index: 10001; + overflow: hidden; + width: 2px; + height: 2px; + background: #E87500; } .PIS_LinkHighlightBoxText { - line-height: 20px; - height: 20px; - font-size: 11px; - color: white; - width: auto; + line-height: 20px; + height: 20px; + font-size: 11px; + color: white; + width: auto; } - - /** * Status bar */ #PIS_StatusBar { - padding: 10px 0; - position: fixed; - z-index: 10020; - bottom: 0; - right: 0; - border-top: 1px solid #ccc; - border-left: 1px solid #ccc; - background: #fbfbfb; - -webkit-border-top-left-radius: 6px; - -webkit-border-top-right-radius: 0px; - -webkit-border-bottom-right-radius: 0px; - -webkit-border-bottom-left-radius: 0px; - -moz-border-radius-topleft: 6px; - -moz-border-radius-topright: 0px; - -moz-border-radius-bottomright: 0px; - -moz-border-radius-bottomleft: 0px; - border-top-left-radius: 6px; - border-top-right-radius: 0px; - border-bottom-right-radius: 0px; - border-bottom-left-radius: 0px; + padding: 10px 0; + position: fixed; + z-index: 10020; + bottom: 0; + right: 0; + border-top: 1px solid #ccc; + border-left: 1px solid #ccc; + background: #fbfbfb; + -webkit-border-top-left-radius: 6px; + -webkit-border-top-right-radius: 0px; + -webkit-border-bottom-right-radius: 0px; + -webkit-border-bottom-left-radius: 0px; + -moz-border-radius-topleft: 6px; + -moz-border-radius-topright: 0px; + -moz-border-radius-bottomright: 0px; + -moz-border-radius-bottomleft: 0px; + border-top-left-radius: 6px; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; } #PIS_StatusBar .PIS_Item { - text-align: right; - padding: 3px 5px 0 0; - margin: 0 15px 0 20px; - - font-weight: bold; + text-align: right; + padding: 3px 5px 0 0; + margin: 0 15px 0 20px; + + font-weight: bold; } #PIS_StatusBar .PIS_Loading { - background: url(./loading.gif) no-repeat right center; - padding-right: 30px; + background: url(./loading.gif) no-repeat right center; + padding-right: 30px; } \ No newline at end of file diff --git a/plugins/Overlay/client/client.js b/plugins/Overlay/client/client.js index 93d790f059..f791f8beb5 100644 --- a/plugins/Overlay/client/client.js +++ b/plugins/Overlay/client/client.js @@ -1,261 +1,261 @@ +var Piwik_Overlay_Client = (function () { + + /** jQuery */ + var $; + + /** Url of the Piwik root */ + var piwikRoot; + + /** Piwik idsite */ + var idSite; + + /** The current period and date */ + var period, date; + + /** Reference to the status bar DOM element */ + var statusBar; + + /** Load the client CSS */ + function loadCss() { + var css = c('link').attr({ + rel: 'stylesheet', + type: 'text/css', + href: piwikRoot + 'plugins/Overlay/client/client.css' + }); + $('head').append(css); + } + + /** + * This method loads jQuery, if it is not there yet. + * The callback is triggered after jQuery is loaded. + */ + function loadJQuery(callback) { + if (typeof jQuery != 'undefined') { + $ = jQuery; + callback(); + } + else { + Piwik_Overlay_Client.loadScript('libs/jquery/jquery.js', function () { + $ = jQuery; + jQuery.noConflict(); + callback(); + }); + } + } + + /** + * Notify Piwik of the current iframe location. + * This way, we can display additional metrics on the side of the iframe. + */ + function notifyPiwikOfLocation() { + // check whether the session has been opened in a new tab (instead of an iframe) + if (window != window.top) { + var iframe = c('iframe', false, { + src: piwikRoot + 'index.php?module=Overlay&action=notifyParentIframe#' + window.location.href + }).css({width: 0, height: 0, border: 0}); + + // in some cases, calling append right away doesn't work in IE8 + $(document).ready(function () { + $('body').append(iframe); + }); + } + } + + /** Create a jqueryfied DOM element */ + function c(tagName, className, attributes) { + var el = $(document.createElement(tagName)); + + if (className) { + if (className.substring(0, 1) == '#') { + var id = className.substring(1, className.length); + id = 'PIS_' + id; + el.attr('id', id); + } + else { + className = 'PIS_' + className; + el.addClass(className); + } + } + + if (attributes) { + el.attr(attributes); + } + + return el; + } + + /** Special treatment for some internet explorers */ + var ieStatusBarEventsBound = false; + + function handleIEStatusBar() { + if (navigator.appVersion.indexOf("MSIE 7.") == -1 + && navigator.appVersion.indexOf("MSIE 8.") == -1) { + // this is not IE8 or lower + return; + } + + // IE7/8 can't handle position:fixed so we need to do it by hand + statusBar.css({ + position: 'absolute', + right: 'auto', + bottom: 'auto', + left: 0, + top: 0 + }); + + var position = function () { + var scrollY = document.body.parentElement.scrollTop; + var scrollX = document.body.parentElement.scrollLeft; + statusBar.css({ + top: (scrollY + $(window).height() - statusBar.outerHeight()) + 'px', + left: (scrollX + $(window).width() - statusBar.outerWidth()) + 'px' + }); + }; + + position(); + + statusBar.css({width: 'auto'}); + if (statusBar.width() < 350) { + statusBar.width(350); + } else { + statusBar.width(statusBar.width()); + } + + if (!ieStatusBarEventsBound) { + ieStatusBarEventsBound = true; + $(window).resize(position); + $(window).scroll(position); + } + } + + return { + + /** Initialize in-site analytics */ + initialize: function (pPiwikRoot, pIdSite, pPeriod, pDate) { + piwikRoot = pPiwikRoot; + idSite = pIdSite; + period = pPeriod; + date = pDate; + + var load = this.loadScript; + var loading = this.loadingNotification; + + loadJQuery(function () { + notifyPiwikOfLocation(); + loadCss(); + + // translations + load('plugins/Overlay/client/translations.js', function () { + Piwik_Overlay_Translations.initialize(function () { + // following pages + var finishPages = loading('Loading following pages'); + load('plugins/Overlay/client/followingpages.js', function () { + Piwik_Overlay_FollowingPages.initialize(finishPages); + }); + + }); + }); + }); + }, + + /** Create a jqueryfied DOM element */ + createElement: function (tagName, className, attributes) { + return c(tagName, className, attributes); + }, + + /** Load a script and wait for it to be loaded */ + loadScript: function (relativePath, callback) { + var loaded = false; + var onLoad = function () { + if (!loaded) { + loaded = true; + callback(); + } + }; + + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + script.type = 'text/javascript'; + + script.onreadystatechange = function () { + if (this.readyState == 'loaded' || this.readyState == 'complete') { + onLoad(); + } + }; + script.onload = onLoad; + + script.src = piwikRoot + relativePath + '?v=1'; + head.appendChild(script); + }, + + /** Piwik Overlay API Request */ + api: function (method, callback, additionalParams) { + var url = piwikRoot + 'index.php?module=API&method=Overlay.' + method + + '&idSite=' + idSite + '&period=' + period + '&date=' + date + '&format=JSON&filter_limit=-1'; + + if (additionalParams) { + url += '&' + additionalParams; + } + + $.getJSON(url + "&jsoncallback=?", function (data) { + if (typeof data.result != 'undefined' && data.result == 'error') { + alert('Error: ' + data.message); + } + else { + callback(data); + } + }); + }, + + /** + * Initialize a notification + * To hide the notification use the returned callback + */ + notification: function (message, addClass) { + if (!statusBar) { + statusBar = c('div', '#StatusBar').css('opacity', .8); + $('body').prepend(statusBar); + } + + var item = c('div', 'Item').html(message); + + if (addClass) { + item.addClass('PIS_' + addClass); + } + + statusBar.show().append(item); + + handleIEStatusBar(); + window.setTimeout(handleIEStatusBar, 100); + + return function () { + item.remove(); + if (statusBar.children().size() == 0) { + statusBar.hide(); + } else { + handleIEStatusBar(); + } + }; + }, + + /** Hide all notifications with a certain class */ + hideNotifications: function (className) { + statusBar.find('.PIS_' + className).remove(); + if (statusBar.children().size() == 0) { + statusBar.hide(); + } else { + handleIEStatusBar(); + } + }, + + /** + * Initialize a loading notification + * To hide the notification use the returned callback + */ + loadingNotification: function (message) { + return Piwik_Overlay_Client.notification(message, 'Loading'); + } + + }; -var Piwik_Overlay_Client = (function() { - - /** jQuery */ - var $; - - /** Url of the Piwik root */ - var piwikRoot; - - /** Piwik idsite */ - var idSite; - - /** The current period and date */ - var period, date; - - /** Reference to the status bar DOM element */ - var statusBar; - - /** Load the client CSS */ - function loadCss() { - var css = c('link').attr({ - rel: 'stylesheet', - type: 'text/css', - href: piwikRoot + 'plugins/Overlay/client/client.css' - }); - $('head').append(css); - } - - /** - * This method loads jQuery, if it is not there yet. - * The callback is triggered after jQuery is loaded. - */ - function loadJQuery(callback) { - if (typeof jQuery != 'undefined') { - $ = jQuery; - callback(); - } - else { - Piwik_Overlay_Client.loadScript('libs/jquery/jquery.js', function() { - $ = jQuery; - jQuery.noConflict(); - callback(); - }); - } - } - - /** - * Notify Piwik of the current iframe location. - * This way, we can display additional metrics on the side of the iframe. - */ - function notifyPiwikOfLocation() { - // check whether the session has been opened in a new tab (instead of an iframe) - if (window != window.top) { - var iframe = c('iframe', false, { - src: piwikRoot + 'index.php?module=Overlay&action=notifyParentIframe#' + window.location.href - }).css({width: 0, height: 0, border: 0}); - - // in some cases, calling append right away doesn't work in IE8 - $(document).ready(function() { - $('body').append(iframe); - }); - } - } - - /** Create a jqueryfied DOM element */ - function c(tagName, className, attributes) { - var el = $(document.createElement(tagName)); - - if (className) { - if (className.substring(0, 1) == '#') { - var id = className.substring(1, className.length); - id = 'PIS_' + id; - el.attr('id', id); - } - else { - className = 'PIS_' + className; - el.addClass(className); - } - } - - if (attributes) { - el.attr(attributes); - } - - return el; - } - - /** Special treatment for some internet explorers */ - var ieStatusBarEventsBound = false; - function handleIEStatusBar() { - if (navigator.appVersion.indexOf("MSIE 7.") == -1 - && navigator.appVersion.indexOf("MSIE 8.") == -1) { - // this is not IE8 or lower - return; - } - - // IE7/8 can't handle position:fixed so we need to do it by hand - statusBar.css({ - position: 'absolute', - right: 'auto', - bottom: 'auto', - left: 0, - top: 0 - }); - - var position = function() { - var scrollY = document.body.parentElement.scrollTop; - var scrollX = document.body.parentElement.scrollLeft; - statusBar.css({ - top: (scrollY + $(window).height() - statusBar.outerHeight()) + 'px', - left: (scrollX + $(window).width() - statusBar.outerWidth()) + 'px' - }); - }; - - position(); - - statusBar.css({width: 'auto'}); - if (statusBar.width() < 350) { - statusBar.width(350); - } else { - statusBar.width(statusBar.width()); - } - - if (!ieStatusBarEventsBound) { - ieStatusBarEventsBound = true; - $(window).resize(position); - $(window).scroll(position); - } - } - - return { - - /** Initialize in-site analytics */ - initialize: function(pPiwikRoot, pIdSite, pPeriod, pDate) { - piwikRoot = pPiwikRoot; - idSite = pIdSite; - period = pPeriod; - date = pDate; - - var load = this.loadScript; - var loading = this.loadingNotification; - - loadJQuery(function() { - notifyPiwikOfLocation(); - loadCss(); - - // translations - load('plugins/Overlay/client/translations.js', function() { - Piwik_Overlay_Translations.initialize(function() { - // following pages - var finishPages = loading('Loading following pages'); - load('plugins/Overlay/client/followingpages.js', function() { - Piwik_Overlay_FollowingPages.initialize(finishPages); - }); - - }); - }); - }); - }, - - /** Create a jqueryfied DOM element */ - createElement: function(tagName, className, attributes) { - return c(tagName, className, attributes); - }, - - /** Load a script and wait for it to be loaded */ - loadScript: function(relativePath, callback) { - var loaded = false; - var onLoad = function() { - if (!loaded) { - loaded = true; - callback(); - } - }; - - var head = document.getElementsByTagName('head')[0]; - var script = document.createElement('script'); - script.type = 'text/javascript'; - - script.onreadystatechange = function () { - if (this.readyState == 'loaded' || this.readyState == 'complete') { - onLoad(); - } - }; - script.onload = onLoad; - - script.src = piwikRoot+relativePath+'?v=1'; - head.appendChild(script); - }, - - /** Piwik Overlay API Request */ - api: function(method, callback, additionalParams) { - var url = piwikRoot+'index.php?module=API&method=Overlay.'+method - +'&idSite='+idSite+'&period='+period+'&date='+date+'&format=JSON&filter_limit=-1'; - - if (additionalParams) { - url += '&' + additionalParams; - } - - $.getJSON(url+"&jsoncallback=?", function(data) { - if (typeof data.result != 'undefined' && data.result == 'error') { - alert('Error: ' + data.message); - } - else { - callback(data); - } - }); - }, - - /** - * Initialize a notification - * To hide the notification use the returned callback - */ - notification: function(message, addClass) { - if (!statusBar) { - statusBar = c('div', '#StatusBar').css('opacity', .8); - $('body').prepend(statusBar); - } - - var item = c('div', 'Item').html(message); - - if (addClass) { - item.addClass('PIS_' + addClass); - } - - statusBar.show().append(item); - - handleIEStatusBar(); - window.setTimeout(handleIEStatusBar, 100); - - return function() { - item.remove(); - if (statusBar.children().size() == 0) { - statusBar.hide(); - } else { - handleIEStatusBar(); - } - }; - }, - - /** Hide all notifications with a certain class */ - hideNotifications: function(className) { - statusBar.find('.PIS_' + className).remove(); - if (statusBar.children().size() == 0) { - statusBar.hide(); - } else { - handleIEStatusBar(); - } - }, - - /** - * Initialize a loading notification - * To hide the notification use the returned callback - */ - loadingNotification: function(message) { - return Piwik_Overlay_Client.notification(message, 'Loading'); - } - - }; - })(); diff --git a/plugins/Overlay/client/followingpages.js b/plugins/Overlay/client/followingpages.js index edb9c40e1e..e20252463f 100644 --- a/plugins/Overlay/client/followingpages.js +++ b/plugins/Overlay/client/followingpages.js @@ -1,511 +1,511 @@ -var Piwik_Overlay_FollowingPages = (function() { - - /** jQuery */ - var $ = jQuery; - - /** Info about the following pages */ - var followingPages = []; - - /** List of excluded get parameters */ - var excludedParams = []; - - /** Index of the links on the page */ - var linksOnPage = {}; - - /** Reference to create element function */ - var c; - - /** Load the following pages */ - function load(callback) { - // normalize current location - var location = window.location.href; - location = Piwik_Overlay_UrlNormalizer.normalize(location); - location = (("https:" == document.location.protocol) ? 'https' : 'http') + '://' + location; - - var excludedParamsLoaded = false; - var followingPagesLoaded = false; - - // load excluded params - Piwik_Overlay_Client.api('getExcludedQueryParameters', function(data) { - for (var i = 0; i < data.length; i++) { - if (typeof data[i] == 'object') { - data[i] = data[i][0]; - } - } - excludedParams = data; - - excludedParamsLoaded = true; - if (followingPagesLoaded) { - callback(); - } - }); - - // load following pages - Piwik_Overlay_Client.api('getFollowingPages', function(data) { - followingPages = data; - processFollowingPages(); - - followingPagesLoaded = true; - if (excludedParamsLoaded) { - callback(); - } - }, 'url=' + encodeURIComponent(location)); - } - - /** Normalize the URLs of following pages and aggregate some stats */ - function processFollowingPages() { - var totalClicks = 0; - for (var i = 0; i < followingPages.length; i++) { - var page = followingPages[i]; - // though the following pages are returned without the prefix, downloads - // and outlinks still have it. - page.label = Piwik_Overlay_UrlNormalizer.removeUrlPrefix(page.label); - totalClicks += followingPages[i].referrals; - } - for (i = 0; i < followingPages.length; i++) { - followingPages[i].clickRate = followingPages[i].referrals / totalClicks * 100; - } - } - - /** - * Build an index of links on the page. - * This function is passed to $('a').each() - */ - var processLinkDelta = false; - - function processLink() { - var a = $(this); - a[0].piwikDiscovered = true; - - var href = a.attr('href'); - href = Piwik_Overlay_UrlNormalizer.normalize(href); - - if (href) { - if (typeof linksOnPage[href] == 'undefined') { - linksOnPage[href] = [a]; - } - else { - linksOnPage[href].push(a); - } - } - - if (href && processLinkDelta !== false) { - if (typeof processLinkDelta[href] == 'undefined') { - processLinkDelta[href] = [a]; - } - else { - processLinkDelta[href].push(a); - } - } - } - - var repositionTimeout = false; - var resizeTimeout = false; - - function build(callback) { - // build an index of all links on the page - $('a').each(processLink); - - // add tags to known following pages - createLinkTags(linksOnPage); - - // position the tags - positionLinkTags(); - - callback(); - - // check on a regular basis whether new links have appeared. - // we use a timeout instead of an interval to make sure one call is done before - // the next one is triggered - var repositionAfterTimeout; - repositionAfterTimeout = function() { - repositionTimeout = window.setTimeout(function() { - findNewLinks(); - positionLinkTags(repositionAfterTimeout); - }, 1800); - }; - repositionAfterTimeout(); - - // reposition link tags on window resize - $(window).resize(function() { - if (repositionTimeout) { - window.clearTimeout(repositionTimeout); - } - if (resizeTimeout) { - window.clearTimeout(resizeTimeout); - } - resizeTimeout = window.setTimeout(function() { - positionLinkTags(); - repositionAfterTimeout(); - }, 70); - }); - } - - /** Create a batch of link tags */ - function createLinkTags(links) { - var body = $('body'); - for (var i = 0; i < followingPages.length; i++) { - var url = followingPages[i].label; - if (typeof links[url] != 'undefined') { - for (var j = 0; j < links[url].length; j++) { - createLinkTag(links[url][j], url, followingPages[i], body); - } - } - } - } - - /** Create the link tag element */ - function createLinkTag(linkTag, linkUrl, data, body) { - if (typeof linkTag[0].piwikTagElement != 'undefined' && linkTag[0].piwikTagElement !== null) { - // this link tag already has a tag element. happens in rare cases. - return; - } - - linkTag[0].piwikTagElement = true; - - var rate = data.clickRate; - if (rate < 10) { - rate = Math.round(rate * 10) / 10; - } else { - rate = Math.round(rate); - } - - var span = c('span').html(rate + '%'); - var tagElement = c('div', 'LinkTag').append(span).hide(); - body.prepend(tagElement); - - linkTag.add(tagElement).hover(function() { - highlightLink(linkTag, linkUrl, data); - }, function() { - unHighlightLink(linkTag, linkUrl); - }); - - // attach the tag element to the link element. we can't use .data() because jquery - // would remove it when removing the link from the dom. but we still need to find - // the tag element to remove it as well. - linkTag[0].piwikTagElement = tagElement; - } - - /** Position the link tags next to the links */ - function positionLinkTags(callback) { - var url, linkTag, tagElement, offset, top, left, isRight, hasOneChild, inlineChild; - var tagWidth = 36, tagHeight = 21; - var tagsToRemove = []; - - for (var i = 0; i < followingPages.length; i++) { - url = followingPages[i].label; - if (typeof linksOnPage[url] != 'undefined') { - for (var j = 0; j < linksOnPage[url].length; j++) { - linkTag = linksOnPage[url][j]; - tagElement = linkTag[0].piwikTagElement; - - if (linkTag.closest('html').length == 0 || !tagElement) { - // the link has been removed from the dom - if (tagElement) { - tagElement.hide(); - } - // mark for deletion. don't delete it now because we - // are iterating of the array it's in. it will be deleted - // below this for loop. - tagsToRemove.push({ - index1: url, - index2: j - }); - continue; - } - - hasOneChild = checkHasOneChild(linkTag); - inlineChild = false; - if (hasOneChild && linkTag.css('display') != 'block') { - inlineChild = linkTag.children().eq(0); - } - - if (getVisibility(linkTag) == 'hidden' || ( - // in case of hasOneChild: jquery always returns linkTag.is(':visible')=false - !linkTag.is(':visible') && !(hasOneChild && inlineChild && inlineChild.is(':visible')) - )) { - // link is not visible - tagElement.hide(); - continue; - } - - tagElement.attr('class', 'PIS_LinkTag'); // reset class - if (tagElement[0].piwikHighlighted) { - tagElement.addClass('PIS_Highlighted'); - } - - // see comment in highlightLink() - if (hasOneChild && linkTag.find('> img').size() == 1) { - offset = linkTag.find('> img').offset(); - if (offset.left == 0 && offset.top == 0) { - offset = linkTag.offset(); - } - } else if (inlineChild !== false) { - offset = inlineChild.offset(); - } else { - offset = linkTag.offset(); - } - - top = offset.top - tagHeight + 6; - left = offset.left - tagWidth + 10; - - if (isRight = (left < 2)) { - tagElement.addClass('PIS_Right'); - left = offset.left + linkTag.outerWidth() - 10; - } - - if (top < 2) { - tagElement.addClass(isRight ? 'PIS_BottomRight' : 'PIS_Bottom'); - top = offset.top + linkTag.outerHeight() - 6; - } - - tagElement.css({ - top: top + 'px', - left: left + 'px' - }).show(); - - } - } - } - - // walk tagsToRemove from back to front because it contains the indexes in ascending - // order. removing something from the front will impact the indexes that come after- - // wards. this can be avoided by starting in the back. - for (var k = tagsToRemove.length - 1; k >= 0; k--) { - var tagToRemove = tagsToRemove[k]; - linkTag = linksOnPage[tagToRemove.index1][tagToRemove.index2]; - // remove the tag element from the dom - if (linkTag && linkTag[0] && linkTag[0].piwikTagElement) { - tagElement = linkTag[0].piwikTagElement; - if (tagElement[0].piwikHighlighted) { - unHighlightLink(linkTag, tagToRemove.index1); - } - tagElement.remove(); - linkTag[0].piwikTagElement = null; - } - // remove the link from the index - linksOnPage[tagToRemove.index1].splice(tagToRemove.index2, 1); - if (linksOnPage[tagToRemove.index1].length == 0) { - delete linksOnPage[tagToRemove.index1]; - } - } - - if (typeof callback == 'function') { - callback(); - } - } - - /** Get the visibility of an element */ - function getVisibility(el) { - var visibility = el.css('visibility'); - if (visibility == 'inherit') { - el = el.parent(); - if (el.size() > 0) { - return getVisibility(el); - } - } - return visibility; - } - - /** - * Find out whether a link has only one child. Using .children().size() == 1 doesn't work - * because it doesn't take additional text nodes into account. - */ - function checkHasOneChild(linkTag) { - var hasOneChild = (linkTag.children().size() == 1); - if (hasOneChild) { - // if the element contains one tag and some text, hasOneChild is set incorrectly - var contents = linkTag.contents(); - if (contents.size() > 1) { - // find non-empty text nodes - contents = contents.filter(function() { - return this.nodeType == 3 && // text node - $.trim(this.data).length > 0; // contains more than whitespaces - }); - if (contents.size() > 0) { - hasOneChild = false; - } - } - } - return hasOneChild; - } - - /** Check whether new links have been added to the dom */ - function findNewLinks() { - var newLinks = $('a').filter(function() { - return typeof this.piwikDiscovered == 'undefined' || this.piwikDiscovered === null; - }); - - if (newLinks.size() == 0) { - return; - } - - processLinkDelta = {}; - newLinks.each(processLink); - createLinkTags(processLinkDelta); - processLinkDelta = false; - } - - /** Dom elements used for drawing a box around the link */ - var highlightElements = []; - - /** Highlight a link on hover */ - function highlightLink(linkTag, linkUrl, data) { - if (highlightElements.length == 0) { - highlightElements.push(c('div', 'LinkHighlightBoxTop')); - highlightElements.push(c('div', 'LinkHighlightBoxRight')); - highlightElements.push(c('div', 'LinkHighlightBoxLeft')); - - highlightElements.push(c('div', 'LinkHighlightBoxText')); - - var body = $('body'); - for (var i = 0; i < highlightElements.length; i++) { - body.prepend(highlightElements[i].css({display: 'none'})); - } - } - - var width = linkTag.outerWidth(); - - var offset, height; - var hasOneChild = checkHasOneChild(linkTag); - if (hasOneChild && linkTag.find('img').size() == 1) { - // if the tag contains only an , the offset and height methods don't work properly. - // as a result, the box around the image link would be wrong. we use the image to derive - // the offset and height instead of the link to get correct values. - var img = linkTag.find('img'); - offset = img.offset(); - height = img.outerHeight(); - } - if (hasOneChild && linkTag.css('display') != 'block') { - // if the tag is not displayed as block and has only one child, using the child to - // derive the offset and dimensions is more robust. - var child = linkTag.children().eq(0); - offset = child.offset(); - height = child.outerHeight(); - width = child.outerWidth(); - } else { - offset = linkTag.offset(); - height = linkTag.outerHeight(); - } - - highlightElements[0].width(width).css({top: offset.top - 2, left: offset.left}).show(); - highlightElements[1].height(height + 4).css({top: offset.top - 2, left: offset.left + width}).show(); - highlightElements[2].height(height + 4).css({top: offset.top - 2, left: offset.left - 2}).show(); - - var numLinks = linksOnPage[linkUrl].length; - var text; - if (numLinks > 1) { - text = Piwik_Overlay_Translations.get('clicksFromXLinks') - .replace(/%1\$s/, data.referrals) - .replace(/%2\$s/, numLinks); - } else if (data.referrals == 1) { - text = Piwik_Overlay_Translations.get('oneClick'); - } else { - text = Piwik_Overlay_Translations.get('clicks') - .replace(/%s/, data.referrals); - } - - var padding = '  '; - highlightElements[3].html(padding + text + padding).css({ - width: 'auto', - top: offset.top + height, - left: offset.left - 2 - }).show(); - if (highlightElements[3].width() < width + 4) { - // we cannot use minWidth because of IE7 - highlightElements[3].width(width + 4); - } - - for (var j = 0; j < numLinks; j++) { - var tag = linksOnPage[linkUrl][j][0].piwikTagElement; - tag.addClass('PIS_Highlighted'); - tag[0].piwikHighlighted = true; - } - - // Sometimes it fails to remove the notification when the hovered element is removed. - // To make sure we don't display more than one location at a time, we hide all before showing the new one. - Piwik_Overlay_Client.hideNotifications('LinkLocation'); - - // we don't use .data() because jquery would remove the callback when the link tag is removed - linkTag[0].piwikHideNotification = Piwik_Overlay_Client.notification( - Piwik_Overlay_Translations.get('link') + ': ' + linkUrl, 'LinkLocation'); - } - - /** Remove highlight from link */ - function unHighlightLink(linkTag, linkUrl) { - for (var i = 0; i < highlightElements.length; i++) { - highlightElements[i].hide(); - } - - var numLinks = linksOnPage[linkUrl].length; - for (var j = 0; j < numLinks; j++) { - var tag = linksOnPage[linkUrl][j][0].piwikTagElement; - if (tag) { - tag.removeClass('PIS_Highlighted'); - tag[0].piwikHighlighted = false; - } - } - - if ((typeof linkTag[0].piwikHideNotification) == 'function') { - linkTag[0].piwikHideNotification(); - linkTag[0].piwikHideNotification = null; - } - } - - - return { - - /** - * The main method - */ - initialize: function(finishCallback) { - c = Piwik_Overlay_Client.createElement; - Piwik_Overlay_Client.loadScript('plugins/Overlay/client/urlnormalizer.js', function() { - Piwik_Overlay_UrlNormalizer.initialize(); - load(function() { - Piwik_Overlay_UrlNormalizer.setExcludedParameters(excludedParams); - build(function() { - finishCallback(); - }) - }); - }); - }, - - /** - * Remove everything from the dom and terminate timeouts. - * This can be used from the console in order to load a new implementation for debugging afterwards. - * If you add `Piwik_Overlay_FollowingPages.remove();` to the beginning and - * `Piwik_Overlay_FollowingPages.initialize(function(){});` to the end of this file, you can just - * paste it into the console to inject the new implementation. - */ - remove: function() { - for (var i = 0; i < followingPages.length; i++) { - var url = followingPages[i].label; - if (typeof linksOnPage[url] != 'undefined') { - for (var j = 0; j < linksOnPage[url].length; j++) { - var linkTag = linksOnPage[url][j]; - var tagElement = linkTag[0].piwikTagElement; - if (tagElement) { - tagElement.remove(); - } - linkTag[0].piwikTagElement = null; - - $(linkTag).unbind('mouseenter').unbind('mouseleave'); - } - } - } - for (i = 0; i < highlightElements.length; i++) { - highlightElements[i].remove(); - } - if (repositionTimeout) { - window.clearTimeout(repositionTimeout); - } - if (resizeTimeout) { - window.clearTimeout(resizeTimeout); - } - $(window).unbind('resize'); - } - - }; +var Piwik_Overlay_FollowingPages = (function () { + + /** jQuery */ + var $ = jQuery; + + /** Info about the following pages */ + var followingPages = []; + + /** List of excluded get parameters */ + var excludedParams = []; + + /** Index of the links on the page */ + var linksOnPage = {}; + + /** Reference to create element function */ + var c; + + /** Load the following pages */ + function load(callback) { + // normalize current location + var location = window.location.href; + location = Piwik_Overlay_UrlNormalizer.normalize(location); + location = (("https:" == document.location.protocol) ? 'https' : 'http') + '://' + location; + + var excludedParamsLoaded = false; + var followingPagesLoaded = false; + + // load excluded params + Piwik_Overlay_Client.api('getExcludedQueryParameters', function (data) { + for (var i = 0; i < data.length; i++) { + if (typeof data[i] == 'object') { + data[i] = data[i][0]; + } + } + excludedParams = data; + + excludedParamsLoaded = true; + if (followingPagesLoaded) { + callback(); + } + }); + + // load following pages + Piwik_Overlay_Client.api('getFollowingPages', function (data) { + followingPages = data; + processFollowingPages(); + + followingPagesLoaded = true; + if (excludedParamsLoaded) { + callback(); + } + }, 'url=' + encodeURIComponent(location)); + } + + /** Normalize the URLs of following pages and aggregate some stats */ + function processFollowingPages() { + var totalClicks = 0; + for (var i = 0; i < followingPages.length; i++) { + var page = followingPages[i]; + // though the following pages are returned without the prefix, downloads + // and outlinks still have it. + page.label = Piwik_Overlay_UrlNormalizer.removeUrlPrefix(page.label); + totalClicks += followingPages[i].referrals; + } + for (i = 0; i < followingPages.length; i++) { + followingPages[i].clickRate = followingPages[i].referrals / totalClicks * 100; + } + } + + /** + * Build an index of links on the page. + * This function is passed to $('a').each() + */ + var processLinkDelta = false; + + function processLink() { + var a = $(this); + a[0].piwikDiscovered = true; + + var href = a.attr('href'); + href = Piwik_Overlay_UrlNormalizer.normalize(href); + + if (href) { + if (typeof linksOnPage[href] == 'undefined') { + linksOnPage[href] = [a]; + } + else { + linksOnPage[href].push(a); + } + } + + if (href && processLinkDelta !== false) { + if (typeof processLinkDelta[href] == 'undefined') { + processLinkDelta[href] = [a]; + } + else { + processLinkDelta[href].push(a); + } + } + } + + var repositionTimeout = false; + var resizeTimeout = false; + + function build(callback) { + // build an index of all links on the page + $('a').each(processLink); + + // add tags to known following pages + createLinkTags(linksOnPage); + + // position the tags + positionLinkTags(); + + callback(); + + // check on a regular basis whether new links have appeared. + // we use a timeout instead of an interval to make sure one call is done before + // the next one is triggered + var repositionAfterTimeout; + repositionAfterTimeout = function () { + repositionTimeout = window.setTimeout(function () { + findNewLinks(); + positionLinkTags(repositionAfterTimeout); + }, 1800); + }; + repositionAfterTimeout(); + + // reposition link tags on window resize + $(window).resize(function () { + if (repositionTimeout) { + window.clearTimeout(repositionTimeout); + } + if (resizeTimeout) { + window.clearTimeout(resizeTimeout); + } + resizeTimeout = window.setTimeout(function () { + positionLinkTags(); + repositionAfterTimeout(); + }, 70); + }); + } + + /** Create a batch of link tags */ + function createLinkTags(links) { + var body = $('body'); + for (var i = 0; i < followingPages.length; i++) { + var url = followingPages[i].label; + if (typeof links[url] != 'undefined') { + for (var j = 0; j < links[url].length; j++) { + createLinkTag(links[url][j], url, followingPages[i], body); + } + } + } + } + + /** Create the link tag element */ + function createLinkTag(linkTag, linkUrl, data, body) { + if (typeof linkTag[0].piwikTagElement != 'undefined' && linkTag[0].piwikTagElement !== null) { + // this link tag already has a tag element. happens in rare cases. + return; + } + + linkTag[0].piwikTagElement = true; + + var rate = data.clickRate; + if (rate < 10) { + rate = Math.round(rate * 10) / 10; + } else { + rate = Math.round(rate); + } + + var span = c('span').html(rate + '%'); + var tagElement = c('div', 'LinkTag').append(span).hide(); + body.prepend(tagElement); + + linkTag.add(tagElement).hover(function () { + highlightLink(linkTag, linkUrl, data); + }, function () { + unHighlightLink(linkTag, linkUrl); + }); + + // attach the tag element to the link element. we can't use .data() because jquery + // would remove it when removing the link from the dom. but we still need to find + // the tag element to remove it as well. + linkTag[0].piwikTagElement = tagElement; + } + + /** Position the link tags next to the links */ + function positionLinkTags(callback) { + var url, linkTag, tagElement, offset, top, left, isRight, hasOneChild, inlineChild; + var tagWidth = 36, tagHeight = 21; + var tagsToRemove = []; + + for (var i = 0; i < followingPages.length; i++) { + url = followingPages[i].label; + if (typeof linksOnPage[url] != 'undefined') { + for (var j = 0; j < linksOnPage[url].length; j++) { + linkTag = linksOnPage[url][j]; + tagElement = linkTag[0].piwikTagElement; + + if (linkTag.closest('html').length == 0 || !tagElement) { + // the link has been removed from the dom + if (tagElement) { + tagElement.hide(); + } + // mark for deletion. don't delete it now because we + // are iterating of the array it's in. it will be deleted + // below this for loop. + tagsToRemove.push({ + index1: url, + index2: j + }); + continue; + } + + hasOneChild = checkHasOneChild(linkTag); + inlineChild = false; + if (hasOneChild && linkTag.css('display') != 'block') { + inlineChild = linkTag.children().eq(0); + } + + if (getVisibility(linkTag) == 'hidden' || ( + // in case of hasOneChild: jquery always returns linkTag.is(':visible')=false + !linkTag.is(':visible') && !(hasOneChild && inlineChild && inlineChild.is(':visible')) + )) { + // link is not visible + tagElement.hide(); + continue; + } + + tagElement.attr('class', 'PIS_LinkTag'); // reset class + if (tagElement[0].piwikHighlighted) { + tagElement.addClass('PIS_Highlighted'); + } + + // see comment in highlightLink() + if (hasOneChild && linkTag.find('> img').size() == 1) { + offset = linkTag.find('> img').offset(); + if (offset.left == 0 && offset.top == 0) { + offset = linkTag.offset(); + } + } else if (inlineChild !== false) { + offset = inlineChild.offset(); + } else { + offset = linkTag.offset(); + } + + top = offset.top - tagHeight + 6; + left = offset.left - tagWidth + 10; + + if (isRight = (left < 2)) { + tagElement.addClass('PIS_Right'); + left = offset.left + linkTag.outerWidth() - 10; + } + + if (top < 2) { + tagElement.addClass(isRight ? 'PIS_BottomRight' : 'PIS_Bottom'); + top = offset.top + linkTag.outerHeight() - 6; + } + + tagElement.css({ + top: top + 'px', + left: left + 'px' + }).show(); + + } + } + } + + // walk tagsToRemove from back to front because it contains the indexes in ascending + // order. removing something from the front will impact the indexes that come after- + // wards. this can be avoided by starting in the back. + for (var k = tagsToRemove.length - 1; k >= 0; k--) { + var tagToRemove = tagsToRemove[k]; + linkTag = linksOnPage[tagToRemove.index1][tagToRemove.index2]; + // remove the tag element from the dom + if (linkTag && linkTag[0] && linkTag[0].piwikTagElement) { + tagElement = linkTag[0].piwikTagElement; + if (tagElement[0].piwikHighlighted) { + unHighlightLink(linkTag, tagToRemove.index1); + } + tagElement.remove(); + linkTag[0].piwikTagElement = null; + } + // remove the link from the index + linksOnPage[tagToRemove.index1].splice(tagToRemove.index2, 1); + if (linksOnPage[tagToRemove.index1].length == 0) { + delete linksOnPage[tagToRemove.index1]; + } + } + + if (typeof callback == 'function') { + callback(); + } + } + + /** Get the visibility of an element */ + function getVisibility(el) { + var visibility = el.css('visibility'); + if (visibility == 'inherit') { + el = el.parent(); + if (el.size() > 0) { + return getVisibility(el); + } + } + return visibility; + } + + /** + * Find out whether a link has only one child. Using .children().size() == 1 doesn't work + * because it doesn't take additional text nodes into account. + */ + function checkHasOneChild(linkTag) { + var hasOneChild = (linkTag.children().size() == 1); + if (hasOneChild) { + // if the element contains one tag and some text, hasOneChild is set incorrectly + var contents = linkTag.contents(); + if (contents.size() > 1) { + // find non-empty text nodes + contents = contents.filter(function () { + return this.nodeType == 3 && // text node + $.trim(this.data).length > 0; // contains more than whitespaces + }); + if (contents.size() > 0) { + hasOneChild = false; + } + } + } + return hasOneChild; + } + + /** Check whether new links have been added to the dom */ + function findNewLinks() { + var newLinks = $('a').filter(function () { + return typeof this.piwikDiscovered == 'undefined' || this.piwikDiscovered === null; + }); + + if (newLinks.size() == 0) { + return; + } + + processLinkDelta = {}; + newLinks.each(processLink); + createLinkTags(processLinkDelta); + processLinkDelta = false; + } + + /** Dom elements used for drawing a box around the link */ + var highlightElements = []; + + /** Highlight a link on hover */ + function highlightLink(linkTag, linkUrl, data) { + if (highlightElements.length == 0) { + highlightElements.push(c('div', 'LinkHighlightBoxTop')); + highlightElements.push(c('div', 'LinkHighlightBoxRight')); + highlightElements.push(c('div', 'LinkHighlightBoxLeft')); + + highlightElements.push(c('div', 'LinkHighlightBoxText')); + + var body = $('body'); + for (var i = 0; i < highlightElements.length; i++) { + body.prepend(highlightElements[i].css({display: 'none'})); + } + } + + var width = linkTag.outerWidth(); + + var offset, height; + var hasOneChild = checkHasOneChild(linkTag); + if (hasOneChild && linkTag.find('img').size() == 1) { + // if the tag contains only an , the offset and height methods don't work properly. + // as a result, the box around the image link would be wrong. we use the image to derive + // the offset and height instead of the link to get correct values. + var img = linkTag.find('img'); + offset = img.offset(); + height = img.outerHeight(); + } + if (hasOneChild && linkTag.css('display') != 'block') { + // if the tag is not displayed as block and has only one child, using the child to + // derive the offset and dimensions is more robust. + var child = linkTag.children().eq(0); + offset = child.offset(); + height = child.outerHeight(); + width = child.outerWidth(); + } else { + offset = linkTag.offset(); + height = linkTag.outerHeight(); + } + + highlightElements[0].width(width).css({top: offset.top - 2, left: offset.left}).show(); + highlightElements[1].height(height + 4).css({top: offset.top - 2, left: offset.left + width}).show(); + highlightElements[2].height(height + 4).css({top: offset.top - 2, left: offset.left - 2}).show(); + + var numLinks = linksOnPage[linkUrl].length; + var text; + if (numLinks > 1) { + text = Piwik_Overlay_Translations.get('clicksFromXLinks') + .replace(/%1\$s/, data.referrals) + .replace(/%2\$s/, numLinks); + } else if (data.referrals == 1) { + text = Piwik_Overlay_Translations.get('oneClick'); + } else { + text = Piwik_Overlay_Translations.get('clicks') + .replace(/%s/, data.referrals); + } + + var padding = '  '; + highlightElements[3].html(padding + text + padding).css({ + width: 'auto', + top: offset.top + height, + left: offset.left - 2 + }).show(); + if (highlightElements[3].width() < width + 4) { + // we cannot use minWidth because of IE7 + highlightElements[3].width(width + 4); + } + + for (var j = 0; j < numLinks; j++) { + var tag = linksOnPage[linkUrl][j][0].piwikTagElement; + tag.addClass('PIS_Highlighted'); + tag[0].piwikHighlighted = true; + } + + // Sometimes it fails to remove the notification when the hovered element is removed. + // To make sure we don't display more than one location at a time, we hide all before showing the new one. + Piwik_Overlay_Client.hideNotifications('LinkLocation'); + + // we don't use .data() because jquery would remove the callback when the link tag is removed + linkTag[0].piwikHideNotification = Piwik_Overlay_Client.notification( + Piwik_Overlay_Translations.get('link') + ': ' + linkUrl, 'LinkLocation'); + } + + /** Remove highlight from link */ + function unHighlightLink(linkTag, linkUrl) { + for (var i = 0; i < highlightElements.length; i++) { + highlightElements[i].hide(); + } + + var numLinks = linksOnPage[linkUrl].length; + for (var j = 0; j < numLinks; j++) { + var tag = linksOnPage[linkUrl][j][0].piwikTagElement; + if (tag) { + tag.removeClass('PIS_Highlighted'); + tag[0].piwikHighlighted = false; + } + } + + if ((typeof linkTag[0].piwikHideNotification) == 'function') { + linkTag[0].piwikHideNotification(); + linkTag[0].piwikHideNotification = null; + } + } + + + return { + + /** + * The main method + */ + initialize: function (finishCallback) { + c = Piwik_Overlay_Client.createElement; + Piwik_Overlay_Client.loadScript('plugins/Overlay/client/urlnormalizer.js', function () { + Piwik_Overlay_UrlNormalizer.initialize(); + load(function () { + Piwik_Overlay_UrlNormalizer.setExcludedParameters(excludedParams); + build(function () { + finishCallback(); + }) + }); + }); + }, + + /** + * Remove everything from the dom and terminate timeouts. + * This can be used from the console in order to load a new implementation for debugging afterwards. + * If you add `Piwik_Overlay_FollowingPages.remove();` to the beginning and + * `Piwik_Overlay_FollowingPages.initialize(function(){});` to the end of this file, you can just + * paste it into the console to inject the new implementation. + */ + remove: function () { + for (var i = 0; i < followingPages.length; i++) { + var url = followingPages[i].label; + if (typeof linksOnPage[url] != 'undefined') { + for (var j = 0; j < linksOnPage[url].length; j++) { + var linkTag = linksOnPage[url][j]; + var tagElement = linkTag[0].piwikTagElement; + if (tagElement) { + tagElement.remove(); + } + linkTag[0].piwikTagElement = null; + + $(linkTag).unbind('mouseenter').unbind('mouseleave'); + } + } + } + for (i = 0; i < highlightElements.length; i++) { + highlightElements[i].remove(); + } + if (repositionTimeout) { + window.clearTimeout(repositionTimeout); + } + if (resizeTimeout) { + window.clearTimeout(resizeTimeout); + } + $(window).unbind('resize'); + } + + }; })(); \ No newline at end of file diff --git a/plugins/Overlay/client/translations.js b/plugins/Overlay/client/translations.js index 1f9e466e6b..9665b920a9 100644 --- a/plugins/Overlay/client/translations.js +++ b/plugins/Overlay/client/translations.js @@ -1,32 +1,30 @@ +var Piwik_Overlay_Translations = (function () { + /** Translations strings */ + var translations = []; + + return { + + /** + * Initialize translations module. + * Callback is triggered when data is available. + */ + initialize: function (callback) { + // Load translation data + Piwik_Overlay_Client.api('getTranslations', function (data) { + translations = data[0]; + callback(); + }); + }, + + /** Get translation string */ + get: function (identifier) { + if (typeof translations[identifier] == 'undefined') { + return identifier; + } + return translations[identifier]; + } + + }; -var Piwik_Overlay_Translations = (function() { - - /** Translations strings */ - var translations = []; - - return { - - /** - * Initialize translations module. - * Callback is triggered when data is available. - */ - initialize: function(callback) { - // Load translation data - Piwik_Overlay_Client.api('getTranslations', function(data) { - translations = data[0]; - callback(); - }); - }, - - /** Get translation string */ - get: function(identifier) { - if (typeof translations[identifier] == 'undefined') { - return identifier; - } - return translations[identifier]; - } - - }; - })(); diff --git a/plugins/Overlay/client/urlnormalizer.js b/plugins/Overlay/client/urlnormalizer.js index 38423bab60..72db61269b 100644 --- a/plugins/Overlay/client/urlnormalizer.js +++ b/plugins/Overlay/client/urlnormalizer.js @@ -1,199 +1,198 @@ - /** * URL NORMALIZER * This utility preprocesses both the URLs in the document and * from the Piwik logs in order to make matching possible. */ -var Piwik_Overlay_UrlNormalizer = (function() { - - /** Base href of the current document */ - var baseHref = false; - - /** Url of current folder */ - var currentFolder; - - /** The current domain */ - var currentDomain; - - /** Regular expressions for parameters to be excluded when matching links on the page */ - var excludedParamsRegEx = []; - - /** - * Basic normalizations for domain names - * - remove protocol and www from absolute urls - * - add a trailing slash to urls without a path - * - * Returns array - * 0: normalized url - * 1: true, if url was absolute (if not, no normalization was performed) - */ - function normalizeDomain(url) { - if (url === null) { - return ''; - } - - var absolute = false; - - // remove protocol - if (url.substring(0, 7) == 'http://') { - absolute = true; - url = url.substring(7, url.length); - } else if (url.substring(0, 8) == 'https://') { - absolute = true; - url = url.substring(8, url.length); - } - - if (absolute) { - // remove www. - url = removeWww(url); - - // add slash to domain names - if (url.indexOf('/') == -1) { - url += '/'; - } - } - - return [url, absolute]; - } - - /** Remove www. from a domain */ - function removeWww(domain) { - if (domain.substring(0, 4) == 'www.') { - return domain.substring(4, domain.length); - } - return domain; - } - - return { - - initialize: function() { - this.setCurrentDomain(document.location.hostname); - this.setCurrentUrl(window.location.href); - - var head = document.getElementsByTagName('head'); - if (head.length) { - var base = head[0].getElementsByTagName('base'); - if (base.length && base[0].href) { - this.setBaseHref(base[0].href); - } - } - }, - - /** - * Explicitly set domain (for testing) - */ - setCurrentDomain: function(pCurrentDomain) { - currentDomain = removeWww(pCurrentDomain); - }, - - /** - * Explicitly set current url (for testing) - */ - setCurrentUrl: function(url) { - var index = url.lastIndexOf('/'); - if (index != url.length - 1) { - currentFolder = url.substring(0, index + 1); - } else { - currentFolder = url; - } - currentFolder = normalizeDomain(currentFolder)[0]; - }, - - /** - * Explicitly set base href (for testing) - */ - setBaseHref: function(pBaseHref) { - if (!pBaseHref) { - baseHref = false; - } else { - baseHref = normalizeDomain(pBaseHref)[0]; - } - }, - - /** - * Set the parameters to be excluded when matching links on the page - */ - setExcludedParameters: function(pExcludedParams) { - excludedParamsRegEx = []; - for (var i = 0; i < pExcludedParams.length; i++) { - var paramString = pExcludedParams[i]; - excludedParamsRegEx.push(new RegExp('&' + paramString + '=([^&#]*)', 'ig')); - } - }, - - /** - * Remove the protocol and the prefix of a URL - */ - removeUrlPrefix: function(url) { - return normalizeDomain(url)[0]; - }, - - /** - * Normalize URL - * Can be an absolute or a relative URL - */ - normalize: function(url) { - if (!url) { - return ''; - } - - // ignore urls starting with # - if (url.substring(0, 1) == '#') { - return ''; - } - - // basic normalizations for absolute urls - var normalized = normalizeDomain(url); - url = normalized[0]; - - var absolute = normalized[1]; - - if (!absolute) { - /** relative url */ - if (url.substring(0, 1) == '/') { - // relative to domain root - url = currentDomain + url; - } else if (baseHref) { - // relative to base href - url = baseHref + url; - } else { - // relative to current folder - url = currentFolder + url; - } - } - - // replace multiple / with a single / - url = url.replace(/\/\/+/g, '/'); - - // handle ./ and ../ - var parts = url.split('/'); - var urlArr = []; - for (var i = 0; i < parts.length; i++) { - if (parts[i] == '.') { - // ignore - } - else if (parts[i] == '..') { - urlArr.pop(); - } - else { - urlArr.push(parts[i]); - } - } - url = urlArr.join('/'); - - // remove ignored parameters - url = url.replace(/\?/, '?&'); - for (i = 0; i < excludedParamsRegEx.length; i++) { - var regEx = excludedParamsRegEx[i]; - url = url.replace(regEx, ''); - } - url = url.replace(/\?&/, '?'); - url = url.replace(/\?#/, '#'); - url = url.replace(/\?$/, ''); - - return url; - } - - }; - +var Piwik_Overlay_UrlNormalizer = (function () { + + /** Base href of the current document */ + var baseHref = false; + + /** Url of current folder */ + var currentFolder; + + /** The current domain */ + var currentDomain; + + /** Regular expressions for parameters to be excluded when matching links on the page */ + var excludedParamsRegEx = []; + + /** + * Basic normalizations for domain names + * - remove protocol and www from absolute urls + * - add a trailing slash to urls without a path + * + * Returns array + * 0: normalized url + * 1: true, if url was absolute (if not, no normalization was performed) + */ + function normalizeDomain(url) { + if (url === null) { + return ''; + } + + var absolute = false; + + // remove protocol + if (url.substring(0, 7) == 'http://') { + absolute = true; + url = url.substring(7, url.length); + } else if (url.substring(0, 8) == 'https://') { + absolute = true; + url = url.substring(8, url.length); + } + + if (absolute) { + // remove www. + url = removeWww(url); + + // add slash to domain names + if (url.indexOf('/') == -1) { + url += '/'; + } + } + + return [url, absolute]; + } + + /** Remove www. from a domain */ + function removeWww(domain) { + if (domain.substring(0, 4) == 'www.') { + return domain.substring(4, domain.length); + } + return domain; + } + + return { + + initialize: function () { + this.setCurrentDomain(document.location.hostname); + this.setCurrentUrl(window.location.href); + + var head = document.getElementsByTagName('head'); + if (head.length) { + var base = head[0].getElementsByTagName('base'); + if (base.length && base[0].href) { + this.setBaseHref(base[0].href); + } + } + }, + + /** + * Explicitly set domain (for testing) + */ + setCurrentDomain: function (pCurrentDomain) { + currentDomain = removeWww(pCurrentDomain); + }, + + /** + * Explicitly set current url (for testing) + */ + setCurrentUrl: function (url) { + var index = url.lastIndexOf('/'); + if (index != url.length - 1) { + currentFolder = url.substring(0, index + 1); + } else { + currentFolder = url; + } + currentFolder = normalizeDomain(currentFolder)[0]; + }, + + /** + * Explicitly set base href (for testing) + */ + setBaseHref: function (pBaseHref) { + if (!pBaseHref) { + baseHref = false; + } else { + baseHref = normalizeDomain(pBaseHref)[0]; + } + }, + + /** + * Set the parameters to be excluded when matching links on the page + */ + setExcludedParameters: function (pExcludedParams) { + excludedParamsRegEx = []; + for (var i = 0; i < pExcludedParams.length; i++) { + var paramString = pExcludedParams[i]; + excludedParamsRegEx.push(new RegExp('&' + paramString + '=([^&#]*)', 'ig')); + } + }, + + /** + * Remove the protocol and the prefix of a URL + */ + removeUrlPrefix: function (url) { + return normalizeDomain(url)[0]; + }, + + /** + * Normalize URL + * Can be an absolute or a relative URL + */ + normalize: function (url) { + if (!url) { + return ''; + } + + // ignore urls starting with # + if (url.substring(0, 1) == '#') { + return ''; + } + + // basic normalizations for absolute urls + var normalized = normalizeDomain(url); + url = normalized[0]; + + var absolute = normalized[1]; + + if (!absolute) { + /** relative url */ + if (url.substring(0, 1) == '/') { + // relative to domain root + url = currentDomain + url; + } else if (baseHref) { + // relative to base href + url = baseHref + url; + } else { + // relative to current folder + url = currentFolder + url; + } + } + + // replace multiple / with a single / + url = url.replace(/\/\/+/g, '/'); + + // handle ./ and ../ + var parts = url.split('/'); + var urlArr = []; + for (var i = 0; i < parts.length; i++) { + if (parts[i] == '.') { + // ignore + } + else if (parts[i] == '..') { + urlArr.pop(); + } + else { + urlArr.push(parts[i]); + } + } + url = urlArr.join('/'); + + // remove ignored parameters + url = url.replace(/\?/, '?&'); + for (i = 0; i < excludedParamsRegEx.length; i++) { + var regEx = excludedParamsRegEx[i]; + url = url.replace(regEx, ''); + } + url = url.replace(/\?&/, '?'); + url = url.replace(/\?#/, '#'); + url = url.replace(/\?$/, ''); + + return url; + } + + }; + })(); \ No newline at end of file diff --git a/plugins/Overlay/templates/error_wrong_domain.tpl b/plugins/Overlay/templates/error_wrong_domain.tpl index 49bd01ed20..4cb400ecce 100644 --- a/plugins/Overlay/templates/error_wrong_domain.tpl +++ b/plugins/Overlay/templates/error_wrong_domain.tpl @@ -1,36 +1,38 @@ - - - - - - - + + {/literal} + + -

{$message}

-

{$troubleshoot}

+

{$message}

+ +

{$troubleshoot}

\ No newline at end of file diff --git a/plugins/Overlay/templates/helper.js b/plugins/Overlay/templates/helper.js index ee62cd60aa..1342bf79f0 100644 --- a/plugins/Overlay/templates/helper.js +++ b/plugins/Overlay/templates/helper.js @@ -6,26 +6,26 @@ */ var Overlay_Helper = { - - /** Encode the iframe url to put it behind the hash in sidebar mode */ - encodeFrameUrl: function(url) { - // url encode + replace % with $ to make sure that browsers don't break the encoding - return encodeURIComponent(url).replace(/%/g, '$') - }, - - /** Decode the url after reading it from the hash */ - decodeFrameUrl: function(url) { - // reverse encodeFrameUrl() - return decodeURIComponent(url.replace(/\$/g, '%')); - }, - - /** Get the url to launch overlay */ - getOverlayLink: function(idSite, period, date, link) { - var url = 'index.php?module=Overlay&period=' + period + '&date=' + date + '&idSite=' + idSite; - if (link) { - url += '#l=' + Overlay_Helper.encodeFrameUrl(link); - } - return url; - } - + + /** Encode the iframe url to put it behind the hash in sidebar mode */ + encodeFrameUrl: function (url) { + // url encode + replace % with $ to make sure that browsers don't break the encoding + return encodeURIComponent(url).replace(/%/g, '$') + }, + + /** Decode the url after reading it from the hash */ + decodeFrameUrl: function (url) { + // reverse encodeFrameUrl() + return decodeURIComponent(url.replace(/\$/g, '%')); + }, + + /** Get the url to launch overlay */ + getOverlayLink: function (idSite, period, date, link) { + var url = 'index.php?module=Overlay&period=' + period + '&date=' + date + '&idSite=' + idSite; + if (link) { + url += '#l=' + Overlay_Helper.encodeFrameUrl(link); + } + return url; + } + }; \ No newline at end of file diff --git a/plugins/Overlay/templates/index.css b/plugins/Overlay/templates/index.css index 154546eeeb..f48a94ec35 100644 --- a/plugins/Overlay/templates/index.css +++ b/plugins/Overlay/templates/index.css @@ -1,144 +1,143 @@ - html { - width: 100%; - height: 100%; - /** hide scroll bar in ie7 */ - *overflow: auto; + width: 100%; + height: 100%; + /** hide scroll bar in ie7 */ + *overflow: auto; } body { - width: 100%; - height: 100%; - overflow: hidden; + width: 100%; + height: 100%; + overflow: hidden; } body #header { - margin-left: -12px; + margin-left: -12px; } body #logo { - margin-top: 5px; + margin-top: 5px; } body h1 { - font-size: 15px; - font-weight: normal; - margin: -3px 0 0 0; - padding: 0 0 0 5px; + font-size: 15px; + font-weight: normal; + margin: -3px 0 0 0; + padding: 0 0 0 5px; } #Overlay_DateRangeSelection { - padding: 30px 0 20px 23px; - background: url(../../../themes/default/images/icon-calendar.gif) 2px 33px no-repeat; - margin-left: 5px; + padding: 30px 0 20px 23px; + background: url(../../../themes/default/images/icon-calendar.gif) 2px 33px no-repeat; + margin-left: 5px; } #Overlay_Location { - width: 200px; - padding: 0 0 30px 0; - margin-left: 5px; - font-size: 12px; + width: 200px; + padding: 0 0 30px 0; + margin-left: 5px; + font-size: 12px; } #Overlay_Loading { - background: url(../../../themes/default/images/loading-blue.gif) no-repeat center 10px; - width: 190px; - padding-top: 30px; - margin-top: 30px; - text-align: center; - display: none; + background: url(../../../themes/default/images/loading-blue.gif) no-repeat center 10px; + width: 190px; + padding-top: 30px; + margin-top: 30px; + text-align: center; + display: none; } #Overlay_Sidebar { - width: 200px; - margin-left: 5px; + width: 200px; + margin-left: 5px; } #Overlay_Sidebar h2 { - font-size: 15px; - margin: 0; - padding: 0 0 8px 23px; - color: #255792; + font-size: 15px; + margin: 0; + padding: 0 0 8px 23px; + color: #255792; } a#Overlay_FullScreen, a#Overlay_RowEvolution, a#Overlay_Transitions { - display: block; - color: #255792; - font-size: 13px; - line-height: 15px; - margin: 0 0 0 5px; - padding: 8px 0 3px 25px; - text-decoration: none; + display: block; + color: #255792; + font-size: 13px; + line-height: 15px; + margin: 0 0 0 5px; + padding: 8px 0 3px 25px; + text-decoration: none; } a#Overlay_RowEvolution { - margin-top: 20px; - background: url(../../../themes/default/images/row_evolution_hover.png) no-repeat 0 7px; + margin-top: 20px; + background: url(../../../themes/default/images/row_evolution_hover.png) no-repeat 0 7px; } a#Overlay_Transitions { - background: url(../../Transitions/templates/transitions_icon_hover.png) no-repeat 0 6px; + background: url(../../Transitions/templates/transitions_icon_hover.png) no-repeat 0 6px; } a#Overlay_FullScreen { - margin-top: 20px; - background: url(../../../themes/default/images/fullscreen.png) no-repeat 3px 8px; + margin-top: 20px; + background: url(../../../themes/default/images/fullscreen.png) no-repeat 3px 8px; } a#Overlay_FullScreen:hover, a#Overlay_RowEvolution:hover, a#Overlay_Transitions:hover { - color: #E87500; + color: #E87500; } #Overlay_Main { - margin-left: 220px; - position: absolute; - top: 0; + margin-left: 220px; + position: absolute; + top: 0; } #Overlay_Iframe { - border-left: 2px solid #ddd; + border-left: 2px solid #ddd; } .Overlay_Metric { - font-size: 12px; - padding-bottom: 4px; + font-size: 12px; + padding-bottom: 4px; } .Overlay_MetricValue { - font-size: 14px; - font-weight: bold; + font-size: 14px; + font-weight: bold; } .Overlay_NoData { - font-size: 12px; - color: #7E7363; + font-size: 12px; + color: #7E7363; } h2.Overlay_MainMetrics { - background: url(./info.png) 0 1px no-repeat + background: url(./info.png) 0 1px no-repeat } body .piwik-tooltip.Overlay_Tooltip { - font-size: 11px; - padding: 3px 5px 3px 6px; + font-size: 11px; + padding: 3px 5px 3px 6px; } #Overlay_NoFrame { - padding: 20px 0 40px 2px + padding: 20px 0 40px 2px } #Overlay_Error_NotLoading { - width: 190px; - display: none; - margin: 20px 0 0 5px; - font-size: 14px; + width: 190px; + display: none; + margin: 20px 0 0 5px; + font-size: 14px; } #Overlay_Error_NotLoading span { - color: #E87500; - font-weight: bold; + color: #E87500; + font-weight: bold; } \ No newline at end of file diff --git a/plugins/Overlay/templates/index.js b/plugins/Overlay/templates/index.js index 62ff3bb513..6ff4ee8ed5 100644 --- a/plugins/Overlay/templates/index.js +++ b/plugins/Overlay/templates/index.js @@ -5,37 +5,37 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ -var Piwik_Overlay = (function() { +var Piwik_Overlay = (function () { - var $body, $iframe, $sidebar, $main, $location, $loading, $errorNotLoading; - var $rowEvolutionLink, $transitionsLink, $fullScreenLink; + var $body, $iframe, $sidebar, $main, $location, $loading, $errorNotLoading; + var $rowEvolutionLink, $transitionsLink, $fullScreenLink; - var idSite, period, date; + var idSite, period, date; - var errorTimeout = false; + var errorTimeout = false; - var iframeSrcBase; - var iframeDomain = ''; - var iframeCurrentPage = ''; - var iframeCurrentPageNormalized = ''; - var iframeCurrentActionLabel = ''; - var updateComesFromInsideFrame = false; + var iframeSrcBase; + var iframeDomain = ''; + var iframeCurrentPage = ''; + var iframeCurrentPageNormalized = ''; + var iframeCurrentActionLabel = ''; + var updateComesFromInsideFrame = false; - /** Load the sidebar for a url */ - function loadSidebar(currentUrl) { - showLoading(); + /** Load the sidebar for a url */ + function loadSidebar(currentUrl) { + showLoading(); - $location.html(' ').unbind('mouseenter').unbind('mouseleave'); + $location.html(' ').unbind('mouseenter').unbind('mouseleave'); - iframeCurrentPage = currentUrl; - iframeDomain = currentUrl.match(/http(s)?:\/\/(www\.)?([^\/]*)/i)[3]; + iframeCurrentPage = currentUrl; + iframeDomain = currentUrl.match(/http(s)?:\/\/(www\.)?([^\/]*)/i)[3]; globalAjaxQueue.abort(); var ajaxRequest = new ajaxHelper(); ajaxRequest.addParams({ - module: 'Overlay', - action: 'renderSidebar', + module: 'Overlay', + action: 'renderSidebar', currentUrl: currentUrl }, 'get'); ajaxRequest.setCallback( @@ -52,9 +52,9 @@ var Piwik_Overlay = (function() { $location.html($responseLocation.html()).show(); $responseLocation.remove(); - var $locationSpan = $location.find('span'); - $locationSpan.html(piwikHelper.addBreakpointsToUrl($locationSpan.text())); - $locationSpan.hover(function () { + var $locationSpan = $location.find('span'); + $locationSpan.html(piwikHelper.addBreakpointsToUrl($locationSpan.text())); + $locationSpan.hover(function () { if (iframeDomain) { // use addBreakpointsToUrl because it also encoded html entities Piwik_Tooltip.show('' + Piwik_Overlay_Translations.domain + ': ' + @@ -74,187 +74,187 @@ var Piwik_Overlay = (function() { ); ajaxRequest.setFormat('html'); ajaxRequest.send(false); - } - - /** Adjust the dimensions of the iframe */ - function adjustDimensions() { - $iframe.height($(window).height()); - $iframe.width($body.width() - $iframe.offset().left - 2); // -2 because of 2px border - } - - /** Display the loading message and hide other containers */ - function showLoading() { - $loading.show(); - - $sidebar.hide(); - $location.hide(); - - $fullScreenLink.hide(); - $rowEvolutionLink.hide(); - $transitionsLink.hide(); - - $errorNotLoading.hide(); - - // Start a timeout that shows an error when nothing is loaded - if (errorTimeout) { - window.clearTimeout(errorTimeout); - } - errorTimeout = window.setTimeout(function() { - hideLoading(); - $errorNotLoading.show(); - }, 9000); - } - - /** Hide the loading message */ - function hideLoading() { - if (errorTimeout) { - window.clearTimeout(errorTimeout); - errorTimeout = false; - } - $loading.hide(); - $fullScreenLink.show(); - } - - /** $.history callback for hash change */ - function hashChangeCallback(urlHash) { - var location = broadcast.getParamValue('l', urlHash); - location = Overlay_Helper.decodeFrameUrl(location); - - if (!updateComesFromInsideFrame) { - var iframeUrl = iframeSrcBase; - if (location) { - iframeUrl += '#' + location; - } - $iframe.attr('src', iframeUrl); - showLoading(); - } else { - loadSidebar(location); - } - - updateComesFromInsideFrame = false; - } - - return { - - /** This method is called when Overlay loads (from index.tpl) */ - init: function(iframeSrc, pIdSite, pPeriod, pDate) { - iframeSrcBase = iframeSrc; - idSite = pIdSite; - period = pPeriod; - date = pDate; - - $body = $('body'); - $iframe = $('#Overlay_Iframe'); - $sidebar = $('#Overlay_Sidebar'); - $location = $('#Overlay_Location'); - $main = $('#Overlay_Main'); - $loading = $('#Overlay_Loading'); - $errorNotLoading = $('#Overlay_Error_NotLoading'); - - $rowEvolutionLink = $('#Overlay_RowEvolution'); - $transitionsLink = $('#Overlay_Transitions'); - $fullScreenLink = $('#Overlay_FullScreen'); - - adjustDimensions(); - - showLoading(); - - // apply initial dimensions - window.setTimeout(function() { - adjustDimensions(); - }, 50); - - // handle window resize - // we manipulate broadcast.pageload because it unbinds all resize events on window - var originalPageload = broadcast.pageload; - broadcast.pageload = function(hash) { - originalPageload(hash); - $(window).resize(function() { - adjustDimensions(); - }); - }; - $(window).resize(function() { - adjustDimensions(); - }); - - // handle hash change - broadcast.loadAjaxContent = hashChangeCallback; - broadcast.init(); - - if (window.location.href.split('#').length == 1) { - // if there's no hash, broadcast won't trigger the callback - we have to do it here - hashChangeCallback(''); - } - - // handle date selection - var $select = $('select#Overlay_DateRangeSelect').change(function() { - var parts = $(this).val().split(';'); - if (parts.length == 2) { - period = parts[0]; - date = parts[1]; - window.location.href = Overlay_Helper.getOverlayLink(idSite, period, date, iframeCurrentPage); - } - }); - - var optionMatchFound = false; - $select.find('option').each(function() { - if ($(this).val() == period + ';' + date) { - $(this).attr('selected', 'selected'); - optionMatchFound = true; - } - }); - - if (!optionMatchFound) { - $select.prepend('
 
@@ -45,17 +47,17 @@
- +
diff --git a/plugins/Overlay/templates/index_noframe.tpl b/plugins/Overlay/templates/index_noframe.tpl index 0897f7cd9c..af2d08ab3d 100644 --- a/plugins/Overlay/templates/index_noframe.tpl +++ b/plugins/Overlay/templates/index_noframe.tpl @@ -2,23 +2,23 @@

{'Overlay_Overlay'|translate|escape:'html'}

- - + + window.location.href = newLocation; + + {/literal} +
diff --git a/plugins/Overlay/templates/notify_parent_iframe.tpl b/plugins/Overlay/templates/notify_parent_iframe.tpl index 4e4dbede4e..19327445a1 100644 --- a/plugins/Overlay/templates/notify_parent_iframe.tpl +++ b/plugins/Overlay/templates/notify_parent_iframe.tpl @@ -1,14 +1,14 @@ - + - + \ No newline at end of file diff --git a/plugins/Overlay/templates/rowaction.js b/plugins/Overlay/templates/rowaction.js index b73cd27c57..ff525c1e38 100644 --- a/plugins/Overlay/templates/rowaction.js +++ b/plugins/Overlay/templates/rowaction.js @@ -10,52 +10,52 @@ */ function DataTable_RowActions_Overlay(dataTable) { - this.dataTable = dataTable; + this.dataTable = dataTable; } DataTable_RowActions_Overlay.prototype = new DataTable_RowAction; -DataTable_RowActions_Overlay.prototype.onClick = function(actionA, tr, e) { - if (!actionA.data('overlay-manipulated')) { - actionA.data('overlay-manipulated', 1); - - var link = tr.find('> td:first > a').attr('href'); - link = $(' -
- {'PDFReports_DescriptionOnFirstPage'|translate} -
- - - - {'PDFReports_EmailSchedule'|translate} - - - -
- {'PDFReports_WeeklyScheduleHelp'|translate} -
- {'PDFReports_MonthlyScheduleHelp'|translate} -
- {'PDFReports_ReportHour'|translate} - - {'PDFReports_OClock'|translate} -
- - +
+ {'PDFReports_CancelAndReturnToReports'|translate:"":""} +
+
+
+ + + + + + + + + + + + + + - - - +
+ {'PDFReports_DescriptionOnFirstPage'|translate} +
+ + + + + - +
+ {'PDFReports_WeeklyScheduleHelp'|translate} +
+ {'PDFReports_MonthlyScheduleHelp'|translate} +
+ {'PDFReports_ReportHour'|translate} + + {'PDFReports_OClock'|translate} +
+ + - - + + + + - {postEvent name="template_reportParametersPDFReports"} + + - - - + - {if $allowMultipleReportsByReportType[$reportType]} - {assign var=reportInputType value='checkbox'} - {else} - {assign var=reportInputType value='radio'} - {/if} + {postEvent name="template_reportParametersPDFReports"} - {assign var=countCategory value=0} + + + - - - -
{'PDFReports_CreateAndScheduleReport'|translate}
{'General_Website'|translate} + {$siteName} +
{'General_Description'|translate} + -
- {'PDFReports_ReportType'|translate} - - -
{'PDFReports_EmailSchedule'|translate} + -
- {'PDFReports_ReportFormat'|translate} -
- {foreach from=$reportFormatsByReportType key=reportType item=reportFormats} - - {/foreach} -
+ {'PDFReports_ReportType'|translate} + + +
+ {'PDFReports_ReportFormat'|translate} +
{'PDFReports_ReportsIncluded'|translate} - {foreach from=$reportsByCategoryByReportType key=reportType item=reportsByCategory} -
+
+ {foreach from=$reportFormatsByReportType key=reportType item=reportFormats} + + {/foreach} +
{'PDFReports_ReportsIncluded'|translate} + {foreach from=$reportsByCategoryByReportType key=reportType item=reportsByCategory} +
- {math - equation="ceil (reportsByCategoryCount / 2)" - reportsByCategoryCount=$reportsByCategory|@count - assign=newColumnAfter - } + {if $allowMultipleReportsByReportType[$reportType]} + {assign var=reportInputType value='checkbox'} + {else} + {assign var=reportInputType value='radio'} + {/if} -
- {foreach from=$reportsByCategory item=reports key=category name=reports} - {if $countCategory >= $newColumnAfter && $newColumnAfter != 0} - {assign var=newColumnAfter value=0} -
- {/if} -
{$category}
    - {foreach from=$reports item=report} -
  • - - -
  • - {/foreach} - {assign var=countCategory value=$countCategory+1} -
-
- {/foreach} -
-
- {/foreach} -
+ {assign var=countCategory value=0} - - + {math + equation="ceil (reportsByCategoryCount / 2)" + reportsByCategoryCount=$reportsByCategory|@count + assign=newColumnAfter + } -
-
- {'General_OrCancel'|translate:"":""} -
+
+ {foreach from=$reportsByCategory item=reports key=category name=reports} + {if $countCategory >= $newColumnAfter && $newColumnAfter != 0} + {assign var=newColumnAfter value=0} +
+
+ {/if} +
{$category}
+
    + {foreach from=$reports item=report} +
  • + + +
  • + {/foreach} + {assign var=countCategory value=$countCategory+1} +
+
+ {/foreach} +
+
+ {/foreach} + + + + + + + + + + +
+ {'General_OrCancel'|translate:"":""} +
\ No newline at end of file diff --git a/plugins/PDFReports/templates/index.tpl b/plugins/PDFReports/templates/index.tpl index 1479c8d6cc..e47b41e665 100644 --- a/plugins/PDFReports/templates/index.tpl +++ b/plugins/PDFReports/templates/index.tpl @@ -7,41 +7,41 @@
-

{'PDFReports_ManageEmailReports'|translate}

- -
- {ajaxErrorDiv} - {ajaxLoadingDiv} - {include file="PDFReports/templates/list.tpl"} - {include file="PDFReports/templates/add.tpl"} - -
+

{'PDFReports_ManageEmailReports'|translate}

+ +
+ {ajaxErrorDiv} + {ajaxLoadingDiv} + {include file="PDFReports/templates/list.tpl"} + {include file="PDFReports/templates/add.tpl"} + +
-

{'PDFReports_AreYouSureDeleteReport'|translate}

- - -
+

{'PDFReports_AreYouSureDeleteReport'|translate}

+ + +
- + {/literal} diff --git a/plugins/PDFReports/templates/list.tpl b/plugins/PDFReports/templates/list.tpl index d21ef65b92..c67c5ac024 100644 --- a/plugins/PDFReports/templates/list.tpl +++ b/plugins/PDFReports/templates/list.tpl @@ -1,90 +1,96 @@
- - - - - - - - - - - - - - {if $userLogin=='anonymous'} - -
{'General_Description'|translate}{'PDFReports_EmailSchedule'|translate}{'PDFReports_ReportFormat'|translate}{'PDFReports_SendReportTo'|translate}{'General_Download'|translate}{'General_Edit'|translate}{'General_Delete'|translate}
-
- {'PDFReports_MustBeLoggedIn'|translate} -
{'Login_LogIn'|translate} -

-
- {elseif empty($reports)} - -
- {'PDFReports_ThereIsNoReportToManage'|translate:$siteName}. -

- › {'PDFReports_CreateAndScheduleReport'|translate} -

- - - {else} - {foreach from=$reports item=report} - - {$report.description} - {$periods[$report.period]} - - - - {if !empty($report.format)} - {$report.format|upper} - {/if} - - - {*report recipients*} - {if $report.recipients|@count eq 0} - {'PDFReports_NoRecipients'|translate} - {else} - {foreach name=recipients from=$report.recipients item=recipient} - {$recipient}
- {/foreach} - {*send now link*} - - - {'PDFReports_SendReportNow'|translate} - - {/if} - - - {*download link*} - - - {'General_Download'|translate} - - - - {*edit link*} - - - {'General_Edit'|translate} - - - - {*delete link *} - - - {'General_Delete'|translate} - - - - {/foreach} - - {if $userLogin != 'anonymous'} -
- › {'PDFReports_CreateAndScheduleReport'|translate} -

- {/if} - {/if} + + + + + + + + + + + + + + {if $userLogin=='anonymous'} + + + +
{'General_Description'|translate}{'PDFReports_EmailSchedule'|translate}{'PDFReports_ReportFormat'|translate}{'PDFReports_SendReportTo'|translate}{'General_Download'|translate}{'General_Edit'|translate}{'General_Delete'|translate}
+
+ {'PDFReports_MustBeLoggedIn'|translate} +
{'Login_LogIn'|translate} +

+
+ {elseif empty($reports)} + + +
+ {'PDFReports_ThereIsNoReportToManage'|translate:$siteName}. +

+ › {'PDFReports_CreateAndScheduleReport'|translate} +

+ + + + {else} + {foreach from=$reports item=report} + + {$report.description} + {$periods[$report.period]} + + + + {if !empty($report.format)} + {$report.format|upper} + {/if} + + + {*report recipients*} + {if $report.recipients|@count eq 0} + {'PDFReports_NoRecipients'|translate} + {else} + {foreach name=recipients from=$report.recipients item=recipient} + {$recipient} +
+ {/foreach} + {*send now link*} + + + {'PDFReports_SendReportNow'|translate} + + {/if} + + + {*download link*} + + + {'General_Download'|translate} + + + + {*edit link*} + + + {'General_Edit'|translate} + + + + {*delete link *} + + + {'General_Delete'|translate} + + + + {/foreach} + + {if $userLogin != 'anonymous'} +
+ › {'PDFReports_CreateAndScheduleReport'|translate} +
+
+ {/if} + {/if}
diff --git a/plugins/PDFReports/templates/pdf.js b/plugins/PDFReports/templates/pdf.js index 221418f318..4115dcc3f1 100644 --- a/plugins/PDFReports/templates/pdf.js +++ b/plugins/PDFReports/templates/pdf.js @@ -9,92 +9,83 @@ var getReportParametersFunctions = Object(); var updateReportParametersFunctions = Object(); var resetReportParametersFunctions = Object(); -function formSetEditReport(idReport) -{ - var report = { - 'type' : ReportPlugin.defaultReportType, - 'format' : ReportPlugin.defaultReportFormat, - 'description' : '', - 'period' : ReportPlugin.defaultPeriod, - 'hour' : ReportPlugin.defaultHour, - 'reports' : [] - }; - - if(idReport > 0) - { - report = ReportPlugin.reportList[idReport]; - $('#report_submit').val(ReportPlugin.updateReportString); - } - else - { - $('#report_submit').val(ReportPlugin.createReportString); - } - - toggleReportType(report.type); - - $('#report_description').html(report.description); - $('#report_type option[value='+report.type+']').prop('selected', 'selected'); - $('#report_period option[value='+report.period+']').prop('selected', 'selected'); - $('#report_hour').val(report.hour); - $('[name=report_format].'+report.type+' option[value='+report.format+']').prop('selected', 'selected'); - - $('[name=reportsList] input').prop('checked', false); - - var key; - for(key in report.reports) - { - $('.' + report.type + ' [report-unique-id=' + report.reports[key] + ']').prop('checked','checked'); - } - - updateReportParametersFunctions[report.type](report.parameters); - - $('#report_idreport').val(idReport); +function formSetEditReport(idReport) { + var report = { + 'type': ReportPlugin.defaultReportType, + 'format': ReportPlugin.defaultReportFormat, + 'description': '', + 'period': ReportPlugin.defaultPeriod, + 'hour': ReportPlugin.defaultHour, + 'reports': [] + }; + + if (idReport > 0) { + report = ReportPlugin.reportList[idReport]; + $('#report_submit').val(ReportPlugin.updateReportString); + } + else { + $('#report_submit').val(ReportPlugin.createReportString); + } + + toggleReportType(report.type); + + $('#report_description').html(report.description); + $('#report_type option[value=' + report.type + ']').prop('selected', 'selected'); + $('#report_period option[value=' + report.period + ']').prop('selected', 'selected'); + $('#report_hour').val(report.hour); + $('[name=report_format].' + report.type + ' option[value=' + report.format + ']').prop('selected', 'selected'); + + $('[name=reportsList] input').prop('checked', false); + + var key; + for (key in report.reports) { + $('.' + report.type + ' [report-unique-id=' + report.reports[key] + ']').prop('checked', 'checked'); + } + + updateReportParametersFunctions[report.type](report.parameters); + + $('#report_idreport').val(idReport); } -function getReportAjaxRequest(idReport, defaultApiMethod) -{ - var parameters = {}; - piwikHelper.lazyScrollTo(".entityContainer", 400); - parameters.module = 'API'; - parameters.method = defaultApiMethod; - if(idReport == 0) - { - parameters.method = 'PDFReports.addReport'; - } - parameters.format = 'json'; - return parameters; +function getReportAjaxRequest(idReport, defaultApiMethod) { + var parameters = {}; + piwikHelper.lazyScrollTo(".entityContainer", 400); + parameters.module = 'API'; + parameters.method = defaultApiMethod; + if (idReport == 0) { + parameters.method = 'PDFReports.addReport'; + } + parameters.format = 'json'; + return parameters; } -function toggleReportType(reportType) -{ - resetReportParametersFunctions[reportType](); - $('#report_type option').each(function(index, type) { - $('.'+$(type).val()).hide(); - }); - $('.'+reportType).show(); +function toggleReportType(reportType) { + resetReportParametersFunctions[reportType](); + $('#report_type option').each(function (index, type) { + $('.' + $(type).val()).hide(); + }); + $('.' + reportType).show(); } -function initManagePdf() -{ - // Click Add/Update Submit - $('#addEditReport').submit( function() { - var idReport = $('#report_idreport').val(); - var apiParameters = getReportAjaxRequest(idReport, 'PDFReports.updateReport'); - apiParameters.idReport = idReport; - apiParameters.description = $('#report_description').val(); - apiParameters.reportType = $('#report_type option:selected').val(); - apiParameters.reportFormat = $('[name=report_format].'+apiParameters.reportType+' option:selected').val(); - - var reports = []; - $('[name=reportsList].'+apiParameters.reportType+' input:checked').each(function() { - reports.push($(this).attr('report-unique-id')); - }); - if(reports.length > 0) - { - apiParameters.reports = reports; - } - - apiParameters.parameters = getReportParametersFunctions[apiParameters.reportType](); +function initManagePdf() { + // Click Add/Update Submit + $('#addEditReport').submit(function () { + var idReport = $('#report_idreport').val(); + var apiParameters = getReportAjaxRequest(idReport, 'PDFReports.updateReport'); + apiParameters.idReport = idReport; + apiParameters.description = $('#report_description').val(); + apiParameters.reportType = $('#report_type option:selected').val(); + apiParameters.reportFormat = $('[name=report_format].' + apiParameters.reportType + ' option:selected').val(); + + var reports = []; + $('[name=reportsList].' + apiParameters.reportType + ' input:checked').each(function () { + reports.push($(this).attr('report-unique-id')); + }); + if (reports.length > 0) { + apiParameters.reports = reports; + } + + apiParameters.parameters = getReportParametersFunctions[apiParameters.reportType](); var ajaxHandler = new ajaxHelper(); ajaxHandler.addParams(apiParameters, 'POST'); @@ -104,27 +95,27 @@ function initManagePdf() ajaxHandler.setLoadingElement(); ajaxHandler.send(true); return false; - }); - - // Email now - $('a[name=linkSendNow]').click(function(){ - var idReport = $(this).attr('idreport'); - var parameters = getReportAjaxRequest(idReport, 'PDFReports.sendReport'); - parameters.idReport = idReport; + }); + + // Email now + $('a[name=linkSendNow]').click(function () { + var idReport = $(this).attr('idreport'); + var parameters = getReportAjaxRequest(idReport, 'PDFReports.sendReport'); + parameters.idReport = idReport; var ajaxHandler = new ajaxHelper(); ajaxHandler.addParams(parameters, 'POST'); ajaxHandler.setLoadingElement(); ajaxHandler.send(true); }); - - // Delete Report - $('a[name=linkDeleteReport]').click(function(){ - var idReport = $(this).attr('id'); - function onDelete() - { - var parameters = getReportAjaxRequest(idReport, 'PDFReports.deleteReport'); - parameters.idReport = idReport; + + // Delete Report + $('a[name=linkDeleteReport]').click(function () { + var idReport = $(this).attr('id'); + + function onDelete() { + var parameters = getReportAjaxRequest(idReport, 'PDFReports.deleteReport'); + parameters.idReport = idReport; var ajaxHandler = new ajaxHelper(); ajaxHandler.addParams(parameters, 'POST'); @@ -132,34 +123,35 @@ function initManagePdf() ajaxHandler.setLoadingElement(); ajaxHandler.send(true); } - piwikHelper.modalConfirm( '#confirm', {yes: onDelete}); - }); - - // Edit Report click - $('a[name=linkEditReport]').click(function(){ - var idReport = $(this).attr('id'); - formSetEditReport( idReport ); - $('.entityAddContainer').show(); - $('#entityEditContainer').hide(); - }); - - // Switch Report Type - $('#report_type').change(function(){ - var reportType = $(this).val(); - toggleReportType(reportType); - }); - - // Add a Report click - $('#linkAddReport').click(function(){ - $('.entityAddContainer').show(); - $('#entityEditContainer').hide(); - formSetEditReport( idReport = 0 ); - }); - - // Cancel click - $('.entityCancelLink').click(function(){ - $('.entityAddContainer').hide(); - $('#entityEditContainer').show(); - piwikHelper.hideAjaxError(); - }).click(); + + piwikHelper.modalConfirm('#confirm', {yes: onDelete}); + }); + + // Edit Report click + $('a[name=linkEditReport]').click(function () { + var idReport = $(this).attr('id'); + formSetEditReport(idReport); + $('.entityAddContainer').show(); + $('#entityEditContainer').hide(); + }); + + // Switch Report Type + $('#report_type').change(function () { + var reportType = $(this).val(); + toggleReportType(reportType); + }); + + // Add a Report click + $('#linkAddReport').click(function () { + $('.entityAddContainer').show(); + $('#entityEditContainer').hide(); + formSetEditReport(idReport = 0); + }); + + // Cancel click + $('.entityCancelLink').click(function () { + $('.entityAddContainer').hide(); + $('#entityEditContainer').show(); + piwikHelper.hideAjaxError(); + }).click(); } diff --git a/plugins/PDFReports/templates/report_parameters.tpl b/plugins/PDFReports/templates/report_parameters.tpl index 053a686bed..1d2cb3c04a 100644 --- a/plugins/PDFReports/templates/report_parameters.tpl +++ b/plugins/PDFReports/templates/report_parameters.tpl @@ -1,99 +1,104 @@ - {'PDFReports_SendReportTo'|translate} - - - - -

- {'PDFReports_AlsoSendReportToTheseEmails'|translate}
- - + {'PDFReports_SendReportTo'|translate} + + + + +

+ {'PDFReports_AlsoSendReportToTheseEmails'|translate}
+ + - - {*PDFReports_AggregateReportsFormat should be named PDFReports_DisplayFormat*} - {'PDFReports_AggregateReportsFormat'|translate} - - - -
-
- - -
- + + {*PDFReports_AggregateReportsFormat should be named PDFReports_DisplayFormat*} + {'PDFReports_AggregateReportsFormat'|translate} + + + + +
+
+ + +
+ diff --git a/plugins/PrivacyManager/Controller.php b/plugins/PrivacyManager/Controller.php index f2cb8f099e..bb25d40173 100644 --- a/plugins/PrivacyManager/Controller.php +++ b/plugins/PrivacyManager/Controller.php @@ -16,290 +16,275 @@ class Piwik_PrivacyManager_Controller extends Piwik_Controller_Admin { - const ANONYMIZE_IP_PLUGIN_NAME = "AnonymizeIP"; - const OPTION_LAST_DELETE_PIWIK_LOGS = "lastDelete_piwik_logs"; - - public function saveSettings() - { - Piwik::checkUserIsSuperUser(); - if ($_SERVER["REQUEST_METHOD"] == "POST") - { - $this->checkTokenInUrl(); - switch (Piwik_Common::getRequestVar('form')) - { - case("formMaskLength"): - $this->handlePluginState(Piwik_Common::getRequestVar("anonymizeIPEnable", 0)); - $trackerConfig = Piwik_Config::getInstance()->Tracker; - $trackerConfig['ip_address_mask_length'] = Piwik_Common::getRequestVar("maskLength", 1); - Piwik_Config::getInstance()->Tracker = $trackerConfig; - Piwik_Config::getInstance()->forceSave(); - break; - - case("formDeleteSettings"): - $settings = $this->getPurgeSettingsFromRequest(); - Piwik_PrivacyManager::savePurgeDataSettings($settings); - break; - - default: //do nothing - break; - } - } - - return $this->redirectToIndex('PrivacyManager', 'privacySettings', null, null, null, array('updated' => 1)); - } - - /** - * Utility function. Gets the delete logs/reports settings from the request and uses - * them to populate config arrays. - * - * @return array An array containing the data deletion settings. - */ - private function getPurgeSettingsFromRequest() - { - $settings = array(); - - // delete logs settings - $settings['delete_logs_enable'] = Piwik_Common::getRequestVar("deleteEnable", 0); - $settings['delete_logs_schedule_lowest_interval'] = Piwik_Common::getRequestVar("deleteLowestInterval", 7); - $settings['delete_logs_older_than'] = ((int)Piwik_Common::getRequestVar("deleteOlderThan", 180) < 1) ? - 1 : Piwik_Common::getRequestVar("deleteOlderThan", 180); - - // delete reports settings - $settings['delete_reports_enable'] = Piwik_Common::getRequestVar("deleteReportsEnable", 0); - $deleteReportsOlderThan = Piwik_Common::getRequestVar("deleteReportsOlderThan", 3); - $settings['delete_reports_older_than'] = $deleteReportsOlderThan < 3 ? 3 : $deleteReportsOlderThan; - $settings['delete_reports_keep_basic_metrics'] = Piwik_Common::getRequestVar("deleteReportsKeepBasic", 0); - $settings['delete_reports_keep_day_reports'] = Piwik_Common::getRequestVar("deleteReportsKeepDay", 0); - $settings['delete_reports_keep_week_reports'] = Piwik_Common::getRequestVar("deleteReportsKeepWeek", 0); - $settings['delete_reports_keep_month_reports'] = Piwik_Common::getRequestVar("deleteReportsKeepMonth", 0); - $settings['delete_reports_keep_year_reports'] = Piwik_Common::getRequestVar("deleteReportsKeepYear", 0); - $settings['delete_reports_keep_range_reports'] = Piwik_Common::getRequestVar("deleteReportsKeepRange", 0); - $settings['delete_reports_keep_segment_reports'] = Piwik_Common::getRequestVar("deleteReportsKeepSegments", 0); - - $settings['delete_logs_max_rows_per_query'] = Piwik_PrivacyManager::DEFAULT_MAX_ROWS_PER_QUERY; - - return $settings; - } - - /** - * Echo's an HTML chunk describing the current database size, and the estimated space - * savings after the scheduled data purge is run. - */ - public function getDatabaseSize() - { - Piwik::checkUserIsSuperUser(); - $view = Piwik_View::factory('databaseSize'); - - $forceEstimate = Piwik_Common::getRequestVar('forceEstimate', 0); - $view->dbStats = $this->getDeleteDBSizeEstimate($getSettingsFromQuery = true, $forceEstimate); - $view->language = Piwik_LanguagesManager::getLanguageCodeForCurrentUser(); - - echo $view->render(); - } - - /** - * Returns true if server side DoNotTrack support is enabled, false if otherwise. - * - * @return bool - */ - public static function isDntSupported() - { - return Piwik_PluginsManager::getInstance()->isPluginActivated('DoNotTrack'); - } - - public function privacySettings() - { - Piwik::checkUserHasSomeAdminAccess(); - $view = Piwik_View::factory('privacySettings'); - - if (Piwik::isUserIsSuperUser()) - { - $view->deleteData = $this->getDeleteDataInfo(); - $view->anonymizeIP = $this->getAnonymizeIPInfo(); - $view->dntSupport = self::isDntSupported(); - $view->canDeleteLogActions = Piwik::isLockPrivilegeGranted(); - $view->dbUser = Piwik_Config::getInstance()->database['username']; - } - $view->language = Piwik_LanguagesManager::getLanguageCodeForCurrentUser(); - - if (!Piwik_Config::getInstance()->isFileWritable()) - { - $view->configFileNotWritable = true; - } - - $this->setBasicVariablesView($view); - $view->menu = Piwik_GetAdminMenu(); - - echo $view->render(); - } - - /** - * Executes a data purge, deleting log data and report data using the current config - * options. Echo's the result of getDatabaseSize after purging. - */ - public function executeDataPurge() - { - Piwik::checkUserIsSuperUser(); - $this->checkTokenInUrl(); - - // if the request isn't a POST, redirect to index - if ($_SERVER["REQUEST_METHOD"] != "POST" - && !Piwik_Common::isPhpCliMode()) - { - return $this->redirectToIndex('PrivacyManager', 'privacySettings'); - } - - $settings = Piwik_PrivacyManager::getPurgeDataSettings(); - if ($settings['delete_logs_enable']) - { - $logDataPurger = Piwik_PrivacyManager_LogDataPurger::make($settings); - $logDataPurger->purgeData(); - } - if ($settings['delete_reports_enable']) - { - $reportsPurger = Piwik_PrivacyManager_ReportsPurger::make( - $settings, Piwik_PrivacyManager::getAllMetricsToKeep()); - $reportsPurger->purgeData(true); - } - } - - protected function getDeleteDBSizeEstimate( $getSettingsFromQuery = false, $forceEstimate = false ) - { - // get the purging settings & create two purger instances - if ($getSettingsFromQuery) - { - $settings = $this->getPurgeSettingsFromRequest(); - } - else - { - $settings = Piwik_PrivacyManager::getPurgeDataSettings(); - } - - $doDatabaseSizeEstimate = Piwik_Config::getInstance()->Deletelogs['enable_auto_database_size_estimate']; - - // determine the DB size & purged DB size - $metadataProvider = new Piwik_DBStats_MySQLMetadataProvider(); - $tableStatuses = $metadataProvider->getAllTablesStatus(); - - $totalBytes = 0; - foreach ($tableStatuses as $status) - { - $totalBytes += $status['Data_length'] + $status['Index_length']; - } - - $result = array( - 'currentSize' => Piwik::getPrettySizeFromBytes($totalBytes) - ); - - // if the db size estimate feature is enabled, get the estimate - if ($doDatabaseSizeEstimate || $forceEstimate == 1) - { - // maps tables whose data will be deleted with number of rows that will be deleted - // if a value is -1, it means the table will be dropped. - $deletedDataSummary = Piwik_PrivacyManager::getPurgeEstimate($settings); - - $totalAfterPurge = $totalBytes; - foreach ($tableStatuses as $status) - { - $tableName = $status['Name']; - if (isset($deletedDataSummary[$tableName])) - { - $tableTotalBytes = $status['Data_length'] + $status['Index_length']; - - // if dropping the table - if ($deletedDataSummary[$tableName] === Piwik_PrivacyManager_ReportsPurger::DROP_TABLE) - { - $totalAfterPurge -= $tableTotalBytes; - } - else // if just deleting rows - { - if($status['Rows'] > 0) { - $totalAfterPurge -= ($tableTotalBytes / $status['Rows']) * $deletedDataSummary[$tableName]; - } - } - } - } - - $result['sizeAfterPurge'] = Piwik::getPrettySizeFromBytes($totalAfterPurge); - $result['spaceSaved'] = Piwik::getPrettySizeFromBytes($totalBytes - $totalAfterPurge); - } - - return $result; - } - - protected function getAnonymizeIPInfo() - { - Piwik::checkUserIsSuperUser(); - $anonymizeIP = array(); - - Piwik_PluginsManager::getInstance()->loadPlugin(self::ANONYMIZE_IP_PLUGIN_NAME); - - $anonymizeIP["name"] = self::ANONYMIZE_IP_PLUGIN_NAME; - $anonymizeIP["enabled"] = Piwik_PluginsManager::getInstance()->isPluginActivated(self::ANONYMIZE_IP_PLUGIN_NAME); - $anonymizeIP["maskLength"] = Piwik_Config::getInstance()->Tracker['ip_address_mask_length']; - $anonymizeIP["info"] = Piwik_PluginsManager::getInstance()->getLoadedPlugin(self::ANONYMIZE_IP_PLUGIN_NAME)->getInformation(); - - return $anonymizeIP; - } - - protected function getDeleteDataInfo() - { - Piwik::checkUserIsSuperUser(); - $deleteDataInfos = array(); - $taskScheduler = new Piwik_TaskScheduler(); - $deleteDataInfos["config"] = Piwik_PrivacyManager::getPurgeDataSettings(); - $deleteDataInfos["deleteTables"] = - "
".implode(", ", Piwik_PrivacyManager_LogDataPurger::getDeleteTableLogTables()); - - $scheduleTimetable = $taskScheduler->getScheduledTimeForMethod("Piwik_PrivacyManager", "deleteLogTables"); - - $optionTable = Piwik_GetOption(self::OPTION_LAST_DELETE_PIWIK_LOGS); - - //If task was already rescheduled, read time from taskTimetable. Else, calculate next possible runtime. - if (!empty($scheduleTimetable) && ($scheduleTimetable - time() > 0)) { - $nextPossibleSchedule = (int)$scheduleTimetable; - } else { - $date = Piwik_Date::factory("today"); - $nextPossibleSchedule = $date->addDay(1)->getTimestamp(); - } - - //deletion schedule did not run before - if (empty($optionTable)) { - $deleteDataInfos["lastRun"] = false; - - //next run ASAP (with next schedule run) - $date = Piwik_Date::factory("today"); - $deleteDataInfos["nextScheduleTime"] = $nextPossibleSchedule; - } else { - $deleteDataInfos["lastRun"] = $optionTable; - $deleteDataInfos["lastRunPretty"] = Piwik_Date::factory((int)$optionTable)->getLocalized('%day% %shortMonth% %longYear%'); - - //Calculate next run based on last run + interval - $nextScheduleRun = (int)($deleteDataInfos["lastRun"] + $deleteDataInfos["config"]["delete_logs_schedule_lowest_interval"] * 24 * 60 * 60); - - //is the calculated next run in the past? (e.g. plugin was disabled in the meantime or something) -> run ASAP - if (($nextScheduleRun - time()) <= 0) { - $deleteDataInfos["nextScheduleTime"] = $nextPossibleSchedule; - } else { - $deleteDataInfos["nextScheduleTime"] = $nextScheduleRun; - } - } - - $deleteDataInfos["nextRunPretty"] = Piwik::getPrettyTimeFromSeconds($deleteDataInfos["nextScheduleTime"] - time()); - - return $deleteDataInfos; - } - - protected function handlePluginState($state = 0) - { - $pluginController = new Piwik_CorePluginsAdmin_Controller(); - - if ($state == 1 && !Piwik_PluginsManager::getInstance()->isPluginActivated(self::ANONYMIZE_IP_PLUGIN_NAME)) { - $pluginController->activate($redirectAfter = false); - } elseif ($state == 0 && Piwik_PluginsManager::getInstance()->isPluginActivated(self::ANONYMIZE_IP_PLUGIN_NAME)) { - $pluginController->deactivate($redirectAfter = false); - } else { - //nothing to do - } - } + const ANONYMIZE_IP_PLUGIN_NAME = "AnonymizeIP"; + const OPTION_LAST_DELETE_PIWIK_LOGS = "lastDelete_piwik_logs"; + + public function saveSettings() + { + Piwik::checkUserIsSuperUser(); + if ($_SERVER["REQUEST_METHOD"] == "POST") { + $this->checkTokenInUrl(); + switch (Piwik_Common::getRequestVar('form')) { + case("formMaskLength"): + $this->handlePluginState(Piwik_Common::getRequestVar("anonymizeIPEnable", 0)); + $trackerConfig = Piwik_Config::getInstance()->Tracker; + $trackerConfig['ip_address_mask_length'] = Piwik_Common::getRequestVar("maskLength", 1); + Piwik_Config::getInstance()->Tracker = $trackerConfig; + Piwik_Config::getInstance()->forceSave(); + break; + + case("formDeleteSettings"): + $settings = $this->getPurgeSettingsFromRequest(); + Piwik_PrivacyManager::savePurgeDataSettings($settings); + break; + + default: //do nothing + break; + } + } + + return $this->redirectToIndex('PrivacyManager', 'privacySettings', null, null, null, array('updated' => 1)); + } + + /** + * Utility function. Gets the delete logs/reports settings from the request and uses + * them to populate config arrays. + * + * @return array An array containing the data deletion settings. + */ + private function getPurgeSettingsFromRequest() + { + $settings = array(); + + // delete logs settings + $settings['delete_logs_enable'] = Piwik_Common::getRequestVar("deleteEnable", 0); + $settings['delete_logs_schedule_lowest_interval'] = Piwik_Common::getRequestVar("deleteLowestInterval", 7); + $settings['delete_logs_older_than'] = ((int)Piwik_Common::getRequestVar("deleteOlderThan", 180) < 1) ? + 1 : Piwik_Common::getRequestVar("deleteOlderThan", 180); + + // delete reports settings + $settings['delete_reports_enable'] = Piwik_Common::getRequestVar("deleteReportsEnable", 0); + $deleteReportsOlderThan = Piwik_Common::getRequestVar("deleteReportsOlderThan", 3); + $settings['delete_reports_older_than'] = $deleteReportsOlderThan < 3 ? 3 : $deleteReportsOlderThan; + $settings['delete_reports_keep_basic_metrics'] = Piwik_Common::getRequestVar("deleteReportsKeepBasic", 0); + $settings['delete_reports_keep_day_reports'] = Piwik_Common::getRequestVar("deleteReportsKeepDay", 0); + $settings['delete_reports_keep_week_reports'] = Piwik_Common::getRequestVar("deleteReportsKeepWeek", 0); + $settings['delete_reports_keep_month_reports'] = Piwik_Common::getRequestVar("deleteReportsKeepMonth", 0); + $settings['delete_reports_keep_year_reports'] = Piwik_Common::getRequestVar("deleteReportsKeepYear", 0); + $settings['delete_reports_keep_range_reports'] = Piwik_Common::getRequestVar("deleteReportsKeepRange", 0); + $settings['delete_reports_keep_segment_reports'] = Piwik_Common::getRequestVar("deleteReportsKeepSegments", 0); + + $settings['delete_logs_max_rows_per_query'] = Piwik_PrivacyManager::DEFAULT_MAX_ROWS_PER_QUERY; + + return $settings; + } + + /** + * Echo's an HTML chunk describing the current database size, and the estimated space + * savings after the scheduled data purge is run. + */ + public function getDatabaseSize() + { + Piwik::checkUserIsSuperUser(); + $view = Piwik_View::factory('databaseSize'); + + $forceEstimate = Piwik_Common::getRequestVar('forceEstimate', 0); + $view->dbStats = $this->getDeleteDBSizeEstimate($getSettingsFromQuery = true, $forceEstimate); + $view->language = Piwik_LanguagesManager::getLanguageCodeForCurrentUser(); + + echo $view->render(); + } + + /** + * Returns true if server side DoNotTrack support is enabled, false if otherwise. + * + * @return bool + */ + public static function isDntSupported() + { + return Piwik_PluginsManager::getInstance()->isPluginActivated('DoNotTrack'); + } + + public function privacySettings() + { + Piwik::checkUserHasSomeAdminAccess(); + $view = Piwik_View::factory('privacySettings'); + + if (Piwik::isUserIsSuperUser()) { + $view->deleteData = $this->getDeleteDataInfo(); + $view->anonymizeIP = $this->getAnonymizeIPInfo(); + $view->dntSupport = self::isDntSupported(); + $view->canDeleteLogActions = Piwik::isLockPrivilegeGranted(); + $view->dbUser = Piwik_Config::getInstance()->database['username']; + } + $view->language = Piwik_LanguagesManager::getLanguageCodeForCurrentUser(); + + if (!Piwik_Config::getInstance()->isFileWritable()) { + $view->configFileNotWritable = true; + } + + $this->setBasicVariablesView($view); + $view->menu = Piwik_GetAdminMenu(); + + echo $view->render(); + } + + /** + * Executes a data purge, deleting log data and report data using the current config + * options. Echo's the result of getDatabaseSize after purging. + */ + public function executeDataPurge() + { + Piwik::checkUserIsSuperUser(); + $this->checkTokenInUrl(); + + // if the request isn't a POST, redirect to index + if ($_SERVER["REQUEST_METHOD"] != "POST" + && !Piwik_Common::isPhpCliMode() + ) { + return $this->redirectToIndex('PrivacyManager', 'privacySettings'); + } + + $settings = Piwik_PrivacyManager::getPurgeDataSettings(); + if ($settings['delete_logs_enable']) { + $logDataPurger = Piwik_PrivacyManager_LogDataPurger::make($settings); + $logDataPurger->purgeData(); + } + if ($settings['delete_reports_enable']) { + $reportsPurger = Piwik_PrivacyManager_ReportsPurger::make( + $settings, Piwik_PrivacyManager::getAllMetricsToKeep()); + $reportsPurger->purgeData(true); + } + } + + protected function getDeleteDBSizeEstimate($getSettingsFromQuery = false, $forceEstimate = false) + { + // get the purging settings & create two purger instances + if ($getSettingsFromQuery) { + $settings = $this->getPurgeSettingsFromRequest(); + } else { + $settings = Piwik_PrivacyManager::getPurgeDataSettings(); + } + + $doDatabaseSizeEstimate = Piwik_Config::getInstance()->Deletelogs['enable_auto_database_size_estimate']; + + // determine the DB size & purged DB size + $metadataProvider = new Piwik_DBStats_MySQLMetadataProvider(); + $tableStatuses = $metadataProvider->getAllTablesStatus(); + + $totalBytes = 0; + foreach ($tableStatuses as $status) { + $totalBytes += $status['Data_length'] + $status['Index_length']; + } + + $result = array( + 'currentSize' => Piwik::getPrettySizeFromBytes($totalBytes) + ); + + // if the db size estimate feature is enabled, get the estimate + if ($doDatabaseSizeEstimate || $forceEstimate == 1) { + // maps tables whose data will be deleted with number of rows that will be deleted + // if a value is -1, it means the table will be dropped. + $deletedDataSummary = Piwik_PrivacyManager::getPurgeEstimate($settings); + + $totalAfterPurge = $totalBytes; + foreach ($tableStatuses as $status) { + $tableName = $status['Name']; + if (isset($deletedDataSummary[$tableName])) { + $tableTotalBytes = $status['Data_length'] + $status['Index_length']; + + // if dropping the table + if ($deletedDataSummary[$tableName] === Piwik_PrivacyManager_ReportsPurger::DROP_TABLE) { + $totalAfterPurge -= $tableTotalBytes; + } else // if just deleting rows + { + if ($status['Rows'] > 0) { + $totalAfterPurge -= ($tableTotalBytes / $status['Rows']) * $deletedDataSummary[$tableName]; + } + } + } + } + + $result['sizeAfterPurge'] = Piwik::getPrettySizeFromBytes($totalAfterPurge); + $result['spaceSaved'] = Piwik::getPrettySizeFromBytes($totalBytes - $totalAfterPurge); + } + + return $result; + } + + protected function getAnonymizeIPInfo() + { + Piwik::checkUserIsSuperUser(); + $anonymizeIP = array(); + + Piwik_PluginsManager::getInstance()->loadPlugin(self::ANONYMIZE_IP_PLUGIN_NAME); + + $anonymizeIP["name"] = self::ANONYMIZE_IP_PLUGIN_NAME; + $anonymizeIP["enabled"] = Piwik_PluginsManager::getInstance()->isPluginActivated(self::ANONYMIZE_IP_PLUGIN_NAME); + $anonymizeIP["maskLength"] = Piwik_Config::getInstance()->Tracker['ip_address_mask_length']; + $anonymizeIP["info"] = Piwik_PluginsManager::getInstance()->getLoadedPlugin(self::ANONYMIZE_IP_PLUGIN_NAME)->getInformation(); + + return $anonymizeIP; + } + + protected function getDeleteDataInfo() + { + Piwik::checkUserIsSuperUser(); + $deleteDataInfos = array(); + $taskScheduler = new Piwik_TaskScheduler(); + $deleteDataInfos["config"] = Piwik_PrivacyManager::getPurgeDataSettings(); + $deleteDataInfos["deleteTables"] = + "
" . implode(", ", Piwik_PrivacyManager_LogDataPurger::getDeleteTableLogTables()); + + $scheduleTimetable = $taskScheduler->getScheduledTimeForMethod("Piwik_PrivacyManager", "deleteLogTables"); + + $optionTable = Piwik_GetOption(self::OPTION_LAST_DELETE_PIWIK_LOGS); + + //If task was already rescheduled, read time from taskTimetable. Else, calculate next possible runtime. + if (!empty($scheduleTimetable) && ($scheduleTimetable - time() > 0)) { + $nextPossibleSchedule = (int)$scheduleTimetable; + } else { + $date = Piwik_Date::factory("today"); + $nextPossibleSchedule = $date->addDay(1)->getTimestamp(); + } + + //deletion schedule did not run before + if (empty($optionTable)) { + $deleteDataInfos["lastRun"] = false; + + //next run ASAP (with next schedule run) + $date = Piwik_Date::factory("today"); + $deleteDataInfos["nextScheduleTime"] = $nextPossibleSchedule; + } else { + $deleteDataInfos["lastRun"] = $optionTable; + $deleteDataInfos["lastRunPretty"] = Piwik_Date::factory((int)$optionTable)->getLocalized('%day% %shortMonth% %longYear%'); + + //Calculate next run based on last run + interval + $nextScheduleRun = (int)($deleteDataInfos["lastRun"] + $deleteDataInfos["config"]["delete_logs_schedule_lowest_interval"] * 24 * 60 * 60); + + //is the calculated next run in the past? (e.g. plugin was disabled in the meantime or something) -> run ASAP + if (($nextScheduleRun - time()) <= 0) { + $deleteDataInfos["nextScheduleTime"] = $nextPossibleSchedule; + } else { + $deleteDataInfos["nextScheduleTime"] = $nextScheduleRun; + } + } + + $deleteDataInfos["nextRunPretty"] = Piwik::getPrettyTimeFromSeconds($deleteDataInfos["nextScheduleTime"] - time()); + + return $deleteDataInfos; + } + + protected function handlePluginState($state = 0) + { + $pluginController = new Piwik_CorePluginsAdmin_Controller(); + + if ($state == 1 && !Piwik_PluginsManager::getInstance()->isPluginActivated(self::ANONYMIZE_IP_PLUGIN_NAME)) { + $pluginController->activate($redirectAfter = false); + } elseif ($state == 0 && Piwik_PluginsManager::getInstance()->isPluginActivated(self::ANONYMIZE_IP_PLUGIN_NAME)) { + $pluginController->deactivate($redirectAfter = false); + } else { + //nothing to do + } + } } diff --git a/plugins/PrivacyManager/LogDataPurger.php b/plugins/PrivacyManager/LogDataPurger.php index da1b48591e..440f8f5e42 100755 --- a/plugins/PrivacyManager/LogDataPurger.php +++ b/plugins/PrivacyManager/LogDataPurger.php @@ -14,334 +14,313 @@ */ class Piwik_PrivacyManager_LogDataPurger { - const TEMP_TABLE_NAME = 'tmp_log_actions_to_keep'; - - /** - * The max set of rows each table scan select should query at one time. - */ - public static $selectSegmentSize = 100000; - - /** - * The number of days after which log entries are considered old. - */ - private $deleteLogsOlderThan; - - /** - * The number of rows to delete per DELETE query. - */ - private $maxRowsToDeletePerQuery; - - /** - * Constructor. - * - * @param int $deleteLogsOlderThan The number of days after which log entires are considered old. - * Visits and related data whose age is greater than this number - * will be purged. - * @param int $maxRowsToDeletePerQuery The maximum number of rows to delete in one query. Used to - * make sure log tables aren't locked for too long. - */ - public function __construct( $deleteLogsOlderThan, $maxRowsToDeletePerQuery ) - { - $this->deleteLogsOlderThan = $deleteLogsOlderThan; - $this->maxRowsToDeletePerQuery = $maxRowsToDeletePerQuery; - } - - /** - * Purges old data from the following tables: - * - log_visit - * - log_link_visit_action - * - log_conversion - * - log_conversion_item - * - log_action - */ - public function purgeData() - { - $maxIdVisit = $this->getDeleteIdVisitOffset(); - - // break if no ID was found (nothing to delete for given period) - if (empty($maxIdVisit)) - { - return; - } - - $logTables = self::getDeleteTableLogTables(); - - // delete data from log tables - $where = "WHERE idvisit <= ?"; - foreach ($logTables as $logTable) - { - // deleting from log_action must be handled differently, so we do it later - if ($logTable != Piwik_Common::prefixTable('log_action')) - { - Piwik_DeleteAllRows($logTable, $where, $this->maxRowsToDeletePerQuery, array($maxIdVisit)); - } - } - - // delete unused actions from the log_action table (but only if we can lock tables) - if (Piwik::isLockPrivilegeGranted()) - { - $this->purgeUnusedLogActions(); - } - else - { - $logMessage = get_class($this).": LOCK TABLES privilege not granted; skipping unused actions purge"; - Piwik::log($logMessage); - } - - // optimize table overhead after deletion - Piwik_OptimizeTables($logTables); - } - - /** - * Returns an array describing what data would be purged if purging were invoked. - * - * This function returns an array that maps table names with the number of rows - * that will be deleted. - * - * @return array - */ - public function getPurgeEstimate() - { - $result = array(); - - // deal w/ log tables that will be purged - $maxIdVisit = $this->getDeleteIdVisitOffset(); - if (!empty($maxIdVisit)) - { - foreach ($this->getDeleteTableLogTables() as $table) - { - // getting an estimate for log_action is not supported since it can take too long - if ($table != Piwik_Common::prefixTable('log_action')) - { - $rowCount = $this->getLogTableDeleteCount($table, $maxIdVisit); - if ($rowCount > 0) - { - $result[$table] = $rowCount; - } - } - } - } - - return $result; - } - - /** - * Safely delete all unused log_action rows. - */ - private function purgeUnusedLogActions() - { - $this->createTempTable(); - - // get current max ID in log tables w/ idaction references. - $maxIds = $this->getMaxIdsInLogTables(); - - // do large insert (inserting everything before maxIds) w/o locking tables... - $this->insertActionsToKeep($maxIds, $deleteOlderThanMax = true); - - // ... then do small insert w/ locked tables to minimize the amount of time tables are locked. - $this->lockLogTables(); - $this->insertActionsToKeep($maxIds, $deleteOlderThanMax = false); - - // delete before unlocking tables so there's no chance a new log row that references an - // unused action will be inserted. - $this->deleteUnusedActions(); - $this->unlockLogTables(); - } - - /** - * get highest idVisit to delete rows from - * @return string - */ - private function getDeleteIdVisitOffset() - { - $logVisit = Piwik_Common::prefixTable("log_visit"); - - // get max idvisit - $maxIdVisit = Piwik_FetchOne("SELECT MAX(idvisit) FROM $logVisit"); - if (empty($maxIdVisit)) - { - return false; - } - - // select highest idvisit to delete from - $dateStart = Piwik_Date::factory("today")->subDay($this->deleteLogsOlderThan); - $sql = "SELECT idvisit + const TEMP_TABLE_NAME = 'tmp_log_actions_to_keep'; + + /** + * The max set of rows each table scan select should query at one time. + */ + public static $selectSegmentSize = 100000; + + /** + * The number of days after which log entries are considered old. + */ + private $deleteLogsOlderThan; + + /** + * The number of rows to delete per DELETE query. + */ + private $maxRowsToDeletePerQuery; + + /** + * Constructor. + * + * @param int $deleteLogsOlderThan The number of days after which log entires are considered old. + * Visits and related data whose age is greater than this number + * will be purged. + * @param int $maxRowsToDeletePerQuery The maximum number of rows to delete in one query. Used to + * make sure log tables aren't locked for too long. + */ + public function __construct($deleteLogsOlderThan, $maxRowsToDeletePerQuery) + { + $this->deleteLogsOlderThan = $deleteLogsOlderThan; + $this->maxRowsToDeletePerQuery = $maxRowsToDeletePerQuery; + } + + /** + * Purges old data from the following tables: + * - log_visit + * - log_link_visit_action + * - log_conversion + * - log_conversion_item + * - log_action + */ + public function purgeData() + { + $maxIdVisit = $this->getDeleteIdVisitOffset(); + + // break if no ID was found (nothing to delete for given period) + if (empty($maxIdVisit)) { + return; + } + + $logTables = self::getDeleteTableLogTables(); + + // delete data from log tables + $where = "WHERE idvisit <= ?"; + foreach ($logTables as $logTable) { + // deleting from log_action must be handled differently, so we do it later + if ($logTable != Piwik_Common::prefixTable('log_action')) { + Piwik_DeleteAllRows($logTable, $where, $this->maxRowsToDeletePerQuery, array($maxIdVisit)); + } + } + + // delete unused actions from the log_action table (but only if we can lock tables) + if (Piwik::isLockPrivilegeGranted()) { + $this->purgeUnusedLogActions(); + } else { + $logMessage = get_class($this) . ": LOCK TABLES privilege not granted; skipping unused actions purge"; + Piwik::log($logMessage); + } + + // optimize table overhead after deletion + Piwik_OptimizeTables($logTables); + } + + /** + * Returns an array describing what data would be purged if purging were invoked. + * + * This function returns an array that maps table names with the number of rows + * that will be deleted. + * + * @return array + */ + public function getPurgeEstimate() + { + $result = array(); + + // deal w/ log tables that will be purged + $maxIdVisit = $this->getDeleteIdVisitOffset(); + if (!empty($maxIdVisit)) { + foreach ($this->getDeleteTableLogTables() as $table) { + // getting an estimate for log_action is not supported since it can take too long + if ($table != Piwik_Common::prefixTable('log_action')) { + $rowCount = $this->getLogTableDeleteCount($table, $maxIdVisit); + if ($rowCount > 0) { + $result[$table] = $rowCount; + } + } + } + } + + return $result; + } + + /** + * Safely delete all unused log_action rows. + */ + private function purgeUnusedLogActions() + { + $this->createTempTable(); + + // get current max ID in log tables w/ idaction references. + $maxIds = $this->getMaxIdsInLogTables(); + + // do large insert (inserting everything before maxIds) w/o locking tables... + $this->insertActionsToKeep($maxIds, $deleteOlderThanMax = true); + + // ... then do small insert w/ locked tables to minimize the amount of time tables are locked. + $this->lockLogTables(); + $this->insertActionsToKeep($maxIds, $deleteOlderThanMax = false); + + // delete before unlocking tables so there's no chance a new log row that references an + // unused action will be inserted. + $this->deleteUnusedActions(); + $this->unlockLogTables(); + } + + /** + * get highest idVisit to delete rows from + * @return string + */ + private function getDeleteIdVisitOffset() + { + $logVisit = Piwik_Common::prefixTable("log_visit"); + + // get max idvisit + $maxIdVisit = Piwik_FetchOne("SELECT MAX(idvisit) FROM $logVisit"); + if (empty($maxIdVisit)) { + return false; + } + + // select highest idvisit to delete from + $dateStart = Piwik_Date::factory("today")->subDay($this->deleteLogsOlderThan); + $sql = "SELECT idvisit FROM $logVisit - WHERE '".$dateStart->toString('Y-m-d H:i:s')."' > visit_last_action_time + WHERE '" . $dateStart->toString('Y-m-d H:i:s') . "' > visit_last_action_time AND idvisit <= ? AND idvisit > ? ORDER BY idvisit DESC LIMIT 1"; - - return Piwik_SegmentedFetchFirst($sql, $maxIdVisit, 0, -self::$selectSegmentSize); - } - - private function getLogTableDeleteCount( $table, $maxIdVisit ) - { - $sql = "SELECT COUNT(*) FROM $table WHERE idvisit <= ?"; - return (int)Piwik_FetchOne($sql, array($maxIdVisit)); - } - - private function createTempTable() - { - $sql = "CREATE TEMPORARY TABLE ".Piwik_Common::prefixTable(self::TEMP_TABLE_NAME)." ( + + return Piwik_SegmentedFetchFirst($sql, $maxIdVisit, 0, -self::$selectSegmentSize); + } + + private function getLogTableDeleteCount($table, $maxIdVisit) + { + $sql = "SELECT COUNT(*) FROM $table WHERE idvisit <= ?"; + return (int)Piwik_FetchOne($sql, array($maxIdVisit)); + } + + private function createTempTable() + { + $sql = "CREATE TEMPORARY TABLE " . Piwik_Common::prefixTable(self::TEMP_TABLE_NAME) . " ( idaction INT(11), PRIMARY KEY (idaction) )"; - Piwik_Query($sql); - } - - private function getMaxIdsInLogTables() - { - $tables = array('log_conversion', 'log_link_visit_action', 'log_visit', 'log_conversion_item'); - $idColumns = $this->getTableIdColumns(); - - $result = array(); - foreach ($tables as $table) - { - $idCol = $idColumns[$table]; - $result[$table] = Piwik_FetchOne("SELECT MAX($idCol) FROM ".Piwik_Common::prefixTable($table)); - } - - return $result; - } - - private function insertActionsToKeep( $maxIds, $olderThan = true ) - { - $tempTableName = Piwik_Common::prefixTable(self::TEMP_TABLE_NAME); - - $idColumns = $this->getTableIdColumns(); - foreach ($this->getIdActionColumns() as $table => $columns) - { - $idCol = $idColumns[$table]; - - foreach ($columns as $col) - { - $select = "SELECT $col FROM ".Piwik_Common::prefixTable($table)." WHERE $idCol >= ? AND $idCol < ?"; - $sql = "INSERT IGNORE INTO $tempTableName $select"; - - if ($olderThan) - { - $start = 0; - $finish = $maxIds[$table]; - } - else - { - $start = $maxIds[$table]; - $finish = Piwik_FetchOne("SELECT MAX($idCol) FROM ".Piwik_Common::prefixTable($table)); - } - - Piwik_SegmentedQuery($sql, $start, $finish, self::$selectSegmentSize); - } - } - - // allow code to be executed after data is inserted. for concurrency testing purposes. - if ($olderThan) - { - Piwik_PostEvent("LogDataPurger.actionsToKeepInserted.olderThan"); - } - else - { - Piwik_PostEvent("LogDataPurger.actionsToKeepInserted.newerThan"); - } - } - - private function lockLogTables() - { - Piwik_LockTables( - $readLocks = Piwik_Common::prefixTables('log_conversion', - 'log_link_visit_action', - 'log_visit', - 'log_conversion_item'), - $writeLocks = Piwik_Common::prefixTables('log_action') - ); - } - - private function unlockLogTables() - { - Piwik_UnlockAllTables(); - } - - private function deleteUnusedActions() - { - list($logActionTable, $tempTableName) = Piwik_Common::prefixTables("log_action", self::TEMP_TABLE_NAME); - - $deleteSql = "DELETE LOW_PRIORITY QUICK IGNORE $logActionTable + Piwik_Query($sql); + } + + private function getMaxIdsInLogTables() + { + $tables = array('log_conversion', 'log_link_visit_action', 'log_visit', 'log_conversion_item'); + $idColumns = $this->getTableIdColumns(); + + $result = array(); + foreach ($tables as $table) { + $idCol = $idColumns[$table]; + $result[$table] = Piwik_FetchOne("SELECT MAX($idCol) FROM " . Piwik_Common::prefixTable($table)); + } + + return $result; + } + + private function insertActionsToKeep($maxIds, $olderThan = true) + { + $tempTableName = Piwik_Common::prefixTable(self::TEMP_TABLE_NAME); + + $idColumns = $this->getTableIdColumns(); + foreach ($this->getIdActionColumns() as $table => $columns) { + $idCol = $idColumns[$table]; + + foreach ($columns as $col) { + $select = "SELECT $col FROM " . Piwik_Common::prefixTable($table) . " WHERE $idCol >= ? AND $idCol < ?"; + $sql = "INSERT IGNORE INTO $tempTableName $select"; + + if ($olderThan) { + $start = 0; + $finish = $maxIds[$table]; + } else { + $start = $maxIds[$table]; + $finish = Piwik_FetchOne("SELECT MAX($idCol) FROM " . Piwik_Common::prefixTable($table)); + } + + Piwik_SegmentedQuery($sql, $start, $finish, self::$selectSegmentSize); + } + } + + // allow code to be executed after data is inserted. for concurrency testing purposes. + if ($olderThan) { + Piwik_PostEvent("LogDataPurger.actionsToKeepInserted.olderThan"); + } else { + Piwik_PostEvent("LogDataPurger.actionsToKeepInserted.newerThan"); + } + } + + private function lockLogTables() + { + Piwik_LockTables( + $readLocks = Piwik_Common::prefixTables('log_conversion', + 'log_link_visit_action', + 'log_visit', + 'log_conversion_item'), + $writeLocks = Piwik_Common::prefixTables('log_action') + ); + } + + private function unlockLogTables() + { + Piwik_UnlockAllTables(); + } + + private function deleteUnusedActions() + { + list($logActionTable, $tempTableName) = Piwik_Common::prefixTables("log_action", self::TEMP_TABLE_NAME); + + $deleteSql = "DELETE LOW_PRIORITY QUICK IGNORE $logActionTable FROM $logActionTable LEFT JOIN $tempTableName tmp ON tmp.idaction = $logActionTable.idaction WHERE tmp.idaction IS NULL"; - - Piwik_Query($deleteSql); - } - - private function getIdActionColumns() - { - return array( - 'log_link_visit_action' => array( 'idaction_url', - 'idaction_url_ref', - 'idaction_name', - 'idaction_name_ref' ), - - 'log_conversion' => array( 'idaction_url' ), - - 'log_visit' => array( 'visit_exit_idaction_url', - 'visit_exit_idaction_name', - 'visit_entry_idaction_url', - 'visit_entry_idaction_name' ), - - 'log_conversion_item' => array( 'idaction_sku', - 'idaction_name', - 'idaction_category', - 'idaction_category2', - 'idaction_category3', - 'idaction_category4', - 'idaction_category5' ) - ); - } - - private function getTableIdColumns() - { - return array( - 'log_link_visit_action' => 'idlink_va', - 'log_conversion' => 'idvisit', - 'log_visit' => 'idvisit', - 'log_conversion_item' => 'idvisit' - ); - } - - // let's hardcode, since these are not dynamically created tables - public static function getDeleteTableLogTables() - { - $result = Piwik_Common::prefixTables('log_conversion', - 'log_link_visit_action', - 'log_visit', - 'log_conversion_item'); - if (Piwik::isLockPrivilegeGranted()) - { - $result[] = Piwik_Common::prefixTable('log_action'); - } - return $result; - } - - /** - * Utility function. Creates a new instance of LogDataPurger with the supplied array - * of settings. - * - * $settings must contain values for the following keys: - * - 'delete_logs_older_than': The number of days after which log entries are considered - * old. - * - 'delete_logs_max_rows_per_query': Max number of rows to DELETE in one query. - * - * @param array $settings Array of settings - * @param bool $useRealTable - * @return Piwik_PrivacyManager_LogDataPurger - */ - public static function make( $settings, $useRealTable = false ) - { - return new Piwik_PrivacyManager_LogDataPurger( - $settings['delete_logs_older_than'], - $settings['delete_logs_max_rows_per_query'] - ); - } + + Piwik_Query($deleteSql); + } + + private function getIdActionColumns() + { + return array( + 'log_link_visit_action' => array('idaction_url', + 'idaction_url_ref', + 'idaction_name', + 'idaction_name_ref'), + + 'log_conversion' => array('idaction_url'), + + 'log_visit' => array('visit_exit_idaction_url', + 'visit_exit_idaction_name', + 'visit_entry_idaction_url', + 'visit_entry_idaction_name'), + + 'log_conversion_item' => array('idaction_sku', + 'idaction_name', + 'idaction_category', + 'idaction_category2', + 'idaction_category3', + 'idaction_category4', + 'idaction_category5') + ); + } + + private function getTableIdColumns() + { + return array( + 'log_link_visit_action' => 'idlink_va', + 'log_conversion' => 'idvisit', + 'log_visit' => 'idvisit', + 'log_conversion_item' => 'idvisit' + ); + } + + // let's hardcode, since these are not dynamically created tables + public static function getDeleteTableLogTables() + { + $result = Piwik_Common::prefixTables('log_conversion', + 'log_link_visit_action', + 'log_visit', + 'log_conversion_item'); + if (Piwik::isLockPrivilegeGranted()) { + $result[] = Piwik_Common::prefixTable('log_action'); + } + return $result; + } + + /** + * Utility function. Creates a new instance of LogDataPurger with the supplied array + * of settings. + * + * $settings must contain values for the following keys: + * - 'delete_logs_older_than': The number of days after which log entries are considered + * old. + * - 'delete_logs_max_rows_per_query': Max number of rows to DELETE in one query. + * + * @param array $settings Array of settings + * @param bool $useRealTable + * @return Piwik_PrivacyManager_LogDataPurger + */ + public static function make($settings, $useRealTable = false) + { + return new Piwik_PrivacyManager_LogDataPurger( + $settings['delete_logs_older_than'], + $settings['delete_logs_max_rows_per_query'] + ); + } } diff --git a/plugins/PrivacyManager/PrivacyManager.php b/plugins/PrivacyManager/PrivacyManager.php index 85c7a8f5ce..840fe5cfc2 100644 --- a/plugins/PrivacyManager/PrivacyManager.php +++ b/plugins/PrivacyManager/PrivacyManager.php @@ -29,70 +29,70 @@ class Piwik_PrivacyManager extends Piwik_Plugin const OPTION_LAST_DELETE_PIWIK_REPORTS = 'lastDelete_piwik_reports'; const OPTION_LAST_DELETE_PIWIK_LOGS_INITIAL = "lastDelete_piwik_logs_initial"; const DEFAULT_MAX_ROWS_PER_QUERY = 100000; - + // default config options for data purging feature public static $defaultPurgeDataOptions = array( - 'delete_logs_enable' => 0, - 'delete_logs_schedule_lowest_interval' => 7, - 'delete_logs_older_than' => 180, - 'delete_logs_max_rows_per_query' => self::DEFAULT_MAX_ROWS_PER_QUERY, - 'delete_reports_enable' => 0, - 'delete_reports_older_than' => 12, - 'delete_reports_keep_basic_metrics' => 1, - 'delete_reports_keep_day_reports' => 0, - 'delete_reports_keep_week_reports' => 0, - 'delete_reports_keep_month_reports' => 1, - 'delete_reports_keep_year_reports' => 1, - 'delete_reports_keep_range_reports' => 0, - 'delete_reports_keep_segment_reports' => 0, - ); - + 'delete_logs_enable' => 0, + 'delete_logs_schedule_lowest_interval' => 7, + 'delete_logs_older_than' => 180, + 'delete_logs_max_rows_per_query' => self::DEFAULT_MAX_ROWS_PER_QUERY, + 'delete_reports_enable' => 0, + 'delete_reports_older_than' => 12, + 'delete_reports_keep_basic_metrics' => 1, + 'delete_reports_keep_day_reports' => 0, + 'delete_reports_keep_week_reports' => 0, + 'delete_reports_keep_month_reports' => 1, + 'delete_reports_keep_year_reports' => 1, + 'delete_reports_keep_range_reports' => 0, + 'delete_reports_keep_segment_reports' => 0, + ); + public function getInformation() { return array( - 'description' => Piwik_Translate('PrivacyManager_PluginDescription'), - 'author' => 'Piwik', + 'description' => Piwik_Translate('PrivacyManager_PluginDescription'), + 'author' => 'Piwik', 'author_homepage' => 'http://piwik.org/', - 'version' => Piwik_Version::VERSION, + 'version' => Piwik_Version::VERSION, ); } public function getListHooksRegistered() { return array( - 'AssetManager.getJsFiles' => 'getJsFiles', - 'AdminMenu.add' => 'addMenu', + 'AssetManager.getJsFiles' => 'getJsFiles', + 'AdminMenu.add' => 'addMenu', 'TaskScheduler.getScheduledTasks' => 'getScheduledTasks', ); } - /** - * @param Piwik_Event_Notification $notification notification object - */ - function getScheduledTasks($notification) - { - $tasks = &$notification->getNotificationObject(); - - // both tasks are low priority so they will execute after most others, but not lowest, so - // they will execute before the optimize tables task - - $purgeReportDataTask = new Piwik_ScheduledTask( - $this, 'deleteReportData', null, new Piwik_ScheduledTime_Daily(), Piwik_ScheduledTask::LOW_PRIORITY - ); - $tasks[] = $purgeReportDataTask; - - $purgeLogDataTask = new Piwik_ScheduledTask( - $this, 'deleteLogData', null, new Piwik_ScheduledTime_Daily(), Piwik_ScheduledTask::LOW_PRIORITY - ); - $tasks[] = $purgeLogDataTask; - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ + /** + * @param Piwik_Event_Notification $notification notification object + */ + function getScheduledTasks($notification) + { + $tasks = & $notification->getNotificationObject(); + + // both tasks are low priority so they will execute after most others, but not lowest, so + // they will execute before the optimize tables task + + $purgeReportDataTask = new Piwik_ScheduledTask( + $this, 'deleteReportData', null, new Piwik_ScheduledTime_Daily(), Piwik_ScheduledTask::LOW_PRIORITY + ); + $tasks[] = $purgeReportDataTask; + + $purgeLogDataTask = new Piwik_ScheduledTask( + $this, 'deleteLogData', null, new Piwik_ScheduledTime_Daily(), Piwik_ScheduledTask::LOW_PRIORITY + ); + $tasks[] = $purgeLogDataTask; + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ function getJsFiles($notification) { - $jsFiles = &$notification->getNotificationObject(); + $jsFiles = & $notification->getNotificationObject(); $jsFiles[] = "plugins/PrivacyManager/templates/privacySettings.js"; } @@ -100,131 +100,118 @@ class Piwik_PrivacyManager extends Piwik_Plugin function addMenu() { Piwik_AddAdminMenu('PrivacyManager_MenuPrivacySettings', - array('module' => 'PrivacyManager', 'action' => 'privacySettings'), - Piwik::isUserHasSomeAdminAccess(), - $order = 7); + array('module' => 'PrivacyManager', 'action' => 'privacySettings'), + Piwik::isUserHasSomeAdminAccess(), + $order = 7); + } + + /** + * Returns the settings for the data purging feature. + * + * @return array + */ + public static function getPurgeDataSettings() + { + $settings = array(); + + // load settings from ini config + try { + $oldSettings = array( + 'enable_auto_database_size_estimate', + + // backwards compatibility: load old values in ini config if present + 'delete_logs_enable', + 'delete_logs_schedule_lowest_interval', + 'delete_logs_older_than', + ); + + $deleteLogsSettings = Piwik_Config::getInstance()->Deletelogs; + foreach ($oldSettings as $settingName) { + $settings[$settingName] = $deleteLogsSettings[$settingName]; + } + } catch (Exception $e) { + // ignore + } + + // load the settings for the data purging settings + foreach (self::$defaultPurgeDataOptions as $optionName => $defaultValue) { + $value = Piwik_GetOption($optionName); + if ($value !== false) { + $settings[$optionName] = $value; + } else { + // if the option hasn't been set/created, use the default value + if (!isset($settings[$optionName])) { + $settings[$optionName] = $defaultValue; + } + + // option is not saved in the DB, so save it now + Piwik_SetOption($optionName, $settings[$optionName]); + } + } + + return $settings; + } + + /** + * Saves the supplied data purging settings. + * + * @param array $settings The settings to save. + */ + public static function savePurgeDataSettings($settings) + { + $plugin = Piwik_PluginsManager::getInstance()->getLoadedPlugin('PrivacyManager'); + + foreach (self::$defaultPurgeDataOptions as $optionName => $defaultValue) { + if (isset($settings[$optionName])) { + Piwik_SetOption($optionName, $settings[$optionName]); + } + } } - - /** - * Returns the settings for the data purging feature. - * - * @return array - */ - public static function getPurgeDataSettings() - { - $settings = array(); - - // load settings from ini config - try - { - $oldSettings = array( - 'enable_auto_database_size_estimate', - - // backwards compatibility: load old values in ini config if present - 'delete_logs_enable', - 'delete_logs_schedule_lowest_interval', - 'delete_logs_older_than', - ); - - $deleteLogsSettings = Piwik_Config::getInstance()->Deletelogs; - foreach ($oldSettings as $settingName) - { - $settings[$settingName] = $deleteLogsSettings[$settingName]; - } - } - catch (Exception $e) - { - // ignore - } - - // load the settings for the data purging settings - foreach (self::$defaultPurgeDataOptions as $optionName => $defaultValue) - { - $value = Piwik_GetOption($optionName); - if ($value !== false) - { - $settings[$optionName] = $value; - } - else - { - // if the option hasn't been set/created, use the default value - if (!isset($settings[$optionName])) - { - $settings[$optionName] = $defaultValue; - } - - // option is not saved in the DB, so save it now - Piwik_SetOption($optionName, $settings[$optionName]); - } - } - - return $settings; - } - - /** - * Saves the supplied data purging settings. - * - * @param array $settings The settings to save. - */ - public static function savePurgeDataSettings( $settings ) - { - $plugin = Piwik_PluginsManager::getInstance()->getLoadedPlugin('PrivacyManager'); - - foreach (self::$defaultPurgeDataOptions as $optionName => $defaultValue) - { - if (isset($settings[$optionName])) - { - Piwik_SetOption($optionName, $settings[$optionName]); - } - } - } - + /** - * Deletes old archived data (reports & metrics). - * - * Archive tables are not optimized after, as that is handled by a separate scheduled task - * in CoreAdminHome. This is a scheduled task and will only execute every N days. The number + * Deletes old archived data (reports & metrics). + * + * Archive tables are not optimized after, as that is handled by a separate scheduled task + * in CoreAdminHome. This is a scheduled task and will only execute every N days. The number * of days is determined by the delete_logs_schedule_lowest_interval config option. - * + * * If delete_reports_enable is set to 1, old archive data is deleted. The following * config options can tweak this behavior: * - delete_reports_older_than: The number of months after which archive data is considered * old. The current month is not considered when applying this * value. - * - delete_reports_keep_basic_metrics: If set to 1, keeps certain metric data. Right now, + * - delete_reports_keep_basic_metrics: If set to 1, keeps certain metric data. Right now, * all metric data is kept. * - delete_reports_keep_day_reports: If set to 1, keeps old daily reports. * - delete_reports_keep_week_reports: If set to 1, keeps old weekly reports. * - delete_reports_keep_month_reports: If set to 1, keeps old monthly reports. * - delete_reports_keep_year_reports: If set to 1, keeps old yearly reports. */ - public function deleteReportData() - { + public function deleteReportData() + { $settings = self::getPurgeDataSettings(); - + // Make sure, data deletion is enabled - if ($settings['delete_reports_enable'] == 0) - { + if ($settings['delete_reports_enable'] == 0) { return; } - + // make sure purging should run at this time (unless this is a forced purge) - if (!$this->shouldPurgeData($settings, self::OPTION_LAST_DELETE_PIWIK_REPORTS)) - { - return; + if (!$this->shouldPurgeData($settings, self::OPTION_LAST_DELETE_PIWIK_REPORTS)) { + return; } - + // set last run time Piwik_SetOption(self::OPTION_LAST_DELETE_PIWIK_REPORTS, Piwik_Date::factory('today')->getTimestamp()); - - Piwik_PrivacyManager_ReportsPurger::make($settings, self::getAllMetricsToKeep())->purgeData(); - } - + + Piwik_PrivacyManager_ReportsPurger::make($settings, self::getAllMetricsToKeep())->purgeData(); + } + /** * Deletes old log data based on the options set in the Deletelogs config * section. This is a scheduled task and will only execute every N days. The number * of days is determined by the delete_logs_schedule_lowest_interval config option. - * + * * If delete_logs_enable is set to 1, old data in the log_visit, log_conversion, * log_conversion_item and log_link_visit_action tables is deleted. The following * options can tweak this behavior: @@ -236,19 +223,17 @@ class Piwik_PrivacyManager extends Piwik_Plugin public function deleteLogData() { $settings = self::getPurgeDataSettings(); - + // Make sure, data deletion is enabled - if ($settings['delete_logs_enable'] == 0) - { + if ($settings['delete_logs_enable'] == 0) { return; } - + // make sure purging should run at this time - if (!$this->shouldPurgeData($settings, self::OPTION_LAST_DELETE_PIWIK_LOGS)) - { - return; + if (!$this->shouldPurgeData($settings, self::OPTION_LAST_DELETE_PIWIK_LOGS)) { + return; } - + /* * Tell the DB that log deletion has run BEFORE deletion is executed; * If deletion / table optimization exceeds execution time, other tasks maybe prevented of being executed @@ -256,169 +241,158 @@ class Piwik_PrivacyManager extends Piwik_Plugin */ $lastDeleteDate = Piwik_Date::factory("today")->getTimestamp(); Piwik_SetOption(self::OPTION_LAST_DELETE_PIWIK_LOGS, $lastDeleteDate); - + // execute the purge Piwik_PrivacyManager_LogDataPurger::make($settings)->purgeData(); } - + /** * Returns an array describing what data would be purged if both log data & report * purging is invoked. - * + * * The returned array maps table names with the number of rows that will be deleted. * If the table name is mapped with -1, the table will be dropped. - * + * * @param array $settings The config options to use in the estimate. If null, the real * options are used. * @return array */ - public static function getPurgeEstimate( $settings = null ) + public static function getPurgeEstimate($settings = null) + { + if (is_null($settings)) { + $settings = self::getPurgeDataSettings(); + } + + $result = array(); + + if ($settings['delete_logs_enable']) { + $logDataPurger = Piwik_PrivacyManager_LogDataPurger::make($settings); + $result = array_merge($result, $logDataPurger->getPurgeEstimate()); + } + + if ($settings['delete_reports_enable']) { + $reportsPurger = Piwik_PrivacyManager_ReportsPurger::make($settings, self::getAllMetricsToKeep()); + $result = array_merge($result, $reportsPurger->getPurgeEstimate()); + } + + return $result; + } + + /** + * Returns true if a report with the given year & month should be purged or not. + * + * If reportsOlderThan is set to null or not supplied, this function will check if + * a report should be purged, based on existing configuration. In this case, if + * delete_reports_enable is set to 0, this function will return false. + * + * @param int $reportDateYear The year of the report in question. + * @param int $reportDateMonth The month of the report in question. + * @param int|Piwik_Date $reportsOlderThan If an int, the number of months a report must be older than + * in order to be purged. If a date, the date a report must be + * older than in order to be purged. + * @return bool + */ + public static function shouldReportBePurged($reportDateYear, $reportDateMonth, $reportsOlderThan = null) + { + // if no 'older than' value/date was supplied, use existing config + if (is_null($reportsOlderThan)) { + // if report deletion is not enabled, the report shouldn't be purged + $settings = self::getPurgeDataSettings(); + if ($settings['delete_reports_enable'] == 0) { + return false; + } + + $reportsOlderThan = $settings['delete_reports_older_than']; + } + + // if a integer was supplied, assume it is the number of months a report must be older than + if (!($reportsOlderThan instanceof Piwik_Date)) { + $reportsOlderThan = Piwik_Date::factory('today')->subMonth(1 + $reportsOlderThan); + } + + return Piwik_PrivacyManager_ReportsPurger::shouldReportBePurged( + $reportDateYear, $reportDateMonth, $reportsOlderThan); + } + + /** + * Returns the general metrics to keep when the 'delete_reports_keep_basic_metrics' + * config is set to 1. + */ + private static function getMetricsToKeep() + { + return array('nb_uniq_visitors', 'nb_visits', 'nb_actions', 'max_actions', + 'sum_visit_length', 'bounce_count', 'nb_visits_converted', 'nb_conversions', + 'revenue', 'quantity', 'price', 'orders'); + } + + /** + * Returns the goal metrics to keep when the 'delete_reports_keep_basic_metrics' + * config is set to 1. + */ + private static function getGoalMetricsToKeep() + { + // keep all goal metrics + return array_values(Piwik_Archive::$mappingFromIdToNameGoal); + } + + /** + * Returns the names of metrics that should be kept when purging as they appear in + * archive tables. + */ + public static function getAllMetricsToKeep() { - if (is_null($settings)) - { - $settings = self::getPurgeDataSettings(); - } - - $result = array(); - - if ($settings['delete_logs_enable']) - { - $logDataPurger = Piwik_PrivacyManager_LogDataPurger::make($settings); - $result = array_merge($result, $logDataPurger->getPurgeEstimate()); - } - - if ($settings['delete_reports_enable']) - { - $reportsPurger = Piwik_PrivacyManager_ReportsPurger::make($settings, self::getAllMetricsToKeep()); - $result = array_merge($result, $reportsPurger->getPurgeEstimate()); - } - - return $result; + $metricsToKeep = self::getMetricsToKeep(); + + // convert goal metric names to correct archive names + if (Piwik_Common::isGoalPluginEnabled()) { + $goalMetricsToKeep = self::getGoalMetricsToKeep(); + + $maxGoalId = self::getMaxGoalId(); + + // for each goal metric, there's a different name for each goal, including the overview, + // the order report & cart report + foreach ($goalMetricsToKeep as $metric) { + for ($i = 1; $i <= $maxGoalId; ++$i) // maxGoalId can be 0 + { + $metricsToKeep[] = Piwik_Goals::getRecordName($metric, $i); + } + + $metricsToKeep[] = Piwik_Goals::getRecordName($metric); + $metricsToKeep[] = Piwik_Goals::getRecordName($metric, Piwik_Tracker_GoalManager::IDGOAL_ORDER); + $metricsToKeep[] = Piwik_Goals::getRecordName($metric, Piwik_Tracker_GoalManager::IDGOAL_CART); + } + } + + return $metricsToKeep; } - - /** - * Returns true if a report with the given year & month should be purged or not. - * - * If reportsOlderThan is set to null or not supplied, this function will check if - * a report should be purged, based on existing configuration. In this case, if - * delete_reports_enable is set to 0, this function will return false. - * - * @param int $reportDateYear The year of the report in question. - * @param int $reportDateMonth The month of the report in question. - * @param int|Piwik_Date $reportsOlderThan If an int, the number of months a report must be older than - * in order to be purged. If a date, the date a report must be - * older than in order to be purged. - * @return bool - */ - public static function shouldReportBePurged( $reportDateYear, $reportDateMonth, $reportsOlderThan = null ) - { - // if no 'older than' value/date was supplied, use existing config - if (is_null($reportsOlderThan)) - { - // if report deletion is not enabled, the report shouldn't be purged - $settings = self::getPurgeDataSettings(); - if ($settings['delete_reports_enable'] == 0) - { - return false; - } - - $reportsOlderThan = $settings['delete_reports_older_than']; - } - - // if a integer was supplied, assume it is the number of months a report must be older than - if (!($reportsOlderThan instanceof Piwik_Date)) - { - $reportsOlderThan = Piwik_Date::factory('today')->subMonth(1 + $reportsOlderThan); - } - - return Piwik_PrivacyManager_ReportsPurger::shouldReportBePurged( - $reportDateYear, $reportDateMonth, $reportsOlderThan); - } - - /** - * Returns the general metrics to keep when the 'delete_reports_keep_basic_metrics' - * config is set to 1. - */ - private static function getMetricsToKeep() - { - return array('nb_uniq_visitors', 'nb_visits', 'nb_actions', 'max_actions', - 'sum_visit_length', 'bounce_count', 'nb_visits_converted', 'nb_conversions', - 'revenue', 'quantity', 'price', 'orders'); - } - - /** - * Returns the goal metrics to keep when the 'delete_reports_keep_basic_metrics' - * config is set to 1. - */ - private static function getGoalMetricsToKeep() - { - // keep all goal metrics - return array_values(Piwik_Archive::$mappingFromIdToNameGoal); - } - - /** - * Returns the names of metrics that should be kept when purging as they appear in - * archive tables. - */ - public static function getAllMetricsToKeep() - { - $metricsToKeep = self::getMetricsToKeep(); - - // convert goal metric names to correct archive names - if (Piwik_Common::isGoalPluginEnabled()) - { - $goalMetricsToKeep = self::getGoalMetricsToKeep(); - - $maxGoalId = self::getMaxGoalId(); - - // for each goal metric, there's a different name for each goal, including the overview, - // the order report & cart report - foreach ($goalMetricsToKeep as $metric) - { - for ($i = 1; $i <= $maxGoalId; ++$i) // maxGoalId can be 0 - { - $metricsToKeep[] = Piwik_Goals::getRecordName($metric, $i); - } - - $metricsToKeep[] = Piwik_Goals::getRecordName($metric); - $metricsToKeep[] = Piwik_Goals::getRecordName($metric, Piwik_Tracker_GoalManager::IDGOAL_ORDER); - $metricsToKeep[] = Piwik_Goals::getRecordName($metric, Piwik_Tracker_GoalManager::IDGOAL_CART); - } - } - - return $metricsToKeep; - } - - /** - * Returns true if one of the purge data tasks should run now, false if it shouldn't. - */ - private function shouldPurgeData( $settings, $lastRanOption ) - { + + /** + * Returns true if one of the purge data tasks should run now, false if it shouldn't. + */ + private function shouldPurgeData($settings, $lastRanOption) + { // Log deletion may not run until it is once rescheduled (initial run). This is the // only way to guarantee the calculated next scheduled deletion time. $initialDelete = Piwik_GetOption(self::OPTION_LAST_DELETE_PIWIK_LOGS_INITIAL); - if (empty($initialDelete)) - { + if (empty($initialDelete)) { Piwik_SetOption(self::OPTION_LAST_DELETE_PIWIK_LOGS_INITIAL, 1); return false; } - + // Make sure, log purging is allowed to run now $lastDelete = Piwik_GetOption($lastRanOption); $deleteIntervalDays = $settings['delete_logs_schedule_lowest_interval']; $deleteIntervalSeconds = $this->getDeleteIntervalInSeconds($deleteIntervalDays); - - if ($lastDelete === false || - ($lastDelete !== false && ((int)$lastDelete + $deleteIntervalSeconds) <= time()) - ) - { - return true; - } - else // not time to run data purge - { - return false; - } - } + + if ($lastDelete === false || + ($lastDelete !== false && ((int)$lastDelete + $deleteIntervalSeconds) <= time()) + ) { + return true; + } else // not time to run data purge + { + return false; + } + } function getDeleteIntervalInSeconds($deleteInterval) { @@ -427,7 +401,7 @@ class Piwik_PrivacyManager extends Piwik_Plugin private static function getMaxGoalId() { - return Piwik_FetchOne("SELECT MAX(idgoal) FROM ".Piwik_Common::prefixTable('goal')); + return Piwik_FetchOne("SELECT MAX(idgoal) FROM " . Piwik_Common::prefixTable('goal')); } } diff --git a/plugins/PrivacyManager/ReportsPurger.php b/plugins/PrivacyManager/ReportsPurger.php index 88cd043b08..61c650a613 100755 --- a/plugins/PrivacyManager/ReportsPurger.php +++ b/plugins/PrivacyManager/ReportsPurger.php @@ -16,404 +16,367 @@ class Piwik_PrivacyManager_ReportsPurger { // constant used in database purging estimate to signify a table should be dropped const DROP_TABLE = -1; - - /** - * The max set of rows each table scan select should query at one time. - */ - public static $selectSegmentSize = 100000; - - /** + + /** + * The max set of rows each table scan select should query at one time. + */ + public static $selectSegmentSize = 100000; + + /** * The number of months after which report/metric data is considered old. */ - private $deleteReportsOlderThan; - - /** - * Whether to keep basic metrics or not. - */ - private $keepBasicMetrics; - - /** - * Array of period types. Reports for these periods will not be purged. - */ - private $reportPeriodsToKeep; - - /** - * Whether to keep reports for segments or not. - */ - private $keepSegmentReports; - - /** - * The maximum number of rows to delete per DELETE query. - */ - private $maxRowsToDeletePerQuery; - - /** - * List of metrics that should be kept when purging. If $keepBasicMetrics is true, - * these metrics will be saved. - */ - private $metricsToKeep; - - /** - * Array that maps a year and month ('2012_01') with lists of archive IDs for segmented - * archives. Used to keep segmented reports when purging. - */ - private $segmentArchiveIds = null; - - /** - * Constructor. - * - * @param int $deleteReportsOlderThan The number of months after which report/metric data - * is considered old. - * @param bool $keepBasicMetrics Whether to keep basic metrics or not. - * @param array $reportPeriodsToKeep Array of period types. Reports for these periods will not - * be purged. - * @param bool $keepSegmentReports Whether to keep reports for segments or not. - * @param array $metricsToKeep List of metrics that should be kept. if $keepBasicMetrics - * is true, these metrics will be saved. - * @param int $maxRowsToDeletePerQuery The maximum number of rows to delete per DELETE query. - */ - public function __construct( $deleteReportsOlderThan, $keepBasicMetrics, $reportPeriodsToKeep, - $keepSegmentReports, $metricsToKeep, $maxRowsToDeletePerQuery ) - { - $this->deleteReportsOlderThan = $deleteReportsOlderThan; - $this->keepBasicMetrics = $keepBasicMetrics; - $this->reportPeriodsToKeep = $reportPeriodsToKeep; - $this->keepSegmentReports = $keepSegmentReports; - $this->metricsToKeep = $metricsToKeep; - $this->maxRowsToDeletePerQuery = $maxRowsToDeletePerQuery; - } - - /** - * Purges old report/metric data. - * - * If $keepBasicMetrics is false, old numeric tables will be dropped, otherwise only - * the metrics not in $metricsToKeep will be deleted. - * - * If $reportPeriodsToKeep is an empty array, old blob tables will be dropped. Otherwise, - * specific reports will be deleted, except reports for periods in $reportPeriodsToKeep. - * - * @param bool $optimize If tables should be optimized after rows are deleted. Normally, - * this is handled by a scheduled task. - */ - public function purgeData($optimize = false) - { - // find archive tables to purge - list($oldNumericTables, $oldBlobTables) = $this->getArchiveTablesToPurge(); - - // process blob tables first, since archive status is stored in the numeric archives - if (!empty($oldBlobTables)) - { - // if no reports should be kept, drop tables, otherwise drop individual reports - if (empty($this->reportPeriodsToKeep) && !$this->keepSegmentReports) - { - Piwik_DropTables($oldBlobTables); - } - else - { - foreach ($oldBlobTables as $table) - { - $where = $this->getBlobTableWhereExpr($oldNumericTables, $table); - if (!empty($where)) - { - $where = "WHERE $where"; - } - Piwik_DeleteAllRows($table, $where, $this->maxRowsToDeletePerQuery); - } - - if ($optimize) - { - Piwik_OptimizeTables($oldBlobTables); - } - } - } - - // deal with numeric tables - if (!empty($oldNumericTables)) - { - // if keep_basic_metrics is set, empty all numeric tables of metrics to purge - if ($this->keepBasicMetrics == 1 && !empty($this->metricsToKeep)) - { - $where = "WHERE name NOT IN ('".implode("','", $this->metricsToKeep)."') AND name NOT LIKE 'done%'"; - foreach ($oldNumericTables as $table) - { - Piwik_DeleteAllRows($table, $where, $this->maxRowsToDeletePerQuery); - } - - if ($optimize) - { - Piwik_OptimizeTables($oldNumericTables); - } - } - else // drop numeric tables - { - Piwik_DropTables($oldNumericTables); - } - } - } - - /** - * Returns an array describing what data would be purged if purging were invoked. - * - * This function returns an array that maps table names with the number of rows - * that will be deleted. If a table name is mapped with self::DROP_TABLE, the table - * will be dropped. - * - * @return array - */ - public function getPurgeEstimate() - { - $result = array(); - - // get archive tables that will be purged - list($oldNumericTables, $oldBlobTables) = $this->getArchiveTablesToPurge(); - - // process blob tables first, since archive status is stored in the numeric archives - if (empty($this->reportPeriodsToKeep) && !$this->keepSegmentReports) - { - // not keeping any reports, so drop all tables - foreach ($oldBlobTables as $table) - { - $result[$table] = self::DROP_TABLE; - } - } - else - { - // figure out which rows will be deleted - foreach ($oldBlobTables as $table) - { - $rowCount = $this->getBlobTableDeleteCount($oldNumericTables, $table); - if ($rowCount > 0) - { - $result[$table] = $rowCount; - } - } - } - - // deal w/ numeric tables - if ($this->keepBasicMetrics == 1) - { - // figure out which rows will be deleted - foreach ($oldNumericTables as $table) - { - $rowCount = $this->getNumericTableDeleteCount($table); - if ($rowCount > 0) - { - $result[$table] = $rowCount; - } - } - } - else - { - // not keeping any metrics, so drop the entire table - foreach ($oldNumericTables as $table) - { - $result[$table] = self::DROP_TABLE; - } - } - - return $result; - } - - /** - * Utility function that finds every archive table whose reports are considered - * old. - * - * @return array An array of two arrays. The first holds the numeric archive table - * names, and the second holds the blob archive table names. - */ - private function getArchiveTablesToPurge() - { - // get month for which reports as old or older than, should be deleted - // reports whose creation date <= this month will be deleted - // (NOTE: we ignore how far we are in the current month) - $toRemoveDate = Piwik_Date::factory('today')->subMonth(1 + $this->deleteReportsOlderThan); - $toRemoveYear = (int)$toRemoveDate->toString('Y'); - $toRemoveMonth = (int)$toRemoveDate->toString('m'); - - // find all archive tables that are older than N months - $oldNumericTables = array(); - $oldBlobTables = array(); - foreach (Piwik::getTablesInstalled() as $table) - { - if (preg_match("/archive_(numeric|blob)_([0-9]+)_([0-9]+)/", $table, $matches)) - { - $type = $matches[1]; - $year = (int)$matches[2]; - $month = (int)$matches[3]; - - if (self::shouldReportBePurged($year, $month, $toRemoveDate)) - { - if ($type == "numeric") - { - $oldNumericTables[] = $table; - } - else - { - $oldBlobTables[] = $table; - } - } - } - } - - return array($oldNumericTables, $oldBlobTables); - } - - /** - * Returns true if a report with the given year & month should be purged or not. - * - * @param int $reportDateYear The year of the report in question. - * @param int $reportDateMonth The month of the report in question. - * @param Piwik_Date $toRemoveDate The date a report must be older than in order to be purged. - * @return bool - */ - public static function shouldReportBePurged( $reportDateYear, $reportDateMonth, $toRemoveDate ) - { - $toRemoveYear = (int)$toRemoveDate->toString('Y'); - $toRemoveMonth = (int)$toRemoveDate->toString('m'); - - return $reportDateYear < $toRemoveYear - || ($reportDateYear == $toRemoveYear && $reportDateMonth <= $toRemoveMonth); - } - - private function getNumericTableDeleteCount( $table ) - { - $maxIdArchive = Piwik_FetchOne("SELECT MAX(idarchive) FROM $table"); - - $sql = "SELECT COUNT(*) + private $deleteReportsOlderThan; + + /** + * Whether to keep basic metrics or not. + */ + private $keepBasicMetrics; + + /** + * Array of period types. Reports for these periods will not be purged. + */ + private $reportPeriodsToKeep; + + /** + * Whether to keep reports for segments or not. + */ + private $keepSegmentReports; + + /** + * The maximum number of rows to delete per DELETE query. + */ + private $maxRowsToDeletePerQuery; + + /** + * List of metrics that should be kept when purging. If $keepBasicMetrics is true, + * these metrics will be saved. + */ + private $metricsToKeep; + + /** + * Array that maps a year and month ('2012_01') with lists of archive IDs for segmented + * archives. Used to keep segmented reports when purging. + */ + private $segmentArchiveIds = null; + + /** + * Constructor. + * + * @param int $deleteReportsOlderThan The number of months after which report/metric data + * is considered old. + * @param bool $keepBasicMetrics Whether to keep basic metrics or not. + * @param array $reportPeriodsToKeep Array of period types. Reports for these periods will not + * be purged. + * @param bool $keepSegmentReports Whether to keep reports for segments or not. + * @param array $metricsToKeep List of metrics that should be kept. if $keepBasicMetrics + * is true, these metrics will be saved. + * @param int $maxRowsToDeletePerQuery The maximum number of rows to delete per DELETE query. + */ + public function __construct($deleteReportsOlderThan, $keepBasicMetrics, $reportPeriodsToKeep, + $keepSegmentReports, $metricsToKeep, $maxRowsToDeletePerQuery) + { + $this->deleteReportsOlderThan = $deleteReportsOlderThan; + $this->keepBasicMetrics = $keepBasicMetrics; + $this->reportPeriodsToKeep = $reportPeriodsToKeep; + $this->keepSegmentReports = $keepSegmentReports; + $this->metricsToKeep = $metricsToKeep; + $this->maxRowsToDeletePerQuery = $maxRowsToDeletePerQuery; + } + + /** + * Purges old report/metric data. + * + * If $keepBasicMetrics is false, old numeric tables will be dropped, otherwise only + * the metrics not in $metricsToKeep will be deleted. + * + * If $reportPeriodsToKeep is an empty array, old blob tables will be dropped. Otherwise, + * specific reports will be deleted, except reports for periods in $reportPeriodsToKeep. + * + * @param bool $optimize If tables should be optimized after rows are deleted. Normally, + * this is handled by a scheduled task. + */ + public function purgeData($optimize = false) + { + // find archive tables to purge + list($oldNumericTables, $oldBlobTables) = $this->getArchiveTablesToPurge(); + + // process blob tables first, since archive status is stored in the numeric archives + if (!empty($oldBlobTables)) { + // if no reports should be kept, drop tables, otherwise drop individual reports + if (empty($this->reportPeriodsToKeep) && !$this->keepSegmentReports) { + Piwik_DropTables($oldBlobTables); + } else { + foreach ($oldBlobTables as $table) { + $where = $this->getBlobTableWhereExpr($oldNumericTables, $table); + if (!empty($where)) { + $where = "WHERE $where"; + } + Piwik_DeleteAllRows($table, $where, $this->maxRowsToDeletePerQuery); + } + + if ($optimize) { + Piwik_OptimizeTables($oldBlobTables); + } + } + } + + // deal with numeric tables + if (!empty($oldNumericTables)) { + // if keep_basic_metrics is set, empty all numeric tables of metrics to purge + if ($this->keepBasicMetrics == 1 && !empty($this->metricsToKeep)) { + $where = "WHERE name NOT IN ('" . implode("','", $this->metricsToKeep) . "') AND name NOT LIKE 'done%'"; + foreach ($oldNumericTables as $table) { + Piwik_DeleteAllRows($table, $where, $this->maxRowsToDeletePerQuery); + } + + if ($optimize) { + Piwik_OptimizeTables($oldNumericTables); + } + } else // drop numeric tables + { + Piwik_DropTables($oldNumericTables); + } + } + } + + /** + * Returns an array describing what data would be purged if purging were invoked. + * + * This function returns an array that maps table names with the number of rows + * that will be deleted. If a table name is mapped with self::DROP_TABLE, the table + * will be dropped. + * + * @return array + */ + public function getPurgeEstimate() + { + $result = array(); + + // get archive tables that will be purged + list($oldNumericTables, $oldBlobTables) = $this->getArchiveTablesToPurge(); + + // process blob tables first, since archive status is stored in the numeric archives + if (empty($this->reportPeriodsToKeep) && !$this->keepSegmentReports) { + // not keeping any reports, so drop all tables + foreach ($oldBlobTables as $table) { + $result[$table] = self::DROP_TABLE; + } + } else { + // figure out which rows will be deleted + foreach ($oldBlobTables as $table) { + $rowCount = $this->getBlobTableDeleteCount($oldNumericTables, $table); + if ($rowCount > 0) { + $result[$table] = $rowCount; + } + } + } + + // deal w/ numeric tables + if ($this->keepBasicMetrics == 1) { + // figure out which rows will be deleted + foreach ($oldNumericTables as $table) { + $rowCount = $this->getNumericTableDeleteCount($table); + if ($rowCount > 0) { + $result[$table] = $rowCount; + } + } + } else { + // not keeping any metrics, so drop the entire table + foreach ($oldNumericTables as $table) { + $result[$table] = self::DROP_TABLE; + } + } + + return $result; + } + + /** + * Utility function that finds every archive table whose reports are considered + * old. + * + * @return array An array of two arrays. The first holds the numeric archive table + * names, and the second holds the blob archive table names. + */ + private function getArchiveTablesToPurge() + { + // get month for which reports as old or older than, should be deleted + // reports whose creation date <= this month will be deleted + // (NOTE: we ignore how far we are in the current month) + $toRemoveDate = Piwik_Date::factory('today')->subMonth(1 + $this->deleteReportsOlderThan); + $toRemoveYear = (int)$toRemoveDate->toString('Y'); + $toRemoveMonth = (int)$toRemoveDate->toString('m'); + + // find all archive tables that are older than N months + $oldNumericTables = array(); + $oldBlobTables = array(); + foreach (Piwik::getTablesInstalled() as $table) { + if (preg_match("/archive_(numeric|blob)_([0-9]+)_([0-9]+)/", $table, $matches)) { + $type = $matches[1]; + $year = (int)$matches[2]; + $month = (int)$matches[3]; + + if (self::shouldReportBePurged($year, $month, $toRemoveDate)) { + if ($type == "numeric") { + $oldNumericTables[] = $table; + } else { + $oldBlobTables[] = $table; + } + } + } + } + + return array($oldNumericTables, $oldBlobTables); + } + + /** + * Returns true if a report with the given year & month should be purged or not. + * + * @param int $reportDateYear The year of the report in question. + * @param int $reportDateMonth The month of the report in question. + * @param Piwik_Date $toRemoveDate The date a report must be older than in order to be purged. + * @return bool + */ + public static function shouldReportBePurged($reportDateYear, $reportDateMonth, $toRemoveDate) + { + $toRemoveYear = (int)$toRemoveDate->toString('Y'); + $toRemoveMonth = (int)$toRemoveDate->toString('m'); + + return $reportDateYear < $toRemoveYear + || ($reportDateYear == $toRemoveYear && $reportDateMonth <= $toRemoveMonth); + } + + private function getNumericTableDeleteCount($table) + { + $maxIdArchive = Piwik_FetchOne("SELECT MAX(idarchive) FROM $table"); + + $sql = "SELECT COUNT(*) FROM $table - WHERE name NOT IN ('".implode("','", $this->metricsToKeep)."') + WHERE name NOT IN ('" . implode("','", $this->metricsToKeep) . "') AND name NOT LIKE 'done%' AND idarchive >= ? AND idarchive < ?"; - - $segments = Piwik_SegmentedFetchOne($sql, 0, $maxIdArchive, self::$selectSegmentSize); - return array_sum($segments); - } - - private function getBlobTableDeleteCount( $oldNumericTables, $table ) - { - $maxIdArchive = Piwik_FetchOne("SELECT MAX(idarchive) FROM $table"); - - $sql = "SELECT COUNT(*) + + $segments = Piwik_SegmentedFetchOne($sql, 0, $maxIdArchive, self::$selectSegmentSize); + return array_sum($segments); + } + + private function getBlobTableDeleteCount($oldNumericTables, $table) + { + $maxIdArchive = Piwik_FetchOne("SELECT MAX(idarchive) FROM $table"); + + $sql = "SELECT COUNT(*) FROM $table - WHERE ".$this->getBlobTableWhereExpr($oldNumericTables, $table)." + WHERE " . $this->getBlobTableWhereExpr($oldNumericTables, $table) . " AND idarchive >= ? AND idarchive < ?"; - - $segments = Piwik_SegmentedFetchOne($sql, 0, $maxIdArchive, self::$selectSegmentSize); - return array_sum($segments); - } - - /** Returns SQL WHERE expression used to find reports that should be purged. */ - private function getBlobTableWhereExpr( $oldNumericTables, $table ) - { - $where = ""; - if (!empty($this->reportPeriodsToKeep)) // if keeping reports - { - $where = "period NOT IN (".implode(',', $this->reportPeriodsToKeep).")"; - - // if not keeping segments make sure segments w/ kept periods are also deleted - if (!$this->keepSegmentReports) - { - $this->findSegmentArchives($oldNumericTables); - $archiveIds = $this->segmentArchiveIds[$this->getArchiveTableDate($table)]; - - if (!empty($archiveIds)) - { - $where .= " OR idarchive IN (".implode(',', $archiveIds).")"; - } - } - - $where = "($where)"; - } - return $where; - } - - /** - * If we're going to keep segmented reports, we need to know which archives are - * for segments. This info is only in the numeric tables, so we must query them. - */ - private function findSegmentArchives( $numericTables ) - { - if (!is_null($this->segmentArchiveIds)) - { - return; - } - - foreach ($numericTables as $table) - { - $tableDate = $this->getArchiveTableDate($table); - - $maxIdArchive = Piwik_FetchOne("SELECT MAX(idarchive) FROM $table"); - - $sql = "SELECT idarchive + + $segments = Piwik_SegmentedFetchOne($sql, 0, $maxIdArchive, self::$selectSegmentSize); + return array_sum($segments); + } + + /** Returns SQL WHERE expression used to find reports that should be purged. */ + private function getBlobTableWhereExpr($oldNumericTables, $table) + { + $where = ""; + if (!empty($this->reportPeriodsToKeep)) // if keeping reports + { + $where = "period NOT IN (" . implode(',', $this->reportPeriodsToKeep) . ")"; + + // if not keeping segments make sure segments w/ kept periods are also deleted + if (!$this->keepSegmentReports) { + $this->findSegmentArchives($oldNumericTables); + $archiveIds = $this->segmentArchiveIds[$this->getArchiveTableDate($table)]; + + if (!empty($archiveIds)) { + $where .= " OR idarchive IN (" . implode(',', $archiveIds) . ")"; + } + } + + $where = "($where)"; + } + return $where; + } + + /** + * If we're going to keep segmented reports, we need to know which archives are + * for segments. This info is only in the numeric tables, so we must query them. + */ + private function findSegmentArchives($numericTables) + { + if (!is_null($this->segmentArchiveIds)) { + return; + } + + foreach ($numericTables as $table) { + $tableDate = $this->getArchiveTableDate($table); + + $maxIdArchive = Piwik_FetchOne("SELECT MAX(idarchive) FROM $table"); + + $sql = "SELECT idarchive FROM $table WHERE name != 'done' AND name LIKE 'done_%.%' AND idarchive >= ? AND idarchive < ?"; - - $this->segmentArchiveIds[$tableDate] = array(); - foreach (Piwik_SegmentedFetchAll($sql, 0, $maxIdArchive, self::$selectSegmentSize) as $row) - { - $this->segmentArchiveIds[$tableDate][] = $row['idarchive']; - } - } - } - - private function getArchiveTableDate( $table ) - { - preg_match("/[a-zA-Z_]+([0-9]+_[0-9]+)/", $table, $matches); - return $matches[1]; - } - - /** - * Utility function. Creates a new instance of ReportsPurger with the supplied array - * of settings. - * - * $settings must contain the following keys: - * -'delete_reports_older_than': The number of months after which reports/metrics are - * considered old. - * -'delete_reports_keep_basic_metrics': 1 if basic metrics should be kept, 0 if otherwise. - * -'delete_reports_keep_day_reports': 1 if daily reports should be kept, 0 if otherwise. - * -'delete_reports_keep_week_reports': 1 if weekly reports should be kept, 0 if otherwise. - * -'delete_reports_keep_month_reports': 1 if monthly reports should be kept, 0 if otherwise. - * -'delete_reports_keep_year_reports': 1 if yearly reports should be kept, 0 if otherwise. - * -'delete_reports_keep_range_reports': 1 if range reports should be kept, 0 if otherwise. - * -'delete_reports_keep_segment_reports': 1 if reports for segments should be kept, 0 if otherwise. - * -'delete_logs_max_rows_per_query': Maximum number of rows to delete in one DELETE query. - */ - public static function make( $settings, $metricsToKeep ) - { - return new Piwik_PrivacyManager_ReportsPurger( - $settings['delete_reports_older_than'], - $settings['delete_reports_keep_basic_metrics'] == 1, - self::getReportPeriodsToKeep($settings), - $settings['delete_reports_keep_segment_reports'] == 1, - $metricsToKeep, - $settings['delete_logs_max_rows_per_query'] - ); - } - - /** - * Utility function that returns an array period values based on the 'delete_reports_keep_*' - * settings. The period values returned are the integer values stored in the DB. - * - * @param array $deleteReportSettings The settings to use. - * @return array An array of period values that should be kept when purging old data. - */ - private static function getReportPeriodsToKeep( $settings ) - { - $keepReportPeriods = array(); - foreach (Piwik::$idPeriods as $strPeriod => $intPeriod) - { - $optionName = "delete_reports_keep_{$strPeriod}_reports"; - if ($settings[$optionName] == 1) - { - $keepReportPeriods[] = $intPeriod; - } - } - return $keepReportPeriods; - } + + $this->segmentArchiveIds[$tableDate] = array(); + foreach (Piwik_SegmentedFetchAll($sql, 0, $maxIdArchive, self::$selectSegmentSize) as $row) { + $this->segmentArchiveIds[$tableDate][] = $row['idarchive']; + } + } + } + + private function getArchiveTableDate($table) + { + preg_match("/[a-zA-Z_]+([0-9]+_[0-9]+)/", $table, $matches); + return $matches[1]; + } + + /** + * Utility function. Creates a new instance of ReportsPurger with the supplied array + * of settings. + * + * $settings must contain the following keys: + * -'delete_reports_older_than': The number of months after which reports/metrics are + * considered old. + * -'delete_reports_keep_basic_metrics': 1 if basic metrics should be kept, 0 if otherwise. + * -'delete_reports_keep_day_reports': 1 if daily reports should be kept, 0 if otherwise. + * -'delete_reports_keep_week_reports': 1 if weekly reports should be kept, 0 if otherwise. + * -'delete_reports_keep_month_reports': 1 if monthly reports should be kept, 0 if otherwise. + * -'delete_reports_keep_year_reports': 1 if yearly reports should be kept, 0 if otherwise. + * -'delete_reports_keep_range_reports': 1 if range reports should be kept, 0 if otherwise. + * -'delete_reports_keep_segment_reports': 1 if reports for segments should be kept, 0 if otherwise. + * -'delete_logs_max_rows_per_query': Maximum number of rows to delete in one DELETE query. + */ + public static function make($settings, $metricsToKeep) + { + return new Piwik_PrivacyManager_ReportsPurger( + $settings['delete_reports_older_than'], + $settings['delete_reports_keep_basic_metrics'] == 1, + self::getReportPeriodsToKeep($settings), + $settings['delete_reports_keep_segment_reports'] == 1, + $metricsToKeep, + $settings['delete_logs_max_rows_per_query'] + ); + } + + /** + * Utility function that returns an array period values based on the 'delete_reports_keep_*' + * settings. The period values returned are the integer values stored in the DB. + * + * @param array $deleteReportSettings The settings to use. + * @return array An array of period values that should be kept when purging old data. + */ + private static function getReportPeriodsToKeep($settings) + { + $keepReportPeriods = array(); + foreach (Piwik::$idPeriods as $strPeriod => $intPeriod) { + $optionName = "delete_reports_keep_{$strPeriod}_reports"; + if ($settings[$optionName] == 1) { + $keepReportPeriods[] = $intPeriod; + } + } + return $keepReportPeriods; + } } diff --git a/plugins/PrivacyManager/templates/databaseSize.tpl b/plugins/PrivacyManager/templates/databaseSize.tpl index 2093a01e82..fba6612d71 100755 --- a/plugins/PrivacyManager/templates/databaseSize.tpl +++ b/plugins/PrivacyManager/templates/databaseSize.tpl @@ -1,7 +1,7 @@

{'PrivacyManager_CurrentDBSize'|translate}: {$dbStats.currentSize}

{if isset($dbStats.sizeAfterPurge)} -

{'PrivacyManager_EstimatedDBSizeAfterPurge'|translate}: {$dbStats.sizeAfterPurge}

+

{'PrivacyManager_EstimatedDBSizeAfterPurge'|translate}: {$dbStats.sizeAfterPurge}

{/if} {if isset($dbStats.spaceSaved)} -

{'PrivacyManager_EstimatedSpaceSaved'|translate}: {$dbStats.spaceSaved}

+

{'PrivacyManager_EstimatedSpaceSaved'|translate}: {$dbStats.spaceSaved}

{/if} diff --git a/plugins/PrivacyManager/templates/privacySettings.js b/plugins/PrivacyManager/templates/privacySettings.js index 509ef85941..75de9b8caa 100644 --- a/plugins/PrivacyManager/templates/privacySettings.js +++ b/plugins/PrivacyManager/templates/privacySettings.js @@ -5,49 +5,49 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ -$(document).ready(function() { - function toggleBlock(id, value) { - $('#' + id).toggle(value == 1); - } - - function isEitherDeleteSectionEnabled() { - return ($('input[name=deleteEnable]:checked').val() == 1) - || ($('input[name=deleteReportsEnable]:checked').val() == 1); - } - - function toggleOtherDeleteSections() { - var showSection = isEitherDeleteSectionEnabled(); - toggleBlock('deleteDataEstimateSect', showSection); - toggleBlock('deleteSchedulingSettings', showSection); - } - - // reloads purged database size estimate - var currentRequest; - function reloadDbStats(forceEstimate) { - if (currentRequest) { - currentRequest.abort(); - } - - // if the section isn't visible or the manual estimate link is showing, abort - // (unless on first load or forcing) - var isFirstLoad = $('#deleteDataEstimate').html() == ''; - if (!isFirstLoad - && forceEstimate !== true - && (!isEitherDeleteSectionEnabled() || $('#getPurgeEstimateLink').length > 0)) - { - return; - } - - $('#deleteDataEstimate').hide(); - - var data = $('#formDeleteSettings').serializeArray(); +$(document).ready(function () { + function toggleBlock(id, value) { + $('#' + id).toggle(value == 1); + } + + function isEitherDeleteSectionEnabled() { + return ($('input[name=deleteEnable]:checked').val() == 1) + || ($('input[name=deleteReportsEnable]:checked').val() == 1); + } + + function toggleOtherDeleteSections() { + var showSection = isEitherDeleteSectionEnabled(); + toggleBlock('deleteDataEstimateSect', showSection); + toggleBlock('deleteSchedulingSettings', showSection); + } + + // reloads purged database size estimate + var currentRequest; + + function reloadDbStats(forceEstimate) { + if (currentRequest) { + currentRequest.abort(); + } + + // if the section isn't visible or the manual estimate link is showing, abort + // (unless on first load or forcing) + var isFirstLoad = $('#deleteDataEstimate').html() == ''; + if (!isFirstLoad + && forceEstimate !== true + && (!isEitherDeleteSectionEnabled() || $('#getPurgeEstimateLink').length > 0)) { + return; + } + + $('#deleteDataEstimate').hide(); + + var data = $('#formDeleteSettings').serializeArray(); var formData = {}; - for(var i=0; ih2').each(function() { - $(this).hide(); - }); - - if (deletingLogs) - { - confirm_id = deletingReports ? "deleteBothConfirm" : "deleteLogsConfirm"; - } - else if (deletingReports) - { - confirm_id = "deleteReportsConfirm"; - } - - if (confirm_id) - { - $("#" + confirm_id).show(); - e.preventDefault(); - - piwikHelper.modalConfirm('#confirmDeleteSettings', { - yes: function() { - $('#formDeleteSettings').submit(); - } - }); - } - else - { - $('#formDeleteSettings').submit(); - } - }); - - // execute purge now link click - $('#purgeDataNowLink').click(function(e) { - e.preventDefault(); - - var link = this; - - // if any option has been modified, abort purging and instruct user to save first - var modified = false; - $('#formDeleteSettings input').each(function() { - if (this.type === 'checkbox' || this.type === 'radio') { - modified |= this.defaultChecked !== this.checked; - } else { - modified |= this.defaultValue !== this.value; - } - }); - - if (modified) { - piwikHelper.modalConfirm('#saveSettingsBeforePurge', {yes: function() {}}); - return; - } - - // ask user if they really want to delete their old data - piwikHelper.modalConfirm('#confirmPurgeNow', { - yes: function() { - $(link).hide(); - - // execute a data purge + } + + // make sure certain sections only display if their corresponding features are enabled + $('input[name=anonymizeIPEnable]').click(function () { + toggleBlock("anonymizeIPenabled", $(this).val()); + }); + + $('input[name=deleteEnable]').click(function () { + toggleBlock("deleteLogSettings", $(this).val()); + toggleOtherDeleteSections(); + }).change(reloadDbStats); + + $('input[name=deleteReportsEnable]').click(function () { + toggleBlock("deleteReportsSettings", $(this).val()); + toggleBlock("deleteOldReportsMoreInfo", $(this).val()); + toggleOtherDeleteSections(); + }).change(reloadDbStats); + + // initial toggling calls + $(function () { + toggleBlock("deleteLogSettings", $("input[name=deleteEnable]:checked").val()); + toggleBlock("anonymizeIPenabled", $("input[name=anonymizeIPEnable]:checked").val()); + toggleBlock("deleteReportsSettings", $("input[name=deleteReportsEnable]:checked").val()); + toggleBlock("deleteOldReportsMoreInfo", $("input[name=deleteReportsEnable]:checked").val()); + toggleOtherDeleteSections(); + }); + + // make sure the DB size estimate is reloaded every time a delete logs/reports setting is changed + $('#formDeleteSettings input[type=text]').each(function () { + $(this).change(reloadDbStats); + }); + $('#formDeleteSettings input[type=checkbox]').each(function () { + $(this).click(reloadDbStats); + }); + + // make sure when the delete log/report settings are submitted, a confirmation popup is + // displayed first + $('#deleteLogSettingsSubmit').click(function (e) { + var deletingLogs = $("input[name=deleteEnable]:checked").val() == 1, + deletingReports = $("input[name=deleteReportsEnable]:checked").val() == 1, + confirm_id; + + // hide all confirmation texts, then show the correct one based on what + // type of deletion is enabled. + $('#confirmDeleteSettings>h2').each(function () { + $(this).hide(); + }); + + if (deletingLogs) { + confirm_id = deletingReports ? "deleteBothConfirm" : "deleteLogsConfirm"; + } + else if (deletingReports) { + confirm_id = "deleteReportsConfirm"; + } + + if (confirm_id) { + $("#" + confirm_id).show(); + e.preventDefault(); + + piwikHelper.modalConfirm('#confirmDeleteSettings', { + yes: function () { + $('#formDeleteSettings').submit(); + } + }); + } + else { + $('#formDeleteSettings').submit(); + } + }); + + // execute purge now link click + $('#purgeDataNowLink').click(function (e) { + e.preventDefault(); + + var link = this; + + // if any option has been modified, abort purging and instruct user to save first + var modified = false; + $('#formDeleteSettings input').each(function () { + if (this.type === 'checkbox' || this.type === 'radio') { + modified |= this.defaultChecked !== this.checked; + } else { + modified |= this.defaultValue !== this.value; + } + }); + + if (modified) { + piwikHelper.modalConfirm('#saveSettingsBeforePurge', {yes: function () {}}); + return; + } + + // ask user if they really want to delete their old data + piwikHelper.modalConfirm('#confirmPurgeNow', { + yes: function () { + $(link).hide(); + + // execute a data purge var ajaxRequest = new ajaxHelper(); ajaxRequest.setLoadingElement('#deleteSchedulingSettings .loadingPiwik'); ajaxRequest.addParams({ @@ -192,16 +188,16 @@ $(document).ready(function() { ); ajaxRequest.setFormat('html'); ajaxRequest.send(false); - } - }); - }); - - // get estimate link click - $('#getPurgeEstimateLink').click(function(e) { - e.preventDefault(); - reloadDbStats(true); - }); - - // load initial db size estimate - reloadDbStats(); + } + }); + }); + + // get estimate link click + $('#getPurgeEstimateLink').click(function (e) { + e.preventDefault(); + reloadDbStats(true); + }); + + // load initial db size estimate + reloadDbStats(); }); diff --git a/plugins/PrivacyManager/templates/privacySettings.tpl b/plugins/PrivacyManager/templates/privacySettings.tpl index 1550ffd472..f1478cb997 100644 --- a/plugins/PrivacyManager/templates/privacySettings.tpl +++ b/plugins/PrivacyManager/templates/privacySettings.tpl @@ -1,251 +1,266 @@ {include file="CoreAdminHome/templates/header.tpl"} {if $isSuperUser} +

{'PrivacyManager_TeaserHeadline'|translate}

+

{'PrivacyManager_Teaser'|translate:'':"":'':"":'':""} + See also our official guide Web Analytics Privacy

+ +

{'PrivacyManager_UseAnonymizeIp'|translate}

+
+
+ + + + + + +
{'PrivacyManager_UseAnonymizeIp'|translate}
+ {'PrivacyManager_AnonymizeIpDescription'|translate} +
+ + + + + + {'AnonymizeIP_PluginDescription'|translate|inlineHelp} +
+
+
+ + + + + + +
{'PrivacyManager_AnonymizeIpMaskLengtDescription'|translate} +
+
+ +
+ {'PrivacyManager_GeolocationAnonymizeIpNote'|translate|inlineHelp} +
+
+ +
+
+

{'PrivacyManager_DeleteLogsConfirm'|translate}

-

{'PrivacyManager_TeaserHeadline'|translate}

-

{'PrivacyManager_Teaser'|translate:'':"":'':"":'':""} -See also our official guide Web Analytics Privacy

+

{'PrivacyManager_DeleteReportsConfirm'|translate}

- -

{'PrivacyManager_UseAnonymizeIp'|translate}

-
-
- - - - - - -
{'PrivacyManager_UseAnonymizeIp'|translate}
- {'PrivacyManager_AnonymizeIpDescription'|translate} -
- - - - - - {'AnonymizeIP_PluginDescription'|translate|inlineHelp} -
-
-
- - - - - - -
{'PrivacyManager_AnonymizeIpMaskLengtDescription'|translate} -
-
- -
- {'PrivacyManager_GeolocationAnonymizeIpNote'|translate|inlineHelp} -
-
- -
+

{'PrivacyManager_DeleteBothConfirm'|translate}

+ + +
+
+

{'PrivacyManager_SaveSettingsBeforePurge'|translate}

+ +
+
+

{'PrivacyManager_PurgeNowConfirm'|translate}

+ + +
+ +

{'PrivacyManager_DeleteDataSettings'|translate}

+

{'PrivacyManager_DeleteDataDescription'|translate} {'PrivacyManager_DeleteDataDescription2'|translate}

+
+ + + + + + + + + + + +
{'PrivacyManager_UseDeleteLog'|translate}
-
-

{'PrivacyManager_DeleteLogsConfirm'|translate}

-

{'PrivacyManager_DeleteReportsConfirm'|translate}

-

{'PrivacyManager_DeleteBothConfirm'|translate}

- - -
- -
-

{'PrivacyManager_SaveSettingsBeforePurge'|translate}

- -
- -
-

{'PrivacyManager_PurgeNowConfirm'|translate}

- - -
- - -

{'PrivacyManager_DeleteDataSettings'|translate}

-

{'PrivacyManager_DeleteDataDescription'|translate} {'PrivacyManager_DeleteDataDescription2'|translate}

- - - - - + - - - - - - + + + + + + - - - - + + + + - - - - - - - - - - - - - - - - - -
{'PrivacyManager_UseDeleteLog'|translate}
- -
- - + + + {'PrivacyManager_DeleteLogDescription2'|translate} - - {'General_ClickHere'|translate} - + + {'General_ClickHere'|translate} + - - {capture assign=deleteLogInfo} - {'PrivacyManager_DeleteLogInfo'|translate:$deleteData.deleteTables} - {if !$canDeleteLogActions} -

{'PrivacyManager_CannotLockSoDeleteLogActions'|translate:$dbUser} - {/if} - {/capture} - {$deleteLogInfo|inlineHelp} -
  -
- {'PrivacyManager_LeastDaysInput'|translate:"1"} -
+ + {capture assign=deleteLogInfo} + {'PrivacyManager_DeleteLogInfo'|translate:$deleteData.deleteTables} + {if !$canDeleteLogActions} +
+
+ {'PrivacyManager_CannotLockSoDeleteLogActions'|translate:$dbUser} + {/if} + {/capture} + {$deleteLogInfo|inlineHelp} +
  +
+ {'PrivacyManager_LeastDaysInput'|translate:"1"} +
-
{'PrivacyManager_UseDeleteReports'|translate} - - - +
{'PrivacyManager_UseDeleteReports'|translate} + + + {capture assign=deleteOldLogs}{'PrivacyManager_UseDeleteLog'|translate}{/capture} - {'PrivacyManager_DeleteReportsInfo'|translate:'':''} -

- {'PrivacyManager_DeleteReportsInfo2'|translate:$deleteOldLogs}

- {'PrivacyManager_DeleteReportsInfo3'|translate:$deleteOldLogs}
+ {'PrivacyManager_DeleteReportsInfo'|translate:'':''} +

+ {'PrivacyManager_DeleteReportsInfo2'|translate:$deleteOldLogs}

+ {'PrivacyManager_DeleteReportsInfo3'|translate:$deleteOldLogs}
-
- {'PrivacyManager_DeleteReportsDetailedInfo'|translate:'archive_numeric_*':'archive_blob_*'|inlineHelp} -
  -
- {'PrivacyManager_LeastMonthsInput'|translate:"3"}

-

- {'PrivacyManager_KeepDataFor'|translate}
-
-
-
-
-

-
-
- -
{'PrivacyManager_ReportsDataSavedEstimate'|translate}
-
- -
- {if $deleteData.config.enable_auto_database_size_estimate eq '0'} - {capture assign=manualEstimate} - {'PrivacyManager_GetPurgeEstimate'|translate} - {/capture} - {$manualEstimate|inlineHelp} - {/if} -
{'PrivacyManager_DeleteSchedulingSettings'|translate}
-

-
- {capture assign=purgeStats} - {if $deleteData.lastRun}{'PrivacyManager_LastDelete'|translate}: - {$deleteData.lastRunPretty} -

{/if} - {'PrivacyManager_NextDelete'|translate}: - {$deleteData.nextRunPretty} -

{'PrivacyManager_PurgeNow'|translate} - - - {/capture} - {$purgeStats|inlineHelp} -
- - +
+ {'PrivacyManager_DeleteReportsDetailedInfo'|translate:'archive_numeric_*':'archive_blob_*'|inlineHelp} +
  +
+ {'PrivacyManager_LeastMonthsInput'|translate:"3"}

+

+ {'PrivacyManager_KeepDataFor'|translate}
+
+
+
+
+

+
+
- - - -

{'PrivacyManager_DoNotTrack_SupportDNTPreference'|translate}

- - - - + + + + + + + + + + + +
-

{if $dntSupport} - {assign var=action value=deactivate} - {'PrivacyManager_DoNotTrack_Enabled'|translate}
{'PrivacyManager_DoNotTrack_EnabledMoreInfo'|translate} - {else} - {assign var=action value=activate} - {'PrivacyManager_DoNotTrack_Disabled'|translate} {'PrivacyManager_DoNotTrack_DisabledMoreInfo'|translate} - {/if}

+
{'PrivacyManager_ReportsDataSavedEstimate'|translate}
+
+ +
+ {if $deleteData.config.enable_auto_database_size_estimate eq '0'} + {capture assign=manualEstimate} + {'PrivacyManager_GetPurgeEstimate'|translate} + {/capture} + {$manualEstimate|inlineHelp} + {/if} +
{'PrivacyManager_DeleteSchedulingSettings'|translate}
+

+
+ {capture assign=purgeStats} + {if $deleteData.lastRun}{'PrivacyManager_LastDelete'|translate}: + {$deleteData.lastRunPretty} +
+
+ {/if} + {'PrivacyManager_NextDelete'|translate}: + {$deleteData.nextRunPretty} +
+
+ {'PrivacyManager_PurgeNow'|translate} + + + {/capture} + {$purgeStats|inlineHelp} +
+ + + +

{'PrivacyManager_DoNotTrack_SupportDNTPreference'|translate}

+ + + - - -
+

{if $dntSupport} + {assign var=action value=deactivate} + {'PrivacyManager_DoNotTrack_Enabled'|translate} +
+ {'PrivacyManager_DoNotTrack_EnabledMoreInfo'|translate} + {else} + {assign var=action value=activate} + {'PrivacyManager_DoNotTrack_Disabled'|translate} {'PrivacyManager_DoNotTrack_DisabledMoreInfo'|translate} + {/if}

- › - {if $dntSupport}{'PrivacyManager_DoNotTrack_Disable'|translate} {'General_NotRecommended'|translate} - {else}{'PrivacyManager_DoNotTrack_Enable'|translate} {'General_Recommended'|translate}{/if} -
-
-
- {'PrivacyManager_DoNotTrack_Description'|translate|inlineHelp} -
- + › + {if $dntSupport}{'PrivacyManager_DoNotTrack_Disable'|translate} {'General_NotRecommended'|translate} + {else}{'PrivacyManager_DoNotTrack_Enable'|translate} {'General_Recommended'|translate}{/if} +
+
+
+ {'PrivacyManager_DoNotTrack_Description'|translate|inlineHelp} +
{/if}

{'CoreAdminHome_OptOutForYourVisitors'|translate}

{'CoreAdminHome_OptOutExplanation'|translate} -{capture name=optOutUrl}{$piwikUrl}index.php?module=CoreAdminHome&action=optOut&language={$language}{/capture} -{assign var=optOutUrl value=$smarty.capture.optOutUrl} -{capture name=iframeOptOut} -{/capture} -{$smarty.capture.iframeOptOut|escape:'html'} -
-{'CoreAdminHome_OptOutExplanationBis'|translate:"":""} + {capture name=optOutUrl}{$piwikUrl}index.php?module=CoreAdminHome&action=optOut&language={$language}{/capture} + {assign var=optOutUrl value=$smarty.capture.optOutUrl} + {capture name=iframeOptOut} + {/capture} + {$smarty.capture.iframeOptOut|escape:'html'} +
+ {'CoreAdminHome_OptOutExplanationBis'|translate:"":""}

diff --git a/plugins/Provider/API.php b/plugins/Provider/API.php index d2fe4b3bee..8ce5a43703 100644 --- a/plugins/Provider/API.php +++ b/plugins/Provider/API.php @@ -1,10 +1,10 @@ getDataTable('Provider_hostnameExt'); - $dataTable->filter('Sort', array(Piwik_Archive::INDEX_NB_VISITS)); - $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', 'Piwik_getHostnameUrl')); - $dataTable->queueFilter('ColumnCallbackReplace', array('label', 'Piwik_getHostnameName')); - $dataTable->queueFilter('ReplaceColumnNames'); - $dataTable->queueFilter('ReplaceSummaryRowLabel'); - return $dataTable; - } + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + public function getProvider($idSite, $period, $date, $segment = false) + { + Piwik::checkUserHasViewAccess($idSite); + $archive = Piwik_Archive::build($idSite, $period, $date, $segment); + $dataTable = $archive->getDataTable('Provider_hostnameExt'); + $dataTable->filter('Sort', array(Piwik_Archive::INDEX_NB_VISITS)); + $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', 'Piwik_getHostnameUrl')); + $dataTable->queueFilter('ColumnCallbackReplace', array('label', 'Piwik_getHostnameName')); + $dataTable->queueFilter('ReplaceColumnNames'); + $dataTable->queueFilter('ReplaceSummaryRowLabel'); + return $dataTable; + } } diff --git a/plugins/Provider/Controller.php b/plugins/Provider/Controller.php index 76ae424841..f59b8d1b29 100644 --- a/plugins/Provider/Controller.php +++ b/plugins/Provider/Controller.php @@ -1,10 +1,10 @@ init( $this->pluginName, __FUNCTION__, "Provider.getProvider" ); - - $this->setPeriodVariablesView($view); - $column = 'nb_visits'; - if($view->period == 'day') - { - $column = 'nb_uniq_visitors'; - } - $view->setColumnsToDisplay( array('label',$column) ); - $view->setColumnTranslation('label', Piwik_Translate('Provider_ColumnProvider')); - $view->setSortedColumn( $column ); - $view->setLimit( 5 ); - $this->setMetricsVariablesView($view); - return $this->renderView($view, $fetch); - } - + /** + * Provider + * @param bool $fetch + * @return string|void + */ + function getProvider($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, "Provider.getProvider"); + + $this->setPeriodVariablesView($view); + $column = 'nb_visits'; + if ($view->period == 'day') { + $column = 'nb_uniq_visitors'; + } + $view->setColumnsToDisplay(array('label', $column)); + $view->setColumnTranslation('label', Piwik_Translate('Provider_ColumnProvider')); + $view->setSortedColumn($column); + $view->setLimit(5); + $this->setMetricsVariablesView($view); + return $this->renderView($view, $fetch); + } + } diff --git a/plugins/Provider/Provider.php b/plugins/Provider/Provider.php index 8dba1153ae..64d18ace74 100644 --- a/plugins/Provider/Provider.php +++ b/plugins/Provider/Provider.php @@ -15,252 +15,239 @@ */ class Piwik_Provider extends Piwik_Plugin { - public function getInformation() - { - $info = array( - 'description' => Piwik_Translate('Provider_PluginDescription'), - 'author' => 'Piwik', - 'author_homepage' => 'http://piwik.org/', - 'version' => Piwik_Version::VERSION, - 'TrackerPlugin' => true, // this plugin must be loaded during the stats logging - ); - - return $info; - } - - public function getListHooksRegistered() - { - $hooks = array( - 'ArchiveProcessing_Day.compute' => 'archiveDay', - 'ArchiveProcessing_Period.compute' => 'archivePeriod', - 'Tracker.newVisitorInformation' => 'logProviderInfo', - 'WidgetsList.add' => 'addWidget', - 'Menu.add' => 'addMenu', - 'API.getReportMetadata' => 'getReportMetadata', - 'API.getSegmentsMetadata' => 'getSegmentsMetadata', - ); - return $hooks; - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - public function getReportMetadata($notification) - { - $reports = &$notification->getNotificationObject(); - $reports[] = array( - 'category' => Piwik_Translate('General_Visitors'), - 'name' => Piwik_Translate('Provider_ColumnProvider'), - 'module' => 'Provider', - 'action' => 'getProvider', - 'dimension' => Piwik_Translate('Provider_ColumnProvider'), - 'documentation' => Piwik_Translate('Provider_ProviderReportDocumentation', '
'), - 'order' => 50 - ); - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - public function getSegmentsMetadata($notification) - { - $segments =& $notification->getNotificationObject(); - $segments[] = array( - 'type' => 'dimension', - 'category' => 'Visit Location', - 'name' => Piwik_Translate('Provider_ColumnProvider'), - 'segment' => 'provider', - 'acceptedValues' => 'comcast.net, proxad.net, etc.', - 'sqlSegment' => 'log_visit.location_provider' - ); - } - - function install() - { - // add column hostname / hostname ext in the visit table - $query = "ALTER IGNORE TABLE `".Piwik_Common::prefixTable('log_visit')."` ADD `location_provider` VARCHAR( 100 ) NULL"; - - // if the column already exist do not throw error. Could be installed twice... - try { - Piwik_Exec($query); - } - catch(Exception $e) { - if(!Zend_Registry::get('db')->isErrNo($e, '1060')) - { - throw $e; - } - } - - } - - function uninstall() - { - // add column hostname / hostname ext in the visit table - $query = "ALTER TABLE `".Piwik_Common::prefixTable('log_visit')."` DROP `location_provider`"; - Piwik_Exec($query); - } - - function addWidget() - { - Piwik_AddWidget( 'General_Visitors', 'Provider_WidgetProviders', 'Provider', 'getProvider'); - } - - function addMenu() - { - Piwik_RenameMenuEntry( 'General_Visitors', 'UserCountry_SubmenuLocations', - 'General_Visitors', 'Provider_SubmenuLocationsProvider'); - } - - function postLoad() - { - Piwik_AddAction('template_footerUserCountry', array('Piwik_Provider','footerUserCountry')); - } - - /** - * @param Piwik_Event_Notification $notification notification object - * @return mixed - */ - function archivePeriod( $notification ) - { - $maximumRowsInDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_standard']; - $archiveProcessing = $notification->getNotificationObject(); - - if(!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; - - $dataTableToSum = array( 'Provider_hostnameExt' ); - $archiveProcessing->archiveDataTable($dataTableToSum, null, $maximumRowsInDataTable); - } - - /** - * Daily archive: processes the report Visits by Provider - * - * @param Piwik_Event_Notification $notification notification object - */ - function archiveDay($notification) - { - $archiveProcessing = $notification->getNotificationObject(); - - if(!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; - - $recordName = 'Provider_hostnameExt'; - $labelSQL = "log_visit.location_provider"; - $interestByProvider = $archiveProcessing->getArrayInterestForLabel($labelSQL); - $tableProvider = $archiveProcessing->getDataTableFromArray($interestByProvider); - $columnToSortByBeforeTruncation = Piwik_Archive::INDEX_NB_VISITS; - $maximumRowsInDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_standard']; - $archiveProcessing->insertBlobRecord($recordName, $tableProvider->getSerialized($maximumRowsInDataTable, null, $columnToSortByBeforeTruncation)); - destroy($tableProvider); - } - - /** - * Logs the provider in the log_visit table - * - * @param Piwik_Event_Notification $notification notification object - */ - public function logProviderInfo($notification) - { - $visitorInfo =& $notification->getNotificationObject(); - - // if provider info has already been set, abort - if (!empty($visitorInfo['location_provider'])) - { - return; - } - - $ip = Piwik_IP::N2P($visitorInfo['location_ip']); - - // In case the IP was anonymized, we should not continue since the DNS reverse lookup will fail and this will slow down tracking - if(substr($ip, -2, 2) == '.0') - { - printDebug("IP Was anonymized so we skip the Provider DNS reverse lookup..."); - return; - } - - $hostname = $this->getHost($ip); - $hostnameExtension = $this->getCleanHostname($hostname); - - // add the provider value in the table log_visit - $visitorInfo['location_provider'] = $hostnameExtension; - $visitorInfo['location_provider'] = substr($visitorInfo['location_provider'], 0, 100); - - // improve the country using the provider extension if valid - $hostnameDomain = substr($hostnameExtension, 1 + strrpos($hostnameExtension, '.')); - if($hostnameDomain == 'uk') - { - $hostnameDomain = 'gb'; - } - if(array_key_exists($hostnameDomain, Piwik_Common::getCountriesList())) - { - $visitorInfo['location_country'] = $hostnameDomain; - } - } - - /** - * Returns the hostname extension (site.co.jp in fvae.VARG.ceaga.site.co.jp) - * given the full hostname looked up from the IP - * - * @param string $hostname - * - * @return string - */ - private function getCleanHostname($hostname) - { - $extToExclude = array( - 'com', 'net', 'org', 'co' - ); - - $off = strrpos($hostname, '.'); - $ext = substr($hostname, $off); - - if(empty($off) || is_numeric($ext) || strlen($hostname) < 5) - { - return 'Ip'; - } - else - { - $cleanHostname = null; - Piwik_PostEvent('Provider.getCleanHostname', $cleanHostname, $hostname); - if($cleanHostname !== null) - { - return $cleanHostname; - } - - $e = explode('.', $hostname); - $s = sizeof($e); - - // if extension not correct - if(isset($e[$s-2]) && in_array($e[$s-2], $extToExclude)) - { - return $e[$s-3].".".$e[$s-2].".".$e[$s-1]; - } - else - { - return $e[$s-2].".".$e[$s-1]; - } - } - } - - /** - * Returns the hostname given the IP address string - * - * @param string $ip IP Address - * @return string hostname (or human-readable IP address) - */ - private function getHost($ip) - { - return trim(strtolower(@Piwik_IP::getHostByAddr($ip))); - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - static public function footerUserCountry($notification) - { - $out =& $notification->getNotificationObject(); - $out = '
-

'.Piwik_Translate('Provider_WidgetProviders').'

'; - $out .= Piwik_FrontController::getInstance()->fetchDispatch('Provider','getProvider'); - $out .= '
'; - } + public function getInformation() + { + $info = array( + 'description' => Piwik_Translate('Provider_PluginDescription'), + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'version' => Piwik_Version::VERSION, + 'TrackerPlugin' => true, // this plugin must be loaded during the stats logging + ); + + return $info; + } + + public function getListHooksRegistered() + { + $hooks = array( + 'ArchiveProcessing_Day.compute' => 'archiveDay', + 'ArchiveProcessing_Period.compute' => 'archivePeriod', + 'Tracker.newVisitorInformation' => 'logProviderInfo', + 'WidgetsList.add' => 'addWidget', + 'Menu.add' => 'addMenu', + 'API.getReportMetadata' => 'getReportMetadata', + 'API.getSegmentsMetadata' => 'getSegmentsMetadata', + ); + return $hooks; + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + public function getReportMetadata($notification) + { + $reports = & $notification->getNotificationObject(); + $reports[] = array( + 'category' => Piwik_Translate('General_Visitors'), + 'name' => Piwik_Translate('Provider_ColumnProvider'), + 'module' => 'Provider', + 'action' => 'getProvider', + 'dimension' => Piwik_Translate('Provider_ColumnProvider'), + 'documentation' => Piwik_Translate('Provider_ProviderReportDocumentation', '
'), + 'order' => 50 + ); + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + public function getSegmentsMetadata($notification) + { + $segments =& $notification->getNotificationObject(); + $segments[] = array( + 'type' => 'dimension', + 'category' => 'Visit Location', + 'name' => Piwik_Translate('Provider_ColumnProvider'), + 'segment' => 'provider', + 'acceptedValues' => 'comcast.net, proxad.net, etc.', + 'sqlSegment' => 'log_visit.location_provider' + ); + } + + function install() + { + // add column hostname / hostname ext in the visit table + $query = "ALTER IGNORE TABLE `" . Piwik_Common::prefixTable('log_visit') . "` ADD `location_provider` VARCHAR( 100 ) NULL"; + + // if the column already exist do not throw error. Could be installed twice... + try { + Piwik_Exec($query); + } catch (Exception $e) { + if (!Zend_Registry::get('db')->isErrNo($e, '1060')) { + throw $e; + } + } + + } + + function uninstall() + { + // add column hostname / hostname ext in the visit table + $query = "ALTER TABLE `" . Piwik_Common::prefixTable('log_visit') . "` DROP `location_provider`"; + Piwik_Exec($query); + } + + function addWidget() + { + Piwik_AddWidget('General_Visitors', 'Provider_WidgetProviders', 'Provider', 'getProvider'); + } + + function addMenu() + { + Piwik_RenameMenuEntry('General_Visitors', 'UserCountry_SubmenuLocations', + 'General_Visitors', 'Provider_SubmenuLocationsProvider'); + } + + function postLoad() + { + Piwik_AddAction('template_footerUserCountry', array('Piwik_Provider', 'footerUserCountry')); + } + + /** + * @param Piwik_Event_Notification $notification notification object + * @return mixed + */ + function archivePeriod($notification) + { + $maximumRowsInDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_standard']; + $archiveProcessing = $notification->getNotificationObject(); + + if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; + + $dataTableToSum = array('Provider_hostnameExt'); + $archiveProcessing->archiveDataTable($dataTableToSum, null, $maximumRowsInDataTable); + } + + /** + * Daily archive: processes the report Visits by Provider + * + * @param Piwik_Event_Notification $notification notification object + */ + function archiveDay($notification) + { + $archiveProcessing = $notification->getNotificationObject(); + + if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; + + $recordName = 'Provider_hostnameExt'; + $labelSQL = "log_visit.location_provider"; + $interestByProvider = $archiveProcessing->getArrayInterestForLabel($labelSQL); + $tableProvider = $archiveProcessing->getDataTableFromArray($interestByProvider); + $columnToSortByBeforeTruncation = Piwik_Archive::INDEX_NB_VISITS; + $maximumRowsInDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_standard']; + $archiveProcessing->insertBlobRecord($recordName, $tableProvider->getSerialized($maximumRowsInDataTable, null, $columnToSortByBeforeTruncation)); + destroy($tableProvider); + } + + /** + * Logs the provider in the log_visit table + * + * @param Piwik_Event_Notification $notification notification object + */ + public function logProviderInfo($notification) + { + $visitorInfo =& $notification->getNotificationObject(); + + // if provider info has already been set, abort + if (!empty($visitorInfo['location_provider'])) { + return; + } + + $ip = Piwik_IP::N2P($visitorInfo['location_ip']); + + // In case the IP was anonymized, we should not continue since the DNS reverse lookup will fail and this will slow down tracking + if (substr($ip, -2, 2) == '.0') { + printDebug("IP Was anonymized so we skip the Provider DNS reverse lookup..."); + return; + } + + $hostname = $this->getHost($ip); + $hostnameExtension = $this->getCleanHostname($hostname); + + // add the provider value in the table log_visit + $visitorInfo['location_provider'] = $hostnameExtension; + $visitorInfo['location_provider'] = substr($visitorInfo['location_provider'], 0, 100); + + // improve the country using the provider extension if valid + $hostnameDomain = substr($hostnameExtension, 1 + strrpos($hostnameExtension, '.')); + if ($hostnameDomain == 'uk') { + $hostnameDomain = 'gb'; + } + if (array_key_exists($hostnameDomain, Piwik_Common::getCountriesList())) { + $visitorInfo['location_country'] = $hostnameDomain; + } + } + + /** + * Returns the hostname extension (site.co.jp in fvae.VARG.ceaga.site.co.jp) + * given the full hostname looked up from the IP + * + * @param string $hostname + * + * @return string + */ + private function getCleanHostname($hostname) + { + $extToExclude = array( + 'com', 'net', 'org', 'co' + ); + + $off = strrpos($hostname, '.'); + $ext = substr($hostname, $off); + + if (empty($off) || is_numeric($ext) || strlen($hostname) < 5) { + return 'Ip'; + } else { + $cleanHostname = null; + Piwik_PostEvent('Provider.getCleanHostname', $cleanHostname, $hostname); + if ($cleanHostname !== null) { + return $cleanHostname; + } + + $e = explode('.', $hostname); + $s = sizeof($e); + + // if extension not correct + if (isset($e[$s - 2]) && in_array($e[$s - 2], $extToExclude)) { + return $e[$s - 3] . "." . $e[$s - 2] . "." . $e[$s - 1]; + } else { + return $e[$s - 2] . "." . $e[$s - 1]; + } + } + } + + /** + * Returns the hostname given the IP address string + * + * @param string $ip IP Address + * @return string hostname (or human-readable IP address) + */ + private function getHost($ip) + { + return trim(strtolower(@Piwik_IP::getHostByAddr($ip))); + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + static public function footerUserCountry($notification) + { + $out =& $notification->getNotificationObject(); + $out = '
+

' . Piwik_Translate('Provider_WidgetProviders') . '

'; + $out .= Piwik_FrontController::getInstance()->fetchDispatch('Provider', 'getProvider'); + $out .= '
'; + } } diff --git a/plugins/Provider/functions.php b/plugins/Provider/functions.php index ec1afd4518..34f09534c0 100644 --- a/plugins/Provider/functions.php +++ b/plugins/Provider/functions.php @@ -1,10 +1,10 @@ imageData = 'data:image/png;base64,'. Piwik_Common::getRequestVar('imageData', self::TRANSPARENT_PNG_PIXEL, 'string', $_POST); - echo $view->render(); - } - - function exportImage() - { - self::exportImageWindow(); - } - - /** - * Output binary image from base-64 encoded data. - * - * @deprecated 1.5.1 - * - * @param string $imageData Base-64 encoded image data (via $_POST) - */ - static public function outputBinaryImage() - { - Piwik::checkUserHasSomeViewAccess(); - - $rawData = Piwik_Common::getRequestVar('imageData', '', 'string', $_POST); - - // returns false if any illegal characters in input - $data = base64_decode($rawData); - if($data !== false) - { - // check for PNG header - if(Piwik_Common::substr($data, 0, 8) === "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a") - { - header('Content-Type: image/png'); - - // more robust validation (if available) - if(function_exists('imagecreatefromstring')) - { - // validate image data - $imgResource = @imagecreatefromstring($data); - if($imgResource !== false) - { - // output image and clean-up - imagepng($imgResource); - imagedestroy($imgResource); - exit; - } - } - else - { - echo $data; - exit; - } - } - } - - Piwik::setHttpStatus('400 Bad Request'); - exit; - } - - function outputImage() - { - self::outputBinaryImage(); - } - - /** - * Output the merged CSS file. - * This method is called when the asset manager is enabled. - * - * @see core/AssetManager.php - */ - public function getCss() - { - $cssMergedFile = Piwik_AssetManager::getMergedCssFileLocation(); - Piwik::serveStaticFile($cssMergedFile, "text/css"); - } - - /** - * Output the merged JavaScript file. - * This method is called when the asset manager is enabled. - * - * @see core/AssetManager.php - */ - public function getJs() - { - $jsMergedFile = Piwik_AssetManager::getMergedJsFileLocation(); - Piwik::serveStaticFile($jsMergedFile, "application/javascript; charset=UTF-8"); - } - - /** - * Output redirection page instead of linking directly to avoid - * exposing the referrer on the Piwik demo. - * - * @param string $url (via $_GET) - */ - public function redirect() - { - $url = Piwik_Common::getRequestVar('url', '', 'string', $_GET); - - // validate referrer - $referrer = Piwik_Url::getReferer(); - if(empty($referrer) || !Piwik_Url::isLocalUrl($referrer)) - { - die('Invalid Referer detected - This means that your web browser is not sending the "Referer URL" which is +{ + const TRANSPARENT_PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII='; + + /** + * Display the "Export Image" window. + * + * @deprecated 1.5.1 + * + * @param string $imageData Base-64 encoded image data (via $_POST) + */ + static public function exportImageWindow() + { + Piwik::checkUserHasSomeViewAccess(); + + $view = Piwik_View::factory('exportImage'); + $view->imageData = 'data:image/png;base64,' . Piwik_Common::getRequestVar('imageData', self::TRANSPARENT_PNG_PIXEL, 'string', $_POST); + echo $view->render(); + } + + function exportImage() + { + self::exportImageWindow(); + } + + /** + * Output binary image from base-64 encoded data. + * + * @deprecated 1.5.1 + * + * @param string $imageData Base-64 encoded image data (via $_POST) + */ + static public function outputBinaryImage() + { + Piwik::checkUserHasSomeViewAccess(); + + $rawData = Piwik_Common::getRequestVar('imageData', '', 'string', $_POST); + + // returns false if any illegal characters in input + $data = base64_decode($rawData); + if ($data !== false) { + // check for PNG header + if (Piwik_Common::substr($data, 0, 8) === "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a") { + header('Content-Type: image/png'); + + // more robust validation (if available) + if (function_exists('imagecreatefromstring')) { + // validate image data + $imgResource = @imagecreatefromstring($data); + if ($imgResource !== false) { + // output image and clean-up + imagepng($imgResource); + imagedestroy($imgResource); + exit; + } + } else { + echo $data; + exit; + } + } + } + + Piwik::setHttpStatus('400 Bad Request'); + exit; + } + + function outputImage() + { + self::outputBinaryImage(); + } + + /** + * Output the merged CSS file. + * This method is called when the asset manager is enabled. + * + * @see core/AssetManager.php + */ + public function getCss() + { + $cssMergedFile = Piwik_AssetManager::getMergedCssFileLocation(); + Piwik::serveStaticFile($cssMergedFile, "text/css"); + } + + /** + * Output the merged JavaScript file. + * This method is called when the asset manager is enabled. + * + * @see core/AssetManager.php + */ + public function getJs() + { + $jsMergedFile = Piwik_AssetManager::getMergedJsFileLocation(); + Piwik::serveStaticFile($jsMergedFile, "application/javascript; charset=UTF-8"); + } + + /** + * Output redirection page instead of linking directly to avoid + * exposing the referrer on the Piwik demo. + * + * @param string $url (via $_GET) + */ + public function redirect() + { + $url = Piwik_Common::getRequestVar('url', '', 'string', $_GET); + + // validate referrer + $referrer = Piwik_Url::getReferer(); + if (empty($referrer) || !Piwik_Url::isLocalUrl($referrer)) { + die('Invalid Referer detected - This means that your web browser is not sending the "Referer URL" which is required to proceed with the redirect. Verify your browser settings and add-ons, to check why your browser is not sending this referer. -

You can access the page at: '. $url); - } - - // mask visits to *.piwik.org - if (!self::isPiwikUrl($url)) - { - Piwik::checkUserHasSomeViewAccess(); - } - if(!Piwik_Common::isLookLikeUrl($url)) - { - die('Please check the &url= parameter: it should to be a valid URL'); - } - @header('Content-Type: text/html; charset=utf-8'); - echo ''; - - exit; - } - - /** - * Validate URL against *.piwik.org domains - * - * @param string $url - * @return bool True if valid; false otherwise - */ - static public function isPiwikUrl($url) - { - // guard for IE6 meta refresh parsing weakness (OSVDB 19029) - if(strpos($url, ';') !== false - || strpos($url, ';') !== false) - { - return false; - } - if(preg_match('~^http://(qa\.|demo\.|dev\.|forum\.)?piwik.org([#?/]|$)~', $url)) - { - return true; - } - - // Allow clockworksms domain - if(strpos($url, 'http://www.clockworksms.com/') === 0) - { - return true; - } - - return false; - } +

You can access the page at: ' . $url); + } + + // mask visits to *.piwik.org + if (!self::isPiwikUrl($url)) { + Piwik::checkUserHasSomeViewAccess(); + } + if (!Piwik_Common::isLookLikeUrl($url)) { + die('Please check the &url= parameter: it should to be a valid URL'); + } + @header('Content-Type: text/html; charset=utf-8'); + echo ''; + + exit; + } + + /** + * Validate URL against *.piwik.org domains + * + * @param string $url + * @return bool True if valid; false otherwise + */ + static public function isPiwikUrl($url) + { + // guard for IE6 meta refresh parsing weakness (OSVDB 19029) + if (strpos($url, ';') !== false + || strpos($url, ';') !== false + ) { + return false; + } + if (preg_match('~^http://(qa\.|demo\.|dev\.|forum\.)?piwik.org([#?/]|$)~', $url)) { + return true; + } + + // Allow clockworksms domain + if (strpos($url, 'http://www.clockworksms.com/') === 0) { + return true; + } + + return false; + } } diff --git a/plugins/Proxy/Proxy.php b/plugins/Proxy/Proxy.php index 619c388644..c669215411 100644 --- a/plugins/Proxy/Proxy.php +++ b/plugins/Proxy/Proxy.php @@ -1,10 +1,10 @@ 'Proxy services', - 'author' => 'Piwik', - 'author_homepage' => 'http://piwik.org/', - 'version' => Piwik_Version::VERSION, - 'translationAvailable' => false, - ); - } + /** + * Return information about this plugin. + * + * @see Piwik_Plugin + * + * @return array + */ + public function getInformation() + { + return array( + 'description' => 'Proxy services', + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'version' => Piwik_Version::VERSION, + 'translationAvailable' => false, + ); + } } diff --git a/plugins/Proxy/templates/exportImage.tpl b/plugins/Proxy/templates/exportImage.tpl index 037b0b8fd8..451b5dedb6 100644 --- a/plugins/Proxy/templates/exportImage.tpl +++ b/plugins/Proxy/templates/exportImage.tpl @@ -1,12 +1,13 @@ + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" /> - - - {'General_ExportAsImage_js'|translate} - - -

-

{'General_SaveImageOnYourComputer_js'|translate}

- + + + {'General_ExportAsImage_js'|translate} + + +

+ +

{'General_SaveImageOnYourComputer_js'|translate}

+ diff --git a/plugins/Referers/API.php b/plugins/Referers/API.php index a74f14290c..ade4026c5b 100644 --- a/plugins/Referers/API.php +++ b/plugins/Referers/API.php @@ -11,510 +11,477 @@ /** * The Referrers API lets you access reports about Websites, Search engines, Keywords, Campaigns used to access your website. - * - * For example, "getKeywords" returns all search engine keywords (with general analytics metrics for each keyword), "getWebsites" returns referrer websites (along with the full Referrer URL if the parameter &expanded=1 is set). + * + * For example, "getKeywords" returns all search engine keywords (with general analytics metrics for each keyword), "getWebsites" returns referrer websites (along with the full Referrer URL if the parameter &expanded=1 is set). * "getRefererType" returns the Referrer overview report. "getCampaigns" returns the list of all campaigns (and all campaign keywords if the parameter &expanded=1 is set). - * - * The methods "getKeywordsForPageUrl" and "getKeywordsForPageTitle" are used to output the top keywords used to find a page. + * + * The methods "getKeywordsForPageUrl" and "getKeywordsForPageTitle" are used to output the top keywords used to find a page. * Check out the widget "Top keywords used to find this page" that you can easily re-use on your website. * @package Piwik_Referers */ -class Piwik_Referers_API +class Piwik_Referers_API { - static private $instance = null; - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - /** - * @return Piwik_DataTable - */ - protected function getDataTable($name, $idSite, $period, $date, $segment, $expanded = false, $idSubtable = null) - { - $dataTable = Piwik_Archive::getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded, $idSubtable); - $dataTable->filter('Sort', array(Piwik_Archive::INDEX_NB_VISITS, 'desc', $naturalSort = false, $expanded)); - $dataTable->queueFilter('ReplaceColumnNames'); - return $dataTable; - } - - /** - * Returns a report describing visit information for each possible referrer type. The - * result is a datatable whose subtables are the reports for each parent row's referrer type. - * - * The subtable reports are: 'getKeywords' (for search engine referrer type), 'getWebsites', - * and 'getCampaigns'. - * - * @param string $idSite The site ID. - * @param string $period The period to get data for, either 'day', 'week', 'month', 'year', - * or 'range'. - * @param string $date The date of the period. - * @param string $segment The segment to use. - * @param int $typeReferer (deprecated) If you want to get data only for a specific referrer - * type, supply a type for this parameter. - * @param int $idSubtable For this report this value is a referrer type ID and not an actual - * subtable ID. The result when using this parameter will be the - * specific report for the given referrer type. - * @param bool $expanded Whether to get report w/ subtables loaded or not. - * @return Piwik_DataTable - */ - public function getRefererType($idSite, $period, $date, $segment = false, $typeReferer = false, - $idSubtable = false, $expanded = false) - { - // if idSubtable is supplied, interpret idSubtable as referrer type and return correct report - if ($idSubtable !== false) - { - $result = false; - switch ($idSubtable) - { - case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: - $result = $this->getKeywords($idSite, $period, $date, $segment); - break; - case Piwik_Common::REFERER_TYPE_WEBSITE: - $result = $this->getWebsites($idSite, $period, $date, $segment); - break; - case Piwik_Common::REFERER_TYPE_CAMPAIGN: - $result = $this->getCampaigns($idSite, $period, $date, $segment); - break; - default: // invalid idSubtable, return whole report - break; - } - - if ($result) - { - return $this->removeSubtableIds($result); // this report won't return subtables of individual reports - } - } - - // get visits by referrer type - $dataTable = $this->getDataTable('Referers_type', $idSite, $period, $date, $segment); - - if ($typeReferer !== false) // filter for a specific referrer type - { - $dataTable->filter('Pattern', array('label', $typeReferer)); - } - - // set subtable IDs for each row to the label (which holds the int referrer type) - // NOTE: not yet possible to do this w/ DataTable_Array instances - if (!($dataTable instanceof Piwik_DataTable_Array)) - { - $this->setGetReferrerTypeSubtables($dataTable, $idSite, $period, $date, $segment, $expanded); - } - - // set referrer type column to readable value - $dataTable->queueFilter('ColumnCallbackReplace', array('label', 'Piwik_getRefererTypeLabel')); - - return $dataTable; - } - - /** - * Returns a report that shows - */ - public function getAll( $idSite, $period, $date, $segment = false ) - { - $dataTable = $this->getRefererType($idSite, $period, $date, $segment, $typeReferer = false, - $idSubtable = false, $expanded = true); - - if ($dataTable instanceof Piwik_DataTable_Array) - { - throw new Exception("Referrers.getAll with multiple sites or dates is not supported (yet)."); - } - - $dataTable = $dataTable->mergeSubtables($labelColumn = 'referrer_type', $useMetadataColumn = true); - - // presentation filters - $dataTable->filter('Sort', array(Piwik_Archive::INDEX_NB_VISITS, 'desc')); - $dataTable->queueFilter('ReplaceColumnNames'); - $dataTable->queueFilter('ReplaceSummaryRowLabel'); - - return $dataTable; - } - - public function getKeywords($idSite, $period, $date, $segment = false, $expanded = false) - { - $dataTable = $this->getDataTable('Referers_searchEngineByKeyword', $idSite, $period, $date, $segment, $expanded); - $dataTable = $this->handleKeywordNotDefined($dataTable); - return $dataTable; - } - - protected function handleKeywordNotDefined($dataTable) - { - $dataTable->queueFilter('ColumnCallbackReplace', array('label', array('Piwik_Referers', 'getCleanKeyword'))); - return $dataTable; - } - - public function getKeywordsForPageUrl($idSite, $period, $date, $url) - { - // Fetch the Top keywords for this page - $segment = 'entryPageUrl=='.$url; - $table = $this->getKeywords($idSite, $period, $date, $segment); - $this->filterOutKeywordNotDefined($table); - return $this->getLabelsFromTable($table); - - } - - public function getKeywordsForPageTitle($idSite, $period, $date, $title) - { - $segment = 'entryPageTitle=='.$title; - $table = $this->getKeywords($idSite, $period, $date, $segment); - $this->filterOutKeywordNotDefined($table); - return $this->getLabelsFromTable($table); - } - - /** - * @param Piwik_Datatable $table - */ - private function filterOutKeywordNotDefined($table) - { - if($table instanceof Piwik_Datatable) - { - $row = $table->getRowIdFromLabel(''); - if($row) - { - $table->deleteRow($row); - } - } - } - - protected function getLabelsFromTable($table) - { - $request = $_GET; - $request['serialize'] = 0; - - // Apply generic filters - $response = new Piwik_API_ResponseBuilder($format = 'original', $request); - $table = $response->getResponse($table); - - // If period=lastX we only keep the first resultset as we want to return a plain list - if($table instanceof Piwik_DataTable_Array) - { - $tables = $table->getArray(); - $table = current($tables); - } - // Keep the response simple, only include keywords - $keywords = $table->getColumn('label'); - return $keywords; - } - - public function getSearchEnginesFromKeywordId($idSite, $period, $date, $idSubtable, $segment = false) - { - $dataTable = $this->getDataTable('Referers_searchEngineByKeyword',$idSite, $period, $date, $segment, $expanded = false, $idSubtable); - $dataTable->queueFilter('ColumnCallbackAddMetadata', array( 'label', 'url', 'Piwik_getSearchEngineUrlFromName') ); - $dataTable->queueFilter('MetadataCallbackAddMetadata', array( 'url', 'logo', 'Piwik_getSearchEngineLogoFromUrl') ); - - // get the keyword and create the URL to the search result page - $keywords = $this->getKeywords($idSite, $period, $date, $segment); - $subTable = $keywords->getRowFromIdSubDataTable($idSubtable); - if($subTable) - { - $keyword = $subTable->getColumn('label'); - $dataTable->queueFilter('MetadataCallbackReplace', array( 'url', 'Piwik_getSearchEngineUrlFromUrlAndKeyword', array($keyword)) ); - } - return $dataTable; - } - - public function getSearchEngines($idSite, $period, $date, $segment = false, $expanded = false) - { - $dataTable = $this->getDataTable('Referers_keywordBySearchEngine',$idSite, $period, $date, $segment, $expanded); - $dataTable->queueFilter('ColumnCallbackAddMetadata', array( 'label', 'url', 'Piwik_getSearchEngineUrlFromName') ); - $dataTable->queueFilter('MetadataCallbackAddMetadata', array( 'url', 'logo', 'Piwik_getSearchEngineLogoFromUrl') ); - return $dataTable; - } - - public function getKeywordsFromSearchEngineId($idSite, $period, $date, $idSubtable, $segment = false) - { - $dataTable = $this->getDataTable('Referers_keywordBySearchEngine',$idSite, $period, $date, $segment, $expanded = false, $idSubtable); - - // get the search engine and create the URL to the search result page - $searchEngines = $this->getSearchEngines($idSite, $period, $date, $segment); - $searchEngines->applyQueuedFilters(); - $subTable = $searchEngines->getRowFromIdSubDataTable($idSubtable); - if($subTable) - { - $searchEngineUrl = $subTable->getMetadata('url'); - $dataTable->queueFilter('ColumnCallbackAddMetadata', array( 'label', 'url', 'Piwik_getSearchEngineUrlFromKeywordAndUrl', array($searchEngineUrl))); - } - $dataTable = $this->handleKeywordNotDefined($dataTable); - return $dataTable; - } - - public function getCampaigns($idSite, $period, $date, $segment = false, $expanded = false) - { - $dataTable = $this->getDataTable('Referers_keywordByCampaign',$idSite, $period, $date, $segment, $expanded); - return $dataTable; - } - - public function getKeywordsFromCampaignId($idSite, $period, $date, $idSubtable, $segment = false) - { - $dataTable = $this->getDataTable('Referers_keywordByCampaign',$idSite, $period, $date, $segment, $expanded = false, $idSubtable); - return $dataTable; - } - - public function getWebsites($idSite, $period, $date, $segment = false, $expanded = false) - { - $dataTable = $this->getDataTable('Referers_urlByWebsite',$idSite, $period, $date, $segment, $expanded); - return $dataTable; - } - - public function getUrlsFromWebsiteId($idSite, $period, $date, $idSubtable, $segment = false) - { - $dataTable = $this->getDataTable('Referers_urlByWebsite',$idSite, $period, $date, $segment, $expanded = false, $idSubtable); - // the htmlspecialchars_decode call is for BC for before 1.1 - // as the Referer URL was previously encoded in the log tables, but is now recorded raw - $dataTable->queueFilter('ColumnCallbackAddMetadata', array( 'label', 'url', create_function('$label', 'return htmlspecialchars_decode($label);')) ); - $dataTable->queueFilter('ColumnCallbackReplace', array('label', 'Piwik_getPathFromUrl')); - return $dataTable; - } - - /** - * Returns report comparing the number of visits (and other info) for social network referrers. - * This is a view of the getWebsites report. - * - * @param string $idSite - * @param string $period - * @param string $date - * @param string|bool $segment - * @param bool $expanded - * @return Piwik_DataTable - */ - public function getSocials($idSite, $period, $date, $segment = false, $expanded = false) - { - require PIWIK_INCLUDE_PATH.'/core/DataFiles/Socials.php'; - - $dataTable = $this->getDataTable('Referers_urlByWebsite', $idSite, $period, $date, $segment, $expanded); - - $dataTable->filter('ColumnCallbackDeleteRow', array('label', 'Piwik_Referrers_isSocialUrl')); - - $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'url', 'Piwik_Referrers_cleanSocialUrl')); - $dataTable->filter('GroupBy', array('label', 'Piwik_Referrers_getSocialNetworkFromDomain')); - - $this->setSocialIdSubtables($dataTable); - $this->removeSubtableMetadata($dataTable); - - $dataTable->queueFilter('MetadataCallbackAddMetadata', array('url', 'logo', 'Piwik_getSocialsLogoFromUrl')); - - return $dataTable; - } - - /** - * Returns report containing individual referrer URLs for a specific social networking - * site. - * - * @param string $idSite - * @param string $period - * @param string $date - * @param string|false $segment - * @param int|false $idSubtable This ID does not reference a real DataTable record. Instead, it - * is the array index of an item in the /core/DataFiles/Socials.php file. - * The urls are filtered by the social network at this index. - * If false, no filtering is done and every social URL is returned. - * @return Piwik_DataTable - */ - public function getUrlsForSocial( $idSite, $period, $date, $segment = false, $idSubtable = false ) - { - require PIWIK_INCLUDE_PATH.'/core/DataFiles/Socials.php'; - - $dataTable = $this->getDataTable( - 'Referers_urlByWebsite', $idSite, $period, $date, $segment, $expanded = true); - - // get the social network domain referred to by $idSubtable - $social = false; - if ($idSubtable !== false) - { - --$idSubtable; - - reset($GLOBALS['Piwik_socialUrl']); - for ($i = 0; $i != (int)$idSubtable; ++$i) - { - next($GLOBALS['Piwik_socialUrl']); - } - - $social = current($GLOBALS['Piwik_socialUrl']); - } - - // filter out everything but social network indicated by $idSubtable - $dataTable->filter('ColumnCallbackDeleteRow', array('label', 'Piwik_Referrers_isSocialUrl', array($social))); - - // merge the datatable's subtables which contain the individual URLs - $dataTable = $dataTable->mergeSubtables(); - - // make url labels clickable - $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'url')); - - // prettify the DataTable - $dataTable->filter('ColumnCallbackReplace', array('label', 'Piwik_Referrers_removeUrlProtocol')); - $dataTable->filter('Sort', array(Piwik_Archive::INDEX_NB_VISITS, 'desc', $naturalSort = false, $expanded)); - $dataTable->queueFilter('ReplaceColumnNames'); - - return $dataTable; - } - - public function getNumberOfDistinctSearchEngines($idSite, $period, $date, $segment = false) - { - return $this->getNumeric('Referers_distinctSearchEngines', $idSite, $period, $date, $segment); - } - - public function getNumberOfDistinctKeywords($idSite, $period, $date, $segment = false) - { - return $this->getNumeric('Referers_distinctKeywords', $idSite, $period, $date, $segment); - } - - public function getNumberOfDistinctCampaigns($idSite, $period, $date, $segment = false) - { - return $this->getNumeric('Referers_distinctCampaigns', $idSite, $period, $date, $segment); - } - - public function getNumberOfDistinctWebsites($idSite, $period, $date, $segment = false) - { - return $this->getNumeric('Referers_distinctWebsites', $idSite, $period, $date, $segment); - } - - public function getNumberOfDistinctWebsitesUrls($idSite, $period, $date, $segment = false) - { - return $this->getNumeric('Referers_distinctWebsitesUrls', $idSite, $period, $date, $segment); - } - - private function getNumeric($name, $idSite, $period, $date, $segment) - { - Piwik::checkUserHasViewAccess( $idSite ); - $archive = Piwik_Archive::build($idSite, $period, $date, $segment ); - return $archive->getDataTableFromNumeric($name); - } - - /** - * Removes idsubdatatable_in_db metadata from a DataTable. Used by Social tables since - * they use fake subtable IDs. - * - * @param Piwik_DataTable $dataTable - */ - private function removeSubtableMetadata( $dataTable ) - { - if ($dataTable instanceof Piwik_DataTable_Array) - { - foreach ($dataTable->getArray() as $childTable) - { - $this->removeSubtableMetadata($childTable); - } - } - else - { - foreach ($dataTable->getRows() as $row) - { - $row->deleteMetadata('idsubdatatable_in_db'); - } - } - } - - /** - * Sets the subtable IDs for the DataTable returned by getSocial. - * - * The IDs are int indexes into the array in /core/DataFiles/Socials.php. - * - * @param Piwik_DataTable $dataTable - */ - private function setSocialIdSubtables( $dataTable ) - { - if ($dataTable instanceof Piwik_DataTable_Array) - { - foreach ($dataTable->getArray() as $childTable) - { - $this->setSocialIdSubtables($childTable); - } - } - else - { - foreach ($dataTable->getRows() as $row) - { - $socialName = $row->getColumn('label'); - - $i = 1; // start at one because idSubtable=0 is equivalent to idSubtable=false - foreach ($GLOBALS['Piwik_socialUrl'] as $domain => $name) - { - if ($name == $socialName) - { - $row->c[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = $i; - break; - } - - ++$i; - } - } - } - } - - /** - * Utility function that removes the subtable IDs for the subtables of the - * getRefererType report. This avoids infinite recursion in said report (ie, - * the grandchildren of the report will be the original report, and it will - * recurse when trying to get a flat report). - * - * @param Piwik_DataTable $table - * @return Piwik_DataTable Returns $table for convenience. - */ - private function removeSubtableIds( $table ) - { - if ($table instanceof Piwik_DataTable_Array) - { - foreach ($table->getArray() as $childTable) - { - $this->removeSubtableIds($childTable); - } - } - else - { - foreach ($table->getRows() as $row) - { - $row->removeSubtable(); - } - } - - return $table; - } - - /** - * Utility function that sets the subtables for the getRefererType report. - * - * If we're not getting an expanded datatable, the subtable ID is set to each parent - * row's referrer type (stored in the label for the getRefererType report). - * - * If we are getting an expanded datatable, the datatable for the row's referrer - * type is loaded and attached to the appropriate row in the getRefererType report. - * - * @param Piwik_DataTable $dataTable - * @param string $idSite - * @param string $period - * @param string $date - * @param string $segment - * @param bool $expanded - */ - private function setGetReferrerTypeSubtables( $dataTable, $idSite, $period, $date, $segment, $expanded ) - { - foreach ($dataTable->getRows() as $row) - { - $typeReferrer = $row->getColumn('label'); - if ($typeReferrer != Piwik_Common::REFERER_TYPE_DIRECT_ENTRY) - { - if (!$expanded) // if we don't want the expanded datatable, then don't do any extra queries - { - $row->c[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = $typeReferrer; - } - else // otherwise, we have to get the othe datatables - { - $subtable = $this->getRefererType($idSite, $period, $date, $segment, $type = false, - $idSubtable = $typeReferrer); - - if ($expanded) - { - $subtable->applyQueuedFilters(); - } - - $row->setSubtable($subtable); - } - } - } - } + static private $instance = null; + + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + /** + * @return Piwik_DataTable + */ + protected function getDataTable($name, $idSite, $period, $date, $segment, $expanded = false, $idSubtable = null) + { + $dataTable = Piwik_Archive::getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded, $idSubtable); + $dataTable->filter('Sort', array(Piwik_Archive::INDEX_NB_VISITS, 'desc', $naturalSort = false, $expanded)); + $dataTable->queueFilter('ReplaceColumnNames'); + return $dataTable; + } + + /** + * Returns a report describing visit information for each possible referrer type. The + * result is a datatable whose subtables are the reports for each parent row's referrer type. + * + * The subtable reports are: 'getKeywords' (for search engine referrer type), 'getWebsites', + * and 'getCampaigns'. + * + * @param string $idSite The site ID. + * @param string $period The period to get data for, either 'day', 'week', 'month', 'year', + * or 'range'. + * @param string $date The date of the period. + * @param string $segment The segment to use. + * @param int $typeReferer (deprecated) If you want to get data only for a specific referrer + * type, supply a type for this parameter. + * @param int $idSubtable For this report this value is a referrer type ID and not an actual + * subtable ID. The result when using this parameter will be the + * specific report for the given referrer type. + * @param bool $expanded Whether to get report w/ subtables loaded or not. + * @return Piwik_DataTable + */ + public function getRefererType($idSite, $period, $date, $segment = false, $typeReferer = false, + $idSubtable = false, $expanded = false) + { + // if idSubtable is supplied, interpret idSubtable as referrer type and return correct report + if ($idSubtable !== false) { + $result = false; + switch ($idSubtable) { + case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: + $result = $this->getKeywords($idSite, $period, $date, $segment); + break; + case Piwik_Common::REFERER_TYPE_WEBSITE: + $result = $this->getWebsites($idSite, $period, $date, $segment); + break; + case Piwik_Common::REFERER_TYPE_CAMPAIGN: + $result = $this->getCampaigns($idSite, $period, $date, $segment); + break; + default: // invalid idSubtable, return whole report + break; + } + + if ($result) { + return $this->removeSubtableIds($result); // this report won't return subtables of individual reports + } + } + + // get visits by referrer type + $dataTable = $this->getDataTable('Referers_type', $idSite, $period, $date, $segment); + + if ($typeReferer !== false) // filter for a specific referrer type + { + $dataTable->filter('Pattern', array('label', $typeReferer)); + } + + // set subtable IDs for each row to the label (which holds the int referrer type) + // NOTE: not yet possible to do this w/ DataTable_Array instances + if (!($dataTable instanceof Piwik_DataTable_Array)) { + $this->setGetReferrerTypeSubtables($dataTable, $idSite, $period, $date, $segment, $expanded); + } + + // set referrer type column to readable value + $dataTable->queueFilter('ColumnCallbackReplace', array('label', 'Piwik_getRefererTypeLabel')); + + return $dataTable; + } + + /** + * Returns a report that shows + */ + public function getAll($idSite, $period, $date, $segment = false) + { + $dataTable = $this->getRefererType($idSite, $period, $date, $segment, $typeReferer = false, + $idSubtable = false, $expanded = true); + + if ($dataTable instanceof Piwik_DataTable_Array) { + throw new Exception("Referrers.getAll with multiple sites or dates is not supported (yet)."); + } + + $dataTable = $dataTable->mergeSubtables($labelColumn = 'referrer_type', $useMetadataColumn = true); + + // presentation filters + $dataTable->filter('Sort', array(Piwik_Archive::INDEX_NB_VISITS, 'desc')); + $dataTable->queueFilter('ReplaceColumnNames'); + $dataTable->queueFilter('ReplaceSummaryRowLabel'); + + return $dataTable; + } + + public function getKeywords($idSite, $period, $date, $segment = false, $expanded = false) + { + $dataTable = $this->getDataTable('Referers_searchEngineByKeyword', $idSite, $period, $date, $segment, $expanded); + $dataTable = $this->handleKeywordNotDefined($dataTable); + return $dataTable; + } + + protected function handleKeywordNotDefined($dataTable) + { + $dataTable->queueFilter('ColumnCallbackReplace', array('label', array('Piwik_Referers', 'getCleanKeyword'))); + return $dataTable; + } + + public function getKeywordsForPageUrl($idSite, $period, $date, $url) + { + // Fetch the Top keywords for this page + $segment = 'entryPageUrl==' . $url; + $table = $this->getKeywords($idSite, $period, $date, $segment); + $this->filterOutKeywordNotDefined($table); + return $this->getLabelsFromTable($table); + + } + + public function getKeywordsForPageTitle($idSite, $period, $date, $title) + { + $segment = 'entryPageTitle==' . $title; + $table = $this->getKeywords($idSite, $period, $date, $segment); + $this->filterOutKeywordNotDefined($table); + return $this->getLabelsFromTable($table); + } + + /** + * @param Piwik_Datatable $table + */ + private function filterOutKeywordNotDefined($table) + { + if ($table instanceof Piwik_Datatable) { + $row = $table->getRowIdFromLabel(''); + if ($row) { + $table->deleteRow($row); + } + } + } + + protected function getLabelsFromTable($table) + { + $request = $_GET; + $request['serialize'] = 0; + + // Apply generic filters + $response = new Piwik_API_ResponseBuilder($format = 'original', $request); + $table = $response->getResponse($table); + + // If period=lastX we only keep the first resultset as we want to return a plain list + if ($table instanceof Piwik_DataTable_Array) { + $tables = $table->getArray(); + $table = current($tables); + } + // Keep the response simple, only include keywords + $keywords = $table->getColumn('label'); + return $keywords; + } + + public function getSearchEnginesFromKeywordId($idSite, $period, $date, $idSubtable, $segment = false) + { + $dataTable = $this->getDataTable('Referers_searchEngineByKeyword', $idSite, $period, $date, $segment, $expanded = false, $idSubtable); + $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', 'Piwik_getSearchEngineUrlFromName')); + $dataTable->queueFilter('MetadataCallbackAddMetadata', array('url', 'logo', 'Piwik_getSearchEngineLogoFromUrl')); + + // get the keyword and create the URL to the search result page + $keywords = $this->getKeywords($idSite, $period, $date, $segment); + $subTable = $keywords->getRowFromIdSubDataTable($idSubtable); + if ($subTable) { + $keyword = $subTable->getColumn('label'); + $dataTable->queueFilter('MetadataCallbackReplace', array('url', 'Piwik_getSearchEngineUrlFromUrlAndKeyword', array($keyword))); + } + return $dataTable; + } + + public function getSearchEngines($idSite, $period, $date, $segment = false, $expanded = false) + { + $dataTable = $this->getDataTable('Referers_keywordBySearchEngine', $idSite, $period, $date, $segment, $expanded); + $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', 'Piwik_getSearchEngineUrlFromName')); + $dataTable->queueFilter('MetadataCallbackAddMetadata', array('url', 'logo', 'Piwik_getSearchEngineLogoFromUrl')); + return $dataTable; + } + + public function getKeywordsFromSearchEngineId($idSite, $period, $date, $idSubtable, $segment = false) + { + $dataTable = $this->getDataTable('Referers_keywordBySearchEngine', $idSite, $period, $date, $segment, $expanded = false, $idSubtable); + + // get the search engine and create the URL to the search result page + $searchEngines = $this->getSearchEngines($idSite, $period, $date, $segment); + $searchEngines->applyQueuedFilters(); + $subTable = $searchEngines->getRowFromIdSubDataTable($idSubtable); + if ($subTable) { + $searchEngineUrl = $subTable->getMetadata('url'); + $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', 'Piwik_getSearchEngineUrlFromKeywordAndUrl', array($searchEngineUrl))); + } + $dataTable = $this->handleKeywordNotDefined($dataTable); + return $dataTable; + } + + public function getCampaigns($idSite, $period, $date, $segment = false, $expanded = false) + { + $dataTable = $this->getDataTable('Referers_keywordByCampaign', $idSite, $period, $date, $segment, $expanded); + return $dataTable; + } + + public function getKeywordsFromCampaignId($idSite, $period, $date, $idSubtable, $segment = false) + { + $dataTable = $this->getDataTable('Referers_keywordByCampaign', $idSite, $period, $date, $segment, $expanded = false, $idSubtable); + return $dataTable; + } + + public function getWebsites($idSite, $period, $date, $segment = false, $expanded = false) + { + $dataTable = $this->getDataTable('Referers_urlByWebsite', $idSite, $period, $date, $segment, $expanded); + return $dataTable; + } + + public function getUrlsFromWebsiteId($idSite, $period, $date, $idSubtable, $segment = false) + { + $dataTable = $this->getDataTable('Referers_urlByWebsite', $idSite, $period, $date, $segment, $expanded = false, $idSubtable); + // the htmlspecialchars_decode call is for BC for before 1.1 + // as the Referer URL was previously encoded in the log tables, but is now recorded raw + $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', create_function('$label', 'return htmlspecialchars_decode($label);'))); + $dataTable->queueFilter('ColumnCallbackReplace', array('label', 'Piwik_getPathFromUrl')); + return $dataTable; + } + + /** + * Returns report comparing the number of visits (and other info) for social network referrers. + * This is a view of the getWebsites report. + * + * @param string $idSite + * @param string $period + * @param string $date + * @param string|bool $segment + * @param bool $expanded + * @return Piwik_DataTable + */ + public function getSocials($idSite, $period, $date, $segment = false, $expanded = false) + { + require PIWIK_INCLUDE_PATH . '/core/DataFiles/Socials.php'; + + $dataTable = $this->getDataTable('Referers_urlByWebsite', $idSite, $period, $date, $segment, $expanded); + + $dataTable->filter('ColumnCallbackDeleteRow', array('label', 'Piwik_Referrers_isSocialUrl')); + + $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'url', 'Piwik_Referrers_cleanSocialUrl')); + $dataTable->filter('GroupBy', array('label', 'Piwik_Referrers_getSocialNetworkFromDomain')); + + $this->setSocialIdSubtables($dataTable); + $this->removeSubtableMetadata($dataTable); + + $dataTable->queueFilter('MetadataCallbackAddMetadata', array('url', 'logo', 'Piwik_getSocialsLogoFromUrl')); + + return $dataTable; + } + + /** + * Returns report containing individual referrer URLs for a specific social networking + * site. + * + * @param string $idSite + * @param string $period + * @param string $date + * @param string|false $segment + * @param int|false $idSubtable This ID does not reference a real DataTable record. Instead, it + * is the array index of an item in the /core/DataFiles/Socials.php file. + * The urls are filtered by the social network at this index. + * If false, no filtering is done and every social URL is returned. + * @return Piwik_DataTable + */ + public function getUrlsForSocial($idSite, $period, $date, $segment = false, $idSubtable = false) + { + require PIWIK_INCLUDE_PATH . '/core/DataFiles/Socials.php'; + + $dataTable = $this->getDataTable( + 'Referers_urlByWebsite', $idSite, $period, $date, $segment, $expanded = true); + + // get the social network domain referred to by $idSubtable + $social = false; + if ($idSubtable !== false) { + --$idSubtable; + + reset($GLOBALS['Piwik_socialUrl']); + for ($i = 0; $i != (int)$idSubtable; ++$i) { + next($GLOBALS['Piwik_socialUrl']); + } + + $social = current($GLOBALS['Piwik_socialUrl']); + } + + // filter out everything but social network indicated by $idSubtable + $dataTable->filter('ColumnCallbackDeleteRow', array('label', 'Piwik_Referrers_isSocialUrl', array($social))); + + // merge the datatable's subtables which contain the individual URLs + $dataTable = $dataTable->mergeSubtables(); + + // make url labels clickable + $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'url')); + + // prettify the DataTable + $dataTable->filter('ColumnCallbackReplace', array('label', 'Piwik_Referrers_removeUrlProtocol')); + $dataTable->filter('Sort', array(Piwik_Archive::INDEX_NB_VISITS, 'desc', $naturalSort = false, $expanded)); + $dataTable->queueFilter('ReplaceColumnNames'); + + return $dataTable; + } + + public function getNumberOfDistinctSearchEngines($idSite, $period, $date, $segment = false) + { + return $this->getNumeric('Referers_distinctSearchEngines', $idSite, $period, $date, $segment); + } + + public function getNumberOfDistinctKeywords($idSite, $period, $date, $segment = false) + { + return $this->getNumeric('Referers_distinctKeywords', $idSite, $period, $date, $segment); + } + + public function getNumberOfDistinctCampaigns($idSite, $period, $date, $segment = false) + { + return $this->getNumeric('Referers_distinctCampaigns', $idSite, $period, $date, $segment); + } + + public function getNumberOfDistinctWebsites($idSite, $period, $date, $segment = false) + { + return $this->getNumeric('Referers_distinctWebsites', $idSite, $period, $date, $segment); + } + + public function getNumberOfDistinctWebsitesUrls($idSite, $period, $date, $segment = false) + { + return $this->getNumeric('Referers_distinctWebsitesUrls', $idSite, $period, $date, $segment); + } + + private function getNumeric($name, $idSite, $period, $date, $segment) + { + Piwik::checkUserHasViewAccess($idSite); + $archive = Piwik_Archive::build($idSite, $period, $date, $segment); + return $archive->getDataTableFromNumeric($name); + } + + /** + * Removes idsubdatatable_in_db metadata from a DataTable. Used by Social tables since + * they use fake subtable IDs. + * + * @param Piwik_DataTable $dataTable + */ + private function removeSubtableMetadata($dataTable) + { + if ($dataTable instanceof Piwik_DataTable_Array) { + foreach ($dataTable->getArray() as $childTable) { + $this->removeSubtableMetadata($childTable); + } + } else { + foreach ($dataTable->getRows() as $row) { + $row->deleteMetadata('idsubdatatable_in_db'); + } + } + } + + /** + * Sets the subtable IDs for the DataTable returned by getSocial. + * + * The IDs are int indexes into the array in /core/DataFiles/Socials.php. + * + * @param Piwik_DataTable $dataTable + */ + private function setSocialIdSubtables($dataTable) + { + if ($dataTable instanceof Piwik_DataTable_Array) { + foreach ($dataTable->getArray() as $childTable) { + $this->setSocialIdSubtables($childTable); + } + } else { + foreach ($dataTable->getRows() as $row) { + $socialName = $row->getColumn('label'); + + $i = 1; // start at one because idSubtable=0 is equivalent to idSubtable=false + foreach ($GLOBALS['Piwik_socialUrl'] as $domain => $name) { + if ($name == $socialName) { + $row->c[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = $i; + break; + } + + ++$i; + } + } + } + } + + /** + * Utility function that removes the subtable IDs for the subtables of the + * getRefererType report. This avoids infinite recursion in said report (ie, + * the grandchildren of the report will be the original report, and it will + * recurse when trying to get a flat report). + * + * @param Piwik_DataTable $table + * @return Piwik_DataTable Returns $table for convenience. + */ + private function removeSubtableIds($table) + { + if ($table instanceof Piwik_DataTable_Array) { + foreach ($table->getArray() as $childTable) { + $this->removeSubtableIds($childTable); + } + } else { + foreach ($table->getRows() as $row) { + $row->removeSubtable(); + } + } + + return $table; + } + + /** + * Utility function that sets the subtables for the getRefererType report. + * + * If we're not getting an expanded datatable, the subtable ID is set to each parent + * row's referrer type (stored in the label for the getRefererType report). + * + * If we are getting an expanded datatable, the datatable for the row's referrer + * type is loaded and attached to the appropriate row in the getRefererType report. + * + * @param Piwik_DataTable $dataTable + * @param string $idSite + * @param string $period + * @param string $date + * @param string $segment + * @param bool $expanded + */ + private function setGetReferrerTypeSubtables($dataTable, $idSite, $period, $date, $segment, $expanded) + { + foreach ($dataTable->getRows() as $row) { + $typeReferrer = $row->getColumn('label'); + if ($typeReferrer != Piwik_Common::REFERER_TYPE_DIRECT_ENTRY) { + if (!$expanded) // if we don't want the expanded datatable, then don't do any extra queries + { + $row->c[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = $typeReferrer; + } else // otherwise, we have to get the othe datatables + { + $subtable = $this->getRefererType($idSite, $period, $date, $segment, $type = false, + $idSubtable = $typeReferrer); + + if ($expanded) { + $subtable->applyQueuedFilters(); + } + + $row->setSubtable($subtable); + } + } + } + } } diff --git a/plugins/Referers/Controller.php b/plugins/Referers/Controller.php index fbbf3c14e4..bc3cc00131 100644 --- a/plugins/Referers/Controller.php +++ b/plugins/Referers/Controller.php @@ -15,579 +15,561 @@ */ class Piwik_Referers_Controller extends Piwik_Controller { - function index() - { - $view = Piwik_View::factory('index'); - - $view->graphEvolutionReferers = $this->getEvolutionGraph(true, Piwik_Common::REFERER_TYPE_DIRECT_ENTRY, array('nb_visits')); - $view->nameGraphEvolutionReferers = 'ReferersgetEvolutionGraph'; - - // building the referers summary report - $view->dataTableRefererType = $this->getRefererType(true); - - $nameValues = $this->getReferersVisitorsByType(); - - $totalVisits = array_sum($nameValues); - foreach($nameValues as $name => $value) - { - $view->$name = $value; - - // calculate percent of total, if there were any visits - if ($value != 0 - && $totalVisits != 0) - { - $percentName = $name.'Percent'; - $view->$percentName = round(($value / $totalVisits) * 100, 0); - } - } - - // set distinct metrics - $distinctMetrics = $this->getDistinctReferrersMetrics(); - foreach ($distinctMetrics as $name => $value) - { - $view->$name = $value; - } - - // calculate evolution for visit metrics & distinct metrics - list($lastPeriodDate, $ignore) = Piwik_Period_Range::getLastDate(); - if ($lastPeriodDate !== false) - { - $date = Piwik_Common::getRequestVar('date'); - $period = Piwik_Common::getRequestVar('period'); - - $prettyDate = self::getPrettyDate($date, $period); - $prettyLastPeriodDate = self::getPrettyDate($lastPeriodDate, $period); - - // visit metrics - $previousValues = $this->getReferersVisitorsByType($lastPeriodDate); - $this->addEvolutionPropertiesToView($view, $prettyDate, $nameValues, $prettyLastPeriodDate, $previousValues); - - // distinct metrics - $previousValues = $this->getDistinctReferrersMetrics($lastPeriodDate); - $this->addEvolutionPropertiesToView($view, $prettyDate, $distinctMetrics, $prettyLastPeriodDate, $previousValues); - } - - // sparkline for the historical data of the above values - $view->urlSparklineSearchEngines = $this->getReferrerUrlSparkline(Piwik_Common::REFERER_TYPE_SEARCH_ENGINE); - $view->urlSparklineDirectEntry = $this->getReferrerUrlSparkline(Piwik_Common::REFERER_TYPE_DIRECT_ENTRY); - $view->urlSparklineWebsites = $this->getReferrerUrlSparkline(Piwik_Common::REFERER_TYPE_WEBSITE); - $view->urlSparklineCampaigns = $this->getReferrerUrlSparkline(Piwik_Common::REFERER_TYPE_CAMPAIGN); - - // sparklines for the evolution of the distinct keywords count/websites count/ etc - $view->urlSparklineDistinctSearchEngines = $this->getUrlSparkline('getLastDistinctSearchEnginesGraph'); - $view->urlSparklineDistinctKeywords = $this->getUrlSparkline('getLastDistinctKeywordsGraph'); - $view->urlSparklineDistinctWebsites = $this->getUrlSparkline('getLastDistinctWebsitesGraph'); - $view->urlSparklineDistinctCampaigns = $this->getUrlSparkline('getLastDistinctCampaignsGraph'); - - $view->totalVisits = $totalVisits; - $view->referrersReportsByDimension = $this->getReferrersReportsByDimensionView($totalVisits); - - echo $view->render(); - } - - /** - * Returns HTML for the Referrers Overview page that categorizes Referrer reports - * & allows the user to switch between them. - * - * @param int $visits The number of visits for this period & site. If <= 0, the - * reports are not shown, since they will have no data. - * @return string The report viewer HTML. - */ - private function getReferrersReportsByDimensionView( $visits ) - { - $result = ''; - - // only display the reports by dimension view if there are visits - if ($visits > 0) - { - $referrersReportsByDimension = new Piwik_View_ReportsByDimension(); - - $referrersReportsByDimension->addReport( - 'Referers_ViewAllReferrers', 'Referers_WidgetGetAll', 'Referers.getAll'); - - $byTypeCategory = Piwik_Translate('Referers_ViewReferrersBy', Piwik_Translate('Live_GoalType')); - $referrersReportsByDimension->addReport( - $byTypeCategory, 'Referers_WidgetKeywords', 'Referers.getKeywords'); - $referrersReportsByDimension->addReport($byTypeCategory, 'SitesManager_Sites', 'Referers.getWebsites'); - $referrersReportsByDimension->addReport($byTypeCategory, 'Referers_Campaigns', 'Referers.getCampaigns'); - - $bySourceCategory = Piwik_Translate('Referers_ViewReferrersBy', Piwik_Translate('General_Source')); - $referrersReportsByDimension->addReport($bySourceCategory, 'Referers_Socials', 'Referers.getSocials'); - $referrersReportsByDimension->addReport( - $bySourceCategory, 'Referers_SearchEngines', 'Referers.getSearchEngines'); - - $result = $referrersReportsByDimension->render(); - } - - return $result; - } + function index() + { + $view = Piwik_View::factory('index'); - function getSearchEnginesAndKeywords() - { - $view = Piwik_View::factory('searchEngines_Keywords'); - $view->searchEngines = $this->getSearchEngines(true) ; - $view->keywords = $this->getKeywords(true); - echo $view->render(); - } - - function getRefererType( $fetch = false) - { - $view = Piwik_ViewDataTable::factory('tableAllColumns'); - $view->init( $this->pluginName, - __FUNCTION__, - 'Referers.getRefererType', - 'getRefererType' - ); - $view->disableSearchBox(); - $view->disableOffsetInformationAndPaginationControls(); - $view->disableExcludeLowPopulation(); - $view->disableSubTableWhenShowGoals(); - $view->enableShowGoals(); - $view->setLimit(10); - $view->setColumnsToDisplay( array('label', 'nb_visits') ); - - $idSubtable = Piwik_Common::getRequestVar('idSubtable', false); - $labelColumnTitle = Piwik_Translate('Referers_ColumnRefererType'); - if ($idSubtable !== false) - { - switch ($idSubtable) - { - case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: - $labelColumnTitle = Piwik_Translate('Referers_ColumnSearchEngine'); - break; - case Piwik_Common::REFERER_TYPE_WEBSITE: - $labelColumnTitle = Piwik_Translate('Referers_ColumnWebsite'); - break; - case Piwik_Common::REFERER_TYPE_CAMPAIGN: - $labelColumnTitle = Piwik_Translate('Referers_ColumnCampaign'); - break; - default: - break; - } - } - $view->setColumnTranslation('label', $labelColumnTitle); - - $this->setMetricsVariablesView($view); - return $this->renderView($view, $fetch); - } - - /** - * Returns or echo's a report that shows all search keyword, website and campaign - * referrer information in one report. - * - * @param bool $fetch True if the report HTML should be returned. If false, the - * report is echo'd and nothing is returned. - * @return string The report HTML or nothing if $fetch is set to false. - */ - public function getAll( $fetch = false ) - { - $view = Piwik_ViewDataTable::factory(); - $view->init($this->pluginName, __FUNCTION__, 'Referers.getAll'); - $view->disableExcludeLowPopulation(); - $view->setColumnTranslation('label', Piwik_Translate('Referers_Referrer')); - $view->setColumnsToDisplay(array('label', 'nb_visits')); - $view->enableShowGoals(); - $view->setLimit(20); - $view->setCustomParameter('disable_row_actions', '1'); - - $setGetAllHtmlPrefix = array($this, 'setGetAllHtmlPrefix'); - $view->queueFilter( - 'MetadataCallbackAddMetadata', array('referrer_type', 'html_label_prefix', $setGetAllHtmlPrefix)); - - $view->setMetricsVariablesView($view); - - return $this->renderView($view, $fetch); - } - - /** - * DataTable filter callback that returns the HTML prefix for a label in the - * 'getAll' report based on the row's referrer type. - * - * @param int $referrerType The referrer type. - * @return string - */ - public function setGetAllHtmlPrefix( $referrerType ) - { - // get singular label for referrer type - $indexTranslation = ''; - switch($referrerType) - { - case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY: - $indexTranslation = 'Referers_DirectEntry'; - break; - case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: - $indexTranslation = 'Referers_ColumnKeyword'; - break; - case Piwik_Common::REFERER_TYPE_WEBSITE: - $indexTranslation = 'Referers_ColumnWebsite'; - break; - case Piwik_Common::REFERER_TYPE_CAMPAIGN: - $indexTranslation = 'Referers_ColumnCampaign'; - break; - default: - // case of newsletter, partners, before Piwik 0.2.25 - $indexTranslation = 'General_Others'; - break; - } - - $label = strtolower(Piwik_Translate($indexTranslation)); - - // return html that displays it as grey & italic - return '('.$label.')'; - } + $view->graphEvolutionReferers = $this->getEvolutionGraph(true, Piwik_Common::REFERER_TYPE_DIRECT_ENTRY, array('nb_visits')); + $view->nameGraphEvolutionReferers = 'ReferersgetEvolutionGraph'; - function getKeywords( $fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, __FUNCTION__, - 'Referers.getKeywords', - 'getSearchEnginesFromKeywordId' - ); - $view->disableExcludeLowPopulation(); - $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnKeyword')); - $view->enableShowGoals(); - $view->setLimit(25); - $view->disableSubTableWhenShowGoals(); - - $this->setMetricsVariablesView($view); - - return $this->renderView($view, $fetch); - } - - function getSearchEnginesFromKeywordId( $fetch = false ) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, __FUNCTION__, - 'Referers.getSearchEnginesFromKeywordId' - ); - $view->disableSearchBox(); - $view->disableExcludeLowPopulation(); - $view->setColumnsToDisplay( array('label','nb_visits') ); - $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnSearchEngine')); - return $this->renderView($view, $fetch); - } - - - function getSearchEngines( $fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, __FUNCTION__, - 'Referers.getSearchEngines', - 'getKeywordsFromSearchEngineId' - ); - $view->disableSearchBox(); - $view->disableExcludeLowPopulation(); - $view->enableShowGoals(); - $view->setLimit(25); - $view->disableSubTableWhenShowGoals(); - $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnSearchEngine')); - - $this->setMetricsVariablesView($view); - - return $this->renderView($view, $fetch); - } + // building the referers summary report + $view->dataTableRefererType = $this->getRefererType(true); - function getKeywordsFromSearchEngineId( $fetch = false ) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, __FUNCTION__, - 'Referers.getKeywordsFromSearchEngineId' - ); - $view->disableSearchBox(); - $view->disableExcludeLowPopulation(); - $view->setColumnsToDisplay( array('label','nb_visits') ); - $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnKeyword')); - return $this->renderView($view, $fetch); - } - - function indexWebsites($fetch = false) - { - $view = Piwik_View::factory('Websites_SocialNetworks'); - $view->websites = $this->getWebsites(true) ; - $view->socials = $this->getSocials(true); - if ($fetch) - { - return $view->render(); - } - else - { - echo $view->render(); - } - } - - function getWebsites( $fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, __FUNCTION__, - 'Referers.getWebsites', - 'getUrlsFromWebsiteId' - ); - $view->disableExcludeLowPopulation(); - $view->enableShowGoals(); - $view->setLimit(25); - $view->disableSubTableWhenShowGoals(); - $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnWebsite')); - - $this->setMetricsVariablesView($view); - - return $this->renderView($view, $fetch); - } - - function getSocials( $fetch = false) - { - $view = Piwik_ViewDataTable::factory('graphPie'); - $view->init($this->pluginName, __FUNCTION__, 'Referers.getSocials', 'getUrlsForSocial'); - $view->disableExcludeLowPopulation(); - $view->setLimit(10); - $view->enableShowGoals(); - $view->disableSubTableWhenShowGoals(); - $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnSocial')); - - if(empty($_REQUEST['widget'])) { - $view->setFooterMessage(Piwik_Translate('Referers_SocialFooterMessage')); - } - - $this->setMetricsVariablesView($view); - - return $this->renderView($view, $fetch); - } - - function getUrlsForSocial( $fetch = false ) - { - $view = Piwik_ViewDataTable::factory(); - $view->init($this->pluginName, __FUNCTION__, 'Referers.getUrlsForSocial'); - $view->disableExcludeLowPopulation(); - $view->setLimit(10); - $view->enableShowGoals(); - $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnWebsitePage')); - - $this->setMetricsVariablesView($view); - - return $this->renderView($view, $fetch); - } - - function indexCampaigns($fetch = false) - { - return Piwik_View::singleReport( - Piwik_Translate('Referers_Campaigns'), - $this->getCampaigns(true), $fetch); - } - - function getCampaigns( $fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, __FUNCTION__, - 'Referers.getCampaigns', - 'getKeywordsFromCampaignId' - ); - $view->disableExcludeLowPopulation(); - $view->enableShowGoals(); - $view->setLimit(25); - $view->setColumnsToDisplay( array('label','nb_visits') ); - $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnCampaign')); - - $help = Piwik_Translate('Referers_CampaignFooterHelp', array( '', - ' - ', - '' - )); - $view->setFooterMessage( $help ); - $this->setMetricsVariablesView($view); - return $this->renderView($view, $fetch); - } - - function getKeywordsFromCampaignId( $fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, __FUNCTION__, - 'Referers.getKeywordsFromCampaignId' - ); - - $view->disableSearchBox(); - $view->disableExcludeLowPopulation(); - $view->setColumnsToDisplay( array('label','nb_visits') ); - $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnKeyword')); - - return $this->renderView($view, $fetch); - } - - function getUrlsFromWebsiteId( $fetch = false) - { - $view = Piwik_ViewDataTable::factory(); - $view->init( $this->pluginName, __FUNCTION__, - 'Referers.getUrlsFromWebsiteId' - ); - $view->disableSearchBox(); - $view->disableExcludeLowPopulation(); - $view->setColumnsToDisplay( array('label','nb_visits') ); - $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnWebsitePage')); - $view->setTooltipMetadataName('url'); - return $this->renderView($view, $fetch); - } - - protected function getReferersVisitorsByType( $date = false ) - { - if ($date === false) - { - $date = Piwik_Common::getRequestVar('date', false); - } - - // we disable the queued filters because here we want to get the visits coming from search engines - // if the filters were applied we would have to look up for a label looking like "Search Engines" - // which is not good when we have translations - $dataTableReferersType = Piwik_API_Request::processRequest( - "Referers.getRefererType", array('disable_queued_filters' => '1', 'date' => $date)); - - $nameToColumnId = array( - 'visitorsFromSearchEngines' => Piwik_Common::REFERER_TYPE_SEARCH_ENGINE, - 'visitorsFromDirectEntry' => Piwik_Common::REFERER_TYPE_DIRECT_ENTRY, - 'visitorsFromWebsites' => Piwik_Common::REFERER_TYPE_WEBSITE, - 'visitorsFromCampaigns' => Piwik_Common::REFERER_TYPE_CAMPAIGN, - ); - $return = array(); - foreach($nameToColumnId as $nameVar => $columnId) - { - $value = 0; - $row = $dataTableReferersType->getRowFromLabel($columnId); - if($row !== false) - { - $value = $row->getColumn(Piwik_Archive::INDEX_NB_VISITS); - } - $return[$nameVar] = $value; - } - return $return; - } + $nameValues = $this->getReferersVisitorsByType(); - protected $referrerTypeToLabel = array( - Piwik_Common::REFERER_TYPE_DIRECT_ENTRY => 'Referers_DirectEntry', - Piwik_Common::REFERER_TYPE_SEARCH_ENGINE => 'Referers_SearchEngines', - Piwik_Common::REFERER_TYPE_WEBSITE => 'Referers_Websites', - Piwik_Common::REFERER_TYPE_CAMPAIGN => 'Referers_Campaigns', - ); - - public function getEvolutionGraph( $fetch = false, $typeReferer = false, array $columns = array()) - { - $view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, 'Referers.getRefererType'); - - $view->addTotalRow(); - - // configure displayed columns - if(empty($columns)) - { - $columns = Piwik_Common::getRequestVar('columns'); - $columns = Piwik::getArrayFromApiParameter($columns); - } - $columns = !is_array($columns) ? array($columns) : $columns; - $view->setColumnsToDisplay($columns); - - // configure selectable columns - if (Piwik_Common::getRequestVar('period', false) == 'day') { - $selectable = array('nb_visits', 'nb_uniq_visitors', 'nb_actions'); - } else { - $selectable = array('nb_visits', 'nb_actions'); - } - $view->setSelectableColumns($selectable); - - // configure displayed rows - $visibleRows = Piwik_Common::getRequestVar('rows', false); - if ($visibleRows !== false) - { - // this happens when the row picker has been used - $visibleRows = Piwik::getArrayFromApiParameter($visibleRows); - - // typeReferer is redundant if rows are defined, so make sure it's not used - $view->setCustomParameter('typeReferer', false); - } - else - { - // use $typeReferer as default - if($typeReferer === false) - { - $typeReferer = Piwik_Common::getRequestVar('typeReferer', false); - } - $label = self::getTranslatedReferrerTypeLabel($typeReferer); - $total = Piwik_Translate('General_Total'); - $visibleRows = array($label, $total); - $view->setParametersToModify(array('rows' => $label.','.$total)); - } - $view->addRowPicker($visibleRows); - - $view->setReportDocumentation(Piwik_Translate('Referers_EvolutionDocumentation').'
' - .Piwik_Translate('General_BrokenDownReportDocumentation').'
' - .Piwik_Translate('Referers_EvolutionDocumentationMoreInfo', '"'.Piwik_Translate('Referers_DetailsByRefererType').'"')); - - return $this->renderView($view, $fetch); - } - - function getLastDistinctSearchEnginesGraph( $fetch = false ) - { - $view = $this->getLastUnitGraph($this->pluginName,__FUNCTION__, "Referers.getNumberOfDistinctSearchEngines"); - $view->setColumnTranslation('Referers_distinctSearchEngines', ucfirst(Piwik_Translate('Referers_DistinctSearchEngines'))); - $view->setColumnsToDisplay(array('Referers_distinctSearchEngines')); - return $this->renderView($view, $fetch); - } - function getLastDistinctKeywordsGraph( $fetch = false ) - { - $view = $this->getLastUnitGraph($this->pluginName,__FUNCTION__, "Referers.getNumberOfDistinctKeywords"); - $view->setColumnTranslation('Referers_distinctKeywords', ucfirst(Piwik_Translate('Referers_DistinctKeywords'))); - $view->setColumnsToDisplay(array('Referers_distinctKeywords')); - return $this->renderView($view, $fetch); - } - function getLastDistinctWebsitesGraph( $fetch = false ) - { - $view = $this->getLastUnitGraph($this->pluginName,__FUNCTION__, "Referers.getNumberOfDistinctWebsites"); - $view->setColumnTranslation('Referers_distinctWebsites', ucfirst(Piwik_Translate('Referers_DistinctWebsites'))); - $view->setColumnsToDisplay(array('Referers_distinctWebsites')); - return $this->renderView($view, $fetch); - } - function getLastDistinctCampaignsGraph( $fetch = false ) - { - $view = $this->getLastUnitGraph($this->pluginName,__FUNCTION__, "Referers.getNumberOfDistinctCampaigns"); - $view->setColumnTranslation('Referers_distinctCampaigns', ucfirst(Piwik_Translate('Referers_DistinctCampaigns'))); - $view->setColumnsToDisplay(array('Referers_distinctCampaigns')); - return $this->renderView($view, $fetch); - } + $totalVisits = array_sum($nameValues); + foreach ($nameValues as $name => $value) { + $view->$name = $value; - function getKeywordsForPage() - { - Piwik::checkUserHasViewAccess($this->idSite); - - $requestUrl = '&date=previous1' - .'&period=week' - .'&idSite='.$this->idSite - ; - - $topPageUrlRequest = $requestUrl - .'&method=Actions.getPageUrls' - .'&filter_limit=50' - .'&format=original'; - $request = new Piwik_API_Request($topPageUrlRequest); - $request = $request->process(); - $tables = $request->getArray(); - - $topPageUrl = false; - $first = key($tables); - if(!empty($first)) - { - $topPageUrls = $tables[$first]; - $topPageUrls = $topPageUrls->getRowsMetadata('url'); - $tmpTopPageUrls = array_values($topPageUrls); - $topPageUrl = current($tmpTopPageUrls); - } - if(empty($topPageUrl)) - { - $topPageUrl = $this->site->getMainUrl(); - } - $url = $topPageUrl; - - // HTML - $api = Piwik_Url::getCurrentUrlWithoutFileName() - .'?module=API&method=Referers.getKeywordsForPageUrl' - .'&format=php' - .'&filter_limit=10' - .'&token_auth='.Piwik::getCurrentUserTokenAuth(); - - $api .= $requestUrl; - $code = ' + // calculate percent of total, if there were any visits + if ($value != 0 + && $totalVisits != 0 + ) { + $percentName = $name . 'Percent'; + $view->$percentName = round(($value / $totalVisits) * 100, 0); + } + } + + // set distinct metrics + $distinctMetrics = $this->getDistinctReferrersMetrics(); + foreach ($distinctMetrics as $name => $value) { + $view->$name = $value; + } + + // calculate evolution for visit metrics & distinct metrics + list($lastPeriodDate, $ignore) = Piwik_Period_Range::getLastDate(); + if ($lastPeriodDate !== false) { + $date = Piwik_Common::getRequestVar('date'); + $period = Piwik_Common::getRequestVar('period'); + + $prettyDate = self::getPrettyDate($date, $period); + $prettyLastPeriodDate = self::getPrettyDate($lastPeriodDate, $period); + + // visit metrics + $previousValues = $this->getReferersVisitorsByType($lastPeriodDate); + $this->addEvolutionPropertiesToView($view, $prettyDate, $nameValues, $prettyLastPeriodDate, $previousValues); + + // distinct metrics + $previousValues = $this->getDistinctReferrersMetrics($lastPeriodDate); + $this->addEvolutionPropertiesToView($view, $prettyDate, $distinctMetrics, $prettyLastPeriodDate, $previousValues); + } + + // sparkline for the historical data of the above values + $view->urlSparklineSearchEngines = $this->getReferrerUrlSparkline(Piwik_Common::REFERER_TYPE_SEARCH_ENGINE); + $view->urlSparklineDirectEntry = $this->getReferrerUrlSparkline(Piwik_Common::REFERER_TYPE_DIRECT_ENTRY); + $view->urlSparklineWebsites = $this->getReferrerUrlSparkline(Piwik_Common::REFERER_TYPE_WEBSITE); + $view->urlSparklineCampaigns = $this->getReferrerUrlSparkline(Piwik_Common::REFERER_TYPE_CAMPAIGN); + + // sparklines for the evolution of the distinct keywords count/websites count/ etc + $view->urlSparklineDistinctSearchEngines = $this->getUrlSparkline('getLastDistinctSearchEnginesGraph'); + $view->urlSparklineDistinctKeywords = $this->getUrlSparkline('getLastDistinctKeywordsGraph'); + $view->urlSparklineDistinctWebsites = $this->getUrlSparkline('getLastDistinctWebsitesGraph'); + $view->urlSparklineDistinctCampaigns = $this->getUrlSparkline('getLastDistinctCampaignsGraph'); + + $view->totalVisits = $totalVisits; + $view->referrersReportsByDimension = $this->getReferrersReportsByDimensionView($totalVisits); + + echo $view->render(); + } + + /** + * Returns HTML for the Referrers Overview page that categorizes Referrer reports + * & allows the user to switch between them. + * + * @param int $visits The number of visits for this period & site. If <= 0, the + * reports are not shown, since they will have no data. + * @return string The report viewer HTML. + */ + private function getReferrersReportsByDimensionView($visits) + { + $result = ''; + + // only display the reports by dimension view if there are visits + if ($visits > 0) { + $referrersReportsByDimension = new Piwik_View_ReportsByDimension(); + + $referrersReportsByDimension->addReport( + 'Referers_ViewAllReferrers', 'Referers_WidgetGetAll', 'Referers.getAll'); + + $byTypeCategory = Piwik_Translate('Referers_ViewReferrersBy', Piwik_Translate('Live_GoalType')); + $referrersReportsByDimension->addReport( + $byTypeCategory, 'Referers_WidgetKeywords', 'Referers.getKeywords'); + $referrersReportsByDimension->addReport($byTypeCategory, 'SitesManager_Sites', 'Referers.getWebsites'); + $referrersReportsByDimension->addReport($byTypeCategory, 'Referers_Campaigns', 'Referers.getCampaigns'); + + $bySourceCategory = Piwik_Translate('Referers_ViewReferrersBy', Piwik_Translate('General_Source')); + $referrersReportsByDimension->addReport($bySourceCategory, 'Referers_Socials', 'Referers.getSocials'); + $referrersReportsByDimension->addReport( + $bySourceCategory, 'Referers_SearchEngines', 'Referers.getSearchEngines'); + + $result = $referrersReportsByDimension->render(); + } + + return $result; + } + + function getSearchEnginesAndKeywords() + { + $view = Piwik_View::factory('searchEngines_Keywords'); + $view->searchEngines = $this->getSearchEngines(true); + $view->keywords = $this->getKeywords(true); + echo $view->render(); + } + + function getRefererType($fetch = false) + { + $view = Piwik_ViewDataTable::factory('tableAllColumns'); + $view->init($this->pluginName, + __FUNCTION__, + 'Referers.getRefererType', + 'getRefererType' + ); + $view->disableSearchBox(); + $view->disableOffsetInformationAndPaginationControls(); + $view->disableExcludeLowPopulation(); + $view->disableSubTableWhenShowGoals(); + $view->enableShowGoals(); + $view->setLimit(10); + $view->setColumnsToDisplay(array('label', 'nb_visits')); + + $idSubtable = Piwik_Common::getRequestVar('idSubtable', false); + $labelColumnTitle = Piwik_Translate('Referers_ColumnRefererType'); + if ($idSubtable !== false) { + switch ($idSubtable) { + case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: + $labelColumnTitle = Piwik_Translate('Referers_ColumnSearchEngine'); + break; + case Piwik_Common::REFERER_TYPE_WEBSITE: + $labelColumnTitle = Piwik_Translate('Referers_ColumnWebsite'); + break; + case Piwik_Common::REFERER_TYPE_CAMPAIGN: + $labelColumnTitle = Piwik_Translate('Referers_ColumnCampaign'); + break; + default: + break; + } + } + $view->setColumnTranslation('label', $labelColumnTitle); + + $this->setMetricsVariablesView($view); + return $this->renderView($view, $fetch); + } + + /** + * Returns or echo's a report that shows all search keyword, website and campaign + * referrer information in one report. + * + * @param bool $fetch True if the report HTML should be returned. If false, the + * report is echo'd and nothing is returned. + * @return string The report HTML or nothing if $fetch is set to false. + */ + public function getAll($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, 'Referers.getAll'); + $view->disableExcludeLowPopulation(); + $view->setColumnTranslation('label', Piwik_Translate('Referers_Referrer')); + $view->setColumnsToDisplay(array('label', 'nb_visits')); + $view->enableShowGoals(); + $view->setLimit(20); + $view->setCustomParameter('disable_row_actions', '1'); + + $setGetAllHtmlPrefix = array($this, 'setGetAllHtmlPrefix'); + $view->queueFilter( + 'MetadataCallbackAddMetadata', array('referrer_type', 'html_label_prefix', $setGetAllHtmlPrefix)); + + $view->setMetricsVariablesView($view); + + return $this->renderView($view, $fetch); + } + + /** + * DataTable filter callback that returns the HTML prefix for a label in the + * 'getAll' report based on the row's referrer type. + * + * @param int $referrerType The referrer type. + * @return string + */ + public function setGetAllHtmlPrefix($referrerType) + { + // get singular label for referrer type + $indexTranslation = ''; + switch ($referrerType) { + case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY: + $indexTranslation = 'Referers_DirectEntry'; + break; + case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: + $indexTranslation = 'Referers_ColumnKeyword'; + break; + case Piwik_Common::REFERER_TYPE_WEBSITE: + $indexTranslation = 'Referers_ColumnWebsite'; + break; + case Piwik_Common::REFERER_TYPE_CAMPAIGN: + $indexTranslation = 'Referers_ColumnCampaign'; + break; + default: + // case of newsletter, partners, before Piwik 0.2.25 + $indexTranslation = 'General_Others'; + break; + } + + $label = strtolower(Piwik_Translate($indexTranslation)); + + // return html that displays it as grey & italic + return '(' . $label . ')'; + } + + function getKeywords($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, + 'Referers.getKeywords', + 'getSearchEnginesFromKeywordId' + ); + $view->disableExcludeLowPopulation(); + $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnKeyword')); + $view->enableShowGoals(); + $view->setLimit(25); + $view->disableSubTableWhenShowGoals(); + + $this->setMetricsVariablesView($view); + + return $this->renderView($view, $fetch); + } + + function getSearchEnginesFromKeywordId($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, + 'Referers.getSearchEnginesFromKeywordId' + ); + $view->disableSearchBox(); + $view->disableExcludeLowPopulation(); + $view->setColumnsToDisplay(array('label', 'nb_visits')); + $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnSearchEngine')); + return $this->renderView($view, $fetch); + } + + + function getSearchEngines($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, + 'Referers.getSearchEngines', + 'getKeywordsFromSearchEngineId' + ); + $view->disableSearchBox(); + $view->disableExcludeLowPopulation(); + $view->enableShowGoals(); + $view->setLimit(25); + $view->disableSubTableWhenShowGoals(); + $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnSearchEngine')); + + $this->setMetricsVariablesView($view); + + return $this->renderView($view, $fetch); + } + + function getKeywordsFromSearchEngineId($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, + 'Referers.getKeywordsFromSearchEngineId' + ); + $view->disableSearchBox(); + $view->disableExcludeLowPopulation(); + $view->setColumnsToDisplay(array('label', 'nb_visits')); + $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnKeyword')); + return $this->renderView($view, $fetch); + } + + function indexWebsites($fetch = false) + { + $view = Piwik_View::factory('Websites_SocialNetworks'); + $view->websites = $this->getWebsites(true); + $view->socials = $this->getSocials(true); + if ($fetch) { + return $view->render(); + } else { + echo $view->render(); + } + } + + function getWebsites($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, + 'Referers.getWebsites', + 'getUrlsFromWebsiteId' + ); + $view->disableExcludeLowPopulation(); + $view->enableShowGoals(); + $view->setLimit(25); + $view->disableSubTableWhenShowGoals(); + $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnWebsite')); + + $this->setMetricsVariablesView($view); + + return $this->renderView($view, $fetch); + } + + function getSocials($fetch = false) + { + $view = Piwik_ViewDataTable::factory('graphPie'); + $view->init($this->pluginName, __FUNCTION__, 'Referers.getSocials', 'getUrlsForSocial'); + $view->disableExcludeLowPopulation(); + $view->setLimit(10); + $view->enableShowGoals(); + $view->disableSubTableWhenShowGoals(); + $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnSocial')); + + if (empty($_REQUEST['widget'])) { + $view->setFooterMessage(Piwik_Translate('Referers_SocialFooterMessage')); + } + + $this->setMetricsVariablesView($view); + + return $this->renderView($view, $fetch); + } + + function getUrlsForSocial($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, 'Referers.getUrlsForSocial'); + $view->disableExcludeLowPopulation(); + $view->setLimit(10); + $view->enableShowGoals(); + $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnWebsitePage')); + + $this->setMetricsVariablesView($view); + + return $this->renderView($view, $fetch); + } + + function indexCampaigns($fetch = false) + { + return Piwik_View::singleReport( + Piwik_Translate('Referers_Campaigns'), + $this->getCampaigns(true), $fetch); + } + + function getCampaigns($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, + 'Referers.getCampaigns', + 'getKeywordsFromCampaignId' + ); + $view->disableExcludeLowPopulation(); + $view->enableShowGoals(); + $view->setLimit(25); + $view->setColumnsToDisplay(array('label', 'nb_visits')); + $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnCampaign')); + + $help = Piwik_Translate('Referers_CampaignFooterHelp', array('', + ' - ', + '' + )); + $view->setFooterMessage($help); + $this->setMetricsVariablesView($view); + return $this->renderView($view, $fetch); + } + + function getKeywordsFromCampaignId($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, + 'Referers.getKeywordsFromCampaignId' + ); + + $view->disableSearchBox(); + $view->disableExcludeLowPopulation(); + $view->setColumnsToDisplay(array('label', 'nb_visits')); + $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnKeyword')); + + return $this->renderView($view, $fetch); + } + + function getUrlsFromWebsiteId($fetch = false) + { + $view = Piwik_ViewDataTable::factory(); + $view->init($this->pluginName, __FUNCTION__, + 'Referers.getUrlsFromWebsiteId' + ); + $view->disableSearchBox(); + $view->disableExcludeLowPopulation(); + $view->setColumnsToDisplay(array('label', 'nb_visits')); + $view->setColumnTranslation('label', Piwik_Translate('Referers_ColumnWebsitePage')); + $view->setTooltipMetadataName('url'); + return $this->renderView($view, $fetch); + } + + protected function getReferersVisitorsByType($date = false) + { + if ($date === false) { + $date = Piwik_Common::getRequestVar('date', false); + } + + // we disable the queued filters because here we want to get the visits coming from search engines + // if the filters were applied we would have to look up for a label looking like "Search Engines" + // which is not good when we have translations + $dataTableReferersType = Piwik_API_Request::processRequest( + "Referers.getRefererType", array('disable_queued_filters' => '1', 'date' => $date)); + + $nameToColumnId = array( + 'visitorsFromSearchEngines' => Piwik_Common::REFERER_TYPE_SEARCH_ENGINE, + 'visitorsFromDirectEntry' => Piwik_Common::REFERER_TYPE_DIRECT_ENTRY, + 'visitorsFromWebsites' => Piwik_Common::REFERER_TYPE_WEBSITE, + 'visitorsFromCampaigns' => Piwik_Common::REFERER_TYPE_CAMPAIGN, + ); + $return = array(); + foreach ($nameToColumnId as $nameVar => $columnId) { + $value = 0; + $row = $dataTableReferersType->getRowFromLabel($columnId); + if ($row !== false) { + $value = $row->getColumn(Piwik_Archive::INDEX_NB_VISITS); + } + $return[$nameVar] = $value; + } + return $return; + } + + protected $referrerTypeToLabel = array( + Piwik_Common::REFERER_TYPE_DIRECT_ENTRY => 'Referers_DirectEntry', + Piwik_Common::REFERER_TYPE_SEARCH_ENGINE => 'Referers_SearchEngines', + Piwik_Common::REFERER_TYPE_WEBSITE => 'Referers_Websites', + Piwik_Common::REFERER_TYPE_CAMPAIGN => 'Referers_Campaigns', + ); + + public function getEvolutionGraph($fetch = false, $typeReferer = false, array $columns = array()) + { + $view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, 'Referers.getRefererType'); + + $view->addTotalRow(); + + // configure displayed columns + if (empty($columns)) { + $columns = Piwik_Common::getRequestVar('columns'); + $columns = Piwik::getArrayFromApiParameter($columns); + } + $columns = !is_array($columns) ? array($columns) : $columns; + $view->setColumnsToDisplay($columns); + + // configure selectable columns + if (Piwik_Common::getRequestVar('period', false) == 'day') { + $selectable = array('nb_visits', 'nb_uniq_visitors', 'nb_actions'); + } else { + $selectable = array('nb_visits', 'nb_actions'); + } + $view->setSelectableColumns($selectable); + + // configure displayed rows + $visibleRows = Piwik_Common::getRequestVar('rows', false); + if ($visibleRows !== false) { + // this happens when the row picker has been used + $visibleRows = Piwik::getArrayFromApiParameter($visibleRows); + + // typeReferer is redundant if rows are defined, so make sure it's not used + $view->setCustomParameter('typeReferer', false); + } else { + // use $typeReferer as default + if ($typeReferer === false) { + $typeReferer = Piwik_Common::getRequestVar('typeReferer', false); + } + $label = self::getTranslatedReferrerTypeLabel($typeReferer); + $total = Piwik_Translate('General_Total'); + $visibleRows = array($label, $total); + $view->setParametersToModify(array('rows' => $label . ',' . $total)); + } + $view->addRowPicker($visibleRows); + + $view->setReportDocumentation(Piwik_Translate('Referers_EvolutionDocumentation') . '
' + . Piwik_Translate('General_BrokenDownReportDocumentation') . '
' + . Piwik_Translate('Referers_EvolutionDocumentationMoreInfo', '"' . Piwik_Translate('Referers_DetailsByRefererType') . '"')); + + return $this->renderView($view, $fetch); + } + + function getLastDistinctSearchEnginesGraph($fetch = false) + { + $view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, "Referers.getNumberOfDistinctSearchEngines"); + $view->setColumnTranslation('Referers_distinctSearchEngines', ucfirst(Piwik_Translate('Referers_DistinctSearchEngines'))); + $view->setColumnsToDisplay(array('Referers_distinctSearchEngines')); + return $this->renderView($view, $fetch); + } + + function getLastDistinctKeywordsGraph($fetch = false) + { + $view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, "Referers.getNumberOfDistinctKeywords"); + $view->setColumnTranslation('Referers_distinctKeywords', ucfirst(Piwik_Translate('Referers_DistinctKeywords'))); + $view->setColumnsToDisplay(array('Referers_distinctKeywords')); + return $this->renderView($view, $fetch); + } + + function getLastDistinctWebsitesGraph($fetch = false) + { + $view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, "Referers.getNumberOfDistinctWebsites"); + $view->setColumnTranslation('Referers_distinctWebsites', ucfirst(Piwik_Translate('Referers_DistinctWebsites'))); + $view->setColumnsToDisplay(array('Referers_distinctWebsites')); + return $this->renderView($view, $fetch); + } + + function getLastDistinctCampaignsGraph($fetch = false) + { + $view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, "Referers.getNumberOfDistinctCampaigns"); + $view->setColumnTranslation('Referers_distinctCampaigns', ucfirst(Piwik_Translate('Referers_DistinctCampaigns'))); + $view->setColumnsToDisplay(array('Referers_distinctCampaigns')); + return $this->renderView($view, $fetch); + } + + function getKeywordsForPage() + { + Piwik::checkUserHasViewAccess($this->idSite); + + $requestUrl = '&date=previous1' + . '&period=week' + . '&idSite=' . $this->idSite; + + $topPageUrlRequest = $requestUrl + . '&method=Actions.getPageUrls' + . '&filter_limit=50' + . '&format=original'; + $request = new Piwik_API_Request($topPageUrlRequest); + $request = $request->process(); + $tables = $request->getArray(); + + $topPageUrl = false; + $first = key($tables); + if (!empty($first)) { + $topPageUrls = $tables[$first]; + $topPageUrls = $topPageUrls->getRowsMetadata('url'); + $tmpTopPageUrls = array_values($topPageUrls); + $topPageUrl = current($tmpTopPageUrls); + } + if (empty($topPageUrl)) { + $topPageUrl = $this->site->getMainUrl(); + } + $url = $topPageUrl; + + // HTML + $api = Piwik_Url::getCurrentUrlWithoutFileName() + . '?module=API&method=Referers.getKeywordsForPageUrl' + . '&format=php' + . '&filter_limit=10' + . '&token_auth=' . Piwik::getCurrentUserTokenAuth(); + + $api .= $requestUrl; + $code = ' // This function will call the API to get best keyword for current URL. // Then it writes the list of best keywords in a HTML list function DisplayTopKeywords($url = "") @@ -596,7 +578,7 @@ function DisplayTopKeywords($url = "") @ini_set("default_socket_timeout", $timeout = 1); // Get the Keywords data $url = empty($url) ? "http://". $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"] : $url; - $api = "'.$api.'&url=" . urlencode($url); + $api = "' . $api . '&url=" . urlencode($url); $keywords = @unserialize(file_get_contents($api)); if($keywords === false || isset($keywords["result"])) { // DEBUG ONLY: uncomment for troubleshooting an empty output (the URL output reveals the token_auth) @@ -616,131 +598,132 @@ function DisplayTopKeywords($url = "") } '; - $jsonRequest = str_replace('format=php', 'format=json', $api); - echo "

This widget is designed to work in your website directly. + $jsonRequest = str_replace('format=php', 'format=json', $api); + echo "

This widget is designed to work in your website directly. This widget makes it easy to use Piwik to automatically display the list of Top Keywords, for each of your website Page URLs.

Example API URL - For example if you would like to get the top 10 keywords, used last week, to land on the page $topPageUrl, - in format JSON: you would dynamically fetch the data using this API request URL. Make sure you encode the 'url' parameter in the URL.

+ in format JSON: you would dynamically fetch the data using this API request URL. Make sure you encode the 'url' parameter in the URL.

PHP Function ready to use! - If you use PHP on your website, we have prepared a small code snippet that you can copy paste in your Website PHP files. You can then simply call the function DisplayTopKeywords(); anywhere in your template, at the bottom of the content or in your blog sidebar. If you run this code in your page $topPageUrl, it would output the following:"; - - echo "

"; - function DisplayTopKeywords($url = "", $api) - { - // Do not spend more than 1 second fetching the data - @ini_set("default_socket_timeout", $timeout = 1); - // Get the Keywords data - $url = empty($url) ? "http://". $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"] : $url; - $api = $api."&url=" . urlencode($url); - $keywords = @unserialize(file_get_contents($api)); - if($keywords === false || isset($keywords["result"])) { - // DEBUG ONLY: uncomment for troubleshooting an empty output (the URL output reveals the token_auth) - //echo "Error while fetching the Top Keywords from Piwik"; - return; - } - - // Display the list in HTML - $url = htmlspecialchars($url, ENT_QUOTES); - $output = "

Top Keywords for $url

    "; - foreach($keywords as $keyword) { - $output .= "
  • ". $keyword[0]. "
  • "; - } - if(empty($keywords)) { $output .= "Nothing yet..."; } - $output .= "
"; - echo $output; - } - DisplayTopKeywords($topPageUrl, $api); - - echo "

+ + echo "
"; + function DisplayTopKeywords($url = "", $api) + { + // Do not spend more than 1 second fetching the data + @ini_set("default_socket_timeout", $timeout = 1); + // Get the Keywords data + $url = empty($url) ? "http://" . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"] : $url; + $api = $api . "&url=" . urlencode($url); + $keywords = @unserialize(file_get_contents($api)); + if ($keywords === false || isset($keywords["result"])) { + // DEBUG ONLY: uncomment for troubleshooting an empty output (the URL output reveals the token_auth) + //echo "Error while fetching the Top Keywords from Piwik"; + return; + } + + // Display the list in HTML + $url = htmlspecialchars($url, ENT_QUOTES); + $output = "

Top Keywords for $url

    "; + foreach ($keywords as $keyword) { + $output .= "
  • " . $keyword[0] . "
  • "; + } + if (empty($keywords)) { + $output .= "Nothing yet..."; + } + $output .= "
"; + echo $output; + } + + DisplayTopKeywords($topPageUrl, $api); + + echo "

Here is the PHP function that you can paste in your pages:

"; - - echo " + + echo "

Notes: You can for example edit the code to to make the Top search keywords link to your Website search result pages.
On medium to large traffic websites, we recommend to cache this data, as to minimize the performance impact of calling the Piwik API on each page view.

"; - - } - - /** - * Returns the i18n-ized label for a referrer type. - * - * @param int $typeReferrer The referrer type. Referrer types are defined in Piwik_Common class. - * @return string The i18n-ized label. - */ - public static function getTranslatedReferrerTypeLabel( $typeReferrer ) - { - $label = Piwik_getRefererTypeLabel($typeReferrer); - return Piwik_Translate($label); - } - - /** - * Returns the URL for the sparkline of visits with a specific referrer type. - * - * @param int $typeReferrer The referrer type. Referrer types are defined in Piwik_Common class. - * @return string The URL that can be used to get a sparkline image. - */ - private function getReferrerUrlSparkline( $referrerType ) - { - $totalRow = Piwik_Translate('General_Total'); - return $this->getUrlSparkline( - 'getEvolutionGraph', - array('columns' => array('nb_visits'), - 'rows' => array(self::getTranslatedReferrerTypeLabel($referrerType), $totalRow), - 'typeReferer' => $referrerType) - ); - } - - /** - * Returns an array containing the number of distinct referrers for each - * referrer type. - * - * @param string|false $date The date to use when getting metrics. If false, the - * date query param is used. - * @return array The metrics. - */ - private function getDistinctReferrersMetrics( $date = false ) - { - $propertyToAccessorMapping = array( - 'numberDistinctSearchEngines' => 'getNumberOfDistinctSearchEngines', - 'numberDistinctKeywords' => 'getNumberOfDistinctKeywords', - 'numberDistinctWebsites' => 'getNumberOfDistinctWebsites', - 'numberDistinctWebsitesUrls' => 'getNumberOfDistinctWebsitesUrls', - 'numberDistinctCampaigns' => 'getNumberOfDistinctCampaigns', - ); - - $result = array(); - foreach ($propertyToAccessorMapping as $property => $method) - { - $result[$property] = $this->getNumericValue('Referers.'.$method, $date); - } - return $result; - } - - /** - * Utility method that calculates evolution values for a set of current & past values - * and sets properties on a Piwik_View w/ HTML that displays the evolution percents. - * - * @param Piwik_View $view The view to set properties on. - * @param string $date The date of the current values. - * @param array $currentValues Array mapping view property names w/ present values. - * @param string $lastPeriodDate The date of the period in the past. - * @param array $previousValues Array mapping view property names w/ past values. Keys - * in this array should be the same as keys in $currentValues. - * @param bool $isVisits Whether the values are counting visits or something else. - */ - private function addEvolutionPropertiesToView( $view, $date, $currentValues, $lastPeriodDate, $previousValues) - { - foreach ($previousValues as $name => $pastValue) - { - $currentValue = $currentValues[$name]; - $evolutionName = $name.'Evolution'; - - $view->$evolutionName = $this->getEvolutionHtml($date, $currentValue, $lastPeriodDate, $pastValue); - } - } + + } + + /** + * Returns the i18n-ized label for a referrer type. + * + * @param int $typeReferrer The referrer type. Referrer types are defined in Piwik_Common class. + * @return string The i18n-ized label. + */ + public static function getTranslatedReferrerTypeLabel($typeReferrer) + { + $label = Piwik_getRefererTypeLabel($typeReferrer); + return Piwik_Translate($label); + } + + /** + * Returns the URL for the sparkline of visits with a specific referrer type. + * + * @param int $typeReferrer The referrer type. Referrer types are defined in Piwik_Common class. + * @return string The URL that can be used to get a sparkline image. + */ + private function getReferrerUrlSparkline($referrerType) + { + $totalRow = Piwik_Translate('General_Total'); + return $this->getUrlSparkline( + 'getEvolutionGraph', + array('columns' => array('nb_visits'), + 'rows' => array(self::getTranslatedReferrerTypeLabel($referrerType), $totalRow), + 'typeReferer' => $referrerType) + ); + } + + /** + * Returns an array containing the number of distinct referrers for each + * referrer type. + * + * @param string|false $date The date to use when getting metrics. If false, the + * date query param is used. + * @return array The metrics. + */ + private function getDistinctReferrersMetrics($date = false) + { + $propertyToAccessorMapping = array( + 'numberDistinctSearchEngines' => 'getNumberOfDistinctSearchEngines', + 'numberDistinctKeywords' => 'getNumberOfDistinctKeywords', + 'numberDistinctWebsites' => 'getNumberOfDistinctWebsites', + 'numberDistinctWebsitesUrls' => 'getNumberOfDistinctWebsitesUrls', + 'numberDistinctCampaigns' => 'getNumberOfDistinctCampaigns', + ); + + $result = array(); + foreach ($propertyToAccessorMapping as $property => $method) { + $result[$property] = $this->getNumericValue('Referers.' . $method, $date); + } + return $result; + } + + /** + * Utility method that calculates evolution values for a set of current & past values + * and sets properties on a Piwik_View w/ HTML that displays the evolution percents. + * + * @param Piwik_View $view The view to set properties on. + * @param string $date The date of the current values. + * @param array $currentValues Array mapping view property names w/ present values. + * @param string $lastPeriodDate The date of the period in the past. + * @param array $previousValues Array mapping view property names w/ past values. Keys + * in this array should be the same as keys in $currentValues. + * @param bool $isVisits Whether the values are counting visits or something else. + */ + private function addEvolutionPropertiesToView($view, $date, $currentValues, $lastPeriodDate, $previousValues) + { + foreach ($previousValues as $name => $pastValue) { + $currentValue = $currentValues[$name]; + $evolutionName = $name . 'Evolution'; + + $view->$evolutionName = $this->getEvolutionHtml($date, $currentValue, $lastPeriodDate, $pastValue); + } + } } diff --git a/plugins/Referers/Referers.php b/plugins/Referers/Referers.php index 88497f8a47..3e40f2d9a9 100644 --- a/plugins/Referers/Referers.php +++ b/plugins/Referers/Referers.php @@ -19,591 +19,570 @@ require_once PIWIK_INCLUDE_PATH . '/plugins/Referers/functions.php'; */ class Piwik_Referers extends Piwik_Plugin { - public $archiveProcessing; - protected $columnToSortByBeforeTruncation; - protected $maximumRowsInDataTableLevelZero; - protected $maximumRowsInSubDataTable; - - public function getInformation() - { - $info = array( - 'description' => Piwik_Translate('Referers_PluginDescription'), - 'author' => 'Piwik', - 'author_homepage' => 'http://piwik.org/', - 'version' => Piwik_Version::VERSION, - ); - - return $info; - } - - function getListHooksRegistered() - { - $hooks = array( - 'ArchiveProcessing_Day.compute' => 'archiveDay', - 'ArchiveProcessing_Period.compute' => 'archivePeriod', - 'WidgetsList.add' => 'addWidgets', - 'Menu.add' => 'addMenus', - 'Goals.getReportsWithGoalMetrics' => 'getReportsWithGoalMetrics', - 'API.getReportMetadata' => 'getReportMetadata', - 'API.getSegmentsMetadata' => 'getSegmentsMetadata', - ); - return $hooks; - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - public function getReportMetadata($notification) - { - $reports = &$notification->getNotificationObject(); - $reports = array_merge($reports, array( - array( - 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_Type'), - 'module' => 'Referers', - 'action' => 'getRefererType', - 'dimension' => Piwik_Translate('Referers_ColumnRefererType'), - 'constantRowsCount' => true, - 'documentation' => Piwik_Translate('Referers_TypeReportDocumentation').'
' - .''.Piwik_Translate('Referers_DirectEntry').': '.Piwik_Translate('Referers_DirectEntryDocumentation').'
' - .''.Piwik_Translate('Referers_SearchEngines').': '.Piwik_Translate('Referers_SearchEnginesDocumentation', - array('
', '"'.Piwik_Translate('Referers_SubmenuSearchEngines').'"')).'
' - .''.Piwik_Translate('Referers_Websites').': '.Piwik_Translate('Referers_WebsitesDocumentation', - array('
', '"'.Piwik_Translate('Referers_SubmenuWebsites').'"')).'
' - .''.Piwik_Translate('Referers_Campaigns').': '.Piwik_Translate('Referers_CampaignsDocumentation', - array('
', '"'.Piwik_Translate('Referers_SubmenuCampaigns').'"')), - 'order' => 1, - ), - array( - 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_WidgetGetAll'), - 'module' => 'Referers', - 'action' => 'getAll', - 'dimension' => Piwik_Translate('Referers_Referrer'), - 'documentation' => Piwik_Translate('Referers_AllReferersReportDocumentation', '
'), - 'order' => 2, - ), - array( - 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_Keywords'), - 'module' => 'Referers', - 'action' => 'getKeywords', - 'actionToLoadSubTables' => 'getSearchEnginesFromKeywordId', - 'dimension' => Piwik_Translate('Referers_ColumnKeyword'), - 'documentation' => Piwik_Translate('Referers_KeywordsReportDocumentation', '
'), - 'order' => 3, - ), - array( // subtable report - 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_Keywords'), - 'module' => 'Referers', - 'action' => 'getSearchEnginesFromKeywordId', - 'dimension' => Piwik_Translate('Referers_ColumnSearchEngine'), - 'documentation' => Piwik_Translate('Referers_KeywordsReportDocumentation', '
'), - 'isSubtableReport' => true, - 'order' => 4 - ), - - array( - 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_Websites'), - 'module' => 'Referers', - 'action' => 'getWebsites', - 'dimension' => Piwik_Translate('Referers_ColumnWebsite'), - 'documentation' => Piwik_Translate('Referers_WebsitesReportDocumentation', '
'), - 'actionToLoadSubTables' => 'getUrlsFromWebsiteId', - 'order' => 5 - ), - array( // subtable report - 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_Websites'), - 'module' => 'Referers', - 'action' => 'getUrlsFromWebsiteId', - 'dimension' => Piwik_Translate('Referers_ColumnWebsitePage'), - 'documentation' => Piwik_Translate('Referers_WebsitesReportDocumentation', '
'), - 'isSubtableReport' => true, - 'order' => 6, - ), - - array( - 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_SearchEngines'), - 'module' => 'Referers', - 'action' => 'getSearchEngines', - 'dimension' => Piwik_Translate('Referers_ColumnSearchEngine'), - 'documentation' => Piwik_Translate('Referers_SearchEnginesReportDocumentation', '
'), - 'actionToLoadSubTables' => 'getKeywordsFromSearchEngineId', - 'order' => 7, - ), - array( // subtable report - 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_SearchEngines'), - 'module' => 'Referers', - 'action' => 'getKeywordsFromSearchEngineId', - 'dimension' => Piwik_Translate('Referers_ColumnKeyword'), - 'documentation' => Piwik_Translate('Referers_SearchEnginesReportDocumentation', '
'), - 'isSubtableReport' => true, - 'order' => 8, - ), - - array( - 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_Campaigns'), - 'module' => 'Referers', - 'action' => 'getCampaigns', - 'dimension' => Piwik_Translate('Referers_ColumnCampaign'), - 'documentation' => Piwik_Translate('Referers_CampaignsReportDocumentation', - array('
', '', '')), - 'actionToLoadSubTables' => 'getKeywordsFromCampaignId', - 'order' => 9, - ), - array( // subtable report - 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_Campaigns'), - 'module' => 'Referers', - 'action' => 'getKeywordsFromCampaignId', - 'dimension' => Piwik_Translate('Referers_ColumnKeyword'), - 'documentation' => Piwik_Translate('Referers_CampaignsReportDocumentation', - array('
', '', '')), - 'isSubtableReport' => true, - 'order' => 10, - ), - array( - 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_Socials'), - 'module' => 'Referers', - 'action' => 'getSocials', - 'actionToLoadSubTables' => 'getUrlsForSocial', - 'dimension' => Piwik_Translate('Referers_ColumnSocial'), - 'documentation' => Piwik_Translate('Referers_WebsitesReportDocumentation', '
'), - 'order' => 11, - ), - )); - } - - /** - * @param Piwik_Event_Notification $notification notification object - */ - public function getSegmentsMetadata($notification) - { - $segments =& $notification->getNotificationObject(); - $segments[] = array( - 'type' => 'dimension', - 'category' => 'Referers_Referers', - 'name' => 'Referers_ColumnRefererType', - 'segment' => 'referrerType', - 'acceptedValues' => 'direct, search, website, campaign', - 'sqlSegment' => 'log_visit.referer_type', - 'sqlFilter' => 'Piwik_getRefererTypeFromShortName', - ); - $segments[] = array( - 'type' => 'dimension', - 'category' => 'Referers_Referers', - 'name' => 'Referers_ColumnKeyword', - 'segment' => 'referrerKeyword', - 'acceptedValues' => 'Encoded%20Keyword, keyword', - 'sqlSegment' => 'log_visit.referer_keyword', - ); - $segments[] = array( - 'type' => 'dimension', - 'category' => 'Referers_Referers', - 'name' => 'Referers_RefererName', - 'segment' => 'referrerName', - 'acceptedValues' => 'twitter.com, www.facebook.com, Bing, Google, Yahoo, CampaignName', - 'sqlSegment' => 'log_visit.referer_name', - ); - $segments[] = array( - 'type' => 'dimension', - 'category' => 'Referers_Referers', - 'name' => 'Live_Referrer_URL', - 'acceptedValues' => 'http%3A%2F%2Fwww.example.org%2Freferer-page.htm', - 'segment' => 'referrerUrl', - 'sqlSegment' => 'log_visit.referer_url', - ); - } - - /** - * Adds Referer widgets - */ - function addWidgets() - { - Piwik_AddWidget( 'Referers_Referers', 'Referers_WidgetKeywords', 'Referers', 'getKeywords'); - Piwik_AddWidget( 'Referers_Referers', 'Referers_WidgetExternalWebsites', 'Referers', 'getWebsites'); - Piwik_AddWidget( 'Referers_Referers', 'Referers_WidgetSocials', 'Referers', 'getSocials'); - Piwik_AddWidget( 'Referers_Referers', 'Referers_WidgetSearchEngines', 'Referers', 'getSearchEngines'); - Piwik_AddWidget( 'Referers_Referers', 'Referers_WidgetCampaigns', 'Referers', 'getCampaigns'); - Piwik_AddWidget( 'Referers_Referers', 'Referers_WidgetOverview', 'Referers', 'getRefererType'); - Piwik_AddWidget( 'Referers_Referers', 'Referers_WidgetGetAll', 'Referers', 'getAll'); - if(Piwik_Archive::isSegmentationEnabled()) - { - Piwik_AddWidget( 'SEO', 'Referers_WidgetTopKeywordsForPages', 'Referers', 'getKeywordsForPage'); - } - } - - /** - * Adds Web Analytics menus - */ - function addMenus() - { - Piwik_AddMenu('Referers_Referers', '', array('module' => 'Referers', 'action' => 'index'), true, 20); - Piwik_AddMenu('Referers_Referers', 'Referers_SubmenuOverview', array('module' => 'Referers', 'action' => 'index'), true, 1); - Piwik_AddMenu('Referers_Referers', 'Referers_SubmenuSearchEngines', array('module' => 'Referers', 'action' => 'getSearchEnginesAndKeywords'), true, 2); - Piwik_AddMenu('Referers_Referers', 'Referers_SubmenuWebsites', array('module' => 'Referers', 'action' => 'indexWebsites'), true, 3); - Piwik_AddMenu('Referers_Referers', 'Referers_SubmenuCampaigns', array('module' => 'Referers', 'action' => 'indexCampaigns'), true, 4); - } - - /** - * Adds Goal dimensions, so that the dimensions are displayed in the UI Goal Overview page - * - * @param Piwik_Event_Notification $notification notification object - * @return void - */ - function getReportsWithGoalMetrics( $notification ) - { - $dimensions =& $notification->getNotificationObject(); - $dimensions = array_merge($dimensions, array( - array( 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_Keywords'), - 'module' => 'Referers', - 'action' => 'getKeywords', - ), - array( 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_SearchEngines'), - 'module' => 'Referers', - 'action' => 'getSearchEngines', - ), - array( 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_Websites'), - 'module' => 'Referers', - 'action' => 'getWebsites', - ), - array( 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_Campaigns'), - 'module' => 'Referers', - 'action' => 'getCampaigns', - ), - array( 'category' => Piwik_Translate('Referers_Referers'), - 'name' => Piwik_Translate('Referers_Type'), - 'module' => 'Referers', - 'action' => 'getRefererType', - ), - )); - } - - function __construct() - { - $this->columnToSortByBeforeTruncation = Piwik_Archive::INDEX_NB_VISITS; - $this->maximumRowsInDataTableLevelZero = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_referers']; - $this->maximumRowsInSubDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_subtable_referers']; - } - - /** - * Period archiving: sums up daily stats and sums report tables, - * making sure that tables are still truncated. - * - * @param Piwik_Event_Notification $notification notification object - * @return void - */ - function archivePeriod( $notification ) - { - $archiveProcessing = $notification->getNotificationObject(); - - if(!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; - - $dataTableToSum = array( - 'Referers_type', - 'Referers_keywordBySearchEngine', - 'Referers_searchEngineByKeyword', - 'Referers_keywordByCampaign', - 'Referers_urlByWebsite', - ); - $nameToCount = $archiveProcessing->archiveDataTable($dataTableToSum, null, $this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation); - - $mappingFromArchiveName = array( - 'Referers_distinctSearchEngines' => - array( 'typeCountToUse' => 'level0', - 'nameTableToUse' => 'Referers_keywordBySearchEngine', - ), - 'Referers_distinctKeywords' => - array( 'typeCountToUse' => 'level0', - 'nameTableToUse' => 'Referers_searchEngineByKeyword', - ), - 'Referers_distinctCampaigns' => - array( 'typeCountToUse' => 'level0', - 'nameTableToUse' => 'Referers_keywordByCampaign', - ), - 'Referers_distinctWebsites' => - array( 'typeCountToUse' => 'level0', - 'nameTableToUse' => 'Referers_urlByWebsite', - ), - 'Referers_distinctWebsitesUrls' => - array( 'typeCountToUse' => 'recursive', - 'nameTableToUse' => 'Referers_urlByWebsite', - ), - ); - - foreach($mappingFromArchiveName as $name => $infoMapping) - { - $typeCountToUse = $infoMapping['typeCountToUse']; - $nameTableToUse = $infoMapping['nameTableToUse']; - - if($typeCountToUse == 'recursive') - { - - $countValue = $nameToCount[$nameTableToUse]['recursive'] - - $nameToCount[$nameTableToUse]['level0']; - } - else - { - $countValue = $nameToCount[$nameTableToUse]['level0']; - } - $archiveProcessing->insertNumericRecord($name, $countValue); - } - } - - const LABEL_KEYWORD_NOT_DEFINED = ""; - - static public function getKeywordNotDefinedString() - { - return Piwik_Translate( 'General_NotDefined', Piwik_Translate('Referers_ColumnKeyword')); - } - static public function getCleanKeyword($label) - { - return $label == Piwik_Referers::LABEL_KEYWORD_NOT_DEFINED - ? self::getKeywordNotDefinedString() - : $label; - } - - /** - * Hooks on daily archive to trigger various log processing - * - * @param Piwik_Event_Notification $notification notification object - * @return void - */ - public function archiveDay( $notification ) - { - /** - * @var Piwik_ArchiveProcessing_Day - */ - $this->archiveProcessing = $notification->getNotificationObject(); - if(!$this->archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; - - $this->archiveDayAggregateVisits($this->archiveProcessing); - $this->archiveDayAggregateGoals($this->archiveProcessing); - Piwik_PostEvent('Referers.archiveDay', $this); - $this->archiveDayRecordInDatabase($this->archiveProcessing); - $this->cleanup(); - } - - protected function cleanup() - { - destroy($this->interestBySearchEngine); - destroy($this->interestByKeyword); - destroy($this->interestBySearchEngineAndKeyword); - destroy($this->interestByKeywordAndSearchEngine); - destroy($this->interestByWebsite); - destroy($this->interestByWebsiteAndUrl); - destroy($this->interestByCampaignAndKeyword); - destroy($this->interestByCampaign); - destroy($this->interestByType); - destroy($this->distinctUrls); - } - - /** - * Daily archive: processes all Referers reports, eg. Visits by Keyword, - * Visits by websites, etc. - * - * @param Piwik_ArchiveProcessing $archiveProcessing - * @throws Exception - * @return void - */ - protected function archiveDayAggregateVisits(Piwik_ArchiveProcessing_Day $archiveProcessing) - { - $dimension = array("referer_type", "referer_name", "referer_keyword", "referer_url"); - $query = $archiveProcessing->queryVisitsByDimension($dimension); - - $this->interestBySearchEngine = - $this->interestByKeyword = - $this->interestBySearchEngineAndKeyword = - $this->interestByKeywordAndSearchEngine = - $this->interestByWebsite = - $this->interestByWebsiteAndUrl = - $this->interestByCampaignAndKeyword = - $this->interestByCampaign = - $this->interestByType = - $this->distinctUrls = array(); - while($row = $query->fetch() ) - { - if(empty($row['referer_type'])) - { - $row['referer_type'] = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY; - } - else - { - switch($row['referer_type']) - { - case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: - if(empty($row['referer_keyword'])) - { - $row['referer_keyword'] = self::LABEL_KEYWORD_NOT_DEFINED; - } - if(!isset($this->interestBySearchEngine[$row['referer_name']])) $this->interestBySearchEngine[$row['referer_name']]= $archiveProcessing->getNewInterestRow(); - if(!isset($this->interestByKeyword[$row['referer_keyword']])) $this->interestByKeyword[$row['referer_keyword']]= $archiveProcessing->getNewInterestRow(); - if(!isset($this->interestBySearchEngineAndKeyword[$row['referer_name']][$row['referer_keyword']])) $this->interestBySearchEngineAndKeyword[$row['referer_name']][$row['referer_keyword']]= $archiveProcessing->getNewInterestRow(); - if(!isset($this->interestByKeywordAndSearchEngine[$row['referer_keyword']][$row['referer_name']])) $this->interestByKeywordAndSearchEngine[$row['referer_keyword']][$row['referer_name']]= $archiveProcessing->getNewInterestRow(); - - $archiveProcessing->updateInterestStats( $row, $this->interestBySearchEngine[$row['referer_name']]); - $archiveProcessing->updateInterestStats( $row, $this->interestByKeyword[$row['referer_keyword']]); - $archiveProcessing->updateInterestStats( $row, $this->interestBySearchEngineAndKeyword[$row['referer_name']][$row['referer_keyword']]); - $archiveProcessing->updateInterestStats( $row, $this->interestByKeywordAndSearchEngine[$row['referer_keyword']][$row['referer_name']]); - break; - - case Piwik_Common::REFERER_TYPE_WEBSITE: - - if(!isset($this->interestByWebsite[$row['referer_name']])) $this->interestByWebsite[$row['referer_name']]= $archiveProcessing->getNewInterestRow(); - $archiveProcessing->updateInterestStats( $row, $this->interestByWebsite[$row['referer_name']]); - - if(!isset($this->interestByWebsiteAndUrl[$row['referer_name']][$row['referer_url']])) $this->interestByWebsiteAndUrl[$row['referer_name']][$row['referer_url']]= $archiveProcessing->getNewInterestRow(); - $archiveProcessing->updateInterestStats( $row, $this->interestByWebsiteAndUrl[$row['referer_name']][$row['referer_url']]); - - if(!isset($this->distinctUrls[$row['referer_url']])) - { - $this->distinctUrls[$row['referer_url']] = true; - } - break; - - case Piwik_Common::REFERER_TYPE_CAMPAIGN: - if(!empty($row['referer_keyword'])) - { - if(!isset($this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']])) $this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']]= $archiveProcessing->getNewInterestRow(); - $archiveProcessing->updateInterestStats( $row, $this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']]); - } - if(!isset($this->interestByCampaign[$row['referer_name']])) $this->interestByCampaign[$row['referer_name']]= $archiveProcessing->getNewInterestRow(); - $archiveProcessing->updateInterestStats( $row, $this->interestByCampaign[$row['referer_name']]); - break; - - case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY: - // direct entry are aggregated below in $this->interestByType array - break; - - default: - throw new Exception("Non expected referer_type = " . $row['referer_type']); - break; - } - } - if(!isset($this->interestByType[$row['referer_type']] )) $this->interestByType[$row['referer_type']] = $archiveProcessing->getNewInterestRow(); - $archiveProcessing->updateInterestStats($row, $this->interestByType[$row['referer_type']]); - } - } - - /** - * Daily Goal archiving: processes reports of Goal conversions by Keyword, - * Goal conversions by Referer Websites, etc. - * - * @param Piwik_ArchiveProcessing $archiveProcessing - * @return void - */ - protected function archiveDayAggregateGoals($archiveProcessing) - { - $query = $archiveProcessing->queryConversionsByDimension(array("referer_type","referer_name","referer_keyword")); - - if($query === false) return; - while($row = $query->fetch() ) - { - if(empty($row['referer_type'])) - { - $row['referer_type'] = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY; - } - else - { - switch($row['referer_type']) - { - case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: - if(empty($row['referer_keyword'])) - { - $row['referer_keyword'] = self::LABEL_KEYWORD_NOT_DEFINED; - } - if(!isset($this->interestBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); - if(!isset($this->interestByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); - - $archiveProcessing->updateGoalStats( $row, $this->interestBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); - $archiveProcessing->updateGoalStats( $row, $this->interestByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); - break; - - case Piwik_Common::REFERER_TYPE_WEBSITE: - if(!isset($this->interestByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); - $archiveProcessing->updateGoalStats( $row, $this->interestByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); - break; - - case Piwik_Common::REFERER_TYPE_CAMPAIGN: - if(!empty($row['referer_keyword'])) - { - if(!isset($this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); - $archiveProcessing->updateGoalStats( $row, $this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); - } - if(!isset($this->interestByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); - $archiveProcessing->updateGoalStats( $row, $this->interestByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); - break; - - case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY: - // Direct entry, no sub dimension - break; - - default: - // The referer type is user submitted for goal conversions, we ignore any malformed value - // Continue to the next while iteration - continue 2; - break; - } - } - if(!isset($this->interestByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] )) $this->interestByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); - $archiveProcessing->updateGoalStats($row, $this->interestByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); - } - - $archiveProcessing->enrichConversionsByLabelArray($this->interestByType); - $archiveProcessing->enrichConversionsByLabelArray($this->interestBySearchEngine); - $archiveProcessing->enrichConversionsByLabelArray($this->interestByKeyword); - $archiveProcessing->enrichConversionsByLabelArray($this->interestByWebsite); - $archiveProcessing->enrichConversionsByLabelArray($this->interestByCampaign); - $archiveProcessing->enrichConversionsByLabelArrayHasTwoLevels($this->interestByCampaignAndKeyword); - } - - /** - * Records the daily stats (numeric or datatable blob) into the archive tables. - * - * @param Piwik_ArchiveProcessing $archiveProcessing - * @return void - */ - protected function archiveDayRecordInDatabase($archiveProcessing) - { - $numericRecords = array( - 'Referers_distinctSearchEngines' => count($this->interestBySearchEngineAndKeyword), - 'Referers_distinctKeywords' => count($this->interestByKeywordAndSearchEngine), - 'Referers_distinctCampaigns' => count($this->interestByCampaign), - 'Referers_distinctWebsites' => count($this->interestByWebsite), - 'Referers_distinctWebsitesUrls' => count($this->distinctUrls), - ); - - foreach($numericRecords as $name => $value) - { - $archiveProcessing->insertNumericRecord($name, $value); - } - - $dataTable = $archiveProcessing->getDataTableSerialized($this->interestByType); - $archiveProcessing->insertBlobRecord('Referers_type', $dataTable); - destroy($dataTable); - - $blobRecords = array( - 'Referers_keywordBySearchEngine' => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->interestBySearchEngineAndKeyword, $this->interestBySearchEngine), - 'Referers_searchEngineByKeyword' => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->interestByKeywordAndSearchEngine, $this->interestByKeyword), - 'Referers_keywordByCampaign' => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->interestByCampaignAndKeyword, $this->interestByCampaign), - 'Referers_urlByWebsite' => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->interestByWebsiteAndUrl, $this->interestByWebsite), - ); - foreach($blobRecords as $recordName => $table ) - { - $blob = $table->getSerialized($this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation); - $archiveProcessing->insertBlobRecord($recordName, $blob); - destroy($table); - } - } + public $archiveProcessing; + protected $columnToSortByBeforeTruncation; + protected $maximumRowsInDataTableLevelZero; + protected $maximumRowsInSubDataTable; + + public function getInformation() + { + $info = array( + 'description' => Piwik_Translate('Referers_PluginDescription'), + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'version' => Piwik_Version::VERSION, + ); + + return $info; + } + + function getListHooksRegistered() + { + $hooks = array( + 'ArchiveProcessing_Day.compute' => 'archiveDay', + 'ArchiveProcessing_Period.compute' => 'archivePeriod', + 'WidgetsList.add' => 'addWidgets', + 'Menu.add' => 'addMenus', + 'Goals.getReportsWithGoalMetrics' => 'getReportsWithGoalMetrics', + 'API.getReportMetadata' => 'getReportMetadata', + 'API.getSegmentsMetadata' => 'getSegmentsMetadata', + ); + return $hooks; + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + public function getReportMetadata($notification) + { + $reports = & $notification->getNotificationObject(); + $reports = array_merge($reports, array( + array( + 'category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_Type'), + 'module' => 'Referers', + 'action' => 'getRefererType', + 'dimension' => Piwik_Translate('Referers_ColumnRefererType'), + 'constantRowsCount' => true, + 'documentation' => Piwik_Translate('Referers_TypeReportDocumentation') . '
' + . '' . Piwik_Translate('Referers_DirectEntry') . ': ' . Piwik_Translate('Referers_DirectEntryDocumentation') . '
' + . '' . Piwik_Translate('Referers_SearchEngines') . ': ' . Piwik_Translate('Referers_SearchEnginesDocumentation', + array('
', '"' . Piwik_Translate('Referers_SubmenuSearchEngines') . '"')) . '
' + . '' . Piwik_Translate('Referers_Websites') . ': ' . Piwik_Translate('Referers_WebsitesDocumentation', + array('
', '"' . Piwik_Translate('Referers_SubmenuWebsites') . '"')) . '
' + . '' . Piwik_Translate('Referers_Campaigns') . ': ' . Piwik_Translate('Referers_CampaignsDocumentation', + array('
', '"' . Piwik_Translate('Referers_SubmenuCampaigns') . '"')), + 'order' => 1, + ), + array( + 'category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_WidgetGetAll'), + 'module' => 'Referers', + 'action' => 'getAll', + 'dimension' => Piwik_Translate('Referers_Referrer'), + 'documentation' => Piwik_Translate('Referers_AllReferersReportDocumentation', '
'), + 'order' => 2, + ), + array( + 'category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_Keywords'), + 'module' => 'Referers', + 'action' => 'getKeywords', + 'actionToLoadSubTables' => 'getSearchEnginesFromKeywordId', + 'dimension' => Piwik_Translate('Referers_ColumnKeyword'), + 'documentation' => Piwik_Translate('Referers_KeywordsReportDocumentation', '
'), + 'order' => 3, + ), + array( // subtable report + 'category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_Keywords'), + 'module' => 'Referers', + 'action' => 'getSearchEnginesFromKeywordId', + 'dimension' => Piwik_Translate('Referers_ColumnSearchEngine'), + 'documentation' => Piwik_Translate('Referers_KeywordsReportDocumentation', '
'), + 'isSubtableReport' => true, + 'order' => 4 + ), + + array( + 'category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_Websites'), + 'module' => 'Referers', + 'action' => 'getWebsites', + 'dimension' => Piwik_Translate('Referers_ColumnWebsite'), + 'documentation' => Piwik_Translate('Referers_WebsitesReportDocumentation', '
'), + 'actionToLoadSubTables' => 'getUrlsFromWebsiteId', + 'order' => 5 + ), + array( // subtable report + 'category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_Websites'), + 'module' => 'Referers', + 'action' => 'getUrlsFromWebsiteId', + 'dimension' => Piwik_Translate('Referers_ColumnWebsitePage'), + 'documentation' => Piwik_Translate('Referers_WebsitesReportDocumentation', '
'), + 'isSubtableReport' => true, + 'order' => 6, + ), + + array( + 'category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_SearchEngines'), + 'module' => 'Referers', + 'action' => 'getSearchEngines', + 'dimension' => Piwik_Translate('Referers_ColumnSearchEngine'), + 'documentation' => Piwik_Translate('Referers_SearchEnginesReportDocumentation', '
'), + 'actionToLoadSubTables' => 'getKeywordsFromSearchEngineId', + 'order' => 7, + ), + array( // subtable report + 'category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_SearchEngines'), + 'module' => 'Referers', + 'action' => 'getKeywordsFromSearchEngineId', + 'dimension' => Piwik_Translate('Referers_ColumnKeyword'), + 'documentation' => Piwik_Translate('Referers_SearchEnginesReportDocumentation', '
'), + 'isSubtableReport' => true, + 'order' => 8, + ), + + array( + 'category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_Campaigns'), + 'module' => 'Referers', + 'action' => 'getCampaigns', + 'dimension' => Piwik_Translate('Referers_ColumnCampaign'), + 'documentation' => Piwik_Translate('Referers_CampaignsReportDocumentation', + array('
', '', '')), + 'actionToLoadSubTables' => 'getKeywordsFromCampaignId', + 'order' => 9, + ), + array( // subtable report + 'category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_Campaigns'), + 'module' => 'Referers', + 'action' => 'getKeywordsFromCampaignId', + 'dimension' => Piwik_Translate('Referers_ColumnKeyword'), + 'documentation' => Piwik_Translate('Referers_CampaignsReportDocumentation', + array('
', '', '')), + 'isSubtableReport' => true, + 'order' => 10, + ), + array( + 'category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_Socials'), + 'module' => 'Referers', + 'action' => 'getSocials', + 'actionToLoadSubTables' => 'getUrlsForSocial', + 'dimension' => Piwik_Translate('Referers_ColumnSocial'), + 'documentation' => Piwik_Translate('Referers_WebsitesReportDocumentation', '
'), + 'order' => 11, + ), + )); + } + + /** + * @param Piwik_Event_Notification $notification notification object + */ + public function getSegmentsMetadata($notification) + { + $segments =& $notification->getNotificationObject(); + $segments[] = array( + 'type' => 'dimension', + 'category' => 'Referers_Referers', + 'name' => 'Referers_ColumnRefererType', + 'segment' => 'referrerType', + 'acceptedValues' => 'direct, search, website, campaign', + 'sqlSegment' => 'log_visit.referer_type', + 'sqlFilter' => 'Piwik_getRefererTypeFromShortName', + ); + $segments[] = array( + 'type' => 'dimension', + 'category' => 'Referers_Referers', + 'name' => 'Referers_ColumnKeyword', + 'segment' => 'referrerKeyword', + 'acceptedValues' => 'Encoded%20Keyword, keyword', + 'sqlSegment' => 'log_visit.referer_keyword', + ); + $segments[] = array( + 'type' => 'dimension', + 'category' => 'Referers_Referers', + 'name' => 'Referers_RefererName', + 'segment' => 'referrerName', + 'acceptedValues' => 'twitter.com, www.facebook.com, Bing, Google, Yahoo, CampaignName', + 'sqlSegment' => 'log_visit.referer_name', + ); + $segments[] = array( + 'type' => 'dimension', + 'category' => 'Referers_Referers', + 'name' => 'Live_Referrer_URL', + 'acceptedValues' => 'http%3A%2F%2Fwww.example.org%2Freferer-page.htm', + 'segment' => 'referrerUrl', + 'sqlSegment' => 'log_visit.referer_url', + ); + } + + /** + * Adds Referer widgets + */ + function addWidgets() + { + Piwik_AddWidget('Referers_Referers', 'Referers_WidgetKeywords', 'Referers', 'getKeywords'); + Piwik_AddWidget('Referers_Referers', 'Referers_WidgetExternalWebsites', 'Referers', 'getWebsites'); + Piwik_AddWidget('Referers_Referers', 'Referers_WidgetSocials', 'Referers', 'getSocials'); + Piwik_AddWidget('Referers_Referers', 'Referers_WidgetSearchEngines', 'Referers', 'getSearchEngines'); + Piwik_AddWidget('Referers_Referers', 'Referers_WidgetCampaigns', 'Referers', 'getCampaigns'); + Piwik_AddWidget('Referers_Referers', 'Referers_WidgetOverview', 'Referers', 'getRefererType'); + Piwik_AddWidget('Referers_Referers', 'Referers_WidgetGetAll', 'Referers', 'getAll'); + if (Piwik_Archive::isSegmentationEnabled()) { + Piwik_AddWidget('SEO', 'Referers_WidgetTopKeywordsForPages', 'Referers', 'getKeywordsForPage'); + } + } + + /** + * Adds Web Analytics menus + */ + function addMenus() + { + Piwik_AddMenu('Referers_Referers', '', array('module' => 'Referers', 'action' => 'index'), true, 20); + Piwik_AddMenu('Referers_Referers', 'Referers_SubmenuOverview', array('module' => 'Referers', 'action' => 'index'), true, 1); + Piwik_AddMenu('Referers_Referers', 'Referers_SubmenuSearchEngines', array('module' => 'Referers', 'action' => 'getSearchEnginesAndKeywords'), true, 2); + Piwik_AddMenu('Referers_Referers', 'Referers_SubmenuWebsites', array('module' => 'Referers', 'action' => 'indexWebsites'), true, 3); + Piwik_AddMenu('Referers_Referers', 'Referers_SubmenuCampaigns', array('module' => 'Referers', 'action' => 'indexCampaigns'), true, 4); + } + + /** + * Adds Goal dimensions, so that the dimensions are displayed in the UI Goal Overview page + * + * @param Piwik_Event_Notification $notification notification object + * @return void + */ + function getReportsWithGoalMetrics($notification) + { + $dimensions =& $notification->getNotificationObject(); + $dimensions = array_merge($dimensions, array( + array('category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_Keywords'), + 'module' => 'Referers', + 'action' => 'getKeywords', + ), + array('category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_SearchEngines'), + 'module' => 'Referers', + 'action' => 'getSearchEngines', + ), + array('category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_Websites'), + 'module' => 'Referers', + 'action' => 'getWebsites', + ), + array('category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_Campaigns'), + 'module' => 'Referers', + 'action' => 'getCampaigns', + ), + array('category' => Piwik_Translate('Referers_Referers'), + 'name' => Piwik_Translate('Referers_Type'), + 'module' => 'Referers', + 'action' => 'getRefererType', + ), + )); + } + + function __construct() + { + $this->columnToSortByBeforeTruncation = Piwik_Archive::INDEX_NB_VISITS; + $this->maximumRowsInDataTableLevelZero = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_referers']; + $this->maximumRowsInSubDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_subtable_referers']; + } + + /** + * Period archiving: sums up daily stats and sums report tables, + * making sure that tables are still truncated. + * + * @param Piwik_Event_Notification $notification notification object + * @return void + */ + function archivePeriod($notification) + { + $archiveProcessing = $notification->getNotificationObject(); + + if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; + + $dataTableToSum = array( + 'Referers_type', + 'Referers_keywordBySearchEngine', + 'Referers_searchEngineByKeyword', + 'Referers_keywordByCampaign', + 'Referers_urlByWebsite', + ); + $nameToCount = $archiveProcessing->archiveDataTable($dataTableToSum, null, $this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation); + + $mappingFromArchiveName = array( + 'Referers_distinctSearchEngines' => + array('typeCountToUse' => 'level0', + 'nameTableToUse' => 'Referers_keywordBySearchEngine', + ), + 'Referers_distinctKeywords' => + array('typeCountToUse' => 'level0', + 'nameTableToUse' => 'Referers_searchEngineByKeyword', + ), + 'Referers_distinctCampaigns' => + array('typeCountToUse' => 'level0', + 'nameTableToUse' => 'Referers_keywordByCampaign', + ), + 'Referers_distinctWebsites' => + array('typeCountToUse' => 'level0', + 'nameTableToUse' => 'Referers_urlByWebsite', + ), + 'Referers_distinctWebsitesUrls' => + array('typeCountToUse' => 'recursive', + 'nameTableToUse' => 'Referers_urlByWebsite', + ), + ); + + foreach ($mappingFromArchiveName as $name => $infoMapping) { + $typeCountToUse = $infoMapping['typeCountToUse']; + $nameTableToUse = $infoMapping['nameTableToUse']; + + if ($typeCountToUse == 'recursive') { + + $countValue = $nameToCount[$nameTableToUse]['recursive'] + - $nameToCount[$nameTableToUse]['level0']; + } else { + $countValue = $nameToCount[$nameTableToUse]['level0']; + } + $archiveProcessing->insertNumericRecord($name, $countValue); + } + } + + const LABEL_KEYWORD_NOT_DEFINED = ""; + + static public function getKeywordNotDefinedString() + { + return Piwik_Translate('General_NotDefined', Piwik_Translate('Referers_ColumnKeyword')); + } + + static public function getCleanKeyword($label) + { + return $label == Piwik_Referers::LABEL_KEYWORD_NOT_DEFINED + ? self::getKeywordNotDefinedString() + : $label; + } + + /** + * Hooks on daily archive to trigger various log processing + * + * @param Piwik_Event_Notification $notification notification object + * @return void + */ + public function archiveDay($notification) + { + /** + * @var Piwik_ArchiveProcessing_Day + */ + $this->archiveProcessing = $notification->getNotificationObject(); + if (!$this->archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; + + $this->archiveDayAggregateVisits($this->archiveProcessing); + $this->archiveDayAggregateGoals($this->archiveProcessing); + Piwik_PostEvent('Referers.archiveDay', $this); + $this->archiveDayRecordInDatabase($this->archiveProcessing); + $this->cleanup(); + } + + protected function cleanup() + { + destroy($this->interestBySearchEngine); + destroy($this->interestByKeyword); + destroy($this->interestBySearchEngineAndKeyword); + destroy($this->interestByKeywordAndSearchEngine); + destroy($this->interestByWebsite); + destroy($this->interestByWebsiteAndUrl); + destroy($this->interestByCampaignAndKeyword); + destroy($this->interestByCampaign); + destroy($this->interestByType); + destroy($this->distinctUrls); + } + + /** + * Daily archive: processes all Referers reports, eg. Visits by Keyword, + * Visits by websites, etc. + * + * @param Piwik_ArchiveProcessing $archiveProcessing + * @throws Exception + * @return void + */ + protected function archiveDayAggregateVisits(Piwik_ArchiveProcessing_Day $archiveProcessing) + { + $dimension = array("referer_type", "referer_name", "referer_keyword", "referer_url"); + $query = $archiveProcessing->queryVisitsByDimension($dimension); + + $this->interestBySearchEngine = + $this->interestByKeyword = + $this->interestBySearchEngineAndKeyword = + $this->interestByKeywordAndSearchEngine = + $this->interestByWebsite = + $this->interestByWebsiteAndUrl = + $this->interestByCampaignAndKeyword = + $this->interestByCampaign = + $this->interestByType = + $this->distinctUrls = array(); + while ($row = $query->fetch()) { + if (empty($row['referer_type'])) { + $row['referer_type'] = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY; + } else { + switch ($row['referer_type']) { + case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: + if (empty($row['referer_keyword'])) { + $row['referer_keyword'] = self::LABEL_KEYWORD_NOT_DEFINED; + } + if (!isset($this->interestBySearchEngine[$row['referer_name']])) $this->interestBySearchEngine[$row['referer_name']] = $archiveProcessing->getNewInterestRow(); + if (!isset($this->interestByKeyword[$row['referer_keyword']])) $this->interestByKeyword[$row['referer_keyword']] = $archiveProcessing->getNewInterestRow(); + if (!isset($this->interestBySearchEngineAndKeyword[$row['referer_name']][$row['referer_keyword']])) $this->interestBySearchEngineAndKeyword[$row['referer_name']][$row['referer_keyword']] = $archiveProcessing->getNewInterestRow(); + if (!isset($this->interestByKeywordAndSearchEngine[$row['referer_keyword']][$row['referer_name']])) $this->interestByKeywordAndSearchEngine[$row['referer_keyword']][$row['referer_name']] = $archiveProcessing->getNewInterestRow(); + + $archiveProcessing->updateInterestStats($row, $this->interestBySearchEngine[$row['referer_name']]); + $archiveProcessing->updateInterestStats($row, $this->interestByKeyword[$row['referer_keyword']]); + $archiveProcessing->updateInterestStats($row, $this->interestBySearchEngineAndKeyword[$row['referer_name']][$row['referer_keyword']]); + $archiveProcessing->updateInterestStats($row, $this->interestByKeywordAndSearchEngine[$row['referer_keyword']][$row['referer_name']]); + break; + + case Piwik_Common::REFERER_TYPE_WEBSITE: + + if (!isset($this->interestByWebsite[$row['referer_name']])) $this->interestByWebsite[$row['referer_name']] = $archiveProcessing->getNewInterestRow(); + $archiveProcessing->updateInterestStats($row, $this->interestByWebsite[$row['referer_name']]); + + if (!isset($this->interestByWebsiteAndUrl[$row['referer_name']][$row['referer_url']])) $this->interestByWebsiteAndUrl[$row['referer_name']][$row['referer_url']] = $archiveProcessing->getNewInterestRow(); + $archiveProcessing->updateInterestStats($row, $this->interestByWebsiteAndUrl[$row['referer_name']][$row['referer_url']]); + + if (!isset($this->distinctUrls[$row['referer_url']])) { + $this->distinctUrls[$row['referer_url']] = true; + } + break; + + case Piwik_Common::REFERER_TYPE_CAMPAIGN: + if (!empty($row['referer_keyword'])) { + if (!isset($this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']])) $this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']] = $archiveProcessing->getNewInterestRow(); + $archiveProcessing->updateInterestStats($row, $this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']]); + } + if (!isset($this->interestByCampaign[$row['referer_name']])) $this->interestByCampaign[$row['referer_name']] = $archiveProcessing->getNewInterestRow(); + $archiveProcessing->updateInterestStats($row, $this->interestByCampaign[$row['referer_name']]); + break; + + case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY: + // direct entry are aggregated below in $this->interestByType array + break; + + default: + throw new Exception("Non expected referer_type = " . $row['referer_type']); + break; + } + } + if (!isset($this->interestByType[$row['referer_type']])) $this->interestByType[$row['referer_type']] = $archiveProcessing->getNewInterestRow(); + $archiveProcessing->updateInterestStats($row, $this->interestByType[$row['referer_type']]); + } + } + + /** + * Daily Goal archiving: processes reports of Goal conversions by Keyword, + * Goal conversions by Referer Websites, etc. + * + * @param Piwik_ArchiveProcessing $archiveProcessing + * @return void + */ + protected function archiveDayAggregateGoals($archiveProcessing) + { + $query = $archiveProcessing->queryConversionsByDimension(array("referer_type", "referer_name", "referer_keyword")); + + if ($query === false) return; + while ($row = $query->fetch()) { + if (empty($row['referer_type'])) { + $row['referer_type'] = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY; + } else { + switch ($row['referer_type']) { + case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: + if (empty($row['referer_keyword'])) { + $row['referer_keyword'] = self::LABEL_KEYWORD_NOT_DEFINED; + } + if (!isset($this->interestBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); + if (!isset($this->interestByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); + + $archiveProcessing->updateGoalStats($row, $this->interestBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); + $archiveProcessing->updateGoalStats($row, $this->interestByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); + break; + + case Piwik_Common::REFERER_TYPE_WEBSITE: + if (!isset($this->interestByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); + $archiveProcessing->updateGoalStats($row, $this->interestByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); + break; + + case Piwik_Common::REFERER_TYPE_CAMPAIGN: + if (!empty($row['referer_keyword'])) { + if (!isset($this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); + $archiveProcessing->updateGoalStats($row, $this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); + } + if (!isset($this->interestByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); + $archiveProcessing->updateGoalStats($row, $this->interestByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); + break; + + case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY: + // Direct entry, no sub dimension + break; + + default: + // The referer type is user submitted for goal conversions, we ignore any malformed value + // Continue to the next while iteration + continue 2; + break; + } + } + if (!isset($this->interestByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); + $archiveProcessing->updateGoalStats($row, $this->interestByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); + } + + $archiveProcessing->enrichConversionsByLabelArray($this->interestByType); + $archiveProcessing->enrichConversionsByLabelArray($this->interestBySearchEngine); + $archiveProcessing->enrichConversionsByLabelArray($this->interestByKeyword); + $archiveProcessing->enrichConversionsByLabelArray($this->interestByWebsite); + $archiveProcessing->enrichConversionsByLabelArray($this->interestByCampaign); + $archiveProcessing->enrichConversionsByLabelArrayHasTwoLevels($this->interestByCampaignAndKeyword); + } + + /** + * Records the daily stats (numeric or datatable blob) into the archive tables. + * + * @param Piwik_ArchiveProcessing $archiveProcessing + * @return void + */ + protected function archiveDayRecordInDatabase($archiveProcessing) + { + $numericRecords = array( + 'Referers_distinctSearchEngines' => count($this->interestBySearchEngineAndKeyword), + 'Referers_distinctKeywords' => count($this->interestByKeywordAndSearchEngine), + 'Referers_distinctCampaigns' => count($this->interestByCampaign), + 'Referers_distinctWebsites' => count($this->interestByWebsite), + 'Referers_distinctWebsitesUrls' => count($this->distinctUrls), + ); + + foreach ($numericRecords as $name => $value) { + $archiveProcessing->insertNumericRecord($name, $value); + } + + $dataTable = $archiveProcessing->getDataTableSerialized($this->interestByType); + $archiveProcessing->insertBlobRecord('Referers_type', $dataTable); + destroy($dataTable); + + $blobRecords = array( + 'Referers_keywordBySearchEngine' => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->interestBySearchEngineAndKeyword, $this->interestBySearchEngine), + 'Referers_searchEngineByKeyword' => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->interestByKeywordAndSearchEngine, $this->interestByKeyword), + 'Referers_keywordByCampaign' => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->interestByCampaignAndKeyword, $this->interestByCampaign), + 'Referers_urlByWebsite' => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->interestByWebsiteAndUrl, $this->interestByWebsite), + ); + foreach ($blobRecords as $recordName => $table) { + $blob = $table->getSerialized($this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation); + $archiveProcessing->insertBlobRecord($recordName, $blob); + destroy($table); + } + } } diff --git a/plugins/Referers/functions.php b/plugins/Referers/functions.php index afa9fe4c3f..28faab9885 100644 --- a/plugins/Referers/functions.php +++ b/plugins/Referers/functions.php @@ -1,10 +1,10 @@ $name) - { - if ($name == $GLOBALS['Piwik_socialUrl'][$domain]) - { - $firstDomain = $domainKey; - break; - } - } - - $pathWithCode = 'plugins/Referers/images/socials/'.$firstDomain.'.png'; - return $pathWithCode; - } - else - { - return 'plugins/Referers/images/socials/xx.png'; - } + if (isset($GLOBALS['Piwik_socialUrl'][$domain])) { + // image names are by first domain in list, so make sure we use the first if $domain isn't it + $firstDomain = $domain; + foreach ($GLOBALS['Piwik_socialUrl'] as $domainKey => $name) { + if ($name == $GLOBALS['Piwik_socialUrl'][$domain]) { + $firstDomain = $domainKey; + break; + } + } + + $pathWithCode = 'plugins/Referers/images/socials/' . $firstDomain . '.png'; + return $pathWithCode; + } else { + return 'plugins/Referers/images/socials/xx.png'; + } } /** @@ -120,16 +111,13 @@ function Piwik_getSocialsLogoFromUrl($domain) */ function Piwik_getSearchEngineUrlFromName($name) { - $searchEngineNames = Piwik_Common::getSearchEngineNames(); - if(isset($searchEngineNames[$name])) - { - $url = 'http://'.$searchEngineNames[$name]; - } - else - { - $url = 'URL unknown!'; - } - return $url; + $searchEngineNames = Piwik_Common::getSearchEngineNames(); + if (isset($searchEngineNames[$name])) { + $url = 'http://' . $searchEngineNames[$name]; + } else { + $url = 'URL unknown!'; + } + return $url; } /** @@ -140,12 +128,11 @@ function Piwik_getSearchEngineUrlFromName($name) */ function Piwik_getSearchEngineHostFromUrl($url) { - $url = substr($url, strpos($url,'//') + 2); - if(($p = strpos($url, '/')) !== false) - { - $url = substr($url, 0, $p); - } - return $url; + $url = substr($url, strpos($url, '//') + 2); + if (($p = strpos($url, '/')) !== false) { + $url = substr($url, 0, $p); + } + return $url; } /** @@ -157,14 +144,13 @@ function Piwik_getSearchEngineHostFromUrl($url) */ function Piwik_getSearchEngineLogoFromUrl($url) { - $pathInPiwik = 'plugins/Referers/images/searchEngines/%s.png'; - $pathWithCode = sprintf($pathInPiwik, Piwik_getSearchEngineHostFromUrl($url)); - $absolutePath = PIWIK_INCLUDE_PATH . '/' . $pathWithCode; - if(file_exists($absolutePath)) - { - return $pathWithCode; - } - return sprintf($pathInPiwik, 'xx'); + $pathInPiwik = 'plugins/Referers/images/searchEngines/%s.png'; + $pathWithCode = sprintf($pathInPiwik, Piwik_getSearchEngineHostFromUrl($url)); + $absolutePath = PIWIK_INCLUDE_PATH . '/' . $pathWithCode; + if (file_exists($absolutePath)) { + return $pathWithCode; + } + return sprintf($pathInPiwik, 'xx'); } /** @@ -175,8 +161,8 @@ function Piwik_getSearchEngineLogoFromUrl($url) */ function Piwik_getSearchEngineHostPathFromUrl($url) { - $url = substr($url, strpos($url,'//') + 2); - return $url; + $url = substr($url, strpos($url, '//') + 2); + return $url; } /** @@ -190,20 +176,18 @@ function Piwik_getSearchEngineHostPathFromUrl($url) */ function Piwik_getSearchEngineUrlFromUrlAndKeyword($url, $keyword) { - if($keyword === Piwik_Referers::LABEL_KEYWORD_NOT_DEFINED) - { - return 'http://piwik.org/faq/general/#faq_144'; - } - $searchEngineUrls = Piwik_Common::getSearchEngineUrls(); - $keyword = urlencode($keyword); - $keyword = str_replace(urlencode('+'), urlencode(' '), $keyword); - $path = @$searchEngineUrls[Piwik_getSearchEngineHostPathFromUrl($url)][2]; - if(empty($path)) - { - return false; - } - $path = str_replace("{k}", $keyword, $path); - return $url . (substr($url, -1) != '/' ? '/' : '') . $path; + if ($keyword === Piwik_Referers::LABEL_KEYWORD_NOT_DEFINED) { + return 'http://piwik.org/faq/general/#faq_144'; + } + $searchEngineUrls = Piwik_Common::getSearchEngineUrls(); + $keyword = urlencode($keyword); + $keyword = str_replace(urlencode('+'), urlencode(' '), $keyword); + $path = @$searchEngineUrls[Piwik_getSearchEngineHostPathFromUrl($url)][2]; + if (empty($path)) { + return false; + } + $path = str_replace("{k}", $keyword, $path); + return $url . (substr($url, -1) != '/' ? '/' : '') . $path; } /** @@ -217,7 +201,7 @@ function Piwik_getSearchEngineUrlFromUrlAndKeyword($url, $keyword) */ function Piwik_getSearchEngineUrlFromKeywordAndUrl($keyword, $url) { - return Piwik_getSearchEngineUrlFromUrlAndKeyword($url, $keyword); + return Piwik_getSearchEngineUrlFromUrlAndKeyword($url, $keyword); } /** @@ -228,27 +212,26 @@ function Piwik_getSearchEngineUrlFromKeywordAndUrl($keyword, $url) */ function Piwik_getRefererTypeLabel($label) { - $indexTranslation = ''; - switch($label) - { - case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY: - $indexTranslation = 'Referers_DirectEntry'; - break; - case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: - $indexTranslation = 'Referers_SearchEngines'; - break; - case Piwik_Common::REFERER_TYPE_WEBSITE: - $indexTranslation = 'Referers_Websites'; - break; - case Piwik_Common::REFERER_TYPE_CAMPAIGN: - $indexTranslation = 'Referers_Campaigns'; - break; - default: - // case of newsletter, partners, before Piwik 0.2.25 - $indexTranslation = 'General_Others'; - break; - } - return Piwik_Translate($indexTranslation); + $indexTranslation = ''; + switch ($label) { + case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY: + $indexTranslation = 'Referers_DirectEntry'; + break; + case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: + $indexTranslation = 'Referers_SearchEngines'; + break; + case Piwik_Common::REFERER_TYPE_WEBSITE: + $indexTranslation = 'Referers_Websites'; + break; + case Piwik_Common::REFERER_TYPE_CAMPAIGN: + $indexTranslation = 'Referers_Campaigns'; + break; + default: + // case of newsletter, partners, before Piwik 0.2.25 + $indexTranslation = 'General_Others'; + break; + } + return Piwik_Translate($indexTranslation); } /** @@ -259,34 +242,31 @@ function Piwik_getRefererTypeLabel($label) */ function Piwik_getRefererTypeFromShortName($name) { - $map = array( - Piwik_Common::REFERER_TYPE_SEARCH_ENGINE => 'search', - Piwik_Common::REFERER_TYPE_WEBSITE => 'website', - Piwik_Common::REFERER_TYPE_DIRECT_ENTRY => 'direct', - Piwik_Common::REFERER_TYPE_CAMPAIGN => 'campaign', - ); - if(isset($map[$name])) - { - return $map[$name]; - } - if($found = array_search($name, $map)) - { - return $found; - } - throw new Exception("Referrer type '$name' is not valid."); + $map = array( + Piwik_Common::REFERER_TYPE_SEARCH_ENGINE => 'search', + Piwik_Common::REFERER_TYPE_WEBSITE => 'website', + Piwik_Common::REFERER_TYPE_DIRECT_ENTRY => 'direct', + Piwik_Common::REFERER_TYPE_CAMPAIGN => 'campaign', + ); + if (isset($map[$name])) { + return $map[$name]; + } + if ($found = array_search($name, $map)) { + return $found; + } + throw new Exception("Referrer type '$name' is not valid."); } /** * Returns a URL w/o the protocol type. - * + * * @param string $url * @return string */ -function Piwik_Referrers_removeUrlProtocol( $url ) +function Piwik_Referrers_removeUrlProtocol($url) { - if (preg_match('/^[a-zA-Z_-]+:\/\//', $url, $matches)) - { - return substr($url, strlen($matches[0])); - } - return $url; + if (preg_match('/^[a-zA-Z_-]+:\/\//', $url, $matches)) { + return substr($url, strlen($matches[0])); + } + return $url; } diff --git a/plugins/Referers/templates/Websites_SocialNetworks.tpl b/plugins/Referers/templates/Websites_SocialNetworks.tpl index 6e964fff26..496bdb085c 100755 --- a/plugins/Referers/templates/Websites_SocialNetworks.tpl +++ b/plugins/Referers/templates/Websites_SocialNetworks.tpl @@ -1,9 +1,9 @@
-

{'Referers_Websites'|translate}

- {$websites} +

{'Referers_Websites'|translate}

+ {$websites}
-

{'Referers_Socials'|translate}

- {$socials} +

{'Referers_Socials'|translate}

+ {$socials}
diff --git a/plugins/Referers/templates/index.tpl b/plugins/Referers/templates/index.tpl index 521209b0ea..a3ac028c86 100644 --- a/plugins/Referers/templates/index.tpl +++ b/plugins/Referers/templates/index.tpl @@ -2,73 +2,87 @@

{'Referers_Evolution'|translate}

{$graphEvolutionReferers} -
+
-

{'Referers_Type'|translate}

-
-
{sparkline src=$urlSparklineDirectEntry} - {'Referers_TypeDirectEntries'|translate:"$visitorsFromDirectEntry"}{if !empty($visitorsFromDirectEntryPercent)}, {$visitorsFromDirectEntryPercent}% of visits{/if}{if !empty($visitorsFromDirectEntryEvolution)} {$visitorsFromDirectEntryEvolution}{/if} -
-
{sparkline src=$urlSparklineSearchEngines} - {'Referers_TypeSearchEngines'|translate:"$visitorsFromSearchEngines"}{if !empty($visitorsFromSearchEnginesPercent)}, {$visitorsFromSearchEnginesPercent}% of visits{/if}{if !empty($visitorsFromSearchEnginesEvolution)} {$visitorsFromSearchEnginesEvolution}{/if} -
-
-
-
{sparkline src=$urlSparklineWebsites} - {'Referers_TypeWebsites'|translate:"$visitorsFromWebsites"}{if !empty($visitorsFromWebsitesPercent)}, {$visitorsFromWebsitesPercent}% of visits{/if}{if !empty($visitorsFromWebsitesEvolution)} {$visitorsFromWebsitesEvolution}{/if} -
-
{sparkline src=$urlSparklineCampaigns} - {'Referers_TypeCampaigns'|translate:"$visitorsFromCampaigns"}{if !empty($visitorsFromCampaignsPercent)}, {$visitorsFromCampaignsPercent}% of visits{/if}{if !empty($visitorsFromCampaignsEvolution)} {$visitorsFromCampaignsEvolution}{/if} -
-
- -
- -
-
-

{'General_MoreDetails'|translate} ({'General_Show_js'|translate})

-
+

{'Referers_Type'|translate}

- - -

-

+
+
{sparkline src=$urlSparklineDirectEntry} + {'Referers_TypeDirectEntries'|translate:"$visitorsFromDirectEntry"}{if !empty($visitorsFromDirectEntryPercent)}, + {$visitorsFromDirectEntryPercent}% + of visits{/if}{if !empty($visitorsFromDirectEntryEvolution)} {$visitorsFromDirectEntryEvolution}{/if} +
+
{sparkline src=$urlSparklineSearchEngines} + {'Referers_TypeSearchEngines'|translate:"$visitorsFromSearchEngines"}{if !empty($visitorsFromSearchEnginesPercent)}, + {$visitorsFromSearchEnginesPercent}% + of visits{/if}{if !empty($visitorsFromSearchEnginesEvolution)} {$visitorsFromSearchEnginesEvolution}{/if} +
+
+
+
{sparkline src=$urlSparklineWebsites} + {'Referers_TypeWebsites'|translate:"$visitorsFromWebsites"}{if !empty($visitorsFromWebsitesPercent)}, + {$visitorsFromWebsitesPercent}% + of visits{/if}{if !empty($visitorsFromWebsitesEvolution)} {$visitorsFromWebsitesEvolution}{/if} +
+
{sparkline src=$urlSparklineCampaigns} + {'Referers_TypeCampaigns'|translate:"$visitorsFromCampaigns"}{if !empty($visitorsFromCampaignsPercent)}, + {$visitorsFromCampaignsPercent}% + of visits{/if}{if !empty($visitorsFromCampaignsEvolution)} {$visitorsFromCampaignsEvolution}{/if} +
+
+ +
+ +
+
+ +

{'General_MoreDetails'|translate} ({'General_Show_js'|translate})

+
+ + + +

+ +

-

{'Referers_DetailsByRefererType'|translate}

- {$dataTableRefererType} +

{'Referers_DetailsByRefererType'|translate}

+ {$dataTableRefererType}
{if $totalVisits > 0} -

{'Referers_ReferrersOverview'|translate}

-{$referrersReportsByDimension} +

{'Referers_ReferrersOverview'|translate}

+ {$referrersReportsByDimension} {/if} {include file="CoreHome/templates/sparkline_footer.tpl"} diff --git a/plugins/Referers/templates/searchEngines_Keywords.tpl b/plugins/Referers/templates/searchEngines_Keywords.tpl index ededce4848..4d685f681c 100644 --- a/plugins/Referers/templates/searchEngines_Keywords.tpl +++ b/plugins/Referers/templates/searchEngines_Keywords.tpl @@ -1,9 +1,9 @@
-

{'Referers_Keywords'|translate}

- {$keywords} +

{'Referers_Keywords'|translate}

+ {$keywords}
-

{'Referers_SearchEngines'|translate}

- {$searchEngines} +

{'Referers_SearchEngines'|translate}

+ {$searchEngines}
diff --git a/plugins/SEO/API.php b/plugins/SEO/API.php index 62613f3de5..26ac00c5bd 100644 --- a/plugins/SEO/API.php +++ b/plugins/SEO/API.php @@ -17,92 +17,91 @@ require_once PIWIK_INCLUDE_PATH . '/plugins/Referers/functions.php'; /** * The SEO API lets you access a list of SEO metrics for the specified URL: Google Pagerank, Goolge/Bing indexed pages * Alexa Rank, age of the Domain name and count of DMOZ entries. - * + * * @package Piwik_SEO */ -class Piwik_SEO_API +class Piwik_SEO_API { - static private $instance = null; - /** - * @return Piwik_SEO_API - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - /** - * Returns SEO statistics for a URL. - * - * @param string $url URL to request SEO stats for - * @return Piwik_DataTable - */ - public function getRank( $url ) - { - Piwik::checkUserHasSomeViewAccess(); - $rank = new Piwik_SEO_RankChecker($url); - - $linkToMajestic = Piwik_SEO_MajesticClient::getLinkForUrl($url); - - $data = array( - 'Google PageRank' => array( - 'rank' => $rank->getPageRank(), - 'logo' => Piwik_getSearchEngineLogoFromUrl('http://google.com'), - 'id' => 'pagerank' - ), + static private $instance = null; + + /** + * @return Piwik_SEO_API + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + /** + * Returns SEO statistics for a URL. + * + * @param string $url URL to request SEO stats for + * @return Piwik_DataTable + */ + public function getRank($url) + { + Piwik::checkUserHasSomeViewAccess(); + $rank = new Piwik_SEO_RankChecker($url); + + $linkToMajestic = Piwik_SEO_MajesticClient::getLinkForUrl($url); + + $data = array( + 'Google PageRank' => array( + 'rank' => $rank->getPageRank(), + 'logo' => Piwik_getSearchEngineLogoFromUrl('http://google.com'), + 'id' => 'pagerank' + ), Piwik_Translate('SEO_Google_IndexedPages') => array( 'rank' => $rank->getIndexedPagesGoogle(), 'logo' => Piwik_getSearchEngineLogoFromUrl('http://google.com'), - 'id' => 'google-index', + 'id' => 'google-index', ), - Piwik_Translate('SEO_Bing_IndexedPages') => array( + Piwik_Translate('SEO_Bing_IndexedPages') => array( 'rank' => $rank->getIndexedPagesBing(), 'logo' => Piwik_getSearchEngineLogoFromUrl('http://bing.com'), - 'id' => 'bing-index', - ), - Piwik_Translate('SEO_AlexaRank') => array( - 'rank' => $rank->getAlexaRank(), - 'logo' => Piwik_getSearchEngineLogoFromUrl('http://alexa.com'), - 'id' => 'alexa', - ), - Piwik_Translate('SEO_DomainAge') => array( - 'rank' => $rank->getAge(), - 'logo' => 'plugins/SEO/images/whois.png', - 'id' => 'domain-age', - ), - Piwik_Translate('SEO_ExternalBacklinks') => array( - 'rank' => $rank->getExternalBacklinkCount(), - 'logo' => 'plugins/SEO/images/majesticseo.png', - 'logo_link' => $linkToMajestic, - 'logo_tooltip' => Piwik_Translate('SEO_ViewBacklinksOnMajesticSEO'), - 'id' => 'external-backlinks', - ), - Piwik_Translate('SEO_ReferrerDomains') => array( - 'rank' => $rank->getReferrerDomainCount(), - 'logo' => 'plugins/SEO/images/majesticseo.png', - 'logo_link' => $linkToMajestic, - 'logo_tooltip' => Piwik_Translate('SEO_ViewBacklinksOnMajesticSEO'), - 'id' => 'referrer-domains', - ), - ); + 'id' => 'bing-index', + ), + Piwik_Translate('SEO_AlexaRank') => array( + 'rank' => $rank->getAlexaRank(), + 'logo' => Piwik_getSearchEngineLogoFromUrl('http://alexa.com'), + 'id' => 'alexa', + ), + Piwik_Translate('SEO_DomainAge') => array( + 'rank' => $rank->getAge(), + 'logo' => 'plugins/SEO/images/whois.png', + 'id' => 'domain-age', + ), + Piwik_Translate('SEO_ExternalBacklinks') => array( + 'rank' => $rank->getExternalBacklinkCount(), + 'logo' => 'plugins/SEO/images/majesticseo.png', + 'logo_link' => $linkToMajestic, + 'logo_tooltip' => Piwik_Translate('SEO_ViewBacklinksOnMajesticSEO'), + 'id' => 'external-backlinks', + ), + Piwik_Translate('SEO_ReferrerDomains') => array( + 'rank' => $rank->getReferrerDomainCount(), + 'logo' => 'plugins/SEO/images/majesticseo.png', + 'logo_link' => $linkToMajestic, + 'logo_tooltip' => Piwik_Translate('SEO_ViewBacklinksOnMajesticSEO'), + 'id' => 'referrer-domains', + ), + ); - // Add DMOZ only if > 0 entries found - $dmozRank = array( - 'rank' => $rank->getDmoz(), - 'logo' => Piwik_getSearchEngineLogoFromUrl('http://dmoz.org'), - 'id' => 'dmoz', - ); - if($dmozRank['rank'] > 0) - { - $data[Piwik_Translate('SEO_Dmoz')] = $dmozRank; - } + // Add DMOZ only if > 0 entries found + $dmozRank = array( + 'rank' => $rank->getDmoz(), + 'logo' => Piwik_getSearchEngineLogoFromUrl('http://dmoz.org'), + 'id' => 'dmoz', + ); + if ($dmozRank['rank'] > 0) { + $data[Piwik_Translate('SEO_Dmoz')] = $dmozRank; + } - $dataTable = new Piwik_DataTable(); - $dataTable->addRowsFromArrayWithIndexLabel($data); - return $dataTable; - } + $dataTable = new Piwik_DataTable(); + $dataTable->addRowsFromArrayWithIndexLabel($data); + return $dataTable; + } } diff --git a/plugins/SEO/Controller.php b/plugins/SEO/Controller.php index 53a749e2d3..f0e75f8200 100644 --- a/plugins/SEO/Controller.php +++ b/plugins/SEO/Controller.php @@ -1,43 +1,42 @@ getMainUrl(); + } + + $dataTable = Piwik_SEO_API::getInstance()->getRank($url); - $url = urldecode(Piwik_Common::getRequestVar('url', '', 'string')); - - if(!empty($url) && strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) { - $url = 'http://'.$url; - } - - if(empty($url) || !Piwik_Common::isLookLikeUrl($url)) - { - $url = $site->getMainUrl(); - } + $view = Piwik_View::factory('index'); + $view->urlToRank = Piwik_SEO_RankChecker::extractDomainFromUrl($url); - $dataTable = Piwik_SEO_API::getInstance()->getRank($url); - - $view = Piwik_View::factory('index'); - $view->urlToRank = Piwik_SEO_RankChecker::extractDomainFromUrl($url); - - $renderer = Piwik_DataTable_Renderer::factory('php'); - $renderer->setSerialize(false); - $view->ranks = $renderer->render($dataTable); - echo $view->render(); - } + $renderer = Piwik_DataTable_Renderer::factory('php'); + $renderer->setSerialize(false); + $view->ranks = $renderer->render($dataTable); + echo $view->render(); + } } diff --git a/plugins/SEO/MajesticClient.php b/plugins/SEO/MajesticClient.php index 61ab4d2d29..f6678be192 100644 --- a/plugins/SEO/MajesticClient.php +++ b/plugins/SEO/MajesticClient.php @@ -11,85 +11,85 @@ /** * Client for Majestic SEO's HTTP API. - * + * * Hides the HTTP request sending logic. */ class Piwik_SEO_MajesticClient { - const API_BASE = 'http://simpleapi.majesticseo.com/sapi/'; - const API_KEY = 'ETHPYY'; // please only use this key within Piwik - - /** - * Returns a URL that can be used to view all SEO data for a particular website. - * - * @param string $targetSiteUrl The URL of the website for whom SEO stats should be - * accessible for. - * @return string - */ - public static function getLinkForUrl( $targetSiteUrl ) - { - $domain = @parse_url($targetSiteUrl, PHP_URL_HOST); - return "http://www.majesticseo.com/reports/site-explorer/summary/$domain?IndexDataSource=F"; - } - - /** - * Returns backlink statistics including the count of backlinks and count of - * referrer domains (domains with backlinks). - * - * This method issues an HTTP request and waits for it to return. - * - * @param string $siteDomain The domain of the website to get stats for. - * @param int $timeout The number of seconds to wait before aborting - * the HTTP request. - * @return array An array containing the backlink count and referrer - * domain count: - * array( - * 'backlink_count' => X, - * 'referrer_domains_count' => Y - * ) - * If either stat is false, either the API returned an - * error, or the IP was blocked for this request. - */ - public function getBacklinkStats( $siteDomain, $timeout = 300 ) - { - $apiUrl = $this->getApiUrl($method = 'GetBacklinkStats', $args = array( - 'items' => '1', - 'item0' => $siteDomain - )); - $apiResponse = Piwik_Http::sendHttpRequest($apiUrl, $timeout); - - $result = array( - 'backlink_count' => false, - 'referrer_domains_count' => false - ); - - $apiResponse = Piwik_Common::json_decode($apiResponse, $assoc = true); - if (!empty($apiResponse) - && !empty($apiResponse['Data'])) - { - $siteSeoStats = reset($apiResponse['Data']); - - if (isset($siteSeoStats['ExtBackLinks']) - && $siteSeoStats['ExtBackLinks'] !== -1) - { - $result['backlink_count'] = $siteSeoStats['ExtBackLinks']; - } - - if (isset($siteSeoStats['RefDomains']) - && $siteSeoStats['RefDomains'] !== -1) - { - $result['referrer_domains_count'] = $siteSeoStats['RefDomains']; - } - } - - return $result; - } - - private function getApiUrl( $method, $args = array() ) - { - $args['sak'] = self::API_KEY; - - $queryString = http_build_query($args); - return self::API_BASE.$method.'?'.$queryString; - } + const API_BASE = 'http://simpleapi.majesticseo.com/sapi/'; + const API_KEY = 'ETHPYY'; // please only use this key within Piwik + + /** + * Returns a URL that can be used to view all SEO data for a particular website. + * + * @param string $targetSiteUrl The URL of the website for whom SEO stats should be + * accessible for. + * @return string + */ + public static function getLinkForUrl($targetSiteUrl) + { + $domain = @parse_url($targetSiteUrl, PHP_URL_HOST); + return "http://www.majesticseo.com/reports/site-explorer/summary/$domain?IndexDataSource=F"; + } + + /** + * Returns backlink statistics including the count of backlinks and count of + * referrer domains (domains with backlinks). + * + * This method issues an HTTP request and waits for it to return. + * + * @param string $siteDomain The domain of the website to get stats for. + * @param int $timeout The number of seconds to wait before aborting + * the HTTP request. + * @return array An array containing the backlink count and referrer + * domain count: + * array( + * 'backlink_count' => X, + * 'referrer_domains_count' => Y + * ) + * If either stat is false, either the API returned an + * error, or the IP was blocked for this request. + */ + public function getBacklinkStats($siteDomain, $timeout = 300) + { + $apiUrl = $this->getApiUrl($method = 'GetBacklinkStats', $args = array( + 'items' => '1', + 'item0' => $siteDomain + )); + $apiResponse = Piwik_Http::sendHttpRequest($apiUrl, $timeout); + + $result = array( + 'backlink_count' => false, + 'referrer_domains_count' => false + ); + + $apiResponse = Piwik_Common::json_decode($apiResponse, $assoc = true); + if (!empty($apiResponse) + && !empty($apiResponse['Data']) + ) { + $siteSeoStats = reset($apiResponse['Data']); + + if (isset($siteSeoStats['ExtBackLinks']) + && $siteSeoStats['ExtBackLinks'] !== -1 + ) { + $result['backlink_count'] = $siteSeoStats['ExtBackLinks']; + } + + if (isset($siteSeoStats['RefDomains']) + && $siteSeoStats['RefDomains'] !== -1 + ) { + $result['referrer_domains_count'] = $siteSeoStats['RefDomains']; + } + } + + return $result; + } + + private function getApiUrl($method, $args = array()) + { + $args['sak'] = self::API_KEY; + + $queryString = http_build_query($args); + return self::API_BASE . $method . '?' . $queryString; + } } diff --git a/plugins/SEO/RankChecker.php b/plugins/SEO/RankChecker.php index da9fa0cf60..57512c4e8f 100644 --- a/plugins/SEO/RankChecker.php +++ b/plugins/SEO/RankChecker.php @@ -20,46 +20,46 @@ */ class Piwik_SEO_RankChecker { - private $url; - private $majesticInfo = null; - - public function __construct($url) - { - $this->url = self::extractDomainFromUrl($url); - } - - /** - * Extract domain from URL as the web services generally - * expect only a domain name (i.e., no protocol, port, path, query, etc). - * - * @param string $url - * @return string - */ - static public function extractDomainFromUrl($url) - { - return preg_replace( - array( - '~^https?\://~si', // strip protocol - '~[/:#?;%&].*~', // strip port, path, query, anchor, etc - '~\.$~', // trailing period - ), - '', $url); - } - - /** - * Web service proxy that retrieves the content at the specified URL - * - * @param string $url - * @return string - */ - private function getPage($url) - { - try { - return str_replace(' ', ' ', Piwik_Http::sendHttpRequest($url, $timeout = 10, @$_SERVER['HTTP_USER_AGENT'])); - } catch(Exception $e) { - return ''; - } - } + private $url; + private $majesticInfo = null; + + public function __construct($url) + { + $this->url = self::extractDomainFromUrl($url); + } + + /** + * Extract domain from URL as the web services generally + * expect only a domain name (i.e., no protocol, port, path, query, etc). + * + * @param string $url + * @return string + */ + static public function extractDomainFromUrl($url) + { + return preg_replace( + array( + '~^https?\://~si', // strip protocol + '~[/:#?;%&].*~', // strip port, path, query, anchor, etc + '~\.$~', // trailing period + ), + '', $url); + } + + /** + * Web service proxy that retrieves the content at the specified URL + * + * @param string $url + * @return string + */ + private function getPage($url) + { + try { + return str_replace(' ', ' ', Piwik_Http::sendHttpRequest($url, $timeout = 10, @$_SERVER['HTTP_USER_AGENT'])); + } catch (Exception $e) { + return ''; + } + } /** * Returns the google page rank for the current url @@ -67,16 +67,16 @@ class Piwik_SEO_RankChecker * @return int */ public function getPageRank() - { - $chwrite = $this->CheckHash($this->HashURL($this->url)); + { + $chwrite = $this->CheckHash($this->HashURL($this->url)); - $url="http://toolbarqueries.google.com/tbr?client=navclient-auto&ch=".$chwrite."&features=Rank&q=info:".$this->url."&num=100&filter=0"; - $data = $this->getPage($url); - preg_match('#Rank_[0-9]:[0-9]:([0-9]+){1,}#si', $data, $p); - $value = isset($p[1]) ? $p[1] : 0; + $url = "http://toolbarqueries.google.com/tbr?client=navclient-auto&ch=" . $chwrite . "&features=Rank&q=info:" . $this->url . "&num=100&filter=0"; + $data = $this->getPage($url); + preg_match('#Rank_[0-9]:[0-9]:([0-9]+){1,}#si', $data, $p); + $value = isset($p[1]) ? $p[1] : 0; - return $value; - } + return $value; + } /** * Returns the alexa traffic rank for the current url @@ -84,26 +84,26 @@ class Piwik_SEO_RankChecker * @return int */ public function getAlexaRank() - { + { $xml = @simplexml_load_string($this->getPage('http://data.alexa.com/data?cli=10&url=' . urlencode($this->url))); - return $xml ? $xml->SD->POPULARITY['TEXT'] : ''; - } + return $xml ? $xml->SD->POPULARITY['TEXT'] : ''; + } /** * Returns the number of Dmoz.org entries for the current url * * @return int */ - public function getDmoz() - { + public function getDmoz() + { $url = 'http://www.dmoz.org/search?q=' . urlencode($this->url); - $data = $this->getPage($url); + $data = $this->getPage($url); preg_match('#Open Directory Sites[^\(]+\([0-9]-[0-9]+ of ([0-9]+)\)#', $data, $p); if (!empty($p[1])) { - return (int) $p[1]; + return (int)$p[1]; } return 0; - } + } /** * Returns the number of pages google holds in it's index for the current url @@ -111,14 +111,14 @@ class Piwik_SEO_RankChecker * @return int */ public function getIndexedPagesGoogle() - { + { $url = 'http://www.google.com/search?hl=en&q=site%3A' . urlencode($this->url); - $data = $this->getPage($url); + $data = $this->getPage($url); if (preg_match('#about ([0-9\,]+) results#i', $data, $p)) { - return (int) str_replace(',', '', $p[1]); - } + return (int)str_replace(',', '', $p[1]); + } return 0; - } + } /** * Returns the number of pages bing holds in it's index for the current url @@ -126,14 +126,14 @@ class Piwik_SEO_RankChecker * @return int */ public function getIndexedPagesBing() - { + { $url = 'http://www.bing.com/search?mkt=en-US&q=site%3A' . urlencode($this->url); - $data = $this->getPage($url); + $data = $this->getPage($url); if (preg_match('#([0-9\,]+) results#i', $data, $p)) { - return (int) str_replace(',', '', $p[1]); - } + return (int)str_replace(',', '', $p[1]); + } return 0; - } + } /** * Returns the domain age for the current url @@ -148,19 +148,19 @@ class Piwik_SEO_RankChecker $ages = array(); - if($ageArchiveOrg > 0) { + if ($ageArchiveOrg > 0) { $ages[] = $ageArchiveOrg; } - if($ageWhoIs > 0) { + if ($ageWhoIs > 0) { $ages[] = $ageWhoIs; } - if($ageWhoisCom > 0) { + if ($ageWhoisCom > 0) { $ages[] = $ageWhoisCom; } - if(count($ages) > 1) { + if (count($ages) > 1) { $maxAge = min($ages); } else { $maxAge = array_shift($ages); @@ -171,27 +171,27 @@ class Piwik_SEO_RankChecker } return false; } - + /** * Returns the number backlinks that link to the current site. - * + * * @return int */ public function getExternalBacklinkCount() { - $majesticInfo = $this->getMajesticInfo(); - return $majesticInfo['backlink_count']; + $majesticInfo = $this->getMajesticInfo(); + return $majesticInfo['backlink_count']; } - + /** * Returns the number of referrer domains that link to the current site. - * + * * @return int */ public function getReferrerDomainCount() { - $majesticInfo = $this->getMajesticInfo(); - return $majesticInfo['referrer_domains_count']; + $majesticInfo = $this->getMajesticInfo(); + return $majesticInfo['referrer_domains_count']; } /** @@ -201,7 +201,7 @@ class Piwik_SEO_RankChecker */ protected function _getAgeArchiveOrg() { - $url = str_replace('www.', '', $this->url); + $url = str_replace('www.', '', $this->url); $data = @$this->getPage('http://wayback.archive.org/web/*/' . urlencode($url)); preg_match('#]*)' . preg_quote($url) . '/\">([^<]*)<\/a>#', $data, $p); if (!empty($p[2])) { @@ -221,8 +221,8 @@ class Piwik_SEO_RankChecker */ protected function _getAgeWhoIs() { - $url = preg_replace('/^www\./', '', $this->url); - $url = 'http://www.who.is/whois/' . urlencode($url); + $url = preg_replace('/^www\./', '', $this->url); + $url = 'http://www.who.is/whois/' . urlencode($url); $data = $this->getPage($url); preg_match('#(?:Creation Date|Created On|Registered on)\.*:\s*([ \ta-z0-9\/\-:\.]+)#si', $data, $p); if (!empty($p[1])) { @@ -242,8 +242,8 @@ class Piwik_SEO_RankChecker */ protected function _getAgeWhoisCom() { - $url = preg_replace('/^www\./', '', $this->url); - $url = 'http://www.whois.com/whois/' . urlencode($url); + $url = preg_replace('/^www\./', '', $this->url); + $url = 'http://www.whois.com/whois/' . urlencode($url); $data = $this->getPage($url); preg_match('#(?:Creation Date|Created On):\s*([ \ta-z0-9\/\-:\.]+)#si', $data, $p); if (!empty($p[1])) { @@ -256,114 +256,107 @@ class Piwik_SEO_RankChecker return 0; } - /** - * Convert numeric string to int - * - * @see getPageRank() - * - * @param string $Str - * @param int $Check - * @param int $Magic - * @return int - */ - private function StrToNum($Str, $Check, $Magic) - { - $Int32Unit = 4294967296; // 2^32 - - $length = strlen($Str); - for($i = 0; $i < $length; $i++) - { - $Check *= $Magic; - // If the float is beyond the boundaries of integer (usually +/- 2.15e+9 = 2^31), - // the result of converting to integer is undefined - // refer to http://www.php.net/manual/en/language.types.integer.php - if($Check >= $Int32Unit) - { - $Check = ($Check - $Int32Unit * (int) ($Check / $Int32Unit)); - //if the check less than -2^31 - $Check = ($Check < -2147483648) ? ($Check + $Int32Unit) : $Check; - } - $Check += ord($Str{$i}); - } - return $Check; - } - - /** - * Generate a hash for a url - * - * @see getPageRank() - * - * @param string $String - * @return int - */ - private function HashURL($String) - { - $Check1 = $this->StrToNum($String, 0x1505, 0x21); - $Check2 = $this->StrToNum($String, 0, 0x1003F); - - $Check1 >>= 2; - $Check1 = (($Check1 >> 4) & 0x3FFFFC0 ) | ($Check1 & 0x3F); - $Check1 = (($Check1 >> 4) & 0x3FFC00 ) | ($Check1 & 0x3FF); - $Check1 = (($Check1 >> 4) & 0x3C000 ) | ($Check1 & 0x3FFF); - - $T1 = (((($Check1 & 0x3C0) << 4) | ($Check1 & 0x3C)) <<2 ) | ($Check2 & 0xF0F ); - $T2 = (((($Check1 & 0xFFFFC000) << 4) | ($Check1 & 0x3C00)) << 0xA) | ($Check2 & 0xF0F0000 ); - - return ($T1 | $T2); - } - - /** - * Generate a checksum for the hash string - * - * @see getPageRank() - * - * @param int $Hashnum - * @return string - */ - private function CheckHash($Hashnum) - { - $CheckByte = 0; - $Flag = 0; - - $HashStr = sprintf('%u', $Hashnum) ; - $length = strlen($HashStr); - - for($i = $length - 1; $i >= 0; $i --) - { - $Re = $HashStr{$i}; - if(1 === ($Flag % 2)) { - $Re += $Re; - $Re = (int)($Re / 10) + ($Re % 10); - } - $CheckByte += $Re; - $Flag ++; - } - - $CheckByte %= 10; - if(0 !== $CheckByte) - { - $CheckByte = 10 - $CheckByte; - if(1 === ($Flag % 2) ) - { - if(1 === ($CheckByte % 2)) - { - $CheckByte += 9; - } - $CheckByte >>= 1; - } - } - - return '7'.$CheckByte.$HashStr; - } - - private function getMajesticInfo() - { - if ($this->majesticInfo === null) - { - $client = new Piwik_SEO_MajesticClient(); - $this->majesticInfo = $client->getBacklinkStats($this->url); - } - - return $this->majesticInfo; - } + /** + * Convert numeric string to int + * + * @see getPageRank() + * + * @param string $Str + * @param int $Check + * @param int $Magic + * @return int + */ + private function StrToNum($Str, $Check, $Magic) + { + $Int32Unit = 4294967296; // 2^32 + + $length = strlen($Str); + for ($i = 0; $i < $length; $i++) { + $Check *= $Magic; + // If the float is beyond the boundaries of integer (usually +/- 2.15e+9 = 2^31), + // the result of converting to integer is undefined + // refer to http://www.php.net/manual/en/language.types.integer.php + if ($Check >= $Int32Unit) { + $Check = ($Check - $Int32Unit * (int)($Check / $Int32Unit)); + //if the check less than -2^31 + $Check = ($Check < -2147483648) ? ($Check + $Int32Unit) : $Check; + } + $Check += ord($Str{$i}); + } + return $Check; + } + + /** + * Generate a hash for a url + * + * @see getPageRank() + * + * @param string $String + * @return int + */ + private function HashURL($String) + { + $Check1 = $this->StrToNum($String, 0x1505, 0x21); + $Check2 = $this->StrToNum($String, 0, 0x1003F); + + $Check1 >>= 2; + $Check1 = (($Check1 >> 4) & 0x3FFFFC0) | ($Check1 & 0x3F); + $Check1 = (($Check1 >> 4) & 0x3FFC00) | ($Check1 & 0x3FF); + $Check1 = (($Check1 >> 4) & 0x3C000) | ($Check1 & 0x3FFF); + + $T1 = (((($Check1 & 0x3C0) << 4) | ($Check1 & 0x3C)) << 2) | ($Check2 & 0xF0F); + $T2 = (((($Check1 & 0xFFFFC000) << 4) | ($Check1 & 0x3C00)) << 0xA) | ($Check2 & 0xF0F0000); + + return ($T1 | $T2); + } + + /** + * Generate a checksum for the hash string + * + * @see getPageRank() + * + * @param int $Hashnum + * @return string + */ + private function CheckHash($Hashnum) + { + $CheckByte = 0; + $Flag = 0; + + $HashStr = sprintf('%u', $Hashnum); + $length = strlen($HashStr); + + for ($i = $length - 1; $i >= 0; $i--) { + $Re = $HashStr{$i}; + if (1 === ($Flag % 2)) { + $Re += $Re; + $Re = (int)($Re / 10) + ($Re % 10); + } + $CheckByte += $Re; + $Flag++; + } + + $CheckByte %= 10; + if (0 !== $CheckByte) { + $CheckByte = 10 - $CheckByte; + if (1 === ($Flag % 2)) { + if (1 === ($CheckByte % 2)) { + $CheckByte += 9; + } + $CheckByte >>= 1; + } + } + + return '7' . $CheckByte . $HashStr; + } + + private function getMajesticInfo() + { + if ($this->majesticInfo === null) { + $client = new Piwik_SEO_MajesticClient(); + $this->majesticInfo = $client->getBacklinkStats($this->url); + } + + return $this->majesticInfo; + } } diff --git a/plugins/SEO/SEO.php b/plugins/SEO/SEO.php index a7e5474506..addc8ae69e 100644 --- a/plugins/SEO/SEO.php +++ b/plugins/SEO/SEO.php @@ -1,10 +1,10 @@ 'This Plugin extracts and displays SEO metrics: Alexa web ranking, Google Pagerank, number of Indexed pages and backlinks of the currently selected website.', - 'author' => 'Piwik', - 'author_homepage' => 'http://piwik.org/', - 'version' => Piwik_Version::VERSION, - ); - } - - function getListHooksRegistered() - { - $hooks = array( 'WidgetsList.add' => 'addWidgets' ); - return $hooks; - } - - function addWidgets() - { - Piwik_AddWidget('SEO', 'SEO_SeoRankings', 'SEO', 'getRank'); - } + public function getInformation() + { + return array( + 'description' => 'This Plugin extracts and displays SEO metrics: Alexa web ranking, Google Pagerank, number of Indexed pages and backlinks of the currently selected website.', + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'version' => Piwik_Version::VERSION, + ); + } + + function getListHooksRegistered() + { + $hooks = array('WidgetsList.add' => 'addWidgets'); + return $hooks; + } + + function addWidgets() + { + Piwik_AddWidget('SEO', 'SEO_SeoRankings', 'SEO', 'getRank'); + } } diff --git a/plugins/SEO/templates/index.tpl b/plugins/SEO/templates/index.tpl index fe1fe903d3..721ca46ee5 100644 --- a/plugins/SEO/templates/index.tpl +++ b/plugins/SEO/templates/index.tpl @@ -1,42 +1,46 @@
- - -
-
- {'Installation_SetupWebSiteURL'|translate|ucfirst} - + + + +
+ {'Installation_SetupWebSiteURL'|translate|ucfirst} + - + -
- - {ajaxLoadingDiv id=ajaxLoadingSEO} +
+ + {ajaxLoadingDiv id=ajaxLoadingSEO} + +
+ {if empty($ranks)} + {'General_Error'|translate} + {else} + {capture name=cleanUrl} + {$urlToRank|escape:'html'} + {/capture} + {'SEO_SEORankingsFor'|translate:$smarty.capture.cleanUrl} + + {foreach from=$ranks item=rank} + + + + + {/foreach} -
- {if empty($ranks)} - {'General_Error'|translate} - {else} -{capture name=cleanUrl} -{$urlToRank|escape:'html'} -{/capture} - {'SEO_SEORankingsFor'|translate:$smarty.capture.cleanUrl} -
{if !empty($rank.logo_link)}{/if}{$rank.label}{if !empty($rank.logo_link)}{/if} {$rank.label} + +
+ {if isset($rank.rank)}{$rank.rank}{else}-{/if} + {if $rank.id=='pagerank'} /10 + {elseif $rank.id=='google-index' || $rank.id=='bing-index'} {'SEO_Pages'|translate} + {/if} +
+
- {foreach from=$ranks item=rank} - - - - {/foreach} - -
{if !empty($rank.logo_link)}{/if}{$rank.label}{if !empty($rank.logo_link)}{/if} {$rank.label} - -
- {if isset($rank.rank)}{$rank.rank}{else}-{/if} - {if $rank.id=='pagerank'} /10 - {elseif $rank.id=='google-index' || $rank.id=='bing-index'} {'SEO_Pages'|translate} - {/if} -
-
- {/if} -
-
+ + {/if} +
+
diff --git a/plugins/SEO/templates/rank.js b/plugins/SEO/templates/rank.js index d7a3802fc9..1a9dc07d0c 100644 --- a/plugins/SEO/templates/rank.js +++ b/plugins/SEO/templates/rank.js @@ -12,7 +12,7 @@ $(document).ready(function () { ajaxRequest.addParams({ module: 'SEO', action: 'getRank', - url: encodeURIComponent($('#seoUrl').val()) + url: encodeURIComponent($('#seoUrl').val()) }, 'get'); ajaxRequest.setCallback( function (response) { diff --git a/plugins/SecurityInfo/Controller.php b/plugins/SecurityInfo/Controller.php index 94a4d6d02c..1c8ff217cd 100644 --- a/plugins/SecurityInfo/Controller.php +++ b/plugins/SecurityInfo/Controller.php @@ -1,7 +1,7 @@ loadAndRun(); + // load and run all tests + $psi->loadAndRun(); - // grab the results as a multidimensional array - $results = $psi->getResultsAsArray(); + // grab the results as a multidimensional array + $results = $psi->getResultsAsArray(); - // suppress results - unset($results['test_results']['Core']['memory_limit']); - unset($results['test_results']['Core']['post_max_size']); - unset($results['test_results']['Core']['upload_max_filesize']); + // suppress results + unset($results['test_results']['Core']['memory_limit']); + unset($results['test_results']['Core']['post_max_size']); + unset($results['test_results']['Core']['upload_max_filesize']); - $view = Piwik_View::factory('index'); - $this->setBasicVariablesView($view); - $view->menu = Piwik_GetAdminMenu(); - $view->results = $results; - echo $view->render(); - } + $view = Piwik_View::factory('index'); + $this->setBasicVariablesView($view); + $view->menu = Piwik_GetAdminMenu(); + $view->results = $results; + echo $view->render(); + } } diff --git a/plugins/SecurityInfo/PhpSecInfo/PhpSecInfo.php b/plugins/SecurityInfo/PhpSecInfo/PhpSecInfo.php index 0e9bbc7768..acdba6b959 100644 --- a/plugins/SecurityInfo/PhpSecInfo/PhpSecInfo.php +++ b/plugins/SecurityInfo/PhpSecInfo/PhpSecInfo.php @@ -95,497 +95,505 @@ define('PHPSECINFO_BASE_DIR', dirname(__FILE__)); class PhpSecInfo { - /** - * An array of tests to run - * - * @var array PhpSecInfo_Test - */ - var $tests_to_run = array(); - - - /** - * An array of results. Each result is an associative array: - * - * $result['result'] = PHPSECINFO_TEST_RESULT_NOTICE; - * $result['message'] = "a string describing the test results and what they mean"; - * - * - * @var array - */ - var $test_results = array(); - - - /** - * An array of tests that were not run - * - * - * $result['result'] = PHPSECINFO_TEST_RESULT_NOTRUN; - * $result['message'] = "a string explaining why the test was not run"; - * - * - * @var array - */ - var $tests_not_run = array(); - - - /** - * The language code used. Defaults to PHPSECINFO_LANG_DEFAULT, which - * is 'en' - * - * @var string - * @see PHPSECINFO_LANG_DEFAULT - */ - var $language = PHPSECINFO_LANG_DEFAULT; - - - /** - * An array of integers recording the number of test results in each category. Categories can include - * some or all of the PHPSECINFO_TEST_* constants. Constants are the keys, # of results are the values. - * - * @var array - */ - var $result_counts = array(); - - - /** - * The number of tests that have been run - * - * @var integer - */ - var $num_tests_run = 0; - - - /** - * The base directory for phpsecinfo. Set within the constructor. Paths are resolved from this. - * @var string - */ - var $_base_dir; - - - /** - * The directory PHPSecInfo will look for views. It defaults to the value - * in PHPSECINFO_VIEW_DIR_DEFAULT, but can be changed with the setViewDirectory() - * method. - * - * @var string - */ - var $_view_directory; - - - /** - * The output format, used to load the proper view - * - * @var string - **/ - var $_format; - - /** - * Constructor - * - * @return PhpSecInfo - */ - function PhpSecInfo($opts = null) { - - $this->_base_dir = dirname(__FILE__); - - if ($opts) { - if (isset($opts['view_directory'])) { - $this->setViewDirectory($opts['view_directory']); - } else { - $this->setViewDirectory(dirname(__FILE__).DIRECTORY_SEPARATOR . PHPSECINFO_VIEW_DIR_DEFAULT); - } - - if (isset($opts['format'])) { - $this->setFormat($opts['format']); - } else { - if (!strcasecmp(PHP_SAPI, 'cli')) { - $this->setFormat('Cli'); - } else { - $this->setFormat(PHPSECINFO_FORMAT_DEFAULT); - } - } - - } else { /* Use defaults */ - $this->setViewDirectory(dirname(__FILE__).DIRECTORY_SEPARATOR . PHPSECINFO_VIEW_DIR_DEFAULT); - if (!strcasecmp(PHP_SAPI, 'cli')) { - $this->setFormat('Cli'); - } else { - $this->setFormat(PHPSECINFO_FORMAT_DEFAULT); - } - } - } - - - /** - * recurses through the Test subdir and includes classes in each test group subdir, - * then builds an array of classnames for the tests that will be run - * - */ - function loadTests() { - - $test_root = dir(dirname(__FILE__).DIRECTORY_SEPARATOR.'Test'); - - //echo "
"; echo print_r($test_root, true); echo "
"; - - while (false !== ($entry = $test_root->read())) { - if ( is_dir($test_root->path.DIRECTORY_SEPARATOR.$entry) && !preg_match('~^(\.|_vti)(.*)$~', $entry) ) { - $test_dirs[] = $entry; - } - } - //echo "
"; echo print_r($test_dirs, true); echo "
"; - - // include_once all files in each test dir - foreach ($test_dirs as $test_dir) { - $this_dir = dir($test_root->path.DIRECTORY_SEPARATOR.$test_dir); - - while (false !== ($entry = $this_dir->read())) { - if (!is_dir($this_dir->path.DIRECTORY_SEPARATOR.$entry)) { - include_once $this_dir->path.DIRECTORY_SEPARATOR.$entry; - $classNames[] = "PhpSecInfo_Test_".$test_dir."_".basename($entry, '.php'); - } - } - - } - - // modded this to not throw a PHP5 STRICT notice, although I don't like passing by value here - $this->tests_to_run = $classNames; - } - - - /** - * This runs the tests in the tests_to_run array and - * places returned data in the following arrays/scalars: - * - $this->test_results - * - $this->result_counts - * - $this->num_tests_run - * - $this->tests_not_run; - * - */ - function runTests() { - // initialize a bunch of arrays - $this->test_results = array(); - $this->result_counts = array(); - $this->result_counts[PHPSECINFO_TEST_RESULT_NOTRUN] = 0; - $this->num_tests_run = 0; - - foreach ($this->tests_to_run as $testClass) { - - /** - * @var $test PhpSecInfo_Test - */ - $test = new $testClass(); - - if ($test->isTestable()) { - $test->test(); - $rs = array( 'result' => $test->getResult(), - 'message' => $test->getMessage(), - 'value_current' => $test->getCurrentTestValue(), - 'value_recommended' => $test->getRecommendedTestValue(), - 'moreinfo_url' => $test->getMoreInfoURL(), - ); - $this->test_results[$test->getTestGroup()][$test->getTestName()] = $rs; - - // initialize if not yet set - if (!isset ($this->result_counts[$rs['result']]) ) { - $this->result_counts[$rs['result']] = 0; - } - - $this->result_counts[$rs['result']]++; - $this->num_tests_run++; - } else { - $rs = array( 'result' => $test->getResult(), - 'message' => $test->getMessage(), - 'value_current' => NULL, - 'value_recommended' => NULL, - 'moreinfo_url' => $test->getMoreInfoURL(), - ); - $this->result_counts[PHPSECINFO_TEST_RESULT_NOTRUN]++; - $this->tests_not_run[$test->getTestGroup()."::".$test->getTestName()] = $rs; - } - } - } - - - /** - * This is the main output method. The look and feel mimics phpinfo() - * - */ - function renderOutput($page_title="Security Information About PHP") { - /** - * We need to use PhpSecInfo_Test::getBooleanIniValue() below - * @see PhpSecInfo_Test::getBooleanIniValue() - */ - if (!class_exists('PhpSecInfo_Test')) { - include( dirname(__FILE__).DIRECTORY_SEPARATOR.'Test'.DIRECTORY_SEPARATOR.'Test.php'); - } - $this->loadView($this->_format); - } - - - /** - * This is a helper method that makes it easy to output tables of test results - * for a given test group - * - * @param string $group_name - * @param array $group_results - */ - function _outputRenderTable($group_name, $group_results) { - - // exit out if $group_results was empty or not an array. This sorta seems a little hacky... - if (!is_array($group_results) || sizeof($group_results) < 1) { - return false; - } - - ksort($group_results); - - $this->loadView($this->_format.'/Result', array('group_name'=>$group_name, 'group_results'=>$group_results)); - - return true; - } - - - - /** - * This outputs a table containing a summary of the test results (counts and % in each result type) - * - * @see PHPSecInfo::_outputRenderTable() - * @see PHPSecInfo::_outputGetResultTypeFromCode() - */ - function _outputRenderStatsTable() { - - foreach($this->result_counts as $code=>$val) { - if ($code != PHPSECINFO_TEST_RESULT_NOTRUN) { - $percentage = round($val/$this->num_tests_run * 100,2); - $result_type = $this->_outputGetResultTypeFromCode($code); - $stats[$result_type] = array( 'count' => $val, - 'result' => $code, - 'message' => "$val out of {$this->num_tests_run} ($percentage%)"); - } - } - - $this->_outputRenderTable('Test Results Summary', $stats); - - } - - - - /** - * This outputs a table containing a summary or test that were not executed, and the reasons why they were skipped - * - * @see PHPSecInfo::_outputRenderTable() - */ - function _outputRenderNotRunTable() { - - $this->_outputRenderTable('Tests Not Run', $this->tests_not_run); - - } - - - - - /** - * This is a helper function that returns a CSS class corresponding to - * the result code the test returned. This allows us to color-code - * results - * - * @param integer $code - * @return string - */ - function _outputGetCssClassFromResult($code) { - - switch ($code) { - case PHPSECINFO_TEST_RESULT_OK: - return 'value-ok'; - break; - - case PHPSECINFO_TEST_RESULT_NOTICE: - return 'value-notice'; - break; - - case PHPSECINFO_TEST_RESULT_WARN: - return 'value-warn'; - break; - - case PHPSECINFO_TEST_RESULT_NOTRUN: - return 'value-notrun'; - break; - - case PHPSECINFO_TEST_RESULT_ERROR: - return 'value-error'; - break; - - default: - return 'value-notrun'; - break; - } - - } - - - - /** - * This is a helper function that returns a label string corresponding to - * the result code the test returned. This is mainly used for the Test - * Results Summary table. - * - * @see PHPSecInfo::_outputRenderStatsTable() - * @param integer $code - * @return string - */ - function _outputGetResultTypeFromCode($code) { - - switch ($code) { - case PHPSECINFO_TEST_RESULT_OK: - return 'Pass'; - break; - - case PHPSECINFO_TEST_RESULT_NOTICE: - return 'Notice'; - break; - - case PHPSECINFO_TEST_RESULT_WARN: - return 'Warning'; - break; - - case PHPSECINFO_TEST_RESULT_NOTRUN: - return 'Not Run'; - break; - - case PHPSECINFO_TEST_RESULT_ERROR: - return 'Error'; - break; - - default: - return 'Invalid Result Code'; - break; - } - - } - - - /** - * Loads and runs all the tests - * - * As loading, then running, is a pretty common process, this saves a extra method call - * - * @since 0.1.1 - * - */ - function loadAndRun() { - $this->loadTests(); - $this->runTests(); - } - - - /** - * returns an associative array of test data. Four keys are set: - * - test_results (array) - * - tests_not_run (array) - * - result_counts (array) - * - num_tests_run (integer) - * - * note that this must be called after tests are loaded and run - * - * @since 0.1.1 - * @return array - */ - function getResultsAsArray() { - $results = array(); - - $results['test_results'] = $this->test_results; - $results['tests_not_run'] = $this->tests_not_run; - $results['result_counts'] = $this->result_counts; - $results['num_tests_run'] = $this->num_tests_run; - - return $results; - } - - - - /** - * returns the standard output as a string instead of echoing it to the browser - * - * note that this must be called after tests are loaded and run - * - * @since 0.1.1 - * - * @return string - */ - function getOutput() { - ob_start(); - $this->renderOutput(); - $output = ob_get_clean(); - return $output; - } - - - /** - * A very, very simple "view" system - * - */ - function loadView($view_name, $data=null) { - if ($data != null) { - extract($data); - } - - $view_file = $this->getViewDirectory().$view_name.".php"; - - if ( file_exists($view_file) && is_readable($view_file) ) { - ob_start(); - include $view_file; - echo ob_get_clean(); - } else { - user_error("The view '{$view_file}' either does not exist or is not readable", E_USER_WARNING); - } - - - } - - - /** - * Returns the current view directory - * - * @return string - */ - function getViewDirectory() { - return $this->_view_directory; - } - - - /** - * Sets the directory that PHPSecInfo will look in for views - * - * @param string $newdir - */ - function setViewDirectory($newdir) { - $this->_view_directory = $newdir; - } - - - - - function getFormat() { - return $this->_format; - } - - - function setFormat($format) { - $this->_format = $format; - } + /** + * An array of tests to run + * + * @var array PhpSecInfo_Test + */ + var $tests_to_run = array(); + + + /** + * An array of results. Each result is an associative array: + * + * $result['result'] = PHPSECINFO_TEST_RESULT_NOTICE; + * $result['message'] = "a string describing the test results and what they mean"; + * + * + * @var array + */ + var $test_results = array(); + + + /** + * An array of tests that were not run + * + * + * $result['result'] = PHPSECINFO_TEST_RESULT_NOTRUN; + * $result['message'] = "a string explaining why the test was not run"; + * + * + * @var array + */ + var $tests_not_run = array(); + + + /** + * The language code used. Defaults to PHPSECINFO_LANG_DEFAULT, which + * is 'en' + * + * @var string + * @see PHPSECINFO_LANG_DEFAULT + */ + var $language = PHPSECINFO_LANG_DEFAULT; + + + /** + * An array of integers recording the number of test results in each category. Categories can include + * some or all of the PHPSECINFO_TEST_* constants. Constants are the keys, # of results are the values. + * + * @var array + */ + var $result_counts = array(); + + + /** + * The number of tests that have been run + * + * @var integer + */ + var $num_tests_run = 0; + + + /** + * The base directory for phpsecinfo. Set within the constructor. Paths are resolved from this. + * @var string + */ + var $_base_dir; + + + /** + * The directory PHPSecInfo will look for views. It defaults to the value + * in PHPSECINFO_VIEW_DIR_DEFAULT, but can be changed with the setViewDirectory() + * method. + * + * @var string + */ + var $_view_directory; + + + /** + * The output format, used to load the proper view + * + * @var string + **/ + var $_format; + + /** + * Constructor + * + * @return PhpSecInfo + */ + function PhpSecInfo($opts = null) + { + + $this->_base_dir = dirname(__FILE__); + + if ($opts) { + if (isset($opts['view_directory'])) { + $this->setViewDirectory($opts['view_directory']); + } else { + $this->setViewDirectory(dirname(__FILE__) . DIRECTORY_SEPARATOR . PHPSECINFO_VIEW_DIR_DEFAULT); + } + + if (isset($opts['format'])) { + $this->setFormat($opts['format']); + } else { + if (!strcasecmp(PHP_SAPI, 'cli')) { + $this->setFormat('Cli'); + } else { + $this->setFormat(PHPSECINFO_FORMAT_DEFAULT); + } + } + + } else { /* Use defaults */ + $this->setViewDirectory(dirname(__FILE__) . DIRECTORY_SEPARATOR . PHPSECINFO_VIEW_DIR_DEFAULT); + if (!strcasecmp(PHP_SAPI, 'cli')) { + $this->setFormat('Cli'); + } else { + $this->setFormat(PHPSECINFO_FORMAT_DEFAULT); + } + } + } + + + /** + * recurses through the Test subdir and includes classes in each test group subdir, + * then builds an array of classnames for the tests that will be run + * + */ + function loadTests() + { + + $test_root = dir(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Test'); + + //echo "
"; echo print_r($test_root, true); echo "
"; + + while (false !== ($entry = $test_root->read())) { + if (is_dir($test_root->path . DIRECTORY_SEPARATOR . $entry) && !preg_match('~^(\.|_vti)(.*)$~', $entry)) { + $test_dirs[] = $entry; + } + } + //echo "
"; echo print_r($test_dirs, true); echo "
"; + + // include_once all files in each test dir + foreach ($test_dirs as $test_dir) { + $this_dir = dir($test_root->path . DIRECTORY_SEPARATOR . $test_dir); + + while (false !== ($entry = $this_dir->read())) { + if (!is_dir($this_dir->path . DIRECTORY_SEPARATOR . $entry)) { + include_once $this_dir->path . DIRECTORY_SEPARATOR . $entry; + $classNames[] = "PhpSecInfo_Test_" . $test_dir . "_" . basename($entry, '.php'); + } + } + + } + + // modded this to not throw a PHP5 STRICT notice, although I don't like passing by value here + $this->tests_to_run = $classNames; + } + + + /** + * This runs the tests in the tests_to_run array and + * places returned data in the following arrays/scalars: + * - $this->test_results + * - $this->result_counts + * - $this->num_tests_run + * - $this->tests_not_run; + * + */ + function runTests() + { + // initialize a bunch of arrays + $this->test_results = array(); + $this->result_counts = array(); + $this->result_counts[PHPSECINFO_TEST_RESULT_NOTRUN] = 0; + $this->num_tests_run = 0; + + foreach ($this->tests_to_run as $testClass) { + + /** + * @var $test PhpSecInfo_Test + */ + $test = new $testClass(); + + if ($test->isTestable()) { + $test->test(); + $rs = array('result' => $test->getResult(), + 'message' => $test->getMessage(), + 'value_current' => $test->getCurrentTestValue(), + 'value_recommended' => $test->getRecommendedTestValue(), + 'moreinfo_url' => $test->getMoreInfoURL(), + ); + $this->test_results[$test->getTestGroup()][$test->getTestName()] = $rs; + + // initialize if not yet set + if (!isset ($this->result_counts[$rs['result']])) { + $this->result_counts[$rs['result']] = 0; + } + + $this->result_counts[$rs['result']]++; + $this->num_tests_run++; + } else { + $rs = array('result' => $test->getResult(), + 'message' => $test->getMessage(), + 'value_current' => NULL, + 'value_recommended' => NULL, + 'moreinfo_url' => $test->getMoreInfoURL(), + ); + $this->result_counts[PHPSECINFO_TEST_RESULT_NOTRUN]++; + $this->tests_not_run[$test->getTestGroup() . "::" . $test->getTestName()] = $rs; + } + } + } + + + /** + * This is the main output method. The look and feel mimics phpinfo() + * + */ + function renderOutput($page_title = "Security Information About PHP") + { + /** + * We need to use PhpSecInfo_Test::getBooleanIniValue() below + * @see PhpSecInfo_Test::getBooleanIniValue() + */ + if (!class_exists('PhpSecInfo_Test')) { + include(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Test' . DIRECTORY_SEPARATOR . 'Test.php'); + } + $this->loadView($this->_format); + } + + + /** + * This is a helper method that makes it easy to output tables of test results + * for a given test group + * + * @param string $group_name + * @param array $group_results + */ + function _outputRenderTable($group_name, $group_results) + { + + // exit out if $group_results was empty or not an array. This sorta seems a little hacky... + if (!is_array($group_results) || sizeof($group_results) < 1) { + return false; + } + + ksort($group_results); + + $this->loadView($this->_format . '/Result', array('group_name' => $group_name, 'group_results' => $group_results)); + + return true; + } + + + /** + * This outputs a table containing a summary of the test results (counts and % in each result type) + * + * @see PHPSecInfo::_outputRenderTable() + * @see PHPSecInfo::_outputGetResultTypeFromCode() + */ + function _outputRenderStatsTable() + { + + foreach ($this->result_counts as $code => $val) { + if ($code != PHPSECINFO_TEST_RESULT_NOTRUN) { + $percentage = round($val / $this->num_tests_run * 100, 2); + $result_type = $this->_outputGetResultTypeFromCode($code); + $stats[$result_type] = array('count' => $val, + 'result' => $code, + 'message' => "$val out of {$this->num_tests_run} ($percentage%)"); + } + } + + $this->_outputRenderTable('Test Results Summary', $stats); + + } + + + /** + * This outputs a table containing a summary or test that were not executed, and the reasons why they were skipped + * + * @see PHPSecInfo::_outputRenderTable() + */ + function _outputRenderNotRunTable() + { + + $this->_outputRenderTable('Tests Not Run', $this->tests_not_run); + + } + + + /** + * This is a helper function that returns a CSS class corresponding to + * the result code the test returned. This allows us to color-code + * results + * + * @param integer $code + * @return string + */ + function _outputGetCssClassFromResult($code) + { + + switch ($code) { + case PHPSECINFO_TEST_RESULT_OK: + return 'value-ok'; + break; + + case PHPSECINFO_TEST_RESULT_NOTICE: + return 'value-notice'; + break; + + case PHPSECINFO_TEST_RESULT_WARN: + return 'value-warn'; + break; + + case PHPSECINFO_TEST_RESULT_NOTRUN: + return 'value-notrun'; + break; + + case PHPSECINFO_TEST_RESULT_ERROR: + return 'value-error'; + break; + + default: + return 'value-notrun'; + break; + } + + } + + + /** + * This is a helper function that returns a label string corresponding to + * the result code the test returned. This is mainly used for the Test + * Results Summary table. + * + * @see PHPSecInfo::_outputRenderStatsTable() + * @param integer $code + * @return string + */ + function _outputGetResultTypeFromCode($code) + { + + switch ($code) { + case PHPSECINFO_TEST_RESULT_OK: + return 'Pass'; + break; + + case PHPSECINFO_TEST_RESULT_NOTICE: + return 'Notice'; + break; + + case PHPSECINFO_TEST_RESULT_WARN: + return 'Warning'; + break; + + case PHPSECINFO_TEST_RESULT_NOTRUN: + return 'Not Run'; + break; + + case PHPSECINFO_TEST_RESULT_ERROR: + return 'Error'; + break; + + default: + return 'Invalid Result Code'; + break; + } + + } + + + /** + * Loads and runs all the tests + * + * As loading, then running, is a pretty common process, this saves a extra method call + * + * @since 0.1.1 + * + */ + function loadAndRun() + { + $this->loadTests(); + $this->runTests(); + } + + + /** + * returns an associative array of test data. Four keys are set: + * - test_results (array) + * - tests_not_run (array) + * - result_counts (array) + * - num_tests_run (integer) + * + * note that this must be called after tests are loaded and run + * + * @since 0.1.1 + * @return array + */ + function getResultsAsArray() + { + $results = array(); + + $results['test_results'] = $this->test_results; + $results['tests_not_run'] = $this->tests_not_run; + $results['result_counts'] = $this->result_counts; + $results['num_tests_run'] = $this->num_tests_run; + + return $results; + } + + + /** + * returns the standard output as a string instead of echoing it to the browser + * + * note that this must be called after tests are loaded and run + * + * @since 0.1.1 + * + * @return string + */ + function getOutput() + { + ob_start(); + $this->renderOutput(); + $output = ob_get_clean(); + return $output; + } + + + /** + * A very, very simple "view" system + * + */ + function loadView($view_name, $data = null) + { + if ($data != null) { + extract($data); + } + + $view_file = $this->getViewDirectory() . $view_name . ".php"; + + if (file_exists($view_file) && is_readable($view_file)) { + ob_start(); + include $view_file; + echo ob_get_clean(); + } else { + user_error("The view '{$view_file}' either does not exist or is not readable", E_USER_WARNING); + } + + + } + + + /** + * Returns the current view directory + * + * @return string + */ + function getViewDirectory() + { + return $this->_view_directory; + } + + + /** + * Sets the directory that PHPSecInfo will look in for views + * + * @param string $newdir + */ + function setViewDirectory($newdir) + { + $this->_view_directory = $newdir; + } + + + function getFormat() + { + return $this->_format; + } + + + function setFormat($format) + { + $this->_format = $format; + } } - - /** * A globally-available function that runs the tests and creates the result page * */ -function phpsecinfo() { - // modded this to not throw a PHP5 STRICT notice, although I don't like passing by value here - $psi = new PhpSecInfo(); - $psi->loadAndRun(); - $psi->renderOutput(); +function phpsecinfo() +{ + // modded this to not throw a PHP5 STRICT notice, although I don't like passing by value here + $psi = new PhpSecInfo(); + $psi->loadAndRun(); + $psi->renderOutput(); } diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Application/php.php b/plugins/SecurityInfo/PhpSecInfo/Test/Application/php.php index 919d73a2cb..3199cb3167 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Application/php.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Application/php.php @@ -9,7 +9,7 @@ /** * require the PhpSecInfo_Test_Application class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Application.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Application.php'); /** * Test class for PHP application @@ -21,49 +21,52 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Application.php'); */ class PhpSecInfo_Test_Application_Php extends PhpSecInfo_Test_Application { - var $test_name = "PHP"; + var $test_name = "PHP"; - var $recommended_value = null; + var $recommended_value = null; - function _retrieveCurrentValue() { - $this->current_value = PHP_VERSION; + function _retrieveCurrentValue() + { + $this->current_value = PHP_VERSION; - $url = 'http://php.net/releases/?serialize=1&version=5'; - $timeout = Piwik_UpdateCheck::SOCKET_TIMEOUT; - try { - $latestVersion = Piwik_Http::sendHttpRequest($url, $timeout); - $versionInfo = safe_unserialize($latestVersion); - $this->recommended_value = $versionInfo['version']; - } catch(Exception $e) { - $this->recommended_value = ''; - } - } + $url = 'http://php.net/releases/?serialize=1&version=5'; + $timeout = Piwik_UpdateCheck::SOCKET_TIMEOUT; + try { + $latestVersion = Piwik_Http::sendHttpRequest($url, $timeout); + $versionInfo = safe_unserialize($latestVersion); + $this->recommended_value = $versionInfo['version']; + } catch (Exception $e) { + $this->recommended_value = ''; + } + } - function _execTest() { - if (version_compare($this->current_value, '5.2.1') < 0) { - return PHPSECINFO_TEST_RESULT_WARN; - } + function _execTest() + { + if (version_compare($this->current_value, '5.2.1') < 0) { + return PHPSECINFO_TEST_RESULT_WARN; + } - if (empty($this->recommended_value)) { - return PHPSECINFO_TEST_RESULT_ERROR; - } + if (empty($this->recommended_value)) { + return PHPSECINFO_TEST_RESULT_ERROR; + } - if (version_compare($this->current_value, $this->recommended_value) >= 0 ) { - return PHPSECINFO_TEST_RESULT_OK; - } + if (version_compare($this->current_value, $this->recommended_value) >= 0) { + return PHPSECINFO_TEST_RESULT_OK; + } - return PHPSECINFO_TEST_RESULT_NOTICE; - } + return PHPSECINFO_TEST_RESULT_NOTICE; + } - function _setMessages() { - parent::_setMessages(); + function _setMessages() + { + parent::_setMessages(); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', "You are running PHP ".$this->current_value. - ($this->current_value == $this->recommended_value - ? " (the latest version)." - : ". The latest version is ".$this->recommended_value.".")); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', "You are running PHP ".$this->current_value.". The latest version of PHP is ".$this->recommended_value."."); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', "You are running PHP ".$this->current_value." which is really old. We recommend running the latest (stable) version of PHP which includes numerous bug fixes and security fixes."); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_ERROR, 'en', "Unable to determine the latest version of PHP available."); - } + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', "You are running PHP " . $this->current_value . + ($this->current_value == $this->recommended_value + ? " (the latest version)." + : ". The latest version is " . $this->recommended_value . ".")); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', "You are running PHP " . $this->current_value . ". The latest version of PHP is " . $this->recommended_value . "."); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', "You are running PHP " . $this->current_value . " which is really old. We recommend running the latest (stable) version of PHP which includes numerous bug fixes and security fixes."); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_ERROR, 'en', "Unable to determine the latest version of PHP available."); + } } diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Application/piwik.php b/plugins/SecurityInfo/PhpSecInfo/Test/Application/piwik.php index 5a495a352c..16e7fbf42f 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Application/piwik.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Application/piwik.php @@ -9,7 +9,7 @@ /** * require the PhpSecInfo_Test_Application class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Application.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Application.php'); /** * Test class for Piwik application @@ -21,38 +21,41 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Application.php'); */ class PhpSecInfo_Test_Application_Piwik extends PhpSecInfo_Test_Application { - var $test_name = "Piwik"; + var $test_name = "Piwik"; - var $recommended_value = null; + var $recommended_value = null; - function _retrieveCurrentValue() { - $this->current_value = Piwik_Version::VERSION; + function _retrieveCurrentValue() + { + $this->current_value = Piwik_Version::VERSION; - $this->recommended_value = Piwik_GetOption(Piwik_UpdateCheck::LATEST_VERSION); - } + $this->recommended_value = Piwik_GetOption(Piwik_UpdateCheck::LATEST_VERSION); + } - function _execTest() { - if (version_compare($this->current_value, '0.5') < 0) { - return PHPSECINFO_TEST_RESULT_WARN; - } + function _execTest() + { + if (version_compare($this->current_value, '0.5') < 0) { + return PHPSECINFO_TEST_RESULT_WARN; + } - if (empty($this->recommended_value)) { - return PHPSECINFO_TEST_RESULT_ERROR; - } + if (empty($this->recommended_value)) { + return PHPSECINFO_TEST_RESULT_ERROR; + } - if (version_compare($this->current_value, $this->recommended_value) >= 0) { - return PHPSECINFO_TEST_RESULT_OK; - } + if (version_compare($this->current_value, $this->recommended_value) >= 0) { + return PHPSECINFO_TEST_RESULT_OK; + } - return PHPSECINFO_TEST_RESULT_NOTICE; - } + return PHPSECINFO_TEST_RESULT_NOTICE; + } - function _setMessages() { - parent::_setMessages(); + function _setMessages() + { + parent::_setMessages(); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', "You are running Piwik ".$this->current_value." (the latest version)."); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', "You are running Piwik ".$this->current_value.". The latest version of Piwik is ".$this->recommended_value."."); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', "You are running Piwik ".$this->current_value." which is no longer supported by the Piwik developers. We recommend running the latest (stable) version of Piwik which includes numerous enhancements, bug fixes, and security fixes."); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_ERROR, 'en', "Unable to determine the latest version of Piwik available."); - } + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', "You are running Piwik " . $this->current_value . " (the latest version)."); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', "You are running Piwik " . $this->current_value . ". The latest version of Piwik is " . $this->recommended_value . "."); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', "You are running Piwik " . $this->current_value . " which is no longer supported by the Piwik developers. We recommend running the latest (stable) version of Piwik which includes numerous enhancements, bug fixes, and security fixes."); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_ERROR, 'en', "Unable to determine the latest version of Piwik available."); + } } diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/CGI/force_redirect.php b/plugins/SecurityInfo/PhpSecInfo/Test/CGI/force_redirect.php index d84d709cc4..cdb0ca2c4f 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/CGI/force_redirect.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/CGI/force_redirect.php @@ -9,7 +9,7 @@ /** * require the PhpSecInfo_Test_Cgi class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Cgi.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Cgi.php'); /** * Test class for cgi force_redirect @@ -20,82 +20,81 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Cgi.php'); class PhpSecInfo_Test_Cgi_Force_Redirect extends PhpSecInfo_Test_Cgi { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "force_redirect"; - - /** - * The recommended setting value - * - * @var mixed - */ - var $recommended_value = TRUE; - - - - function _retrieveCurrentValue() { - $this->current_value = $this->getBooleanIniValue('cgi.force_redirect'); - } - - - - private function skipTest() { - if (strpos(PHP_SAPI, 'cgi') === false) { - return PHP_SAPI . ' SAPI for php'; - } - - // these web servers require cgi.force_redirect = 0 - $webServers = array('Microsoft-IIS', 'OmniHTTPd', 'Xitami'); - if (isset($_SERVER['SERVER_SOFTWARE'])) { - foreach ($webServers as $webServer) { - if (strpos($_SERVER['SERVER_SOFTWARE'], $webServer) === 0) { - return $_SERVER['SERVER_SOFTWARE']; - } - } - } - - return false; - } - - - - /** - * Checks to see if cgi.force_redirect is enabled - * - */ - function _execTest() { - if ($this->current_value == $this->recommended_value) { - return PHPSECINFO_TEST_RESULT_OK; - } - - if ($this->skipTest()) - { - return PHPSECINFO_TEST_RESULT_NOTICE; - } - - return PHPSECINFO_TEST_RESULT_WARN; - } - - - - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); - - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', "force_redirect is enabled, which is the recommended setting"); - $ini = ini_get_all(); - if (isset($ini['cgi.force_redirect'])) { - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', "force_redirect is disabled. In most cases, this is a security vulnerability, but it appears this is not needed because you are running " . $this->skipTest()); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', "force_redirect is disabled. In most cases, this is a serious security vulnerability. Unless you are absolutely sure this is not needed, enable this setting"); - } else { - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', "force_redirect is disabled because php was not compiled with --enable-force-cgi-redirect. In most cases, this is a security vulnerability, but it appears this is not needed because you are running " . $this->skipTest()); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', "force_redirect is disabled because php was not compiled with --enable-force-cgi-redirect. In most cases, this is a serious security vulnerability. Unless you are absolutely sure this is not needed, recompile php with --enable-force-cgi-redirect and enable cgi.force_redirect"); - } - } + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "force_redirect"; + + /** + * The recommended setting value + * + * @var mixed + */ + var $recommended_value = TRUE; + + + function _retrieveCurrentValue() + { + $this->current_value = $this->getBooleanIniValue('cgi.force_redirect'); + } + + + private function skipTest() + { + if (strpos(PHP_SAPI, 'cgi') === false) { + return PHP_SAPI . ' SAPI for php'; + } + + // these web servers require cgi.force_redirect = 0 + $webServers = array('Microsoft-IIS', 'OmniHTTPd', 'Xitami'); + if (isset($_SERVER['SERVER_SOFTWARE'])) { + foreach ($webServers as $webServer) { + if (strpos($_SERVER['SERVER_SOFTWARE'], $webServer) === 0) { + return $_SERVER['SERVER_SOFTWARE']; + } + } + } + + return false; + } + + + /** + * Checks to see if cgi.force_redirect is enabled + * + */ + function _execTest() + { + if ($this->current_value == $this->recommended_value) { + return PHPSECINFO_TEST_RESULT_OK; + } + + if ($this->skipTest()) { + return PHPSECINFO_TEST_RESULT_NOTICE; + } + + return PHPSECINFO_TEST_RESULT_WARN; + } + + + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); + + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', "force_redirect is enabled, which is the recommended setting"); + $ini = ini_get_all(); + if (isset($ini['cgi.force_redirect'])) { + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', "force_redirect is disabled. In most cases, this is a security vulnerability, but it appears this is not needed because you are running " . $this->skipTest()); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', "force_redirect is disabled. In most cases, this is a serious security vulnerability. Unless you are absolutely sure this is not needed, enable this setting"); + } else { + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', "force_redirect is disabled because php was not compiled with --enable-force-cgi-redirect. In most cases, this is a security vulnerability, but it appears this is not needed because you are running " . $this->skipTest()); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', "force_redirect is disabled because php was not compiled with --enable-force-cgi-redirect. In most cases, this is a serious security vulnerability. Unless you are absolutely sure this is not needed, recompile php with --enable-force-cgi-redirect and enable cgi.force_redirect"); + } + } } diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Core/allow_url_fopen.php b/plugins/SecurityInfo/PhpSecInfo/Test/Core/allow_url_fopen.php index 799bb88110..b6df400148 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Core/allow_url_fopen.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Core/allow_url_fopen.php @@ -10,7 +10,7 @@ /** * require the PhpSecInfo_Test_Core class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Core.php'); /** * Test Class for allow_url_fopen @@ -21,59 +21,61 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); */ class PhpSecInfo_Test_Core_Allow_Url_Fopen extends PhpSecInfo_Test_Core { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "allow_url_fopen"; - - /** - * The recommended setting value - * - * @var mixed - */ - var $recommended_value = FALSE; + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "allow_url_fopen"; + /** + * The recommended setting value + * + * @var mixed + */ + var $recommended_value = FALSE; - - function _retrieveCurrentValue() { - $this->current_value = $this->getBooleanIniValue('allow_url_fopen'); - } - - - /** - * Checks to see if allow_url_fopen is enabled - * - */ - function _execTest() { - if ( version_compare(PHP_VERSION, '5.2', '<') ) { /* this is much more severe if we're running < 5.2 */ - if ($this->current_value == $this->recommended_value) { - return PHPSECINFO_TEST_RESULT_OK; - } - return PHPSECINFO_TEST_RESULT_WARN; - } else { /* In 5.2, we'll consider allow_url_fopen "safe" */ - $this->recommended_value = TRUE; - return PHPSECINFO_TEST_RESULT_OK; - } - } + function _retrieveCurrentValue() + { + $this->current_value = $this->getBooleanIniValue('allow_url_fopen'); + } - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); - if ( version_compare(PHP_VERSION, '5.2', '<') ) { /* this is much more severe if we're running < 5.2 */ - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'allow_url_fopen is disabled, which is the recommended setting'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'allow_url_fopen is enabled. This could be a serious security risk. You should disable allow_url_fopen and consider using the PHP cURL functions instead.'); - - } else { - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'You are running PHP 5.2 or greater, which makes allow_url_fopen significantly safer. Make sure allow_url_include is disabled, though'); - } - } + /** + * Checks to see if allow_url_fopen is enabled + * + */ + function _execTest() + { + if (version_compare(PHP_VERSION, '5.2', '<')) { /* this is much more severe if we're running < 5.2 */ + if ($this->current_value == $this->recommended_value) { + return PHPSECINFO_TEST_RESULT_OK; + } + + return PHPSECINFO_TEST_RESULT_WARN; + } else { /* In 5.2, we'll consider allow_url_fopen "safe" */ + $this->recommended_value = TRUE; + return PHPSECINFO_TEST_RESULT_OK; + } + } + + + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); + if (version_compare(PHP_VERSION, '5.2', '<')) { /* this is much more severe if we're running < 5.2 */ + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'allow_url_fopen is disabled, which is the recommended setting'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'allow_url_fopen is enabled. This could be a serious security risk. You should disable allow_url_fopen and consider using the PHP cURL functions instead.'); + + } else { + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'You are running PHP 5.2 or greater, which makes allow_url_fopen significantly safer. Make sure allow_url_include is disabled, though'); + } + } } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Core/allow_url_include.php b/plugins/SecurityInfo/PhpSecInfo/Test/Core/allow_url_include.php index c9bea4d74d..d80b04753c 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Core/allow_url_include.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Core/allow_url_include.php @@ -10,7 +10,7 @@ /** * require the PhpSecInfo_Test_Core class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Core.php'); /** * Test Class for allow_url_include @@ -21,57 +21,60 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); */ class PhpSecInfo_Test_Core_Allow_Url_Include extends PhpSecInfo_Test_Core { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "allow_url_include"; - - - var $recommended_value = FALSE; - - - function _retrieveCurrentValue() { - $this->current_value = $this->getBooleanIniValue('allow_url_include'); - } - - - /** - * Checks to see if allow_url_fopen is enabled - * - */ - function _execTest() { - if ($this->current_value == $this->recommended_value) { - return PHPSECINFO_TEST_RESULT_OK; - } - - return PHPSECINFO_TEST_RESULT_WARN; - } - - - /** - * allow_url_include is only available since PHP 5.2 - * - * @return boolean - */ - function isTestable() { - return version_compare(PHP_VERSION, '5.2', '>='); - } - - - - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); - - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'You are running a version of PHP older than 5.2, and allow_url_include is not available'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'allow_url_include is disabled, which is the recommended setting'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'allow_url_include is enabled. This could be a serious security risk. You should disable allow_url_include and consider using the PHP cURL functions instead.'); - } + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "allow_url_include"; + + + var $recommended_value = FALSE; + + + function _retrieveCurrentValue() + { + $this->current_value = $this->getBooleanIniValue('allow_url_include'); + } + + + /** + * Checks to see if allow_url_fopen is enabled + * + */ + function _execTest() + { + if ($this->current_value == $this->recommended_value) { + return PHPSECINFO_TEST_RESULT_OK; + } + + return PHPSECINFO_TEST_RESULT_WARN; + } + + + /** + * allow_url_include is only available since PHP 5.2 + * + * @return boolean + */ + function isTestable() + { + return version_compare(PHP_VERSION, '5.2', '>='); + } + + + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); + + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'You are running a version of PHP older than 5.2, and allow_url_include is not available'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'allow_url_include is disabled, which is the recommended setting'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'allow_url_include is enabled. This could be a serious security risk. You should disable allow_url_include and consider using the PHP cURL functions instead.'); + } } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Core/display_errors.php b/plugins/SecurityInfo/PhpSecInfo/Test/Core/display_errors.php index e0e93d6748..3908b289a3 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Core/display_errors.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Core/display_errors.php @@ -1,7 +1,7 @@ */ @@ -10,53 +10,56 @@ /** * require the PhpSecInfo_Test_Core class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Core.php'); /** * Test class for display_errors - * + * * @package PhpSecInfo */ class PhpSecInfo_Test_Core_Display_Errors extends PhpSecInfo_Test_Core { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "display_errors"; - - var $recommended_value = FALSE; - - function _retrieveCurrentValue() { - $this->current_value = $this->getBooleanIniValue('display_errors'); - } - - - /** - * Checks to see if display_errors is enabled - * - */ - function _execTest() { - if ($this->current_value == $this->recommended_value) { - return PHPSECINFO_TEST_RESULT_OK; - } - - return PHPSECINFO_TEST_RESULT_NOTICE; - } - - - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); - - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'display_errors is disabled, which is the recommended setting'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'display_errors is enabled. This is not recommended on "production" servers, as it could reveal sensitive information. You should consider disabling this feature'); - } - + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "display_errors"; + + var $recommended_value = FALSE; + + function _retrieveCurrentValue() + { + $this->current_value = $this->getBooleanIniValue('display_errors'); + } + + + /** + * Checks to see if display_errors is enabled + * + */ + function _execTest() + { + if ($this->current_value == $this->recommended_value) { + return PHPSECINFO_TEST_RESULT_OK; + } + + return PHPSECINFO_TEST_RESULT_NOTICE; + } + + + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); + + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'display_errors is disabled, which is the recommended setting'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'display_errors is enabled. This is not recommended on "production" servers, as it could reveal sensitive information. You should consider disabling this feature'); + } + } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Core/expose_php.php b/plugins/SecurityInfo/PhpSecInfo/Test/Core/expose_php.php index 3f26648193..da1666eeff 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Core/expose_php.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Core/expose_php.php @@ -1,7 +1,7 @@ */ @@ -10,55 +10,58 @@ /** * require the PhpSecInfo_Test_Core class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Core.php'); /** * Test class for expose_php - * + * * @package PhpSecInfo */ class PhpSecInfo_Test_Core_Expose_Php extends PhpSecInfo_Test_Core { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "expose_php"; - - var $recommended_value = FALSE; - - function _retrieveCurrentValue() { - $this->current_value = $this->returnBytes(ini_get('expose_php')); - } - - /** - * Checks to see if expose_php is enabled - * - */ - function _execTest() { - - if ($this->current_value == $this->recommended_value) { - return PHPSECINFO_TEST_RESULT_OK; - } - - return PHPSECINFO_TEST_RESULT_NOTICE; - } - - - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); - - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'expose_php is disabled, which is the recommended setting'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'expose_php is enabled. This adds + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "expose_php"; + + var $recommended_value = FALSE; + + function _retrieveCurrentValue() + { + $this->current_value = $this->returnBytes(ini_get('expose_php')); + } + + /** + * Checks to see if expose_php is enabled + * + */ + function _execTest() + { + + if ($this->current_value == $this->recommended_value) { + return PHPSECINFO_TEST_RESULT_OK; + } + + return PHPSECINFO_TEST_RESULT_NOTICE; + } + + + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); + + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'expose_php is disabled, which is the recommended setting'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'expose_php is enabled. This adds the PHP "signature" to the web server header, including the PHP version number. This could attract attackers looking for vulnerable versions of PHP'); - } - + } + } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Core/file_uploads.php b/plugins/SecurityInfo/PhpSecInfo/Test/Core/file_uploads.php index 6a3c822aae..fc4f17e07c 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Core/file_uploads.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Core/file_uploads.php @@ -1,7 +1,7 @@ */ @@ -10,54 +10,56 @@ /** * require the PhpSecInfo_Test_Core class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Core.php'); /** * Test Class for file_uploads - * + * * @package PhpSecInfo */ class PhpSecInfo_Test_Core_File_Uploads extends PhpSecInfo_Test_Core { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "file_uploads"; - - var $recommended_value = FALSE; - - function _retrieveCurrentValue() { - $this->current_value = $this->returnBytes(ini_get('file_uploads')); - } - - /** - * Checks to see if expose_php is enabled - * - */ - function _execTest() { - - if ($this->current_value == $this->recommended_value) { - return PHPSECINFO_TEST_RESULT_OK; - } - - return PHPSECINFO_TEST_RESULT_NOTICE; - } - - - - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); - - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'file_uploads are disabled. Unless you\'re sure you need them, this is the recommended setting'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'file_uploads are enabled. If you do not require file upload capability, consider disabling them.'); - } - + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "file_uploads"; + + var $recommended_value = FALSE; + + function _retrieveCurrentValue() + { + $this->current_value = $this->returnBytes(ini_get('file_uploads')); + } + + /** + * Checks to see if expose_php is enabled + * + */ + function _execTest() + { + + if ($this->current_value == $this->recommended_value) { + return PHPSECINFO_TEST_RESULT_OK; + } + + return PHPSECINFO_TEST_RESULT_NOTICE; + } + + + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); + + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'file_uploads are disabled. Unless you\'re sure you need them, this is the recommended setting'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'file_uploads are enabled. If you do not require file upload capability, consider disabling them.'); + } + } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Core/gid.php b/plugins/SecurityInfo/PhpSecInfo/Test/Core/gid.php index 88a6f3c6c1..89265da2c1 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Core/gid.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Core/gid.php @@ -10,7 +10,7 @@ /** * require the PhpSecInfo_Test_Core class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Core.php'); /** @@ -28,32 +28,34 @@ define ('PHPSECINFO_MIN_SAFE_GID', 100); class PhpSecInfo_Test_Core_Gid extends PhpSecInfo_Test_Core { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "group_id"; - - var $recommended_value = PHPSECINFO_MIN_SAFE_GID; - - - /** - * This test only works under Unix OSes - * - * @return boolean - */ - function isTestable() { - if ($this->osIsWindows()) { - return false; - } elseif ($this->getUnixId() === false) { - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'Functions required to retrieve group ID not available'); - return false; - } - return true; - } - - function _retrieveCurrentValue() { + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "group_id"; + + var $recommended_value = PHPSECINFO_MIN_SAFE_GID; + + + /** + * This test only works under Unix OSes + * + * @return boolean + */ + function isTestable() + { + if ($this->osIsWindows()) { + return false; + } elseif ($this->getUnixId() === false) { + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'Functions required to retrieve group ID not available'); + return false; + } + return true; + } + + function _retrieveCurrentValue() + { $id = $this->getUnixId(); if (is_array($id)) { $lowest_gid = key($id['groups']); @@ -61,33 +63,35 @@ class PhpSecInfo_Test_Core_Gid extends PhpSecInfo_Test_Core } else { $this->current_value = false; } - } - - /** - * Checks the GID of the PHP process to make sure it is above PHPSECINFO_MIN_SAFE_GID - * - * @see PHPSECINFO_MIN_SAFE_GID - */ - function _execTest() { - if ($this->current_value >= $this->recommended_value) { - return PHPSECINFO_TEST_RESULT_OK; - } - - return PHPSECINFO_TEST_RESULT_WARN; - } - - - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); - - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'PHP is executing as what is probably a non-privileged group'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'PHP may be executing as a "privileged" group, which could be a serious security vulnerability.'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'This test will not run on Windows OSes'); - } + } + + /** + * Checks the GID of the PHP process to make sure it is above PHPSECINFO_MIN_SAFE_GID + * + * @see PHPSECINFO_MIN_SAFE_GID + */ + function _execTest() + { + if ($this->current_value >= $this->recommended_value) { + return PHPSECINFO_TEST_RESULT_OK; + } + + return PHPSECINFO_TEST_RESULT_WARN; + } + + + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); + + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'PHP is executing as what is probably a non-privileged group'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'PHP may be executing as a "privileged" group, which could be a serious security vulnerability.'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'This test will not run on Windows OSes'); + } } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Core/magic_quotes_gpc.php b/plugins/SecurityInfo/PhpSecInfo/Test/Core/magic_quotes_gpc.php index 89a0ff6f0d..2a69954f0e 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Core/magic_quotes_gpc.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Core/magic_quotes_gpc.php @@ -7,11 +7,10 @@ */ - /** * require the PhpSecInfo_Test_Core class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Core.php'); /** * Test Class for magic_quotes_gpc @@ -20,61 +19,65 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); */ class PhpSecInfo_Test_Core_Magic_Quotes_GPC extends PhpSecInfo_Test_Core { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "magic_quotes_gpc"; + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "magic_quotes_gpc"; - var $recommended_value = FALSE; + var $recommended_value = FALSE; - function _retrieveCurrentValue() { - $this->current_value = $this->getBooleanIniValue('magic_quotes_gpc'); - } + function _retrieveCurrentValue() + { + $this->current_value = $this->getBooleanIniValue('magic_quotes_gpc'); + } - /** - * magic_quotes_gpc has been removed since PHP 6.0 - * - * @return boolean - */ - function isTestable() { - return version_compare(PHP_VERSION, '6', '<') ; - } + /** + * magic_quotes_gpc has been removed since PHP 6.0 + * + * @return boolean + */ + function isTestable() + { + return version_compare(PHP_VERSION, '6', '<'); + } - /** - * Checks to see if allow_url_fopen is enabled - * - */ - function _execTest() { - if ($this->current_value == $this->recommended_value) { - return PHPSECINFO_TEST_RESULT_OK; - } + /** + * Checks to see if allow_url_fopen is enabled + * + */ + function _execTest() + { + if ($this->current_value == $this->recommended_value) { + return PHPSECINFO_TEST_RESULT_OK; + } - return PHPSECINFO_TEST_RESULT_NOTICE; - } + return PHPSECINFO_TEST_RESULT_NOTICE; + } - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'You are running PHP 6 or later and magic_quotes_gpc has been removed'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'magic_quotes_gpc is disabled, which is the recommended setting'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'magic_quotes_gpc is enabled. This + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'You are running PHP 6 or later and magic_quotes_gpc has been removed'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'magic_quotes_gpc is disabled, which is the recommended setting'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'magic_quotes_gpc is enabled. This feature is inconsistent in blocking attacks, and can in some cases cause data loss with uploaded files. You should not rely on magic_quotes_gpc to block attacks. It is recommended that magic_quotes_gpc be disabled, and input filtering be handled by your PHP scripts'); - } + } } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Core/memory_limit.php b/plugins/SecurityInfo/PhpSecInfo/Test/Core/memory_limit.php index 82a33d8db9..cd76ecd3a8 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Core/memory_limit.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Core/memory_limit.php @@ -12,13 +12,13 @@ /** * require the PhpSecInfo_Test_Core class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Core.php'); /** * The max recommended size for the memory_limit setting, in bytes * */ -define ('PHPSECINFO_MEMORY_LIMIT', 8*1024*1024); +define ('PHPSECINFO_MEMORY_LIMIT', 8 * 1024 * 1024); /** * Test Class for memory_limit setting @@ -29,60 +29,63 @@ class PhpSecInfo_Test_Core_Memory_Limit extends PhpSecInfo_Test_Core { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "memory_limit"; + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "memory_limit"; - var $recommended_value = PHPSECINFO_MEMORY_LIMIT; + var $recommended_value = PHPSECINFO_MEMORY_LIMIT; - function _retrieveCurrentValue() { - $this->current_value = $this->returnBytes(ini_get('memory_limit')); - } + function _retrieveCurrentValue() + { + $this->current_value = $this->returnBytes(ini_get('memory_limit')); + } - /** - * Check to see if the memory_limit setting is enabled. - * - * Test conditions and results: - * OK: memory_limit enabled and set to a value of 8MB or less. - * NOTICE: memory_limit enabled and set to a value greater than 8MB. - * WARNING: memory_limit disabled (compile time option). - * - * @return integer - */ - function _execTest() { - if (!$this->current_value) { - return PHPSECINFO_TEST_RESULT_WARN; - } else if ($this->returnBytes($this->current_value) <= PHPSECINFO_MEMORY_LIMIT) { - return PHPSECINFO_TEST_RESULT_OK; - } - return PHPSECINFO_TEST_RESULT_NOTICE; - } + /** + * Check to see if the memory_limit setting is enabled. + * + * Test conditions and results: + * OK: memory_limit enabled and set to a value of 8MB or less. + * NOTICE: memory_limit enabled and set to a value greater than 8MB. + * WARNING: memory_limit disabled (compile time option). + * + * @return integer + */ + function _execTest() + { + if (!$this->current_value) { + return PHPSECINFO_TEST_RESULT_WARN; + } else if ($this->returnBytes($this->current_value) <= PHPSECINFO_MEMORY_LIMIT) { + return PHPSECINFO_TEST_RESULT_OK; + } + return PHPSECINFO_TEST_RESULT_NOTICE; + } - /** - * Set the messages specific to this test - * - * @access public - * @return null - */ - function _setMessages() { - parent::_setMessages(); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'memory_limit is enabled, and appears to be set + /** + * Set the messages specific to this test + * + * @access public + * @return null + */ + function _setMessages() + { + parent::_setMessages(); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'memory_limit is enabled, and appears to be set to a realistic value.'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'memory_limit is set to a very high value. Are + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'memory_limit is set to a very high value. Are you sure your apps require this much memory? If not, lower the limit, as certain attacks or poor programming practices can lead to exhaustion of server resources. It is recommended that you set this to a realistic value (8M for example) from which it can be expanded as required.'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'memory_limit does not appear to be enabled. This + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'memory_limit does not appear to be enabled. This leaves the server vulnerable to attacks that attempt to exhaust resources and creates an environment where poor programming practices can propagate unchecked. This must be enabled at compile time by including the parameter "--enable-memory-limit" in the configure line. Once enabled "memory_limit" may be set in php.ini to define the maximum amount of memory a script is allowed to allocate.'); - } + } } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Core/open_basedir.php b/plugins/SecurityInfo/PhpSecInfo/Test/Core/open_basedir.php index d5b0d282f6..4363dbdda4 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Core/open_basedir.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Core/open_basedir.php @@ -1,7 +1,7 @@ */ @@ -10,60 +10,63 @@ /** * require the PhpSecInfo_Test_Core class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Core.php'); /** * Test Class for open_basedir - * + * * @package PhpSecInfo */ class PhpSecInfo_Test_Core_Open_Basedir extends PhpSecInfo_Test_Core { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "open_basedir"; + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "open_basedir"; - var $recommended_value = TRUE; + var $recommended_value = TRUE; - - function _retrieveCurrentValue() { - $this->current_value = $this->getBooleanIniValue('open_basedir'); - } - - - /** - * Checks to see if allow_url_fopen is enabled - * - */ - function _execTest() { - if ($this->current_value == $this->recommended_value) { - return PHPSECINFO_TEST_RESULT_OK; - } - return PHPSECINFO_TEST_RESULT_NOTICE; - } - - - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); - - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'open_basedir is enabled, which is the + function _retrieveCurrentValue() + { + $this->current_value = $this->getBooleanIniValue('open_basedir'); + } + + + /** + * Checks to see if allow_url_fopen is enabled + * + */ + function _execTest() + { + if ($this->current_value == $this->recommended_value) { + return PHPSECINFO_TEST_RESULT_OK; + } + + return PHPSECINFO_TEST_RESULT_NOTICE; + } + + + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); + + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'open_basedir is enabled, which is the recommended setting. Keep in mind that other web applications not written in PHP will not be restricted by this setting.'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'open_basedir is disabled. When + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'open_basedir is disabled. When this is enabled, only files that are in the given directory/directories and their subdirectories can be read by PHP scripts. You should consider turning this on. Keep in mind that other web applications not written in PHP will not be restricted by this setting.'); - } - + } + } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Core/post_max_size.php b/plugins/SecurityInfo/PhpSecInfo/Test/Core/post_max_size.php index 09bd2474ec..c2d9b633be 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Core/post_max_size.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Core/post_max_size.php @@ -10,13 +10,13 @@ /** * require the PhpSecInfo_Test_Core class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Core.php'); /** * The max recommended size for the post_max_size setting, in bytes * */ -define ('PHPSECINFO_POST_MAXLIMIT', 1024*256); +define ('PHPSECINFO_POST_MAXLIMIT', 1024 * 256); /** * Test Class for post_max_size @@ -26,46 +26,50 @@ define ('PHPSECINFO_POST_MAXLIMIT', 1024*256); class PhpSecInfo_Test_Core_Post_Max_Size extends PhpSecInfo_Test_Core { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "post_max_size"; + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "post_max_size"; - var $recommended_value = PHPSECINFO_POST_MAXLIMIT; + var $recommended_value = PHPSECINFO_POST_MAXLIMIT; - function _retrieveCurrentValue() { - $this->current_value = $this->returnBytes(ini_get('post_max_size')); - } + function _retrieveCurrentValue() + { + $this->current_value = $this->returnBytes(ini_get('post_max_size')); + } - /** - * Check to see if the post_max_size setting is enabled. - */ - function _execTest() { + /** + * Check to see if the post_max_size setting is enabled. + */ + function _execTest() + { - if ($this->current_value - && $this->current_value <= $this->recommended_value - && $post_max_size != -1) { - return PHPSECINFO_TEST_RESULT_OK; - } + if ($this->current_value + && $this->current_value <= $this->recommended_value + && $post_max_size != -1 + ) { + return PHPSECINFO_TEST_RESULT_OK; + } - return PHPSECINFO_TEST_RESULT_NOTICE; - } + return PHPSECINFO_TEST_RESULT_NOTICE; + } - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'post_max_size is enabled, and appears to + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'post_max_size is enabled, and appears to be a relatively low value'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'post_max_size is not enabled, or is set to + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'post_max_size is not enabled, or is set to a high value. Allowing a large value may open up your server to denial-of-service attacks'); - } + } } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Core/register_globals.php b/plugins/SecurityInfo/PhpSecInfo/Test/Core/register_globals.php index 1caa7e8e30..522813e94b 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Core/register_globals.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Core/register_globals.php @@ -10,7 +10,7 @@ /** * require the PhpSecInfo_Test_Core class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Core.php'); /** @@ -21,57 +21,60 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); class PhpSecInfo_Test_Core_Register_Globals extends PhpSecInfo_Test_Core { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "register_globals"; + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "register_globals"; - var $recommended_value = FALSE; + var $recommended_value = FALSE; - function _retrieveCurrentValue() { - $this->current_value = $this->getBooleanIniValue('register_globals'); - } + function _retrieveCurrentValue() + { + $this->current_value = $this->getBooleanIniValue('register_globals'); + } - /** - * register_globals has been removed since PHP 6.0 - * - * @return boolean - */ - function isTestable() { - return version_compare(PHP_VERSION, '6', '<') ; - } + /** + * register_globals has been removed since PHP 6.0 + * + * @return boolean + */ + function isTestable() + { + return version_compare(PHP_VERSION, '6', '<'); + } + /** + * Checks to see if allow_url_fopen is enabled + * + */ + function _execTest() + { + if ($this->current_value == $this->recommended_value) { + return PHPSECINFO_TEST_RESULT_OK; + } - /** - * Checks to see if allow_url_fopen is enabled - * - */ - function _execTest() { - if ($this->current_value == $this->recommended_value) { - return PHPSECINFO_TEST_RESULT_OK; - } + return PHPSECINFO_TEST_RESULT_WARN; + } - return PHPSECINFO_TEST_RESULT_WARN; - } + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); - - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'You are running PHP 6 or later and register_globals has been removed'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'register_globals is disabled, which is the recommended setting'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'register_globals is enabled. This could be a serious security risk. You should disable register_globals immediately'); - } + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'You are running PHP 6 or later and register_globals has been removed'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'register_globals is disabled, which is the recommended setting'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'register_globals is enabled. This could be a serious security risk. You should disable register_globals immediately'); + } } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Core/uid.php b/plugins/SecurityInfo/PhpSecInfo/Test/Core/uid.php index cbc3ae13ec..a07f268836 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Core/uid.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Core/uid.php @@ -10,7 +10,7 @@ /** * require the PhpSecInfo_Test_Core class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Core.php'); /** @@ -28,66 +28,70 @@ define ('PHPSECINFO_MIN_SAFE_UID', 100); class PhpSecInfo_Test_Core_Uid extends PhpSecInfo_Test_Core { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "user_id"; - - var $recommended_value = PHPSECINFO_MIN_SAFE_UID; - - /** - * This test only works under Unix OSes - * - * @return boolean - */ - function isTestable() { - if ($this->osIsWindows()) { - return false; - } elseif ($this->getUnixId() === false) { - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'Functions required to retrieve user ID not available'); - return false; - } - return true; - } - - - function _retrieveCurrentValue() { - $id = $this->getUnixId(); + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "user_id"; + + var $recommended_value = PHPSECINFO_MIN_SAFE_UID; + + /** + * This test only works under Unix OSes + * + * @return boolean + */ + function isTestable() + { + if ($this->osIsWindows()) { + return false; + } elseif ($this->getUnixId() === false) { + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'Functions required to retrieve user ID not available'); + return false; + } + return true; + } + + + function _retrieveCurrentValue() + { + $id = $this->getUnixId(); if (is_array($id)) { $this->current_value = $id['uid']; } else { $this->current_value = false; } - } - - /** - * Checks the GID of the PHP process to make sure it is above PHPSECINFO_MIN_SAFE_UID - * - * @see PHPSECINFO_MIN_SAFE_UID - */ - function _execTest() { - if ($this->current_value >= $this->recommended_value) { - return PHPSECINFO_TEST_RESULT_OK; - } - - return PHPSECINFO_TEST_RESULT_WARN; - } - - - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); - - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'PHP is executing as what is probably a non-privileged user'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'PHP may be executing as a "privileged" user, which could be a serious security vulnerability.'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'This test will not run on Windows OSes'); - } + } + + /** + * Checks the GID of the PHP process to make sure it is above PHPSECINFO_MIN_SAFE_UID + * + * @see PHPSECINFO_MIN_SAFE_UID + */ + function _execTest() + { + if ($this->current_value >= $this->recommended_value) { + return PHPSECINFO_TEST_RESULT_OK; + } + + return PHPSECINFO_TEST_RESULT_WARN; + } + + + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); + + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'PHP is executing as what is probably a non-privileged user'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'PHP may be executing as a "privileged" user, which could be a serious security vulnerability.'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'This test will not run on Windows OSes'); + } } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Core/upload_max_filesize.php b/plugins/SecurityInfo/PhpSecInfo/Test/Core/upload_max_filesize.php index bc0b66b42b..07502a4153 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Core/upload_max_filesize.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Core/upload_max_filesize.php @@ -9,13 +9,13 @@ /** * require the PhpSecInfo_Test_Core class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Core.php'); /** * The max recommended size for the upload_max_filesize setting, in bytes * */ -define ('PHPSECINFO_UPLOAD_MAXLIMIT', 1024*256); +define ('PHPSECINFO_UPLOAD_MAXLIMIT', 1024 * 256); /** @@ -27,44 +27,48 @@ class PhpSecInfo_Test_Core_Upload_Max_Filesize extends PhpSecInfo_Test_Core { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "upload_max_filesize"; - - var $recommended_value = PHPSECINFO_UPLOAD_MAXLIMIT; - - function _retrieveCurrentValue() { - $this->current_value = $this->returnBytes(ini_get('upload_max_filesize')); - } - - /** - * Check to see if the post_max_size setting is enabled. - */ - function _execTest() { - - if ($this->current_value - && $this->current_value <= $this->recommended_value - && $post_max_size != -1) { - return PHPSECINFO_TEST_RESULT_OK; - } - - return PHPSECINFO_TEST_RESULT_NOTICE; - } - - - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); - - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'upload_max_filesize is enabled, and appears to be a relatively low value.'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'upload_max_filesize is not enabled, or is set to a high value. Are you sure your apps require uploading files of this size? If not, lower the limit, as large file uploads can impact server performance'); - } + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "upload_max_filesize"; + + var $recommended_value = PHPSECINFO_UPLOAD_MAXLIMIT; + + function _retrieveCurrentValue() + { + $this->current_value = $this->returnBytes(ini_get('upload_max_filesize')); + } + + /** + * Check to see if the post_max_size setting is enabled. + */ + function _execTest() + { + + if ($this->current_value + && $this->current_value <= $this->recommended_value + && $post_max_size != -1 + ) { + return PHPSECINFO_TEST_RESULT_OK; + } + + return PHPSECINFO_TEST_RESULT_NOTICE; + } + + + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); + + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'upload_max_filesize is enabled, and appears to be a relatively low value.'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'upload_max_filesize is not enabled, or is set to a high value. Are you sure your apps require uploading files of this size? If not, lower the limit, as large file uploads can impact server performance'); + } } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Core/upload_tmp_dir.php b/plugins/SecurityInfo/PhpSecInfo/Test/Core/upload_tmp_dir.php index e0b173dbea..4a83e0f7cd 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Core/upload_tmp_dir.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Core/upload_tmp_dir.php @@ -9,7 +9,7 @@ /** * require the PhpSecInfo_Test_Core class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Core.php'); /** * Test Class for upload_tmp_dir @@ -19,82 +19,87 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Core.php'); class PhpSecInfo_Test_Core_Upload_Tmp_Dir extends PhpSecInfo_Test_Core { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "upload_tmp_dir"; + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "upload_tmp_dir"; - var $recommended_value = "A non-world readable/writable directory"; + var $recommended_value = "A non-world readable/writable directory"; - function _retrieveCurrentValue() { - $this->current_value = ini_get('upload_tmp_dir'); + function _retrieveCurrentValue() + { + $this->current_value = ini_get('upload_tmp_dir'); - if( empty($this->current_value) ) { - if (function_exists("sys_get_temp_dir")) { - $this->current_value = sys_get_temp_dir(); - } else { - $this->current_value = $this->sys_get_temp_dir(); - } - } - } + if (empty($this->current_value)) { + if (function_exists("sys_get_temp_dir")) { + $this->current_value = sys_get_temp_dir(); + } else { + $this->current_value = $this->sys_get_temp_dir(); + } + } + } - /** - * We are disabling this function on Windows OSes right now until - * we can be certain of the proper way to check world-readability - * - * @return unknown - */ - function isTestable() { - if ($this->osIsWindows()) { - return FALSE; - } else { - return TRUE; - } - } + /** + * We are disabling this function on Windows OSes right now until + * we can be certain of the proper way to check world-readability + * + * @return unknown + */ + function isTestable() + { + if ($this->osIsWindows()) { + return FALSE; + } else { + return TRUE; + } + } - /** - * Check if upload_tmp_dir matches PHPSECINFO_TEST_COMMON_TMPDIR, or is word-writable - * - * This is still unix-specific, and it's possible that for now - * this test should be disabled under Windows builds. - * - * @see PHPSECINFO_TEST_COMMON_TMPDIR - */ - function _execTest() { + /** + * Check if upload_tmp_dir matches PHPSECINFO_TEST_COMMON_TMPDIR, or is word-writable + * + * This is still unix-specific, and it's possible that for now + * this test should be disabled under Windows builds. + * + * @see PHPSECINFO_TEST_COMMON_TMPDIR + */ + function _execTest() + { - $perms = @fileperms($this->current_value); - if ($perms === false) { - return PHPSECINFO_TEST_RESULT_WARN; - } else if ($this->current_value - && !preg_match("|".PHPSECINFO_TEST_COMMON_TMPDIR."/?|", $this->current_value) - && ! ($perms & 0x0004) - && ! ($perms & 0x0002) ) { - return PHPSECINFO_TEST_RESULT_OK; - } + $perms = @fileperms($this->current_value); + if ($perms === false) { + return PHPSECINFO_TEST_RESULT_WARN; + } else if ($this->current_value + && !preg_match("|" . PHPSECINFO_TEST_COMMON_TMPDIR . "/?|", $this->current_value) + && !($perms & 0x0004) + && !($perms & 0x0002) + ) { + return PHPSECINFO_TEST_RESULT_OK; + } - // rewrite current_value to display perms - $this->current_value .= " (".substr(sprintf('%o', $perms), -4).")"; + // rewrite current_value to display perms + $this->current_value .= " (" . substr(sprintf('%o', $perms), -4) . ")"; - return PHPSECINFO_TEST_RESULT_NOTICE; - } + return PHPSECINFO_TEST_RESULT_NOTICE; + } - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'Test not run -- currently disabled on Windows OSes'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'upload_tmp_dir is enabled, which is the + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'Test not run -- currently disabled on Windows OSes'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'upload_tmp_dir is enabled, which is the recommended setting. Make sure your upload_tmp_dir path is not world-readable'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'unable to retrieve file permissions on upload_tmp_dir'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'upload_tmp_dir is disabled, or is set to a + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'unable to retrieve file permissions on upload_tmp_dir'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'upload_tmp_dir is disabled, or is set to a common world-writable directory. This typically allows other users on this server to access temporary copies of files uploaded via your PHP scripts. You should set upload_tmp_dir to a non-world-readable directory'); - } + } } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Curl/file_support.php b/plugins/SecurityInfo/PhpSecInfo/Test/Curl/file_support.php index 816d747259..b02428f62f 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Curl/file_support.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Curl/file_support.php @@ -9,7 +9,7 @@ /** * require the PhpSecInfo_Test_Curl class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Curl.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Curl.php'); /** * Test class for CURL file_support @@ -24,52 +24,54 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Curl.php'); class PhpSecInfo_Test_Curl_File_Support extends PhpSecInfo_Test_Curl { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "file_support"; + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "file_support"; - var $recommended_value = '5.1.6+ or 4.4.4+'; + var $recommended_value = '5.1.6+ or 4.4.4+'; - function _retrieveCurrentValue() { - $this->current_value = PHP_VERSION; - } - + function _retrieveCurrentValue() + { + $this->current_value = PHP_VERSION; + } - /** - * Checks to see if libcurl's "file://" support is enabled by examining the "protocols" array - * in the info returned from curl_version() - * @return integer - * - */ - function _execTest() { - $curlinfo = curl_version(); + /** + * Checks to see if libcurl's "file://" support is enabled by examining the "protocols" array + * in the info returned from curl_version() + * @return integer + * + */ + function _execTest() + { - if ( version_compare($this->current_value, '5.1.6', '>=') || - (version_compare($this->current_value, '4.4.4', '>=')) && ( version_compare($this->current_value, '5', '<') ) - ) { - return PHPSECINFO_TEST_RESULT_OK; - } else { - return PHPSECINFO_TEST_RESULT_WARN; - } + $curlinfo = curl_version(); - } + if (version_compare($this->current_value, '5.1.6', '>=') || + (version_compare($this->current_value, '4.4.4', '>=')) && (version_compare($this->current_value, '5', '<')) + ) { + return PHPSECINFO_TEST_RESULT_OK; + } else { + return PHPSECINFO_TEST_RESULT_WARN; + } + } - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', "You are running PHP 4.4.4 or higher, or PHP 5.1.6 or higher. These versions fix the security hole present in the cURL functions that allow it to bypass safe_mode and open_basedir restrictions."); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', "A security hole present in your version of PHP allows the cURL functions to bypass safe_mode and open_basedir restrictions. You should upgrade to the latest version of PHP."); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', "You are running PHP 4.4.4 or higher, or PHP 5.1.6 or higher. These versions fix the security hole present in the cURL functions that allow it to bypass safe_mode and open_basedir restrictions."); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', "A security hole present in your version of PHP allows the cURL functions to bypass safe_mode and open_basedir restrictions. You should upgrade to the latest version of PHP."); - } + } } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Session/save_path.php b/plugins/SecurityInfo/PhpSecInfo/Test/Session/save_path.php index 910e5c7fd2..f04d087802 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Session/save_path.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Session/save_path.php @@ -9,7 +9,7 @@ /** * require the PhpSecInfo_Test_Core class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Session.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Session.php'); /** * Test class for session save_path @@ -19,87 +19,92 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Session.php'); class PhpSecInfo_Test_Session_Save_Path extends PhpSecInfo_Test_Session { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "save_path"; - - var $recommended_value = "A non-world readable/writable directory"; - - function _retrieveCurrentValue() { - $this->current_value = ini_get('session.save_path'); - - if( empty($this->current_value) ) { - if (function_exists("sys_get_temp_dir")) { - $this->current_value = sys_get_temp_dir(); - } else { - $this->current_value = $this->sys_get_temp_dir(); - } - } - - if( preg_match('/^[0-9]+;(.+)/', $this->current_value, $matches) ) { - $this->current_value = $matches[1]; - } - } - - - /** - * We are disabling this function on Windows OSes right now until - * we can be certain of the proper way to check world-readability - * - * @return unknown - */ - function isTestable() { - if ($this->osIsWindows()) { - return FALSE; - } else { - return TRUE; - } - } - - - /** - * Check if session.save_path matches PHPSECINFO_TEST_COMMON_TMPDIR, or is word-writable - * - * This is still unix-specific, and it's possible that for now - * this test should be disabled under Windows builds. - * - * @see PHPSECINFO_TEST_COMMON_TMPDIR - */ - function _execTest() { - - $perms = @fileperms($this->current_value); - if ($perms === false) { - return PHPSECINFO_TEST_RESULT_WARN; - } else if ($this->current_value - && !preg_match("|".PHPSECINFO_TEST_COMMON_TMPDIR."/?|", $this->current_value) - && ! ($perms & 0x0004) - && ! ($perms & 0x0002) ) { - return PHPSECINFO_TEST_RESULT_OK; - } - - // rewrite current_value to display perms - $this->current_value .= " (".substr(sprintf('%o', $perms), -4).")"; - - return PHPSECINFO_TEST_RESULT_NOTICE; - } - - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); - - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'Test not run -- currently disabled on Windows OSes'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'save_path is enabled, which is the + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "save_path"; + + var $recommended_value = "A non-world readable/writable directory"; + + function _retrieveCurrentValue() + { + $this->current_value = ini_get('session.save_path'); + + if (empty($this->current_value)) { + if (function_exists("sys_get_temp_dir")) { + $this->current_value = sys_get_temp_dir(); + } else { + $this->current_value = $this->sys_get_temp_dir(); + } + } + + if (preg_match('/^[0-9]+;(.+)/', $this->current_value, $matches)) { + $this->current_value = $matches[1]; + } + } + + + /** + * We are disabling this function on Windows OSes right now until + * we can be certain of the proper way to check world-readability + * + * @return unknown + */ + function isTestable() + { + if ($this->osIsWindows()) { + return FALSE; + } else { + return TRUE; + } + } + + + /** + * Check if session.save_path matches PHPSECINFO_TEST_COMMON_TMPDIR, or is word-writable + * + * This is still unix-specific, and it's possible that for now + * this test should be disabled under Windows builds. + * + * @see PHPSECINFO_TEST_COMMON_TMPDIR + */ + function _execTest() + { + + $perms = @fileperms($this->current_value); + if ($perms === false) { + return PHPSECINFO_TEST_RESULT_WARN; + } else if ($this->current_value + && !preg_match("|" . PHPSECINFO_TEST_COMMON_TMPDIR . "/?|", $this->current_value) + && !($perms & 0x0004) + && !($perms & 0x0002) + ) { + return PHPSECINFO_TEST_RESULT_OK; + } + + // rewrite current_value to display perms + $this->current_value .= " (" . substr(sprintf('%o', $perms), -4) . ")"; + + return PHPSECINFO_TEST_RESULT_NOTICE; + } + + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); + + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'Test not run -- currently disabled on Windows OSes'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'save_path is enabled, which is the recommended setting. Make sure your save_path path is not world-readable'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'unable to retrieve file permissions on save_path'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'save_path is disabled, or is set to a + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'unable to retrieve file permissions on save_path'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'save_path is disabled, or is set to a common world-writable directory. This typically allows other users on this server to access session files. You should set save_path to a non-world-readable directory'); - } + } } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Session/use_trans_sid.php b/plugins/SecurityInfo/PhpSecInfo/Test/Session/use_trans_sid.php index f7f3f2e4a9..c3a61e5d59 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Session/use_trans_sid.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Session/use_trans_sid.php @@ -10,7 +10,7 @@ /** * require the PhpSecInfo_Test_Session class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Session.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Session.php'); /** * Test class for session use_trans_sid @@ -21,46 +21,49 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Session.php'); class PhpSecInfo_Test_Session_Use_Trans_Sid extends PhpSecInfo_Test_Session { - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = "use_trans_sid"; + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = "use_trans_sid"; - var $recommended_value = FALSE; + var $recommended_value = FALSE; - function _retrieveCurrentValue() { - $this->current_value = $this->getBooleanIniValue('session.use_trans_sid'); - } + function _retrieveCurrentValue() + { + $this->current_value = $this->getBooleanIniValue('session.use_trans_sid'); + } - /** - * Checks to see if allow_url_fopen is enabled - * - */ - function _execTest() { - if ($this->current_value == $this->recommended_value) { - return PHPSECINFO_TEST_RESULT_OK; - } + /** + * Checks to see if allow_url_fopen is enabled + * + */ + function _execTest() + { + if ($this->current_value == $this->recommended_value) { + return PHPSECINFO_TEST_RESULT_OK; + } - return PHPSECINFO_TEST_RESULT_NOTICE; - } + return PHPSECINFO_TEST_RESULT_NOTICE; + } - /** - * Set the messages specific to this test - * - */ - function _setMessages() { - parent::_setMessages(); + /** + * Set the messages specific to this test + * + */ + function _setMessages() + { + parent::_setMessages(); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'use_trans_sid is disabled, which is the recommended setting'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'use_trans_sid is enabled. This makes session hijacking easier. Consider disabling this feature'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'use_trans_sid is disabled, which is the recommended setting'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'use_trans_sid is enabled. This makes session hijacking easier. Consider disabling this feature'); - } + } } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Suhosin/extension.php b/plugins/SecurityInfo/PhpSecInfo/Test/Suhosin/extension.php index 50e4d05359..f6a0d340d2 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Suhosin/extension.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Suhosin/extension.php @@ -9,7 +9,7 @@ /** * require the PhpSecInfo_Test_Suhosin class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Suhosin.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Suhosin.php'); /** * Test class for Suhosin extension @@ -21,26 +21,29 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Suhosin.php'); */ class PhpSecInfo_Test_Suhosin_Extension extends PhpSecInfo_Test_Suhosin { - var $test_name = "Suhosin extension"; + var $test_name = "Suhosin extension"; - var $recommended_value = true; + var $recommended_value = true; - function _retrieveCurrentValue() { - $this->current_value = extension_loaded('suhosin'); - } + function _retrieveCurrentValue() + { + $this->current_value = extension_loaded('suhosin'); + } - function _execTest() { - if ( $this->current_value === true ) { - return PHPSECINFO_TEST_RESULT_OK; - } else { - return PHPSECINFO_TEST_RESULT_NOTICE; - } - } + function _execTest() + { + if ($this->current_value === true) { + return PHPSECINFO_TEST_RESULT_OK; + } else { + return PHPSECINFO_TEST_RESULT_NOTICE; + } + } - function _setMessages() { - parent::_setMessages(); + function _setMessages() + { + parent::_setMessages(); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', "You are running PHP with the Suhosin extension loaded. This extension provides high-level runtime protections, and additional filtering and logging features."); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', "You are not running PHP with the Suhosin extension loaded. We recommend both the patch and extension for low- and high-level protections including transparent cookie encryption and remote inclusion vulnerabilities."); - } + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', "You are running PHP with the Suhosin extension loaded. This extension provides high-level runtime protections, and additional filtering and logging features."); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', "You are not running PHP with the Suhosin extension loaded. We recommend both the patch and extension for low- and high-level protections including transparent cookie encryption and remote inclusion vulnerabilities."); + } } diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Suhosin/patch.php b/plugins/SecurityInfo/PhpSecInfo/Test/Suhosin/patch.php index 6e982edae2..ec475281bd 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Suhosin/patch.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Suhosin/patch.php @@ -9,7 +9,7 @@ /** * require the PhpSecInfo_Test_Suhosin class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Suhosin.php'); +require_once(PHPSECINFO_BASE_DIR . '/Test/Test_Suhosin.php'); /** * Test class for Suhosin @@ -21,35 +21,38 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test_Suhosin.php'); */ class PhpSecInfo_Test_Suhosin_Patch extends PhpSecInfo_Test_Suhosin { - var $test_name = "Suhosin patch"; - - var $recommended_value = true; - - function _retrieveCurrentValue() { - if (preg_match('/Suhosin/', $_SERVER['SERVER_SOFTWARE'])) { - $this->current_value = true; - } else { - $this->current_value = false; - - $constants = get_defined_constants(); - if(isset($constants['SUHOSIN_PATCH']) && $constants['SUHOSIN_PATCH'] == 1) { - $this->current_value = true; - } - } - } - - function _execTest() { - if ( $this->current_value === true ) { - return PHPSECINFO_TEST_RESULT_OK; - } else { - return PHPSECINFO_TEST_RESULT_NOTICE; - } - } - - function _setMessages() { - parent::_setMessages(); - - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', "You are running PHP with the Suhosin patch applied against the PHP core. This patch implements various low-level protections against (for example) buffer overflows and format string vulnerabilities."); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', "You are not running PHP with the Suhosin patch applied. We recommend both the patch and extension for low- and high-level protections against (for example) buffer overflows and format string vulnerabilities."); - } + var $test_name = "Suhosin patch"; + + var $recommended_value = true; + + function _retrieveCurrentValue() + { + if (preg_match('/Suhosin/', $_SERVER['SERVER_SOFTWARE'])) { + $this->current_value = true; + } else { + $this->current_value = false; + + $constants = get_defined_constants(); + if (isset($constants['SUHOSIN_PATCH']) && $constants['SUHOSIN_PATCH'] == 1) { + $this->current_value = true; + } + } + } + + function _execTest() + { + if ($this->current_value === true) { + return PHPSECINFO_TEST_RESULT_OK; + } else { + return PHPSECINFO_TEST_RESULT_NOTICE; + } + } + + function _setMessages() + { + parent::_setMessages(); + + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', "You are running PHP with the Suhosin patch applied against the PHP core. This patch implements various low-level protections against (for example) buffer overflows and format string vulnerabilities."); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', "You are not running PHP with the Suhosin patch applied. We recommend both the patch and extension for low- and high-level protections against (for example) buffer overflows and format string vulnerabilities."); + } } diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Test.php b/plugins/SecurityInfo/PhpSecInfo/Test/Test.php index b4902a718c..978309879d 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Test.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Test.php @@ -9,8 +9,7 @@ /** * require the main PhpSecInfo class */ -require_once(PHPSECINFO_BASE_DIR.'/PhpSecInfo.php'); - +require_once(PHPSECINFO_BASE_DIR . '/PhpSecInfo.php'); define ('PHPSECINFO_TEST_RESULT_OK', -1); @@ -36,540 +35,554 @@ define ('PHPSECINFO_TEST_MOREINFO_BASEURL', 'http://phpsec.org/projects/phpsecin class PhpSecInfo_Test { - /** - * This value is used to group test results together. - * - * For example, all tests related to the mysql lib should be grouped under "mysql." - * - * @var string - */ - var $test_group = 'misc'; - - - /** - * This should be a unique, human-readable identifier for this test - * - * @var string - */ - var $test_name = 'misc_test'; - - - /** - * This is the recommended value the test will be looking for - * - * @var mixed - */ - var $recommended_value = "bar"; - - - /** - * The result returned from the test - * - * @var integer - */ - var $_result = PHPSECINFO_TEST_RESULT_NOTRUN; - - - /** - * The message corresponding to the result of the test - * - * @var string - */ - var $_message; - - - /** - * the language code. Should be a pointer to the setting in the PhpSecInfo object - * - * @var string - */ - var $_language = PHPSECINFO_LANG_DEFAULT; - - /** - * Enter description here... - * - * @var mixed - */ - var $current_value; - - /** - * This is a hash of messages that correspond to various test result levels. - * - * There are five messages, each corresponding to one of the result constants - * (PHPSECINFO_TEST_RESULT_OK, PHPSECINFO_TEST_RESULT_NOTICE, PHPSECINFO_TEST_RESULT_WARN, - * PHPSECINFO_TEST_RESULT_ERROR, PHPSECINFO_TEST_RESULT_NOTRUN) - * - * - * @var array array - */ - var $_messages = array(); - - - - - /** - * Constructor for Test skeleton class - * - * @return PhpSecInfo_Test - */ - function PhpSecInfo_Test() { - //$this->_setTestValues(); - - $this->_retrieveCurrentValue(); - //$this->setRecommendedValue(); - - $this->_setMessages(); - } - - - /** - * Determines whether or not it's appropriate to run this test (for example, if - * this test is for a particular library, it shouldn't be run if the lib isn't - * loaded). - * - * This is a terrible name, but I couldn't think of a better one atm. - * - * @return boolean - */ - function isTestable() { - - return true; - } - - - /** - * The "meat" of the test. This is where the real test code goes. You should override this when extending - * - * @return integer - */ - function _execTest() { - - return PHPSECINFO_TEST_RESULT_NOTRUN; - } - - - /** - * This function loads up result messages into the $this->_messages array. - * - * Using this method rather than setting $this->_messages directly allows result - * messages to be inherited. This is broken out into a separate function rather - * than the constructor for ease of extension purposes (php4 is whack, man). - * - */ - function _setMessages() { - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'This setting should be safe'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'This could potentially be a security issue'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'This setting may be a serious security problem'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_ERROR, 'en', 'There was an error running this test'); - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'This test cannot be run'); - } - - - /** - * Placeholder - extend for tests - * - */ - function _retrieveCurrentValue() { - $this->current_value = "foo"; - } - - - - /** - * This is the wrapper that executes the test and sets the result code and message - */ - function test() { - $result = $this->_execTest(); - $this->_setResult($result); - - } - - - - /** - * Retrieves the result - * - * @return integer - */ - function getResult() { - return $this->_result; - } - - - - - /** - * Retrieves the message for the current result - * - * @return string - */ - function getMessage() { - if (!isset($this->_message) || empty($this->_message)) { - $this->_setMessage($this->_result, $this->_language); - } - - return $this->_message; - } - - - - /** - * Sets the message for a given result code and language - * - * - * $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'This test cannot be run'); - * - * - * @param integer $result_code - * @param string $language_code - * @param string $message - * - */ - function setMessageForResult($result_code, $language_code, $message) { - - if ( !isset($this->_messages[$result_code]) ) { - $this->_messages[$result_code] = array(); - } - - if ( !is_array($this->_messages[$result_code]) ) { - $this->_messages[$result_code] = array(); - } - - $this->_messages[$result_code][$language_code] = $message; - - } - - - - - /** - * returns the current value. This function should be used to access the - * value for display. All values are cast as strings - * - * @return string - */ - function getCurrentTestValue() { - return $this->getStringValue($this->current_value); - } - - /** - * returns the recommended value. This function should be used to access the - * value for display. All values are cast as strings - * - * @return string - */ - function getRecommendedTestValue() { - return $this->getStringValue($this->recommended_value); - } - - - /** - * Sets the result code - * - * @param integer $result_code - */ - function _setResult($result_code) { - $this->_result = $result_code; - } - - - /** - * Sets the $this->_message variable based on the passed result and language codes - * - * @param integer $result_code - * @param string $language_code - */ - function _setMessage($result_code, $language_code) { - $messages = $this->_messages[$result_code]; - $message = $messages[$language_code]; - $this->_message = $message; - } - - - /** - * Returns a link to a page with detailed information about the test - * - * URL is formatted as PHPSECINFO_TEST_MOREINFO_BASEURL + testName - * - * @see PHPSECINFO_TEST_MOREINFO_BASEURL - * - * @return string|boolean - */ - function getMoreInfoURL() { - if ($tn = $this->getTestName()) { - return PHPSECINFO_TEST_MOREINFO_BASEURL.strtolower("{$tn}.html"); - } else { - return false; - } - } - - - - - /** - * This retrieves the name of this test. - * - * If a name has not been set, this returns a formatted version of the class name. - * - * @return string - */ - function getTestName() { - if (isset($this->test_name) && !empty($this->test_name)) { - return $this->test_name; - } else { - return ucwords( - str_replace('_', ' ', - get_class($this) - ) - ); - } - - } - - - /** - * sets the test name - * - * @param string $test_name - */ - function setTestName($test_name) { - $this->test_name = $test_name; - } - - - /** - * Returns the test group this test belongs to - * - * @return string - */ - function getTestGroup() { - return $this->test_group; - } - - - /** - * sets the test group - * - * @param string $test_group - */ - function setTestGroup($test_group) { - $this->test_group = $test_group; - } - - - /** - * This function takes the shorthand notation used in memory limit settings for PHP - * and returns the byte value. Totally stolen from http://us3.php.net/manual/en/function.ini-get.php - * - * - * echo 'post_max_size in bytes = ' . $this->return_bytes(ini_get('post_max_size')); - * - * - * @link http://php.net/manual/en/function.ini-get.php - * @param string $val - * @return integer - */ - function returnBytes($val) { - $val = trim($val); - - if ( (int)$val === 0 ) { - return 0; - } - - $last = strtolower($val{strlen($val)-1}); - switch($last) { - // The 'G' modifier is available since PHP 5.1.0 - case 'g': - $val *= 1024; - case 'm': - $val *= 1024; - case 'k': - $val *= 1024; - } - - return $val; - } - - - /** - * This just does the usual PHP string casting, except for - * the boolean FALSE value, where the string "0" is returned - * instead of an empty string - * - * @param mixed $val - * @return string - */ - function getStringValue($val) { - if ($val === FALSE) { - return "0"; - } else { - return (string)$val; - } - } - - - /** - * This method converts the several possible return values from - * allegedly "boolean" ini settings to proper booleans - * - * Properly converted input values are: 'off', 'on', 'false', 'true', '', '0', '1' - * (the last two might not be neccessary, but I'd rather be safe) - * - * If the ini_value doesn't match any of those, the value is returned as-is. - * - * @param string $ini_key the ini_key you need the value of - * @return boolean|mixed - */ - function getBooleanIniValue($ini_key) { - - $ini_val = ini_get($ini_key); - - switch ( strtolower($ini_val) ) { - - case 'off': - return false; - break; - case 'on': - return true; - break; - case 'false': - return false; - break; - case 'true': - return true; - break; - case '0': - return false; - break; - case '1': - return true; - break; - case '': - return false; - break; - default: - return $ini_val; - - } - - } - - /** - * sys_get_temp_dir provides some temp dir detection capability - * that is lacking in versions of PHP that do not have the - * sys_get_temp_dir() function - * - * @return string|NULL - */ - function sys_get_temp_dir() { - // Try to get from environment variable - $vars = array('TMP', 'TMPDIR', 'TEMP'); - foreach($vars as $var) { - $tmp = getenv($var); - if ( !empty($tmp) ) { - return realpath( $tmp ); - } - } - return NULL; - } - - - /** - * A quick function to determine whether we're running on Windows. - * Uses the PHP_OS constant. - * - * @return boolean - */ - function osIsWindows() { - if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { - return true; - } else { - return false; - } - } - - - /** - * Returns an array of data returned from the UNIX 'id' command - * - * includes uid, username, gid, groupname, and groups (if "exec" - * is enabled). Groups is an array of all the groups the user - * belongs to. Keys are the group ids, values are the group names. - * - * returns FALSE if no suitable function is available to retrieve - * the data - * - * @return array|boolean - */ - function getUnixId() { - - if ($this->osIsWindows()) { - return false; - } - - $success = false; - - - if (function_exists("exec") && !PhpSecInfo_Test::getBooleanIniValue('safe_mode')) { - $id_raw = exec('id'); - // uid=1000(coj) gid=1000(coj) groups=1000(coj),1001(admin) - preg_match( "|uid=(\d+)\((\S+)\)\s+gid=(\d+)\((\S+)\)\s+groups=(.+)|i", - $id_raw, - $matches); - - if (!$matches) { - /** - * for some reason the output from 'id' wasn't as we expected. - * return false so the test doesn't run. - */ - $success = false; - } else { - $id_data = array( 'uid'=>$matches[1], - 'username'=>$matches[2], - 'gid'=>$matches[3], - 'group'=>$matches[4] ); - - $groups = array(); - if ($matches[5]) { - $gs = $matches[5]; - $gs = explode(',', $gs); - foreach ($gs as $groupstr) { - if (preg_match("/(\d+)\(([^\)]+)\)/", $groupstr, $subs)) { - $groups[$subs[1]] = $subs[2]; - } else { - $groups[$groupstr] = ''; - } - } - ksort($groups); - } - $id_data['groups'] = $groups; - $success = true; - } - - } - - if (!$success && function_exists("posix_getpwuid") && function_exists("posix_geteuid") - && function_exists('posix_getgrgid') && function_exists('posix_getgroups') ) { - $data = posix_getpwuid( posix_getuid() ); - $id_data['uid'] = $data['uid']; - $id_data['username'] = $data['name']; - $id_data['gid'] = $data['gid']; - //$group_data = posix_getgrgid( posix_getegid() ); - //$id_data['group'] = $group_data['name']; - $id_data['groups'] = array(); - $groups = posix_getgroups(); - foreach ( $groups as $gid ) { - //$group_data = posix_getgrgid(posix_getgid()); - $id_data['groups'][$gid] = ''; - } - $success = true; - } - - if ($success) { - return $id_data; - } else { - return false; - } - } + /** + * This value is used to group test results together. + * + * For example, all tests related to the mysql lib should be grouped under "mysql." + * + * @var string + */ + var $test_group = 'misc'; + + + /** + * This should be a unique, human-readable identifier for this test + * + * @var string + */ + var $test_name = 'misc_test'; + + + /** + * This is the recommended value the test will be looking for + * + * @var mixed + */ + var $recommended_value = "bar"; + + + /** + * The result returned from the test + * + * @var integer + */ + var $_result = PHPSECINFO_TEST_RESULT_NOTRUN; + + + /** + * The message corresponding to the result of the test + * + * @var string + */ + var $_message; + + + /** + * the language code. Should be a pointer to the setting in the PhpSecInfo object + * + * @var string + */ + var $_language = PHPSECINFO_LANG_DEFAULT; + + /** + * Enter description here... + * + * @var mixed + */ + var $current_value; + + /** + * This is a hash of messages that correspond to various test result levels. + * + * There are five messages, each corresponding to one of the result constants + * (PHPSECINFO_TEST_RESULT_OK, PHPSECINFO_TEST_RESULT_NOTICE, PHPSECINFO_TEST_RESULT_WARN, + * PHPSECINFO_TEST_RESULT_ERROR, PHPSECINFO_TEST_RESULT_NOTRUN) + * + * + * @var array array + */ + var $_messages = array(); + + + /** + * Constructor for Test skeleton class + * + * @return PhpSecInfo_Test + */ + function PhpSecInfo_Test() + { + //$this->_setTestValues(); + + $this->_retrieveCurrentValue(); + //$this->setRecommendedValue(); + + $this->_setMessages(); + } + + + /** + * Determines whether or not it's appropriate to run this test (for example, if + * this test is for a particular library, it shouldn't be run if the lib isn't + * loaded). + * + * This is a terrible name, but I couldn't think of a better one atm. + * + * @return boolean + */ + function isTestable() + { + + return true; + } + + + /** + * The "meat" of the test. This is where the real test code goes. You should override this when extending + * + * @return integer + */ + function _execTest() + { + + return PHPSECINFO_TEST_RESULT_NOTRUN; + } + + + /** + * This function loads up result messages into the $this->_messages array. + * + * Using this method rather than setting $this->_messages directly allows result + * messages to be inherited. This is broken out into a separate function rather + * than the constructor for ease of extension purposes (php4 is whack, man). + * + */ + function _setMessages() + { + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_OK, 'en', 'This setting should be safe'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTICE, 'en', 'This could potentially be a security issue'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_WARN, 'en', 'This setting may be a serious security problem'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_ERROR, 'en', 'There was an error running this test'); + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'This test cannot be run'); + } + + + /** + * Placeholder - extend for tests + * + */ + function _retrieveCurrentValue() + { + $this->current_value = "foo"; + } + + + /** + * This is the wrapper that executes the test and sets the result code and message + */ + function test() + { + $result = $this->_execTest(); + $this->_setResult($result); + + } + + + /** + * Retrieves the result + * + * @return integer + */ + function getResult() + { + return $this->_result; + } + + + /** + * Retrieves the message for the current result + * + * @return string + */ + function getMessage() + { + if (!isset($this->_message) || empty($this->_message)) { + $this->_setMessage($this->_result, $this->_language); + } + + return $this->_message; + } + + + /** + * Sets the message for a given result code and language + * + * + * $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', 'This test cannot be run'); + * + * + * @param integer $result_code + * @param string $language_code + * @param string $message + * + */ + function setMessageForResult($result_code, $language_code, $message) + { + + if (!isset($this->_messages[$result_code])) { + $this->_messages[$result_code] = array(); + } + + if (!is_array($this->_messages[$result_code])) { + $this->_messages[$result_code] = array(); + } + + $this->_messages[$result_code][$language_code] = $message; + + } + + + /** + * returns the current value. This function should be used to access the + * value for display. All values are cast as strings + * + * @return string + */ + function getCurrentTestValue() + { + return $this->getStringValue($this->current_value); + } + + /** + * returns the recommended value. This function should be used to access the + * value for display. All values are cast as strings + * + * @return string + */ + function getRecommendedTestValue() + { + return $this->getStringValue($this->recommended_value); + } + + + /** + * Sets the result code + * + * @param integer $result_code + */ + function _setResult($result_code) + { + $this->_result = $result_code; + } + + + /** + * Sets the $this->_message variable based on the passed result and language codes + * + * @param integer $result_code + * @param string $language_code + */ + function _setMessage($result_code, $language_code) + { + $messages = $this->_messages[$result_code]; + $message = $messages[$language_code]; + $this->_message = $message; + } + + + /** + * Returns a link to a page with detailed information about the test + * + * URL is formatted as PHPSECINFO_TEST_MOREINFO_BASEURL + testName + * + * @see PHPSECINFO_TEST_MOREINFO_BASEURL + * + * @return string|boolean + */ + function getMoreInfoURL() + { + if ($tn = $this->getTestName()) { + return PHPSECINFO_TEST_MOREINFO_BASEURL . strtolower("{$tn}.html"); + } else { + return false; + } + } + + + /** + * This retrieves the name of this test. + * + * If a name has not been set, this returns a formatted version of the class name. + * + * @return string + */ + function getTestName() + { + if (isset($this->test_name) && !empty($this->test_name)) { + return $this->test_name; + } else { + return ucwords( + str_replace('_', ' ', + get_class($this) + ) + ); + } + + } + + + /** + * sets the test name + * + * @param string $test_name + */ + function setTestName($test_name) + { + $this->test_name = $test_name; + } + + + /** + * Returns the test group this test belongs to + * + * @return string + */ + function getTestGroup() + { + return $this->test_group; + } + + + /** + * sets the test group + * + * @param string $test_group + */ + function setTestGroup($test_group) + { + $this->test_group = $test_group; + } + + + /** + * This function takes the shorthand notation used in memory limit settings for PHP + * and returns the byte value. Totally stolen from http://us3.php.net/manual/en/function.ini-get.php + * + * + * echo 'post_max_size in bytes = ' . $this->return_bytes(ini_get('post_max_size')); + * + * + * @link http://php.net/manual/en/function.ini-get.php + * @param string $val + * @return integer + */ + function returnBytes($val) + { + $val = trim($val); + + if ((int)$val === 0) { + return 0; + } + + $last = strtolower($val{strlen($val) - 1}); + switch ($last) { + // The 'G' modifier is available since PHP 5.1.0 + case 'g': + $val *= 1024; + case 'm': + $val *= 1024; + case 'k': + $val *= 1024; + } + + return $val; + } + + + /** + * This just does the usual PHP string casting, except for + * the boolean FALSE value, where the string "0" is returned + * instead of an empty string + * + * @param mixed $val + * @return string + */ + function getStringValue($val) + { + if ($val === FALSE) { + return "0"; + } else { + return (string)$val; + } + } + + + /** + * This method converts the several possible return values from + * allegedly "boolean" ini settings to proper booleans + * + * Properly converted input values are: 'off', 'on', 'false', 'true', '', '0', '1' + * (the last two might not be neccessary, but I'd rather be safe) + * + * If the ini_value doesn't match any of those, the value is returned as-is. + * + * @param string $ini_key the ini_key you need the value of + * @return boolean|mixed + */ + function getBooleanIniValue($ini_key) + { + + $ini_val = ini_get($ini_key); + + switch (strtolower($ini_val)) { + + case 'off': + return false; + break; + case 'on': + return true; + break; + case 'false': + return false; + break; + case 'true': + return true; + break; + case '0': + return false; + break; + case '1': + return true; + break; + case '': + return false; + break; + default: + return $ini_val; + + } + + } + + /** + * sys_get_temp_dir provides some temp dir detection capability + * that is lacking in versions of PHP that do not have the + * sys_get_temp_dir() function + * + * @return string|NULL + */ + function sys_get_temp_dir() + { + // Try to get from environment variable + $vars = array('TMP', 'TMPDIR', 'TEMP'); + foreach ($vars as $var) { + $tmp = getenv($var); + if (!empty($tmp)) { + return realpath($tmp); + } + } + return NULL; + } + + + /** + * A quick function to determine whether we're running on Windows. + * Uses the PHP_OS constant. + * + * @return boolean + */ + function osIsWindows() + { + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + return true; + } else { + return false; + } + } + + + /** + * Returns an array of data returned from the UNIX 'id' command + * + * includes uid, username, gid, groupname, and groups (if "exec" + * is enabled). Groups is an array of all the groups the user + * belongs to. Keys are the group ids, values are the group names. + * + * returns FALSE if no suitable function is available to retrieve + * the data + * + * @return array|boolean + */ + function getUnixId() + { + + if ($this->osIsWindows()) { + return false; + } + + $success = false; + + + if (function_exists("exec") && !PhpSecInfo_Test::getBooleanIniValue('safe_mode')) { + $id_raw = exec('id'); + // uid=1000(coj) gid=1000(coj) groups=1000(coj),1001(admin) + preg_match("|uid=(\d+)\((\S+)\)\s+gid=(\d+)\((\S+)\)\s+groups=(.+)|i", + $id_raw, + $matches); + + if (!$matches) { + /** + * for some reason the output from 'id' wasn't as we expected. + * return false so the test doesn't run. + */ + $success = false; + } else { + $id_data = array('uid' => $matches[1], + 'username' => $matches[2], + 'gid' => $matches[3], + 'group' => $matches[4]); + + $groups = array(); + if ($matches[5]) { + $gs = $matches[5]; + $gs = explode(',', $gs); + foreach ($gs as $groupstr) { + if (preg_match("/(\d+)\(([^\)]+)\)/", $groupstr, $subs)) { + $groups[$subs[1]] = $subs[2]; + } else { + $groups[$groupstr] = ''; + } + } + ksort($groups); + } + $id_data['groups'] = $groups; + $success = true; + } + + } + + if (!$success && function_exists("posix_getpwuid") && function_exists("posix_geteuid") + && function_exists('posix_getgrgid') && function_exists('posix_getgroups') + ) { + $data = posix_getpwuid(posix_getuid()); + $id_data['uid'] = $data['uid']; + $id_data['username'] = $data['name']; + $id_data['gid'] = $data['gid']; + //$group_data = posix_getgrgid( posix_getegid() ); + //$id_data['group'] = $group_data['name']; + $id_data['groups'] = array(); + $groups = posix_getgroups(); + foreach ($groups as $gid) { + //$group_data = posix_getgrgid(posix_getgid()); + $id_data['groups'][$gid] = ''; + } + $success = true; + } + + if ($success) { + return $id_data; + } else { + return false; + } + } } diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Test_Application.php b/plugins/SecurityInfo/PhpSecInfo/Test/Test_Application.php index af7a2f9926..af97310aad 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Test_Application.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Test_Application.php @@ -1,7 +1,7 @@ 'http://piwik.org/changelog', - 'PHP' => 'http://php.net/', - ); - - if ($tn = $this->getTestName()) { - return $urls[$tn]; - } else { - return false; - } - } + + /** + * This value is used to group test results together. + * + * For example, all tests related to the mysql lib should be grouped under "mysql." + * + * @var string + */ + var $test_group = 'Application'; + + + /** + * "Application" tests should pretty much be always testable, so the default is just to return true + * + * @return boolean + */ + function isTestable() + { + return Piwik_Http::getTransportMethod() !== null; + } + + function getMoreInfoURL() + { + $urls = array( + 'Piwik' => 'http://piwik.org/changelog', + 'PHP' => 'http://php.net/', + ); + + if ($tn = $this->getTestName()) { + return $urls[$tn]; + } else { + return false; + } + } } diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Test_Cgi.php b/plugins/SecurityInfo/PhpSecInfo/Test/Test_Cgi.php index 1a4156a3f4..5384bfbf50 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Test_Cgi.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Test_Cgi.php @@ -9,8 +9,7 @@ /** * require the main PhpSecInfo class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test.php'); - +require_once(PHPSECINFO_BASE_DIR . '/Test/Test.php'); /** @@ -20,42 +19,43 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test.php'); class PhpSecInfo_Test_Cgi extends PhpSecInfo_Test { - /** - * This value is used to group test results together. - * - * For example, all tests related to the mysql lib should be grouped under "mysql." - * - * @var string - */ - var $test_group = 'CGI'; - - - - /** - * "CGI" tests should only be run if we're running as a CGI. The best way I could think of - * to test this was to preg against the php_sapi_name() return value. - * - * @return boolean - */ - function isTestable() { - /*if ( preg_match('/^cgi.*$/', PHP_SAPI) ) { - return true; - } else { - return false; - }*/ - return !strncmp(PHP_SAPI, 'cgi', 3); - } - - - /** - * Set the messages for CGI tests - * - */ - function _setMessages() { - parent::_setMessages(); - - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', "You don't seem to be using the CGI SAPI"); - - } + /** + * This value is used to group test results together. + * + * For example, all tests related to the mysql lib should be grouped under "mysql." + * + * @var string + */ + var $test_group = 'CGI'; + + + /** + * "CGI" tests should only be run if we're running as a CGI. The best way I could think of + * to test this was to preg against the php_sapi_name() return value. + * + * @return boolean + */ + function isTestable() + { + /*if ( preg_match('/^cgi.*$/', PHP_SAPI) ) { + return true; + } else { + return false; + }*/ + return !strncmp(PHP_SAPI, 'cgi', 3); + } + + + /** + * Set the messages for CGI tests + * + */ + function _setMessages() + { + parent::_setMessages(); + + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', "You don't seem to be using the CGI SAPI"); + + } } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Test_Core.php b/plugins/SecurityInfo/PhpSecInfo/Test/Test_Core.php index 58230794d2..4778394f6f 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Test_Core.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Test_Core.php @@ -1,7 +1,7 @@ */ @@ -9,8 +9,7 @@ /** * require the main PhpSecInfo class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test.php'); - +require_once(PHPSECINFO_BASE_DIR . '/Test/Test.php'); /** @@ -19,26 +18,27 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test.php'); */ class PhpSecInfo_Test_Core extends PhpSecInfo_Test { - - /** - * This value is used to group test results together. - * - * For example, all tests related to the mysql lib should be grouped under "mysql." - * - * @var string - */ - var $test_group = 'Core'; - - - /** - * "Core" tests should pretty much be always testable, so the default is just to return true - * - * @return boolean - */ - function isTestable() { - - return true; - } - - + + /** + * This value is used to group test results together. + * + * For example, all tests related to the mysql lib should be grouped under "mysql." + * + * @var string + */ + var $test_group = 'Core'; + + + /** + * "Core" tests should pretty much be always testable, so the default is just to return true + * + * @return boolean + */ + function isTestable() + { + + return true; + } + + } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Test_Curl.php b/plugins/SecurityInfo/PhpSecInfo/Test/Test_Curl.php index a1e7d57a6f..78deeaea6c 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Test_Curl.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Test_Curl.php @@ -9,8 +9,7 @@ /** * require the main PhpSecInfo class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test.php'); - +require_once(PHPSECINFO_BASE_DIR . '/Test/Test.php'); /** @@ -20,43 +19,44 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test.php'); class PhpSecInfo_Test_Curl extends PhpSecInfo_Test { - /** - * This value is used to group test results together. - * - * For example, all tests related to the mysql lib should be grouped under "mysql." - * - * @var string - */ - var $test_group = 'Curl'; - - - - /** - * "Curl" tests should only be run if the curl extension is installed. We can check - * for this by seeing if the function curl_init() is defined - * - * @return boolean - */ - function isTestable() { -/* if ( function_exists('curl_init') ) { - return true; - } else { - return false; - } -*/ - return extension_loaded('curl'); - } - - - /** - * Set the messages for Curl tests - * - */ - function _setMessages() { - parent::_setMessages(); - - $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', "CURL support is not enabled in your PHP install"); - - } + /** + * This value is used to group test results together. + * + * For example, all tests related to the mysql lib should be grouped under "mysql." + * + * @var string + */ + var $test_group = 'Curl'; + + + /** + * "Curl" tests should only be run if the curl extension is installed. We can check + * for this by seeing if the function curl_init() is defined + * + * @return boolean + */ + function isTestable() + { + /* if ( function_exists('curl_init') ) { + return true; + } else { + return false; + } + */ + return extension_loaded('curl'); + } + + + /** + * Set the messages for Curl tests + * + */ + function _setMessages() + { + parent::_setMessages(); + + $this->setMessageForResult(PHPSECINFO_TEST_RESULT_NOTRUN, 'en', "CURL support is not enabled in your PHP install"); + + } } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Test_Session.php b/plugins/SecurityInfo/PhpSecInfo/Test/Test_Session.php index 5270fa62c4..32185eee89 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Test_Session.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Test_Session.php @@ -1,7 +1,7 @@ */ @@ -10,8 +10,7 @@ /** * require the main PhpSecInfo class */ -require_once(PHPSECINFO_BASE_DIR.'/Test/Test.php'); - +require_once(PHPSECINFO_BASE_DIR . '/Test/Test.php'); /** @@ -20,27 +19,28 @@ require_once(PHPSECINFO_BASE_DIR.'/Test/Test.php'); */ class PhpSecInfo_Test_Session extends PhpSecInfo_Test { - - /** - * This value is used to group test results together. - * - * For example, all tests related to the mysql lib should be grouped under "mysql." - * - * @var string - */ - var $test_group = 'Session'; - - - /** - * "Session" tests should pretty much be always testable, so the default is - * just to return true - * - * @return boolean - */ - function isTestable() { - - return true; - } - - + + /** + * This value is used to group test results together. + * + * For example, all tests related to the mysql lib should be grouped under "mysql." + * + * @var string + */ + var $test_group = 'Session'; + + + /** + * "Session" tests should pretty much be always testable, so the default is + * just to return true + * + * @return boolean + */ + function isTestable() + { + + return true; + } + + } \ No newline at end of file diff --git a/plugins/SecurityInfo/PhpSecInfo/Test/Test_Suhosin.php b/plugins/SecurityInfo/PhpSecInfo/Test/Test_Suhosin.php index 1f8883eeba..7228fde5e0 100644 --- a/plugins/SecurityInfo/PhpSecInfo/Test/Test_Suhosin.php +++ b/plugins/SecurityInfo/PhpSecInfo/Test/Test_Suhosin.php @@ -1,7 +1,7 @@ = 0) { - return false; - } - return true; - } - function getMoreInfoURL() { - if ($tn = $this->getTestName()) { - return 'http://www.hardened-php.net/suhosin/index.html'; - } else { - return false; - } - } + /** + * This value is used to group test results together. + * + * For example, all tests related to the mysql lib should be grouped under "mysql." + * + * @var string + */ + var $test_group = 'Suhosin'; + + + /** + * "Suhosin" tests should pretty much be always testable, so the default is just to return true + * + * @return boolean + */ + function isTestable() + { + if (version_compare(PHP_VERSION, '5.3.9') >= 0) { + return false; + } + return true; + } + + function getMoreInfoURL() + { + if ($tn = $this->getTestName()) { + return 'http://www.hardened-php.net/suhosin/index.html'; + } else { + return false; + } + } } diff --git a/plugins/SecurityInfo/SecurityInfo.php b/plugins/SecurityInfo/SecurityInfo.php index c42ad9aeee..487d8a92cd 100644 --- a/plugins/SecurityInfo/SecurityInfo.php +++ b/plugins/SecurityInfo/SecurityInfo.php @@ -1,10 +1,10 @@ Piwik_Translate('SecurityInfo_PluginDescription'), - 'author' => 'Piwik', - 'author_homepage' => 'http://piwik.org/', - 'version' => Piwik_Version::VERSION, - ); - return $info; - } - - function getListHooksRegistered() - { - return array( - 'AdminMenu.add' => 'addMenu', - ); - } - - function addMenu() - { - Piwik_AddAdminSubMenu('CoreAdminHome_MenuDiagnostic', 'SecurityInfo_Security', - array('module' => 'SecurityInfo', 'action' => 'index'), - Piwik::isUserIsSuperUser(), - $order = 10); - } +{ + public function getInformation() + { + $info = array( + 'description' => Piwik_Translate('SecurityInfo_PluginDescription'), + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'version' => Piwik_Version::VERSION, + ); + return $info; + } + + function getListHooksRegistered() + { + return array( + 'AdminMenu.add' => 'addMenu', + ); + } + + function addMenu() + { + Piwik_AddAdminSubMenu('CoreAdminHome_MenuDiagnostic', 'SecurityInfo_Security', + array('module' => 'SecurityInfo', 'action' => 'index'), + Piwik::isUserIsSuperUser(), + $order = 10); + } } diff --git a/plugins/SecurityInfo/templates/index.tpl b/plugins/SecurityInfo/templates/index.tpl index ac9f8c9254..f40b86f1b7 100644 --- a/plugins/SecurityInfo/templates/index.tpl +++ b/plugins/SecurityInfo/templates/index.tpl @@ -3,27 +3,28 @@

{'SecurityInfo_SecurityInformation'|translate}

{'SecurityInfo_PluginDescription'|translate}

-

Learn more: read our guide Hardening Piwik: How to make Piwik and your web server more secure?

+

Learn more: read our guide Hardening Piwik: How to make Piwik and your web server + more secure?

-{foreach from=$results.test_results key=i item=section} -

{$i}

- - - - - - - - - {foreach from=$section key=j item=test} - - - - - {/foreach} - -
{'SecurityInfo_Test'|translate}{'SecurityInfo_Result'|translate}
{$j}{$test.message}
-{/foreach} + {foreach from=$results.test_results key=i item=section} +

{$i}

+ + + + + + + + + {foreach from=$section key=j item=test} + + + + + {/foreach} + +
{'SecurityInfo_Test'|translate}{'SecurityInfo_Result'|translate}
{$j}{$test.message}
+ {/foreach}
{include file="CoreAdminHome/templates/footer.tpl"} \ No newline at end of file diff --git a/plugins/SitesManager/API.php b/plugins/SitesManager/API.php index 3b5e5e9ec0..a87a35551a 100644 --- a/plugins/SitesManager/API.php +++ b/plugins/SitesManager/API.php @@ -1,1411 +1,1344 @@ Managing Websites in Piwik. * @package Piwik_SitesManager */ -class Piwik_SitesManager_API +class Piwik_SitesManager_API { - static private $instance = null; - const DEFAULT_SEARCH_KEYWORD_PARAMETERS = 'q,query,s,search,searchword,k,keyword'; - - /** - * @return Piwik_SitesManager_API - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - const OPTION_EXCLUDED_IPS_GLOBAL = 'SitesManager_ExcludedIpsGlobal'; - const OPTION_DEFAULT_TIMEZONE = 'SitesManager_DefaultTimezone'; - const OPTION_DEFAULT_CURRENCY = 'SitesManager_DefaultCurrency'; - const OPTION_EXCLUDED_QUERY_PARAMETERS_GLOBAL = 'SitesManager_ExcludedQueryParameters'; - const OPTION_SEARCH_KEYWORD_QUERY_PARAMETERS_GLOBAL = 'SitesManager_SearchKeywordParameters'; - const OPTION_SEARCH_CATEGORY_QUERY_PARAMETERS_GLOBAL = 'SitesManager_SearchCategoryParameters'; - const OPTION_EXCLUDED_USER_AGENTS_GLOBAL = 'SitesManager_ExcludedUserAgentsGlobal'; - const OPTION_SITE_SPECIFIC_USER_AGENT_EXCLUDE_ENABLE = 'SitesManager_EnableSiteSpecificUserAgentExclude'; - const OPTION_KEEP_URL_FRAGMENTS_GLOBAL = 'SitesManager_KeepURLFragmentsGlobal'; - - /** - * Returns the javascript tag for the given idSite. - * This tag must be included on every page to be tracked by Piwik - * - * @param int $idSite - * @param string $customTitle Custom title given to the pageview - * @return string The Javascript tag ready to be included on the HTML pages - */ - public function getJavascriptTag( $idSite, $piwikUrl = '') - { - Piwik::checkUserHasViewAccess($idSite); - - if(empty($piwikUrl)) - { - $piwikUrl = Piwik_Url::getCurrentUrlWithoutFileName(); - } - $piwikUrl = Piwik_Common::sanitizeInputValues($piwikUrl); - - $htmlEncoded = Piwik::getJavascriptCode($idSite, $piwikUrl); - $htmlEncoded = str_replace(array('
','
','
'), '', $htmlEncoded); - return $htmlEncoded; - } - - /** - * Returns all websites belonging to the specified group - * @param string $group Group name - */ - public function getSitesFromGroup($group) - { - Piwik::checkUserIsSuperUser(); - $group = trim($group); - - $sites = Zend_Registry::get('db')->fetchAll("SELECT * - FROM ".Piwik_Common::prefixTable("site")." + static private $instance = null; + const DEFAULT_SEARCH_KEYWORD_PARAMETERS = 'q,query,s,search,searchword,k,keyword'; + + /** + * @return Piwik_SitesManager_API + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + const OPTION_EXCLUDED_IPS_GLOBAL = 'SitesManager_ExcludedIpsGlobal'; + const OPTION_DEFAULT_TIMEZONE = 'SitesManager_DefaultTimezone'; + const OPTION_DEFAULT_CURRENCY = 'SitesManager_DefaultCurrency'; + const OPTION_EXCLUDED_QUERY_PARAMETERS_GLOBAL = 'SitesManager_ExcludedQueryParameters'; + const OPTION_SEARCH_KEYWORD_QUERY_PARAMETERS_GLOBAL = 'SitesManager_SearchKeywordParameters'; + const OPTION_SEARCH_CATEGORY_QUERY_PARAMETERS_GLOBAL = 'SitesManager_SearchCategoryParameters'; + const OPTION_EXCLUDED_USER_AGENTS_GLOBAL = 'SitesManager_ExcludedUserAgentsGlobal'; + const OPTION_SITE_SPECIFIC_USER_AGENT_EXCLUDE_ENABLE = 'SitesManager_EnableSiteSpecificUserAgentExclude'; + const OPTION_KEEP_URL_FRAGMENTS_GLOBAL = 'SitesManager_KeepURLFragmentsGlobal'; + + /** + * Returns the javascript tag for the given idSite. + * This tag must be included on every page to be tracked by Piwik + * + * @param int $idSite + * @param string $customTitle Custom title given to the pageview + * @return string The Javascript tag ready to be included on the HTML pages + */ + public function getJavascriptTag($idSite, $piwikUrl = '') + { + Piwik::checkUserHasViewAccess($idSite); + + if (empty($piwikUrl)) { + $piwikUrl = Piwik_Url::getCurrentUrlWithoutFileName(); + } + $piwikUrl = Piwik_Common::sanitizeInputValues($piwikUrl); + + $htmlEncoded = Piwik::getJavascriptCode($idSite, $piwikUrl); + $htmlEncoded = str_replace(array('
', '
', '
'), '', $htmlEncoded); + return $htmlEncoded; + } + + /** + * Returns all websites belonging to the specified group + * @param string $group Group name + */ + public function getSitesFromGroup($group) + { + Piwik::checkUserIsSuperUser(); + $group = trim($group); + + $sites = Zend_Registry::get('db')->fetchAll("SELECT * + FROM " . Piwik_Common::prefixTable("site") . " WHERE `group` = ?", $group); - return $sites; - } - - /** - * Returns the list of website groups, including the empty group - * if no group were specified for some websites - * - * @return array of group names strings - */ - public function getSitesGroups() - { - Piwik::checkUserIsSuperUser(); - $groups = Zend_Registry::get('db')->fetchAll("SELECT DISTINCT `group` FROM ".Piwik_Common::prefixTable("site")); - $cleanedGroups = array(); - foreach($groups as $group) - { - $cleanedGroups[] = $group['group']; - } - $cleanedGroups = array_map('trim', $cleanedGroups); - return $cleanedGroups; - } - - /** - * Returns the website information : name, main_url - * - * @exception if the site ID doesn't exist or the user doesn't have access to it - * @return array - */ - public function getSiteFromId( $idSite ) - { - Piwik::checkUserHasViewAccess( $idSite ); - $site = Zend_Registry::get('db')->fetchRow("SELECT * - FROM ".Piwik_Common::prefixTable("site")." + return $sites; + } + + /** + * Returns the list of website groups, including the empty group + * if no group were specified for some websites + * + * @return array of group names strings + */ + public function getSitesGroups() + { + Piwik::checkUserIsSuperUser(); + $groups = Zend_Registry::get('db')->fetchAll("SELECT DISTINCT `group` FROM " . Piwik_Common::prefixTable("site")); + $cleanedGroups = array(); + foreach ($groups as $group) { + $cleanedGroups[] = $group['group']; + } + $cleanedGroups = array_map('trim', $cleanedGroups); + return $cleanedGroups; + } + + /** + * Returns the website information : name, main_url + * + * @exception if the site ID doesn't exist or the user doesn't have access to it + * @return array + */ + public function getSiteFromId($idSite) + { + Piwik::checkUserHasViewAccess($idSite); + $site = Zend_Registry::get('db')->fetchRow("SELECT * + FROM " . Piwik_Common::prefixTable("site") . " WHERE idsite = ?", $idSite); - return $site; - } - - /** - * Returns the list of alias URLs registered for the given idSite. - * The website ID must be valid when calling this method! - * - * @return array list of alias URLs - */ - private function getAliasSiteUrlsFromId( $idsite ) - { - $db = Zend_Registry::get('db'); - $result = $db->fetchAll("SELECT url - FROM ".Piwik_Common::prefixTable("site_url"). " + return $site; + } + + /** + * Returns the list of alias URLs registered for the given idSite. + * The website ID must be valid when calling this method! + * + * @return array list of alias URLs + */ + private function getAliasSiteUrlsFromId($idsite) + { + $db = Zend_Registry::get('db'); + $result = $db->fetchAll("SELECT url + FROM " . Piwik_Common::prefixTable("site_url") . " WHERE idsite = ?", $idsite); - $urls = array(); - foreach($result as $url) - { - $urls[] = $url['url']; - } - return $urls; - } - - /** - * Returns the list of all URLs registered for the given idSite (main_url + alias URLs). - * - * @exception if the website ID doesn't exist or the user doesn't have access to it - * @return array list of URLs - */ - public function getSiteUrlsFromId( $idSite ) - { - Piwik::checkUserHasViewAccess($idSite); - $site = new Piwik_Site($idSite); - $urls = $this->getAliasSiteUrlsFromId($idSite); - return array_merge(array($site->getMainUrl()), $urls); - } - - /** - * Returns the list of all the website IDs registered. - * Caller must check access. - * - * @return array The list of website IDs - */ - private function getSitesId() - { - $result = Piwik_FetchAll("SELECT idsite FROM ".Piwik_Common::prefixTable('site')); - $idSites = array(); - foreach($result as $idSite) - { - $idSites[] = $idSite['idsite']; - } - return $idSites; - } - - /** - * Returns all websites, requires Super User access - * - * @return array The list of websites, indexed by idsite - */ - public function getAllSites() - { - Piwik::checkUserIsSuperUser(); - $sites = Zend_Registry::get('db')->fetchAll("SELECT * FROM ".Piwik_Common::prefixTable("site")); - $return = array(); - foreach($sites as $site) - { - $return[$site['idsite']] = $site; - } - return $return; - } - - /** - * Returns the list of all the website IDs registered. - * Requires super user access. - * - * @return array The list of website IDs - */ - public function getAllSitesId() - { - Piwik::checkUserIsSuperUser(); - return Piwik_SitesManager_API::getInstance()->getSitesId(); - } - - /** - * Returns the list of the website IDs that received some visits since the specified timestamp. - * Requires super user access. - * - * @return array The list of website IDs - */ - public function getSitesIdWithVisits($timestamp = false) - { - Piwik::checkUserIsSuperUser(); - - if(empty($timestamp)) $timestamp = time(); - + $urls = array(); + foreach ($result as $url) { + $urls[] = $url['url']; + } + return $urls; + } + + /** + * Returns the list of all URLs registered for the given idSite (main_url + alias URLs). + * + * @exception if the website ID doesn't exist or the user doesn't have access to it + * @return array list of URLs + */ + public function getSiteUrlsFromId($idSite) + { + Piwik::checkUserHasViewAccess($idSite); + $site = new Piwik_Site($idSite); + $urls = $this->getAliasSiteUrlsFromId($idSite); + return array_merge(array($site->getMainUrl()), $urls); + } + + /** + * Returns the list of all the website IDs registered. + * Caller must check access. + * + * @return array The list of website IDs + */ + private function getSitesId() + { + $result = Piwik_FetchAll("SELECT idsite FROM " . Piwik_Common::prefixTable('site')); + $idSites = array(); + foreach ($result as $idSite) { + $idSites[] = $idSite['idsite']; + } + return $idSites; + } + + /** + * Returns all websites, requires Super User access + * + * @return array The list of websites, indexed by idsite + */ + public function getAllSites() + { + Piwik::checkUserIsSuperUser(); + $sites = Zend_Registry::get('db')->fetchAll("SELECT * FROM " . Piwik_Common::prefixTable("site")); + $return = array(); + foreach ($sites as $site) { + $return[$site['idsite']] = $site; + } + return $return; + } + + /** + * Returns the list of all the website IDs registered. + * Requires super user access. + * + * @return array The list of website IDs + */ + public function getAllSitesId() + { + Piwik::checkUserIsSuperUser(); + return Piwik_SitesManager_API::getInstance()->getSitesId(); + } + + /** + * Returns the list of the website IDs that received some visits since the specified timestamp. + * Requires super user access. + * + * @return array The list of website IDs + */ + public function getSitesIdWithVisits($timestamp = false) + { + Piwik::checkUserIsSuperUser(); + + if (empty($timestamp)) $timestamp = time(); + $time = Piwik_Date::factory((int)$timestamp)->getDatetime(); - $result = Piwik_FetchAll(" + $result = Piwik_FetchAll(" SELECT idsite FROM - ".Piwik_Common::prefixTable('site')." s + " . Piwik_Common::prefixTable('site') . " s WHERE EXISTS ( SELECT 1 - FROM ".Piwik_Common::prefixTable('log_visit'). " v + FROM " . Piwik_Common::prefixTable('log_visit') . " v WHERE v.idsite = s.idsite AND visit_last_action_time > ? AND visit_last_action_time <= ? LIMIT 1) ", array($time, $now = Piwik_Date::now()->addHour(1)->getDatetime())); - $idSites = array(); - foreach($result as $idSite) - { - $idSites[] = $idSite['idsite']; - } - return $idSites; - } - - - /** - * Returns the list of websites with the 'admin' access for the current user. - * For the superUser it returns all the websites in the database. - * - * @return array for each site, an array of information (idsite, name, main_url, etc.) - */ - public function getSitesWithAdminAccess() - { - $sitesId = $this->getSitesIdWithAdminAccess(); - return $this->getSitesFromIds($sitesId); - } - - /** - * Returns the list of websites with the 'view' access for the current user. - * For the superUser it doesn't return any result because the superUser has admin access on all the websites (use getSitesWithAtLeastViewAccess() instead). - * - * @return array for each site, an array of information (idsite, name, main_url, etc.) - */ - public function getSitesWithViewAccess() - { - $sitesId = $this->getSitesIdWithViewAccess(); - return $this->getSitesFromIds($sitesId); - } - - /** - * Returns the list of websites with the 'view' or 'admin' access for the current user. - * For the superUser it returns all the websites in the database. - * - * @param int $limit Specify max number of sites to return - * @param bool $_restrictSitesToLogin Hack necessary when runnning scheduled tasks, where "Super User" is forced, but sometimes not desired, see #3017 - * @return array array for each site, an array of information (idsite, name, main_url, etc.) - */ - public function getSitesWithAtLeastViewAccess($limit = false, $_restrictSitesToLogin = false) - { - $sitesId = $this->getSitesIdWithAtLeastViewAccess($_restrictSitesToLogin); - return $this->getSitesFromIds($sitesId, $limit); - } - - /** - * Returns the list of websites ID with the 'admin' access for the current user. - * For the superUser it returns all the websites in the database. - * - * @return array list of websites ID - */ - public function getSitesIdWithAdminAccess() - { - $sitesId = Zend_Registry::get('access')->getSitesIdWithAdminAccess(); - return $sitesId; - } - - /** - * Returns the list of websites ID with the 'view' access for the current user. - * For the superUser it doesn't return any result because the superUser has admin access on all the websites (use getSitesIdWithAtLeastViewAccess() instead). - * - * @return array list of websites ID - */ - public function getSitesIdWithViewAccess() - { - return Zend_Registry::get('access')->getSitesIdWithViewAccess(); - } - - /** - * Returns the list of websites ID with the 'view' or 'admin' access for the current user. - * For the superUser it returns all the websites in the database. - * - * @return array list of websites ID - */ - public function getSitesIdWithAtLeastViewAccess($_restrictSitesToLogin = false) - { - if(!empty($_restrictSitesToLogin) - // Very important here to make sure we only proceed when in a scheduled task - // Otherwise anyone could get all websites for a given user - && Piwik_TaskScheduler::isTaskBeingExecuted()) - { - $accessRaw = Piwik_Access::getRawSitesWithSomeViewAccess($_restrictSitesToLogin); - $sitesId = array(); - foreach($accessRaw as $access) - { - $sitesId[] = $access['idsite']; - } - return $sitesId; - } - else - { - return Zend_Registry::get('access')->getSitesIdWithAtLeastViewAccess(); - } - } - - /** - * Returns the list of websites from the ID array in parameters. - * The user access is not checked in this method so the ID have to be accessible by the user! - * - * @param array list of website ID - */ - private function getSitesFromIds( $idSites, $limit = false ) - { - if(count($idSites) === 0) - { - return array(); - } - - if($limit) - { - $limit = "LIMIT " . (int)$limit; - } - - $db = Zend_Registry::get('db'); - $sites = $db->fetchAll("SELECT * - FROM ".Piwik_Common::prefixTable("site")." - WHERE idsite IN (".implode(", ", $idSites).") + $idSites = array(); + foreach ($result as $idSite) { + $idSites[] = $idSite['idsite']; + } + return $idSites; + } + + + /** + * Returns the list of websites with the 'admin' access for the current user. + * For the superUser it returns all the websites in the database. + * + * @return array for each site, an array of information (idsite, name, main_url, etc.) + */ + public function getSitesWithAdminAccess() + { + $sitesId = $this->getSitesIdWithAdminAccess(); + return $this->getSitesFromIds($sitesId); + } + + /** + * Returns the list of websites with the 'view' access for the current user. + * For the superUser it doesn't return any result because the superUser has admin access on all the websites (use getSitesWithAtLeastViewAccess() instead). + * + * @return array for each site, an array of information (idsite, name, main_url, etc.) + */ + public function getSitesWithViewAccess() + { + $sitesId = $this->getSitesIdWithViewAccess(); + return $this->getSitesFromIds($sitesId); + } + + /** + * Returns the list of websites with the 'view' or 'admin' access for the current user. + * For the superUser it returns all the websites in the database. + * + * @param int $limit Specify max number of sites to return + * @param bool $_restrictSitesToLogin Hack necessary when runnning scheduled tasks, where "Super User" is forced, but sometimes not desired, see #3017 + * @return array array for each site, an array of information (idsite, name, main_url, etc.) + */ + public function getSitesWithAtLeastViewAccess($limit = false, $_restrictSitesToLogin = false) + { + $sitesId = $this->getSitesIdWithAtLeastViewAccess($_restrictSitesToLogin); + return $this->getSitesFromIds($sitesId, $limit); + } + + /** + * Returns the list of websites ID with the 'admin' access for the current user. + * For the superUser it returns all the websites in the database. + * + * @return array list of websites ID + */ + public function getSitesIdWithAdminAccess() + { + $sitesId = Zend_Registry::get('access')->getSitesIdWithAdminAccess(); + return $sitesId; + } + + /** + * Returns the list of websites ID with the 'view' access for the current user. + * For the superUser it doesn't return any result because the superUser has admin access on all the websites (use getSitesIdWithAtLeastViewAccess() instead). + * + * @return array list of websites ID + */ + public function getSitesIdWithViewAccess() + { + return Zend_Registry::get('access')->getSitesIdWithViewAccess(); + } + + /** + * Returns the list of websites ID with the 'view' or 'admin' access for the current user. + * For the superUser it returns all the websites in the database. + * + * @return array list of websites ID + */ + public function getSitesIdWithAtLeastViewAccess($_restrictSitesToLogin = false) + { + if (!empty($_restrictSitesToLogin) + // Very important here to make sure we only proceed when in a scheduled task + // Otherwise anyone could get all websites for a given user + && Piwik_TaskScheduler::isTaskBeingExecuted() + ) { + $accessRaw = Piwik_Access::getRawSitesWithSomeViewAccess($_restrictSitesToLogin); + $sitesId = array(); + foreach ($accessRaw as $access) { + $sitesId[] = $access['idsite']; + } + return $sitesId; + } else { + return Zend_Registry::get('access')->getSitesIdWithAtLeastViewAccess(); + } + } + + /** + * Returns the list of websites from the ID array in parameters. + * The user access is not checked in this method so the ID have to be accessible by the user! + * + * @param array list of website ID + */ + private function getSitesFromIds($idSites, $limit = false) + { + if (count($idSites) === 0) { + return array(); + } + + if ($limit) { + $limit = "LIMIT " . (int)$limit; + } + + $db = Zend_Registry::get('db'); + $sites = $db->fetchAll("SELECT * + FROM " . Piwik_Common::prefixTable("site") . " + WHERE idsite IN (" . implode(", ", $idSites) . ") ORDER BY idsite ASC $limit"); - return $sites; - } - - protected function getNormalizedUrls($url) - { - if(strpos($url, 'www.') !== false) - { - $urlBis = str_replace('www.', '', $url); - } - else - { - $urlBis = str_replace('://', '://www.', $url); - } - return array($url, $urlBis); - } - - /** - * Returns the list of websites ID associated with a URL. - * - * @param string $url - * @return array list of websites ID - */ - public function getSitesIdFromSiteUrl( $url ) - { - $url = $this->removeTrailingSlash($url); - list($url, $urlBis) = $this->getNormalizedUrls($url); - if(Piwik::isUserIsSuperUser()) - { - $ids = Zend_Registry::get('db')->fetchAll( - 'SELECT idsite - FROM ' . Piwik_Common::prefixTable('site') . ' + return $sites; + } + + protected function getNormalizedUrls($url) + { + if (strpos($url, 'www.') !== false) { + $urlBis = str_replace('www.', '', $url); + } else { + $urlBis = str_replace('://', '://www.', $url); + } + return array($url, $urlBis); + } + + /** + * Returns the list of websites ID associated with a URL. + * + * @param string $url + * @return array list of websites ID + */ + public function getSitesIdFromSiteUrl($url) + { + $url = $this->removeTrailingSlash($url); + list($url, $urlBis) = $this->getNormalizedUrls($url); + if (Piwik::isUserIsSuperUser()) { + $ids = Zend_Registry::get('db')->fetchAll( + 'SELECT idsite + FROM ' . Piwik_Common::prefixTable('site') . ' WHERE (main_url = ? OR main_url = ?) ' . - 'UNION - SELECT idsite - FROM ' . Piwik_Common::prefixTable('site_url') . ' + 'UNION + SELECT idsite + FROM ' . Piwik_Common::prefixTable('site_url') . ' WHERE (url = ? OR url = ?) ', array($url, $urlBis, $url, $urlBis)); - } - else - { - $login = Piwik::getCurrentUserLogin(); - $ids = Zend_Registry::get('db')->fetchAll( - 'SELECT idsite - FROM ' . Piwik_Common::prefixTable('site') . ' + } else { + $login = Piwik::getCurrentUserLogin(); + $ids = Zend_Registry::get('db')->fetchAll( + 'SELECT idsite + FROM ' . Piwik_Common::prefixTable('site') . ' WHERE (main_url = ? OR main_url = ?)' . - 'AND idsite IN (' . Piwik_Access::getSqlAccessSite('idsite') . ') ' . - 'UNION - SELECT idsite - FROM ' . Piwik_Common::prefixTable('site_url') . ' + 'AND idsite IN (' . Piwik_Access::getSqlAccessSite('idsite') . ') ' . + 'UNION + SELECT idsite + FROM ' . Piwik_Common::prefixTable('site_url') . ' WHERE (url = ? OR url = ?)' . - 'AND idsite IN (' . Piwik_Access::getSqlAccessSite('idsite') . ')', - array($url, $urlBis, $login, $url, $urlBis, $login)); - } - - return $ids; - } - - /** - * Returns all websites with a timezone matching one the specified timezones - * - * @param array $timezones - * @ignore - */ - public function getSitesIdFromTimezones( $timezones ) - { - Piwik::checkUserIsSuperUser(); - $timezones = Piwik::getArrayFromApiParameter($timezones); - $timezones = array_unique($timezones); - $ids = Zend_Registry::get('db')->fetchAll( - 'SELECT idsite - FROM ' . Piwik_Common::prefixTable('site') . ' - WHERE timezone IN ('.Piwik_Common::getSqlStringFieldsArray($timezones).') - ORDER BY idsite ASC', - $timezones); - $return = array(); - foreach($ids as $id) - { - $return[] = $id['idsite']; - } - return $return; - } - - /** - * Add a website. - * Requires Super User access. - * - * The website is defined by a name and an array of URLs. - * @param string Site name - * @param array|string The URLs array must contain at least one URL called the 'main_url' ; - * if several URLs are provided in the array, they will be recorded - * as Alias URLs for this website. - * @param int Is Ecommerce Reporting enabled for this website? - * @param int $sitesearch Whether site search is enabled, 0 or 1 - * @param string $searchKeywordParameters Comma separated list of search keyword parameter names - * @param string $searchCategoryParameters Comma separated list of search category parameter names - * @param string Comma separated list of IPs to exclude from the reports (allows wildcards) - * @param string Timezone string, eg. 'Europe/London' - * @param string Currency, eg. 'EUR' - * @param string Website group identifier - * @param string Date at which the statistics for this website will start. Defaults to today's date in YYYY-MM-DD format - * @param int $keepURLFragments If 1, URL fragments will be kept when tracking. If 2, they - * will be removed. If 0, the default global behavior will be used. - * @see getKeepURLFragmentsGlobal. - * - * @return int the website ID created - */ - public function addSite( $siteName, - $urls, - $ecommerce = null, - $siteSearch = null, - $searchKeywordParameters = null, - $searchCategoryParameters = null, - $excludedIps = null, - $excludedQueryParameters = null, - $timezone = null, - $currency = null, - $group = null, - $startDate = null, - $excludedUserAgents = null, - $keepURLFragments = 0 ) - { - Piwik::checkUserIsSuperUser(); - - $this->checkName($siteName); - $urls = $this->cleanParameterUrls($urls); - $this->checkUrls($urls); - $this->checkAtLeastOneUrl($urls); - $siteSearch = $this->checkSiteSearch($siteSearch); - list($searchKeywordParameters, $searchCategoryParameters ) = $this->checkSiteSearchParameters($searchKeywordParameters, $searchCategoryParameters); - - $keepURLFragments = (int)$keepURLFragments; - self::checkKeepURLFragmentsValue($keepURLFragments); - - $timezone = trim($timezone); - if(empty($timezone)) - { - $timezone = $this->getDefaultTimezone(); - } - $this->checkValidTimezone($timezone); - - if(empty($currency)) - { - $currency = $this->getDefaultCurrency(); - } - $this->checkValidCurrency($currency); - - $db = Zend_Registry::get('db'); - - $url = $urls[0]; - $urls = array_slice($urls, 1); - - $bind = array( 'name' => $siteName, - 'main_url' => $url, - - ); - - $bind['excluded_ips'] = $this->checkAndReturnExcludedIps($excludedIps); - $bind['excluded_parameters'] = $this->checkAndReturnCommaSeparatedStringList($excludedQueryParameters); - $bind['excluded_user_agents'] = $this->checkAndReturnCommaSeparatedStringList($excludedUserAgents); - $bind['keep_url_fragment'] = $keepURLFragments; - $bind['timezone'] = $timezone; - $bind['currency'] = $currency; - $bind['ecommerce'] = (int)$ecommerce; - $bind['sitesearch'] = $siteSearch; - $bind['sitesearch_keyword_parameters'] = $searchKeywordParameters; - $bind['sitesearch_category_parameters'] = $searchCategoryParameters; - $bind['ts_created'] = !is_null($startDate) - ? Piwik_Date::factory($startDate)->getDatetime() - : Piwik_Date::now()->getDatetime(); - - if(!empty($group) - && Piwik::isUserIsSuperUser()) - { - $bind['group'] = trim($group); - } - else - { - $bind['group'] = ""; - } - - $db->insert(Piwik_Common::prefixTable("site"), $bind); - - $idSite = $db->lastInsertId(); - - $this->insertSiteUrls($idSite, $urls); - - // we reload the access list which doesn't yet take in consideration this new website - Zend_Registry::get('access')->reloadAccess(); - $this->postUpdateWebsite($idSite); - - Piwik_PostEvent('SitesManager.addSite', $idSite); - - return (int)$idSite; - } - - private function postUpdateWebsite($idSite) - { - Piwik_Site::clearCache(); - Piwik_Tracker_Cache::regenerateCacheWebsiteAttributes($idSite); - } - - /** - * Delete a website from the database, given its Id. - * - * Requires Super User access. - * - * @param int $idSite - * @throws Exception - */ - public function deleteSite( $idSite ) - { - Piwik::checkUserIsSuperUser(); - - $idSites = Piwik_SitesManager_API::getInstance()->getSitesId(); - if(!in_array($idSite, $idSites)) - { - throw new Exception("website id = $idSite not found"); - } - $nbSites = count($idSites); - if($nbSites == 1) - { - throw new Exception(Piwik_TranslateException("SitesManager_ExceptionDeleteSite")); - } - - $db = Zend_Registry::get('db'); - - $db->query("DELETE FROM ".Piwik_Common::prefixTable("site")." + 'AND idsite IN (' . Piwik_Access::getSqlAccessSite('idsite') . ')', + array($url, $urlBis, $login, $url, $urlBis, $login)); + } + + return $ids; + } + + /** + * Returns all websites with a timezone matching one the specified timezones + * + * @param array $timezones + * @ignore + */ + public function getSitesIdFromTimezones($timezones) + { + Piwik::checkUserIsSuperUser(); + $timezones = Piwik::getArrayFromApiParameter($timezones); + $timezones = array_unique($timezones); + $ids = Zend_Registry::get('db')->fetchAll( + 'SELECT idsite + FROM ' . Piwik_Common::prefixTable('site') . ' + WHERE timezone IN (' . Piwik_Common::getSqlStringFieldsArray($timezones) . ') + ORDER BY idsite ASC', + $timezones); + $return = array(); + foreach ($ids as $id) { + $return[] = $id['idsite']; + } + return $return; + } + + /** + * Add a website. + * Requires Super User access. + * + * The website is defined by a name and an array of URLs. + * @param string Site name + * @param array|string The URLs array must contain at least one URL called the 'main_url' ; + * if several URLs are provided in the array, they will be recorded + * as Alias URLs for this website. + * @param int Is Ecommerce Reporting enabled for this website? + * @param int $sitesearch Whether site search is enabled, 0 or 1 + * @param string $searchKeywordParameters Comma separated list of search keyword parameter names + * @param string $searchCategoryParameters Comma separated list of search category parameter names + * @param string Comma separated list of IPs to exclude from the reports (allows wildcards) + * @param string Timezone string, eg. 'Europe/London' + * @param string Currency, eg. 'EUR' + * @param string Website group identifier + * @param string Date at which the statistics for this website will start. Defaults to today's date in YYYY-MM-DD format + * @param int $keepURLFragments If 1, URL fragments will be kept when tracking. If 2, they + * will be removed. If 0, the default global behavior will be used. + * @see getKeepURLFragmentsGlobal. + * + * @return int the website ID created + */ + public function addSite($siteName, + $urls, + $ecommerce = null, + $siteSearch = null, + $searchKeywordParameters = null, + $searchCategoryParameters = null, + $excludedIps = null, + $excludedQueryParameters = null, + $timezone = null, + $currency = null, + $group = null, + $startDate = null, + $excludedUserAgents = null, + $keepURLFragments = 0) + { + Piwik::checkUserIsSuperUser(); + + $this->checkName($siteName); + $urls = $this->cleanParameterUrls($urls); + $this->checkUrls($urls); + $this->checkAtLeastOneUrl($urls); + $siteSearch = $this->checkSiteSearch($siteSearch); + list($searchKeywordParameters, $searchCategoryParameters) = $this->checkSiteSearchParameters($searchKeywordParameters, $searchCategoryParameters); + + $keepURLFragments = (int)$keepURLFragments; + self::checkKeepURLFragmentsValue($keepURLFragments); + + $timezone = trim($timezone); + if (empty($timezone)) { + $timezone = $this->getDefaultTimezone(); + } + $this->checkValidTimezone($timezone); + + if (empty($currency)) { + $currency = $this->getDefaultCurrency(); + } + $this->checkValidCurrency($currency); + + $db = Zend_Registry::get('db'); + + $url = $urls[0]; + $urls = array_slice($urls, 1); + + $bind = array('name' => $siteName, + 'main_url' => $url, + + ); + + $bind['excluded_ips'] = $this->checkAndReturnExcludedIps($excludedIps); + $bind['excluded_parameters'] = $this->checkAndReturnCommaSeparatedStringList($excludedQueryParameters); + $bind['excluded_user_agents'] = $this->checkAndReturnCommaSeparatedStringList($excludedUserAgents); + $bind['keep_url_fragment'] = $keepURLFragments; + $bind['timezone'] = $timezone; + $bind['currency'] = $currency; + $bind['ecommerce'] = (int)$ecommerce; + $bind['sitesearch'] = $siteSearch; + $bind['sitesearch_keyword_parameters'] = $searchKeywordParameters; + $bind['sitesearch_category_parameters'] = $searchCategoryParameters; + $bind['ts_created'] = !is_null($startDate) + ? Piwik_Date::factory($startDate)->getDatetime() + : Piwik_Date::now()->getDatetime(); + + if (!empty($group) + && Piwik::isUserIsSuperUser() + ) { + $bind['group'] = trim($group); + } else { + $bind['group'] = ""; + } + + $db->insert(Piwik_Common::prefixTable("site"), $bind); + + $idSite = $db->lastInsertId(); + + $this->insertSiteUrls($idSite, $urls); + + // we reload the access list which doesn't yet take in consideration this new website + Zend_Registry::get('access')->reloadAccess(); + $this->postUpdateWebsite($idSite); + + Piwik_PostEvent('SitesManager.addSite', $idSite); + + return (int)$idSite; + } + + private function postUpdateWebsite($idSite) + { + Piwik_Site::clearCache(); + Piwik_Tracker_Cache::regenerateCacheWebsiteAttributes($idSite); + } + + /** + * Delete a website from the database, given its Id. + * + * Requires Super User access. + * + * @param int $idSite + * @throws Exception + */ + public function deleteSite($idSite) + { + Piwik::checkUserIsSuperUser(); + + $idSites = Piwik_SitesManager_API::getInstance()->getSitesId(); + if (!in_array($idSite, $idSites)) { + throw new Exception("website id = $idSite not found"); + } + $nbSites = count($idSites); + if ($nbSites == 1) { + throw new Exception(Piwik_TranslateException("SitesManager_ExceptionDeleteSite")); + } + + $db = Zend_Registry::get('db'); + + $db->query("DELETE FROM " . Piwik_Common::prefixTable("site") . " WHERE idsite = ?", $idSite); - - $db->query("DELETE FROM ".Piwik_Common::prefixTable("site_url")." + + $db->query("DELETE FROM " . Piwik_Common::prefixTable("site_url") . " WHERE idsite = ?", $idSite); - - $db->query("DELETE FROM ".Piwik_Common::prefixTable("access")." + + $db->query("DELETE FROM " . Piwik_Common::prefixTable("access") . " WHERE idsite = ?", $idSite); - // we do not delete logs here on purpose (you can run these queries on the log_ tables to delete all data) - Piwik_Tracker_Cache::deleteCacheWebsiteAttributes($idSite); - - Piwik_PostEvent('SitesManager.deleteSite', $idSite); - } - - - /** - * Checks that the array has at least one element - * - * @param array $urls - * @throws Exception - */ - private function checkAtLeastOneUrl( $urls ) - { - if(!is_array($urls) - || count($urls) == 0) - { - throw new Exception(Piwik_TranslateException("SitesManager_ExceptionNoUrl")); - } - } - - private function checkValidTimezone($timezone) - { - $timezones = $this->getTimezonesList(); - foreach(array_values($timezones) as $cities) - { - foreach($cities as $timezoneId => $city) - { - if($timezoneId == $timezone) - { - return true; - } - } - } - throw new Exception(Piwik_TranslateException('SitesManager_ExceptionInvalidTimezone', array($timezone))); - } - - private function checkValidCurrency($currency) - { - if(!in_array($currency, array_keys($this->getCurrencyList()))) - { - throw new Exception(Piwik_TranslateException('SitesManager_ExceptionInvalidCurrency', array($currency, "USD, EUR, etc."))); - } - } - - /** - * Checks that the submitted IPs (comma separated list) are valid - * Returns the cleaned up IPs - * - * @param string $excludedIps Comma separated list of IP addresses - * @throws Exception - * @return array of IPs - */ - private function checkAndReturnExcludedIps($excludedIps) - { - if(empty($excludedIps)) - { - return ''; - } - $ips = explode(',', $excludedIps); - $ips = array_map('trim', $ips); - $ips = array_filter($ips, 'strlen'); - foreach($ips as $ip) - { - if(!$this->isValidIp($ip)) - { - throw new Exception(Piwik_TranslateException('SitesManager_ExceptionInvalidIPFormat', array($ip, "1.2.3.4, 1.2.3.*, or 1.2.3.4/5"))); - } - } - $ips = implode(',', $ips); - return $ips; - } - /** - * Add a list of alias Urls to the given idSite - * - * If some URLs given in parameter are already recorded as alias URLs for this website, - * they won't be duplicated. The 'main_url' of the website won't be affected by this method. - * - * @return int the number of inserted URLs - */ - public function addSiteAliasUrls( $idSite, $urls) - { - Piwik::checkUserHasAdminAccess( $idSite ); - - $urls = $this->cleanParameterUrls($urls); - $this->checkUrls($urls); - - $urlsInit = $this->getSiteUrlsFromId($idSite); - $toInsert = array_diff($urls, $urlsInit); - $this->insertSiteUrls($idSite, $toInsert); - $this->postUpdateWebsite($idSite); - - return count($toInsert); - } - - /** - * Get the start and end IP addresses for an IP address range - * - * @param string $ipRange IP address range in presentation format - * @return array|false Array( low, high ) IP addresses in presentation format; or false if error - */ - public function getIpsForRange($ipRange) - { - $range = Piwik_IP::getIpsForRange($ipRange); - if($range === false) - { - return false; - } - - return array( Piwik_IP::N2P($range[0]), Piwik_IP::N2P($range[1]) ); - } - - /** - * Sets IPs to be excluded from all websites. IPs can contain wildcards. - * Will also apply to websites created in the future. - * - * @param string Comma separated list of IPs to exclude from being tracked (allows wildcards) - * @return bool - */ - public function setGlobalExcludedIps($excludedIps) - { - Piwik::checkUserIsSuperUser(); - $excludedIps = $this->checkAndReturnExcludedIps($excludedIps); - Piwik_SetOption(self::OPTION_EXCLUDED_IPS_GLOBAL, $excludedIps); - Piwik_Tracker_Cache::deleteTrackerCache(); - return true; - } - - /** - * Sets Site Search keyword/category parameter names, to be used on websites which have not specified these values - * Expects Comma separated list of query params names - * - * @param string - * @param string - * @return bool - */ - public function setGlobalSearchParameters($searchKeywordParameters, $searchCategoryParameters) - { - Piwik::checkUserIsSuperUser(); - Piwik_SetOption(self::OPTION_SEARCH_KEYWORD_QUERY_PARAMETERS_GLOBAL, $searchKeywordParameters); - Piwik_SetOption(self::OPTION_SEARCH_CATEGORY_QUERY_PARAMETERS_GLOBAL, $searchCategoryParameters); - Piwik_Tracker_Cache::deleteTrackerCache(); - return true; - } - - /** - * @return string Comma separated list of URL parameters - */ - public function getSearchKeywordParametersGlobal() - { - Piwik::checkUserHasSomeAdminAccess(); - $names = Piwik_GetOption(self::OPTION_SEARCH_KEYWORD_QUERY_PARAMETERS_GLOBAL); - if($names === false) { - $names = self::DEFAULT_SEARCH_KEYWORD_PARAMETERS; - } - if(empty($names)) { - $names = ''; - } - return $names; - } - - /** - * @return string Comma separated list of URL parameters - */ - public function getSearchCategoryParametersGlobal() - { - Piwik::checkUserHasSomeAdminAccess(); - return Piwik_GetOption(self::OPTION_SEARCH_CATEGORY_QUERY_PARAMETERS_GLOBAL); - } - - /** - * Returns the list of URL query parameters that are excluded from all websites - * - * @return string Comma separated list of URL parameters - */ - public function getExcludedQueryParametersGlobal() - { - Piwik::checkUserHasSomeViewAccess(); - return Piwik_GetOption(self::OPTION_EXCLUDED_QUERY_PARAMETERS_GLOBAL); - } - - /** - * Returns the list of user agent substrings to look for when excluding visits for - * all websites. If a visitor's user agent string contains one of these substrings, - * their visits will not be included. - * - * @return string Comma separated list of strings. - */ - public function getExcludedUserAgentsGlobal() - { - Piwik::checkUserHasSomeAdminAccess(); - return Piwik_GetOption(self::OPTION_EXCLUDED_USER_AGENTS_GLOBAL); - } - - /** - * Sets list of user agent substrings to look for when excluding visits. For more info, - * @see getExcludedUserAgentsGlobal. - * - * @param string $excludedUserAgents Comma separated list of strings. Each element is trimmed, - * and empty strings are removed. - */ - public function setGlobalExcludedUserAgents( $excludedUserAgents ) - { - Piwik::checkUserIsSuperUser(); - - // update option - $excludedUserAgents = $this->checkAndReturnCommaSeparatedStringList($excludedUserAgents); - Piwik_SetOption(self::OPTION_EXCLUDED_USER_AGENTS_GLOBAL, $excludedUserAgents); - - // make sure tracker cache will reflect change - Piwik_Tracker_Cache::deleteTrackerCache(); - } - - /** - * Returns true if site-specific user agent exclusion has been enabled. If it hasn't, - * only the global user agent substrings (see @setGlobalExcludedUserAgents) will be used. - * - * @return bool - */ - public function isSiteSpecificUserAgentExcludeEnabled() - { - Piwik::checkUserHasSomeAdminAccess(); - return (bool)Piwik_GetOption(self::OPTION_SITE_SPECIFIC_USER_AGENT_EXCLUDE_ENABLE); - } - - /** - * Sets whether it should be allowed to exclude different user agents for different - * websites. - * - * @param bool $enabled - */ - public function setSiteSpecificUserAgentExcludeEnabled( $enabled ) - { - Piwik::checkUserIsSuperUser(); - - // update option - Piwik_SetOption(self::OPTION_SITE_SPECIFIC_USER_AGENT_EXCLUDE_ENABLE, $enabled); - - // make sure tracker cache will reflect change - Piwik_Tracker_Cache::deleteTrackerCache(); - } - - /** - * Returns true if the default behavior is to keep URL fragments when tracking, - * false if otherwise. - * - * @return bool - */ - public function getKeepURLFragmentsGlobal() - { - Piwik::checkUserHasSomeViewAccess(); - return (bool)Piwik_GetOption(self::OPTION_KEEP_URL_FRAGMENTS_GLOBAL); - } - - /** - * Sets whether the default behavior should be to keep URL fragments when - * tracking or not. - * - * @param $enabled bool If true, the default behavior will be to keep URL - * fragments when tracking. If false, the default - * behavior will be to remove them. - */ - public function setKeepURLFragmentsGlobal( $enabled ) - { - Piwik::checkUserIsSuperUser(); - - // update option - Piwik_SetOption(self::OPTION_KEEP_URL_FRAGMENTS_GLOBAL, $enabled); - - // make sure tracker cache will reflect change - Piwik_Tracker_Cache::deleteTrackerCache(); - } - - /** - * Sets list of URL query parameters to be excluded on all websites. - * Will also apply to websites created in the future. - * - * @param string Comma separated list of URL query parameters to exclude from URLs - * @return bool - */ - public function setGlobalExcludedQueryParameters($excludedQueryParameters) - { - Piwik::checkUserIsSuperUser(); - $excludedQueryParameters = $this->checkAndReturnCommaSeparatedStringList($excludedQueryParameters); - Piwik_SetOption(self::OPTION_EXCLUDED_QUERY_PARAMETERS_GLOBAL, $excludedQueryParameters); - Piwik_Tracker_Cache::deleteTrackerCache(); - return true; - } - - /** - * Returns the list of IPs that are excluded from all websites - * - * @return string Comma separated list of IPs - */ - public function getExcludedIpsGlobal() - { - Piwik::checkUserHasSomeAdminAccess(); - return Piwik_GetOption(self::OPTION_EXCLUDED_IPS_GLOBAL); - } - - /** - * Returns the default currency that will be set when creating a website through the API. - * - * @return string Currency ID eg. 'USD' - */ - public function getDefaultCurrency() - { - Piwik::checkUserHasSomeAdminAccess(); - $defaultCurrency = Piwik_GetOption(self::OPTION_DEFAULT_CURRENCY); - if($defaultCurrency) - { - return $defaultCurrency; - } - return 'USD'; - } - - /** - * Sets the default currency that will be used when creating websites - * - * @param string $defaultCurrency Currency code, eg. 'USD' - * @return bool - */ - public function setDefaultCurrency($defaultCurrency) - { - Piwik::checkUserIsSuperUser(); - $this->checkValidCurrency($defaultCurrency); - Piwik_SetOption(self::OPTION_DEFAULT_CURRENCY, $defaultCurrency); - return true; - } - - /** - * Returns the default timezone that will be set when creating a website through the API. - * Via the UI, if the default timezone is not UTC, it will be pre-selected in the drop down - * - * @return string Timezone eg. UTC+7 or Europe/Paris - */ - public function getDefaultTimezone() - { - $defaultTimezone = Piwik_GetOption(self::OPTION_DEFAULT_TIMEZONE); - if($defaultTimezone) - { - return $defaultTimezone; - } - return 'UTC'; - } - - /** - * Sets the default timezone that will be used when creating websites - * - * @param string $defaultTimezone Timezone string eg. Europe/Paris or UTC+8 - * @return bool - */ - public function setDefaultTimezone($defaultTimezone) - { - Piwik::checkUserIsSuperUser(); - $this->checkValidTimezone($defaultTimezone); - Piwik_SetOption(self::OPTION_DEFAULT_TIMEZONE, $defaultTimezone); - return true; - } - - /** - * Update an existing website. - * If only one URL is specified then only the main url will be updated. - * If several URLs are specified, both the main URL and the alias URLs will be updated. - * - * @param int $idSite website ID defining the website to edit - * @param string $siteName website name - * @param string|array $urls the website URLs - * @param int $ecommerce Whether Ecommerce is enabled, 0 or 1 - * @param int $sitesearch Whether site search is enabled, 0 or 1 - * @param string $searchKeywordParameters Comma separated list of search keyword parameter names - * @param string $searchCategoryParameters Comma separated list of search category parameter names - * @param string $excludedIps Comma separated list of IPs to exclude from being tracked (allows wildcards) - * @param null $excludedQueryParameters - * @param string $timezone Timezone - * @param string $currency Currency code - * @param string $group Group name where this website belongs - * @param string $startDate Date at which the statistics for this website will start. Defaults to today's date in YYYY-MM-DD format - * @param int|null $keepURLFragments If 1, URL fragments will be kept when tracking. If 2, they - * will be removed. If 0, the default global behavior will be used. - * @see getKeepURLFragmentsGlobal. If null, the existing value will - * not be modified. - * - * @throws Exception - * @return bool true on success - */ - public function updateSite( $idSite, - $siteName, - $urls = null, - $ecommerce = null, - $siteSearch = null, - $searchKeywordParameters = null, - $searchCategoryParameters = null, - $excludedIps = null, - $excludedQueryParameters = null, - $timezone = null, - $currency = null, - $group = null, - $startDate = null, - $excludedUserAgents = null, - $keepURLFragments = null) - { - Piwik::checkUserHasAdminAccess($idSite); - - $idSites = Piwik_SitesManager_API::getInstance()->getSitesId(); - if(!in_array($idSite, $idSites)) - { - throw new Exception("website id = $idSite not found"); - } - - $this->checkName($siteName); - - // Build the SQL UPDATE based on specified updates to perform - $bind = array(); - if(!is_null($urls)) - { - $urls = $this->cleanParameterUrls($urls); - $this->checkUrls($urls); - $this->checkAtLeastOneUrl($urls); - $url = $urls[0]; - $bind['main_url'] = $url; - } - - if(!is_null($currency)) - { - $currency = trim($currency); - $this->checkValidCurrency($currency); - $bind['currency'] = $currency; - } - if(!is_null($timezone)) - { - $timezone = trim($timezone); - $this->checkValidTimezone($timezone); - $bind['timezone'] = $timezone; - } - if(!is_null($group) - && Piwik::isUserIsSuperUser()) - { - $bind['group'] = trim($group); - } - if(!is_null($ecommerce)) - { - $bind['ecommerce'] = (int)(bool)$ecommerce; - } - if(!is_null($startDate)) - { - $bind['ts_created'] = Piwik_Date::factory($startDate)->getDatetime(); - } - $bind['excluded_ips'] = $this->checkAndReturnExcludedIps($excludedIps); - $bind['excluded_parameters'] = $this->checkAndReturnCommaSeparatedStringList($excludedQueryParameters); - $bind['excluded_user_agents'] = $this->checkAndReturnCommaSeparatedStringList($excludedUserAgents); - - if (!is_null($keepURLFragments)) - { - $keepURLFragments = (int)$keepURLFragments; - self::checkKeepURLFragmentsValue($keepURLFragments); - - $bind['keep_url_fragment'] = $keepURLFragments; - } - - $bind['sitesearch'] = $this->checkSiteSearch($siteSearch); - list($searchKeywordParameters, $searchCategoryParameters ) = $this->checkSiteSearchParameters($searchKeywordParameters, $searchCategoryParameters); - $bind['sitesearch_keyword_parameters'] = $searchKeywordParameters; - $bind['sitesearch_category_parameters'] = $searchCategoryParameters; - - $bind['name'] = $siteName; - $db = Zend_Registry::get('db'); - $db->update(Piwik_Common::prefixTable("site"), - $bind, - "idsite = $idSite" - ); - - // we now update the main + alias URLs - $this->deleteSiteAliasUrls($idSite); - if(count($urls) > 1) - { - $this->addSiteAliasUrls($idSite, array_slice($urls,1)); - } - $this->postUpdateWebsite($idSite); - - Piwik_PostEvent('SitesManager.updateSite', $idSite); - } - - private function checkAndReturnCommaSeparatedStringList($parameters) - { - $parameters = trim($parameters); - if(empty($parameters)) - { - return ''; - } - - $parameters = explode(',', $parameters); - $parameters = array_map('trim', $parameters); - $parameters = array_filter($parameters, 'strlen'); - $parameters = array_unique($parameters); - return implode(',', $parameters); - } - - /** - * Returns the list of supported currencies - * @see getCurrencySymbols() - * @return array ( currencyId => currencyName) - */ - public function getCurrencyList() - { - $currencies = Piwik::getCurrencyList(); - return array_map(create_function('$a', 'return $a[1]." (".$a[0].")";'), $currencies); - } - - /** - * Returns the list of currency symbols - * @see getCurrencyList() - * @return array( currencyId => currencySymbol ) - */ - public function getCurrencySymbols() - { - $currencies = Piwik::getCurrencyList(); - return array_map(create_function('$a', 'return $a[0];'), $currencies); - } - - /** - * Returns the list of timezones supported. - * Used for addSite and updateSite - * - * @TODO NOT COMPATIBLE WITH API RESPONSE AUTO BUILDER - * - * @return array of timezone strings - */ - public function getTimezonesList() - { - if(!Piwik::isTimezoneSupportEnabled()) - { - return array('UTC' => $this->getTimezonesListUTCOffsets()); - } - - $continents = array( 'Africa', 'America', 'Antarctica', 'Arctic', 'Asia', 'Atlantic', 'Australia', 'Europe', 'Indian', 'Pacific'); - $timezones = timezone_identifiers_list(); - - $return = array(); - foreach($timezones as $timezone) - { - // filter out timezones not recognized by strtotime() - // @see http://bugs.php.net/46111 - $testDate = '2008-09-18 13:00:00 ' . $timezone; - if(!strtotime($testDate)) - { - continue; - } - - $timezoneExploded = explode('/', $timezone); - $continent = $timezoneExploded[0]; - - // only display timezones that are grouped by continent - if(!in_array($continent, $continents)) - { - continue; - } - $city = $timezoneExploded[1]; - if(!empty($timezoneExploded[2])) - { - $city .= ' - '.$timezoneExploded[2]; - } - $city = str_replace('_', ' ', $city); - $return[$continent][$timezone] = $city; - } - - foreach($continents as $continent) - { - if(!empty($return[$continent])) - { - ksort($return[$continent]); - } - } - - $return['UTC'] = $this->getTimezonesListUTCOffsets(); - return $return; - } - - private function getTimezonesListUTCOffsets() - { - // manually add the UTC offsets - $GmtOffsets = array (-12, -11.5, -11, -10.5, -10, -9.5, -9, -8.5, -8, -7.5, -7, -6.5, -6, -5.5, -5, -4.5, -4, -3.5, -3, -2.5, -2, -1.5, -1, -0.5, - 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 5.75, 6, 6.5, 7, 7.5, 8, 8.5, 8.75, 9, 9.5, 10, 10.5, 11, 11.5, 12, 12.75, 13, 13.75, 14); - - $return = array(); - foreach($GmtOffsets as $offset) - { - if($offset > 0) - { - $offset = '+'.$offset; - } - elseif($offset == 0) - { - $offset = ''; - } - $offset = 'UTC' . $offset; - $offsetName = str_replace(array('.25','.5','.75'), array(':15',':30',':45'), $offset); - $return[$offset] = $offsetName; - } - return $return; - } - - /** - * Returns the list of unique timezones from all configured sites. - * - * @return array ( string ) - */ - public function getUniqueSiteTimezones() - { - Piwik::checkUserIsSuperUser(); - $results = Piwik_FetchAll("SELECT distinct timezone FROM ".Piwik_Common::prefixTable('site')); - $timezones = array(); - foreach($results as $result) - { - $timezones[] = $result['timezone']; - } - return $timezones; - } - - /** - * Insert the list of alias URLs for the website. - * The URLs must not exist already for this website! - */ - private function insertSiteUrls($idSite, $urls) - { - if(count($urls) != 0) - { - $db = Zend_Registry::get('db'); - foreach($urls as $url) - { - $db->insert(Piwik_Common::prefixTable("site_url"), array( - 'idsite' => $idSite, - 'url' => $url - ) - ); - } - } - } - - /** - * Delete all the alias URLs for the given idSite. - */ - private function deleteSiteAliasUrls($idsite) - { - $db = Zend_Registry::get('db'); - $db->query("DELETE FROM ".Piwik_Common::prefixTable("site_url") ." + // we do not delete logs here on purpose (you can run these queries on the log_ tables to delete all data) + Piwik_Tracker_Cache::deleteCacheWebsiteAttributes($idSite); + + Piwik_PostEvent('SitesManager.deleteSite', $idSite); + } + + + /** + * Checks that the array has at least one element + * + * @param array $urls + * @throws Exception + */ + private function checkAtLeastOneUrl($urls) + { + if (!is_array($urls) + || count($urls) == 0 + ) { + throw new Exception(Piwik_TranslateException("SitesManager_ExceptionNoUrl")); + } + } + + private function checkValidTimezone($timezone) + { + $timezones = $this->getTimezonesList(); + foreach (array_values($timezones) as $cities) { + foreach ($cities as $timezoneId => $city) { + if ($timezoneId == $timezone) { + return true; + } + } + } + throw new Exception(Piwik_TranslateException('SitesManager_ExceptionInvalidTimezone', array($timezone))); + } + + private function checkValidCurrency($currency) + { + if (!in_array($currency, array_keys($this->getCurrencyList()))) { + throw new Exception(Piwik_TranslateException('SitesManager_ExceptionInvalidCurrency', array($currency, "USD, EUR, etc."))); + } + } + + /** + * Checks that the submitted IPs (comma separated list) are valid + * Returns the cleaned up IPs + * + * @param string $excludedIps Comma separated list of IP addresses + * @throws Exception + * @return array of IPs + */ + private function checkAndReturnExcludedIps($excludedIps) + { + if (empty($excludedIps)) { + return ''; + } + $ips = explode(',', $excludedIps); + $ips = array_map('trim', $ips); + $ips = array_filter($ips, 'strlen'); + foreach ($ips as $ip) { + if (!$this->isValidIp($ip)) { + throw new Exception(Piwik_TranslateException('SitesManager_ExceptionInvalidIPFormat', array($ip, "1.2.3.4, 1.2.3.*, or 1.2.3.4/5"))); + } + } + $ips = implode(',', $ips); + return $ips; + } + + /** + * Add a list of alias Urls to the given idSite + * + * If some URLs given in parameter are already recorded as alias URLs for this website, + * they won't be duplicated. The 'main_url' of the website won't be affected by this method. + * + * @return int the number of inserted URLs + */ + public function addSiteAliasUrls($idSite, $urls) + { + Piwik::checkUserHasAdminAccess($idSite); + + $urls = $this->cleanParameterUrls($urls); + $this->checkUrls($urls); + + $urlsInit = $this->getSiteUrlsFromId($idSite); + $toInsert = array_diff($urls, $urlsInit); + $this->insertSiteUrls($idSite, $toInsert); + $this->postUpdateWebsite($idSite); + + return count($toInsert); + } + + /** + * Get the start and end IP addresses for an IP address range + * + * @param string $ipRange IP address range in presentation format + * @return array|false Array( low, high ) IP addresses in presentation format; or false if error + */ + public function getIpsForRange($ipRange) + { + $range = Piwik_IP::getIpsForRange($ipRange); + if ($range === false) { + return false; + } + + return array(Piwik_IP::N2P($range[0]), Piwik_IP::N2P($range[1])); + } + + /** + * Sets IPs to be excluded from all websites. IPs can contain wildcards. + * Will also apply to websites created in the future. + * + * @param string Comma separated list of IPs to exclude from being tracked (allows wildcards) + * @return bool + */ + public function setGlobalExcludedIps($excludedIps) + { + Piwik::checkUserIsSuperUser(); + $excludedIps = $this->checkAndReturnExcludedIps($excludedIps); + Piwik_SetOption(self::OPTION_EXCLUDED_IPS_GLOBAL, $excludedIps); + Piwik_Tracker_Cache::deleteTrackerCache(); + return true; + } + + /** + * Sets Site Search keyword/category parameter names, to be used on websites which have not specified these values + * Expects Comma separated list of query params names + * + * @param string + * @param string + * @return bool + */ + public function setGlobalSearchParameters($searchKeywordParameters, $searchCategoryParameters) + { + Piwik::checkUserIsSuperUser(); + Piwik_SetOption(self::OPTION_SEARCH_KEYWORD_QUERY_PARAMETERS_GLOBAL, $searchKeywordParameters); + Piwik_SetOption(self::OPTION_SEARCH_CATEGORY_QUERY_PARAMETERS_GLOBAL, $searchCategoryParameters); + Piwik_Tracker_Cache::deleteTrackerCache(); + return true; + } + + /** + * @return string Comma separated list of URL parameters + */ + public function getSearchKeywordParametersGlobal() + { + Piwik::checkUserHasSomeAdminAccess(); + $names = Piwik_GetOption(self::OPTION_SEARCH_KEYWORD_QUERY_PARAMETERS_GLOBAL); + if ($names === false) { + $names = self::DEFAULT_SEARCH_KEYWORD_PARAMETERS; + } + if (empty($names)) { + $names = ''; + } + return $names; + } + + /** + * @return string Comma separated list of URL parameters + */ + public function getSearchCategoryParametersGlobal() + { + Piwik::checkUserHasSomeAdminAccess(); + return Piwik_GetOption(self::OPTION_SEARCH_CATEGORY_QUERY_PARAMETERS_GLOBAL); + } + + /** + * Returns the list of URL query parameters that are excluded from all websites + * + * @return string Comma separated list of URL parameters + */ + public function getExcludedQueryParametersGlobal() + { + Piwik::checkUserHasSomeViewAccess(); + return Piwik_GetOption(self::OPTION_EXCLUDED_QUERY_PARAMETERS_GLOBAL); + } + + /** + * Returns the list of user agent substrings to look for when excluding visits for + * all websites. If a visitor's user agent string contains one of these substrings, + * their visits will not be included. + * + * @return string Comma separated list of strings. + */ + public function getExcludedUserAgentsGlobal() + { + Piwik::checkUserHasSomeAdminAccess(); + return Piwik_GetOption(self::OPTION_EXCLUDED_USER_AGENTS_GLOBAL); + } + + /** + * Sets list of user agent substrings to look for when excluding visits. For more info, + * @see getExcludedUserAgentsGlobal. + * + * @param string $excludedUserAgents Comma separated list of strings. Each element is trimmed, + * and empty strings are removed. + */ + public function setGlobalExcludedUserAgents($excludedUserAgents) + { + Piwik::checkUserIsSuperUser(); + + // update option + $excludedUserAgents = $this->checkAndReturnCommaSeparatedStringList($excludedUserAgents); + Piwik_SetOption(self::OPTION_EXCLUDED_USER_AGENTS_GLOBAL, $excludedUserAgents); + + // make sure tracker cache will reflect change + Piwik_Tracker_Cache::deleteTrackerCache(); + } + + /** + * Returns true if site-specific user agent exclusion has been enabled. If it hasn't, + * only the global user agent substrings (see @setGlobalExcludedUserAgents) will be used. + * + * @return bool + */ + public function isSiteSpecificUserAgentExcludeEnabled() + { + Piwik::checkUserHasSomeAdminAccess(); + return (bool)Piwik_GetOption(self::OPTION_SITE_SPECIFIC_USER_AGENT_EXCLUDE_ENABLE); + } + + /** + * Sets whether it should be allowed to exclude different user agents for different + * websites. + * + * @param bool $enabled + */ + public function setSiteSpecificUserAgentExcludeEnabled($enabled) + { + Piwik::checkUserIsSuperUser(); + + // update option + Piwik_SetOption(self::OPTION_SITE_SPECIFIC_USER_AGENT_EXCLUDE_ENABLE, $enabled); + + // make sure tracker cache will reflect change + Piwik_Tracker_Cache::deleteTrackerCache(); + } + + /** + * Returns true if the default behavior is to keep URL fragments when tracking, + * false if otherwise. + * + * @return bool + */ + public function getKeepURLFragmentsGlobal() + { + Piwik::checkUserHasSomeViewAccess(); + return (bool)Piwik_GetOption(self::OPTION_KEEP_URL_FRAGMENTS_GLOBAL); + } + + /** + * Sets whether the default behavior should be to keep URL fragments when + * tracking or not. + * + * @param $enabled bool If true, the default behavior will be to keep URL + * fragments when tracking. If false, the default + * behavior will be to remove them. + */ + public function setKeepURLFragmentsGlobal($enabled) + { + Piwik::checkUserIsSuperUser(); + + // update option + Piwik_SetOption(self::OPTION_KEEP_URL_FRAGMENTS_GLOBAL, $enabled); + + // make sure tracker cache will reflect change + Piwik_Tracker_Cache::deleteTrackerCache(); + } + + /** + * Sets list of URL query parameters to be excluded on all websites. + * Will also apply to websites created in the future. + * + * @param string Comma separated list of URL query parameters to exclude from URLs + * @return bool + */ + public function setGlobalExcludedQueryParameters($excludedQueryParameters) + { + Piwik::checkUserIsSuperUser(); + $excludedQueryParameters = $this->checkAndReturnCommaSeparatedStringList($excludedQueryParameters); + Piwik_SetOption(self::OPTION_EXCLUDED_QUERY_PARAMETERS_GLOBAL, $excludedQueryParameters); + Piwik_Tracker_Cache::deleteTrackerCache(); + return true; + } + + /** + * Returns the list of IPs that are excluded from all websites + * + * @return string Comma separated list of IPs + */ + public function getExcludedIpsGlobal() + { + Piwik::checkUserHasSomeAdminAccess(); + return Piwik_GetOption(self::OPTION_EXCLUDED_IPS_GLOBAL); + } + + /** + * Returns the default currency that will be set when creating a website through the API. + * + * @return string Currency ID eg. 'USD' + */ + public function getDefaultCurrency() + { + Piwik::checkUserHasSomeAdminAccess(); + $defaultCurrency = Piwik_GetOption(self::OPTION_DEFAULT_CURRENCY); + if ($defaultCurrency) { + return $defaultCurrency; + } + return 'USD'; + } + + /** + * Sets the default currency that will be used when creating websites + * + * @param string $defaultCurrency Currency code, eg. 'USD' + * @return bool + */ + public function setDefaultCurrency($defaultCurrency) + { + Piwik::checkUserIsSuperUser(); + $this->checkValidCurrency($defaultCurrency); + Piwik_SetOption(self::OPTION_DEFAULT_CURRENCY, $defaultCurrency); + return true; + } + + /** + * Returns the default timezone that will be set when creating a website through the API. + * Via the UI, if the default timezone is not UTC, it will be pre-selected in the drop down + * + * @return string Timezone eg. UTC+7 or Europe/Paris + */ + public function getDefaultTimezone() + { + $defaultTimezone = Piwik_GetOption(self::OPTION_DEFAULT_TIMEZONE); + if ($defaultTimezone) { + return $defaultTimezone; + } + return 'UTC'; + } + + /** + * Sets the default timezone that will be used when creating websites + * + * @param string $defaultTimezone Timezone string eg. Europe/Paris or UTC+8 + * @return bool + */ + public function setDefaultTimezone($defaultTimezone) + { + Piwik::checkUserIsSuperUser(); + $this->checkValidTimezone($defaultTimezone); + Piwik_SetOption(self::OPTION_DEFAULT_TIMEZONE, $defaultTimezone); + return true; + } + + /** + * Update an existing website. + * If only one URL is specified then only the main url will be updated. + * If several URLs are specified, both the main URL and the alias URLs will be updated. + * + * @param int $idSite website ID defining the website to edit + * @param string $siteName website name + * @param string|array $urls the website URLs + * @param int $ecommerce Whether Ecommerce is enabled, 0 or 1 + * @param int $sitesearch Whether site search is enabled, 0 or 1 + * @param string $searchKeywordParameters Comma separated list of search keyword parameter names + * @param string $searchCategoryParameters Comma separated list of search category parameter names + * @param string $excludedIps Comma separated list of IPs to exclude from being tracked (allows wildcards) + * @param null $excludedQueryParameters + * @param string $timezone Timezone + * @param string $currency Currency code + * @param string $group Group name where this website belongs + * @param string $startDate Date at which the statistics for this website will start. Defaults to today's date in YYYY-MM-DD format + * @param int|null $keepURLFragments If 1, URL fragments will be kept when tracking. If 2, they + * will be removed. If 0, the default global behavior will be used. + * @see getKeepURLFragmentsGlobal. If null, the existing value will + * not be modified. + * + * @throws Exception + * @return bool true on success + */ + public function updateSite($idSite, + $siteName, + $urls = null, + $ecommerce = null, + $siteSearch = null, + $searchKeywordParameters = null, + $searchCategoryParameters = null, + $excludedIps = null, + $excludedQueryParameters = null, + $timezone = null, + $currency = null, + $group = null, + $startDate = null, + $excludedUserAgents = null, + $keepURLFragments = null) + { + Piwik::checkUserHasAdminAccess($idSite); + + $idSites = Piwik_SitesManager_API::getInstance()->getSitesId(); + if (!in_array($idSite, $idSites)) { + throw new Exception("website id = $idSite not found"); + } + + $this->checkName($siteName); + + // Build the SQL UPDATE based on specified updates to perform + $bind = array(); + if (!is_null($urls)) { + $urls = $this->cleanParameterUrls($urls); + $this->checkUrls($urls); + $this->checkAtLeastOneUrl($urls); + $url = $urls[0]; + $bind['main_url'] = $url; + } + + if (!is_null($currency)) { + $currency = trim($currency); + $this->checkValidCurrency($currency); + $bind['currency'] = $currency; + } + if (!is_null($timezone)) { + $timezone = trim($timezone); + $this->checkValidTimezone($timezone); + $bind['timezone'] = $timezone; + } + if (!is_null($group) + && Piwik::isUserIsSuperUser() + ) { + $bind['group'] = trim($group); + } + if (!is_null($ecommerce)) { + $bind['ecommerce'] = (int)(bool)$ecommerce; + } + if (!is_null($startDate)) { + $bind['ts_created'] = Piwik_Date::factory($startDate)->getDatetime(); + } + $bind['excluded_ips'] = $this->checkAndReturnExcludedIps($excludedIps); + $bind['excluded_parameters'] = $this->checkAndReturnCommaSeparatedStringList($excludedQueryParameters); + $bind['excluded_user_agents'] = $this->checkAndReturnCommaSeparatedStringList($excludedUserAgents); + + if (!is_null($keepURLFragments)) { + $keepURLFragments = (int)$keepURLFragments; + self::checkKeepURLFragmentsValue($keepURLFragments); + + $bind['keep_url_fragment'] = $keepURLFragments; + } + + $bind['sitesearch'] = $this->checkSiteSearch($siteSearch); + list($searchKeywordParameters, $searchCategoryParameters) = $this->checkSiteSearchParameters($searchKeywordParameters, $searchCategoryParameters); + $bind['sitesearch_keyword_parameters'] = $searchKeywordParameters; + $bind['sitesearch_category_parameters'] = $searchCategoryParameters; + + $bind['name'] = $siteName; + $db = Zend_Registry::get('db'); + $db->update(Piwik_Common::prefixTable("site"), + $bind, + "idsite = $idSite" + ); + + // we now update the main + alias URLs + $this->deleteSiteAliasUrls($idSite); + if (count($urls) > 1) { + $this->addSiteAliasUrls($idSite, array_slice($urls, 1)); + } + $this->postUpdateWebsite($idSite); + + Piwik_PostEvent('SitesManager.updateSite', $idSite); + } + + private function checkAndReturnCommaSeparatedStringList($parameters) + { + $parameters = trim($parameters); + if (empty($parameters)) { + return ''; + } + + $parameters = explode(',', $parameters); + $parameters = array_map('trim', $parameters); + $parameters = array_filter($parameters, 'strlen'); + $parameters = array_unique($parameters); + return implode(',', $parameters); + } + + /** + * Returns the list of supported currencies + * @see getCurrencySymbols() + * @return array ( currencyId => currencyName) + */ + public function getCurrencyList() + { + $currencies = Piwik::getCurrencyList(); + return array_map(create_function('$a', 'return $a[1]." (".$a[0].")";'), $currencies); + } + + /** + * Returns the list of currency symbols + * @see getCurrencyList() + * @return array( currencyId => currencySymbol ) + */ + public function getCurrencySymbols() + { + $currencies = Piwik::getCurrencyList(); + return array_map(create_function('$a', 'return $a[0];'), $currencies); + } + + /** + * Returns the list of timezones supported. + * Used for addSite and updateSite + * + * @TODO NOT COMPATIBLE WITH API RESPONSE AUTO BUILDER + * + * @return array of timezone strings + */ + public function getTimezonesList() + { + if (!Piwik::isTimezoneSupportEnabled()) { + return array('UTC' => $this->getTimezonesListUTCOffsets()); + } + + $continents = array('Africa', 'America', 'Antarctica', 'Arctic', 'Asia', 'Atlantic', 'Australia', 'Europe', 'Indian', 'Pacific'); + $timezones = timezone_identifiers_list(); + + $return = array(); + foreach ($timezones as $timezone) { + // filter out timezones not recognized by strtotime() + // @see http://bugs.php.net/46111 + $testDate = '2008-09-18 13:00:00 ' . $timezone; + if (!strtotime($testDate)) { + continue; + } + + $timezoneExploded = explode('/', $timezone); + $continent = $timezoneExploded[0]; + + // only display timezones that are grouped by continent + if (!in_array($continent, $continents)) { + continue; + } + $city = $timezoneExploded[1]; + if (!empty($timezoneExploded[2])) { + $city .= ' - ' . $timezoneExploded[2]; + } + $city = str_replace('_', ' ', $city); + $return[$continent][$timezone] = $city; + } + + foreach ($continents as $continent) { + if (!empty($return[$continent])) { + ksort($return[$continent]); + } + } + + $return['UTC'] = $this->getTimezonesListUTCOffsets(); + return $return; + } + + private function getTimezonesListUTCOffsets() + { + // manually add the UTC offsets + $GmtOffsets = array(-12, -11.5, -11, -10.5, -10, -9.5, -9, -8.5, -8, -7.5, -7, -6.5, -6, -5.5, -5, -4.5, -4, -3.5, -3, -2.5, -2, -1.5, -1, -0.5, + 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 5.75, 6, 6.5, 7, 7.5, 8, 8.5, 8.75, 9, 9.5, 10, 10.5, 11, 11.5, 12, 12.75, 13, 13.75, 14); + + $return = array(); + foreach ($GmtOffsets as $offset) { + if ($offset > 0) { + $offset = '+' . $offset; + } elseif ($offset == 0) { + $offset = ''; + } + $offset = 'UTC' . $offset; + $offsetName = str_replace(array('.25', '.5', '.75'), array(':15', ':30', ':45'), $offset); + $return[$offset] = $offsetName; + } + return $return; + } + + /** + * Returns the list of unique timezones from all configured sites. + * + * @return array ( string ) + */ + public function getUniqueSiteTimezones() + { + Piwik::checkUserIsSuperUser(); + $results = Piwik_FetchAll("SELECT distinct timezone FROM " . Piwik_Common::prefixTable('site')); + $timezones = array(); + foreach ($results as $result) { + $timezones[] = $result['timezone']; + } + return $timezones; + } + + /** + * Insert the list of alias URLs for the website. + * The URLs must not exist already for this website! + */ + private function insertSiteUrls($idSite, $urls) + { + if (count($urls) != 0) { + $db = Zend_Registry::get('db'); + foreach ($urls as $url) { + $db->insert(Piwik_Common::prefixTable("site_url"), array( + 'idsite' => $idSite, + 'url' => $url + ) + ); + } + } + } + + /** + * Delete all the alias URLs for the given idSite. + */ + private function deleteSiteAliasUrls($idsite) + { + $db = Zend_Registry::get('db'); + $db->query("DELETE FROM " . Piwik_Common::prefixTable("site_url") . " WHERE idsite = ?", $idsite); - } - - /** - * Remove the final slash in the URLs if found - * - * @return string the URL without the trailing slash - */ - private function removeTrailingSlash($url) - { - // if there is a final slash, we take the URL without this slash (expected URL format) - if(strlen($url) > 5 - && $url[strlen($url)-1] == '/') - { - $url = substr($url,0,strlen($url)-1); - } - return $url; - } - - /** - * Tests if the URL is a valid URL - * - * @return bool - */ - private function isValidUrl( $url ) - { - return Piwik_Common::isLookLikeUrl($url); - } - - /** - * Tests if the IP is a valid IP, allowing wildcards, except in the first octet. - * Wildcards can only be used from right to left, ie. 1.1.*.* is allowed, but 1.1.*.1 is not. - * - * @param string $ip IP address - * @return bool - */ - private function isValidIp( $ip ) - { - return Piwik_IP::getIpsForRange($ip) !== false; - } - - /** - * Check that the website name has a correct format. - * - * @param $siteName - * @throws Exception - */ - private function checkName($siteName) - { - if(empty($siteName)) - { - throw new Exception(Piwik_TranslateException("SitesManager_ExceptionEmptyName")); - } - } - - - private function checkSiteSearch($siteSearch) - { - if($siteSearch === null) { - return "1"; - } - return $siteSearch == 1 ? "1" : "0"; - } - - private function checkSiteSearchParameters($searchKeywordParameters, $searchCategoryParameters) - { - $searchKeywordParameters = trim($searchKeywordParameters); - $searchCategoryParameters = trim($searchCategoryParameters); - if(empty($searchKeywordParameters)) { - $searchKeywordParameters = ''; - } - if(empty($searchCategoryParameters)) { - $searchCategoryParameters = ''; - } - - return array($searchKeywordParameters, $searchCategoryParameters); - } - - /** - * Check that the array of URLs are valid URLs - * - * @param array $urls - * @throws Exception if any of the urls is not valid - */ - private function checkUrls($urls) - { - foreach($urls as $url) - { - if(!$this->isValidUrl($url)) - { - throw new Exception(sprintf(Piwik_TranslateException("SitesManager_ExceptionInvalidUrl"),$url)); - } - } - } - - /** - * Clean the parameter URLs: - * - if the parameter is a string make it an array - * - remove the trailing slashes if found - * - * @param string|array urls - * @return array the array of cleaned URLs - */ - private function cleanParameterUrls( $urls ) - { - if(!is_array($urls)) - { - $urls = array($urls); - } - $urls = array_filter($urls); - - $urls = array_map('urldecode', $urls); - foreach($urls as &$url) - { - $url = $this->removeTrailingSlash($url); - if(strpos($url, 'http') !== 0) - { - $url = 'http://'.$url; - } - $url = Piwik_Common::sanitizeInputValue($url); - } - $urls = array_unique($urls); - return $urls; - } - - public function getPatternMatchSites($pattern) - { - $ids = $this->getSitesIdWithAtLeastViewAccess(); - if(empty($ids)) - { - return array(); - } - - $ids_str = ''; - foreach($ids as $id_num => $id_val) - { - $ids_str .= $id_val.' , '; - } - $ids_str .= $id_val; - - $db = Zend_Registry::get('db'); - $bind = array('%'.$pattern.'%', 'http%'.$pattern.'%'); - - // Also match the idsite - $where = ''; - if(is_numeric($pattern)) - { - $bind[] = $pattern; - $where = 'OR s.idsite = ?'; - } - $sites = $db->fetchAll("SELECT idsite, name, main_url - FROM ".Piwik_Common::prefixTable('site')." s + } + + /** + * Remove the final slash in the URLs if found + * + * @return string the URL without the trailing slash + */ + private function removeTrailingSlash($url) + { + // if there is a final slash, we take the URL without this slash (expected URL format) + if (strlen($url) > 5 + && $url[strlen($url) - 1] == '/' + ) { + $url = substr($url, 0, strlen($url) - 1); + } + return $url; + } + + /** + * Tests if the URL is a valid URL + * + * @return bool + */ + private function isValidUrl($url) + { + return Piwik_Common::isLookLikeUrl($url); + } + + /** + * Tests if the IP is a valid IP, allowing wildcards, except in the first octet. + * Wildcards can only be used from right to left, ie. 1.1.*.* is allowed, but 1.1.*.1 is not. + * + * @param string $ip IP address + * @return bool + */ + private function isValidIp($ip) + { + return Piwik_IP::getIpsForRange($ip) !== false; + } + + /** + * Check that the website name has a correct format. + * + * @param $siteName + * @throws Exception + */ + private function checkName($siteName) + { + if (empty($siteName)) { + throw new Exception(Piwik_TranslateException("SitesManager_ExceptionEmptyName")); + } + } + + + private function checkSiteSearch($siteSearch) + { + if ($siteSearch === null) { + return "1"; + } + return $siteSearch == 1 ? "1" : "0"; + } + + private function checkSiteSearchParameters($searchKeywordParameters, $searchCategoryParameters) + { + $searchKeywordParameters = trim($searchKeywordParameters); + $searchCategoryParameters = trim($searchCategoryParameters); + if (empty($searchKeywordParameters)) { + $searchKeywordParameters = ''; + } + if (empty($searchCategoryParameters)) { + $searchCategoryParameters = ''; + } + + return array($searchKeywordParameters, $searchCategoryParameters); + } + + /** + * Check that the array of URLs are valid URLs + * + * @param array $urls + * @throws Exception if any of the urls is not valid + */ + private function checkUrls($urls) + { + foreach ($urls as $url) { + if (!$this->isValidUrl($url)) { + throw new Exception(sprintf(Piwik_TranslateException("SitesManager_ExceptionInvalidUrl"), $url)); + } + } + } + + /** + * Clean the parameter URLs: + * - if the parameter is a string make it an array + * - remove the trailing slashes if found + * + * @param string|array urls + * @return array the array of cleaned URLs + */ + private function cleanParameterUrls($urls) + { + if (!is_array($urls)) { + $urls = array($urls); + } + $urls = array_filter($urls); + + $urls = array_map('urldecode', $urls); + foreach ($urls as &$url) { + $url = $this->removeTrailingSlash($url); + if (strpos($url, 'http') !== 0) { + $url = 'http://' . $url; + } + $url = Piwik_Common::sanitizeInputValue($url); + } + $urls = array_unique($urls); + return $urls; + } + + public function getPatternMatchSites($pattern) + { + $ids = $this->getSitesIdWithAtLeastViewAccess(); + if (empty($ids)) { + return array(); + } + + $ids_str = ''; + foreach ($ids as $id_num => $id_val) { + $ids_str .= $id_val . ' , '; + } + $ids_str .= $id_val; + + $db = Zend_Registry::get('db'); + $bind = array('%' . $pattern . '%', 'http%' . $pattern . '%'); + + // Also match the idsite + $where = ''; + if (is_numeric($pattern)) { + $bind[] = $pattern; + $where = 'OR s.idsite = ?'; + } + $sites = $db->fetchAll("SELECT idsite, name, main_url + FROM " . Piwik_Common::prefixTable('site') . " s WHERE ( s.name like ? OR s.main_url like ? $where ) AND idsite in ($ids_str) - LIMIT ".Piwik::getWebsitesCountToDisplay(), - $bind) ; - return $sites; - } - - /** - * Utility function that throws if a value is not valid for the 'keep_url_fragment' - * column of the piwik_site table. - * - * @param int $keepURLFragments - * @throws Exception - */ - private static function checkKeepURLFragmentsValue( $keepURLFragments ) - { - // make sure value is between 0 & 2 - if (!in_array($keepURLFragments, array(0,1,2))) - { - throw new Exception("Error in SitesManager.updateSite: keepURLFragments must be between 0 & 2" + - " (actual value: $keepURLFragments)."); - } - } + LIMIT " . Piwik::getWebsitesCountToDisplay(), + $bind); + return $sites; + } + + /** + * Utility function that throws if a value is not valid for the 'keep_url_fragment' + * column of the piwik_site table. + * + * @param int $keepURLFragments + * @throws Exception + */ + private static function checkKeepURLFragmentsValue($keepURLFragments) + { + // make sure value is between 0 & 2 + if (!in_array($keepURLFragments, array(0, 1, 2))) { + throw new Exception("Error in SitesManager.updateSite: keepURLFragments must be between 0 & 2" + + " (actual value: $keepURLFragments)."); + } + } } diff --git a/plugins/SitesManager/Controller.php b/plugins/SitesManager/Controller.php index 6b230e8193..c99ed03705 100644 --- a/plugins/SitesManager/Controller.php +++ b/plugins/SitesManager/Controller.php @@ -15,189 +15,178 @@ */ class Piwik_SitesManager_Controller extends Piwik_Controller_Admin { - /* - * Main view showing listing of websites and settings - */ - function index() - { - $view = Piwik_View::factory('SitesManager'); - - if (Piwik::isUserIsSuperUser()) - { - $sites = Piwik_SitesManager_API::getInstance()->getAllSites(); - Piwik_Site::setSites($sites); - $sites = array_values($sites); - } - else - { - $sites = Piwik_SitesManager_API::getInstance()->getSitesWithAdminAccess(); - Piwik_Site::setSitesFromArray($sites); - } - - foreach($sites as &$site) - { - $site['alias_urls'] = Piwik_SitesManager_API::getInstance()->getSiteUrlsFromId($site['idsite']); - $site['excluded_ips'] = str_replace(',','
', $site['excluded_ips']); - $site['excluded_parameters'] = str_replace(',','
', $site['excluded_parameters']); - $site['excluded_user_agents'] = str_replace(',', '
', $site['excluded_user_agents']); - } - $view->adminSites = $sites; - $view->adminSitesCount = count($sites); - - $timezones = Piwik_SitesManager_API::getInstance()->getTimezonesList(); - $view->timezoneSupported = Piwik::isTimezoneSupportEnabled(); - $view->timezones = Piwik_Common::json_encode($timezones); - $view->defaultTimezone = Piwik_SitesManager_API::getInstance()->getDefaultTimezone(); - - $view->currencies = Piwik_Common::json_encode(Piwik_SitesManager_API::getInstance()->getCurrencyList()); - $view->defaultCurrency = Piwik_SitesManager_API::getInstance()->getDefaultCurrency(); - - $view->utcTime = Piwik_Date::now()->getDatetime(); - $excludedIpsGlobal = Piwik_SitesManager_API::getInstance()->getExcludedIpsGlobal(); - $view->globalExcludedIps = str_replace(',',"\n", $excludedIpsGlobal); - $excludedQueryParametersGlobal = Piwik_SitesManager_API::getInstance()->getExcludedQueryParametersGlobal(); - $view->globalExcludedQueryParameters = str_replace(',',"\n", $excludedQueryParametersGlobal); - - $globalExcludedUserAgents = Piwik_SitesManager_API::getInstance()->getExcludedUserAgentsGlobal(); - $view->globalExcludedUserAgents = str_replace(',', "\n", $globalExcludedUserAgents); - - $view->globalSearchKeywordParameters = Piwik_SitesManager_API::getInstance()->getSearchKeywordParametersGlobal(); - $view->globalSearchCategoryParameters = Piwik_SitesManager_API::getInstance()->getSearchCategoryParametersGlobal(); - $view->isSearchCategoryTrackingEnabled = Piwik_PluginsManager::getInstance()->isPluginActivated('CustomVariables'); - $view->allowSiteSpecificUserAgentExclude = - Piwik_SitesManager_API::getInstance()->isSiteSpecificUserAgentExcludeEnabled(); - - $view->globalKeepURLFragments = Piwik_SitesManager_API::getInstance()->getKeepURLFragmentsGlobal(); - - $view->currentIpAddress = Piwik_IP::getIpFromHeader(); - - $view->showAddSite = (boolean) Piwik_Common::getRequestVar('showaddsite', false); - - $this->setBasicVariablesView($view); - $view->menu = Piwik_GetAdminMenu(); - echo $view->render(); - } - - /* - * Records Global settings when user submit changes - */ - function setGlobalSettings() - { - $response = new Piwik_API_ResponseBuilder(Piwik_Common::getRequestVar('format')); - - try { - $this->checkTokenInUrl(); - $timezone = Piwik_Common::getRequestVar('timezone', false); - $excludedIps = Piwik_Common::getRequestVar('excludedIps', false); - $excludedQueryParameters = Piwik_Common::getRequestVar('excludedQueryParameters', false); - $excludedUserAgents = Piwik_Common::getRequestVar('excludedUserAgents', false); - $currency = Piwik_Common::getRequestVar('currency', false); - $searchKeywordParameters = Piwik_Common::getRequestVar('searchKeywordParameters', $default = ""); - $searchCategoryParameters = Piwik_Common::getRequestVar('searchCategoryParameters', $default = ""); - $enableSiteUserAgentExclude = Piwik_Common::getRequestVar('enableSiteUserAgentExclude', $default = 0); - $keepURLFragments = Piwik_Common::getRequestVar('keepURLFragments', $default = 0); - - $api = Piwik_SitesManager_API::getInstance(); - $api->setDefaultTimezone($timezone); - $api->setDefaultCurrency($currency); - $api->setGlobalExcludedQueryParameters($excludedQueryParameters); - $api->setGlobalExcludedIps($excludedIps); - $api->setGlobalExcludedUserAgents($excludedUserAgents); - $api->setGlobalSearchParameters($searchKeywordParameters, $searchCategoryParameters); - $api->setSiteSpecificUserAgentExcludeEnabled($enableSiteUserAgentExclude == 1); - $api->setKeepURLFragmentsGlobal($keepURLFragments); - - $toReturn = $response->getResponse(); - } catch(Exception $e ) { - $toReturn = $response->getResponseException( $e ); - } - echo $toReturn; - } - - /** - * Displays the admin UI page showing all tracking tags - * @return unknown_type - */ - function displayJavascriptCode() - { - $idSite = Piwik_Common::getRequestVar('idSite'); - Piwik::checkUserHasViewAccess($idSite); - $jsTag = Piwik::getJavascriptCode($idSite, Piwik_Url::getCurrentUrlWithoutFileName()); - $view = Piwik_View::factory('Tracking'); - $this->setBasicVariablesView($view); - $view->menu = Piwik_GetAdminMenu(); - $view->idSite = $idSite; - $site = new Piwik_Site($idSite); - $view->displaySiteName = $site->getName(); - $view->jsTag = $jsTag; - echo $view->render(); - } - - /* - * User will download a file called PiwikTracker.php that is the content of the actual script - */ - function downloadPiwikTracker() - { - $path = PIWIK_INCLUDE_PATH . '/libs/PiwikTracker/'; - $filename = 'PiwikTracker.php'; - header('Content-type: text/php'); - header('Content-Disposition: attachment; filename="'.$filename.'"'); - echo file_get_contents( $path . $filename); - } - - /** - * Used to generate the doc at http://piwik.org/docs/tracking-api/ - */ - function displayAlternativeTagsHelp() - { - $view = Piwik_View::factory('DisplayAlternativeTags'); - $view->idSite = Piwik_Common::getRequestVar('idSite'); - $url = Piwik_Common::getRequestVar('piwikUrl', '', 'string'); - if(empty($url) - || !Piwik_Common::isLookLikeUrl($url) ) - { - $url = $view->piwikUrl; - } - $view->piwikUrlRequest = $url; - $view->calledExternally = true; - echo $view->render(); - } - - function getSitesForAutocompleter() - { - $pattern = Piwik_Common::getRequestVar('term'); - $sites = Piwik_SitesManager_API::getInstance()->getPatternMatchSites($pattern); - $pattern = str_replace('%', '', $pattern); - if(!count($sites)) - { - $results[] = array('label' => Piwik_Translate('SitesManager_NotFound')." $pattern.", 'id' => '#'); - } - else - { - if(strpos($pattern, '/') !== false - && strpos($pattern, '\\/') === false) - { - $pattern = str_replace('/', '\\/', $pattern); - } - foreach($sites as $s) - { - $hl_name = $s['name']; - if(strlen($pattern) > 0) - { - @preg_match_all("/$pattern+/i", $hl_name, $matches); - if (is_array($matches[0]) && count($matches[0]) >= 1) - { - foreach ($matches[0] as $match) - { - $hl_name = str_replace($match, ''.$match.'', $s['name']); - } - } - } - $results[] = array('label' => $hl_name, 'id' => $s['idsite'], 'name' => $s['name'] ); - } - } - - Piwik_DataTable_Renderer_Json::sendHeaderJSON(); - print Piwik_Common::json_encode($results); - } + /* + * Main view showing listing of websites and settings + */ + function index() + { + $view = Piwik_View::factory('SitesManager'); + + if (Piwik::isUserIsSuperUser()) { + $sites = Piwik_SitesManager_API::getInstance()->getAllSites(); + Piwik_Site::setSites($sites); + $sites = array_values($sites); + } else { + $sites = Piwik_SitesManager_API::getInstance()->getSitesWithAdminAccess(); + Piwik_Site::setSitesFromArray($sites); + } + + foreach ($sites as &$site) { + $site['alias_urls'] = Piwik_SitesManager_API::getInstance()->getSiteUrlsFromId($site['idsite']); + $site['excluded_ips'] = str_replace(',', '
', $site['excluded_ips']); + $site['excluded_parameters'] = str_replace(',', '
', $site['excluded_parameters']); + $site['excluded_user_agents'] = str_replace(',', '
', $site['excluded_user_agents']); + } + $view->adminSites = $sites; + $view->adminSitesCount = count($sites); + + $timezones = Piwik_SitesManager_API::getInstance()->getTimezonesList(); + $view->timezoneSupported = Piwik::isTimezoneSupportEnabled(); + $view->timezones = Piwik_Common::json_encode($timezones); + $view->defaultTimezone = Piwik_SitesManager_API::getInstance()->getDefaultTimezone(); + + $view->currencies = Piwik_Common::json_encode(Piwik_SitesManager_API::getInstance()->getCurrencyList()); + $view->defaultCurrency = Piwik_SitesManager_API::getInstance()->getDefaultCurrency(); + + $view->utcTime = Piwik_Date::now()->getDatetime(); + $excludedIpsGlobal = Piwik_SitesManager_API::getInstance()->getExcludedIpsGlobal(); + $view->globalExcludedIps = str_replace(',', "\n", $excludedIpsGlobal); + $excludedQueryParametersGlobal = Piwik_SitesManager_API::getInstance()->getExcludedQueryParametersGlobal(); + $view->globalExcludedQueryParameters = str_replace(',', "\n", $excludedQueryParametersGlobal); + + $globalExcludedUserAgents = Piwik_SitesManager_API::getInstance()->getExcludedUserAgentsGlobal(); + $view->globalExcludedUserAgents = str_replace(',', "\n", $globalExcludedUserAgents); + + $view->globalSearchKeywordParameters = Piwik_SitesManager_API::getInstance()->getSearchKeywordParametersGlobal(); + $view->globalSearchCategoryParameters = Piwik_SitesManager_API::getInstance()->getSearchCategoryParametersGlobal(); + $view->isSearchCategoryTrackingEnabled = Piwik_PluginsManager::getInstance()->isPluginActivated('CustomVariables'); + $view->allowSiteSpecificUserAgentExclude = + Piwik_SitesManager_API::getInstance()->isSiteSpecificUserAgentExcludeEnabled(); + + $view->globalKeepURLFragments = Piwik_SitesManager_API::getInstance()->getKeepURLFragmentsGlobal(); + + $view->currentIpAddress = Piwik_IP::getIpFromHeader(); + + $view->showAddSite = (boolean)Piwik_Common::getRequestVar('showaddsite', false); + + $this->setBasicVariablesView($view); + $view->menu = Piwik_GetAdminMenu(); + echo $view->render(); + } + + /* + * Records Global settings when user submit changes + */ + function setGlobalSettings() + { + $response = new Piwik_API_ResponseBuilder(Piwik_Common::getRequestVar('format')); + + try { + $this->checkTokenInUrl(); + $timezone = Piwik_Common::getRequestVar('timezone', false); + $excludedIps = Piwik_Common::getRequestVar('excludedIps', false); + $excludedQueryParameters = Piwik_Common::getRequestVar('excludedQueryParameters', false); + $excludedUserAgents = Piwik_Common::getRequestVar('excludedUserAgents', false); + $currency = Piwik_Common::getRequestVar('currency', false); + $searchKeywordParameters = Piwik_Common::getRequestVar('searchKeywordParameters', $default = ""); + $searchCategoryParameters = Piwik_Common::getRequestVar('searchCategoryParameters', $default = ""); + $enableSiteUserAgentExclude = Piwik_Common::getRequestVar('enableSiteUserAgentExclude', $default = 0); + $keepURLFragments = Piwik_Common::getRequestVar('keepURLFragments', $default = 0); + + $api = Piwik_SitesManager_API::getInstance(); + $api->setDefaultTimezone($timezone); + $api->setDefaultCurrency($currency); + $api->setGlobalExcludedQueryParameters($excludedQueryParameters); + $api->setGlobalExcludedIps($excludedIps); + $api->setGlobalExcludedUserAgents($excludedUserAgents); + $api->setGlobalSearchParameters($searchKeywordParameters, $searchCategoryParameters); + $api->setSiteSpecificUserAgentExcludeEnabled($enableSiteUserAgentExclude == 1); + $api->setKeepURLFragmentsGlobal($keepURLFragments); + + $toReturn = $response->getResponse(); + } catch (Exception $e) { + $toReturn = $response->getResponseException($e); + } + echo $toReturn; + } + + /** + * Displays the admin UI page showing all tracking tags + * @return unknown_type + */ + function displayJavascriptCode() + { + $idSite = Piwik_Common::getRequestVar('idSite'); + Piwik::checkUserHasViewAccess($idSite); + $jsTag = Piwik::getJavascriptCode($idSite, Piwik_Url::getCurrentUrlWithoutFileName()); + $view = Piwik_View::factory('Tracking'); + $this->setBasicVariablesView($view); + $view->menu = Piwik_GetAdminMenu(); + $view->idSite = $idSite; + $site = new Piwik_Site($idSite); + $view->displaySiteName = $site->getName(); + $view->jsTag = $jsTag; + echo $view->render(); + } + + /* + * User will download a file called PiwikTracker.php that is the content of the actual script + */ + function downloadPiwikTracker() + { + $path = PIWIK_INCLUDE_PATH . '/libs/PiwikTracker/'; + $filename = 'PiwikTracker.php'; + header('Content-type: text/php'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + echo file_get_contents($path . $filename); + } + + /** + * Used to generate the doc at http://piwik.org/docs/tracking-api/ + */ + function displayAlternativeTagsHelp() + { + $view = Piwik_View::factory('DisplayAlternativeTags'); + $view->idSite = Piwik_Common::getRequestVar('idSite'); + $url = Piwik_Common::getRequestVar('piwikUrl', '', 'string'); + if (empty($url) + || !Piwik_Common::isLookLikeUrl($url) + ) { + $url = $view->piwikUrl; + } + $view->piwikUrlRequest = $url; + $view->calledExternally = true; + echo $view->render(); + } + + function getSitesForAutocompleter() + { + $pattern = Piwik_Common::getRequestVar('term'); + $sites = Piwik_SitesManager_API::getInstance()->getPatternMatchSites($pattern); + $pattern = str_replace('%', '', $pattern); + if (!count($sites)) { + $results[] = array('label' => Piwik_Translate('SitesManager_NotFound') . " $pattern.", 'id' => '#'); + } else { + if (strpos($pattern, '/') !== false + && strpos($pattern, '\\/') === false + ) { + $pattern = str_replace('/', '\\/', $pattern); + } + foreach ($sites as $s) { + $hl_name = $s['name']; + if (strlen($pattern) > 0) { + @preg_match_all("/$pattern+/i", $hl_name, $matches); + if (is_array($matches[0]) && count($matches[0]) >= 1) { + foreach ($matches[0] as $match) { + $hl_name = str_replace($match, '' . $match . '', $s['name']); + } + } + } + $results[] = array('label' => $hl_name, 'id' => $s['idsite'], 'name' => $s['name']); + } + } + + Piwik_DataTable_Renderer_Json::sendHeaderJSON(); + print Piwik_Common::json_encode($results); + } } diff --git a/plugins/SitesManager/SitesManager.php b/plugins/SitesManager/SitesManager.php index 6f596f9f87..b258a29b8f 100644 --- a/plugins/SitesManager/SitesManager.php +++ b/plugins/SitesManager/SitesManager.php @@ -15,212 +15,204 @@ */ class Piwik_SitesManager extends Piwik_Plugin { - const KEEP_URL_FRAGMENT_USE_DEFAULT = 0; - const KEEP_URL_FRAGMENT_YES = 1; - const KEEP_URL_FRAGMENT_NO = 2; - - public function getInformation() - { - $info = array( - 'description' => Piwik_Translate('SitesManager_PluginDescription'), - 'author' => 'Piwik', - 'author_homepage' => 'http://piwik.org/', - 'version' => Piwik_Version::VERSION, - ); - return $info; - } - - function getListHooksRegistered() - { - return array( - 'AssetManager.getJsFiles' => 'getJsFiles', - 'AssetManager.getCssFiles' => 'getCssFiles', - 'AdminMenu.add' => 'addMenu', - 'Common.fetchWebsiteAttributes' => 'recordWebsiteDataInCache', - ); - } - - function addMenu() - { - Piwik_AddAdminSubMenu('CoreAdminHome_MenuManage', 'SitesManager_MenuSites', - array('module' => 'SitesManager', 'action' => 'index'), - Piwik::isUserHasSomeAdminAccess(), - $order = 1); - } - - /** - * Get CSS files - * - * @param Piwik_Event_Notification $notification notification object - */ - function getCssFiles( $notification ) - { - $cssFiles = &$notification->getNotificationObject(); - - $cssFiles[] = "themes/default/styles.css"; - } - - /** - * Get JavaScript files - * - * @param Piwik_Event_Notification $notification notification object - */ - function getJsFiles( $notification ) - { - $jsFiles = &$notification->getNotificationObject(); - - $jsFiles[] = "plugins/SitesManager/templates/SitesManager.js"; - } - - /** - * Hooks when a website tracker cache is flushed (website updated, cache deleted, or empty cache) - * Will record in the tracker config file all data needed for this website in Tracker. - * - * @param Piwik_Event_Notification $notification notification object - * @return void - */ - function recordWebsiteDataInCache($notification) - { - $idSite = (int)$notification->getNotificationInfo(); - // add the 'hosts' entry in the website array - $array =& $notification->getNotificationObject(); - $array['hosts'] = $this->getTrackerHosts($idSite); - - $website = Piwik_SitesManager_API::getInstance()->getSiteFromId($idSite); - $array['excluded_ips'] = $this->getTrackerExcludedIps($website); - $array['excluded_parameters'] = self::getTrackerExcludedQueryParameters($website); - $array['excluded_user_agents'] = self::getExcludedUserAgents($website); - $array['keep_url_fragment'] = self::shouldKeepURLFragmentsFor($website); - $array['sitesearch'] = $website['sitesearch']; - $array['sitesearch_keyword_parameters'] = $this->getTrackerSearchKeywordParameters($website); - $array['sitesearch_category_parameters'] = $this->getTrackerSearchCategoryParameters($website); - } - - /** - * Returns whether we should keep URL fragments for a specific site. - * - * @param array $site DB data for the site. - * @return bool - */ - private static function shouldKeepURLFragmentsFor( $site ) - { - if ($site['keep_url_fragment'] == self::KEEP_URL_FRAGMENT_YES) - { - return true; - } - else if ($site['keep_url_fragment'] == self::KEEP_URL_FRAGMENT_NO) - { - return false; - } - - return Piwik_SitesManager_API::getInstance()->getKeepURLFragmentsGlobal(); - } - - private function getTrackerSearchKeywordParameters($website) - { - $searchParameters = $website['sitesearch_keyword_parameters']; - if(empty($searchParameters)) { - $searchParameters = Piwik_SitesManager_API::getInstance()->getSearchKeywordParametersGlobal(); - } - return explode(",", $searchParameters); - } - - private function getTrackerSearchCategoryParameters($website) - { - $searchParameters = $website['sitesearch_category_parameters']; - if(empty($searchParameters)) { - $searchParameters = Piwik_SitesManager_API::getInstance()->getSearchCategoryParametersGlobal(); - } - return explode(",", $searchParameters); - } - - /** - * Returns the array of excluded IPs to save in the config file - * - * @return array - */ - private function getTrackerExcludedIps($website) - { - $excludedIps = $website['excluded_ips']; - $globalExcludedIps = Piwik_SitesManager_API::getInstance()->getExcludedIpsGlobal(); - - $excludedIps .= ',' . $globalExcludedIps; - - $ipRanges = array(); - foreach(explode(',', $excludedIps) as $ip) - { - $ipRange = Piwik_SitesManager_API::getInstance()->getIpsForRange($ip); - if($ipRange !== false) - { - $ipRanges[] = $ipRange; - } - } - return $ipRanges; - } - - /** - * Returns the array of excluded user agent substrings for a site. Filters out - * any garbage data & trims each entry. - * - * @param array $website The full set of information for a site. - * @return array - */ - private static function getExcludedUserAgents( $website ) - { - $excludedUserAgents = Piwik_SitesManager_API::getInstance()->getExcludedUserAgentsGlobal(); - if (Piwik_SitesManager_API::getInstance()->isSiteSpecificUserAgentExcludeEnabled()) - { - $excludedUserAgents .= ','.$website['excluded_user_agents']; - } - return self::filterBlankFromCommaSepList($excludedUserAgents); - } - - /** - * Returns the array of URL query parameters to exclude from URLs - * - * @return array - */ - public static function getTrackerExcludedQueryParameters($website) - { - $excludedQueryParameters = $website['excluded_parameters']; - $globalExcludedQueryParameters = Piwik_SitesManager_API::getInstance()->getExcludedQueryParametersGlobal(); - - $excludedQueryParameters .= ',' . $globalExcludedQueryParameters; - return self::filterBlankFromCommaSepList($excludedQueryParameters); - } - - /** - * Trims each element of a comma-separated list of strings, removes empty elements and - * returns the result (as an array). - * - * @param string $parameters The unfiltered list. - * @return array The filtered list of strings as an array. - */ - static private function filterBlankFromCommaSepList( $parameters ) - { - $parameters = explode(',', $parameters); - $parameters = array_filter($parameters, 'strlen'); - $parameters = array_unique($parameters); - return $parameters; - } - - /** - * Returns the hosts alias URLs - * @param int $idSite - * @return array - */ - private function getTrackerHosts($idSite) - { - $urls = Piwik_SitesManager_API::getInstance()->getSiteUrlsFromId($idSite); - $hosts = array(); - foreach($urls as $url) - { - $url = parse_url($url); - if(isset($url['host'])) - { - $hosts[] = $url['host']; - } - } - return $hosts; - } + const KEEP_URL_FRAGMENT_USE_DEFAULT = 0; + const KEEP_URL_FRAGMENT_YES = 1; + const KEEP_URL_FRAGMENT_NO = 2; + + public function getInformation() + { + $info = array( + 'description' => Piwik_Translate('SitesManager_PluginDescription'), + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'version' => Piwik_Version::VERSION, + ); + return $info; + } + + function getListHooksRegistered() + { + return array( + 'AssetManager.getJsFiles' => 'getJsFiles', + 'AssetManager.getCssFiles' => 'getCssFiles', + 'AdminMenu.add' => 'addMenu', + 'Common.fetchWebsiteAttributes' => 'recordWebsiteDataInCache', + ); + } + + function addMenu() + { + Piwik_AddAdminSubMenu('CoreAdminHome_MenuManage', 'SitesManager_MenuSites', + array('module' => 'SitesManager', 'action' => 'index'), + Piwik::isUserHasSomeAdminAccess(), + $order = 1); + } + + /** + * Get CSS files + * + * @param Piwik_Event_Notification $notification notification object + */ + function getCssFiles($notification) + { + $cssFiles = & $notification->getNotificationObject(); + + $cssFiles[] = "themes/default/styles.css"; + } + + /** + * Get JavaScript files + * + * @param Piwik_Event_Notification $notification notification object + */ + function getJsFiles($notification) + { + $jsFiles = & $notification->getNotificationObject(); + + $jsFiles[] = "plugins/SitesManager/templates/SitesManager.js"; + } + + /** + * Hooks when a website tracker cache is flushed (website updated, cache deleted, or empty cache) + * Will record in the tracker config file all data needed for this website in Tracker. + * + * @param Piwik_Event_Notification $notification notification object + * @return void + */ + function recordWebsiteDataInCache($notification) + { + $idSite = (int)$notification->getNotificationInfo(); + // add the 'hosts' entry in the website array + $array =& $notification->getNotificationObject(); + $array['hosts'] = $this->getTrackerHosts($idSite); + + $website = Piwik_SitesManager_API::getInstance()->getSiteFromId($idSite); + $array['excluded_ips'] = $this->getTrackerExcludedIps($website); + $array['excluded_parameters'] = self::getTrackerExcludedQueryParameters($website); + $array['excluded_user_agents'] = self::getExcludedUserAgents($website); + $array['keep_url_fragment'] = self::shouldKeepURLFragmentsFor($website); + $array['sitesearch'] = $website['sitesearch']; + $array['sitesearch_keyword_parameters'] = $this->getTrackerSearchKeywordParameters($website); + $array['sitesearch_category_parameters'] = $this->getTrackerSearchCategoryParameters($website); + } + + /** + * Returns whether we should keep URL fragments for a specific site. + * + * @param array $site DB data for the site. + * @return bool + */ + private static function shouldKeepURLFragmentsFor($site) + { + if ($site['keep_url_fragment'] == self::KEEP_URL_FRAGMENT_YES) { + return true; + } else if ($site['keep_url_fragment'] == self::KEEP_URL_FRAGMENT_NO) { + return false; + } + + return Piwik_SitesManager_API::getInstance()->getKeepURLFragmentsGlobal(); + } + + private function getTrackerSearchKeywordParameters($website) + { + $searchParameters = $website['sitesearch_keyword_parameters']; + if (empty($searchParameters)) { + $searchParameters = Piwik_SitesManager_API::getInstance()->getSearchKeywordParametersGlobal(); + } + return explode(",", $searchParameters); + } + + private function getTrackerSearchCategoryParameters($website) + { + $searchParameters = $website['sitesearch_category_parameters']; + if (empty($searchParameters)) { + $searchParameters = Piwik_SitesManager_API::getInstance()->getSearchCategoryParametersGlobal(); + } + return explode(",", $searchParameters); + } + + /** + * Returns the array of excluded IPs to save in the config file + * + * @return array + */ + private function getTrackerExcludedIps($website) + { + $excludedIps = $website['excluded_ips']; + $globalExcludedIps = Piwik_SitesManager_API::getInstance()->getExcludedIpsGlobal(); + + $excludedIps .= ',' . $globalExcludedIps; + + $ipRanges = array(); + foreach (explode(',', $excludedIps) as $ip) { + $ipRange = Piwik_SitesManager_API::getInstance()->getIpsForRange($ip); + if ($ipRange !== false) { + $ipRanges[] = $ipRange; + } + } + return $ipRanges; + } + + /** + * Returns the array of excluded user agent substrings for a site. Filters out + * any garbage data & trims each entry. + * + * @param array $website The full set of information for a site. + * @return array + */ + private static function getExcludedUserAgents($website) + { + $excludedUserAgents = Piwik_SitesManager_API::getInstance()->getExcludedUserAgentsGlobal(); + if (Piwik_SitesManager_API::getInstance()->isSiteSpecificUserAgentExcludeEnabled()) { + $excludedUserAgents .= ',' . $website['excluded_user_agents']; + } + return self::filterBlankFromCommaSepList($excludedUserAgents); + } + + /** + * Returns the array of URL query parameters to exclude from URLs + * + * @return array + */ + public static function getTrackerExcludedQueryParameters($website) + { + $excludedQueryParameters = $website['excluded_parameters']; + $globalExcludedQueryParameters = Piwik_SitesManager_API::getInstance()->getExcludedQueryParametersGlobal(); + + $excludedQueryParameters .= ',' . $globalExcludedQueryParameters; + return self::filterBlankFromCommaSepList($excludedQueryParameters); + } + + /** + * Trims each element of a comma-separated list of strings, removes empty elements and + * returns the result (as an array). + * + * @param string $parameters The unfiltered list. + * @return array The filtered list of strings as an array. + */ + static private function filterBlankFromCommaSepList($parameters) + { + $parameters = explode(',', $parameters); + $parameters = array_filter($parameters, 'strlen'); + $parameters = array_unique($parameters); + return $parameters; + } + + /** + * Returns the hosts alias URLs + * @param int $idSite + * @return array + */ + private function getTrackerHosts($idSite) + { + $urls = Piwik_SitesManager_API::getInstance()->getSiteUrlsFromId($idSite); + $hosts = array(); + foreach ($urls as $url) { + $url = parse_url($url); + if (isset($url['host'])) { + $hosts[] = $url['host']; + } + } + return $hosts; + } } diff --git a/plugins/SitesManager/templates/DisplayAlternativeTags.tpl b/plugins/SitesManager/templates/DisplayAlternativeTags.tpl index 25328f351f..3dee0c15c4 100644 --- a/plugins/SitesManager/templates/DisplayAlternativeTags.tpl +++ b/plugins/SitesManager/templates/DisplayAlternativeTags.tpl @@ -1,101 +1,124 @@

Image Tracker code

-The Image Tracker code can be used when Javascript is not allowed. -
+The Image Tracker code can be used when Javascript is not allowed. +
+
-

Some websites like MySpace or eBay will not allow users to add Javascript to their profile but accept HTML. In this case, you can still track visits with Piwik using the Image Tracker. -
-Note: the code doesn't use Javascript so Piwik will not be able to track some user information - such as search keywords, referrers, screen resolutions, browser plugins and page titles. -

- -<!-- Piwik Image Tracker -->
-<img src="{if isset($piwikUrlRequest)}{$piwikUrlRequest}{else}{$piwikUrl}{/if}piwik.php?idsite={$idSite}&amp;rec=1" style="border:0" alt="" />
-<!-- End Piwik -->
-
-
-The following parameters can also be passed to the image URL: -
    -
  • rec - (required) The parameter &rec=1 is required to force the request to be recorded
  • -
  • idsite - (required) Defines the Website ID being tracked
  • -
  • action_name - Defines the custom Page Title for this page view
  • -
  • urlref - The Referrer URL: must be set to the referrer URL used before landing on the page containing the Image tracker. For example, in PHP this value is accessible via
    $_SERVER['HTTP_REFERER']
  • -
  • idgoal - The request will trigger the given Goal
  • -
  • revenue - Used with idgoal, defines the custom revenue for this conversion
  • -
  • and more! - There are many more parameters you can set beyond the main ones above. See the Tracking API documentation page.
  • -
+

Some websites like MySpace or eBay will not allow users to add Javascript to their profile but accept HTML. In this case, you can still track visits with + Piwik using the Image Tracker. +
+ Note: the code doesn't use Javascript so Piwik will not be able to track some user information + such as search keywords, referrers, screen resolutions, browser plugins and page titles. +

+ + <!-- Piwik Image Tracker -->
+ <img src="{if isset($piwikUrlRequest)}{$piwikUrlRequest}{else}{$piwikUrl}{/if}piwik.php?idsite={$idSite}&amp;rec=1" style="border:0" alt="" />
+ <!-- End Piwik -->
+
+
+ The following parameters can also be passed to the image URL: +
    +
  • rec - (required) The parameter &rec=1 is required to force the request to be recorded
  • +
  • idsite - (required) Defines the Website ID being tracked
  • +
  • action_name - Defines the custom Page Title for this page view
  • +
  • urlref - The Referrer URL: must be set to the referrer URL used before landing on the page containing the Image tracker. For example, in PHP + this value is accessible via +
    $_SERVER['HTTP_REFERER']
    +
  • +
  • idgoal - The request will trigger the given Goal
  • +
  • revenue - Used with idgoal, defines the custom revenue for this conversion
  • +
  • and more! - There are many more parameters you can set beyond the main ones above. See the Tracking API documentation page. +
  • +

Piwik Tracking API (Advanced users)

-It is also possible to call the Piwik Tracking API using your favorite programming language. -
+It is also possible to call the Piwik Tracking API using your favorite programming language. +
+
-

-The Piwik Tracking API allows to trigger visits (page views and Goal conversions) from any environment (Desktop App, iPhone or Android app, Mobile website, etc.). -

+

+ The Piwik Tracking API allows to trigger visits (page views and Goal conversions) from any environment (Desktop App, iPhone or Android app, Mobile + website, etc.). +

-

We currently provide a PHP client to call the API from your PHP projects. -If you would like to contribute a version of the client in another programming language (Python, Java, Ruby, Perl, etc.) please create a ticket in our developer area (please attach the client code to the ticket). -

Follow these instructions to get started with the Tracking API: -

    -
  • Click here to download the file PiwikTracker.php -
  • Upload the PiwikTracker.php file in the same path as your project files -
  • Copy the following code, then paste it onto every page you want to track. - -<?php
    -// -- Piwik Tracking API init --
    -require_once "/path/to/PiwikTracker.php";
    -PiwikTracker::$URL = '{if isset($piwikUrlRequest)}{$piwikUrlRequest}{else}{$piwikUrl}{/if}';
    - ?> -
    -
  • Choose a Tracking method, then paste the code onto every page you want to track. +

    We currently provide a PHP client to call the API from your PHP projects. + If you would like to contribute a version of the client in another programming language (Python, Java, Ruby, Perl, etc.) please create + a ticket in our developer area (please attach the client code to the ticket). +

    -
      -
    • Method 1: Advanced Image Tracker -
      -

      The client is used to generate the tracking URL that is wrapped inside a HTML <img src=''> code. -
      Paste this code before the </body> code in your pages. - -<?php
      -// Example 1: Tracks a pageview for Website id = {$idSite}
      -echo '<img src="'. str_replace("&","&amp;", Piwik_getUrlTrackPageView( $idSite = {$idSite}, $customTitle = 'This title will appear in the report Actions > Page titles')) . '" alt="" />';
      -// Example 2: Triggers a Goal conversion for Website id = {$idSite} and Goal id = 2
      -// $customRevenue is optional and is set to the amount generated by the current transaction (in online shops for example)
      -echo '<img src="'. str_replace("&","&amp;", Piwik_getUrlTrackGoal( $idSite = {$idSite}, $idGoal = 2, $customRevenue = 39)) . '" alt="" />';
      - ?> -
      -
      -The Advanced Image Tracker method is similar to using the standard Javascript Tracking code. However, some user settings are not detected (resolution, local time, plugins and cookie support). -

      - -
    • -
    • Method 2: HTTP Request -
      -

      You can also query the Piwik Tracker API remotely via HTTP. -This is useful for environment where you can't execute HTML nor Javascript. -
      Paste this code anywhere in your code where you wish to track a user interaction. - - -<?php
      -$piwikTracker = new PiwikTracker( $idSite = {$idSite} );
      -// You can manually set the visitor details (resolution, time, plugins, etc.)
      -// See all other ->set* functions available in the PiwikTracker.php file
      -$piwikTracker->setResolution(1600, 1400);

      -// Sends Tracker request via http
      -$piwikTracker->doTrackPageView('Document title of current page view');

      -// You can also track Goal conversions
      -$piwikTracker->doTrackGoal($idGoal = 1, $revenue = 42);
      - ?> -
      -

      -
    -
  • -
-

-{if !isset($calledExternally) || !$calledExternally} -

- Read more about the Piwik Tracking API in the documentation -

-{/if} +

Follow these instructions to get started with the Tracking API: +

    +
  • Click here to + download the file PiwikTracker.php +
  • +
  • Upload the PiwikTracker.php file in the same path as your project files +
  • +
  • Copy the following code, then paste it onto every page you want to track. + + <?php
    + // -- Piwik Tracking API init --
    + require_once "/path/to/PiwikTracker.php";
    + PiwikTracker::$URL = '{if isset($piwikUrlRequest)}{$piwikUrlRequest}{else}{$piwikUrl}{/if}';
    + ?> +
    +
  • +
  • Choose a Tracking method, then paste the code onto every page you want to track. + +
      +
    • Method 1: Advanced Image Tracker +
      + +

      The client is used to generate the tracking URL that is wrapped inside a HTML <img src=''> code. +
      Paste this code before the </body> code in your pages. + + <?php
      + // Example 1: Tracks a pageview for Website id = {$idSite}
      + echo '<img src="'. str_replace("&","&amp;", Piwik_getUrlTrackPageView( $idSite = {$idSite}, $customTitle = 'This title + will appear in the report Actions > Page titles')) . '" alt="" />';
      + // Example 2: Triggers a Goal conversion for Website id = {$idSite} and Goal id = 2
      + // $customRevenue is optional and is set to the amount generated by the current transaction (in online shops for example)
      + echo '<img src="'. str_replace("&","&amp;", Piwik_getUrlTrackGoal( $idSite = {$idSite}, $idGoal = 2, $customRevenue = + 39)) . '" alt="" />';
      + ?> +
      +
      + The Advanced Image Tracker method is similar to using the standard Javascript Tracking code. However, some user settings are not + detected (resolution, local time, plugins and cookie support). +

      + +
    • +
    • Method 2: HTTP Request +
      + +

      You can also query the Piwik Tracker API remotely via HTTP. + This is useful for environment where you can't execute HTML nor Javascript. +
      Paste this code anywhere in your code where you wish to track a user interaction. + + + <?php
      + $piwikTracker = new PiwikTracker( $idSite = {$idSite} );
      + // You can manually set the visitor details (resolution, time, plugins, etc.)
      + // See all other ->set* functions available in the PiwikTracker.php file
      + $piwikTracker->setResolution(1600, 1400);

      + // Sends Tracker request via http
      + $piwikTracker->doTrackPageView('Document title of current page view');

      + // You can also track Goal conversions
      + $piwikTracker->doTrackGoal($idGoal = 1, $revenue = 42);
      + ?> +
      +

      +
    • +
    +
  • +
+

+ {if !isset($calledExternally) || !$calledExternally} +

+ Read more about the Piwik Tracking API in the documentation +

+ {/if}
diff --git a/plugins/SitesManager/templates/DisplayJavascriptCode.tpl b/plugins/SitesManager/templates/DisplayJavascriptCode.tpl index 51bf27e2fd..b34e38fec0 100644 --- a/plugins/SitesManager/templates/DisplayJavascriptCode.tpl +++ b/plugins/SitesManager/templates/DisplayJavascriptCode.tpl @@ -1,66 +1,69 @@ - {literal} - + {/literal}

{'SitesManager_TrackingTags'|translate:$displaySiteName}

-{'Installation_JSTracking_Intro'|translate} -

-{'CoreAdminHome_JSTrackingIntro3'|translate:'':''} + {'Installation_JSTracking_Intro'|translate} +

+ {'CoreAdminHome_JSTrackingIntro3'|translate:'':''} -

{'SitesManager_JsTrackingTag'|translate}

-

{'CoreAdminHome_JSTracking_CodeNote'|translate:"</body>"}

+

{'SitesManager_JsTrackingTag'|translate}

-
{$jsTag}
+

{'CoreAdminHome_JSTracking_CodeNote'|translate:"</body>"}

-
-{'CoreAdminHome_JSTrackingIntro5'|translate:'':''} -

-{'Installation_JSTracking_EndNote'|translate:'':''} +
{$jsTag}
+ +
+ {'CoreAdminHome_JSTrackingIntro5'|translate:'':''} +

+ {'Installation_JSTracking_EndNote'|translate:'':''}
{literal} - + {/literal} diff --git a/plugins/SitesManager/templates/SitesManager.js b/plugins/SitesManager/templates/SitesManager.js index 70a42bf7db..24d6afa849 100644 --- a/plugins/SitesManager/templates/SitesManager.js +++ b/plugins/SitesManager/templates/SitesManager.js @@ -6,17 +6,16 @@ */ // NOTE: if you cannot find the definition of a variable here, look in SitesManager.tpl -function SitesManager ( _timezones, _currencies, _defaultTimezone, _defaultCurrency ) { +function SitesManager(_timezones, _currencies, _defaultTimezone, _defaultCurrency) { - var timezones = _timezones; - var currencies = _currencies; - var defaultTimezone = _defaultTimezone; - var defaultCurrency = _defaultCurrency; - var siteBeingEdited = false; - var siteBeingEditedName = ''; + var timezones = _timezones; + var currencies = _currencies; + var defaultTimezone = _defaultTimezone; + var defaultCurrency = _defaultCurrency; + var siteBeingEdited = false; + var siteBeingEditedName = ''; - function sendDeleteSiteAJAX( idSite ) - { + function sendDeleteSiteAJAX(idSite) { var ajaxHandler = new ajaxHelper(); ajaxHandler.addParams({ idSite: idSite, @@ -27,27 +26,26 @@ function SitesManager ( _timezones, _currencies, _defaultTimezone, _defaultCurre ajaxHandler.redirectOnSuccess(); ajaxHandler.setLoadingElement(); ajaxHandler.send(true); - } - - function sendAddSiteAJAX( row ) - { - var siteName = $(row).find('input#name').val(); - var urls = $(row).find('textarea#urls').val(); - urls = urls.trim().split("\n"); - var excludedIps = $(row).find('textarea#excludedIps').val(); - excludedIps = piwikHelper.getApiFormatTextarea(excludedIps); - var timezone = $(row).find('#timezones option:selected').val(); - var currency = $(row).find('#currencies option:selected').val(); - var excludedQueryParameters = $(row).find('textarea#excludedQueryParameters').val(); - excludedQueryParameters = piwikHelper.getApiFormatTextarea(excludedQueryParameters); - var excludedUserAgents = $(row).find('textarea#excludedUserAgents').val(); - excludedUserAgents = piwikHelper.getApiFormatTextarea(excludedUserAgents); - var keepURLFragments = $('#keepURLFragmentSelect', row).val(); - var ecommerce = $(row).find('#ecommerce option:selected').val(); + } + + function sendAddSiteAJAX(row) { + var siteName = $(row).find('input#name').val(); + var urls = $(row).find('textarea#urls').val(); + urls = urls.trim().split("\n"); + var excludedIps = $(row).find('textarea#excludedIps').val(); + excludedIps = piwikHelper.getApiFormatTextarea(excludedIps); + var timezone = $(row).find('#timezones option:selected').val(); + var currency = $(row).find('#currencies option:selected').val(); + var excludedQueryParameters = $(row).find('textarea#excludedQueryParameters').val(); + excludedQueryParameters = piwikHelper.getApiFormatTextarea(excludedQueryParameters); + var excludedUserAgents = $(row).find('textarea#excludedUserAgents').val(); + excludedUserAgents = piwikHelper.getApiFormatTextarea(excludedUserAgents); + var keepURLFragments = $('#keepURLFragmentSelect', row).val(); + var ecommerce = $(row).find('#ecommerce option:selected').val(); var sitesearch = $(row).find('#sitesearch option:selected').val(); var searchKeywordParameters = $('input#searchKeywordParameters').val(); var searchCategoryParameters = $('input#searchCategoryParameters').val(); - + var ajaxHandler = new ajaxHelper(); ajaxHandler.addParams({ module: 'API', @@ -72,22 +70,21 @@ function SitesManager ( _timezones, _currencies, _defaultTimezone, _defaultCurre ajaxHandler.setLoadingElement(); ajaxHandler.send(true); } - - function sendUpdateSiteAJAX( row ) - { - var siteName = $(row).find('input#siteName').val(); - var idSite = $(row).children('#idSite').html(); - var urls = $(row).find('textarea#urls').val(); - urls = urls.trim().split("\n"); - var excludedIps = $(row).find('textarea#excludedIps').val(); - excludedIps = piwikHelper.getApiFormatTextarea(excludedIps); - var excludedQueryParameters = $(row).find('textarea#excludedQueryParameters').val(); - excludedQueryParameters = piwikHelper.getApiFormatTextarea(excludedQueryParameters); - var excludedUserAgents = $(row).find('textarea#excludedUserAgents').val(); - excludedUserAgents = piwikHelper.getApiFormatTextarea(excludedUserAgents); - var keepURLFragments = $('#keepURLFragmentSelect', row).val(); - var timezone = $(row).find('#timezones option:selected').val(); - var currency = $(row).find('#currencies option:selected').val(); + + function sendUpdateSiteAJAX(row) { + var siteName = $(row).find('input#siteName').val(); + var idSite = $(row).children('#idSite').html(); + var urls = $(row).find('textarea#urls').val(); + urls = urls.trim().split("\n"); + var excludedIps = $(row).find('textarea#excludedIps').val(); + excludedIps = piwikHelper.getApiFormatTextarea(excludedIps); + var excludedQueryParameters = $(row).find('textarea#excludedQueryParameters').val(); + excludedQueryParameters = piwikHelper.getApiFormatTextarea(excludedQueryParameters); + var excludedUserAgents = $(row).find('textarea#excludedUserAgents').val(); + excludedUserAgents = piwikHelper.getApiFormatTextarea(excludedUserAgents); + var keepURLFragments = $('#keepURLFragmentSelect', row).val(); + var timezone = $(row).find('#timezones option:selected').val(); + var currency = $(row).find('#currencies option:selected').val(); var ecommerce = $(row).find('#ecommerce option:selected').val(); var sitesearch = $(row).find('#sitesearch option:selected').val(); var searchKeywordParameters = $('input#searchKeywordParameters').val(); @@ -117,19 +114,18 @@ function SitesManager ( _timezones, _currencies, _defaultTimezone, _defaultCurre ajaxHandler.redirectOnSuccess(); ajaxHandler.setLoadingElement(); ajaxHandler.send(true); - } - - function sendGlobalSettingsAJAX() - { - var timezone = $('#defaultTimezone option:selected').val(); - var currency = $('#defaultCurrency option:selected').val(); - var excludedIps = $('textarea#globalExcludedIps').val(); - excludedIps = piwikHelper.getApiFormatTextarea(excludedIps); - var excludedQueryParameters = $('textarea#globalExcludedQueryParameters').val(); - excludedQueryParameters = piwikHelper.getApiFormatTextarea(excludedQueryParameters); - var globalExcludedUserAgents = $('textarea#globalExcludedUserAgents').val(); - globalExcludedUserAgents = piwikHelper.getApiFormatTextarea(globalExcludedUserAgents); - var globalKeepURLFragments = $('#globalKeepURLFragments').is(':checked') ? 1 : 0; + } + + function sendGlobalSettingsAJAX() { + var timezone = $('#defaultTimezone option:selected').val(); + var currency = $('#defaultCurrency option:selected').val(); + var excludedIps = $('textarea#globalExcludedIps').val(); + excludedIps = piwikHelper.getApiFormatTextarea(excludedIps); + var excludedQueryParameters = $('textarea#globalExcludedQueryParameters').val(); + excludedQueryParameters = piwikHelper.getApiFormatTextarea(excludedQueryParameters); + var globalExcludedUserAgents = $('textarea#globalExcludedUserAgents').val(); + globalExcludedUserAgents = piwikHelper.getApiFormatTextarea(globalExcludedUserAgents); + var globalKeepURLFragments = $('#globalKeepURLFragments').is(':checked') ? 1 : 0; var searchKeywordParameters = $('input#globalSearchKeywordParameters').val(); var searchCategoryParameters = $('input#globalSearchCategoryParameters').val(); var enableSiteUserAgentExclude = $('input#enableSiteUserAgentExclude').is(':checked') ? 1 : 0; @@ -155,172 +151,162 @@ function SitesManager ( _timezones, _currencies, _defaultTimezone, _defaultCurre ajaxHandler.setLoadingElement('#ajaxLoadingGlobalSettings'); ajaxHandler.setErrorElement('#ajaxErrorGlobalSettings'); ajaxHandler.send(true); - } - - this.init = function () { - $('.addRowSite').click( function() { - piwikHelper.hideAjaxError(); - $('.addRowSite').toggle(); - - var numberOfRows = $('table#editSites')[0].rows.length; - var newRowId = 'rowNew' + numberOfRows; - var submitButtonHtml = ''; - $(' \ + } + + this.init = function () { + $('.addRowSite').click(function () { + piwikHelper.hideAjaxError(); + $('.addRowSite').toggle(); + + var numberOfRows = $('table#editSites')[0].rows.length; + var newRowId = 'rowNew' + numberOfRows; + var submitButtonHtml = ''; + $(' \  \ -


'+submitButtonHtml+'\ -
'+aliasUrlsHelp+keepURLFragmentSelectHTML+'\ -
'+excludedIpHelp+'\ -
'+excludedQueryParametersHelp+'\ -
'+excludedUserAgentsHelp+'\ - '+getSitesearchSelector(false)+'\ - '+getTimezoneSelector(defaultTimezone)+'
' + timezoneHelp + '\ - '+getCurrencySelector(defaultCurrency)+'
' + currencyHelp + '\ - '+getEcommerceSelector(0) + '
' + ecommerceHelp+ '\ - '+submitButtonHtml+'\ - '+sprintf(_pk_translate('General_OrCancel_js'),"","")+'\ +


' + submitButtonHtml + '\ +
' + aliasUrlsHelp + keepURLFragmentSelectHTML + '\ +
' + excludedIpHelp + '\ +
' + excludedQueryParametersHelp + '\ +
' + excludedUserAgentsHelp + '\ + ' + getSitesearchSelector(false) + '\ + ' + getTimezoneSelector(defaultTimezone) + '
' + timezoneHelp + '\ + ' + getCurrencySelector(defaultCurrency) + '
' + currencyHelp + '\ + ' + getEcommerceSelector(0) + '
' + ecommerceHelp + '\ + ' + submitButtonHtml + '\ + ' + sprintf(_pk_translate('General_OrCancel_js'), "", "") + '\ ') - .appendTo('#editSites') - ; - - piwikHelper.lazyScrollTo('#'+newRowId); - - $('.addsite').click( function(){ - sendAddSiteAJAX($('tr#'+newRowId)); - }); - - $('.cancel').click(function() { - piwikHelper.hideAjaxError(); - $(this).parents('tr').remove(); - $('.addRowSite').toggle(); - }); - return false; - } ); - - // when click on deleteuser, the we ask for confirmation and then delete the user - $('.deleteSite').click( function() { - piwikHelper.hideAjaxError(); - var idRow = $(this).attr('id'); - var nameToDelete = $(this).parent().parent().find('input#siteName').val() || $(this).parent().parent().find('td#siteName').html(); - var idsiteToDelete = $(this).parent().parent().find('#idSite').html(); - - $('#confirm h2').text(sprintf(_pk_translate('SitesManager_DeleteConfirm_js'),'"'+nameToDelete+'" (idSite = '+idsiteToDelete+')')); - piwikHelper.modalConfirm('#confirm', {yes: function(){ - sendDeleteSiteAJAX( idsiteToDelete ); - }}); - } - ); - - var alreadyEdited = new Array; - $('.editSite') - .click( function() { - piwikHelper.hideAjaxError(); - var idRow = $(this).attr('id'); - if(alreadyEdited[idRow]==1) return; - if(siteBeingEdited) - { - $('#alert h2').text(sprintf(_pk_translate('SitesManager_OnlyOneSiteAtTime_js'), '"'+$("
").html(siteBeingEditedName).text()+'"')); - piwikHelper.modalConfirm('#alert', {}); - return; - } - siteBeingEdited = true; - - alreadyEdited[idRow] = 1; - $('tr#'+idRow+' .editableSite').each( - // make the fields editable - // change the EDIT button to VALID button - function (i,n) { - var contentBefore = $(n).html(); - - var idName = $(n).attr('id'); - if(idName == 'siteName') - { - siteBeingEditedName = contentBefore; - var contentAfter = ''; - - var inputSave = $('
') - .click( function(){ submitUpdateSite($(this).parent()); }); - var spanCancel = $('

'+sprintf(_pk_translate('General_OrCancel_js'),"","")+'
') - .click( function(){ piwikHelper.refreshAfter(0); } ); - $(n) - .html(contentAfter) - .keypress( submitSiteOnEnter ) - .append(inputSave) - .append(spanCancel); - } - else if(idName == 'urls') - { - var keepURLFragmentsForSite = $(this).closest('tr').attr('data-keep-url-fragments'); - - var contentAfter = ''; - contentAfter += '
'+aliasUrlsHelp+keepURLFragmentSelectHTML; - $(n).html(contentAfter).find('select').val(keepURLFragmentsForSite); - } - else if(idName == 'excludedIps') - { - var contentAfter = ''; - contentAfter += '
'+excludedIpHelp; - $(n).html(contentAfter); - } - else if(idName == 'excludedQueryParameters') - { - var contentAfter = ''; - contentAfter += '
'+excludedQueryParametersHelp; - $(n).html(contentAfter); - } - else if (idName == 'excludedUserAgents') - { - var contentAfter = '
'+excludedUserAgentsHelp; - $(n).html(contentAfter); - } - else if(idName == 'timezone') - { - var contentAfter = getTimezoneSelector(contentBefore); - contentAfter += '
' + timezoneHelp; - $(n).html(contentAfter); - } - else if(idName == 'currency') - { - var contentAfter = getCurrencySelector(contentBefore); - contentAfter += '
' + currencyHelp; - $(n).html(contentAfter); - } - else if(idName == 'ecommerce') - { - ecommerceActive = contentBefore.indexOf("ecommerceActive") > 0 ? 1 : 0; - contentAfter = getEcommerceSelector(ecommerceActive) + '
' + ecommerceHelp; - $(n).html(contentAfter); - } - else if(idName == 'sitesearch') { - contentAfter = getSitesearchSelector(contentBefore); - $(n).html(contentAfter); - onClickSiteSearchUseDefault(); + .appendTo('#editSites') + ; + + piwikHelper.lazyScrollTo('#' + newRowId); + + $('.addsite').click(function () { + sendAddSiteAJAX($('tr#' + newRowId)); + }); + + $('.cancel').click(function () { + piwikHelper.hideAjaxError(); + $(this).parents('tr').remove(); + $('.addRowSite').toggle(); + }); + return false; + }); + + // when click on deleteuser, the we ask for confirmation and then delete the user + $('.deleteSite').click(function () { + piwikHelper.hideAjaxError(); + var idRow = $(this).attr('id'); + var nameToDelete = $(this).parent().parent().find('input#siteName').val() || $(this).parent().parent().find('td#siteName').html(); + var idsiteToDelete = $(this).parent().parent().find('#idSite').html(); + + $('#confirm h2').text(sprintf(_pk_translate('SitesManager_DeleteConfirm_js'), '"' + nameToDelete + '" (idSite = ' + idsiteToDelete + ')')); + piwikHelper.modalConfirm('#confirm', {yes: function () { + sendDeleteSiteAJAX(idsiteToDelete); + }}); + } + ); + + var alreadyEdited = new Array; + $('.editSite') + .click(function () { + piwikHelper.hideAjaxError(); + var idRow = $(this).attr('id'); + if (alreadyEdited[idRow] == 1) return; + if (siteBeingEdited) { + $('#alert h2').text(sprintf(_pk_translate('SitesManager_OnlyOneSiteAtTime_js'), '"' + $("
").html(siteBeingEditedName).text() + '"')); + piwikHelper.modalConfirm('#alert', {}); + return; + } + siteBeingEdited = true; + + alreadyEdited[idRow] = 1; + $('tr#' + idRow + ' .editableSite').each( + // make the fields editable + // change the EDIT button to VALID button + function (i, n) { + var contentBefore = $(n).html(); + + var idName = $(n).attr('id'); + if (idName == 'siteName') { + siteBeingEditedName = contentBefore; + var contentAfter = ''; + + var inputSave = $('
') + .click(function () { submitUpdateSite($(this).parent()); }); + var spanCancel = $('

' + sprintf(_pk_translate('General_OrCancel_js'), "", "") + '
') + .click(function () { piwikHelper.refreshAfter(0); }); + $(n) + .html(contentAfter) + .keypress(submitSiteOnEnter) + .append(inputSave) + .append(spanCancel); + } + else if (idName == 'urls') { + var keepURLFragmentsForSite = $(this).closest('tr').attr('data-keep-url-fragments'); + + var contentAfter = ''; + contentAfter += '
' + aliasUrlsHelp + keepURLFragmentSelectHTML; + $(n).html(contentAfter).find('select').val(keepURLFragmentsForSite); + } + else if (idName == 'excludedIps') { + var contentAfter = ''; + contentAfter += '
' + excludedIpHelp; + $(n).html(contentAfter); + } + else if (idName == 'excludedQueryParameters') { + var contentAfter = ''; + contentAfter += '
' + excludedQueryParametersHelp; + $(n).html(contentAfter); + } + else if (idName == 'excludedUserAgents') { + var contentAfter = '
' + excludedUserAgentsHelp; + $(n).html(contentAfter); + } + else if (idName == 'timezone') { + var contentAfter = getTimezoneSelector(contentBefore); + contentAfter += '
' + timezoneHelp; + $(n).html(contentAfter); + } + else if (idName == 'currency') { + var contentAfter = getCurrencySelector(contentBefore); + contentAfter += '
' + currencyHelp; + $(n).html(contentAfter); + } + else if (idName == 'ecommerce') { + ecommerceActive = contentBefore.indexOf("ecommerceActive") > 0 ? 1 : 0; + contentAfter = getEcommerceSelector(ecommerceActive) + '
' + ecommerceHelp; + $(n).html(contentAfter); + } + else if (idName == 'sitesearch') { + contentAfter = getSitesearchSelector(contentBefore); + $(n).html(contentAfter); + onClickSiteSearchUseDefault(); + } } - } - ); - $(this) - .toggle() - .parent() - .prepend( $('') - .click( function(){ sendUpdateSiteAJAX( $('tr#'+idRow) ); } ) - ); - }); - - $('#globalSettingsSubmit').click( function() { - sendGlobalSettingsAJAX(); - }); - - $('#defaultTimezone').html( getTimezoneSelector(defaultTimezone)); - $('#defaultCurrency').html( getCurrencySelector(defaultCurrency)); - - $('td.editableSite').click( function(){ $(this).parent().find('.editSite').click(); } ); - } - - function getSitesearchSelector(contentBefore) - { + ); + $(this) + .toggle() + .parent() + .prepend($('') + .click(function () { sendUpdateSiteAJAX($('tr#' + idRow)); }) + ); + }); + + $('#globalSettingsSubmit').click(function () { + sendGlobalSettingsAJAX(); + }); + + $('#defaultTimezone').html(getTimezoneSelector(defaultTimezone)); + $('#defaultCurrency').html(getCurrencySelector(defaultCurrency)); + + $('td.editableSite').click(function () { $(this).parent().find('.editSite').click(); }); + } + + function getSitesearchSelector(contentBefore) { var globalKeywordParameters = $('input#globalSearchKeywordParameters').val().trim(); var globalCategoryParameters = $('input#globalSearchCategoryParameters').val().trim(); - if(contentBefore) { + if (contentBefore) { enabled = contentBefore.indexOf("sitesearchActive") > 0 ? 1 : 0; spanSearch = $(contentBefore).filter('.sskp'); var searchKeywordParameters = spanSearch.attr('sitesearch_keyword_parameters').trim(); @@ -341,103 +327,96 @@ function SitesManager ( _timezones, _currencies, _defaultTimezone, _defaultCurre html += ''; html += '
'; - if(searchGlobalHasValues) - { + if (searchGlobalHasValues) { checkedStr = checked ? ' checked ' : ''; - html += '
'; return html; } - function getEcommerceSelector(enabled) - { - var html = ''; - return html; - } - - function getTimezoneSelector(selectedTimezone) - { - var html = ''; - return html; - } - - - function getCurrencySelector(selectedCurrency) - { - var html = ''; - return html; - } - - function submitSiteOnEnter(e) - { - var key=e.keyCode || e.which; - if (key==13) - { - submitUpdateSite(this); - $(this).find('.addsite').click(); - } - } - function submitUpdateSite(self) - { - $(self).parent().find('.updateSite').click(); - } + function getEcommerceSelector(enabled) { + var html = ''; + return html; + } + + function getTimezoneSelector(selectedTimezone) { + var html = ''; + return html; + } + + + function getCurrencySelector(selectedCurrency) { + var html = ''; + return html; + } + + function submitSiteOnEnter(e) { + var key = e.keyCode || e.which; + if (key == 13) { + submitUpdateSite(this); + $(this).find('.addsite').click(); + } + } + + function submitUpdateSite(self) { + $(self).parent().find('.updateSite').click(); + } } -function onClickSiteSearchUseDefault() -{ +function onClickSiteSearchUseDefault() { // Site Search enabled - if($('select#sitesearch').val() == "1") { + if ($('select#sitesearch').val() == "1") { $('#sitesearchUseDefault').show(); // Use default is checked - if($('#sitesearchUseDefaultCheck').is(':checked')) { + if ($('#sitesearchUseDefaultCheck').is(':checked')) { $('#searchSiteParameters').hide(); $('#sitesearchIntro').show(); $('#searchKeywordParameters,#searchCategoryParameters').val(''); $('.searchDisplayParams').show(); - // Use default is unchecked + // Use default is unchecked } else { $('#sitesearchIntro').hide(); diff --git a/plugins/SitesManager/templates/SitesManager.tpl b/plugins/SitesManager/templates/SitesManager.tpl index 775e82f816..f720fc2f21 100644 --- a/plugins/SitesManager/templates/SitesManager.tpl +++ b/plugins/SitesManager/templates/SitesManager.tpl @@ -2,322 +2,410 @@ {loadJavascriptTranslations plugins='SitesManager'} - + {/literal}

{'SitesManager_WebsitesManagement'|translate}

{'SitesManager_MainDescription'|translate} -{'SitesManager_YouCurrentlyHaveAccessToNWebsites'|translate:"$adminSitesCount"} -{if $isSuperUser} -
{'SitesManager_SuperUserCan'|translate:"":""} -{/if} + {'SitesManager_YouCurrentlyHaveAccessToNWebsites'|translate:"$adminSitesCount"} + {if $isSuperUser} +
+ {'SitesManager_SuperUserCan'|translate:"":""} + {/if}

{ajaxErrorDiv} {ajaxLoadingDiv} {capture assign=createNewWebsite} -
{'SitesManager_AddSite'|translate}
+
{'SitesManager_AddSite'|translate}
{/capture} {if $adminSites|@count == 0} - {'SitesManager_NoWebsites'|translate} + {'SitesManager_NoWebsites'|translate} {else}

- - + + +
+
+ {if $isSuperUser} + {$createNewWebsite} + {/if} + + + + + + + + + + + + + + + + + + + + {foreach from=$adminSites key=i item=site} + + + + + + + + + + + + + + + + {/foreach} + +
{'General_Id'|translate}{'General_Name'|translate}{'SitesManager_Urls'|translate}{'SitesManager_ExcludedIps'|translate}{'SitesManager_ExcludedParameters'|translate|replace:" ":"
"}
{'SitesManager_ExcludedUserAgents'|translate}{'Actions_SubmenuSitesearch'|translate}{'SitesManager_Timezone'|translate}{'SitesManager_Currency'|translate}{'Goals_Ecommerce'|translate} {'SitesManager_JsTrackingTag'|translate}
{$site.idsite}{$site.name}{foreach from=$site.alias_urls item=url}{$url|replace:"http://":""}
{/foreach}
{foreach from=$site.excluded_ips item=ip}{$ip}
{/foreach}
{foreach from=$site.excluded_parameters item=parameter}{$parameter}
{/foreach} +
{foreach from=$site.excluded_user_agents item=ua}{$ua}
{/foreach} +
{if $site.sitesearch}{'General_Yes'|translate}{else}-{/if}{$site.timezone}{$site.currency}{if $site.ecommerce}{'General_Yes'|translate}{else} + - + {/if} {'General_Edit'|translate} {'General_Delete'|translate} + {'SitesManager_ShowTrackingTag'|translate} +
+ {if $isSuperUser} + {$createNewWebsite} + {/if}
- -
- {if $isSuperUser} - {$createNewWebsite} - {/if} - - - - - - - - - - - - - - - - - - - - {foreach from=$adminSites key=i item=site} - - - - - - - - - - - - - - - - {/foreach} - -
{'General_Id'|translate}{'General_Name'|translate}{'SitesManager_Urls'|translate}{'SitesManager_ExcludedIps'|translate}{'SitesManager_ExcludedParameters'|translate|replace:" ":"
"}
{'SitesManager_ExcludedUserAgents'|translate}{'Actions_SubmenuSitesearch'|translate}{'SitesManager_Timezone'|translate}{'SitesManager_Currency'|translate}{'Goals_Ecommerce'|translate} {'SitesManager_JsTrackingTag'|translate}
{$site.idsite}{$site.name}{foreach from=$site.alias_urls item=url}{$url|replace:"http://":""}
{/foreach}
{foreach from=$site.excluded_ips item=ip}{$ip}
{/foreach}
{foreach from=$site.excluded_parameters item=parameter}{$parameter}
{/foreach}
{foreach from=$site.excluded_user_agents item=ua}{$ua}
{/foreach}
{if $site.sitesearch}{'General_Yes'|translate}{else}-{/if}{$site.timezone}{$site.currency}{if $site.ecommerce}{'General_Yes'|translate}{else}-{/if} {'General_Edit'|translate} {'General_Delete'|translate}{'SitesManager_ShowTrackingTag'|translate}
- {if $isSuperUser} - {$createNewWebsite} - {/if} -
{/if} {* Admin users use these values for Site Search column, when editing websites *} {if !$isSuperUser} - - + + {/if} {if $isSuperUser} -
- -

{'SitesManager_GlobalWebsitesSettings'|translate}

-
- - - - - - - - - - {* global excluded user agents *} - - - - - - - {* global keep URL fragments *} - - - {* global site search *} - - - - - - - - - - - -
- {'SitesManager_GlobalListExcludedIps'|translate} -

{'SitesManager_ListOfIpsToBeExcludedOnAllWebsites'|translate}

-
- - - -
- {'SitesManager_GlobalListExcludedQueryParameters'|translate} -

{'SitesManager_ListOfQueryParametersToBeExcludedOnAllWebsites'|translate}

-
- - -
- {'SitesManager_GlobalListExcludedUserAgents'|translate} -

{'SitesManager_GlobalListExcludedUserAgents_Desc'|translate}

-
- - -
- - - {'SitesManager_EnableSiteSpecificUserAgentExclude_Help'|translate:'':''|inlineHelp} -
- {'SitesManager_KeepURLFragments'|translate} -

{'SitesManager_KeepURLFragmentsHelp'|translate:"#":"example.org/index.html#first_section":"example.org/index.html"} -

- - -

{'SitesManager_KeepURLFragmentsHelp2'|translate}

-
- {'SitesManager_TrackingSiteSearch'|translate} -

{$sitesearchIntro}

- {'SitesManager_SearchParametersNote'|translate} {'SitesManager_SearchParametersNote2'|translate} -
- -
- {if !$isSearchCategoryTrackingEnabled} - - Note: you could also track your Internal Search Engine Categories, but the plugin Custom Variables is required. Please enable the plugin CustomVariables (or ask your Piwik admin). - {else} - {'Goals_Optional'|translate} {'SitesManager_SearchCategoryDesc'|translate}
-
- - {/if} -
- {'SitesManager_DefaultTimezoneForNewWebsites'|translate} -

{'SitesManager_SelectDefaultTimezone'|translate}

-
-
-
- {$defaultTimezoneHelp} -
- {'SitesManager_DefaultCurrencyForNewWebsites'|translate} -

{'SitesManager_SelectDefaultCurrency'|translate}

-
-
-
- {$currencyHelpPlain} -
- - +
+ +

{'SitesManager_GlobalWebsitesSettings'|translate}

+
+ + + + + + + + + + + + + + + + + + + + {* global excluded user agents *} + + + + + + + + + + + + + + + {* global keep URL fragments *} + + + + + {* global site search *} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {'SitesManager_GlobalListExcludedIps'|translate} + +

{'SitesManager_ListOfIpsToBeExcludedOnAllWebsites'|translate}

+
+ + + +
+ {'SitesManager_GlobalListExcludedQueryParameters'|translate} + +

{'SitesManager_ListOfQueryParametersToBeExcludedOnAllWebsites'|translate}

+
+ + +
+ {'SitesManager_GlobalListExcludedUserAgents'|translate} + +

{'SitesManager_GlobalListExcludedUserAgents_Desc'|translate}

+
+ + +
+ + + {'SitesManager_EnableSiteSpecificUserAgentExclude_Help'|translate:'':''|inlineHelp} +
+ {'SitesManager_KeepURLFragments'|translate} + +

{'SitesManager_KeepURLFragmentsHelp'|translate:"#":"example.org/index.html#first_section":"example.org/index.html"} +

+ + + +

{'SitesManager_KeepURLFragmentsHelp2'|translate}

+
+ {'SitesManager_TrackingSiteSearch'|translate} + +

{$sitesearchIntro}

+ {'SitesManager_SearchParametersNote'|translate} {'SitesManager_SearchParametersNote2'|translate} +
+ +
+ {if !$isSearchCategoryTrackingEnabled} + + Note: you could also track your Internal Search Engine Categories, but the plugin Custom Variables is required. Please enable the plugin CustomVariables (or ask your Piwik admin). + {else} + {'Goals_Optional'|translate} {'SitesManager_SearchCategoryDesc'|translate}
+
+ + {/if} +
+ {'SitesManager_DefaultTimezoneForNewWebsites'|translate} + +

{'SitesManager_SelectDefaultTimezone'|translate}

+
+
+
+ {$defaultTimezoneHelp} +
+ {'SitesManager_DefaultCurrencyForNewWebsites'|translate} + +

{'SitesManager_SelectDefaultCurrency'|translate}

+
+
+
+ {$currencyHelpPlain} +
+ + - {ajaxErrorDiv id=ajaxErrorGlobalSettings} - {ajaxLoadingDiv id=ajaxLoadingGlobalSettings} + {ajaxErrorDiv id=ajaxErrorGlobalSettings} + {ajaxLoadingDiv id=ajaxLoadingGlobalSettings} {/if} {if $showAddSite} - + {/if} -



+



{include file="CoreAdminHome/templates/footer.tpl"} diff --git a/plugins/Transitions/API.php b/plugins/Transitions/API.php index ca178a02a2..67e4735dbd 100644 --- a/plugins/Transitions/API.php +++ b/plugins/Transitions/API.php @@ -1,10 +1,10 @@ getTransitionsForAction($pageTitle, 'title', $idSite, $period, $date, $segment, $limitBeforeGrouping); - } - - public function getTransitionsForPageUrl($pageUrl, $idSite, $period, $date, $segment = false, $limitBeforeGrouping = false) - { - return $this->getTransitionsForAction($pageUrl, 'url', $idSite, $period, $date, $segment, $limitBeforeGrouping); - } - - /** - * General method to get transitions for an action - * - * @param $actionName - * @param $actionType "url"|"title" - * @param $idSite - * @param $period - * @param $date - * @param bool $segment - * @param bool $limitBeforeGrouping - * @param string $parts - * @param bool $returnNormalizedUrls - * @return array - * @throws Exception - */ - public function getTransitionsForAction($actionName, $actionType, $idSite, $period, $date, - $segment = false, $limitBeforeGrouping = false, $parts = 'all', $returnNormalizedUrls = false) - { - Piwik::checkUserHasViewAccess($idSite); - - // get idaction of the requested action - $idaction = $this->deriveIdAction($actionName, $actionType); - if ($idaction < 0) - { - throw new Exception('NoDataForAction'); - } - - // prepare archive processing that can be used by the archiving code - $archiveProcessing = new Piwik_ArchiveProcessing_Day(); - $archiveProcessing->setSite(new Piwik_Site($idSite)); - $archiveProcessing->setPeriod(Piwik_Period::advancedFactory($period, $date)); - $archiveProcessing->setSegment(new Piwik_Segment($segment, $idSite)); - $archiveProcessing->initForLiveUsage(); - - // prepare the report - $report = array( - 'date' => Piwik_Period_Day::advancedFactory($period, $date)->getLocalizedShortString() - ); - - // add data to the report - $transitionsArchiving = new Piwik_Transitions; - if ($returnNormalizedUrls) - { - $transitionsArchiving->returnNormalizedUrls(); - } - - $partsArray = explode(',', $parts); - - if ($parts == 'all' || in_array('internalReferrers', $partsArray)) - { - $this->addInternalReferrers($transitionsArchiving, $archiveProcessing, $report, $idaction, - $actionType, $limitBeforeGrouping); - } - if ($parts == 'all' || in_array('followingActions', $partsArray)) - { - $includeLoops = $parts != 'all' && !in_array('internalReferrers', $partsArray); - $this->addFollowingActions($transitionsArchiving, $archiveProcessing, $report, $idaction, - $actionType, $limitBeforeGrouping, $includeLoops); - } - if ($parts == 'all' || in_array('externalReferrers', $partsArray)) - { - $this->addExternalReferrers($transitionsArchiving, $archiveProcessing, $report, $idaction, - $actionType, $limitBeforeGrouping); - } - - // derive the number of exits from the other metrics - if ($parts == 'all') { - $report['pageMetrics']['exits'] = $report['pageMetrics']['pageviews'] - - $transitionsArchiving->getTotalTransitionsToFollowingActions() - - $report['pageMetrics']['loops']; - } - - // replace column names in the data tables - $reportNames = array( - 'previousPages' => true, - 'previousSiteSearches' => false, - 'followingPages' => true, - 'followingSiteSearches' => false, - 'outlinks' => true, - 'downloads' => true - ); - foreach ($reportNames as $reportName => $replaceLabel) - { - if (isset($report[$reportName])) - { - $columnNames = array(Piwik_Archive::INDEX_NB_ACTIONS => 'referrals'); - if ($replaceLabel) - { - $columnNames[Piwik_Archive::INDEX_NB_ACTIONS] = 'referrals'; - } - $report[$reportName]->filter('ReplaceColumnNames', array($columnNames)); - } - } - - return $report; - } - - /** - * Derive the action ID from the request action name and type. - */ - private function deriveIdAction($actionName, $actionType) - { - $actionsPlugin = new Piwik_Actions; - switch ($actionType) - { - case 'url': - $originalActionName = $actionName; - $actionName = Piwik_Common::unsanitizeInputValue($actionName); - $id = $actionsPlugin->getIdActionFromSegment($actionName, 'idaction_url'); - - if ($id < 0) - { - // an example where this is needed is urls containing < or > - $actionName = $originalActionName; - $id = $actionsPlugin->getIdActionFromSegment($actionName, 'idaction_url'); - } - - return $id; - - case 'title': - $id = $actionsPlugin->getIdActionFromSegment($actionName, 'idaction_name'); - - if ($id < 0) - { - $unkown = Piwik_Actions_ArchivingHelper::getUnknownActionName( - Piwik_Tracker_Action::TYPE_ACTION_NAME); - - if (trim($actionName) == trim($unkown)) - { - $id = $actionsPlugin->getIdActionFromSegment('', 'idaction_name'); - } - } - - return $id; - - default: - throw new Exception('Unknown action type'); - } - } - - /** - * Add the internal referrers to the report: - * previous pages and previous site searches - * - * @param Piwik_Transitions $transitionsArchiving - * @param $archiveProcessing - * @param $report - * @param $idaction - * @param string $actionType - * @param $limitBeforeGrouping - * @throws Exception - */ - private function addInternalReferrers($transitionsArchiving, $archiveProcessing, &$report, - $idaction, $actionType, $limitBeforeGrouping) { - - $data = $transitionsArchiving->queryInternalReferrers( - $idaction, $actionType, $archiveProcessing, $limitBeforeGrouping); - - if ($data['pageviews'] == 0) - { - throw new Exception('NoDataForAction'); - } - - $report['previousPages'] = &$data['previousPages']; - $report['previousSiteSearches'] = &$data['previousSiteSearches']; - $report['pageMetrics']['loops'] = $data['loops']; - $report['pageMetrics']['pageviews'] = $data['pageviews']; - } - - /** - * Add the following actions to the report: - * following pages, downloads, outlinks - * - * @param Piwik_Transitions $transitionsArchiving - * @param $archiveProcessing - * @param $report - * @param $idaction - * @param string $actionType - * @param $limitBeforeGrouping - * @param boolean $includeLoops - */ - private function addFollowingActions($transitionsArchiving, $archiveProcessing, &$report, - $idaction, $actionType, $limitBeforeGrouping, $includeLoops=false) { - - $data = $transitionsArchiving->queryFollowingActions( - $idaction, $actionType, $archiveProcessing, $limitBeforeGrouping, $includeLoops); - - foreach ($data as $tableName => $table) - { - $report[$tableName] = $table; - } - } - - /** - * Add the external referrers to the report: - * direct entries, websites, campaigns, search engines - * - * @param Piwik_Transitions $transitionsArchiving - * @param $archiveProcessing - * @param $report - * @param $idaction - * @param string $actionType - * @param $limitBeforeGrouping - */ - private function addExternalReferrers($transitionsArchiving, $archiveProcessing, &$report, - $idaction, $actionType, $limitBeforeGrouping) { - - $data = $transitionsArchiving->queryExternalReferrers( - $idaction, $actionType, $archiveProcessing, $limitBeforeGrouping); - - $report['pageMetrics']['entries'] = 0; - $report['referrers'] = array(); - foreach ($data->getRows() as $row) - { - $referrerId = $row->getColumn('label'); - $visits = $row->getColumn(Piwik_Archive::INDEX_NB_VISITS); - if ($visits) - { - // load details (i.e. subtables) - $details = array(); - if ($idSubTable = $row->getIdSubDataTable()) - { - $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); - foreach ($subTable->getRows() as $subRow) - { - $details[] = array( - 'label' => $subRow->getColumn('label'), - 'referrals' => $subRow->getColumn(Piwik_Archive::INDEX_NB_VISITS) - ); - } - } - $report['referrers'][] = array( - 'label' => $this->getReferrerLabel($referrerId), - 'shortName' => Piwik_getRefererTypeFromShortName($referrerId), - 'visits' => $visits, - 'details' => $details - ); - $report['pageMetrics']['entries'] += $visits; - } - } - - // if there's no data for referrers, Piwik_API_ResponseBuilder::handleMultiDimensionalArray - // does not detect the multi dimensional array and the data is rendered differently, which - // causes an exception. - if (count($report['referrers']) == 0) - { - $report['referrers'][] = array( - 'label' => $this->getReferrerLabel(Piwik_Common::REFERER_TYPE_DIRECT_ENTRY), - 'shortName' => Piwik_getRefererTypeLabel(Piwik_Common::REFERER_TYPE_DIRECT_ENTRY), - 'visits' => 0 - ); - } - } - - private function getReferrerLabel($referrerId) { - switch ($referrerId) - { - case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY: - return Piwik_Transitions_Controller::getTranslation('directEntries'); - case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: - return Piwik_Transitions_Controller::getTranslation('fromSearchEngines'); - case Piwik_Common::REFERER_TYPE_WEBSITE: - return Piwik_Transitions_Controller::getTranslation('fromWebsites'); - case Piwik_Common::REFERER_TYPE_CAMPAIGN: - return Piwik_Transitions_Controller::getTranslation('fromCampaigns'); - default: - return Piwik_Translate('General_Others'); - } - } - - public function getTranslations() { - $controller = new Piwik_Transitions_Controller(); - return $controller->getTranslations(); - } - + + static private $instance = null; + + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + public function getTransitionsForPageTitle($pageTitle, $idSite, $period, $date, $segment = false, $limitBeforeGrouping = false) + { + return $this->getTransitionsForAction($pageTitle, 'title', $idSite, $period, $date, $segment, $limitBeforeGrouping); + } + + public function getTransitionsForPageUrl($pageUrl, $idSite, $period, $date, $segment = false, $limitBeforeGrouping = false) + { + return $this->getTransitionsForAction($pageUrl, 'url', $idSite, $period, $date, $segment, $limitBeforeGrouping); + } + + /** + * General method to get transitions for an action + * + * @param $actionName + * @param $actionType "url"|"title" + * @param $idSite + * @param $period + * @param $date + * @param bool $segment + * @param bool $limitBeforeGrouping + * @param string $parts + * @param bool $returnNormalizedUrls + * @return array + * @throws Exception + */ + public function getTransitionsForAction($actionName, $actionType, $idSite, $period, $date, + $segment = false, $limitBeforeGrouping = false, $parts = 'all', $returnNormalizedUrls = false) + { + Piwik::checkUserHasViewAccess($idSite); + + // get idaction of the requested action + $idaction = $this->deriveIdAction($actionName, $actionType); + if ($idaction < 0) { + throw new Exception('NoDataForAction'); + } + + // prepare archive processing that can be used by the archiving code + $archiveProcessing = new Piwik_ArchiveProcessing_Day(); + $archiveProcessing->setSite(new Piwik_Site($idSite)); + $archiveProcessing->setPeriod(Piwik_Period::advancedFactory($period, $date)); + $archiveProcessing->setSegment(new Piwik_Segment($segment, $idSite)); + $archiveProcessing->initForLiveUsage(); + + // prepare the report + $report = array( + 'date' => Piwik_Period_Day::advancedFactory($period, $date)->getLocalizedShortString() + ); + + // add data to the report + $transitionsArchiving = new Piwik_Transitions; + if ($returnNormalizedUrls) { + $transitionsArchiving->returnNormalizedUrls(); + } + + $partsArray = explode(',', $parts); + + if ($parts == 'all' || in_array('internalReferrers', $partsArray)) { + $this->addInternalReferrers($transitionsArchiving, $archiveProcessing, $report, $idaction, + $actionType, $limitBeforeGrouping); + } + if ($parts == 'all' || in_array('followingActions', $partsArray)) { + $includeLoops = $parts != 'all' && !in_array('internalReferrers', $partsArray); + $this->addFollowingActions($transitionsArchiving, $archiveProcessing, $report, $idaction, + $actionType, $limitBeforeGrouping, $includeLoops); + } + if ($parts == 'all' || in_array('externalReferrers', $partsArray)) { + $this->addExternalReferrers($transitionsArchiving, $archiveProcessing, $report, $idaction, + $actionType, $limitBeforeGrouping); + } + + // derive the number of exits from the other metrics + if ($parts == 'all') { + $report['pageMetrics']['exits'] = $report['pageMetrics']['pageviews'] + - $transitionsArchiving->getTotalTransitionsToFollowingActions() + - $report['pageMetrics']['loops']; + } + + // replace column names in the data tables + $reportNames = array( + 'previousPages' => true, + 'previousSiteSearches' => false, + 'followingPages' => true, + 'followingSiteSearches' => false, + 'outlinks' => true, + 'downloads' => true + ); + foreach ($reportNames as $reportName => $replaceLabel) { + if (isset($report[$reportName])) { + $columnNames = array(Piwik_Archive::INDEX_NB_ACTIONS => 'referrals'); + if ($replaceLabel) { + $columnNames[Piwik_Archive::INDEX_NB_ACTIONS] = 'referrals'; + } + $report[$reportName]->filter('ReplaceColumnNames', array($columnNames)); + } + } + + return $report; + } + + /** + * Derive the action ID from the request action name and type. + */ + private function deriveIdAction($actionName, $actionType) + { + $actionsPlugin = new Piwik_Actions; + switch ($actionType) { + case 'url': + $originalActionName = $actionName; + $actionName = Piwik_Common::unsanitizeInputValue($actionName); + $id = $actionsPlugin->getIdActionFromSegment($actionName, 'idaction_url'); + + if ($id < 0) { + // an example where this is needed is urls containing < or > + $actionName = $originalActionName; + $id = $actionsPlugin->getIdActionFromSegment($actionName, 'idaction_url'); + } + + return $id; + + case 'title': + $id = $actionsPlugin->getIdActionFromSegment($actionName, 'idaction_name'); + + if ($id < 0) { + $unkown = Piwik_Actions_ArchivingHelper::getUnknownActionName( + Piwik_Tracker_Action::TYPE_ACTION_NAME); + + if (trim($actionName) == trim($unkown)) { + $id = $actionsPlugin->getIdActionFromSegment('', 'idaction_name'); + } + } + + return $id; + + default: + throw new Exception('Unknown action type'); + } + } + + /** + * Add the internal referrers to the report: + * previous pages and previous site searches + * + * @param Piwik_Transitions $transitionsArchiving + * @param $archiveProcessing + * @param $report + * @param $idaction + * @param string $actionType + * @param $limitBeforeGrouping + * @throws Exception + */ + private function addInternalReferrers($transitionsArchiving, $archiveProcessing, &$report, + $idaction, $actionType, $limitBeforeGrouping) + { + + $data = $transitionsArchiving->queryInternalReferrers( + $idaction, $actionType, $archiveProcessing, $limitBeforeGrouping); + + if ($data['pageviews'] == 0) { + throw new Exception('NoDataForAction'); + } + + $report['previousPages'] = & $data['previousPages']; + $report['previousSiteSearches'] = & $data['previousSiteSearches']; + $report['pageMetrics']['loops'] = $data['loops']; + $report['pageMetrics']['pageviews'] = $data['pageviews']; + } + + /** + * Add the following actions to the report: + * following pages, downloads, outlinks + * + * @param Piwik_Transitions $transitionsArchiving + * @param $archiveProcessing + * @param $report + * @param $idaction + * @param string $actionType + * @param $limitBeforeGrouping + * @param boolean $includeLoops + */ + private function addFollowingActions($transitionsArchiving, $archiveProcessing, &$report, + $idaction, $actionType, $limitBeforeGrouping, $includeLoops = false) + { + + $data = $transitionsArchiving->queryFollowingActions( + $idaction, $actionType, $archiveProcessing, $limitBeforeGrouping, $includeLoops); + + foreach ($data as $tableName => $table) { + $report[$tableName] = $table; + } + } + + /** + * Add the external referrers to the report: + * direct entries, websites, campaigns, search engines + * + * @param Piwik_Transitions $transitionsArchiving + * @param $archiveProcessing + * @param $report + * @param $idaction + * @param string $actionType + * @param $limitBeforeGrouping + */ + private function addExternalReferrers($transitionsArchiving, $archiveProcessing, &$report, + $idaction, $actionType, $limitBeforeGrouping) + { + + $data = $transitionsArchiving->queryExternalReferrers( + $idaction, $actionType, $archiveProcessing, $limitBeforeGrouping); + + $report['pageMetrics']['entries'] = 0; + $report['referrers'] = array(); + foreach ($data->getRows() as $row) { + $referrerId = $row->getColumn('label'); + $visits = $row->getColumn(Piwik_Archive::INDEX_NB_VISITS); + if ($visits) { + // load details (i.e. subtables) + $details = array(); + if ($idSubTable = $row->getIdSubDataTable()) { + $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); + foreach ($subTable->getRows() as $subRow) { + $details[] = array( + 'label' => $subRow->getColumn('label'), + 'referrals' => $subRow->getColumn(Piwik_Archive::INDEX_NB_VISITS) + ); + } + } + $report['referrers'][] = array( + 'label' => $this->getReferrerLabel($referrerId), + 'shortName' => Piwik_getRefererTypeFromShortName($referrerId), + 'visits' => $visits, + 'details' => $details + ); + $report['pageMetrics']['entries'] += $visits; + } + } + + // if there's no data for referrers, Piwik_API_ResponseBuilder::handleMultiDimensionalArray + // does not detect the multi dimensional array and the data is rendered differently, which + // causes an exception. + if (count($report['referrers']) == 0) { + $report['referrers'][] = array( + 'label' => $this->getReferrerLabel(Piwik_Common::REFERER_TYPE_DIRECT_ENTRY), + 'shortName' => Piwik_getRefererTypeLabel(Piwik_Common::REFERER_TYPE_DIRECT_ENTRY), + 'visits' => 0 + ); + } + } + + private function getReferrerLabel($referrerId) + { + switch ($referrerId) { + case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY: + return Piwik_Transitions_Controller::getTranslation('directEntries'); + case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: + return Piwik_Transitions_Controller::getTranslation('fromSearchEngines'); + case Piwik_Common::REFERER_TYPE_WEBSITE: + return Piwik_Transitions_Controller::getTranslation('fromWebsites'); + case Piwik_Common::REFERER_TYPE_CAMPAIGN: + return Piwik_Transitions_Controller::getTranslation('fromCampaigns'); + default: + return Piwik_Translate('General_Others'); + } + } + + public function getTranslations() + { + $controller = new Piwik_Transitions_Controller(); + return $controller->getTranslations(); + } + } \ No newline at end of file diff --git a/plugins/Transitions/Controller.php b/plugins/Transitions/Controller.php index 86e093c523..5ef2dc539e 100644 --- a/plugins/Transitions/Controller.php +++ b/plugins/Transitions/Controller.php @@ -1,10 +1,10 @@ 'Transitions_PageviewsInline', - 'loopsInline' => 'Transitions_LoopsInline', - 'fromPreviousPages' => 'Transitions_FromPreviousPages', - 'fromPreviousPagesInline' => 'Transitions_FromPreviousPagesInline', - 'fromPreviousSiteSearches' => 'Transitions_FromPreviousSiteSearches', - 'fromPreviousSiteSearchesInline' => 'Transitions_FromPreviousSiteSearchesInline', - 'fromSearchEngines' => 'Transitions_FromSearchEngines', - 'fromSearchEnginesInline' => 'Transitions_FromSearchEnginesInline', - 'fromWebsites' => 'Transitions_FromWebsites', - 'fromWebsitesInline' => 'Transitions_FromWebsitesInline', - 'fromCampaigns' => 'Transitions_FromCampaigns', - 'fromCampaignsInline' => 'Transitions_FromCampaignsInline', - 'directEntries' => 'Transitions_DirectEntries', - 'directEntriesInline' => 'Referers_TypeDirectEntries', - 'toFollowingPages' => 'Transitions_ToFollowingPages', - 'toFollowingPagesInline' => 'Transitions_ToFollowingPagesInline', - 'toFollowingSiteSearches' => 'Transitions_ToFollowingSiteSearches', - 'toFollowingSiteSearchesInline' => 'Transitions_ToFollowingSiteSearchesInline', - 'downloads' => 'Actions_ColumnDownloads', - 'downloadsInline' => 'VisitsSummary_NbDownloadsDescription', - 'outlinks' => 'Actions_ColumnOutlinks', - 'outlinksInline' => 'VisitsSummary_NbOutlinksDescription', - 'exits' => 'General_ColumnExits', - 'exitsInline' => 'Transitions_ExitsInline', - 'bouncesInline' => 'Transitions_BouncesInline' - ); - /** - * Translations that are added to JS - * (object Piwik_Transitions_Translations) - */ - private static $jsTranslations = array( - 'XOfY' => 'Transitions_XOutOfYVisits', - 'XOfAllPageviews' => 'Transitions_XOfAllPageviews', - 'NoDataForAction' => 'Transitions_NoDataForAction', - 'NoDataForActionDetails' => 'Transitions_NoDataForActionDetails', - 'NoDataForActionBack' => 'Transitions_ErrorBack', - 'ShareOfAllPageviews' => 'Transitions_ShareOfAllPageviews', - 'DateRange' => 'General_DateRange' - ); - - public static function getTranslation($key) - { - return Piwik_Translate(self::$metricTranslations[$key]); - } - - /** - * The main method of the plugin. - * It is triggered from the Transitions data table action. - */ - public function renderPopover() - { - $view = Piwik_View::factory('transitions'); - $view->translations = $this->getTranslations(); - echo $view->render(); - } - - public function getTranslations() - { - $translations = self::$metricTranslations + self::$jsTranslations; - foreach ($translations as &$message) { - $message = Piwik_Translate($message); - } - return $translations; - } - + /** + * Since the metric translations are taken from different plugins, + * it makes the rest of the code easier to read and maintain when we + * use this indirection to map between the metrics and the actual + * translation keys. + */ + private static $metricTranslations = array( + 'pageviewsInline' => 'Transitions_PageviewsInline', + 'loopsInline' => 'Transitions_LoopsInline', + 'fromPreviousPages' => 'Transitions_FromPreviousPages', + 'fromPreviousPagesInline' => 'Transitions_FromPreviousPagesInline', + 'fromPreviousSiteSearches' => 'Transitions_FromPreviousSiteSearches', + 'fromPreviousSiteSearchesInline' => 'Transitions_FromPreviousSiteSearchesInline', + 'fromSearchEngines' => 'Transitions_FromSearchEngines', + 'fromSearchEnginesInline' => 'Transitions_FromSearchEnginesInline', + 'fromWebsites' => 'Transitions_FromWebsites', + 'fromWebsitesInline' => 'Transitions_FromWebsitesInline', + 'fromCampaigns' => 'Transitions_FromCampaigns', + 'fromCampaignsInline' => 'Transitions_FromCampaignsInline', + 'directEntries' => 'Transitions_DirectEntries', + 'directEntriesInline' => 'Referers_TypeDirectEntries', + 'toFollowingPages' => 'Transitions_ToFollowingPages', + 'toFollowingPagesInline' => 'Transitions_ToFollowingPagesInline', + 'toFollowingSiteSearches' => 'Transitions_ToFollowingSiteSearches', + 'toFollowingSiteSearchesInline' => 'Transitions_ToFollowingSiteSearchesInline', + 'downloads' => 'Actions_ColumnDownloads', + 'downloadsInline' => 'VisitsSummary_NbDownloadsDescription', + 'outlinks' => 'Actions_ColumnOutlinks', + 'outlinksInline' => 'VisitsSummary_NbOutlinksDescription', + 'exits' => 'General_ColumnExits', + 'exitsInline' => 'Transitions_ExitsInline', + 'bouncesInline' => 'Transitions_BouncesInline' + ); + + /** + * Translations that are added to JS + * (object Piwik_Transitions_Translations) + */ + private static $jsTranslations = array( + 'XOfY' => 'Transitions_XOutOfYVisits', + 'XOfAllPageviews' => 'Transitions_XOfAllPageviews', + 'NoDataForAction' => 'Transitions_NoDataForAction', + 'NoDataForActionDetails' => 'Transitions_NoDataForActionDetails', + 'NoDataForActionBack' => 'Transitions_ErrorBack', + 'ShareOfAllPageviews' => 'Transitions_ShareOfAllPageviews', + 'DateRange' => 'General_DateRange' + ); + + public static function getTranslation($key) + { + return Piwik_Translate(self::$metricTranslations[$key]); + } + + /** + * The main method of the plugin. + * It is triggered from the Transitions data table action. + */ + public function renderPopover() + { + $view = Piwik_View::factory('transitions'); + $view->translations = $this->getTranslations(); + echo $view->render(); + } + + public function getTranslations() + { + $translations = self::$metricTranslations + self::$jsTranslations; + foreach ($translations as &$message) { + $message = Piwik_Translate($message); + } + return $translations; + } + } diff --git a/plugins/Transitions/Transitions.php b/plugins/Transitions/Transitions.php index 18b45bf61b..4adaf3f997 100644 --- a/plugins/Transitions/Transitions.php +++ b/plugins/Transitions/Transitions.php @@ -8,321 +8,299 @@ * @category Piwik_Plugins * @package Piwik_Transitions */ - + /** * @package Piwik_Transitions */ class Piwik_Transitions extends Piwik_Plugin { - - private $limitBeforeGrouping = 5; - private $totalTransitionsToFollowingActions = 0; - - private $returnNormalizedUrls = false; - - public function getInformation() - { - return array( - 'description' => Piwik_Translate('Transitions_PluginDescription'), - 'author' => 'Piwik', - 'author_homepage' => 'http://piwik.org/', - 'version' => Piwik_Version::VERSION, - ); - } - - function getListHooksRegistered() - { - return array( - 'AssetManager.getCssFiles' => 'getCssFiles', - 'AssetManager.getJsFiles' => 'getJsFiles' - ); - } - - public function getCssFiles($notification) - { - $cssFiles = &$notification->getNotificationObject(); - $cssFiles[] = 'plugins/Transitions/templates/transitions.css'; - } - - public function getJsFiles($notification) - { - $jsFiles = &$notification->getNotificationObject(); - $jsFiles[] = 'plugins/Transitions/templates/transitions.js'; - } - - /** - * After calling this method, the query*()-Methods will return urls in their - * normalized form (without the prefix reconstructed) - */ - public function returnNormalizedUrls() - { - $this->returnNormalizedUrls = true; - } - - /** - * Get information about external referrers (i.e. search engines, websites & campaigns) - * - * @param $idaction - * @param $actionType - * @param Piwik_ArchiveProcessing_Day $archiveProcessing - * @param $limitBeforeGrouping - * @return Piwik_DataTable - */ - public function queryExternalReferrers($idaction, $actionType, $archiveProcessing, - $limitBeforeGrouping = false) - { - $rankingQuery = new Piwik_RankingQuery($limitBeforeGrouping ? $limitBeforeGrouping : $this->limitBeforeGrouping); - - // we generate a single column that contains the interesting data for each referrer. - // the reason we cannot group by referer_* becomes clear when we look at search engine keywords. - // referer_url contains the url from the search engine, referer_keyword the keyword we want to - // group by. when we group by both, we don't get a single column for the keyword but instead - // one column per keyword + search engine url. this way, we could not get the top keywords using - // the ranking query. - $dimension = 'referrer_data'; - $rankingQuery->addLabelColumn('referrer_data'); - $select = ' + + private $limitBeforeGrouping = 5; + private $totalTransitionsToFollowingActions = 0; + + private $returnNormalizedUrls = false; + + public function getInformation() + { + return array( + 'description' => Piwik_Translate('Transitions_PluginDescription'), + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'version' => Piwik_Version::VERSION, + ); + } + + function getListHooksRegistered() + { + return array( + 'AssetManager.getCssFiles' => 'getCssFiles', + 'AssetManager.getJsFiles' => 'getJsFiles' + ); + } + + public function getCssFiles($notification) + { + $cssFiles = & $notification->getNotificationObject(); + $cssFiles[] = 'plugins/Transitions/templates/transitions.css'; + } + + public function getJsFiles($notification) + { + $jsFiles = & $notification->getNotificationObject(); + $jsFiles[] = 'plugins/Transitions/templates/transitions.js'; + } + + /** + * After calling this method, the query*()-Methods will return urls in their + * normalized form (without the prefix reconstructed) + */ + public function returnNormalizedUrls() + { + $this->returnNormalizedUrls = true; + } + + /** + * Get information about external referrers (i.e. search engines, websites & campaigns) + * + * @param $idaction + * @param $actionType + * @param Piwik_ArchiveProcessing_Day $archiveProcessing + * @param $limitBeforeGrouping + * @return Piwik_DataTable + */ + public function queryExternalReferrers($idaction, $actionType, $archiveProcessing, + $limitBeforeGrouping = false) + { + $rankingQuery = new Piwik_RankingQuery($limitBeforeGrouping ? $limitBeforeGrouping : $this->limitBeforeGrouping); + + // we generate a single column that contains the interesting data for each referrer. + // the reason we cannot group by referer_* becomes clear when we look at search engine keywords. + // referer_url contains the url from the search engine, referer_keyword the keyword we want to + // group by. when we group by both, we don't get a single column for the keyword but instead + // one column per keyword + search engine url. this way, we could not get the top keywords using + // the ranking query. + $dimension = 'referrer_data'; + $rankingQuery->addLabelColumn('referrer_data'); + $select = ' CASE referer_type - WHEN '.Piwik_Common::REFERER_TYPE_DIRECT_ENTRY.' THEN \'\' - WHEN '.Piwik_Common::REFERER_TYPE_SEARCH_ENGINE.' THEN referer_keyword - WHEN '.Piwik_Common::REFERER_TYPE_WEBSITE.' THEN referer_url - WHEN '.Piwik_Common::REFERER_TYPE_CAMPAIGN.' THEN CONCAT(referer_name, \' \', referer_keyword) + WHEN ' . Piwik_Common::REFERER_TYPE_DIRECT_ENTRY . ' THEN \'\' + WHEN ' . Piwik_Common::REFERER_TYPE_SEARCH_ENGINE . ' THEN referer_keyword + WHEN ' . Piwik_Common::REFERER_TYPE_WEBSITE . ' THEN referer_url + WHEN ' . Piwik_Common::REFERER_TYPE_CAMPAIGN . ' THEN CONCAT(referer_name, \' \', referer_keyword) END AS referrer_data, referer_type'; - - // get one limited group per referrer type - $rankingQuery->partitionResultIntoMultipleGroups('referer_type', array( - Piwik_Common::REFERER_TYPE_DIRECT_ENTRY, - Piwik_Common::REFERER_TYPE_SEARCH_ENGINE, - Piwik_Common::REFERER_TYPE_WEBSITE, - Piwik_Common::REFERER_TYPE_CAMPAIGN - )); - - $orderBy = '`'.Piwik_Archive::INDEX_NB_VISITS.'` DESC'; - - $type = $this->getColumnTypeSuffix($actionType); - $where = 'visit_entry_idaction_'.$type.' = '.intval($idaction); - - $metrics = array(Piwik_Archive::INDEX_NB_VISITS); - $data = $archiveProcessing->queryVisitsByDimension($dimension, $where, $metrics, $orderBy, - $rankingQuery, $select, $selectGeneratesLabelColumn = true); - - $referrerData = array(); - $referrerSubData = array(); - - foreach ($data as $referrerType => &$subData) - { - $referrerData[$referrerType] = array(Piwik_Archive::INDEX_NB_VISITS => 0); - if ($referrerType != Piwik_Common::REFERER_TYPE_DIRECT_ENTRY) - { - $referrerSubData[$referrerType] = array(); - } - - foreach ($subData as &$row) - { - if ($referrerType == Piwik_Common::REFERER_TYPE_SEARCH_ENGINE && empty($row['referrer_data'])) - { - $row['referrer_data'] = Piwik_Referers::LABEL_KEYWORD_NOT_DEFINED; - } - - $referrerData[$referrerType][Piwik_Archive::INDEX_NB_VISITS] += $row[Piwik_Archive::INDEX_NB_VISITS]; - - $label = $row['referrer_data']; - if ($label) - { - $referrerSubData[$referrerType][$label] = array( - Piwik_Archive::INDEX_NB_VISITS => $row[Piwik_Archive::INDEX_NB_VISITS] - ); - } - } - } - - return $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($referrerSubData, $referrerData); - } - - /** - * Get information about internal referrers (previous pages & loops, i.e. page refreshes) - * - * @param $idaction - * @param $actionType - * @param Piwik_ArchiveProcessing_Day $archiveProcessing - * @param $limitBeforeGrouping - * @return array(previousPages:Piwik_DataTable, loops:integer) - */ - public function queryInternalReferrers($idaction, $actionType, $archiveProcessing, - $limitBeforeGrouping = false) - { - $rankingQuery = new Piwik_RankingQuery($limitBeforeGrouping ? $limitBeforeGrouping : $this->limitBeforeGrouping); - $rankingQuery->addLabelColumn(array('name', 'url_prefix')); - $rankingQuery->setColumnToMarkExcludedRows('is_self'); - $rankingQuery->partitionResultIntoMultipleGroups('action_partition', array(0, 1, 2)); - - $type = $this->getColumnTypeSuffix($actionType); - $mainActionType = Piwik_Tracker_Action::TYPE_ACTION_URL; - $dimension = 'idaction_url_ref'; - $isTitle = $actionType == 'title'; - if ($isTitle) - { - $mainActionType = Piwik_Tracker_Action::TYPE_ACTION_NAME; - $dimension = 'idaction_name_ref'; - } - - $addSelect = ' + + // get one limited group per referrer type + $rankingQuery->partitionResultIntoMultipleGroups('referer_type', array( + Piwik_Common::REFERER_TYPE_DIRECT_ENTRY, + Piwik_Common::REFERER_TYPE_SEARCH_ENGINE, + Piwik_Common::REFERER_TYPE_WEBSITE, + Piwik_Common::REFERER_TYPE_CAMPAIGN + )); + + $orderBy = '`' . Piwik_Archive::INDEX_NB_VISITS . '` DESC'; + + $type = $this->getColumnTypeSuffix($actionType); + $where = 'visit_entry_idaction_' . $type . ' = ' . intval($idaction); + + $metrics = array(Piwik_Archive::INDEX_NB_VISITS); + $data = $archiveProcessing->queryVisitsByDimension($dimension, $where, $metrics, $orderBy, + $rankingQuery, $select, $selectGeneratesLabelColumn = true); + + $referrerData = array(); + $referrerSubData = array(); + + foreach ($data as $referrerType => &$subData) { + $referrerData[$referrerType] = array(Piwik_Archive::INDEX_NB_VISITS => 0); + if ($referrerType != Piwik_Common::REFERER_TYPE_DIRECT_ENTRY) { + $referrerSubData[$referrerType] = array(); + } + + foreach ($subData as &$row) { + if ($referrerType == Piwik_Common::REFERER_TYPE_SEARCH_ENGINE && empty($row['referrer_data'])) { + $row['referrer_data'] = Piwik_Referers::LABEL_KEYWORD_NOT_DEFINED; + } + + $referrerData[$referrerType][Piwik_Archive::INDEX_NB_VISITS] += $row[Piwik_Archive::INDEX_NB_VISITS]; + + $label = $row['referrer_data']; + if ($label) { + $referrerSubData[$referrerType][$label] = array( + Piwik_Archive::INDEX_NB_VISITS => $row[Piwik_Archive::INDEX_NB_VISITS] + ); + } + } + } + + return $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($referrerSubData, $referrerData); + } + + /** + * Get information about internal referrers (previous pages & loops, i.e. page refreshes) + * + * @param $idaction + * @param $actionType + * @param Piwik_ArchiveProcessing_Day $archiveProcessing + * @param $limitBeforeGrouping + * @return array(previousPages:Piwik_DataTable, loops:integer) + */ + public function queryInternalReferrers($idaction, $actionType, $archiveProcessing, + $limitBeforeGrouping = false) + { + $rankingQuery = new Piwik_RankingQuery($limitBeforeGrouping ? $limitBeforeGrouping : $this->limitBeforeGrouping); + $rankingQuery->addLabelColumn(array('name', 'url_prefix')); + $rankingQuery->setColumnToMarkExcludedRows('is_self'); + $rankingQuery->partitionResultIntoMultipleGroups('action_partition', array(0, 1, 2)); + + $type = $this->getColumnTypeSuffix($actionType); + $mainActionType = Piwik_Tracker_Action::TYPE_ACTION_URL; + $dimension = 'idaction_url_ref'; + $isTitle = $actionType == 'title'; + if ($isTitle) { + $mainActionType = Piwik_Tracker_Action::TYPE_ACTION_NAME; + $dimension = 'idaction_name_ref'; + } + + $addSelect = ' log_action.name, log_action.url_prefix, - CASE WHEN log_link_visit_action.idaction_'.$type.'_ref = '.intval($idaction).' THEN 1 ELSE 0 END AS is_self, + CASE WHEN log_link_visit_action.idaction_' . $type . '_ref = ' . intval($idaction) . ' THEN 1 ELSE 0 END AS is_self, CASE - WHEN log_action.type = '.$mainActionType.' THEN 1 - WHEN log_action.type = '.Piwik_Tracker_Action::TYPE_SITE_SEARCH.' THEN 2 + WHEN log_action.type = ' . $mainActionType . ' THEN 1 + WHEN log_action.type = ' . Piwik_Tracker_Action::TYPE_SITE_SEARCH . ' THEN 2 ELSE 0 END AS action_partition'; - - $where = ' - log_link_visit_action.idaction_'.$type.' = '.intval($idaction); - - if ($dimension == 'idaction_url_ref') - { - // site search referrers are logged with url_ref=NULL - // when we find one, we have to join on name_ref - $dimension = 'IF( idaction_url_ref IS NULL, idaction_name_ref, idaction_url_ref )'; - $joinLogActionOn = $dimension; - } - else - { - $joinLogActionOn = $dimension; - $dimension = array($dimension); - } - - $orderBy = '`'.Piwik_Archive::INDEX_NB_ACTIONS.'` DESC'; - - $metrics = array(Piwik_Archive::INDEX_NB_ACTIONS); - $data = $archiveProcessing->queryActionsByDimension($dimension, $where, $metrics, $orderBy, - $rankingQuery, $joinLogActionOn, $addSelect); - - $loops = 0; - $nbPageviews = 0; - $previousPagesDataTable = new Piwik_DataTable; - if (isset($data['result'][1])) - { - foreach ($data['result'][1] as &$page) - { - $nbActions = intval($page[Piwik_Archive::INDEX_NB_ACTIONS]); - $previousPagesDataTable->addRow(new Piwik_DataTable_Row(array( - Piwik_DataTable_Row::COLUMNS => array( - 'label' => $this->getPageLabel($page, $isTitle), - Piwik_Archive::INDEX_NB_ACTIONS => $nbActions - ) - ))); - $nbPageviews += $nbActions; - } - } - - $previousSearchesDataTable = new Piwik_DataTable; - if (isset($data['result'][2])) - { - foreach ($data['result'][2] as &$search) - { - $nbActions = intval($search[Piwik_Archive::INDEX_NB_ACTIONS]); - $previousSearchesDataTable->addRow(new Piwik_DataTable_Row(array( - Piwik_DataTable_Row::COLUMNS => array( - 'label' => $search['name'], - Piwik_Archive::INDEX_NB_ACTIONS => $nbActions - ) - ))); - $nbPageviews += $nbActions; - } - } - - if (isset($data['result'][0])) - { - foreach ($data['result'][0] as &$referrer) - { - $nbPageviews += intval($referrer[Piwik_Archive::INDEX_NB_ACTIONS]); - } - } - - if (count($data['excludedFromLimit'])) - { - $loops += intval($data['excludedFromLimit'][0][Piwik_Archive::INDEX_NB_ACTIONS]); - $nbPageviews += $loops; - } - - return array( - 'pageviews' => $nbPageviews, - 'previousPages' => $previousPagesDataTable, - 'previousSiteSearches' => $previousSearchesDataTable, - 'loops' => $loops - ); - } - - private function getPageLabel(&$pageRecord, $isTitle) - { - if ($isTitle) - { - $label = $pageRecord['name']; - if (empty($label)) - { - $label = Piwik_Actions_ArchivingHelper::getUnknownActionName( - Piwik_Tracker_Action::TYPE_ACTION_NAME); - } - return $label; - } - else if ($this->returnNormalizedUrls) - { - return $pageRecord['name']; - } - else - { - return Piwik_Tracker_Action::reconstructNormalizedUrl( - $pageRecord['name'], $pageRecord['url_prefix']); - } - } - - /** - * Get information about the following actions (following pages, site searches, outlinks, downloads) - * - * @param $idaction - * @param $actionType - * @param Piwik_ArchiveProcessing_Day $archiveProcessing - * @param $limitBeforeGrouping - * @param $includeLoops - * @return array(followingPages:Piwik_DataTable, outlinks:Piwik_DataTable, downloads:Piwik_DataTable) - */ - public function queryFollowingActions($idaction, $actionType, Piwik_ArchiveProcessing_Day $archiveProcessing, - $limitBeforeGrouping = false, $includeLoops = false) - { - $types = array(); - - $isTitle = ($actionType == 'title'); - if (!$isTitle) { - // specific setup for page urls - $types[Piwik_Tracker_Action::TYPE_ACTION_URL] = 'followingPages'; - $dimension = 'IF( idaction_url IS NULL, idaction_name, idaction_url )'; - // site search referrers are logged with url=NULL - // when we find one, we have to join on name - $joinLogActionColumn = $dimension; - $addSelect = 'log_action.name, log_action.url_prefix, log_action.type'; - } else { - // specific setup for page titles: - $types[Piwik_Tracker_Action::TYPE_ACTION_NAME] = 'followingPages'; - // join log_action on name and url and pick depending on url type - // the table joined on url is log_action1 - $joinLogActionColumn = array('idaction_url', 'idaction_name'); - $dimension = ' + + $where = ' + log_link_visit_action.idaction_' . $type . ' = ' . intval($idaction); + + if ($dimension == 'idaction_url_ref') { + // site search referrers are logged with url_ref=NULL + // when we find one, we have to join on name_ref + $dimension = 'IF( idaction_url_ref IS NULL, idaction_name_ref, idaction_url_ref )'; + $joinLogActionOn = $dimension; + } else { + $joinLogActionOn = $dimension; + $dimension = array($dimension); + } + + $orderBy = '`' . Piwik_Archive::INDEX_NB_ACTIONS . '` DESC'; + + $metrics = array(Piwik_Archive::INDEX_NB_ACTIONS); + $data = $archiveProcessing->queryActionsByDimension($dimension, $where, $metrics, $orderBy, + $rankingQuery, $joinLogActionOn, $addSelect); + + $loops = 0; + $nbPageviews = 0; + $previousPagesDataTable = new Piwik_DataTable; + if (isset($data['result'][1])) { + foreach ($data['result'][1] as &$page) { + $nbActions = intval($page[Piwik_Archive::INDEX_NB_ACTIONS]); + $previousPagesDataTable->addRow(new Piwik_DataTable_Row(array( + Piwik_DataTable_Row::COLUMNS => array( + 'label' => $this->getPageLabel($page, $isTitle), + Piwik_Archive::INDEX_NB_ACTIONS => $nbActions + ) + ))); + $nbPageviews += $nbActions; + } + } + + $previousSearchesDataTable = new Piwik_DataTable; + if (isset($data['result'][2])) { + foreach ($data['result'][2] as &$search) { + $nbActions = intval($search[Piwik_Archive::INDEX_NB_ACTIONS]); + $previousSearchesDataTable->addRow(new Piwik_DataTable_Row(array( + Piwik_DataTable_Row::COLUMNS => array( + 'label' => $search['name'], + Piwik_Archive::INDEX_NB_ACTIONS => $nbActions + ) + ))); + $nbPageviews += $nbActions; + } + } + + if (isset($data['result'][0])) { + foreach ($data['result'][0] as &$referrer) { + $nbPageviews += intval($referrer[Piwik_Archive::INDEX_NB_ACTIONS]); + } + } + + if (count($data['excludedFromLimit'])) { + $loops += intval($data['excludedFromLimit'][0][Piwik_Archive::INDEX_NB_ACTIONS]); + $nbPageviews += $loops; + } + + return array( + 'pageviews' => $nbPageviews, + 'previousPages' => $previousPagesDataTable, + 'previousSiteSearches' => $previousSearchesDataTable, + 'loops' => $loops + ); + } + + private function getPageLabel(&$pageRecord, $isTitle) + { + if ($isTitle) { + $label = $pageRecord['name']; + if (empty($label)) { + $label = Piwik_Actions_ArchivingHelper::getUnknownActionName( + Piwik_Tracker_Action::TYPE_ACTION_NAME); + } + return $label; + } else if ($this->returnNormalizedUrls) { + return $pageRecord['name']; + } else { + return Piwik_Tracker_Action::reconstructNormalizedUrl( + $pageRecord['name'], $pageRecord['url_prefix']); + } + } + + /** + * Get information about the following actions (following pages, site searches, outlinks, downloads) + * + * @param $idaction + * @param $actionType + * @param Piwik_ArchiveProcessing_Day $archiveProcessing + * @param $limitBeforeGrouping + * @param $includeLoops + * @return array(followingPages:Piwik_DataTable, outlinks:Piwik_DataTable, downloads:Piwik_DataTable) + */ + public function queryFollowingActions($idaction, $actionType, Piwik_ArchiveProcessing_Day $archiveProcessing, + $limitBeforeGrouping = false, $includeLoops = false) + { + $types = array(); + + $isTitle = ($actionType == 'title'); + if (!$isTitle) { + // specific setup for page urls + $types[Piwik_Tracker_Action::TYPE_ACTION_URL] = 'followingPages'; + $dimension = 'IF( idaction_url IS NULL, idaction_name, idaction_url )'; + // site search referrers are logged with url=NULL + // when we find one, we have to join on name + $joinLogActionColumn = $dimension; + $addSelect = 'log_action.name, log_action.url_prefix, log_action.type'; + } else { + // specific setup for page titles: + $types[Piwik_Tracker_Action::TYPE_ACTION_NAME] = 'followingPages'; + // join log_action on name and url and pick depending on url type + // the table joined on url is log_action1 + $joinLogActionColumn = array('idaction_url', 'idaction_name'); + $dimension = ' CASE ' /* following site search */ . ' WHEN log_link_visit_action.idaction_url IS NULL THEN log_action2.idaction ' /* following page view: use page title */ . ' - WHEN log_action1.type = '.Piwik_Tracker_Action::TYPE_ACTION_URL.' THEN log_action2.idaction + WHEN log_action1.type = ' . Piwik_Tracker_Action::TYPE_ACTION_URL . ' THEN log_action2.idaction ' /* following download or outlink: use url */ . ' ELSE log_action1.idaction END '; - $addSelect = ' + $addSelect = ' CASE ' /* following site search */ . ' WHEN log_link_visit_action.idaction_url IS NULL THEN log_action2.name ' /* following page view: use page title */ . ' - WHEN log_action1.type = '.Piwik_Tracker_Action::TYPE_ACTION_URL.' THEN log_action2.name + WHEN log_action1.type = ' . Piwik_Tracker_Action::TYPE_ACTION_URL . ' THEN log_action2.name ' /* following download or outlink: use url */ . ' ELSE log_action1.name END AS name, @@ -330,76 +308,73 @@ class Piwik_Transitions extends Piwik_Plugin ' /* following site search */ . ' WHEN log_link_visit_action.idaction_url IS NULL THEN log_action2.type ' /* following page view: use page title */ . ' - WHEN log_action1.type = '.Piwik_Tracker_Action::TYPE_ACTION_URL.' THEN log_action2.type + WHEN log_action1.type = ' . Piwik_Tracker_Action::TYPE_ACTION_URL . ' THEN log_action2.type ' /* following download or outlink: use url */ . ' ELSE log_action1.type END AS type, NULL AS url_prefix '; - } - - // these types are available for both titles and urls - $types[Piwik_Tracker_Action::TYPE_SITE_SEARCH] = 'followingSiteSearches'; - $types[Piwik_Tracker_Action::TYPE_OUTLINK] = 'outlinks'; - $types[Piwik_Tracker_Action::TYPE_DOWNLOAD] = 'downloads'; - - $rankingQuery = new Piwik_RankingQuery($limitBeforeGrouping ? $limitBeforeGrouping : $this->limitBeforeGrouping); - $rankingQuery->addLabelColumn(array('name', 'url_prefix')); - $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($types)); - - $type = $this->getColumnTypeSuffix($actionType); - $where = 'log_link_visit_action.idaction_'.$type.'_ref = '.intval($idaction); - if (!$includeLoops) { - $where .= ' AND (log_link_visit_action.idaction_'.$type.' IS NULL OR ' - . 'log_link_visit_action.idaction_'.$type.' != '.intval($idaction).')'; - } - - $orderBy = '`'.Piwik_Archive::INDEX_NB_ACTIONS.'` DESC'; - - $metrics = array(Piwik_Archive::INDEX_NB_ACTIONS); - $data = $archiveProcessing->queryActionsByDimension($dimension, $where, $metrics, $orderBy, - $rankingQuery, $joinLogActionColumn, $addSelect); - - $this->totalTransitionsToFollowingActions = 0; - $dataTables = array(); - foreach ($types as $type => $recordName) - { - $dataTable = new Piwik_DataTable; - if (isset($data[$type])) - { - foreach ($data[$type] as &$record) - { - $actions = intval($record[Piwik_Archive::INDEX_NB_ACTIONS]); - $dataTable->addRow(new Piwik_DataTable_Row(array( - Piwik_DataTable_Row::COLUMNS => array( - 'label' => $this->getPageLabel($record, $isTitle), - Piwik_Archive::INDEX_NB_ACTIONS => $actions - ) - ))); - $this->totalTransitionsToFollowingActions += $actions; - } - } - $dataTables[$recordName] = $dataTable; - } - - return $dataTables; - } - - /** - * Get the sum of all transitions to following actions (pages, outlinks, downloads). - * Only works if queryFollowingActions() has been used directly before. - */ - public function getTotalTransitionsToFollowingActions() - { - return $this->totalTransitionsToFollowingActions; - } - - private function getColumnTypeSuffix($actionType) - { - if ($actionType == 'title') { - return 'name'; - } - return 'url'; - } - + } + + // these types are available for both titles and urls + $types[Piwik_Tracker_Action::TYPE_SITE_SEARCH] = 'followingSiteSearches'; + $types[Piwik_Tracker_Action::TYPE_OUTLINK] = 'outlinks'; + $types[Piwik_Tracker_Action::TYPE_DOWNLOAD] = 'downloads'; + + $rankingQuery = new Piwik_RankingQuery($limitBeforeGrouping ? $limitBeforeGrouping : $this->limitBeforeGrouping); + $rankingQuery->addLabelColumn(array('name', 'url_prefix')); + $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($types)); + + $type = $this->getColumnTypeSuffix($actionType); + $where = 'log_link_visit_action.idaction_' . $type . '_ref = ' . intval($idaction); + if (!$includeLoops) { + $where .= ' AND (log_link_visit_action.idaction_' . $type . ' IS NULL OR ' + . 'log_link_visit_action.idaction_' . $type . ' != ' . intval($idaction) . ')'; + } + + $orderBy = '`' . Piwik_Archive::INDEX_NB_ACTIONS . '` DESC'; + + $metrics = array(Piwik_Archive::INDEX_NB_ACTIONS); + $data = $archiveProcessing->queryActionsByDimension($dimension, $where, $metrics, $orderBy, + $rankingQuery, $joinLogActionColumn, $addSelect); + + $this->totalTransitionsToFollowingActions = 0; + $dataTables = array(); + foreach ($types as $type => $recordName) { + $dataTable = new Piwik_DataTable; + if (isset($data[$type])) { + foreach ($data[$type] as &$record) { + $actions = intval($record[Piwik_Archive::INDEX_NB_ACTIONS]); + $dataTable->addRow(new Piwik_DataTable_Row(array( + Piwik_DataTable_Row::COLUMNS => array( + 'label' => $this->getPageLabel($record, $isTitle), + Piwik_Archive::INDEX_NB_ACTIONS => $actions + ) + ))); + $this->totalTransitionsToFollowingActions += $actions; + } + } + $dataTables[$recordName] = $dataTable; + } + + return $dataTables; + } + + /** + * Get the sum of all transitions to following actions (pages, outlinks, downloads). + * Only works if queryFollowingActions() has been used directly before. + */ + public function getTotalTransitionsToFollowingActions() + { + return $this->totalTransitionsToFollowingActions; + } + + private function getColumnTypeSuffix($actionType) + { + if ($actionType == 'title') { + return 'name'; + } + return 'url'; + } + } \ No newline at end of file diff --git a/plugins/Transitions/templates/transitions.css b/plugins/Transitions/templates/transitions.css index 75ecacb825..b0e3b0278a 100644 --- a/plugins/Transitions/templates/transitions.css +++ b/plugins/Transitions/templates/transitions.css @@ -1,197 +1,198 @@ - #Transitions_Container { - position: relative; - z-index: 1500; - height: 550px; - text-align: left; - margin-left: 7px; + position: relative; + z-index: 1500; + height: 550px; + text-align: left; + margin-left: 7px; } .Transitions_Canvas_Container { - position: absolute; + position: absolute; } #Transitions_Canvas_Background_Left { - z-index: 1501; + z-index: 1501; } #Transitions_Canvas_Background_Right { - z-index: 1502; + z-index: 1502; } #Transitions_Canvas_Left { - z-index: 1503; + z-index: 1503; } #Transitions_Canvas_Right { - z-index: 1504; + z-index: 1504; } #Transitions_Canvas_Loops { - z-index: 1505; + z-index: 1505; } .Transitions_Text { - color: black; - font-size: 11px; - line-height: 14px; - position: absolute; - background: rgba(0,0,0,0); /* without this, IE9 triggers hover only on the text, not the box */ - z-index: 1506; - font-family: Arial, Helvetica, sans-serif; - word-wrap: break-word; - text-align: left; - cursor: default; + color: black; + font-size: 11px; + line-height: 14px; + position: absolute; + background: rgba(0, 0, 0, 0); /* without this, IE9 triggers hover only on the text, not the box */ + z-index: 1506; + font-family: Arial, Helvetica, sans-serif; + word-wrap: break-word; + text-align: left; + cursor: default; } #Transitions_CenterBox { - margin: 27px 0 0 345px; - width: 208px; - height: 373px; - background: #f7f7f7; - border: 1px solid #a9a399; - border-radius:10px; - -moz-border-radius:10px; - -webkit-border-radius:10px; - -webkit-box-shadow: 0px 0px 9px 0px #999; - -moz-box-shadow: 0px 0px 9px 0px #999; - box-shadow: 0px 0px 9px 0px #999; - z-index: 1507; + margin: 27px 0 0 345px; + width: 208px; + height: 373px; + background: #f7f7f7; + border: 1px solid #a9a399; + border-radius: 10px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + -webkit-box-shadow: 0px 0px 9px 0px #999; + -moz-box-shadow: 0px 0px 9px 0px #999; + box-shadow: 0px 0px 9px 0px #999; + z-index: 1507; } #Transitions_CenterBox h2 { - font-size: 12px; - line-height: 16px; - padding: 10px; - border-bottom: 1px dotted #a9a399; - font-weight: bold; - overflow: hidden; - color: #255792; + font-size: 12px; + line-height: 16px; + padding: 10px; + border-bottom: 1px dotted #a9a399; + font-weight: bold; + overflow: hidden; + color: #255792; } .Transitions_Pageviews { - text-align: center; + text-align: center; } .Transitions_OutgoingTraffic { - text-align: right; + text-align: right; } .Transitions_CenterBoxMetrics { - padding: 15px 10px 0 10px; - display: none; - font-size: 12px; + padding: 15px 10px 0 10px; + display: none; + font-size: 12px; } .Transitions_CenterBoxMetrics table td { - padding: 0 0 5px 0; + padding: 0 0 5px 0; } .Transitions_CenterBoxMetrics table td.Transitions_Percentage { - padding-right: 6px; - font-weight: bold; + padding-right: 6px; + font-weight: bold; } #Transitions_CenterBox h3 { - font-weight: bold; - font-size: 12px; - margin: 15px 0 7px 0; - padding: 0; - color: #7E7363; + font-weight: bold; + font-size: 12px; + margin: 15px 0 7px 0; + padding: 0; + color: #7E7363; } #Transitions_Loops { - margin: 445px 0 0 346px; - width: 208px; - text-align: center; - line-height: 25px; - font-size: 12px; - display: none; - z-index: 1506; - cursor: default; + margin: 445px 0 0 346px; + width: 208px; + text-align: center; + line-height: 25px; + font-size: 12px; + display: none; + z-index: 1506; + cursor: default; } .Transitions_CenterBoxMetrics p { - margin: 0 0 3px 0; - padding: 0; - cursor: default; - font-size: 12px; - line-height: 15px; + margin: 0 0 3px 0; + padding: 0; + cursor: default; + font-size: 12px; + line-height: 15px; } .Transitions_CenterBoxMetrics p.Transitions_Margin { - margin-bottom: 11px; + margin-bottom: 11px; } .Transitions_CenterBoxMetrics .Transitions_Highlighted { - color: #E87500; + color: #E87500; } span.Transitions_Metric { - font-weight: bold; - cursor: default; + font-weight: bold; + cursor: default; } .Transitions_Value0 { - color: #666; + color: #666; } .Transitions_TitleOfOpenGroup { - font-size: 12px; - color: #E87500; - font-weight: bold; + font-size: 12px; + color: #E87500; + font-weight: bold; } .Transitions_BoxTextLeft, .Transitions_BoxTextRight { - width: 165px; - height: 42px; - overflow: hidden; + width: 165px; + height: 42px; + overflow: hidden; } .Transitions_BoxTextRight { - text-align: right; + text-align: right; } .Transitions_BoxTextLeft.Transitions_HasBackground, .Transitions_BoxTextRight.Transitions_HasBackground { - background-repeat: no-repeat; - height: 18px; + background-repeat: no-repeat; + height: 18px; } .Transitions_BoxTextLeft.Transitions_HasBackground { - background-position: 0 1px; - width: 175px; + background-position: 0 1px; + width: 175px; } + .Transitions_BoxTextLeft.Transitions_HasBackground span { - display: block; - padding-left: 16px; + display: block; + padding-left: 16px; } .Transitions_BoxTextRight.Transitions_HasBackground { - background-position: right 1px; + background-position: right 1px; } + .Transitions_BoxTextRight.Transitions_HasBackground span { - display: block; - padding-right: 17px; + display: block; + padding-right: 17px; } .Transitions_CurveTextLeft, .Transitions_CurveTextRight { - color: #255792; - font-weight: bold; - width: 34px; - text-align: center; - cursor: default; + color: #255792; + font-weight: bold; + width: 34px; + text-align: center; + cursor: default; } body .piwik-tooltip.Transitions_Tooltip_Small { - font-size: 11px; - padding: 3px 5px 3px 6px; - background: white; + font-size: 11px; + padding: 3px 5px 3px 6px; + background: white; } .Transitions_SingleLine { - font-size: 12px; - height: 21px; + font-size: 12px; + height: 21px; } \ No newline at end of file diff --git a/plugins/Transitions/templates/transitions.js b/plugins/Transitions/templates/transitions.js index 3f2d63db99..dc54e80d54 100644 --- a/plugins/Transitions/templates/transitions.js +++ b/plugins/Transitions/templates/transitions.js @@ -11,106 +11,106 @@ // function DataTable_RowActions_Transitions(dataTable) { - this.dataTable = dataTable; - this.transitions = null; + this.dataTable = dataTable; + this.transitions = null; } DataTable_RowActions_Transitions.prototype = new DataTable_RowAction; /** Static helper method to launch transitions from anywhere */ -DataTable_RowActions_Transitions.launchForUrl = function(url) { - broadcast.propagateNewPopoverParameter('RowAction', 'Transitions:url:' + url); +DataTable_RowActions_Transitions.launchForUrl = function (url) { + broadcast.propagateNewPopoverParameter('RowAction', 'Transitions:url:' + url); }; -DataTable_RowActions_Transitions.isPageUrlReport = function(module, action) { - return module == 'Actions' && - (action == 'getPageUrls' || action == 'getEntryPageUrls' || action == 'getExitPageUrls' || action == 'getPageUrlsFollowingSiteSearch'); +DataTable_RowActions_Transitions.isPageUrlReport = function (module, action) { + return module == 'Actions' && + (action == 'getPageUrls' || action == 'getEntryPageUrls' || action == 'getExitPageUrls' || action == 'getPageUrlsFollowingSiteSearch'); }; -DataTable_RowActions_Transitions.isPageTitleReport = function(module, action) { - return module == 'Actions' && (action == 'getPageTitles' || action == 'getPageTitlesFollowingSiteSearch'); +DataTable_RowActions_Transitions.isPageTitleReport = function (module, action) { + return module == 'Actions' && (action == 'getPageTitles' || action == 'getPageTitlesFollowingSiteSearch'); } -DataTable_RowActions_Transitions.prototype.trigger = function(tr, e, subTableLabel) { - var link = tr.find('> td:first > a').attr('href'); - link = $('