Completed
Branch master (e8947e)
by Andreas
15:09
created

midcom_db_attachment::get_parent_guid_uncached()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @package midcom.db
4
 * @author The Midgard Project, http://www.midgard-project.org
5
 * @copyright The Midgard Project, http://www.midgard-project.org
6
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
7
 */
8
9
/**
10
 * MidCOM level replacement for the Midgard Attachment record with framework support.
11
 *
12
 * @package midcom.db
13
 */
14
class midcom_db_attachment extends midcom_core_dbaobject
1 ignored issue
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
15
{
16
    public $__midcom_class_name__ = __CLASS__;
17
    public $__mgdschema_class_name__ = 'midgard_attachment';
18
19
    public $_use_activitystream = false;
20
    public $_use_rcs = false;
21
22
    /**
23
     * Internal tracking state variable, holds the file handle of any open
24
     * attachment.
25
     *
26
     * @var resource
27
     */
28
    private $_open_handle = null;
29
30
    /**
31
     * Internal tracking state variable, true if the attachment has a handle opened in write mode
32
     */
33
    private $_open_write_mode = false;
34
35
    public function get_parent_guid_uncached()
36
    {
37
        return $this->parentguid;
38
    }
39
40
    /**
41
     * Returns the Parent of the Attachment, which is identified by the table/id combination
42
     * in the attachment record. The table in question is used to identify the object to
43
     * use. If multiple objects are registered for a given table, the first matching class
44
     * returned by the dbfactory is used (which is usually rather arbitrary).
45
     *
46
     * @return string Parent GUID.
47
     */
48 View Code Duplication
    public static function get_parent_guid_uncached_static($guid, $classname = __CLASS__)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
49
    {
50
        $mc = new midgard_collector('midgard_attachment', 'guid', $guid);
51
        $mc->set_key_property('parentguid');
52
        $mc->execute();
53
        $link_values = $mc->list_keys();
54
        if (empty($link_values))
55
        {
56
            return null;
57
        }
58
        return key($link_values);
59
    }
60
61
    /**
62
     * Opens the attachment for file IO.
63
     *
64
     * Returns a filehandle that can be used with the usual PHP file functions if successful,
65
     * the handle has to be closed with the close() method when you no longer need it, don't
66
     * let it fall over the end of the script.
67
     *
68
     * <b>Important Note:</b> It is important to use the close() member function of
69
     * this class to close the file handle, not just fclose(). Otherwise, the upgrade
70
     * notification switches will fail.
71
     *
72
     * @param string $mode The mode which should be used to open the attachment, same as
73
     *     the mode parameter of the PHP fopen call. This defaults to write access (see
74
     *     mgd_open_attachmentl for details).
75
     * @return resource A file handle to the attachment if successful, false on failure.
76
     */
77
    public function open($mode = 'default')
78
    {
79 View Code Duplication
        if (! $this->id)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
80
        {
81
            debug_add('Cannot open a non-persistent attachment.', MIDCOM_LOG_WARN);
82
            debug_print_r('Object state:', $this);
83
            return false;
84
        }
85
86
        if ($this->_open_handle !== null)
87
        {
88
            debug_add("Warning, the Attachment {$this->id} already had an open file handle, we close it implicitly.", MIDCOM_LOG_WARN);
89
            fclose($this->_open_handle);
90
            $this->_open_handle = null;
91
        }
92
93
        $blob = new midgard_blob($this->__object);
94
        if ($mode == 'default')
95
        {
96
            $this->_open_write_mode = true;
97
            $handle = $blob->get_handler();
98
        }
99
        else
100
        {
101
            /* WARNING, read mode not supported by midgard_blob! */
102
            $this->_open_write_mode = ($mode{0} != 'r');
103
            $handle = fopen($blob->get_path(), $mode);
104
        }
105
106
        if (!$handle)
107
        {
108
            debug_add("Failed to open attachment with mode {$mode}, last Midgard error was: " . midcom_connection::get_error_string(), MIDCOM_LOG_WARN);
109
        }
110
111
        $this->_open_handle = $handle;
112
113
        return $handle;
114
    }
115
116
    /**
117
     * Read the file and return its contents
118
     *
119
     * @return string
120
     */
121
    public function read()
122
    {
123
        $attachment = new midgard_attachment($this->guid);
124
        $blob = new midgard_blob($attachment);
125
126
        return $blob->read_content();
127
    }
128
129
    /**
130
     * Close the open write handle obtained by the open() call again.
131
     * It is required to call this function instead of a simple fclose to ensure proper
132
     * upgrade notifications.
133
     */
134
    public function close()
135
    {
136
        if ($this->_open_handle === null)
137
        {
138
            debug_add("Tried to close non-open attachment {$this->id}", MIDCOM_LOG_WARN);
139
            return;
140
        }
141
142
        fclose ($this->_open_handle);
143
        $this->_open_handle = null;
144
145
        if ($this->_open_write_mode)
146
        {
147
            // We need to update the attachment now, this cannot be done in the Midgard Core
148
            // at this time.
149
            if (! $this->update())
150
            {
151
                debug_add("Failed to update attachment {$this->id}", MIDCOM_LOG_WARN);
152
                return;
153
            }
154
155
            $this->file_to_cache();
156
        }
157
    }
158
159
    /**
160
     * Rewrite a filename to URL safe form
161
     *
162
     * @param string $filename file name to rewrite
163
     * @param boolean $force_single_extension force file to single extension (defaults to true)
164
     * @return string rewritten filename
165
     * @todo add possibility to use the file utility to determine extension if missing.
166
     */
167
    public static function safe_filename($filename, $force_single_extension = true)
168
    {
169
        $filename = basename(trim($filename));
170
        if ($force_single_extension)
171
        {
172
            $regex = '/^(.*)(\..*?)$/';
173
        }
174
        else
175
        {
176
            $regex = '/^(.*?)(\..*)$/';
177
        }
178
        if (preg_match($regex, $filename, $ext_matches))
179
        {
180
            $name = $ext_matches[1];
181
            $ext = $ext_matches[2];
182
        }
183
        else
184
        {
185
            $name = $filename;
186
            $ext = '';
187
        }
188
        return midcom::get()->serviceloader->load('midcom_core_service_urlgenerator')->from_string($name) . $ext;
189
    }
190
191
    /**
192
     * Get the path to the document in the static cache
193
     *
194
     * @return string
195
     */
196
    public function get_cache_path()
197
    {
198
        if (!midcom::get()->config->get('attachment_cache_enabled'))
199
        {
200
            return null;
201
        }
202
203
        // Copy the file to the static directory
204
        $cacheroot = midcom::get()->config->get('attachment_cache_root');
205
        $subdir = substr($this->guid, 0, 1);
206
        if (!file_exists("{$cacheroot}/{$subdir}"))
207
        {
208
            mkdir("{$cacheroot}/{$subdir}", 0777, true);
209
        }
210
211
        return "{$cacheroot}/{$subdir}/{$this->guid}_{$this->name}";
212
    }
213
214
    public static function get_url($attachment, $name = null)
215
    {
216
        if (is_string($attachment))
217
        {
218
            $guid = $attachment;
219
            if (null === $name)
220
            {
221
                $mc = self::new_collector('guid', $guid);
222
                $names = $mc->get_values('name');
223
                $name = array_pop($names);
224
            }
225
        }
226
        else if (midcom::get()->dbfactory->is_a($attachment, 'midgard_attachment'))
227
        {
228
            $guid = $attachment->guid;
229
            $name = $attachment->name;
230
        }
231
        else
232
        {
233
            throw new midcom_error('Invalid attachment identifier');
234
        }
235
236
        if (midcom::get()->config->get('attachment_cache_enabled'))
237
        {
238
            $subdir = substr($guid, 0, 1);
239
240
            if (file_exists(midcom::get()->config->get('attachment_cache_root') . '/' . $subdir . '/' . $guid . '_' . $name))
241
            {
242
                return midcom::get()->config->get('attachment_cache_url') . '/' . $subdir . '/' . $guid . '_' . urlencode($name);
243
            }
244
        }
245
246
        // Use regular MidCOM attachment server
247
        return midcom_connection::get_url('self') . 'midcom-serveattachmentguid-' . $guid . '/' . urlencode($name);
248
    }
249
250
    function file_to_cache()
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
251
    {
252
        // Check if the attachment can be read anonymously
253
        if (!midcom::get()->config->get('attachment_cache_enabled'))
254
        {
255
            return;
256
        }
257
258
        if (!$this->can_do('midgard:read', 'EVERYONE'))
259
        {
260
            debug_add("Attachment {$this->name} ({$this->guid}) is not publicly readable, not caching.");
261
            return;
262
        }
263
264
        $filename = $this->get_cache_path();
265
266
        if (!$filename)
267
        {
268
            debug_add("Failed to generate cache path for attachment {$this->name} ({$this->guid}), not caching.");
269
            return;
270
        }
271
272
        if (   file_exists($filename)
273
            && is_link($filename))
274
        {
275
            debug_add("Attachment {$this->name} ({$this->guid}) is already in cache as {$filename}, skipping.");
276
            return;
277
        }
278
279
        // Then symlink the file
280
        $blob = new midgard_blob($this->__object);
281
282
        if (@symlink($blob->get_path(), $filename))
283
        {
284
            debug_add("Symlinked attachment {$this->name} ({$this->guid}) as {$filename}.");
285
            return;
286
        }
287
288
        // Symlink failed, actually copy the data
289
        $fh = $this->open('r');
290
        if (!$fh)
291
        {
292
            debug_add("Failed to cache attachment {$this->name} ({$this->guid}), opening failed.");
293
            return;
294
        }
295
296
        $data = '';
297
        while (!feof($fh))
298
        {
299
            $data .= fgets($fh);
300
        }
301
        fclose($fh);
302
        $this->_open_handle = null;
303
304
        file_put_contents($filename, $data);
305
306
        debug_add("Symlinking attachment {$this->name} ({$this->guid}) as {$filename} failed, data copied instead.");
307
    }
308
309
    /**
310
     * Simple wrapper for stat() on the blob object.
311
     *
312
     * @return mixed Either a stat array as for stat() or false on failure.
313
     */
314
    public function stat()
315
    {
316 View Code Duplication
        if (!$this->id)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
317
        {
318
            debug_add('Cannot open a non-persistent attachment.', MIDCOM_LOG_WARN);
319
            debug_print_r('Object state:', $this);
320
            return false;
321
        }
322
323
        $blob = new midgard_blob($this->__object);
324
325
        $path = $blob->get_path();
326
        if (!file_exists($path))
327
        {
328
            debug_add("File {$path} that blob {$this->guid} points to cannot be found", MIDCOM_LOG_WARN);
329
            return false;
330
        }
331
332
        return stat($path);
333
    }
334
335
    /**
336
     * Internal helper, computes an MD5 string which is used as an attachment location.
337
     * It should be random enough, even if the algorithm used does not match the one
338
     * Midgard uses. If the location already exists, it will iterate until an unused
339
     * location is found.
340
     *
341
     * @return string An unused attachment location.
342
     */
343
    private function _create_attachment_location()
344
    {
345
        $max_tries = 500;
346
347
        for ($i = 0; $i < $max_tries; $i++)
348
        {
349
            $name = strtolower(md5(uniqid('', true)));
350
            $location = strtoupper(substr($name, 0, 1) . '/' . substr($name, 1, 1) . '/') . $name;
351
352
            // Check uniqueness
353
            $qb = midcom_db_attachment::new_query_builder();
354
            $qb->add_constraint('location', '=', $location);
355
            $result = $qb->count_unchecked();
356
357
            if ($result == 0)
358
            {
359
                debug_add("Created this location: {$location}");
360
                return $location;
361
            }
362
            debug_add("Location {$location} is in use, retrying");
363
        }
364
        throw new midcom_error('could not create attachment location');
365
    }
366
367
    /**
368
     * Simple creation event handler which fills out the location field if it
369
     * is still empty with a location generated by _create_attachment_location().
370
     *
371
     * @return boolean True if creation may commence.
372
     */
373
    public function _on_creating()
374
    {
375
        if (empty($this->mimetype))
376
        {
377
            $this->mimetype = 'application/octet-stream';
378
        }
379
380
        $this->location = $this->_create_attachment_location();
381
382
        return true;
383
    }
384
385
    function update_cache()
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
386
    {
387
        // Check if the attachment can be read anonymously
388
        if (   midcom::get()->config->get('attachment_cache_enabled')
389
            && !$this->can_do('midgard:read', 'EVERYONE'))
390
        {
391
            // Not public file, ensure it is removed
392
            $subdir = substr($this->guid, 0, 1);
393
            $filename = midcom::get()->config->get('attachment_cache_root') . "/{$subdir}/{$this->guid}_{$this->name}";
394
            if (file_exists($filename))
395
            {
396
                @unlink($filename);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
397
            }
398
        }
399
    }
400
401
    /**
402
     * Updated callback, triggers watches on the parent(!) object.
403
     */
404
    public function _on_updated()
405
    {
406
        $this->update_cache();
407
    }
408
409
    /**
410
     * Deleted callback, triggers watches on the parent(!) object.
411
     */
412
    public function _on_deleted()
413
    {
414
        if (midcom::get()->config->get('attachment_cache_enabled'))
415
        {
416
            // Remove attachment cache
417
            $filename = $this->get_cache_path();
418
            if (file_exists($filename))
419
            {
420
                @unlink($filename);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
421
            }
422
        }
423
    }
424
425
    /**
426
     * Updates the contents of the attachments with the contents given.
427
     *
428
     * @param mixed $source File contents.
429
     * @return boolean Indicating success.
430
     */
431 View Code Duplication
    public function copy_from_memory($source)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
432
    {
433
        $dest = $this->open();
434
        if (! $dest)
435
        {
436
            debug_add('Could not open attachment for writing, last Midgard error was: ' . midcom_connection::get_error_string(), MIDCOM_LOG_WARN);
437
            return false;
438
        }
439
440
        fwrite($dest, $source);
441
442
        $this->close();
443
        return true;
444
    }
445
446
    /**
447
     * Updates the contents of the attachments with the contents of the resource identified
448
     * by the filehandle passed.
449
     *
450
     * @param resource $source The handle to read from.
451
     * @return boolean Indicating success.
452
     */
453 View Code Duplication
    public function copy_from_handle($source)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
454
    {
455
        $dest = $this->open();
456
        if (! $dest)
457
        {
458
            debug_add('Could not open attachment for writing, last Midgard error was: ' . midcom_connection::get_error_string(), MIDCOM_LOG_WARN);
459
            return false;
460
        }
461
462
        stream_copy_to_stream($source, $dest);
463
464
        $this->close();
465
        return true;
466
    }
467
468
    /**
469
     * Updates the contents of the attachments with the contents of the file specified.
470
     * This is a wrapper for copy_from_handle.
471
     *
472
     * @param string $filename The file to read.
473
     * @return boolean Indicating success.
474
     */
475
    public function copy_from_file($filename)
476
    {
477
        $source = @fopen ($filename, 'r');
478 View Code Duplication
        if (! $source)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
479
        {
480
            debug_add('Could not open file for reading.' . midcom_connection::get_error_string(), MIDCOM_LOG_WARN);
481
            midcom::get()->debug->log_php_error(MIDCOM_LOG_WARN);
482
            return false;
483
        }
484
        $result = $this->copy_from_handle($source);
485
        fclose($source);
486
        return $result;
487
    }
488
}
489