1
|
<?php
|
2
|
|
3
|
/**
|
4
|
* @file
|
5
|
* Reusable API for l10n remote updates using $source objects
|
6
|
*
|
7
|
* These functions may not be safe for the installer as they use variables and report using watchdog
|
8
|
*/
|
9
|
|
10
|
/**
|
11
|
* Threshold for timestamp comparison.
|
12
|
*
|
13
|
* Eliminates a difference between the download time
|
14
|
* (Database: l10n_update_file.timestamp) and the actual .po file timestamp.
|
15
|
*/
|
16
|
define('L10N_UPDATE_TIMESTAMP_THRESHOLD', 2);
|
17
|
|
18
|
module_load_include('inc', 'l10n_update');
|
19
|
|
20
|
/**
|
21
|
* Fetch update information for all projects / all languages.
|
22
|
*
|
23
|
* @param boolean $refresh
|
24
|
* TRUE = refresh the release data.
|
25
|
* We refresh anyway if the data is older than a day.
|
26
|
*
|
27
|
* @return array
|
28
|
* Available releases indexed by project and language.
|
29
|
*/
|
30
|
function l10n_update_available_releases($refresh = FALSE) {
|
31
|
$frequency = variable_get('l10n_update_check_frequency', 0) * 24 * 3600;
|
32
|
if (!$refresh && ($cache = cache_get('l10n_update_available_releases', 'cache_l10n_update')) && (!$frequency || $cache->created > REQUEST_TIME - $frequency)) {
|
33
|
return $cache->data;
|
34
|
}
|
35
|
else {
|
36
|
$projects = l10n_update_get_projects(TRUE);
|
37
|
$languages = l10n_update_language_list();
|
38
|
$available = l10n_update_check_projects($projects, array_keys($languages));
|
39
|
cache_set('l10n_update_available_releases', $available, 'cache_l10n_update', $frequency ? REQUEST_TIME + $frequency : CACHE_PERMANENT);
|
40
|
return $available;
|
41
|
}
|
42
|
}
|
43
|
|
44
|
/**
|
45
|
* Check latest release for project, language.
|
46
|
*
|
47
|
* @param $projects
|
48
|
* Projects to check (objects).
|
49
|
* @param $languages
|
50
|
* Array of language codes to check, none to check all.
|
51
|
* @param $check_local
|
52
|
* Check local translation file.
|
53
|
* @param $check_remote
|
54
|
* Check remote translation file.
|
55
|
*
|
56
|
* @return array
|
57
|
* Available sources indexed by project, language.
|
58
|
*/
|
59
|
function l10n_update_check_projects($projects, $languages = NULL, $check_local = NULL, $check_remote = NULL) {
|
60
|
if (!isset($check_local)) {
|
61
|
$check_local = (bool) (variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_LOCAL);
|
62
|
}
|
63
|
if (!isset($check_remote)) {
|
64
|
$check_remote = (bool) (variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_REMOTE);
|
65
|
}
|
66
|
|
67
|
$languages = $languages ? $languages : array_keys(l10n_update_language_list());
|
68
|
$result = array();
|
69
|
foreach ($projects as $name => $project) {
|
70
|
foreach ($languages as $lang) {
|
71
|
$source = l10n_update_source_build($project, $lang);
|
72
|
if ($update = l10n_update_source_check($source, $check_local, $check_remote)) {
|
73
|
$result[$name][$lang] = $update;
|
74
|
}
|
75
|
}
|
76
|
}
|
77
|
return $result;
|
78
|
}
|
79
|
|
80
|
/**
|
81
|
* Compare available releases with history and get list of downloadable updates.
|
82
|
*
|
83
|
* @param $history
|
84
|
* Update history of projects.
|
85
|
* @param $available
|
86
|
* Available project releases.
|
87
|
* @return array
|
88
|
* Projects to be updated: 'not yet downloaded', 'newer timestamp available',
|
89
|
* 'new version available'.
|
90
|
* Up to date projects are not included in the array.
|
91
|
*/
|
92
|
function l10n_update_build_updates($history, $available) {
|
93
|
$updates = array();
|
94
|
foreach ($available as $name => $project_updates) {
|
95
|
foreach ($project_updates as $lang => $update) {
|
96
|
if (!empty($update->timestamp)) {
|
97
|
$current = !empty($history[$name][$lang]) ? $history[$name][$lang] : NULL;
|
98
|
// Add when not current, timestamp newer or version difers (newer version)
|
99
|
if (_l10n_update_source_compare($current, $update) == -1 || $current->version != $update->version) {
|
100
|
$updates[$name][$lang] = $update;
|
101
|
}
|
102
|
}
|
103
|
}
|
104
|
}
|
105
|
return $updates;
|
106
|
}
|
107
|
|
108
|
/**
|
109
|
* Check updates for active projects and languages.
|
110
|
*
|
111
|
* @param $count
|
112
|
* Number of package translations to check.
|
113
|
* @param $before
|
114
|
* Unix timestamp, check only updates that haven't been checked for this time.
|
115
|
* @param $limit
|
116
|
* Maximum number of updates to do. We check $count translations
|
117
|
* but we stop after we do $limit updates.
|
118
|
* @return array
|
119
|
*/
|
120
|
function l10n_update_check_translations($count, $before, $limit = 1) {
|
121
|
$projects = l10n_update_get_projects();
|
122
|
$updated = $checked = array();
|
123
|
|
124
|
// Select active projects x languages ordered by last checked time
|
125
|
$q = db_select('l10n_update_project', 'p');
|
126
|
$q->leftJoin('l10n_update_file', 'f', 'p.name = f.project');
|
127
|
$q->innerJoin('languages', 'l', 'l.language = f.language');
|
128
|
$q->condition('p.status', 1);
|
129
|
$q->condition('l.enabled', 1);
|
130
|
// If the file is not there, or it is there, but we did not check since $before.
|
131
|
$q->condition(db_or()->isNull('f.status')->condition(db_and()->condition('f.status', 1)->condition('f.last_checked', $before, '<')));
|
132
|
$q->range(0, $count);
|
133
|
$q->fields('p', array('name'));
|
134
|
$q->fields('f');
|
135
|
$q->addField('l', 'language', 'lang');
|
136
|
$q->orderBy('last_checked');
|
137
|
$result = $q->execute();
|
138
|
|
139
|
if ($result) {
|
140
|
$local = (bool) (variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_LOCAL);
|
141
|
$remote = (bool) (variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_REMOTE);
|
142
|
foreach ($result as $check) {
|
143
|
if (count($updated) >= $limit) {
|
144
|
break;
|
145
|
}
|
146
|
$checked[] = $check;
|
147
|
if (!empty($projects[$check->name])) {
|
148
|
$project = $projects[$check->name];
|
149
|
$update = NULL;
|
150
|
$source = l10n_update_source_build($project, $check->lang);
|
151
|
$current = $check->filename ? $check : NULL;
|
152
|
if ($available = l10n_update_source_check($source, $local, $remote)) {
|
153
|
if (!$current || _l10n_update_source_compare($current, $available) == -1 || $current->version != $available->version) {
|
154
|
$update = $available;
|
155
|
}
|
156
|
}
|
157
|
if ($update) {
|
158
|
// The update functions will update data and timestamps too
|
159
|
l10n_update_source_update($update, variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP));
|
160
|
$updated[] = $update;
|
161
|
}
|
162
|
elseif ($current) {
|
163
|
// No update available, just update timestamp for this row
|
164
|
db_update('l10n_update_file')
|
165
|
->fields(array(
|
166
|
'last_checked' => REQUEST_TIME,
|
167
|
))
|
168
|
->condition('project', $current->project)
|
169
|
->condition('language', $current->language)
|
170
|
->execute();
|
171
|
}
|
172
|
elseif ($source) {
|
173
|
// Create a new record just for keeping last checked time
|
174
|
$source->last_checked = REQUEST_TIME;
|
175
|
drupal_write_record('l10n_update_file', $source);
|
176
|
}
|
177
|
}
|
178
|
}
|
179
|
}
|
180
|
return array($checked, $updated);
|
181
|
}
|
182
|
|
183
|
/**
|
184
|
* Build abstract translation source, to be mapped to a file or a download.
|
185
|
*
|
186
|
* @param $project
|
187
|
* Project object.
|
188
|
* @param $langcode
|
189
|
* Language code.
|
190
|
* @param $filename
|
191
|
* File name of translation file. May contains placeholders.
|
192
|
* @return object
|
193
|
* Source object, which may have these properties:
|
194
|
* - 'project': Project name.
|
195
|
* - 'language': Language code.
|
196
|
* - 'type': Source type 'download' or 'localfile'.
|
197
|
* - 'uri': Local file path.
|
198
|
* - 'fileurl': Remote file URL for downloads.
|
199
|
* - 'filename': File name.
|
200
|
* - 'keep': TRUE to keep the downloaded file.
|
201
|
* - 'timestamp': Last update time of the file.
|
202
|
*/
|
203
|
function l10n_update_source_build($project, $langcode, $filename = L10N_UPDATE_DEFAULT_FILENAME) {
|
204
|
$source = clone $project;
|
205
|
$source->project = $project->name;
|
206
|
$source->language = $langcode;
|
207
|
$source->filename = l10n_update_build_string($source, $filename);
|
208
|
return $source;
|
209
|
}
|
210
|
|
211
|
/**
|
212
|
* Check local and remote sources for the file.
|
213
|
*
|
214
|
* @param $source
|
215
|
* Translation source object.
|
216
|
* @see l10n_update_source_build()
|
217
|
* @param $check_local
|
218
|
* File object of local translation file.
|
219
|
* @param $check_remote
|
220
|
* File object of remote translation file.
|
221
|
* @return object
|
222
|
* File object of most recent translation; local or remote.
|
223
|
*/
|
224
|
function l10n_update_source_check($source, $check_local = TRUE, $check_remote = TRUE) {
|
225
|
$local = $remote = NULL;
|
226
|
if ($check_local) {
|
227
|
$check = clone $source;
|
228
|
if (l10n_update_source_check_file($check)) {
|
229
|
$local = $check;
|
230
|
}
|
231
|
}
|
232
|
if ($check_remote) {
|
233
|
$check = clone $source;
|
234
|
if (l10n_update_source_check_download($check)) {
|
235
|
$remote = $check;
|
236
|
}
|
237
|
}
|
238
|
// Get remote if newer than local only, they both can be empty
|
239
|
return _l10n_update_source_compare($local, $remote) < 0 ? $remote : $local;
|
240
|
}
|
241
|
|
242
|
/**
|
243
|
* Check remote file object.
|
244
|
*
|
245
|
* @param $source
|
246
|
* Remote translation file object. The object will be update
|
247
|
* with data of the remote file:
|
248
|
* - 'type': Fixed value 'download'.
|
249
|
* - 'fileurl': File name and path.
|
250
|
* - 'timestamp': Last updated time.
|
251
|
* @see l10n_update_source_build()
|
252
|
* @return object
|
253
|
* An object containing the HTTP request headers, response code, headers,
|
254
|
* data, redirect status and updated timestamp.
|
255
|
* NULL if failure.
|
256
|
*/
|
257
|
function l10n_update_source_check_download($source) {
|
258
|
$url = l10n_update_build_string($source, $source->l10n_path);
|
259
|
$result = l10n_update_http_check($url);
|
260
|
if ($result && !empty($result->updated)) {
|
261
|
$source->type = 'download';
|
262
|
// There may have been redirects so we store the resulting url
|
263
|
$source->fileurl = isset($result->redirect_url) ? $result->redirect_url : $url;
|
264
|
$source->timestamp = $result->updated;
|
265
|
return $result;
|
266
|
}
|
267
|
}
|
268
|
|
269
|
/**
|
270
|
* Check whether we've got the file in the filesystem under 'translations'.
|
271
|
*
|
272
|
* It will search, similar to modules and themes:
|
273
|
* - translations
|
274
|
* - sites/all/translations
|
275
|
* - sites/mysite/translations
|
276
|
*
|
277
|
* Using name as the key will return just the last one found.
|
278
|
*
|
279
|
* @param $source
|
280
|
* Translation file object. The object will be updated with data of local file.
|
281
|
* - 'type': Fixed value 'localfile'.
|
282
|
* - 'uri': File name and path.
|
283
|
* - 'timestamp': Last updated time.
|
284
|
* @see l10n_update_source_build()
|
285
|
* @param $directory
|
286
|
* Files directory.
|
287
|
* @return Object
|
288
|
* File object (filename, basename, name)
|
289
|
* NULL if failure.
|
290
|
*/
|
291
|
function l10n_update_source_check_file($source, $directory = 'translations') {
|
292
|
$filename = '/' . preg_quote($source->filename) . '$/';
|
293
|
|
294
|
// Using the 'name' key will return
|
295
|
if ($files = drupal_system_listing($filename, $directory, 'name', 0)) {
|
296
|
$file = current($files);
|
297
|
$source->type = 'localfile';
|
298
|
$source->uri = $file->uri;
|
299
|
$source->timestamp = filemtime($file->uri);
|
300
|
return $file;
|
301
|
}
|
302
|
}
|
303
|
|
304
|
/**
|
305
|
* Download and import or just import source, depending on type.
|
306
|
*
|
307
|
* @param $source
|
308
|
* Translation source object with information about the file location.
|
309
|
* Object will be updated with :
|
310
|
* - 'last_checked': Timestamp of current time;
|
311
|
* - 'import_date': Timestamp of current time;
|
312
|
* @param $mode
|
313
|
* Download mode. How to treat exising and modified translations.
|
314
|
* @return boolean
|
315
|
* TRUE on success, NULL on failure.
|
316
|
*/
|
317
|
function l10n_update_source_update($source, $mode) {
|
318
|
if ($source->type == 'localfile' || l10n_update_source_download($source)) {
|
319
|
if (l10n_update_source_import($source, $mode)) {
|
320
|
l10n_update_source_history($source);
|
321
|
return TRUE;
|
322
|
}
|
323
|
}
|
324
|
}
|
325
|
|
326
|
/**
|
327
|
* Import source into locales table.
|
328
|
*
|
329
|
* @param $source
|
330
|
* Translation source object with information about the file location.
|
331
|
* Object will be updated with :
|
332
|
* - 'last_checked': Timestamp of current time;
|
333
|
* - 'import_date': Timestamp of current time;
|
334
|
* @param $mode
|
335
|
* Download mode. How to treat exising and modified translations.
|
336
|
* @return boolean
|
337
|
* Result array on success, FALSE on failure.
|
338
|
*/
|
339
|
function l10n_update_source_import($source, $mode) {
|
340
|
if (!empty($source->uri) && $result = l10n_update_import_file($source->uri, $source->language, $mode)) {
|
341
|
$source->last_checked = REQUEST_TIME;
|
342
|
|
343
|
// We override the file timestamp here. The default file time stamp is the
|
344
|
// release date from the l.d.o server. We change the timestamp to the
|
345
|
// creation time on the webserver. On multi sites that share a common
|
346
|
// sites/all/translations directory, the sharing sites use the local file
|
347
|
// creation date as release date. Without this correction the local
|
348
|
// file is always newer than the l.d.o. file, which results in unnecessary
|
349
|
// translation import.
|
350
|
$source->timestamp = time();
|
351
|
|
352
|
return $result;
|
353
|
}
|
354
|
}
|
355
|
|
356
|
/**
|
357
|
* Download source file from remote server.
|
358
|
*
|
359
|
* If succesful this function returns the downloaded file in two ways:
|
360
|
* - As a temporary $file object
|
361
|
* - As a file path on the $source->uri property.
|
362
|
*
|
363
|
* @param $source
|
364
|
* Source object with all parameters
|
365
|
* - 'fileurl': url to download.
|
366
|
* - 'uri': alternate destination. If not present a temporary file
|
367
|
* will be used and the path returned here.
|
368
|
* @return object
|
369
|
* $file object if download successful.
|
370
|
*/
|
371
|
function l10n_update_source_download($source) {
|
372
|
if (!empty($source->uri)) {
|
373
|
$destination = $source->uri;
|
374
|
}
|
375
|
elseif ($directory = variable_get('l10n_update_download_store', '')) {
|
376
|
$destination = $directory . '/' . $source->filename;
|
377
|
}
|
378
|
else {
|
379
|
$destination = NULL;
|
380
|
}
|
381
|
if ($file = l10n_update_download_file($source->fileurl, $destination)) {
|
382
|
$source->uri = $file;
|
383
|
return $file;
|
384
|
}
|
385
|
}
|
386
|
|
387
|
/**
|
388
|
* Update the file history table and delete the file if temporary.
|
389
|
*
|
390
|
* @param $file
|
391
|
* Source object representing the file just imported or downloaded.
|
392
|
*/
|
393
|
function l10n_update_source_history($file) {
|
394
|
// Update history table
|
395
|
l10n_update_file_history($file);
|
396
|
|
397
|
// If it's a downloaded file and not marked for keeping, delete the file.
|
398
|
if ($file->type == 'download' && empty($file->keep)) {
|
399
|
file_unmanaged_delete($file->uri);
|
400
|
$file->uri = '';
|
401
|
}
|
402
|
}
|
403
|
|
404
|
/**
|
405
|
* Compare two update sources, looking for the newer one (bigger timestamp).
|
406
|
*
|
407
|
* This function can be used as a callback to compare two source objects.
|
408
|
*
|
409
|
* @param $current
|
410
|
* Source object of current project.
|
411
|
* @param $update
|
412
|
* Source object of available update.
|
413
|
* @return integer
|
414
|
* - '-1': $current < $update OR $current is missing
|
415
|
* - '0': $current == $update OR both $current and $updated are missing
|
416
|
* - '1': $current > $update OR $update is missing
|
417
|
*/
|
418
|
function _l10n_update_source_compare($current, $update) {
|
419
|
if ($current && $update) {
|
420
|
if (abs($current->timestamp - $update->timestamp) < L10N_UPDATE_TIMESTAMP_THRESHOLD) {
|
421
|
return 0;
|
422
|
}
|
423
|
else {
|
424
|
return $current->timestamp > $update->timestamp ? 1 : -1;
|
425
|
}
|
426
|
}
|
427
|
elseif ($current && !$update) {
|
428
|
return 1;
|
429
|
}
|
430
|
elseif (!$current && $update) {
|
431
|
return -1;
|
432
|
}
|
433
|
else {
|
434
|
return 0;
|
435
|
}
|
436
|
}
|
437
|
|
438
|
/**
|
439
|
* Prepare update list.
|
440
|
*
|
441
|
* @param $updates
|
442
|
* Array of update sources that may be indexed in multiple ways.
|
443
|
* @param $projects
|
444
|
* Array of project names to be included, others will be filtered out.
|
445
|
* @param $languages
|
446
|
* Array of language codes to be included, others will be filtered out.
|
447
|
* @return array
|
448
|
* Plain array of filtered updates with directory applied.
|
449
|
*/
|
450
|
function _l10n_update_prepare_updates($updates, $projects = NULL, $languages = NULL) {
|
451
|
$result = array();
|
452
|
foreach ($updates as $key => $update) {
|
453
|
if (is_array($update)) {
|
454
|
// It is a sub array of updates yet, process and merge
|
455
|
$result = array_merge($result, _l10n_update_prepare_updates($update, $projects, $languages));
|
456
|
}
|
457
|
elseif ((!$projects || in_array($update->project, $projects)) && (!$languages || in_array($update->language, $languages))) {
|
458
|
$directory = variable_get('l10n_update_download_store', '');
|
459
|
if ($directory && empty($update->uri)) {
|
460
|
// If we have a destination folder set just if we have no uri
|
461
|
if (empty($update->uri)) {
|
462
|
$update->uri = $directory . '/' . $update->filename;
|
463
|
$update->keep = TRUE;
|
464
|
}
|
465
|
}
|
466
|
$result[] = $update;
|
467
|
}
|
468
|
}
|
469
|
return $result;
|
470
|
}
|
471
|
|
472
|
/**
|
473
|
* Language refresh. Runs a batch for loading the selected languages.
|
474
|
*
|
475
|
* To be used after adding a new language.
|
476
|
*
|
477
|
* @param $languages
|
478
|
* Array of language codes to check and download.
|
479
|
*/
|
480
|
function l10n_update_language_refresh($languages) {
|
481
|
$projects = l10n_update_get_projects();
|
482
|
if ($available = l10n_update_check_projects($projects, $languages)) {
|
483
|
$history = l10n_update_get_history();
|
484
|
if ($updates = l10n_update_build_updates($history, $available)) {
|
485
|
module_load_include('batch.inc', 'l10n_update');
|
486
|
// Filter out updates in other languages. If no languages, all of them will be updated
|
487
|
$updates = _l10n_update_prepare_updates($updates);
|
488
|
$batch = l10n_update_batch_multiple($updates, variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP));
|
489
|
batch_set($batch);
|
490
|
}
|
491
|
}
|
492
|
}
|