1<?php
2
3/**
4 * Simple elFinder driver for OneDrive
5 * onedrive api v5.0.
6 *
7 * @author Dmitry (dio) Levashov
8 * @author Cem (discofever)
9 **/
10class elFinderVolumeOneDrive extends elFinderVolumeDriver
11{
12    /**
13     * Driver id
14     * Must be started from letter and contains [a-z0-9]
15     * Used as part of volume id.
16     *
17     * @var string
18     **/
19    protected $driverId = 'od';
20
21    /**
22     * @var string The base URL for API requests
23     **/
24    const API_URL = 'https://graph.microsoft.com/v1.0/me/drive/items/';
25
26    /**
27     * @var string The base URL for authorization requests
28     */
29    const AUTH_URL = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize';
30
31    /**
32     * @var string The base URL for token requests
33     */
34    const TOKEN_URL = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
35
36    /**
37     * OneDrive token object.
38     *
39     * @var object
40     **/
41    protected $token = null;
42
43    /**
44     * Directory for tmp files
45     * If not set driver will try to use tmbDir as tmpDir.
46     *
47     * @var string
48     **/
49    protected $tmp = '';
50
51    /**
52     * Net mount key.
53     *
54     * @var string
55     **/
56    public $netMountKey = '';
57
58    /**
59     * Thumbnail prefix.
60     *
61     * @var string
62     **/
63    protected $tmbPrefix = '';
64
65    /**
66     * hasCache by folders.
67     *
68     * @var array
69     **/
70    protected $HasdirsCache = array();
71
72    /**
73     * Query options of API call.
74     *
75     * @var array
76     */
77    protected $queryOptions = array();
78
79    /**
80     * Current token expires
81     *
82     * @var integer
83     **/
84    private $expires;
85
86    /**
87     * Path to access token file for permanent mount
88     *
89     * @var string
90     */
91    private $aTokenFile = '';
92
93    /**
94     * Constructor
95     * Extend options with required fields.
96     *
97     * @author Dmitry (dio) Levashov
98     * @author Cem (DiscoFever)
99     **/
100    public function __construct()
101    {
102        $opts = array(
103            'client_id' => '',
104            'client_secret' => '',
105            'accessToken' => '',
106            'root' => 'OneDrive.com',
107            'OneDriveApiClient' => '',
108            'path' => '/',
109            'separator' => '/',
110            'tmbPath' => '',
111            'tmbURL' => '',
112            'tmpPath' => '',
113            'acceptedName' => '#^[^/\\?*:|"<>]*[^./\\?*:|"<>]$#',
114            'rootCssClass' => 'elfinder-navbar-root-onedrive',
115            'useApiThumbnail' => true,
116        );
117        $this->options = array_merge($this->options, $opts);
118        $this->options['mimeDetect'] = 'internal';
119    }
120
121    /*********************************************************************/
122    /*                        ORIGINAL FUNCTIONS                         */
123    /*********************************************************************/
124
125    /**
126     * Obtains a new access token from OAuth. This token is valid for one hour.
127     *
128     * @param        $client_id
129     * @param        $client_secret
130     * @param string $code         The code returned by OneDrive after
131     *                             successful log in
132     *
133     * @return object|string
134     * @throws Exception Thrown if the redirect URI of this Client instance's
135     *                    state is not set
136     */
137    protected function _od_obtainAccessToken($client_id, $client_secret, $code, $nodeid)
138    {
139        if (null === $client_id) {
140            return 'The client ID must be set to call obtainAccessToken()';
141        }
142
143        if (null === $client_secret) {
144            return 'The client Secret must be set to call obtainAccessToken()';
145        }
146
147        $redirect = elFinder::getConnectorUrl();
148        if (strpos($redirect, '/netmount/onedrive/') === false) {
149            $redirect .= '/netmount/onedrive/' . ($nodeid === 'elfinder'? '1' : $nodeid);
150        }
151
152        $url = self::TOKEN_URL;
153
154        $curl = curl_init();
155
156        $fields = http_build_query(
157            array(
158                'client_id' => $client_id,
159                'redirect_uri' => $redirect,
160                'client_secret' => $client_secret,
161                'code' => $code,
162                'grant_type' => 'authorization_code',
163            )
164        );
165
166        curl_setopt_array($curl, array(
167            // General options.
168            CURLOPT_RETURNTRANSFER => true,
169            CURLOPT_POST => true,
170            CURLOPT_POSTFIELDS => $fields,
171
172            CURLOPT_HTTPHEADER => array(
173                'Content-Length: ' . strlen($fields),
174            ),
175
176            CURLOPT_URL => $url,
177        ));
178
179        $result = curl_exec($curl);
180
181        if (false === $result) {
182            if (curl_errno($curl)) {
183                throw new \Exception('curl_setopt_array() failed: '
184                    . curl_error($curl));
185            } else {
186                throw new \Exception('curl_setopt_array(): empty response');
187            }
188        }
189        curl_close($curl);
190
191        $decoded = json_decode($result);
192
193        if (null === $decoded) {
194            throw new \Exception('json_decode() failed');
195        }
196
197        $res = (object)array(
198            'expires' => time() + $decoded->expires_in - 30,
199            'initialToken' => '',
200            'data' => $decoded
201        );
202        if (!empty($decoded->refresh_token)) {
203            $res->initialToken = md5($client_id . $decoded->refresh_token);
204        }
205        return $res;
206    }
207
208    /**
209     * Get token and auto refresh.
210     *
211     * @return true
212     * @throws Exception
213     */
214    protected function _od_refreshToken()
215    {
216        if (!property_exists($this->token, 'expires') || $this->token->expires < time()) {
217            if (!$this->options['client_id']) {
218                $this->options['client_id'] = ELFINDER_ONEDRIVE_CLIENTID;
219            }
220
221            if (!$this->options['client_secret']) {
222                $this->options['client_secret'] = ELFINDER_ONEDRIVE_CLIENTSECRET;
223            }
224
225            if (empty($this->token->data->refresh_token)) {
226                throw new \Exception(elFinder::ERROR_REAUTH_REQUIRE);
227            } else {
228                $refresh_token = $this->token->data->refresh_token;
229                $initialToken = $this->_od_getInitialToken();
230            }
231
232            $url = self::TOKEN_URL;
233
234            $curl = curl_init();
235
236            curl_setopt_array($curl, array(
237                // General options.
238                CURLOPT_RETURNTRANSFER => true,
239                CURLOPT_POST => true, // i am sending post data
240                CURLOPT_POSTFIELDS => 'client_id=' . urlencode($this->options['client_id'])
241                    . '&client_secret=' . urlencode($this->options['client_secret'])
242                    . '&grant_type=refresh_token'
243                    . '&refresh_token=' . urlencode($this->token->data->refresh_token),
244
245                CURLOPT_URL => $url,
246            ));
247
248            $result = curl_exec($curl);
249
250            if (!$result) {
251                if (curl_errno($curl)) {
252                    throw new \Exception('curl_setopt_array() failed: ' . curl_error($curl));
253                } else {
254                    throw new \Exception('curl_setopt_array(): empty response');
255                }
256            }
257            curl_close($curl);
258
259            $decoded = json_decode($result);
260
261            if (!$decoded) {
262                throw new \Exception('json_decode() failed');
263            }
264
265            if (empty($decoded->access_token)) {
266                if ($this->aTokenFile) {
267                    if (is_file($this->aTokenFile)) {
268                        unlink($this->aTokenFile);
269                    }
270                }
271                $err = property_exists($decoded, 'error')? ' ' . $decoded->error : '';
272                $err .= property_exists($decoded, 'error_description')? ' ' . $decoded->error_description : '';
273                throw new \Exception($err? $err : elFinder::ERROR_REAUTH_REQUIRE);
274            }
275
276            $token = (object)array(
277                'expires' => time() + $decoded->expires_in - 30,
278                'initialToken' => $initialToken,
279                'data' => $decoded,
280            );
281
282            $this->token = $token;
283            $json = json_encode($token);
284
285            if (!empty($decoded->refresh_token)) {
286                if (empty($this->options['netkey']) && $this->aTokenFile) {
287                    file_put_contents($this->aTokenFile, json_encode($token));
288                    $this->options['accessToken'] = $json;
289                } else if (!empty($this->options['netkey'])) {
290                    // OAuth2 refresh token can be used only once,
291                    // so update it if it is the same as the token file
292                    $aTokenFile = $this->_od_getATokenFile();
293                    if ($aTokenFile && is_file($aTokenFile)) {
294                        if ($_token = json_decode(file_get_contents($aTokenFile))) {
295                            if ($_token->data->refresh_token === $refresh_token) {
296                                file_put_contents($aTokenFile, $json);
297                            }
298                        }
299                    }
300                    $this->options['accessToken'] = $json;
301                    // update session value
302                    elFinder::$instance->updateNetVolumeOption($this->options['netkey'], 'accessToken', $this->options['accessToken']);
303                    $this->session->set('OneDriveTokens', $token);
304                } else {
305                    throw new \Exception(elFinder::ERROR_CREATING_TEMP_DIR);
306                }
307            }
308        }
309
310        return true;
311    }
312
313    /**
314     * Get Parent ID, Item ID, Parent Path as an array from path.
315     *
316     * @param string $path
317     *
318     * @return array
319     */
320    protected function _od_splitPath($path)
321    {
322        $path = trim($path, '/');
323        $pid = '';
324        if ($path === '') {
325            $id = 'root';
326            $parent = '';
327        } else {
328            $paths = explode('/', trim($path, '/'));
329            $id = array_pop($paths);
330            if ($paths) {
331                $parent = '/' . implode('/', $paths);
332                $pid = array_pop($paths);
333            } else {
334                $pid = 'root';
335                $parent = '/';
336            }
337        }
338
339        return array($pid, $id, $parent);
340    }
341
342    /**
343     * Creates a base cURL object which is compatible with the OneDrive API.
344     *
345     * @return resource A compatible cURL object
346     */
347    protected function _od_prepareCurl($url = null)
348    {
349        $curl = curl_init($url);
350
351        $defaultOptions = array(
352            // General options.
353            CURLOPT_RETURNTRANSFER => true,
354            CURLOPT_HTTPHEADER => array(
355                'Content-Type: application/json',
356                'Authorization: Bearer ' . $this->token->data->access_token,
357            ),
358        );
359
360        curl_setopt_array($curl, $defaultOptions);
361
362        return $curl;
363    }
364
365    /**
366     * Creates a base cURL object which is compatible with the OneDrive API.
367     *
368     * @param string $path The path of the API call (eg. me/skydrive)
369     * @param bool   $contents
370     *
371     * @return resource A compatible cURL object
372     * @throws elFinderAbortException
373     */
374    protected function _od_createCurl($path, $contents = false)
375    {
376        elFinder::checkAborted();
377        $curl = $this->_od_prepareCurl($path);
378
379        if ($contents) {
380            $res = elFinder::curlExec($curl);
381        } else {
382            $result = json_decode(curl_exec($curl));
383            curl_close($curl);
384            if (isset($result->value)) {
385                $res = $result->value;
386                unset($result->value);
387                $result = (array)$result;
388                if (!empty($result['@odata.nextLink'])) {
389                    $nextRes = $this->_od_createCurl($result['@odata.nextLink'], false);
390                    if (is_array($nextRes)) {
391                        $res = array_merge($res, $nextRes);
392                    }
393                }
394            } else {
395                $res = $result;
396            }
397        }
398
399        return $res;
400    }
401
402    /**
403     * Drive query and fetchAll.
404     *
405     * @param       $itemId
406     * @param bool  $fetch_self
407     * @param bool  $recursive
408     * @param array $options
409     *
410     * @return object|array
411     * @throws elFinderAbortException
412     */
413    protected function _od_query($itemId, $fetch_self = false, $recursive = false, $options = array())
414    {
415        $result = array();
416
417        if (null === $itemId) {
418            $itemId = 'root';
419        }
420
421        if ($fetch_self == true) {
422            $path = $itemId;
423        } else {
424            $path = $itemId . '/children';
425        }
426
427        if (isset($options['query'])) {
428            $path .= '?' . http_build_query($options['query']);
429        }
430
431        $url = self::API_URL . $path;
432
433        $res = $this->_od_createCurl($url);
434        if (!$fetch_self && $recursive && is_array($res)) {
435            foreach ($res as $file) {
436                $result[] = $file;
437                if (!empty($file->folder)) {
438                    $result = array_merge($result, $this->_od_query($file->id, false, true, $options));
439                }
440            }
441        } else {
442            $result = $res;
443        }
444
445        return isset($result->error) ? array() : $result;
446    }
447
448    /**
449     * Parse line from onedrive metadata output and return file stat (array).
450     *
451     * @param object $raw line from ftp_rawlist() output
452     *
453     * @return array
454     * @author Dmitry Levashov
455     **/
456    protected function _od_parseRaw($raw)
457    {
458        $stat = array();
459
460        $folder = isset($raw->folder) ? $raw->folder : null;
461
462        $stat['rev'] = isset($raw->id) ? $raw->id : 'root';
463        $stat['name'] = $raw->name;
464        if (isset($raw->lastModifiedDateTime)) {
465            $stat['ts'] = strtotime($raw->lastModifiedDateTime);
466        }
467
468        if ($folder) {
469            $stat['mime'] = 'directory';
470            $stat['size'] = 0;
471            if (empty($folder->childCount)) {
472                $stat['dirs'] = 0;
473            } else {
474                $stat['dirs'] = -1;
475            }
476        } else {
477            if (isset($raw->file->mimeType)) {
478                $stat['mime'] = $raw->file->mimeType;
479            }
480            $stat['size'] = (int)$raw->size;
481            if (!$this->disabledGetUrl) {
482                $stat['url'] = '1';
483            }
484            if (isset($raw->image) && $img = $raw->image) {
485                isset($img->width) ? $stat['width'] = $img->width : $stat['width'] = 0;
486                isset($img->height) ? $stat['height'] = $img->height : $stat['height'] = 0;
487            }
488            if (!empty($raw->thumbnails)) {
489                if ($raw->thumbnails[0]->small->url) {
490                    $stat['tmb'] = substr($raw->thumbnails[0]->small->url, 8); // remove "https://"
491                }
492            } elseif (!empty($raw->file->processingMetadata)) {
493                $stat['tmb'] = '1';
494            }
495        }
496
497        return $stat;
498    }
499
500    /**
501     * Get raw data(onedrive metadata) from OneDrive.
502     *
503     * @param string $path
504     *
505     * @return array|object onedrive metadata
506     */
507    protected function _od_getFileRaw($path)
508    {
509        list(, $itemId) = $this->_od_splitPath($path);
510        try {
511            $res = $this->_od_query($itemId, true, false, $this->queryOptions);
512
513            return $res;
514        } catch (Exception $e) {
515            return array();
516        }
517    }
518
519    /**
520     * Get thumbnail from OneDrive.com.
521     *
522     * @param string $path
523     *
524     * @return string | boolean
525     */
526    protected function _od_getThumbnail($path)
527    {
528        list(, $itemId) = $this->_od_splitPath($path);
529
530        try {
531            $url = self::API_URL . $itemId . '/thumbnails/0/medium/content';
532
533            return $this->_od_createCurl($url, $contents = true);
534        } catch (Exception $e) {
535            return false;
536        }
537    }
538
539    /**
540     * Upload large files with an upload session.
541     *
542     * @param resource $fp       source file pointer
543     * @param number   $size     total size
544     * @param string   $name     item name
545     * @param string   $itemId   item identifier
546     * @param string   $parent   parent
547     * @param string   $parentId parent identifier
548     *
549     * @return string The item path
550     */
551    protected function _od_uploadSession($fp, $size, $name, $itemId, $parent, $parentId)
552    {
553        try {
554            $send = $this->_od_getChunkData($fp);
555            if ($send === false) {
556                throw new Exception('Data can not be acquired from the source.');
557            }
558
559            // create upload session
560            if ($itemId) {
561                $url = self::API_URL . $itemId . '/createUploadSession';
562            } else {
563                $url = self::API_URL . $parentId . ':/' . rawurlencode($name) . ':/createUploadSession';
564            }
565            $curl = $this->_od_prepareCurl($url);
566            curl_setopt_array($curl, array(
567                CURLOPT_POST => true,
568                CURLOPT_POSTFIELDS => '{}',
569            ));
570            $sess = json_decode(curl_exec($curl));
571            curl_close($curl);
572
573            if ($sess) {
574                if (isset($sess->error)) {
575                    throw new Exception($sess->error->message);
576                }
577                $next = strlen($send);
578                $range = '0-' . ($next - 1) . '/' . $size;
579            } else {
580                throw new Exception('API response can not be obtained.');
581            }
582
583            $id = null;
584            $retry = 0;
585            while ($sess) {
586                elFinder::extendTimeLimit();
587                $putFp = tmpfile();
588                fwrite($putFp, $send);
589                rewind($putFp);
590                $_size = strlen($send);
591                $url = $sess->uploadUrl;
592                $curl = curl_init();
593                $options = array(
594                    CURLOPT_URL => $url,
595                    CURLOPT_PUT => true,
596                    CURLOPT_RETURNTRANSFER => true,
597                    CURLOPT_INFILE => $putFp,
598                    CURLOPT_INFILESIZE => $_size,
599                    CURLOPT_HTTPHEADER => array(
600                        'Content-Length: ' . $_size,
601                        'Content-Range: bytes ' . $range,
602                    ),
603                );
604                curl_setopt_array($curl, $options);
605                $sess = json_decode(curl_exec($curl));
606                curl_close($curl);
607                if ($sess) {
608                    if (isset($sess->error)) {
609                        throw new Exception($sess->error->message);
610                    }
611                    if (isset($sess->id)) {
612                        $id = $sess->id;
613                        break;
614                    }
615                    if (isset($sess->nextExpectedRanges)) {
616                        list($_next) = explode('-', $sess->nextExpectedRanges[0]);
617                        if ($next == $_next) {
618                            $send = $this->_od_getChunkData($fp);
619                            if ($send === false) {
620                                throw new Exception('Data can not be acquired from the source.');
621                            }
622                            $next += strlen($send);
623                            $range = $_next . '-' . ($next - 1) . '/' . $size;
624                            $retry = 0;
625                        } else {
626                            if (++$retry > 3) {
627                                throw new Exception('Retry limit exceeded with uploadSession API call.');
628                            }
629                        }
630                        $sess->uploadUrl = $url;
631                    }
632                } else {
633                    throw new Exception('API response can not be obtained.');
634                }
635            }
636
637            if ($id) {
638                return $this->_joinPath($parent, $id);
639            } else {
640                throw new Exception('An error occurred in the uploadSession API call.');
641            }
642        } catch (Exception $e) {
643            return $this->setError('OneDrive error: ' . $e->getMessage());
644        }
645    }
646
647    /**
648     * Get chunk data by file pointer to upload session.
649     *
650     * @param resource $fp source file pointer
651     *
652     * @return bool|string chunked data
653     */
654    protected function _od_getChunkData($fp)
655    {
656        static $chunkSize = null;
657        if ($chunkSize === null) {
658            $mem = elFinder::getIniBytes('memory_limit');
659            if ($mem < 1) {
660                $mem = 10485760; // 10 MiB
661            } else {
662                $mem -= memory_get_usage() - 1061548;
663                $mem = min($mem, 10485760);
664            }
665            if ($mem > 327680) {
666                $chunkSize = floor($mem / 327680) * 327680;
667            } else {
668                $chunkSize = $mem;
669            }
670        }
671        if ($chunkSize < 8192) {
672            return false;
673        }
674
675        $contents = '';
676        while (!feof($fp) && strlen($contents) < $chunkSize) {
677            $contents .= fread($fp, 8192);
678        }
679
680        return $contents;
681    }
682
683    /**
684     * Get AccessToken file path
685     *
686     * @return string  ( description_of_the_return_value )
687     */
688    protected function _od_getATokenFile()
689    {
690        $tmp = $aTokenFile = '';
691        if (!empty($this->token->data->refresh_token)) {
692            if (!$this->tmp) {
693                $tmp = elFinder::getStaticVar('commonTempPath');
694                if (!$tmp) {
695                    $tmp = $this->getTempPath();
696                }
697                $this->tmp = $tmp;
698            }
699            if ($tmp) {
700                $aTokenFile = $tmp . DIRECTORY_SEPARATOR . $this->_od_getInitialToken() . '.otoken';
701            }
702        }
703        return $aTokenFile;
704    }
705
706    /**
707     * Get Initial Token (MD5 hash)
708     *
709     * @return string
710     */
711    protected function _od_getInitialToken()
712    {
713        return (empty($this->token->initialToken)? md5($this->options['client_id'] . (!empty($this->token->data->refresh_token)? $this->token->data->refresh_token : $this->token->data->access_token)) : $this->token->initialToken);
714    }
715
716    /*********************************************************************/
717    /*                        OVERRIDE FUNCTIONS                         */
718    /*********************************************************************/
719
720    /**
721     * Prepare
722     * Call from elFinder::netmout() before volume->mount().
723     *
724     * @return array
725     * @author Naoki Sawada
726     * @author Raja Sharma updating for OneDrive
727     **/
728    public function netmountPrepare($options)
729    {
730        if (empty($options['client_id']) && defined('ELFINDER_ONEDRIVE_CLIENTID')) {
731            $options['client_id'] = ELFINDER_ONEDRIVE_CLIENTID;
732        }
733        if (empty($options['client_secret']) && defined('ELFINDER_ONEDRIVE_CLIENTSECRET')) {
734            $options['client_secret'] = ELFINDER_ONEDRIVE_CLIENTSECRET;
735        }
736
737        if (isset($options['pass']) && $options['pass'] === 'reauth') {
738            $options['user'] = 'init';
739            $options['pass'] = '';
740            $this->session->remove('OneDriveTokens');
741        }
742
743        if (isset($options['id'])) {
744            $this->session->set('nodeId', $options['id']);
745        } elseif ($_id = $this->session->get('nodeId')) {
746            $options['id'] = $_id;
747            $this->session->set('nodeId', $_id);
748        }
749
750        if (!empty($options['tmpPath'])) {
751            if ((is_dir($options['tmpPath']) || mkdir($this->options['tmpPath'])) && is_writable($options['tmpPath'])) {
752                $this->tmp = $options['tmpPath'];
753            }
754        }
755
756        try {
757            if (empty($options['client_id']) || empty($options['client_secret'])) {
758                return array('exit' => true, 'body' => '{msg:errNetMountNoDriver}');
759            }
760
761            $itpCare = isset($options['code']);
762            $code = $itpCare? $options['code'] : (isset($_GET['code'])? $_GET['code'] : '');
763            if ($code) {
764                try {
765                    if (!empty($options['id'])) {
766                        // Obtain the token using the code received by the OneDrive API
767                        $this->session->set('OneDriveTokens',
768                            $this->_od_obtainAccessToken($options['client_id'], $options['client_secret'], $code, $options['id']));
769
770                        $out = array(
771                            'node' => $options['id'],
772                            'json' => '{"protocol": "onedrive", "mode": "done", "reset": 1}',
773                            'bind' => 'netmount',
774                        );
775                    } else {
776                        $nodeid = ($_GET['host'] === '1')? 'elfinder' : $_GET['host'];
777                        $out = array(
778                            'node' => $nodeid,
779                            'json' => json_encode(array(
780                                'protocol' => 'onedrive',
781                                'host' => $nodeid,
782                                'mode' => 'redirect',
783                                'options' => array(
784                                    'id' => $nodeid,
785                                    'code'=> $code
786                                )
787                            )),
788                            'bind' => 'netmount'
789                        );
790                    }
791                    if (!$itpCare) {
792                        return array('exit' => 'callback', 'out' => $out);
793                    } else {
794                        return array('exit' => true, 'body' => $out['json']);
795                    }
796                } catch (Exception $e) {
797                    $out = array(
798                        'node' => $options['id'],
799                        'json' => json_encode(array('error' => elFinder::ERROR_ACCESS_DENIED . ' ' . $e->getMessage())),
800                    );
801
802                    return array('exit' => 'callback', 'out' => $out);
803                }
804            } elseif (!empty($_GET['error'])) {
805                $out = array(
806                    'node' => $options['id'],
807                    'json' => json_encode(array('error' => elFinder::ERROR_ACCESS_DENIED)),
808                );
809
810                return array('exit' => 'callback', 'out' => $out);
811            }
812
813            if ($options['user'] === 'init') {
814                $this->token = $this->session->get('OneDriveTokens');
815
816                if ($this->token) {
817                    try {
818                        $this->_od_refreshToken();
819                    } catch (Exception $e) {
820                        $this->setError($e->getMessage());
821                        $this->token = null;
822                        $this->session->remove('OneDriveTokens');
823                    }
824                }
825
826                if (empty($this->token)) {
827                    $result = false;
828                } else {
829                    $path = $options['path'];
830                    if ($path === '/') {
831                        $path = 'root';
832                    }
833                    $result = $this->_od_query($path, false, false, array(
834                        'query' => array(
835                            'select' => 'id,name',
836                            'filter' => 'folder ne null',
837                        ),
838                    ));
839                }
840
841                if ($result === false) {
842                    try {
843                        $this->session->set('OneDriveTokens', (object)array('token' => null));
844
845                        $offline = '';
846                        // Gets a log in URL with sufficient privileges from the OneDrive API
847                        if (!empty($options['offline'])) {
848                            $offline = ' offline_access';
849                        }
850
851                        $redirect_uri = elFinder::getConnectorUrl() . '/netmount/onedrive/' . ($options['id'] === 'elfinder'? '1' : $options['id']);
852                        $url = self::AUTH_URL
853                            . '?client_id=' . urlencode($options['client_id'])
854                            . '&scope=' . urlencode('files.readwrite.all' . $offline)
855                            . '&response_type=code'
856                            . '&redirect_uri=' . urlencode($redirect_uri);
857
858                    } catch (Exception $e) {
859                        return array('exit' => true, 'body' => '{msg:errAccess}');
860                    }
861
862                    $html = '<input id="elf-volumedriver-onedrive-host-btn" class="ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only" value="{msg:btnApprove}" type="button">';
863                    $html .= '<script>
864                            $("#' . $options['id'] . '").elfinder("instance").trigger("netmount", {protocol: "onedrive", mode: "makebtn", url: "' . $url . '"});
865                            </script>';
866
867                    return array('exit' => true, 'body' => $html);
868                } else {
869                    $folders = [];
870
871                    if ($result) {
872                        foreach ($result as $res) {
873                            $folders[$res->id] = $res->name;
874                        }
875                        natcasesort($folders);
876                    }
877
878                    if ($options['pass'] === 'folders') {
879                        return ['exit' => true, 'folders' => $folders];
880                    }
881
882                    $folders = ['root' => 'My OneDrive'] + $folders;
883                    $folders = json_encode($folders);
884
885                    $expires = empty($this->token->data->refresh_token) ? (int)$this->token->expires : 0;
886                    $mnt2res = empty($this->token->data->refresh_token) ? '' : ', "mnt2res": 1';
887                    $json = '{"protocol": "onedrive", "mode": "done", "folders": ' . $folders . ', "expires": ' . $expires . $mnt2res .'}';
888                    $html = 'OneDrive.com';
889                    $html .= '<script>
890                            $("#' . $options['id'] . '").elfinder("instance").trigger("netmount", ' . $json . ');
891                            </script>';
892
893                    return array('exit' => true, 'body' => $html);
894                }
895            }
896        } catch (Exception $e) {
897            return array('exit' => true, 'body' => '{msg:errNetMountNoDriver}');
898        }
899
900        if ($_aToken = $this->session->get('OneDriveTokens')) {
901            $options['accessToken'] = json_encode($_aToken);
902            if ($this->options['path'] === 'root' || !$this->options['path']) {
903                $this->options['path'] = '/';
904            }
905        } else {
906            $this->session->remove('OneDriveTokens');
907            $this->setError(elFinder::ERROR_NETMOUNT, $options['host'], implode(' ', $this->error()));
908
909            return array('exit' => true, 'error' => $this->error());
910        }
911
912        $this->session->remove('nodeId');
913        unset($options['user'], $options['pass'], $options['id']);
914
915        return $options;
916    }
917
918    /**
919     * process of on netunmount
920     * Drop `onedrive` & rm thumbs.
921     *
922     * @param array $options
923     *
924     * @return bool
925     */
926    public function netunmount($netVolumes, $key)
927    {
928        if (!$this->options['useApiThumbnail'] && ($tmbs = glob(rtrim($this->options['tmbPath'], '\\/') . DIRECTORY_SEPARATOR . $this->tmbPrefix . '*.png'))) {
929            foreach ($tmbs as $file) {
930                unlink($file);
931            }
932        }
933
934        return true;
935    }
936
937    /**
938     * Return debug info for client.
939     *
940     * @return array
941     **/
942    public function debug()
943    {
944        $res = parent::debug();
945        if (!empty($this->options['netkey']) && !empty($this->options['accessToken'])) {
946            $res['accessToken'] = $this->options['accessToken'];
947        }
948
949        return $res;
950    }
951
952    /*********************************************************************/
953    /*                        INIT AND CONFIGURE                         */
954    /*********************************************************************/
955
956    /**
957     * Prepare FTP connection
958     * Connect to remote server and check if credentials are correct, if so, store the connection id in $ftp_conn.
959     *
960     * @return bool
961     * @throws elFinderAbortException
962     * @author Dmitry (dio) Levashov
963     * @author Cem (DiscoFever)
964     */
965    protected function init()
966    {
967        if (!$this->options['accessToken']) {
968            return $this->setError('Required option `accessToken` is undefined.');
969        }
970
971        if (!empty($this->options['tmpPath'])) {
972            if ((is_dir($this->options['tmpPath']) || mkdir($this->options['tmpPath'])) && is_writable($this->options['tmpPath'])) {
973                $this->tmp = $this->options['tmpPath'];
974            }
975        }
976
977        $error = false;
978        try {
979            $this->token = json_decode($this->options['accessToken']);
980            if (!is_object($this->token)) {
981                throw new Exception('Required option `accessToken` is invalid JSON.');
982            }
983
984            // make net mount key
985            if (empty($this->options['netkey'])) {
986                $this->netMountKey = $this->_od_getInitialToken();
987            } else {
988                $this->netMountKey = $this->options['netkey'];
989            }
990
991            if ($this->aTokenFile = $this->_od_getATokenFile()) {
992                if (empty($this->options['netkey'])) {
993                    if ($this->aTokenFile) {
994                        if (is_file($this->aTokenFile)) {
995                            $this->token = json_decode(file_get_contents($this->aTokenFile));
996                            if (!is_object($this->token)) {
997                                unlink($this->aTokenFile);
998                                throw new Exception('Required option `accessToken` is invalid JSON.');
999                            }
1000                        } else {
1001                            file_put_contents($this->aTokenFile, $this->token);
1002                        }
1003                    }
1004                } else if (is_file($this->aTokenFile)) {
1005                    // If the refresh token is the same as the permanent volume
1006                    $this->token = json_decode(file_get_contents($this->aTokenFile));
1007                }
1008            }
1009
1010            if ($this->needOnline) {
1011                $this->_od_refreshToken();
1012
1013                $this->expires = empty($this->token->data->refresh_token) ? (int)$this->token->expires : 0;
1014            }
1015        } catch (Exception $e) {
1016            $this->token = null;
1017            $error = true;
1018            $this->setError($e->getMessage());
1019        }
1020
1021        if ($this->netMountKey) {
1022            $this->tmbPrefix = 'onedrive' . base_convert($this->netMountKey, 16, 32);
1023        }
1024
1025        if ($error) {
1026            if (empty($this->options['netkey']) && $this->tmbPrefix) {
1027                // for delete thumbnail
1028                $this->netunmount(null, null);
1029            }
1030            return false;
1031        }
1032
1033        // normalize root path
1034        if ($this->options['path'] == 'root') {
1035            $this->options['path'] = '/';
1036        }
1037
1038        $this->root = $this->options['path'] = $this->_normpath($this->options['path']);
1039
1040        $this->options['root'] = ($this->options['root'] == '')? 'OneDrive.com' : $this->options['root'];
1041
1042        if (empty($this->options['alias'])) {
1043            if ($this->needOnline) {
1044                $this->options['alias'] = ($this->options['path'] === '/') ? $this->options['root'] :
1045                    $this->_od_query(basename($this->options['path']), $fetch_self = true)->name . '@OneDrive';
1046                if (!empty($this->options['netkey'])) {
1047                    elFinder::$instance->updateNetVolumeOption($this->options['netkey'], 'alias', $this->options['alias']);
1048                }
1049            } else {
1050                $this->options['alias'] = $this->options['root'];
1051            }
1052        }
1053
1054        $this->rootName = $this->options['alias'];
1055
1056        // This driver dose not support `syncChkAsTs`
1057        $this->options['syncChkAsTs'] = false;
1058
1059        // 'lsPlSleep' minmum 10 sec
1060        $this->options['lsPlSleep'] = max(10, $this->options['lsPlSleep']);
1061
1062        $this->queryOptions = array(
1063            'query' => array(
1064                'select' => 'id,name,lastModifiedDateTime,file,folder,size,image',
1065            ),
1066        );
1067
1068        if ($this->options['useApiThumbnail']) {
1069            $this->options['tmbURL'] = 'https://';
1070            $this->options['tmbPath'] = '';
1071            $this->queryOptions['query']['expand'] = 'thumbnails(select=small)';
1072        }
1073
1074        // enable command archive
1075        $this->options['useRemoteArchive'] = true;
1076
1077        return true;
1078    }
1079
1080    /**
1081     * Configure after successfull mount.
1082     *
1083     * @author Dmitry (dio) Levashov
1084     **/
1085    protected function configure()
1086    {
1087        parent::configure();
1088
1089        // fallback of $this->tmp
1090        if (!$this->tmp && $this->tmbPathWritable) {
1091            $this->tmp = $this->tmbPath;
1092        }
1093    }
1094
1095    /*********************************************************************/
1096    /*                               FS API                              */
1097    /*********************************************************************/
1098
1099    /**
1100     * Close opened connection.
1101     *
1102     * @author Dmitry (dio) Levashov
1103     **/
1104    public function umount()
1105    {
1106    }
1107
1108    protected function isNameExists($path)
1109    {
1110        list($pid, $name) = $this->_od_splitPath($path);
1111
1112        $raw = $this->_od_query($pid . '/children/' . rawurlencode($name), true);
1113        return $raw ? $this->_od_parseRaw($raw) : false;
1114    }
1115
1116    /**
1117     * Cache dir contents.
1118     *
1119     * @param string $path dir path
1120     *
1121     * @return array
1122     * @throws elFinderAbortException
1123     * @author Dmitry Levashov
1124     */
1125    protected function cacheDir($path)
1126    {
1127        $this->dirsCache[$path] = array();
1128        $hasDir = false;
1129
1130        list(, $itemId) = $this->_od_splitPath($path);
1131
1132        $res = $this->_od_query($itemId, false, false, $this->queryOptions);
1133
1134        if ($res) {
1135            foreach ($res as $raw) {
1136                if ($stat = $this->_od_parseRaw($raw)) {
1137                    $itemPath = $this->_joinPath($path, $raw->id);
1138                    $stat = $this->updateCache($itemPath, $stat);
1139                    if (empty($stat['hidden'])) {
1140                        if (!$hasDir && $stat['mime'] === 'directory') {
1141                            $hasDir = true;
1142                        }
1143                        $this->dirsCache[$path][] = $itemPath;
1144                    }
1145                }
1146            }
1147        }
1148
1149        if (isset($this->sessionCache['subdirs'])) {
1150            $this->sessionCache['subdirs'][$path] = $hasDir;
1151        }
1152
1153        return $this->dirsCache[$path];
1154    }
1155
1156    /**
1157     * Copy file/recursive copy dir only in current volume.
1158     * Return new file path or false.
1159     *
1160     * @param string $src  source path
1161     * @param string $dst  destination dir path
1162     * @param string $name new file name (optionaly)
1163     *
1164     * @return string|false
1165     * @throws elFinderAbortException
1166     * @author Dmitry (dio) Levashov
1167     * @author Naoki Sawada
1168     */
1169    protected function copy($src, $dst, $name)
1170    {
1171        $itemId = '';
1172        if ($this->options['copyJoin']) {
1173            $test = $this->joinPathCE($dst, $name);
1174            if ($testStat = $this->isNameExists($test)) {
1175                $this->remove($test);
1176            }
1177        }
1178
1179        if ($path = $this->_copy($src, $dst, $name)) {
1180            $this->added[] = $this->stat($path);
1181        } else {
1182            $this->setError(elFinder::ERROR_COPY, $this->_path($src));
1183        }
1184
1185        return $path;
1186    }
1187
1188    /**
1189     * Remove file/ recursive remove dir.
1190     *
1191     * @param string $path  file path
1192     * @param bool   $force try to remove even if file locked
1193     *
1194     * @return bool
1195     * @throws elFinderAbortException
1196     * @author Dmitry (dio) Levashov
1197     * @author Naoki Sawada
1198     */
1199    protected function remove($path, $force = false)
1200    {
1201        $stat = $this->stat($path);
1202        $stat['realpath'] = $path;
1203        $this->rmTmb($stat);
1204        $this->clearcache();
1205
1206        if (empty($stat)) {
1207            return $this->setError(elFinder::ERROR_RM, $this->_path($path), elFinder::ERROR_FILE_NOT_FOUND);
1208        }
1209
1210        if (!$force && !empty($stat['locked'])) {
1211            return $this->setError(elFinder::ERROR_LOCKED, $this->_path($path));
1212        }
1213
1214        if ($stat['mime'] == 'directory') {
1215            if (!$this->_rmdir($path)) {
1216                return $this->setError(elFinder::ERROR_RM, $this->_path($path));
1217            }
1218        } else {
1219            if (!$this->_unlink($path)) {
1220                return $this->setError(elFinder::ERROR_RM, $this->_path($path));
1221            }
1222        }
1223
1224        $this->removed[] = $stat;
1225
1226        return true;
1227    }
1228
1229    /**
1230     * Create thumnbnail and return it's URL on success.
1231     *
1232     * @param string $path file path
1233     * @param        $stat
1234     *
1235     * @return string|false
1236     * @throws ImagickException
1237     * @throws elFinderAbortException
1238     * @author Dmitry (dio) Levashov
1239     * @author Naoki Sawada
1240     */
1241    protected function createTmb($path, $stat)
1242    {
1243        if ($this->options['useApiThumbnail']) {
1244            if (func_num_args() > 2) {
1245                list(, , $count) = func_get_args();
1246            } else {
1247                $count = 0;
1248            }
1249            if ($count < 10) {
1250                if (isset($stat['tmb']) && $stat['tmb'] != '1') {
1251                    return $stat['tmb'];
1252                } else {
1253                    sleep(2);
1254                    elFinder::extendTimeLimit();
1255                    $this->clearcache();
1256                    $stat = $this->stat($path);
1257
1258                    return $this->createTmb($path, $stat, ++$count);
1259                }
1260            }
1261
1262            return false;
1263        }
1264        if (!$stat || !$this->canCreateTmb($path, $stat)) {
1265            return false;
1266        }
1267
1268        $name = $this->tmbname($stat);
1269        $tmb = $this->tmbPath . DIRECTORY_SEPARATOR . $name;
1270
1271        // copy image into tmbPath so some drivers does not store files on local fs
1272        if (!$data = $this->_od_getThumbnail($path)) {
1273            return false;
1274        }
1275        if (!file_put_contents($tmb, $data)) {
1276            return false;
1277        }
1278
1279        $result = false;
1280
1281        $tmbSize = $this->tmbSize;
1282
1283        if (($s = getimagesize($tmb)) == false) {
1284            return false;
1285        }
1286
1287        /* If image smaller or equal thumbnail size - just fitting to thumbnail square */
1288        if ($s[0] <= $tmbSize && $s[1] <= $tmbSize) {
1289            $result = $this->imgSquareFit($tmb, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png');
1290        } else {
1291            if ($this->options['tmbCrop']) {
1292
1293                /* Resize and crop if image bigger than thumbnail */
1294                if (!(($s[0] > $tmbSize && $s[1] <= $tmbSize) || ($s[0] <= $tmbSize && $s[1] > $tmbSize)) || ($s[0] > $tmbSize && $s[1] > $tmbSize)) {
1295                    $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, false, 'png');
1296                }
1297
1298                if (($s = getimagesize($tmb)) != false) {
1299                    $x = $s[0] > $tmbSize ? intval(($s[0] - $tmbSize) / 2) : 0;
1300                    $y = $s[1] > $tmbSize ? intval(($s[1] - $tmbSize) / 2) : 0;
1301                    $result = $this->imgCrop($tmb, $tmbSize, $tmbSize, $x, $y, 'png');
1302                }
1303            } else {
1304                $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, true, 'png');
1305            }
1306
1307            $result = $this->imgSquareFit($tmb, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png');
1308        }
1309
1310        if (!$result) {
1311            unlink($tmb);
1312
1313            return false;
1314        }
1315
1316        return $name;
1317    }
1318
1319    /**
1320     * Return thumbnail file name for required file.
1321     *
1322     * @param array $stat file stat
1323     *
1324     * @return string
1325     * @author Dmitry (dio) Levashov
1326     **/
1327    protected function tmbname($stat)
1328    {
1329        return $this->tmbPrefix . $stat['rev'] . $stat['ts'] . '.png';
1330    }
1331
1332    /**
1333     * Return content URL.
1334     *
1335     * @param string $hash    file hash
1336     * @param array  $options options
1337     *
1338     * @return string
1339     * @author Naoki Sawada
1340     **/
1341    public function getContentUrl($hash, $options = array())
1342    {
1343        if (!empty($options['onetime']) && $this->options['onetimeUrl']) {
1344            return parent::getContentUrl($hash, $options);
1345        }
1346        if (!empty($options['temporary'])) {
1347            // try make temporary file
1348            $url = parent::getContentUrl($hash, $options);
1349            if ($url) {
1350                return $url;
1351            }
1352        }
1353        $res = '';
1354        if (($file = $this->file($hash)) == false || !$file['url'] || $file['url'] == 1) {
1355            $path = $this->decode($hash);
1356
1357            list(, $itemId) = $this->_od_splitPath($path);
1358            try {
1359                $url = self::API_URL . $itemId . '/createLink';
1360                $data = (object)array(
1361                    'type' => 'embed',
1362                    'scope' => 'anonymous',
1363                );
1364                $curl = $this->_od_prepareCurl($url);
1365                curl_setopt_array($curl, array(
1366                    CURLOPT_POST => true,
1367                    CURLOPT_POSTFIELDS => json_encode($data),
1368                ));
1369
1370                $result = curl_exec($curl);
1371                curl_close($curl);
1372                if ($result) {
1373                    $result = json_decode($result);
1374                    if (isset($result->link)) {
1375                        list(, $res) = explode('?', $result->link->webUrl);
1376                        $res = 'https://onedrive.live.com/download.aspx?' . $res;
1377                    }
1378                }
1379            } catch (Exception $e) {
1380                $res = '';
1381            }
1382        }
1383
1384        return $res;
1385    }
1386
1387    /*********************** paths/urls *************************/
1388
1389    /**
1390     * Return parent directory path.
1391     *
1392     * @param string $path file path
1393     *
1394     * @return string
1395     * @author Dmitry (dio) Levashov
1396     **/
1397    protected function _dirname($path)
1398    {
1399        list(, , $dirname) = $this->_od_splitPath($path);
1400
1401        return $dirname;
1402    }
1403
1404    /**
1405     * Return file name.
1406     *
1407     * @param string $path file path
1408     *
1409     * @return string
1410     * @author Dmitry (dio) Levashov
1411     **/
1412    protected function _basename($path)
1413    {
1414        list(, $basename) = $this->_od_splitPath($path);
1415
1416        return $basename;
1417    }
1418
1419    /**
1420     * Join dir name and file name and retur full path.
1421     *
1422     * @param string $dir
1423     * @param string $name
1424     *
1425     * @return string
1426     * @author Dmitry (dio) Levashov
1427     **/
1428    protected function _joinPath($dir, $name)
1429    {
1430        if ($dir === 'root') {
1431            $dir = '';
1432        }
1433
1434        return $this->_normpath($dir . '/' . $name);
1435    }
1436
1437    /**
1438     * Return normalized path, this works the same as os.path.normpath() in Python.
1439     *
1440     * @param string $path path
1441     *
1442     * @return string
1443     * @author Troex Nevelin
1444     **/
1445    protected function _normpath($path)
1446    {
1447        if (DIRECTORY_SEPARATOR !== '/') {
1448            $path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
1449        }
1450        $path = '/' . ltrim($path, '/');
1451
1452        return $path;
1453    }
1454
1455    /**
1456     * Return file path related to root dir.
1457     *
1458     * @param string $path file path
1459     *
1460     * @return string
1461     * @author Dmitry (dio) Levashov
1462     **/
1463    protected function _relpath($path)
1464    {
1465        return $path;
1466    }
1467
1468    /**
1469     * Convert path related to root dir into real path.
1470     *
1471     * @param string $path file path
1472     *
1473     * @return string
1474     * @author Dmitry (dio) Levashov
1475     **/
1476    protected function _abspath($path)
1477    {
1478        return $path;
1479    }
1480
1481    /**
1482     * Return fake path started from root dir.
1483     *
1484     * @param string $path file path
1485     *
1486     * @return string
1487     * @author Dmitry (dio) Levashov
1488     **/
1489    protected function _path($path)
1490    {
1491        return $this->rootName . $this->_normpath(substr($path, strlen($this->root)));
1492    }
1493
1494    /**
1495     * Return true if $path is children of $parent.
1496     *
1497     * @param string $path   path to check
1498     * @param string $parent parent path
1499     *
1500     * @return bool
1501     * @author Dmitry (dio) Levashov
1502     **/
1503    protected function _inpath($path, $parent)
1504    {
1505        return $path == $parent || strpos($path, $parent . '/') === 0;
1506    }
1507
1508    /***************** file stat ********************/
1509    /**
1510     * Return stat for given path.
1511     * Stat contains following fields:
1512     * - (int)    size    file size in b. required
1513     * - (int)    ts      file modification time in unix time. required
1514     * - (string) mime    mimetype. required for folders, others - optionally
1515     * - (bool)   read    read permissions. required
1516     * - (bool)   write   write permissions. required
1517     * - (bool)   locked  is object locked. optionally
1518     * - (bool)   hidden  is object hidden. optionally
1519     * - (string) alias   for symlinks - link target path relative to root path. optionally
1520     * - (string) target  for symlinks - link target path. optionally.
1521     * If file does not exists - returns empty array or false.
1522     *
1523     * @param string $path file path
1524     *
1525     * @return array|false
1526     * @author Dmitry (dio) Levashov
1527     **/
1528    protected function _stat($path)
1529    {
1530        if ($raw = $this->_od_getFileRaw($path)) {
1531            $stat = $this->_od_parseRaw($raw);
1532            if ($path === $this->root) {
1533                $stat['expires'] = $this->expires;
1534            }
1535            return $stat;
1536        }
1537
1538        return false;
1539    }
1540
1541    /**
1542     * Return true if path is dir and has at least one childs directory.
1543     *
1544     * @param string $path dir path
1545     *
1546     * @return bool
1547     * @throws elFinderAbortException
1548     * @author Dmitry (dio) Levashov
1549     */
1550    protected function _subdirs($path)
1551    {
1552        list(, $itemId) = $this->_od_splitPath($path);
1553
1554        return (bool)$this->_od_query($itemId, false, false, array(
1555            'query' => array(
1556                'top' => 1,
1557                'select' => 'id',
1558                'filter' => 'folder ne null',
1559            ),
1560        ));
1561    }
1562
1563    /**
1564     * Return object width and height
1565     * Ususaly used for images, but can be realize for video etc...
1566     *
1567     * @param string $path file path
1568     * @param string $mime file mime type
1569     *
1570     * @return string
1571     * @throws elFinderAbortException
1572     * @author Dmitry (dio) Levashov
1573     */
1574    protected function _dimensions($path, $mime)
1575    {
1576        if (strpos($mime, 'image') !== 0) {
1577            return '';
1578        }
1579
1580        //$cache = $this->_od_getFileRaw($path);
1581        if (func_num_args() > 2) {
1582            $args = func_get_arg(2);
1583        } else {
1584            $args = array();
1585        }
1586        if (!empty($args['substitute'])) {
1587            $tmbSize = intval($args['substitute']);
1588        } else {
1589            $tmbSize = null;
1590        }
1591        list(, $itemId) = $this->_od_splitPath($path);
1592        $options = array(
1593            'query' => array(
1594                'select' => 'id,image',
1595            ),
1596        );
1597        if ($tmbSize) {
1598            $tmb = 'c' . $tmbSize . 'x' . $tmbSize;
1599            $options['query']['expand'] = 'thumbnails(select=' . $tmb . ')';
1600        }
1601        $raw = $this->_od_query($itemId, true, false, $options);
1602
1603        if ($raw && property_exists($raw, 'image') && $img = $raw->image) {
1604            if (isset($img->width) && isset($img->height)) {
1605                $ret = array('dim' => $img->width . 'x' . $img->height);
1606                if ($tmbSize) {
1607                    $srcSize = explode('x', $ret['dim']);
1608                    if (min(($tmbSize / $srcSize[0]), ($tmbSize / $srcSize[1])) < 1) {
1609                        if (!empty($raw->thumbnails)) {
1610                            $tmbArr = (array)$raw->thumbnails[0];
1611                            if (!empty($tmbArr[$tmb]->url)) {
1612                                $ret['url'] = $tmbArr[$tmb]->url;
1613                            }
1614                        }
1615                    }
1616                }
1617
1618                return $ret;
1619            }
1620        }
1621
1622        $ret = '';
1623        if ($work = $this->getWorkFile($path)) {
1624            if ($size = @getimagesize($work)) {
1625                $cache['width'] = $size[0];
1626                $cache['height'] = $size[1];
1627                $ret = $size[0] . 'x' . $size[1];
1628            }
1629        }
1630        is_file($work) && @unlink($work);
1631
1632        return $ret;
1633    }
1634
1635    /******************** file/dir content *********************/
1636
1637    /**
1638     * Return files list in directory.
1639     *
1640     * @param string $path dir path
1641     *
1642     * @return array
1643     * @throws elFinderAbortException
1644     * @author Dmitry (dio) Levashov
1645     * @author Cem (DiscoFever)
1646     */
1647    protected function _scandir($path)
1648    {
1649        return isset($this->dirsCache[$path])
1650            ? $this->dirsCache[$path]
1651            : $this->cacheDir($path);
1652    }
1653
1654    /**
1655     * Open file and return file pointer.
1656     *
1657     * @param string $path  file path
1658     * @param bool   $write open file for writing
1659     *
1660     * @return resource|false
1661     * @author Dmitry (dio) Levashov
1662     **/
1663    protected function _fopen($path, $mode = 'rb')
1664    {
1665        if ($mode === 'rb' || $mode === 'r') {
1666            list(, $itemId) = $this->_od_splitPath($path);
1667            $data = array(
1668                'target' => self::API_URL . $itemId . '/content',
1669                'headers' => array('Authorization: Bearer ' . $this->token->data->access_token),
1670            );
1671
1672            // to support range request
1673            if (func_num_args() > 2) {
1674                $opts = func_get_arg(2);
1675            } else {
1676                $opts = array();
1677            }
1678            if (!empty($opts['httpheaders'])) {
1679                $data['headers'] = array_merge($opts['httpheaders'], $data['headers']);
1680            }
1681
1682            return elFinder::getStreamByUrl($data);
1683        }
1684
1685        return false;
1686    }
1687
1688    /**
1689     * Close opened file.
1690     *
1691     * @param resource $fp file pointer
1692     *
1693     * @return bool
1694     * @author Dmitry (dio) Levashov
1695     **/
1696    protected function _fclose($fp, $path = '')
1697    {
1698        is_resource($fp) && fclose($fp);
1699        if ($path) {
1700            unlink($this->getTempFile($path));
1701        }
1702    }
1703
1704    /********************  file/dir manipulations *************************/
1705
1706    /**
1707     * Create dir and return created dir path or false on failed.
1708     *
1709     * @param string $path parent dir path
1710     * @param string $name new directory name
1711     *
1712     * @return string|bool
1713     * @author Dmitry (dio) Levashov
1714     **/
1715    protected function _mkdir($path, $name)
1716    {
1717        $namePath = $this->_joinPath($path, $name);
1718        list($parentId) = $this->_od_splitPath($namePath);
1719
1720        try {
1721            $properties = array(
1722                'name' => (string)$name,
1723                'folder' => (object)array(),
1724            );
1725
1726            $data = (object)$properties;
1727
1728            $url = self::API_URL . $parentId . '/children';
1729
1730            $curl = $this->_od_prepareCurl($url);
1731
1732            curl_setopt_array($curl, array(
1733                CURLOPT_POST => true,
1734                CURLOPT_POSTFIELDS => json_encode($data),
1735            ));
1736
1737            //create the Folder in the Parent
1738            $result = curl_exec($curl);
1739            curl_close($curl);
1740            $folder = json_decode($result);
1741
1742            return $this->_joinPath($path, $folder->id);
1743        } catch (Exception $e) {
1744            return $this->setError('OneDrive error: ' . $e->getMessage());
1745        }
1746    }
1747
1748    /**
1749     * Create file and return it's path or false on failed.
1750     *
1751     * @param string $path parent dir path
1752     * @param string $name new file name
1753     *
1754     * @return string|bool
1755     * @author Dmitry (dio) Levashov
1756     **/
1757    protected function _mkfile($path, $name)
1758    {
1759        return $this->_save($this->tmpfile(), $path, $name, array());
1760    }
1761
1762    /**
1763     * Create symlink. FTP driver does not support symlinks.
1764     *
1765     * @param string $target link target
1766     * @param string $path   symlink path
1767     *
1768     * @return bool
1769     * @author Dmitry (dio) Levashov
1770     **/
1771    protected function _symlink($target, $path, $name)
1772    {
1773        return false;
1774    }
1775
1776    /**
1777     * Copy file into another file.
1778     *
1779     * @param string $source    source file path
1780     * @param string $targetDir target directory path
1781     * @param string $name      new file name
1782     *
1783     * @return bool
1784     * @author Dmitry (dio) Levashov
1785     **/
1786    protected function _copy($source, $targetDir, $name)
1787    {
1788        $path = $this->_joinPath($targetDir, $name);
1789
1790        try {
1791            //Set the Parent id
1792            list(, $parentId) = $this->_od_splitPath($targetDir);
1793            list(, $itemId) = $this->_od_splitPath($source);
1794
1795            $url = self::API_URL . $itemId . '/copy';
1796
1797            $properties = array(
1798                'name' => (string)$name,
1799            );
1800            if ($parentId === 'root') {
1801                $properties['parentReference'] = (object)array('path' => '/drive/root:');
1802            } else {
1803                $properties['parentReference'] = (object)array('id' => (string)$parentId);
1804            }
1805            $data = (object)$properties;
1806            $curl = $this->_od_prepareCurl($url);
1807            curl_setopt_array($curl, array(
1808                CURLOPT_POST => true,
1809                CURLOPT_HEADER => true,
1810                CURLOPT_HTTPHEADER => array(
1811                    'Content-Type: application/json',
1812                    'Authorization: Bearer ' . $this->token->data->access_token,
1813                    'Prefer: respond-async',
1814                ),
1815                CURLOPT_POSTFIELDS => json_encode($data),
1816            ));
1817            $result = curl_exec($curl);
1818            curl_close($curl);
1819
1820            $res = new stdClass();
1821            if (preg_match('/Location: (.+)/', $result, $m)) {
1822                $monUrl = trim($m[1]);
1823                while ($res) {
1824                    usleep(200000);
1825                    $curl = curl_init($monUrl);
1826                    curl_setopt_array($curl, array(
1827                        CURLOPT_RETURNTRANSFER => true,
1828                        CURLOPT_HTTPHEADER => array(
1829                            'Content-Type: application/json',
1830                        ),
1831                    ));
1832                    $res = json_decode(curl_exec($curl));
1833                    curl_close($curl);
1834                    if (isset($res->status)) {
1835                        if ($res->status === 'completed' || $res->status === 'failed') {
1836                            break;
1837                        }
1838                    } elseif (isset($res->error)) {
1839                        return $this->setError('OneDrive error: ' . $res->error->message);
1840                    }
1841                }
1842            }
1843
1844            if ($res && isset($res->resourceId)) {
1845                if (isset($res->folder) && isset($this->sessionCache['subdirs'])) {
1846                    $this->sessionCache['subdirs'][$targetDir] = true;
1847                }
1848
1849                return $this->_joinPath($targetDir, $res->resourceId);
1850            }
1851
1852            return false;
1853        } catch (Exception $e) {
1854            return $this->setError('OneDrive error: ' . $e->getMessage());
1855        }
1856
1857        return true;
1858    }
1859
1860    /**
1861     * Move file into another parent dir.
1862     * Return new file path or false.
1863     *
1864     * @param string $source source file path
1865     * @param        $targetDir
1866     * @param string $name   file name
1867     *
1868     * @return string|bool
1869     * @author Dmitry (dio) Levashov
1870     */
1871    protected function _move($source, $targetDir, $name)
1872    {
1873        try {
1874            list(, $targetParentId) = $this->_od_splitPath($targetDir);
1875            list($sourceParentId, $itemId, $srcParent) = $this->_od_splitPath($source);
1876
1877            $properties = array(
1878                'name' => (string)$name,
1879            );
1880            if ($targetParentId !== $sourceParentId) {
1881                $properties['parentReference'] = (object)array('id' => (string)$targetParentId);
1882            }
1883
1884            $url = self::API_URL . $itemId;
1885            $data = (object)$properties;
1886
1887            $curl = $this->_od_prepareCurl($url);
1888
1889            curl_setopt_array($curl, array(
1890                CURLOPT_CUSTOMREQUEST => 'PATCH',
1891                CURLOPT_POSTFIELDS => json_encode($data),
1892            ));
1893
1894            $result = json_decode(curl_exec($curl));
1895            curl_close($curl);
1896            if ($result && isset($result->id)) {
1897                return $targetDir . '/' . $result->id;
1898            } else {
1899                return false;
1900            }
1901        } catch (Exception $e) {
1902            return $this->setError('OneDrive error: ' . $e->getMessage());
1903        }
1904
1905        return false;
1906    }
1907
1908    /**
1909     * Remove file.
1910     *
1911     * @param string $path file path
1912     *
1913     * @return bool
1914     * @author Dmitry (dio) Levashov
1915     **/
1916    protected function _unlink($path)
1917    {
1918        $stat = $this->stat($path);
1919        try {
1920            list(, $itemId) = $this->_od_splitPath($path);
1921
1922            $url = self::API_URL . $itemId;
1923
1924            $curl = $this->_od_prepareCurl($url);
1925            curl_setopt_array($curl, array(
1926                CURLOPT_CUSTOMREQUEST => 'DELETE',
1927            ));
1928
1929            //unlink or delete File or Folder in the Parent
1930            $result = curl_exec($curl);
1931            curl_close($curl);
1932        } catch (Exception $e) {
1933            return $this->setError('OneDrive error: ' . $e->getMessage());
1934        }
1935
1936        return true;
1937    }
1938
1939    /**
1940     * Remove dir.
1941     *
1942     * @param string $path dir path
1943     *
1944     * @return bool
1945     * @author Dmitry (dio) Levashov
1946     **/
1947    protected function _rmdir($path)
1948    {
1949        return $this->_unlink($path);
1950    }
1951
1952    /**
1953     * Create new file and write into it from file pointer.
1954     * Return new file path or false on error.
1955     *
1956     * @param resource $fp   file pointer
1957     * @param          $path
1958     * @param string   $name file name
1959     * @param array    $stat file stat (required by some virtual fs)
1960     *
1961     * @return bool|string
1962     * @author Dmitry (dio) Levashov
1963     */
1964    protected function _save($fp, $path, $name, $stat)
1965    {
1966        $itemId = '';
1967        $size = null;
1968        if ($name === '') {
1969            list($parentId, $itemId, $parent) = $this->_od_splitPath($path);
1970        } else {
1971            if ($stat) {
1972                if (isset($stat['name'])) {
1973                    $name = $stat['name'];
1974                }
1975                if (isset($stat['rev']) && strpos($stat['hash'], $this->id) === 0) {
1976                    $itemId = $stat['rev'];
1977                }
1978            }
1979            list(, $parentId) = $this->_od_splitPath($path);
1980            $parent = $path;
1981        }
1982
1983        if ($stat && isset($stat['size'])) {
1984            $size = $stat['size'];
1985        } else {
1986            $stats = fstat($fp);
1987            if (isset($stats[7])) {
1988                $size = $stats[7];
1989            }
1990        }
1991
1992        if ($size > 4194304) {
1993            return $this->_od_uploadSession($fp, $size, $name, $itemId, $parent, $parentId);
1994        }
1995
1996        try {
1997            // for unseekable file pointer
1998            if (!elFinder::isSeekableStream($fp)) {
1999                if ($tfp = tmpfile()) {
2000                    if (stream_copy_to_stream($fp, $tfp, $size? $size : -1) !== false) {
2001                        rewind($tfp);
2002                        $fp = $tfp;
2003                    }
2004                }
2005            }
2006
2007            //Create or Update a file
2008            if ($itemId === '') {
2009                $url = self::API_URL . $parentId . ':/' . rawurlencode($name) . ':/content';
2010            } else {
2011                $url = self::API_URL . $itemId . '/content';
2012            }
2013            $curl = $this->_od_prepareCurl();
2014
2015            $options = array(
2016                CURLOPT_URL => $url,
2017                CURLOPT_PUT => true,
2018                CURLOPT_INFILE => $fp,
2019            );
2020            // Size
2021            if ($size !== null) {
2022                $options[CURLOPT_INFILESIZE] = $size;
2023            }
2024
2025            curl_setopt_array($curl, $options);
2026
2027            //create or update File in the Target
2028            $file = json_decode(curl_exec($curl));
2029            curl_close($curl);
2030
2031            return $this->_joinPath($parent, $file->id);
2032        } catch (Exception $e) {
2033            return $this->setError('OneDrive error: ' . $e->getMessage());
2034        }
2035    }
2036
2037    /**
2038     * Get file contents.
2039     *
2040     * @param string $path file path
2041     *
2042     * @return string|false
2043     * @author Dmitry (dio) Levashov
2044     **/
2045    protected function _getContents($path)
2046    {
2047        $contents = '';
2048
2049        try {
2050            list(, $itemId) = $this->_od_splitPath($path);
2051            $url = self::API_URL . $itemId . '/content';
2052            $contents = $this->_od_createCurl($url, $contents = true);
2053        } catch (Exception $e) {
2054            return $this->setError('OneDrive error: ' . $e->getMessage());
2055        }
2056
2057        return $contents;
2058    }
2059
2060    /**
2061     * Write a string to a file.
2062     *
2063     * @param string $path    file path
2064     * @param string $content new file content
2065     *
2066     * @return bool
2067     * @author Dmitry (dio) Levashov
2068     **/
2069    protected function _filePutContents($path, $content)
2070    {
2071        $res = false;
2072
2073        if ($local = $this->getTempFile($path)) {
2074            if (file_put_contents($local, $content, LOCK_EX) !== false
2075                && ($fp = fopen($local, 'rb'))) {
2076                clearstatcache();
2077                $res = $this->_save($fp, $path, '', array());
2078                fclose($fp);
2079            }
2080            file_exists($local) && unlink($local);
2081        }
2082
2083        return $res;
2084    }
2085
2086    /**
2087     * Detect available archivers.
2088     **/
2089    protected function _checkArchivers()
2090    {
2091        // die('Not yet implemented. (_checkArchivers)');
2092        return array();
2093    }
2094
2095    /**
2096     * chmod implementation.
2097     *
2098     * @return bool
2099     **/
2100    protected function _chmod($path, $mode)
2101    {
2102        return false;
2103    }
2104
2105    /**
2106     * Unpack archive.
2107     *
2108     * @param string $path archive path
2109     * @param array  $arc  archiver command and arguments (same as in $this->archivers)
2110     *
2111     * @return void
2112     * @author Dmitry (dio) Levashov
2113     * @author Alexey Sukhotin
2114     */
2115    protected function _unpack($path, $arc)
2116    {
2117        die('Not yet implemented. (_unpack)');
2118        //return false;
2119    }
2120
2121    /**
2122     * Extract files from archive.
2123     *
2124     * @param string $path archive path
2125     * @param array  $arc  archiver command and arguments (same as in $this->archivers)
2126     *
2127     * @return void
2128     * @author Dmitry (dio) Levashov,
2129     * @author Alexey Sukhotin
2130     */
2131    protected function _extract($path, $arc)
2132    {
2133        die('Not yet implemented. (_extract)');
2134    }
2135
2136    /**
2137     * Create archive and return its path.
2138     *
2139     * @param string $dir   target dir
2140     * @param array  $files files names list
2141     * @param string $name  archive name
2142     * @param array  $arc   archiver options
2143     *
2144     * @return string|bool
2145     * @author Dmitry (dio) Levashov,
2146     * @author Alexey Sukhotin
2147     **/
2148    protected function _archive($dir, $files, $name, $arc)
2149    {
2150        die('Not yet implemented. (_archive)');
2151    }
2152} // END class
2153