PageRenderTime 47ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/stats/core/Segment.php

https://bitbucket.org/webstar1987923/mycampaignsio
PHP | 353 lines | 171 code | 46 blank | 136 comment | 27 complexity | e8c630588c231d431250a894508ef243 MD5 | raw file
Possible License(s): BSD-3-Clause, MPL-2.0-no-copyleft-exception, GPL-3.0, GPL-2.0, WTFPL, BSD-2-Clause, LGPL-2.1, Apache-2.0, MIT, AGPL-3.0
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik;
  10. use Exception;
  11. use Piwik\ArchiveProcessor\Rules;
  12. use Piwik\Container\StaticContainer;
  13. use Piwik\DataAccess\LogQueryBuilder;
  14. use Piwik\Plugins\API\API;
  15. use Piwik\Segment\SegmentExpression;
  16. /**
  17. * Limits the set of visits Piwik uses when aggregating analytics data.
  18. *
  19. * A segment is a condition used to filter visits. They can, for example,
  20. * select visits that have a specific browser or come from a specific
  21. * country, or both.
  22. *
  23. * Plugins that aggregate data stored in Piwik can support segments by
  24. * using this class when generating aggregation SQL queries.
  25. *
  26. * ### Examples
  27. *
  28. * **Basic usage**
  29. *
  30. * $idSites = array(1,2,3);
  31. * $segmentStr = "browserCode==ff;countryCode==CA";
  32. * $segment = new Segment($segmentStr, $idSites);
  33. *
  34. * $query = $segment->getSelectQuery(
  35. * $select = "table.col1, table2.col2",
  36. * $from = array("table", "table2"),
  37. * $where = "table.col3 = ?",
  38. * $bind = array(5),
  39. * $orderBy = "table.col1 DESC",
  40. * $groupBy = "table2.col2"
  41. * );
  42. *
  43. * Db::fetchAll($query['sql'], $query['bind']);
  44. *
  45. * **Creating a _null_ segment**
  46. *
  47. * $idSites = array(1,2,3);
  48. * $segment = new Segment('', $idSites);
  49. * // $segment->getSelectQuery will return a query that selects all visits
  50. *
  51. * @api
  52. */
  53. class Segment
  54. {
  55. /**
  56. * @var SegmentExpression
  57. */
  58. protected $segmentExpression = null;
  59. /**
  60. * @var string
  61. */
  62. protected $string = null;
  63. /**
  64. * @var array
  65. */
  66. protected $idSites = null;
  67. /**
  68. * @var LogQueryBuilder
  69. */
  70. private $segmentQueryBuilder;
  71. /**
  72. * Truncate the Segments to 8k
  73. */
  74. const SEGMENT_TRUNCATE_LIMIT = 8192;
  75. /**
  76. * Constructor.
  77. *
  78. * @param string $segmentCondition The segment condition, eg, `'browserCode=ff;countryCode=CA'`.
  79. * @param array $idSites The list of sites the segment will be used with. Some segments are
  80. * dependent on the site, such as goal segments.
  81. * @throws
  82. */
  83. public function __construct($segmentCondition, $idSites)
  84. {
  85. $this->segmentQueryBuilder = StaticContainer::get('Piwik\DataAccess\LogQueryBuilder');
  86. $segmentCondition = trim($segmentCondition);
  87. if (!SettingsPiwik::isSegmentationEnabled()
  88. && !empty($segmentCondition)
  89. ) {
  90. throw new Exception("The Super User has disabled the Segmentation feature.");
  91. }
  92. // First try with url decoded value. If that fails, try with raw value.
  93. // If that also fails, it will throw the exception
  94. try {
  95. $this->initializeSegment(urldecode($segmentCondition), $idSites);
  96. } catch (Exception $e) {
  97. $this->initializeSegment($segmentCondition, $idSites);
  98. }
  99. }
  100. /**
  101. * Returns the segment expression.
  102. * @return SegmentExpression
  103. * @api since Piwik 3.2.0
  104. */
  105. public function getSegmentExpression()
  106. {
  107. return $this->segmentExpression;
  108. }
  109. private function getAvailableSegments()
  110. {
  111. // segment metadata
  112. if (empty($this->availableSegments)) {
  113. $this->availableSegments = API::getInstance()->getSegmentsMetadata($this->idSites, $_hideImplementationData = false);
  114. }
  115. return $this->availableSegments;
  116. }
  117. private function getSegmentByName($name)
  118. {
  119. $segments = $this->getAvailableSegments();
  120. foreach ($segments as $segment) {
  121. if ($segment['segment'] == $name && !empty($name)) {
  122. // check permission
  123. if (isset($segment['permission']) && $segment['permission'] != 1) {
  124. throw new NoAccessException("You do not have enough permission to access the segment " . $name);
  125. }
  126. return $segment;
  127. }
  128. }
  129. throw new Exception("Segment '$name' is not a supported segment.");
  130. }
  131. /**
  132. * @param $string
  133. * @param $idSites
  134. * @throws Exception
  135. */
  136. protected function initializeSegment($string, $idSites)
  137. {
  138. // As a preventive measure, we restrict the filter size to a safe limit
  139. $string = substr($string, 0, self::SEGMENT_TRUNCATE_LIMIT);
  140. $this->string = $string;
  141. $this->idSites = $idSites;
  142. $segment = new SegmentExpression($string);
  143. $this->segmentExpression = $segment;
  144. // parse segments
  145. $expressions = $segment->parseSubExpressions();
  146. $expressions = $this->getExpressionsWithUnionsResolved($expressions);
  147. // convert segments name to sql segment
  148. // check that user is allowed to view this segment
  149. // and apply a filter to the value to match if necessary (to map DB fields format)
  150. $cleanedExpressions = array();
  151. foreach ($expressions as $expression) {
  152. $operand = $expression[SegmentExpression::INDEX_OPERAND];
  153. $cleanedExpression = $this->getCleanedExpression($operand);
  154. $expression[SegmentExpression::INDEX_OPERAND] = $cleanedExpression;
  155. $cleanedExpressions[] = $expression;
  156. }
  157. $segment->setSubExpressionsAfterCleanup($cleanedExpressions);
  158. }
  159. private function getExpressionsWithUnionsResolved($expressions)
  160. {
  161. $expressionsWithUnions = array();
  162. foreach ($expressions as $expression) {
  163. $operand = $expression[SegmentExpression::INDEX_OPERAND];
  164. $name = $operand[SegmentExpression::INDEX_OPERAND_NAME];
  165. $availableSegment = $this->getSegmentByName($name);
  166. if (!empty($availableSegment['unionOfSegments'])) {
  167. $count = 0;
  168. foreach ($availableSegment['unionOfSegments'] as $segmentNameOfUnion) {
  169. $count++;
  170. $operator = SegmentExpression::BOOL_OPERATOR_OR; // we connect all segments within that union via OR
  171. if ($count === count($availableSegment['unionOfSegments'])) {
  172. $operator = $expression[SegmentExpression::INDEX_BOOL_OPERATOR];
  173. }
  174. $operand[SegmentExpression::INDEX_OPERAND_NAME] = $segmentNameOfUnion;
  175. $expressionsWithUnions[] = array(
  176. SegmentExpression::INDEX_BOOL_OPERATOR => $operator,
  177. SegmentExpression::INDEX_OPERAND => $operand
  178. );
  179. }
  180. } else {
  181. $expressionsWithUnions[] = array(
  182. SegmentExpression::INDEX_BOOL_OPERATOR => $expression[SegmentExpression::INDEX_BOOL_OPERATOR],
  183. SegmentExpression::INDEX_OPERAND => $operand
  184. );
  185. }
  186. }
  187. return $expressionsWithUnions;
  188. }
  189. /**
  190. * Returns `true` if the segment is empty, `false` if otherwise.
  191. */
  192. public function isEmpty()
  193. {
  194. return $this->segmentExpression->isEmpty();
  195. }
  196. /**
  197. * Detects whether the Piwik instance is configured to be able to archive this segment. It checks whether the segment
  198. * will be either archived via browser or cli archiving. It does not check if the segment has been archived. If you
  199. * want to know whether the segment has been archived, the actual report data needs to be requested.
  200. *
  201. * This method does not take any date/period into consideration. Meaning a Piwik instance might be able to archive
  202. * this segment in general, but not for a certain period if eg the archiving of range dates is disabled.
  203. *
  204. * @return bool
  205. */
  206. public function willBeArchived()
  207. {
  208. if ($this->isEmpty()) {
  209. return true;
  210. }
  211. $idSites = $this->idSites;
  212. if (!is_array($idSites)) {
  213. $idSites = array($this->idSites);
  214. }
  215. return Rules::isRequestAuthorizedToArchive()
  216. || Rules::isBrowserArchivingAvailableForSegments()
  217. || Rules::isSegmentPreProcessed($idSites, $this);
  218. }
  219. protected $availableSegments = array();
  220. protected function getCleanedExpression($expression)
  221. {
  222. $name = $expression[SegmentExpression::INDEX_OPERAND_NAME];
  223. $matchType = $expression[SegmentExpression::INDEX_OPERAND_OPERATOR];
  224. $value = $expression[SegmentExpression::INDEX_OPERAND_VALUE];
  225. $segment = $this->getSegmentByName($name);
  226. $sqlName = $segment['sqlSegment'];
  227. if ($matchType != SegmentExpression::MATCH_IS_NOT_NULL_NOR_EMPTY
  228. && $matchType != SegmentExpression::MATCH_IS_NULL_OR_EMPTY) {
  229. if (isset($segment['sqlFilterValue'])) {
  230. $value = call_user_func($segment['sqlFilterValue'], $value, $segment['sqlSegment']);
  231. }
  232. // apply presentation filter
  233. if (isset($segment['sqlFilter'])) {
  234. $value = call_user_func($segment['sqlFilter'], $value, $segment['sqlSegment'], $matchType, $name);
  235. if(is_null($value)) { // null is returned in TableLogAction::getIdActionFromSegment()
  236. return array(null, $matchType, null);
  237. }
  238. // sqlFilter-callbacks might return arrays for more complex cases
  239. // e.g. see TableLogAction::getIdActionFromSegment()
  240. if (is_array($value) && isset($value['SQL'])) {
  241. // Special case: returned value is a sub sql expression!
  242. $matchType = SegmentExpression::MATCH_ACTIONS_CONTAINS;
  243. }
  244. }
  245. }
  246. return array($sqlName, $matchType, $value);
  247. }
  248. /**
  249. * Returns the segment condition.
  250. *
  251. * @return string
  252. */
  253. public function getString()
  254. {
  255. return $this->string;
  256. }
  257. /**
  258. * Returns a hash of the segment condition, or the empty string if the segment
  259. * condition is empty.
  260. *
  261. * @return string
  262. */
  263. public function getHash()
  264. {
  265. if (empty($this->string)) {
  266. return '';
  267. }
  268. // normalize the string as browsers may send slightly different payloads for the same archive
  269. $normalizedSegmentString = urldecode($this->string);
  270. return md5($normalizedSegmentString);
  271. }
  272. /**
  273. * Extend an SQL query that aggregates data over one of the 'log_' tables with segment expressions.
  274. *
  275. * @param string $select The select clause. Should NOT include the **SELECT** just the columns, eg,
  276. * `'t1.col1 as col1, t2.col2 as col2'`.
  277. * @param array $from Array of table names (without prefix), eg, `array('log_visit', 'log_conversion')`.
  278. * @param false|string $where (optional) Where clause, eg, `'t1.col1 = ? AND t2.col2 = ?'`.
  279. * @param array|string $bind (optional) Bind parameters, eg, `array($col1Value, $col2Value)`.
  280. * @param false|string $orderBy (optional) Order by clause, eg, `"t1.col1 ASC"`.
  281. * @param false|string $groupBy (optional) Group by clause, eg, `"t2.col2"`.
  282. * @param int $limit Limit number of result to $limit
  283. * @param int $offset Specified the offset of the first row to return
  284. * @param int If set to value >= 1 then the Select query (and All inner queries) will be LIMIT'ed by this value.
  285. * Use only when you're not aggregating or it will sample the data.
  286. * @return string The entire select query.
  287. */
  288. public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false, $limit = 0, $offset = 0)
  289. {
  290. $segmentExpression = $this->segmentExpression;
  291. $limitAndOffset = null;
  292. if($limit > 0) {
  293. $limitAndOffset = (int) $offset . ', ' . (int) $limit;
  294. }
  295. return $this->segmentQueryBuilder->getSelectQueryString($segmentExpression, $select, $from, $where, $bind,
  296. $groupBy, $orderBy, $limitAndOffset);
  297. }
  298. /**
  299. * Returns the segment string.
  300. *
  301. * @return string
  302. */
  303. public function __toString()
  304. {
  305. return (string) $this->getString();
  306. }
  307. }