import
[web.mtrack] / inc / attachment.php
1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
3
4 class MTrackAttachment {
5
6   static function add($object, $local_filename, $filename,
7       MTrackChangeset $CS)
8   {
9     $size = filesize($local_filename);
10     if (!$size) {
11       return false;
12     }
13     $hash = self::import_file($local_filename);
14     $fp = fopen($local_filename, 'rb');
15     $q = MTrackDB::get()->prepare(
16       'insert into attachments (object, hash, filename, size, cid, payload)
17       values (?, ?, ?, ?, ?, ?)');
18     $q->bindValue(1, $object);
19     $q->bindValue(2, $hash);
20     $q->bindValue(3, $filename);
21     $q->bindValue(4, $size);
22     $q->bindValue(5, $CS->cid);
23     $q->bindValue(6, $fp, PDO::PARAM_LOB);
24     $q->execute();
25     $CS->add("$object:@attachment:", '', $filename);
26   }
27
28   static function process_delete($relobj, MTrackChangeset $CS) {
29     if (!isset($_POST['delete_attachment'])) return;
30     if (!is_array($_POST['delete_attachment'])) return;
31     foreach ($_POST['delete_attachment'] as $name) {
32       $vars = explode('/', $name);
33       $filename = array_pop($vars);
34       $cid = array_pop($vars);
35       $object = join('/', $vars);
36
37       if ($object != $relobj) return;
38       MTrackDB::q('delete from attachments where object = ? and
39           cid = ? and filename = ?', $object, $cid, $filename);
40       $CS->add("$object:@attachment:", $filename, '');
41     }
42   }
43
44   /* this function is registered into sqlite and invoked from
45    * a trigger whenever an attachment row is deleted */
46   static function attachment_row_deleted($hash, $count)
47   {
48     if ($count == 0) {
49       // unlink the underlying file here
50       unlink(self::local_path($hash, false));
51     }
52     return $count;
53   }
54
55   static function hash_file($filename)
56   {
57     return sha1_file($filename);
58   }
59
60   static function local_path($hash, $fetch = true)
61   {
62     $adir = MTrackConfig::get('core', 'vardir') . '/attach';
63
64     /* 40 hex digits: split into 16, 16, 4, 4 */
65     $a = substr($hash, 0, 16);
66     $b = substr($hash, 16, 16);
67     $c = substr($hash, 32, 4);
68     $d = substr($hash, 36, 4);
69
70     $dir = "$adir/$a/$b/$c";
71     if (!is_dir($dir)) {
72       $mask = umask(0);
73       mkdir($dir, 02777, true);
74       umask($mask);
75     }
76     $filename = $dir . "/$d";
77
78     if ($fetch) {
79       // Tricky locking bit
80       $fp = @fopen($filename, 'c+');
81       flock($fp, LOCK_EX);
82       $st = fstat($fp);
83       if ($st['size'] == 0) {
84         /* we get to fill it out */
85
86         $db = MTrackDB::get();
87         $q = $db->prepare(
88             'select payload from attachments where hash = ?');
89         $q->execute(array($hash));
90         $q->bindColumn(1, $blob, PDO::PARAM_LOB);
91         $q->fetch();
92         if (is_string($blob)) {
93           fwrite($fp, $blob);
94         } else {
95           stream_copy_to_stream($blob, $fp);
96         }
97         rewind($fp);
98       }
99     }
100
101     return $filename;
102   }
103
104   /* calculates the hash of the filename.  If another file with
105    * the same hash does not already exist in the attachment area,
106    * the file is copied in.
107    * Returns the hash */
108   static function import_file($filename)
109   {
110     $h = self::hash_file($filename);
111     $dest = self::local_path($h, false);
112     if (!file_exists($dest)) {
113       if (is_uploaded_file($filename)) {
114         move_uploaded_file($filename, $dest);
115       } else if (!is_file($filename)) {
116         throw new Exception("$filename does not exist");
117       } else {
118         copy($filename, $dest);
119       }
120     }
121     return $h;
122   }
123
124   static function renderDeleteList($object)
125   {
126     global $ABSWEB;
127
128     $atts = MTrackDB::q('
129       select * from attachments
130       left join changes on (attachments.cid = changes.cid)
131       where attachments.object = ? order by changedate, filename',
132         $object)->fetchAll(PDO::FETCH_ASSOC);
133
134     if (count($atts) == 0) return '';
135
136     $max_dim = 150;
137
138     $html = <<<HTML
139 <em>Select the checkbox to delete an attachment</em>
140 <table>
141   <tr>
142     <td>&nbsp;</td>
143     <td>Attachment</td>
144     <td>Size</td>
145     <td>Added</td>
146   </tr>
147 HTML;
148
149     foreach ($atts as $row) {
150       $url = "{$ABSWEB}attachment.php/$object/$row[cid]/$row[filename]";
151       $html .= <<<HTML
152 <tr>
153   <td><input type='checkbox' name='delete_attachment[]'
154       value='$object/$row[cid]/$row[filename]'></td>
155   <td><a class='attachment' href='$url'>$row[filename]</a></td>
156   <td>$row[size]</td>
157   <td>
158 HTML;
159       $html .= mtrack_username($row['who'], array(
160           'no_image' => true
161         )) .
162         " " . mtrack_date($row['changedate']) . "</td></tr>\n";
163     }
164     $html .= "</table><br>";
165     return $html;
166   }
167
168   /* renders the attachment list for a given object */
169   static function renderList($object)
170   {
171     global $ABSWEB;
172
173     $atts = MTrackDB::q('
174       select * from attachments
175       left join changes on (attachments.cid = changes.cid)
176       where attachments.object = ? order by changedate, filename',
177         $object)->fetchAll(PDO::FETCH_ASSOC);
178
179     if (count($atts) == 0) return '';
180
181     $max_dim = 150;
182
183     $html = "<div class='attachment-list'><b>Attachments</b><ul>";
184     foreach ($atts as $row) {
185       $url = "{$ABSWEB}attachment.php/$object/$row[cid]/$row[filename]";
186       $html .= "<li><a class='attachment'" .
187         " href='$url'>".
188         "$row[filename]</a> ($row[size]) added by " .
189         mtrack_username($row['who'], array(
190           'no_image' => true
191         )) .
192         " " . mtrack_date($row['changedate']);
193
194       list($width, $height) = getimagesize(self::local_path($row['hash']));
195       if ($width + $height) {
196         /* limit maximum size */
197         if ($width > $max_dim) {
198           $height *= $max_dim / $width;
199           $width = $max_dim;
200         }
201         if ($height > $max_dim) {
202           $width *= $max_dim / $height;
203           $height = $max_dim;
204         }
205         $html .= "<br><a href='$url'><img src='$url' width='$width' border='0' height='$height'></a>";
206       }
207
208       $html .= "</li>\n";
209     }
210     $html .= "</ul></div>";
211     return $html;
212   }
213 }