add piwik installation
This commit is contained in:
parent
5a6b53d222
commit
823913bfd5
3197 changed files with 563902 additions and 0 deletions
|
|
@ -4,3 +4,5 @@ syntax: regexp
|
|||
^uploads/*
|
||||
^seminarymedia/*
|
||||
^seminaryuploads/*
|
||||
^www/analytics/config/*
|
||||
^www/analytics/temp/*
|
||||
|
|
|
|||
|
|
@ -58,6 +58,19 @@
|
|||
<?=$seminarybar?>
|
||||
<?php endif ?>
|
||||
</aside>
|
||||
<script type="text/javascript">
|
||||
var _paq = _paq || [];
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u=(("https:" == document.location.protocol) ? "https" : "http") + "://" + document.location.hostname + "/analytics/";
|
||||
_paq.push(['setTrackerUrl', u+'piwik.php']);
|
||||
_paq.push(['setSiteId', 1]);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; g.type='text/javascript';
|
||||
g.defer=true; g.async=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
<noscript><p><img src="/analytics/piwik.php?idsite=1" style="border:0;" alt="" /></p></noscript>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
|||
265
www/analytics/LEGALNOTICE
Normal file
265
www/analytics/LEGALNOTICE
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
COPYRIGHT
|
||||
|
||||
Piwik - Open Source Web Analytics
|
||||
|
||||
The software package is:
|
||||
|
||||
Copyright (C) 2013 Matthieu Aubry
|
||||
|
||||
Individual contributions, components, and libraries are copyright
|
||||
of their respective authors.
|
||||
|
||||
|
||||
SOFTWARE LICENSE
|
||||
|
||||
The free software license of Piwik is GNU General Public License v3
|
||||
or later. A copy of GNU GPL v3 should have been included in this
|
||||
software package in misc/gpl-3.0.txt.
|
||||
|
||||
|
||||
TRADEMARK
|
||||
|
||||
Piwik (TM) is an internationally registered trademark.
|
||||
|
||||
The software license does not grant any rights under trademark
|
||||
law for use of the trademark. Refer to http://piwik.org/trademark/
|
||||
for up-to-date trademark licensing information.
|
||||
|
||||
*
|
||||
* The software license applies to both the aggregate software and
|
||||
* application-specific portions of the software.
|
||||
*
|
||||
* You may not remove this legal notice or modify the software
|
||||
* in such a way that misrepresents the origin of the software.
|
||||
*
|
||||
|
||||
CREDITS
|
||||
|
||||
The software consists of contributions made by many individuals.
|
||||
Major contributors are listed in http://piwik.org/the-piwik-team/.
|
||||
|
||||
For detailed contribution history, refer to the source, tickets,
|
||||
patches, and Git revision history, available at
|
||||
http://dev.piwik.org/trac/
|
||||
https://github.com/piwik/piwik
|
||||
|
||||
|
||||
SEPARATELY LICENSED COMPONENTS AND LIBRARIES
|
||||
|
||||
The following components/libraries are distributed in this package,
|
||||
and subject to their respective licenses.
|
||||
|
||||
Name: javascriptCode.tpl - tracking tag to embed in your web pages
|
||||
Link: https://github.com/piwik/piwik/blob/master/core/Tracker/javascriptTag.tpl
|
||||
License: Public Domain
|
||||
|
||||
Name: jquery.truncate
|
||||
Link: https://github.com/piwik/piwik/blob/master/libs/jquery/truncate/
|
||||
License: New BSD
|
||||
|
||||
Name: piwik.js - JavaScript tracker
|
||||
Link: https://github.com/piwik/piwik/blob/master/js/piwik.js
|
||||
License: New BSD
|
||||
|
||||
Name: PiwikTracker - server-side tracker (PHP)
|
||||
Link: https://github.com/piwik/piwik/blob/master/libs/PiwikTracker/
|
||||
License: New BSD
|
||||
|
||||
Name: UserAgentParser
|
||||
Link: https://github.com/piwik/piwik/blob/master/libs/UserAgentParser/
|
||||
License: New BSD
|
||||
|
||||
|
||||
THIRD-PARTY COMPONENTS AND LIBRARIES
|
||||
|
||||
The following components/libraries are redistributed in this package,
|
||||
and subject to their respective licenses.
|
||||
|
||||
Name: jqPlot
|
||||
Link: http://www.jqplot.com/
|
||||
License: Dual-licensed: MIT or GPL v2
|
||||
|
||||
Name: jQuery
|
||||
Link: http://jquery.com/
|
||||
License: Dual-licensed: MIT or GPL
|
||||
Notes:
|
||||
- GPL version not explicitly stated in source but GPL v2 is in git
|
||||
- includes Sizzle.js - multi-licensed: MIT, New BSD, or GPL [v2]
|
||||
|
||||
Name: jQuery UI
|
||||
Link: http://jqueryui.com/
|
||||
License: Dual-licensed: MIT or GPL
|
||||
Notes:
|
||||
- GPL version not explicitly stated in source but GPL v2 is in git
|
||||
|
||||
Name: jquery.history
|
||||
Link: http://tkyk.github.com/jquery-history-plugin/
|
||||
License: MIT
|
||||
|
||||
Name: jquery.scrollTo
|
||||
Link: http://plugins.jquery.com/project/ScrollTo
|
||||
License: Dual licensed: MIT or GPL
|
||||
|
||||
Name: jquery Tooltip
|
||||
Link: http://bassistance.de/jquery-plugins/jquery-plugin-tooltip/
|
||||
License: Dual licensed: MIT or GPL
|
||||
|
||||
Name: jquery placeholder
|
||||
Link: http://mths.be/placeholder
|
||||
License: Dual licensed: MIT or GPL
|
||||
|
||||
Name: jquery smartbanner
|
||||
Link: https://github.com/jasny/jquery.smartbanner
|
||||
License: Dual licensed: MIT
|
||||
|
||||
Name: json2.js
|
||||
Link: http://json.org/
|
||||
License: Public domain
|
||||
Notes:
|
||||
- reference implementation
|
||||
|
||||
Name: jshrink
|
||||
Link: https://github.com/tedivm/jshrink
|
||||
License: BSD-3-Clause
|
||||
|
||||
Name: sparkline
|
||||
Link: https//sourceforge.net/projects/sparkline/
|
||||
License: Dual-licensed: New BSD or GPL v2
|
||||
|
||||
Name: sprintf
|
||||
Link: http://www.diveintojavascript.com/projects/javascript-sprintf
|
||||
License: New BSD
|
||||
|
||||
Name: upgrade.php
|
||||
Link: http://upgradephp.berlios.de/
|
||||
License: Public domain
|
||||
|
||||
Name: Archive Tar
|
||||
Link: http://pear.php.net/package/Archive_Tar
|
||||
License: New BSD
|
||||
|
||||
Name: Event Dispatcher (and Notification)
|
||||
Link: http://pear.php.net/package/Event_Dispatcher/
|
||||
License: New BSD
|
||||
|
||||
Name: HTML Common2
|
||||
Link: http://pear.php.net/package/HTML_Common2/
|
||||
License: New BSD
|
||||
|
||||
Name: HTML QuickForm2
|
||||
Link: http://pear.php.net/package/HTML_QuickForm2/
|
||||
License: New BSD
|
||||
|
||||
Name: HTML QuickForm2_Renderer_Smarty
|
||||
Link: http://www.phcomp.co.uk/tmp/Smarty.phps
|
||||
License: New BSD
|
||||
|
||||
Name: MaxMindGeoIP
|
||||
Link: http://dev.maxmind.com/geoip/downloadable#PHP-7
|
||||
License: LGPL
|
||||
|
||||
Name: PclZip
|
||||
Link: http://www.phpconcept.net/pclzip/
|
||||
License: LGPL
|
||||
Notes:
|
||||
- GPL version not explicitly stated but tarball contains LGPL v2.1
|
||||
|
||||
Name: PEAR (base system)
|
||||
Link: http://pear.php.net/package/PEAR
|
||||
License: New BSD
|
||||
|
||||
Name: PhpSecInfo
|
||||
Link: http://phpsec.org/projects/phpsecinfo/
|
||||
License: New BSD
|
||||
|
||||
Name: RankChecker
|
||||
Link: http://www.getrank.org/free-pagerank-script
|
||||
License: GPL
|
||||
|
||||
Name: Twig
|
||||
Link: http://twig.sensiolabs.org/
|
||||
License: BSD
|
||||
|
||||
Name: TCPDF
|
||||
Link: http://sourceforge.net/projects/tcpdf
|
||||
License: LGPL v3 or later
|
||||
|
||||
Name: Zend Framework
|
||||
Link: http://www.zendframework.com/
|
||||
License: New BSD
|
||||
|
||||
Name: pChart 2.1.3
|
||||
Link: http://www.pchart.net
|
||||
License: GPL v3
|
||||
|
||||
Name: Chroma.js
|
||||
Link: https://github.com/gka/chroma.js
|
||||
License: GPL v3
|
||||
|
||||
Name: qTip2 - Pretty powerful tooltips
|
||||
Link: http://craigsworks.com/projects/qtip2/
|
||||
License: GPL
|
||||
|
||||
Name: Kartograph.js
|
||||
Link: http://kartograph.org/
|
||||
License: LGPL v3 or later
|
||||
|
||||
Name: Raphaël - JavaScript Vector Library
|
||||
Link: http://raphaeljs.com/
|
||||
License: MIT
|
||||
|
||||
Name: lessphp
|
||||
Link: http://leafo.net/lessphp
|
||||
License: GPL3/MIT
|
||||
|
||||
Name: Symfony Console Component
|
||||
Link: https://github.com/symfony/Console
|
||||
License: MIT
|
||||
|
||||
|
||||
THIRD-PARTY CONTENT
|
||||
|
||||
Name: FamFamFam icons - Mark James
|
||||
Link: http://www.famfamfam.com/lab/icons/
|
||||
License: CC BY 3.0
|
||||
|
||||
Name: Solar System icons - Dan Wiersema
|
||||
Link: http://www.iconspedia.com/icon/neptune-4672.html
|
||||
License: Free for non-commercial use
|
||||
Notes:
|
||||
- used in Piwik's ExampleUI plugin
|
||||
|
||||
Name: Wine project - tahoma.ttf font
|
||||
Link: http://source.winehq.org/git/wine.git/blob_plain/HEAD:/fonts/tahoma.ttf
|
||||
License: LGPL v2.1
|
||||
Notes:
|
||||
- used in ImageGraph plugin
|
||||
|
||||
Name: plugins/CorePluginsAdmin/images/themes.png
|
||||
Link: https://www.iconfinder.com/icons/17022/colors_draw_paint_icon
|
||||
License: Free for commercial use
|
||||
|
||||
Name: plugins/Feedback/angularjs/ratefeature/thumbs-down.png
|
||||
Link: https://www.iconfinder.com/icons/216428/down_thumbs_icon
|
||||
License: Creative Commons (Attribution-Share Alike 3.0 Unported)
|
||||
|
||||
Name: plugins/Feedback/angularjs/ratefeature/thumbs-up.png
|
||||
Link: https://www.iconfinder.com/icons/216429/thumbs_up_icon
|
||||
License: Creative Commons (Attribution-Share Alike 3.0 Unported)
|
||||
|
||||
Name: plugins/CorePluginsAdmin/images/plugins.png
|
||||
Link: http://findicons.com/icon/94051/tools_wizard?id=396912
|
||||
License: GNU/GPL
|
||||
|
||||
Name: plugins/Insights/images/idea.png
|
||||
Link: https://www.iconfinder.com/icons/6074/brainstorm_bulb_idea_jabber_light_icon
|
||||
License: GPL
|
||||
By: Alessandro Rei - http://www.kde-look.org/usermanager/search.php?username=mentalrey
|
||||
|
||||
Notes:
|
||||
- the "New BSD" license refers to either the "Modified BSD" and "Simplified BSD"
|
||||
licenses (2- or 3-clause), which are GPL compatible.
|
||||
- the "MIT" license is also referred to as the "X11" license
|
||||
- icons for browsers, operating systems, browser plugins, search engines, and
|
||||
and flags of countries are nominative use of third-party trademarks when
|
||||
referring to the corresponding product or entity
|
||||
92
www/analytics/README.md
Normal file
92
www/analytics/README.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# Piwik - piwik.org
|
||||
|
||||
## Description
|
||||
|
||||
Piwik is the leading Free/Libre open source Web Analytics platform.
|
||||
|
||||
Piwik is a full featured PHP MySQL software program that you download and install on your own webserver.
|
||||
At the end of the five minute installation process you will be given a JavaScript code.
|
||||
Simply copy and paste this tag on websites you wish to track and access your analytics reports in real time.
|
||||
|
||||
Piwik aims to be a Free software alternative to Google Analytics, and is already used on more than 1,000,000 websites.
|
||||
|
||||
## Mission Statement
|
||||
|
||||
> « To create, as a community, the leading international Free/Libre web analytics platform, providing access to all functionality through open components and open APIs. »
|
||||
|
||||
Or in short:
|
||||
> « Liberate Web Analytics »
|
||||
|
||||
## License
|
||||
|
||||
Piwik is released under the GPL v3 (or later) license, see [misc/gpl-3.0.txt](misc/gpl-3.0.txt)
|
||||
|
||||
## Requirements
|
||||
|
||||
* PHP 5.3.2 or greater
|
||||
* MySQL 4.1 or greater, and either MySQLi or PDO library must be enabled
|
||||
* Piwik is OS / server independent
|
||||
|
||||
See http://piwik.org/docs/requirements/
|
||||
|
||||
## Install
|
||||
|
||||
* Upload piwik to your webserver
|
||||
* Point your browser to the directory
|
||||
* Follow the steps
|
||||
* Add the given javascript code to your pages
|
||||
* (You may also generate fake data to experiment, by enabling the plugin VisitorGenerator)
|
||||
|
||||
See http://piwik.org/docs/installation/
|
||||
|
||||
If you do not have a server, consider our Piwik Hosting partner: http://piwik.org/hosting/
|
||||
|
||||
## Changelog
|
||||
|
||||
For the list of all tickets closed in the current and past releases, see http://piwik.org/changelog/
|
||||
|
||||
## Participate!
|
||||
|
||||
We believe in liberating Web Analytics, providing a free platform for simple and advanced analytics. Piwik was built by dozens of people like you,
|
||||
and we need your help to make Piwik better… Why not participate in a useful project today?
|
||||
|
||||
You will find pointers on how you can participate in Piwik at http://piwik.org/get-involved/
|
||||
|
||||
## Contact
|
||||
|
||||
http://piwik.org
|
||||
|
||||
hello@piwik.org
|
||||
|
||||
About us: http://piwik.org/the-piwik-team/
|
||||
|
||||
## More information
|
||||
|
||||
What makes Piwik unique from the competition:
|
||||
|
||||
* Real time web analytics reports: in Piwik, reports are by default generated in real time.
|
||||
For high traffic websites, you can choose the frequency for reports to be processed.
|
||||
|
||||
* You own your web analytics data: since Piwik is installed on your server, the data is stored in your own database and you can get all the statistics
|
||||
using the powerful Piwik Analytics API.
|
||||
|
||||
* Piwik is a Free Software which can easily be configured to respect your visitors privacy.
|
||||
|
||||
* Modern, easy to use User Interface: you can fully customize your dashboard, drag and drop widgets and more.
|
||||
|
||||
* Piwik features are built inside plugins: you can add new features and remove the ones you don’t need.
|
||||
You can build your own web analytics plugins or hire a consultant to have your custom feature built in Piwik
|
||||
|
||||
* Vibrant international Open community of more than 200,000 active users (tracking even more websites!)
|
||||
|
||||
* Advanced Web Analytics capabilities such as Ecommerce Tracking, Goal tracking, Campaign tracking,
|
||||
Custom Variables, Email Reports, Custom Segment Editor, Geo Location, Real time maps, and more!
|
||||
|
||||
Documentation and more info on http://piwik.org
|
||||
|
||||
## Code Status
|
||||
The Piwik project uses an ever-expanding comprehensive set of thousands of unit tests and dozens of integration [tests](https://github.com/piwik/piwik/tree/master/tests),
|
||||
running on the hosted distributed continuous integration platform Travis-CI.
|
||||
|
||||
Build status (master branch) [](https://travis-ci.org/piwik/piwik) - Screenshot tests Build [](https://travis-ci.org/piwik/piwik-ui-tests)
|
||||
|
||||
30
www/analytics/composer.json
Normal file
30
www/analytics/composer.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "piwik/piwik",
|
||||
"type": "application",
|
||||
"description": "Open Source Real Time Web Analytics Platform",
|
||||
"keywords": ["piwik","web","analytics"],
|
||||
"homepage": "http://piwik.org",
|
||||
"license": "GPL-3.0+",
|
||||
"authors": [
|
||||
{
|
||||
"name": "The Piwik Team",
|
||||
"email": "hello@piwik.org",
|
||||
"homepage": "http://piwik.org/the-piwik-team/"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"forum": "http://forum.piwik.org/",
|
||||
"issues": "http://dev.piwik.org/trac/roadmap",
|
||||
"wiki": "http://dev.piwik.org/",
|
||||
"source": "https://github.com/piwik/piwik"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.2",
|
||||
"twig/twig": "1.*",
|
||||
"leafo/lessphp": "~0.3",
|
||||
"symfony/console": ">=v2.3.5",
|
||||
"tedivm/jshrink": "v0.5.1",
|
||||
"mustangostang/spyc": "0.5.*",
|
||||
"piwik/device-detector": "*"
|
||||
}
|
||||
}
|
||||
310
www/analytics/composer.lock
generated
Normal file
310
www/analytics/composer.lock
generated
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file"
|
||||
],
|
||||
"hash": "69246584e6b57bfbc8d39799cd3b9213",
|
||||
"packages": [
|
||||
{
|
||||
"name": "leafo/lessphp",
|
||||
"version": "v0.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/leafo/lessphp.git",
|
||||
"reference": "51f3f06f0fe78a722dabfd14578444bdd078d9de"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/leafo/lessphp/zipball/51f3f06f0fe78a722dabfd14578444bdd078d9de",
|
||||
"reference": "51f3f06f0fe78a722dabfd14578444bdd078d9de",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "0.3-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"lessc.inc.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT",
|
||||
"GPL-3.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Leaf Corcoran",
|
||||
"email": "leafot@gmail.com",
|
||||
"homepage": "http://leafo.net"
|
||||
}
|
||||
],
|
||||
"description": "lessphp is a compiler for LESS written in PHP.",
|
||||
"homepage": "http://leafo.net/lessphp/",
|
||||
"time": "2013-08-09 17:09:19"
|
||||
},
|
||||
{
|
||||
"name": "mustangostang/spyc",
|
||||
"version": "0.5.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mustangostang/spyc.git",
|
||||
"reference": "dc4785b4d7227fd9905e086d499fb8abfadf9977"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/mustangostang/spyc/zipball/dc4785b4d7227fd9905e086d499fb8abfadf9977",
|
||||
"reference": "dc4785b4d7227fd9905e086d499fb8abfadf9977",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "0.5.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"Spyc.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT License"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "mustangostang",
|
||||
"email": "vlad.andersen@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "A simple YAML loader/dumper class for PHP",
|
||||
"homepage": "https://github.com/mustangostang/spyc/",
|
||||
"keywords": [
|
||||
"spyc",
|
||||
"yaml",
|
||||
"yml"
|
||||
],
|
||||
"time": "2013-02-21 10:52:01"
|
||||
},
|
||||
{
|
||||
"name": "piwik/device-detector",
|
||||
"version": "1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/piwik/device-detector.git",
|
||||
"reference": "ea7c5d8b76def0d8345a4eba59c5f98ec0109de6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/piwik/device-detector/zipball/ea7c5d8b76def0d8345a4eba59c5f98ec0109de6",
|
||||
"reference": "ea7c5d8b76def0d8345a4eba59c5f98ec0109de6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"mustangostang/spyc": "*",
|
||||
"php": ">=5.3.1"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"DeviceDetector.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"GPL-3.0+"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "The Piwik Team",
|
||||
"email": "hello@piwik.org",
|
||||
"homepage": "http://piwik.org/the-piwik-team/"
|
||||
}
|
||||
],
|
||||
"description": "The Universal Device Detection library, that parses User Agents and detects devices (desktop, tablet, mobile, tv, cars, console, etc.), and detects browsers, operating systems, devices, brands and models.",
|
||||
"homepage": "http://piwik.org",
|
||||
"keywords": [
|
||||
"devicedetection",
|
||||
"parser",
|
||||
"useragent"
|
||||
],
|
||||
"time": "2014-04-03 08:59:48"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v2.4.3",
|
||||
"target-dir": "Symfony/Component/Console",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/Console.git",
|
||||
"reference": "ef20f1f58d7f693ee888353962bd2db336e3bbcb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/Console/zipball/ef20f1f58d7f693ee888353962bd2db336e3bbcb",
|
||||
"reference": "ef20f1f58d7f693ee888353962bd2db336e3bbcb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/event-dispatcher": "~2.1"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/event-dispatcher": ""
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.4-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Symfony\\Component\\Console\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com",
|
||||
"homepage": "http://fabien.potencier.org",
|
||||
"role": "Lead Developer"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "http://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Console Component",
|
||||
"homepage": "http://symfony.com",
|
||||
"time": "2014-03-01 17:35:04"
|
||||
},
|
||||
{
|
||||
"name": "tedivm/jshrink",
|
||||
"version": "v0.5.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tedivm/JShrink.git",
|
||||
"reference": "2d3f1a7d336ad54bdf2180732b806c768a791cbf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/tedivm/JShrink/zipball/2d3f1a7d336ad54bdf2180732b806c768a791cbf",
|
||||
"reference": "2d3f1a7d336ad54bdf2180732b806c768a791cbf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"JShrink": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Robert Hafner",
|
||||
"email": "tedivm@tedivm.com"
|
||||
}
|
||||
],
|
||||
"description": "Javascript Minifier built in PHP",
|
||||
"homepage": "http://github.com/tedivm/JShrink",
|
||||
"keywords": [
|
||||
"javascript",
|
||||
"minifier"
|
||||
],
|
||||
"time": "2012-11-26 04:48:55"
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "v1.15.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/fabpot/Twig.git",
|
||||
"reference": "1fb5784662f438d7d96a541e305e28b812e2eeed"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/fabpot/Twig/zipball/1fb5784662f438d7d96a541e305e28b812e2eeed",
|
||||
"reference": "1fb5784662f438d7d96a541e305e28b812e2eeed",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.2.4"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.15-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Twig_": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com",
|
||||
"homepage": "http://fabien.potencier.org",
|
||||
"role": "Lead Developer"
|
||||
},
|
||||
{
|
||||
"name": "Armin Ronacher",
|
||||
"email": "armin.ronacher@active-4.com",
|
||||
"role": "Project Founder"
|
||||
},
|
||||
{
|
||||
"name": "Twig Team",
|
||||
"homepage": "https://github.com/fabpot/Twig/graphs/contributors",
|
||||
"role": "Contributors"
|
||||
}
|
||||
],
|
||||
"description": "Twig, the flexible, fast, and secure template language for PHP",
|
||||
"homepage": "http://twig.sensiolabs.org",
|
||||
"keywords": [
|
||||
"templating"
|
||||
],
|
||||
"time": "2014-02-13 10:19:29"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
|
||||
],
|
||||
"aliases": [
|
||||
|
||||
],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [
|
||||
|
||||
],
|
||||
"platform": {
|
||||
"php": ">=5.3.2"
|
||||
},
|
||||
"platform-dev": [
|
||||
|
||||
]
|
||||
}
|
||||
28
www/analytics/console
Executable file
28
www/analytics/console
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
if (!defined('PIWIK_DOCUMENT_ROOT')) {
|
||||
define('PIWIK_DOCUMENT_ROOT', dirname(__FILE__) == '/' ? '' : dirname(__FILE__));
|
||||
}
|
||||
if (!defined('PIWIK_INCLUDE_PATH')) {
|
||||
define('PIWIK_INCLUDE_PATH', PIWIK_DOCUMENT_ROOT);
|
||||
}
|
||||
if (!defined('PIWIK_USER_PATH')) {
|
||||
define('PIWIK_USER_PATH', PIWIK_DOCUMENT_ROOT);
|
||||
}
|
||||
|
||||
require_once PIWIK_INCLUDE_PATH . '/core/testMinimumPhpVersion.php';
|
||||
require_once file_exists(PIWIK_INCLUDE_PATH . '/vendor/autoload.php')
|
||||
? PIWIK_INCLUDE_PATH . '/vendor/autoload.php' // Piwik is the main project
|
||||
: PIWIK_INCLUDE_PATH . '/../../autoload.php'; // Piwik is installed as a dependency
|
||||
require_once PIWIK_INCLUDE_PATH . '/core/Loader.php';
|
||||
require_once PIWIK_INCLUDE_PATH . '/libs/upgradephp/upgrade.php';
|
||||
|
||||
Piwik\Translate::loadEnglishTranslation();
|
||||
|
||||
if (!Piwik\Common::isPhpCliMode()) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$console = new Piwik\Console();
|
||||
$console->init();
|
||||
$console->run();
|
||||
149
www/analytics/core/API/DataTableGenericFilter.php
Normal file
149
www/analytics/core/API/DataTableGenericFilter.php
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\API;
|
||||
|
||||
use Exception;
|
||||
use Piwik\Common;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Filter\AddColumnsProcessedMetricsGoal;
|
||||
|
||||
class DataTableGenericFilter
|
||||
{
|
||||
private static $genericFiltersInfo = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param $request
|
||||
*/
|
||||
function __construct($request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the given data table
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
$this->applyGenericFilters($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing the information of the generic 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', 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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 DataTable $datatable
|
||||
* @return bool
|
||||
*/
|
||||
protected function applyGenericFilters($datatable)
|
||||
{
|
||||
if ($datatable instanceof DataTable\Map) {
|
||||
$tables = $datatable->getDataTables();
|
||||
foreach ($tables as $table) {
|
||||
$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 = 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;
|
||||
}
|
||||
}
|
||||
192
www/analytics/core/API/DataTableManipulator.php
Normal file
192
www/analytics/core/API/DataTableManipulator.php
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\API;
|
||||
|
||||
use Exception;
|
||||
use Piwik\Archive\DataTableFactory;
|
||||
use Piwik\Common;
|
||||
use Piwik\DataTable\Row;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\Period\Range;
|
||||
use Piwik\Plugins\API\API;
|
||||
|
||||
/**
|
||||
* Base class for manipulating data tables.
|
||||
* It provides generic mechanisms like iteration and loading subtables.
|
||||
*
|
||||
* The manipulators are used in ResponseBuilder and are triggered by
|
||||
* API parameters. They are not filters because they don't work on the pre-
|
||||
* fetched nested data tables. Instead, they load subtables using this base
|
||||
* class. This way, they can only load the tables they really need instead
|
||||
* of using expanded=1. Another difference between manipulators and filters
|
||||
* is that filters keep the overall structure of the table intact while
|
||||
* manipulators can change the entire thing.
|
||||
*/
|
||||
abstract class DataTableManipulator
|
||||
{
|
||||
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 maps. It calls back the template method self::doManipulate for each table.
|
||||
* This way, data table arrays can be handled in a transparent fashion.
|
||||
*
|
||||
* @param DataTable\Map|DataTable $dataTable
|
||||
* @throws Exception
|
||||
* @return DataTable\Map|DataTable
|
||||
*/
|
||||
protected function manipulate($dataTable)
|
||||
{
|
||||
if ($dataTable instanceof DataTable\Map) {
|
||||
return $this->manipulateDataTableMap($dataTable);
|
||||
} else if ($dataTable instanceof DataTable) {
|
||||
return $this->manipulateDataTable($dataTable);
|
||||
} else {
|
||||
return $dataTable;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manipulates child DataTables of a DataTable\Map. See @manipulate for more info.
|
||||
*
|
||||
* @param DataTable\Map $dataTable
|
||||
* @return DataTable\Map
|
||||
*/
|
||||
protected function manipulateDataTableMap($dataTable)
|
||||
{
|
||||
$result = $dataTable->getEmptyClone();
|
||||
foreach ($dataTable->getDataTables() as $tableLabel => $childTable) {
|
||||
$newTable = $this->manipulate($childTable);
|
||||
$result->addTable($newTable, $tableLabel);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manipulates a single 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 DataTable $dataTable
|
||||
* @param Row $row
|
||||
*
|
||||
* @return 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) {
|
||||
$period = $dataTable->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX);
|
||||
if ($period instanceof Range) {
|
||||
$request['date'] = $period->getDateStart() . ',' . $period->getDateEnd();
|
||||
} else {
|
||||
$request['date'] = $period->getDateStart()->toString();
|
||||
}
|
||||
}
|
||||
|
||||
$method = $this->getApiMethodForSubtable();
|
||||
return $this->callApiAndReturnDataTable($this->apiModule, $method, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* In this method, subclasses can clean up the request array for loading subtables
|
||||
* in order to make 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 = API::getInstance()->getMetadata('all', $this->apiModule, $this->apiMethod);
|
||||
|
||||
if(empty($meta)) {
|
||||
throw new Exception(sprintf(
|
||||
"The DataTable cannot be manipulated: Metadata for report %s.%s could not be found. You can define the metadata in a hook, see example at: http://developer.piwik.org/api-reference/events#apigetreportmetadata",
|
||||
$this->apiModule, $this->apiMethod
|
||||
));
|
||||
}
|
||||
|
||||
if (isset($meta[0]['actionToLoadSubTables'])) {
|
||||
$this->apiMethodForSubtable = $meta[0]['actionToLoadSubTables'];
|
||||
} else {
|
||||
$this->apiMethodForSubtable = $this->apiMethod;
|
||||
}
|
||||
}
|
||||
return $this->apiMethodForSubtable;
|
||||
}
|
||||
|
||||
protected function callApiAndReturnDataTable($apiModule, $method, $request)
|
||||
{
|
||||
$class = Request::getClassNameAPI($apiModule);
|
||||
|
||||
$request = $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 = Proxy::getInstance()->call($class, $method, $request);
|
||||
$response = new ResponseBuilder($format = 'original', $request);
|
||||
$dataTable = $response->getResponse($dataTable);
|
||||
|
||||
if (Common::getRequestVar('disable_queued_filters', 0, 'int', $request) == 0) {
|
||||
if (method_exists($dataTable, 'applyQueuedFilters')) {
|
||||
$dataTable->applyQueuedFilters();
|
||||
}
|
||||
}
|
||||
|
||||
return $dataTable;
|
||||
}
|
||||
}
|
||||
137
www/analytics/core/API/DataTableManipulator/Flattener.php
Normal file
137
www/analytics/core/API/DataTableManipulator/Flattener.php
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\API\DataTableManipulator;
|
||||
|
||||
use Piwik\API\DataTableManipulator;
|
||||
use Piwik\Common;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Row;
|
||||
|
||||
/**
|
||||
* This class is responsible for flattening data tables.
|
||||
*
|
||||
* It loads subtables and combines them into a single table by concatenating the labels.
|
||||
* This manipulator is triggered by using flat=1 in the API request.
|
||||
*/
|
||||
class Flattener extends DataTableManipulator
|
||||
{
|
||||
|
||||
private $includeAggregateRows = false;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Separator for building recursive labels (or paths)
|
||||
* @var string
|
||||
*/
|
||||
public $recursiveLabelSeparator = ' - ';
|
||||
|
||||
/**
|
||||
* @param DataTable $dataTable
|
||||
* @return DataTable|DataTable\Map
|
||||
*/
|
||||
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 DataTable $dataTable
|
||||
* @return DataTable
|
||||
*/
|
||||
protected function manipulateDataTable($dataTable)
|
||||
{
|
||||
// apply filters now since subtables have their filters applied before generic filters. if we don't do this
|
||||
// now, we'll try to apply filters to rows that have already been manipulated. this results in errors like
|
||||
// 'column ... already exists'.
|
||||
$keepFilters = true;
|
||||
if (Common::getRequestVar('disable_queued_filters', 0, 'int', $this->request) == 0) {
|
||||
$dataTable->applyQueuedFilters();
|
||||
$keepFilters = false;
|
||||
}
|
||||
|
||||
$newDataTable = $dataTable->getEmptyClone($keepFilters);
|
||||
foreach ($dataTable->getRows() as $row) {
|
||||
$this->flattenRow($row, $newDataTable);
|
||||
}
|
||||
return $newDataTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Row $row
|
||||
* @param DataTable $dataTable
|
||||
* @param string $labelPrefix
|
||||
* @param bool $parentLogo
|
||||
*/
|
||||
private function flattenRow(Row $row, 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']);
|
||||
|
||||
return $request;
|
||||
}
|
||||
}
|
||||
167
www/analytics/core/API/DataTableManipulator/LabelFilter.php
Normal file
167
www/analytics/core/API/DataTableManipulator/LabelFilter.php
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\API\DataTableManipulator;
|
||||
|
||||
use Piwik\API\DataTableManipulator;
|
||||
use Piwik\Common;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Row;
|
||||
|
||||
/**
|
||||
* This class is responsible for handling the label parameter that can be
|
||||
* added to every API call. If the parameter is set, only the row with the matching
|
||||
* label is returned.
|
||||
*
|
||||
* The labels passed to this class should be urlencoded.
|
||||
* Some reports use recursive labels (e.g. action reports). Use > to join them.
|
||||
*/
|
||||
class LabelFilter extends DataTableManipulator
|
||||
{
|
||||
const SEPARATOR_RECURSIVE_LABEL = '>';
|
||||
|
||||
private $labels;
|
||||
private $addLabelIndex;
|
||||
const FLAG_IS_ROW_EVOLUTION = 'label_index';
|
||||
|
||||
/**
|
||||
* 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 DataTable $dataTable the data table to be filtered
|
||||
* @param bool $addLabelIndex Whether to add label_index metadata describing which
|
||||
* label a row corresponds to.
|
||||
* @return DataTable
|
||||
*/
|
||||
public function filter($labels, $dataTable, $addLabelIndex = false)
|
||||
{
|
||||
if (!is_array($labels)) {
|
||||
$labels = array($labels);
|
||||
}
|
||||
|
||||
$this->labels = $labels;
|
||||
$this->addLabelIndex = (bool)$addLabelIndex;
|
||||
return $this->manipulate($dataTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for the recursive descend
|
||||
*
|
||||
* @param array $labelParts
|
||||
* @param DataTable $dataTable
|
||||
* @return Row|bool
|
||||
*/
|
||||
private function doFilterRecursiveDescend($labelParts, $dataTable)
|
||||
{
|
||||
// search for the first part of the tree search
|
||||
$labelPart = array_shift($labelParts);
|
||||
|
||||
$row = false;
|
||||
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 ResponseBuilder to behave correctly
|
||||
*
|
||||
* @param $request
|
||||
*/
|
||||
protected function manipulateSubtableRequest($request)
|
||||
{
|
||||
unset($request['label']);
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ResponseBuilder the $label is unsanitized
|
||||
* via Common::unsanitizeLabelParameter.
|
||||
*
|
||||
* @param string $label
|
||||
* @return array
|
||||
*/
|
||||
private function getLabelVariations($label)
|
||||
{
|
||||
static $pageTitleReports = array('getPageTitles', 'getEntryPageTitles', 'getExitPageTitles');
|
||||
|
||||
$variations = array();
|
||||
$label = urldecode($label);
|
||||
$label = trim($label);
|
||||
|
||||
$sanitizedLabel = Common::sanitizeInputValue($label);
|
||||
$variations[] = $sanitizedLabel;
|
||||
|
||||
if ($this->apiModule == 'Actions'
|
||||
&& in_array($this->apiMethod, $pageTitleReports)
|
||||
) {
|
||||
// special case: the Actions.getPageTitles report prefixes some labels with a blank.
|
||||
// the blank might be passed by the user but is removed in Request::getRequestArrayFromString.
|
||||
$variations[] = ' ' . $sanitizedLabel;
|
||||
$variations[] = ' ' . $label;
|
||||
}
|
||||
$variations[] = $label;
|
||||
|
||||
return $variations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter a DataTable instance. See @filter for more info.
|
||||
*
|
||||
* @param DataTable\Simple|DataTable\Map $dataTable
|
||||
* @return mixed
|
||||
*/
|
||||
protected function manipulateDataTable($dataTable)
|
||||
{
|
||||
$result = $dataTable->getEmptyClone();
|
||||
foreach ($this->labels as $labelIndex => $label) {
|
||||
$row = null;
|
||||
foreach ($this->getLabelVariations($label) as $labelVariation) {
|
||||
$labelVariation = explode(self::SEPARATOR_RECURSIVE_LABEL, $labelVariation);
|
||||
|
||||
$row = $this->doFilterRecursiveDescend($labelVariation, $dataTable);
|
||||
if ($row) {
|
||||
if ($this->addLabelIndex) {
|
||||
$row->setMetadata(self::FLAG_IS_ROW_EVOLUTION, $labelIndex);
|
||||
}
|
||||
$result->addRow($row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\API\DataTableManipulator;
|
||||
|
||||
use Piwik\API\DataTableManipulator;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Row;
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
use Piwik\Period\Range;
|
||||
use Piwik\Period;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Metrics;
|
||||
use Piwik\Plugins\API\API;
|
||||
|
||||
/**
|
||||
* This class is responsible for setting the metadata property 'totals' on each dataTable if the report
|
||||
* has a dimension. 'Totals' means it tries to calculate the total report value for each metric. For each
|
||||
* the total number of visits, actions, ... for a given report / dataTable.
|
||||
*/
|
||||
class ReportTotalsCalculator extends DataTableManipulator
|
||||
{
|
||||
/**
|
||||
* Cached report metadata array.
|
||||
* @var array
|
||||
*/
|
||||
private static $reportMetadata = array();
|
||||
|
||||
/**
|
||||
* @param DataTable $table
|
||||
* @return \Piwik\DataTable|\Piwik\DataTable\Map
|
||||
*/
|
||||
public function calculate($table)
|
||||
{
|
||||
// apiModule and/or apiMethod is empty for instance in case when flat=1 is called. Basically whenever a
|
||||
// datamanipulator calls the API and wants the dataTable in return, see callApiAndReturnDataTable().
|
||||
// it is also not set for some settings API request etc.
|
||||
if (empty($this->apiModule) || empty($this->apiMethod)) {
|
||||
return $table;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->manipulate($table);
|
||||
} catch(\Exception $e) {
|
||||
// eg. requests with idSubtable may trigger this exception
|
||||
// (where idSubtable was removed in
|
||||
// ?module=API&method=Events.getNameFromCategoryId&idSubtable=1&secondaryDimension=eventName&format=XML&idSite=1&period=day&date=yesterday&flat=0
|
||||
return $table;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds ratio metrics if possible.
|
||||
*
|
||||
* @param DataTable $dataTable
|
||||
* @return DataTable
|
||||
*/
|
||||
protected function manipulateDataTable($dataTable)
|
||||
{
|
||||
$report = $this->findCurrentReport();
|
||||
|
||||
if (!empty($report) && empty($report['dimension'])) {
|
||||
// we currently do not calculate the total value for reports having no dimension
|
||||
return $dataTable;
|
||||
}
|
||||
|
||||
// Array [readableMetric] => [summed value]
|
||||
$totalValues = array();
|
||||
|
||||
$firstLevelTable = $this->makeSureToWorkOnFirstLevelDataTable($dataTable);
|
||||
$metricsToCalculate = Metrics::getMetricIdsToProcessReportTotal();
|
||||
|
||||
foreach ($metricsToCalculate as $metricId) {
|
||||
if (!$this->hasDataTableMetric($firstLevelTable, $metricId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($firstLevelTable->getRows() as $row) {
|
||||
$totalValues = $this->sumColumnValueToTotal($row, $metricId, $totalValues);
|
||||
}
|
||||
}
|
||||
|
||||
$dataTable->setMetadata('totals', $totalValues);
|
||||
|
||||
return $dataTable;
|
||||
}
|
||||
|
||||
private function hasDataTableMetric(DataTable $dataTable, $metricId)
|
||||
{
|
||||
$firstRow = $dataTable->getFirstRow();
|
||||
|
||||
if (empty($firstRow)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (false === $this->getColumn($firstRow, $metricId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns column from a given row.
|
||||
* Will work with 2 types of datatable
|
||||
* - raw datatables coming from the archive DB, which columns are int indexed
|
||||
* - datatables processed resulting of API calls, which columns have human readable english names
|
||||
*
|
||||
* @param Row|array $row
|
||||
* @param int $columnIdRaw see consts in Metrics::
|
||||
* @return mixed Value of column, false if not found
|
||||
*/
|
||||
private function getColumn($row, $columnIdRaw)
|
||||
{
|
||||
$columnIdReadable = Metrics::getReadableColumnName($columnIdRaw);
|
||||
|
||||
if ($row instanceof Row) {
|
||||
$raw = $row->getColumn($columnIdRaw);
|
||||
if ($raw !== false) {
|
||||
return $raw;
|
||||
}
|
||||
|
||||
return $row->getColumn($columnIdReadable);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function makeSureToWorkOnFirstLevelDataTable($table)
|
||||
{
|
||||
if (!array_key_exists('idSubtable', $this->request)) {
|
||||
return $table;
|
||||
}
|
||||
|
||||
$firstLevelReport = $this->findFirstLevelReport();
|
||||
|
||||
if (empty($firstLevelReport)) {
|
||||
// it is not a subtable report
|
||||
$module = $this->apiModule;
|
||||
$action = $this->apiMethod;
|
||||
} else {
|
||||
$module = $firstLevelReport['module'];
|
||||
$action = $firstLevelReport['action'];
|
||||
}
|
||||
|
||||
$request = $this->request;
|
||||
|
||||
/** @var \Piwik\Period $period */
|
||||
$period = $table->getMetadata('period');
|
||||
|
||||
if (!empty($period)) {
|
||||
// we want a dataTable, not a dataTable\map
|
||||
if (Period::isMultiplePeriod($request['date'], $request['period']) || 'range' == $period->getLabel()) {
|
||||
$request['date'] = $period->getRangeString();
|
||||
$request['period'] = 'range';
|
||||
} else {
|
||||
$request['date'] = $period->getDateStart()->toString();
|
||||
$request['period'] = $period->getLabel();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->callApiAndReturnDataTable($module, $action, $request);
|
||||
}
|
||||
|
||||
private function sumColumnValueToTotal(Row $row, $metricId, $totalValues)
|
||||
{
|
||||
$value = $this->getColumn($row, $metricId);
|
||||
|
||||
if (false === $value) {
|
||||
|
||||
return $totalValues;
|
||||
}
|
||||
|
||||
$metricName = Metrics::getReadableColumnName($metricId);
|
||||
|
||||
if (array_key_exists($metricName, $totalValues)) {
|
||||
$totalValues[$metricName] += $value;
|
||||
} else {
|
||||
$totalValues[$metricName] = $value;
|
||||
}
|
||||
|
||||
return $totalValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure to get all rows of the first level table.
|
||||
*
|
||||
* @param array $request
|
||||
*/
|
||||
protected function manipulateSubtableRequest($request)
|
||||
{
|
||||
$request['totals'] = 0;
|
||||
$request['expanded'] = 0;
|
||||
$request['filter_limit'] = -1;
|
||||
$request['filter_offset'] = 0;
|
||||
|
||||
$parametersToRemove = array('flat');
|
||||
|
||||
if (!array_key_exists('idSubtable', $this->request)) {
|
||||
$parametersToRemove[] = 'idSubtable';
|
||||
}
|
||||
|
||||
foreach ($parametersToRemove as $param) {
|
||||
if (array_key_exists($param, $request)) {
|
||||
unset($request[$param]);
|
||||
}
|
||||
}
|
||||
return $request;
|
||||
}
|
||||
|
||||
private function getReportMetadata()
|
||||
{
|
||||
if (!empty(static::$reportMetadata)) {
|
||||
return static::$reportMetadata;
|
||||
}
|
||||
|
||||
static::$reportMetadata = API::getInstance()->getReportMetadata();
|
||||
|
||||
return static::$reportMetadata;
|
||||
}
|
||||
|
||||
private function findCurrentReport()
|
||||
{
|
||||
foreach ($this->getReportMetadata() as $report) {
|
||||
if ($this->apiMethod == $report['action']
|
||||
&& $this->apiModule == $report['module']) {
|
||||
|
||||
return $report;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function findFirstLevelReport()
|
||||
{
|
||||
foreach ($this->getReportMetadata() as $report) {
|
||||
if (!empty($report['actionToLoadSubTables'])
|
||||
&& $this->apiMethod == $report['actionToLoadSubTables']
|
||||
&& $this->apiModule == $report['module']
|
||||
) {
|
||||
|
||||
return $report;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
238
www/analytics/core/API/DocumentationGenerator.php
Normal file
238
www/analytics/core/API/DocumentationGenerator.php
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\API;
|
||||
|
||||
use Exception;
|
||||
use Piwik\Common;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Url;
|
||||
|
||||
class DocumentationGenerator
|
||||
{
|
||||
protected $modulesToHide = array('CoreAdminHome', 'DBStats');
|
||||
protected $countPluginsLoaded = 0;
|
||||
|
||||
/**
|
||||
* trigger loading all plugins with an API.php file in the Proxy
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$plugins = \Piwik\Plugin\Manager::getInstance()->getLoadedPluginsName();
|
||||
foreach ($plugins as $plugin) {
|
||||
try {
|
||||
$className = Request::getClassNameAPI($plugin);
|
||||
Proxy::getInstance()->registerClass($className);
|
||||
} 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' => Common::getRequestVar('idSite', 1, 'int'),
|
||||
'period' => Common::getRequestVar('period', 'day', 'string'),
|
||||
'date' => Common::getRequestVar('date', 'today', 'string')
|
||||
);
|
||||
|
||||
foreach (Proxy::getInstance()->getMetadata() as $class => $info) {
|
||||
$moduleName = Proxy::getInstance()->getModuleNameFromClassName($class);
|
||||
if (in_array($moduleName, $this->modulesToHide)) {
|
||||
continue;
|
||||
}
|
||||
$toc .= "<a href='#$moduleName'>$moduleName</a><br/>";
|
||||
$str .= "\n<a name='$moduleName' id='$moduleName'></a><h2>Module " . $moduleName . "</h2>";
|
||||
$str .= "<div class='apiDescription'> " . $info['__documentation'] . " </div>";
|
||||
foreach ($info as $methodName => $infoMethod) {
|
||||
if ($methodName == '__documentation') {
|
||||
continue;
|
||||
}
|
||||
$params = $this->getParametersString($class, $methodName);
|
||||
$str .= "\n <div class='apiMethod'>- <b>$moduleName.$methodName </b>" . $params . "";
|
||||
$str .= '<small>';
|
||||
|
||||
if ($outputExampleUrls) {
|
||||
// we prefix all URLs with $prefixUrls
|
||||
// used when we include this output in the Piwik official documentation for example
|
||||
$str .= "<span class=\"example\">";
|
||||
$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 <a target=_blank href='$exampleUrlRss1&format=rss$token_auth&translateColumnNames=1'>10 days</a>";
|
||||
}
|
||||
$exampleUrl = $prefixUrls . $exampleUrl;
|
||||
$str .= " [ Example in
|
||||
<a target=_blank href='$exampleUrl&format=xml$token_auth'>XML</a>,
|
||||
<a target=_blank href='$exampleUrl&format=JSON$token_auth'>Json</a>,
|
||||
<a target=_blank href='$exampleUrl&format=Tsv$token_auth&translateColumnNames=1'>Tsv (Excel)</a>
|
||||
$lastNUrls
|
||||
]";
|
||||
} else {
|
||||
$str .= " [ No example available ]";
|
||||
}
|
||||
$str .= "</span>";
|
||||
}
|
||||
$str .= '</small>';
|
||||
$str .= "</div>\n";
|
||||
}
|
||||
$str .= '<div style="margin:15px;"><a href="#topApiRef">↑ Back to top</a></div>';
|
||||
}
|
||||
|
||||
$str = "<h2 id='topApiRef' name='topApiRef'>Quick access to APIs</h2>
|
||||
$toc
|
||||
$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|bool 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/',
|
||||
'pageUrl' => 'http://forum.piwik.org/',
|
||||
'apiModule' => 'UserCountry',
|
||||
'apiAction' => 'getCountry',
|
||||
'lastMinutes' => '30',
|
||||
'abandonedCarts' => '0',
|
||||
'segmentName' => 'pageTitle',
|
||||
'ip' => '194.57.91.215',
|
||||
'idSites' => '1,2',
|
||||
'idAlert' => '1',
|
||||
// 'segmentName' => 'browserCode',
|
||||
);
|
||||
|
||||
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 = 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['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 = 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 NoDefaultValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return '?' . 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 = 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 NoDefaultValue)) {
|
||||
if (is_array($defaultValue)) {
|
||||
$str .= " = 'Array'";
|
||||
} else {
|
||||
$str .= " = '$defaultValue'";
|
||||
}
|
||||
}
|
||||
$asParameters[] = $str;
|
||||
}
|
||||
$sParameters = implode(", ", $asParameters);
|
||||
return "($sParameters)";
|
||||
}
|
||||
}
|
||||
514
www/analytics/core/API/Proxy.php
Normal file
514
www/analytics/core/API/Proxy.php
Normal file
|
|
@ -0,0 +1,514 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Piwik\API;
|
||||
|
||||
use Exception;
|
||||
use Piwik\Common;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Singleton;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
|
||||
/**
|
||||
* Proxy is a singleton that has the knowledge of every method available, their parameters
|
||||
* and default values.
|
||||
* Proxy receives all the API calls requests via call() and forwards them to the right
|
||||
* object, with the parameters in the right order.
|
||||
*
|
||||
* It will also log the performance of API calls (time spent, parameter values, etc.) if logger available
|
||||
*
|
||||
* @method static \Piwik\API\Proxy getInstance()
|
||||
*/
|
||||
class Proxy extends Singleton
|
||||
{
|
||||
// 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;
|
||||
|
||||
/**
|
||||
* protected constructor
|
||||
*/
|
||||
protected function __construct()
|
||||
{
|
||||
$this->noDefaultValue = new NoDefaultValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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/Referrers/API.php
|
||||
*
|
||||
* The method will introspect the methods, their parameters, etc.
|
||||
*
|
||||
* @param string $className ModuleName eg. "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, "<br>", $doc);
|
||||
|
||||
// boldify the first line only if there is more than one line, otherwise too much bold
|
||||
if (substr_count($doc, '<br>') > 1) {
|
||||
$firstLineBreak = strpos($doc, "<br>");
|
||||
$doc = "<div class='apiFirstLine'>" . substr($doc, 0, $firstLineBreak) . "</div>" . substr($doc, $firstLineBreak + strlen("<br>"));
|
||||
}
|
||||
$doc = preg_replace("/(@package)[a-z _A-Z]*/", "", $doc);
|
||||
$doc = preg_replace("/(@method).*/", "", $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. API)
|
||||
* @param string $methodName The method name
|
||||
* @param array $parametersRequest The parameters pairs (name=>value)
|
||||
*
|
||||
* @return mixed|null
|
||||
* @throws Exception|\Piwik\NoAccessException
|
||||
*/
|
||||
public function call($className, $methodName, $parametersRequest)
|
||||
{
|
||||
$returnedValue = null;
|
||||
|
||||
// Temporarily sets the Request array to this API call context
|
||||
$saveGET = $_GET;
|
||||
$saveQUERY_STRING = @$_SERVER['QUERY_STRING'];
|
||||
foreach ($parametersRequest as $param => $value) {
|
||||
$_GET[$param] = $value;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->registerClass($className);
|
||||
|
||||
// instanciate the object
|
||||
$object = $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);
|
||||
|
||||
// allow plugins to manipulate the value
|
||||
$pluginName = $this->getModuleNameFromClassName($className);
|
||||
|
||||
/**
|
||||
* Triggered before an API request is dispatched.
|
||||
*
|
||||
* This event can be used to modify the arguments passed to one or more API methods.
|
||||
*
|
||||
* **Example**
|
||||
*
|
||||
* Piwik::addAction('API.Request.dispatch', function (&$parameters, $pluginName, $methodName) {
|
||||
* if ($pluginName == 'Actions') {
|
||||
* if ($methodName == 'getPageUrls') {
|
||||
* // ... do something ...
|
||||
* } else {
|
||||
* // ... do something else ...
|
||||
* }
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* @param array &$finalParameters List of parameters that will be passed to the API method.
|
||||
* @param string $pluginName The name of the plugin the API method belongs to.
|
||||
* @param string $methodName The name of the API method that will be called.
|
||||
*/
|
||||
Piwik::postEvent('API.Request.dispatch', array(&$finalParameters, $pluginName, $methodName));
|
||||
|
||||
/**
|
||||
* Triggered before an API request is dispatched.
|
||||
*
|
||||
* This event exists for convenience and is triggered directly after the {@hook API.Request.dispatch}
|
||||
* event is triggered. It can be used to modify the arguments passed to a **single** API method.
|
||||
*
|
||||
* _Note: This is can be accomplished with the {@hook API.Request.dispatch} event as well, however
|
||||
* event handlers for that event will have to do more work._
|
||||
*
|
||||
* **Example**
|
||||
*
|
||||
* Piwik::addAction('API.Actions.getPageUrls', function (&$parameters) {
|
||||
* // force use of a single website. for some reason.
|
||||
* $parameters['idSite'] = 1;
|
||||
* });
|
||||
*
|
||||
* @param array &$finalParameters List of parameters that will be passed to the API method.
|
||||
*/
|
||||
Piwik::postEvent(sprintf('API.%s.%s', $pluginName, $methodName), array(&$finalParameters));
|
||||
|
||||
// call the method
|
||||
$returnedValue = call_user_func_array(array($object, $methodName), $finalParameters);
|
||||
|
||||
$endHookParams = array(
|
||||
&$returnedValue,
|
||||
array('className' => $className,
|
||||
'module' => $pluginName,
|
||||
'action' => $methodName,
|
||||
'parameters' => $finalParameters)
|
||||
);
|
||||
|
||||
/**
|
||||
* Triggered directly after an API request is dispatched.
|
||||
*
|
||||
* This event exists for convenience and is triggered immediately before the
|
||||
* {@hook API.Request.dispatch.end} event. It can be used to modify the output of a **single**
|
||||
* API method.
|
||||
*
|
||||
* _Note: This can be accomplished with the {@hook API.Request.dispatch.end} event as well,
|
||||
* however event handlers for that event will have to do more work._
|
||||
*
|
||||
* **Example**
|
||||
*
|
||||
* // append (0 hits) to the end of row labels whose row has 0 hits
|
||||
* Piwik::addAction('API.Actions.getPageUrls', function (&$returnValue, $info)) {
|
||||
* $returnValue->filter('ColumnCallbackReplace', 'label', function ($label, $hits) {
|
||||
* if ($hits === 0) {
|
||||
* return $label . " (0 hits)";
|
||||
* } else {
|
||||
* return $label;
|
||||
* }
|
||||
* }, null, array('nb_hits'));
|
||||
* }
|
||||
*
|
||||
* @param mixed &$returnedValue The API method's return value. Can be an object, such as a
|
||||
* {@link Piwik\DataTable DataTable} instance.
|
||||
* could be a {@link Piwik\DataTable DataTable}.
|
||||
* @param array $extraInfo An array holding information regarding the API request. Will
|
||||
* contain the following data:
|
||||
*
|
||||
* - **className**: The namespace-d class name of the API instance
|
||||
* that's being called.
|
||||
* - **module**: The name of the plugin the API request was
|
||||
* dispatched to.
|
||||
* - **action**: The name of the API method that was executed.
|
||||
* - **parameters**: The array of parameters passed to the API
|
||||
* method.
|
||||
*/
|
||||
Piwik::postEvent(sprintf('API.%s.%s.end', $pluginName, $methodName), $endHookParams);
|
||||
|
||||
/**
|
||||
* Triggered directly after an API request is dispatched.
|
||||
*
|
||||
* This event can be used to modify the output of any API method.
|
||||
*
|
||||
* **Example**
|
||||
*
|
||||
* // append (0 hits) to the end of row labels whose row has 0 hits for any report that has the 'nb_hits' metric
|
||||
* Piwik::addAction('API.Actions.getPageUrls', function (&$returnValue, $info)) {
|
||||
* // don't process non-DataTable reports and reports that don't have the nb_hits column
|
||||
* if (!($returnValue instanceof DataTableInterface)
|
||||
* || in_array('nb_hits', $returnValue->getColumns())
|
||||
* ) {
|
||||
* return;
|
||||
* }
|
||||
*
|
||||
* $returnValue->filter('ColumnCallbackReplace', 'label', function ($label, $hits) {
|
||||
* if ($hits === 0) {
|
||||
* return $label . " (0 hits)";
|
||||
* } else {
|
||||
* return $label;
|
||||
* }
|
||||
* }, null, array('nb_hits'));
|
||||
* }
|
||||
*
|
||||
* @param mixed &$returnedValue The API method's return value. Can be an object, such as a
|
||||
* {@link Piwik\DataTable DataTable} instance.
|
||||
* @param array $extraInfo An array holding information regarding the API request. Will
|
||||
* contain the following data:
|
||||
*
|
||||
* - **className**: The namespace-d class name of the API instance
|
||||
* that's being called.
|
||||
* - **module**: The name of the plugin the API request was
|
||||
* dispatched to.
|
||||
* - **action**: The name of the API method that was executed.
|
||||
* - **parameters**: The array of parameters passed to the API
|
||||
* method.
|
||||
*/
|
||||
Piwik::postEvent('API.Request.dispatch.end', $endHookParams);
|
||||
|
||||
// Restore the request
|
||||
$_GET = $saveGET;
|
||||
$_SERVER['QUERY_STRING'] = $saveQUERY_STRING;
|
||||
} 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\\Plugins\\moduleName\\API'
|
||||
*
|
||||
* @param string $className "API"
|
||||
* @return string "Referrers"
|
||||
*/
|
||||
public function getModuleNameFromClassName($className)
|
||||
{
|
||||
return str_replace(array('\\Piwik\\Plugins\\', '\\API'), '', $className);
|
||||
}
|
||||
|
||||
public function isExistingApiAction($pluginName, $apiAction)
|
||||
{
|
||||
$namespacedApiClassName = "\\Piwik\\Plugins\\$pluginName\\API";
|
||||
$api = $namespacedApiClassName::getInstance();
|
||||
|
||||
return method_exists($api, $apiAction);
|
||||
}
|
||||
|
||||
public function buildApiActionName($pluginName, $apiAction)
|
||||
{
|
||||
return sprintf("%s.%s", $pluginName, $apiAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 NoDefaultValue) {
|
||||
$requestValue = Common::getRequestVar($name, null, null, $parametersRequest);
|
||||
} else {
|
||||
try {
|
||||
|
||||
if ($name == 'segment' && !empty($parametersRequest['segment'])) {
|
||||
// segment parameter is an exception: we do not want to sanitize user input or it would break the segment encoding
|
||||
$requestValue = ($parametersRequest['segment']);
|
||||
} else {
|
||||
$requestValue = 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::translate('General_PleaseSpecifyValue', array($name)));
|
||||
}
|
||||
$finalParameters[] = $requestValue;
|
||||
}
|
||||
return $finalParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Includes the class API by looking up plugins/UserSettings/API.php
|
||||
*
|
||||
* @param string $fileName api class name eg. "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::translate('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("$className that provide an API must be Singleton and have a 'static public function getInstance()' method.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To differentiate between "no value" and default value of null
|
||||
*
|
||||
*/
|
||||
class NoDefaultValue
|
||||
{
|
||||
}
|
||||
398
www/analytics/core/API/Request.php
Normal file
398
www/analytics/core/API/Request.php
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\API;
|
||||
|
||||
use Exception;
|
||||
use Piwik\Access;
|
||||
use Piwik\Common;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\PluginDeactivatedException;
|
||||
use Piwik\SettingsServer;
|
||||
use Piwik\Url;
|
||||
use Piwik\UrlHelper;
|
||||
|
||||
/**
|
||||
* Dispatches API requests to the appropriate API method.
|
||||
*
|
||||
* The Request class is used throughout Piwik to call API methods. The difference
|
||||
* between using Request and calling API methods directly is that Request
|
||||
* will do more after calling the API including: applying generic filters, applying queued filters,
|
||||
* and handling the **flat** and **label** query parameters.
|
||||
*
|
||||
* Additionally, the Request class will **forward current query parameters** to the request
|
||||
* which is more convenient than calling {@link Piwik\Common::getRequestVar()} many times over.
|
||||
*
|
||||
* In most cases, using a Request object to query the API is the correct approach.
|
||||
*
|
||||
* ### Post-processing
|
||||
*
|
||||
* The return value of API methods undergo some extra processing before being returned by Request.
|
||||
* To learn more about what happens to API results, read [this](/guides/piwiks-web-api#extra-report-processing).
|
||||
*
|
||||
* ### Output Formats
|
||||
*
|
||||
* The value returned by Request will be serialized to a certain format before being returned.
|
||||
* To see the list of supported output formats, read [this](/guides/piwiks-web-api#output-formats).
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* **Basic Usage**
|
||||
*
|
||||
* $request = new Request('method=UserSettings.getWideScreen&idSite=1&date=yesterday&period=week'
|
||||
* . '&format=xml&filter_limit=5&filter_offset=0')
|
||||
* $result = $request->process();
|
||||
* echo $result;
|
||||
*
|
||||
* **Getting a unrendered DataTable**
|
||||
*
|
||||
* // use the convenience method 'processRequest'
|
||||
* $dataTable = Request::processRequest('UserSettings.getWideScreen', array(
|
||||
* 'idSite' => 1,
|
||||
* 'date' => 'yesterday',
|
||||
* 'period' => 'week',
|
||||
* 'filter_limit' => 5,
|
||||
* 'filter_offset' => 0
|
||||
*
|
||||
* 'format' => 'original', // this is the important bit
|
||||
* ));
|
||||
* echo "This DataTable has " . $dataTable->getRowsCount() . " rows.";
|
||||
*
|
||||
* @see http://piwik.org/docs/analytics-api
|
||||
* @api
|
||||
*/
|
||||
class Request
|
||||
{
|
||||
protected $request = null;
|
||||
|
||||
/**
|
||||
* Converts the supplied request string into an array of query paramater name/value
|
||||
* mappings. The current query parameters (everything in `$_GET` and `$_POST`) are
|
||||
* forwarded to request array before it is returned.
|
||||
*
|
||||
* @param string|array $request The base request string or array, eg,
|
||||
* `'module=UserSettings&action=getWidescreen'`.
|
||||
* @return array
|
||||
*/
|
||||
static public function getRequestArrayFromString($request)
|
||||
{
|
||||
$defaultRequest = $_GET + $_POST;
|
||||
|
||||
$requestRaw = self::getRequestParametersGET();
|
||||
if (!empty($requestRaw['segment'])) {
|
||||
$defaultRequest['segment'] = $requestRaw['segment'];
|
||||
}
|
||||
|
||||
$requestArray = $defaultRequest;
|
||||
|
||||
if (!is_null($request)) {
|
||||
if (is_array($request)) {
|
||||
$url = array();
|
||||
foreach ($request as $key => $value) {
|
||||
$url[] = $key . "=" . $value;
|
||||
}
|
||||
$request = implode("&", $url);
|
||||
}
|
||||
|
||||
$request = trim($request);
|
||||
$request = str_replace(array("\n", "\t"), '', $request);
|
||||
|
||||
$requestParsed = UrlHelper::getArrayFromQueryString($request);
|
||||
$requestArray = $requestParsed + $defaultRequest;
|
||||
}
|
||||
|
||||
foreach ($requestArray as &$element) {
|
||||
if (!is_array($element)) {
|
||||
$element = trim($element);
|
||||
}
|
||||
}
|
||||
return $requestArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string|array $request Query string that defines the API call (must at least contain a **method** parameter),
|
||||
* eg, `'method=UserSettings.getWideScreen&idSite=1&date=yesterday&period=week&format=xml'`
|
||||
* If a request is not provided, then we use the values in the `$_GET` and `$_POST`
|
||||
* superglobals.
|
||||
*/
|
||||
public function __construct($request = null)
|
||||
{
|
||||
$this->request = self::getRequestArrayFromString($request);
|
||||
$this->sanitizeRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* For backward compatibility: Piwik API still works if module=Referers,
|
||||
* we rewrite to correct renamed plugin: Referrers
|
||||
*
|
||||
* @param $module
|
||||
* @return string
|
||||
* @ignore
|
||||
*/
|
||||
public static function renameModule($module)
|
||||
{
|
||||
$moduleToRedirect = array(
|
||||
'Referers' => 'Referrers',
|
||||
'PDFReports' => 'ScheduledReports',
|
||||
);
|
||||
if (isset($moduleToRedirect[$module])) {
|
||||
return $moduleToRedirect[$module];
|
||||
}
|
||||
return $module;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure that the request contains no logical errors
|
||||
*/
|
||||
private function sanitizeRequest()
|
||||
{
|
||||
// The label filter does not work with expanded=1 because the data table IDs have a different meaning
|
||||
// depending on whether the table has been loaded yet. expanded=1 causes all tables to be loaded, which
|
||||
// is why the label filter can't descend when a recursive label has been requested.
|
||||
// To fix this, we remove the expanded parameter if a label parameter is set.
|
||||
if (isset($this->request['label']) && !empty($this->request['label'])
|
||||
&& isset($this->request['expanded']) && $this->request['expanded']
|
||||
) {
|
||||
unset($this->request['expanded']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the API request to the appropriate API method and returns the result
|
||||
* after post-processing.
|
||||
*
|
||||
* Post-processing includes:
|
||||
*
|
||||
* - flattening if **flat** is 0
|
||||
* - running generic filters unless **disable_generic_filters** is set to 1
|
||||
* - URL decoding label column values
|
||||
* - running queued filters unless **disable_queued_filters** is set to 1
|
||||
* - removing columns based on the values of the **hideColumns** and **showColumns** query parameters
|
||||
* - filtering rows if the **label** query parameter is set
|
||||
* - converting the result to the appropriate format (ie, XML, JSON, etc.)
|
||||
*
|
||||
* If `'original'` is supplied for the output format, the result is returned as a PHP
|
||||
* object.
|
||||
*
|
||||
* @throws PluginDeactivatedException if the module plugin is not activated.
|
||||
* @throws Exception if the requested API method cannot be called, if required parameters for the
|
||||
* API method are missing or if the API method throws an exception and the **format**
|
||||
* query parameter is **original**.
|
||||
* @return DataTable|Map|string The data resulting from the API call.
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
// read the format requested for the output data
|
||||
$outputFormat = strtolower(Common::getRequestVar('format', 'xml', 'string', $this->request));
|
||||
|
||||
// create the response
|
||||
$response = new ResponseBuilder($outputFormat, $this->request);
|
||||
|
||||
try {
|
||||
// read parameters
|
||||
$moduleMethod = Common::getRequestVar('method', null, 'string', $this->request);
|
||||
|
||||
list($module, $method) = $this->extractModuleAndMethod($moduleMethod);
|
||||
|
||||
$module = $this->renameModule($module);
|
||||
|
||||
if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated($module)) {
|
||||
throw new PluginDeactivatedException($module);
|
||||
}
|
||||
$apiClassName = $this->getClassNameAPI($module);
|
||||
|
||||
self::reloadAuthUsingTokenAuth($this->request);
|
||||
|
||||
// call the method
|
||||
$returnedValue = Proxy::getInstance()->call($apiClassName, $method, $this->request);
|
||||
|
||||
$toReturn = $response->getResponse($returnedValue, $module, $method);
|
||||
} catch (Exception $e) {
|
||||
$toReturn = $response->getResponseException($e);
|
||||
}
|
||||
return $toReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of a plugin's API class by plugin name.
|
||||
*
|
||||
* @param string $plugin The plugin name, eg, `'Referrers'`.
|
||||
* @return string The fully qualified API class name, eg, `'\Piwik\Plugins\Referrers\API'`.
|
||||
*/
|
||||
static public function getClassNameAPI($plugin)
|
||||
{
|
||||
return sprintf('\Piwik\Plugins\%s\API', $plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @ignore
|
||||
*/
|
||||
static public function reloadAuthUsingTokenAuth($request = null)
|
||||
{
|
||||
// if a token_auth is specified in the API request, we load the right permissions
|
||||
$token_auth = Common::getRequestVar('token_auth', '', 'string', $request);
|
||||
if ($token_auth) {
|
||||
|
||||
/**
|
||||
* Triggered when authenticating an API request, but only if the **token_auth**
|
||||
* query parameter is found in the request.
|
||||
*
|
||||
* Plugins that provide authentication capabilities should subscribe to this event
|
||||
* and make sure the global authentication object (the object returned by `Registry::get('auth')`)
|
||||
* is setup to use `$token_auth` when its `authenticate()` method is executed.
|
||||
*
|
||||
* @param string $token_auth The value of the **token_auth** query parameter.
|
||||
*/
|
||||
Piwik::postEvent('API.Request.authenticate', array($token_auth));
|
||||
Access::getInstance()->reloadAccess();
|
||||
SettingsServer::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 that processes an API request in one line 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`.
|
||||
* @return mixed The result of the API request. See {@link process()}.
|
||||
*/
|
||||
public static function processRequest($method, $paramOverride = array())
|
||||
{
|
||||
$params = array();
|
||||
$params['format'] = 'original';
|
||||
$params['module'] = 'API';
|
||||
$params['method'] = $method;
|
||||
$params = $paramOverride + $params;
|
||||
|
||||
// process request
|
||||
$request = new Request($params);
|
||||
return $request->process();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the original request parameters in the current query string as an array mapping
|
||||
* query parameter names with values. The result of this function will not be affected
|
||||
* by any modifications to `$_GET` and will not include parameters in `$_POST`.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getRequestParametersGET()
|
||||
{
|
||||
if (empty($_SERVER['QUERY_STRING'])) {
|
||||
return array();
|
||||
}
|
||||
$GET = UrlHelper::getArrayFromQueryString($_SERVER['QUERY_STRING']);
|
||||
return $GET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL for the current requested report w/o any filter parameters.
|
||||
*
|
||||
* @param string $module The API module.
|
||||
* @param string $action The API action.
|
||||
* @param array $queryParams Query parameter overrides.
|
||||
* @return string
|
||||
*/
|
||||
public static function getBaseReportUrl($module, $action, $queryParams = array())
|
||||
{
|
||||
$params = array_merge($queryParams, array('module' => $module, 'action' => $action));
|
||||
return Request::getCurrentUrlWithoutGenericFilters($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current URL without generic filter query parameters.
|
||||
*
|
||||
* @param array $params Query parameter values to override in the new URL.
|
||||
* @return string
|
||||
*/
|
||||
public static function getCurrentUrlWithoutGenericFilters($params)
|
||||
{
|
||||
// unset all filter query params so the related report will show up in its default state,
|
||||
// unless the filter param was in $queryParams
|
||||
$genericFiltersInfo = DataTableGenericFilter::getGenericFiltersInformation();
|
||||
foreach ($genericFiltersInfo as $filter) {
|
||||
foreach ($filter as $queryParamName => $queryParamInfo) {
|
||||
if (!isset($params[$queryParamName])) {
|
||||
$params[$queryParamName] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Url::getCurrentQueryStringWithParametersModified($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the DataTable result will have to be expanded for the
|
||||
* current request before rendering.
|
||||
*
|
||||
* @return bool
|
||||
* @ignore
|
||||
*/
|
||||
public static function shouldLoadExpanded()
|
||||
{
|
||||
// if filter_column_recursive & filter_pattern_recursive are supplied, and flat isn't supplied
|
||||
// we have to load all the child subtables.
|
||||
return Common::getRequestVar('filter_column_recursive', false) !== false
|
||||
&& Common::getRequestVar('filter_pattern_recursive', false) !== false
|
||||
&& !self::shouldLoadFlatten();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public static function shouldLoadFlatten()
|
||||
{
|
||||
return Common::getRequestVar('flat', false) == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the segment query parameter from the original request, without modifications.
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
static public function getRawSegmentFromRequest()
|
||||
{
|
||||
// we need the URL encoded segment parameter, we fetch it from _SERVER['QUERY_STRING'] instead of default URL decoded _GET
|
||||
$segmentRaw = false;
|
||||
$segment = Common::getRequestVar('segment', '', 'string');
|
||||
if (!empty($segment)) {
|
||||
$request = Request::getRequestParametersGET();
|
||||
if (!empty($request['segment'])) {
|
||||
$segmentRaw = $request['segment'];
|
||||
}
|
||||
}
|
||||
return $segmentRaw;
|
||||
}
|
||||
}
|
||||
478
www/analytics/core/API/ResponseBuilder.php
Normal file
478
www/analytics/core/API/ResponseBuilder.php
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\API;
|
||||
|
||||
use Exception;
|
||||
use Piwik\API\DataTableManipulator\Flattener;
|
||||
use Piwik\API\DataTableManipulator\LabelFilter;
|
||||
use Piwik\API\DataTableManipulator\ReportTotalsCalculator;
|
||||
use Piwik\Common;
|
||||
use Piwik\DataTable\Renderer\Json;
|
||||
use Piwik\DataTable\Renderer;
|
||||
use Piwik\DataTable\Simple;
|
||||
use Piwik\DataTable;
|
||||
|
||||
/**
|
||||
*/
|
||||
class ResponseBuilder
|
||||
{
|
||||
private $request = null;
|
||||
private $outputFormat = null;
|
||||
|
||||
private $apiModule = false;
|
||||
private $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 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 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;
|
||||
|
||||
if($this->outputFormat == 'original') {
|
||||
@header('Content-Type: text/plain; charset=utf-8');
|
||||
}
|
||||
return $this->renderValue($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 = Renderer::factory($format);
|
||||
} catch (Exception $exceptionRenderer) {
|
||||
return "Error: " . $e->getMessage() . " and: " . $exceptionRenderer->getMessage();
|
||||
}
|
||||
|
||||
$e = $this->decorateExceptionWithDebugTrace($e);
|
||||
|
||||
$renderer->setException($e);
|
||||
|
||||
if ($format == 'php') {
|
||||
$renderer->setSerialize($this->caseRendererPHPSerialize());
|
||||
}
|
||||
|
||||
return $renderer->renderException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @return string
|
||||
*/
|
||||
protected function renderValue($value)
|
||||
{
|
||||
// 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 DataTable
|
||||
|| $value instanceof DataTable\Map
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Exception $e
|
||||
* @return Exception
|
||||
*/
|
||||
protected function decorateExceptionWithDebugTrace(Exception $e)
|
||||
{
|
||||
// If we are in tests, show full backtrace
|
||||
if (defined('PIWIK_PATH_TEST_TO_ROOT')) {
|
||||
if (\Piwik_ShouldPrintBackTraceWithMessage()) {
|
||||
$message = $e->getMessage() . " in \n " . $e->getFile() . ":" . $e->getLine() . " \n " . $e->getTraceAsString();
|
||||
} else {
|
||||
$message = $e->getMessage() . "\n \n --> To temporarily debug this error further, set const PIWIK_PRINT_ERROR_BACKTRACE=true; in index.php";
|
||||
}
|
||||
return new Exception($message);
|
||||
}
|
||||
return $e;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = Common::getRequestVar('serialize', $defaultSerializeValue, 'int', $this->request);
|
||||
if ($serialize) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the specified renderer to the DataTable
|
||||
*
|
||||
* @param 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 = Common::getRequestVar('method', '', 'string', $this->request);
|
||||
|
||||
$renderer = Renderer::factory($format);
|
||||
$renderer->setTable($dataTable);
|
||||
$renderer->setRenderSubTables(Common::getRequestVar('expanded', false, 'int', $this->request));
|
||||
$renderer->setHideIdSubDatableFromResponse(Common::getRequestVar('hideIdSubDatable', false, 'int', $this->request));
|
||||
|
||||
if ($format == 'php') {
|
||||
$renderer->setSerialize($this->caseRendererPHPSerialize());
|
||||
$renderer->setPrettyDisplay(Common::getRequestVar('prettyDisplay', false, 'int', $this->request));
|
||||
} else if ($format == 'html') {
|
||||
$renderer->setTableId($this->request['method']);
|
||||
} else if ($format == 'csv' || $format == 'tsv') {
|
||||
$renderer->setConvertToUnicode(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(Common::getRequestVar('idSite', false, 'int', $this->request));
|
||||
$renderer->setTranslateColumnNames(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 =
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" .
|
||||
"<result>\n" .
|
||||
"\t<success message=\"" . $message . "\" />\n" .
|
||||
"</result>";
|
||||
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 Simple();
|
||||
$dataTable->addRowsFromArray(array($scalar));
|
||||
return $this->getRenderedDataTable($dataTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the given data table
|
||||
*
|
||||
* @param DataTable $datatable
|
||||
* @return string
|
||||
*/
|
||||
protected function handleDataTable($datatable)
|
||||
{
|
||||
// if requested, flatten nested tables
|
||||
if (Common::getRequestVar('flat', '0', 'string', $this->request) == '1') {
|
||||
$flattener = new Flattener($this->apiModule, $this->apiMethod, $this->request);
|
||||
if (Common::getRequestVar('include_aggregate_rows', '0', 'string', $this->request) == '1') {
|
||||
$flattener->includeAggregateRows();
|
||||
}
|
||||
$datatable = $flattener->flatten($datatable);
|
||||
}
|
||||
|
||||
if (1 == Common::getRequestVar('totals', '1', 'integer', $this->request)) {
|
||||
$genericFilter = new ReportTotalsCalculator($this->apiModule, $this->apiMethod, $this->request);
|
||||
$datatable = $genericFilter->calculate($datatable);
|
||||
}
|
||||
|
||||
// if the flag disable_generic_filters is defined we skip the generic filters
|
||||
if (0 == Common::getRequestVar('disable_generic_filters', '0', 'string', $this->request)) {
|
||||
$genericFilter = new 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 (Common::getRequestVar('disable_queued_filters', 0, 'int', $this->request) == 0) {
|
||||
$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 = Common::getRequestVar('hideColumns', '', 'string', $this->request);
|
||||
$showColumns = 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->getLabelFromRequest($this->request);
|
||||
if (!empty($label)) {
|
||||
$addLabelIndex = Common::getRequestVar('labelFilterAddLabelIndex', 0, 'int', $this->request) == 1;
|
||||
|
||||
$filter = new LabelFilter($this->apiModule, $this->apiMethod, $this->request);
|
||||
$datatable = $filter->filter($label, $datatable, $addLabelIndex);
|
||||
}
|
||||
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 DataTable|Set elements in the first dimension only, following case does not work:
|
||||
* array(
|
||||
* array(
|
||||
* DataTable,
|
||||
* 2 => array(
|
||||
* 1,
|
||||
* 2
|
||||
* ),
|
||||
* ),
|
||||
* );
|
||||
*
|
||||
* @param array $array can contain scalar, arrays, DataTable and Set
|
||||
* @return string
|
||||
*/
|
||||
public static function convertMultiDimensionalArrayToJson($array)
|
||||
{
|
||||
$jsonRenderer = new Json();
|
||||
$jsonRenderer->setTable($array);
|
||||
$renderedReport = $jsonRenderer->render();
|
||||
return $renderedReport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for the label query parameter which can be either a string
|
||||
* (ie, label=...) or array (ie, label[]=...).
|
||||
*
|
||||
* @param array $request
|
||||
* @return array
|
||||
*/
|
||||
static public function getLabelFromRequest($request)
|
||||
{
|
||||
$label = Common::getRequestVar('label', array(), 'array', $request);
|
||||
if (empty($label)) {
|
||||
$label = Common::getRequestVar('label', '', 'string', $request);
|
||||
if (!empty($label)) {
|
||||
$label = array($label);
|
||||
}
|
||||
}
|
||||
|
||||
$label = self::unsanitizeLabelParameter($label);
|
||||
return $label;
|
||||
}
|
||||
|
||||
static public function unsanitizeLabelParameter($label)
|
||||
{
|
||||
// this is needed because Proxy uses Common::getRequestVar which in turn
|
||||
// uses Common::sanitizeInputValue. This causes the > that separates recursive labels
|
||||
// to become > and we need to undo that here.
|
||||
$label = Common::unsanitizeInputValues($label);
|
||||
return $label;
|
||||
}
|
||||
}
|
||||
418
www/analytics/core/Access.php
Normal file
418
www/analytics/core/Access.php
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik;
|
||||
|
||||
use Piwik\Db;
|
||||
use Piwik\Plugins\UsersManager\API as APIUsersManager;
|
||||
|
||||
/**
|
||||
* Singleton that manages user access to Piwik resources.
|
||||
*
|
||||
* To check whether a user has access to a resource, use one of the {@link Piwik Piwik::checkUser...}
|
||||
* methods.
|
||||
*
|
||||
* In Piwik there are four different access levels:
|
||||
*
|
||||
* - **no access**: Users with this access level cannot view the resource.
|
||||
* - **view access**: Users with this access level can view the resource, but cannot modify it.
|
||||
* - **admin access**: Users with this access level can view and modify the resource.
|
||||
* - **Super User access**: Only the Super User has this access level. It means the user can do
|
||||
* whatever he/she wants.
|
||||
*
|
||||
* Super user access is required to set some configuration options.
|
||||
* All other options are specific to the user or to a website.
|
||||
*
|
||||
* Access is granted per website. Uses with access for a website can view all
|
||||
* data associated with that website.
|
||||
*
|
||||
*/
|
||||
class Access
|
||||
{
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Gets the singleton instance. Creates it if necessary.
|
||||
*/
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance == null) {
|
||||
self::$instance = new self;
|
||||
|
||||
Piwik::postEvent('Access.createAccessSingleton', array(&self::$instance));
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the singleton instance. For testing purposes.
|
||||
*/
|
||||
public static function setSingletonInstance($instance)
|
||||
{
|
||||
self::$instance = $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 hasSuperUserAccess()
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasSuperUserAccess = false;
|
||||
|
||||
/**
|
||||
* List of available permissions in Piwik
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $availableAccess = array('noaccess', 'view', 'admin', 'superuser');
|
||||
|
||||
/**
|
||||
* Authentification object (see Auth)
|
||||
*
|
||||
* @var 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
|
||||
*/
|
||||
public static function getListAccess()
|
||||
{
|
||||
return self::$availableAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public 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|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(Auth $auth = null)
|
||||
{
|
||||
if (!is_null($auth)) {
|
||||
$this->auth = $auth;
|
||||
}
|
||||
|
||||
// if the Auth wasn't set, we may be in the special case of setSuperUser(), otherwise we fail
|
||||
if (is_null($this->auth)) {
|
||||
if ($this->hasSuperUserAccess()) {
|
||||
return $this->reloadAccessSuperUser();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// access = array ( idsite => accessIdSite, idsite2 => accessIdSite2)
|
||||
$result = $this->auth->authenticate();
|
||||
|
||||
if (!$result->wasAuthenticationSuccessful()) {
|
||||
return false;
|
||||
}
|
||||
$this->login = $result->getIdentity();
|
||||
$this->token_auth = $result->getTokenAuth();
|
||||
|
||||
// case the superUser is logged in
|
||||
if ($result->hasSuperUserAccess()) {
|
||||
return $this->reloadAccessSuperUser();
|
||||
}
|
||||
// in case multiple calls to API using different tokens, we ensure we reset it as not SU
|
||||
$this->setSuperUserAccess(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 = $this->getRawSitesWithSomeViewAccess($this->login);
|
||||
foreach ($accessRaw as $access) {
|
||||
$this->idsitesByAccess[$access['access']][] = $access['idsite'];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getRawSitesWithSomeViewAccess($login)
|
||||
{
|
||||
return Db::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
|
||||
*/
|
||||
public static function getSqlAccessSite($select)
|
||||
{
|
||||
return "SELECT " . $select . "
|
||||
FROM " . Common::prefixTable('access') . " as t1
|
||||
JOIN " . Common::prefixTable('site') . " as t2 USING (idsite) " .
|
||||
" WHERE login = ?";
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload Super User access
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function reloadAccessSuperUser()
|
||||
{
|
||||
$this->hasSuperUserAccess = true;
|
||||
|
||||
try {
|
||||
$allSitesId = Plugins\SitesManager\API::getInstance()->getAllSitesId();
|
||||
} catch (\Exception $e) {
|
||||
$allSitesId = array();
|
||||
}
|
||||
$this->idsitesByAccess['superuser'] = $allSitesId;
|
||||
|
||||
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 setSuperUserAccess($bool = true)
|
||||
{
|
||||
if ($bool) {
|
||||
$this->reloadAccessSuperUser();
|
||||
} else {
|
||||
$this->hasSuperUserAccess = false;
|
||||
$this->idsitesByAccess['superuser'] = array();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current user is logged in as the Super User
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSuperUserAccess()
|
||||
{
|
||||
return $this->hasSuperUserAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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\NoAccessException
|
||||
*/
|
||||
public function checkUserHasSuperUserAccess()
|
||||
{
|
||||
if (!$this->hasSuperUserAccess()) {
|
||||
throw new NoAccessException(Piwik::translate('General_ExceptionPrivilege', array("'superuser'")));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user doesn't have an ADMIN access for at least one website, throws an exception
|
||||
*
|
||||
* @throws \Piwik\NoAccessException
|
||||
*/
|
||||
public function checkUserHasSomeAdminAccess()
|
||||
{
|
||||
if ($this->hasSuperUserAccess()) {
|
||||
return;
|
||||
}
|
||||
$idSitesAccessible = $this->getSitesIdWithAdminAccess();
|
||||
if (count($idSitesAccessible) == 0) {
|
||||
throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAtLeastOneWebsite', array('admin')));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user doesn't have any view permission, throw exception
|
||||
*
|
||||
* @throws \Piwik\NoAccessException
|
||||
*/
|
||||
public function checkUserHasSomeViewAccess()
|
||||
{
|
||||
if ($this->hasSuperUserAccess()) {
|
||||
return;
|
||||
}
|
||||
$idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess();
|
||||
if (count($idSitesAccessible) == 0) {
|
||||
throw new NoAccessException(Piwik::translate('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\NoAccessException If for any of the websites the user doesn't have an ADMIN access
|
||||
*/
|
||||
public function checkUserHasAdminAccess($idSites)
|
||||
{
|
||||
if ($this->hasSuperUserAccess()) {
|
||||
return;
|
||||
}
|
||||
$idSites = $this->getIdSites($idSites);
|
||||
$idSitesAccessible = $this->getSitesIdWithAdminAccess();
|
||||
foreach ($idSites as $idsite) {
|
||||
if (!in_array($idsite, $idSitesAccessible)) {
|
||||
throw new NoAccessException(Piwik::translate('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\NoAccessException If for any of the websites the user doesn't have an VIEW or ADMIN access
|
||||
*/
|
||||
public function checkUserHasViewAccess($idSites)
|
||||
{
|
||||
if ($this->hasSuperUserAccess()) {
|
||||
return;
|
||||
}
|
||||
$idSites = $this->getIdSites($idSites);
|
||||
$idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess();
|
||||
foreach ($idSites as $idsite) {
|
||||
if (!in_array($idsite, $idSitesAccessible)) {
|
||||
throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'view'", $idsite)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|array|string $idSites
|
||||
* @return array
|
||||
* @throws \Piwik\NoAccessException
|
||||
*/
|
||||
protected function getIdSites($idSites)
|
||||
{
|
||||
if ($idSites === 'all') {
|
||||
$idSites = $this->getSitesIdWithAtLeastViewAccess();
|
||||
}
|
||||
|
||||
$idSites = Site::getIdSitesFromIdSitesString($idSites);
|
||||
if (empty($idSites)) {
|
||||
throw new NoAccessException("The parameter 'idSite=' is missing from the request.");
|
||||
}
|
||||
return $idSites;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception thrown when a user doesn't have sufficient access to a resource.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class NoAccessException extends \Exception
|
||||
{
|
||||
}
|
||||
809
www/analytics/core/Archive.php
Normal file
809
www/analytics/core/Archive.php
Normal file
|
|
@ -0,0 +1,809 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik;
|
||||
|
||||
use Piwik\Archive\Parameters;
|
||||
|
||||
use Piwik\ArchiveProcessor\Rules;
|
||||
use Piwik\DataAccess\ArchiveSelector;
|
||||
use Piwik\Period\Range;
|
||||
|
||||
/**
|
||||
* The **Archive** class is used to query cached analytics statistics
|
||||
* (termed "archive data").
|
||||
*
|
||||
* You can use **Archive** instances to get data that was archived for one or more sites,
|
||||
* for one or more periods and one optional segment.
|
||||
*
|
||||
* If archive data is not found, this class will initiate the archiving process. [1](#footnote-1)
|
||||
*
|
||||
* **Archive** instances must be created using the {@link build()} factory method;
|
||||
* they cannot be constructed.
|
||||
*
|
||||
* You can search for metrics (such as `nb_visits`) using the {@link getNumeric()} and
|
||||
* {@link getDataTableFromNumeric()} methods. You can search for
|
||||
* reports using the {@link getBlob()}, {@link getDataTable()} and {@link getDataTableExpanded()} methods.
|
||||
*
|
||||
* If you're creating an API that returns report data, you may want to use the
|
||||
* {@link getDataTableFromArchive()} helper function.
|
||||
*
|
||||
* ### Learn more
|
||||
*
|
||||
* Learn more about _archiving_ [here](/guides/all-about-analytics-data).
|
||||
*
|
||||
* ### Limitations
|
||||
*
|
||||
* - You cannot get data for multiple range periods in a single query.
|
||||
* - You cannot get data for periods of different types in a single query.
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* **_Querying metrics for an API method_**
|
||||
*
|
||||
* // one site and one period
|
||||
* $archive = Archive::build($idSite = 1, $period = 'week', $date = '2013-03-08');
|
||||
* return $archive->getDataTableFromNumeric(array('nb_visits', 'nb_actions'));
|
||||
*
|
||||
* // all sites and multiple dates
|
||||
* $archive = Archive::build($idSite = 'all', $period = 'month', $date = '2013-01-02,2013-03-08');
|
||||
* return $archive->getDataTableFromNumeric(array('nb_visits', 'nb_actions'));
|
||||
*
|
||||
* **_Querying and using metrics immediately_**
|
||||
*
|
||||
* // one site and one period
|
||||
* $archive = Archive::build($idSite = 1, $period = 'week', $date = '2013-03-08');
|
||||
* $data = $archive->getNumeric(array('nb_visits', 'nb_actions'));
|
||||
*
|
||||
* $visits = $data['nb_visits'];
|
||||
* $actions = $data['nb_actions'];
|
||||
*
|
||||
* // ... do something w/ metric data ...
|
||||
*
|
||||
* // multiple sites and multiple dates
|
||||
* $archive = Archive::build($idSite = '1,2,3', $period = 'month', $date = '2013-01-02,2013-03-08');
|
||||
* $data = $archive->getNumeric('nb_visits');
|
||||
*
|
||||
* $janSite1Visits = $data['1']['2013-01-01,2013-01-31']['nb_visits'];
|
||||
* $febSite1Visits = $data['1']['2013-02-01,2013-02-28']['nb_visits'];
|
||||
* // ... etc.
|
||||
*
|
||||
* **_Querying for reports_**
|
||||
*
|
||||
* $archive = Archive::build($idSite = 1, $period = 'week', $date = '2013-03-08');
|
||||
* $dataTable = $archive->getDataTable('MyPlugin_MyReport');
|
||||
* // ... manipulate $dataTable ...
|
||||
* return $dataTable;
|
||||
*
|
||||
* **_Querying a report for an API method_**
|
||||
*
|
||||
* public function getMyReport($idSite, $period, $date, $segment = false, $expanded = false)
|
||||
* {
|
||||
* $dataTable = Archive::getDataTableFromArchive('MyPlugin_MyReport', $idSite, $period, $date, $segment, $expanded);
|
||||
* $dataTable->queueFilter('ReplaceColumnNames');
|
||||
* return $dataTable;
|
||||
* }
|
||||
*
|
||||
* **_Querying data for multiple range periods_**
|
||||
*
|
||||
* // get data for first range
|
||||
* $archive = Archive::build($idSite = 1, $period = 'range', $date = '2013-03-08,2013-03-12');
|
||||
* $dataTable = $archive->getDataTableFromNumeric(array('nb_visits', 'nb_actions'));
|
||||
*
|
||||
* // get data for second range
|
||||
* $archive = Archive::build($idSite = 1, $period = 'range', $date = '2013-03-15,2013-03-20');
|
||||
* $dataTable = $archive->getDataTableFromNumeric(array('nb_visits', 'nb_actions'));
|
||||
*
|
||||
* <a name="footnote-1"></a>
|
||||
* [1]: The archiving process will not be launched if browser archiving is disabled
|
||||
* and the current request came from a browser (and not the **archive.php** cron
|
||||
* script).
|
||||
*
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class Archive
|
||||
{
|
||||
const REQUEST_ALL_WEBSITES_FLAG = 'all';
|
||||
const ARCHIVE_ALL_PLUGINS_FLAG = 'all';
|
||||
const ID_SUBTABLE_LOAD_ALL_SUBTABLES = 'all';
|
||||
|
||||
/**
|
||||
* List of archive IDs for the site, periods and segment we are querying with.
|
||||
* Archive IDs are indexed by done flag and period, ie:
|
||||
*
|
||||
* array(
|
||||
* 'done.Referrers' => array(
|
||||
* '2010-01-01' => 1,
|
||||
* '2010-01-02' => 2,
|
||||
* ),
|
||||
* 'done.VisitsSummary' => array(
|
||||
* '2010-01-01' => 3,
|
||||
* '2010-01-02' => 4,
|
||||
* ),
|
||||
* )
|
||||
*
|
||||
* or,
|
||||
*
|
||||
* array(
|
||||
* 'done.all' => array(
|
||||
* '2010-01-01' => 1,
|
||||
* '2010-01-02' => 2
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $idarchives = array();
|
||||
|
||||
/**
|
||||
* If set to true, the result of all get functions (ie, getNumeric, getBlob, etc.)
|
||||
* will be indexed by the site ID, even if we're only querying data for one site.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $forceIndexedBySite;
|
||||
|
||||
/**
|
||||
* If set to true, the result of all get functions (ie, getNumeric, getBlob, etc.)
|
||||
* will be indexed by the period, even if we're only querying data for one period.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $forceIndexedByDate;
|
||||
|
||||
/**
|
||||
* @var Parameters
|
||||
*/
|
||||
private $params;
|
||||
|
||||
/**
|
||||
* @param Parameters $params
|
||||
* @param bool $forceIndexedBySite Whether to force index the result of a query by site ID.
|
||||
* @param bool $forceIndexedByDate Whether to force index the result of a query by period.
|
||||
*/
|
||||
protected function __construct(Parameters $params, $forceIndexedBySite = false,
|
||||
$forceIndexedByDate = false)
|
||||
{
|
||||
$this->params = $params;
|
||||
$this->forceIndexedBySite = $forceIndexedBySite;
|
||||
$this->forceIndexedByDate = $forceIndexedByDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new Archive instance that will query archive data for the given set of
|
||||
* sites and periods, using an optional Segment.
|
||||
*
|
||||
* This method uses data that is found in query parameters, so the parameters to this
|
||||
* function can be string values.
|
||||
*
|
||||
* If you want to create an Archive instance with an array of Period instances, use
|
||||
* {@link Archive::factory()}.
|
||||
*
|
||||
* @param string|int|array $idSites A single ID (eg, `'1'`), multiple IDs (eg, `'1,2,3'` or `array(1, 2, 3)`),
|
||||
* or `'all'`.
|
||||
* @param string $period 'day', `'week'`, `'month'`, `'year'` or `'range'`
|
||||
* @param Date|string $strDate 'YYYY-MM-DD', magic keywords (ie, 'today'; {@link Date::factory()}
|
||||
* or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
|
||||
* @param bool|false|string $segment Segment definition or false if no segment should be used. {@link Piwik\Segment}
|
||||
* @param bool|false|string $_restrictSitesToLogin Used only when running as a scheduled task.
|
||||
* @param bool $skipAggregationOfSubTables Whether the archive, when it is processed, should also aggregate all sub-tables
|
||||
* @return Archive
|
||||
*/
|
||||
public static function build($idSites, $period, $strDate, $segment = false, $_restrictSitesToLogin = false, $skipAggregationOfSubTables = false)
|
||||
{
|
||||
$websiteIds = Site::getIdSitesFromIdSitesString($idSites, $_restrictSitesToLogin);
|
||||
|
||||
if (Period::isMultiplePeriod($strDate, $period)) {
|
||||
$oPeriod = new Range($period, $strDate);
|
||||
$allPeriods = $oPeriod->getSubperiods();
|
||||
} else {
|
||||
$timezone = count($websiteIds) == 1 ? Site::getTimezoneFor($websiteIds[0]) : false;
|
||||
$oPeriod = Period::makePeriodFromQueryParams($timezone, $period, $strDate);
|
||||
$allPeriods = array($oPeriod);
|
||||
}
|
||||
$segment = new Segment($segment, $websiteIds);
|
||||
$idSiteIsAll = $idSites == self::REQUEST_ALL_WEBSITES_FLAG;
|
||||
$isMultipleDate = Period::isMultiplePeriod($strDate, $period);
|
||||
return Archive::factory($segment, $allPeriods, $websiteIds, $idSiteIsAll, $isMultipleDate, $skipAggregationOfSubTables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new Archive instance that will query archive data for the given set of
|
||||
* sites and periods, using an optional segment.
|
||||
*
|
||||
* This method uses an array of Period instances and a Segment instance, instead of strings
|
||||
* like {@link build()}.
|
||||
*
|
||||
* If you want to create an Archive instance using data found in query parameters,
|
||||
* use {@link build()}.
|
||||
*
|
||||
* @param Segment $segment The segment to use. For no segment, use `new Segment('', $idSites)`.
|
||||
* @param array $periods An array of Period instances.
|
||||
* @param array $idSites An array of site IDs (eg, `array(1, 2, 3)`).
|
||||
* @param bool $idSiteIsAll Whether `'all'` sites are being queried or not. If true, then
|
||||
* the result of querying functions will be indexed by site, regardless
|
||||
* of whether `count($idSites) == 1`.
|
||||
* @param bool $isMultipleDate Whether multiple dates are being queried or not. If true, then
|
||||
* the result of querying functions will be indexed by period,
|
||||
* regardless of whether `count($periods) == 1`.
|
||||
* @param bool $skipAggregationOfSubTables Whether the archive should skip aggregation of all sub-tables
|
||||
*
|
||||
* @return Archive
|
||||
*/
|
||||
public static function factory(Segment $segment, array $periods, array $idSites, $idSiteIsAll = false, $isMultipleDate = false, $skipAggregationOfSubTables = false)
|
||||
{
|
||||
$forceIndexedBySite = false;
|
||||
$forceIndexedByDate = false;
|
||||
if ($idSiteIsAll || count($idSites) > 1) {
|
||||
$forceIndexedBySite = true;
|
||||
}
|
||||
if (count($periods) > 1 || $isMultipleDate) {
|
||||
$forceIndexedByDate = true;
|
||||
}
|
||||
|
||||
$params = new Parameters($idSites, $periods, $segment, $skipAggregationOfSubTables);
|
||||
|
||||
return new Archive($params, $forceIndexedBySite, $forceIndexedByDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries and returns metric data in an array.
|
||||
*
|
||||
* If multiple sites were requested in {@link build()} or {@link factory()} the result will
|
||||
* be indexed by site ID.
|
||||
*
|
||||
* If multiple periods were requested in {@link build()} or {@link factory()} the result will
|
||||
* be indexed by period.
|
||||
*
|
||||
* The site ID index is always first, so if multiple sites & periods were requested, the result
|
||||
* will be indexed by site ID first, then period.
|
||||
*
|
||||
* @param string|array $names One or more archive names, eg, `'nb_visits'`, `'Referrers_distinctKeywords'`,
|
||||
* etc.
|
||||
* @return false|numeric|array `false` if there is no data to return, a single numeric value if we're not querying
|
||||
* for multiple sites/periods, or an array if multiple sites, periods or names are
|
||||
* queried for.
|
||||
*/
|
||||
public function getNumeric($names)
|
||||
{
|
||||
$data = $this->get($names, 'numeric');
|
||||
|
||||
$resultIndices = $this->getResultIndices();
|
||||
$result = $data->getIndexedArray($resultIndices);
|
||||
|
||||
// if only one metric is returned, just return it as a numeric value
|
||||
if (empty($resultIndices)
|
||||
&& count($result) <= 1
|
||||
&& (!is_array($names) || count($names) == 1)
|
||||
) {
|
||||
$result = (float)reset($result); // convert to float in case $result is empty
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries and returns blob data in an array.
|
||||
*
|
||||
* Reports are stored in blobs as serialized arrays of {@link DataTable\Row} instances, but this
|
||||
* data can technically be anything. In other words, you can store whatever you want
|
||||
* as archive data blobs.
|
||||
*
|
||||
* If multiple sites were requested in {@link build()} or {@link factory()} the result will
|
||||
* be indexed by site ID.
|
||||
*
|
||||
* If multiple periods were requested in {@link build()} or {@link factory()} the result will
|
||||
* be indexed by period.
|
||||
*
|
||||
* The site ID index is always first, so if multiple sites & periods were requested, the result
|
||||
* will be indexed by site ID first, then period.
|
||||
*
|
||||
* @param string|array $names One or more archive names, eg, `'Referrers_keywordBySearchEngine'`.
|
||||
* @param null|string $idSubtable If we're returning serialized DataTable data, then this refers
|
||||
* to the subtable ID to return. If set to 'all', all subtables
|
||||
* of each requested report are returned.
|
||||
* @return array An array of appropriately indexed blob data.
|
||||
*/
|
||||
public function getBlob($names, $idSubtable = null)
|
||||
{
|
||||
$data = $this->get($names, 'blob', $idSubtable);
|
||||
return $data->getIndexedArray($this->getResultIndices());
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries and returns metric data in a DataTable instance.
|
||||
*
|
||||
* If multiple sites were requested in {@link build()} or {@link factory()} the result will
|
||||
* be a DataTable\Map that is indexed by site ID.
|
||||
*
|
||||
* If multiple periods were requested in {@link build()} or {@link factory()} the result will
|
||||
* be a {@link DataTable\Map} that is indexed by period.
|
||||
*
|
||||
* The site ID index is always first, so if multiple sites & periods were requested, the result
|
||||
* will be a {@link DataTable\Map} indexed by site ID which contains {@link DataTable\Map} instances that are
|
||||
* indexed by period.
|
||||
*
|
||||
* _Note: Every DataTable instance returned will have at most one row in it. The contents of each
|
||||
* row will be the requested metrics for the appropriate site and period._
|
||||
*
|
||||
* @param string|array $names One or more archive names, eg, 'nb_visits', 'Referrers_distinctKeywords',
|
||||
* etc.
|
||||
* @return DataTable|DataTable\Map A DataTable if multiple sites and periods were not requested.
|
||||
* An appropriately indexed DataTable\Map if otherwise.
|
||||
*/
|
||||
public function getDataTableFromNumeric($names)
|
||||
{
|
||||
$data = $this->get($names, 'numeric');
|
||||
return $data->getDataTable($this->getResultIndices());
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries and returns one or more reports as DataTable instances.
|
||||
*
|
||||
* This method will query blob data that is a serialized array of of {@link DataTable\Row}'s and
|
||||
* unserialize it.
|
||||
*
|
||||
* If multiple sites were requested in {@link build()} or {@link factory()} the result will
|
||||
* be a {@link DataTable\Map} that is indexed by site ID.
|
||||
*
|
||||
* If multiple periods were requested in {@link build()} or {@link factory()} the result will
|
||||
* be a DataTable\Map that is indexed by period.
|
||||
*
|
||||
* The site ID index is always first, so if multiple sites & periods were requested, the result
|
||||
* will be a {@link DataTable\Map} indexed by site ID which contains {@link DataTable\Map} instances that are
|
||||
* indexed by period.
|
||||
*
|
||||
* @param string $name The name of the record to get. This method can only query one record at a time.
|
||||
* @param int|string|null $idSubtable The ID of the subtable to get (if any).
|
||||
* @return DataTable|DataTable\Map A DataTable if multiple sites and periods were not requested.
|
||||
* An appropriately indexed {@link DataTable\Map} if otherwise.
|
||||
*/
|
||||
public function getDataTable($name, $idSubtable = null)
|
||||
{
|
||||
$data = $this->get($name, 'blob', $idSubtable);
|
||||
return $data->getDataTable($this->getResultIndices());
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries and returns one report with all of its subtables loaded.
|
||||
*
|
||||
* If multiple sites were requested in {@link build()} or {@link factory()} the result will
|
||||
* be a DataTable\Map that is indexed by site ID.
|
||||
*
|
||||
* If multiple periods were requested in {@link build()} or {@link factory()} the result will
|
||||
* be a DataTable\Map that is indexed by period.
|
||||
*
|
||||
* The site ID index is always first, so if multiple sites & periods were requested, the result
|
||||
* will be a {@link DataTable\Map indexed} by site ID which contains {@link DataTable\Map} instances that are
|
||||
* indexed by period.
|
||||
*
|
||||
* @param string $name The name of the record to get.
|
||||
* @param int|string|null $idSubtable The ID of the subtable to get (if any). The subtable will be expanded.
|
||||
* @param int|null $depth The maximum number of subtable levels to load. If null, all levels are loaded.
|
||||
* For example, if `1` is supplied, then the DataTable returned will have its subtables
|
||||
* loaded. Those subtables, however, will NOT have their subtables loaded.
|
||||
* @param bool $addMetadataSubtableId Whether to add the database subtable ID as metadata to each datatable,
|
||||
* or not.
|
||||
* @return DataTable|DataTable\Map
|
||||
*/
|
||||
public function getDataTableExpanded($name, $idSubtable = null, $depth = null, $addMetadataSubtableId = true)
|
||||
{
|
||||
$data = $this->get($name, 'blob', self::ID_SUBTABLE_LOAD_ALL_SUBTABLES);
|
||||
return $data->getExpandedDataTable($this->getResultIndices(), $idSubtable, $depth, $addMetadataSubtableId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of plugins that archive the given reports.
|
||||
*
|
||||
* @param array $archiveNames
|
||||
* @return array
|
||||
*/
|
||||
private function getRequestedPlugins($archiveNames)
|
||||
{
|
||||
$result = array();
|
||||
foreach ($archiveNames as $name) {
|
||||
$result[] = self::getPluginForReport($name);
|
||||
}
|
||||
return array_unique($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object describing the set of sites, the set of periods and the segment
|
||||
* this Archive will query data for.
|
||||
*
|
||||
* @return Parameters
|
||||
*/
|
||||
public function getParams()
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that creates an Archive instance and queries for report data using
|
||||
* query parameter data. API methods can use this method to reduce code redundancy.
|
||||
*
|
||||
* @param string $name The name of the report to return.
|
||||
* @param int|string|array $idSite @see {@link build()}
|
||||
* @param string $period @see {@link build()}
|
||||
* @param string $date @see {@link build()}
|
||||
* @param string $segment @see {@link build()}
|
||||
* @param bool $expanded If true, loads all subtables. See {@link getDataTableExpanded()}
|
||||
* @param int|null $idSubtable See {@link getDataTableExpanded()}
|
||||
* @param bool $skipAggregationOfSubTables Whether or not we should skip the aggregation of all sub-tables and only aggregate parent DataTable.
|
||||
* @param int|null $depth See {@link getDataTableExpanded()}
|
||||
* @return DataTable|DataTable\Map See {@link getDataTable()} and
|
||||
* {@link getDataTableExpanded()} for more
|
||||
* information
|
||||
*/
|
||||
public static function getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded,
|
||||
$idSubtable = null, $skipAggregationOfSubTables = false, $depth = null)
|
||||
{
|
||||
Piwik::checkUserHasViewAccess($idSite);
|
||||
|
||||
if($skipAggregationOfSubTables && ($expanded || $idSubtable)) {
|
||||
throw new \Exception("Not expected to skipAggregationOfSubTables when expanded=1 or idSubtable is set.");
|
||||
}
|
||||
$archive = Archive::build($idSite, $period, $date, $segment, $_restrictSitesToLogin = false, $skipAggregationOfSubTables);
|
||||
if ($idSubtable === false) {
|
||||
$idSubtable = null;
|
||||
}
|
||||
|
||||
if ($expanded) {
|
||||
$dataTable = $archive->getDataTableExpanded($name, $idSubtable, $depth);
|
||||
} else {
|
||||
$dataTable = $archive->getDataTable($name, $idSubtable);
|
||||
}
|
||||
|
||||
$dataTable->queueFilter('ReplaceSummaryRowLabel');
|
||||
|
||||
return $dataTable;
|
||||
}
|
||||
|
||||
private function appendIdSubtable($recordName, $id)
|
||||
{
|
||||
return $recordName . "_" . $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries archive tables for data and returns the result.
|
||||
* @param array|string $archiveNames
|
||||
* @param $archiveDataType
|
||||
* @param null|int $idSubtable
|
||||
* @return Archive\DataCollection
|
||||
*/
|
||||
private function get($archiveNames, $archiveDataType, $idSubtable = null)
|
||||
{
|
||||
if (!is_array($archiveNames)) {
|
||||
$archiveNames = array($archiveNames);
|
||||
}
|
||||
|
||||
// apply idSubtable
|
||||
if ($idSubtable !== null
|
||||
&& $idSubtable != self::ID_SUBTABLE_LOAD_ALL_SUBTABLES
|
||||
) {
|
||||
foreach ($archiveNames as &$name) {
|
||||
$name = $this->appendIdsubtable($name, $idSubtable);
|
||||
}
|
||||
}
|
||||
|
||||
$result = new Archive\DataCollection(
|
||||
$archiveNames, $archiveDataType, $this->params->getIdSites(), $this->params->getPeriods(), $defaultRow = null);
|
||||
|
||||
$archiveIds = $this->getArchiveIds($archiveNames);
|
||||
if (empty($archiveIds)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$loadAllSubtables = $idSubtable == self::ID_SUBTABLE_LOAD_ALL_SUBTABLES;
|
||||
$archiveData = ArchiveSelector::getArchiveData($archiveIds, $archiveNames, $archiveDataType, $loadAllSubtables);
|
||||
foreach ($archiveData as $row) {
|
||||
// values are grouped by idsite (site ID), date1-date2 (date range), then name (field name)
|
||||
$idSite = $row['idsite'];
|
||||
$periodStr = $row['date1'] . "," . $row['date2'];
|
||||
|
||||
if ($archiveDataType == 'numeric') {
|
||||
$value = $this->formatNumericValue($row['value']);
|
||||
} else {
|
||||
$value = $this->uncompress($row['value']);
|
||||
$result->addMetadata($idSite, $periodStr, 'ts_archived', $row['ts_archived']);
|
||||
}
|
||||
|
||||
$resultRow = & $result->get($idSite, $periodStr);
|
||||
$resultRow[$row['name']] = $value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns archive IDs for the sites, periods and archive names that are being
|
||||
* queried. This function will use the idarchive cache if it has the right data,
|
||||
* query archive tables for IDs w/o launching archiving, or launch archiving and
|
||||
* get the idarchive from ArchiveProcessor instances.
|
||||
*/
|
||||
private function getArchiveIds($archiveNames)
|
||||
{
|
||||
$plugins = $this->getRequestedPlugins($archiveNames);
|
||||
|
||||
// figure out which archives haven't been processed (if an archive has been processed,
|
||||
// then we have the archive IDs in $this->idarchives)
|
||||
$doneFlags = array();
|
||||
$archiveGroups = array();
|
||||
foreach ($plugins as $plugin) {
|
||||
$doneFlag = $this->getDoneStringForPlugin($plugin);
|
||||
|
||||
$doneFlags[$doneFlag] = true;
|
||||
if (!isset($this->idarchives[$doneFlag])) {
|
||||
$archiveGroup = $this->getArchiveGroupOfPlugin($plugin);
|
||||
|
||||
if($archiveGroup == self::ARCHIVE_ALL_PLUGINS_FLAG) {
|
||||
$archiveGroup = reset($plugins);
|
||||
}
|
||||
$archiveGroups[] = $archiveGroup;
|
||||
}
|
||||
}
|
||||
|
||||
$archiveGroups = array_unique($archiveGroups);
|
||||
|
||||
// cache id archives for plugins we haven't processed yet
|
||||
if (!empty($archiveGroups)) {
|
||||
if (!Rules::isArchivingDisabledFor($this->params->getIdSites(), $this->params->getSegment(), $this->getPeriodLabel())) {
|
||||
$this->cacheArchiveIdsAfterLaunching($archiveGroups, $plugins);
|
||||
} else {
|
||||
$this->cacheArchiveIdsWithoutLaunching($plugins);
|
||||
}
|
||||
}
|
||||
|
||||
// order idarchives by the table month they belong to
|
||||
$idArchivesByMonth = array();
|
||||
foreach (array_keys($doneFlags) as $doneFlag) {
|
||||
if (empty($this->idarchives[$doneFlag])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->idarchives[$doneFlag] as $dateRange => $idarchives) {
|
||||
foreach ($idarchives as $id) {
|
||||
$idArchivesByMonth[$dateRange][] = $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $idArchivesByMonth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the IDs of the archives we're querying for and stores them in $this->archives.
|
||||
* This function will launch the archiving process for each period/site/plugin if
|
||||
* metrics/reports have not been calculated/archived already.
|
||||
*
|
||||
* @param array $archiveGroups @see getArchiveGroupOfReport
|
||||
* @param array $plugins List of plugin names to archive.
|
||||
*/
|
||||
private function cacheArchiveIdsAfterLaunching($archiveGroups, $plugins)
|
||||
{
|
||||
$today = Date::today();
|
||||
|
||||
foreach ($this->params->getPeriods() as $period) {
|
||||
$twoDaysBeforePeriod = $period->getDateStart()->subDay(2);
|
||||
$twoDaysAfterPeriod = $period->getDateEnd()->addDay(2);
|
||||
|
||||
foreach ($this->params->getIdSites() as $idSite) {
|
||||
$site = new Site($idSite);
|
||||
|
||||
// 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 ($twoDaysAfterPeriod->isEarlier($site->getCreationDate())) {
|
||||
Log::verbose("Archive site %s, %s (%s) skipped, archive is before the website was created.",
|
||||
$idSite, $period->getLabel(), $period->getPrettyString());
|
||||
continue;
|
||||
}
|
||||
|
||||
// if the starting date is in the future we know there is no visiidsite = ?t
|
||||
if ($twoDaysBeforePeriod->isLater($today)) {
|
||||
Log::verbose("Archive site %s, %s (%s) skipped, archive is after today.",
|
||||
$idSite, $period->getLabel(), $period->getPrettyString());
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->prepareArchive($archiveGroups, $site, $period);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the IDs of the archives we're querying for and stores them in $this->archives.
|
||||
* This function will not launch the archiving process (and is thus much, much faster
|
||||
* than cacheArchiveIdsAfterLaunching).
|
||||
*
|
||||
* @param array $plugins List of plugin names from which data is being requested.
|
||||
*/
|
||||
private function cacheArchiveIdsWithoutLaunching($plugins)
|
||||
{
|
||||
$idarchivesByReport = ArchiveSelector::getArchiveIds(
|
||||
$this->params->getIdSites(), $this->params->getPeriods(), $this->params->getSegment(), $plugins, $this->params->isSkipAggregationOfSubTables());
|
||||
|
||||
// initialize archive ID cache for each report
|
||||
foreach ($plugins as $plugin) {
|
||||
$doneFlag = $this->getDoneStringForPlugin($plugin);
|
||||
$this->initializeArchiveIdCache($doneFlag);
|
||||
}
|
||||
|
||||
foreach ($idarchivesByReport as $doneFlag => $idarchivesByDate) {
|
||||
foreach ($idarchivesByDate as $dateRange => $idarchives) {
|
||||
foreach ($idarchives as $idarchive) {
|
||||
$this->idarchives[$doneFlag][$dateRange][] = $idarchive;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the done string flag for a plugin using this instance's segment & periods.
|
||||
* @param string $plugin
|
||||
* @return string
|
||||
*/
|
||||
private function getDoneStringForPlugin($plugin)
|
||||
{
|
||||
return Rules::getDoneStringFlagFor(
|
||||
$this->params->getIdSites(),
|
||||
$this->params->getSegment(),
|
||||
$this->getPeriodLabel(),
|
||||
$plugin,
|
||||
$this->params->isSkipAggregationOfSubTables()
|
||||
);
|
||||
}
|
||||
|
||||
private function getPeriodLabel()
|
||||
{
|
||||
$periods = $this->params->getPeriods();
|
||||
return reset($periods)->getLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array describing what metadata to use when indexing a query result.
|
||||
* For use with DataCollection.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getResultIndices()
|
||||
{
|
||||
$indices = array();
|
||||
|
||||
if (count($this->params->getIdSites()) > 1
|
||||
|| $this->forceIndexedBySite
|
||||
) {
|
||||
$indices['site'] = 'idSite';
|
||||
}
|
||||
|
||||
if (count($this->params->getPeriods()) > 1
|
||||
|| $this->forceIndexedByDate
|
||||
) {
|
||||
$indices['period'] = 'date';
|
||||
}
|
||||
|
||||
return $indices;
|
||||
}
|
||||
|
||||
private 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
|
||||
return round((float)$value, 2);
|
||||
}
|
||||
|
||||
private function uncompress($data)
|
||||
{
|
||||
return @gzuncompress($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the archive ID cache ($this->idarchives) for a particular 'done' flag.
|
||||
*
|
||||
* It is necessary that each archive ID caching function call this method for each
|
||||
* unique 'done' flag it encounters, since the getArchiveIds function determines
|
||||
* whether archiving should be launched based on whether $this->idarchives has a
|
||||
* an entry for a specific 'done' flag.
|
||||
*
|
||||
* If this function is not called, then periods with no visits will not add
|
||||
* entries to the cache. If the archive is used again, SQL will be executed to
|
||||
* try and find the archive IDs even though we know there are none.
|
||||
*/
|
||||
private function initializeArchiveIdCache($doneFlag)
|
||||
{
|
||||
if (!isset($this->idarchives[$doneFlag])) {
|
||||
$this->idarchives[$doneFlag] = array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the archiving group identifier given a plugin.
|
||||
*
|
||||
* More than one plugin can be called at once when archiving. In such a case
|
||||
* we don't want to launch archiving three times for three plugins if doing
|
||||
* it once is enough, so getArchiveIds makes sure to get the archive group of
|
||||
* all reports.
|
||||
*
|
||||
* If the period isn't a range, then all plugins' archiving code is executed.
|
||||
* If the period is a range, then archiving code is executed individually for
|
||||
* each plugin.
|
||||
*/
|
||||
private function getArchiveGroupOfPlugin($plugin)
|
||||
{
|
||||
if ($this->getPeriodLabel() != 'range') {
|
||||
return self::ARCHIVE_ALL_PLUGINS_FLAG;
|
||||
}
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the plugin that archives a given report.
|
||||
*
|
||||
* @param string $report Archive data name, eg, `'nb_visits'`, `'UserSettings_...'`, etc.
|
||||
* @return string Plugin name.
|
||||
* @throws \Exception If a plugin cannot be found or if the plugin for the report isn't
|
||||
* activated.
|
||||
*/
|
||||
private static function getPluginForReport($report)
|
||||
{
|
||||
// Core metrics are always processed in Core, for the requested date/period/segment
|
||||
if (in_array($report, Metrics::getVisitsMetricNames())) {
|
||||
$report = 'VisitsSummary_CoreMetrics';
|
||||
} // Goal_* metrics are processed by the Goals plugin (HACK)
|
||||
else if (strpos($report, 'Goal_') === 0) {
|
||||
$report = 'Goals_Metrics';
|
||||
} else if (strrpos($report, '_returning') === strlen($report) - strlen('_returning')) { // HACK
|
||||
$report = 'VisitFrequency_Metrics';
|
||||
}
|
||||
|
||||
$plugin = substr($report, 0, strpos($report, '_'));
|
||||
if (empty($plugin)
|
||||
|| !\Piwik\Plugin\Manager::getInstance()->isPluginActivated($plugin)
|
||||
) {
|
||||
throw new \Exception("Error: The report '$report' was requested but it is not available at this stage."
|
||||
. " (Plugin '$plugin' is not activated.)");
|
||||
}
|
||||
return $plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $archiveGroups
|
||||
* @param $site
|
||||
* @param $period
|
||||
*/
|
||||
private function prepareArchive(array $archiveGroups, Site $site, Period $period)
|
||||
{
|
||||
$parameters = new ArchiveProcessor\Parameters($site, $period, $this->params->getSegment(), $this->params->isSkipAggregationOfSubTables());
|
||||
$archiveLoader = new ArchiveProcessor\Loader($parameters);
|
||||
|
||||
$periodString = $period->getRangeString();
|
||||
|
||||
// process for each plugin as well
|
||||
foreach ($archiveGroups as $plugin) {
|
||||
$doneFlag = $this->getDoneStringForPlugin($plugin);
|
||||
$this->initializeArchiveIdCache($doneFlag);
|
||||
|
||||
$idArchive = $archiveLoader->prepareArchive($plugin);
|
||||
|
||||
if($idArchive) {
|
||||
$this->idarchives[$doneFlag][$periodString][] = $idArchive;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
336
www/analytics/core/Archive/DataCollection.php
Normal file
336
www/analytics/core/Archive/DataCollection.php
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Piwik\Archive;
|
||||
|
||||
use Exception;
|
||||
use Piwik\DataTable;
|
||||
|
||||
/**
|
||||
* This class is used to hold and transform archive data for the Archive class.
|
||||
*
|
||||
* Archive data is loaded into an instance of this type, can be indexed by archive
|
||||
* metadata (such as the site ID, period string, etc.), and can be transformed into
|
||||
* DataTable and Map instances.
|
||||
*/
|
||||
class DataCollection
|
||||
{
|
||||
const METADATA_CONTAINER_ROW_KEY = '_metadata';
|
||||
|
||||
/**
|
||||
* The archive data, indexed first by site ID and then by period date range. Eg,
|
||||
*
|
||||
* array(
|
||||
* '0' => array(
|
||||
* array(
|
||||
* '2012-01-01,2012-01-01' => array(...),
|
||||
* '2012-01-02,2012-01-02' => array(...),
|
||||
* )
|
||||
* ),
|
||||
* '1' => array(
|
||||
* array(
|
||||
* '2012-01-01,2012-01-01' => array(...),
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* Archive data can be either a numeric value or a serialized string blob. Every
|
||||
* piece of archive data is associated by it's archive name. For example,
|
||||
* the array(...) above could look like:
|
||||
*
|
||||
* array(
|
||||
* 'nb_visits' => 1,
|
||||
* 'nb_actions' => 2
|
||||
* )
|
||||
*
|
||||
* There is a special element '_metadata' in data rows that holds values treated
|
||||
* as DataTable metadata.
|
||||
*/
|
||||
private $data = array();
|
||||
|
||||
/**
|
||||
* The whole list of metric/record names that were used in the archive query.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $dataNames;
|
||||
|
||||
/**
|
||||
* The type of data that was queried for (ie, "blob" or "numeric").
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $dataType;
|
||||
|
||||
/**
|
||||
* The default values to use for each metric/record name that's being queried
|
||||
* for.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $defaultRow;
|
||||
|
||||
/**
|
||||
* The list of all site IDs that were queried for.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $sitesId;
|
||||
|
||||
/**
|
||||
* The list of all periods that were queried for. Each period is associated with
|
||||
* the period's range string. Eg,
|
||||
*
|
||||
* array(
|
||||
* '2012-01-01,2012-01-31' => new Period(...),
|
||||
* '2012-02-01,2012-02-28' => new Period(...),
|
||||
* )
|
||||
*
|
||||
* @var \Piwik\Period[]
|
||||
*/
|
||||
private $periods;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $dataNames @see $this->dataNames
|
||||
* @param string $dataType @see $this->dataType
|
||||
* @param array $sitesId @see $this->sitesId
|
||||
* @param \Piwik\Period[] $periods @see $this->periods
|
||||
* @param array $defaultRow @see $this->defaultRow
|
||||
*/
|
||||
public function __construct($dataNames, $dataType, $sitesId, $periods, $defaultRow = null)
|
||||
{
|
||||
$this->dataNames = $dataNames;
|
||||
$this->dataType = $dataType;
|
||||
|
||||
if ($defaultRow === null) {
|
||||
$defaultRow = array_fill_keys($dataNames, 0);
|
||||
}
|
||||
|
||||
$this->sitesId = $sitesId;
|
||||
|
||||
foreach ($periods as $period) {
|
||||
$this->periods[$period->getRangeString()] = $period;
|
||||
}
|
||||
$this->defaultRow = $defaultRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reference to the data for a specific site & period. If there is
|
||||
* no data for the given site ID & period, it is set to the default row.
|
||||
*
|
||||
* @param int $idSite
|
||||
* @param string $period eg, '2012-01-01,2012-01-31'
|
||||
*/
|
||||
public function &get($idSite, $period)
|
||||
{
|
||||
if (!isset($this->data[$idSite][$period])) {
|
||||
$this->data[$idSite][$period] = $this->defaultRow;
|
||||
}
|
||||
return $this->data[$idSite][$period];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new metadata to the data for specific site & period. If there is no
|
||||
* data for the given site ID & period, it is set to the default row.
|
||||
*
|
||||
* Note: Site ID and period range string are two special types of metadata. Since
|
||||
* the data stored in this class is indexed by site & period, this metadata is not
|
||||
* stored in individual data rows.
|
||||
*
|
||||
* @param int $idSite
|
||||
* @param string $period eg, '2012-01-01,2012-01-31'
|
||||
* @param string $name The metadata name.
|
||||
* @param mixed $value The metadata name.
|
||||
*/
|
||||
public function addMetadata($idSite, $period, $name, $value)
|
||||
{
|
||||
$row = & $this->get($idSite, $period);
|
||||
$row[self::METADATA_CONTAINER_ROW_KEY][$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns archive data as an array indexed by metadata.
|
||||
*
|
||||
* @param array $resultIndices An array mapping metadata names to pretty labels
|
||||
* for them. Each archive data row will be indexed
|
||||
* by the metadata specified here.
|
||||
*
|
||||
* Eg, array('site' => 'idSite', 'period' => 'Date')
|
||||
* @return array
|
||||
*/
|
||||
public function getIndexedArray($resultIndices)
|
||||
{
|
||||
$indexKeys = array_keys($resultIndices);
|
||||
|
||||
$result = $this->createOrderedIndex($indexKeys);
|
||||
foreach ($this->data as $idSite => $rowsByPeriod) {
|
||||
foreach ($rowsByPeriod as $period => $row) {
|
||||
// FIXME: This hack works around a strange bug that occurs when getting
|
||||
// archive IDs through ArchiveProcessing instances. When a table
|
||||
// does not already exist, for some reason the archive ID for
|
||||
// today (or from two days ago) will be added to the Archive
|
||||
// instances list. The Archive instance will then select data
|
||||
// for periods outside of the requested set.
|
||||
// working around the bug here, but ideally, we need to figure
|
||||
// out why incorrect idarchives are being selected.
|
||||
if (empty($this->periods[$period])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->putRowInIndex($result, $indexKeys, $row, $idSite, $period);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns archive data as a DataTable indexed by metadata. Indexed data will
|
||||
* be represented by Map instances.
|
||||
*
|
||||
* @param array $resultIndices An array mapping metadata names to pretty labels
|
||||
* for them. Each archive data row will be indexed
|
||||
* by the metadata specified here.
|
||||
*
|
||||
* Eg, array('site' => 'idSite', 'period' => 'Date')
|
||||
* @return DataTable|DataTable\Map
|
||||
*/
|
||||
public function getDataTable($resultIndices)
|
||||
{
|
||||
$dataTableFactory = new DataTableFactory(
|
||||
$this->dataNames, $this->dataType, $this->sitesId, $this->periods, $this->defaultRow);
|
||||
|
||||
$index = $this->getIndexedArray($resultIndices);
|
||||
return $dataTableFactory->make($index, $resultIndices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns archive data as a DataTable indexed by metadata. Indexed data will
|
||||
* be represented by Map instances. Each DataTable will have
|
||||
* its subtable IDs set.
|
||||
*
|
||||
* This function will only work if blob data was loaded and only one record
|
||||
* was loaded (not including subtables of the record).
|
||||
*
|
||||
* @param array $resultIndices An array mapping metadata names to pretty labels
|
||||
* for them. Each archive data row will be indexed
|
||||
* by the metadata specified here.
|
||||
*
|
||||
* Eg, array('site' => 'idSite', 'period' => 'Date')
|
||||
* @param int|null $idSubTable The subtable to return.
|
||||
* @param int|null $depth max depth for subtables.
|
||||
* @param bool $addMetadataSubTableId Whether to add the DB subtable ID as metadata
|
||||
* to each datatable, or not.
|
||||
* @throws Exception
|
||||
* @return DataTable|DataTable\Map
|
||||
*/
|
||||
public function getExpandedDataTable($resultIndices, $idSubTable = null, $depth = null, $addMetadataSubTableId = false)
|
||||
{
|
||||
if ($this->dataType != 'blob') {
|
||||
throw new Exception("DataCollection: cannot call getExpandedDataTable with "
|
||||
. "{$this->dataType} data types. Only works with blob data.");
|
||||
}
|
||||
|
||||
if (count($this->dataNames) !== 1) {
|
||||
throw new Exception("DataCollection: cannot call getExpandedDataTable with "
|
||||
. "more than one record.");
|
||||
}
|
||||
|
||||
$dataTableFactory = new DataTableFactory(
|
||||
$this->dataNames, 'blob', $this->sitesId, $this->periods, $this->defaultRow);
|
||||
$dataTableFactory->expandDataTable($depth, $addMetadataSubTableId);
|
||||
$dataTableFactory->useSubtable($idSubTable);
|
||||
|
||||
$index = $this->getIndexedArray($resultIndices);
|
||||
return $dataTableFactory->make($index, $resultIndices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns metadata for a data row.
|
||||
*
|
||||
* @param array $data The data row.
|
||||
* @return array
|
||||
*/
|
||||
public static function getDataRowMetadata($data)
|
||||
{
|
||||
if (isset($data[self::METADATA_CONTAINER_ROW_KEY])) {
|
||||
return $data[self::METADATA_CONTAINER_ROW_KEY];
|
||||
} else {
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all table metadata from a data row.
|
||||
*
|
||||
* @param array $data The data row.
|
||||
*/
|
||||
public static function removeMetadataFromDataRow(&$data)
|
||||
{
|
||||
unset($data[self::METADATA_CONTAINER_ROW_KEY]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty index using a list of metadata names. If the 'site' and/or
|
||||
* 'period' metadata names are supplied, empty rows are added for every site/period
|
||||
* that was queried for.
|
||||
*
|
||||
* Using this function ensures consistent ordering in the indexed result.
|
||||
*
|
||||
* @param array $metadataNamesToIndexBy List of metadata names to index archive data by.
|
||||
* @return array
|
||||
*/
|
||||
private function createOrderedIndex($metadataNamesToIndexBy)
|
||||
{
|
||||
$result = array();
|
||||
|
||||
if (!empty($metadataNamesToIndexBy)) {
|
||||
$metadataName = array_shift($metadataNamesToIndexBy);
|
||||
|
||||
if ($metadataName == DataTableFactory::TABLE_METADATA_SITE_INDEX) {
|
||||
$indexKeyValues = array_values($this->sitesId);
|
||||
} else if ($metadataName == DataTableFactory::TABLE_METADATA_PERIOD_INDEX) {
|
||||
$indexKeyValues = array_keys($this->periods);
|
||||
}
|
||||
|
||||
foreach ($indexKeyValues as $key) {
|
||||
$result[$key] = $this->createOrderedIndex($metadataNamesToIndexBy);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts an archive data row in an index.
|
||||
*/
|
||||
private function putRowInIndex(&$index, $metadataNamesToIndexBy, $row, $idSite, $period)
|
||||
{
|
||||
$currentLevel = & $index;
|
||||
|
||||
foreach ($metadataNamesToIndexBy as $metadataName) {
|
||||
if ($metadataName == DataTableFactory::TABLE_METADATA_SITE_INDEX) {
|
||||
$key = $idSite;
|
||||
} else if ($metadataName == DataTableFactory::TABLE_METADATA_PERIOD_INDEX) {
|
||||
$key = $period;
|
||||
} else {
|
||||
$key = $row[self::METADATA_CONTAINER_ROW_KEY][$metadataName];
|
||||
}
|
||||
|
||||
if (!isset($currentLevel[$key])) {
|
||||
$currentLevel[$key] = array();
|
||||
}
|
||||
|
||||
$currentLevel = & $currentLevel[$key];
|
||||
}
|
||||
|
||||
$currentLevel = $row;
|
||||
}
|
||||
}
|
||||
426
www/analytics/core/Archive/DataTableFactory.php
Normal file
426
www/analytics/core/Archive/DataTableFactory.php
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Piwik\Archive;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Row;
|
||||
use Piwik\Site;
|
||||
|
||||
/**
|
||||
* Creates a DataTable or Set instance based on an array
|
||||
* index created by DataCollection.
|
||||
*
|
||||
* This class is only used by DataCollection.
|
||||
*/
|
||||
class DataTableFactory
|
||||
{
|
||||
/**
|
||||
* @see DataCollection::$dataNames.
|
||||
*/
|
||||
private $dataNames;
|
||||
|
||||
/**
|
||||
* @see DataCollection::$dataType.
|
||||
*/
|
||||
private $dataType;
|
||||
|
||||
/**
|
||||
* Whether to expand the DataTables that're created or not. Expanding a DataTable
|
||||
* means creating DataTables using subtable blobs and correctly setting the subtable
|
||||
* IDs of all DataTables.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $expandDataTable = false;
|
||||
|
||||
/**
|
||||
* Whether to add the subtable ID used in the database to the in-memory DataTables
|
||||
* as metadata or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $addMetadataSubtableId = false;
|
||||
|
||||
/**
|
||||
* The maximum number of subtable levels to create when creating an expanded
|
||||
* DataTable.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $maxSubtableDepth = null;
|
||||
|
||||
/**
|
||||
* @see DataCollection::$sitesId.
|
||||
*/
|
||||
private $sitesId;
|
||||
|
||||
/**
|
||||
* @see DataCollection::$periods.
|
||||
*/
|
||||
private $periods;
|
||||
|
||||
/**
|
||||
* The ID of the subtable to create a DataTable for. Only relevant for blob data.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $idSubtable = null;
|
||||
|
||||
/**
|
||||
* @see DataCollection::$defaultRow.
|
||||
*/
|
||||
private $defaultRow;
|
||||
|
||||
const TABLE_METADATA_SITE_INDEX = 'site';
|
||||
const TABLE_METADATA_PERIOD_INDEX = 'period';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct($dataNames, $dataType, $sitesId, $periods, $defaultRow)
|
||||
{
|
||||
$this->dataNames = $dataNames;
|
||||
$this->dataType = $dataType;
|
||||
$this->sitesId = $sitesId;
|
||||
|
||||
//here index period by string only
|
||||
$this->periods = $periods;
|
||||
$this->defaultRow = $defaultRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the factory instance to expand the DataTables that are created by
|
||||
* creating subtables and setting the subtable IDs of rows w/ subtables correctly.
|
||||
*
|
||||
* @param null|int $maxSubtableDepth max depth for subtables.
|
||||
* @param bool $addMetadataSubtableId Whether to add the subtable ID used in the
|
||||
* database to the in-memory DataTables as
|
||||
* metadata or not.
|
||||
*/
|
||||
public function expandDataTable($maxSubtableDepth = null, $addMetadataSubtableId = false)
|
||||
{
|
||||
$this->expandDataTable = true;
|
||||
$this->maxSubtableDepth = $maxSubtableDepth;
|
||||
$this->addMetadataSubtableId = $addMetadataSubtableId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the factory instance to create a DataTable using a blob with the
|
||||
* supplied subtable ID.
|
||||
*
|
||||
* @param int $idSubtable An in-database subtable ID.
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function useSubtable($idSubtable)
|
||||
{
|
||||
if (count($this->dataNames) !== 1) {
|
||||
throw new \Exception("DataTableFactory: Getting subtables for multiple records in one"
|
||||
. " archive query is not currently supported.");
|
||||
}
|
||||
|
||||
$this->idSubtable = $idSubtable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DataTable|Set instance using an index of
|
||||
* archive data.
|
||||
*
|
||||
* @param array $index @see DataCollection
|
||||
* @param array $resultIndices an array mapping metadata names with pretty metadata
|
||||
* labels.
|
||||
* @return DataTable|DataTable\Map
|
||||
*/
|
||||
public function make($index, $resultIndices)
|
||||
{
|
||||
if (empty($resultIndices)) {
|
||||
// for numeric data, if there's no index (and thus only 1 site & period in the query),
|
||||
// we want to display every queried metric name
|
||||
if (empty($index)
|
||||
&& $this->dataType == 'numeric'
|
||||
) {
|
||||
$index = $this->defaultRow;
|
||||
}
|
||||
|
||||
$dataTable = $this->createDataTable($index, $keyMetadata = array());
|
||||
} else {
|
||||
$dataTable = $this->createDataTableMapFromIndex($index, $resultIndices, $keyMetadata = array());
|
||||
}
|
||||
|
||||
$this->transformMetadata($dataTable);
|
||||
return $dataTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DataTable|Set instance using an array
|
||||
* of blobs.
|
||||
*
|
||||
* If only one record is being queried, a single DataTable will
|
||||
* be returned. Otherwise, a DataTable\Map is returned that indexes
|
||||
* DataTables by record name.
|
||||
*
|
||||
* If expandDataTable was called, and only one record is being queried,
|
||||
* the created DataTable's subtables will be expanded.
|
||||
*
|
||||
* @param array $blobRow
|
||||
* @return DataTable|DataTable\Map
|
||||
*/
|
||||
private function makeFromBlobRow($blobRow)
|
||||
{
|
||||
if ($blobRow === false) {
|
||||
return new DataTable();
|
||||
}
|
||||
|
||||
if (count($this->dataNames) === 1) {
|
||||
return $this->makeDataTableFromSingleBlob($blobRow);
|
||||
} else {
|
||||
return $this->makeIndexedByRecordNameDataTable($blobRow);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DataTable for one record from an archive data row.
|
||||
*
|
||||
* @see makeFromBlobRow
|
||||
*
|
||||
* @param array $blobRow
|
||||
* @return DataTable
|
||||
*/
|
||||
private function makeDataTableFromSingleBlob($blobRow)
|
||||
{
|
||||
$recordName = reset($this->dataNames);
|
||||
if ($this->idSubtable !== null) {
|
||||
$recordName .= '_' . $this->idSubtable;
|
||||
}
|
||||
|
||||
if (!empty($blobRow[$recordName])) {
|
||||
$table = DataTable::fromSerializedArray($blobRow[$recordName]);
|
||||
} else {
|
||||
$table = new DataTable();
|
||||
}
|
||||
|
||||
// set table metadata
|
||||
$table->setMetadataValues(DataCollection::getDataRowMetadata($blobRow));
|
||||
|
||||
if ($this->expandDataTable) {
|
||||
$table->enableRecursiveFilters();
|
||||
$this->setSubtables($table, $blobRow);
|
||||
}
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DataTable for every record in an archive data row and puts them
|
||||
* in a DataTable\Map instance.
|
||||
*
|
||||
* @param array $blobRow
|
||||
* @return DataTable\Map
|
||||
*/
|
||||
private function makeIndexedByRecordNameDataTable($blobRow)
|
||||
{
|
||||
$table = new DataTable\Map();
|
||||
$table->setKeyName('recordName');
|
||||
|
||||
$tableMetadata = DataCollection::getDataRowMetadata($blobRow);
|
||||
|
||||
foreach ($blobRow as $name => $blob) {
|
||||
$newTable = DataTable::fromSerializedArray($blob);
|
||||
$newTable->setAllTableMetadata($tableMetadata);
|
||||
|
||||
$table->addTable($newTable, $name);
|
||||
}
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Set from an array index.
|
||||
*
|
||||
* @param array $index @see DataCollection
|
||||
* @param array $resultIndices @see make
|
||||
* @param array $keyMetadata The metadata to add to the table when it's created.
|
||||
* @return DataTable\Map
|
||||
*/
|
||||
private function createDataTableMapFromIndex($index, $resultIndices, $keyMetadata = array())
|
||||
{
|
||||
$resultIndexLabel = reset($resultIndices);
|
||||
$resultIndex = key($resultIndices);
|
||||
|
||||
array_shift($resultIndices);
|
||||
|
||||
$result = new DataTable\Map();
|
||||
$result->setKeyName($resultIndexLabel);
|
||||
|
||||
foreach ($index as $label => $value) {
|
||||
$keyMetadata[$resultIndex] = $label;
|
||||
|
||||
if (empty($resultIndices)) {
|
||||
$newTable = $this->createDataTable($value, $keyMetadata);
|
||||
} else {
|
||||
$newTable = $this->createDataTableMapFromIndex($value, $resultIndices, $keyMetadata);
|
||||
}
|
||||
|
||||
$result->addTable($newTable, $this->prettifyIndexLabel($resultIndex, $label));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DataTable instance from an index row.
|
||||
*
|
||||
* @param array $data An archive data row.
|
||||
* @param array $keyMetadata The metadata to add to the table(s) when created.
|
||||
* @return DataTable|DataTable\Map
|
||||
*/
|
||||
private function createDataTable($data, $keyMetadata)
|
||||
{
|
||||
if ($this->dataType == 'blob') {
|
||||
$result = $this->makeFromBlobRow($data);
|
||||
} else {
|
||||
$result = $this->makeFromMetricsArray($data);
|
||||
}
|
||||
$this->setTableMetadata($keyMetadata, $result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates DataTables from $dataTable's subtable blobs (stored in $blobRow) and sets
|
||||
* the subtable IDs of each DataTable row.
|
||||
*
|
||||
* @param DataTable $dataTable
|
||||
* @param array $blobRow An array associating record names (w/ subtable if applicable)
|
||||
* with blob values. This should hold every subtable blob for
|
||||
* the loaded DataTable.
|
||||
* @param int $treeLevel
|
||||
*/
|
||||
private function setSubtables($dataTable, $blobRow, $treeLevel = 0)
|
||||
{
|
||||
if ($this->maxSubtableDepth
|
||||
&& $treeLevel >= $this->maxSubtableDepth
|
||||
) {
|
||||
// unset the subtables so DataTableManager doesn't throw
|
||||
foreach ($dataTable->getRows() as $row) {
|
||||
$row->removeSubtable();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$dataName = reset($this->dataNames);
|
||||
|
||||
foreach ($dataTable->getRows() as $row) {
|
||||
$sid = $row->getIdSubDataTable();
|
||||
if ($sid === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$blobName = $dataName . "_" . $sid;
|
||||
if (isset($blobRow[$blobName])) {
|
||||
$subtable = DataTable::fromSerializedArray($blobRow[$blobName]);
|
||||
$this->setSubtables($subtable, $blobRow, $treeLevel + 1);
|
||||
|
||||
// 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 ($this->addMetadataSubtableId) {
|
||||
// this will be written back to the column 'idsubdatatable' just before rendering,
|
||||
// see Renderer/Php.php
|
||||
$row->addMetadata('idsubdatatable_in_db', $row->getIdSubDataTable());
|
||||
}
|
||||
|
||||
$row->setSubtable($subtable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts site IDs and period string ranges into Site instances and
|
||||
* Period instances in DataTable metadata.
|
||||
*/
|
||||
private function transformMetadata($table)
|
||||
{
|
||||
$periods = $this->periods;
|
||||
$table->filter(function ($table) use ($periods) {
|
||||
$table->setMetadata(DataTableFactory::TABLE_METADATA_SITE_INDEX, new Site($table->getMetadata(DataTableFactory::TABLE_METADATA_SITE_INDEX)));
|
||||
$table->setMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX, $periods[$table->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pretty version of an index label.
|
||||
*
|
||||
* @param string $labelType eg, 'site', 'period', etc.
|
||||
* @param string $label eg, '0', '1', '2012-01-01,2012-01-31', etc.
|
||||
* @return string
|
||||
*/
|
||||
private function prettifyIndexLabel($labelType, $label)
|
||||
{
|
||||
if ($labelType == self::TABLE_METADATA_PERIOD_INDEX) { // prettify period labels
|
||||
return $this->periods[$label]->getPrettyString();
|
||||
}
|
||||
return $label;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $keyMetadata
|
||||
* @param $result
|
||||
*/
|
||||
private function setTableMetadata($keyMetadata, $result)
|
||||
{
|
||||
if (!isset($keyMetadata[DataTableFactory::TABLE_METADATA_SITE_INDEX])) {
|
||||
$keyMetadata[DataTableFactory::TABLE_METADATA_SITE_INDEX] = reset($this->sitesId);
|
||||
}
|
||||
|
||||
if (!isset($keyMetadata[DataTableFactory::TABLE_METADATA_PERIOD_INDEX])) {
|
||||
reset($this->periods);
|
||||
$keyMetadata[DataTableFactory::TABLE_METADATA_PERIOD_INDEX] = key($this->periods);
|
||||
}
|
||||
|
||||
// Note: $result can be a DataTable\Map
|
||||
$result->filter(function ($table) use ($keyMetadata) {
|
||||
foreach ($keyMetadata as $name => $value) {
|
||||
$table->setMetadata($name, $value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $data
|
||||
* @return DataTable\Simple
|
||||
*/
|
||||
private function makeFromMetricsArray($data)
|
||||
{
|
||||
$table = new DataTable\Simple();
|
||||
|
||||
if (!empty($data)) {
|
||||
$table->setAllTableMetadata(DataCollection::getDataRowMetadata($data));
|
||||
|
||||
DataCollection::removeMetadataFromDataRow($data);
|
||||
|
||||
$table->addRow(new Row(array(Row::COLUMNS => $data)));
|
||||
} else {
|
||||
// if we're querying numeric data, we couldn't find any, and we're only
|
||||
// looking for one metric, add a row w/ one column w/ value 0. this is to
|
||||
// ensure that the PHP renderer outputs 0 when only one column is queried.
|
||||
// w/o this code, an empty array would be created, and other parts of Piwik
|
||||
// would break.
|
||||
if (count($this->dataNames) == 1
|
||||
&& $this->dataType == 'numeric'
|
||||
) {
|
||||
$name = reset($this->dataNames);
|
||||
$table->addRow(new Row(array(Row::COLUMNS => array($name => 0))));
|
||||
}
|
||||
}
|
||||
|
||||
$result = $table;
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue