-
Notifications
You must be signed in to change notification settings - Fork 0
/
optimizely.module
290 lines (241 loc) · 9.59 KB
/
optimizely.module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
<?php
/**
* @file
* Optimizely module.
*
* Originally for Visual Website Optimizer by
* Awesome Software, http://www.awesome-software.net/ and
* Ted Cooper (ELC) http://drupal.org/user/784944
*
* Ported to Optimizely by netstudio.gr and Yannis Karampelas
* (http://drupal.org/user/1145950).
*
* Adds Optimizely javascript (snippet) to the page which loads the A/B test
* from the optimizely.com website.
*
* 7.x-2.x Functionality added to support multiple project entries from an
* Optimizely account. Each entry specifies target paths to include the
* Optimizely projects on the specific site paths. Targeting paths
* eliminates the need to load Optimizely tests on every page of a site.
*
* 7.x-2.x by Darren "Dee" Lee (DeeZone: http://drupal.org/user/288060)
* and Peter Lehrer (plehrer: http://drupal.org/user/2257350)
* Sponsored by DoSomething.org (http://www.dosomething.org)
*
* 8.x-dev by Earl Fong (tz_earl: https://www.drupal.org/user/2531114/)
*/
use Drupal\Core\Url;
/**
* Implements hook_help().
*
* Help text related to the module's functionality and use.
*/
function optimizely_help($route_name, $arg) {
switch ($route_name) {
case 'help.page.optimizely':
return t('<p><a href="http://optimize.ly/OZRdc0">Optimizely</a> is a third
party service to add A/B testing to a web site. The tests are applied to
the site by loading javascript snippets generated by the Optimizely web
site. The generated javascript files are applied to certain paths on the
site based on Project entries managed by the Optimizely module. To start
to apply the general, sitewide Optimizely javascript file the
<a href="@settings">Optimizely account ID</a> must be entered in the
module administration page.</p>
<p>Enable or disable each project entry to apply the project settings
while not needing to remove the actual entry. The default entry can be
disabled when additional project entries are made with more specific
settings. This can include using the orginal Project Code.</p>',
['@settings' => Url::fromRoute('optimizely.settings')->toString()]);
case 'optimizely.listing':
return t('<p>A listing of the Optimizely projects. Each entry can be
enabled / disabled for specific or wildcard paths. Enabled entries are
highlighted in green while disabled entries are in red. The top,
"Default" entry cannot be deleted but its settings can be adjusted or
completely disabled.</p>
<p>The goal of having multiple projects is to minimize the size of the
optimizely hosted javascript file. If all experiments are contained in a
single file and processed on every page load there may be an issue with
increased page load time. Having multiple projects and loading them on
specific paths that apply to the experiments helps to minimize the size
of the file and eliminate processing unused javascript on the user\'s
browser.</p>');
case 'optimizely.add_update':
case 'optimizely.add_update.oid':
return t('Add or edit specific project entries. Each entry should have an
Optimizely project / experiment assigned to it, as well as a range of
website paths where the Optimizely javascript hosted file should be
included.');
case 'optimizely.settings':
return t('Add the Optimizely account ID supplied by the Optimizely website.
The account ID is essential to setting up the initial sitewide default
project entry.');
}
}
/**
* Implements hook_theme().
*
* All of the template definitions. All related templates can be found in
* /template in the module folder. The template layout can be overridden
* by the site theme by copying these template files into theme directory.
*/
function optimizely_theme($existing, $type, $theme, $path) {
return [
'optimizely_account_settings_form' => [
'render element' => 'form',
'template' => 'optimizely-account-settings-form',
],
'optimizely_add_update_form' => [
'render element' => 'form',
'template' => 'optimizely-add-update-form',
],
];
}
/**
* Implements hook_page_attachments().
*
* Checks each page that is about to be rendered as to
* whether to insert a snippet of Optimizely code or not.
*/
function optimizely_page_attachments(array &$page) {
$url = Url::fromRoute('<current>');
$current_path = $url->getInternalPath();
$current_path = _check_path($current_path);
$current_path_alias = _lookup_path_alias($current_path);
$add_snippet = FALSE;
// Load all entries in the optimizely table.
$query = \Drupal::database()->select('optimizely', 'o', ['target' => 'slave'])
->fields('o')
->condition('o.enabled', 1, '=')
->orderBy('oid');
// Fetch the result set.
$result = $query->execute();
if (!$result) {
return;
}
// Query results found.
$cache_tag = NULL;
// Loop through each row of the found results.
while ($project = $result->fetchAssoc()) {
// Only process the entries that are enabled.
if (!$project['enabled']) {
continue;
}
// Target paths from database for project.
$project_paths = unserialize($project['path']);
// Remember to check all paths for alias or sytem URL values.
foreach ($project_paths as $proj_path) {
// If the Optimizely snippet is added, use the project's path as one
// of the cache tags for this page. Cache invalidation is triggered
// via cache tags when project paths are modified or deleted. Spaces
// are not allowed in cache_tags, but all other characters seem to be
// allowed, including *. See class CacheRefresher which does the
// qinvalidating.
$cache_tag = 'optimizely:' . $proj_path;
// Remove parameters, if any.
if (strpos($proj_path, '?') !== FALSE) {
$proj_path = substr($proj_path, 0, strpos($proj_path, '?'));
}
// Look for sitewide wild card.
if ($proj_path == '*') {
$add_snippet = TRUE;
break 2;
}
// Look for wildcard match that is not sitewide.
if (strpos($proj_path, '*') !== FALSE) {
// Remove wildcard, get base path(s)
$proj_path = substr($proj_path, 0, -2);
// Look for wildcard match.
if (stripos($current_path, $proj_path) === 0 ||
stripos(_lookup_path_alias($current_path), $proj_path) === 0 ||
stripos(_lookup_system_path($current_path), $proj_path) === 0) {
$add_snippet = TRUE;
break 2;
}
}
// Build a string containing as many as three possible variants
// of $proj_path.
$paths_to_check = $proj_path;
$proj_path_source = _lookup_system_path($proj_path);
if ($proj_path_source) {
$paths_to_check .= "\n" . $proj_path_source;
}
$proj_path_alias = _lookup_path_alias($proj_path);
if ($proj_path_alias) {
$paths_to_check .= "\n" . $proj_path_alias;
}
// See if there is a match against any variant of $proj_path.
if (\Drupal::service('path.matcher')->matchPath($current_path, $paths_to_check) ||
\Drupal::service('path.matcher')->matchPath($current_path_alias, $paths_to_check)) {
$add_snippet = TRUE;
break 2;
}
} // foreach
}
if ($add_snippet) {
// Add javascript call to page markup.
$snippet_url = '//cdn.optimizely.com/js/' . $project['project_code'] . '.js';
$page['#attached']['html_head'][] = [
[
'#tag' => 'script',
'#attributes' => [
'type' => 'text/javascript',
'src' => $snippet_url,
],
'#value' => '',
],
'optimizely-snippet',
];
$page['#cache']['tags'][] = $cache_tag;
}
// For every page, add all other cache tags that might possibly cause it
// to be invalidated.
// Site-wide wildcard.
$page['#cache']['tags'][] = 'optimizely:*';
// Non site-wide wildcards. Repeat for every directory level.
$dirname = pathinfo($current_path, PATHINFO_DIRNAME);
while ($dirname && $dirname != '/' && $dirname != '\\') {
$page['#cache']['tags'][] = 'optimizely:' . $dirname . '/*';
$dirname = pathinfo($dirname, PATHINFO_DIRNAME);
}
// The specific page url.
$page['#cache']['tags'][] = 'optimizely:' . $current_path;
// Finally, if there is an alias for the page, tag it.
if ($current_path_alias) {
$page['#cache']['tags'][] = 'optimizely:' . $current_path_alias;
}
}
// The following three functions are redundant with and copied verbatim
// from trait LookupPath because I haven't been able to re-use the trait
// in this .module file.
/**
* Helper function to lookup a path alias, given a path.
*
* This function acts as an adapter and passes back a return value
* like those of drupal_lookup_path(), which has been removed
* as of Drupal 8.
*/
function _lookup_path_alias($path) {
$path = _check_path($path);
$alias = \Drupal::service('path.alias_manager')->getAliasByPath($path);
return (strcmp($alias, $path) == 0) ? FALSE : $alias;
}
/**
* Helper function to lookup a system path, given a path alias.
*
* This function acts as an adapter and passes back a return value
* like those of drupal_lookup_path(), which has been removed
* as of Drupal 8.
*/
function _lookup_system_path($alias) {
$alias = _check_path($alias);
$path = \Drupal::service('path.alias_manager')->getPathByAlias($alias);
return (strcmp($path, $alias) == 0) ? FALSE : $path;
}
/**
* Ensure that $path starts with a forward slash.
*
* The alias_manager requires it.
*/
function _check_path($path) {
return ($path[0] == '/') ? $path : '/' . $path;
}