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