ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç thumbnail.php000064400000004012152266407670007254 0ustar00. /** * This script displays one thumbnail of the image in current user's dropbox. * If {@link repository_dropbox::send_thumbnail()} can not display image * the default 64x64 filetype icon is returned * * @package repository_dropbox * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../config.php'); require_once(__DIR__.'/lib.php'); $repoid = optional_param('repo_id', 0, PARAM_INT); // Repository ID $contextid = optional_param('ctx_id', SYSCONTEXTID, PARAM_INT); // Context ID $source = optional_param('source', '', PARAM_TEXT); // File path in current user's dropbox $thumbnailavailable = isloggedin(); $thumbnailavailable = $thumbnailavailable && $repoid; $thumbnailavailable = $thumbnailavailable && $source; $thumbnailavailable = $thumbnailavailable && ($repo = repository::get_repository_by_id($repoid, $contextid)); $thumbnailavailable = $thumbnailavailable && method_exists($repo, 'send_thumbnail'); if ($thumbnailavailable) { // Try requesting thumbnail and outputting it. // This function exits if thumbnail was retrieved. $repo->send_thumbnail($source); } // Send default icon for the file type. $fileicon = file_extension_icon($source); send_file($CFG->dirroot . '/pix/' . $fileicon . '.png', basename($fileicon) . '.svg'); db/tasks.php000064400000002244152266407670007010 0ustar00. /** * Definition of dropbox repository scheduled tasks. * * @package repository_dropbox * @copyright 2019 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $tasks = array( array( 'classname' => '\repository_dropbox\task\cron_task', 'blocking' => 0, 'minute' => '*', 'hour' => '*', 'day' => '*', 'month' => '*', 'dayofweek' => '*' ) ); db/access.php000064400000002225152266407670007123 0ustar00. /** * Plugin capabilities. * * @package repository_dropbox * @copyright 2010 Dongsheng Cai * @author Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'repository/dropbox:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'user' => CAP_ALLOW ) ) ); db/upgrade.php000064400000002611152266407670007310 0ustar00. /** * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_repository_dropbox_upgrade($oldversion) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } lang/en/repository_dropbox.php000064400000004141152266407670012573 0ustar00. /** * Strings for component 'repository_dropbox', language 'en', branch 'MOODLE_20_STABLE' * * @package repository_dropbox * @copyright 2010 Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['configplugin'] = 'Dropbox configuration'; $string['crontask'] = 'Background processing for Dropbox repository'; $string['notitle'] = 'notitle'; $string['remember'] = 'Remember me'; $string['pluginname'] = 'Dropbox'; $string['dropbox'] = 'Dropbox'; $string['issuer'] = 'OAuth 2 service'; $string['issuer_help'] = 'Select the OAuth 2 service that is configured to talk to the Dropbox API. If the service doesn\'t exist yet, you will need to create it.'; $string['cachelimit'] = 'Cache limit'; $string['cachelimit_info'] = 'Enter the maximum size of files (in bytes) to be cached on server for Dropbox aliases/shortcuts. Cached files will be served when the source is no longer available. Empty value or zero mean caching of all files regardless of size.'; $string['dropbox:view'] = 'View a Dropbox folder'; $string['logoutdesc'] = '(Logout when you finish using Dropbox)'; $string['oauth2redirecturi'] = 'OAuth 2 Redirect URI'; $string['privacy:metadata:repository_dropbox'] = 'The Dropbox repository plugin does not store any personal data, but does transmit user data from Moodle to the remote system.'; $string['privacy:metadata:repository_dropbox:query'] = 'The Dropbox repository user search text query.'; pix/icon.svg000064400000000465152266407670007041 0ustar00pix/icon.png000064400000046560152266407670007034 0ustar00PNG  IHDR szz pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-:6iTXtXML:com.adobe.xmp Adobe Photoshop CC 2015 (Macintosh) 2018-09-22T22:35:50+02:00 2018-09-22T22:35:50+02:00 2018-09-22T22:35:50+02:00 xmp.iid:e28d98f0-f5c9-4e39-b3b9-adbb140da153 adobe:docid:photoshop:78e97379-ff35-117b-b0d5-af33bbf3b734 xmp.did:a67dba50-7fc3-4f8f-96ba-8599b1a73371 created xmp.iid:a67dba50-7fc3-4f8f-96ba-8599b1a73371 2018-09-22T22:35:50+02:00 Adobe Photoshop CC 2015 (Macintosh) saved xmp.iid:e28d98f0-f5c9-4e39-b3b9-adbb140da153 2018-09-22T22:35:50+02:00 Adobe Photoshop CC 2015 (Macintosh) / image/png 3 sRGB IEC61966-2.1 1 720000/10000 720000/10000 2 1 32 32 lh cHRMz%u0`:o_FYIDATxėypTUk B [%,6D\J%V93ZXSVY U S"c!NDa@uAqd(Ȗ!![wqc W~~|>IUUn&?7aphR•P.f}1KlϢJ\m{}Iډ&lǠqGNB +9(*O[Z-/Z,꪿zWm8-A¹ۃX 2LTT 'H8em;^_^:c}@\U~o9tFYof)kI^?5E:V~޲;%I)VsR=vjl\M>vi#ξC;{eY8?jab4(8]aώXS?9_p#51(;0$dDe]''[™qR 35?לPT v+dqn% C@kFW~=kcF#T@vFjk3Xh4PqE!ij Ĩ1c(R=\~+&V,ID?@Y. v:OF HHVt~WD&�h,3UŬ: @NΠD bIn/Cy-s_!{Nd(OA2 X@QD~0_GV2B[u[5K'򧾈${[%tFٽ}Wcq,3x 8-M@< >'F! ÞF6"xղTU',ej_\ 9 $2p%Mm̻Ŭ/Yn:!fV +C/\bj_-Tα@{P(f6NHBHgTQ{KRP>NG8(Ըm$IPi#_4hҔOfS;9K1ck%rqwQ8;Oa8H6H2~ mEFNl8ƨO졆4s~LǘƎE2Y6{Q?pn(5H`wqf#M0 *kYK{xr iW3ܷp9,'8 G,`:@^ ( =ùcs̗$d]m@2)p* P)vC . /p5h [L u5<2}X-Tn1ҔQ+UÈB1(l&aяle{yqCVU5AlZɋ~HVB0CP6 ZE@M5蛻Y`` huxV, Io~|:~ *6DžGQnSCi bzJuŠ7Jx͙k^H-d]Hpo@5T+JŊƒdM,鴐h{0ind{IENDB`lib.php000064400000067342152266407670006056 0ustar00. /** * This plugin is used to access user's dropbox files * * @since Moodle 2.0 * @package repository_dropbox * @copyright 2012 Marina Glancy * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot . '/repository/lib.php'); /** * Repository to access Dropbox files * * @package repository_dropbox * @copyright 2010 Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_dropbox extends repository { /** * @var dropbox The instance of dropbox client. */ private $dropbox; /** * @var int The maximum file size to cache in the moodle filepool. */ public $cachelimit = null; /** * Constructor of dropbox plugin. * * @inheritDocs */ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = []) { $options['page'] = optional_param('p', 1, PARAM_INT); parent::__construct($repositoryid, $context, $options); $returnurl = new moodle_url('/repository/repository_callback.php', [ 'callback' => 'yes', 'repo_id' => $repositoryid, 'sesskey' => sesskey(), ]); // Create the dropbox API instance. $issuer = \core\oauth2\api::get_issuer(get_config('dropbox', 'dropbox_issuerid')); $this->dropbox = new repository_dropbox\dropbox($issuer, $returnurl); } /** * Repository method to serve the referenced file. * * @inheritDocs */ public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, ?array $options = null) { $reference = $this->unpack_reference($storedfile->get_reference()); $maxcachesize = $this->max_cache_bytes(); if (empty($maxcachesize)) { // Always cache the file, regardless of size. $cachefile = true; } else { // Size available. Only cache if it is under maxcachesize. $cachefile = $storedfile->get_filesize() < $maxcachesize; } if (!$cachefile) { \core\session\manager::write_close(); header('Location: ' . $this->get_file_download_link($reference->url)); die; } try { $this->import_external_file_contents($storedfile, $this->max_cache_bytes()); if (!is_array($options)) { $options = array(); } $options['sendcachedexternalfile'] = true; \core\session\manager::write_close(); send_stored_file($storedfile, $lifetime, $filter, $forcedownload, $options); } catch (moodle_exception $e) { // Redirect to Dropbox, it will show the error. // Note: We redirect to Dropbox shared link, not to the download link here! \core\session\manager::write_close(); header('Location: ' . $reference->url); die; } } /** * Return human readable reference information. * {@link stored_file::get_reference()} * * @inheritDocs */ public function get_reference_details($reference, $filestatus = 0) { global $USER; $ref = unserialize_object($reference); $detailsprefix = $this->get_name(); if (isset($ref->userid) && $ref->userid != $USER->id && isset($ref->username)) { $detailsprefix .= ' ('.$ref->username.')'; } $details = $detailsprefix; if (isset($ref->path)) { $details .= ': '. $ref->path; } if (isset($ref->path) && !$filestatus) { // Indicate this is from dropbox with path. return $details; } else { if (isset($ref->url)) { $details = $detailsprefix. ': '. $ref->url; } return get_string('lostsource', 'repository', $details); } } /** * Cache file from external repository by reference. * {@link repository::get_file_reference()} * {@link repository::get_file()} * Invoked at MOODLE/repository/repository_ajax.php. * * @inheritDocs */ public function cache_file_by_reference($reference, $storedfile) { try { $this->import_external_file_contents($storedfile, $this->max_cache_bytes()); } catch (Exception $e) { // Cache failure should not cause a fatal error. This is only a nice-to-have feature. } } /** * Return the source information. * * The result of the function is stored in files.source field. It may be analysed * when the source file is lost or repository may use it to display human-readable * location of reference original. * * This method is called when file is picked for the first time only. When file * (either copy or a reference) is already in moodle and it is being picked * again to another file area (also as a copy or as a reference), the value of * files.source is copied. * * @inheritDocs */ public function get_file_source_info($source) { global $USER; return 'Dropbox ('.fullname($USER).'): ' . $source; } /** * Prepare file reference information. * * @inheritDocs */ public function get_file_reference($source) { global $USER; $reference = new stdClass; $reference->userid = $USER->id; $reference->username = fullname($USER); $reference->path = $source; // Determine whether we are downloading the file, or should use a file reference. $usefilereference = optional_param('usefilereference', false, PARAM_BOOL); if ($usefilereference) { if ($data = $this->dropbox->get_file_share_info($source)) { $reference = (object) array_merge((array) $data, (array) $reference); } } return serialize($reference); } /** * Return file URL for external link. * * @inheritDocs */ public function get_link($reference) { $unpacked = $this->unpack_reference($reference); return $this->get_file_download_link($unpacked->url); } /** * Downloads a file from external repository and saves it in temp dir. * * @inheritDocs */ public function get_file($reference, $saveas = '') { $unpacked = $this->unpack_reference($reference); // This is a shared link, and hopefully it is still active. $downloadlink = $this->get_file_download_link($unpacked->url); $saveas = $this->prepare_file($saveas); file_put_contents($saveas, fopen($downloadlink, 'r')); return ['path' => $saveas]; } /** * Dropbox plugin supports all kinds of files. * * @inheritDocs */ public function supported_filetypes() { return '*'; } /** * User cannot use the external link to dropbox. * * @inheritDocs */ public function supported_returntypes() { return FILE_INTERNAL | FILE_REFERENCE | FILE_EXTERNAL; } /** * Get dropbox files. * * @inheritDocs */ public function get_listing($path = '', $page = '1') { if (empty($path) || $path == '/') { $path = ''; } else { $path = file_correct_filepath($path); } $list = [ 'list' => [], 'manage' => 'https://www.dropbox.com/home', 'logouturl' => 'https://www.dropbox.com/logout', 'message' => get_string('logoutdesc', 'repository_dropbox'), 'dynload' => true, 'path' => $this->process_breadcrumbs($path), ]; // Note - we deliberately do not catch the coding exceptions here. try { $result = $this->dropbox->get_listing($path); } catch (\repository_dropbox\authentication_exception $e) { // The token has expired. return $this->print_login(); } catch (\repository_dropbox\dropbox_exception $e) { // There was some other form of non-coding failure. // This could be a rate limit, or it could be a server-side error. // Just return early instead. return $list; } if (!is_object($result) || empty($result)) { return $list; } if (empty($result->entries) or !is_array($result->entries)) { return $list; } $list['list'] = $this->process_entries($result->entries); return $list; } /** * Get dropbox files in the specified path. * * @param string $query The search query * @param int $page The page number * @return array */ public function search($query, $page = 0) { $list = [ 'list' => [], 'manage' => 'https://www.dropbox.com/home', 'logouturl' => 'https://www.dropbox.com/logout', 'message' => get_string('logoutdesc', 'repository_dropbox'), 'dynload' => true, ]; // Note - we deliberately do not catch the coding exceptions here. try { $result = $this->dropbox->search($query); } catch (\repository_dropbox\authentication_exception $e) { // The token has expired. return $this->print_login(); } catch (\repository_dropbox\dropbox_exception $e) { // There was some other form of non-coding failure. // This could be a rate limit, or it could be a server-side error. // Just return early instead. return $list; } if (!is_object($result) || empty($result)) { return $list; } if (empty($result->matches) or !is_array($result->matches)) { return $list; } $list['list'] = $this->process_entries($result->matches); return $list; } /** * Displays a thumbnail for current user's dropbox file. * * @inheritDocs */ public function send_thumbnail($source) { $content = $this->dropbox->get_thumbnail($source); // Set 30 days lifetime for the image. // If the image is changed in dropbox it will have different revision number and URL will be different. // It is completely safe to cache the thumbnail in the browser for a long time. send_file($content, basename($source), 30 * DAYSECS, 0, true); } /** * Fixes references in DB that contains user credentials. * * @param string $packed Content of DB field files_reference.reference * @return string New serialized reference */ protected function fix_old_style_reference($packed) { $ref = unserialize_object($packed); $ref = $this->dropbox->get_file_share_info($ref->path ?? ''); if (!$ref || empty($ref->url)) { // Some error occurred, do not fix reference for now. return $packed; } $newreference = serialize($ref); if ($newreference !== $packed) { // We need to update references in the database. global $DB; $params = array( 'newreference' => $newreference, 'newhash' => sha1($newreference), 'reference' => $packed, 'hash' => sha1($packed), 'repoid' => $this->id, ); $refid = $DB->get_field_sql('SELECT id FROM {files_reference} WHERE reference = :reference AND referencehash = :hash AND repositoryid = :repoid', $params); if (!$refid) { return $newreference; } $existingrefid = $DB->get_field_sql('SELECT id FROM {files_reference} WHERE reference = :newreference AND referencehash = :newhash AND repositoryid = :repoid', $params); if ($existingrefid) { // The same reference already exists, we unlink all files from it, // link them to the current reference and remove the old one. $DB->execute('UPDATE {files} SET referencefileid = :refid WHERE referencefileid = :existingrefid', array('refid' => $refid, 'existingrefid' => $existingrefid)); $DB->delete_records('files_reference', array('id' => $existingrefid)); } // Update the reference. $params['refid'] = $refid; $DB->execute('UPDATE {files_reference} SET reference = :newreference, referencehash = :newhash WHERE id = :refid', $params); } return $newreference; } /** * Unpack the supplied serialized reference, fixing it if required. * * @param string $packed The packed reference * @return object The unpacked reference */ protected function unpack_reference($packed) { $reference = unserialize_object($packed); if (empty($reference->url)) { // The reference is missing some information. Attempt to update it. return unserialize_object($this->fix_old_style_reference($packed)); } return $reference; } /** * Converts a URL received from dropbox API function 'shares' into URL that * can be used to download/access file directly * * @param string $sharedurl * @return string */ protected function get_file_download_link($sharedurl) { $url = new \moodle_url($sharedurl); $url->param('dl', 1); return $url->out(false); } /** * Logout from dropbox. * * @inheritDocs */ public function logout() { $this->dropbox->logout(); return $this->print_login(); } /** * Check if the dropbox is logged in via the oauth process. * * @inheritDocs */ public function check_login() { return $this->dropbox->is_logged_in(); } /** * Generate dropbox login url. * * @inheritDocs */ public function print_login() { $url = $this->dropbox->get_login_url(); if ($this->options['ajax']) { $ret = array(); $btn = new \stdClass(); $btn->type = 'popup'; $btn->url = $url->out(false); $ret['login'] = array($btn); return $ret; } else { echo html_writer::link($url, get_string('login', 'repository'), array('target' => '_blank')); } } /** * Request access token. * * @inheritDocs */ public function callback() { $this->dropbox->callback(); } /** * Caches all references to Dropbox files in moodle filepool. * * Invoked by {@link repository_dropbox_cron()}. Only files smaller than * {@link repository_dropbox::max_cache_bytes()} and only files which * synchronisation timeout have not expired are cached. * * @inheritDocs */ public function cron() { $fs = get_file_storage(); $files = $fs->get_external_files($this->id); $fetchedreferences = []; foreach ($files as $file) { if (isset($fetchedreferences[$file->get_referencefileid()])) { continue; } try { // This call will cache all files that are smaller than max_cache_bytes() // and synchronise file size of all others. $this->import_external_file_contents($file, $this->max_cache_bytes()); $fetchedreferences[$file->get_referencefileid()] = true; } catch (moodle_exception $e) { // If an exception is thrown, just continue. This is only a pre-fetch to help speed up general use. } } } /** * Add Plugin settings input to Moodle form. * * @inheritDocs */ public static function type_config_form($mform, $classname = 'repository') { parent::type_config_form($mform); $options = []; $issuers = \core\oauth2\api::get_all_issuers(); foreach ($issuers as $issuer) { $options[$issuer->get('id')] = s($issuer->get('name')); } $strrequired = get_string('required'); $mform->addElement('select', 'dropbox_issuerid', get_string('issuer', 'repository_dropbox'), $options); $mform->addHelpButton('dropbox_issuerid', 'issuer', 'repository_dropbox'); $mform->addRule('dropbox_issuerid', $strrequired, 'required', null, 'client'); $mform->addElement('text', 'dropbox_cachelimit', get_string('cachelimit', 'repository_dropbox'), array('size' => '40')); $mform->addRule('dropbox_cachelimit', null, 'numeric', null, 'client'); $mform->setType('dropbox_cachelimit', PARAM_INT); $mform->addElement('static', 'dropbox_cachelimit_info', '', get_string('cachelimit_info', 'repository_dropbox')); } /** * Set options. * * @param array $options * @return mixed */ public function set_option($options = []) { if (!empty($options['dropbox_issuerid'])) { set_config('dropbox_issuerid', trim($options['dropbox_issuerid']), 'dropbox'); unset($options['dropbox_issuerid']); } if (!empty($options['dropbox_cachelimit'])) { $this->cachelimit = (int) trim($options['dropbox_cachelimit']); set_config('dropbox_cachelimit', $this->cachelimit, 'dropbox'); unset($options['dropbox_cachelimit']); } return parent::set_option($options); } /** * Get dropbox options * @param string $config * @return mixed */ public function get_option($config = '') { if ($config === 'dropbox_issuerid') { return trim(get_config('dropbox', 'dropbox_issuerid')); } else if ($config === 'dropbox_cachelimit') { return $this->max_cache_bytes(); } else { $options = parent::get_option(); $options['dropbox_issuerid'] = trim(get_config('dropbox', 'dropbox_issuerid')); $options['dropbox_cachelimit'] = $this->max_cache_bytes(); } return $options; } /** * Return the OAuth 2 Redirect URI. * * @return moodle_url */ public static function get_oauth2callbackurl() { global $CFG; return new moodle_url('/admin/oauth2callback.php'); } /** * Option names of dropbox plugin. * * @inheritDocs */ public static function get_type_option_names() { return [ 'dropbox_issuerid', 'pluginname', 'dropbox_cachelimit', ]; } /** * Performs synchronisation of an external file if the previous one has expired. * * This function must be implemented for external repositories supporting * FILE_REFERENCE, it is called for existing aliases when their filesize, * contenthash or timemodified are requested. It is not called for internal * repositories (see {@link repository::has_moodle_files()}), references to * internal files are updated immediately when source is modified. * * Referenced files may optionally keep their content in Moodle filepool (for * thumbnail generation or to be able to serve cached copy). In this * case both contenthash and filesize need to be synchronized. Otherwise repositories * should use contenthash of empty file and correct filesize in bytes. * * Note that this function may be run for EACH file that needs to be synchronised at the * moment. If anything is being downloaded or requested from external sources there * should be a small timeout. The synchronisation is performed to update the size of * the file and/or to update image and re-generated image preview. There is nothing * fatal if syncronisation fails but it is fatal if syncronisation takes too long * and hangs the script generating a page. * * Note: If you wish to call $file->get_filesize(), $file->get_contenthash() or * $file->get_timemodified() make sure that recursion does not happen. * * Called from {@link stored_file::sync_external_file()} * * @inheritDocs */ public function sync_reference(stored_file $file) { global $CFG; if ($file->get_referencelastsync() + DAYSECS > time()) { // Only synchronise once per day. return false; } $reference = $this->unpack_reference($file->get_reference()); if (!isset($reference->url)) { // The URL to sync with is missing. return false; } $c = new curl; $url = $this->get_file_download_link($reference->url); if (file_extension_in_typegroup($reference->path, 'web_image')) { $saveas = $this->prepare_file(''); try { $result = $c->download_one($url, [], [ 'filepath' => $saveas, 'timeout' => $CFG->repositorysyncimagetimeout, 'followlocation' => true, ]); $info = $c->get_info(); if ($result === true && isset($info['http_code']) && $info['http_code'] == 200) { $file->set_synchronised_content_from_file($saveas); return true; } } catch (Exception $e) { // IF the download_one fails, we will attempt to download // again with get() anyway. } } $c->get($url, null, array('timeout' => $CFG->repositorysyncimagetimeout, 'followlocation' => true, 'nobody' => true)); $info = $c->get_info(); if (isset($info['http_code']) && $info['http_code'] == 200 && array_key_exists('download_content_length', $info) && $info['download_content_length'] >= 0) { $filesize = (int)$info['download_content_length']; $file->set_synchronized(null, $filesize); return true; } $file->set_missingsource(); return true; } /** * Process a standard entries list. * * @param array $entries The list of entries returned from the API * @return array The manipulated entries for display in the file picker */ protected function process_entries(array $entries) { global $OUTPUT; $dirslist = []; $fileslist = []; foreach ($entries as $entry) { $entrydata = $entry; if (isset($entrydata->metadata)) { // If this is metadata, fetch the metadata content. // We only use the consistent parts of the file, folder, and metadata. $entrydata = $entrydata->metadata; } // Due to a change in the api, the actual content is in a nested metadata tree. if ($entrydata->{".tag"} == "metadata" && isset($entrydata->metadata)) { $entrydata = $entrydata->metadata; } if ($entrydata->{".tag"} === "folder") { $dirslist[] = [ 'title' => $entrydata->name, // Use the display path here rather than lower. // Dropbox is case insensitive but this leads to more accurate breadcrumbs. 'path' => file_correct_filepath($entrydata->path_display), 'thumbnail' => $OUTPUT->image_url(file_folder_icon())->out(false), 'thumbnail_height' => 64, 'thumbnail_width' => 64, 'children' => array(), ]; } else if ($entrydata->{".tag"} === "file") { $fileslist[] = [ 'title' => $entrydata->name, // Use the path_lower here to make life easier elsewhere. 'source' => $entrydata->path_lower, 'size' => $entrydata->size, 'date' => strtotime($entrydata->client_modified), 'thumbnail' => $OUTPUT->image_url(file_extension_icon($entrydata->path_lower))->out(false), 'realthumbnail' => $this->get_thumbnail_url($entrydata), 'thumbnail_height' => 64, 'thumbnail_width' => 64, ]; } } $fileslist = array_filter($fileslist, array($this, 'filter')); return array_merge($dirslist, array_values($fileslist)); } /** * Process the breadcrumbs for a listing. * * @param string $path The path to create breadcrumbs for * @return array */ protected function process_breadcrumbs($path) { // Process breadcrumb trail. // Note: Dropbox is case insensitive. // Without performing an additional API call, it isn't possible to get the path_display. // As a result, the path here is the path_lower. $breadcrumbs = [ [ 'path' => '/', 'name' => get_string('dropbox', 'repository_dropbox'), ], ]; $path = rtrim($path, '/'); $directories = explode('/', $path); $pathtodate = ''; foreach ($directories as $directory) { if ($directory === '') { continue; } $pathtodate .= '/' . $directory; $breadcrumbs[] = [ 'path' => $pathtodate, 'name' => $directory, ]; } return $breadcrumbs; } /** * Grab the thumbnail URL for the specified entry. * * @param object $entry The file entry as retrieved from the API * @return moodle_url */ protected function get_thumbnail_url($entry) { if ($this->dropbox->supports_thumbnail($entry)) { $thumburl = new moodle_url('/repository/dropbox/thumbnail.php', [ // The id field in dropbox is unique - no need to specify a revision. 'source' => $entry->id, 'path' => $entry->path_lower, 'repo_id' => $this->id, 'ctx_id' => $this->context->id, ]); return $thumburl->out(false); } return ''; } /** * Returns the maximum size of the Dropbox files to cache in moodle. * * Note that {@link repository_dropbox::sync_reference()} will try to cache images even * when they are bigger in order to generate thumbnails. However there is * a small timeout for downloading images for synchronisation and it will * probably fail if the image is too big. * * @return int */ public function max_cache_bytes() { if ($this->cachelimit === null) { $this->cachelimit = (int) get_config('dropbox', 'dropbox_cachelimit'); } return $this->cachelimit; } } version.php000064400000002267152266407670006770 0ustar00. /** * Version details * * @package repository_dropbox * @copyright 2010 Dongsheng Cai * @author Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'repository_dropbox'; // Full name of the plugin (used for diagnostics) classes/privacy/provider.php000064400000007750152266407670012251 0ustar00. /** * Privacy Subsystem implementation for repository_dropbox. * * @package repository_dropbox * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_dropbox\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\context; use core_privacy\local\request\contextlist; use core_privacy\local\request\userlist; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for repository_dropbox implementing metadata and plugin providers. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->add_external_location_link( 'dropbox.com', [ 'query' => 'privacy:metadata:repository_dropbox:query' ], 'privacy:metadata:repository_dropbox' ); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { return new contextlist(); } /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { } /** * Delete all data for all users in the specified context. * * @param context $context The specific context to delete data for. */ public static function delete_data_for_all_users_in_context(\context $context) { } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { } } classes/rate_limit_exception.php000064400000002576152266407670013152 0ustar00. /** * Dropbox Rate Limit Encountered. * * @since Moodle 3.2 * @package repository_dropbox * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_dropbox; defined('MOODLE_INTERNAL') || die(); /** * Dropbox Rate Limit Encountered. * * @package repository_dropbox * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class rate_limit_exception extends dropbox_exception { /** * Constructor for rate_limit_exception. */ public function __construct() { parent::__construct('Rate limit hit'); } } classes/authentication_exception.php000064400000002360152266407670014027 0ustar00. /** * Dropbox Authentication exception. * * @since Moodle 3.2 * @package repository_dropbox * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_dropbox; defined('MOODLE_INTERNAL') || die(); /** * Dropbox Authentication exception. * * @package repository_dropbox * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class authentication_exception extends dropbox_exception { } classes/task/cron_task.php000064400000003042152266407670011655 0ustar00. namespace repository_dropbox\task; defined('MOODLE_INTERNAL') || die(); /** * A schedule task for dropbox repository cron. * * @package repository_dropbox * @copyright 2019 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cron_task extends \core\task\scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('crontask', 'repository_dropbox'); } /** * Run dropbox repository cron. */ public function execute() { global $CFG; require_once($CFG->dirroot . '/repository/lib.php'); $instances = \repository::get_instances(['type' => 'dropbox']); foreach ($instances as $instance) { $instance->cron(); } } } classes/dropbox.php000064400000033345152266407670010416 0ustar00. /** * Dropbox V2 API. * * @since Moodle 3.2 * @package repository_dropbox * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_dropbox; use core\oauth2\client; use core\oauth2\issuer; /** * Dropbox V2 API. * * @package repository_dropbox * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class dropbox extends client { /** * @var array Custom continue endpoints that differ from the standard. */ private $mappedcontinueoverides = [ 'files/search_v2' => 'files/search/continue_v2' ]; /** * Create the DropBox API Client. * * @param issuer $issuer The dropbox issuer * @param string $callback The callback URL */ public function __construct(issuer $issuer, $callback) { parent::__construct($issuer, $callback, '', false, true); } /** * Override - Return an empty string to override parent function. * * Dropbox does not require scopes to be provided and can function without them. * Additional information MDL-70268 * * @return string */ protected function get_login_scopes() { return ''; } /** * Returns the auth url for OAuth 2.0 request. * * @return string the auth url */ protected function auth_url() { return 'https://www.dropbox.com/oauth2/authorize'; } /** * Returns the token url for OAuth 2.0 request. * * @return string the auth url */ protected function token_url() { return 'https://api.dropboxapi.com/oauth2/token'; } /** * Return the constructed API endpoint URL. * * @param string $endpoint The endpoint to be contacted * @return moodle_url The constructed API URL */ protected function get_api_endpoint($endpoint) { return new \moodle_url('https://api.dropboxapi.com/2/' . $endpoint); } /** * Return the constructed content endpoint URL. * * @param string $endpoint The endpoint to be contacted * @return moodle_url The constructed content URL */ protected function get_content_endpoint($endpoint) { return new \moodle_url('https://api-content.dropbox.com/2/' . $endpoint); } /** * Get the continue endpoint for the provided endpoint. * * @param string $endpoint The original endpoint * @return string $endpoint The generated/mapped continue link */ protected function get_endpoint_for_continue(string $endpoint) { // Any API endpoint returning 'has_more' will provide a cursor, and also have a matching endpoint suffixed // with /continue which takes that cursor. if (preg_match('_/continue$_', $endpoint) === 0) { // First check if the API call uses a custom mapped continue endpoint. if (isset($this->mappedcontinueoverides[$endpoint])) { $endpoint = $this->mappedcontinueoverides[$endpoint]; } else { // Only add /continue if it is not already present. $endpoint .= '/continue'; } } return $endpoint; } /** * Make an API call against the specified endpoint with supplied data. * * @param string $endpoint The endpoint to be contacted * @param array $data Any data to pass to the endpoint * @param string $resultnode The name of the node that contains the data * @return object Content decoded from the endpoint */ protected function fetch_dropbox_data($endpoint, $data = [], string $resultnode = 'entries') { $url = $this->get_api_endpoint($endpoint); $this->cleanopt(); $this->resetHeader(); if ($data === null) { // Some API endpoints explicitly expect a data submission of 'null'. $options['CURLOPT_POSTFIELDS'] = 'null'; } else { $options['CURLOPT_POSTFIELDS'] = json_encode($data); } $options['CURLOPT_POST'] = 1; $this->setHeader('Content-Type: application/json'); $response = $this->request($url, $options); $result = json_decode($response); $this->check_and_handle_api_errors($result); if ($this->has_additional_results($result)) { $endpoint = $this->get_endpoint_for_continue($endpoint); // Fetch the next page of results. $additionaldata = $this->fetch_dropbox_data($endpoint, [ 'cursor' => $result->cursor, ], $resultnode); // Merge the list of entries. $result->$resultnode = array_merge($result->$resultnode, $additionaldata->$resultnode); } if (isset($result->has_more)) { // Unset the cursor and has_more flags. unset($result->cursor); unset($result->has_more); } return $result; } /** * Whether the supplied result is paginated and not the final page. * * @param object $result The result of an operation * @return boolean */ public function has_additional_results($result) { return !empty($result->has_more) && !empty($result->cursor); } /** * Fetch content from the specified endpoint with the supplied data. * * @param string $endpoint The endpoint to be contacted * @param array $data Any data to pass to the endpoint * @return string The returned data */ protected function fetch_dropbox_content($endpoint, $data = []) { $url = $this->get_content_endpoint($endpoint); $this->cleanopt(); $this->resetHeader(); $options['CURLOPT_POST'] = 1; $this->setHeader('Content-Type: '); $this->setHeader('Dropbox-API-Arg: ' . json_encode($data)); $response = $this->request($url, $options); $this->check_and_handle_api_errors($response); return $response; } /** * Check for an attempt to handle API errors. * * This function attempts to deal with errors as per * https://www.dropbox.com/developers/documentation/http/documentation#error-handling. * * @param mixed $data The returned content. * @throws moodle_exception */ protected function check_and_handle_api_errors($data) { if (!is_array($this->info) or $this->info['http_code'] == 200) { // Dropbox only returns errors on non-200 response codes. return; } switch($this->info['http_code']) { case 400: // Bad input parameter. Error message should indicate which one and why. throw new \coding_exception('Invalid input parameter passed to DropBox API.'); break; case 401: // Bad or expired token. This can happen if the access token is expired or if the access token has been // revoked by Dropbox or the user. To fix this, you should re-authenticate the user. throw new authentication_exception('Authentication token expired'); break; case 409: // Endpoint-specific error. Look to the JSON response body for the specifics of the error. throw new \coding_exception('Endpoint specific error: ' . $data->error_summary); break; case 429: // Your app is making too many requests for the given user or team and is being rate limited. Your app // should wait for the number of seconds specified in the "Retry-After" response header before trying // again. throw new rate_limit_exception(); break; default: break; } if ($this->info['http_code'] >= 500 && $this->info['http_code'] < 600) { throw new \invalid_response_exception($this->info['http_code'] . ": " . $data); } } /** * Get file listing from dropbox. * * @param string $path The path to query * @return object The returned directory listing, or null on failure */ public function get_listing($path = '') { if ($path === '/') { $path = ''; } $data = $this->fetch_dropbox_data('files/list_folder', [ 'path' => $path, ]); return $data; } /** * Get file search results from dropbox. * * @param string $query The search query * @return object The returned directory listing, or null on failure */ public function search($query = '') { // There is nothing to be searched. Return an empty array to mimic the response from Dropbox. if (!$query) { return []; } $data = $this->fetch_dropbox_data('files/search_v2', [ 'options' => [ 'path' => '', 'filename_only' => true, ], 'query' => $query, ], 'matches'); return $data; } /** * Whether the entry is expected to have a thumbnail. * See docs at https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail. * * @param object $entry The file entry received from the DropBox API * @return boolean Whether dropbox has a thumbnail available */ public function supports_thumbnail($entry) { if ($entry->{".tag"} !== "file") { // Not a file. No thumbnail available. return false; } // Thumbnails are available for files under 20MB with file extensions jpg, jpeg, png, tiff, tif, gif, and bmp. if ($entry->size > 20 * 1024 * 1024) { return false; } $supportedtypes = [ 'jpg' => true, 'jpeg' => true, 'png' => true, 'tiff' => true, 'tif' => true, 'gif' => true, 'bmp' => true, ]; $extension = substr($entry->path_lower, strrpos($entry->path_lower, '.') + 1); return isset($supportedtypes[$extension]) && $supportedtypes[$extension]; } /** * Retrieves the thumbnail for the content, as supplied by dropbox. * * @param string $path The path to fetch a thumbnail for * @return string Thumbnail image content */ public function get_thumbnail($path) { $content = $this->fetch_dropbox_content('files/get_thumbnail', [ 'path' => $path, ]); return $content; } /** * Fetch a valid public share link for the specified file. * * @param string $id The file path or file id of the file to fetch information for. * @return object An object containing the id, path, size, and URL of the entry */ public function get_file_share_info($id) { // Attempt to fetch any existing shared link first. $data = $this->fetch_dropbox_data('sharing/list_shared_links', [ 'path' => $id, ]); if (isset($data->links)) { $link = reset($data->links); if (isset($link->{".tag"}) && $link->{".tag"} === "file") { return $this->normalize_file_share_info($link); } } // No existing link available. // Create a new one. $link = $this->fetch_dropbox_data('sharing/create_shared_link_with_settings', [ 'path' => $id, 'settings' => [ 'requested_visibility' => 'public', ], ]); if (isset($link->{".tag"}) && $link->{".tag"} === "file") { return $this->normalize_file_share_info($link); } // Some kind of error we don't know how to handle at this stage. return null; } /** * Normalize the file share info. * * @param object $entry Information retrieved from share endpoints * @return object Normalized entry information to store as repository information */ protected function normalize_file_share_info($entry) { return (object) [ 'id' => $entry->id, 'path' => $entry->path_lower, 'url' => $entry->url, ]; } /** * Process the callback. */ public function callback() { $this->log_out(); $this->is_logged_in(); } /** * Revoke the current access token. * * @return string */ public function logout() { try { $this->fetch_dropbox_data('auth/token/revoke', null); } catch(authentication_exception $e) { // An authentication_exception may be expected if the token has // already expired. } } } classes/provider_exception.php000064400000002332152266407670012641 0ustar00. /** * Upstream issue exception. * * @since Moodle 3.2 * @package repository_dropbox * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_dropbox; defined('MOODLE_INTERNAL') || die(); /** * Upstream issue exception. * * @package repository_dropbox * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider_exception extends dropbox_exception { } classes/dropbox_exception.php000064400000002333152266407670012465 0ustar00. /** * General Dropbox Exception. * * @since Moodle 3.2 * @package repository_dropbox * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_dropbox; defined('MOODLE_INTERNAL') || die(); /** * General Dropbox Exception. * * @package repository_dropbox * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class dropbox_exception extends \moodle_exception { } tests/generator/lib.php000064400000003210152266407670011166 0ustar00. /** * Dropbox repository data generator * * @package repository_dropbox * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Dropbox repository data generator class * * @package repository_dropbox * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_dropbox_generator extends testing_repository_generator { /** * Fill in type record defaults. * * @param array $record * @return array */ protected function prepare_type_record(array $record) { $record = parent::prepare_type_record($record); if (!isset($record['dropbox_issuerid'])) { $record['dropbox_issuerid'] = 0; } if (!isset($record['dropbox_cachelimit'])) { $record['dropbox_cachelimit'] = 0; } return $record; } } tests/api_test.php000064400000055163152266407670010260 0ustar00. namespace repository_dropbox; /** * Tests for the Dropbox API (v2). * * @package repository_dropbox * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class api_test extends \advanced_testcase { /** * Data provider for has_additional_results. * * @return array */ public static function has_additional_results_provider(): array { return [ 'No more results' => [ (object) [ 'has_more' => false, 'cursor' => '', ], false ], 'Has more, No cursor' => [ (object) [ 'has_more' => true, 'cursor' => '', ], false ], 'Has more, Has cursor' => [ (object) [ 'has_more' => true, 'cursor' => 'example_cursor', ], true ], 'Missing has_more' => [ (object) [ 'cursor' => 'example_cursor', ], false ], 'Missing cursor' => [ (object) [ 'has_more' => 'example_cursor', ], false ], ]; } /** * Tests for the has_additional_results API function. * * @dataProvider has_additional_results_provider * @param object $result The data to test * @param bool $expected The expected result */ public function test_has_additional_results($result, $expected): void { $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class) ->disableOriginalConstructor() ->onlyMethods([]) ->getMock(); $this->assertEquals($expected, $mock->has_additional_results($result)); } /** * Data provider for check_and_handle_api_errors. * * @return array */ public static function check_and_handle_api_errors_provider(): array { return [ '200 http_code' => [ ['http_code' => 200], '', null, null, ], '400 http_code' => [ ['http_code' => 400], 'Unused', 'coding_exception', 'Invalid input parameter passed to DropBox API.', ], '401 http_code' => [ ['http_code' => 401], 'Unused', \repository_dropbox\authentication_exception::class, 'Authentication token expired', ], '409 http_code' => [ ['http_code' => 409], json_decode('{"error": "Some value", "error_summary": "Some data here"}'), 'coding_exception', 'Endpoint specific error: Some data here', ], '429 http_code' => [ ['http_code' => 429], 'Unused', \repository_dropbox\rate_limit_exception::class, 'Rate limit hit', ], '500 http_code' => [ ['http_code' => 500], 'Response body', 'invalid_response_exception', '500: Response body', ], '599 http_code' => [ ['http_code' => 599], 'Response body', 'invalid_response_exception', '599: Response body', ], '600 http_code (invalid, but not officially an error)' => [ ['http_code' => 600], '', null, null, ], ]; } /** * Tests for check_and_handle_api_errors. * * @dataProvider check_and_handle_api_errors_provider * @param object $info The response to test * @param string $data The contented returned by the curl call * @param string $exception The name of the expected exception * @param string $exceptionmessage The expected message in the exception */ public function test_check_and_handle_api_errors($info, $data, $exception, $exceptionmessage): void { $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class) ->disableOriginalConstructor() ->onlyMethods([]) ->getMock(); $mock->info = $info; $rc = new \ReflectionClass(\repository_dropbox\dropbox::class); $rcm = $rc->getMethod('check_and_handle_api_errors'); if ($exception) { $this->expectException($exception); } if ($exceptionmessage) { $this->expectExceptionMessage($exceptionmessage); } $result = $rcm->invoke($mock, $data); $this->assertNull($result); } /** * Data provider for the supports_thumbnail function. * * @return array */ public static function supports_thumbnail_provider(): array { $tests = [ 'Only files support thumbnails' => [ (object) ['.tag' => 'folder'], false, ], 'Dropbox currently only supports thumbnail generation for files under 20MB' => [ (object) [ '.tag' => 'file', 'size' => 21 * 1024 * 1024, ], false, ], 'Unusual file extension containing a working format but ending in a non-working one' => [ (object) [ '.tag' => 'file', 'size' => 100 * 1024, 'path_lower' => 'Example.jpg.pdf', ], false, ], 'Unusual file extension ending in a working extension' => [ (object) [ '.tag' => 'file', 'size' => 100 * 1024, 'path_lower' => 'Example.pdf.jpg', ], true, ], ]; // See docs at https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail. $types = [ 'pdf' => false, 'doc' => false, 'docx' => false, 'jpg' => true, 'jpeg' => true, 'png' => true, 'tiff' => true, 'tif' => true, 'gif' => true, 'bmp' => true, ]; foreach ($types as $type => $result) { $tests["Test support for {$type}"] = [ (object) [ '.tag' => 'file', 'size' => 100 * 1024, 'path_lower' => "example_filename.{$type}", ], $result, ]; } return $tests; } /** * Test the supports_thumbnail function. * * @dataProvider supports_thumbnail_provider * @param object $entry The entry to test * @param bool $expected Whether this entry supports thumbnail generation */ public function test_supports_thumbnail($entry, $expected): void { $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class) ->disableOriginalConstructor() ->onlyMethods([]) ->getMock(); $this->assertEquals($expected, $mock->supports_thumbnail($entry)); } /** * Test that the logout makes a call to the correct revocation endpoint. */ public function test_logout_revocation(): void { $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class) ->disableOriginalConstructor() ->onlyMethods(['fetch_dropbox_data']) ->getMock(); $mock->expects($this->once()) ->method('fetch_dropbox_data') ->with($this->equalTo('auth/token/revoke'), $this->equalTo(null)); $this->assertNull($mock->logout()); } /** * Test that the logout function catches authentication_exception exceptions and discards them. */ public function test_logout_revocation_catch_auth_exception(): void { $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class) ->disableOriginalConstructor() ->onlyMethods(['fetch_dropbox_data']) ->getMock(); $mock->expects($this->once()) ->method('fetch_dropbox_data') ->will($this->throwException(new \repository_dropbox\authentication_exception('Exception should be caught'))); $this->assertNull($mock->logout()); } /** * Test that the logout function does not catch any other exception. */ public function test_logout_revocation_does_not_catch_other_exceptions(): void { $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class) ->disableOriginalConstructor() ->onlyMethods(['fetch_dropbox_data']) ->getMock(); $mock->expects($this->once()) ->method('fetch_dropbox_data') ->will($this->throwException(new \repository_dropbox\rate_limit_exception)); $this->expectException(\repository_dropbox\rate_limit_exception::class); $mock->logout(); } /** * Test basic fetch_dropbox_data function. */ public function test_fetch_dropbox_data_endpoint(): void { $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class) ->disableOriginalConstructor() ->onlyMethods([ 'request', 'get_api_endpoint', 'get_content_endpoint', ]) ->getMock(); $endpoint = 'testEndpoint'; // The fetch_dropbox_data call should be called against the standard endpoint only. $mock->expects($this->once()) ->method('get_api_endpoint') ->with($endpoint) ->will($this->returnValue("https://example.com/api/2/{$endpoint}")); $mock->expects($this->never()) ->method('get_content_endpoint'); $mock->expects($this->once()) ->method('request') ->will($this->returnValue(json_encode([]))); // Make the call. $rc = new \ReflectionClass(\repository_dropbox\dropbox::class); $rcm = $rc->getMethod('fetch_dropbox_data'); $rcm->invoke($mock, $endpoint); } /** * Some Dropbox endpoints require that the POSTFIELDS be set to null exactly. */ public function test_fetch_dropbox_data_postfields_null(): void { $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class) ->disableOriginalConstructor() ->onlyMethods([ 'request', ]) ->getMock(); $endpoint = 'testEndpoint'; $mock->expects($this->once()) ->method('request') ->with($this->anything(), $this->callback(function($d) { return $d['CURLOPT_POSTFIELDS'] === 'null'; })) ->will($this->returnValue(json_encode([]))); // Make the call. $rc = new \ReflectionClass(\repository_dropbox\dropbox::class); $rcm = $rc->getMethod('fetch_dropbox_data'); $rcm->invoke($mock, $endpoint, null); } /** * When data is specified, it should be json_encoded in POSTFIELDS. */ public function test_fetch_dropbox_data_postfields_data(): void { $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class) ->disableOriginalConstructor() ->onlyMethods([ 'request', ]) ->getMock(); $endpoint = 'testEndpoint'; $data = ['something' => 'somevalue']; $mock->expects($this->once()) ->method('request') ->with($this->anything(), $this->callback(function($d) use ($data) { return $d['CURLOPT_POSTFIELDS'] === json_encode($data); })) ->will($this->returnValue(json_encode([]))); // Make the call. $rc = new \ReflectionClass(\repository_dropbox\dropbox::class); $rcm = $rc->getMethod('fetch_dropbox_data'); $rcm->invoke($mock, $endpoint, $data); } /** * When more results are available, these should be fetched until there are no more. */ public function test_fetch_dropbox_data_recurse_on_additional_records(): void { $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class) ->disableOriginalConstructor() ->onlyMethods([ 'request', 'get_api_endpoint', ]) ->getMock(); $endpoint = 'testEndpoint'; $requestinvocations = $this->exactly(3); $mock->expects($requestinvocations) ->method('request') ->willReturnCallback(function () use ($requestinvocations): string { return match (self::getInvocationCount($requestinvocations)) { 1 => json_encode(['has_more' => true, 'cursor' => 'Example', 'matches' => ['foo', 'bar']]), 2 => json_encode(['has_more' => true, 'cursor' => 'Example', 'matches' => ['baz']]), 3 => json_encode(['has_more' => false, 'cursor' => '', 'matches' => ['bum']]), default => $this->fail('Unexpected call to the call() method.'), }; }); // We automatically adjust for the /continue endpoint. $apiinvocations = $this->exactly(3); $mock->expects($apiinvocations) ->method('get_api_endpoint') ->willReturnCallback(function ($endpoint) use ($apiinvocations): string { switch (self::getInvocationCount($apiinvocations)) { case 1: $this->assertEquals('testEndpoint', $endpoint); return 'https://example.com/api/2/testEndpoint'; case 2: $this->assertEquals('testEndpoint/continue', $endpoint); return 'https://example.com/api/2/testEndpoint/continue'; case 3: $this->assertEquals('testEndpoint/continue', $endpoint); return 'https://example.com/api/2/testEndpoint/continue'; default: $this->fail('Unexpected call to the get_api_endpoint() method.'); } }); // Make the call. $rc = new \ReflectionClass(\repository_dropbox\dropbox::class); $rcm = $rc->getMethod('fetch_dropbox_data'); $result = $rcm->invoke($mock, $endpoint, null, 'matches'); $this->assertEquals([ 'foo', 'bar', 'baz', 'bum', ], $result->matches); $this->assertFalse(isset($result->cursor)); $this->assertFalse(isset($result->has_more)); } /** * Base tests for the fetch_dropbox_content function. */ public function test_fetch_dropbox_content(): void { $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class) ->disableOriginalConstructor() ->onlyMethods([ 'request', 'setHeader', 'get_content_endpoint', 'get_api_endpoint', 'check_and_handle_api_errors', ]) ->getMock(); $data = ['exampledata' => 'examplevalue']; $endpoint = 'getContent'; $url = "https://example.com/api/2/{$endpoint}"; $response = 'Example content'; // Only the content endpoint should be called. $mock->expects($this->once()) ->method('get_content_endpoint') ->with($endpoint) ->will($this->returnValue($url)); $mock->expects($this->never()) ->method('get_api_endpoint'); $headerinvocations = $this->exactly(2); $mock->expects($headerinvocations) ->method('setHeader') ->willReturnCallback(function ($header) use ($data, $headerinvocations): void { switch (self::getInvocationCount($headerinvocations)) { case 1: $this->assertEquals('Content-Type: ', $header); break; case 2: $this->assertEquals('Dropbox-API-Arg: ' . json_encode($data), $header); break; default: $this->fail('Unexpected call to the setHeader() method.'); } }); // Only one request should be made, and it should forcibly be a POST. $mock->expects($this->once()) ->method('request') ->with($this->equalTo($url), $this->callback(function($options) { return $options['CURLOPT_POST'] === 1; })) ->willReturn($response); $mock->expects($this->once()) ->method('check_and_handle_api_errors') ->with($this->equalTo($response)) ; // Make the call. $rc = new \ReflectionClass(\repository_dropbox\dropbox::class); $rcm = $rc->getMethod('fetch_dropbox_content'); $result = $rcm->invoke($mock, $endpoint, $data); $this->assertEquals($response, $result); } /** * Test that the get_file_share_info function returns an existing link if one is available. */ public function test_get_file_share_info_existing(): void { $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class) ->disableOriginalConstructor() ->onlyMethods([ 'fetch_dropbox_data', 'normalize_file_share_info', ]) ->getMock(); $id = 'LifeTheUniverseAndEverything'; $file = (object) ['.tag' => 'file', 'id' => $id, 'path_lower' => 'SomeValue']; $sharelink = 'https://example.com/share/link'; // Mock fetch_dropbox_data to return an existing file. $mock->expects($this->once()) ->method('fetch_dropbox_data') ->with( $this->equalTo('sharing/list_shared_links'), $this->equalTo(['path' => $id]) ) ->willReturn((object) ['links' => [$file]]); $mock->expects($this->once()) ->method('normalize_file_share_info') ->with($this->equalTo($file)) ->will($this->returnValue($sharelink)); $this->assertEquals($sharelink, $mock->get_file_share_info($id)); } /** * Test that the get_file_share_info function creates a new link if one is not available. */ public function test_get_file_share_info_new(): void { $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class) ->disableOriginalConstructor() ->onlyMethods([ 'fetch_dropbox_data', 'normalize_file_share_info', ]) ->getMock(); $id = 'LifeTheUniverseAndEverything'; $file = (object) ['.tag' => 'file', 'id' => $id, 'path_lower' => 'SomeValue']; $sharelink = 'https://example.com/share/link'; // Mock fetch_dropbox_data to return an existing file. $fetchinvocations = $this->exactly(2); $mock->expects($fetchinvocations) ->method('fetch_dropbox_data') ->willReturnCallback(function ($path, $values) use ($fetchinvocations, $id, $file): object { switch (self::getInvocationCount($fetchinvocations)) { case 1: $this->assertEquals('sharing/list_shared_links', $path); $this->assertEquals(['path' => $id], $values); return (object) ['links' => []]; case 2: $this->assertEquals('sharing/create_shared_link_with_settings', $path); $this->assertEquals(['path' => $id, 'settings' => ['requested_visibility' => 'public']], $values); return $file; default: $this->fail('Unexpected call to the fetch_dropbox_data() method.'); } }); $mock->expects($this->once()) ->method('normalize_file_share_info') ->with($this->equalTo($file)) ->will($this->returnValue($sharelink)); $this->assertEquals($sharelink, $mock->get_file_share_info($id)); } /** * Test failure behaviour with get_file_share_info fails to create a new link. */ public function test_get_file_share_info_new_failure(): void { $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class) ->disableOriginalConstructor() ->onlyMethods([ 'fetch_dropbox_data', 'normalize_file_share_info', ]) ->getMock(); $id = 'LifeTheUniverseAndEverything'; // Mock fetch_dropbox_data to return an existing file. $mock->expects($this->exactly(2)) ->method('fetch_dropbox_data') ->willReturnCallback(function ($path, $values) use ($id): ?object { switch ($path) { case 'sharing/list_shared_links': $this->assertEquals(['path' => $id], $values); return (object) ['links' => []]; case 'sharing/create_shared_link_with_settings': $this->assertEquals(['path' => $id, 'settings' => ['requested_visibility' => 'public']], $values); return null; default: $this->fail('Unexpected call to the fetch_dropbox_data() method.'); } }); $mock->expects($this->never()) ->method('normalize_file_share_info'); $this->assertNull($mock->get_file_share_info($id)); } }