Overview

Packages

  • Vfs

Classes

  • Horde_Vfs
  • Horde_Vfs_Base
  • Horde_Vfs_Browser
  • Horde_Vfs_Exception
  • Horde_Vfs_File
  • Horde_Vfs_Ftp
  • Horde_Vfs_Gc
  • Horde_Vfs_Horde
  • Horde_Vfs_Kolab
  • Horde_Vfs_ListItem
  • Horde_Vfs_Musql
  • Horde_Vfs_Object
  • Horde_Vfs_Smb
  • Horde_Vfs_Sql
  • Horde_Vfs_SqlFile
  • Horde_Vfs_Ssh2
  • Horde_Vfs_Translation
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Stateless VFS implementation for a SMB server, based on smbclient.
  4:  *
  5:  * Required values for $params:
  6:  * <pre>
  7:  * username - (string)The username with which to connect to the SMB server.
  8:  * password - (string) The password with which to connect to the SMB server.
  9:  * hostspec - (string) The SMB server to connect to.
 10:  * port' - (integer) The SMB port number to connect to.
 11:  * share - (string) The share to access on the SMB server.
 12:  * smbclient - (string) The path to the 'smbclient' executable.
 13:  * </pre>
 14:  *
 15:  * Optional values for $params:
 16:  * <pre>
 17:  * ipaddress - (string) The address of the server to connect to.
 18:  * </pre>
 19:  *
 20:  * Functions not implemented:
 21:  * - changePermissions(): The SMB permission style does not fit with the
 22:  *                        module.
 23:  *
 24:  * Codebase copyright 2002 Paul Gareau <paul@xhawk.net>.  Adapted with
 25:  * permission by Patrice Levesque <wayne@ptaff.ca> from phpsmb-0.8 code, and
 26:  * converted to the LGPL.  Please do not taunt original author, contact
 27:  * Patrice Levesque or dev@lists.horde.org.
 28:  *
 29:  * See the enclosed file COPYING for license information (LGPL). If you
 30:  * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 31:  *
 32:  * @author  Paul Gareau <paul@xhawk.net>
 33:  * @author  Patrice Levesque <wayne@ptaff.ca>
 34:  * @package Vfs
 35:  */
 36: class Horde_Vfs_Smb extends Horde_Vfs_Base
 37: {
 38:     /**
 39:      * List of additional credentials required for this VFS backend.
 40:      *
 41:      * @var array
 42:      */
 43:     protected $_credentials = array('username', 'password');
 44: 
 45:     /**
 46:      * Authenticates a user on the SMB server and share.
 47:      *
 48:      * @throws Horde_Vfs_Exception
 49:      */
 50:     protected function _connect()
 51:     {
 52:         try {
 53:             $this->_command('', array('quit'));
 54:         } catch (Horde_Vfs_Exception $e) {
 55:             throw new Horde_Vfs_Exception('Authentication to the SMB server failed.');
 56:         }
 57:     }
 58: 
 59:     /**
 60:      * Retrieves the size of a file from the VFS.
 61:      *
 62:      * @param string $path  The pathname to the file.
 63:      * @param string $name  The filename to retrieve.
 64:      *
 65:      * @return integer  The file size.
 66:      * @throws Horde_Vfs_Exception
 67:      */
 68:     public function size($path, $name)
 69:     {
 70:         $file = $this->readFile($path, $name);
 71:         return filesize($file);
 72:     }
 73: 
 74:     /**
 75:      * Retrieves a file from the VFS.
 76:      *
 77:      * @param string $path  The pathname to the file.
 78:      * @param string $name  The filename to retrieve.
 79:      *
 80:      * @return string  The file data.
 81:      */
 82:     public function read($path, $name)
 83:     {
 84:         $file = $this->readFile($path, $name);
 85:         $size = filesize($file);
 86:         return ($size === 0)
 87:             ? ''
 88:             : file_get_contents($file);
 89:     }
 90: 
 91:     /**
 92:      * Retrieves a file from the VFS as an on-disk local file.
 93:      *
 94:      * This function provides a file on local disk with the data of a VFS file
 95:      * in it. This file <em>cannot</em> be modified! The behavior if you do
 96:      * modify it is undefined. It will be removed at the end of the request.
 97:      *
 98:      * @param string $path  The pathname to the file.
 99:      * @param string $name  The filename to retrieve.
100:      *
101:      * @return string  A local filename.
102:      */
103:     public function readFile($path, $name)
104:     {
105:         // Create a temporary file and register it for deletion at the
106:         // end of this request.
107:         if (!($localFile = Horde_Util::getTempFile('vfs'))) {
108:             throw new Horde_Vfs_Exception('Unable to create temporary file.');
109:         }
110: 
111:         list($path, $name) = $this->_escapeShellCommand($path, $name);
112:         $cmd = array('get \"' . $name . '\" ' . $localFile);
113:         $this->_command($path, $cmd);
114:         if (!file_exists($localFile)) {
115:             throw new Horde_Vfs_Exception(sprintf('Unable to open VFS file "%s".', $this->_getPath($path, $name)));
116:         }
117: 
118:         clearstatcache();
119: 
120:         return $localFile;
121:     }
122: 
123:     /**
124:      * Open a stream to a file in the VFS.
125:      *
126:      * @param string $path  The pathname to the file.
127:      * @param string $name  The filename to retrieve.
128:      *
129:      * @return resource  The stream.
130:      */
131:     public function readStream($path, $name)
132:     {
133:         return fopen($this->readFile($path, $name),OS_WINDOWS ? 'rb' : 'r');
134:     }
135: 
136:     /**
137:      * Stores a file in the VFS.
138:      *
139:      * @param string $path         The path to store the file in.
140:      * @param string $name         The filename to use.
141:      * @param string $tmpFile      The temporary file containing the data to be
142:      *                             stored.
143:      * @param boolean $autocreate  Automatically create directories?
144:      *
145:      * @throws Horde_Vfs_Exception
146:      */
147:     public function write($path, $name, $tmpFile, $autocreate = false)
148:     {
149:         // Double quotes not allowed in SMB filename.
150:         $name = str_replace('"', "'", $name);
151: 
152:         list($path, $name) = $this->_escapeShellCommand($path, $name);
153:         $cmd = array('put \"' . $tmpFile . '\" \"' . $name . '\"');
154:         // do we need to first autocreate the directory?
155:         if ($autocreate) {
156:             $this->autocreatePath($path);
157:         }
158: 
159:         $this->_command($path, $cmd);
160:     }
161: 
162:     /**
163:      * Stores a file in the VFS from raw data.
164:      *
165:      * @param string $path         The path to store the file in.
166:      * @param string $name         The filename to use.
167:      * @param string $data         The file data.
168:      * @param boolean $autocreate  Automatically create directories?
169:      *
170:      * @throws Horde_Vfs_Exception
171:      */
172:     public function writeData($path, $name, $data, $autocreate = false)
173:     {
174:         $tmpFile = Horde_Util::getTempFile('vfs');
175:         file_put_contents($tmpFile, $data);
176:         try {
177:             $this->write($path, $name, $tmpFile, $autocreate);
178:             unlink($tmpFile);
179:         } catch (Horde_Vfs_Exception $e) {
180:             unlink($tmpFile);
181:             throw $e;
182:         }
183:     }
184: 
185:     /**
186:      * Deletes a file from the VFS.
187:      *
188:      * @param string $path  The path to delete the file from.
189:      * @param string $name  The filename to use.
190:      *
191:      * @throws Horde_Vfs_Exception
192:      */
193:     public function deleteFile($path, $name)
194:     {
195:         // In some samba versions after samba-3.0.25-pre2, $path must
196:         // end in a trailing slash.
197:         if (substr($path, -1) != '/') {
198:             $path .= '/';
199:         }
200: 
201:         list($path, $name) = $this->_escapeShellCommand($path, $name);
202:         $cmd = array('del \"' . $name . '\"');
203:         $this->_command($path, $cmd);
204:     }
205: 
206:     /**
207:      * Checks if a given pathname is a folder.
208:      *
209:      * @param string $path  The path to the folder.
210:      * @param string $name  The file or folder name.
211:      *
212:      * @return boolean  True if it is a folder, false otherwise.
213:      */
214:     public function isFolder($path, $name)
215:     {
216:         list($path, $name) = $this->_escapeShellCommand($path, $name);
217:         $cmd = array('quit');
218:         try {
219:             $this->_command($this->_getPath($path, $name), array('quit'));
220:             return true;
221:         } catch (Horde_Vfs_Exception $e) {
222:             return false;
223:         }
224:     }
225: 
226:     /**
227:      * Deletes a folder from the VFS.
228:      *
229:      * @param string $path        The path to delete the folder from.
230:      * @param string $name        The name of the folder to delete.
231:      * @param boolean $recursive  Force a recursive delete?
232:      *
233:      * @throws Horde_Vfs_Exception
234:      */
235:     public function deleteFolder($path, $name, $recursive = false)
236:     {
237:         // In some samba versions after samba-3.0.25-pre2, $path must
238:         // end in a trailing slash.
239:         if (substr($path, -1) != '/') {
240:             $path .= '/';
241:         }
242: 
243:         if (!$this->isFolder($path, $name)) {
244:             throw new Horde_Vfs_Exception(sprintf('"%s" is not a directory.', $path . '/' . $name));
245:         }
246: 
247:         $file_list = $this->listFolder($this->_getPath($path, $name));
248: 
249:         if ($file_list && !$recursive) {
250:             throw new Horde_Vfs_Exception(sprintf('Unable to delete "%s", the directory is not empty.', $this->_getPath($path, $name)));
251:         }
252: 
253:         foreach ($file_list as $file) {
254:             if ($file['type'] == '**dir') {
255:                 $this->deleteFolder($this->_getPath($path, $name), $file['name'], $recursive);
256:             } else {
257:                 $this->deleteFile($this->_getPath($path, $name), $file['name']);
258:             }
259:         }
260: 
261:         // Really delete the folder.
262:         list($path, $name) = $this->_escapeShellCommand($path, $name);
263:         $cmd = array('rmdir \"' . $name . '\"');
264: 
265:         try {
266:             $this->_command($path, $cmd);
267:         } catch (Horde_Vfs_Exception $e) {
268:             throw new Horde_Vfs_Exception(sprintf('Unable to delete VFS folder "%s".', $this->_getPath($path, $name)));
269:         }
270:     }
271: 
272:     /**
273:      * Renames a file in the VFS.
274:      *
275:      * @param string $oldpath  The old path to the file.
276:      * @param string $oldname  The old filename.
277:      * @param string $newpath  The new path of the file.
278:      * @param string $newname  The new filename.
279:      *
280:      * @throws Horde_Vfs_Exception
281:      */
282:     public function rename($oldpath, $oldname, $newpath, $newname)
283:     {
284:         $this->autocreatePath($newpath);
285: 
286:         // Double quotes not allowed in SMB filename. The '/' character should
287:         // also be removed from the beginning/end of the names.
288:         $oldname = str_replace('"', "'", trim($oldname, '/'));
289:         $newname = str_replace('"', "'", trim($newname, '/'));
290: 
291:         if (empty($oldname)) {
292:             throw new Horde_Vfs_Exception('Unable to rename VFS file to same name.');
293:         }
294: 
295:         /* If the path was not empty (i.e. the path is not the root path),
296:          * then add the trailing '/' character to path. */
297:         if (!empty($oldpath)) {
298:             $oldpath .= '/';
299:         }
300:         if (!empty($newpath)) {
301:             $newpath .= '/';
302:         }
303: 
304:         list($file, $name) = $this->_escapeShellCommand($oldname, $newname);
305:         $cmd = array('rename \"' .  str_replace('/', '\\\\', $oldpath) . $file . '\" \"' .
306:                                     str_replace('/', '\\\\', $newpath) . $name . '\"');
307: 
308:         try {
309:             $this->_command('', $cmd);
310:         } catch (Horde_Vfs_Exception $e) {
311:             throw new Horde_Vfs_Exception(sprintf('Unable to rename VFS file "%s".', $this->_getPath($oldpath, $oldname)));
312:         }
313:     }
314: 
315:     /**
316:      * Creates a folder on the VFS.
317:      *
318:      * @param string $path  The path of directory to create folder.
319:      * @param string $name  The name of the new folder.
320:      *
321:      * @throws Horde_Vfs_Exception
322:      */
323:     public function createFolder($path, $name)
324:     {
325:         // In some samba versions after samba-3.0.25-pre2, $path must
326:         // end in a trailing slash.
327:         if (substr($path, -1) != '/') {
328:             $path .= '/';
329:         }
330: 
331:         // Double quotes not allowed in SMB filename.
332:         $name = str_replace('"', "'", $name);
333: 
334:         list($dir, $mkdir) = $this->_escapeShellCommand($path, $name);
335:         $cmd = array('mkdir \"' . $mkdir . '\"');
336: 
337:         try {
338:             $this->_command($dir, $cmd);
339:         } catch (Horde_Vfs_Exception $e) {
340:             throw new Horde_Vfs_Exception(sprintf('Unable to create VFS folder "%s".', $this->_getPath($path, $name)));
341:         }
342:     }
343: 
344:     /**
345:      * Returns an unsorted file list.
346:      *
347:      * @param string $path       The path of the directory to get the file list
348:      *                           for.
349:      * @param mixed $filter      Hash of items to filter based on filename.
350:      * @param boolean $dotfiles  Show dotfiles? This is irrelevant with
351:      *                           smbclient.
352:      * @param boolean $dironly   Show directories only?
353:      * @param boolean $recursive  Return all directory levels recursively?
354:      *
355:      * @return array  File list.
356:      * @throws Horde_Vfs_Exception
357:      */
358:     public function listFolder($path = '', $filter = null, $dotfiles = true,
359:                                $dironly = false, $recursive = false)
360:     {
361:         list($path) = $this->_escapeShellCommand($path);
362:         return $this->parseListing($this->_command($path, array('ls')), $filter, $dotfiles, $dironly);
363:     }
364: 
365:     /**
366:      */
367:     public function parseListing($res, $filter, $dotfiles, $dironly)
368:     {
369:         $num_lines = count($res);
370:         $files = array();
371:         for ($r = 0; $r < $num_lines; $r++) {
372:             // Match file listing.
373:             if (!preg_match('/^  (.+?) +([A-Z]*) +(\d+)  (\w\w\w \w\w\w [ \d]\d \d\d:\d\d:\d\d \d\d\d\d)$/', $res[$r], $match)) {
374:                 continue;
375:             }
376: 
377:             // If the file name isn't . or ..
378:             if ($match[1] == '.' || $match[1] == '..') {
379:                 continue;
380:             }
381: 
382:             $my_name = $match[1];
383: 
384:             // Filter out dotfiles if they aren't wanted.
385:             if (!$dotfiles && substr($my_name, 0, 1) == '.') {
386:                 continue;
387:             }
388: 
389:             $my_size = $match[3];
390:             $ext_name = explode('.', $my_name);
391: 
392:             if ((strpos($match[2], 'D') !== false)) {
393:                 $my_type = '**dir';
394:                 $my_size = -1;
395:             } else {
396:                 $my_type = Horde_String::lower($ext_name[count($ext_name) - 1]);
397:             }
398:             $my_date = strtotime($match[4]);
399:             $filedata = array('owner' => '',
400:                               'group' => '',
401:                               'perms' => '',
402:                               'name' => $my_name,
403:                               'type' => $my_type,
404:                               'date' => $my_date,
405:                               'size' => $my_size);
406:             // watch for filters and dironly
407:             if ($this->_filterMatch($filter, $my_name)) {
408:                 unset($file);
409:                 continue;
410:             }
411:             if ($dironly && $my_type !== '**dir') {
412:                 unset($file);
413:                 continue;
414:             }
415: 
416:             $files[$filedata['name']] = $filedata;
417:         }
418: 
419:         return $files;
420:     }
421: 
422:     /**
423:      * Returns a sorted list of folders in specified directory.
424:      *
425:      * @param string $path         The path of the directory to get the
426:      *                             directory list for.
427:      * @param mixed $filter        Hash of items to filter based on folderlist.
428:      * @param boolean $dotfolders  Include dotfolders? Irrelevant for SMB.
429:      *
430:      * @return array  Folder list.
431:      * @throws Horde_Vfs_Exception
432:      */
433:     public function listFolders($path = '', $filter = null, $dotfolders = true)
434:     {
435:         // dirname will strip last component from path, even on a directory
436:         $folder = array(
437:             'val' => dirname($path),
438:             'abbrev' => '..',
439:             'label' => '..'
440:         );
441:         $folders = array($folder['val'] => $folder);
442: 
443:         $folderList = $this->listFolder($path, null, $dotfolders, true);
444:         foreach ($folderList as $files) {
445:             $folders[$folder['val']] = array(
446:                 'val' => $this->_getPath($path, $files['name']),
447:                 'abbrev' => $files['name'],
448:                 'label' => $folder['val']
449:             );
450:         }
451: 
452:         ksort($folders);
453:         return $folders;
454:     }
455: 
456:     /**
457:      * Copies a file through the backend.
458:      *
459:      * @param string $path         The path to store the file in.
460:      * @param string $name         The filename to use.
461:      * @param string $dest         The destination of the file.
462:      * @param boolean $autocreate  Automatically create directories?
463:      *
464:      * @throws Horde_Vfs_Exception
465:      */
466:     public function copy($path, $name, $dest, $autocreate = false)
467:     {
468:         $orig = $this->_getPath($path, $name);
469:         if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
470:             throw new Horde_Vfs_Exception('Cannot copy file(s) - source and destination are the same.');
471:         }
472: 
473:         if ($autocreate) {
474:             $this->autocreatePath($dest);
475:         }
476: 
477:         foreach ($this->listFolder($dest, null, true) as $file) {
478:             if ($file['name'] == $name) {
479:                 throw new Horde_Vfs_Exception(sprintf('%s already exists.', $this->_getPath($dest, $name)));
480:             }
481:         }
482: 
483:         if ($this->isFolder($path, $name)) {
484:             $this->_copyRecursive($path, $name, $dest);
485:         } else {
486:             try {
487:                 $this->write($dest, $name, $this->readFile($path, $name));
488:             } catch (Horde_Vfs_Exception $e) {
489:                 throw new Horde_Vfs_Exception(sprintf('Copy failed: %s', $this->_getPath($dest, $name)));
490:             }
491:         }
492:     }
493: 
494:     /**
495:      * Moves a file through the backend.
496:      *
497:      * @param string $path         The path to store the file in.
498:      * @param string $name         The filename to use.
499:      * @param string $dest         The destination of the file.
500:      * @param boolean $autocreate  Automatically create directories?
501:      *
502:      * @throws Horde_Vfs_Exception
503:      */
504:     public function move($path, $name, $dest, $autocreate = false)
505:     {
506:         $orig = $this->_getPath($path, $name);
507:         if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
508:             throw new Horde_Vfs_Exception('Cannot copy file(s) - destination is within source.');
509:         }
510: 
511:         if ($autocreate) {
512:             $this->autocreatePath($dest);
513:         }
514: 
515:         foreach ($this->listFolder($dest, null, true) as $file) {
516:             if ($file['name'] == $name) {
517:                 throw new Horde_Vfs_Exception(sprintf('%s already exists.', $this->_getPath($dest, $name)));
518:             }
519:         }
520: 
521:         try {
522:             $this->rename($path, $name, $dest, $name);
523:         } catch (Horde_Vfs_Exception $e) {
524:             throw new Horde_Vfs_Exception(sprintf('Failed to move to "%s".', $this->_getPath($dest, $name)));
525:         }
526:     }
527: 
528:     /**
529:      * Replacement for escapeshellcmd(), variable length args, as we only want
530:      * certain characters escaped.
531:      *
532:      * @param array $array  Strings to escape.
533:      *
534:      * @return array  TODO
535:      */
536:     protected function _escapeShellCommand()
537:     {
538:         $ret = array();
539:         $args = func_get_args();
540:         foreach ($args as $arg) {
541:             $ret[] = str_replace(array(';', '\\'), array('\;', '\\\\'), $arg);
542:         }
543:         return $ret;
544:     }
545: 
546:     /**
547:      * Executes a command and returns output lines in array.
548:      *
549:      * @param string $cmd  Command to be executed.
550:      *
551:      * @return array  Array on success.
552:      * @throws Horde_Vfs_Exception
553:      */
554:     protected function _execute($cmd)
555:     {
556:         $cmd = str_replace('"-U%"', '-N', $cmd);
557:         $proc = proc_open(
558:             $cmd,
559:             array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')),
560:             $pipes);
561:         if (!is_resource($proc)) {
562:             // This should never happen.
563:             throw new Horde_Vfs_Exception('Failed to call proc_open().');
564:         }
565:         $out   = explode("\n", stream_get_contents($pipes[1]));
566:         $error = explode("\n", stream_get_contents($pipes[2]));
567:         $ret = proc_close($proc);
568: 
569:         // In some cases, (like trying to delete a nonexistant file),
570:         // smbclient will return success (at least on 2.2.7 version I'm
571:         // testing on). So try to match error strings, even after success.
572:         if ($ret != 0) {
573:             $err = '';
574:             foreach ($error as $line) {
575:                 if (strpos($line, 'Usage:') === 0) {
576:                     $err = 'Command syntax incorrect';
577:                     break;
578:                 }
579:                 if (strpos($line, 'ERRSRV') !== false ||
580:                     strpos($line, 'ERRDOS') !== false) {
581:                     $err = preg_replace('/.*\((.+)\).*/', '\\1', $line);
582:                     if (!$err) {
583:                         $err = $line;
584:                     }
585:                     break;
586:                 }
587:             }
588:             if (!$err) {
589:                 $err = $out ? $out[count($out) - 1] : $ret;
590:             }
591: 
592:             throw new Horde_Vfs_Exception($err);
593:         }
594: 
595:         // Check for errors even on success.
596:         $err = '';
597:         foreach ($out as $line) {
598:             if (strpos($line, 'NT_STATUS_NO_SUCH_FILE') !== false ||
599:                 strpos($line, 'NT_STATUS_OBJECT_NAME_NOT_FOUND') !== false) {
600:                 $err = Horde_Vfs_Translation::t("No such file");
601:                 break;
602:             } elseif (strpos($line, 'NT_STATUS_ACCESS_DENIED') !== false) {
603:                 $err = Horde_Vfs_Translation::t("Permission Denied");
604:                 break;
605:             }
606:         }
607: 
608:         if ($err) {
609:             throw new Horde_Vfs_Exception($err);
610:         }
611: 
612:         return $out;
613:     }
614: 
615:     /**
616:      * Executes SMB commands - without authentication - and returns output
617:      * lines in array.
618:      *
619:      * @param array $path  Base path for command.
620:      * @param array $cmd   Commands to be executed.
621:      *
622:      * @return array  Array on success.
623:      * @throws Horde_Vfs_Exception
624:      */
625:     protected function _command($path, $cmd)
626:     {
627:         list($share) = $this->_escapeShellCommand($this->_params['share']);
628:         putenv('PASSWD=' . $this->_params['password']);
629:         $ipoption = (isset($this->_params['ipaddress'])) ? (' -I ' . $this->_params['ipaddress']) : null;
630:         $fullcmd = $this->_params['smbclient'] .
631:             ' "//' . $this->_params['hostspec'] . '/' . $share . '"' .
632:             ' "-p' . $this->_params['port'] . '"' .
633:             ' "-U' . $this->_params['username'] . '"' .
634:             ' -D "' . $path . '" ' .
635:             $ipoption .
636:             ' -c "';
637:         foreach ($cmd as $c) {
638:             $fullcmd .= $c . ";";
639:         }
640:         $fullcmd .= '"';
641: 
642:         return $this->_execute($fullcmd);
643:     }
644: 
645: }
646: 
API documentation generated by ApiGen