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