lib.php 73.8 KB
Newer Older
1
2
<?php
/**
Francois Marier's avatar
Francois Marier committed
3
 * Mahara: Electronic portfolio, weblog, resume builder and social networking
4
5
 * Copyright (C) 2006-2009 Catalyst IT Ltd and others; see:
 *                         http://wiki.mahara.org/Contributors
6
 *
Francois Marier's avatar
Francois Marier committed
7
8
9
10
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
11
 *
Francois Marier's avatar
Francois Marier committed
12
13
14
15
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
16
 *
Francois Marier's avatar
Francois Marier committed
17
18
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20
21
 *
 * @package    mahara
 * @subpackage artefact-internal
22
 * @author     Catalyst IT Ltd
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL
24
 * @copyright  (C) 2006-2009 Catalyst IT Ltd http://catalyst.net.nz
25
26
27
28
29
 *
 */

defined('INTERNAL') || die();

30
31
32
class PluginArtefactFile extends PluginArtefact {

    public static function get_artefact_types() {
33
34
35
36
        return array(
            'file',
            'folder',
            'image',
37
            'profileicon',
38
            'archive',
39
        );
40
    }
41
42
    
    public static function get_block_types() {
43
        return array('image');
44
    }
45
46
47
48
49
50

    public static function get_plugin_name() {
        return 'file';
    }

    public static function menu_items() {
Richard Mansfield's avatar
Richard Mansfield committed
51
52
        return array(
            array(
53
54
                'path' => 'myportfolio/files',
                'url' => 'artefact/file/',
55
                'title' => get_string('myfiles', 'artefact.file'),
56
57
                'weight' => 20,
            ),
58
59
60
61
62
63
            array(
                'path' => 'profile/icons',
                'url' => 'artefact/file/profileicons.php',
                'title' => get_string('profileicons', 'artefact.file'),
                'weight' => 11,
            ),
Richard Mansfield's avatar
Richard Mansfield committed
64
        );
65
    }
66

67
68
    public static function group_tabs($groupid) {
        return array(
69
            'files' => array(
70
                'path' => 'groups/files',
71
72
                'url' => 'artefact/file/groupfiles.php?group='.$groupid,
                'title' => get_string('Files', 'artefact.file'),
73
                'weight' => 60,
74
75
76
77
            ),
        );
    }

78
79
80
81
82
83
84
85
86
87
88
89
    public static function get_event_subscriptions() {
        $subscriptions = array(
            (object)array(
                'plugin'       => 'file',
                'event'        => 'createuser',
                'callfunction' => 'newuser',
            ),
        );

        return $subscriptions;
    }

90
91
    public static function postinst($prevversion) {
        if ($prevversion == 0) {
92
            set_config_plugin('artefact', 'file', 'defaultquota', 52428800);
93
            set_config_plugin('artefact', 'file', 'uploadagreement', 1);
94
        }
95
        self::resync_filetype_list();
96
97
    }

98
    public static function newuser($event, $user) {
99
100
101
        if (empty($user->quota)) {
            update_record('usr', array('quota' => get_config_plugin('artefact', 'file', 'defaultquota')), array('id' => $user['id']));
        }
102
    }
103
    
Richard Mansfield's avatar
Richard Mansfield committed
104

105
106
107
108
109
110
111
112
113
    public static function sort_child_data($a, $b) {
        if ($a->container && !$b->container) {
            return -1;
        }
        else if (!$a->container && $b->container) {
            return 1;
        }
        return strnatcasecmp($a->text, $b->text);
    }
Richard Mansfield's avatar
Richard Mansfield committed
114

115
116
    public static function jsstrings($type) {
        static $jsstrings = array(
117
118
119
120
121
            'filebrowser' => array(
                'mahara' => array(
                    'remove',
                ),
                'artefact.file' => array(
122
123
124
                    'confirmdeletefile',
                    'confirmdeletefolder',
                    'confirmdeletefolderandcontents',
125
126
                    'editfile',
                    'editfolder',
127
                    'fileappearsinviews',
128
                    'fileattached',
129
                    'filewithnameexists',
130
                    'folderappearsinviews',
131
                    'foldernamerequired',
132
                    'foldernotempty',
133
134
135
136
137
138
                    'nametoolong',
                    'namefieldisrequired',
                    'uploadingfiletofolder',
                    'youmustagreetothecopyrightnotice',
                ),
            ),
139
140
141
        );
        return $jsstrings[$type];
    }
142

143
144
    public static function jshelp($type) {
        static $jshelp = array(
145
            'filebrowser' => array(
146
147
148
149
                'artefact.file' => array(
                    'notice',
                    'quota_message',
                    'uploadfile',
150
                    'tags',
151
152
153
154
155
156
                ),
            ),
        );
        return $jshelp[$type];
    }

157
158
159
160
161
162
163
164
165
166
167
168

