Issues (1176)

admin_media.php (10 issues)

1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2019 webtrees development team
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
 * GNU General Public License for more details.
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15
 */
16
namespace Fisharebest\Webtrees;
17
18
use Fisharebest\Webtrees\Controller\AjaxController;
19
use Fisharebest\Webtrees\Controller\PageController;
20
use Fisharebest\Webtrees\Functions\FunctionsEdit;
21
22
define('WT_SCRIPT_NAME', 'admin_media.php');
23
require './includes/session.php';
24
25
// type of file/object to include
26
$files = Filter::get('files', 'local|external|unused', 'local');
27
28
// family tree setting MEDIA_DIRECTORY
29
$media_folders = all_media_folders();
30
$media_folder  = Filter::get('media_folder', null, ''); // MySQL needs an empty string, not NULL
31
// User folders may contain special characters. Restrict to actual folders.
32
if (!array_key_exists($media_folder, $media_folders)) {
33
    $media_folder = reset($media_folders);
34
}
35
36
// prefix to filename
37
$media_paths = media_paths($media_folder);
38
$media_path  = Filter::get('media_path', null, ''); // MySQL needs an empty string, not NULL
39
// User paths may contain special characters. Restrict to actual paths.
40
if (!array_key_exists($media_path, $media_paths)) {
41
    $media_path = reset($media_paths);
42
}
43
44
// subfolders within $media_path
45
$subfolders = Filter::get('subfolders', 'include|exclude', 'include');
46
$action     = Filter::get('action');
47
48
////////////////////////////////////////////////////////////////////////////////
49
// POST callback for file deletion
50
////////////////////////////////////////////////////////////////////////////////
51
$delete_file = Filter::post('delete');
52
if ($delete_file) {
53
    $controller = new AjaxController;
54
    // Only delete valid (i.e. unused) media files
55
    $media_folder = Filter::post('media_folder', null, ''); // MySQL needs an empty string, not NULL
56
    $disk_files   = all_disk_files($media_folder, '', 'include', '');
57
    if (in_array($delete_file, $disk_files)) {
58
        $tmp = WT_DATA_DIR . $media_folder . $delete_file;
59
        try {
60
            unlink($tmp);
61
            FlashMessages::addMessage(I18N::translate('The file %s has been deleted.', Html::filename($tmp)), 'success');
62
        } catch (\ErrorException $ex) {
63
            FlashMessages::addMessage(I18N::translate('The file %s could not be deleted.', Html::filename($tmp)) . '<hr><samp dir="ltr">' . $ex->getMessage() . '</samp>', 'danger');
64
        }
65
        // Delete any corresponding thumbnail
66
        $tmp = WT_DATA_DIR . $media_folder . 'thumbs/' . $delete_file;
67
        if (file_exists($tmp)) {
68
            try {
69
                unlink($tmp);
70
                FlashMessages::addMessage(I18N::translate('The file %s has been deleted.', Html::filename($tmp)), 'success');
71
            } catch (\ErrorException $ex) {
72
                FlashMessages::addMessage(I18N::translate('The file %s could not be deleted.', Html::filename($tmp)) . '<hr><samp dir="ltr">' . $ex->getMessage() . '</samp>', 'danger');
73
            }
74
        }
75
    } else {
76
        // File no longer exists? Maybe it was already deleted or renamed.
77
    }
78
    $controller->pageHeader();
79
80
    return;
81
}
82
83
////////////////////////////////////////////////////////////////////////////////
84
// GET callback for server-side pagination
85
////////////////////////////////////////////////////////////////////////////////
86
87
switch ($action) {
88
    case 'load_json':
89
        $search = Filter::get('search');
90
        $search = $search['value'];
91
        $start  = Filter::getInteger('start');
92
        $length = Filter::getInteger('length');
93
94
        switch ($files) {
95
            case 'local':
96
                // Filtered rows
97
                $SELECT1 =
98
                "SELECT SQL_CALC_FOUND_ROWS TRIM(LEADING :media_path_1 FROM m_filename) AS media_path, m_id AS xref, m_titl, m_file AS gedcom_id, m_gedcom AS gedcom" .
99
                " FROM  `##media`" .
100
                " JOIN  `##gedcom_setting` ON (m_file = gedcom_id AND setting_name = 'MEDIA_DIRECTORY')" .
101
                " JOIN  `##gedcom` USING (gedcom_id)" .
102
                " WHERE setting_value = :media_folder" .
103
                " AND   m_filename LIKE CONCAT(:media_path_2, '%')" .
104
                " AND   (SUBSTRING_INDEX(m_filename, '/', -1) LIKE CONCAT('%', :search_1, '%')" .
105
                "  OR   m_titl LIKE CONCAT('%', :search_2, '%'))" .
106
                " AND   m_filename NOT LIKE 'http://%'" .
107
                " AND   m_filename NOT LIKE 'https://%'";
108
                $ARGS1 = array(
109
                'media_path_1' => $media_path,
110
                'media_folder' => $media_folder,
111
                'media_path_2' => Filter::escapeLike($media_path),
112
                'search_1'     => Filter::escapeLike($search),
113
                'search_2'     => Filter::escapeLike($search),
114
                );
115
                // Unfiltered rows
116
                $SELECT2 =
117
                    "SELECT COUNT(*)" .
118
                    " FROM  `##media`" .
119
                    " JOIN  `##gedcom_setting` ON (m_file = gedcom_id AND setting_name = 'MEDIA_DIRECTORY')" .
120
                    " WHERE setting_value = :media_folder" .
121
                    " AND   m_filename LIKE CONCAT(:media_path_3, '%')" .
122
                    " AND   m_filename NOT LIKE 'http://%'" .
123
                    " AND   m_filename NOT LIKE 'https://%'";
124
                $ARGS2 = array(
125
                    'media_folder' => $media_folder,
126
                    'media_path_3' => $media_path,
127
                );
128
129
                if ($subfolders == 'exclude') {
130
                    $SELECT1 .= " AND m_filename NOT LIKE CONCAT(:media_path_4, '%/%')";
131
                    $ARGS1['media_path_4'] = Filter::escapeLike($media_path);
132
                    $SELECT2 .= " AND m_filename NOT LIKE CONCAT(:media_path_4, '%/%')";
133
                    $ARGS2['media_path_4'] = Filter::escapeLike($media_path);
134
                }
135
136
                $order = Filter::getArray('order');
137
                $SELECT1 .= " ORDER BY ";
138
                if ($order) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $order of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
139
                    foreach ($order as $key => $value) {
140
                        if ($key > 0) {
141
                            $SELECT1 .= ',';
142
                        }
143
                        // Datatables numbers columns 0, 1, 2
144
                        // MySQL numbers columns 1, 2, 3
145
                        switch ($value['dir']) {
146
                            case 'asc':
147
                                $SELECT1 .= ":col_" . $key . " ASC";
148
                                break;
149
                            case 'desc':
150
                                $SELECT1 .= ":col_" . $key . " DESC";
151
                            break;
152
                        }
153
                        $ARGS1['col_' . $key] = 1 + $value['column'];
154
                    }
155
                } else {
156
                    $SELECT1 = " 1 ASC";
157
                }
158
159
                if ($length > 0) {
160
                    $SELECT1 .= " LIMIT :length OFFSET :start";
161
                    $ARGS1['length'] = $length;
162
                    $ARGS1['start']  = $start;
163
                }
164
165
                $rows = Database::prepare($SELECT1)->execute($ARGS1)->fetchAll();
166
                // Total filtered/unfiltered rows
167
                $recordsFiltered = Database::prepare("SELECT FOUND_ROWS()")->fetchOne();
168
                $recordsTotal    = Database::prepare($SELECT2)->execute($ARGS2)->fetchOne();
169
170
                $data = array();
171
                foreach ($rows as $row) {
172
                    $media  = Media::getInstance($row->xref, Tree::findById($row->gedcom_id), $row->gedcom);
173
                    $data[] = array(
174
                    mediaFileInfo($media_folder, $media_path, $row->media_path),
175
                    $media->displayImage(),
0 ignored issues
show
The method displayImage() does not exist on Fisharebest\Webtrees\GedcomRecord. It seems like you code against a sub-type of Fisharebest\Webtrees\GedcomRecord such as Fisharebest\Webtrees\Individual or Fisharebest\Webtrees\Media. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

175
                    $media->/** @scrutinizer ignore-call */ 
176
                            displayImage(),
Loading history...
The method displayImage() does not exist on Fisharebest\Webtrees\Note. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

175
                    $media->/** @scrutinizer ignore-call */ 
176
                            displayImage(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
The method displayImage() does not exist on Fisharebest\Webtrees\Repository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

175
                    $media->/** @scrutinizer ignore-call */ 
176
                            displayImage(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
The method displayImage() does not exist on Fisharebest\Webtrees\Family. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

175
                    $media->/** @scrutinizer ignore-call */ 
176
                            displayImage(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
The method displayImage() does not exist on Fisharebest\Webtrees\Source. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

175
                    $media->/** @scrutinizer ignore-call */ 
176
                            displayImage(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
176
                    mediaObjectInfo($media),
0 ignored issues
show
It seems like $media can also be of type Fisharebest\Webtrees\Family and Fisharebest\Webtrees\Individual and Fisharebest\Webtrees\Note and Fisharebest\Webtrees\Repository and Fisharebest\Webtrees\Source and null; however, parameter $media of Fisharebest\Webtrees\mediaObjectInfo() does only seem to accept Fisharebest\Webtrees\Media, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

176
                    mediaObjectInfo(/** @scrutinizer ignore-type */ $media),
Loading history...
177
                    );
178
                }
179
            break;
180
181
            case 'external':
182
                // Filtered rows
183
                $SELECT1 =
184
                "SELECT SQL_CALC_FOUND_ROWS m_filename, m_id AS xref, m_titl, m_file AS gedcom_id, m_gedcom AS gedcom" .
185
                " FROM  `##media`" .
186
                " WHERE (m_filename LIKE 'http://%' OR m_filename LIKE 'https://%')" .
187
                " AND   (m_filename LIKE CONCAT('%', :search_1, '%') OR m_titl LIKE CONCAT('%', :search_2, '%'))";
188
                $ARGS1 = array(
189
                'search_1' => Filter::escapeLike($search),
190
                'search_2' => Filter::escapeLike($search),
191
                );
192
                // Unfiltered rows
193
                $SELECT2 =
194
                    "SELECT COUNT(*)" .
195
                    " FROM  `##media`" .
196
                    " WHERE (m_filename LIKE 'http://%' OR m_filename LIKE 'https://%')";
197
                $ARGS2 = array();
198
199
                $order = Filter::getArray('order');
200
                $SELECT1 .= " ORDER BY ";
201
                if ($order) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $order of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
202
                    foreach ($order as $key => $value) {
203
                        if ($key > 0) {
204
                            $SELECT1 .= ',';
205
                        }
206
                        // Datatables numbers columns 0, 1, 2
207
                        // MySQL numbers columns 1, 2, 3
208
                        switch ($value['dir']) {
209
                            case 'asc':
210
                                $SELECT1 .= ":col_" . $key . " ASC";
211
                                break;
212
                            case 'desc':
213
                                $SELECT1 .= ":col_" . $key . " DESC";
214
                            break;
215
                        }
216
                        $ARGS1['col_' . $key] = 1 + $value['column'];
217
                    }
218
                } else {
219
                    $SELECT1 = " 1 ASC";
220
                }
221
222
                if ($length > 0) {
223
                    $SELECT1 .= " LIMIT :length OFFSET :start";
224
                    $ARGS1['length'] = $length;
225
                    $ARGS1['start']  = $start;
226
                }
227
228
                $rows = Database::prepare($SELECT1)->execute($ARGS1)->fetchAll();
229
230
                // Total filtered/unfiltered rows
231
                $recordsFiltered = Database::prepare("SELECT FOUND_ROWS()")->fetchOne();
232
                $recordsTotal    = Database::prepare($SELECT2)->execute($ARGS2)->fetchOne();
233
234
                $data = array();
235
                foreach ($rows as $row) {
236
                    $media  = Media::getInstance($row->xref, Tree::findById($row->gedcom_id), $row->gedcom);
237
                    $data[] = array(
238
                    GedcomTag::getLabelValue('URL', $row->m_filename),
239
                    $media->displayImage(),
240
                    mediaObjectInfo($media),
241
                    );
242
                }
243
            break;
244
245
            case 'unused':
246
                // Which trees use this media folder?
247
                $media_trees = Database::prepare(
248
                "SELECT gedcom_name, gedcom_name" .
249
                " FROM `##gedcom`" .
250
                " JOIN `##gedcom_setting` USING (gedcom_id)" .
251
                " WHERE setting_name='MEDIA_DIRECTORY' AND setting_value = :media_folder AND gedcom_id > 0"
252
                )->execute(array(
253
                    'media_folder' => $media_folder,
254
                ))->fetchAssoc();
255
256
                $disk_files = all_disk_files($media_folder, $media_path, $subfolders, $search);
257
                $db_files   = all_media_files($media_folder, $media_path, $subfolders, $search);
258
259
                // All unused files
260
                $unused_files = array_diff($disk_files, $db_files);
261
                $recordsTotal = count($unused_files);
262
263
                // Filter unused files
264
                if ($search) {
265
                    $unused_files = array_filter($unused_files, function ($x) use ($search) {
266
                        return strpos($x, $search) !== false;
267
                    });
268
                }
269
                $recordsFiltered = count($unused_files);
270
271
                // Sort files - only option is column 0
272
                sort($unused_files);
273
                $order = Filter::get('order');
274
                if ($order && $order[0]['dir'] === 'desc') {
275
                    $unused_files = array_reverse($unused_files);
276
                }
277
278
                // Paginate unused files
279
                $unused_files = array_slice($unused_files, $start, $length);
280
281
                $data = array();
282
                foreach ($unused_files as $unused_file) {
283
                    $full_path  = WT_DATA_DIR . $media_folder . $media_path . $unused_file;
284
                    $thumb_path = WT_DATA_DIR . $media_folder . 'thumbs/' . $media_path . $unused_file;
285
                    if (!file_exists($thumb_path)) {
286
                        $thumb_path = $full_path;
287
                    }
288
289
                    try {
290
                        $imgsize = getimagesize($thumb_path);
291
                        // We can’t create a URL (not in public_html) or use the media firewall (no such object)
292
                        // so just the base64-encoded image inline.
293
                        if ($imgsize === false) {
294
                            // not an image
295
                            $img = '-';
296
                        } else {
297
                            $img = '<img src="data:' . $imgsize['mime'] . ';base64,' . base64_encode(file_get_contents($thumb_path)) . '" class="thumbnail" ' . $imgsize[3] . '" style="max-width:100px;height:auto;">';
298
                        }
299
                    } catch (\ErrorException $ex) {
300
                        // Not an image, or not a valid image?
301
                        $img = '-';
302
                    }
303
304
                    // Is there a pending record for this file?
305
                    $exists_pending = Database::prepare(
306
                    "SELECT 1 FROM `##change` WHERE status='pending' AND new_gedcom LIKE CONCAT('%\n1 FILE ', :unused_file, '\n%')"
307
                    )->execute(array(
308
                        'unused_file' => Filter::escapeLike($unused_file),
309
                    ))->fetchOne();
310
311
                    // Form to create new media object in each tree
312
                    $create_form = '';
313
                    if (!$exists_pending) {
314
                        foreach ($media_trees as $media_tree) {
315
                            $create_form .=
316
                                '<p><a href="" onclick="window.open(\'addmedia.php?action=showmediaform&amp;ged=' . rawurlencode($media_tree) . '&amp;filename=' . rawurlencode($unused_file) . '\', \'_blank\', edit_window_specs); return false;">' . I18N::translate('Create') . '</a> — ' . Filter::escapeHtml($media_tree) . '<p>';
317
                        }
318
                    }
319
320
                    $conf        = I18N::translate('Are you sure you want to delete “%s”?', Filter::escapeJs($unused_file));
321
                    $delete_link =
322
                    '<p><a onclick="if (confirm(\'' . Filter::escapeJs($conf) . '\')) jQuery.post(\'admin_media.php\',{delete:\'' . Filter::escapeJs($media_path . $unused_file) . '\',media_folder:\'' . Filter::escapeJs($media_folder) . '\'},function(){location.reload();})" href="#">' . I18N::translate('Delete') . '</a></p>';
323
324
                    $data[] = array(
325
                    mediaFileInfo($media_folder, $media_path, $unused_file) . $delete_link,
326
                    $img,
327
                    $create_form,
328
                    );
329
                }
330
            break;
331
332
            default:
333
            throw new \DomainException('Invalid action');
334
        }
335
336
        header('Content-type: application/json');
337
        // See http://www.datatables.net/usage/server-side
338
        echo json_encode(array(
339
        'draw'            => Filter::getInteger('draw'), // String, but always an integer
340
        'recordsTotal'    => $recordsTotal,
341
        'recordsFiltered' => $recordsFiltered,
342
        'data'            => $data,
343
        ));
344
345
        return;
346
}
347
348
/**
349
 * A unique list of media folders, from all trees.
350
 *
351
 * @return string[]
352
 */
353
function all_media_folders()
354
{
355
    return Database::prepare(
356
        "SELECT setting_value, setting_value" .
357
        " FROM `##gedcom_setting`" .
358
        " WHERE setting_name='MEDIA_DIRECTORY' AND gedcom_id > 0" .
359
        " GROUP BY 1" .
360
        " ORDER BY 1"
361
    )->execute(array())->fetchAssoc();
362
}
363
364
/**
365
 * Generate a list of media paths (within a media folder) used by all media objects.
366
 *
367
 * @param string $media_folder
368
 *
369
 * @return string[]
370
 */
371
function media_paths($media_folder)
372
{
373
    $media_paths = Database::prepare(
374
        "SELECT LEFT(m_filename, CHAR_LENGTH(m_filename) - CHAR_LENGTH(SUBSTRING_INDEX(m_filename, '/', -1))) AS media_path" .
375
        " FROM  `##media`" .
376
        " JOIN  `##gedcom_setting` ON (m_file = gedcom_id AND setting_name = 'MEDIA_DIRECTORY')" .
377
        " WHERE setting_value = :media_folder" .
378
        " AND   m_filename NOT LIKE 'http://%'" .
379
        " AND   m_filename NOT LIKE 'https://%'" .
380
        " GROUP BY 1" .
381
        " ORDER BY 1"
382
    )->execute(array(
383
        'media_folder' => $media_folder,
384
    ))->fetchOneColumn();
385
386
    if (!$media_paths || reset($media_paths) != '') {
0 ignored issues
show
Bug Best Practice introduced by
The expression $media_paths of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
387
        // Always include a (possibly empty) top-level folder
388
        array_unshift($media_paths, '');
389
    }
390
391
    return array_combine($media_paths, $media_paths);
392
}
393
394
/**
395
 * Search a folder (and optional subfolders) for filenames that match a search pattern.
396
 *
397
 * @param string $dir
398
 * @param bool   $recursive
399
 * @param string $filter
400
 *
401
 * @return string[]
402
 */
403
function scan_dirs($dir, $recursive, $filter)
404
{
405
    $files = array();
406
407
    // $dir comes from the database. The actual folder may not exist.
408
    if (is_dir($dir)) {
409
        foreach (scandir($dir) as $path) {
410
            if (is_dir($dir . $path)) {
411
                // What if there are user-defined subfolders “thumbs” or “watermarks”?
412
                if ($path != '.' && $path != '..' && $path != 'thumbs' && $path != 'watermark' && $recursive) {
413
                    foreach (scan_dirs($dir . $path . '/', $recursive, $filter) as $subpath) {
414
                        $files[] = $path . '/' . $subpath;
415
                    }
416
                }
417
            } elseif (!$filter || stripos($path, $filter) !== false) {
418
                $files[] = $path;
419
            }
420
        }
421
    }
422
423
    return $files;
424
}
425
426
/**
427
 * Fetch a list of all files on disk
428
 *
429
 * @param string $media_folder Location of root folder
430
 * @param string $media_path   Any subfolder
431
 * @param string $subfolders   Include or exclude subfolders
432
 * @param string $filter       Filter files whose name contains this test
433
 *
434
 * @return string[]
435
 */
436
function all_disk_files($media_folder, $media_path, $subfolders, $filter)
437
{
438
    return scan_dirs(WT_DATA_DIR . $media_folder . $media_path, $subfolders == 'include', $filter);
439
}
440
441
/**
442
 * Fetch a list of all files on in the database.
443
 *
444
 * The subfolders parameter is not implemented. However, as we
445
 * currently use this function as an exclusion list, it is harmless
446
 * to always include sub-folders.
447
 *
448
 * @param string $media_folder
449
 * @param string $media_path
450
 * @param string $subfolders
451
 * @param string $filter
452
 *
453
 * @return string[]
454
 */
455
function all_media_files($media_folder, $media_path, $subfolders, $filter)
0 ignored issues
show
The parameter $subfolders is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

455
function all_media_files($media_folder, $media_path, /** @scrutinizer ignore-unused */ $subfolders, $filter)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
456
{
457
    return Database::prepare(
458
        "SELECT SQL_CALC_FOUND_ROWS TRIM(LEADING :media_path_1 FROM m_filename) AS media_path, 'OBJE' AS type, m_titl, m_id AS xref, m_file AS ged_id, m_gedcom AS gedrec, m_filename" .
459
        " FROM  `##media`" .
460
        " JOIN  `##gedcom_setting` ON (m_file = gedcom_id AND setting_name = 'MEDIA_DIRECTORY')" .
461
        " JOIN  `##gedcom`         USING (gedcom_id)" .
462
        " WHERE setting_value = :media_folder" .
463
        " AND   m_filename LIKE CONCAT(:media_path_2, '%')" .
464
        " AND   (SUBSTRING_INDEX(m_filename, '/', -1) LIKE CONCAT('%', :filter_1, '%')" .
465
        "  OR   m_titl LIKE CONCAT('%', :filter_2, '%'))" .
466
        " AND   m_filename NOT LIKE 'http://%'" .
467
        " AND   m_filename NOT LIKE 'https://%'"
468
    )->execute(array(
469
        'media_path_1' => $media_path,
470
        'media_folder' => $media_folder,
471
        'media_path_2' => Filter::escapeLike($media_path),
472
        'filter_1'     => Filter::escapeLike($filter),
473
        'filter_2'     => Filter::escapeLike($filter),
474
    ))->fetchOneColumn();
475
}
476
477
/**
478
 * Generate some useful information and links about a media file.
479
 *
480
 * @param string $media_folder
481
 * @param string $media_path
482
 * @param string $file
483
 *
484
 * @return string
485
 */
486
function mediaFileInfo($media_folder, $media_path, $file)
487
{
488
    $html = '<dl>';
489
    $html .= '<dt>' . I18N::translate('Filename') . '</dt>';
490
    $html .= '<dd>' . Filter::escapeHtml($file) . '</dd>';
491
492
    $full_path = WT_DATA_DIR . $media_folder . $media_path . $file;
493
    if ($file && file_exists($full_path)) {
494
        try {
495
            $size = filesize($full_path);
496
            $size = (int) (($size + 1023) / 1024); // Round up to next KB
497
            $size = /* I18N: size of file in KB */ I18N::translate('%s KB', I18N::number($size));
498
            $html .= '<dt>' . I18N::translate('File size') . '</dt>';
499
            $html .= '<dd>' . $size . '</dd>';
500
501
            try {
502
                $imgsize = getimagesize($full_path);
503
                $html .= '<dt>' . I18N::translate('Image dimensions') . '</dt>';
504
                $html .= '<dd>' . /* I18N: image dimensions, width × height */
505
                    I18N::translate('%1$s × %2$s pixels', I18N::number($imgsize['0']), I18N::number($imgsize['1'])) . '</dd>';
506
            } catch (\ErrorException $ex) {
507
                // Not an image, or not a valid image?
508
            }
509
510
            $html .= '</dl>';
511
        } catch (\ErrorException $ex) {
512
            $html .= '</dl>';
513
            $html .= '<div class="alert alert-danger">' . I18N::translate('This media file exists, but cannot be accessed.') . '</div>';
514
        }
515
    } else {
516
        $html .= '</dl>';
517
        $html .= '<div class="alert alert-danger">' . I18N::translate('This media file does not exist.') . '</div>';
518
    }
519
520
    return $html;
521
}
522
523
/**
524
 * Generate some useful information and links about a media object.
525
 *
526
 * @param Media $media
527
 *
528
 * @return string HTML
529
 */
530
function mediaObjectInfo(Media $media)
531
{
532
    $xref   = $media->getXref();
533
    $gedcom = $media->getTree()->getName();
534
535
    $html =
536
        '<div class="btn-group"><button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-expanded="false"><i class="fa fa-pencil"></i> <span class="caret"></span></button><ul class="dropdown-menu" role="menu">' .
537
        '<li><a href="#" onclick="window.open(\'addmedia.php?action=editmedia&amp;pid=' . $xref . '&ged=' . Filter::escapeJs($gedcom) . '\', \'_blank\', edit_window_specs);"><i class="fa fa-fw fa-pencil"></i> ' . I18N::translate('Edit') . '</a></li>' .
538
        '<li><a href="#" onclick="return delete_record(\'' . I18N::translate('Are you sure you want to delete “%s”?', Filter::escapeJs(Filter::unescapeHtml($media->getFullName()))) . '\', \'' . $media->getXref() . '\', \'' . Filter::escapeJs($gedcom) . '\');"><i class="fa fa-fw fa-trash-o"></i> ' . I18N::translate('Delete') . '</a></li>' .
539
        '<li><a href="#" onclick="return ilinkitem(\'' . $media->getXref() . '\', \'person\', WT_GEDCOM)"><i class="fa fa-fw fa-link"></i> ' . I18N::translate('Link this media object to an individual') . '</a></li>' .
540
        '<li><a href="#" onclick="return ilinkitem(\'' . $media->getXref() . '\', \'family\', WT_GEDCOM)"><i class="fa fa-fw fa-link"></i> ' . I18N::translate('Link this media object to a family') . '</a></li>' .
541
        '<li><a href="#" onclick="return ilinkitem(\'' . $media->getXref() . '\', \'source\', WT_GEDCOM)"><i class="fa fa-fw fa-link"></i> ' . I18N::translate('Link this media object to a source') . '</a></li>';
542
543
    if (Module::getModuleByName('GEDFact_assistant')) {
544
        $html .= '<li><a href="#" onclick="return ilinkitem(\'' . $media->getXref() . '\', \'manage\', WT_GEDCOM)"><i class="fa fa-fw fa-link"></i> ' . I18N::translate('Manage the links') . '</a></li>';
545
    }
546
547
    $html .=
548
        '</ul></div> ' .
549
        '<b><a href="' . $media->getHtmlUrl() . '">' . $media->getFullName() . '</a></b>' .
550
        '<div><i>' . Filter::escapeHtml($media->getNote()) . '</i></div>';
551
552
    $html .= '<br>';
553
554
    $linked = array();
555
    foreach ($media->linkedIndividuals('OBJE') as $link) {
556
        $linked[] = '<a href="' . $link->getHtmlUrl() . '">' . $link->getFullName() . '</a>';
557
    }
558
    foreach ($media->linkedFamilies('OBJE') as $link) {
559
        $linked[] = '<a href="' . $link->getHtmlUrl() . '">' . $link->getFullName() . '</a>';
560
    }
561
    foreach ($media->linkedSources('OBJE') as $link) {
562
        $linked[] = '<a href="' . $link->getHtmlUrl() . '">' . $link->getFullName() . '</a>';
563
    }
564
    foreach ($media->linkedNotes('OBJE') as $link) {
565
        // Invalid GEDCOM - you cannot link a NOTE to an OBJE
566
        $linked[] = '<a href="' . $link->getHtmlUrl() . '">' . $link->getFullName() . '</a>';
567
    }
568
    foreach ($media->linkedRepositories('OBJE') as $link) {
569
        // Invalid GEDCOM - you cannot link a REPO to an OBJE
570
        $linked[] = '<a href="' . $link->getHtmlUrl() . '">' . $link->getFullName() . '</a>';
571
    }
572
    if ($linked) {
573
        $html .= '<ul>';
574
        foreach ($linked as $link) {
575
            $html .= '<li>' . $link . '</li>';
576
        }
577
        $html .= '</ul>';
578
    } else {
579
        $html .= '<div class="alert alert-danger">' . I18N::translate('This media object is not linked to any other record.') . '</div>';
580
    }
581
582
    return $html;
583
}
584
585
////////////////////////////////////////////////////////////////////////////////
586
// Start here
587
////////////////////////////////////////////////////////////////////////////////
588
589
// Preserve the pagination/filtering/sorting between requests, so that the
590
// browser’s back button works. Pagination is dependent on the currently
591
// selected folder.
592
$table_id = md5($files . $media_folder . $media_path . $subfolders);
593
594
$controller = new PageController;
595
$controller
596
    ->restrictAccess(Auth::isAdmin())
597
    ->setPageTitle(I18N::translate('Manage media'))
598
    ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)
599
    ->addExternalJavascript(WT_DATATABLES_BOOTSTRAP_JS_URL)
600
    ->pageHeader()
601
    ->addInlineJavascript('
602
	jQuery("#media-table-' . $table_id . '").dataTable({
603
		processing: true,
604
		serverSide: true,
605
		ajax: "' . WT_BASE_URL . WT_SCRIPT_NAME . '?action=load_json&files=' . $files . '&media_folder=' . $media_folder . '&media_path=' . $media_path . '&subfolders=' . $subfolders . '",
606
		' . I18N::datatablesI18N(array(5, 10, 20, 50, 100, 500, 1000, -1)) . ',
607
		autoWidth:false,
608
		pageLength: 10,
609
		pagingType: "full_numbers",
610
		stateSave: true,
611
		stateDuration: 300,
612
		columns: [
613
			{},
614
			{ sortable: false },
615
			{ sortable: ' . ($files === 'unused' ? 'false' : 'true') . ' }
616
		]
617
	});
618
	');
619
620
?>
621
<ol class="breadcrumb small">
622
    <li><a href="admin.php"><?php echo I18N::translate('Control panel'); ?></a></li>
623
    <li class="active"><?php echo $controller->getPageTitle(); ?></li>
624
</ol>
625
626
<h1><?php echo $controller->getPageTitle(); ?></h1>
627
628
<form>
629
    <table class="table table-bordered table-condensed">
630
        <thead>
631
            <tr>
632
                <th><?php echo I18N::translate('Media files'); ?></th>
633
                <th><?php echo I18N::translate('Media folders'); ?></th>
634
            </tr>
635
        </thead>
636
        <tbody>
637
            <tr>
638
                <td>
639
                    <label>
640
                        <input type="radio" name="files" value="local" <?php echo $files === 'local' ? 'checked' : ''; ?> onchange="this.form.submit();">
641
                        <?php echo /* I18N: “Local files” are stored on this computer */ I18N::translate('Local files'); ?>
642
                    </label>
643
                    <br>
644
                    <label>
645
                        <input type="radio" name="files" value="external" <?php echo $files === 'external' ? 'checked' : ''; ?> onchange="this.form.submit();">
646
                        <?php echo /* I18N: “External files” are stored on other computers */ I18N::translate('External files'); ?>
647
                    </label>
648
                    <br>
649
                    <label>
650
                        <input type="radio" name="files" value="unused" <?php echo $files === 'unused' ? 'checked' : ''; ?> onchange="this.form.submit();">
651
                        <?php echo I18N::translate('Unused files'); ?>
652
                    </label>
653
                </td>
654
                <td>
655
                    <?php if ($files === 'local' || $files === 'unused'): ?>
656
657
                    <div dir="ltr">
658
                        <?php if (count($media_folders) > 1): ?>
659
                            <?php echo WT_DATA_DIR, FunctionsEdit::selectEditControl('media_folder', $media_folders, null, $media_folder, 'onchange="this.form.submit();"'); ?>
660
                        <?php else: ?>
661
                        <?php echo WT_DATA_DIR, Filter::escapeHtml($media_folder); ?>
662
                        <input type="hidden" name="media_folder" value="<?php echo Filter::escapeHtml($media_folder); ?>">
663
                        <?php endif; ?>
664
                    </div>
665
666
                        <?php if (count($media_paths) > 1): ?>
667
                            <?php echo FunctionsEdit::selectEditControl('media_path', $media_paths, null, $media_path, 'onchange="this.form.submit();"'); ?>
668
                    <?php else: ?>
669
                    <?php echo Filter::escapeHtml($media_path); ?>
670
                    <input type="hidden" name="media_path" value="<?php echo Filter::escapeHtml($media_path); ?>">
671
                    <?php endif; ?>
672
673
                    <label>
674
                        <input type="radio" name="subfolders" value="include" <?php echo $subfolders === 'include' ? 'checked' : ''; ?> onchange="this.form.submit();">
675
                        <?php echo I18N::translate('Include subfolders'); ?>
676
                    </label>
677
                    <br>
678
                    <label>
679
                        <input type="radio" name="subfolders" value="exclude" <?php echo $subfolders === 'exclude' ? ' checked' : ''; ?> onchange="this.form.submit();">
680
                        <?php echo I18N::translate('Exclude subfolders'); ?>
681
                    </label>
682
683
                    <?php elseif ($files === 'external'): ?>
684
685
                    <?php echo I18N::translate('External media files have a URL instead of a filename.'); ?>
686
                    <input type="hidden" name="media_folder" value="<?php echo Filter::escapeHtml($media_folder); ?>">
687
                    <input type="hidden" name="media_path" value="<?php echo Filter::escapeHtml($media_path); ?>">
688
689
                    <?php endif; ?>
690
                </td>
691
            </tr>
692
        </tbody>
693
    </table>
694
</form>
695
<br>
696
<br>
697
<table class="table table-bordered table-condensed" id="media-table-<?php echo $table_id; ?>">
698
    <thead>
699
        <tr>
700
            <th><?php echo I18N::translate('Media file'); ?></th>
701
            <th><?php echo I18N::translate('Media'); ?></th>
702
            <th><?php echo I18N::translate('Media object'); ?></th>
703
        </tr>
704
    </thead>
705
    <tbody>
706
    </tbody>
707
</table>
708