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

Namespaces

  • Alo
    • Cache
    • Controller
    • Db
    • Exception
    • Session
    • Statics
    • Test
    • Validators
  • 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\SFTPException as SE;
  7    use Alo\Exception\SFTPException;
  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        * @see self::P_RETRY_COUNT
118        * @see self::P_RETRY_TIME
119        * @throws EE When the SSH2 extension is not loaded
120        */
121       function __construct($params = []) {
122          if (!function_exists('ssh2_connect')) {
123             throw new EE('SSH2 extension not loaded', EE::E_EXT_NOT_LOADED);
124          } else {
125             $this->dir = DIR_INDEX;
126 
127             $this->retry_count_max = (int)\get($params[self::P_RETRY_COUNT]);
128             $this->retry_time = (int)\get($params[self::P_RETRY_TIME]) ? $params[self::P_RETRY_TIME] : 3;
129 
130             \Log::debug('SSH2 class initialised');
131          }
132       }
133 
134       /**
135        * If no parameter is passed gets the maximum amount of retry attempts for failed operations, otherwise sets it.
136        *
137        * @author Art <a.molcanovas@gmail.com>
138        * @param int $int The amount
139        * @return boolean|int
140        */
141       function retryCount($int = -1) {
142          if ($int === -1) {
143             return $this->retry_count_max;
144          } elseif (is_numeric($int) && $int >= 0) {
145             $this->retry_count_max = (int)$int;
146             \Log::debug('Retry count set to ' . $int);
147 
148             return true;
149          } else {
150             return false;
151          }
152       }
153 
154       /**
155        * If no parameter is passed gets the time to wait between operation retries, otherwise sets it
156        *
157        * @author Art <a.molcanovas@gmail.com>
158        * @param int $int The time in seconds
159        * @return boolean
160        */
161       function retryTime($int = -1) {
162          if ($int === -1) {
163             return $this->retry_time;
164          } elseif (is_numeric($int) && $int > 0) {
165             $this->retry_time = (int)$int;
166             \Log::debug('Retry time set to ' . $int);
167 
168             return true;
169          } else {
170             return false;
171          }
172       }
173 
174       /**
175        * Sets the local directory
176        *
177        * @author Art <a.molcanovas@gmail.com>
178        * @param string $dir The directory path
179        * @return SFTP
180        */
181       function loc($dir) {
182          $dir = rtrim($dir, ' ' . DIRECTORY_SEPARATOR);
183          if (!$dir) {
184             $dir = DIRECTORY_SEPARATOR;
185          }
186          $this->local_dir = $dir;
187          \Log::debug('Local dir set to ' . $dir);
188 
189          return $this;
190       }
191 
192       /**
193        * Scans a directory for files and subdirectories
194        *
195        * @author Art <a.molcanovas@gmail.com>
196        * @param int $sorting_order The sorting order
197        * @return array ["dirs" => [], "files" => []]
198        */
199       function scandir($sorting_order = self::SORT_ASC) {
200          $this->checkSubsystem();
201 
202          $dir = scandir('ssh2.sftp://' . $this->sftp . DIRECTORY_SEPARATOR . $this->dir, $sorting_order);
203          $r = [
204             'dirs'  => [],
205             'files' => []
206          ];
207 
208          foreach ($dir as $v) {
209             //Ignore hidden
210             if ($v == '.' || $v == '..' || stripos($v, '.') === 0) {
211                continue;
212             } elseif (self::isFile($v)) {
213                $r['files'][] = $v;
214             } else {
215                $r['dirs'][] = $v;
216             }
217          }
218 
219          return $r;
220       }
221 
222       /**
223        * Checks if the SFTP subsystem was initialised
224        *
225        * @author Art <a.molcanovas@gmail.com>
226        * @return SFTP
227        */
228       function checkSubsystem() {
229          if (!$this->sftp) {
230             \Log::debug('SFTP subsystem wasn\'t initialised when a dependant'
231                . ' method was called. Initialising.');
232             $this->connect();
233          }
234 
235          return $this;
236       }
237 
238       /**
239        * Creates a SSH2 connection
240        *
241        * @author Art <a.molcanovas@gmail.com>
242        * @param int $attempt Current attempt number
243        * @return SFTP
244        * @throws SE When the connection ultimately fails
245        */
246       function connect($attempt = 0) {
247          if (!($this->connection = @ssh2_connect($this->url))) {
248             $msg = 'Failed to initialise SSH2 connection';
249             $attempt++;
250 
251             if ($attempt - 1 < $this->retry_count_max) {
252                \Log::error($msg . '. Retrying again' . ' in '
253                   . $this->retry_time . ' seconds [' . $attempt
254                   . '/' . $this->retry_count_max . ']');
255 
256                sleep($this->retry_time);
257 
258                return $this->connect($attempt);
259             } else {
260                throw new SE($msg . ' after ' . $attempt . ' attempts', SE::E_CONNECT);
261             }
262          } else {
263             \Log::debug('Initialised SSH2 connection');
264 
265             return $this->auth();
266          }
267       }
268 
269       /**
270        * Authenticates the SSH2 connection
271        *
272        * @author Art <a.molcanovas@gmail.com>
273        * @param int $attempt The current attempt # at authentication
274        * @return SFTP
275        * @throws SE When authentication permanently fails
276        */
277       protected function auth($attempt = 0) {
278          if (!ssh2_auth_pubkey_file($this->connection, $this->user, $this->pubkey, $this->privkey, $this->pw)) {
279             $msg = 'Failed to authenticate SSH2 connection';
280             ++$attempt;
281 
282             if ($attempt - 1 < $this->retry_count_max) {
283                \Log::error($msg . '. Retrying in ' . $this->retry_time
284                   . ' seconds [' . $attempt . '/'
285                   . $this->retry_count_max . ']');
286                sleep($this->retry_time);
287 
288                return $this->auth($attempt);
289             } else {
290                throw new SE($msg . ' after ' . $attempt . ' retries', SE::E_AUTH);
291             }
292          } else {
293             \Log::debug('SSH2 connection authenticated');
294 
295             return $this->ssh2_sftp();
296          }
297       }
298 
299       /**
300        * Initialises the SFTP subsystem
301        *
302        * @author Art <a.molcanovas@gmail.com>
303        * @param int $attempt Current retry number
304        * @return SFTP
305        * @throws SE When initialising the SFTP system permanently fails
306        */
307       protected function ssh2_sftp($attempt = 0) {
308          if ($this->sftp = @ssh2_sftp($this->connection)) {
309             \Log::debug('Initialised SFTP subsystem');
310 
311             return $this;
312          } else {
313             $msg = 'Failed to initialise SFTP subsystem';
314             ++$attempt;
315 
316             if ($attempt - 1 < $this->retry_count_max) {
317                \Log::error($msg . '. Retrying again' . ' in '
318                   . $this->retry_time . ' seconds [' . $attempt
319                   . '/' . $this->retry_count_max . ']');
320                sleep($this->retry_time);
321 
322                return $this->ssh2_sftp($attempt);
323             } else {
324                throw new SE($msg . ' after ' . $attempt . ' attempts', SE::E_SUBSYSTEM);
325             }
326          }
327       }
328 
329       /**
330        * Checks whether a resource is a file or directory based on whether it has a file extension
331        *
332        * @author Art <a.molcanovas@gmail.com>
333        * @param string $resource The resource name to check
334        * @return boolean
335        */
336       protected static function isFile($resource) {
337          $pos = stripos($resource, '.');
338 
339          return $pos !== false && $pos !== 0;
340       }
341 
342       /**
343        * Downloads a file to $this->local_dir
344        *
345        * @author Art <a.molcanovas@gmail.com>
346        * @see    self::$local_dir
347        * @param string $file Remote file name
348        * @return SFTP
349        */
350       function downloadFile($file) {
351          \Log::debug('Downloading file ' . $file . ' to ' . $this->local_dir);
352          $fetch = $this->getFileContents($file);
353 
354          $local = new File();
355          $local->dir($this->local_dir)->name($file);
356          $local->content($fetch)->write();
357 
358          return $this;
359       }
360 
361       /**
362        * Gets the file contents
363        *
364        * @author Art <a.molcanovas@gmail.com>
365        * @param string $file File name
366        * @return string String representation of the file
367        * @throws SE When the file cannot be fetched
368        */
369       function getFileContents($file) {
370          $this->checkSubsystem();
371          $remoteFile = $this->resolvePath($file);
372 
373          $file = @file_get_contents('ssh2.sftp://' . $this->sftp . '/' . $remoteFile);
374          if ($file === false) {
375             throw new SE('Failed to fetch file ' . $remoteFile, SE::E_FILE_NOT_FETCHED);
376          } else {
377             return $file;
378          }
379       }
380 
381       /**
382        * Modifies the path based on whether it's relative or absolute
383        *
384        * @author Art <a.molcanovas@gmail.com>
385        * @param string $item Item name
386        * @return string The resolved path
387        */
388       protected function resolvePath($item) {
389          return (stripos($item, DIRECTORY_SEPARATOR) === 0) ? substr($item, 1) : $this->dir . DIRECTORY_SEPARATOR . $item;
390       }
391 
392       /**
393        * Uploads a file to the SFTP folder from the local folder
394        *
395        * @author Art <a.molcanovas@gmail.com>
396        * @param string $file File name
397        * @return boolean
398        * @throws SE When the local file cannot be read
399        */
400       function upload($file) {
401          $this->checkSubsystem();
402          $path = $this->local_dir . DIRECTORY_SEPARATOR . $file;
403 
404          if (!$content = @file_get_contents($path)) {
405             throw new SE('Local file ' . $path . ' cannot be read', SE::E_LOCAL_FILE_NOT_READ);
406          } else {
407             \Log::debug('Uploading remote file ' . $file);
408 
409             return $this->makeFile($file, $content);
410          }
411       }
412 
413       /**
414        * Creates a file in the SFTP directory
415        *
416        * @author Art <a.molcanovas@gmail.com>
417        * @param string $file    File name
418        * @param mixed  $content File content
419        * @return boolean
420        * @throws SE When remote fopen fails
421        */
422       function makeFile($file, $content) {
423          $this->checkSubsystem();
424          $remoteFile = $this->resolvePath($file);
425 
426          if (!$fp = @fopen('ssh2.sftp://' . $this->sftp . DIRECTORY_SEPARATOR . $remoteFile, File::M_WRITE_TRUNCATE_BEGIN)) {
427             throw new SE('Failed to remotely fopen ' . $remoteFile, SE::E_FILE_CREATE_FAIL);
428          } else {
429             flock($fp, LOCK_EX);
430             fwrite($fp, $content);
431             flock($fp, LOCK_UN);
432             fclose($fp);
433             \Log::debug('Wrote remote file ' . $remoteFile);
434 
435             return true;
436          }
437       }
438 
439       /**
440        * Deletes an item on the SFTP server
441        *
442        * @author Art <a.molcanovas@gmail.com>
443        * @param string $item File or directory name
444        * @return boolean
445        */
446       function delete($item) {
447          $this->checkSubsystem();
448          $path = $this->resolvePath($item);
449 
450          if (self::isFile($item)) {
451             $success = ssh2_sftp_unlink($this->sftp, $path);
452          } else {
453             $success = ssh2_sftp_rmdir($this->sftp, $path);
454          }
455 
456          if ($success) {
457             \Log::debug('Deleted ' . $item);
458 
459             return true;
460          } else {
461             \Log::error('Failed to delete ' . $item);
462 
463             return false;
464          }
465       }
466 
467       /**
468        * Returns a string representation of SFTP credentials
469        *
470        * @author Art <a.molcanovas@gmail.com>
471        * @return string
472        */
473       function __toString() {
474          return 'User: ' . $this->user . '; PrivKey:' . $this->privkey . '; PubKey: ' . $this->pubkey . '; Password hash: ' . get($this->pw) ? md5($this->pw) : 'NO HASH CONTENT SET';
475       }
476 
477       /**
478        * If no parameter is passed gets the SFTP server URL, otherwise sets it.
479        *
480        * @author Art <a.molcanovas@gmail.com>
481        * @param string $url
482        * @return SFTP
483        * @throws SE When the URL is not a string
484        */
485       function url($url = '') {
486          if ($url === '') {
487             return $this->url;
488          } elseif (is_string($url)) {
489             $this->url = $url;
490             \Log::debug('SFTP URL set to ' . $url);
491          } else {
492             throw new SE('Invalid URL', SE::E_URL_INVALID);
493          }
494 
495          return $this;
496       }
497 
498       /**
499        * If no parameter is passed gets the SFTP username, otherwise sets it.
500        *
501        * @author Art <a.molcanovas@gmail.com>
502        * @param string $user The username
503        * @throws SE When $user isn't scalar
504        * @return SFTP
505        */
506       function user($user = '') {
507          if ($user === '') {
508             return $this->user;
509          } elseif (is_scalar($user)) {
510             $this->user = $user;
511             \Log::debug('SFTP user set to ' . $user);
512          } else {
513             throw new SE('Invalid username', SE::E_USER_INVALID);
514          }
515 
516          return $this;
517       }
518 
519       /**
520        * Sets the SFTP public key path
521        *
522        * @author Art <a.molcanovas@gmail.com>
523        * @param string $pubkey The path
524        * @throws SE When $pubkey isn't a string
525        * @return SFTP
526        */
527       function pubkey($pubkey = '') {
528          if ($pubkey === '') {
529             return $this->pubkey;
530          } elseif (is_string($pubkey)) {
531             $this->pubkey = $pubkey;
532             \Log::debug('SFTP pubkey set');
533          } else {
534             throw new SE('$pubkey must be a valid path', SE::E_PATH_INVALID);
535          }
536 
537          return $this;
538       }
539 
540       /**
541        * If no parameter is passed gets the SFTP private key path, otherwise sets it
542        *
543        * @author Art <a.molcanovas@gmail.com>
544        * @param string $privkey The path
545        * @throws SE When the $privkey isn't a string
546        * @return SFTP
547        */
548       function privkey($privkey = '') {
549          if ($privkey === '') {
550             return $this->privkey;
551          } elseif (is_string($privkey)) {
552             $this->privkey = $privkey;
553             \Log::debug('SFTP privkey set');
554          } else {
555             throw new SE('$privkey must be a valid path', SE::E_PATH_INVALID);
556          }
557 
558          return $this;
559       }
560 
561       /**
562        * If no parameter is passed gets the SFTP private key password, otherwise sets it
563        *
564        * @author Art <a.molcanovas@gmail.com>
565        * @param string $pw The password
566        * @throws SE When the password isn't scalar
567        * @return SFTP
568        */
569       function pw($pw = '') {
570          if ($pw === '') {
571             return $this->pw;
572          } elseif (is_scalar($pw)) {
573             $this->pw = $pw;
574             \Log::debug('SFTP password set');
575          } else {
576             throw new SE('Invalid password provided', SE::E_PW_INVALID);
577          }
578 
579          return $this;
580       }
581 
582       /**
583        * If no argument is passed, gets the working directory, otherwise sets it.
584        *
585        * @author Art <a.molcanovas@gmail.com>
586        * @param mixed $dir
587        * @throws SE When the name is invalid
588        * @return SFTP
589        */
590       function dir($dir = -1) {
591          if ($dir === -1) {
592             return $this->dir;
593          } elseif (is_scalar($dir)) {
594             $dir = trim($dir, DIRECTORY_SEPARATOR . ' ');
595             if (!$dir || $dir == DIRECTORY_SEPARATOR) {
596                $dir = '.' . DIRECTORY_SEPARATOR;
597             }
598             $this->dir = $dir;
599             \Log::debug('Directory set to ' . $dir);
600          } else {
601             throw new SE('Directory name not scalar', SE::E_NAME_INVALID);
602          }
603 
604          return $this;
605       }
606 
607    }
608 
AloFramework documentation API documentation generated by ApiGen 2.8.0