1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22:
23: class Horde_Vfs_Ssh2 extends Horde_Vfs_Base
24: {
25: 26: 27: 28: 29:
30: protected $_credentials = array('username', 'password');
31:
32: 33: 34: 35: 36:
37: protected $_permissions = array(
38: 'owner' => array(
39: 'read' => true,
40: 'write' => true,
41: 'execute' => true
42: ),
43: 'group' => array(
44: 'read' => true,
45: 'write' => true,
46: 'execute' => true
47: ),
48: 'all' => array(
49: 'read' => true,
50: 'write' => true,
51: 'execute' => true
52: )
53: );
54:
55: 56: 57: 58: 59:
60: protected $_stream = false;
61:
62: 63: 64: 65: 66:
67: protected $_sftp;
68:
69: 70: 71: 72: 73:
74: protected $_cwd;
75:
76: 77: 78: 79: 80:
81: protected $_uids = array();
82:
83: 84: 85: 86: 87:
88: protected $_gids = array();
89:
90: 91: 92: 93: 94: 95: 96: 97: 98:
99: public function size($path, $name)
100: {
101: $this->_connect();
102:
103: $statinfo = @ssh2_sftp_stat($this->_sftp, $this->_getPath($path, $name));
104: if (($size = $statinfo['size']) === false) {
105: throw new Horde_Vfs_Exception(sprintf('Unable to check file size of "%s".', $this->_getPath($path, $name)));
106: }
107:
108: return $size;
109: }
110:
111: 112: 113: 114: 115: 116: 117: 118: 119:
120: public function read($path, $name)
121: {
122: $file = $this->readFile($path, $name);
123: clearstatcache();
124:
125: return (filesize($file) === 0)
126: ? ''
127: : file_get_contents($file);
128: }
129:
130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142:
143: public function readFile($path, $name)
144: {
145: $this->_connect();
146:
147:
148:
149: if (!($localFile = Horde_Util::getTempFile('vfs'))) {
150: throw new Horde_Vfs_Exception('Unable to create temporary file.');
151: }
152:
153: if (!$this->_recv($this->_getPath($path, $name), $localFile)) {
154: throw new Horde_Vfs_Exception(sprintf('Unable to open VFS file "%s".', $this->_getPath($path, $name)));
155: }
156:
157: return $localFile;
158: }
159:
160: 161: 162: 163: 164: 165: 166: 167: 168:
169: public function readStream($path, $name)
170: {
171: return fopen($this->readFile($path, $name), OS_WINDOWS ? 'rb' : 'r');
172: }
173:
174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184:
185: public function write($path, $name, $tmpFile, $autocreate = false)
186: {
187: $this->_connect();
188: $this->_checkQuotaWrite('file', $tmpFile);
189:
190: if (!$this->_send($tmpFile, $this->_getPath($path, $name))) {
191: if ($autocreate) {
192: $this->autocreatePath($path);
193: if ($this->_send($tmpFile, $this->_getPath($path, $name))) {
194: return;
195: }
196: }
197:
198: throw new Horde_Vfs_Exception(sprintf('Unable to write VFS file "%s".', $this->_getPath($path, $name)));
199: }
200: }
201:
202: 203: 204: 205: 206: 207: 208: 209: 210: 211:
212: public function writeData($path, $name, $data, $autocreate = false)
213: {
214: $tmpFile = Horde_Util::getTempFile('vfs');
215: file_put_contents($tmpFile, $data);
216: clearstatcache();
217: $this->write($path, $name, $tmpFile, $autocreate);
218: }
219:
220: 221: 222: 223: 224: 225: 226: 227:
228: public function deleteFile($path, $name)
229: {
230: $this->_checkQuotaDelete($path, $name);
231: $this->_connect();
232:
233: if (!@ssh2_sftp_unlink($this->_sftp, $this->_getPath($path, $name))) {
234: throw new Horde_Vfs_Exception(sprintf('Unable to delete VFS file "%s".', $this->_getPath($path, $name)));
235: }
236: }
237:
238: 239: 240: 241: 242: 243: 244: 245:
246: public function isFolder($path, $name)
247: {
248: try {
249: $this->_connect();
250: } catch (Horde_Vfs_Exception $e) {
251: return false;
252: }
253:
254: 255:
256: $statinfo = @ssh2_sftp_stat($this->_sftp, $this->_getPath($path, $name));
257: return $statinfo['mode'] & 040000;
258: }
259:
260: 261: 262: 263: 264: 265: 266: 267: 268:
269: public function deleteFolder($path, $name, $recursive = false)
270: {
271: $this->_connect();
272:
273: $isDir = false;
274: foreach ($this->listFolder($path) as $file) {
275: if ($file['name'] == $name && $file['type'] == '**dir') {
276: $isDir = true;
277: break;
278: }
279: }
280:
281: if ($isDir) {
282: $file_list = $this->listFolder($path . '/' . $name);
283: if (count($file_list) && !$recursive) {
284: throw new Horde_Vfs_Exception(sprintf('Unable to delete "%s", the directory is not empty.', $this->_getPath($path, $name)));
285: }
286: foreach ($file_list as $file) {
287: if ($file['type'] == '**dir') {
288: $this->deleteFolder($path . '/' . $name, $file['name'], $recursive);
289: } else {
290: $this->deleteFile($path . '/' . $name, $file['name']);
291: }
292: }
293:
294: if (!@ssh2_sftp_rmdir($this->_sftp, $this->_getPath($path, $name))) {
295: throw new Horde_Vfs_Exception(sprintf('Cannot remove directory "%s".', $this->_getPath($path, $name)));
296: }
297: } else {
298: if (!@ssh2_sftp_unlink($this->_sftp, $this->_getPath($path, $name))) {
299: throw new Horde_Vfs_Exception(sprintf('Cannot delete file "%s".', $this->_getPath($path, $name)));
300: }
301: }
302: }
303:
304: 305: 306: 307: 308: 309: 310: 311: 312: 313:
314: public function rename($oldpath, $oldname, $newpath, $newname)
315: {
316: $this->_connect();
317: $this->autocreatePath($newpath);
318:
319: if (!@ssh2_sftp_rename($this->_sftp, $this->_getPath($oldpath, $oldname), $this->_getPath($newpath, $newname))) {
320: throw new Horde_Vfs_Exception(sprintf('Unable to rename VFS file "%s".', $this->_getPath($oldpath, $oldname)));
321: }
322: }
323:
324: 325: 326: 327: 328: 329: 330: 331:
332: public function createFolder($path, $name)
333: {
334: $this->_connect();
335:
336: if (!@ssh2_sftp_mkdir($this->_sftp, $this->_getPath($path, $name))) {
337: throw new Horde_Vfs_Exception(sprintf('Unable to create VFS directory "%s".', $this->_getPath($path, $name)));
338: }
339: }
340:
341: 342: 343: 344: 345: 346: 347: 348: 349:
350: public function changePermissions($path, $name, $permission)
351: {
352: $this->_connect();
353:
354: if (!@ssh2_exec($this->_stream, 'chmod ' . escapeshellarg($permission) . ' ' . escapeshellarg($this->_getPath($path, $name)))) {
355: throw new Horde_Vfs_Exception(sprintf('Unable to change permission for VFS file "%s".', $this->_getPath($path, $name)));
356: }
357: }
358:
359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369:
370: protected function _listFolder($path = '', $filter = null,
371: $dotfiles = true, $dironly = false)
372: {
373: $this->_connect();
374:
375: $files = array();
376:
377:
378: $mapids = (!empty($this->_params['maplocalids']) && extension_loaded('posix'));
379:
380:
381:
382: $type = 'unix';
383:
384: $olddir = $this->getCurrentDirectory();
385: $path = $this->_getPath('', $path);
386: if (strlen($path)) {
387: $this->_setPath($path);
388: }
389:
390: if ($type == 'unix') {
391: $ls_args = 'l';
392:
393:
394:
395: if ($mapids) {
396: $ls_args .= 'n';
397: }
398:
399:
400:
401:
402: if ($dotfiles) {
403: $ls_args .= 'a';
404: $dotfiles = true;
405: }
406:
407: $stream = @ssh2_exec(
408: $this->_stream,
409: 'ls -' . $ls_args . ' ' . escapeshellarg($path),
410: null,
411: array('LC_TIME' => 'C'));
412: } else {
413: $stream = @ssh2_exec($this->_stream, '');
414: }
415: stream_set_blocking($stream, true);
416:
417: $errstream = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR);
418: if ($error = stream_get_contents($errstream)) {
419: fclose($errstream);
420: fclose($stream);
421: throw new Horde_Vfs_Exception($error);
422: }
423:
424: unset($list);
425: while (!feof($stream)) {
426: $line = fgets($stream);
427: if ($line === false) {
428: break;
429: }
430: $list[] = trim($line);
431: }
432: fclose($errstream);
433: fclose($stream);
434:
435: if (!is_array($list)) {
436: if (isset($olddir)) {
437: $this->_setPath($olddir);
438: }
439: return array();
440: }
441:
442: $currtime = time();
443:
444: foreach ($list as $line) {
445: $file = array();
446: $item = preg_split('/\s+/', $line);
447: if (($type == 'unix') ||
448: (($type == 'win') &&
449: !preg_match('|\d\d-\d\d-\d\d|', $item[0]))) {
450: if ((count($item) < 8) || (substr($line, 0, 5) == 'total')) {
451: continue;
452: }
453: $file['perms'] = $item[0];
454: if ($mapids) {
455: if (!isset($this->_uids[$item[2]])) {
456: $entry = posix_getpwuid($item[2]);
457: $this->_uids[$item[2]] = (empty($entry)) ? $item[2] : $entry['name'];
458: }
459: $file['owner'] = $this->_uids[$item[2]];
460: if (!isset($this->_uids[$item[3]])) {
461: $entry = posix_getgrgid($item[3]);
462: $this->_uids[$item[3]] = (empty($entry)) ? $item[3] : $entry['name'];
463: }
464: $file['group'] = $this->_uids[$item[3]];
465:
466: } else {
467: $file['owner'] = $item[2];
468: $file['group'] = $item[3];
469: }
470:
471:
472: $addcol = 0;
473: if (substr($item[4], -1) == ',') {
474: $addcol = 1;
475: }
476: $file['name'] = substr($line, strpos($line, sprintf("%s %2s %5s", $item[5 + $addcol], $item[6 + $addcol], $item[7 + $addcol])) + 13);
477:
478:
479: if (preg_match('/^\.\.?\/?$/', $file['name'])) {
480: continue;
481: }
482:
483:
484: if (!$dotfiles && (substr($file['name'], 0, 1) == '.')) {
485: continue;
486: }
487:
488: $p1 = substr($file['perms'], 0, 1);
489: if ($p1 === 'l') {
490: $file['link'] = substr($file['name'], strpos($file['name'], '->') + 3);
491: $file['name'] = substr($file['name'], 0, strpos($file['name'], '->') - 1);
492: $file['type'] = '**sym';
493:
494: if ($this->isFolder('', $file['link'])) {
495: $file['linktype'] = '**dir';
496: } else {
497: $parts = explode('/', $file['link']);
498: $name = explode('.', array_pop($parts));
499: if ((count($name) == 1) ||
500: (($name[0] === '') && (count($name) == 2))) {
501: $file['linktype'] = '**none';
502: } else {
503: $file['linktype'] = Horde_String::lower(array_pop($name));
504: }
505: }
506: } elseif ($p1 === 'd') {
507: $file['type'] = '**dir';
508: } else {
509: $name = explode('.', $file['name']);
510: if ((count($name) == 1) ||
511: ((substr($file['name'], 0, 1) === '.') &&
512: (count($name) == 2))) {
513: $file['type'] = '**none';
514: } else {
515: $file['type'] = Horde_String::lower($name[count($name) - 1]);
516: }
517: }
518: if ($file['type'] == '**dir') {
519: $file['size'] = -1;
520: } else {
521: $file['size'] = $item[4 + $addcol];
522: }
523: if (strpos($item[7 + $addcol], ':') !== false) {
524: $file['date'] = strtotime($item[7 + $addcol] . ':00' . $item[5 + $addcol] . ' ' . $item[6 + $addcol] . ' ' . date('Y', $currtime));
525:
526:
527:
528:
529:
530:
531: if ($file['date'] > ($currtime + 86400)) {
532: $file['date'] = strtotime($item[7 + $addcol] . ':00' . $item[5 + $addcol] . ' ' . $item[6 + $addcol] . ' ' . (date('Y', $currtime) - 1));
533: }
534: } else {
535: $file['date'] = strtotime('00:00:00' . $item[5 + $addcol] . ' ' . $item[6 + $addcol] . ' ' . $item[7 + $addcol]);
536: }
537: } elseif ($type == 'netware') {
538: $file = array();
539: $file['perms'] = $item[1];
540: $file['owner'] = $item[2];
541: if ($item[0] == 'd') {
542: $file['type'] = '**dir';
543: } else {
544: $file['type'] = '**none';
545: }
546: $file['size'] = $item[3];
547: $file['name'] = $item[7];
548: for ($index = 8, $c = count($item); $index < $c; $index++) {
549: $file['name'] .= ' ' . $item[$index];
550: }
551: } else {
552: 553:
554: $file['perms'] = '';
555: $file['owner'] = '';
556: $file['group'] = '';
557: $file['name'] = $item[3];
558: for ($index = 4, $c = count($item); $index < $c; $index++) {
559: $file['name'] .= ' ' . $item[$index];
560: }
561: $file['date'] = strtotime($item[0] . ' ' . $item[1]);
562: if ($item[2] == '<DIR>') {
563: $file['type'] = '**dir';
564: $file['size'] = -1;
565: } else {
566: $file['size'] = $item[2];
567: $name = explode('.', $file['name']);
568: if ((count($name) == 1) ||
569: ((substr($file['name'], 0, 1) === '.') &&
570: (count($name) == 2))) {
571: $file['type'] = '**none';
572: } else {
573: $file['type'] = Horde_String::lower($name[count($name) - 1]);
574: }
575: }
576: }
577:
578:
579: if ($this->_filterMatch($filter, $file['name'])) {
580: unset($file);
581: continue;
582: }
583: if ($dironly && $file['type'] !== '**dir') {
584: unset($file);
585: continue;
586: }
587:
588: $files[$file['name']] = $file;
589: unset($file);
590: }
591:
592: if (isset($olddir)) {
593: $res = $this->_setPath($olddir);
594: if (is_a($res, 'PEAR_Error')) {
595: return $res;
596: }
597: }
598:
599: return $files;
600: }
601:
602: 603: 604: 605: 606: 607: 608: 609:
610: public function exists($path, $name)
611: {
612: $conn = $this->_connect();
613: if (is_a($conn, 'PEAR_Error')) {
614: return $conn;
615: }
616:
617: return @ssh2_sftp_stat($this->_sftp, $this->_getPath($path, $name)) !== false;
618: }
619:
620: 621: 622: 623: 624: 625: 626: 627: 628: 629: 630:
631: public function listFolders($path = '', $filter = null, $dotfolders = true)
632: {
633: $this->_connect();
634:
635: $folder = array(
636: 'abbrev' => '..',
637: 'val' => $this->_parentDir($path),
638: 'label' => '..'
639: );
640: $folders[$folder['val']] = $folder;
641:
642: $folderList = $this->listFolder($path, null, $dotfolders, true);
643: foreach ($folderList as $files) {
644: $folders[$folder['val']] = array(
645: 'val' => $this->_getPath($path, $files['name']),
646: 'abbrev' => $files['name'],
647: 'label' => $folder['val']
648: );
649: }
650:
651: ksort($folders);
652: return $folders;
653: }
654:
655: 656: 657: 658: 659: 660: 661: 662: 663: 664: 665:
666: public function copy($path, $name, $dest, $autocreate = false)
667: {
668: $orig = $this->_getPath($path, $name);
669: if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
670: throw new Horde_Vfs_Exception('Cannot copy file(s) - source and destination are the same.');
671: }
672:
673: $this->_connect();
674:
675: if ($autocreate) {
676: $this->autocreatePath($dest);
677: }
678:
679: foreach ($this->listFolder($dest, null, true) as $file) {
680: if ($file['name'] == $name) {
681: throw new Horde_Vfs_Exception(sprintf('%s already exists.', $this->_getPath($dest, $name)));
682: }
683: }
684:
685: if ($this->isFolder($path, $name)) {
686: $this->_copyRecursive($path, $name, $dest);
687: } else {
688: $tmpFile = Horde_Util::getTempFile('vfs');
689: if (!$this->_recv($orig, $tmpFile)) {
690: throw new Horde_Vfs_Exception(sprintf('Failed to copy from "%s".', $orig));
691: }
692:
693: clearstatcache();
694: $this->_checkQuotaWrite('file', $tmpFile);
695:
696: if (!$this->_send($tmpFile, $this->_getPath($dest, $name))) {
697: throw new Horde_Vfs_Exception(sprintf('Failed to copy to "%s".', $this->_getPath($dest, $name)));
698: }
699: }
700: }
701:
702: 703: 704: 705: 706: 707: 708: 709: 710: 711: 712:
713: public function move($path, $name, $dest, $autocreate = false)
714: {
715: $orig = $this->_getPath($path, $name);
716: if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
717: throw new Horde_Vfs_Exception('Cannot move file(s) - destination is within source.');
718: }
719:
720: $this->_connect();
721:
722: if ($autocreate) {
723: $this->autocreatePath($dest);
724: }
725:
726: foreach ($this->listFolder($dest, null, true) as $file) {
727: if ($file['name'] == $name) {
728: throw new Horde_Vfs_Exception(sprintf('%s already exists.', $this->_getPath($dest, $name)));
729: }
730: }
731:
732: if (!@ssh2_sftp_rename($this->_sftp, $orig, $this->_getPath($dest, $name))) {
733: throw new Horde_Vfs_Exception(sprintf('Failed to move to "%s".', $this->_getPath($dest, $name)));
734: }
735: }
736:
737: 738: 739: 740: 741: 742:
743: public function getCurrentDirectory()
744: {
745: $this->_connect();
746:
747: if (!strlen($this->_cwd)) {
748: $stream = @ssh2_exec($this->_stream, 'pwd');
749: stream_set_blocking($stream, true);
750: $this->_cwd = trim(fgets($stream));
751: }
752:
753: return $this->_cwd;
754: }
755:
756: 757: 758: 759: 760: 761: 762:
763: protected function _setPath($path)
764: {
765: if (!($stream = @ssh2_exec($this->_stream, 'cd ' . escapeshellarg($path) . '; pwd'))) {
766: throw new Horde_Vfs_Exception(sprintf('Unable to change to %s.', $path));
767: }
768:
769: stream_set_blocking($stream, true);
770: $this->_cwd = trim(fgets($stream));
771: fclose($stream);
772: }
773:
774: 775: 776: 777: 778: 779: 780: 781: 782:
783: protected function _getPath($path, $name)
784: {
785: if (strlen($this->_params['vfsroot'])) {
786: if (strlen($path)) {
787: $path = $this->_params['vfsroot'] . '/' . $path;
788: } else {
789: $path = $this->_params['vfsroot'];
790: }
791: }
792: return parent::_getPath($path, $name);
793: }
794:
795: 796: 797: 798: 799: 800: 801: 802:
803: protected function _parentDir($path)
804: {
805: $this->_connect();
806: $this->_setPath('cd ' . $path . '/..');
807:
808: return $this->getCurrentDirectory();
809: }
810:
811: 812: 813: 814: 815:
816: protected function _connect()
817: {
818: if ($this->_stream !== false) {
819: return;
820: }
821:
822: if (!extension_loaded('ssh2')) {
823: throw new Horde_Vfs_Exception('The SSH2 PECL extension is not available.');
824: }
825:
826: if (!is_array($this->_params)) {
827: throw new Horde_Vfs_Exception('No configuration information specified for SSH2 VFS.');
828: }
829:
830: $required = array('hostspec', 'username', 'password');
831: foreach ($required as $val) {
832: if (!isset($this->_params[$val])) {
833: throw new Horde_Vfs_Exception(sprintf('Required "%s" not specified in VFS configuration.', $val));
834: }
835: }
836:
837:
838: if (empty($this->_params['port'])) {
839: $this->_stream = @ssh2_connect($this->_params['hostspec']);
840: } else {
841: $this->_stream = @ssh2_connect($this->_params['hostspec'], $this->_params['port']);
842: }
843:
844: if (!$this->_stream) {
845: $this->_stream = false;
846: throw new Horde_Vfs_Exception('Connection to SSH2 server failed.');
847: }
848:
849: if (!@ssh2_auth_password($this->_stream, $this->_params['username'], $this->_params['password'])) {
850: $this->_stream = false;
851: throw new Horde_Vfs_Exception('Authentication to SSH2 server failed.');
852: }
853:
854:
855: $this->_sftp = @ssh2_sftp($this->_stream);
856:
857: if (!empty($this->_params['vfsroot']) &&
858: !@ssh2_sftp_stat($this->_sftp, $this->_params['vfsroot']) &&
859: !@ssh2_sftp_mkdir($this->_sftp, $this->_params['vfsroot'])) {
860: throw new Horde_Vfs_Exception(sprintf('Unable to create VFS root directory "%s".', $this->_params['vfsroot']));
861: }
862: }
863:
864: 865: 866: 867: 868: 869: 870: 871: 872: 873: 874:
875: protected function _send($local, $remote)
876: {
877: return @copy($local, $this->_wrap($remote));
878: }
879:
880: 881: 882: 883: 884: 885: 886: 887: 888: 889: 890:
891: protected function _recv($remote, $local)
892: {
893: return @copy($this->_wrap($remote), $local);
894: }
895:
896: 897: 898: 899: 900: 901: 902:
903: protected function _wrap($remote)
904: {
905: $wrapper = 'ssh2.sftp://' . $this->_params['username'] . ':'
906: . $this->_params['password'] . '@' . $this->_params['hostspec'];
907: if (!empty($this->_params['port'])) {
908: $wrapper .= ':' . $this->_params['port'];
909: }
910: return $wrapper . $remote;
911: }
912: }
913: