-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtimeline.module
556 lines (446 loc) · 16.4 KB
/
timeline.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
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
<?php
// $Id$
/**
* @file
*
* This file contains the code for archives.
*
*/
module_load_include('inc', 'timeline', '/timeline.field');
/*
* Downloads the image
*
* @param $url
* a string containing the url to download
* @param $subdir
* the subdir, where this file should be downloaded to
*
* @return
* an array in the form
* array(
* errmsg => an array of error messages
* httpcode => the http code returned in the response header
* file => a drupal file object
* )
* it is usually safe to check if the errmsg array is empty and the httpcode
* is in the range 200-300
*
*/
function _timeline_download_image($url, $subdir) {
global $user;
$retval = array(
'errmsg' => array(),
);
$maxsize = variable_get('timeline_max_image_size', 1000000);
$ch = curl_init($url);
if ($ch === FALSE)
{
$retval['errmsg'][] = 'Could not open connection.';
}
else {
// ToDo: munge the filename because of security
$directory = 'public://timeline/images/' . $subdir;
file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
$tempnam = file_create_filename(basename($url), $directory);
$fh = fopen($tempnam, 'w');
curl_setopt($ch, CURLOPT_FAILONERROR, TRUE); // this works
curl_setopt($ch, CURLOPT_HTTPHEADER, array("User-Agent: Timeline Download Script; See http://tiva.geo.uzh.ch;") );
curl_setopt($ch, CURLOPT_FILE, $fh);
curl_setopt($ch, CURLOPT_TIMEOUT, 30); // Set a timeout (for the whole operation)
curl_setopt($ch, CURLOPT_RANGE,"-$maxsize"); // Limit the maximum file size
/* @TODO: what actually is done here is, a Range-Header is sent in the request.
* So we just download the first $maxsize bytes of the file, which obviously is not
* what we want. Probably this should be done with a write callback function.
* (see: CURLOPT_WRITEFUNCTION at http://php.net/manual/en/function.curl-setopt.php)
*/
$curlresult = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
fclose($fh);
$file = new stdClass();
$file->uri = $tempnam;
$file->filename = basename($file->uri);
$file->filemime = file_get_mimetype($file->uri);
$file->uid = $user->uid;
// The file is saved with status = 0. That means it is a temporary file and
// will be deleted after a certain time. That's okay for preview, but in case
// of a download we need to set status to 1 to get the system to keep it.
file_save($file);
if (!$curlresult) {
$retval['errmsg'][] = t('The connection to :url could not be opened.', array('url' => $url));
}
if ($httpcode >= 300) {
$retval['errmsg'][] = t('File could not be downloaded. (HTTP Code %httpcode)', array('%httpcode' => $httpcode));
}
$retval['httpcode'] = $httpcode;
$retval['file'] = $file;
}
return $retval;
}
/*
*
*/
function _timeline_save_image($request_time, $file, $archive, $error) {
$file->status = FILE_STATUS_PERMANENT;
file_save($file);
$archive->archive['preview_picture'] = $file->fid;
node_save($archive);
db_insert('records')
->fields(array(
'nid' => $archive->nid,
'fid' => $file->fid,
'request_time' => $request_time,
'saved_time' => time(),
'error' => $error,
))
->execute();
}
/**
* Check that the filename ends with an allowed extension.
* Copied from modules/file/file.inc
*
* @param $filename
* A filename to check.
* @param $extensions
* A string with a space separated list of allowed extensions.
*
* @return
* An array. If the file extension is not allowed, it will contain an error
* message.
*
* @see hook_file_validate()
*/
function timeline_validate_extensions($filename, $extensions) {
$errors = array();
$regex = '/\.(' . preg_replace('/ +/', '|', preg_quote($extensions)) . ')$/i';
if (!preg_match($regex, $filename)) {
$errors[] = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => $extensions));
}
return $errors;
}
function timeline_form_archive_node_form_alter(&$form, $form_state, $form_id) {
// Only enable this fields if we're creating a new node
$disabled = isset($form['#node']->nid);
$form['archive'] = array(
'#type' => 'fieldset',
'#title' => t('Base Information'),
'#collapsible' => TRUE,
'#collapsed' => $disabled,
'#tree' => TRUE,
'#weight' => -5,
);
$form['archive']['url'] = array (
'#type' => 'textfield',
'#title' => t('URL'),
'#description' => t('The URL of the image to be downloaded. Normally ending with .jpg, .jpeg or .png.'),
'#default_value' => isset($form['#node']->archive['url']) ? $form['#node']->archive['url'] : '',
'#disabled' => $disabled,
'#ajax' => array(
'callback' => '_ajax_check_url_callback',
'wrapper' => 'url-check-div',
'method' => 'replace',
'effect' => 'fade',
),
'#required' => true,
'#element_validate' => array('timeline_url_validate'),
'#suffix' => '<div id="url-check-div"></div>',
);
module_load_include('module', 'taxonomy', '/taxonomy');
$taxonomy_field['settings']['allowed_values'][0]['vocabulary'] = 'timeline_frequencies';
$taxonomy_field['settings']['allowed_values'][0]['parent'] = 0;
$form['archive']['frequency'] = array (
'#type' => 'select',
'#title' => t('Interval'),
'#description' => t('In this interval images will be downloaded.'),
'#default_value' => isset($form['#node']->archive['frequency']) ? $form['#node']->archive['frequency'] : '',
'#disabled' => $disabled,
'#options' => taxonomy_allowed_values($taxonomy_field),
);
module_load_include('module', 'date_popup', '/date_popup');
$form['archive']['basetime'] = array_pop(date_popup_element_info());
$form['archive']['basetime'] += array (
'#type' => 'date_combo',
'#title' => t('Basetime'),
'#description' => t('The basetime is the time you want your pictures to be taken. It is used in low-frequency archives do determine the time of the picture. E.g. If in a daily archive the pictures should be taken midnight, morning or noon.'),
'#default_value' => isset($form['#node']->archive['basetime']) ? $form['#node']->archive['basetime'] : null,
'#disabled' => $disabled,
);
// Unset, as this relies on this widget being part of a field
unset($form['archive']['basetime']['#element_validate']['date_combo_validate']);
}
/**
* hook_nodeapi() has been replaced in Drupal 7 with a set of different hooks
* providing the same or improved functionality.
*
* The replacement functions providing access to events ocurred to content in
* Drupal is listed below, and detailled in the following location:
* http://api.drupal.org/api/group/hooks/7
* or in the node API declaration file: modules/node/node.api.php
*
* hook_node_access() - Control access to a node.
* hook_node_access_records() - Set permissions for a node to be written to the database.
* hook_node_access_records_alter() - Alter permissions for a node before it is written to the database.
* hook_node_build_alter() - The node content was built, the module may modify the structured content.
* hook_node_delete() - Act on node deletion.
* hook_node_grants() - Inform the node access system what permissions the user has.
* hook_node_grants_alter() - Alter user access rules when trying to view, edit or delete a node.
* hook_node_info() - Defines module-provided node types.
* hook_node_insert() - Respond to node insertion.
* hook_node_load() - Act on node objects when loaded.
* hook_node_operations() - Add mass node operations.
* hook_node_prepare() - The node is about to be shown on the add/edit form.
* hook_node_prepare_translation() - The node is being cloned for translation.
* hook_node_presave() - The node passed validation and is about to be saved.
* hook_node_revision_delete() - A revision of the node is deleted.
* hook_node_search_result() - The node is being displayed as a search result.
* hook_node_type_delete() - Act on node type deletion.
* hook_node_type_insert() - Act on node type creation.
* hook_node_type_update() - Act on node type changes.
* hook_node_update() - The node being updated.
* hook_node_update_index() - The node is being indexed.
* hook_node_validate() - The user has finished editing the node and is previewing or submitting it.
* hook_node_view() - The node content is being assembled before rendering.
*
*/
/**
* Implements hook_node_validate().
*
* Check that rating attribute is set in the form submission, the field is
* required
*/
function timeline_node_validate($node, $form) {
}
/**
* Implements hook_node_load().
*
* Load the rating information if available for any of the nodes in the argument
* list.
*/
function timeline_node_load($nodes, $form) {
foreach ($nodes as $node) {
if ($node->type == 'archive') {
$nids[] = $node->nid;
}
}
// Check if we should load any of the nodes
if (!isset($nids) || !count($nids)) {
return;
}
$result = db_select('archives', 'a')
->fields('a', array(
'nid',
'url',
'frequency',
'basetime',
'preview_picture',
))
->where('a.nid IN (:nids)', array(':nids' => $nids))
->execute();
foreach ($result as $record) {
$nodes[$record->nid]->archive['url'] = $record->url;
$nodes[$record->nid]->archive['frequency'] = $record->frequency;
$nodes[$record->nid]->archive['basetime'] = $record->basetime;
$nodes[$record->nid]->archive['preview_picture'] = $record->preview_picture;
}
return $nodes;
}
/**
* Implements hook_node_insert().
*
* As a new node is being inserted into the database, we need to do our own
* database inserts.
*/
function timeline_node_insert($node) {
if ($node->type == 'archive') {
db_insert('archives')
->fields(array(
'nid' => $node->nid,
'url' => $node->archive['url'],
'frequency' => $node->archive['frequency'],
'basetime' => $node->archive['basetime'],
'preview_picture' => 0,
))
->execute();
}
}
/**
* Implements hook_node_delete().
*
* When a node is deleted, we need to remove all related records from our table.
*/
function timeline_node_delete($node) {
if ($node->type == 'archive') {
// Notice that we're deleting even if the content type has no rating enabled.
db_delete('archives')
->condition('nid', $node->nid)
->execute();
db_delete('records')
->condition('nid', $node->nid)
->execute();
// ToDo: Proper file delete handling
}
}
/**
* Implements hook_view().
*
* This is a typical implementation that simply runs the node text through
* the output filters.
*
* Finally, we need to take care of displaying our rating when the node is
* viewed. This operation is called after the node has already been prepared
* into HTML and filtered as necessary, so we know we are dealing with an
* HTML teaser and body. We will inject our additional information at the front
* of the node copy.
*
* Using node API 'hook_node_view' is more appropriate than using a filter here, because
* filters transform user-supplied content, whereas we are extending it with
* additional information.
*/
function timeline_node_view($node, $build_mode = 'full') {
if ($node->type == 'archive') {
$node->content['archive'] = array(
'template' => 'archive',
'variables' => $node->archive,
);
}
}
/*
*
*/
function _ajax_check_url_callback ($form, $form_state) {
$img_tag = '';
$url = $form_state['values']['archive']['url'];
if (isset($form_state['storage']['timeline']['url'][$url])) {
$variables = array(
'style_name' => 'thumbnail',
'path' => $form_state['storage']['timeline']['url'][$url]->uri,
'alt' => 'Image Preview',
'title' => 'Image Preview',
'getsize' => TRUE,
);
$img_tag = theme('image_style', $variables);
}
return '<div id="url-check-div">' . $img_tag . '</div>';
}
function timeline_url_validate($element, &$form_state) {
$errors = array();
$url = $element['#value'];
// If we already validated that url, return.
if (isset($form_state['storage']['timeline']['url'][$url])) {
return;
}
// default supported extensions.
// ToDo: make them configurable in admin display.
$extensions = 'png gif jpg jpeg';
$errors += timeline_validate_extensions($url, $extensions);
if (empty($errors)) {
$download = _timeline_download_image($url, 'preview');
$errors += $download['errmsg'];
// cache value
if (empty($errors)) {
$form_state['storage'] = array(
'timeline' => array(
'url' => array(
$url => $download['file'],
),
),
);
}
}
foreach ($errors as $error) {
form_error($element, t($error));
}
}
/*
* Implement hook_cron
*
* ToDo:
* This function should probably not be run from drupal's own cron but rather
* from a dedicated function.
*
*/
function timeline_cron() {
// load all archives that need update
// I'm pretty sure there is a nicer way to join the field data
$nids = db_query("
SELECT n.nid
FROM {node} n
# join the last record, so we now the time of the last download
LEFT JOIN
(SELECT nid, MAX(request_time) AS last_request
FROM {records}
GROUP BY nid
) r
ON r.nid = n.nid
LEFT JOIN {archives} a
ON a.nid = n.nid
# now join all the interval information (frequency / basetime)
LEFT JOIN field_data_timeline_frequency_seconds field_seconds
ON field_seconds.entity_id = a.frequency
WHERE n.type = :type
AND (
FLOOR(r.last_request / field_seconds.timeline_frequency_seconds_value) * field_seconds.timeline_frequency_seconds_value + (a.basetime % field_seconds.timeline_frequency_seconds_value)
< FLOOR(:now / field_seconds.timeline_frequency_seconds_value) * field_seconds.timeline_frequency_seconds_value
OR r.last_request IS NULL
)
", array(
':type' => 'archive',
':now' => time(),
))
->fetchCol();
$archives = node_load_multiple($nids);
$max_processes = 10;
$running_processes = 0;
if (!function_exists('pcntl_fork')) {
watchdog('warning', "Function pcntl_fork doesn't exist. Downloading images as batch job. Please run from cli with pcntl enabled to run parallel jobs. scripts/drupal.sh http://[your site]/cron.php");
}
watchdog('timeline archive', t('starting download for :count images.', array(':count' => count($archives))));
// start a seperate process for each archive
foreach ($archives as $archive) {
if (!function_exists('pcntl_fork')) {
$timestamp = time();
$download = _timeline_download_image($archive->archive['url'], $archive->nid);
if (empty($download['errmsg'])) {
_timeline_save_image($timestamp, $download['file'], $archive, $download['httpcode']);
} else {
watchdog ('timeline archive', 'we got an error: ' . implode('<br/>', $download['errmsg']));
}
} else {
// If we close the connection, a new one will be made for the child and
// it's not going to kill a connection that is in use just in the very
// same moment...
// Maybe persistent connections would be a way to circumvent that.
// If you like, go ahead and have a try
Database::closeConnection();
$pid = pcntl_fork();
if ($pid == -1) {
// could not fork
} else if ($pid) {
// we are the parent => babysit the childs
$running_processes++;
if (count($running_processes) >= $max_processes) {
$die_pid = pcntl_wait($status); // Wait for a child to exit
$running_processes --;
}
} else {
// we are the child
// here is the actual download done
// as soon as it's done, exit and let the parent spawn more processes
$timestamp = time();
$download = _timeline_download_image($archive->archive['url'], $archive->nid);
if (empty($download['errmsg'])) {
_timeline_save_image($timestamp, $download['file'], $archive, $download['httpcode']);
} else {
watchdog ('timeline archive', 'error downloading image for archive ' . implode('<br/>', $download['errmsg']));
}
exit;
}
}
}
}
/*
* Implements hook_views_api
*/
function timeline_views_api() {
return array('api' => 2);
}