1<?php
2
3use League\Flysystem\Filesystem;
4use League\Flysystem\Adapter\Local;
5use League\Flysystem\Cached\CachedAdapter;
6use League\Flysystem\Cached\Storage\Adapter as ACache;
7use Hypweb\Flysystem\GoogleDrive\GoogleDriveAdapter;
8use Hypweb\Flysystem\Cached\Extra\Hasdir;
9use Hypweb\Flysystem\Cached\Extra\DisableEnsureParentDirectories;
10use Hypweb\elFinderFlysystemDriverExt\Driver as ExtDriver;
11
12elFinder::$netDrivers['googledrive'] = 'FlysystemGoogleDriveNetmount';
13
14if (!class_exists('elFinderVolumeFlysystemGoogleDriveCache', false)) {
15    class elFinderVolumeFlysystemGoogleDriveCache extends ACache
16    {
17        use Hasdir;
18        use DisableEnsureParentDirectories;
19    }
20}
21
22class elFinderVolumeFlysystemGoogleDriveNetmount extends ExtDriver
23{
24
25    public function __construct()
26    {
27        parent::__construct();
28
29        $opts = array(
30            'acceptedName' => '#^[^/\\?*:|"<>]*[^./\\?*:|"<>]$#',
31            'rootCssClass' => 'elfinder-navbar-root-googledrive',
32            'gdAlias' => '%s@GDrive',
33            'gdCacheDir' => __DIR__ . '/.tmp',
34            'gdCachePrefix' => 'gd-',
35            'gdCacheExpire' => 600
36        );
37
38        $this->options = array_merge($this->options, $opts);
39    }
40
41    /**
42     * Prepare driver before mount volume.
43     * Return true if volume is ready.
44     *
45     * @return bool
46     **/
47    protected function init()
48    {
49        if (empty($this->options['icon'])) {
50            $this->options['icon'] = true;
51        }
52        if ($res = parent::init()) {
53            if ($this->options['icon'] === true) {
54                unset($this->options['icon']);
55            }
56            // enable command archive
57            $this->options['useRemoteArchive'] = true;
58        }
59        return $res;
60    }
61
62    /**
63     * Prepare
64     * Call from elFinder::netmout() before volume->mount()
65     *
66     * @param $options
67     *
68     * @return Array
69     * @author Naoki Sawada
70     */
71    public function netmountPrepare($options)
72    {
73        if (empty($options['client_id']) && defined('ELFINDER_GOOGLEDRIVE_CLIENTID')) {
74            $options['client_id'] = ELFINDER_GOOGLEDRIVE_CLIENTID;
75        }
76        if (empty($options['client_secret']) && defined('ELFINDER_GOOGLEDRIVE_CLIENTSECRET')) {
77            $options['client_secret'] = ELFINDER_GOOGLEDRIVE_CLIENTSECRET;
78        }
79
80        if (!isset($options['pass'])) {
81            $options['pass'] = '';
82        }
83
84        try {
85            $client = new \Google_Client();
86            $client->setClientId($options['client_id']);
87            $client->setClientSecret($options['client_secret']);
88
89            if ($options['pass'] === 'reauth') {
90                $options['pass'] = '';
91                $this->session->set('GoogleDriveAuthParams', [])->set('GoogleDriveTokens', []);
92            } else if ($options['pass'] === 'googledrive') {
93                $options['pass'] = '';
94            }
95
96            $options = array_merge($this->session->get('GoogleDriveAuthParams', []), $options);
97
98            if (!isset($options['access_token'])) {
99                $options['access_token'] = $this->session->get('GoogleDriveTokens', []);
100                $this->session->remove('GoogleDriveTokens');
101            }
102            $aToken = $options['access_token'];
103
104            $rootObj = $service = null;
105            if ($aToken) {
106                try {
107                    $client->setAccessToken($aToken);
108                    if ($client->isAccessTokenExpired()) {
109                        $aToken = array_merge($aToken, $client->fetchAccessTokenWithRefreshToken());
110                        $client->setAccessToken($aToken);
111                    }
112                    $service = new \Google_Service_Drive($client);
113                    $rootObj = $service->files->get('root');
114
115                    $options['access_token'] = $aToken;
116                    $this->session->set('GoogleDriveAuthParams', $options);
117
118                } catch (Exception $e) {
119                    $aToken = [];
120                    $options['access_token'] = [];
121                    if ($options['user'] !== 'init') {
122                        $this->session->set('GoogleDriveAuthParams', $options);
123                        return array('exit' => true, 'error' => elFinder::ERROR_REAUTH_REQUIRE);
124                    }
125                }
126
127            }
128
129            $itpCare = isset($options['code']);
130            $code = $itpCare? $options['code'] : (isset($_GET['code'])? $_GET['code'] : '');
131            if ($code || $options['user'] === 'init') {
132                if (empty($options['url'])) {
133                    $options['url'] = elFinder::getConnectorUrl();
134                }
135
136                if (isset($options['id'])) {
137                    $callback = $options['url'] . (strpos($options['url'], '?') !== false? '&' : '?') . 'cmd=netmount&protocol=googledrive&host=' . ($options['id'] === 'elfinder'? '1' : $options['id']);
138                    $client->setRedirectUri($callback);
139                }
140
141                if (!$aToken && empty($code)) {
142                    $client->setScopes([Google_Service_Drive::DRIVE]);
143                    if (!empty($options['offline'])) {
144                        $client->setApprovalPrompt('force');
145                        $client->setAccessType('offline');
146                    }
147                    $url = $client->createAuthUrl();
148
149                    $html = '<input id="elf-volumedriver-googledrive-host-btn" class="ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only" value="{msg:btnApprove}" type="button">';
150                    $html .= '<script>
151                        $("#' . $options['id'] . '").elfinder("instance").trigger("netmount", {protocol: "googledrive", mode: "makebtn", url: "' . $url . '"});
152                    </script>';
153                    if (empty($options['pass']) && $options['host'] !== '1') {
154                        $options['pass'] = 'return';
155                        $this->session->set('GoogleDriveAuthParams', $options);
156                        return array('exit' => true, 'body' => $html);
157                    } else {
158                        $out = array(
159                            'node' => $options['id'],
160                            'json' => '{"protocol": "googledrive", "mode": "makebtn", "body" : "' . str_replace($html, '"', '\\"') . '", "error" : "' . elFinder::ERROR_ACCESS_DENIED . '"}',
161                            'bind' => 'netmount'
162                        );
163                        return array('exit' => 'callback', 'out' => $out);
164                    }
165                } else {
166                    if ($code) {
167                        if (!empty($options['id'])) {
168                            $aToken = $client->fetchAccessTokenWithAuthCode($code);
169                            $options['access_token'] = $aToken;
170                            unset($options['code']);
171                            $this->session->set('GoogleDriveTokens', $aToken)->set('GoogleDriveAuthParams', $options);
172                            $out = array(
173                                'node' => $options['id'],
174                                'json' => '{"protocol": "googledrive", "mode": "done", "reset": 1}',
175                                'bind' => 'netmount'
176                            );
177                        } else {
178                            $nodeid = ($_GET['host'] === '1')? 'elfinder' : $_GET['host'];
179                            $out = array(
180                                'node' => $nodeid,
181                                'json' => json_encode(array(
182                                    'protocol' => 'googledrive',
183                                    'host' => $nodeid,
184                                    'mode' => 'redirect',
185                                    'options' => array(
186                                        'id' => $nodeid,
187                                        'code'=> $code
188                                    )
189                                )),
190                                'bind' => 'netmount'
191                            );
192                        }
193                        if (!$itpCare) {
194                            return array('exit' => 'callback', 'out' => $out);
195                        } else {
196                            return array('exit' => true, 'body' => $out['json']);
197                        }
198                    }
199                    $folders = [];
200                    foreach ($service->files->listFiles([
201                        'pageSize' => 1000,
202                        'q' => 'trashed = false and mimeType = "application/vnd.google-apps.folder"'
203                    ]) as $f) {
204                        $folders[$f->getId()] = $f->getName();
205                    }
206                    natcasesort($folders);
207                    $folders = ['root' => $rootObj->getName()] + $folders;
208                    $folders = json_encode($folders);
209                    $json = '{"protocol": "googledrive", "mode": "done", "folders": ' . $folders . '}';
210                    $options['pass'] = 'return';
211                    $html = 'Google.com';
212                    $html .= '<script>
213                        $("#' . $options['id'] . '").elfinder("instance").trigger("netmount", ' . $json . ');
214                    </script>';
215                    $this->session->set('GoogleDriveAuthParams', $options);
216                    return array('exit' => true, 'body' => $html);
217                }
218            }
219        } catch (Exception $e) {
220            $this->session->remove('GoogleDriveAuthParams')->remove('GoogleDriveTokens');
221            if (empty($options['pass'])) {
222                return array('exit' => true, 'body' => '{msg:' . elFinder::ERROR_ACCESS_DENIED . '}' . ' ' . $e->getMessage());
223            } else {
224                return array('exit' => true, 'error' => [elFinder::ERROR_ACCESS_DENIED, $e->getMessage()]);
225            }
226        }
227
228        if (!$aToken) {
229            return array('exit' => true, 'error' => elFinder::ERROR_REAUTH_REQUIRE);
230        }
231
232        if ($options['path'] === '/') {
233            $options['path'] = 'root';
234        }
235
236        try {
237            $file = $service->files->get($options['path']);
238            $options['alias'] = sprintf($this->options['gdAlias'], $file->getName());
239            if (!empty($this->options['netkey'])) {
240                elFinder::$instance->updateNetVolumeOption($this->options['netkey'], 'alias', $this->options['alias']);
241            }
242        } catch (Google_Service_Exception $e) {
243            $err = json_decode($e->getMessage(), true);
244            if (isset($err['error']) && $err['error']['code'] == 404) {
245                return array('exit' => true, 'error' => [elFinder::ERROR_TRGDIR_NOT_FOUND, $options['path']]);
246            } else {
247                return array('exit' => true, 'error' => $e->getMessage());
248            }
249        } catch (Exception $e) {
250            return array('exit' => true, 'error' => $e->getMessage());
251        }
252
253        foreach (['host', 'user', 'pass', 'id', 'offline'] as $key) {
254            unset($options[$key]);
255        }
256
257        return $options;
258    }
259
260    /**
261     * process of on netunmount
262     * Drop table `dropbox` & rm thumbs
263     *
264     * @param $netVolumes
265     * @param $key
266     *
267     * @return bool
268     * @internal param array $options
269     */
270    public function netunmount($netVolumes, $key)
271    {
272        $cache = $this->options['gdCacheDir'] . DIRECTORY_SEPARATOR . $this->options['gdCachePrefix'] . $this->netMountKey;
273        if (file_exists($cache) && is_writeable($cache)) {
274            unlink($cache);
275        }
276        if ($tmbs = glob($this->tmbPath . DIRECTORY_SEPARATOR . $this->netMountKey . '*')) {
277            foreach ($tmbs as $file) {
278                unlink($file);
279            }
280        }
281        return true;
282    }
283
284    /**
285     * "Mount" volume.
286     * Return true if volume available for read or write,
287     * false - otherwise
288     *
289     * @param array $opts
290     *
291     * @return bool
292     * @author Naoki Sawada
293     */
294    public function mount(array $opts)
295    {
296        $creds = null;
297        if (isset($opts['access_token'])) {
298            $this->netMountKey = md5(join('-', array('googledrive', $opts['path'], (isset($opts['access_token']['refresh_token']) ? $opts['access_token']['refresh_token'] : $opts['access_token']['access_token']))));
299        }
300
301        $client = new \Google_Client();
302        $client->setClientId($opts['client_id']);
303        $client->setClientSecret($opts['client_secret']);
304
305        if (!empty($opts['access_token'])) {
306            $client->setAccessToken($opts['access_token']);
307        }
308        if ($this->needOnline && $client->isAccessTokenExpired()) {
309            try {
310                $creds = $client->fetchAccessTokenWithRefreshToken();
311            } catch (LogicException $e) {
312                $this->session->remove('GoogleDriveAuthParams');
313                throw $e;
314            }
315        }
316
317        $service = new \Google_Service_Drive($client);
318
319        // If path is not set, use the root
320        if (!isset($opts['path']) || $opts['path'] === '') {
321            $opts['path'] = 'root';
322        }
323
324        $googleDrive = new GoogleDriveAdapter($service, $opts['path'], ['useHasDir' => true]);
325
326        $opts['fscache'] = null;
327        if ($this->options['gdCacheDir'] && is_writeable($this->options['gdCacheDir'])) {
328            if ($this->options['gdCacheExpire']) {
329                $opts['fscache'] = new elFinderVolumeFlysystemGoogleDriveCache(new Local($this->options['gdCacheDir']), $this->options['gdCachePrefix'] . $this->netMountKey, $this->options['gdCacheExpire']);
330            }
331        }
332        if ($opts['fscache']) {
333            $filesystem = new Filesystem(new CachedAdapter($googleDrive, $opts['fscache']));
334        } else {
335            $filesystem = new Filesystem($googleDrive);
336        }
337
338        $opts['driver'] = 'FlysystemExt';
339        $opts['filesystem'] = $filesystem;
340        $opts['separator'] = '/';
341        $opts['checkSubfolders'] = true;
342        if (!isset($opts['alias'])) {
343            $opts['alias'] = 'GoogleDrive';
344        }
345
346        if ($res = parent::mount($opts)) {
347            // update access_token of session data
348            if ($creds) {
349                $netVolumes = $this->session->get('netvolume');
350                $netVolumes[$this->netMountKey]['access_token'] = array_merge($netVolumes[$this->netMountKey]['access_token'], $creds);
351                $this->session->set('netvolume', $netVolumes);
352            }
353        }
354
355        return $res;
356    }
357
358    /**
359     * @inheritdoc
360     */
361    protected function tmbname($stat)
362    {
363        return $this->netMountKey . substr(substr($stat['hash'], strlen($this->id)), -38) . $stat['ts'] . '.png';
364    }
365
366    /**
367     * Return debug info for client.
368     *
369     * @return array
370     **/
371    public function debug()
372    {
373        $res = parent::debug();
374        if (!empty($this->options['netkey']) && empty($this->options['refresh_token']) && $this->options['access_token'] && isset($this->options['access_token']['refresh_token'])) {
375            $res['refresh_token'] = $this->options['access_token']['refresh_token'];
376        }
377
378        return $res;
379    }
380}
381