PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.0.7
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.0.7
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Segment.php
matomo / app / core Last commit date
API 2 years ago Access 2 years ago Application 2 years ago Archive 2 years ago ArchiveProcessor 2 years ago Archiver 2 years ago AssetManager 2 years ago Auth 2 years ago Category 2 years ago Changes 2 years ago CliMulti 2 years ago Columns 2 years ago Concurrency 2 years ago Config 2 years ago Container 2 years ago CronArchive 2 years ago DataAccess 2 years ago DataFiles 2 years ago DataTable 2 years ago Db 2 years ago DeviceDetector 2 years ago Email 2 years ago Exception 2 years ago Http 2 years ago Intl 2 years ago Log 2 years ago Mail 2 years ago Measurable 2 years ago Menu 2 years ago Metrics 2 years ago Notification 2 years ago Period 2 years ago Plugin 2 years ago ProfessionalServices 2 years ago Report 2 years ago ReportRenderer 2 years ago Scheduler 2 years ago Segment 2 years ago Session 2 years ago Settings 2 years ago Tracker 2 years ago Translation 2 years ago Twig 2 years ago UpdateCheck 2 years ago Updater 2 years ago Updates 2 years ago Validators 2 years ago View 2 years ago ViewDataTable 2 years ago Visualization 2 years ago Widget 2 years ago .htaccess 2 years ago Access.php 2 years ago Archive.php 2 years ago ArchiveProcessor.php 2 years ago AssetManager.php 2 years ago Auth.php 2 years ago AuthResult.php 2 years ago BaseFactory.php 2 years ago Cache.php 2 years ago CacheId.php 2 years ago CliMulti.php 2 years ago Common.php 2 years ago Config.php 2 years ago Console.php 2 years ago Context.php 2 years ago Cookie.php 2 years ago CronArchive.php 2 years ago DI.php 2 years ago DataArray.php 2 years ago DataTable.php 2 years ago Date.php 2 years ago Db.php 2 years ago DbHelper.php 2 years ago Development.php 2 years ago ErrorHandler.php 2 years ago EventDispatcher.php 2 years ago ExceptionHandler.php 2 years ago FileIntegrity.php 2 years ago Filechecks.php 2 years ago Filesystem.php 2 years ago FrontController.php 2 years ago Http.php 2 years ago IP.php 2 years ago Log.php 2 years ago LogDeleter.php 2 years ago Mail.php 2 years ago Metrics.php 2 years ago NoAccessException.php 2 years ago Nonce.php 2 years ago Notification.php 2 years ago NumberFormatter.php 2 years ago Option.php 2 years ago Period.php 2 years ago Piwik.php 2 years ago Plugin.php 2 years ago Profiler.php 2 years ago ProxyHeaders.php 2 years ago ProxyHttp.php 2 years ago QuickForm2.php 2 years ago RankingQuery.php 2 years ago ReportRenderer.php 2 years ago Request.php 2 years ago Segment.php 2 years ago Sequence.php 2 years ago Session.php 2 years ago SettingsPiwik.php 2 years ago SettingsServer.php 2 years ago Singleton.php 2 years ago Site.php 2 years ago SiteContentDetector.php 2 years ago SupportedBrowser.php 2 years ago TCPDF.php 2 years ago Theme.php 2 years ago Timer.php 2 years ago Tracker.php 2 years ago Twig.php 2 years ago Unzip.php 2 years ago UpdateCheck.php 2 years ago Updater.php 2 years ago UpdaterErrorException.php 2 years ago Updates.php 2 years ago Url.php 2 years ago UrlHelper.php 2 years ago Version.php 2 years ago View.php 2 years ago bootstrap.php 2 years ago dispatch.php 2 years ago testMinimumPhpVersion.php 2 years ago
Segment.php
625 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 *
9 */
10 namespace Piwik;
11
12 use Exception;
13 use Piwik\API\Request;
14 use Piwik\ArchiveProcessor\Rules;
15 use Piwik\Cache as PiwikCache;
16 use Piwik\Container\StaticContainer;
17 use Piwik\DataAccess\LogQueryBuilder;
18 use Piwik\Plugins\SegmentEditor\SegmentEditor;
19 use Piwik\Segment\SegmentExpression;
20 use Piwik\Plugins\SegmentEditor\Model as SegmentEditorModel;
21 use Piwik\Segment\SegmentsList;
22 /**
23 * Limits the set of visits Piwik uses when aggregating analytics data.
24 *
25 * A segment is a condition used to filter visits. They can, for example,
26 * select visits that have a specific browser or come from a specific
27 * country, or both.
28 *
29 * Plugins that aggregate data stored in Piwik can support segments by
30 * using this class when generating aggregation SQL queries.
31 *
32 * ### Examples
33 *
34 * **Basic usage**
35 *
36 * $idSites = array(1,2,3);
37 * $segmentStr = "browserCode==ff;countryCode==CA";
38 * $segment = new Segment($segmentStr, $idSites);
39 *
40 * $query = $segment->getSelectQuery(
41 * $select = "table.col1, table2.col2",
42 * $from = array("table", "table2"),
43 * $where = "table.col3 = ?",
44 * $bind = array(5),
45 * $orderBy = "table.col1 DESC",
46 * $groupBy = "table2.col2"
47 * );
48 *
49 * Db::fetchAll($query['sql'], $query['bind']);
50 *
51 * **Creating a _null_ segment**
52 *
53 * $idSites = array(1,2,3);
54 * $segment = new Segment('', $idSites);
55 * // $segment->getSelectQuery will return a query that selects all visits
56 *
57 * @api
58 */
59 class Segment
60 {
61 /**
62 * @var SegmentExpression
63 */
64 protected $segmentExpression = null;
65 /**
66 * @var string
67 */
68 protected $string = null;
69 /**
70 * @var string
71 */
72 protected $originalString = null;
73 /**
74 * @var array
75 */
76 protected $idSites = null;
77 /**
78 * @var Date
79 */
80 protected $startDate = null;
81 /**
82 * @var Date
83 */
84 protected $endDate = null;
85 /**
86 * @var LogQueryBuilder
87 */
88 private $segmentQueryBuilder;
89 /**
90 * @var bool
91 */
92 private $isSegmentEncoded;
93 /**
94 * Truncate the Segments to 8k
95 */
96 const SEGMENT_TRUNCATE_LIMIT = 8192;
97 const CACHE_KEY = 'segmenthashes';
98 const SEGMENT_HAS_BUILT_CACHE_KEY = 'segmenthashbuilt';
99 /**
100 * Constructor.
101 *
102 * When using segments that contain a != or !@ condition on a non visit dimension (e.g. action, conversion, ...) it
103 * is needed to use a subquery to get correct results. To avoid subqueries that fetch too many data it's required to
104 * set a startDate and/or an endDate in this case. That date will be used to limit the subquery (along with possibly
105 * given idSites). If no startDate and endDate is given for such a segment it will generate a query that directly
106 * joins the according tables, but trigger a php warning as results might be incorrect.
107 *
108 * @param string $segmentCondition The segment condition, eg, `'browserCode=ff;countryCode=CA'`.
109 * @param array $idSites The list of sites the segment will be used with. Some segments are
110 * dependent on the site, such as goal segments.
111 * @param Date|null $startDate start date used to limit subqueries
112 * @param Date|null $endDate end date used to limit subqueries
113 * @throws
114 */
115 public function __construct($segmentCondition, $idSites, \Piwik\Date $startDate = null, \Piwik\Date $endDate = null)
116 {
117 $this->segmentQueryBuilder = StaticContainer::get('Piwik\\DataAccess\\LogQueryBuilder');
118 $segmentCondition = trim($segmentCondition ?: '');
119 if (!\Piwik\SettingsPiwik::isSegmentationEnabled() && !empty($segmentCondition)) {
120 throw new Exception("The Super User has disabled the Segmentation feature.");
121 }
122 $this->originalString = $segmentCondition;
123 if ($startDate instanceof \Piwik\Date) {
124 $this->startDate = $startDate;
125 }
126 if ($endDate instanceof \Piwik\Date) {
127 $this->endDate = $endDate;
128 }
129 // The segment expression can be urlencoded. Unfortunately, both the encoded and decoded versions
130 // can usually be parsed successfully. To pick the right one, we try both and pick the one w/ more
131 // successfully parsed subexpressions.
132 $subexpressionsDecoded = 0;
133 if (urldecode($segmentCondition) !== $segmentCondition) {
134 try {
135 $this->initializeSegment(urldecode($segmentCondition), $idSites);
136 $subexpressionsDecoded = $this->segmentExpression->getSubExpressionCount();
137 } catch (Exception $e) {
138 // ignore
139 }
140 }
141 $subexpressionsRaw = 0;
142 try {
143 $this->initializeSegment($segmentCondition, $idSites);
144 $subexpressionsRaw = $this->segmentExpression->getSubExpressionCount();
145 } catch (Exception $e) {
146 // ignore
147 }
148 if ($subexpressionsRaw > $subexpressionsDecoded) {
149 // segment initialized above
150 $this->isSegmentEncoded = false;
151 } else {
152 $this->initializeSegment(urldecode($segmentCondition), $idSites);
153 $this->isSegmentEncoded = true;
154 }
155 }
156 /**
157 * Returns the segment expression.
158 * @return SegmentExpression
159 * @api since Piwik 3.2.0
160 */
161 public function getSegmentExpression()
162 {
163 return $this->segmentExpression;
164 }
165 /**
166 * @throws Exception
167 */
168 private function getAvailableSegments()
169 {
170 // start cache
171 $cache = PiwikCache::getTransientCache();
172 //covert cache id
173 $cacheId = 'API.getSegmentsMetadata.' . \Piwik\SettingsPiwik::getPiwikInstanceId() . '.' . implode(",", $this->idSites);
174 //fetch cache lockId
175 $availableSegments = $cache->fetch($cacheId);
176 // segment metadata
177 if (empty($availableSegments)) {
178 $availableSegments = Request::processRequest('API.getSegmentsMetadata', array('idSites' => $this->idSites, '_hideImplementationData' => 0, 'filter_limit' => -1, 'filter_offset' => 0, '_showAllSegments' => 1), []);
179 // index by segment name
180 $availableSegments = array_column($availableSegments, null, 'segment');
181 // remove segments we don't have permission to use
182 foreach ($availableSegments as $segment => $segmentInfo) {
183 if (isset($segmentInfo['permission']) && $segmentInfo['permission'] != 1) {
184 $availableSegments[$segment] = null;
185 }
186 }
187 $cache->save($cacheId, $availableSegments);
188 }
189 return $availableSegments;
190 }
191 private function getSegmentByName($name)
192 {
193 $segments = $this->getAvailableSegments();
194 if (array_key_exists($name, $segments)) {
195 if ($segments[$name] === null) {
196 throw new \Piwik\NoAccessException("You do not have enough permission to access the segment " . $name);
197 }
198 return $segments[$name];
199 }
200 throw new Exception("Segment '{$name}' is not a supported segment.");
201 }
202 /**
203 * @param $string
204 * @param $idSites
205 * @throws Exception
206 */
207 protected function initializeSegment($string, $idSites)
208 {
209 // As a preventive measure, we restrict the filter size to a safe limit
210 $string = substr($string, 0, self::SEGMENT_TRUNCATE_LIMIT);
211 $this->string = $string;
212 if (empty($idSites)) {
213 $idSites = [];
214 } else {
215 if (!is_array($idSites)) {
216 $idSites = [$idSites];
217 }
218 }
219 $this->idSites = $idSites;
220 $segment = new SegmentExpression($string);
221 $this->segmentExpression = $segment;
222 // parse segments
223 $expressions = $segment->parseSubExpressions();
224 $expressions = $this->getExpressionsWithUnionsResolved($expressions);
225 $expressions = $this->mergeSubqueryExpressionsInTree($expressions);
226 // convert segments name to sql segment
227 // check that user is allowed to view this segment
228 // and apply a filter to the value to match if necessary (to map DB fields format)
229 $cleanedExpressions = array_map(function (array $orExpressions) {
230 return array_map(function (array $operand) {
231 return $this->getCleanedExpression($operand);
232 }, $orExpressions);
233 }, $expressions);
234 $segment->setSubExpressionsAfterCleanup($cleanedExpressions);
235 }
236 private function getExpressionsWithUnionsResolved(array $expressions) : array
237 {
238 $expressionsWithUnions = array_map(function ($orExpressions) {
239 $mappedOrExpressions = [];
240 foreach ($orExpressions as $operand) {
241 $name = $operand[SegmentExpression::INDEX_OPERAND_NAME];
242 $availableSegment = $this->getSegmentByName($name);
243 // We leave segments using !@ and != operands untouched for segments not on log_visit table as they will be build using a subquery
244 if (!$this->doesSegmentNeedSubquery($operand[SegmentExpression::INDEX_OPERAND_OPERATOR], $name) && !empty($availableSegment['unionOfSegments'])) {
245 foreach ($availableSegment['unionOfSegments'] as $segmentNameOfUnion) {
246 $operand[SegmentExpression::INDEX_OPERAND_NAME] = $segmentNameOfUnion;
247 $mappedOrExpressions[] = $operand;
248 }
249 } else {
250 $mappedOrExpressions[] = $operand;
251 }
252 }
253 return $mappedOrExpressions;
254 }, $expressions);
255 return $expressionsWithUnions;
256 }
257 private function isVisitSegment($name)
258 {
259 $availableSegment = $this->getSegmentByName($name);
260 if (!empty($availableSegment['unionOfSegments'])) {
261 foreach ($availableSegment['unionOfSegments'] as $segmentNameOfUnion) {
262 $unionSegment = $this->getSegmentByName($segmentNameOfUnion);
263 if (strpos($unionSegment['sqlSegment'], 'log_visit.') === 0) {
264 return true;
265 }
266 }
267 } else {
268 if (strpos($availableSegment['sqlSegment'], 'log_visit.') === 0) {
269 return true;
270 }
271 }
272 return false;
273 }
274 private function doesSegmentNeedSubquery($operator, $segmentName)
275 {
276 $requiresSubQuery = in_array($operator, [SegmentExpression::MATCH_DOES_NOT_CONTAIN, SegmentExpression::MATCH_NOT_EQUAL]) && !$this->isVisitSegment($segmentName);
277 if ($requiresSubQuery && empty($this->startDate) && empty($this->endDate)) {
278 if (\Piwik\Development::isEnabled()) {
279 $e = new Exception();
280 \Piwik\Log::warning("Avoiding segment subquery due to missing start date and/or an end date. Please ensure a start date and/or end date is set when initializing a segment if it's used to build a query. Stacktrace:\n" . $e->getTraceAsString());
281 }
282 return false;
283 }
284 return $requiresSubQuery;
285 }
286 private function getInvertedOperatorForSubQuery($operator)
287 {
288 if ($operator === SegmentExpression::MATCH_DOES_NOT_CONTAIN) {
289 return SegmentExpression::MATCH_CONTAINS;
290 } else {
291 if ($operator === SegmentExpression::MATCH_NOT_EQUAL) {
292 return SegmentExpression::MATCH_EQUAL;
293 }
294 }
295 throw new Exception("Operator not support for subqueries");
296 }
297 /**
298 * Returns `true` if the segment is empty, `false` if otherwise.
299 */
300 public function isEmpty()
301 {
302 return $this->segmentExpression->isEmpty();
303 }
304 /**
305 * Detects whether the Piwik instance is configured to be able to archive this segment. It checks whether the segment
306 * will be either archived via browser or cli archiving. It does not check if the segment has been archived. If you
307 * want to know whether the segment has been archived, the actual report data needs to be requested.
308 *
309 * This method does not take any date/period into consideration. Meaning a Piwik instance might be able to archive
310 * this segment in general, but not for a certain period if eg the archiving of range dates is disabled.
311 *
312 * @return bool
313 */
314 public function willBeArchived()
315 {
316 if ($this->isEmpty()) {
317 return true;
318 }
319 $idSites = $this->idSites;
320 return Rules::isRequestAuthorizedToArchive() || Rules::isBrowserArchivingAvailableForSegments() || Rules::isSegmentPreProcessed($idSites, $this);
321 }
322 protected function getCleanedExpression(array $expression) : array
323 {
324 $name = $expression[SegmentExpression::INDEX_OPERAND_NAME];
325 $matchType = $expression[SegmentExpression::INDEX_OPERAND_OPERATOR];
326 $value = $expression[SegmentExpression::INDEX_OPERAND_VALUE];
327 if (empty($this->idSites)) {
328 $segmentsList = SegmentsList::get();
329 } else {
330 $segmentsList = \Piwik\Context::changeIdSite(implode(',', $this->idSites), function () {
331 return SegmentsList::get();
332 });
333 }
334 $segmentObject = $segmentsList->getSegment($name);
335 $sqlName = $segmentObject ? $segmentObject->getSqlSegment() : null;
336 $joinTable = null;
337 if ($segmentObject && $segmentObject->dimension && $segmentObject->dimension->getDbColumnJoin()) {
338 $join = $segmentObject->dimension->getDbColumnJoin();
339 $dbDiscriminator = $segmentObject->dimension->getDbDiscriminator();
340 // we append alias since an archive query may add the table with a different join. we could eg add $table_$segmentName but
341 // then we would join an extra table per segment when we ideally want to join each table only once. However, we still need
342 // to see which table/column it joins to join it accurately each table extra if the same table is joined with different columns;
343 $tableAlias = $join->getTable() . '_segment_' . str_replace('.', '', $sqlName ?: '');
344 $joinTable = ['table' => $join->getTable(), 'tableAlias' => $tableAlias, 'field' => $tableAlias . '.' . $join->getTargetColumn(), 'joinOn' => $sqlName . ' = ' . $tableAlias . '.' . $join->getColumn()];
345 if ($dbDiscriminator) {
346 $joinTable['discriminator'] = $tableAlias . '.' . $dbDiscriminator->getColumn() . ' = \'' . $dbDiscriminator->getValue() . '\'';
347 }
348 }
349 if ($matchType == SegmentExpression::MATCH_IDVISIT_NOT_IN) {
350 $segmentObj = new \Piwik\Segment($value, $this->idSites, $this->startDate, $this->endDate);
351 $select = 'log_visit.idvisit';
352 $from = 'log_visit';
353 $datetimeField = 'visit_last_action_time';
354 $where = [];
355 $bind = [];
356 if (!empty($this->idSites)) {
357 $where[] = "{$from}.idsite IN (" . \Piwik\Common::getSqlStringFieldsArray($this->idSites) . ")";
358 $bind = $this->idSites;
359 }
360 if ($this->startDate instanceof \Piwik\Date) {
361 $where[] = "{$from}.{$datetimeField} >= ?";
362 $bind[] = $this->startDate->toString(\Piwik\Date::DATE_TIME_FORMAT);
363 }
364 if ($this->endDate instanceof \Piwik\Date) {
365 $where[] = "{$from}.{$datetimeField} <= ?";
366 $bind[] = $this->endDate->toString(\Piwik\Date::DATE_TIME_FORMAT);
367 }
368 $logQueryBuilder = StaticContainer::get('Piwik\\DataAccess\\LogQueryBuilder');
369 $forceGroupByBackup = $logQueryBuilder->getForcedInnerGroupBySubselect();
370 $logQueryBuilder->forceInnerGroupBySubselect(LogQueryBuilder::FORCE_INNER_GROUP_BY_NO_SUBSELECT);
371 $query = $segmentObj->getSelectQuery($select, $from, implode(' AND ', $where), $bind);
372 $logQueryBuilder->forceInnerGroupBySubselect($forceGroupByBackup);
373 return ['log_visit.idvisit', SegmentExpression::MATCH_ACTIONS_NOT_CONTAINS, $query, null, null];
374 }
375 if (empty($segmentObject)) {
376 throw new Exception("Segment '{$name}' is not a supported segment.");
377 }
378 $segment = $this->getSegmentByName($name);
379 if ($matchType != SegmentExpression::MATCH_IS_NOT_NULL_NOR_EMPTY && $matchType != SegmentExpression::MATCH_IS_NULL_OR_EMPTY) {
380 if (isset($segment['sqlFilterValue'])) {
381 $value = call_user_func($segment['sqlFilterValue'], $value, $segment['sqlSegment']);
382 }
383 // apply presentation filter
384 if (isset($segment['sqlFilter'])) {
385 $value = call_user_func($segment['sqlFilter'], $value, $segment['sqlSegment'], $matchType, $name);
386 if (is_null($value)) {
387 // null is returned in TableLogAction::getIdActionFromSegment()
388 return array(null, $matchType, null, null, $segment);
389 }
390 // sqlFilter-callbacks might return arrays for more complex cases
391 // e.g. see TableLogAction::getIdActionFromSegment()
392 if (is_array($value) && isset($value['SQL'])) {
393 // Special case: returned value is a sub sql expression!
394 $matchType = SegmentExpression::MATCH_ACTIONS_CONTAINS;
395 $joinTable = null;
396 }
397 if (is_array($value) && isset($value['value'])) {
398 $value = $value['value'];
399 $joinTable = !empty($value['joinTable']);
400 }
401 }
402 }
403 return array($sqlName, $matchType, $value, $joinTable, $segment);
404 }
405 /**
406 * Returns the segment condition.
407 *
408 * @return string
409 */
410 public function getString()
411 {
412 return $this->string;
413 }
414 /**
415 * Returns a hash of the segment condition, or the empty string if the segment
416 * condition is empty.
417 *
418 * @return string
419 */
420 public function getHash()
421 {
422 if (empty($this->string)) {
423 return '';
424 }
425 return self::getSegmentHash($this->string);
426 }
427 public static function getSegmentHash($definition)
428 {
429 $cache = \Piwik\Cache::getEagerCache();
430 $cacheKey = self::CACHE_KEY . md5($definition);
431 if ($cache->contains($cacheKey)) {
432 return $cache->fetch($cacheKey);
433 }
434 $defaultHash = md5(urldecode($definition));
435 // if the cache for segments already built, but this segment was not found,
436 // we return the default segment, this can be a segment from url or
437 // something like "visitorType==new"
438 if ($cache->contains(self::SEGMENT_HAS_BUILT_CACHE_KEY)) {
439 return $defaultHash;
440 }
441 // the segment hash is not built yet, let's do it
442 $model = new SegmentEditorModel();
443 $segments = $model->getAllSegmentsAndIgnoreVisibility();
444 foreach ($segments as $segment) {
445 $cacheKeyTemp = self::CACHE_KEY . md5($segment['definition']);
446 $cache->save($cacheKeyTemp, $segment['hash']);
447 $cacheKeyTemp = self::CACHE_KEY . md5(urldecode($segment['definition']));
448 $cache->save($cacheKeyTemp, $segment['hash']);
449 $cacheKeyTemp = self::CACHE_KEY . md5(urlencode($segment['definition']));
450 $cache->save($cacheKeyTemp, $segment['hash']);
451 }
452 $cache->save(self::SEGMENT_HAS_BUILT_CACHE_KEY, true);
453 // if we found the segment, return it's hash, but maybe this
454 // segment is not stored in the db, return the default
455 if ($cache->contains($cacheKey)) {
456 return $cache->fetch($cacheKey);
457 }
458 return $defaultHash;
459 }
460 /**
461 * Extend an SQL query that aggregates data over one of the 'log_' tables with segment expressions.
462 *
463 * @param string $select The select clause. Should NOT include the **SELECT** just the columns, eg,
464 * `'t1.col1 as col1, t2.col2 as col2'`.
465 * @param array|string $from Array of table names (without prefix), eg, `array('log_visit', 'log_conversion')`.
466 * @param false|string $where (optional) Where clause, eg, `'t1.col1 = ? AND t2.col2 = ?'`.
467 * @param array|string $bind (optional) Bind parameters, eg, `array($col1Value, $col2Value)`.
468 * @param false|string $orderBy (optional) Order by clause, eg, `"t1.col1 ASC"`.
469 * @param false|string $groupBy (optional) Group by clause, eg, `"t2.col2"`.
470 * @param int $limit Limit number of result to $limit
471 * @param int $offset Specified the offset of the first row to return
472 * @param bool $forceGroupBy Force the group by and not using a subquery. Note: This may make the query slower see https://github.com/matomo-org/matomo/issues/9200#issuecomment-183641293
473 * A $groupBy value needs to be set for this to work.
474 * @param int If set to value >= 1 then the Select query (and All inner queries) will be LIMIT'ed by this value.
475 * Use only when you're not aggregating or it will sample the data.
476 * @return array The entire select query.
477 */
478 public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false, $limit = 0, $offset = 0, $forceGroupBy = false)
479 {
480 $segmentExpression = $this->segmentExpression;
481 $limitAndOffset = null;
482 if ($limit > 0) {
483 $limitAndOffset = (int) $offset . ', ' . (int) $limit;
484 }
485 try {
486 if ($forceGroupBy && $groupBy) {
487 $this->segmentQueryBuilder->forceInnerGroupBySubselect(LogQueryBuilder::FORCE_INNER_GROUP_BY_NO_SUBSELECT);
488 }
489 $result = $this->segmentQueryBuilder->getSelectQueryString($segmentExpression, $select, $from, $where, $bind, $groupBy, $orderBy, $limitAndOffset);
490 } catch (Exception $e) {
491 if ($forceGroupBy && $groupBy) {
492 $this->segmentQueryBuilder->forceInnerGroupBySubselect('');
493 }
494 throw $e;
495 }
496 if ($forceGroupBy && $groupBy) {
497 $this->segmentQueryBuilder->forceInnerGroupBySubselect('');
498 }
499 return $result;
500 }
501 /**
502 * Returns the segment string.
503 *
504 * @return string
505 */
506 public function __toString()
507 {
508 return (string) $this->getString();
509 }
510 /**
511 * Combines this segment with another segment condition, if the segment condition is not already
512 * in the segment.
513 *
514 * The combination is naive in that it does not take order of operations into account.
515 *
516 * @param string $segment
517 * @param string $operator The operator to use. Should be either SegmentExpression::AND_DELIMITER
518 * or SegmentExpression::OR_DELIMITER.
519 * @param string $segmentCondition The segment condition to add.
520 * @return string
521 * @throws Exception
522 */
523 public static function combine($segment, $operator, $segmentCondition)
524 {
525 if (empty($segment)) {
526 return $segmentCondition;
527 }
528 if (empty($segmentCondition) || self::containsCondition($segment, $operator, $segmentCondition)) {
529 return $segment;
530 }
531 return $segment . $operator . $segmentCondition;
532 }
533 private static function containsCondition($segment, $operator, $segmentCondition)
534 {
535 // check when segment/condition are of same encoding
536 return strpos($segment, $operator . $segmentCondition) !== false || strpos($segment, $segmentCondition . $operator) !== false || strpos($segment, urlencode($operator . $segmentCondition)) !== false || strpos($segment, urlencode($segmentCondition . $operator)) !== false || strpos($segment, $operator . urlencode($segmentCondition)) !== false || strpos($segment, urlencode($segmentCondition) . $operator) !== false || strpos($segment, $operator . urldecode($segmentCondition)) !== false || strpos($segment, urldecode($segmentCondition) . $operator) !== false || $segment === $segmentCondition || $segment === urlencode($segmentCondition) || $segment === urldecode($segmentCondition);
537 }
538 public function getStoredSegmentName($idSite)
539 {
540 $segment = $this->getString();
541 if (empty($segment)) {
542 return \Piwik\Piwik::translate('SegmentEditor_DefaultAllVisits');
543 }
544 $availableSegments = SegmentEditor::getAllSegmentsForSite($idSite);
545 $foundStoredSegment = null;
546 foreach ($availableSegments as $storedSegment) {
547 if ($storedSegment['definition'] == $segment || $storedSegment['definition'] == urldecode($segment) || $storedSegment['definition'] == urlencode($segment) || $storedSegment['definition'] == $this->originalString || $storedSegment['definition'] == urldecode($this->originalString) || $storedSegment['definition'] == urlencode($this->originalString)) {
548 $foundStoredSegment = $storedSegment;
549 }
550 }
551 if (isset($foundStoredSegment)) {
552 return $foundStoredSegment['name'];
553 }
554 return $this->isSegmentEncoded ? urldecode($segment) : $segment;
555 }
556 public function getOriginalString()
557 {
558 return $this->originalString;
559 }
560 /**
561 * Build subqueries for segments that are not on log_visit table but use !@ or != as operator
562 * This is required to ensure segments like actionUrl!@value really do not include any visit having an action containing `value`
563 *
564 * Adjacent segment conditions that both require subqueries are merged here into single NOT IN sql subqueries,
565 * which improves performance.
566 *
567 * Subquery segment conditions that are next to each other in a chain of OR's are merged together and
568 * subquery segment conditions that are next to each other in a chain of AND's, but are also alone and not
569 * a part of an OR expression, are merged.
570 *
571 * The operands for the merged conditions in the parsed intermediate structure use the special MATCH_IDVISIT_NOT_IN
572 * operator.
573 */
574 private function mergeSubqueryExpressionsInTree(array $tree) : array
575 {
576 $andExpressions = array_map(function ($orExpressions) {
577 return $this->mergeSubqueryExpressionsInExpr($orExpressions, false);
578 }, $tree);
579 $mappedAndExpressions = $this->mergeSubqueryExpressionsInExpr($andExpressions, true);
580 return $mappedAndExpressions;
581 }
582 private function mergeSubqueryExpressionsInExpr(array $expressions, bool $isAndChain) : array
583 {
584 // nothing to merge if there's only one expression
585 if (!$isAndChain && count($expressions) <= 1) {
586 return $expressions;
587 }
588 $mappedExpressions = [];
589 $idvisitNotInExpressions = [];
590 foreach ($expressions as $childExpressionsOrOperand) {
591 // if this is an AND chain w/ more than one sub-expression being OR-ed together, we can't do anything about the NOT IN subqueries there
592 if ($isAndChain && count($childExpressionsOrOperand) > 1) {
593 $mappedExpressions[] = $childExpressionsOrOperand;
594 continue;
595 }
596 $operand = $isAndChain ? $childExpressionsOrOperand[0] : $childExpressionsOrOperand;
597 $name = $operand[SegmentExpression::INDEX_OPERAND_NAME];
598 $matchType = $operand[SegmentExpression::INDEX_OPERAND_OPERATOR];
599 $value = $operand[SegmentExpression::INDEX_OPERAND_VALUE];
600 if (!$this->doesSegmentNeedSubquery($matchType, $name)) {
601 $mappedExpressions[] = $childExpressionsOrOperand;
602 continue;
603 }
604 // if the segment is pageTitle!=def, then NOT IN sql will have to be idvisit NOT IN (... WHERE pageTitle == def),
605 // so we must invert the operator before we create a MATCH_IDVISIT_NOT_IN operand below
606 $operator = $this->getInvertedOperatorForSubQuery($matchType);
607 $idvisitNotInExpressions[] = $name . $operator . $this->escapeSegmentValue($value);
608 }
609 if (!empty($idvisitNotInExpressions)) {
610 $newOperand = [SegmentExpression::INDEX_OPERAND_NAME => null, SegmentExpression::INDEX_OPERAND_OPERATOR => SegmentExpression::MATCH_IDVISIT_NOT_IN, SegmentExpression::INDEX_OPERAND_VALUE => implode($isAndChain ? SegmentExpression::OR_DELIMITER : SegmentExpression::AND_DELIMITER, $idvisitNotInExpressions)];
611 $mappedExpressions[] = $isAndChain ? [$newOperand] : $newOperand;
612 }
613 return $mappedExpressions;
614 }
615 /**
616 * Escapes segment expression delimiters in a segment value with a backslash if not already done.
617 */
618 private function escapeSegmentValue(string $value) : string
619 {
620 $delimiterPattern = SegmentExpression::AND_DELIMITER . SegmentExpression::OR_DELIMITER;
621 $pattern = '/((?<!\\\\)[' . preg_quote($delimiterPattern) . '])/';
622 return preg_replace($pattern, '\\\\$1', $value);
623 }
624 }
625