elFinderVolumeDropbox2.class.php 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493
  1. <?php
  2. use \Kunnu\Dropbox\DropboxApp;
  3. use \Kunnu\Dropbox\Dropbox;
  4. use \Kunnu\Dropbox\DropboxFile;
  5. use \Kunnu\Dropbox\Models\FolderMetadata;
  6. use \Kunnu\Dropbox\Models\FileMetadata;
  7. use \Kunnu\Dropbox\Exceptions\DropboxClientException;
  8. elFinder::$netDrivers['dropbox2'] = 'Dropbox2';
  9. /**
  10. * Simple elFinder driver for Dropbox
  11. * kunalvarma05/dropbox-php-sdk:0.1.5 or above.
  12. *
  13. * @author Naoki Sawada
  14. **/
  15. class elFinderVolumeDropbox2 extends elFinderVolumeDriver
  16. {
  17. /**
  18. * Driver id
  19. * Must be started from letter and contains [a-z0-9]
  20. * Used as part of volume id.
  21. *
  22. * @var string
  23. **/
  24. protected $driverId = 'db';
  25. /**
  26. * Dropbox service object.
  27. *
  28. * @var object
  29. **/
  30. protected $service = null;
  31. /**
  32. * Fetch options.
  33. *
  34. * @var string
  35. */
  36. private $FETCH_OPTIONS = [];
  37. /**
  38. * Directory for tmp files
  39. * If not set driver will try to use tmbDir as tmpDir.
  40. *
  41. * @var string
  42. **/
  43. protected $tmp = '';
  44. /**
  45. * Net mount key.
  46. *
  47. * @var string
  48. **/
  49. public $netMountKey = '';
  50. /**
  51. * Constructor
  52. * Extend options with required fields.
  53. *
  54. * @author Naoki Sawada
  55. **/
  56. public function __construct()
  57. {
  58. $opts = [
  59. 'app_key' => '',
  60. 'app_secret' => '',
  61. 'access_token' => '',
  62. 'aliasFormat' => '%s@Dropbox',
  63. 'path' => '/',
  64. 'separator' => '/',
  65. 'acceptedName' => '#^[^/\\?*:|"<>]*[^./\\?*:|"<>]$#',
  66. 'rootCssClass' => 'elfinder-navbar-root-dropbox',
  67. 'publishPermission' => [
  68. 'requested_visibility' => 'public',
  69. //'link_password' => '',
  70. //'expires' => '',
  71. ],
  72. 'getThumbSize' => 'medium', // Available sizes: 'thumb', 'small', 'medium', 'large', 'huge'
  73. ];
  74. $this->options = array_merge($this->options, $opts);
  75. $this->options['mimeDetect'] = 'internal';
  76. }
  77. /*********************************************************************/
  78. /* ORIGINAL FUNCTIONS */
  79. /*********************************************************************/
  80. /**
  81. * Get Parent ID, Item ID, Parent Path as an array from path.
  82. *
  83. * @param string $path
  84. *
  85. * @return array
  86. */
  87. protected function _db_splitPath($path)
  88. {
  89. $path = trim($path, '/');
  90. if ($path === '') {
  91. $dirname = '/';
  92. $basename = '';
  93. } else {
  94. $pos = strrpos($path, '/');
  95. if ($pos === false) {
  96. $dirname = '/';
  97. $basename = $path;
  98. } else {
  99. $dirname = '/'.substr($path, 0, $pos);
  100. $basename = substr($path, $pos + 1);
  101. }
  102. }
  103. return [$dirname, $basename];
  104. }
  105. /**
  106. * Get dat(Dropbox metadata) from Dropbox.
  107. *
  108. * @param string $path
  109. *
  110. * @return object Dropbox metadata
  111. */
  112. private function _db_getFile($path, $fields = '')
  113. {
  114. if ($path === '/') {
  115. return true;
  116. }
  117. $res = false;
  118. try {
  119. $file = $this->service->getMetadata($path, $this->FETCH_OPTIONS);
  120. if ($file instanceof FolderMetadata || $file instanceof FileMetadata) {
  121. $res = $file;
  122. }
  123. return $res;
  124. } catch (DropboxClientException $e) {
  125. return false;
  126. }
  127. }
  128. /**
  129. * Parse line from Dropbox metadata output and return file stat (array).
  130. *
  131. * @param string $raw line from ftp_rawlist() output
  132. *
  133. * @return array
  134. *
  135. * @author Naoki Sawada
  136. **/
  137. protected function _db_parseRaw($raw)
  138. {
  139. $stat = [];
  140. $isFolder = false;
  141. if ($raw === true) {
  142. // root folder
  143. $isFolder = true;
  144. $stat['name'] = '';
  145. $stat['iid'] = '0';
  146. }
  147. $data = [];
  148. if (is_object($raw)) {
  149. $isFolder = $raw instanceof FolderMetadata;
  150. $data = $raw->getData();
  151. } elseif (is_array($raw)) {
  152. $isFolder = $raw['.tag'] === 'folder';
  153. $data = $raw;
  154. }
  155. if (isset($data['path_lower'])) {
  156. $stat['path'] = $data['path_lower'];
  157. }
  158. if (isset($data['name'])) {
  159. $stat['name'] = $data['name'];
  160. }
  161. if (isset($data['id'])) {
  162. $stat['iid'] = substr($data['id'], 3);
  163. }
  164. if ($isFolder) {
  165. $stat['mime'] = 'directory';
  166. $stat['size'] = 0;
  167. $stat['ts'] = 0;
  168. $stat['dirs'] = -1;
  169. } else {
  170. $stat['size'] = isset($data['size']) ? (int) $data['size'] : 0;
  171. if (isset($data['server_modified'])) {
  172. $stat['ts'] = strtotime($data['server_modified']);
  173. } elseif (isset($data['client_modified'])) {
  174. $stat['ts'] = strtotime($data['client_modified']);
  175. } else {
  176. $stat['ts'] = 0;
  177. }
  178. $stat['url'] = '1';
  179. }
  180. return $stat;
  181. }
  182. /**
  183. * Get thumbnail from Dropbox.
  184. *
  185. * @param string $path
  186. * @param string $size
  187. *
  188. * @return string | boolean
  189. */
  190. protected function _db_getThumbnail($path)
  191. {
  192. try {
  193. return $this->service->getThumbnail($path, $this->options['getThumbSize'])->getContents();
  194. } catch (DropboxClientException $e) {
  195. return false;
  196. }
  197. }
  198. /**
  199. * Join dir name and file name(display name) and retur full path.
  200. *
  201. * @param unknown $dir
  202. * @param unknown $displayName
  203. *
  204. * @return string
  205. */
  206. protected function _db_joinName($dir, $displayName)
  207. {
  208. return rtrim($dir, '/').'/'.$displayName;
  209. }
  210. /**
  211. * Get OAuth2 access token form OAuth1 tokens.
  212. *
  213. * @param string $app_key
  214. * @param string $app_secret
  215. * @param string $oauth1_token
  216. * @param string $oauth1_secret
  217. *
  218. * @return string|false
  219. */
  220. public static function getTokenFromOauth1($app_key, $app_secret, $oauth1_token, $oauth1_secret)
  221. {
  222. $data = [
  223. 'oauth1_token' => $oauth1_token,
  224. 'oauth1_token_secret' => $oauth1_secret,
  225. ];
  226. $auth = base64_encode($app_key.':'.$app_secret);
  227. $ch = curl_init('https://api.dropboxapi.com/2/auth/token/from_oauth1');
  228. curl_setopt($ch, CURLOPT_POST, true);
  229. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  230. curl_setopt($ch, CURLOPT_HTTPHEADER, [
  231. 'Content-Type: application/json',
  232. 'Authorization: Basic '.$auth,
  233. ]);
  234. curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
  235. $result = curl_exec($ch);
  236. curl_close($ch);
  237. $res = $result ? json_decode($result, true) : [];
  238. return isset($res['oauth2_token']) ? $res['oauth2_token'] : false;
  239. }
  240. /*********************************************************************/
  241. /* EXTENDED FUNCTIONS */
  242. /*********************************************************************/
  243. /**
  244. * Prepare
  245. * Call from elFinder::netmout() before volume->mount().
  246. *
  247. * @return array
  248. *
  249. * @author Naoki Sawada
  250. **/
  251. public function netmountPrepare($options)
  252. {
  253. if (empty($options['app_key']) && defined('ELFINDER_DROPBOX_APPKEY')) {
  254. $options['app_key'] = ELFINDER_DROPBOX_APPKEY;
  255. }
  256. if (empty($options['app_secret']) && defined('ELFINDER_DROPBOX_APPSECRET')) {
  257. $options['app_secret'] = ELFINDER_DROPBOX_APPSECRET;
  258. }
  259. if (!isset($options['pass'])) {
  260. $options['pass'] = '';
  261. }
  262. try {
  263. $this->session->start();
  264. $app = new DropboxApp($options['app_key'], $options['app_secret']);
  265. $dropbox = new Dropbox($app);
  266. $authHelper = $dropbox->getAuthHelper();
  267. if ($options['pass'] === 'reauth') {
  268. $options['pass'] = '';
  269. $this->session->set('Dropbox2AuthParams', [])->set('Dropbox2Tokens', []);
  270. } elseif ($options['pass'] === 'dropbox2') {
  271. $options['pass'] = '';
  272. }
  273. $options = array_merge($this->session->get('Dropbox2AuthParams', []), $options);
  274. if (!isset($options['tokens'])) {
  275. $options['tokens'] = $this->session->get('Dropbox2Tokens', []);
  276. $this->session->remove('Dropbox2Tokens');
  277. }
  278. $aToken = $options['tokens'];
  279. if (!is_array($aToken) || !isset($aToken['access_token'])) {
  280. $aToken = [];
  281. }
  282. $rootObj = $service = null;
  283. if ($aToken) {
  284. try {
  285. $dropbox->setAccessToken($aToken['access_token']);
  286. $this->session->set('Dropbox2AuthParams', $options);
  287. } catch (DropboxClientException $e) {
  288. $aToken = [];
  289. $options['tokens'] = [];
  290. if ($options['user'] !== 'init') {
  291. $this->session->set('Dropbox2AuthParams', $options);
  292. return ['exit' => true, 'error' => elFinder::ERROR_REAUTH_REQUIRE];
  293. }
  294. }
  295. }
  296. if ($options['user'] === 'init') {
  297. if (empty($options['url'])) {
  298. $options['url'] = elFinder::getConnectorUrl();
  299. }
  300. $callback = $options['url']
  301. .'?cmd=netmount&protocol=dropbox2&host=1';
  302. if (!$aToken && empty($_GET['code'])) {
  303. $url = $authHelper->getAuthUrl($callback);
  304. $html = '<input id="elf-volumedriver-dropbox2-host-btn" class="ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only" value="{msg:btnApprove}" type="button" onclick="window.open(\''.$url.'\')">';
  305. $html .= '<script>
  306. $("#'.$options['id'].'").elfinder("instance").trigger("netmount", {protocol: "dropbox2", mode: "makebtn"});
  307. </script>';
  308. if (empty($options['pass']) && $options['host'] !== '1') {
  309. $options['pass'] = 'return';
  310. $this->session->set('Dropbox2AuthParams', $options);
  311. return ['exit' => true, 'body' => $html];
  312. } else {
  313. $out = [
  314. 'node' => $options['id'],
  315. 'json' => '{"protocol": "dropbox2", "mode": "makebtn", "body" : "'.str_replace($html, '"', '\\"').'", "error" : "'.elFinder::ERROR_ACCESS_DENIED.'"}',
  316. 'bind' => 'netmount',
  317. ];
  318. return ['exit' => 'callback', 'out' => $out];
  319. }
  320. } else {
  321. if (!empty($_GET['code']) && isset($_GET['state'])) {
  322. $tokenObj = $authHelper->getAccessToken($_GET['code'], $_GET['state'], $callback);
  323. $options['tokens'] = [
  324. 'access_token' => $tokenObj->getToken(),
  325. 'uid' => $tokenObj->getUid(),
  326. ];
  327. $this->session->set('Dropbox2Tokens', $options['tokens'])->set('Dropbox2AuthParams', $options);
  328. $out = [
  329. 'node' => $options['id'],
  330. 'json' => '{"protocol": "dropbox2", "mode": "done", "reset": 1}',
  331. 'bind' => 'netmount',
  332. ];
  333. return ['exit' => 'callback', 'out' => $out];
  334. }
  335. $path = $options['path'];
  336. $folders = [];
  337. $listFolderContents = $dropbox->listFolder($path);
  338. $items = $listFolderContents->getItems();
  339. foreach ($items as $item) {
  340. $data = $item->getData();
  341. if ($data['.tag'] === 'folder') {
  342. $folders[$data['path_lower']] = $data['name'];
  343. }
  344. }
  345. natcasesort($folders);
  346. if ($options['pass'] === 'folders') {
  347. return ['exit' => true, 'folders' => $folders];
  348. }
  349. $folders = ['/' => '/'] + $folders;
  350. $folders = json_encode($folders);
  351. $json = '{"protocol": "dropbox2", "mode": "done", "folders": '.$folders.'}';
  352. $options['pass'] = 'return';
  353. $html = 'Dropbox.com';
  354. $html .= '<script>
  355. $("#'.$options['id'].'").elfinder("instance").trigger("netmount", '.$json.');
  356. </script>';
  357. $this->session->set('Dropbox2AuthParams', $options);
  358. return ['exit' => true, 'body' => $html];
  359. }
  360. }
  361. } catch (DropboxClientException $e) {
  362. $this->session->remove('Dropbox2AuthParams')->remove('Dropbox2Tokens');
  363. if (empty($options['pass'])) {
  364. return ['exit' => true, 'body' => '{msg:'.elFinder::ERROR_ACCESS_DENIED.'}'.' '.$e->getMessage()];
  365. } else {
  366. return ['exit' => true, 'error' => [elFinder::ERROR_ACCESS_DENIED, $e->getMessage()]];
  367. }
  368. }
  369. if (!$aToken) {
  370. return ['exit' => true, 'error' => elFinder::ERROR_REAUTH_REQUIRE];
  371. }
  372. if ($options['path'] === 'root') {
  373. $options['path'] = '/';
  374. }
  375. try {
  376. if ($options['path'] !== '/') {
  377. $file = $dropbox->getMetadata($options['path']);
  378. $name = $file->getName();
  379. } else {
  380. $name = 'root';
  381. }
  382. $options['alias'] = sprintf($this->options['aliasFormat'], $name);
  383. } catch (DropboxClientException $e) {
  384. return ['exit' => true, 'error' => $e->getMessage()];
  385. }
  386. foreach (['host', 'user', 'pass', 'id', 'offline'] as $key) {
  387. unset($options[$key]);
  388. }
  389. return $options;
  390. }
  391. /**
  392. * process of on netunmount
  393. * Drop `Dropbox` & rm thumbs.
  394. *
  395. * @param array $options
  396. *
  397. * @return bool
  398. */
  399. public function netunmount($netVolumes, $key)
  400. {
  401. if ($tmbs = glob(rtrim($this->options['tmbPath'], '\\/').DIRECTORY_SEPARATOR.$this->driverId.'_'.$this->options['tokens']['uid'].'*.png')) {
  402. foreach ($tmbs as $file) {
  403. unlink($file);
  404. }
  405. }
  406. return true;
  407. }
  408. /*********************************************************************/
  409. /* INIT AND CONFIGURE */
  410. /*********************************************************************/
  411. /**
  412. * Prepare FTP connection
  413. * Connect to remote server and check if credentials are correct, if so, store the connection id in $ftp_conn.
  414. *
  415. * @return bool
  416. *
  417. * @author Naoki Sawada
  418. **/
  419. protected function init()
  420. {
  421. if (empty($options['app_key'])) {
  422. if (defined('ELFINDER_DROPBOX_APPKEY') && ELFINDER_DROPBOX_APPKEY) {
  423. $this->options['app_key'] = ELFINDER_DROPBOX_APPKEY;
  424. } else {
  425. return $this->setError('Required option "app_key" is undefined.');
  426. }
  427. }
  428. if (empty($options['app_secret'])) {
  429. if (defined('ELFINDER_DROPBOX_APPSECRET') && ELFINDER_DROPBOX_APPSECRET) {
  430. $this->options['app_secret'] = ELFINDER_DROPBOX_APPSECRET;
  431. } else {
  432. return $this->setError('Required option "app_secret" is undefined.');
  433. }
  434. }
  435. if (isset($this->options['tokens']) && is_array($this->options['tokens']) && !empty($this->options['tokens']['access_token'])) {
  436. $this->options['access_token'] = $this->options['tokens']['access_token'];
  437. }
  438. if (!$this->options['access_token']) {
  439. return $this->setError('Required option "access_token" or "refresh_token" is undefined.');
  440. }
  441. try {
  442. // make net mount key for network mount
  443. $aToken = $this->options['access_token'];
  444. $this->netMountKey = md5($aToken.'-'.$this->options['path']);
  445. $errors = [];
  446. if (!$this->service) {
  447. $app = new DropboxApp($this->options['app_key'], $this->options['app_secret'], $aToken);
  448. $this->service = new Dropbox($app);
  449. }
  450. } catch (DropboxClientException $e) {
  451. $errors[] = 'Dropbox error: '.$e->getMessage();
  452. } catch (Exception $e) {
  453. $errors[] = $e->getMessage();
  454. }
  455. if (!$this->service) {
  456. $errors[] = 'Dropbox Service could not be loaded.';
  457. return $this->setError($errors);
  458. }
  459. // normalize root path
  460. $this->options['path'] = strtolower($this->options['path']);
  461. if ($this->options['path'] == 'root') {
  462. $this->options['path'] = '/';
  463. }
  464. $this->root = $this->options['path'] = $this->_normpath($this->options['path']);
  465. if (empty($this->options['alias'])) {
  466. $this->options['alias'] = sprintf($this->options['aliasFormat'], ($this->options['path'] === '/') ? 'root' : $this->_basename($this->options['path']));
  467. }
  468. $this->rootName = $this->options['alias'];
  469. if (!empty($this->options['tmpPath'])) {
  470. if ((is_dir($this->options['tmpPath']) || mkdir($this->options['tmpPath'])) && is_writable($this->options['tmpPath'])) {
  471. $this->tmp = $this->options['tmpPath'];
  472. }
  473. }
  474. if (!$this->tmp && ($tmp = elFinder::getStaticVar('commonTempPath'))) {
  475. $this->tmp = $tmp;
  476. }
  477. // This driver dose not support `syncChkAsTs`
  478. $this->options['syncChkAsTs'] = false;
  479. // 'lsPlSleep' minmum 10 sec
  480. $this->options['lsPlSleep'] = max(10, $this->options['lsPlSleep']);
  481. // enable command archive
  482. $this->options['useRemoteArchive'] = true;
  483. return true;
  484. }
  485. /**
  486. * Configure after successfull mount.
  487. *
  488. * @author Naoki Sawada
  489. **/
  490. protected function configure()
  491. {
  492. parent::configure();
  493. // fallback of $this->tmp
  494. if (!$this->tmp && $this->tmbPathWritable) {
  495. $this->tmp = $this->tmbPath;
  496. }
  497. if ($this->isMyReload()) {
  498. //$this->_db_getDirectoryData(false);
  499. }
  500. }
  501. /*********************************************************************/
  502. /* FS API */
  503. /*********************************************************************/
  504. /**
  505. * Close opened connection.
  506. **/
  507. public function umount()
  508. {
  509. }
  510. /**
  511. * Cache dir contents.
  512. *
  513. * @param string $path dir path
  514. *
  515. * @author Naoki Sawada
  516. **/
  517. protected function cacheDir($path)
  518. {
  519. $this->dirsCache[$path] = [];
  520. $hasDir = false;
  521. $res = $this->service->listFolder($path, $this->FETCH_OPTIONS);
  522. if ($res) {
  523. $items = $res->getItems()->all();
  524. foreach ($items as $raw) {
  525. if ($stat = $this->_db_parseRaw($raw)) {
  526. $mountPath = $this->_joinPath($path, $stat['name']);
  527. $stat = $this->updateCache($mountPath, $stat);
  528. if (empty($stat['hidden']) && $path !== $mountPath) {
  529. if (!$hasDir && $stat['mime'] === 'directory') {
  530. $hasDir = true;
  531. }
  532. $this->dirsCache[$path][] = $mountPath;
  533. }
  534. }
  535. }
  536. }
  537. if (isset($this->sessionCache['subdirs'])) {
  538. $this->sessionCache['subdirs'][$path] = $hasDir;
  539. }
  540. return $this->dirsCache[$path];
  541. }
  542. /**
  543. * Recursive files search.
  544. *
  545. * @param string $path dir path
  546. * @param string $q search string
  547. * @param array $mimes
  548. *
  549. * @return array
  550. *
  551. * @author Naoki Sawada
  552. **/
  553. protected function doSearch($path, $q, $mimes)
  554. {
  555. if ($mimes) {
  556. return parent::doSearch($path, $q, $mimes);
  557. }
  558. $timeout = $this->options['searchTimeout'] ? $this->searchStart + $this->options['searchTimeout'] : 0;
  559. $searchRes = $this->service->search($path, $q, ['start' => 0, 'max_results' => 1000]);
  560. $items = $searchRes->getItems();
  561. $more = $searchRes->hasMoreItems();
  562. while ($more) {
  563. if ($timeout && $timeout < time()) {
  564. $this->setError(elFinder::ERROR_SEARCH_TIMEOUT, $this->_path($path));
  565. break;
  566. }
  567. $searchRes = $this->service->search($path, $q, ['start' => $searchRes->getCursor(), 'max_results' => 1000]);
  568. $more = $searchRes->hasMoreItems();
  569. $items = $items->merge($searchRes->getItems());
  570. }
  571. $result = [];
  572. foreach ($items as $raw) {
  573. if ($stat = $this->_db_parseRaw($raw->getMetadata())) {
  574. $stat = $this->updateCache($stat['path'], $stat);
  575. if (empty($stat['hidden'])) {
  576. $result[] = $stat;
  577. }
  578. }
  579. }
  580. return $result;
  581. }
  582. /**
  583. * Copy file/recursive copy dir only in current volume.
  584. * Return new file path or false.
  585. *
  586. * @param string $src source path
  587. * @param string $dst destination dir path
  588. * @param string $name new file name (optionaly)
  589. *
  590. * @return string|false
  591. *
  592. * @author Naoki Sawada
  593. **/
  594. protected function copy($src, $dst, $name)
  595. {
  596. $srcStat = $this->stat($src);
  597. $target = $this->_joinPath($dst, $name);
  598. $tgtStat = $this->stat($target);
  599. if ($tgtStat) {
  600. if ($srcStat['mime'] === 'directory') {
  601. return parent::copy($src, $dst, $name);
  602. } else {
  603. $this->_unlink($target);
  604. }
  605. }
  606. $this->clearcache();
  607. if ($res = $this->_copy($src, $dst, $name)) {
  608. $this->added[] = $this->stat($target);
  609. $res = $target;
  610. }
  611. return $res;
  612. }
  613. /**
  614. * Remove file/ recursive remove dir.
  615. *
  616. * @param string $path file path
  617. * @param bool $force try to remove even if file locked
  618. *
  619. * @return bool
  620. *
  621. * @author Naoki Sawada
  622. **/
  623. protected function remove($path, $force = false, $recursive = false)
  624. {
  625. $stat = $this->stat($path);
  626. $stat['realpath'] = $path;
  627. $this->rmTmb($stat);
  628. $this->clearcache();
  629. if (empty($stat)) {
  630. return $this->setError(elFinder::ERROR_RM, $this->_path($path), elFinder::ERROR_FILE_NOT_FOUND);
  631. }
  632. if (!$force && !empty($stat['locked'])) {
  633. return $this->setError(elFinder::ERROR_LOCKED, $this->_path($path));
  634. }
  635. if ($stat['mime'] == 'directory') {
  636. if (!$recursive && !$this->_rmdir($path)) {
  637. return $this->setError(elFinder::ERROR_RM, $this->_path($path));
  638. }
  639. } else {
  640. if (!$recursive && !$this->_unlink($path)) {
  641. return $this->setError(elFinder::ERROR_RM, $this->_path($path));
  642. }
  643. }
  644. $this->removed[] = $stat;
  645. return true;
  646. }
  647. /**
  648. * Create thumnbnail and return it's URL on success.
  649. *
  650. * @param string $path file path
  651. * @param string $mime file mime type
  652. *
  653. * @return string|false
  654. *
  655. * @author Naoki Sawada
  656. **/
  657. protected function createTmb($path, $stat)
  658. {
  659. if (!$stat || !$this->canCreateTmb($path, $stat)) {
  660. return false;
  661. }
  662. $name = $this->tmbname($stat);
  663. $tmb = $this->tmbPath.DIRECTORY_SEPARATOR.$name;
  664. // copy image into tmbPath so some drivers does not store files on local fs
  665. if (!$data = $this->_db_getThumbnail($path)) {
  666. return false;
  667. }
  668. if (!file_put_contents($tmb, $data)) {
  669. return false;
  670. }
  671. $result = false;
  672. $tmbSize = $this->tmbSize;
  673. if (($s = getimagesize($tmb)) == false) {
  674. return false;
  675. }
  676. /* If image smaller or equal thumbnail size - just fitting to thumbnail square */
  677. if ($s[0] <= $tmbSize && $s[1] <= $tmbSize) {
  678. $result = $this->imgSquareFit($tmb, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png');
  679. } else {
  680. if ($this->options['tmbCrop']) {
  681. /* Resize and crop if image bigger than thumbnail */
  682. if (!(($s[0] > $tmbSize && $s[1] <= $tmbSize) || ($s[0] <= $tmbSize && $s[1] > $tmbSize)) || ($s[0] > $tmbSize && $s[1] > $tmbSize)) {
  683. $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, false, 'png');
  684. }
  685. if (($s = getimagesize($tmb)) != false) {
  686. $x = $s[0] > $tmbSize ? intval(($s[0] - $tmbSize) / 2) : 0;
  687. $y = $s[1] > $tmbSize ? intval(($s[1] - $tmbSize) / 2) : 0;
  688. $result = $this->imgCrop($tmb, $tmbSize, $tmbSize, $x, $y, 'png');
  689. }
  690. } else {
  691. $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, true, 'png');
  692. }
  693. $result = $this->imgSquareFit($tmb, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png');
  694. }
  695. if (!$result) {
  696. unlink($tmb);
  697. return false;
  698. }
  699. return $name;
  700. }
  701. /**
  702. * Return thumbnail file name for required file.
  703. *
  704. * @param array $stat file stat
  705. *
  706. * @return string
  707. *
  708. * @author Naoki Sawada
  709. **/
  710. protected function tmbname($stat)
  711. {
  712. $name = $this->driverId.'_';
  713. if (isset($this->options['tokens']) && is_array($this->options['tokens'])) {
  714. $name .= $this->options['tokens']['uid'];
  715. }
  716. return $name.md5($stat['iid']).$stat['ts'].'.png';
  717. }
  718. /**
  719. * Return content URL (for netmout volume driver)
  720. * If file.url == 1 requests from JavaScript client with XHR.
  721. *
  722. * @param string $hash file hash
  723. * @param array $options options array
  724. *
  725. * @return bool|string
  726. *
  727. * @author Naoki Sawada
  728. */
  729. public function getContentUrl($hash, $options = [])
  730. {
  731. if (!empty($options['temporary'])) {
  732. // try make temporary file
  733. $url = parent::getContentUrl($hash, $options);
  734. if ($url) {
  735. return $url;
  736. }
  737. }
  738. $file = $this->file($hash);
  739. if (($file = $this->file($hash)) !== false && (!$file['url'] || $file['url'] == 1)) {
  740. $path = $this->decode($hash);
  741. $url = '';
  742. try {
  743. $res = $this->service->postToAPI('/sharing/list_shared_links', ['path' => $path, 'direct_only' => true])->getDecodedBody();
  744. if ($res && !empty($res['links'])) {
  745. foreach ($res['links'] as $link) {
  746. if (isset($link['link_permissions'])
  747. && isset($link['link_permissions']['requested_visibility'])
  748. && $link['link_permissions']['requested_visibility']['.tag'] === $this->options['publishPermission']['requested_visibility']) {
  749. $url = $link['url'];
  750. break;
  751. }
  752. }
  753. }
  754. if (!$url) {
  755. $res = $this->service->postToAPI('/sharing/create_shared_link_with_settings', ['path' => $path, 'settings' => $this->options['publishPermission']])->getDecodedBody();
  756. if (isset($res['url'])) {
  757. $url = $res['url'];
  758. }
  759. }
  760. if ($url) {
  761. $url = str_replace('www.dropbox.com', 'dl.dropboxusercontent.com', $url);
  762. $url = str_replace('?dl=0', '', $url);
  763. return $url;
  764. }
  765. } catch (DropboxClientException $e) {
  766. return $this->$this->setError('Dropbox error: '.$e->getMessage());
  767. }
  768. }
  769. return false;
  770. }
  771. /**
  772. * Return debug info for client.
  773. *
  774. * @return array
  775. **/
  776. public function debug()
  777. {
  778. $res = parent::debug();
  779. if (isset($this->options['tokens']) && !empty($this->options['tokens']['uid'])) {
  780. $res['Dropbox uid'] = $this->options['tokens']['uid'];
  781. $res['access_token'] = $this->options['tokens']['access_token'];
  782. }
  783. return $res;
  784. }
  785. /*********************** paths/urls *************************/
  786. /**
  787. * Return parent directory path.
  788. *
  789. * @param string $path file path
  790. *
  791. * @return string
  792. *
  793. * @author Naoki Sawada
  794. **/
  795. protected function _dirname($path)
  796. {
  797. list($dirname) = $this->_db_splitPath($path);
  798. return $dirname;
  799. }
  800. /**
  801. * Return file name.
  802. *
  803. * @param string $path file path
  804. *
  805. * @return string
  806. *
  807. * @author Naoki Sawada
  808. **/
  809. protected function _basename($path)
  810. {
  811. list(, $basename) = $this->_db_splitPath($path);
  812. return $basename;
  813. }
  814. /**
  815. * Join dir name and file name and retur full path.
  816. *
  817. * @param string $dir
  818. * @param string $name
  819. *
  820. * @return string
  821. *
  822. * @author Dmitry (dio) Levashov
  823. **/
  824. protected function _joinPath($dir, $name)
  825. {
  826. return rtrim($dir, '/').'/'.strtolower($name);
  827. }
  828. /**
  829. * Return normalized path, this works the same as os.path.normpath() in Python.
  830. *
  831. * @param string $path path
  832. *
  833. * @return string
  834. *
  835. * @author Naoki Sawada
  836. **/
  837. protected function _normpath($path)
  838. {
  839. return '/'.ltrim($path, '/');
  840. }
  841. /**
  842. * Return file path related to root dir.
  843. *
  844. * @param string $path file path
  845. *
  846. * @return string
  847. *
  848. * @author Dmitry (dio) Levashov
  849. **/
  850. protected function _relpath($path)
  851. {
  852. if ($path === $this->root) {
  853. return '';
  854. } else {
  855. return ltrim(substr($path, strlen($this->root)), '/');
  856. }
  857. }
  858. /**
  859. * Convert path related to root dir into real path.
  860. *
  861. * @param string $path file path
  862. *
  863. * @return string
  864. *
  865. * @author Naoki Sawada
  866. **/
  867. protected function _abspath($path)
  868. {
  869. if ($path === '/') {
  870. return $this->root;
  871. } else {
  872. return $this->_joinPath($this->root, $path);
  873. }
  874. }
  875. /**
  876. * Return fake path started from root dir.
  877. *
  878. * @param string $path file path
  879. *
  880. * @return string
  881. *
  882. * @author Naoki Sawada
  883. **/
  884. protected function _path($path)
  885. {
  886. $path = $this->_normpath(substr($path, strlen($this->root)));
  887. return $path;
  888. }
  889. /**
  890. * Return true if $path is children of $parent.
  891. *
  892. * @param string $path path to check
  893. * @param string $parent parent path
  894. *
  895. * @return bool
  896. *
  897. * @author Naoki Sawada
  898. **/
  899. protected function _inpath($path, $parent)
  900. {
  901. return $path == $parent || strpos($path, $parent.'/') === 0;
  902. }
  903. /***************** file stat ********************/
  904. /**
  905. * Return stat for given path.
  906. * Stat contains following fields:
  907. * - (int) size file size in b. required
  908. * - (int) ts file modification time in unix time. required
  909. * - (string) mime mimetype. required for folders, others - optionally
  910. * - (bool) read read permissions. required
  911. * - (bool) write write permissions. required
  912. * - (bool) locked is object locked. optionally
  913. * - (bool) hidden is object hidden. optionally
  914. * - (string) alias for symlinks - link target path relative to root path. optionally
  915. * - (string) target for symlinks - link target path. optionally.
  916. *
  917. * If file does not exists - returns empty array or false.
  918. *
  919. * @param string $path file path
  920. *
  921. * @return array|false
  922. *
  923. * @author Dmitry (dio) Levashov
  924. **/
  925. protected function _stat($path)
  926. {
  927. if ($raw = $this->_db_getFile($path)) {
  928. return $this->_db_parseRaw($raw);
  929. }
  930. return false;
  931. }
  932. /**
  933. * Return true if path is dir and has at least one childs directory.
  934. *
  935. * @param string $path dir path
  936. *
  937. * @return bool
  938. *
  939. * @author Naoki Sawada
  940. **/
  941. protected function _subdirs($path)
  942. {
  943. $hasdir = false;
  944. try {
  945. $res = $this->service->listFolder($path);
  946. if ($res) {
  947. $items = $res->getItems();
  948. foreach ($items as $raw) {
  949. if ($raw instanceof FolderMetadata) {
  950. $hasdir = true;
  951. break;
  952. }
  953. }
  954. }
  955. } catch (DropboxClientException $e) {
  956. $this->setError('Dropbox error: '.$e->getMessage());
  957. }
  958. return $hasdir;
  959. }
  960. /**
  961. * Return object width and height
  962. * Ususaly used for images, but can be realize for video etc...
  963. *
  964. * @param string $path file path
  965. * @param string $mime file mime type
  966. *
  967. * @return string
  968. *
  969. * @author Naoki Sawada
  970. **/
  971. protected function _dimensions($path, $mime)
  972. {
  973. if (strpos($mime, 'image') !== 0) {
  974. return '';
  975. }
  976. $ret = '';
  977. if ($data = $this->_getContents($path)) {
  978. $tmp = $this->getTempFile();
  979. file_put_contents($tmp, $data);
  980. $size = getimagesize($tmp);
  981. if ($size) {
  982. $ret = array('dim' => $size[0].'x'.$size[1]);
  983. $srcfp = fopen($tmp, 'rb');
  984. if ($subImgLink = $this->getSubstituteImgLink(elFinder::$currentArgs['target'], $size, $srcfp)) {
  985. $ret['url'] = $subImgLink;
  986. }
  987. }
  988. }
  989. return $ret;
  990. }
  991. /******************** file/dir content *********************/
  992. /**
  993. * Return files list in directory.
  994. *
  995. * @param string $path dir path
  996. *
  997. * @return array
  998. *
  999. * @author Naoki Sawada
  1000. **/
  1001. protected function _scandir($path)
  1002. {
  1003. return isset($this->dirsCache[$path])
  1004. ? $this->dirsCache[$path]
  1005. : $this->cacheDir($path);
  1006. }
  1007. /**
  1008. * Open file and return file pointer.
  1009. *
  1010. * @param string $path file path
  1011. * @param bool $write open file for writing
  1012. *
  1013. * @return resource|false
  1014. *
  1015. * @author Naoki Sawada
  1016. **/
  1017. protected function _fopen($path, $mode = 'rb')
  1018. {
  1019. if ($mode === 'rb' || $mode === 'r') {
  1020. if ($link = $this->service->getTemporaryLink($path)) {
  1021. $access_token = $this->service->getAccessToken();
  1022. if ($access_token) {
  1023. $data = array(
  1024. 'target' => $link->getLink(),
  1025. 'headers' => array('Authorization: Bearer '.$access_token),
  1026. );
  1027. return elFinder::getStreamByUrl($data);
  1028. }
  1029. }
  1030. }
  1031. return false;
  1032. }
  1033. /**
  1034. * Close opened file.
  1035. *
  1036. * @param resource $fp file pointer
  1037. *
  1038. * @return bool
  1039. *
  1040. * @author Naoki Sawada
  1041. **/
  1042. protected function _fclose($fp, $path = '')
  1043. {
  1044. fclose($fp);
  1045. }
  1046. /******************** file/dir manipulations *************************/
  1047. /**
  1048. * Create dir and return created dir path or false on failed.
  1049. *
  1050. * @param string $path parent dir path
  1051. * @param string $name new directory name
  1052. *
  1053. * @return string|bool
  1054. *
  1055. * @author Naoki Sawada
  1056. **/
  1057. protected function _mkdir($path, $name)
  1058. {
  1059. try {
  1060. return $this->service->createFolder($this->_db_joinName($path, $name))->getPathLower();
  1061. } catch (DropboxClientException $e) {
  1062. return $this->setError('Dropbox error: '.$e->getMessage());
  1063. }
  1064. }
  1065. /**
  1066. * Create file and return it's path or false on failed.
  1067. *
  1068. * @param string $path parent dir path
  1069. * @param string $name new file name
  1070. *
  1071. * @return string|bool
  1072. *
  1073. * @author Naoki Sawada
  1074. **/
  1075. protected function _mkfile($path, $name)
  1076. {
  1077. return $this->_save($this->tmpfile(), $path, $name, []);
  1078. }
  1079. /**
  1080. * Create symlink. FTP driver does not support symlinks.
  1081. *
  1082. * @param string $target link target
  1083. * @param string $path symlink path
  1084. *
  1085. * @return bool
  1086. *
  1087. * @author Naoki Sawada
  1088. **/
  1089. protected function _symlink($target, $path, $name)
  1090. {
  1091. return false;
  1092. }
  1093. /**
  1094. * Copy file into another file.
  1095. *
  1096. * @param string $source source file path
  1097. * @param string $targetDir target directory path
  1098. * @param string $name new file name
  1099. *
  1100. * @return bool
  1101. *
  1102. * @author Naoki Sawada
  1103. **/
  1104. protected function _copy($source, $targetDir, $name)
  1105. {
  1106. try {
  1107. $this->service->copy($source, $this->_db_joinName($targetDir, $name))->getPathLower();
  1108. } catch (DropboxClientException $e) {
  1109. return $this->setError('Dropbox error: '.$e->getMessage());
  1110. }
  1111. return true;
  1112. }
  1113. /**
  1114. * Move file into another parent dir.
  1115. * Return new file path or false.
  1116. *
  1117. * @param string $source source file path
  1118. * @param string $target target dir path
  1119. * @param string $name file name
  1120. *
  1121. * @return string|bool
  1122. *
  1123. * @author Naoki Sawada
  1124. **/
  1125. protected function _move($source, $targetDir, $name)
  1126. {
  1127. try {
  1128. return $this->service->move($source, $this->_db_joinName($targetDir, $name))->getPathLower();
  1129. } catch (DropboxClientException $e) {
  1130. return $this->setError('Dropbox error: '.$e->getMessage());
  1131. }
  1132. }
  1133. /**
  1134. * Remove file.
  1135. *
  1136. * @param string $path file path
  1137. *
  1138. * @return bool
  1139. *
  1140. * @author Naoki Sawada
  1141. **/
  1142. protected function _unlink($path)
  1143. {
  1144. try {
  1145. $this->service->delete($path);
  1146. return true;
  1147. } catch (DropboxClientException $e) {
  1148. return $this->setError('Dropbox error: '.$e->getMessage());
  1149. }
  1150. return true;
  1151. }
  1152. /**
  1153. * Remove dir.
  1154. *
  1155. * @param string $path dir path
  1156. *
  1157. * @return bool
  1158. *
  1159. * @author Naoki Sawada
  1160. **/
  1161. protected function _rmdir($path)
  1162. {
  1163. return $this->_unlink($path);
  1164. }
  1165. /**
  1166. * Create new file and write into it from file pointer.
  1167. * Return new file path or false on error.
  1168. *
  1169. * @param resource $fp file pointer
  1170. * @param string $dir target dir path
  1171. * @param string $name file name
  1172. * @param array $stat file stat (required by some virtual fs)
  1173. *
  1174. * @return bool|string
  1175. *
  1176. * @author Naoki Sawada
  1177. **/
  1178. protected function _save($fp, $path, $name, $stat)
  1179. {
  1180. try {
  1181. $info = stream_get_meta_data($fp);
  1182. if (empty($info['uri']) || preg_match('#^[a-z0-9.-]+://#', $info['uri'])) {
  1183. if ($filepath = $this->getTempFile()) {
  1184. $_fp = fopen($filepath, 'wb');
  1185. stream_copy_to_stream($fp, $_fp);
  1186. fclose($_fp);
  1187. }
  1188. } else {
  1189. $filepath = $info['uri'];
  1190. }
  1191. $dropboxFile = new DropboxFile($filepath);
  1192. if ($name === '') {
  1193. $fullpath = $path;
  1194. } else {
  1195. $fullpath = $this->_db_joinName($path, $name);
  1196. }
  1197. return $this->service->upload($dropboxFile, $fullpath, ['mode' => 'overwrite'])->getPathLower();
  1198. } catch (DropboxClientException $e) {
  1199. return $this->setError('Dropbox error: '.$e->getMessage());
  1200. }
  1201. }
  1202. /**
  1203. * Get file contents.
  1204. *
  1205. * @param string $path file path
  1206. *
  1207. * @return string|false
  1208. *
  1209. * @author Naoki Sawada
  1210. **/
  1211. protected function _getContents($path)
  1212. {
  1213. $contents = '';
  1214. try {
  1215. $file = $this->service->download($path);
  1216. $contents = $file->getContents();
  1217. } catch (Exception $e) {
  1218. return $this->setError('Dropbox error: '.$e->getMessage());
  1219. }
  1220. return $contents;
  1221. }
  1222. /**
  1223. * Write a string to a file.
  1224. *
  1225. * @param string $path file path
  1226. * @param string $content new file content
  1227. *
  1228. * @return bool
  1229. *
  1230. * @author Naoki Sawada
  1231. **/
  1232. protected function _filePutContents($path, $content)
  1233. {
  1234. $res = false;
  1235. if ($local = $this->getTempFile($path)) {
  1236. if (file_put_contents($local, $content, LOCK_EX) !== false
  1237. && ($fp = fopen($local, 'rb'))) {
  1238. clearstatcache();
  1239. $name = '';
  1240. $stat = $this->stat($path);
  1241. if ($stat) {
  1242. // keep real name
  1243. $path = $this->_dirname($path);
  1244. $name = $stat['name'];
  1245. }
  1246. $res = $this->_save($fp, $path, $name, []);
  1247. fclose($fp);
  1248. }
  1249. file_exists($local) && unlink($local);
  1250. }
  1251. return $res;
  1252. }
  1253. /**
  1254. * Detect available archivers.
  1255. **/
  1256. protected function _checkArchivers()
  1257. {
  1258. // die('Not yet implemented. (_checkArchivers)');
  1259. return [];
  1260. }
  1261. /**
  1262. * chmod implementation.
  1263. *
  1264. * @return bool
  1265. **/
  1266. protected function _chmod($path, $mode)
  1267. {
  1268. return false;
  1269. }
  1270. /**
  1271. * Unpack archive.
  1272. *
  1273. * @param string $path archive path
  1274. * @param array $arc archiver command and arguments (same as in $this->archivers)
  1275. *
  1276. * @return true
  1277. *
  1278. * @author Dmitry (dio) Levashov
  1279. * @author Alexey Sukhotin
  1280. **/
  1281. protected function _unpack($path, $arc)
  1282. {
  1283. die('Not yet implemented. (_unpack)');
  1284. //return false;
  1285. }
  1286. /**
  1287. * Recursive symlinks search.
  1288. *
  1289. * @param string $path file/dir path
  1290. *
  1291. * @return bool
  1292. *
  1293. * @author Dmitry (dio) Levashov
  1294. **/
  1295. protected function _findSymlinks($path)
  1296. {
  1297. die('Not yet implemented. (_findSymlinks)');
  1298. }
  1299. /**
  1300. * Extract files from archive.
  1301. *
  1302. * @param string $path archive path
  1303. * @param array $arc archiver command and arguments (same as in $this->archivers)
  1304. *
  1305. * @return true
  1306. *
  1307. * @author Dmitry (dio) Levashov,
  1308. * @author Alexey Sukhotin
  1309. **/
  1310. protected function _extract($path, $arc)
  1311. {
  1312. die('Not yet implemented. (_extract)');
  1313. }
  1314. /**
  1315. * Create archive and return its path.
  1316. *
  1317. * @param string $dir target dir
  1318. * @param array $files files names list
  1319. * @param string $name archive name
  1320. * @param array $arc archiver options
  1321. *
  1322. * @return string|bool
  1323. *
  1324. * @author Dmitry (dio) Levashov,
  1325. * @author Alexey Sukhotin
  1326. **/
  1327. protected function _archive($dir, $files, $name, $arc)
  1328. {
  1329. die('Not yet implemented. (_archive)');
  1330. }
  1331. } // END class