AloFramework documentation
  • Namespace
  • Class
  • Tree
  • Deprecated
  • Todo
  • Download

Namespaces

  • Alo
    • Cache
    • CLI
    • Controller
    • Db
    • Exception
    • FileSystem
    • Session
    • Statics
    • Test
    • Validators
    • Windows
  • Controller
  • None
  • PHP

Classes

  • Cron
  • cURL
  • Email
  • File
  • Handler
  • Profiler
  • SFTP
  1 <?php
  2 
  3    namespace Alo;
  4 
  5    use Alo\Exception\ExtensionException as EE;
  6    use Alo\Exception\FileSystemException as FE;
  7    use Alo\Exception\SFTPException as SE;
  8 
  9    if(!defined('GEN_START')) {
 10       http_response_code(404);
 11       die();
 12    }
 13 
 14    /**
 15     * SFTP handler
 16     *
 17     * @author Arturas Molcanovas <a.molcanovas@gmail.com>
 18     */
 19    class SFTP {
 20 
 21       /**
 22        * Defines the sort direction as ascending
 23        *
 24        * @var int
 25        */
 26       const SORT_ASC = SCANDIR_SORT_ASCENDING;
 27       /**
 28        * Defines the sort direction as descending
 29        *
 30        * @var int
 31        */
 32       const SORT_DESC = SCANDIR_SORT_DESCENDING;
 33       /**
 34        * Defines a parameter as "retry count"
 35        *
 36        * @var int
 37        */
 38       const P_RETRY_COUNT = 101;
 39       /**
 40        * Defines a parameter as "retry wait time"
 41        *
 42        * @var int
 43        */
 44       const P_RETRY_TIME = 102;
 45       /**
 46        * The endpoint URL
 47        *
 48        * @var string
 49        */
 50       protected $url;
 51       /**
 52        * The SFTP username
 53        *
 54        * @var string
 55        */
 56       protected $user;
 57       /**
 58        * Path to the public authentication key
 59        *
 60        * @var string
 61        */
 62       protected $pubkey;
 63       /**
 64        * Path to the private authentication key
 65        *
 66        * @var string
 67        */
 68       protected $privkey;
 69       /**
 70        * Private authentication key password
 71        *
 72        * @var string
 73        */
 74       protected $pw;
 75       /**
 76        * Directory in use
 77        *
 78        * @var string
 79        */
 80       protected $dir;
 81       /**
 82        * The SSH2 connection
 83        *
 84        * @var resource
 85        */
 86       protected $connection;
 87       /**
 88        * The SFTP subsystem
 89        *
 90        * @var resource
 91        */
 92       protected $sftp;
 93       /**
 94        * The local directory set
 95        *
 96        * @var string
 97        */
 98       protected $local_dir;
 99       /**
100        * Maximum amount of retries for an operation
101        *
102        * @var int
103        */
104       protected $retry_count_max;
105 
106       /**
107        * Time in seconds between retries
108        *
109        * @var int
110        */
111       protected $retry_time;
112 
113       /**
114        * Instantiates the library
115        *
116        * @param array $params Optional parameters - see class P_* constants
117        *
118        * @see self::P_RETRY_COUNT
119        * @see self::P_RETRY_TIME
120        * @throws EE When the SSH2 extension is not loaded
121        */
122       function __construct($params = []) {
123          if(!function_exists('ssh2_connect')) {
124             throw new EE('SSH2 extension not loaded', EE::E_EXT_NOT_LOADED);
125          } else {
126             $this->dir = DIR_INDEX;
127 
128             $this->retry_count_max = (int)\get($params[self::P_RETRY_COUNT]);
129             $this->retry_time      = (int)\get($params[self::P_RETRY_TIME]) ? $params[self::P_RETRY_TIME] : 3;
130 
131             \Log::debug('SSH2 class initialised');
132          }
133       }
134 
135       /**
136        * Instantiates the library
137        *
138        * @param array $params Optional parameters - see class P_* constants
139        *
140        * @see self::P_RETRY_COUNT
141        * @see self::P_RETRY_TIME
142        * @throws EE When the SSH2 extension is not loaded
143        *
144        * @return SFTP
145        */
146       static function SFTP($params = []) {
147          return new SFTP($params);
148       }
149 
150       /**
151        * If no parameter is passed gets the maximum amount of retry attempts for failed operations, otherwise sets it.
152        *
153        * @author Art <a.molcanovas@gmail.com>
154        *
155        * @param int $int The amount
156        *
157        * @return boolean|int
158        */
159       function retryCount($int = -1) {
160          if($int === -1) {
161             return $this->retry_count_max;
162          } elseif(is_numeric($int) && $int >= 0) {
163             $this->retry_count_max = (int)$int;
164             \Log::debug('Retry count set to ' . $int);
165 
166             return true;
167          } else {
168             return false;
169          }
170       }
171 
172       /**
173        * If no parameter is passed gets the time to wait between operation retries, otherwise sets it
174        *
175        * @author Art <a.molcanovas@gmail.com>
176        *
177        * @param int $int The time in seconds
178        *
179        * @return boolean
180        */
181       function retryTime($int = -1) {
182          if($int === -1) {
183             return $this->retry_time;
184          } elseif(is_numeric($int) && $int > 0) {
185             $this->retry_time = (int)$int;
186             \Log::debug('Retry time set to ' . $int);
187 
188             return true;
189          } else {
190             return false;
191          }
192       }
193 
194       /**
195        * Sets the local directory
196        *
197        * @author Art <a.molcanovas@gmail.com>
198        *
199        * @param string $dir The directory path
200        *
201        * @return SFTP
202        */
203       function loc($dir) {
204          $dir = rtrim($dir, ' ' . DIRECTORY_SEPARATOR);
205          if(!$dir) {
206             $dir = DIRECTORY_SEPARATOR;
207          }
208          $this->local_dir = $dir;
209          \Log::debug('Local dir set to ' . $dir);
210 
211          return $this;
212       }
213 
214       /**
215        * Scans a directory for files and subdirectories
216        *
217        * @author Art <a.molcanovas@gmail.com>
218        *
219        * @param int $sorting_order The sorting order
220        *
221        * @return array ["dirs" => [], "files" => []]
222        */
223       function scandir($sorting_order = self::SORT_ASC) {
224          $this->checkSubsystem();
225 
226          $dir = scandir('ssh2.sftp://' . $this->sftp . DIRECTORY_SEPARATOR . $this->dir, $sorting_order);
227          $r   = [
228             'dirs'  => [],
229             'files' => []
230          ];
231 
232          foreach($dir as $v) {
233             //Ignore hidden
234             if($v == '.' || $v == '..' || stripos($v, '.') === 0) {
235                continue;
236             } elseif(self::isFile($v)) {
237                $r['files'][] = $v;
238             } else {
239                $r['dirs'][] = $v;
240             }
241          }
242 
243          return $r;
244       }
245 
246       /**
247        * Checks if the SFTP subsystem was initialised
248        *
249        * @author Art <a.molcanovas@gmail.com>
250        * @throws SE When the connection ultimately fails
251        * @return SFTP
252        */
253       function checkSubsystem() {
254          if(!$this->sftp) {
255             \Log::debug('SFTP subsystem wasn\'t initialised when a dependant'
256                         . ' method was called. Initialising.');
257             $this->connect();
258          }
259 
260          return $this;
261       }
262 
263       /**
264        * Creates a SSH2 connection
265        *
266        * @author Art <a.molcanovas@gmail.com>
267        *
268        * @param int $attempt Current attempt number
269        *
270        * @return SFTP
271        * @throws SE When the connection ultimately fails
272        */
273       function connect($attempt = 0) {
274          if(!($this->connection = @ssh2_connect($this->url))) {
275             $msg = 'Failed to initialise SSH2 connection';
276             $attempt++;
277 
278             if($attempt - 1 < $this->retry_count_max) {
279                \Log::error($msg . '. Retrying again' . ' in '
280                            . $this->retry_time . ' seconds [' . $attempt
281                            . '/' . $this->retry_count_max . ']');
282 
283                sleep($this->retry_time);
284 
285                return $this->connect($attempt);
286             } else {
287                throw new SE($msg . ' after ' . $attempt . ' attempts', SE::E_CONNECT);
288             }
289          } else {
290             \Log::debug('Initialised SSH2 connection');
291 
292             return $this->auth();
293          }
294       }
295 
296       /**
297        * Authenticates the SSH2 connection
298        *
299        * @author Art <a.molcanovas@gmail.com>
300        *
301        * @param int $attempt The current attempt # at authentication
302        *
303        * @return SFTP
304        * @throws SE When authentication permanently fails
305        */
306       protected function auth($attempt = 0) {
307          if(!ssh2_auth_pubkey_file($this->connection, $this->user, $this->pubkey, $this->privkey, $this->pw)) {
308             $msg = 'Failed to authenticate SSH2 connection';
309             ++$attempt;
310 
311             if($attempt - 1 < $this->retry_count_max) {
312                \Log::error($msg . '. Retrying in ' . $this->retry_time
313                            . ' seconds [' . $attempt . '/'
314                            . $this->retry_count_max . ']');
315                sleep($this->retry_time);
316 
317                return $this->auth($attempt);
318             } else {
319                throw new SE($msg . ' after ' . $attempt . ' retries', SE::E_AUTH);
320             }
321          } else {
322             \Log::debug('SSH2 connection authenticated');
323 
324             return $this->ssh2_sftp();
325          }
326       }
327 
328       /**
329        * Initialises the SFTP subsystem
330        *
331        * @author Art <a.molcanovas@gmail.com>
332        *
333        * @param int $attempt Current retry number
334        *
335        * @return SFTP
336        * @throws SE When initialising the SFTP system permanently fails
337        */
338       protected function ssh2_sftp($attempt = 0) {
339          if($this->sftp = @ssh2_sftp($this->connection)) {
340             \Log::debug('Initialised SFTP subsystem');
341 
342             return $this;
343          } else {
344             $msg = 'Failed to initialise SFTP subsystem';
345             ++$attempt;
346 
347             if($attempt - 1 < $this->retry_count_max) {
348                \Log::error($msg . '. Retrying again' . ' in '
349                            . $this->retry_time . ' seconds [' . $attempt
350                            . '/' . $this->retry_count_max . ']');
351                sleep($this->retry_time);
352 
353                return $this->ssh2_sftp($attempt);
354             } else {
355                throw new SE($msg . ' after ' . $attempt . ' attempts', SE::E_SUBSYSTEM);
356             }
357          }
358       }
359 
360       /**
361        * Checks whether a resource is a file or directory based on whether it has a file extension
362        *
363        * @author Art <a.molcanovas@gmail.com>
364        *
365        * @param string $resource The resource name to check
366        *
367        * @return boolean
368        */
369       protected static function isFile($resource) {
370          $pos = stripos($resource, '.');
371 
372          return $pos !== false && $pos !== 0;
373       }
374 
375       /**
376        * Downloads a file to $this->local_dir
377        *
378        * @author Art <a.molcanovas@gmail.com>
379        * @see    self::$local_dir
380        *
381        * @param string $file Remote file name
382        *
383        * @throws SE When the file cannot be fetched
384        * @throws FE When the name is invalid
385        * @return SFTP
386        */
387       function downloadFile($file) {
388          \Log::debug('Downloading file ' . $file . ' to ' . $this->local_dir);
389          $fetch = $this->getFileContents($file);
390 
391          $local = new File();
392          $local->dir($this->local_dir)->name($file);
393          $local->content($fetch)->write();
394 
395          return $this;
396       }
397 
398       /**
399        * Gets the file contents
400        *
401        * @author Art <a.molcanovas@gmail.com>
402        *
403        * @param string $file File name
404        *
405        * @return string String representation of the file
406        * @throws SE When the file cannot be fetched
407        */
408       function getFileContents($file) {
409          $this->checkSubsystem();
410          $remoteFile = $this->resolvePath($file);
411 
412          $file = @file_get_contents('ssh2.sftp://' . $this->sftp . '/' . $remoteFile);
413          if($file === false) {
414             throw new SE('Failed to fetch file ' . $remoteFile, SE::E_FILE_NOT_FETCHED);
415          } else {
416             return $file;
417          }
418       }
419 
420       /**
421        * Modifies the path based on whether it's relative or absolute
422        *
423        * @author Art <a.molcanovas@gmail.com>
424        *
425        * @param string $item Item name
426        *
427        * @return string The resolved path
428        */
429       protected function resolvePath($item) {
430          return (stripos($item, DIRECTORY_SEPARATOR) === 0) ? substr($item, 1) :
431             $this->dir . DIRECTORY_SEPARATOR . $item;
432       }
433 
434       /**
435        * Uploads a file to the SFTP folder from the local folder
436        *
437        * @author Art <a.molcanovas@gmail.com>
438        *
439        * @param string $file File name
440        *
441        * @return boolean
442        * @throws SE When the local file cannot be read
443        */
444       function upload($file) {
445          $this->checkSubsystem();
446          $path = $this->local_dir . DIRECTORY_SEPARATOR . $file;
447 
448          if(!$content = @file_get_contents($path)) {
449             throw new SE('Local file ' . $path . ' cannot be read', SE::E_LOCAL_FILE_NOT_READ);
450          } else {
451             \Log::debug('Uploading remote file ' . $file);
452 
453             return $this->makeFile($file, $content);
454          }
455       }
456 
457       /**
458        * Creates a file in the SFTP directory
459        *
460        * @author Art <a.molcanovas@gmail.com>
461        *
462        * @param string $file    File name
463        * @param mixed  $content File content
464        *
465        * @return boolean
466        * @throws SE When remote fopen fails
467        */
468       function makeFile($file, $content) {
469          $this->checkSubsystem();
470          $remoteFile = $this->resolvePath($file);
471 
472          if(!$fp =
473             @fopen('ssh2.sftp://' . $this->sftp . DIRECTORY_SEPARATOR . $remoteFile, File::M_WRITE_TRUNCATE_BEGIN)
474          ) {
475             throw new SE('Failed to remotely fopen ' . $remoteFile, SE::E_FILE_CREATE_FAIL);
476          } else {
477             flock($fp, LOCK_EX);
478             fwrite($fp, $content);
479             flock($fp, LOCK_UN);
480             fclose($fp);
481             \Log::debug('Wrote remote file ' . $remoteFile);
482 
483             return true;
484          }
485       }
486 
487       /**
488        * Deletes an item on the SFTP server
489        *
490        * @author Art <a.molcanovas@gmail.com>
491        *
492        * @param string $item File or directory name
493        *
494        * @return boolean
495        */
496       function delete($item) {
497          $this->checkSubsystem();
498          $path = $this->resolvePath($item);
499 
500          if(self::isFile($item)) {
501             $success = ssh2_sftp_unlink($this->sftp, $path);
502          } else {
503             $success = ssh2_sftp_rmdir($this->sftp, $path);
504          }
505 
506          if($success) {
507             \Log::debug('Deleted ' . $item);
508 
509             return true;
510          } else {
511             \Log::error('Failed to delete ' . $item);
512 
513             return false;
514          }
515       }
516 
517       /**
518        * Returns a string representation of SFTP credentials
519        *
520        * @author Art <a.molcanovas@gmail.com>
521        * @return string
522        */
523       function __toString() {
524          return
525             'User: ' .
526             $this->user .
527             '; PrivKey:' .
528             $this->privkey .
529             '; PubKey: ' .
530             $this->pubkey .
531             '; Password hash: ' .
532             get($this->pw) ? md5($this->pw) : 'NO HASH CONTENT SET';
533       }
534 
535       /**
536        * If no parameter is passed gets the SFTP server URL, otherwise sets it.
537        *
538        * @author Art <a.molcanovas@gmail.com>
539        *
540        * @param string $url
541        *
542        * @return SFTP
543        * @throws SE When the URL is not a string
544        */
545       function url($url = '') {
546          if($url === '') {
547             return $this->url;
548          } elseif(is_string($url)) {
549             $this->url = $url;
550             \Log::debug('SFTP URL set to ' . $url);
551          } else {
552             throw new SE('Invalid URL', SE::E_URL_INVALID);
553          }
554 
555          return $this;
556       }
557 
558       /**
559        * If no parameter is passed gets the SFTP username, otherwise sets it.
560        *
561        * @author Art <a.molcanovas@gmail.com>
562        *
563        * @param string $user The username
564        *
565        * @throws SE When $user isn't scalar
566        * @return SFTP
567        */
568       function user($user = '') {
569          if($user === '') {
570             return $this->user;
571          } elseif(is_scalar($user)) {
572             $this->user = $user;
573             \Log::debug('SFTP user set to ' . $user);
574          } else {
575             throw new SE('Invalid username', SE::E_USER_INVALID);
576          }
577 
578          return $this;
579       }
580 
581       /**
582        * Sets the SFTP public key path
583        *
584        * @author Art <a.molcanovas@gmail.com>
585        *
586        * @param string $pubkey The path
587        *
588        * @throws SE When $pubkey isn't a string
589        * @return SFTP
590        */
591       function pubkey($pubkey = '') {
592          if($pubkey === '') {
593             return $this->pubkey;
594          } elseif(is_string($pubkey)) {
595             $this->pubkey = $pubkey;
596             \Log::debug('SFTP pubkey set');
597          } else {
598             throw new SE('$pubkey must be a valid path', SE::E_PATH_INVALID);
599          }
600 
601          return $this;
602       }
603 
604       /**
605        * If no parameter is passed gets the SFTP private key path, otherwise sets it
606        *
607        * @author Art <a.molcanovas@gmail.com>
608        *
609        * @param string $privkey The path
610        *
611        * @throws SE When the $privkey isn't a string
612        * @return SFTP
613        */
614       function privkey($privkey = '') {
615          if($privkey === '') {
616             return $this->privkey;
617          } elseif(is_string($privkey)) {
618             $this->privkey = $privkey;
619             \Log::debug('SFTP privkey set');
620          } else {
621             throw new SE('$privkey must be a valid path', SE::E_PATH_INVALID);
622          }
623 
624          return $this;
625       }
626 
627       /**
628        * If no parameter is passed gets the SFTP private key password, otherwise sets it
629        *
630        * @author Art <a.molcanovas@gmail.com>
631        *
632        * @param string $pw The password
633        *
634        * @throws SE When the password isn't scalar
635        * @return SFTP
636        */
637       function pw($pw = '') {
638          if($pw === '') {
639             return $this->pw;
640          } elseif(is_scalar($pw)) {
641             $this->pw = $pw;
642             \Log::debug('SFTP password set');
643          } else {
644             throw new SE('Invalid password provided', SE::E_PW_INVALID);
645          }
646 
647          return $this;
648       }
649 
650       /**
651        * If no argument is passed, gets the working directory, otherwise sets it.
652        *
653        * @author Art <a.molcanovas@gmail.com>
654        *
655        * @param mixed $dir
656        *
657        * @throws SE When the name is invalid
658        * @return SFTP
659        */
660       function dir($dir = -1) {
661          if($dir === -1) {
662             return $this->dir;
663          } elseif(is_scalar($dir)) {
664             $dir = trim($dir, DIRECTORY_SEPARATOR . ' ');
665             if(!$dir || $dir == DIRECTORY_SEPARATOR) {
666                $dir = '.' . DIRECTORY_SEPARATOR;
667             }
668             $this->dir = $dir;
669             \Log::debug('Directory set to ' . $dir);
670          } else {
671             throw new SE('Directory name not scalar', SE::E_NAME_INVALID);
672          }
673 
674          return $this;
675       }
676 
677    }
678 
AloFramework documentation API documentation generated by ApiGen 2.8.0