    /**
     * Resyncs the allowed filetypes list with the XML configuration file.
     *
     * This can be called on install (and is, in the postinst method above),
     * and every time an upgrade is made that changes the file.
     */
    function resync_filetype_list() {
        require_once('xmlize.php');
        db_begin();
        log_info('Beginning resync of filetype list');

169
        $currentlist = get_records_assoc('artefact_file_mime_types');
170
        $newlist     = xmlize(file_get_contents(get_config('docroot') . 'artefact/file/filetypes.xml'));
171
172
        $filetypes   = $newlist['filetypes']['#']['filetype'];
        $newtypes    = array();
173

174
        // Step one: if a mimetype is in the new list that is not in the current
175
176
        // list, add it to the current list.
        foreach ($filetypes as $filetype) {
177
178
179
180
181
182
183
184
185
186
187
            $description = $filetype['#']['description'][0]['#'];
            foreach ($filetype['#']['mimetypes'][0]['#']['mimetype'] as $type) {
                $mimetype = $type['#'];
                if (!isset($currentlist[$mimetype])) {
                    log_debug('Adding mimetype: ' . $mimetype . ' (' . $description . ')');
                    execute_sql("INSERT INTO {artefact_file_mime_types} (mimetype, description) VALUES (?,?)", array($mimetype, $description));
                }
                else if ($currentlist[$mimetype]->description != $description) {
                    log_debug('Updating mimetype: ' . $mimetype . ' (' . $description . ')');
                    execute_sql("UPDATE {artefact_file_mime_types} SET description = ? WHERE mimetype = ?", array($description, $mimetype));
                }
188
                $newtypes[$mimetype] = true;
189
190
191
192
                $currentlist[$mimetype] = (object) array(
                    'mimetype'    => $mimetype,
                    'description' => $description,
                );
193
194
195
            }
        }

196
        // Step two: If a mimetype is in the current list that is not in the
197
        // new list, remove it from the current list.
198
199
200
201
        foreach ($currentlist as $mimetype => $type) {
            if (!isset($newtypes[$mimetype])) {
                log_debug('Removing mimetype: ' . $mimetype);
                delete_records('artefact_file_mime_types', 'mimetype', $mimetype);
202
203
204
205
206
207
            }
        }
       
        db_commit();
    }

208
209
210
211
212
213
214
    public static function get_mimetypes_from_description($description=null) {
        if (is_null($description)) {
            return get_column('artefact_file_mime_types', 'mimetype');
        }
        return get_column('artefact_file_mime_types', 'mimetype', 'description', $description);
    }

215
216
217
    public static function can_be_disabled() {
        return false;
    }
218
219
220
221
222
223

    public static function get_artefact_type_content_types() {
        return array(
            'file'        => array('file'),
            'image'       => array('file', 'image'),
            'profileicon' => array('image'),
224
            'archive'     => array('file'),
225
226
        );
    }
227
228

    public static function get_attachment_types() {
229
        return array('file', 'image', 'archive');
230
    }
231
232
233

    public static function recalculate_quota() {
        $data = get_records_sql_assoc("
234
            SELECT a.owner, SUM(f.size) AS bytes
235
236
237
238
239
240
            FROM {artefact} a JOIN {artefact_file_files} f ON a.id = f.artefact
            WHERE a.artefacttype IN ('file', 'image', 'profileicon', 'archive')
            AND a.owner IS NOT NULL
            GROUP BY a.owner", array()
        );
        if ($data) {
241
            return array_map(create_function('$a', 'return $a->bytes;'), $data);
242
243
244
        }
        return array();
    }
245
246
}

Martyn Smith's avatar
Martyn Smith committed
247
abstract class ArtefactTypeFileBase extends ArtefactType {
248

Nigel McNie's avatar
Nigel McNie committed
249
    public static function is_singular() {
Penny Leach's avatar
Penny Leach committed
250
251
252
        return false;
    }

253
    public static function get_icon($options=null) {
254
255
256
257
258
259
260

    }

