1
|
<?php
|
2
|
|
3
|
/**
|
4
|
* @file
|
5
|
* Contains the FeedsFetcher and related classes.
|
6
|
*/
|
7
|
|
8
|
/**
|
9
|
* Base class for all fetcher results.
|
10
|
*/
|
11
|
class FeedsFetcherResult extends FeedsResult {
|
12
|
/**
|
13
|
* The raw fetched data.
|
14
|
*
|
15
|
* @var string
|
16
|
*/
|
17
|
protected $raw;
|
18
|
|
19
|
/**
|
20
|
* The path to a file where the raw data is stored.
|
21
|
*
|
22
|
* @var string
|
23
|
*/
|
24
|
protected $file_path;
|
25
|
|
26
|
/**
|
27
|
* Constructor.
|
28
|
*/
|
29
|
public function __construct($raw) {
|
30
|
$this->raw = $raw;
|
31
|
}
|
32
|
|
33
|
/**
|
34
|
* Prevent saving the raw result when serializing object.
|
35
|
*/
|
36
|
public function __sleep() {
|
37
|
if (!empty($this->raw)) {
|
38
|
// Save contents of raw to a file for later use.
|
39
|
$this->saveRawToFile();
|
40
|
}
|
41
|
|
42
|
// Save anything but 'raw'.
|
43
|
unset($this->raw);
|
44
|
return array_keys(get_object_vars($this));
|
45
|
}
|
46
|
|
47
|
/**
|
48
|
* Returns the raw content.
|
49
|
*
|
50
|
* @return string
|
51
|
* The raw content from the source as a string.
|
52
|
*
|
53
|
* @throws Exception
|
54
|
* Extending classes MAY throw an exception if a problem occurred.
|
55
|
*/
|
56
|
public function getRaw() {
|
57
|
if (empty($this->raw)) {
|
58
|
// Return raw contents from file.
|
59
|
return $this->getFileContents();
|
60
|
}
|
61
|
return $this->sanitizeRawOptimized($this->raw);
|
62
|
}
|
63
|
|
64
|
/**
|
65
|
* Get a path to a temporary file containing the resource provided by the
|
66
|
* fetcher.
|
67
|
*
|
68
|
* File will be deleted after DRUPAL_MAXIMUM_TEMP_FILE_AGE.
|
69
|
*
|
70
|
* @return string
|
71
|
* A path to a file containing the raw content as a source.
|
72
|
*
|
73
|
* @throws Exception
|
74
|
* If an unexpected problem occurred.
|
75
|
*/
|
76
|
public function getFilePath() {
|
77
|
if (empty($this->file_path)) {
|
78
|
// No file exists yet. Save any raw data that we got.
|
79
|
$this->saveRawToFile();
|
80
|
}
|
81
|
|
82
|
// Check if given file exists now.
|
83
|
$this->checkFile();
|
84
|
|
85
|
// Return file path.
|
86
|
return $this->sanitizeFile($this->file_path);
|
87
|
}
|
88
|
|
89
|
/**
|
90
|
* Returns directory for storing files that are in progress of import.
|
91
|
*
|
92
|
* @return string
|
93
|
* The cache dir to use.
|
94
|
*/
|
95
|
public function getFeedsInProgressDir() {
|
96
|
$dir = variable_get('feeds_in_progress_dir', NULL);
|
97
|
if ($dir) {
|
98
|
return $dir;
|
99
|
}
|
100
|
else {
|
101
|
$schemes = file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE);
|
102
|
$scheme = isset($schemes['private']) ? 'private' : 'public';
|
103
|
return $scheme . '://feeds/in_progress';
|
104
|
}
|
105
|
}
|
106
|
|
107
|
/**
|
108
|
* Constructs file name for saving the raw data.
|
109
|
*/
|
110
|
public function constructFilePath() {
|
111
|
return $this->getFeedsInProgressDir() . '/' . get_class($this) . REQUEST_TIME;
|
112
|
}
|
113
|
|
114
|
/**
|
115
|
* Returns if raw data exists.
|
116
|
*
|
117
|
* This checks if either $this->raw is set or if the raw data exists in a
|
118
|
* file. This is better then calling just ::getRaw() as that would return a
|
119
|
* copy of all raw data which may lead to memory issues if the data is very
|
120
|
* large.
|
121
|
*
|
122
|
* @return bool
|
123
|
* True if the raw data exists.
|
124
|
* False otherwise.
|
125
|
*/
|
126
|
public function rawExists() {
|
127
|
return !empty($this->raw) || $this->fileExists();
|
128
|
}
|
129
|
|
130
|
/**
|
131
|
* Returns if the file to parse exists.
|
132
|
*
|
133
|
* @return bool
|
134
|
* True if the file exists.
|
135
|
* False otherwise.
|
136
|
*/
|
137
|
public function fileExists() {
|
138
|
if (!empty($this->file_path) && is_readable($this->file_path)) {
|
139
|
return TRUE;
|
140
|
}
|
141
|
return FALSE;
|
142
|
}
|
143
|
|
144
|
/**
|
145
|
* Returns the contents of a file, if it exists.
|
146
|
*
|
147
|
* @return string
|
148
|
* The file contents.
|
149
|
*/
|
150
|
public function getFileContents() {
|
151
|
if ($this->fileExists()) {
|
152
|
$this->sanitizeFile($this->file_path);
|
153
|
return file_get_contents($this->file_path);
|
154
|
}
|
155
|
}
|
156
|
|
157
|
/**
|
158
|
* Checks that a file exists and is readable.
|
159
|
*
|
160
|
* @throws RuntimeException
|
161
|
* Thrown if the file isn't readable or writable.
|
162
|
*/
|
163
|
protected function checkFile() {
|
164
|
if (!file_exists($this->file_path)) {
|
165
|
throw new RuntimeException(t('File %filepath does not exist.', array(
|
166
|
'%filepath' => $this->file_path,
|
167
|
)));
|
168
|
}
|
169
|
|
170
|
if (!is_readable($this->file_path)) {
|
171
|
throw new RuntimeException(t('File %filepath is not readable.', array(
|
172
|
'%filepath' => $this->file_path,
|
173
|
)));
|
174
|
}
|
175
|
}
|
176
|
|
177
|
/**
|
178
|
* Saves the raw fetcher result to a file.
|
179
|
*
|
180
|
* @throws RuntimeException
|
181
|
* In case the destination wasn't writable.
|
182
|
*/
|
183
|
public function saveRawToFile() {
|
184
|
$file_in_progress_dir = $this->getFeedsInProgressDir();
|
185
|
if (!file_prepare_directory($file_in_progress_dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
|
186
|
throw new RuntimeException(t('Feeds directory either cannot be created or is not writable.'));
|
187
|
}
|
188
|
|
189
|
$this->file_path = FALSE;
|
190
|
if ($file = file_save_data($this->getRaw(), $this->constructFilePath())) {
|
191
|
$file->status = 0;
|
192
|
file_save($file);
|
193
|
$this->file_path = $file->uri;
|
194
|
|
195
|
// Clear raw data to save memory, but also to prevent saving the same raw data
|
196
|
// to a file again in the same request.
|
197
|
$this->raw = NULL;
|
198
|
}
|
199
|
else {
|
200
|
throw new RuntimeException(t('Cannot write content to %dest', array('%dest' => $destination)));
|
201
|
}
|
202
|
}
|
203
|
|
204
|
/**
|
205
|
* Sanitize the raw content string. Currently supported sanitizations:
|
206
|
*
|
207
|
* - Remove BOM header from UTF-8 files.
|
208
|
*
|
209
|
* Consider using ::sanitizeRawOptimized() instead that receives the variable
|
210
|
* by reference and thus saves memory.
|
211
|
*
|
212
|
* @param string $raw
|
213
|
* The raw content string to be sanitized.
|
214
|
*
|
215
|
* @return string
|
216
|
* The sanitized content as a string.
|
217
|
*/
|
218
|
public function sanitizeRaw($raw) {
|
219
|
if (substr($raw, 0, 3) == pack('CCC', 0xef, 0xbb, 0xbf)) {
|
220
|
$raw = substr($raw, 3);
|
221
|
}
|
222
|
return $raw;
|
223
|
}
|
224
|
|
225
|
/**
|
226
|
* Sanitize the raw content string. Currently supported sanitizations:
|
227
|
*
|
228
|
* - Remove BOM header from UTF-8 files.
|
229
|
*
|
230
|
* This accepts the raw contents by reference to prevent having the whole raw
|
231
|
* contents in memory again.
|
232
|
*
|
233
|
* @param string $raw
|
234
|
* The raw content string to be sanitized.
|
235
|
*
|
236
|
* @return string
|
237
|
* The sanitized content as a string.
|
238
|
*/
|
239
|
public function sanitizeRawOptimized(&$raw) {
|
240
|
if (substr($raw, 0, 3) == pack('CCC', 0xef, 0xbb, 0xbf)) {
|
241
|
$raw = substr($raw, 3);
|
242
|
}
|
243
|
return $raw;
|
244
|
}
|
245
|
|
246
|
/**
|
247
|
* Sanitize the file in place. Currently supported sanitizations:
|
248
|
*
|
249
|
* - Remove BOM header from UTF-8 files.
|
250
|
*
|
251
|
* @param string $filepath
|
252
|
* The file path of the file to be sanitized.
|
253
|
*
|
254
|
* @return string
|
255
|
* The file path of the sanitized file.
|
256
|
*
|
257
|
* @throws RuntimeException
|
258
|
* Thrown if the file is not writeable.
|
259
|
*/
|
260
|
public function sanitizeFile($filepath) {
|
261
|
$handle = fopen($filepath, 'r');
|
262
|
$line = fgets($handle);
|
263
|
fclose($handle);
|
264
|
|
265
|
// If BOM header is present, read entire contents of file and overwrite the
|
266
|
// file with corrected contents.
|
267
|
if (substr($line, 0, 3) !== pack('CCC', 0xef, 0xbb, 0xbf)) {
|
268
|
return $filepath;
|
269
|
}
|
270
|
|
271
|
if (!is_writable($filepath)) {
|
272
|
throw new RuntimeException(t('File %filepath is not writable.', array(
|
273
|
'%filepath' => $filepath,
|
274
|
)));
|
275
|
}
|
276
|
|
277
|
$contents = file_get_contents($filepath);
|
278
|
$contents = substr($contents, 3);
|
279
|
$status = file_put_contents($filepath, $contents);
|
280
|
|
281
|
return $filepath;
|
282
|
}
|
283
|
}
|
284
|
|
285
|
/**
|
286
|
* Abstract class, defines shared functionality between fetchers.
|
287
|
*
|
288
|
* Implements FeedsSourceInfoInterface to expose source forms to Feeds.
|
289
|
*/
|
290
|
abstract class FeedsFetcher extends FeedsPlugin {
|
291
|
|
292
|
/**
|
293
|
* Implements FeedsPlugin::pluginType().
|
294
|
*/
|
295
|
public function pluginType() {
|
296
|
return 'fetcher';
|
297
|
}
|
298
|
|
299
|
/**
|
300
|
* Fetch content from a source and return it.
|
301
|
*
|
302
|
* Every class that extends FeedsFetcher must implement this method.
|
303
|
*
|
304
|
* @param $source
|
305
|
* Source value as entered by user through sourceForm().
|
306
|
*
|
307
|
* @return
|
308
|
* A FeedsFetcherResult object.
|
309
|
*/
|
310
|
public abstract function fetch(FeedsSource $source);
|
311
|
|
312
|
/**
|
313
|
* Clear all caches for results for given source.
|
314
|
*
|
315
|
* @param FeedsSource $source
|
316
|
* Source information for this expiry. Implementers can choose to only clear
|
317
|
* caches pertaining to this source.
|
318
|
*/
|
319
|
public function clear(FeedsSource $source) {}
|
320
|
|
321
|
/**
|
322
|
* Request handler invoked if callback URL is requested. Locked down by
|
323
|
* default. For a example usage see FeedsHTTPFetcher.
|
324
|
*
|
325
|
* Note: this method may exit the script.
|
326
|
*
|
327
|
* @return
|
328
|
* A string to be returned to the client.
|
329
|
*/
|
330
|
public function request($feed_nid = 0) {
|
331
|
drupal_access_denied();
|
332
|
}
|
333
|
|
334
|
/**
|
335
|
* Construct a path for a concrete fetcher/source combination. The result of
|
336
|
* this method matches up with the general path definition in
|
337
|
* FeedsFetcher::menuItem(). For example usage look at FeedsHTTPFetcher.
|
338
|
*
|
339
|
* @return
|
340
|
* Path for this fetcher/source combination.
|
341
|
*/
|
342
|
public function path($feed_nid = 0) {
|
343
|
$id = urlencode($this->id);
|
344
|
if ($feed_nid && is_numeric($feed_nid)) {
|
345
|
return "feeds/importer/$id/$feed_nid";
|
346
|
}
|
347
|
return "feeds/importer/$id";
|
348
|
}
|
349
|
|
350
|
/**
|
351
|
* Menu item definition for fetchers of this class. Note how the path
|
352
|
* component in the item definition matches the return value of
|
353
|
* FeedsFetcher::path();
|
354
|
*
|
355
|
* Requests to this menu item will be routed to FeedsFetcher::request().
|
356
|
*
|
357
|
* @return
|
358
|
* An array where the key is the Drupal menu item path and the value is
|
359
|
* a valid Drupal menu item definition.
|
360
|
*/
|
361
|
public function menuItem() {
|
362
|
return array(
|
363
|
'feeds/importer/%feeds_importer' => array(
|
364
|
'page callback' => 'feeds_fetcher_callback',
|
365
|
'page arguments' => array(2, 3),
|
366
|
'access callback' => TRUE,
|
367
|
'file' => 'feeds.pages.inc',
|
368
|
'type' => MENU_CALLBACK,
|
369
|
),
|
370
|
);
|
371
|
}
|
372
|
|
373
|
/**
|
374
|
* Subscribe to a source. Only implement if fetcher requires subscription.
|
375
|
*
|
376
|
* @param FeedsSource $source
|
377
|
* Source information for this subscription.
|
378
|
*/
|
379
|
public function subscribe(FeedsSource $source) {}
|
380
|
|
381
|
/**
|
382
|
* Unsubscribe from a source. Only implement if fetcher requires subscription.
|
383
|
*
|
384
|
* @param FeedsSource $source
|
385
|
* Source information for unsubscribing.
|
386
|
*/
|
387
|
public function unsubscribe(FeedsSource $source) {}
|
388
|
|
389
|
/**
|
390
|
* Override import period settings. This can be used to force a certain import
|
391
|
* interval.
|
392
|
*
|
393
|
* @param $source
|
394
|
* A FeedsSource object.
|
395
|
*
|
396
|
* @return
|
397
|
* A time span in seconds if periodic import should be overridden for given
|
398
|
* $source, NULL otherwise.
|
399
|
*/
|
400
|
public function importPeriod(FeedsSource $source) {}
|
401
|
|
402
|
/**
|
403
|
* Invoked after an import is finished.
|
404
|
*
|
405
|
* @param $source
|
406
|
* A FeedsSource object.
|
407
|
*/
|
408
|
public function afterImport(FeedsSource $source) {}
|
409
|
}
|