    public static function collapse_config() {
        return 'file';
    }

Richard Mansfield's avatar
Richard Mansfield committed
261
262
263
264
265
266
    public function move($newparentid) {
        $this->set('parent', $newparentid);
        $this->commit();
        return true;
    }

Richard Mansfield's avatar
Richard Mansfield committed
267
268
    // Check if something exists in the db with a given title and parent,
    // either in adminfiles or with a specific owner.
Richard Mansfield's avatar
Richard Mansfield committed
269
    public static function file_exists($title, $owner, $folder, $institution=null, $group=null) {
270
        $filetypesql = "('" . join("','", array_diff(PluginArtefactFile::get_artefact_types(), array('profileicon'))) . "')";
271
        $ownersql = artefact_owner_sql($owner, $group, $institution);
272
273
        return get_field_sql('SELECT a.id FROM {artefact} a
            LEFT OUTER JOIN {artefact_file_files} f ON f.artefact = a.id
274
            WHERE a.title = ?
275
            AND a.' . $ownersql . '
276
            AND a.parent ' . (empty($folder) ? ' IS NULL' : ' = ' . (int)$folder) . '
277
            AND a.artefacttype IN ' . $filetypesql, array($title));
278
279
    }

280
281
282

    // Sort folders before files; then use nat sort order.
    public static function my_files_cmp($a, $b) {
283
284
        return strnatcasecmp((-2 * isset($a->isparent) + ($a->artefacttype != 'folder')) . $a->title,
                             (-2 * isset($b->isparent) + ($b->artefacttype != 'folder')) . $b->title);
285
286
287
    }


288
289
290
291
292
293
294
295
296
297
298
    /**
     * Gets a list of files in one folder
     *
     * @param integer $parentfolderid    Artefact id of the folder
     * @param integer $userid            Id of the owner, if the owner is a user
     * @param integer $group             Id of the owner, if the owner is a group
     * @param string  $institution       Id of the owner, if the owner is a institution
     * @param array   $filters           Filters to apply to the results. An array with keys 'artefacttype', 'filetype',
     *                                   where array values are arrays of artefacttype or mimetype strings.
     * @return array  A list of artefacts
     */
299
    public static function get_my_files_data($parentfolderid, $userid, $group=null, $institution=null, $filters=null) {
300
        global $USER;
Richard Mansfield's avatar
Richard Mansfield committed
301
        $select = '
Richard Mansfield's avatar
Richard Mansfield committed
302
            SELECT
303
                a.id, a.artefacttype, a.mtime, f.size, a.title, a.description, a.locked,
304
                COUNT(DISTINCT c.id) AS childcount, COUNT (DISTINCT aa.artefact) AS attachcount, COUNT(DISTINCT va.view) AS viewcount';
Richard Mansfield's avatar
Richard Mansfield committed
305
        $from = '
306
307
            FROM {artefact} a
                LEFT OUTER JOIN {artefact_file_files} f ON f.artefact = a.id
Richard Mansfield's avatar
Richard Mansfield committed
308
                LEFT OUTER JOIN {artefact} c ON c.parent = a.id 
309
                LEFT OUTER JOIN {view_artefact} va ON va.artefact = a.id
310
                LEFT OUTER JOIN {artefact_attachment} aa ON aa.attachment = a.id';
311
312

        if (!empty($filters['artefacttype'])) {
313
314
315
316
317
318
            $artefacttypes = $filters['artefacttype'];
            $artefacttypes[] = 'folder';
        }
        else {
            $artefacttypes = array_diff(PluginArtefactFile::get_artefact_types(), array('profileicon'));
        }
Richard Mansfield's avatar
Richard Mansfield committed
319
        $where = "
320
321
322
323
324
325
            WHERE a.artefacttype IN (" . join(',',  array_map('db_quote', $artefacttypes)) . ")";
        if (!empty($filters['filetype']) && is_array($filters['filetype'])) {
            $where .= "
            AND (a.artefacttype = 'folder' OR f.filetype IN (" . join(',',  array_map('db_quote', $filters['filetype'])) . '))';
        }

Richard Mansfield's avatar
Richard Mansfield committed
326
        $groupby = '
327
            GROUP BY
328
                a.id, a.artefacttype, a.mtime, f.size, a.title, a.description, a.locked';
Richard Mansfield's avatar
Richard Mansfield committed
329
330

        $phvals = array();
331

Richard Mansfield's avatar
Richard Mansfield committed
332
        if ($institution) {
333
334
335
336
337
338
339
340
341
            if ($institution == 'mahara' && !$USER->get('admin')) {
                // If non-admins are browsing site files, only let them see the public folder & its contents
                $publicfolder = ArtefactTypeFolder::admin_public_folder_id();
                $from .= '
                LEFT OUTER JOIN {artefact_parent_cache} pub ON (a.id = pub.artefact AND pub.parent = ?)';
                $where .= '
                AND (pub.parent = ? OR a.id = ?)';
                $phvals = array($publicfolder, $publicfolder, $publicfolder);
            }
Richard Mansfield's avatar
Richard Mansfield committed
342
343
344
345
346
347
            $where .= '
            AND a.institution = ? AND a.owner IS NULL';
            $phvals[] = $institution;
        }
        else if ($group) {
            $select .= ',
348
                r.can_edit, r.can_view, r.can_republish';
Richard Mansfield's avatar
Richard Mansfield committed
349
350
            $from .= '
                LEFT OUTER JOIN (
351
                    SELECT ar.artefact, ar.can_edit, ar.can_view, ar.can_republish
Richard Mansfield's avatar
Richard Mansfield committed
352
353
354
355
356
357
358
359
360
                    FROM {artefact_access_role} ar
                    INNER JOIN {group_member} gm ON ar.role = gm.role
                    WHERE gm.group = ? AND gm.member = ? 
                ) r ON r.artefact = a.id';
            $phvals[] = $group;
            $phvals[] = $USER->get('id');
            $where .= '
            AND a.group = ? AND a.owner IS NULL AND r.can_view = 1';
            $phvals[] = $group;
361
            $groupby .= ', r.can_edit, r.can_view, r.can_republish';
Richard Mansfield's avatar
Richard Mansfield committed
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
        }
        else {
            $where .= '
            AND a.institution IS NULL AND a.owner = ?';
            $phvals[] = $userid;
        }

        if ($parentfolderid) {
            $where .= '
            AND a.parent = ? ';
            $phvals[] = $parentfolderid;
        }
        else {
            $where .= '
            AND a.parent IS NULL';
        }
378

Richard Mansfield's avatar
Richard Mansfield committed
379
        $filedata = get_records_sql_assoc($select . $from . $where . $groupby, $phvals);
380
381
382
383
384
        if (!$filedata) {
            $filedata = array();
        }
        else {
            foreach ($filedata as $item) {
Richard Mansfield's avatar
Richard Mansfield committed
385
                $item->mtime = format_date(strtotime($item->mtime), 'strfdaymonthyearshort');
Richard Mansfield's avatar
Richard Mansfield committed
386
                $item->tags = array();
387
                $item->icon = call_static_method(generate_artefact_class_name($item->artefacttype), 'get_icon', array('id' => $item->id));
388
389
390
                if ($item->size) { // Doing this here now for non-js users
                    $item->size = ArtefactTypeFile::short_size($item->size, true);
                }
Richard Mansfield's avatar
Richard Mansfield committed
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
            }
            $where = 'artefact IN (' . join(',', array_keys($filedata)) . ')';
            $tags = get_records_select_array('artefact_tag', $where);
            if ($tags) {
                foreach ($tags as $t) {
                    $filedata[$t->artefact]->tags[] = $t->tag;
                }
            }
            if ($group) {  // Fetch permissions for each artefact
                $perms = get_records_select_array('artefact_access_role', $where);
                if ($perms) {
                    foreach ($perms as $perm) {
                        $filedata[$perm->artefact]->permissions[$perm->role] = array(
                            'view' => $perm->can_view,
                            'edit' => $perm->can_edit,
                            'republish' => $perm->can_republish
                        );
                    }
409
                }
410
411
412
            }
        }

Richard Mansfield's avatar
Richard Mansfield committed
413
414
        // Add parent folder to the list
        if (!empty($parentfolderid)) {
415
416
417
            $grandparentid = (int) get_field('artefact', 'parent', 'id', $parentfolderid);
            $filedata[$grandparentid] = (object) array(
                'title'        => get_string('parentfolder', 'artefact.file'),
Richard Mansfield's avatar
Richard Mansfield committed
418
419
420
                'artefacttype' => 'folder',
                'description'  => get_string('parentfolder', 'artefact.file'),
                'isparent'     => true,
421
422
                'id'           => $grandparentid,
                'icon'         => ArtefactTypeFolder::get_icon(),
Richard Mansfield's avatar
Richard Mansfield committed
423
424
425
            );
        }

426
        uasort($filedata, array("ArtefactTypeFileBase", "my_files_cmp"));
427
428
        return $filedata;
    }
429

430
431
432
433

    /**
     * Creates pieforms definition for forms on the my files, group files, etc. pages.
     */
434
    public static function files_form($page='', $group=null, $institution=null, $folder=null, $highlight=null, $edit=null) {
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
        $folder = param_integer('folder', 0);
        $edit = param_variable('edit', 0);
        if (is_array($edit)) {
            $edit = array_keys($edit);
            $edit = $edit[0];
        }
        $edit = (int) $edit;
        $highlight = null;
        if ($file = param_integer('file', 0)) {
            $highlight = array($file); // todo convert to file1=1&file2=2 etc
        }

        $form = array(
            'name'               => 'files',
            'jsform'             => true,
            'newiframeonsubmit'  => true,
            'jssuccesscallback'  => 'files_success',
            'renderer'           => 'oneline',
            'plugintype'         => 'artefact',
            'pluginname'         => 'file',
            'configdirs'         => array(get_config('libroot') . 'form/', get_config('docroot') . 'artefact/file/form/'),
456
457
            'group'              => $group,
            'institution'        => $institution,
458
459
460
461
462
463
            'elements'           => array(
                'filebrowser' => array(
                    'type'         => 'filebrowser',
                    'folder'       => $folder,
                    'highlight'    => $highlight,
                    'edit'         => $edit,
464
                    'page'         => $page,
465
466
                    'config'       => array(
                        'upload'          => true,
467
                        'uploadagreement' => get_config_plugin('artefact', 'file', 'uploadagreement'),
468
469
470
471
472
473
474
475
476
                        'createfolder'    => true,
                        'edit'            => true,
                        'select'          => false,
                    ),
                ),
            ),
        );
        return $form;
    }
477

478
    public static function files_js() {
479
        return "function files_success(form, data) { files_filebrowser.success(form, data); }";
480
481
    }

482
    public static function count_user_files($owner=null, $group=null, $institution=null) {
483
        $filetypes = array_diff(PluginArtefactFile::get_artefact_types(), array('profileicon'));
484
485
486
487
488
489
490
        foreach ($filetypes as $k => $v) {
            if ($v == 'folder') {
                unset($filetypes[$k]);
            }
        }
        $filetypesql = "('" . join("','", $filetypes) . "')";

491
        $ownersql = artefact_owner_sql($owner, $group, $institution);
492
        return (object) array(
493
494
            'files'   => count_records_select('artefact', "artefacttype IN $filetypesql AND $ownersql", array()),
            'folders' => count_records_select('artefact', "artefacttype = 'folder' AND $ownersql", array())
495
496
497
        );
    }

498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
    public static function artefactchooser_get_file_data($artefact) {
        $artefact->icon = call_static_method(generate_artefact_class_name($artefact->artefacttype), 'get_icon', array('id' => $artefact->id));
        if ($artefact->artefacttype == 'profileicon') {
            $artefact->hovertitle  =  $artefact->note;
            if ($artefact->title) {
                $artefact->hovertitle .= ': ' . $artefact->title;
            }
        }
        else {
            $artefact->hovertitle  =  $artefact->title;
            if ($artefact->description) {
                $artefact->hovertitle .= ': ' . $artefact->description;
            }
        }

513
        $folderdata = self::artefactchooser_folder_data($artefact);
514
515

        if ($artefact->artefacttype == 'profileicon') {
516
            $artefact->description = str_shorten_text($artefact->title, 30);
517
518
519
        }
        else {
            $path = $artefact->parent ? self::get_full_path($artefact->parent, $folderdata->data) : '';
520
            $artefact->description = str_shorten_text($folderdata->ownername . $path . $artefact->title, 30);
521
522
523
524
525
        }

        return $artefact;
    }

526
    public static function artefactchooser_folder_data(&$artefact) {
527
528
529
530
531
532
533
534
535
536
537
538
539
540
        // Grab data about all folders the artefact owner has, so we
        // can make full paths to them, and show the artefact owner if
        // it's a group or institution.
        static $folderdata = array();

        $ownerkey = $artefact->owner . '::' . $artefact->group . '::' . $artefact->institution;
        if (!isset($folderdata[$ownerkey])) {
            $ownersql = artefact_owner_sql($artefact->owner, $artefact->group, $artefact->institution);
            $folderdata[$ownerkey]->data = get_records_select_assoc('artefact', "artefacttype='folder' AND $ownersql", array(), '', 'id, title, parent');
            if ($artefact->group) {
                $folderdata[$ownerkey]->ownername = get_field('group', 'name', 'id', $artefact->group) . ':';
            }
            else if ($artefact->institution) {
                if ($artefact->institution == 'mahara') {
541
                    $folderdata[$ownerkey]->ownername = get_config('sitename') . ':';
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
                }
                else {
                    $folderdata[$ownerkey]->ownername = get_field('institution', 'displayname', 'name', $artefact->institution) . ':';
                }
            }
            else {
                $folderdata[$ownerkey]->ownername = '';
            }
        }

        return $folderdata[$ownerkey];
    }

    /**
     * Works out a full path to a folder, given an ID. Implemented this way so 
     * only one query is made.
     */
559
    public static function get_full_path($id, &$folderdata) {
560
561
562
563
564
565
566
567
        $path = '';
        while (!empty($id)) {
            $path = $folderdata[$id]->title . '/' . $path;
            $id = $folderdata[$id]->parent;
        }
        return $path;
    }

568
    public function default_parent_for_copy(&$view, &$template, $artefactstoignore) {
569
570
571
572
573
574
575
        static $folderid;

        if (!empty($folderid)) {
            return $folderid;
        }

        $viewfilesfolder = ArtefactTypeFolder::get_folder_id(get_string('viewfilesdirname', 'view'), get_string('viewfilesdirdesc', 'view'),
576
                                                             null, true, $view->get('owner'), $view->get('group'), $view->get('institution'), $artefactstoignore);
577
        $foldername = $view->get('id');
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
        $existing = get_column_sql("
            SELECT title
            FROM {artefact}
            WHERE parent = ? AND title LIKE ? || '%'", array($viewfilesfolder, $foldername));
        $sep = '';
        $ext = '';
        if ($existing) {
            while (in_array($foldername . $sep . $ext, $existing)) {
                $sep = '-';
                $ext++;
            }
        }
        $data = (object) array(
            'title'       => $foldername . $sep . $ext,
            'description' => get_string('filescopiedfromviewtemplate', 'view', $template->get('title')),
            'owner'       => $view->get('owner'),
            'group'       => $view->get('group'),
            'institution' => $view->get('institution'),
            'parent'      => $viewfilesfolder,
        );
        $folder = new ArtefactTypeFolder(0, $data);
        $folder->commit();

        $folderid = $folder->get('id');

        return $folderid;
    }

606
    /**
607
608
609
610
611
     * Return a unique artefact title for a given owner & parent.
     *
     * Try to add digits before the filename extension: If the desired
     * title contains a ".", add "." plus digits before the final ".",
     * otherwise append "." and digits.
612
613
614
615
616
617
618
     * 
     * @param string $desired
     * @param integer $parent
     * @param integer $owner
     * @param integer $group
     * @param string $institution
     */
619
    public static function get_new_file_title($desired, $parent, $owner=null, $group=null, $institution=null) {
620
        $bits = split('\.', $desired);
621
        if (count($bits) > 1 && preg_match('/[^0-9]/', end($bits))) {
622
623
624
625
626
627
628
629
            $start = join('.', array_slice($bits, 0, count($bits)-1));
            $end = '.' . end($bits);
        }
        else {
            $start = $desired;
            $end = '';
        }

630
        $where = ($parent && is_int($parent)) ? "parent = $parent" : 'parent IS NULL';
631
        $where .=  ' AND ' . artefact_owner_sql($owner, $group, $institution);
632

633
634
635
        $taken = get_column_sql("
            SELECT title FROM {artefact}
            WHERE artefacttype IN ('" . join("','", array_diff(PluginArtefactFile::get_artefact_types(), array('profileicon'))) . "')
636
            AND title LIKE ? || '%' || ? AND " . $where, array($start, $end));
637
        $taken = array_flip($taken);
638
639
640

        $i = 0;
        $newname = $start . $end;
641
642
        while (isset($taken[$newname])) {
            $i++;
643
            $newname = $start . '.' . $i . $end;
644
645
646
        }
        return $newname;
    }
647

648
649
    public static function blockconfig_filebrowser_element(&$instance, $default=array()) {
        return array(
650
651
652
653
654
655
            'name'         => 'filebrowser',
            'type'         => 'filebrowser',
            'title'        => get_string('file', 'artefact.file'),
            'folder'       => (int) param_variable('folder', 0),
            'highlight'    => null,
            'browse'       => true,
656
            'page'         => '/view/blocks.php' . View::make_base_url(),
657
658
659
660
661
            'config'       => array(
                'upload'          => true,
                'uploadagreement' => get_config_plugin('artefact', 'file', 'uploadagreement'),
                'createfolder'    => false,
                'edit'            => false,
662
                'tag'             => true,
663
664
                'select'          => true,
                'alwaysopen'      => true,
665
                'publishing'      => true,
666
667
            ),
            'tabs'         => $instance->get_view()->ownership(),
668
669
            'defaultvalue' => $default,
            'selectlistcallback' => 'artefact_get_records_by_id',
670
671
672
        );
    }

673
674
}

675

676
class ArtefactTypeFile extends ArtefactTypeFileBase {
677

678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
    protected $size;

    // The original filename extension (when the file is first
    // uploaded) is saved here.  This is used as a workaround for IE's
    // detecting filetypes by extension: when the file is downloaded,
    // the extension can be appended to the name if it's not there
    // already.
    protected $oldextension;

    // The id used for the filename on the filesystem.  Usually this
    // is the same as the artefact id, but it can be different if the
    // file is a copy of another file artefact.
    protected $fileid;

    protected $filetype; // Mime type

694
695
696
    public function __construct($id = 0, $data = null) {
        parent::__construct($id, $data);
        
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
        if ($this->id && ($filedata = get_record('artefact_file_files', 'artefact', $this->id))) {
            foreach($filedata as $name => $value) {
                if (property_exists($this, $name)) {
                    $this->{$name} = $value;
                }
            }
        }
    }

    /**
     * This function updates or inserts the artefact.  This involves putting
     * some data in the artefact table (handled by parent::commit()), and then
     * some data in the artefact_file_files table.
     */
    public function commit() {
        // Just forget the whole thing when we're clean.
        if (empty($this->dirty)) {
            return;
        }

        // We need to keep track of newness before and after.
        $new = empty($this->id);

        // Commit to the artefact table.
        parent::commit();

        // Reset dirtyness for the time being.
        $this->dirty = true;
725

726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
        $data = (object)array(
            'artefact'      => $this->get('id'),
            'size'          => $this->get('size'),
            'oldextension'  => $this->get('oldextension'),
            'fileid'        => $this->get('fileid'),
            'filetype'      => $this->get('filetype'),
        );

        if ($new) {
            if (empty($data->fileid)) {
                $data->fileid = $data->artefact;
            }
            insert_record('artefact_file_files', $data);
        }
        else {
            update_record('artefact_file_files', $data, 'artefact');
        }

        $this->dirty = false;
745
746
    }

747
    public static function get_file_directory($id) {
748
        return "artefact/file/originals/" . ($id % 256);
749
750
    }

751
    public function get_path() {
752
        return get_config('dataroot') . self::get_file_directory($this->fileid) . '/' .  $this->fileid;
753
754
    }

755

756
757
758
759
    /**
     * Test file type and return a new Image or File.
     */
    public static function new_file($path, $data) {
760
761
762
763
764
765
766
        require_once('file.php');
        if (is_image_file($path)) {
            // If it's detected as an image, overwrite the browser mime type
            $imageinfo      = getimagesize($path);
            $data->filetype = $imageinfo['mime'];
            $data->width    = $imageinfo[0];
            $data->height   = $imageinfo[1];
767
768
            return new ArtefactTypeImage(0, $data);
        }
769
770
        if ($archive = ArtefactTypeArchive::new_archive($path, $data)) {
            return $archive;
771
        }
772
773
774
        return new ArtefactTypeFile(0, $data);
    }

775
776
777
778
    /**
     * Moves a file into the myfiles area.
     * Takes the name of a file outside the myfiles area.
     * Returns a boolean indicating success or failure.
779
780
781
782
     *
     * Note: this method is crappy because it returns false instead of throwing 
     * exceptions. It's not used in many places, and should probably die in a 
     * future version. So think twice before using it :)
783
     */
784
    public static function save_file($pathname, $data, User &$user=null, $outsidedataroot=false) {
785
        $dataroot = get_config('dataroot');
786
787
788
        if (!$outsidedataroot) {
            $pathname = $dataroot . $pathname;
        }
789
        if (!file_exists($pathname) || !is_readable($pathname)) {
790
791
            return false;
        }
792
        $size = filesize($pathname);
793
        $f = self::new_file($pathname, $data);
Richard Mansfield's avatar
Richard Mansfield committed
794
        $f->set('size', $size);
795
        // @todo: Set mime type! (and old extension)
Richard Mansfield's avatar
Richard Mansfield committed
796
797
798
799
        $f->commit();
        $id = $f->get('id');

        $newdir = $dataroot . self::get_file_directory($id);
800
        check_dir_exists($newdir);
Richard Mansfield's avatar
Richard Mansfield committed
801
        $newname = $newdir . '/' . $id;
802
        if (!rename($pathname, $newname)) {
Richard Mansfield's avatar
Richard Mansfield committed
803
            $f->delete();
804
805
806
807
808
809
810
811
812
813
814
815
816
            return false;
        }
        if (empty($user)) {
            global $USER;
            $user = $USER;
        }
        try {
            $user->quota_add($size);
            $user->commit();
            return $id;
        }
        catch (QuotaExceededException $e) {
            $f->delete();
817
            return false;
818
819
        }
    }
Richard Mansfield's avatar
Richard Mansfield committed
820

821

822
    /**
Richard Mansfield's avatar
Richard Mansfield committed
823
824
     * Processes a newly uploaded file, copies it to disk, and creates
     * a new artefact object.
825
826
827
828
829
830
831
     * Takes the name of a file input.
     * Returns false for no errors, or a string describing the error.
     */
    public static function save_uploaded_file($inputname, $data) {
        require_once('uploadmanager.php');
        $um = new upload_manager($inputname);
        if ($error = $um->preprocess_file()) {
832
            throw new UploadException($error);
833
        }
Richard Mansfield's avatar
Richard Mansfield committed
834
        $size = $um->file['size'];
835
836
837
838
        if (!empty($data->owner)) {
            global $USER;
            if ($data->owner == $USER->get('id')) {
                $owner = $USER;
839
                $owner->quota_refresh();
840
841
842
843
844
845
846
847
            }
            else {
                $owner = new User;
                $owner->find_by_id($data->owner);
            }
            if (!$owner->quota_allowed($size)) {
                throw new QuotaExceededException(get_string('uploadexceedsquota', 'artefact.file'));
            }
Richard Mansfield's avatar
Richard Mansfield committed
848
        }
849
850
        $data->size = $size;

851
852
853
854
        if ($um->file['type'] == 'application/octet-stream') {
            // the browser wasn't sure, so use file_mime_type to guess
            require_once('file.php');
            $data->filetype = file_mime_type($um->file['tmp_name']);
855
856
857
858
859
        }
        else {
            $data->filetype = $um->file['type'];
        }

860
        $data->oldextension = $um->original_filename_extension();
Richard Mansfield's avatar
Richard Mansfield committed
861
        $f = self::new_file($um->file['tmp_name'], $data);
862
863
864
865
866
867
        $f->commit();
        $id = $f->get('id');
        // Save the file using its id as the filename, and use its id modulo
        // the number of subdirectories as the directory name.
        if ($error = $um->save_file(self::get_file_directory($id) , $id)) {
            $f->delete();
868
            throw new UploadException($error);
869
        }
870
        else if (isset($owner)) {
871
872
            $owner->quota_add($size);
            $owner->commit();
Richard Mansfield's avatar
Richard Mansfield committed
873
        }
874
        return $id;
875
    }
Richard Mansfield's avatar
Richard Mansfield committed
876

877

878
879
880
881
882
883
884
885
886
887
888
    // Return the title with the original extension appended to it if
    // it's not already there.
    public function download_title() {
        $extn = $this->get('oldextension');
        $name = $this->get('title');
        if (substr($name, -1-strlen($extn)) == '.' . $extn) {
            return $name;
        }
        return $name . (substr($name, -1) == '.' ? '' : '.') . $extn;
    }

889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
    public function render_self($options) {
        $options['id'] = $this->get('id');

        $downloadpath = get_config('wwwroot') . 'artefact/file/download.php?file=' . $this->get('id');
        if (isset($options['viewid'])) {
            $downloadpath .= '&view=' . $options['viewid'];
        }
        $filetype = get_string($this->get('oldextension'), 'artefact.file');
        if (substr($filetype, 0, 2) == '[[') {
            $filetype = $this->get('oldextension') . ' ' . get_string('file', 'artefact.file');
        }

        $smarty = smarty_core();
        $smarty->assign('iconpath', $this->get_icon($options));
        $smarty->assign('downloadpath', $downloadpath);
        $smarty->assign('filetype', $filetype);
        $smarty->assign('ownername', $this->display_owner());
        $smarty->assign('created', strftime(get_string('strftimedaydatetime'), $this->get('ctime')));
        $smarty->assign('modified', strftime(get_string('strftimedaydatetime'), $this->get('mtime')));
        $smarty->assign('size', $this->describe_size() . ' (' . $this->get('size') . ' ' . get_string('bytes', 'artefact.file') . ')');

        foreach (array('title', 'description', 'artefacttype', 'owner', 'tags') as $field) {
            $smarty->assign($field, $this->get($field));
        }

        return array('html' => $smarty->fetch('artefact:file:file_render_self.tpl'), 'javascript' => '');
    }

917

918
    public static function get_admin_files($public) {
919
        $pubfolder = ArtefactTypeFolder::admin_public_folder_id();
920
        $artefacts = get_records_sql_assoc("
921
            SELECT
922
                a.id, a.title, a.parent, a.artefacttype
923
            FROM {artefact} a
924
                LEFT OUTER JOIN {artefact_file_files} f ON f.artefact = a.id
925
            WHERE a.institution = 'mahara'", array());
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947

        $files = array();
        if (!empty($artefacts)) {
            foreach ($artefacts as $a) {
                if ($a->artefacttype != 'folder') {
                    $title = $a->title;
                    $parent = $a->parent;
                    while (!empty($parent)) {
                        if ($public && $parent == $pubfolder) {
                            $files[] = array('name' => $title, 'id' => $a->id);
                            continue 2;
                        }
                        $title = $artefacts[$parent]->title . '/' . $title;
                        $parent = $artefacts[$parent]->parent;
                    }
                    if (!$public) {
                        $files[] = array('name' => $title, 'id' => $a->id);
                    }
                }
            }
        }
        return $files;
948
    }
Richard Mansfield's avatar
Richard Mansfield committed
949

950
951
952
953
    public function delete() {
        if (empty($this->id)) {
            return; 
        }
Richard Mansfield's avatar
Richard Mansfield committed
954
        $file = $this->get_path();
955
956
        if (is_file($file)) {
            $size = filesize($file);
957
958
959
960
            // Only delete the file on disk if no other artefacts point to it
            if (count_records('artefact_file_files', 'fileid', $this->get('id')) == 1) {
                unlink($file);
            }
961
962
            global $USER;
            // Deleting other users' files won't lower their quotas yet...
963
            if (!$this->institution && $USER->id == $this->get('owner')) {
964
965
                $USER->quota_remove($size);
                $USER->commit();
966
            }
Richard Mansfield's avatar
Richard Mansfield committed
967
        }
968
969
970

        delete_records('artefact_attachment', 'attachment', $this->id);
        delete_records('artefact_file_files', 'artefact', $this->id);
971
        delete_records('site_menu', 'file', $this->id);
972
973
974
        parent::delete();
    }

975
976
977
978
979
980
981
    public static function bulk_delete($artefactids) {
        global $USER;

        if (empty($artefactids)) {
            return;
        }

982
        $idstr = join(',', array_map('intval', $artefactids));
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997

        db_begin();
        // Get the size of all the files we're about to delete that belong to
        // the user.
        $totalsize = get_field_sql('
            SELECT SUM(size)
            FROM {artefact_file_files} f JOIN {artefact} a ON f.artefact = a.id
            WHERE a.owner = ? AND f.artefact IN (' . $idstr . ')',
            array($USER->get('id'))
        );

        // Get all fileids so that we can delete the files on disk
        $fileids = get_records_select_assoc('artefact_file_files', 'artefact IN (' . $idstr . ')', array());

        $fileidcounts = get_records_sql_assoc('
998
            SELECT fileid, COUNT(fileid) AS fileidcount
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
            FROM {artefact_file_files}
            WHERE artefact IN (' . $idstr . ')
            GROUP BY fileid',
            null
        );

        // The current rule is that file deletion should be logged in the artefact_log table
        // only for group-owned files.  To save time we will be slightly naughty here and
        // log deletion for all these files if at least one is group-owned.
        $log = (bool) count_records_select('artefact', 'id IN (' . $idstr . ') AND "group" IS NOT NULL');

        delete_records_select('artefact_attachment', 'attachment IN (' . $idstr . ')');
        delete_records_select('artefact_file_files', 'artefact IN (' . $idstr . ')');
        parent::bulk_delete($artefactids, $log);

        foreach ($fileids as $r) {
            // Delete the file on disk if there's only one artefact left pointing to it
1016
            if ($fileidcounts[$r->fileid]->fileidcount == 1) {
1017
1018
1019
1020
1021
                $file = get_config('dataroot') . self::get_file_directory($r->fileid) . '/' .  $r->fileid;
                if (is_file($file)) {
                    unlink($file);
                }
            }
1022
            $fileidcounts[$r->fileid]->fileidcount--;
1023
1024
1025
1026
1027
1028
1029
1030
1031
        }

        if ($totalsize) {
            $USER->quota_remove($totalsize);
            $USER->commit();
        }
        db_commit();
    }

1032
1033
1034
1035
    public static function has_config() {
        return true;
    }

1036
    public static function get_icon($options=null) {
1037
1038
        global $THEME;
        return $THEME->get_url('images/file.gif');
1039
1040
    }

1041
    public static function get_config_options() {
1042
        $elements = array();
1043
1044
1045
1046
        $defaultquota = get_config_plugin('artefact', 'file', 'defaultquota');
        if (empty($defaultquota)) {
            $defaultquota = 1024 * 1024 * 10;
        }
1047
1048
1049
        $elements['quotafieldset'] = array(
            'type' => 'fieldset',
            'legend' => get_string('defaultquota', 'artefact.file'),
1050
            'elements' => array(
1051
1052
1053
                'defaultquotadescription' => array(
                    'value' => '<tr><td colspan="2">' . get_string('defaultquotadescription', 'artefact.file') . '</td></tr>'
                ),
1054
1055
1056
1057
                'defaultquota' => array(
                    'title'        => get_string('defaultquota', 'artefact.file'), 
                    'type'         => 'bytes',
                    'defaultvalue' => $defaultquota,
1058
                )
1059
            ),
1060
1061
1062
            'collapsible' => true
        );

1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
        $maxquota = get_config_plugin('artefact', 'file', 'maxquota');
        $maxquotaenabled = get_config_plugin('artefact', 'file', 'maxquotaenabled');
        if (empty($maxquota)) {
            $maxquota = 1024 * 1024 * 1024;
        }
        $elements['maxquota'] = array(
            'type' => 'fieldset',
            'legend' => get_string('maxquota', 'artefact.file'),
            'elements' => array(
                'maxquotadescription' => array(
                    'value' => '<tr><td colspan="2">' . get_string('maxquotadescription', 'artefact.file') . '</td></tr>'
                ),
                'maxquotaenabled' => array(
                    'title'        => get_string('maxquotaenabled', 'artefact.file'),
                    'type'         => 'checkbox',
                    'defaultvalue' => $maxquotaenabled,
                ),
                'maxquota' => array(
                    'title'        => get_string('maxquota', 'artefact.file'),
                    'type'         => 'bytes',
                    'defaultvalue' => $maxquota,
                ),
            ),
            'collapsible' => true
        );

1089
1090
1091
        // Require user agreement before uploading files
        // Rework this when/if we provide translatable agreements
        $uploadagreement = get_config_plugin('artefact', 'file', 'uploadagreement');
1092
        $usecustomagreement = get_config_plugin('artefact', 'file', 'usecustomagreement');
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
        $elements['uploadagreementfieldset'] = array(
            'type' => 'fieldset',
            'legend' => get_string('uploadagreement', 'artefact.file'),
            'elements' => array(
                'uploadagreementdescription' => array(
                    'value' => '<tr><td colspan="2">' . get_string('uploadagreementdescription', 'artefact.file') . '</td></tr>'
                ),
                'uploadagreement' => array(
                    'title'        => get_string('requireagreement', 'artefact.file'), 
                    'type'         => 'checkbox',
                    'defaultvalue' => $uploadagreement,
                ),
1105
                'defaultagreement' => array(
1106
                    'type'         => 'html',