ÿØÿà 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-Ї Ç PK*\AU|user/db/install.phpnu[. function xmldb_repository_user_install() { global $CFG; $result = true; require_once($CFG->dirroot.'/repository/lib.php'); $user_plugin = new repository_type('user', array(), true); if(!$id = $user_plugin->create(true)) { $result = false; } return $result; } PK*\&user/db/access.phpnu[. /** * Plugin capabilities. * * @package repository_user * @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/user:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'user' => CAP_ALLOW ) ) ); PK*\=:\\ user/lang/en/repository_user.phpnu[. /** * Strings for component 'repository_user', language 'en', branch 'MOODLE_20_STABLE' * * @package repository_user * @copyright 2010 Dongsheng Cai * @author Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['configplugin'] = 'Configuration for user private files repository'; $string['pluginname_help'] = 'Files in user private area'; $string['pluginname'] = 'Private files'; $string['emptyfilelist'] = 'There are no files to show'; $string['user:view'] = 'View user private files'; $string['privacy:metadata'] = 'The Private files repository plugin does not store or transmit any personal data.'; PK*\2%user/pix/icon.svgnu[ PK*\$user/pix/icon.pngnu[PNG  IHDR^`>\sRGB, pHYs9]9]C! {IDATx \U;2*BH K - tw [B(AE$,ʠ1#K$ԭNqT@epAQD1l# ,A!p;NѦutCuys.紴B!B!B!B!B!B!BMƕJ2rrC! m,D#ז_#/#@] !y$HsW3cq1_B܍Q7K=B(h[PǏgWC#pkלz~τbHo:Ü W ?DYu |;2=͜qkLr*ri5"0)EJHNdX7tjҶwKBP(9:Eސiᾂ$#! i"$+2>oCXKK3Z .V۰pe&s[rӅGa,{}<@!c"LCMrtB\(`,Ds%H!D9ts>~UKXWXH!DӦM 3@"K˃ta̕&GQ7˜8uHLMbfv$8JkkƘ.?pc9U] !޾1biY%![8~ytB1g |9mMܠ,.?3"ۜ3f)ASȏ}ֵ"gc۟#8 i}7>@y~>dIkLKB%0pI} `p̂2%e[46AO: !h+!$Z"_C^ ޘE+Qb߯cѱt!n "/H Vh !$$'O~OX<e4瑙} 0x*1m 2ABP(^S 6FI!BH /ryޘJ}U,lZ_{UZn[.\27sT%!dC[ޕ-2n<YiQ0Jls-yq|>_FZL}P(/ݿ! ̃K ;;!D ʇ},g!I=)+-N6H5BRY+ܒYqsu;$>ko y}'?I;BR;3QD2~HH*0ޚ]x1v`3򹮵uc~IH"ikkAvfw(0|t%$1ԅk}Ł8rt%D5c1 Ԍ\ w Qn҃j~L rܦ0Y-=pD2 dL6mrc2jЧIq))7Uzȅ5߻V.߯dƚ w;׿nu/+JOs{2ͦI_B!S,hXy߲Io7Nd/ݵJ9@@3 utR.Ê,҃IOJ`f!~l{IJ#r%<~;l3擺N麦<>.#5{獝15^-9a0;?cRI.[?:Ee|qmٸIJ YH8i ccڲͦawiw%L%P߹ers%,(=fV$H9 ½M\lb.T*$90ÝNUM0]vA斫jfiߕD; 掎Iz ya|2=>+ZY3AV 1r||S`4K3_ATSxlS`{<:3҃aȜ=7F9>/<]xmsUa1x(eю|=Y瀄Ҝ{zDvG0GM 9àɤ–MJK9ӕ)FCtttl ~Y'=0V39g`z/--Eì/ޛ]<fFt.d`)k{o6q꡷<*/f8?-1k H|Ǔq4kp\lL{gY͎B`l'>SnxI/j谷1xYt⃚QGbSDW5t2/4&:~]_;nq/ P*SZ7#UD##2 aN6vwLrОӷp`1iOq@'ǟ_,i0aIo7# K 3t?-X[h Ә> g#yQ\ -fubp3\`.z͢8 g{N=^?3kTlOl7rvaq90T5|1ɵ{2vąČw`FZ{Bp''+.Xt3ԝxEՎn6\~p9>{/b*^-tQ07F($[^X21eb݂}ۃ9{ yf%0tdӧo.]рZ#>bKzGYL]vz sf- vUvc~~{,l .>b`Ӄܽ:v+H^><;Z?=Xzn\5WnD/CtW[raZ;H{3C74[SH7Tc^[oӷ V-*)ߞug >cb jЕ-](1GbI%tNǬPEhY{1GK7x"u==[kmkk5UE4 7eVyxBtmkuA}WcZJO;)tQJҺv ) e9ߐ jrjk5ԁuAΩUφ. rÌw)I!]cԪE⵵-?o{'k-lWo~;EOe e3!5fjn"eSڋY"tQ U >mN+(]cٖxMi,S gYS$}d. ̢1ҵ:xOkl|Hs]@6K%]_'^ck񮑮Mjs[kc dAi85EJ&IsD 5- 3?*u[vZMxt}m*DbxqjI'kkl`. 7-J/v;AśȧU?$|ƛ셎B׬D6 X>ϛpV6{Nƶ9[6Uklm5/^ԲHHH9{ks5d;|m%9R[,${ƛtz+Ip 1xJ^3@ wa†v9x rPzxBvf7nS@ '+uXn (^Pzx[[,?f-DxBmy m!ūW/oKFMn%~Ɛ ūԋsj3Ŭ#v(^PzIx!}cchXxB%-7+|>K m+^(^^f{fś;(^Pzxe͊T*$^(^PzIx!ӋC7qkRzxBJW/chXxB-٬xmHK ^B&.AKŋ0;.A Z1KlPzxzB]a QCūW//DzzHCūW//dzrHCūW//DzDȻ,AK[,w)DKK[(2!O5CūW/B}9|&B w7!f/XnXxBm~;xYnXxBmvF lY ūOB>6$V(^PzxP] C zjC @.ۮ#v(^PzxjB,^(^P(J&;B[@ ;pD"PzxBzv6^(^Pr=kq;A Ż"8ÈA W/^(!0{>^xƗM(!(m`{@m ūW/ׇ@W/^( ~eQšxBlQšxBRt`]"PzxByXb="PzxBپ(Ω (^Pzx3 u_6x<5 ֍#yaxBmGE ūI ¾ kE ū7Cȓ0^(^P!tO4[EPzxB~k͑0^(^P!)"QA(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śr(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śr(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śr(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śr(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śr(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śrz| ڤV.vVgH&xJV sK!B!B!B!B!B!B!B!B!B!B哏&IENDB`PK*\`h user/lib.phpnu[. /** * This plugin is used to access user's private files * * @since Moodle 2.0 * @package repository_user * @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_user class is used to browse user private files * * @since Moodle 2.0 * @package repository_user * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_user extends repository { /** * user plugin doesn't require login * * @return mixed */ public function print_login() { return $this->get_listing(); } /** * Get file listing * * @param string $encodedpath * @return mixed */ public function get_listing($encodedpath = '', $page = '') { global $CFG, $USER, $OUTPUT; $ret = array(); $ret['dynload'] = true; $ret['nosearch'] = true; $ret['nologin'] = true; $manageurl = new moodle_url('/user/files.php'); $ret['manage'] = $manageurl->out(); $list = array(); if (!empty($encodedpath)) { $params = json_decode(base64_decode($encodedpath), true); if (is_array($params)) { $filepath = clean_param($params['filepath'], PARAM_PATH); $filename = clean_param($params['filename'], PARAM_FILE); } } else { $itemid = 0; $filepath = '/'; $filename = null; } $filearea = 'private'; $component = 'user'; $itemid = 0; $context = context_user::instance($USER->id); try { $browser = get_file_browser(); if ($fileinfo = $browser->get_file_info($context, $component, $filearea, $itemid, $filepath, $filename)) { $pathnodes = array(); $level = $fileinfo; $params = $fileinfo->get_params(); while ($level && $params['component'] == 'user' && $params['filearea'] == 'private') { $encodedpath = base64_encode(json_encode($level->get_params())); $pathnodes[] = array('name'=>$level->get_visible_name(), 'path'=>$encodedpath); $level = $level->get_parent(); $params = $level->get_params(); } $ret['path'] = array_reverse($pathnodes); // build file tree $children = $fileinfo->get_children(); foreach ($children as $child) { if ($child->is_directory()) { $encodedpath = base64_encode(json_encode($child->get_params())); $node = array( 'title' => $child->get_visible_name(), 'datemodified' => $child->get_timemodified(), 'datecreated' => $child->get_timecreated(), 'path' => $encodedpath, 'children'=>array(), 'thumbnail' => $OUTPUT->image_url(file_folder_icon())->out(false) ); $list[] = $node; } else { $encodedpath = base64_encode(json_encode($child->get_params())); $node = array( 'title' => $child->get_visible_name(), 'size' => $child->get_filesize(), 'datemodified' => $child->get_timemodified(), 'datecreated' => $child->get_timecreated(), 'author' => $child->get_author(), 'license' => $child->get_license(), 'isref' => $child->is_external_file(), 'source'=> $encodedpath, 'icon' => $OUTPUT->image_url(file_file_icon($child))->out(false), 'thumbnail' => $OUTPUT->image_url(file_file_icon($child))->out(false) ); if ($child->get_status() == 666) { $node['originalmissing'] = true; } if ($imageinfo = $child->get_imageinfo()) { $fileurl = new moodle_url($child->get_url()); $node['realthumbnail'] = $fileurl->out(false, array('preview' => 'thumb', 'oid' => $child->get_timemodified())); $node['realicon'] = $fileurl->out(false, array('preview' => 'tinyicon', 'oid' => $child->get_timemodified())); $node['image_width'] = $imageinfo['width']; $node['image_height'] = $imageinfo['height']; } $list[] = $node; } } } } catch (Exception $e) { throw new repository_exception('emptyfilelist', 'repository_user'); } $ret['list'] = $list; $ret['list'] = array_filter($list, array($this, 'filter')); return $ret; } /** * Does this repository used to browse moodle files? * * @return boolean */ public function has_moodle_files() { return true; } /** * User cannot use the external link to dropbox * * @return int */ public function supported_returntypes() { return FILE_INTERNAL | FILE_REFERENCE; } /** * Is this repository accessing private data? * * @return bool */ public function contains_private_data() { return false; } #[\Override] public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, ?array $options = null) { // If the file is linked to the original then we should not cache it. $lifetime = 0; parent::send_file($storedfile, $lifetime, $filter, $forcedownload, $options); } } PK*\n9user/version.phpnu[. /** * Version details * * @package repository * @subpackage user * @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_user'; // Full name of the plugin (used for diagnostics) PK*\p!user/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for repository_user. * * @package repository_user * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_user\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for repository_user implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK*\*?user/tests/generator/lib.phpnu[. /** * User private files repository data generator * * @package repository_user * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * User private files repository data generator class * * @package repository_user * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_user_generator extends testing_repository_generator { } PK*\d==repository_ajax.phpnu[. /** * The Web service script that is called from the filepicker front end * * @since Moodle 2.0 * @package repository * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('AJAX_SCRIPT', true); require_once(__DIR__ . '/../config.php'); require_once(__DIR__ . '/../lib/filelib.php'); require_once(__DIR__.'/lib.php'); $err = new stdClass(); // Parameters $action = optional_param('action', '', PARAM_ALPHA); $repo_id = optional_param('repo_id', 0, PARAM_INT); // Repository ID $contextid = optional_param('ctx_id', SYSCONTEXTID, PARAM_INT); // Context ID $env = optional_param('env', 'filepicker', PARAM_ALPHA); // Opened in editor or moodleform $license = optional_param('license', $CFG->sitedefaultlicense, PARAM_TEXT); $author = optional_param('author', '', PARAM_TEXT); // File author $source = optional_param('source', '', PARAM_RAW); // File to download $sourcekey = optional_param('sourcekey', '', PARAM_RAW); // Used to verify the source. $itemid = optional_param('itemid', 0, PARAM_INT); // Itemid $page = optional_param('page', '', PARAM_RAW); // Page $maxbytes = optional_param('maxbytes', 0, PARAM_INT); // Maxbytes $req_path = optional_param('p', '', PARAM_RAW); // Path $accepted_types = optional_param_array('accepted_types', '*', PARAM_RAW); $saveas_filename = optional_param('title', '', PARAM_FILE); // save as file name $areamaxbytes = optional_param('areamaxbytes', FILE_AREA_MAX_BYTES_UNLIMITED, PARAM_INT); // Area max bytes. $saveas_path = optional_param('savepath', '/', PARAM_PATH); // save as file path $search_text = optional_param('s', '', PARAM_CLEANHTML); $linkexternal = optional_param('linkexternal', '', PARAM_ALPHA); $usefilereference = optional_param('usefilereference', false, PARAM_BOOL); $usecontrolledlink = optional_param('usecontrolledlink', false, PARAM_BOOL); list($context, $course, $cm) = get_context_info_array($contextid); require_login($course, false, $cm, false, true); $PAGE->set_context($context); echo $OUTPUT->header(); // send headers // If uploaded file is larger than post_max_size (php.ini) setting, $_POST content will be empty. if (empty($_POST) && !empty($action)) { $err->error = get_string('errorpostmaxsize', 'repository'); die(json_encode($err)); } if (!confirm_sesskey()) { $err->error = get_string('invalidsesskey', 'error'); die(json_encode($err)); } // Get repository instance information $repooptions = array( 'ajax' => true, 'mimetypes' => $accepted_types ); ajax_capture_output(); $repo = repository::get_repository_by_id($repo_id, $contextid, $repooptions); // Check permissions $repo->check_capability(); $coursemaxbytes = 0; if (!empty($course)) { $coursemaxbytes = $course->maxbytes; } // Make sure maxbytes passed is within site filesize limits. $maxbytes = get_user_max_upload_file_size($context, $CFG->maxbytes, $coursemaxbytes, $maxbytes); // Wait as long as it takes for this script to finish core_php_time_limit::raise(); // These actions all occur on the currently active repository instance switch ($action) { case 'sign': case 'signin': case 'list': if ($repo->check_login()) { $listing = repository::prepare_listing($repo->get_listing($req_path, $page)); $listing['repo_id'] = $repo_id; ajax_check_captured_output(); echo json_encode($listing); break; } else { $action = 'login'; } case 'login': $listing = $repo->print_login(); $listing['repo_id'] = $repo_id; ajax_check_captured_output(); echo json_encode($listing); break; case 'logout': $logout = $repo->logout(); $logout['repo_id'] = $repo_id; ajax_check_captured_output(); echo json_encode($logout); break; case 'searchform': $search_form['repo_id'] = $repo_id; $search_form['form'] = $repo->print_search(); $search_form['allowcaching'] = true; ajax_check_captured_output(); echo json_encode($search_form); break; case 'search': $search_result = repository::prepare_listing($repo->search($search_text, (int)$page)); $search_result['repo_id'] = $repo_id; $search_result['issearchresult'] = true; ajax_check_captured_output(); echo json_encode($search_result); break; case 'download': // validate mimetype $mimetypes = array(); if ((is_array($accepted_types) and in_array('*', $accepted_types)) or $accepted_types == '*') { $mimetypes = '*'; } else { foreach ($accepted_types as $type) { $mimetypes[] = mimeinfo('type', $type); } if (!in_array(mimeinfo('type', $saveas_filename), $mimetypes)) { throw new moodle_exception('invalidfiletype', 'repository', '', get_mimetype_description(array('filename' => $saveas_filename))); } } // We have two special repository type need to deal with // local and recent plugins don't added new files to moodle, just add new records to database // so we don't check user quota and maxbytes here $allowexternallink = (int)get_config(null, 'repositoryallowexternallinks'); if (!empty($allowexternallink)) { $allowexternallink = true; } else { $allowexternallink = false; } // allow external links in url element all the time $allowexternallink = ($allowexternallink || ($env == 'url')); // Validate the sourcekey. if (empty($sourcekey)) { throw new moodle_exception('missingsourcekey', 'repository'); } // Check that the sourcekey matches. if (sha1($source . repository::get_secret_key() . sesskey()) !== $sourcekey) { throw new moodle_exception('sourcekeymismatch', 'repository'); } $reference = $repo->get_file_reference($source); // Use link of the files if ($allowexternallink and $linkexternal === 'yes' and ($repo->supported_returntypes() & FILE_EXTERNAL)) { // use external link $link = $repo->get_link($reference); $info = array(); $info['file'] = $saveas_filename; $info['type'] = 'link'; $info['url'] = $link; ajax_check_captured_output(); echo json_encode($info); die; } else { $fs = get_file_storage(); // Prepare file record. $record = new stdClass(); $record->filepath = $saveas_path; $record->filename = $saveas_filename; $record->component = 'user'; $record->filearea = 'draft'; $record->itemid = $itemid; $record->license = $license; $record->author = $author; if ($record->filepath !== '/') { $record->filepath = trim($record->filepath, '/'); $record->filepath = '/'.$record->filepath.'/'; } $usercontext = context_user::instance($USER->id); $now = time(); $record->contextid = $usercontext->id; $record->timecreated = $now; $record->timemodified = $now; $record->userid = $USER->id; $record->sortorder = 0; // Check that user has permission to access this file if (!$repo->file_is_accessible($source)) { throw new file_exception('storedfilecannotread'); } // {@link repository::build_source_field()} $sourcefield = $repo->get_file_source_info($source); $record->source = $repo::build_source_field($sourcefield); // If file is already a reference, set $source = file source, $repo = file repository // note that in this case user may not have permission to access the source file directly // so no file_browser/file_info can be used below if ($repo->has_moodle_files()) { $file = repository::get_moodle_file($reference); if ($file && $file->is_external_file()) { $sourcefield = $file->get_source(); // remember the original source $record->source = $repo::build_source_field($sourcefield); $record->contenthash = $file->get_contenthash(); $record->filesize = $file->get_filesize(); $reference = $file->get_reference(); $repo_id = $file->get_repository_id(); $repo = repository::get_repository_by_id($repo_id, $contextid, $repooptions); } } if ($usefilereference || $usecontrolledlink) { if ($repo->has_moodle_files()) { $sourcefile = repository::get_moodle_file($reference); $record->contenthash = $sourcefile->get_contenthash(); $record->filesize = $sourcefile->get_filesize(); } // Check if file exists. if (repository::draftfile_exists($itemid, $saveas_path, $saveas_filename)) { // File name being used, rename it. $unused_filename = repository::get_unused_filename($itemid, $saveas_path, $saveas_filename); $record->filename = $unused_filename; // Create a file copy using unused filename. $storedfile = $fs->create_file_from_reference($record, $repo_id, $reference); $event = array(); $event['event'] = 'fileexists'; $event['newfile'] = new stdClass; $event['newfile']->filepath = $saveas_path; $event['newfile']->filename = $unused_filename; $event['newfile']->url = moodle_url::make_draftfile_url($itemid, $saveas_path, $unused_filename)->out(); $event['existingfile'] = new stdClass; $event['existingfile']->filepath = $saveas_path; $event['existingfile']->filename = $saveas_filename; $event['existingfile']->url = moodle_url::make_draftfile_url($itemid, $saveas_path, $saveas_filename)->out(); } else { $storedfile = $fs->create_file_from_reference($record, $repo_id, $reference); $event = array( 'url'=>moodle_url::make_draftfile_url($storedfile->get_itemid(), $storedfile->get_filepath(), $storedfile->get_filename())->out(), 'id'=>$storedfile->get_itemid(), 'file'=>$storedfile->get_filename(), 'icon' => $OUTPUT->image_url(file_file_icon($storedfile))->out(), ); } // Repository plugin callback // You can cache reository file in this callback // or complete other tasks. $repo->cache_file_by_reference($reference, $storedfile); ajax_check_captured_output(); echo json_encode($event); die; } else if ($repo->has_moodle_files()) { // Some repository plugins (local, user, coursefiles, recent) are hosting moodle // internal files, we cannot use get_file method, so we use copy_to_area method // If the moodle file is an alias we copy this alias, otherwise we copy the file // {@link repository::copy_to_area()}. $fileinfo = $repo->copy_to_area($reference, $record, $maxbytes, $areamaxbytes); ajax_check_captured_output(); echo json_encode($fileinfo); die; } else { // Download file to moodle. $downloadedfile = $repo->get_file($reference, $saveas_filename); if (!empty($downloadedfile['newfilename'])) { $record->filename = $downloadedfile['newfilename']; } if (empty($downloadedfile['path'])) { $err->error = get_string('cannotdownload', 'repository'); die(json_encode($err)); } // Check if exceed maxbytes. if ($maxbytes != -1 && filesize($downloadedfile['path']) > $maxbytes) { $maxbytesdisplay = display_size($maxbytes, 0); throw new file_exception('maxbytesfile', (object) array('file' => $record->filename, 'size' => $maxbytesdisplay)); } // Check if we exceed the max bytes of the area. if (file_is_draft_area_limit_reached($itemid, $areamaxbytes, filesize($downloadedfile['path']))) { throw new file_exception('maxareabytes'); } // Ensure the user does not upload too many draft files in a short period. if (file_is_draft_areas_limit_reached($USER->id)) { throw new file_exception('maxdraftitemids'); } $info = repository::move_to_filepool($downloadedfile['path'], $record); if (empty($info)) { $info['e'] = get_string('error', 'moodle'); } } ajax_check_captured_output(); echo json_encode($info); die; } break; case 'upload': $result = $repo->upload($saveas_filename, $maxbytes); ajax_check_captured_output(); echo json_encode($result); break; case 'overwrite': // existing file $filepath = required_param('existingfilepath', PARAM_PATH); $filename = required_param('existingfilename', PARAM_FILE); // user added file which needs to replace the existing file $newfilepath = required_param('newfilepath', PARAM_PATH); $newfilename = required_param('newfilename', PARAM_FILE); $info = repository::overwrite_existing_draftfile($itemid, $filepath, $filename, $newfilepath, $newfilename); ajax_check_captured_output(); echo json_encode($info); break; case 'deletetmpfile': // delete tmp file $newfilepath = required_param('newfilepath', PARAM_PATH); $newfilename = required_param('newfilename', PARAM_FILE); ajax_check_captured_output(); echo json_encode(repository::delete_tempfile_from_draft($itemid, $newfilepath, $newfilename)); break; } PK*\s[local/db/install.phpnu[. function xmldb_repository_local_install() { global $CFG; $result = true; require_once($CFG->dirroot.'/repository/lib.php'); $local_plugin = new repository_type('local', array(), true); if(!$id = $local_plugin->create(true)) { $result = false; } return $result; } PK*\0&  local/db/access.phpnu[. /** * Plugin capabilities. * * @package repository_local * @copyright 2009 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/local:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'coursecreator' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ) ); PK*\ ұ"local/lang/en/repository_local.phpnu[. /** * Strings for component 'repository_local', language 'en', branch 'MOODLE_20_STABLE' * * @package repository_local * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['configplugin'] = 'Configuration for server file repository'; $string['currentusefiles'] = 'Currently used files'; $string['emptyfilelist'] = 'There are no files to show'; $string['local:view'] = 'View server repository'; $string['notitle'] = 'notitle'; $string['remember'] = 'Remember me'; $string['pluginname_help'] = 'Files previously uploaded to the Moodle server'; $string['pluginname'] = 'Server files'; $string['privacy:metadata'] = 'The Server files repository plugin does not store or transmit any personal data.'; PK*\local/pix/icon.svgnu[ ]> PK*\3ԭlocal/pix/icon.pngnu[PNG  IHDRatIDATx3Qmo۶U'UltqbL۶m083֯yyC7E4 Yj[VļՇ \ѣ /=16՟4%|̘575tkP-?cY&LnҢN'P7 9`5p(ąs['$&ׯԉG"~~*DP4yZq DUs-R"M1 A +EDޯ'~B5W͖&)imXYy6>1gƍ>8k%@`*@TI[k7J @>HY +. /** * This plugin is used to access local files * * @since Moodle 2.0 * @package repository_local * @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_local class is used to browse moodle files * * @since Moodle 2.0 * @package repository_local * @copyright 2012 Marina Glancy * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_local extends repository { /** * Get file listing * * @param string $encodedpath * @param string $page no paging is used in repository_local * @return mixed */ public function get_listing($encodedpath = '', $page = '') { global $CFG, $USER, $OUTPUT; $ret = array(); $ret['dynload'] = true; $ret['nosearch'] = false; $ret['nologin'] = true; $ret['list'] = array(); $itemid = null; $filename = null; $filearea = null; $filepath = null; $component = null; if (!empty($encodedpath)) { $params = json_decode(base64_decode($encodedpath), true); if (is_array($params) && isset($params['contextid'])) { $component = is_null($params['component']) ? NULL : clean_param($params['component'], PARAM_COMPONENT); $filearea = is_null($params['filearea']) ? NULL : clean_param($params['filearea'], PARAM_AREA); $itemid = is_null($params['itemid']) ? NULL : clean_param($params['itemid'], PARAM_INT); $filepath = is_null($params['filepath']) ? NULL : clean_param($params['filepath'], PARAM_PATH); $filename = is_null($params['filename']) ? NULL : clean_param($params['filename'], PARAM_FILE); $context = context::instance_by_id(clean_param($params['contextid'], PARAM_INT)); } } if (empty($context) && !empty($this->context)) { $context = $this->context->get_course_context(false); } if (empty($context)) { $context = context_system::instance(); } // prepare list of allowed extensions: $extensions is either string '*' // or array of lowercase extensions, i.e. array('.gif','.jpg') $extensions = optional_param_array('accepted_types', '', PARAM_RAW); if (empty($extensions) || $extensions === '*' || (is_array($extensions) && in_array('*', $extensions))) { $extensions = '*'; } else { if (!is_array($extensions)) { $extensions = array($extensions); } $extensions = array_map('core_text::strtolower', $extensions); } // build file tree $browser = get_file_browser(); if (!($fileinfo = $browser->get_file_info($context, $component, $filearea, $itemid, $filepath, $filename))) { // if file doesn't exist, build path nodes root of current context $fileinfo = $browser->get_file_info($context, null, null, null, null, null); } $ret['list'] = $this->get_non_empty_children($fileinfo, $extensions); // build path navigation $path = array(); for ($level = $fileinfo; $level; $level = $level->get_parent()) { array_unshift($path, $level); } array_unshift($path, null); $ret['path'] = array(); for ($i=1; $ican_skip($path[$i], $extensions, $path[$i-1])) { $ret['path'][] = $this->get_node_path($path[$i]); } } return $ret; } /** * Tells how the file can be picked from this repository * * @return int */ public function supported_returntypes() { return FILE_INTERNAL | FILE_REFERENCE; } /** * Does this repository used to browse moodle files? * * @return boolean */ public function has_moodle_files() { return true; } /** * Returns all children elements that have one of the specified extensions * * This function may skip subfolders and recursively add their children * {@link repository_local::can_skip()} * * @param file_info|null $fileinfo * @param string|string[] $extensions for example '*' or ['.gif', '.jpg'] * @return array array of file_info elements */ private function get_non_empty_children(?file_info $fileinfo, $extensions): array { if ($fileinfo === null) { return []; } $nonemptychildren = $fileinfo->get_non_empty_children($extensions); $list = array(); foreach ($nonemptychildren as $child) { if ($this->can_skip($child, $extensions, $fileinfo)) { $list = array_merge($list, $this->get_non_empty_children($child, $extensions)); } else { $list[] = $this->get_node($child); } } return $list; } /** * Whether this folder may be skipped in folder hierarchy * * 1. Skip the name of a single filearea in a module * 2. Skip course categories for non-admins who do not have navshowmycoursecategories setting * * @param file_info $fileinfo * @param string|array $extensions, for example '*' or array('.gif','.jpg') * @param file_info|int $parent specify parent here if we know it to avoid creating extra objects * @return bool */ private function can_skip(file_info $fileinfo, $extensions, $parent = -1) { global $CFG; if (!$fileinfo->is_directory()) { // do not skip files return false; } if ($fileinfo instanceof file_info_context_course || $fileinfo instanceof file_info_context_user || $fileinfo instanceof file_info_area_course_legacy || $fileinfo instanceof file_info_context_module || $fileinfo instanceof file_info_context_system) { // These instances can never be filearea inside an activity, they will never be skipped. return false; } else if ($fileinfo instanceof file_info_context_coursecat) { // This is a course category. For non-admins we do not display categories return empty($CFG->navshowmycoursecategories) && !has_capability('moodle/course:update', context_system::instance()); } else { $params = $fileinfo->get_params(); if (strlen($params['filearea']) && ($params['filepath'] === '/' || empty($params['filepath'])) && ($params['filename'] === '.' || empty($params['filename'])) && context::instance_by_id($params['contextid'])->contextlevel == CONTEXT_MODULE) { if ($parent === -1) { $parent = $fileinfo->get_parent(); } // This is a filearea inside an activity, it can be skipped if it has no non-empty siblings if ($parent && ($parent instanceof file_info_context_module)) { if ($parent->count_non_empty_children($extensions, 2) <= 1) { return true; } } } } return false; } /** * Converts file_info object to element of repository return list * * @param file_info $fileinfo * @return array */ private function get_node(file_info $fileinfo) { global $OUTPUT; $encodedpath = base64_encode(json_encode($fileinfo->get_params())); $node = array( 'title' => $fileinfo->get_visible_name(), 'datemodified' => $fileinfo->get_timemodified(), 'datecreated' => $fileinfo->get_timecreated() ); if ($fileinfo->is_directory()) { $node['path'] = $encodedpath; $node['thumbnail'] = $OUTPUT->image_url(file_folder_icon())->out(false); $node['children'] = array(); } else { $node['size'] = $fileinfo->get_filesize(); $node['author'] = $fileinfo->get_author(); $node['license'] = $fileinfo->get_license(); $node['isref'] = $fileinfo->is_external_file(); if ($fileinfo->get_status() == 666) { $node['originalmissing'] = true; } $node['source'] = $encodedpath; $node['thumbnail'] = $OUTPUT->image_url(file_file_icon($fileinfo))->out(false); $node['icon'] = $OUTPUT->image_url(file_file_icon($fileinfo))->out(false); if ($imageinfo = $fileinfo->get_imageinfo()) { // what a beautiful picture, isn't it $fileurl = new moodle_url($fileinfo->get_url()); $node['realthumbnail'] = $fileurl->out(false, array('preview' => 'thumb', 'oid' => $fileinfo->get_timemodified())); $node['realicon'] = $fileurl->out(false, array('preview' => 'tinyicon', 'oid' => $fileinfo->get_timemodified())); $node['image_width'] = $imageinfo['width']; $node['image_height'] = $imageinfo['height']; } } return $node; } /** * Converts file_info object to element of repository return path * * @param file_info $fileinfo * @return array */ private function get_node_path(file_info $fileinfo) { $encodedpath = base64_encode(json_encode($fileinfo->get_params())); return array( 'path' => $encodedpath, 'name' => $fileinfo->get_visible_name() ); } /** * Search through all the files. * * This method will do a raw search through the database, then will try * to match with files that a user can access. A maximum of 50 files will be * returned at a time, excluding possible duplicates found along the way. * * Queries are done in chunk of 100 files to prevent too many records to be fetched * at once. When too many files are not included, or a maximum of 10 queries are * performed we consider that this was the last page. * * @param String $q The query string. * @param integer $page The page number. * @return array of results. */ public function search($q, $page = 1) { global $DB, $SESSION; // Because the repository API is weird, the first page is 0, but it should be 1. if (!$page) { $page = 1; } if (!isset($SESSION->repository_local_search)) { $SESSION->repository_local_search = array(); } $fs = get_file_storage(); $fb = get_file_browser(); $max = 50; $limit = 100; if ($page <= 1) { $SESSION->repository_local_search['query'] = $q; $SESSION->repository_local_search['from'] = 0; $from = 0; } else { // Yes, the repository does not send the query again... $q = $SESSION->repository_local_search['query']; $from = (int) $SESSION->repository_local_search['from']; } $count = $fs->search_server_files('%' . $DB->sql_like_escape($q) . '%', null, null, true); $remaining = $count - $from; $maxloops = 3000; $loops = 0; $results = array(); while (count($results) < $max && $maxloops > 0 && $remaining > 0) { if (empty($files)) { $files = $fs->search_server_files('%' . $DB->sql_like_escape($q) . '%', $from, $limit); $from += $limit; }; $remaining--; $maxloops--; $loops++; $file = array_shift($files); if (!$file) { // This should not happen. throw new coding_exception('Unexpected end of files list.'); } $key = $file->get_contenthash() . ':' . $file->get_filename(); if (isset($results[$key])) { // We found the file with same content and same name, let's skip it. continue; } $ctx = context::instance_by_id($file->get_contextid()); $fileinfo = $fb->get_file_info($ctx, $file->get_component(), $file->get_filearea(), $file->get_itemid(), $file->get_filepath(), $file->get_filename()); if ($fileinfo) { $results[$key] = $this->get_node($fileinfo); } } // Save the position for the paging to work. if ($maxloops > 0 && $remaining > 0) { $SESSION->repository_local_search['from'] += $loops; $pages = -1; } else { $SESSION->repository_local_search['from'] = 0; $pages = 0; } $return = array( 'list' => array_values($results), 'dynload' => true, 'nosearch' => false, 'nologin' => true, 'pages' => $pages, 'page' => $page ); return $return; } /** * Is this repository accessing private data? * * @return bool */ public function contains_private_data() { return false; } } PK*\local/version.phpnu[. /** * Version details * * @package repository * @subpackage local * @copyright 2009 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_local'; // Full name of the plugin (used for diagnostics) PK*\iR"local/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for repository_local. * * @package repository_local * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_local\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for repository_local implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK*\O+local/tests/generator/lib.phpnu[. /** * Local repository data generator * * @package repository_local * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Local repository data generator class * * @package repository_local * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_local_generator extends testing_repository_generator { } PK*\[1lib.phpnu[. /** * This file contains classes used to manage the repository plugins in Moodle * * @since Moodle 2.0 * @package core_repository * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/filelib.php'); require_once($CFG->libdir . '/formslib.php'); define('FILE_EXTERNAL', 1); define('FILE_INTERNAL', 2); define('FILE_REFERENCE', 4); define('FILE_CONTROLLED_LINK', 8); define('RENAME_SUFFIX', '_2'); /** * This class is used to manage repository plugins * * A repository_type is a repository plug-in. It can be Box.net, Flick-r, ... * A repository type can be edited, sorted and hidden. It is mandatory for an * administrator to create a repository type in order to be able to create * some instances of this type. * Coding note: * - a repository_type object is mapped to the "repository" database table * - "typename" attibut maps the "type" database field. It is unique. * - general "options" for a repository type are saved in the config_plugin table * - when you delete a repository, all instances are deleted, and general * options are also deleted from database * - When you create a type for a plugin that can't have multiple instances, a * instance is automatically created. * * @package core_repository * @copyright 2009 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_type implements cacheable_object { /** * Type name (no whitespace) - A type name is unique * Note: for a user-friendly type name see get_readablename() * @var String */ private $_typename; /** * Options of this type * They are general options that any instance of this type would share * e.g. API key * These options are saved in config_plugin table * @var array */ private $_options; /** * Is the repository type visible or hidden * If false (hidden): no instances can be created, edited, deleted, showned , used... * @var boolean */ private $_visible; /** * 0 => not ordered, 1 => first position, 2 => second position... * A not order type would appear in first position (should never happened) * @var integer */ private $_sortorder; /** * Return if the instance is visible in a context * * @todo check if the context visibility has been overwritten by the plugin creator * (need to create special functions to be overvwritten in repository class) * @param stdClass $context context * @return bool */ public function get_contextvisibility($context) { global $USER; if ($context->contextlevel == CONTEXT_COURSE) { return $this->_options['enablecourseinstances']; } if ($context->contextlevel == CONTEXT_USER) { return $this->_options['enableuserinstances']; } //the context is SITE return true; } /** * repository_type constructor * * @param int $typename * @param array $typeoptions * @param bool $visible * @param int $sortorder (don't really need set, it will be during create() call) */ public function __construct($typename = '', $typeoptions = array(), $visible = true, $sortorder = 0) { global $CFG; //set type attributs $this->_typename = $typename; $this->_visible = $visible; $this->_sortorder = $sortorder; //set options attribut $this->_options = array(); $options = repository::static_function($typename, 'get_type_option_names'); //check that the type can be setup if (!empty($options)) { //set the type options foreach ($options as $config) { if (array_key_exists($config, $typeoptions)) { $this->_options[$config] = $typeoptions[$config]; } } } //retrieve visibility from option if (array_key_exists('enablecourseinstances',$typeoptions)) { $this->_options['enablecourseinstances'] = $typeoptions['enablecourseinstances']; } else { $this->_options['enablecourseinstances'] = 0; } if (array_key_exists('enableuserinstances',$typeoptions)) { $this->_options['enableuserinstances'] = $typeoptions['enableuserinstances']; } else { $this->_options['enableuserinstances'] = 0; } } /** * Get the type name (no whitespace) * For a human readable name, use get_readablename() * * @return string the type name */ public function get_typename() { return $this->_typename; } /** * Return a human readable and user-friendly type name * * @return string user-friendly type name */ public function get_readablename() { return get_string('pluginname','repository_'.$this->_typename); } /** * Return general options * * @return array the general options */ public function get_options() { return $this->_options; } /** * Return visibility * * @return bool */ public function get_visible() { return $this->_visible; } /** * Return order / position of display in the file picker * * @return int */ public function get_sortorder() { return $this->_sortorder; } /** * Create a repository type (the type name must not already exist) * @param bool $silent throw exception? * @return mixed return int if create successfully, return false if */ public function create($silent = false) { global $DB; //check that $type has been set $timmedtype = trim($this->_typename); if (empty($timmedtype)) { throw new repository_exception('emptytype', 'repository'); } //set sortorder as the last position in the list if (!isset($this->_sortorder) || $this->_sortorder == 0 ) { $sql = "SELECT MAX(sortorder) FROM {repository}"; $this->_sortorder = 1 + $DB->get_field_sql($sql); } //only create a new type if it doesn't already exist $existingtype = $DB->get_record('repository', array('type'=>$this->_typename)); if (!$existingtype) { //create the type $newtype = new stdClass(); $newtype->type = $this->_typename; $newtype->visible = $this->_visible; $newtype->sortorder = $this->_sortorder; $plugin_id = $DB->insert_record('repository', $newtype); //save the options in DB $this->update_options(); $instanceoptionnames = repository::static_function($this->_typename, 'get_instance_option_names'); //if the plugin type has no multiple instance (e.g. has no instance option name) so it wont //be possible for the administrator to create a instance //in this case we need to create an instance if (empty($instanceoptionnames)) { $instanceoptions = array(); if (empty($this->_options['pluginname'])) { // when moodle trying to install some repo plugin automatically // this option will be empty, get it from language string when display $instanceoptions['name'] = ''; } else { // when admin trying to add a plugin manually, he will type a name // for it $instanceoptions['name'] = $this->_options['pluginname']; } repository::static_function($this->_typename, 'create', $this->_typename, 0, context_system::instance(), $instanceoptions); } //run plugin_init function if (!repository::static_function($this->_typename, 'plugin_init')) { $this->update_visibility(false); if (!$silent) { throw new repository_exception('cannotinitplugin', 'repository'); } } cache::make('core', 'repositories')->purge(); if(!empty($plugin_id)) { // return plugin_id if create successfully return $plugin_id; } else { return false; } } else { if (!$silent) { throw new repository_exception('existingrepository', 'repository'); } // If plugin existed, return false, tell caller no new plugins were created. return false; } } /** * Update plugin options into the config_plugin table * * @param array $options * @return bool */ public function update_options($options = null) { global $DB; $classname = 'repository_' . $this->_typename; $instanceoptions = repository::static_function($this->_typename, 'get_instance_option_names'); if (empty($instanceoptions)) { // update repository instance name if this plugin type doesn't have muliti instances $params = array(); $params['type'] = $this->_typename; $instances = repository::get_instances($params); $instance = array_pop($instances); if ($instance) { $DB->set_field('repository_instances', 'name', $options['pluginname'], array('id'=>$instance->id)); } unset($options['pluginname']); } if (!empty($options)) { $this->_options = $options; } foreach ($this->_options as $name => $value) { set_config($name, $value, $this->_typename); } cache::make('core', 'repositories')->purge(); return true; } /** * Update visible database field with the value given as parameter * or with the visible value of this object * This function is private. * For public access, have a look to switch_and_update_visibility() * * @param bool $visible * @return bool */ private function update_visible($visible = null) { global $DB; if (!empty($visible)) { $this->_visible = $visible; } else if (!isset($this->_visible)) { throw new repository_exception('updateemptyvisible', 'repository'); } cache::make('core', 'repositories')->purge(); return $DB->set_field('repository', 'visible', $this->_visible, array('type'=>$this->_typename)); } /** * Update database sortorder field with the value given as parameter * or with the sortorder value of this object * This function is private. * For public access, have a look to move_order() * * @param int $sortorder * @return bool */ private function update_sortorder($sortorder = null) { global $DB; if (!empty($sortorder) && $sortorder!=0) { $this->_sortorder = $sortorder; } //if sortorder is not set, we set it as the ;ast position in the list else if (!isset($this->_sortorder) || $this->_sortorder == 0 ) { $sql = "SELECT MAX(sortorder) FROM {repository}"; $this->_sortorder = 1 + $DB->get_field_sql($sql); } cache::make('core', 'repositories')->purge(); return $DB->set_field('repository', 'sortorder', $this->_sortorder, array('type'=>$this->_typename)); } /** * Change order of the type with its adjacent upper or downer type * (database fields are updated) * Algorithm details: * 1. retrieve all types in an array. This array is sorted by sortorder, * and the array keys start from 0 to X (incremented by 1) * 2. switch sortorder values of this type and its adjacent type * * @param string $move "up" or "down" */ public function move_order($move) { global $DB; $types = repository::get_types(); // retrieve all types // retrieve this type into the returned array $i = 0; while (!isset($indice) && $iget_typename() == $this->_typename) { $indice = $i; } $i++; } // retrieve adjacent indice switch ($move) { case "up": $adjacentindice = $indice - 1; break; case "down": $adjacentindice = $indice + 1; break; default: throw new repository_exception('movenotdefined', 'repository'); } //switch sortorder of this type and the adjacent type //TODO: we could reset sortorder for all types. This is not as good in performance term, but //that prevent from wrong behaviour on a screwed database. As performance are not important in this particular case //it worth to change the algo. if ($adjacentindice>=0 && !empty($types[$adjacentindice])) { $DB->set_field('repository', 'sortorder', $this->_sortorder, array('type'=>$types[$adjacentindice]->get_typename())); $this->update_sortorder($types[$adjacentindice]->get_sortorder()); } } /** * 1. Change visibility to the value chosen * 2. Update the type * * @param bool $visible * @return bool */ public function update_visibility($visible = null) { if (is_bool($visible)) { $this->_visible = $visible; } else { $this->_visible = !$this->_visible; } return $this->update_visible(); } /** * Delete a repository_type (general options are removed from config_plugin * table, and all instances are deleted) * * @param bool $downloadcontents download external contents if exist * @return bool */ public function delete($downloadcontents = false) { global $DB; //delete all instances of this type $params = array(); $params['context'] = array(); $params['onlyvisible'] = false; $params['type'] = $this->_typename; $instances = repository::get_instances($params); foreach ($instances as $instance) { $instance->delete($downloadcontents); } //delete all general options foreach ($this->_options as $name => $value) { set_config($name, null, $this->_typename); } cache::make('core', 'repositories')->purge(); try { $DB->delete_records('repository', array('type' => $this->_typename)); } catch (dml_exception $ex) { return false; } return true; } /** * Prepares the repository type to be cached. Implements method from cacheable_object interface. * * @return array */ public function prepare_to_cache() { return array( 'typename' => $this->_typename, 'typeoptions' => $this->_options, 'visible' => $this->_visible, 'sortorder' => $this->_sortorder ); } /** * Restores repository type from cache. Implements method from cacheable_object interface. * * @return array */ public static function wake_from_cache($data) { return new repository_type($data['typename'], $data['typeoptions'], $data['visible'], $data['sortorder']); } } /** * This is the base class of the repository class. * * To create repository plugin, see: {@link https://moodledev.io/docs/apis/plugintypes/repository} * See an example: repository_dropbox * * @package core_repository * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class repository implements cacheable_object { /** * Timeout in seconds for downloading the external file into moodle * @deprecated since Moodle 2.7, please use $CFG->repositorygetfiletimeout instead */ const GETFILE_TIMEOUT = 30; /** * Timeout in seconds for syncronising the external file size * @deprecated since Moodle 2.7, please use $CFG->repositorysyncfiletimeout instead */ const SYNCFILE_TIMEOUT = 1; /** * Timeout in seconds for downloading an image file from external repository during syncronisation * @deprecated since Moodle 2.7, please use $CFG->repositorysyncimagetimeout instead */ const SYNCIMAGE_TIMEOUT = 3; // $disabled can be set to true to disable a plugin by force // example: self::$disabled = true /** @var bool force disable repository instance */ public $disabled = false; /** @var int repository instance id */ public $id; /** @var context current context */ public $context; /** @var array repository options */ public $options; /** @var bool Whether or not the repository instance is editable */ public $readonly; /** @var int return types */ public $returntypes; /** @var stdClass repository instance database record */ public $instance; /** @var string Type of repository (webdav, google_docs, dropbox, ...). Read from $this->get_typename(). */ protected $typename; /** @var string instance name. */ public $name; /** @var bool true if the super construct is called, otherwise false. */ public $super_called; /** @var array List of file ids currently being synced, to avoid endless recursion */ protected static $syncfileids = []; /** * Constructor * * @param int $repositoryid repository instance id * @param int|stdClass $context a context id or context object * @param array $options repository options * @param int $readonly indicate this repo is readonly or not */ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) { global $DB; $this->id = $repositoryid; if (is_object($context)) { $this->context = $context; } else { $this->context = context::instance_by_id($context); } $cache = cache::make('core', 'repositories'); if (($this->instance = $cache->get('i:'. $this->id)) === false) { $this->instance = $DB->get_record_sql("SELECT i.*, r.type AS repositorytype, r.sortorder, r.visible FROM {repository} r, {repository_instances} i WHERE i.typeid = r.id and i.id = ?", array('id' => $this->id)); $cache->set('i:'. $this->id, $this->instance); } $this->readonly = $readonly; $this->options = array(); if (is_array($options)) { // The get_option() method will get stored options in database. $options = array_merge($this->get_option(), $options); } else { $options = $this->get_option(); } foreach ($options as $n => $v) { $this->options[$n] = $v; } $this->name = $this->get_name(); $this->returntypes = $this->supported_returntypes(); $this->super_called = true; } /** * Get repository instance using repository id * * Note that this function does not check permission to access repository contents * * @throws repository_exception * * @param int $repositoryid repository instance ID * @param context|int $context context instance or context ID where this repository will be used * @param array $options additional repository options * @return repository */ public static function get_repository_by_id($repositoryid, $context, $options = array()) { global $CFG, $DB; $cache = cache::make('core', 'repositories'); if (!is_object($context)) { $context = context::instance_by_id($context); } $cachekey = 'rep:'. $repositoryid. ':'. $context->id. ':'. serialize($options); if ($repository = $cache->get($cachekey)) { return $repository; } if (!$record = $cache->get('i:'. $repositoryid)) { $sql = "SELECT i.*, r.type AS repositorytype, r.visible, r.sortorder FROM {repository_instances} i JOIN {repository} r ON r.id = i.typeid WHERE i.id = ?"; if (!$record = $DB->get_record_sql($sql, array($repositoryid))) { throw new repository_exception('invalidrepositoryid', 'repository'); } $cache->set('i:'. $record->id, $record); } $type = $record->repositorytype; if (file_exists($CFG->dirroot . "/repository/$type/lib.php")) { require_once($CFG->dirroot . "/repository/$type/lib.php"); $classname = 'repository_' . $type; $options['type'] = $type; $options['typeid'] = $record->typeid; $options['visible'] = $record->visible; if (empty($options['name'])) { $options['name'] = $record->name; } $repository = new $classname($repositoryid, $context, $options, $record->readonly); if (empty($repository->super_called)) { // to make sure the super construct is called debugging('parent::__construct must be called by '.$type.' plugin.'); } $cache->set($cachekey, $repository); return $repository; } else { throw new repository_exception('invalidplugin', 'repository'); } } /** * Returns the type name of the repository. * * @return string type name of the repository. * @since Moodle 2.5 */ public function get_typename() { if (empty($this->typename)) { $matches = array(); if (!preg_match("/^repository_(.*)$/", get_class($this), $matches)) { throw new coding_exception('The class name of a repository should be repository_, '. 'e.g. repository_dropbox'); } $this->typename = $matches[1]; } return $this->typename; } /** * Get a repository type object by a given type name. * * @static * @param string $typename the repository type name * @return repository_type|bool */ public static function get_type_by_typename($typename) { global $DB; $cache = cache::make('core', 'repositories'); if (($repositorytype = $cache->get('typename:'. $typename)) === false) { $repositorytype = null; if ($record = $DB->get_record('repository', array('type' => $typename))) { $repositorytype = new repository_type($record->type, (array)get_config($record->type), $record->visible, $record->sortorder); $cache->set('typeid:'. $record->id, $repositorytype); } $cache->set('typename:'. $typename, $repositorytype); } return $repositorytype; } /** * Get the repository type by a given repository type id. * * @static * @param int $id the type id * @return object */ public static function get_type_by_id($id) { global $DB; $cache = cache::make('core', 'repositories'); if (($repositorytype = $cache->get('typeid:'. $id)) === false) { $repositorytype = null; if ($record = $DB->get_record('repository', array('id' => $id))) { $repositorytype = new repository_type($record->type, (array)get_config($record->type), $record->visible, $record->sortorder); $cache->set('typename:'. $record->type, $repositorytype); } $cache->set('typeid:'. $id, $repositorytype); } return $repositorytype; } /** * Return all repository types ordered by sortorder field * first repository type in returnedarray[0], second repository type in returnedarray[1], ... * * @static * @param bool $visible can return types by visiblity, return all types if null * @return array Repository types */ public static function get_types($visible=null) { global $DB, $CFG; $cache = cache::make('core', 'repositories'); if (!$visible) { $typesnames = $cache->get('types'); } else { $typesnames = $cache->get('typesvis'); } $types = array(); if ($typesnames === false) { $typesnames = array(); $vistypesnames = array(); if ($records = $DB->get_records('repository', null ,'sortorder')) { foreach($records as $type) { if (($repositorytype = $cache->get('typename:'. $type->type)) === false) { // Create new instance of repository_type. if (file_exists($CFG->dirroot . '/repository/'. $type->type .'/lib.php')) { $repositorytype = new repository_type($type->type, (array)get_config($type->type), $type->visible, $type->sortorder); $cache->set('typeid:'. $type->id, $repositorytype); $cache->set('typename:'. $type->type, $repositorytype); } } if ($repositorytype) { if (empty($visible) || $repositorytype->get_visible()) { $types[] = $repositorytype; $vistypesnames[] = $repositorytype->get_typename(); } $typesnames[] = $repositorytype->get_typename(); } } } $cache->set('types', $typesnames); $cache->set('typesvis', $vistypesnames); } else { foreach ($typesnames as $typename) { $types[] = self::get_type_by_typename($typename); } } return $types; } /** * Checks if user has a capability to view the current repository. * * @return bool true when the user can, otherwise throws an exception. * @throws repository_exception when the user does not meet the requirements. */ final public function check_capability() { global $USER; // The context we are on. $currentcontext = $this->context; // Ensure that the user can view the repository in the current context. $can = has_capability('repository/'.$this->get_typename().':view', $currentcontext); // Context in which the repository has been created. $repocontext = context::instance_by_id($this->instance->contextid); // Prevent access to private repositories when logged in as. if ($can && \core\session\manager::is_loggedinas()) { if ($this->contains_private_data() || $repocontext->contextlevel == CONTEXT_USER) { $can = false; } } // We are going to ensure that the current context was legit, and reliable to check // the capability against. (No need to do that if we already cannot). if ($can) { if ($repocontext->contextlevel == CONTEXT_USER) { // The repository is a user instance, ensure we're the right user to access it! if ($repocontext->instanceid != $USER->id) { $can = false; } } else if ($repocontext->contextlevel == CONTEXT_COURSE) { // The repository is a course one. Let's check that we are on the right course. if (in_array($currentcontext->contextlevel, array(CONTEXT_COURSE, CONTEXT_MODULE, CONTEXT_BLOCK))) { $coursecontext = $currentcontext->get_course_context(); if ($coursecontext->instanceid != $repocontext->instanceid) { $can = false; } } else { // We are on a parent context, therefore it's legit to check the permissions // in the current context. } } else { // Nothing to check here, system instances can have different permissions on different // levels. We do not want to prevent URL hack here, because it does not make sense to // prevent a user to access a repository in a context if it's accessible in another one. } } if ($can) { return true; } throw new repository_exception('nopermissiontoaccess', 'repository'); } /** * Check if file already exists in draft area. * * @static * @param int $itemid of the draft area. * @param string $filepath path to the file. * @param string $filename file name. * @return bool */ public static function draftfile_exists($itemid, $filepath, $filename) { global $USER; $fs = get_file_storage(); $usercontext = context_user::instance($USER->id); return $fs->file_exists($usercontext->id, 'user', 'draft', $itemid, $filepath, $filename); } /** * Parses the moodle file reference and returns an instance of stored_file * * @param string $reference reference to the moodle internal file as retruned by * {@link repository::get_file_reference()} or {@link file_storage::pack_reference()} * @return stored_file|null */ public static function get_moodle_file($reference) { $params = file_storage::unpack_reference($reference, true); $fs = get_file_storage(); return $fs->get_file($params['contextid'], $params['component'], $params['filearea'], $params['itemid'], $params['filepath'], $params['filename']); } /** * Repository method to make sure that user can access particular file. * * This is checked when user tries to pick the file from repository to deal with * potential parameter substitutions is request * * @param string $source source of the file, returned by repository as 'source' and received back from user (not cleaned) * @return bool whether the file is accessible by current user */ public function file_is_accessible($source) { if ($this->has_moodle_files()) { $reference = $this->get_file_reference($source); try { $params = file_storage::unpack_reference($reference, true); } catch (file_reference_exception $e) { return false; } $browser = get_file_browser(); $context = context::instance_by_id($params['contextid']); $file_info = $browser->get_file_info($context, $params['component'], $params['filearea'], $params['itemid'], $params['filepath'], $params['filename']); return !empty($file_info); } return true; } /** * This function is used to copy a moodle file to draft area. * * It DOES NOT check if the user is allowed to access this file because the actual file * can be located in the area where user does not have access to but there is an alias * to this file in the area where user CAN access it. * {@link file_is_accessible} should be called for alias location before calling this function. * * @param string $source The metainfo of file, it is base64 encoded php serialized data * @param stdClass|array $filerecord contains itemid, filepath, filename and optionally other * attributes of the new file * @param int $maxbytes maximum allowed size of file, -1 if unlimited. If size of file exceeds * the limit, the file_exception is thrown. * @param int $areamaxbytes the maximum size of the area. A file_exception is thrown if the * new file will reach the limit. * @return array The information about the created file */ public function copy_to_area($source, $filerecord, $maxbytes = -1, $areamaxbytes = FILE_AREA_MAX_BYTES_UNLIMITED) { global $USER; $fs = get_file_storage(); if ($this->has_moodle_files() == false) { throw new coding_exception('Only repository used to browse moodle files can use repository::copy_to_area()'); } $user_context = context_user::instance($USER->id); $filerecord = (array)$filerecord; // make sure the new file will be created in user draft area $filerecord['component'] = 'user'; $filerecord['filearea'] = 'draft'; $filerecord['contextid'] = $user_context->id; $draftitemid = $filerecord['itemid']; $new_filepath = $filerecord['filepath']; $new_filename = $filerecord['filename']; // the file needs to copied to draft area $stored_file = self::get_moodle_file($source); if ($maxbytes != -1 && $stored_file->get_filesize() > $maxbytes) { $maxbytesdisplay = display_size($maxbytes, 0); throw new file_exception('maxbytesfile', (object) array('file' => $filerecord['filename'], 'size' => $maxbytesdisplay)); } // Validate the size of the draft area. if (file_is_draft_area_limit_reached($draftitemid, $areamaxbytes, $stored_file->get_filesize())) { throw new file_exception('maxareabytes'); } if (repository::draftfile_exists($draftitemid, $new_filepath, $new_filename)) { // create new file $unused_filename = repository::get_unused_filename($draftitemid, $new_filepath, $new_filename); $filerecord['filename'] = $unused_filename; $fs->create_file_from_storedfile($filerecord, $stored_file); $event = array(); $event['event'] = 'fileexists'; $event['newfile'] = new stdClass; $event['newfile']->filepath = $new_filepath; $event['newfile']->filename = $unused_filename; $event['newfile']->url = moodle_url::make_draftfile_url($draftitemid, $new_filepath, $unused_filename)->out(); $event['existingfile'] = new stdClass; $event['existingfile']->filepath = $new_filepath; $event['existingfile']->filename = $new_filename; $event['existingfile']->url = moodle_url::make_draftfile_url($draftitemid, $new_filepath, $new_filename)->out(); return $event; } else { $fs->create_file_from_storedfile($filerecord, $stored_file); $info = array(); $info['itemid'] = $draftitemid; $info['file'] = $new_filename; $info['title'] = $new_filename; $info['contextid'] = $user_context->id; $info['url'] = moodle_url::make_draftfile_url($draftitemid, $new_filepath, $new_filename)->out(); $info['filesize'] = $stored_file->get_filesize(); return $info; } } /** * Get an unused filename from the current draft area. * * Will check if the file ends with ([0-9]) and increase the number. * * @static * @param int $itemid draft item ID. * @param string $filepath path to the file. * @param string $filename name of the file. * @return string an unused file name. */ public static function get_unused_filename($itemid, $filepath, $filename) { global $USER; $contextid = context_user::instance($USER->id)->id; $fs = get_file_storage(); return $fs->get_unused_filename($contextid, 'user', 'draft', $itemid, $filepath, $filename); } /** * Append a suffix to filename. * * @static * @param string $filename * @return string * @deprecated since 2.5 */ public static function append_suffix($filename) { debugging('The function repository::append_suffix() has been deprecated. Use repository::get_unused_filename() instead.', DEBUG_DEVELOPER); $pathinfo = pathinfo($filename); if (empty($pathinfo['extension'])) { return $filename . RENAME_SUFFIX; } else { return $pathinfo['filename'] . RENAME_SUFFIX . '.' . $pathinfo['extension']; } } /** * Return all types that you a user can create/edit and which are also visible * Note: Mostly used in order to know if at least one editable type can be set * * @static * @param stdClass $context the context for which we want the editable types * @return array types */ public static function get_editable_types($context = null) { if (empty($context)) { $context = context_system::instance(); } $types= repository::get_types(true); $editabletypes = array(); foreach ($types as $type) { $instanceoptionnames = repository::static_function($type->get_typename(), 'get_instance_option_names'); if (!empty($instanceoptionnames)) { if ($type->get_contextvisibility($context)) { $editabletypes[]=$type; } } } return $editabletypes; } /** * Return repository instances * * @static * @param array $args Array containing the following keys: * currentcontext : instance of context (default system context) * context : array of instances of context (default empty array) * onlyvisible : bool (default true) * type : string return instances of this type only * accepted_types : string|array return instances that contain files of those types (*, web_image, .pdf, ...) * return_types : int combination of FILE_INTERNAL & FILE_EXTERNAL & FILE_REFERENCE & FILE_CONTROLLED_LINK. * 0 means every type. The default is FILE_INTERNAL | FILE_EXTERNAL. * userid : int if specified, instances belonging to other users will not be returned * * @return array repository instances */ public static function get_instances($args = array()) { global $DB, $CFG, $USER; // Fill $args attributes with default values unless specified if (isset($args['currentcontext'])) { if ($args['currentcontext'] instanceof context) { $current_context = $args['currentcontext']; } else { debugging('currentcontext passed to repository::get_instances was ' . 'not a context object. Using system context instead, but ' . 'you should probably fix your code.', DEBUG_DEVELOPER); $current_context = context_system::instance(); } } else { $current_context = context_system::instance(); } $args['currentcontext'] = $current_context->id; $contextids = array(); if (!empty($args['context'])) { foreach ($args['context'] as $context) { $contextids[] = $context->id; } } $args['context'] = $contextids; if (!isset($args['onlyvisible'])) { $args['onlyvisible'] = true; } if (!isset($args['return_types'])) { $args['return_types'] = FILE_INTERNAL | FILE_EXTERNAL; } if (!isset($args['type'])) { $args['type'] = null; } if (empty($args['disable_types']) || !is_array($args['disable_types'])) { $args['disable_types'] = null; } if (empty($args['userid']) || !is_numeric($args['userid'])) { $args['userid'] = null; } if (!isset($args['accepted_types']) || (is_array($args['accepted_types']) && in_array('*', $args['accepted_types']))) { $args['accepted_types'] = '*'; } ksort($args); $cachekey = 'all:'. serialize($args); // Check if we have cached list of repositories with the same query $cache = cache::make('core', 'repositories'); if (($cachedrepositories = $cache->get($cachekey)) !== false) { // convert from cacheable_object_array to array $repositories = array(); foreach ($cachedrepositories as $repository) { $repositories[$repository->id] = $repository; } return $repositories; } // Prepare DB SQL query to retrieve repositories $params = array(); $sql = "SELECT i.*, r.type AS repositorytype, r.sortorder, r.visible FROM {repository} r, {repository_instances} i WHERE i.typeid = r.id "; if ($args['disable_types']) { list($types, $p) = $DB->get_in_or_equal($args['disable_types'], SQL_PARAMS_NAMED, 'distype', false); $sql .= " AND r.type $types"; $params = array_merge($params, $p); } if ($args['userid']) { $sql .= " AND (i.userid = 0 or i.userid = :userid)"; $params['userid'] = $args['userid']; } if ($args['context']) { list($ctxsql, $p2) = $DB->get_in_or_equal($args['context'], SQL_PARAMS_NAMED, 'ctx'); $sql .= " AND i.contextid $ctxsql"; $params = array_merge($params, $p2); } if ($args['onlyvisible'] == true) { $sql .= " AND r.visible = 1"; } if ($args['type'] !== null) { $sql .= " AND r.type = :type"; $params['type'] = $args['type']; } $sql .= " ORDER BY r.sortorder, i.name"; if (!$records = $DB->get_records_sql($sql, $params)) { $records = array(); } $repositories = array(); // Sortorder should be unique, which is not true if we use $record->sortorder // and there are multiple instances of any repository type $sortorder = 1; foreach ($records as $record) { $cache->set('i:'. $record->id, $record); if (!file_exists($CFG->dirroot . '/repository/'. $record->repositorytype.'/lib.php')) { continue; } $repository = self::get_repository_by_id($record->id, $current_context); $repository->options['sortorder'] = $sortorder++; $is_supported = true; // check mimetypes if ($args['accepted_types'] !== '*' and $repository->supported_filetypes() !== '*') { $accepted_ext = file_get_typegroup('extension', $args['accepted_types']); $supported_ext = file_get_typegroup('extension', $repository->supported_filetypes()); $valid_ext = array_intersect($accepted_ext, $supported_ext); $is_supported = !empty($valid_ext); } // Check return values. if (!empty($args['return_types']) && !($repository->supported_returntypes() & $args['return_types'])) { $is_supported = false; } if (!$args['onlyvisible'] || ($repository->is_visible() && !$repository->disabled)) { // check capability in current context $capability = has_capability('repository/'.$record->repositorytype.':view', $current_context); if ($record->repositorytype == 'coursefiles') { // coursefiles plugin needs managefiles permission $capability = $capability && has_capability('moodle/course:managefiles', $current_context); } if ($is_supported && $capability) { $repositories[$repository->id] = $repository; } } } $cache->set($cachekey, new cacheable_object_array($repositories)); return $repositories; } /** * Get single repository instance for administrative actions * * Do not use this function to access repository contents, because it * does not set the current context * * @see repository::get_repository_by_id() * * @static * @param integer $id repository instance id * @return repository */ public static function get_instance($id) { return self::get_repository_by_id($id, context_system::instance()); } /** * Call a static function. Any additional arguments than plugin and function will be passed through. * * @static * @param string $plugin repository plugin name * @param string $function function name * @return mixed */ public static function static_function($plugin, $function) { global $CFG; //check that the plugin exists $typedirectory = $CFG->dirroot . '/repository/'. $plugin . '/lib.php'; if (!file_exists($typedirectory)) { //throw new repository_exception('invalidplugin', 'repository'); return false; } $args = func_get_args(); if (count($args) <= 2) { $args = array(); } else { array_shift($args); array_shift($args); } require_once($typedirectory); return call_user_func_array(array('repository_' . $plugin, $function), $args); } /** * Scan file, throws exception in case of infected file. * * Please note that the scanning engine must be able to access the file, * permissions of the file are not modified here! * * @static * @deprecated since Moodle 3.0 * @param string $thefile * @param string $filename name of the file * @param bool $deleteinfected */ public static function antivir_scan_file($thefile, $filename, $deleteinfected) { debugging('Please upgrade your code to use \core\antivirus\manager::scan_file instead', DEBUG_DEVELOPER); \core\antivirus\manager::scan_file($thefile, $filename, $deleteinfected); } /** * Repository method to serve the referenced file * * @see send_stored_file * * @param stored_file $storedfile the file that contains the reference * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime) * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin * @param array $options additional options affecting the file serving */ public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, ?array $options = null) { if ($this->has_moodle_files()) { $fs = get_file_storage(); $params = file_storage::unpack_reference($storedfile->get_reference(), true); $srcfile = null; if (is_array($params)) { $srcfile = $fs->get_file($params['contextid'], $params['component'], $params['filearea'], $params['itemid'], $params['filepath'], $params['filename']); } if (empty($options)) { $options = array(); } if (!isset($options['filename'])) { $options['filename'] = $storedfile->get_filename(); } if (!$srcfile) { send_file_not_found(); } else { send_stored_file($srcfile, $lifetime, $filter, $forcedownload, $options); } } else { throw new coding_exception("Repository plugin must implement send_file() method."); } } /** * Return human readable reference information * * @param string $reference value of DB field files_reference.reference * @param int $filestatus status of the file, 0 - ok, 666 - source missing * @return string */ public function get_reference_details($reference, $filestatus = 0) { if ($this->has_moodle_files()) { $fileinfo = null; $params = file_storage::unpack_reference($reference, true); if (is_array($params)) { $context = context::instance_by_id($params['contextid'], IGNORE_MISSING); if ($context) { $browser = get_file_browser(); $fileinfo = $browser->get_file_info($context, $params['component'], $params['filearea'], $params['itemid'], $params['filepath'], $params['filename']); } } if (empty($fileinfo)) { if ($filestatus == 666) { if (is_siteadmin() || ($context && has_capability('moodle/course:managefiles', $context))) { return get_string('lostsource', 'repository', $params['contextid']. '/'. $params['component']. '/'. $params['filearea']. '/'. $params['itemid']. $params['filepath']. $params['filename']); } else { return get_string('lostsource', 'repository', ''); } } return get_string('undisclosedsource', 'repository'); } else { return $fileinfo->get_readable_fullname(); } } return ''; } /** * Cache file from external repository by reference * {@link repository::get_file_reference()} * {@link repository::get_file()} * Invoked at MOODLE/repository/repository_ajax.php * * @param string $reference this reference is generated by * repository::get_file_reference() * @param stored_file $storedfile created file reference */ public function cache_file_by_reference($reference, $storedfile) { } /** * reference_file_selected * * This function is called when a controlled link file is selected in a file picker and the form is * saved. The expected behaviour for repositories supporting controlled links is to * - copy the file to the moodle system account * - put it in a folder that reflects the context it is being used * - make sure the sharing permissions are correct (read-only with the link) * - return a new reference string pointing to the newly copied file. * * @param string $reference this reference is generated by * repository::get_file_reference() * @param context $context the target context for this new file. * @param string $component the target component for this new file. * @param string $filearea the target filearea for this new file. * @param string $itemid the target itemid for this new file. * @return string updated reference (final one before it's saved to db). */ public function reference_file_selected($reference, $context, $component, $filearea, $itemid) { return $reference; } /** * 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. * * @param string $source source of the file, returned by repository as 'source' and received back from user (not cleaned) * @return string|null */ public function get_file_source_info($source) { if ($this->has_moodle_files()) { $reference = $this->get_file_reference($source); return $this->get_reference_details($reference, 0); } return $source; } /** * Move file from download folder to file pool using FILE API * * @todo MDL-28637 * @static * @param string $thefile file path in download folder * @param stdClass $record * @return array containing the following keys: * icon * file * id * url */ public static function move_to_filepool($thefile, $record) { global $DB, $CFG, $USER, $OUTPUT; // scan for viruses if possible, throws exception if problem found // TODO: MDL-28637 this repository_no_delete is a bloody hack! \core\antivirus\manager::scan_file($thefile, $record->filename, empty($CFG->repository_no_delete)); $fs = get_file_storage(); // If file name being used. if (repository::draftfile_exists($record->itemid, $record->filepath, $record->filename)) { $draftitemid = $record->itemid; $new_filename = repository::get_unused_filename($draftitemid, $record->filepath, $record->filename); $old_filename = $record->filename; // Create a tmp file. $record->filename = $new_filename; $newfile = $fs->create_file_from_pathname($record, $thefile); $event = array(); $event['event'] = 'fileexists'; $event['newfile'] = new stdClass; $event['newfile']->filepath = $record->filepath; $event['newfile']->filename = $new_filename; $event['newfile']->url = moodle_url::make_draftfile_url($draftitemid, $record->filepath, $new_filename)->out(); $event['existingfile'] = new stdClass; $event['existingfile']->filepath = $record->filepath; $event['existingfile']->filename = $old_filename; $event['existingfile']->url = moodle_url::make_draftfile_url($draftitemid, $record->filepath, $old_filename)->out(); return $event; } if ($file = $fs->create_file_from_pathname($record, $thefile)) { if (empty($CFG->repository_no_delete)) { $delete = unlink($thefile); unset($CFG->repository_no_delete); } return array( 'url'=>moodle_url::make_draftfile_url($file->get_itemid(), $file->get_filepath(), $file->get_filename())->out(), 'id'=>$file->get_itemid(), 'file'=>$file->get_filename(), 'icon' => $OUTPUT->image_url(file_extension_icon($thefile))->out(), ); } else { return null; } } /** * Builds a tree of files This function is then called recursively. * * @static * @todo take $search into account, and respect a threshold for dynamic loading * @param file_info $fileinfo an object returned by file_browser::get_file_info() * @param string $search searched string * @param bool $dynamicmode no recursive call is done when in dynamic mode * @param array $list the array containing the files under the passed $fileinfo * @return int the number of files found */ public static function build_tree($fileinfo, $search, $dynamicmode, &$list) { global $CFG, $OUTPUT; $filecount = 0; $children = $fileinfo->get_children(); foreach ($children as $child) { $filename = $child->get_visible_name(); $filesize = $child->get_filesize(); $filesize = $filesize ? display_size($filesize) : ''; $filedate = $child->get_timemodified(); $filedate = $filedate ? userdate($filedate) : ''; $filetype = $child->get_mimetype(); if ($child->is_directory()) { $path = array(); $level = $child->get_parent(); while ($level) { $params = $level->get_params(); $path[] = array($params['filepath'], $level->get_visible_name()); $level = $level->get_parent(); } $tmp = array( 'title' => $child->get_visible_name(), 'size' => 0, 'date' => $filedate, 'path' => array_reverse($path), 'thumbnail' => $OUTPUT->image_url(file_folder_icon())->out(false) ); //if ($dynamicmode && $child->is_writable()) { // $tmp['children'] = array(); //} else { // if folder name matches search, we send back all files contained. $_search = $search; if ($search && stristr($tmp['title'], $search) !== false) { $_search = false; } $tmp['children'] = array(); $_filecount = repository::build_tree($child, $_search, $dynamicmode, $tmp['children']); if ($search && $_filecount) { $tmp['expanded'] = 1; } //} if (!$search || $_filecount || (stristr($tmp['title'], $search) !== false)) { $filecount += $_filecount; $list[] = $tmp; } } else { // not a directory // skip the file, if we're in search mode and it's not a match if ($search && (stristr($filename, $search) === false)) { continue; } $params = $child->get_params(); $source = serialize(array($params['contextid'], $params['component'], $params['filearea'], $params['itemid'], $params['filepath'], $params['filename'])); $list[] = array( 'title' => $filename, 'size' => $filesize, 'date' => $filedate, //'source' => $child->get_url(), 'source' => base64_encode($source), 'icon' => $OUTPUT->image_url(file_file_icon($child))->out(false), 'thumbnail' => $OUTPUT->image_url(file_file_icon($child))->out(false), ); $filecount++; } } return $filecount; } /** * Display a repository instance list (with edit/delete/create links) * * @static * @param stdClass $context the context for which we display the instance * @param string $typename if set, we display only one type of instance */ public static function display_instances_list($context, $typename = null) { global $CFG, $USER, $OUTPUT; $output = $OUTPUT->box_start('generalbox'); //if the context is SYSTEM, so we call it from administration page $admin = ($context->id == SYSCONTEXTID) ? true : false; if ($admin) { $baseurl = new moodle_url('/admin/repositoryinstance.php'); $output .= $OUTPUT->heading(get_string('siteinstances', 'repository')); } else { $baseurl = new moodle_url('/repository/manage_instances.php', ['contextid' => $context->id]); } $namestr = get_string('name'); $pluginstr = get_string('plugin', 'repository'); $settingsstr = get_string('settings'); $deletestr = get_string('delete'); // Retrieve list of instances. In administration context we want to display all // instances of a type, even if this type is not visible. In course/user context we // want to display only visible instances, but for every type types. The repository::get_instances() // third parameter displays only visible type. $params = array(); $params['context'] = array($context); $params['currentcontext'] = $context; $params['return_types'] = 0; $params['onlyvisible'] = !$admin; $params['type'] = $typename; $instances = repository::get_instances($params); $instancesnumber = count($instances); $alreadyplugins = array(); $table = new html_table(); $table->head = array($namestr, $pluginstr, $settingsstr, $deletestr); $table->align = array('left', 'left', 'center','center'); $table->data = array(); $updowncount = 1; foreach ($instances as $i) { $settings = ''; $delete = ''; $type = repository::get_type_by_id($i->options['typeid']); if ($type->get_contextvisibility($context)) { if (!$i->readonly) { $settingurl = new moodle_url($baseurl); $settingurl->param('type', $i->options['type']); $settingurl->param('edit', $i->id); $settings .= html_writer::link($settingurl, $settingsstr); $deleteurl = new moodle_url($baseurl); $deleteurl->param('delete', $i->id); $deleteurl->param('type', $i->options['type']); $delete .= html_writer::link($deleteurl, $deletestr); } } $type = repository::get_type_by_id($i->options['typeid']); $table->data[] = array(format_string($i->name), $type->get_readablename(), $settings, $delete); //display a grey row if the type is defined as not visible if (isset($type) && !$type->get_visible()) { $table->rowclasses[] = 'dimmed_text'; } else { $table->rowclasses[] = ''; } if (!in_array($i->name, $alreadyplugins)) { $alreadyplugins[] = $i->name; } } $output .= html_writer::table($table); $instancehtml = '
'; $addable = 0; //if no type is set, we can create all type of instance if (!$typename) { $instancehtml .= '

'; $instancehtml .= get_string('createrepository', 'repository'); $instancehtml .= '

    '; $types = repository::get_editable_types($context); foreach ($types as $type) { if (!empty($type) && $type->get_visible()) { // If the user does not have the permission to view the repository, it won't be displayed in // the list of instances. Hiding the link to create new instances will prevent the // user from creating them without being able to find them afterwards, which looks like a bug. if (!has_capability('repository/'.$type->get_typename().':view', $context)) { continue; } $instanceoptionnames = repository::static_function($type->get_typename(), 'get_instance_option_names'); if (!empty($instanceoptionnames)) { $baseurl->param('new', $type->get_typename()); $instancehtml .= '
  • '.get_string('createxxinstance', 'repository', get_string('pluginname', 'repository_'.$type->get_typename())). '
  • '; $baseurl->remove_params('new'); $addable++; } } } $instancehtml .= '
'; } else { $instanceoptionnames = repository::static_function($typename, 'get_instance_option_names'); if (!empty($instanceoptionnames)) { //create a unique type of instance $addable = 1; $baseurl->param('new', $typename); $output .= $OUTPUT->single_button($baseurl, get_string('createinstance', 'repository'), 'get'); $baseurl->remove_params('new'); } } if ($addable) { $instancehtml .= '
'; $output .= $instancehtml; } $output .= $OUTPUT->box_end(); //print the list + creation links print($output); } /** * Prepare file reference information * * @param string $source source of the file, returned by repository as 'source' and received back from user (not cleaned) * @return string file reference, ready to be stored */ public function get_file_reference($source) { if ($source && $this->has_moodle_files()) { $params = @json_decode(base64_decode($source), true); if (!is_array($params) || empty($params['contextid'])) { throw new repository_exception('invalidparams', 'repository'); } $params = array( 'component' => empty($params['component']) ? '' : clean_param($params['component'], PARAM_COMPONENT), 'filearea' => empty($params['filearea']) ? '' : clean_param($params['filearea'], PARAM_AREA), 'itemid' => empty($params['itemid']) ? 0 : clean_param($params['itemid'], PARAM_INT), 'filename' => empty($params['filename']) ? null : clean_param($params['filename'], PARAM_FILE), 'filepath' => empty($params['filepath']) ? null : clean_param($params['filepath'], PARAM_PATH), 'contextid' => clean_param($params['contextid'], PARAM_INT) ); // Check if context exists. if (!context::instance_by_id($params['contextid'], IGNORE_MISSING)) { throw new repository_exception('invalidparams', 'repository'); } return file_storage::pack_reference($params); } return $source; } /** * Get a unique file path in which to save the file. * * The filename returned will be removed at the end of the request and * should not be relied upon to exist in subsequent requests. * * @param string $filename file name * @return file path */ public function prepare_file($filename) { if (empty($filename)) { $filename = 'file'; } return sprintf('%s/%s', make_request_directory(), $filename); } /** * Does this repository used to browse moodle files? * * @return bool */ public function has_moodle_files() { return false; } /** * Return file URL, for most plugins, the parameter is the original * url, but some plugins use a file id, so we need this function to * convert file id to original url. * * @param string $url the url of file * @return string */ public function get_link($url) { return $url; } /** * Downloads a file from external repository and saves it in temp dir * * Function get_file() must be implemented by repositories that support returntypes * FILE_INTERNAL or FILE_REFERENCE. It is invoked to pick up the file and copy it * to moodle. This function is not called for moodle repositories, the function * {@link repository::copy_to_area()} is used instead. * * This function can be overridden by subclass if the files.reference field contains * not just URL or if request should be done differently. * * @see curl * @throws file_exception when error occured * * @param string $url the content of files.reference field, in this implementaion * it is asssumed that it contains the string with URL of the file * @param string $filename filename (without path) to save the downloaded file in the * temporary directory, if omitted or file already exists the new filename will be generated * @return array with elements: * path: internal location of the file * url: URL to the source (from parameters) */ public function get_file($url, $filename = '') { global $CFG; $path = $this->prepare_file($filename); $c = new curl; $result = $c->download_one($url, null, array('filepath' => $path, 'timeout' => $CFG->repositorygetfiletimeout)); if ($result !== true) { throw new moodle_exception('errorwhiledownload', 'repository', '', $result); } return array('path'=>$path, 'url'=>$url); } /** * Downloads the file from external repository and saves it in moodle filepool. * This function is different from {@link repository::sync_reference()} because it has * bigger request timeout and always downloads the content. * * This function is invoked when we try to unlink the file from the source and convert * a reference into a true copy. * * @throws exception when file could not be imported * * @param stored_file $file * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit) */ public function import_external_file_contents(stored_file $file, $maxbytes = 0) { if (!$file->is_external_file()) { // nothing to import if the file is not a reference return; } else if ($file->get_repository_id() != $this->id) { // error debugging('Repository instance id does not match'); return; } else if ($this->has_moodle_files()) { // files that are references to local files are already in moodle filepool // just validate the size if ($maxbytes > 0 && $file->get_filesize() > $maxbytes) { $maxbytesdisplay = display_size($maxbytes, 0); throw new file_exception('maxbytesfile', (object) array('file' => $file->get_filename(), 'size' => $maxbytesdisplay)); } return; } else { if ($maxbytes > 0 && $file->get_filesize() > $maxbytes) { // note that stored_file::get_filesize() also calls synchronisation $maxbytesdisplay = display_size($maxbytes, 0); throw new file_exception('maxbytesfile', (object) array('file' => $file->get_filename(), 'size' => $maxbytesdisplay)); } $fs = get_file_storage(); // If a file has been downloaded, the file record should report both a positive file // size, and a contenthash which does not related to empty content. // If thereis no file size, or the contenthash is for an empty file, then the file has // yet to be successfully downloaded. $contentexists = $file->get_filesize() && !$file->compare_to_string(''); if (!$file->get_status() && $contentexists) { // we already have the content in moodle filepool and it was synchronised recently. // Repositories may overwrite it if they want to force synchronisation anyway! return; } else { // attempt to get a file try { $fileinfo = $this->get_file($file->get_reference()); if (isset($fileinfo['path'])) { $file->set_synchronised_content_from_file($fileinfo['path']); } else { throw new moodle_exception('errorwhiledownload', 'repository', '', ''); } } catch (Exception $e) { if ($contentexists) { // better something than nothing. We have a copy of file. It's sync time // has expired but it is still very likely that it is the last version } else { throw($e); } } } } } /** * @deprecated since Moodle 4.3 */ #[\core\attribute\deprecated(null, reason: 'No longer used', since: '4.3', mdl: 'MDL-50272', final: true)] public function get_file_size() { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); } /** * Return is the instance is visible * (is the type visible ? is the context enable ?) * * @return bool */ public function is_visible() { $type = repository::get_type_by_id($this->options['typeid']); $instanceoptions = repository::static_function($type->get_typename(), 'get_instance_option_names'); if ($type->get_visible()) { //if the instance is unique so it's visible, otherwise check if the instance has a enabled context if (empty($instanceoptions) || $type->get_contextvisibility(context::instance_by_id($this->instance->contextid))) { return true; } } return false; } /** * Can the instance be edited by the current user? * * The property $readonly must not be used within this method because * it only controls if the options from self::get_instance_option_names() * can be edited. * * @return bool true if the user can edit the instance. * @since Moodle 2.5 */ final public function can_be_edited_by_user() { global $USER; // We need to be able to explore the repository. try { $this->check_capability(); } catch (repository_exception $e) { return false; } $repocontext = context::instance_by_id($this->instance->contextid); if ($repocontext->contextlevel == CONTEXT_USER && $repocontext->instanceid != $USER->id) { // If the context of this instance is a user context, we need to be this user. return false; } else if ($repocontext->contextlevel == CONTEXT_MODULE && !has_capability('moodle/course:update', $repocontext)) { // We need to have permissions on the course to edit the instance. return false; } else if ($repocontext->contextlevel == CONTEXT_SYSTEM && !has_capability('moodle/site:config', $repocontext)) { // Do not meet the requirements for the context system. return false; } return true; } /** * Return the name of this instance, can be overridden. * * @return string */ public function get_name() { if ($name = $this->instance->name) { return $name; } else { return get_string('pluginname', 'repository_' . $this->get_typename()); } } /** * Is this repository accessing private data? * * This function should return true for the repositories which access external private * data from a user. This is the case for repositories such as Dropbox, Google Docs or Box.net * which authenticate the user and then store the auth token. * * Of course, many repositories store 'private data', but we only want to set * contains_private_data() to repositories which are external to Moodle and shouldn't be accessed * to by the users having the capability to 'login as' someone else. For instance, the repository * 'Private files' is not considered as private because it's part of Moodle. * * You should not set contains_private_data() to true on repositories which allow different types * of instances as the levels other than 'user' are, by definition, not private. Also * the user instances will be protected when they need to. * * @return boolean True when the repository accesses private external data. * @since Moodle 2.5 */ public function contains_private_data() { return true; } /** * What kind of files will be in this repository? * * @return array return '*' means this repository support any files, otherwise * return mimetypes of files, it can be an array */ public function supported_filetypes() { // return array('text/plain', 'image/gif'); return '*'; } /** * Tells how the file can be picked from this repository * * Maximum value is FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE * * @return int */ public function supported_returntypes() { return (FILE_INTERNAL | FILE_EXTERNAL); } /** * Tells how the file can be picked from this repository * * Maximum value is FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE * * @return int */ public function default_returntype() { return FILE_INTERNAL; } /** * Provide repository instance information for Ajax * * @return stdClass */ final public function get_meta() { global $CFG, $OUTPUT; $meta = new stdClass(); $meta->id = $this->id; $meta->name = format_string($this->get_name()); $meta->type = $this->get_typename(); $meta->icon = $OUTPUT->image_url('icon', 'repository_'.$meta->type)->out(false); $meta->supported_types = file_get_typegroup('extension', $this->supported_filetypes()); $meta->return_types = $this->supported_returntypes(); $meta->defaultreturntype = $this->default_returntype(); $meta->sortorder = $this->options['sortorder']; return $meta; } /** * Create an instance for this plug-in * * @static * @param string $type the type of the repository * @param int $userid the user id * @param stdClass $context the context * @param array $params the options for this instance * @param int $readonly whether to create it readonly or not (defaults to not) * @return mixed */ public static function create($type, $userid, $context, $params, $readonly=0) { global $CFG, $DB; $params = (array)$params; require_once($CFG->dirroot . '/repository/'. $type . '/lib.php'); $classname = 'repository_' . $type; if ($repo = $DB->get_record('repository', array('type'=>$type))) { $record = new stdClass(); $record->name = $params['name']; $record->typeid = $repo->id; $record->timecreated = time(); $record->timemodified = time(); $record->contextid = $context->id; $record->readonly = $readonly; $record->userid = $userid; $id = $DB->insert_record('repository_instances', $record); cache::make('core', 'repositories')->purge(); $options = array(); $configs = call_user_func($classname . '::get_instance_option_names'); if (!empty($configs)) { foreach ($configs as $config) { if (isset($params[$config])) { $options[$config] = $params[$config]; } else { $options[$config] = null; } } } if (!empty($id)) { unset($options['name']); $instance = repository::get_instance($id); $instance->set_option($options); return $id; } else { return null; } } else { return null; } } /** * delete a repository instance * * @param bool $downloadcontents * @return bool */ final public function delete($downloadcontents = false) { global $DB; if ($downloadcontents) { $this->convert_references_to_local(); } else { $this->remove_files(); } cache::make('core', 'repositories')->purge(); try { $DB->delete_records('repository_instances', array('id'=>$this->id)); $DB->delete_records('repository_instance_config', array('instanceid'=>$this->id)); } catch (dml_exception $ex) { return false; } return true; } /** * Delete all the instances associated to a context. * * This method is intended to be a callback when deleting * a course or a user to delete all the instances associated * to their context. The usual way to delete a single instance * is to use {@link self::delete()}. * * @param int $contextid context ID. * @param boolean $downloadcontents true to convert references to hard copies. * @return void */ final public static function delete_all_for_context($contextid, $downloadcontents = true) { global $DB; $repoids = $DB->get_fieldset_select('repository_instances', 'id', 'contextid = :contextid', array('contextid' => $contextid)); if ($downloadcontents) { foreach ($repoids as $repoid) { $repo = repository::get_repository_by_id($repoid, $contextid); $repo->convert_references_to_local(); } } cache::make('core', 'repositories')->purge(); $DB->delete_records_list('repository_instances', 'id', $repoids); $DB->delete_records_list('repository_instance_config', 'instanceid', $repoids); } /** * Hide/Show a repository * * @param string $hide * @return bool */ final public function hide($hide = 'toggle') { global $DB; if ($entry = $DB->get_record('repository', array('id'=>$this->id))) { if ($hide === 'toggle' ) { if (!empty($entry->visible)) { $entry->visible = 0; } else { $entry->visible = 1; } } else { if (!empty($hide)) { $entry->visible = 0; } else { $entry->visible = 1; } } return $DB->update_record('repository', $entry); } return false; } /** * Save settings for repository instance * $repo->set_option(array('api_key'=>'f2188bde132', 'name'=>'dongsheng')); * * @param array $options settings * @return bool */ public function set_option($options = array()) { global $DB; if (!empty($options['name'])) { $r = new stdClass(); $r->id = $this->id; $r->name = $options['name']; $DB->update_record('repository_instances', $r); unset($options['name']); } foreach ($options as $name=>$value) { if ($id = $DB->get_field('repository_instance_config', 'id', array('name'=>$name, 'instanceid'=>$this->id))) { $DB->set_field('repository_instance_config', 'value', $value, array('id'=>$id)); } else { $config = new stdClass(); $config->instanceid = $this->id; $config->name = $name; $config->value = $value; $DB->insert_record('repository_instance_config', $config); } } cache::make('core', 'repositories')->purge(); return true; } /** * Get settings for repository instance. * * @param string $config a specific option to get. * @return mixed returns an array of options. If $config is not empty, then it returns that option, * or null if the option does not exist. */ public function get_option($config = '') { global $DB; $cache = cache::make('core', 'repositories'); if (($entries = $cache->get('ops:'. $this->id)) === false) { $entries = $DB->get_records('repository_instance_config', array('instanceid' => $this->id)); $cache->set('ops:'. $this->id, $entries); } $ret = array(); foreach($entries as $entry) { $ret[$entry->name] = $entry->value; } if (!empty($config)) { if (isset($ret[$config])) { return $ret[$config]; } else { return null; } } else { return $ret; } } /** * Filter file listing to display specific types * * @param array $value * @return bool */ public function filter($value) { $accepted_types = optional_param_array('accepted_types', '', PARAM_RAW); if (isset($value['children'])) { return true; // always return directories } else { if ($accepted_types == '*' or empty($accepted_types) or (is_array($accepted_types) and in_array('*', $accepted_types))) { return true; } else { foreach ($accepted_types as $ext) { if (preg_match('#'.$ext.'$#i', $value['title'])) { return true; } } } } return false; } /** * Given a path, and perhaps a search, get a list of files. * * See details on {@link https://moodledev.io/docs/apis/plugintypes/repository} * * @param string $path this parameter can a folder name, or a identification of folder * @param string $page the page number of file list * @return array the list of files, including meta infomation, containing the following keys * manage, url to manage url * client_id * login, login form * repo_id, active repository id * login_btn_action, the login button action * login_btn_label, the login button label * total, number of results * perpage, items per page * page * pages, total pages * issearchresult, is it a search result? * list, file list * path, current path and parent path */ public function get_listing($path = '', $page = '') { } /** * Prepare the breadcrumb. * * @param array $breadcrumb contains each element of the breadcrumb. * @return array of breadcrumb elements. * @since Moodle 2.3.3 */ protected static function prepare_breadcrumb($breadcrumb) { global $OUTPUT; $foldericon = $OUTPUT->image_url(file_folder_icon())->out(false); $len = count($breadcrumb); for ($i = 0; $i < $len; $i++) { if (is_array($breadcrumb[$i]) && !isset($breadcrumb[$i]['icon'])) { $breadcrumb[$i]['icon'] = $foldericon; } else if (is_object($breadcrumb[$i]) && !isset($breadcrumb[$i]->icon)) { $breadcrumb[$i]->icon = $foldericon; } } return $breadcrumb; } /** * Prepare the file/folder listing. * * @param array $list of files and folders. * @return array of files and folders. * @since Moodle 2.3.3 */ protected static function prepare_list($list) { global $OUTPUT; $foldericon = $OUTPUT->image_url(file_folder_icon())->out(false); // Reset the array keys because non-numeric keys will create an object when converted to JSON. $list = array_values($list); $len = count($list); for ($i = 0; $i < $len; $i++) { if (is_object($list[$i])) { $file = (array)$list[$i]; $converttoobject = true; } else { $file =& $list[$i]; $converttoobject = false; } if (isset($file['source'])) { $file['sourcekey'] = sha1($file['source'] . self::get_secret_key() . sesskey()); } if (isset($file['size'])) { $file['size'] = (int)$file['size']; $file['size_f'] = display_size($file['size']); } if (isset($file['license']) && get_string_manager()->string_exists($file['license'], 'license')) { $file['license_f'] = get_string($file['license'], 'license'); } if (isset($file['image_width']) && isset($file['image_height'])) { $a = array('width' => $file['image_width'], 'height' => $file['image_height']); $file['dimensions'] = get_string('imagesize', 'repository', (object)$a); } foreach (array('date', 'datemodified', 'datecreated') as $key) { if (!isset($file[$key]) && isset($file['date'])) { $file[$key] = $file['date']; } if (isset($file[$key])) { // must be UNIX timestamp $file[$key] = (int)$file[$key]; if (!$file[$key]) { unset($file[$key]); } else { $file[$key.'_f'] = userdate($file[$key], get_string('strftimedatetime', 'langconfig')); $file[$key.'_f_s'] = userdate($file[$key], get_string('strftimedatetimeshort', 'langconfig')); } } } $isfolder = (array_key_exists('children', $file) || (isset($file['type']) && $file['type'] == 'folder')); $filename = null; if (isset($file['title'])) { $filename = $file['title']; } else if (isset($file['fullname'])) { $filename = $file['fullname']; } if (!isset($file['mimetype']) && !$isfolder && $filename) { $file['mimetype'] = get_mimetype_description(array('filename' => $filename)); } if (!isset($file['icon'])) { if ($isfolder) { $file['icon'] = $foldericon; } else if ($filename) { $file['icon'] = $OUTPUT->image_url(file_extension_icon($filename))->out(false); } } // Recursively loop over children. if (isset($file['children'])) { $file['children'] = self::prepare_list($file['children']); } // Convert the array back to an object. if ($converttoobject) { $list[$i] = (object)$file; } } return $list; } /** * Prepares list of files before passing it to AJAX, makes sure data is in the correct * format and stores formatted values. * * @param array|stdClass $listing result of get_listing() or search() or file_get_drafarea_files() * @return stdClass */ public static function prepare_listing($listing) { $wasobject = false; if (is_object($listing)) { $listing = (array) $listing; $wasobject = true; } // Prepare the breadcrumb, passed as 'path'. if (isset($listing['path']) && is_array($listing['path'])) { $listing['path'] = self::prepare_breadcrumb($listing['path']); } // Prepare the listing of objects. if (isset($listing['list']) && is_array($listing['list'])) { $listing['list'] = self::prepare_list($listing['list']); } // Convert back to an object. if ($wasobject) { $listing = (object) $listing; } return $listing; } /** * Search files in repository * When doing global search, $search_text will be used as * keyword. * * @param string $search_text search key word * @param int $page page * @return mixed see {@link repository::get_listing()} */ public function search($search_text, $page = 0) { $list = array(); $list['list'] = array(); return false; } /** * Logout from repository instance * By default, this function will return a login form * * @return string */ public function logout(){ return $this->print_login(); } /** * To check whether the user is logged in. * * @return bool */ public function check_login(){ return true; } /** * Show the login screen, if required * * @return string */ public function print_login(){ return $this->get_listing(); } /** * Show the search screen, if required * * @return string */ public function print_search() { global $PAGE; $renderer = $PAGE->get_renderer('core', 'files'); return $renderer->repository_default_searchform(); } /** * For oauth like external authentication, when external repository direct user back to moodle, * this function will be called to set up token and token_secret */ public function callback() { } /** * is it possible to do glboal search? * * @return bool */ public function global_search() { return false; } /** * Defines operations that happen occasionally on cron * * @return bool */ public function cron() { return true; } /** * function which is run when the type is created (moodle administrator add the plugin) * * @return bool success or fail? */ public static function plugin_init() { return true; } /** * Edit/Create Admin Settings Moodle form * * @param MoodleQuickForm $mform Moodle form (passed by reference) * @param string $classname repository class name */ public static function type_config_form($mform, $classname = 'repository') { $instnaceoptions = call_user_func(array($classname, 'get_instance_option_names'), $mform, $classname); if (empty($instnaceoptions)) { // this plugin has only one instance // so we need to give it a name // it can be empty, then moodle will look for instance name from language string $mform->addElement('text', 'pluginname', get_string('pluginname', 'repository'), array('size' => '40')); $mform->addElement('static', 'pluginnamehelp', '', get_string('pluginnamehelp', 'repository')); $mform->setType('pluginname', PARAM_TEXT); } } /** * Validate Admin Settings Moodle form * * @static * @param moodleform $mform Moodle form (passed by reference) * @param array $data array of ("fieldname"=>value) of submitted data * @param array $errors array of ("fieldname"=>errormessage) of errors * @return array array of errors */ public static function type_form_validation($mform, $data, $errors) { return $errors; } /** * Edit/Create Instance Settings Moodle form * * @param moodleform $mform Moodle form (passed by reference) */ public static function instance_config_form($mform) { } /** * Return names of the general options. * By default: no general option name * * @return array */ public static function get_type_option_names() { return array('pluginname'); } /** * Return names of the instance options. * By default: no instance option name * * @return array */ public static function get_instance_option_names() { return array(); } /** * Validate repository plugin instance form * * @param moodleform $mform moodle form * @param array $data form data * @param array $errors errors * @return array errors */ public static function instance_form_validation($mform, $data, $errors) { return $errors; } /** * Create a shorten filename * * @param string $str filename * @param int $maxlength max file name length * @return string short filename */ public function get_short_filename($str, $maxlength) { if (core_text::strlen($str) >= $maxlength) { return trim(core_text::substr($str, 0, $maxlength)).'...'; } else { return $str; } } /** * Overwrite an existing file * * @param int $itemid * @param string $filepath * @param string $filename * @param string $newfilepath * @param string $newfilename * @return bool */ public static function overwrite_existing_draftfile($itemid, $filepath, $filename, $newfilepath, $newfilename) { global $USER; $fs = get_file_storage(); $user_context = context_user::instance($USER->id); if ($file = $fs->get_file($user_context->id, 'user', 'draft', $itemid, $filepath, $filename)) { if ($tempfile = $fs->get_file($user_context->id, 'user', 'draft', $itemid, $newfilepath, $newfilename)) { // Remember original file source field. $source = unserialize_object($file->get_source()); // Remember the original sortorder. $sortorder = $file->get_sortorder(); if ($tempfile->is_external_file()) { // New file is a reference. Check that existing file does not have any other files referencing to it if (isset($source->original) && $fs->search_references_count($source->original)) { return (object)array('error' => get_string('errordoublereference', 'repository')); } } // delete existing file to release filename $file->delete(); // create new file $newfile = $fs->create_file_from_storedfile(array('filepath'=>$filepath, 'filename'=>$filename), $tempfile); // Preserve original file location (stored in source field) for handling references if (isset($source->original)) { $newfilesource = unserialize_object($newfile->get_source()); $newfilesource->original = $source->original; $newfile->set_source(serialize($newfilesource)); } $newfile->set_sortorder($sortorder); // remove temp file $tempfile->delete(); return true; } } return false; } /** * Updates a file in draft filearea. * * This function can only update fields filepath, filename, author, license. * If anything (except filepath) is updated, timemodified is set to current time. * If filename or filepath is updated the file unconnects from it's origin * and therefore all references to it will be converted to copies when * filearea is saved. * * @param int $draftid * @param string $filepath path to the directory containing the file, or full path in case of directory * @param string $filename name of the file, or '.' in case of directory * @param array $updatedata array of fields to change (only filename, filepath, license and/or author can be updated) * @throws moodle_exception if for any reason file can not be updated (file does not exist, target already exists, etc.) */ public static function update_draftfile($draftid, $filepath, $filename, $updatedata) { global $USER; $fs = get_file_storage(); $usercontext = context_user::instance($USER->id); // make sure filename and filepath are present in $updatedata $updatedata = $updatedata + array('filepath' => $filepath, 'filename' => $filename); $filemodified = false; if (!$file = $fs->get_file($usercontext->id, 'user', 'draft', $draftid, $filepath, $filename)) { if ($filename === '.') { throw new moodle_exception('foldernotfound', 'repository'); } else { throw new moodle_exception('filenotfound', 'error'); } } if (!$file->is_directory()) { // This is a file if ($updatedata['filepath'] !== $filepath || $updatedata['filename'] !== $filename) { // Rename/move file: check that target file name does not exist. if ($fs->file_exists($usercontext->id, 'user', 'draft', $draftid, $updatedata['filepath'], $updatedata['filename'])) { throw new moodle_exception('fileexists', 'repository'); } // Unset original so the references are not shown any more. $filesource = unserialize_object($file->get_source()); if (isset($filesource->original)) { unset($filesource->original); $file->set_source(serialize($filesource)); } $file->rename($updatedata['filepath'], $updatedata['filename']); // timemodified is updated only when file is renamed and not updated when file is moved. $filemodified = $filemodified || ($updatedata['filename'] !== $filename); } if (array_key_exists('license', $updatedata) && $updatedata['license'] !== $file->get_license()) { // Update license and timemodified. $file->set_license($updatedata['license']); $filemodified = true; } if (array_key_exists('author', $updatedata) && $updatedata['author'] !== $file->get_author()) { // Update author and timemodified. $file->set_author($updatedata['author']); $filemodified = true; } // Update timemodified: if ($filemodified) { $file->set_timemodified(time()); } } else { // This is a directory - only filepath can be updated for a directory (it was moved). if ($updatedata['filepath'] === $filepath) { // nothing to update return; } if ($fs->file_exists($usercontext->id, 'user', 'draft', $draftid, $updatedata['filepath'], '.')) { // bad luck, we can not rename if something already exists there throw new moodle_exception('folderexists', 'repository'); } $xfilepath = preg_quote($filepath, '|'); if (preg_match("|^$xfilepath|", $updatedata['filepath'])) { // we can not move folder to it's own subfolder throw new moodle_exception('folderrecurse', 'repository'); } // If directory changed the name, update timemodified. $filemodified = (basename(rtrim($file->get_filepath(), '/')) !== basename(rtrim($updatedata['filepath'], '/'))); // Now update directory and all children. $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftid); foreach ($files as $f) { if (preg_match("|^$xfilepath|", $f->get_filepath())) { $path = preg_replace("|^$xfilepath|", $updatedata['filepath'], $f->get_filepath()); // Unset original so the references are not shown any more. $filesource = unserialize_object($f->get_source()); if (isset($filesource->original)) { unset($filesource->original); $f->set_source(serialize($filesource)); } $f->rename($path, $f->get_filename()); if ($filemodified && $f->get_filepath() === $updatedata['filepath'] && $f->get_filename() === $filename) { $f->set_timemodified(time()); } } } } } /** * Delete a temp file from draft area * * @param int $draftitemid * @param string $filepath * @param string $filename * @return bool */ public static function delete_tempfile_from_draft($draftitemid, $filepath, $filename) { global $USER; $fs = get_file_storage(); $user_context = context_user::instance($USER->id); if ($file = $fs->get_file($user_context->id, 'user', 'draft', $draftitemid, $filepath, $filename)) { $file->delete(); return true; } else { return false; } } /** * Find all external files in this repo and import them */ public function convert_references_to_local() { $fs = get_file_storage(); $files = $fs->get_external_files($this->id); foreach ($files as $storedfile) { $fs->import_external_file($storedfile); } } /** * Find all external files linked to this repository and delete them. */ public function remove_files() { $fs = get_file_storage(); $files = $fs->get_external_files($this->id); foreach ($files as $storedfile) { $storedfile->delete(); } } /** * 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()} * * @uses stored_file::set_missingsource() * @uses stored_file::set_synchronized() * @param stored_file $file * @return bool false when file does not need synchronisation, true if it was synchronised */ public function sync_reference(stored_file $file) { if ($file->get_repository_id() != $this->id) { // This should not really happen because the function can be called from stored_file only. return false; } if ($this->has_moodle_files()) { // References to local files need to be synchronised only once. // Later they will be synchronised automatically when the source is changed. if ($file->get_referencelastsync()) { return false; } if (in_array($file->get_id(), self::$syncfileids)) { throw new \coding_exception('File references itself: ' . $file->get_id()); } try { array_push(self::$syncfileids, $file->get_id()); $fs = get_file_storage(); $params = file_storage::unpack_reference($file->get_reference(), true); if (!is_array($params) || !($storedfile = $fs->get_file($params['contextid'], $params['component'], $params['filearea'], $params['itemid'], $params['filepath'], $params['filename']))) { $file->set_missingsource(); } else { $file->set_synchronized( $storedfile->get_contenthash(), $storedfile->get_filesize(), 0, $storedfile->get_timemodified(), ); } } finally { array_pop(self::$syncfileids); } return true; } return false; } /** * Build draft file's source field * * {@link file_restore_source_field_from_draft_file()} * XXX: This is a hack for file manager (MDL-28666) * For newly created draft files we have to construct * source filed in php serialized data format. * File manager needs to know the original file information before copying * to draft area, so we append these information in mdl_files.source field * * @param string $source * @return string serialised source field */ public static function build_source_field($source) { $sourcefield = new stdClass; $sourcefield->source = $source; return serialize($sourcefield); } /** * Prepares the repository to be cached. Implements method from cacheable_object interface. * * @return array */ public function prepare_to_cache() { return array( 'class' => get_class($this), 'id' => $this->id, 'ctxid' => $this->context->id, 'options' => $this->options, 'readonly' => $this->readonly ); } /** * Restores the repository from cache. Implements method from cacheable_object interface. * * @return array */ public static function wake_from_cache($data) { $classname = $data['class']; return new $classname($data['id'], $data['ctxid'], $data['options'], $data['readonly']); } /** * Gets a file relative to this file in the repository and sends it to the browser. * Used to allow relative file linking within a repository without creating file records * for linked files * * Repositories that overwrite this must be very careful - see filesystem repository for example. * * @param stored_file $mainfile The main file we are trying to access relative files for. * @param string $relativepath the relative path to the file we are trying to access. * */ public function send_relative_file(stored_file $mainfile, $relativepath) { // This repository hasn't implemented this so send_file_not_found. send_file_not_found(); } /** * helper function to check if the repository supports send_relative_file. * * @return true|false */ public function supports_relative_file() { return false; } /** * Helper function to indicate if this repository uses post requests for uploading files. * * @deprecated since Moodle 3.2, 3.1.1, 3.0.5 * @return bool */ public function uses_post_requests() { debugging('The method repository::uses_post_requests() is deprecated and must not be used anymore.', DEBUG_DEVELOPER); return false; } /** * Generate a secret key to be used for passing sensitive information around. * * @return string repository secret key. */ final public static function get_secret_key() { global $CFG; if (!isset($CFG->reposecretkey)) { set_config('reposecretkey', time() . random_string(32)); } return $CFG->reposecretkey; } } /** * Exception class for repository api * * @since Moodle 2.0 * @package core_repository * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_exception extends moodle_exception { } /** * This is a class used to define a repository instance form * * @since Moodle 2.0 * @package core_repository * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class repository_instance_form extends moodleform { /** @var stdClass repository instance */ protected $instance; /** @var string repository plugin type */ protected $plugin; /** @var string repository type ID */ protected $typeid; /** @var string repository context ID */ protected $contextid; /** * Added defaults to moodle form */ protected function add_defaults() { $mform =& $this->_form; $strrequired = get_string('required'); $mform->addElement('hidden', 'edit', ($this->instance) ? $this->instance->id : 0); $mform->setType('edit', PARAM_INT); $mform->addElement('hidden', 'new', $this->plugin); $mform->setType('new', PARAM_ALPHANUMEXT); $mform->addElement('hidden', 'plugin', $this->plugin); $mform->setType('plugin', PARAM_PLUGIN); $mform->addElement('hidden', 'typeid', $this->typeid); $mform->setType('typeid', PARAM_INT); $mform->addElement('hidden', 'contextid', $this->contextid); $mform->setType('contextid', PARAM_INT); $mform->addElement('text', 'name', get_string('name'), 'maxlength="100" size="30"'); $mform->addRule('name', $strrequired, 'required', null, 'client'); $mform->setType('name', PARAM_TEXT); } /** * Define moodle form elements */ public function definition() { global $CFG; // type of plugin, string $this->plugin = $this->_customdata['plugin']; $this->typeid = $this->_customdata['typeid']; $this->contextid = $this->_customdata['contextid']; $this->instance = (isset($this->_customdata['instance']) && is_subclass_of($this->_customdata['instance'], 'repository')) ? $this->_customdata['instance'] : null; $mform =& $this->_form; $this->add_defaults(); // Add instance config options. $result = repository::static_function($this->plugin, 'instance_config_form', $mform); if ($result === false) { // Remove the name element if no other config options. $mform->removeElement('name'); } if ($this->instance) { $data = array(); $data['name'] = $this->instance->name; if (!$this->instance->readonly) { // and set the data if we have some. foreach ($this->instance->get_instance_option_names() as $config) { if (!empty($this->instance->options[$config])) { $data[$config] = $this->instance->options[$config]; } else { $data[$config] = ''; } } } $this->set_data($data); } if ($result === false) { $mform->addElement('cancel'); } else { $this->add_action_buttons(true, get_string('save','repository')); } } /** * Validate moodle form data * * @param array $data form data * @param array $files files in form * @return array errors */ public function validation($data, $files) { global $DB; $errors = array(); $plugin = $this->_customdata['plugin']; $instance = (isset($this->_customdata['instance']) && is_subclass_of($this->_customdata['instance'], 'repository')) ? $this->_customdata['instance'] : null; if (!$instance) { $errors = repository::static_function($plugin, 'instance_form_validation', $this, $data, $errors); } else { $errors = $instance->instance_form_validation($this, $data, $errors); } $sql = "SELECT count('x') FROM {repository_instances} i, {repository} r WHERE r.type=:plugin AND r.id=i.typeid AND i.name=:name AND i.contextid=:contextid"; $params = array('name' => $data['name'], 'plugin' => $this->plugin, 'contextid' => $this->contextid); if ($instance) { $sql .= ' AND i.id != :instanceid'; $params['instanceid'] = $instance->id; } if ($DB->count_records_sql($sql, $params) > 0) { $errors['name'] = get_string('erroruniquename', 'repository'); } return $errors; } } /** * This is a class used to define a repository type setting form * * @since Moodle 2.0 * @package core_repository * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class repository_type_form extends moodleform { /** @var stdClass repository instance */ protected $instance; /** @var string repository plugin name */ protected $plugin; /** @var string action */ protected $action; /** @var string plugin name */ protected $pluginname; /** * Definition of the moodleform */ public function definition() { global $CFG; // type of plugin, string $this->plugin = $this->_customdata['plugin']; $this->instance = (isset($this->_customdata['instance']) && is_a($this->_customdata['instance'], 'repository_type')) ? $this->_customdata['instance'] : null; $this->action = $this->_customdata['action']; $this->pluginname = $this->_customdata['pluginname']; $mform =& $this->_form; $strrequired = get_string('required'); $mform->addElement('hidden', 'action', $this->action); $mform->setType('action', PARAM_TEXT); $mform->addElement('hidden', 'repos', $this->plugin); $mform->setType('repos', PARAM_PLUGIN); // let the plugin add its specific fields $classname = 'repository_' . $this->plugin; require_once($CFG->dirroot . '/repository/' . $this->plugin . '/lib.php'); //add "enable course/user instances" checkboxes if multiple instances are allowed $instanceoptionnames = repository::static_function($this->plugin, 'get_instance_option_names'); $result = call_user_func(array($classname, 'type_config_form'), $mform, $classname); if (!empty($instanceoptionnames)) { $sm = get_string_manager(); $component = 'repository'; if ($sm->string_exists('enablecourseinstances', 'repository_' . $this->plugin)) { $component .= ('_' . $this->plugin); } $mform->addElement('checkbox', 'enablecourseinstances', get_string('enablecourseinstances', $component)); $mform->setType('enablecourseinstances', PARAM_BOOL); $component = 'repository'; if ($sm->string_exists('enableuserinstances', 'repository_' . $this->plugin)) { $component .= ('_' . $this->plugin); } $mform->addElement('checkbox', 'enableuserinstances', get_string('enableuserinstances', $component)); $mform->setType('enableuserinstances', PARAM_BOOL); } // set the data if we have some. if ($this->instance) { $data = array(); $option_names = call_user_func(array($classname,'get_type_option_names')); if (!empty($instanceoptionnames)){ $option_names[] = 'enablecourseinstances'; $option_names[] = 'enableuserinstances'; } $instanceoptions = $this->instance->get_options(); foreach ($option_names as $config) { if (!empty($instanceoptions[$config])) { $data[$config] = $instanceoptions[$config]; } else { $data[$config] = ''; } } // XXX: set plugin name for plugins which doesn't have muliti instances if (empty($instanceoptionnames)){ $data['pluginname'] = $this->pluginname; } $this->set_data($data); } $this->add_action_buttons(true, get_string('save','repository')); } /** * Validate moodle form data * * @param array $data moodle form data * @param array $files * @return array errors */ public function validation($data, $files) { $errors = array(); $plugin = $this->_customdata['plugin']; $instance = (isset($this->_customdata['instance']) && is_subclass_of($this->_customdata['instance'], 'repository')) ? $this->_customdata['instance'] : null; if (!$instance) { $errors = repository::static_function($plugin, 'type_form_validation', $this, $data, $errors); } else { $errors = $instance->type_form_validation($this, $data, $errors); } return $errors; } } /** * Generate all options needed by filepicker * * @param stdClass $args including following keys * context * accepted_types * return_types * * @return stdClass the list of repository instances, including meta infomation, containing the following keys * externallink * repositories * accepted_types */ function initialise_filepicker($args) { global $CFG, $USER, $PAGE; static $templatesinitialized = array(); require_once($CFG->libdir . '/licenselib.php'); $return = new stdClass(); $licenses = license_manager::get_licenses(); if (!empty($CFG->sitedefaultlicense)) { $return->defaultlicense = $CFG->sitedefaultlicense; } $return->licenses = $licenses; $return->author = fullname($USER); if (empty($args->context)) { $context = $PAGE->context; } else { $context = $args->context; } $disable_types = array(); if (!empty($args->disable_types)) { $disable_types = $args->disable_types; } $user_context = context_user::instance($USER->id); list($context, $course, $cm) = get_context_info_array($context->id); $contexts = array($user_context, context_system::instance()); if (!empty($course)) { // adding course context $contexts[] = context_course::instance($course->id); } $externallink = (int)get_config(null, 'repositoryallowexternallinks'); $repositories = repository::get_instances(array( 'context'=>$contexts, 'currentcontext'=> $context, 'accepted_types'=>$args->accepted_types, 'return_types'=>$args->return_types, 'disable_types'=>$disable_types )); $return->repositories = array(); if (empty($externallink)) { $return->externallink = false; } else { $return->externallink = true; } $return->rememberuserlicensepref = (bool) get_config(null, 'rememberuserlicensepref'); $return->userprefs = array(); $return->userprefs['recentrepository'] = get_user_preferences('filepicker_recentrepository', ''); $return->userprefs['recentlicense'] = get_user_preferences('filepicker_recentlicense', ''); $return->userprefs['recentviewmode'] = get_user_preferences('filepicker_recentviewmode', ''); // provided by form element $return->accepted_types = file_get_typegroup('extension', $args->accepted_types); $return->return_types = $args->return_types; $templates = array(); foreach ($repositories as $repository) { $meta = $repository->get_meta(); // Please note that the array keys for repositories are used within // JavaScript a lot, the key NEEDS to be the repository id. $return->repositories[$repository->id] = $meta; // Register custom repository template if it has one if(method_exists($repository, 'get_upload_template') && !array_key_exists('uploadform_' . $meta->type, $templatesinitialized)) { $templates['uploadform_' . $meta->type] = $repository->get_upload_template(); $templatesinitialized['uploadform_' . $meta->type] = true; } } if (!array_key_exists('core', $templatesinitialized)) { // we need to send each filepicker template to the browser just once $fprenderer = $PAGE->get_renderer('core', 'files'); $templates = array_merge($templates, $fprenderer->filepicker_js_templates()); $templatesinitialized['core'] = true; } if (sizeof($templates)) { $PAGE->requires->js_init_call('M.core_filepicker.set_templates', array($templates), true); } return $return; } /** * Convenience function to handle deletion of files. * * @param object $context The context where the delete is called * @param string $component component * @param string $filearea filearea * @param int $itemid the item id * @param array $files Array of files object with each item having filename/filepath as values * @return array $return Array of strings matching up to the parent directory of the deleted files * @throws coding_exception */ function repository_delete_selected_files($context, string $component, string $filearea, $itemid, array $files) { $fs = get_file_storage(); $return = []; foreach ($files as $selectedfile) { $filename = clean_filename($selectedfile->filename); $filepath = clean_param($selectedfile->filepath, PARAM_PATH); $filepath = file_correct_filepath($filepath); if ($storedfile = $fs->get_file($context->id, $component, $filearea, $itemid, $filepath, $filename)) { $parentpath = $storedfile->get_parent_directory()->get_filepath(); if ($storedfile->is_directory()) { $files = $fs->get_directory_files($context->id, $component, $filearea, $itemid, $filepath, true); foreach ($files as $file) { $file->delete(); // Log the event when a file is deleted from the draft area. create_event_draft_file_deleted($context, $file); } $storedfile->delete(); create_event_draft_file_deleted($context, $storedfile); $return[$parentpath] = ""; } else { if ($result = $storedfile->delete()) { create_event_draft_file_deleted($context, $storedfile); $return[$parentpath] = ""; } } } } return $return; } /** * Convenience function to create draft_file_deleted log event. * * @param context $context The context where delete is called. * @param stored_file $storedfile the file to be logged. */ function create_event_draft_file_deleted(context $context, stored_file $storedfile): void { $logevent = \core\event\draft_file_deleted::create([ 'objectid' => $storedfile->get_id(), 'context' => $context, 'other' => [ 'itemid' => $storedfile->get_itemid(), 'filename' => $storedfile->get_filename(), 'filesize' => $storedfile->get_filesize(), 'filepath' => $storedfile->get_filepath(), 'contenthash' => $storedfile->get_contenthash(), ], ]); $logevent->trigger(); } /** * Convenience function to handle deletion of files. * * @param object $context The context where the delete is called * @param string $component component * @param string $filearea filearea * @param int $itemid the item id * @param array $files Array of files object with each item having filename/filepath as values * @return false|stdClass $return Object containing URL of zip archive and a file path * @throws coding_exception */ function repository_download_selected_files($context, string $component, string $filearea, $itemid, array $files) { global $USER; $return = false; $zipper = get_file_packer('application/zip'); $fs = get_file_storage(); // Archive compressed file to an unused draft area. $newdraftitemid = file_get_unused_draft_itemid(); $filestoarchive = []; foreach ($files as $selectedfile) { $filename = $selectedfile->filename ? clean_filename($selectedfile->filename) : '.'; // Default to '.' for root. $filepath = clean_param($selectedfile->filepath, PARAM_PATH); // Default to '/' for downloadall. $filepath = file_correct_filepath($filepath); $storedfile = $fs->get_file($context->id, $component, $filearea, $itemid, $filepath, $filename); // If it is empty we are downloading a directory. $archivefile = $storedfile->get_filename(); if (!$filename || $filename == '.' ) { $foldername = explode('/', trim($filepath, '/')); $folder = trim(array_pop($foldername), '/'); $archivefile = $folder ?? '/'; } $filestoarchive[$archivefile] = $storedfile; } $zippedfile = get_string('files') . '.zip'; if ($zipper->archive_to_storage( $filestoarchive, $context->id, $component, $filearea, $newdraftitemid, "/", $zippedfile, $USER->id, )) { $return = new stdClass(); $return->fileurl = moodle_url::make_draftfile_url($newdraftitemid, '/', $zippedfile)->out(); $return->filepath = $filepath; } return $return; } PK*\sZǓFFfilepicker.phpnu[. // /** * This file is used to browse repositories in non-javascript mode * * @since Moodle 2.0 * @package core * @subpackage repository * @copyright 2009 Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../config.php'); require_once($CFG->libdir.'/filelib.php'); require_once('lib.php'); /// Wait as long as it takes for this script to finish core_php_time_limit::raise(); require_sesskey(); require_login(); // disable blocks in this page $PAGE->set_pagelayout('embedded'); // general parameters $action = optional_param('action', '', PARAM_ALPHA); $client_id = optional_param('client_id', '', PARAM_RAW); // client ID $itemid = optional_param('itemid', '', PARAM_INT); // parameters for repository $callback = optional_param('callback', '', PARAM_CLEANHTML); $contextid = optional_param('ctx_id', SYSCONTEXTID, PARAM_INT); // context ID $courseid = optional_param('course', SITEID, PARAM_INT); // course ID $env = optional_param('env', 'filepicker', PARAM_ALPHA); // opened in file picker, file manager or html editor $filename = optional_param('filename', '', PARAM_FILE); $fileurl = optional_param('fileurl', '', PARAM_RAW); $thumbnail = optional_param('thumbnail', '', PARAM_RAW); $targetpath = optional_param('targetpath', '', PARAM_PATH); $repo_id = optional_param('repo_id', 0, PARAM_INT); // repository ID $req_path = optional_param('p', '', PARAM_RAW); // the path in repository $curr_page = optional_param('page', '', PARAM_RAW); // What page in repository? $search_text = optional_param('s', '', PARAM_CLEANHTML); $maxfiles = optional_param('maxfiles', -1, PARAM_INT); // maxfiles $maxbytes = optional_param('maxbytes', 0, PARAM_INT); // maxbytes $subdirs = optional_param('subdirs', 0, PARAM_INT); // maxbytes $areamaxbytes = optional_param('areamaxbytes', FILE_AREA_MAX_BYTES_UNLIMITED, PARAM_INT); // Area maxbytes. $accepted_types = optional_param_array('accepted_types', '*', PARAM_RAW); // the path to save files $savepath = optional_param('savepath', '/', PARAM_PATH); // path in draft area $draftpath = optional_param('draftpath', '/', PARAM_PATH); // user context $user_context = context_user::instance($USER->id); $PAGE->set_context($user_context); if (!$course = $DB->get_record('course', array('id'=>$courseid))) { throw new \moodle_exception('invalidcourseid'); } $PAGE->set_course($course); if ($repo_id) { // Get repository instance information $repooptions = array( 'ajax' => false, 'mimetypes' => $accepted_types ); $repo = repository::get_repository_by_id($repo_id, $contextid, $repooptions); // Check permissions $repo->check_capability(); } $context = context::instance_by_id($contextid); // Make sure maxbytes passed is within site filesize limits. $maxbytes = get_user_max_upload_file_size($context, $CFG->maxbytes, $course->maxbytes, $maxbytes); $params = array('ctx_id' => $contextid, 'itemid' => $itemid, 'env' => $env, 'course'=>$courseid, 'maxbytes'=>$maxbytes, 'areamaxbytes'=>$areamaxbytes, 'maxfiles'=>$maxfiles, 'subdirs'=>$subdirs, 'sesskey'=>sesskey()); $params['action'] = 'browse'; $params['draftpath'] = $draftpath; $home_url = new moodle_url('/repository/draftfiles_manager.php', $params); $params['savepath'] = $savepath; $params['repo_id'] = $repo_id; $url = new moodle_url("/repository/filepicker.php", $params); $PAGE->set_url('/repository/filepicker.php', $params); switch ($action) { case 'upload': // The uploaded file has been processed in plugin construct function // redirect to default page try { $repo->upload('', $maxbytes); redirect($home_url, get_string('uploadsucc','repository')); } catch (moodle_exception $e) { // inject target URL $e->link = $PAGE->url->out(); echo $OUTPUT->header(); // hack: we need the embedded header here, standard error printing would not use it throw $e; } break; case 'search': echo '"; try { $search_result = $repo->search($search_text); $search_result['issearchresult'] = true; $search_result['repo_id'] = $repo_id; // TODO: need a better solution $purl = new moodle_url($url, array('search_paging' => 1, 'action' => 'search', 'repo_id' => $repo_id)); $pagingbar = new paging_bar($search_result['total'], $search_result['page'] - 1, $search_result['perpage'], $purl, 'p'); echo $OUTPUT->render($pagingbar); echo ''; foreach ($search_result['list'] as $item) { echo ''; echo ''; echo ''; echo ''; } echo '
'; $style = ''; if (isset($item['thumbnail_height'])) { $style .= 'max-height:'.$item['thumbnail_height'].'px;'; } if (isset($item['thumbnail_width'])) { $style .= 'max-width:'.$item['thumbnail_width'].'px;'; } echo html_writer::empty_tag('img', array('src' => $item['thumbnail'], 'alt' => '', 'style' => $style)); echo ''; if (!empty($item['url'])) { echo html_writer::link($item['url'], s($item['title']), array('target'=>'_blank')); } else { echo s($item['title']); } echo ''; echo '
'; echo ''; echo ''; echo ''; echo ''; echo ''; echo '
'; echo '
'; } catch (repository_exception $e) { } break; case 'list': case 'sign': echo $OUTPUT->header(); echo $OUTPUT->container_start(); echo html_writer::link($url, get_string('back', 'repository')); echo $OUTPUT->container_end(); if ($repo->check_login()) { $list = $repo->get_listing($req_path, $curr_page); $dynload = !empty($list['dynload'])?true:false; if (!empty($list['upload'])) { echo '
'; echo ''; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo ''; echo '
'; } else { if (!empty($list['path'])) { foreach ($list['path'] as $p) { //echo '
'; //echo '
'; //echo '
'; //echo ''; //echo '
'; $pathurl = new moodle_url($url, array( 'p'=>$p['path'], 'action'=>'list', 'draftpath'=>$draftpath, 'savepath'=>$savepath )); echo '' . html_writer::link($pathurl, s($p['name'])) . ''; echo ' / '; } } if (!empty($list['page'])) { // TODO MDL-28482: need a better solution // paging_bar is not a good option because it starts page numbering from 0 and // repositories number pages starting from 1. $pagingurl = new moodle_url("/repository/filepicker.php?action=list&itemid=$itemid&ctx_id=$contextid&repo_id=$repo_id&course=$courseid&sesskey=". sesskey()); if (!isset($list['perpage']) && !isset($list['total'])) { $list['perpage'] = 10; // instead of setting perpage&total we use number of pages, the result is the same } if (empty($list['total'])) { if ($list['pages'] == -1) { $total = ($list['page'] + 2) * $list['perpage']; } else { $total = $list['pages'] * $list['perpage']; } } else { $total = $list['total']; } echo $OUTPUT->paging_bar($total, $list['page'], $list['perpage'], $pagingurl); } echo ''; foreach ($list['list'] as $item) { echo ''; echo ''; echo ''; echo ''; } echo '
'; $style = ''; if (isset($item['thumbnail_height'])) { $style .= 'max-height:'.$item['thumbnail_height'].'px;'; } if (isset($item['thumbnail_width'])) { $style .= 'max-width:'.$item['thumbnail_width'].'px;'; } echo html_writer::empty_tag('img', array('src' => $item['thumbnail'], 'style' => $style)); echo ''; if (!empty($item['url'])) { echo html_writer::link($item['url'], s($item['title']), array('target'=>'_blank')); } else { echo s($item['title']); } echo ''; if (!isset($item['children'])) { echo '
'; echo ''; echo ''; echo '
'; echo '
'; echo ''; echo ''; echo ''; echo '
'; } else { echo '
'; echo ''; echo ''; echo '
'; } echo '
'; } } else { echo '
'; echo ''; echo ''; $repo->print_login(); echo '
'; } echo $OUTPUT->footer(); break; case 'download': // Check that user has permission to access this file if (!$repo->file_is_accessible($fileurl)) { throw new \moodle_exception('storedfilecannotread'); } $record = new stdClass(); $reference = $repo->get_file_reference($fileurl); $sourcefield = $repo->get_file_source_info($fileurl); $record->source = repository::build_source_field($sourcefield); // If file is already a reference, set $fileurl = file source, $repo = file repository // note that in this case user may not have permission to access the source file directly // so no file_browser/file_info can be used below if ($repo->has_moodle_files()) { $file = repository::get_moodle_file($reference); if ($file && $file->is_external_file()) { $sourcefield = $file->get_source(); // remember the original source $record->source = $repo::build_source_field($sourcefield); $reference = $file->get_reference(); $repo_id = $file->get_repository_id(); $repo = repository::get_repository_by_id($repo_id, $contextid, $repooptions); } } $record->filepath = $savepath; $record->filename = $filename; $record->component = 'user'; $record->filearea = 'draft'; $record->itemid = $itemid; $record->license = ''; $record->author = ''; $now = time(); $record->timecreated = $now; $record->timemodified = $now; $record->userid = $USER->id; $record->contextid = $user_context->id; $record->sortorder = 0; if ($repo->has_moodle_files()) { $fileinfo = $repo->copy_to_area($reference, $record, $maxbytes, $areamaxbytes); redirect($home_url, get_string('downloadsucc', 'repository')); } else { $thefile = $repo->get_file($reference, $filename); if (!empty($thefile['path'])) { $filesize = filesize($thefile['path']); if ($maxbytes != -1 && $filesize>$maxbytes) { unlink($thefile['path']); throw new \moodle_exception('maxbytes'); } // Ensure the file will not make the area exceed its size limit. if (file_is_draft_area_limit_reached($record->itemid, $areamaxbytes, $filesize)) { unlink($thefile['path']); throw new \moodle_exception('maxareabytes'); } // Ensure the user does not upload too many draft files in a short period. if (file_is_draft_areas_limit_reached($USER->id)) { unlink($thefile['path']); throw new \moodle_exception('maxdraftitemids'); } try { $info = repository::move_to_filepool($thefile['path'], $record); redirect($home_url, get_string('downloadsucc', 'repository')); } catch (moodle_exception $e) { // inject target URL $e->link = $PAGE->url->out(); echo $OUTPUT->header(); // hack: we need the embedded header here, standard error printing would not use it throw $e; } } else { throw new \moodle_exception('cannotdownload', 'repository'); } } break; case 'confirm': echo $OUTPUT->header(); echo ''; echo ''; echo '
'; echo ''; echo ' '; echo ' '; echo ' '; echo ' '; echo ' '; echo ' '; echo ' '; echo '
'. html_writer::label(get_string('filename', 'repository'), 'filename'). '
'; echo '
'; // the save path echo ' '; echo ' '; echo ' '; echo '
'; echo '
'; echo $OUTPUT->footer(); break; default: case 'plugins': $params = array(); $params['context'] = array($user_context, context_system::instance()); $params['currentcontext'] = $PAGE->context; $params['return_types'] = FILE_INTERNAL; $repos = repository::get_instances($params); echo $OUTPUT->header(); echo html_writer::link($home_url->out(false), get_string('backtodraftfiles', 'repository')); echo '
'; echo '
    '; foreach($repos as $repo) { $info = $repo->get_meta(); $aurl = clone($url); $aurl->params(array('savepath'=>$savepath, 'action' => 'list', 'repo_id' => $info->id, 'draftpath'=>$draftpath)); echo '
  • '; echo html_writer::empty_tag('img', ['src' => $info->icon, 'alt' => $info->name, 'class' => 'icon']); echo html_writer::link($aurl, s($info->name)); echo '
  • '; } echo '
'; echo '
'; echo $OUTPUT->footer(); break; } PK*\)Csmerlot/db/access.phpnu[. /** * Plugin capabilities. * * @package repository_merlot * @copyright 2009 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/merlot:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'user' => CAP_ALLOW ) ) ); PK*\l'$merlot/lang/en/repository_merlot.phpnu[. /** * Strings for component 'repository_merlot', language 'en', branch 'MOODLE_20_STABLE' * * @package repository_merlot * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['configplugin'] = 'Merlot.org configuration'; $string['licensekey'] = 'Licence key'; $string['pluginname_help'] = 'Merlot.org'; $string['pluginname'] = 'Merlot.org'; $string['merlot:view'] = 'View the Merlot repository'; $string['privacy:metadata:repository_merlot'] = 'The Merlot.org repository plugin does not store any personal data, but does transmit user data from Moodle to the remote system.'; $string['privacy:metadata:repository_merlot:author'] = 'The Merlot.org repository content author.'; $string['privacy:metadata:repository_merlot:keywords'] = 'The Merlot.org repository user search text query.'; $string['privacy:metadata:repository_merlot:licencekey'] = 'The Merlot.org repository licence key.'; PK*\NA~merlot/pix/icon.pngnu[PNG  IHDR !7D>IDATxm+Mߪd,>Ƶm۶mm۶m>l2`(WIMmlbw`"#:@A, 89TDJ ū86ݦg@p`!j 8qp!Z,R0g0"4c@0X0'Bak9߫'BYʤQ`kp!I!d:FX\]6t~>R&|eWǎ~<\}WW15j8b2%dl0"%Z ռC/ۇ܋=8v-xz(&*3C*ld K$WuBoϼ*fň(_ 0ûGKiރ̄b3»jx@ B(LBo_3}DYiEr$-{=vz dSd攩yfj{lSti5oimu4#,"h6^Y7yӠ-am!T<u- QbnwwϹ4lΩP0&H~!!*g{MY*%y"H$jͬ/.Yo w0 Әv;@$IICumbaw+ucćNp%u} d1Vv|">9B,4Yҽ㝭ﭤY'N傡#+)0 z/޷}+\s}~[<|t=h%bP Q1fyA?c{SᏜ~XRdtMa=o;+511abPGHꈛUPV7ﳖ;/.~ӞCc/|Ƴ^"\z58C*qA@@`XNCÿsG/nwۏ|w~^ucn,!*܁*Azȑ:'=ɿ#?닿kOWu?p/oNfsQB:+S Rg} ^==]z~v'ox{2:?~. /** * This plugin is used to access merlot files * * @since Moodle 2.0 * @package repository_merlot * @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_merlot is used to search merlot.org in moodle * * @since Moodle 2.0 * @package repository_merlot * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_merlot extends repository { /** @var string merlot keyword. */ protected $keyword; /** @var string merlot author. */ protected $author; /** @var string merlot license key. */ protected $licensekey; /** @var string API URL. */ protected $api; public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) { parent::__construct($repositoryid, $context, $options); $this->keyword = optional_param('merlot_keyword', '', PARAM_RAW); $this->author = optional_param('merlot_author', '', PARAM_RAW); $this->licensekey = trim(get_config('merlot', 'licensekey')); } /** * Display login screen or not * * @return boolean */ public function check_login() { return !empty($this->keyword); } /** * Doesn't support global search * * @return boolean */ public function global_search() { return false; } /** * Look for a link in merlot.org * @param string $search_text * @return array */ public function search($search_text, $page = 0) { $ret = array(); $ret['nologin'] = true; $ret['list'] = $this->_get_collection($this->keyword, $this->author); return $ret; } /** * Get a list of links * @return array */ public function get_listing($path = '', $page = '') { $ret = array(); $ret['nologin'] = true; $ret['list'] = $this->_get_collection($this->keyword); return $ret; } private function _get_collection($keyword) { global $OUTPUT; $list = array(); $this->api = 'https://www.merlot.org/merlot/materials.rest?keywords=' . urlencode($keyword) . '&licenseKey='.$this->licensekey; $c = new curl(array('cache'=>true, 'module_cache'=>'repository')); $content = $c->get($this->api); $xml = simplexml_load_string($content); foreach ($xml->results->material as $entry) { $list[] = array( 'title' => (string)$entry->title, 'thumbnail' => $OUTPUT->image_url(file_extension_icon($entry->title))->out(false), 'date' => userdate((int)$entry->creationDate), 'size' => '', 'source' => (string)$entry->URL, ); } return $list; } /** * Define a search form * * @return array */ public function print_login(){ $ret = array(); $search = new stdClass(); $search->type = 'text'; $search->id = 'merlog_search'; $search->name = 'merlot_keyword'; $search->label = get_string('search').': '; $author = new stdClass(); $author->type = 'text'; $author->id = 'merlog_author'; $author->name = 'merlot_author'; $author->label = get_string('author', 'search').': '; $ret['login'] = array($search, $author); $ret['login_btn_label'] = get_string('search'); $ret['login_btn_action'] = 'search'; return $ret; } /** * Names of the plugin settings * * @return array */ public static function get_type_option_names() { return array('licensekey', 'pluginname'); } /** * Add Plugin settings input to Moodle form * * @param object $mform */ public static function type_config_form($mform, $classname = 'repository') { parent::type_config_form($mform); $licensekey = get_config('merlot', 'licensekey'); if (empty($licensekey)) { $licensekey = ''; } $strrequired = get_string('required'); $mform->addElement('text', 'licensekey', get_string('licensekey', 'repository_merlot'), array('value'=>$licensekey,'size' => '40')); $mform->setType('licensekey', PARAM_RAW_TRIMMED); $mform->addRule('licensekey', $strrequired, 'required', null, 'client'); } /** * Support external link only * * @return int */ public function supported_returntypes() { return FILE_EXTERNAL; } public function supported_filetypes() { return array('link'); } /** * Is this repository accessing private data? * * @return bool */ public function contains_private_data() { return false; } } PK*\4!merlot/version.phpnu[. /** * Version details * * @package repository * @subpackage merlot * @copyright 2009 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_merlot'; // Full name of the plugin (used for diagnostics) PK*\##merlot/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for repository_merlot. * * @package repository_merlot * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_merlot\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_merlot 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( 'merlot.org', [ 'author' => 'privacy:metadata:repository_merlot:author', 'keywords' => 'privacy:metadata:repository_merlot:keywords', 'licencekey' => 'privacy:metadata:repository_merlot:licencekey' ], 'privacy:metadata:repository_merlot' ); 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) { } } PK*\'zmerlot/tests/generator/lib.phpnu[. /** * Merlot repository data generator * * @package repository_merlot * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Merlot repository data generator class * * @package repository_merlot * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_merlot_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['licensekey'])) { $record['licensekey'] = 'licensekey'; } return $record; } } PK*\equella/db/access.phpnu[. /** * Capabilities for equella repository. * * @package repository_equella * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org} * @author Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'repository/equella:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'user' => CAP_ALLOW ) ) ); PK*\ &equella/lang/en/repository_equella.phpnu[. /** * Strings for equella repository. * * @package repository_equella * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['breadcrumb'] = 'EQUELLA'; $string['configplugin'] = 'Configuration for EQUELLA repository'; $string['equella:view'] = 'View EQUELLA repository'; $string['equellaaction'] = 'EQUELLA action'; $string['equellaoptions'] = 'EQUELLA options'; $string['equellaurl'] = 'EQUELLA URL'; $string['equellauserfield'] = 'EQUELLA user field'; $string['equellauserfield_help'] = 'Choose the user field to be used for matching the EQUELLA user. (Only user fields of type short text are listed.)'; $string['equellausername'] = 'Username'; $string['group'] = '{$a} role settings'; $string['groupdefault'] = 'Default'; $string['pluginname'] = 'EQUELLA repository'; $string['privacy:metadata'] = 'The EQUELLA repository plugin does not store any personal data, but does transmit user data from Moodle to the remote system.'; $string['restrictionattachmentsonly'] = 'Attached resource only'; $string['restrictionitemsonly'] = 'Item summary only'; $string['restrictionnone'] = 'No restriction'; $string['search'] = 'Search EQUELLA'; $string['selectrestriction'] = 'Restrict selection'; $string['selectrestriction.desc'] = 'Choose whether course editors should only be able to select an item summary, an attached resources or either'; $string['sharedid'] = 'Shared secret ID'; $string['sharedidtitle'] = 'Shared secret ID'; $string['sharedsecrets'] = 'Shared secret'; $string['sharedsecretsheading'] = 'Shared Secret Settings'; $string['sharedsecretshelp'] = '

Below you can set a default EQUELLA shared secret for single signing-on users. You can configure different shared secrets for general (read) usage, and a specialised role based shared secret for each write role in your Moodle site. If a shared secret ID is not configured for a role then the default shared secret ID and shared secret are used.

All shared secret IDs and shared secrets must also be configured within EQUELLA and the shared secret module enabled. This configuration is found in the EQUELLA Administration Console under User Management > Shared Secrets.

'; $string['sharedsecrettitle'] = 'Shared secret'; PK*\t]equella/pix/icon.pngnu[PNG  IHDRqPLTE]vZiRE}9~p%\n,x LQo&l}fKSXl橿U"iL3f,pl.qbyxe*_ޔT䡸k ,pu;zdIDATxUCAضmѳ} mu*Gi`뿀j} YQ'GVT?{ QT*Zq$yq>1}d絃0,:3?L̚!ۊ D8éeA&3R{ݞ 0 0BŭPF0ºܮ2^b+t͕2|/*y,=sIENDB`PK*\m}H}Hequella/lib.phpnu[. /** * This plugin is used to access equella repositories. * * @since Moodle 2.3 * @package repository_equella * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/repository/lib.php'); /** * repository_equella class implements equella_client * * @since Moodle 2.3 * @package repository_equella * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_equella extends repository { /** @var array mimetype filter */ private $mimetypes = array(); /** * Constructor * * @param int $repositoryid repository instance id * @param int|stdClass $context a context id or context object * @param array $options repository options */ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) { parent::__construct($repositoryid, $context, $options); if (isset($this->options['mimetypes'])) { $mt = $this->options['mimetypes']; if (!empty($mt) && is_array($mt) && !in_array('*', $mt)) { $this->mimetypes = array_unique(array_map(array($this, 'to_mime_type'), $mt)); } } } /** * Display embedded equella interface * * @param string $path * @param mixed $page * @return array */ public function get_listing($path = null, $page = null) { global $COURSE; $callbackurl = new moodle_url('/repository/equella/callback.php', array('repo_id'=>$this->id)); $mimetypesstr = ''; $restrict = ''; if (!empty($this->mimetypes)) { $mimetypesstr = '&mimeTypes=' . implode(',', $this->mimetypes); // We're restricting to a mime type, so we always restrict to selecting resources only. $restrict = '&attachmentonly=true'; } else if ($this->get_option('equella_select_restriction') != 'none') { // The option value matches the EQUELLA paramter name. $restrict = '&' . $this->get_option('equella_select_restriction') . '=true'; } $url = $this->get_option('equella_url') . '?method=lms' . '&returnurl='.urlencode($callbackurl) . '&returnprefix=tle' . '&template=standard' . '&token='.urlencode($this->getssotoken('write')) . '&courseId='.urlencode($COURSE->idnumber) . '&courseCode='.urlencode($COURSE->shortname) . '&action=searchThin' . '&forcePost=true' . '&cancelDisabled=true' . '&attachmentUuidUrls=true' . '&options='.urlencode($this->get_option('equella_options') . $mimetypesstr) . $restrict; $manageurl = $this->get_option('equella_url'); $manageurl = str_ireplace('signon.do', 'logon.do', $manageurl); $manageurl = $this->appendtoken($manageurl); $list = array(); $list['object'] = array(); $list['object']['type'] = 'text/html'; $list['object']['src'] = $url; $list['nologin'] = true; $list['nosearch'] = true; $list['norefresh'] = true; $list['manage'] = $manageurl; return $list; } /** * Supported equella file types * * @return int */ public function supported_returntypes() { return (FILE_INTERNAL | FILE_REFERENCE); } /** * Prepare file reference information * * @param string $source * @return string file referece */ public function get_file_reference($source) { // Internally we store serialized value but user input is json-encoded for security reasons. $ref = json_decode(base64_decode($source)); $filename = clean_param($ref->filename, PARAM_FILE); $url = clean_param($ref->url, PARAM_URL); return base64_encode(serialize((object)array('url' => $url, 'filename' => $filename))); } /** * Counts the number of failed connections. * * If we received the connection timeout more than 3 times in a row, we don't attemt to * connect to the server any more during this request. * * This function is used by {@link repository_equella::sync_reference()} that * synchronises the file size of referenced files. * * @param int $errno omit if we just want to know the return value, the last curl_errno otherwise * @return bool true if we had less than 3 failed connections, false if no more connections * attempts recommended */ private function connection_result($errno = null) { static $countfailures = array(); $sess = sesskey(); if (!array_key_exists($sess, $countfailures)) { $countfailures[$sess] = 0; } if ($errno !== null) { if ($errno == 0) { // reset count of failed connections $countfailures[$sess] = 0; } else if ($errno == 7 /*CURLE_COULDNT_CONNECT*/ || $errno == 9 /*CURLE_REMOTE_ACCESS_DENIED*/) { // problems with server $countfailures[$sess]++; } } return ($countfailures[$sess] < 3); } /** * Returned unserialized object from base64 encoded file reference data * * @param string $reference * @return stdClass */ private function unserialize_reference(string $reference): stdClass { $decoded = base64_decode($reference); return unserialize_object($decoded); } /** * Download a file, this function can be overridden by subclass. {@link curl} * * @param string $reference the source of the file * @param string $filename filename (without path) to save the downloaded file in the * temporary directory * @return null|array null if download failed or array with elements: * path: internal location of the file * url: URL to the source (from parameters) */ public function get_file($reference, $filename = '') { global $USER, $CFG; $ref = $this->unserialize_reference($reference); if (!isset($ref->url) || !($url = $this->appendtoken($ref->url))) { // Occurs when the user isn't known.. return null; } $path = $this->prepare_file($filename); $cookiepathname = $this->prepare_file($USER->id. '_'. uniqid('', true). '.cookie'); $c = new curl(array('cookie'=>$cookiepathname)); $result = $c->download_one($url, null, array('filepath' => $path, 'followlocation' => true, 'timeout' => $CFG->repositorygetfiletimeout)); // Delete cookie jar. if (file_exists($cookiepathname)) { unlink($cookiepathname); } if ($result !== true) { throw new moodle_exception('errorwhiledownload', 'repository', '', $result); } return array('path'=>$path, 'url'=>$url); } public function sync_reference(stored_file $file) { global $USER, $CFG; if ($file->get_referencelastsync() + DAYSECS > time() || !$this->connection_result()) { // Synchronise not more often than once a day. // if we had several unsuccessfull attempts to connect to server - do not try any more. return false; } $ref = $this->unserialize_reference($file->get_reference()); if (!isset($ref->url) || !($url = $this->appendtoken($ref->url))) { // Occurs when the user isn't known.. $file->set_missingsource(); return true; } $cookiepathname = $this->prepare_file($USER->id. '_'. uniqid('', true). '.cookie'); $c = new curl(array('cookie' => $cookiepathname)); if (file_extension_in_typegroup($ref->filename, 'web_image')) { $path = $this->prepare_file(''); $result = $c->download_one($url, null, array('filepath' => $path, 'followlocation' => true, 'timeout' => $CFG->repositorysyncimagetimeout)); if ($result === true) { $file->set_synchronised_content_from_file($path); return true; } } else { $result = $c->head($url, array('followlocation' => true, 'timeout' => $CFG->repositorysyncfiletimeout)); } // Delete cookie jar. if (file_exists($cookiepathname)) { unlink($cookiepathname); } $this->connection_result($c->get_errno()); $curlinfo = $c->get_info(); if (isset($curlinfo['http_code']) && $curlinfo['http_code'] == 200 && array_key_exists('download_content_length', $curlinfo) && $curlinfo['download_content_length'] >= 0) { // we received a correct header and at least can tell the file size $file->set_synchronized(null, $curlinfo['download_content_length']); return true; } $file->set_missingsource(); return true; } /** * Repository method to serve the referenced file * * @param stored_file $storedfile the file that contains the reference * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime) * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin * @param array $options additional options affecting the file serving */ public function send_file($stored_file, $lifetime=null , $filter=0, $forcedownload=false, ?array $options = null) { $ref = $this->unserialize_reference($stored_file->get_reference()); if (isset($ref->url) && $url = $this->appendtoken($ref->url)) { header('Location: ' . $url); } else { send_file_not_found(); } } /** * Add Instance settings input to Moodle form * * @param MoodleQuickForm $mform */ public static function instance_config_form($mform) { global $CFG; require_once("{$CFG->dirroot}/user/profile/lib.php"); $mform->addElement('text', 'equella_url', get_string('equellaurl', 'repository_equella')); $mform->setType('equella_url', PARAM_URL); $strrequired = get_string('required'); $mform->addRule('equella_url', $strrequired, 'required', null, 'client'); $userfieldoptions = ['default' => get_string('equellausername', 'repository_equella')]; foreach (profile_get_custom_fields() as $field) { if ($field->datatype != 'text') { continue; } $userfieldoptions[$field->shortname] = format_string($field->name, true, ['context' => context_system::instance()]); } $mform->addElement('select', 'equella_userfield', get_string('equellauserfield', 'repository_equella'), $userfieldoptions); $mform->setDefault('equella_userfield', $userfieldoptions['default']); $mform->addHelpButton('equella_userfield', 'equellauserfield', 'repository_equella'); $mform->addElement('text', 'equella_options', get_string('equellaoptions', 'repository_equella')); $mform->setType('equella_options', PARAM_NOTAGS); $choices = array( 'none' => get_string('restrictionnone', 'repository_equella'), 'itemonly' => get_string('restrictionitemsonly', 'repository_equella'), 'attachmentonly' => get_string('restrictionattachmentsonly', 'repository_equella'), ); $mform->addElement('select', 'equella_select_restriction', get_string('selectrestriction', 'repository_equella'), $choices); $mform->addElement('header', 'groupheader', get_string('group', 'repository_equella', get_string('groupdefault', 'repository_equella'))); $mform->addElement('text', 'equella_shareid', get_string('sharedid', 'repository_equella')); $mform->setType('equella_shareid', PARAM_RAW); $mform->addRule('equella_shareid', $strrequired, 'required', null, 'client'); $mform->addElement('text', 'equella_sharedsecret', get_string('sharedsecrets', 'repository_equella')); $mform->setType('equella_sharedsecret', PARAM_RAW); $mform->addRule('equella_sharedsecret', $strrequired, 'required', null, 'client'); foreach (self::get_all_editing_roles() as $role) { $mform->addElement('header', 'groupheader_'.$role->shortname, get_string('group', 'repository_equella', role_get_name($role))); $mform->addElement('text', "equella_{$role->shortname}_shareid", get_string('sharedid', 'repository_equella')); $mform->setType("equella_{$role->shortname}_shareid", PARAM_RAW); $mform->addElement('text', "equella_{$role->shortname}_sharedsecret", get_string('sharedsecrets', 'repository_equella')); $mform->setType("equella_{$role->shortname}_sharedsecret", PARAM_RAW); } } /** * Names of the instance settings * * @return array */ public static function get_instance_option_names() { $rv = array('equella_url', 'equella_select_restriction', 'equella_options', 'equella_shareid', 'equella_sharedsecret', 'equella_userfield', ); foreach (self::get_all_editing_roles() as $role) { array_push($rv, "equella_{$role->shortname}_shareid"); array_push($rv, "equella_{$role->shortname}_sharedsecret"); } return $rv; } /** * Generate equella token * * @param string $username * @param string $shareid * @param string $sharedsecret * @return string */ private static function getssotoken_raw($username, $shareid, $sharedsecret) { $time = time() . '000'; return urlencode($username) . ':' . $shareid . ':' . $time . ':' . base64_encode(pack('H*', md5($username . $shareid . $time . $sharedsecret))); } /** * Append token * * @param string $url * @param $readwrite * @return string */ private function appendtoken($url, $readwrite = null) { $ssotoken = $this->getssotoken($readwrite); if (!$ssotoken) { return false; } return $url . (strpos($url, '?') != false ? '&' : '?') . 'token=' . urlencode($ssotoken); } /** * Generate equella sso token * * @param string $readwrite * @return string */ private function getssotoken($readwrite = 'read') { global $USER; if (empty($USER->username)) { return false; } $equellauserfield = $this->get_userfield_value(); if ($readwrite == 'write') { foreach (self::get_all_editing_roles() as $role) { if (user_has_role_assignment($USER->id, $role->id, $this->context->id)) { // See if the user has a role that is linked to an equella role. $shareid = $this->get_option("equella_{$role->shortname}_shareid"); if (!empty($shareid)) { return $this->getssotoken_raw($equellauserfield, $shareid, $this->get_option("equella_{$role->shortname}_sharedsecret")); } } } } // If we are only reading, use the unadorned shareid and secret. $shareid = $this->get_option('equella_shareid'); if (!empty($shareid)) { return $this->getssotoken_raw($equellauserfield, $shareid, $this->get_option('equella_sharedsecret')); } } private static function get_all_editing_roles() { return get_roles_with_capability('moodle/course:manageactivities', CAP_ALLOW); } /** * Convert moodle mimetypes list to equella format * * @param string $value * @return string */ private static function to_mime_type($value) { return mimeinfo('type', $value); } /** * Return the source information * * @param string $source * @return string|null */ public function get_file_source_info($source) { $ref = json_decode(base64_decode($source)); $filename = clean_param($ref->filename, PARAM_FILE); return 'EQUELLA: ' . $filename; } /** * Return human readable reference information * {@link stored_file::get_reference()} * * @param string $reference * @param int $filestatus status of the file, 0 - ok, 666 - source missing * @return string */ public function get_reference_details($reference, $filestatus = 0) { if (!$filestatus) { $ref = $this->unserialize_reference($reference); return $this->get_name(). ': '. ($ref->filename ?? ''); } else { return get_string('lostsource', 'repository', ''); } } /** * Is this repository accessing private data? * * @return bool */ public function contains_private_data() { return false; } /** * Retrieve the userfield/username. * * @return string */ public function get_userfield_value(): string { global $USER; $userfield = $this->get_option('equella_userfield'); if ($userfield != 'default' && isset($USER->profile[$userfield])) { return $USER->profile[$userfield]; } else { return $USER->username; } } } PK*\M*.equella/version.phpnu[. /** * Version information for equella repository. * * @since Moodle 2.3 * @package repository_equella * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org} * @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_equella'; // Full name of the plugin (used for diagnostics). PK*\>}$equella/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for repository_equella. * * @package repository_equella * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_equella\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for repository_equella implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK*\Y# equella/tests/generator/lib.phpnu[. /** * Equella repository data generator * * @package repository_equella * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Equella repository data generator class * * @package repository_equella * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_equella_generator extends testing_repository_generator { /** * Fill in record defaults. * * @param array $record * @return array */ protected function prepare_record(array $record) { $record = parent::prepare_record($record); if (!isset($record['equella_url'])) { $record['equella_url'] = 'http://dummy.url.com'; } if (!isset($record['equella_userfield'])) { $record['equella_userfield'] = 'default'; } if (!isset($record['equella_select_restriction'])) { $record['equella_select_restriction'] = 'none'; } if (!isset($record['equella_options'])) { $record['equella_options'] = ''; } if (!isset($record['equella_shareid'])) { $record['equella_shareid'] = 'id'; } if (!isset($record['equella_sharedsecret'])) { $record['equella_sharedsecret'] = 'secret'; } // Set defaults for all generated options for each role with editing capabilities. $editingroles = get_roles_with_capability('moodle/course:manageactivities', CAP_ALLOW); foreach ($editingroles as $role) { if (!isset($record["equella_{$role->shortname}_shareid"])) { $record["equella_{$role->shortname}_shareid"] = ''; } if (!isset($record["equella_{$role->shortname}_sharedsecret"])) { $record["equella_{$role->shortname}_sharedsecret"] = ''; } } return $record; } } PK*\S 4"4"equella/tests/lib_test.phpnu[. /** * This file contains tests for the repository_equella class. * * @package repository_equella * * @author Guillaume BARAT * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_equella; use repository_equella; use stdClass; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/repository/lib.php'); require_once($CFG->libdir . '/webdavlib.php'); /** * Class repository_equella_lib_testcase * * @group repository_equella * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class lib_test extends \advanced_testcase { /** @var null|\repository_equella the repository_equella object, which the tests are run on. */ private $repo = null; /** * Create some data for repository. * * @return stdClass */ private function create_new_form_data(): stdClass { $record = new stdClass(); $record->equella_url = 'http://dummy.url.com'; $record->equella_userfield = 'default'; $record->equella_select_restriction = 'none'; $record->equella_options = ''; $record->equella_shareid = 'id'; $record->equella_sharedsecret = 'secret'; $record->equella_manager_shareid = ''; $record->equella_manager_sharedsecret = ''; $record->equella_editingteacher_shareid = ''; $record->equella_editingteacher_sharedsecret = ''; return $record; } /** * Create repository for testing. * * @return repository_equella */ private function create_repository(): repository_equella { $record = new \stdClass(); $this->getDataGenerator()->create_repository_type('equella', $record); $generator = $this->getDataGenerator()->get_plugin_generator('repository_equella'); $instance = $generator->create_instance(); $this->repo = new repository_equella($instance->id); return $this->repo; } /** * Test that environment is created. * @covers \repository_equella::get_repository_by_id * @return void */ public function test_repository_is_created(): void { $this->initialise_repository(); $actual = repository_equella::get_repository_by_id($this->repo->id, $this->repo->context); $this->assertEquals($this->repo->options['equella_url'], $actual->get_option('equella_url')); $this->assertEquals($this->repo->options['equella_userfield'], $actual->get_option('equella_userfield')); $this->assertEquals($this->repo->options['equella_select_restriction'], $actual->get_option('equella_select_restriction')); $this->assertEquals($this->repo->options['equella_options'], $actual->get_option('equella_options')); $this->assertEquals($this->repo->options['equella_shareid'], $actual->get_option('equella_shareid')); $this->assertEquals($this->repo->options['equella_sharedsecret'], $actual->get_option('equella_sharedsecret')); $this->assertEquals($this->repo->options['equella_manager_shareid'], $actual->get_option('equella_manager_shareid')); $this->assertEquals($this->repo->options['equella_manager_sharedsecret'], $actual->get_option('equella_manager_sharedsecret')); $this->assertEquals($this->repo->options['equella_editingteacher_shareid'], $actual->get_option('equella_editingteacher_shareid')); $this->assertEquals($this->repo->options['equella_editingteacher_sharedsecret'], $actual->get_option('equella_editingteacher_sharedsecret')); $this->resetAfterTest(true); } /** * Data provider for get_userfield_value. * * @return array * @covers ::get_userfield_value */ public static function get_userfield_value_provider(): array { return [ [ 'input' => [ 'userfield' => 'nickname', 'value' => 'administrator', ], 'expected' => [ 'username' => 'administrator', ], ], [ 'input' => [ 'userfield' => 'default', 'value' => 'default', ], 'expected' => [ 'username' => 'admin', ], ], [ 'input' => [ 'userfield' => 'test', 'value' => 'test', ], 'expected' => [ 'username' => 'test', ], ], ]; } /** * Test method get_userfield_value. * * @dataProvider get_userfield_value_provider * * @param array $input * @param array $expected * @covers ::get_userfield_value * * @return void */ public function test_get_userfield_value($input, $expected): void { global $USER; $this->initialise_repository(); $USER->profile[$input['userfield']] = $input['value']; $this->repo->set_option(['equella_userfield' => $input['userfield']]); $return = $this->repo->get_userfield_value(); $this->assertEquals($expected['username'], $return); } /** * Data provider for get_listing. * * @return array * @covers ::get_listing */ public static function get_listing_provider(): array { return [ [ 'input' => [ 'url' => 'http://dummy.url.com', 'userfield' => 'nickname', 'value' => 'administrator', ], 'expected' => [ 'username' => 'administrator', ], ], [ 'input' => [ 'url' => 'http://dummy.url.com', 'userfield' => 'default', 'value' => '', ], 'expected' => [ 'username' => 'admin', ], ], [ 'input' => [ 'url' => 'http://dummy.url.com', 'userfield' => 'test', 'value' => 'test', ], 'expected' => [ 'username' => 'test', ], ], ]; } /** * Test that the method get_listing return the correct array. * * @dataProvider get_listing_provider * * @param array $input * @param array $expected * @covers ::get_listing * * @return void */ public function test_get_listing($input, $expected): void { global $USER; $this->initialise_repository(); $USER->profile[$input['userfield']] = $input['value']; $this->repo->set_option(['url' => $input['url'], 'equella_userfield' => $input['userfield']]); $listing = $this->repo->get_listing(); $this->assertArrayHasKey('manage', $listing); $this->assertStringContainsString($expected['username'], $listing['manage']); } /** * Create and initialise the repository for test. * @return void */ public function initialise_repository(): void { $this->resetAfterTest(true); // Admin is neccessary to create repository. $this->setAdminUser(); $this->create_repository(); $this->create_new_form_data(); } } PK*\YKN N equella/callback.phpnu[. /** * Callback for equella repository. * * @since Moodle 2.3 * @package repository_equella * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../config.php'); require_once($CFG->dirroot . '/repository/lib.php'); $json = required_param('tlelinks', PARAM_RAW); require_login(); $decodedinfo = json_decode($json); $info = array_pop($decodedinfo); $url = ''; if (isset($info->url)) { $url = s(clean_param($info->url, PARAM_URL)); } $filename = ''; // Use $info->filename if exists, $info->name is a display name, // it may not have extension if (isset($info->filename)) { $filename = s(clean_param($info->filename, PARAM_FILE)); } else if (isset($info->name)) { $filename = s(clean_param($info->name, PARAM_FILE)); } $thumbnail = ''; if (isset($info->thumbnail)) { $thumbnail = s(clean_param($info->thumbnail, PARAM_URL)); } $author = ''; if (isset($info->owner)) { $author = s(clean_param($info->owner, PARAM_NOTAGS)); } $license = ''; if (isset($info->license)) { $license = s(clean_param($info->license, PARAM_ALPHAEXT)); } $source = base64_encode(json_encode(array('url'=>$url,'filename'=>$filename))); $sourcekey = sha1($source . repository::get_secret_key() . sesskey()); $js =<< EOD; header('Content-Type: text/html; charset=utf-8'); die($js); PK*\4#filesystem/db/tasks.phpnu[. /** * Definition of file system repository scheduled tasks. * * @package repository_filesystem * @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_filesystem\task\cron_task', 'blocking' => 0, 'minute' => '*', 'hour' => '*', 'day' => '*', 'month' => '*', 'dayofweek' => '*' ) ); PK*\P0filesystem/db/access.phpnu[. /** * Plugin capabilities. * * @package repository_filesystem * @copyright 2009 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/filesystem:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'coursecreator' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ) ); PK*\{k  ,filesystem/lang/en/repository_filesystem.phpnu[. /** * Strings for component 'repository_filesystem', language 'en', branch 'MOODLE_20_STABLE' * * @package repository_filesystem * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['blockedpath'] = 'Blocked root path'; $string['configplugin'] = 'Configuration for file system repository'; $string['crontask'] = 'Background processing for file system repository'; $string['donotusesysdir'] = 'Don\'t use system directory as repository, including'; $string['filesystem:view'] = 'View file system repository'; $string['information'] = 'These folders are within the {$a} directory.'; $string['invalidpath'] = 'Invalid root path'; $string['path'] = 'Select a subdirectory'; $string['relativefiles'] = 'Allow relative files'; $string['relativefiles_desc'] = 'This allows all files in the repository to be accessible using relative links.'; $string['root'] = 'Root'; $string['nosubdir'] = 'You need to create at least one folder inside the {$a} directory so you can select it here.'; $string['pluginname_help'] = 'Create repository from local directory'; $string['pluginname'] = 'File system'; $string['searchresults'] = 'Search results'; $string['enablecourseinstances'] = 'Allow admins to add a file system repository instance to a course (configurable only by admins)'; $string['enableuserinstances'] = 'Allow admins to add a file system repository instance for personal use (configurable only by admins)'; $string['privacy:metadata'] = 'The File system repository plugin does not store or transmit any personal data.'; PK*\6aofilesystem/pix/icon.pngnu[PNG  IHDR(-SPLTEnpknpk KtRNS+NIDATxeυUDA /!<-3q۽\F)?ˉD5MP. /** * This plugin is used to access files on server file system * * @since Moodle 2.0 * @package repository_filesystem * @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'); require_once($CFG->libdir . '/filelib.php'); /** * repository_filesystem class * * Create a repository from your local filesystem * *NOTE* for security issue, we use a fixed repository path * which is %moodledata%/repository * * @package repository * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_filesystem extends repository { /** * The subdirectory of the instance. * * @var string */ protected $subdir; /** * Constructor * * @param int $repositoryid repository ID * @param int $context context ID * @param array $options */ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) { parent::__construct($repositoryid, $context, $options); $this->subdir = $this->get_option('fs_path'); } /** * Get the list of files and directories in that repository. * * @param string $fullpath Path to explore. This is assembled by {@link self::build_node_path()}. * @param string $page Page number. * @return array List of files and folders. */ public function get_listing($fullpath = '', $page = '') { global $OUTPUT; $list = array( 'list' => array(), 'manage' => false, 'dynload' => true, 'nologin' => true, 'path' => array() ); // We analyse the path to extract what to browse. $fullpath = empty($fullpath) ? $this->build_node_path('root') : $fullpath; $trail = explode('|', $fullpath); $trail = array_pop($trail); list($mode, $path, $unused) = $this->explode_node_path($trail); // Is that a search? if ($mode === 'search') { return $this->search($path, $page); } // Cleaning up the requested path. $path = trim($path, '/'); if (!$this->is_in_repository($path)) { // In case of doubt on the path, reset to default. $path = ''; } $rootpath = $this->get_rootpath(); $abspath = rtrim($rootpath . $path, '/') . '/'; // Retrieve list of files and directories and sort them. $fileslist = array(); $dirslist = array(); if ($dh = opendir($abspath)) { while (false !== ($file = readdir($dh))) { if ($file != '.' and $file != '..') { if (is_file($abspath . $file)) { $fileslist[] = $file; } else { $dirslist[] = $file; } } } closedir($dh); } core_collator::asort($fileslist, core_collator::SORT_NATURAL); core_collator::asort($dirslist, core_collator::SORT_NATURAL); // Fill the results. foreach ($dirslist as $file) { $list['list'][] = $this->build_node($rootpath, $path, $file, true, $fullpath); } foreach ($fileslist as $file) { $list['list'][] = $this->build_node($rootpath, $path, $file, false, $fullpath); } $list['path'] = $this->build_breadcrumb($fullpath); $list['list'] = array_filter($list['list'], array($this, 'filter')); return $list; } /** * Search files in repository. * * This search works by walking through the directories returning the files that match. Once * the limit of files is reached the walk stops. Whenever more files are requested, the walk * starts from the beginning until it reaches an additional set of files to return. * * @param string $query The query string. * @param int $page The page number. * @return mixed */ public function search($query, $page = 1) { global $OUTPUT, $SESSION; $query = core_text::strtolower($query); $remainingdirs = 1000; $remainingobjects = 5000; $perpage = 50; // Because the repository API is weird, the first page is 0, but it should be 1. if (!$page) { $page = 1; } // Initialise the session variable in which we store the search related things. if (!isset($SESSION->repository_filesystem_search)) { $SESSION->repository_filesystem_search = array(); } // Restore, or initialise the session search variables. if ($page <= 1) { $SESSION->repository_filesystem_search['query'] = $query; $SESSION->repository_filesystem_search['from'] = 0; $from = 0; } else { // Yes, the repository does not send the query again... $query = $SESSION->repository_filesystem_search['query']; $from = (int) $SESSION->repository_filesystem_search['from']; } $limit = $from + $perpage; $searchpath = $this->build_node_path('search', $query); // Pre-search initialisation. $rootpath = $this->get_rootpath(); $found = 0; $toexplore = array(''); // Retrieve list of matching files and directories. $matches = array(); while (($path = array_shift($toexplore)) !== null) { $remainingdirs--; if ($objects = scandir($rootpath . $path)) { foreach ($objects as $object) { $objectabspath = $rootpath . $path . $object; if ($object == '.' || $object == '..') { continue; } $remainingobjects--; $isdir = is_dir($objectabspath); // It is a match! if (strpos(core_text::strtolower($object), $query) !== false) { $found++; $matches[] = array($path, $object, $isdir); // That's enough, no need to find more. if ($found >= $limit) { break 2; } } // I've seen enough files, I give up! if ($remainingobjects <= 0) { break 2; } // Add the directory to things to explore later. if ($isdir) { $toexplore[] = $path . trim($object, '/') . '/'; } } } if ($remainingdirs <= 0) { break; } } // Extract the results from all the matches. $matches = array_slice($matches, $from, $perpage); // If we didn't reach our limits of browsing, and we appear to still have files to find. if ($remainingdirs > 0 && $remainingobjects > 0 && count($matches) >= $perpage) { $SESSION->repository_filesystem_search['from'] = $limit; $pages = -1; // We reached the end of the repository, or our limits. } else { $SESSION->repository_filesystem_search['from'] = 0; $pages = 0; } // Organise the nodes. $results = array(); foreach ($matches as $match) { list($path, $name, $isdir) = $match; $results[] = $this->build_node($rootpath, $path, $name, $isdir, $searchpath); } $list = array(); $list['list'] = array_filter($results, array($this, 'filter')); $list['dynload'] = true; $list['nologin'] = true; $list['page'] = $page; $list['pages'] = $pages; $list['path'] = $this->build_breadcrumb($searchpath); return $list; } /** * Build the breadcrumb from a full path. * * @param string $path A path generated by {@link self::build_node_path()}. * @return array */ protected function build_breadcrumb($path) { $breadcrumb = array(array( 'name' => get_string('root', 'repository_filesystem'), 'path' => $this->build_node_path('root') )); $crumbs = explode('|', $path); $trail = ''; foreach ($crumbs as $crumb) { list($mode, $nodepath, $display) = $this->explode_node_path($crumb); switch ($mode) { case 'search': $breadcrumb[] = array( 'name' => get_string('searchresults', 'repository_filesystem'), 'path' => $this->build_node_path($mode, $nodepath, $display, $trail), ); break; case 'browse': $breadcrumb[] = array( 'name' => $display, 'path' => $this->build_node_path($mode, $nodepath, $display, $trail), ); break; } $lastcrumb = end($breadcrumb); $trail = $lastcrumb['path']; } return $breadcrumb; } /** * Build a file or directory node. * * @param string $rootpath The absolute path to the repository. * @param string $path The relative path of the object * @param string $name The name of the object * @param string $isdir Is the object a directory? * @param string $rootnodepath The node leading to this node (for breadcrumb). * @return array */ protected function build_node($rootpath, $path, $name, $isdir, $rootnodepath) { global $OUTPUT; $relpath = trim($path, '/') . '/' . $name; $abspath = $rootpath . $relpath; $node = array( 'title' => $name, 'datecreated' => filectime($abspath), 'datemodified' => filemtime($abspath), ); if ($isdir) { $node['children'] = array(); $node['thumbnail'] = $OUTPUT->image_url(file_folder_icon())->out(false); $node['path'] = $this->build_node_path('browse', $relpath, $name, $rootnodepath); } else { $node['source'] = $relpath; $node['size'] = filesize($abspath); $node['thumbnail'] = $OUTPUT->image_url(file_extension_icon($name))->out(false); $node['icon'] = $OUTPUT->image_url(file_extension_icon($name))->out(false); $node['path'] = $relpath; if (file_extension_in_typegroup($name, 'image') && ($imageinfo = @getimagesize($abspath))) { // This means it is an image and we can return dimensions and try to generate thumbnail/icon. $token = $node['datemodified'] . $node['size']; // To prevent caching by browser. $node['realthumbnail'] = $this->get_thumbnail_url($relpath, 'thumb', $token)->out(false); $node['realicon'] = $this->get_thumbnail_url($relpath, 'icon', $token)->out(false); $node['image_width'] = $imageinfo[0]; $node['image_height'] = $imageinfo[1]; } } return $node; } /** * Build the path to a browsable node. * * @param string $mode The type of browse mode. * @param string $realpath The path, or similar. * @param string $display The way to display the node. * @param string $root The path preceding this node. * @return string */ protected function build_node_path($mode, $realpath = '', $display = '', $root = '') { $path = $mode . ':' . base64_encode($realpath) . ':' . base64_encode($display); if (!empty($root)) { $path = $root . '|' . $path; } return $path; } /** * Extract information from a node path. * * Note, this should not include preceding paths. * * @param string $path The path of the node. * @return array Contains the mode, the relative path, and the display text. */ protected function explode_node_path($path) { list($mode, $realpath, $display) = explode(':', $path); return array( $mode, base64_decode($realpath), base64_decode($display) ); } /** * To check whether the user is logged in. * * @return bool */ public function check_login() { return true; } /** * Show the login screen, if required. * * @return string */ public function print_login() { return true; } /** * Is it possible to do a global search? * * @return bool */ public function global_search() { return false; } /** * Return file path. * @return array */ public function get_file($file, $title = '') { global $CFG; $file = ltrim($file, '/'); if (!$this->is_in_repository($file)) { throw new repository_exception('Invalid file requested.'); } $file = $this->get_rootpath() . $file; // This is a hack to prevent move_to_file deleting files in local repository. $CFG->repository_no_delete = true; return array('path' => $file, 'url' => ''); } /** * Return the source information * * @param stdClass $filepath * @return string|null */ public function get_file_source_info($filepath) { return $filepath; } /** * Logout from repository instance * * @return string */ public function logout() { return true; } /** * Return names of the instance options. * * @return array */ public static function get_instance_option_names() { return array('fs_path', 'relativefiles'); } /** * Save settings for repository instance * * @param array $options settings * @return bool */ public function set_option($options = array()) { $options['fs_path'] = clean_param($options['fs_path'], PARAM_PATH); $options['relativefiles'] = clean_param($options['relativefiles'], PARAM_INT); $ret = parent::set_option($options); return $ret; } /** * Edit/Create Instance Settings Moodle form * * @param moodleform $mform Moodle form (passed by reference) */ public static function instance_config_form($mform) { global $CFG; if (has_capability('moodle/site:config', context_system::instance())) { $path = $CFG->dataroot . '/repository/'; if (!is_dir($path)) { mkdir($path, $CFG->directorypermissions, true); } if ($handle = opendir($path)) { $fieldname = get_string('path', 'repository_filesystem'); $choices = array(); while (false !== ($file = readdir($handle))) { if (is_dir($path . $file) && $file != '.' && $file != '..') { $choices[$file] = $file; $fieldname = ''; } } if (empty($choices)) { $mform->addElement('static', '', '', get_string('nosubdir', 'repository_filesystem', $path)); $mform->addElement('hidden', 'fs_path', ''); $mform->setType('fs_path', PARAM_PATH); } else { $mform->addElement('select', 'fs_path', $fieldname, $choices); $mform->addElement('static', null, '', get_string('information', 'repository_filesystem', $path)); } closedir($handle); } $mform->addElement('checkbox', 'relativefiles', get_string('relativefiles', 'repository_filesystem'), get_string('relativefiles_desc', 'repository_filesystem')); $mform->setType('relativefiles', PARAM_INT); } else { $mform->addElement('static', null, '', get_string('nopermissions', 'error', get_string('configplugin', 'repository_filesystem'))); return false; } } /** * Create an instance for this plug-in * * @static * @param string $type the type of the repository * @param int $userid the user id * @param stdClass $context the context * @param array $params the options for this instance * @param int $readonly whether to create it readonly or not (defaults to not) * @return mixed */ public static function create($type, $userid, $context, $params, $readonly=0) { if (has_capability('moodle/site:config', context_system::instance())) { return parent::create($type, $userid, $context, $params, $readonly); } else { require_capability('moodle/site:config', context_system::instance()); return false; } } /** * Validate repository plugin instance form * * @param moodleform $mform moodle form * @param array $data form data * @param array $errors errors * @return array errors */ public static function instance_form_validation($mform, $data, $errors) { $fspath = clean_param(trim($data['fs_path'], '/'), PARAM_PATH); if (empty($fspath) && !is_numeric($fspath)) { $errors['fs_path'] = get_string('invalidadminsettingname', 'error', 'fs_path'); } return $errors; } /** * User cannot use the external link to dropbox * * @return int */ public function supported_returntypes() { return FILE_INTERNAL | FILE_REFERENCE; } /** * Return human readable reference information * * @param string $reference value of DB field files_reference.reference * @param int $filestatus status of the file, 0 - ok, 666 - source missing * @return string */ public function get_reference_details($reference, $filestatus = 0) { $details = $this->get_name().': '.$reference; if ($filestatus) { return get_string('lostsource', 'repository', $details); } else { return $details; } } public function sync_reference(stored_file $file) { if ($file->get_referencelastsync() + 60 > time()) { // Does not cost us much to synchronise within our own filesystem, check every 1 minute. return false; } static $issyncing = false; if ($issyncing) { // Avoid infinite recursion when calling $file->get_filesize() and get_contenthash(). return false; } $filepath = $this->get_rootpath() . ltrim($file->get_reference(), '/'); if ($this->is_in_repository($file->get_reference()) && file_exists($filepath) && is_readable($filepath)) { $fs = get_file_storage(); $issyncing = true; if (file_extension_in_typegroup($filepath, 'web_image')) { $contenthash = file_storage::hash_from_path($filepath); if ($file->get_contenthash() == $contenthash) { // File did not change since the last synchronisation. $filesize = filesize($filepath); } else { // Copy file into moodle filepool (used to generate an image thumbnail). $file->set_timemodified(filemtime($filepath)); $file->set_synchronised_content_from_file($filepath); return true; } } else { // Update only file size so file will NOT be copied into moodle filepool. if ($file->compare_to_string('') || !$file->compare_to_path($filepath)) { // File is not synchronized or the file has changed. $contenthash = file_storage::hash_from_string(''); } else { // File content was synchronised and has not changed since then, leave it. $contenthash = null; } $filesize = filesize($filepath); } $issyncing = false; $modified = filemtime($filepath); $file->set_synchronized($contenthash, $filesize, 0, $modified); } else { $file->set_missingsource(); } return true; } /** * Repository method to serve the referenced file * * @see send_stored_file * * @param stored_file $storedfile the file that contains the reference * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime) * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin * @param array $options additional options affecting the file serving */ public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, ?array $options = null) { $reference = $storedfile->get_reference(); $file = $this->get_rootpath() . ltrim($reference, '/'); if ($this->is_in_repository($reference) && is_readable($file)) { $filename = $storedfile->get_filename(); if ($options && isset($options['filename'])) { $filename = $options['filename']; } $dontdie = ($options && isset($options['dontdie'])); // If the file is linked to the original then we should not cache it. $lifetime = 0; send_file($file, $filename, $lifetime , $filter, false, $forcedownload, '', $dontdie); } else { send_file_not_found(); } } /** * Is this repository accessing private data? * * @return bool */ public function contains_private_data() { return false; } /** * Return the rootpath of this repository instance. * * Trim() is a necessary step to ensure that the subdirectory is not '/'. * * @return string path * @throws repository_exception If the subdir is unsafe, or invalid. */ public function get_rootpath() { global $CFG; $subdir = clean_param(trim($this->subdir, '/'), PARAM_PATH); $path = $CFG->dataroot . '/repository/' . $this->subdir . '/'; if ((empty($this->subdir) && !is_numeric($this->subdir)) || $subdir != $this->subdir || !is_dir($path)) { throw new repository_exception('The instance is not properly configured, invalid path.'); } return $path; } /** * Checks if $path is part of this repository. * * Try to prevent $path hacks such as ../ . * * We do not use clean_param(, PARAM_PATH) here because it also trims down some * characters that are allowed, like < > ' . But we do ensure that the directory * is safe by checking that it starts with $rootpath. * * @param string $path relative path to a file or directory in the repo. * @return boolean false when not. */ protected function is_in_repository($path) { $rootpath = $this->get_rootpath(); if (strpos(realpath($rootpath . $path), realpath($rootpath)) !== 0) { return false; } return true; } /** * Returns url of thumbnail file. * * @param string $filepath current path in repository (dir and filename) * @param string $thumbsize 'thumb' or 'icon' * @param string $token identifier of the file contents - to prevent browser from caching changed file * @return moodle_url */ protected function get_thumbnail_url($filepath, $thumbsize, $token) { return moodle_url::make_pluginfile_url($this->context->id, 'repository_filesystem', $thumbsize, $this->id, '/' . trim($filepath, '/') . '/', $token); } /** * Returns the stored thumbnail file, generates it if not present. * * @param string $filepath current path in repository (dir and filename) * @param string $thumbsize 'thumb' or 'icon' * @return null|stored_file */ public function get_thumbnail($filepath, $thumbsize) { global $CFG; $filepath = trim($filepath, '/'); $origfile = $this->get_rootpath() . $filepath; // As thumbnail filename we use original file content hash. if (!$this->is_in_repository($filepath) || !($filecontents = @file_get_contents($origfile))) { // File is not found or is not readable. return null; } $filename = file_storage::hash_from_string($filecontents); // Try to get generated thumbnail for this file. $fs = get_file_storage(); if (!($file = $fs->get_file(SYSCONTEXTID, 'repository_filesystem', $thumbsize, $this->id, '/' . $filepath . '/', $filename))) { // Thumbnail not found . Generate and store thumbnail. require_once($CFG->libdir . '/gdlib.php'); if ($thumbsize === 'thumb') { $size = 90; } else { $size = 24; } if (!$data = generate_image_thumbnail_from_string($filecontents, $size, $size)) { // Generation failed. return null; } $record = array( 'contextid' => SYSCONTEXTID, 'component' => 'repository_filesystem', 'filearea' => $thumbsize, 'itemid' => $this->id, 'filepath' => '/' . $filepath . '/', 'filename' => $filename, ); $file = $fs->create_file_from_string($record, $data); } return $file; } /** * Run in cron for particular repository instance. Removes thumbnails for deleted/modified files. * * @param stored_file[] $storedfiles */ public function remove_obsolete_thumbnails($storedfiles) { // Group found files by filepath ('filepath' in Moodle file storage is dir+name in filesystem repository). $files = array(); foreach ($storedfiles as $file) { if (!isset($files[$file->get_filepath()])) { $files[$file->get_filepath()] = array(); } $files[$file->get_filepath()][] = $file; } // Loop through all files and make sure the original exists and has the same contenthash. $deletedcount = 0; foreach ($files as $filepath => $filesinpath) { if ($filecontents = @file_get_contents($this->get_rootpath() . trim($filepath, '/'))) { // The 'filename' in Moodle file storage is contenthash of the file in filesystem repository. $filename = file_storage::hash_from_string($filecontents); foreach ($filesinpath as $file) { if ($file->get_filename() !== $filename && $file->get_filename() !== '.') { // Contenthash does not match, this is an old thumbnail. $deletedcount++; $file->delete(); } } } else { // Thumbnail exist but file not. foreach ($filesinpath as $file) { if ($file->get_filename() !== '.') { $deletedcount++; } $file->delete(); } } } if ($deletedcount) { mtrace(" instance {$this->id}: deleted $deletedcount thumbnails"); } } /** * Gets a file relative to this file in the repository and sends it to the browser. * * @param stored_file $mainfile The main file we are trying to access relative files for. * @param string $relativepath the relative path to the file we are trying to access. */ public function send_relative_file(stored_file $mainfile, $relativepath) { global $CFG; // Check if this repository is allowed to use relative linking. $allowlinks = $this->supports_relative_file(); if (!empty($allowlinks)) { // Get path to the mainfile. $mainfilepath = $mainfile->get_source(); // Strip out filename from the path. $filename = $mainfile->get_filename(); $basepath = strstr($mainfilepath, $filename, true); $fullrelativefilepath = realpath($this->get_rootpath().$basepath.$relativepath); // Sanity check to make sure this path is inside this repository and the file exists. if (strpos($fullrelativefilepath, realpath($this->get_rootpath())) === 0 && file_exists($fullrelativefilepath)) { send_file($fullrelativefilepath, basename($relativepath), null, 0); } } send_file_not_found(); } /** * helper function to check if the repository supports send_relative_file. * * @return true|false */ public function supports_relative_file() { return $this->get_option('relativefiles'); } } /** * Generates and sends the thumbnail for an image in filesystem. * * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context context object * @param string $filearea file area * @param array $args extra arguments * @param bool $forcedownload whether or not force download * @param array $options additional options affecting the file serving * @return bool */ function repository_filesystem_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { global $OUTPUT, $CFG; // Allowed filearea is either thumb or icon - size of the thumbnail. if ($filearea !== 'thumb' && $filearea !== 'icon') { return false; } // As itemid we pass repository instance id. $itemid = array_shift($args); // Filename is some token that we can ignore (used only to make sure browser does not serve cached copy when file is changed). array_pop($args); // As filepath we use full filepath (dir+name) of the file in this instance of filesystem repository. $filepath = implode('/', $args); // Make sure file exists in the repository and is accessible. $repo = repository::get_repository_by_id($itemid, $context); $repo->check_capability(); // Find stored or generated thumbnail. if (!($file = $repo->get_thumbnail($filepath, $filearea))) { // Generation failed, redirect to default icon for file extension. // Do not use redirect() here because is not compatible with webservice/pluginfile.php. header('Location: ' . $OUTPUT->image_url(file_extension_icon($file))); } // The thumbnails should not be changing much, but maybe the default lifetime is too long. $lifetime = $CFG->filelifetime; if ($lifetime > 60*10) { $lifetime = 60*10; } send_stored_file($file, $lifetime, 0, $forcedownload, $options); } PK*\Ζfilesystem/version.phpnu[. /** * Version details * * @package repository * @subpackage filesystem * @copyright 2009 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_filesystem'; // Full name of the plugin (used for diagnostics) PK*\4'filesystem/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for repository_filesystem. * * @package repository_filesystem * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_filesystem\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for repository_filesystem implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK*\X>{ { %filesystem/classes/task/cron_task.phpnu[. namespace repository_filesystem\task; defined('MOODLE_INTERNAL') || die(); /** * A schedule task for file system repository cron. * * @package repository_filesystem * @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_filesystem'); } /** * Run file system repository cron. */ public function execute() { global $CFG; require_once($CFG->dirroot . '/repository/lib.php'); $fs = get_file_storage(); // Find all generated thumbnails and group them in array by itemid (itemid == repository instance id). $allfiles = array_merge( $fs->get_area_files(SYSCONTEXTID, 'repository_filesystem', 'thumb'), $fs->get_area_files(SYSCONTEXTID, 'repository_filesystem', 'icon') ); $filesbyitem = array(); foreach ($allfiles as $file) { if (!isset($filesbyitem[$file->get_itemid()])) { $filesbyitem[$file->get_itemid()] = array(); } $filesbyitem[$file->get_itemid()][] = $file; } // Find all instances of repository_filesystem. $instances = \repository::get_instances(array('type' => 'filesystem')); // Loop through all itemids of generated thumbnails. foreach ($filesbyitem as $itemid => $files) { if (!isset($instances[$itemid]) || !($instances[$itemid] instanceof repository_filesystem)) { // Instance was deleted. $fs->delete_area_files(SYSCONTEXTID, 'repository_filesystem', 'thumb', $itemid); $fs->delete_area_files(SYSCONTEXTID, 'repository_filesystem', 'icon', $itemid); mtrace(" instance $itemid does not exist: deleted all thumbnails"); } else { // Instance has some generated thumbnails, check that they are not outdated. $instances[$itemid]->remove_obsolete_thumbnails($files); } } } } PK*\N~~"filesystem/tests/generator/lib.phpnu[. /** * File system repository data generator * * @package repository_filesystem * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * File system repository data generator class * * @package repository_filesystem * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_filesystem_generator extends testing_repository_generator { /** * Fill in record defaults. * * @param array $record * @return array */ protected function prepare_record(array $record) { $record = parent::prepare_record($record); if (!isset($record['fs_path'])) { $record['fs_path'] = '/i/do/not/exist'; } if (!isset($record['relativefiles'])) { $record['relativefiles'] = 0; } return $record; } } PK*\9^^/filesystem/tests/repository_filesystem_test.phpnu[. /** * Tests for the repository_filesystem plugin. * * @package repository_filesystem * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \repository_filesystem */ final class repository_filesystem_test extends \advanced_testcase { public function test_get_listing(): void { global $CFG; $this->resetAfterTest(true); $user = get_admin(); $this->setUser($user); $this->getDataGenerator()->create_repository_type('filesystem'); mkdir($CFG->dataroot . '/repository/test/0', recursive: true); file_put_contents($CFG->dataroot . '/repository/test/0/test.txt', 'test'); $record = $this->getDataGenerator()->create_repository('filesystem', [ 'fs_path' => 'test', ]); /** @var repository_filesystem $repository */ $repository = repository::get_repository_by_id($record->id, \core\context\user::instance($user->id)); $listing = $repository->get_listing(); $this->assertCount(1, $listing['list']); $this->assertEquals(['0'], array_column($listing['list'], 'title')); } } PK*\0799draftfiles_manager.phpnu[. // // // NOTE TO ALL DEVELOPERS: this script must deal only with draft area of current user, it has to use only file_storage and no file_browser!! // /** * This file is used to manage draft files in non-javascript browsers * * @since Moodle 2.0 * @package core * @subpackage repository * @copyright 2010 Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../config.php'); require_once($CFG->libdir.'/filelib.php'); require_once('lib.php'); require_sesskey(); require_login(); // disable blocks in this page $PAGE->set_pagelayout('embedded'); // general parameters $action = optional_param('action', '', PARAM_ALPHA); $itemid = optional_param('itemid', '', PARAM_INT); // parameters for repository $contextid = optional_param('ctx_id', SYSCONTEXTID, PARAM_INT); // context ID $courseid = optional_param('course', SITEID, PARAM_INT); // course ID $env = optional_param('env', 'filepicker', PARAM_ALPHA); // opened in file picker, file manager or html editor $filename = optional_param('filename', '', PARAM_FILE); $targetpath = optional_param('targetpath', '', PARAM_PATH); $maxfiles = optional_param('maxfiles', -1, PARAM_INT); // maxfiles $maxbytes = optional_param('maxbytes', 0, PARAM_INT); // maxbytes $subdirs = optional_param('subdirs', 0, PARAM_INT); // maxbytes $areamaxbytes = optional_param('areamaxbytes', FILE_AREA_MAX_BYTES_UNLIMITED, PARAM_INT); // Area maxbytes. // draft area $newdirname = optional_param('newdirname', '', PARAM_FILE); $newfilename = optional_param('newfilename', '', PARAM_FILE); // path in draft area $draftpath = optional_param('draftpath', '/', PARAM_PATH); // user context $user_context = context_user::instance($USER->id); $PAGE->set_context($user_context); $fs = get_file_storage(); $params = array('ctx_id' => $contextid, 'itemid' => $itemid, 'env' => $env, 'course'=>$courseid, 'maxbytes'=>$maxbytes, 'areamaxbytes'=>$areamaxbytes, 'maxfiles'=>$maxfiles, 'subdirs'=>$subdirs, 'sesskey'=>sesskey()); $PAGE->set_url('/repository/draftfiles_manager.php', $params); $filepicker_url = new moodle_url("/repository/filepicker.php", $params); $params['action'] = 'browse'; $home_url = new moodle_url('/repository/draftfiles_manager.php', $params); switch ($action) { // delete draft files case 'deletedraft': if ($file = $fs->get_file($user_context->id, 'user', 'draft', $itemid, $draftpath, $filename)) { if ($file->is_directory()) { $pathname = $draftpath; if ($file->get_parent_directory()) { $draftpath = $file->get_parent_directory()->get_filepath(); } else { $draftpath = '/'; } // delete files in folder $files = $fs->get_directory_files($user_context->id, 'user', 'draft', $itemid, $pathname, true); foreach ($files as $storedfile) { $storedfile->delete(); } $file->delete(); } else { $file->delete(); } $home_url->param('draftpath', $draftpath); $home_url->param('action', 'browse'); redirect($home_url); } break; case 'renameform': echo $OUTPUT->header(); echo '"; $home_url->param('draftpath', $draftpath); $home_url->param('action', 'rename'); echo '
'; echo html_writer::label(get_string('enternewname', 'repository'), 'newfilename', array('class' => 'accesshide')); echo ' '; echo ' '; echo ' '; echo ' '; echo '
'; echo $OUTPUT->footer(); break; case 'rename': repository::update_draftfile($itemid, $draftpath, $filename, array('filename' => $newfilename)); $home_url->param('action', 'browse'); $home_url->param('draftpath', $draftpath); redirect($home_url); break; case 'downloaddir': $zipper = new zip_packer(); $file = $fs->get_file($user_context->id, 'user', 'draft', $itemid, $draftpath, '.'); if ($draftpath === '/') { $filename = get_string('files').'.zip'; } else { $filename = explode('/', trim($draftpath, '/')); $filename = array_pop($filename) . '.zip'; } $newdraftitemid = file_get_unused_draft_itemid(); if ($newfile = $zipper->archive_to_storage(array('/' => $file), $user_context->id, 'user', 'draft', $newdraftitemid, '/', $filename, $USER->id)) { $fileurl = moodle_url::make_draftfile_url($newdraftitemid, '/', $filename)->out(); header('Location: ' . $fileurl); } else { throw new \moodle_exception('cannotdownloaddir', 'repository'); } break; case 'zip': $zipper = new zip_packer(); $file = $fs->get_file($user_context->id, 'user', 'draft', $itemid, $draftpath, '.'); if (!$file->get_parent_directory()) { $parent_path = '/'; $filepath = '/'; $filename = get_string('files').'.zip'; } else { $parent_path = $file->get_parent_directory()->get_filepath(); $filepath = explode('/', trim($file->get_filepath(), '/')); $filepath = array_pop($filepath); $filename = $filepath.'.zip'; } $filename = repository::get_unused_filename($itemid, $parent_path, $filename); $newfile = $zipper->archive_to_storage(array($filepath => $file), $user_context->id, 'user', 'draft', $itemid, $parent_path, $filename, $USER->id); $home_url->param('action', 'browse'); $home_url->param('draftpath', $parent_path); redirect($home_url, get_string('ziped', 'repository')); break; case 'unzip': $zipper = new zip_packer(); $file = $fs->get_file($user_context->id, 'user', 'draft', $itemid, $draftpath, $filename); if ($newfile = $file->extract_to_storage($zipper, $user_context->id, 'user', 'draft', $itemid, $draftpath, $USER->id)) { $str = get_string('unzipped', 'repository'); } else { $str = get_string('cannotunzip', 'error'); } $home_url->param('action', 'browse'); $home_url->param('draftpath', $draftpath); redirect($home_url, $str); break; case 'movefile': if (!empty($targetpath)) { repository::update_draftfile($itemid, $draftpath, $filename, array('filepath' => $targetpath)); $home_url->param('action', 'browse'); $home_url->param('draftpath', $targetpath); redirect($home_url); } echo $OUTPUT->header(); echo $OUTPUT->container_start(); echo html_writer::link($home_url, get_string('back', 'repository')); echo $OUTPUT->container_end(); $data = new stdClass(); $home_url->param('action', 'movefile'); $home_url->param('draftpath', $draftpath); $home_url->param('filename', $filename); file_get_drafarea_folders($itemid, '/', $data); print_draft_area_tree($data, true, $home_url); echo $OUTPUT->footer(); break; case 'mkdirform': echo $OUTPUT->header(); echo $OUTPUT->container_start(); echo html_writer::link($home_url, get_string('back', 'repository')); echo $OUTPUT->container_end(); $home_url->param('draftpath', $draftpath); $home_url->param('action', 'mkdir'); echo '
'; echo html_writer::label(get_string('entername', 'repository'), 'newdirname', array('class' => 'accesshide')); echo ' '; echo ' '; echo ' '; echo '
'; echo $OUTPUT->footer(); break; case 'mkdir': $newfolderpath = $draftpath . trim($newdirname, '/') . '/'; $fs->create_directory($user_context->id, 'user', 'draft', $itemid, $newfolderpath); $home_url->param('action', 'browse'); if (!empty($newdirname)) { $home_url->param('draftpath', $newfolderpath); $str = get_string('createfoldersuccess', 'repository'); } else { $home_url->param('draftpath', $draftpath); $str = get_string('createfolderfail', 'repository'); } redirect($home_url, $str); break; case 'browse': default: $files = file_get_drafarea_files($itemid, $draftpath); $info = file_get_draft_area_info($itemid); $filecount = $info['filecount']; echo $OUTPUT->header(); if ((!empty($files) or $draftpath != '/') and $env == 'filemanager') { echo '
'; $home_url->param('action', 'browse'); $home_url->param('draftpath', '/'); echo '' . get_string('files') . ' ▶'; $trail = ''; if ($draftpath !== '/') { $path = '/' . trim($draftpath, '/') . '/'; $parts = explode('/', $path); foreach ($parts as $part) { if ($part != '') { $trail .= ('/'.$part.'/'); $data->path[] = array('name'=>$part, 'path'=>$trail); $home_url->param('draftpath', $trail); echo ' '.$part.' ▶ '; } } } echo '
'; } $filepicker_url->param('draftpath', $draftpath); $filepicker_url->param('savepath', $draftpath); $filepicker_url->param('action', 'plugins'); echo '
'; if ($env == 'filepicker') { $maxfiles = 1; } if ($filecount < $maxfiles || $maxfiles == -1) { echo ' '.get_string('addfile', 'repository').''; } if ($env == 'filemanager') { if (!empty($subdirs)) { $home_url->param('action', 'mkdirform'); echo ' '.get_string('makeafolder', 'moodle').''; } if (!empty($files->list)) { $home_url->param('action', 'downloaddir'); echo ' ' . html_writer::link($home_url, get_string('downloadfolder', 'repository'), array('target'=>'_blank')); } } echo '
'; if (!empty($files->list)) { echo '
    '; foreach ($files->list as $file) { if ($file->type != 'folder') { $drafturl = $file->url; // a file echo '
  • '; echo $OUTPUT->pix_icon(file_file_icon($file), '', 'moodle', array('class' => 'iconsmall')); echo html_writer::link($drafturl, $file->filename); $home_url->param('filename', $file->filename); $home_url->param('draftpath', $file->filepath); $home_url->param('action', 'deletedraft'); echo ' ['.get_string('delete').']'; $home_url->param('action', 'movefile'); echo ' ['.get_string('move').']'; $home_url->param('action', 'renameform'); echo ' ['.get_string('rename').']'; if (file_extension_in_typegroup($file->filename, 'archive', true)) { $home_url->param('action', 'unzip'); $home_url->param('draftpath', $file->filepath); echo ' ['.get_string('unzip').']'; } echo '
  • '; } else { // a folder echo '
  • '; echo $OUTPUT->pix_icon(file_folder_icon(), get_string('folder')); $home_url->param('action', 'browse'); $home_url->param('draftpath', $file->filepath); $filepathchunks = explode('/', trim($file->filepath, '/')); $foldername = trim(array_pop($filepathchunks), '/'); echo html_writer::link($home_url, $foldername); $home_url->param('draftpath', $file->filepath); $home_url->param('filename', $file->filename); $home_url->param('action', 'deletedraft'); echo ' ['.get_string('delete').']'; $home_url->param('action', 'zip'); echo ' [Zip]'; echo '
  • '; } } echo '
'; } else { echo get_string('nofilesavailable', 'repository'); } echo $OUTPUT->footer(); break; } function print_draft_area_tree($tree, $root, $url) { echo '
    '; if ($root) { $url->param('targetpath', '/'); if ($url->param('draftpath') == '/') { echo '
  • '.get_string('files').'
  • '; } else { echo '
  • '.get_string('files').'
  • '; } echo '
      '; if (isset($tree->children)) { $tree = $tree->children; } } if (!empty($tree)) { foreach ($tree as $node) { echo '
    • '; $url->param('targetpath', $node->filepath); if ($url->param('draftpath') != $node->filepath) { echo ''.$node->fullname.''; } else { echo $node->fullname; } echo '
    • '; if (!empty($node->children)) { print_draft_area_tree($node->children, false, $url); } } } if ($root) { echo '
    '; } echo '
'; } PK*\.}}googledocs/db/access.phpnu[. /** * Plugin capabilities. * * @package repository_googledocs * @copyright 2009 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'repository/googledocs:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'user' => CAP_ALLOW ) ) ); PK*\d%Čgoogledocs/db/upgrade.phpnu[. /** * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_repository_googledocs_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; } PK*\&!!googledocs/db/caches.phpnu[. /** * Googledocs repository cache definitions. * * This file is part of Moodle's cache API, affectionately called MUC. * It contains the components that are requried in order to use caching. * * @package repository_googledocs * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $definitions = array( // Used to store file ids for folders. // The keys used are full path to the folder, the values are the id in google drive. // The static acceleration size has been based upon the depths of a single path. 'folder' => array( 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => false, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 10, 'canuselocalstore' => true ), ); PK*\5Ʉ googledocs/repository_ajax.phpnu[. /** * The Web service script that is called from the filepicker upload. * * @package repository_googledocs * @copyright 2025 Raquel Ortega * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('AJAX_SCRIPT', true); require(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/lib/filelib.php'); require_once($CFG->dirroot . '/repository/lib.php'); $action = required_param('action', PARAM_ALPHA); $repoid = optional_param('repo_id', 0, PARAM_INT); $contextid = optional_param('contextid', 0, PARAM_INT); $itemid = optional_param('itemid', 0, PARAM_INT); require_login(); if (!confirm_sesskey()) { die(json_encode(['error' => get_string('invalidsesskey', 'error')])); } $context = context::instance_by_id($contextid, true); $repo = repository::get_repository_by_id($repoid, $contextid); if (!$repo) { die(json_encode(['error' => get_string('invalidrepositoryid', 'repository')])); } // Check permissions. $repo->check_capability(); $repo->check_login(); switch ($action) { case 'upload': $files = $_FILES['files']; $tmp = make_request_directory(); $savedfiles = []; if (is_array($files['name'])) { // Multiple files. foreach ($files['name'] as $idx => $name) { $dest = $tmp . '/' . basename($name); move_uploaded_file($files['tmp_name'][$idx], $dest); $savedfiles[] = $dest; } } else { // Single file. $dest = $tmp . '/' . basename($files['name']); move_uploaded_file($files['tmp_name'], $dest); $savedfiles[] = $dest; } // Upload the file to Google Drive repository. $ha = new repository_googledocs($repoid, $context); $userauth = $ha->get_user_oauth_client(); $userservice = new repository_googledocs\rest($userauth); foreach ($savedfiles as $file) { $filename = basename($file); $ha->upload_file($userservice, $file, $filename, 'download', "root"); } echo json_encode(['success' => true]); break; default: echo json_encode(['error' => get_string('error', 'repository')]); } PK*\4إ ,googledocs/lang/en/repository_googledocs.phpnu[. /** * Strings for component 'repository_googledocs', language 'en', branch 'MOODLE_20_STABLE' * * @package repository_googledocs * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['configplugin'] = 'Configure Google Drive plugin'; $string['docsformat'] = 'Default document import format'; $string['drawingformat'] = 'Default drawing import format'; $string['googledocs:view'] = 'View Google Drive repository'; $string['importformat'] = 'Configure the default import formats from Google'; $string['mydrive'] = 'My Drive'; $string['pluginname'] = 'Google Drive'; $string['presentationformat'] = 'Default presentation import format'; $string['shareddrives'] = 'Shared Drives'; $string['spreadsheetformat'] = 'Default spreadsheet import format'; $string['issuer'] = 'OAuth 2 service'; $string['issuer_help'] = 'Select the OAuth 2 service that is configured to talk to the Google Drive API. If the service does not exist yet, you will need to create it.'; $string['servicenotenabled'] = 'Access not configured. Make sure the service \'Drive API\' is enabled.'; $string['oauth2serviceslink'] = 'OAuth 2 services configuration'; $string['searchfor'] = 'Search results for:'; $string['internal'] = 'Internal (files stored in Moodle)'; $string['external'] = 'External (only links stored in Moodle)'; $string['both'] = 'Internal and external'; $string['supportedreturntypes'] = 'Supported files'; $string['defaultreturntype'] = 'Default return type'; $string['fileoptions'] = 'The types and defaults for returned files is configurable here. Note that all files linked externally will be updated so that the owner is the Moodle system account.'; $string['owner'] = 'Owned by: {$a}'; $string['cachedef_folder'] = 'Google file IDs for folders in the system account'; $string['privacy:metadata:repository_googledocs'] = 'The Google Drive repository plugin does not store any personal data, but does transmit user data from Moodle to the remote system.'; $string['privacy:metadata:repository_googledocs:email'] = 'The email of the Google Drive repository user.'; $string['privacy:metadata:repository_googledocs:searchtext'] = 'The Google Drive repository user search text query.'; PK*\PPgoogledocs/pix/icon.pngnu[PNG  IHDRaIDATx3[ߙm]اo┱ضm~g&=@ܝHxK9Zή_<DDe]&]Mtk 2$TR[$LHy>E4dCJ.A_ Sc7AO{JUxLd”K|/h?9A(#j&*x̱X_J_NTЧ4 f7#= X2"**!STm+Cuߺ K#ӈqKt0_N63Āhy.|x#ƓE^PH5̨Hm/ {,JjS@Ty$}"3r£@~&*gG^D? jH3QeH#Ь*$M3Q>G*N4#ONDd@j^煟j FQ"8e]dhfF'eIENDB`PK*\aagoogledocs/lib.phpnu[. /** * This plugin is used to access Google Drive. * * @since Moodle 2.0 * @package repository_googledocs * @copyright 2009 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/repository/lib.php'); require_once($CFG->libdir . '/filebrowser/file_browser.php'); require_once($CFG->libdir . '/google/lib.php'); use repository_googledocs\helper; use repository_googledocs\googledocs_content_search; /** * Google Docs Plugin * * @since Moodle 2.0 * @package repository_googledocs * @copyright 2009 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_googledocs extends repository { /** * OAuth 2 client * @var \core\oauth2\client */ private $client = null; /** * OAuth 2 Issuer * @var \core\oauth2\issuer */ private $issuer = null; /** @var string Defines the path node identifier for the repository root. */ const REPOSITORY_ROOT_ID = 'repository_root'; /** @var string Defines the path node identifier for the my drive root. */ const MY_DRIVE_ROOT_ID = 'root'; /** @var string Defines the path node identifier for the shared drives root. */ const SHARED_DRIVES_ROOT_ID = 'shared_drives_root'; /** @var string Defines the path node identifier for the content search root. */ const SEARCH_ROOT_ID = 'search'; /** * Constructor. * * @param int $repositoryid repository instance id. * @param int|stdClass $context a context id or context object. * @param array $options repository options. * @param int $readonly indicate this repo is readonly or not. * @return void */ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) { global $PAGE; parent::__construct($repositoryid, $context, $options, $readonly = 0); try { $this->issuer = \core\oauth2\api::get_issuer(get_config('googledocs', 'issuerid')); } catch (dml_missing_record_exception $e) { $this->disabled = true; } if ($this->issuer && !$this->issuer->get('enabled')) { $this->disabled = true; } $PAGE->requires->js_call_amd('repository_googledocs/upload', 'init'); } /** * Get a cached user authenticated oauth client. * * @param moodle_url $overrideurl - Use this url instead of the repo callback. * @return \core\oauth2\client */ public function get_user_oauth_client($overrideurl = false) { if ($this->client) { return $this->client; } if ($overrideurl) { $returnurl = $overrideurl; } else { $returnurl = new moodle_url('/repository/repository_callback.php'); $returnurl->param('callback', 'yes'); $returnurl->param('repo_id', $this->id); $returnurl->param('sesskey', sesskey()); } $this->client = \core\oauth2\api::get_user_oauth_client($this->issuer, $returnurl, Google_Service_Drive::DRIVE_FILE, true); return $this->client; } /** * Checks whether the user is authenticate or not. * * @return bool true when logged in. */ public function check_login() { $client = $this->get_user_oauth_client(); return $client->is_logged_in(); } /** * Print or return the login form. * * @return void|array for ajax. */ public function print_login() { $client = $this->get_user_oauth_client(); $url = $client->get_login_url(); if ($this->options['ajax']) { $popup = new stdClass(); $popup->type = 'popup'; $popup->url = $url->out(false); return array('login' => array($popup)); } else { echo ''.get_string('login', 'repository').''; } } /** * Print the login in a popup. * * @param array|null $attr Custom attributes to be applied to popup div. */ public function print_login_popup($attr = null) { global $OUTPUT, $PAGE; $client = $this->get_user_oauth_client(false); $url = new moodle_url($client->get_login_url()); $state = $url->get_param('state') . '&reloadparent=true'; $url->param('state', $state); $PAGE->set_pagelayout('embedded'); echo $OUTPUT->header(); $repositoryname = get_string('pluginname', 'repository_googledocs'); $button = new single_button( $url, get_string('logintoaccount', 'repository', $repositoryname), 'post', single_button::BUTTON_PRIMARY ); $button->add_action(new popup_action('click', $url, 'Login')); $button->class = 'mdl-align'; $button = $OUTPUT->render($button); echo html_writer::div($button, '', $attr); echo $OUTPUT->footer(); } /** * Build the breadcrumb from a path. * * @deprecated since Moodle 3.11. * @param string $path to create a breadcrumb from. * @return array containing name and path of each crumb. */ protected function build_breadcrumb($path) { debugging('The function build_breadcrumb() is deprecated, please use get_navigation() from the ' . 'googledocs repository content classes instead.', DEBUG_DEVELOPER); $bread = explode('/', $path); $crumbtrail = ''; foreach ($bread as $crumb) { list($id, $name) = $this->explode_node_path($crumb); $name = empty($name) ? $id : $name; $breadcrumb[] = array( 'name' => $name, 'path' => $this->build_node_path($id, $name, $crumbtrail) ); $tmp = end($breadcrumb); $crumbtrail = $tmp['path']; } return $breadcrumb; } /** * Generates a safe path to a node. * * Typically, a node will be id|Name of the node. * * @deprecated since Moodle 3.11. * @param string $id of the node. * @param string $name of the node, will be URL encoded. * @param string $root to append the node on, must be a result of this function. * @return string path to the node. */ protected function build_node_path($id, $name = '', $root = '') { debugging('The function build_node_path() is deprecated, please use ' . '\repository_googledocs\helper::build_node_path() instead.', DEBUG_DEVELOPER); $path = $id; if (!empty($name)) { $path .= '|' . urlencode($name); } if (!empty($root)) { $path = trim($root, '/') . '/' . $path; } return $path; } /** * Returns information about a node in a path. * * @deprecated since Moodle 3.11. * @see self::build_node_path() * @param string $node to extrat information from. * @return array about the node. */ protected function explode_node_path($node) { debugging('The function explode_node_path() is deprecated, please use ' . '\repository_googledocs\helper::explode_node_path() instead.', DEBUG_DEVELOPER); if (strpos($node, '|') !== false) { list($id, $name) = explode('|', $node, 2); $name = urldecode($name); } else { $id = $node; $name = ''; } $id = urldecode($id); return array( 0 => $id, 1 => $name, 'id' => $id, 'name' => $name ); } /** * List the files and folders. * * @param string $path path to browse. * @param string $page page to browse. * @return array of result. */ public function get_listing($path='', $page = '') { if (empty($path)) { $pluginname = get_string('pluginname', 'repository_googledocs'); $path = helper::build_node_path('repository_root', $pluginname); } // Make sure the current path points to "My Drive" before listing files. $mydrive = get_string('mydrive', 'repository_googledocs'); $mydrivepath = helper::build_node_path('root', $mydrive); if (!str_contains($path, $mydrivepath)) { $path = $path . '/' . $mydrivepath; } if (!$this->issuer->get('enabled')) { // Empty list of files for disabled repository. return [ 'dynload' => false, 'list' => [], 'nologin' => true, ]; } // We analyse the path to extract what to browse. $trail = explode('/', $path); $uri = array_pop($trail); list($id, $name) = helper::explode_node_path($uri); $service = new repository_googledocs\rest($this->get_user_oauth_client()); // Define the content class object and query which will be used to get the contents for this path. if ($id === self::SEARCH_ROOT_ID) { // The special keyword 'search' is the ID of the node. This is possible as we can set up a breadcrumb in // the search results. Therefore, we should use the content search object to get the results from the // previously performed search. $contentobj = new googledocs_content_search($service, $path); // We need to deconstruct the node name in order to obtain the search term and use it as a query. $query = str_replace(get_string('searchfor', 'repository_googledocs'), '', $name); $query = trim(str_replace("'", "", $query)); } else { // Otherwise, return and use the appropriate (based on the path) content browser object. $contentobj = helper::get_browser($service, $path); // Use the node ID as a query. $query = $id; } return [ 'dynload' => true, 'defaultreturntype' => $this->default_returntype(), 'path' => $contentobj->get_navigation(), 'list' => $contentobj->get_content_nodes($query, [$this, 'filter']), 'uploadfile' => true, 'uploadevent' => 'repository_googledocs_upload', 'repo_id' => $this->id, 'contextid' => $this->context->id, 'sesskey' => sesskey(), ]; } /** * Search throughout the Google Drive. * * @param string $searchtext text to search for. * @param int $page search page. * @return array of results. */ public function search($searchtext, $page = 0) { // Construct the path to the repository root. $pluginname = get_string('pluginname', 'repository_googledocs'); $rootpath = helper::build_node_path(self::REPOSITORY_ROOT_ID, $pluginname); // Construct the path to the search results node. // Currently, when constructing the search node name, the search term is concatenated to the lang string. // This was done deliberately so that we can easily and accurately obtain the search term from the search node // name later when navigating to the search results through the breadcrumb navigation. $name = get_string('searchfor', 'repository_googledocs') . " '{$searchtext}'"; $path = helper::build_node_path(self::SEARCH_ROOT_ID, $name, $rootpath); $service = new repository_googledocs\rest($this->get_user_oauth_client()); $searchobj = new googledocs_content_search($service, $path); return [ 'dynload' => true, 'path' => $searchobj->get_navigation(), 'list' => $searchobj->get_content_nodes($searchtext, [$this, 'filter']), 'manage' => 'https://drive.google.com/', ]; } /** * Query Google Drive for files and folders using a search query. * * Documentation about the query format can be found here: * https://developers.google.com/drive/search-parameters * * This returns a list of files and folders with their details as they should be * formatted and returned by functions such as get_listing() or search(). * * @deprecated since Moodle 3.11. * @param string $q search query as expected by the Google API. * @param string $path parent path of the current files, will not be used for the query. * @param int $page page. * @return array of files and folders. */ protected function query($q, $path = null, $page = 0) { debugging('The function query() is deprecated, please use get_content_nodes() from the ' . 'googledocs repository content classes instead.', DEBUG_DEVELOPER); global $OUTPUT; $files = array(); $folders = array(); $config = get_config('googledocs'); $fields = "files(id,name,mimeType,webContentLink,webViewLink,fileExtension,modifiedTime,size,thumbnailLink,iconLink)"; $params = array('q' => $q, 'fields' => $fields, 'spaces' => 'drive'); try { // Retrieving files and folders. $client = $this->get_user_oauth_client(); $service = new repository_googledocs\rest($client); $response = $service->call('list', $params); } catch (Exception $e) { if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) { // This is raised when the service Drive API has not been enabled on Google APIs control panel. throw new repository_exception('servicenotenabled', 'repository_googledocs'); } else { throw $e; } } $gfiles = isset($response->files) ? $response->files : array(); foreach ($gfiles as $gfile) { if ($gfile->mimeType == 'application/vnd.google-apps.folder') { // This is a folder. $folders[$gfile->name . $gfile->id] = array( 'title' => $gfile->name, 'path' => $this->build_node_path($gfile->id, $gfile->name, $path), 'date' => strtotime($gfile->modifiedTime), 'thumbnail' => $OUTPUT->image_url(file_folder_icon())->out(false), 'thumbnail_height' => 64, 'thumbnail_width' => 64, 'children' => array() ); } else { // This is a file. $link = isset($gfile->webViewLink) ? $gfile->webViewLink : ''; if (empty($link)) { $link = isset($gfile->webContentLink) ? $gfile->webContentLink : ''; } if (isset($gfile->fileExtension)) { // The file has an extension, therefore we can download it. $source = json_encode([ 'id' => $gfile->id, 'name' => $gfile->name, 'exportformat' => 'download', 'link' => $link ]); $title = $gfile->name; } else { // The file is probably a Google Doc file, we get the corresponding export link. // This should be improved by allowing the user to select the type of export they'd like. $type = str_replace('application/vnd.google-apps.', '', $gfile->mimeType); $title = ''; $exporttype = ''; $types = get_mimetypes_array(); switch ($type){ case 'document': $ext = $config->documentformat; $title = $gfile->name . '.gdoc'; if ($ext === 'rtf') { // Moodle user 'text/rtf' as the MIME type for RTF files. // Google uses 'application/rtf' for the same type of file. // See https://developers.google.com/drive/v3/web/manage-downloads. $exporttype = 'application/rtf'; } else { $exporttype = $types[$ext]['type']; } break; case 'presentation': $ext = $config->presentationformat; $title = $gfile->name . '.gslides'; $exporttype = $types[$ext]['type']; break; case 'spreadsheet': $ext = $config->spreadsheetformat; $title = $gfile->name . '.gsheet'; $exporttype = $types[$ext]['type']; break; case 'drawing': $ext = $config->drawingformat; $title = $gfile->name . '.'. $ext; $exporttype = $types[$ext]['type']; break; } // Skips invalid/unknown types. if (empty($title)) { continue; } $source = json_encode([ 'id' => $gfile->id, 'exportformat' => $exporttype, 'link' => $link, 'name' => $gfile->name ]); } // Adds the file to the file list. Using the itemId along with the name as key // of the array because Google Drive allows files with identical names. $thumb = ''; if (isset($gfile->thumbnailLink)) { $thumb = $gfile->thumbnailLink; } else if (isset($gfile->iconLink)) { $thumb = $gfile->iconLink; } $files[$title . $gfile->id] = array( 'title' => $title, 'source' => $source, 'date' => strtotime($gfile->modifiedTime), 'size' => isset($gfile->size) ? $gfile->size : null, 'thumbnail' => $thumb, 'thumbnail_height' => 64, 'thumbnail_width' => 64, ); } } // Filter and order the results. $files = array_filter($files, array($this, 'filter')); core_collator::ksort($files, core_collator::SORT_NATURAL); core_collator::ksort($folders, core_collator::SORT_NATURAL); return array_merge(array_values($folders), array_values($files)); } /** * Logout. * * @return string */ public function logout() { $client = $this->get_user_oauth_client(); $client->log_out(); return parent::logout(); } /** * Get a file. * * @param string $reference reference of the file. * @param string $file name to save the file to. * @return string JSON encoded array of information about the file. */ public function get_file($reference, $filename = '') { global $CFG; if (!$this->issuer->get('enabled')) { throw new repository_exception('cannotdownload', 'repository'); } $source = json_decode($reference); $client = null; if (!empty($source->usesystem)) { $client = \core\oauth2\api::get_system_oauth_client($this->issuer); } else { $client = $this->get_user_oauth_client(); } $base = 'https://www.googleapis.com/drive/v3'; $newfilename = false; if ($source->exportformat == 'download') { $params = ['alt' => 'media']; $sourceurl = new moodle_url($base . '/files/' . $source->id, $params); $source = $sourceurl->out(false); } else { $params = ['mimeType' => $source->exportformat]; $sourceurl = new moodle_url($base . '/files/' . $source->id . '/export', $params); $types = get_mimetypes_array(); $checktype = $source->exportformat; if ($checktype == 'application/rtf') { $checktype = 'text/rtf'; } // Determine the relevant default import format config for the given file. switch ($source->googledoctype) { case 'document': $importformatconfig = get_config('googledocs', 'documentformat'); break; case 'presentation': $importformatconfig = get_config('googledocs', 'presentationformat'); break; case 'spreadsheet': $importformatconfig = get_config('googledocs', 'spreadsheetformat'); break; case 'drawing': $importformatconfig = get_config('googledocs', 'drawingformat'); break; default: $importformatconfig = null; } foreach ($types as $extension => $info) { if ($info['type'] == $checktype && $extension === $importformatconfig) { $newfilename = $source->name . '.' . $extension; break; } } $source = $sourceurl->out(false); } // We use download_one and not the rest API because it has special timeouts etc. $path = $this->prepare_file($filename); $options = ['filepath' => $path, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5]; $success = $client->download_one($source, null, $options); if ($success) { @chmod($path, $CFG->filepermissions); $result = [ 'path' => $path, 'url' => $reference, ]; if (!empty($newfilename)) { $result['newfilename'] = $newfilename; } return $result; } throw new repository_exception('cannotdownload', 'repository'); } /** * Prepare file reference information. * * We are using this method to clean up the source to make sure that it * is a valid source. * * @param string $source of the file. * @return string file reference. */ public function get_file_reference($source) { // We could do some magic upgrade code here. return $source; } /** * What kind of files will be in this repository? * * @return array return '*' means this repository support any files, otherwise * return mimetypes of files, it can be an array */ public function supported_filetypes() { return '*'; } /** * Tells how the file can be picked from this repository. * * @return int */ public function supported_returntypes() { // We can only support references if the system account is connected. if (!empty($this->issuer) && $this->issuer->is_system_account_connected()) { $setting = get_config('googledocs', 'supportedreturntypes'); if ($setting == 'internal') { return FILE_INTERNAL; } else if ($setting == 'external') { return FILE_CONTROLLED_LINK; } else { return FILE_CONTROLLED_LINK | FILE_INTERNAL; } } else { return FILE_INTERNAL; } } /** * Which return type should be selected by default. * * @return int */ public function default_returntype() { $setting = get_config('googledocs', 'defaultreturntype'); $supported = get_config('googledocs', 'supportedreturntypes'); if (($setting == FILE_INTERNAL && $supported != 'external') || $supported == 'internal') { return FILE_INTERNAL; } else { return FILE_CONTROLLED_LINK; } } /** * Return names of the general options. * By default: no general option name. * * @return array */ public static function get_type_option_names() { return array('issuerid', 'pluginname', 'documentformat', 'drawingformat', 'presentationformat', 'spreadsheetformat', 'defaultreturntype', 'supportedreturntypes'); } /** * Store the access token. */ public function callback() { $client = $this->get_user_oauth_client(); // This will upgrade to an access token if we have an authorization code and save the access token in the session. $client->is_logged_in(); } /** * Repository method to serve the referenced file * * @see send_stored_file * * @param stored_file $storedfile the file that contains the reference * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime) * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin * @param array $options additional options affecting the file serving */ public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, ?array $options = null) { if (!$this->issuer->get('enabled')) { throw new repository_exception('cannotdownload', 'repository'); } $source = json_decode($storedfile->get_reference()); $fb = get_file_browser(); $context = context::instance_by_id($storedfile->get_contextid(), MUST_EXIST); $info = $fb->get_file_info($context, $storedfile->get_component(), $storedfile->get_filearea(), $storedfile->get_itemid(), $storedfile->get_filepath(), $storedfile->get_filename()); if (empty($options['offline']) && !empty($info) && $info->is_writable() && !empty($source->usesystem)) { // Add the current user as an OAuth writer. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); if ($systemauth === false) { $details = 'Cannot connect as system user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } $systemservice = new repository_googledocs\rest($systemauth); // Get the user oauth so we can get the account to add. $url = moodle_url::make_pluginfile_url($storedfile->get_contextid(), $storedfile->get_component(), $storedfile->get_filearea(), $storedfile->get_itemid(), $storedfile->get_filepath(), $storedfile->get_filename(), $forcedownload); $url->param('sesskey', sesskey()); $param = (isset($options['embed']) && $options['embed'] == true) ? false : $url; $userauth = $this->get_user_oauth_client($param); if (!$userauth->is_logged_in()) { if (isset($options['embed']) && $options['embed'] == true) { // Due to Same-origin policy, we cannot redirect to googledocs login page. // If the requested file is embed and the user is not logged in, add option to log in using a popup. $this->print_login_popup(['style' => 'margin-top: 250px']); exit; } redirect($userauth->get_login_url()); } if ($userauth === false) { $details = 'Cannot connect as current user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } $userinfo = $userauth->get_userinfo(); $useremail = $userinfo['email']; $this->add_temp_writer_to_file($systemservice, $source->id, $useremail); } if (!empty($options['offline'])) { $downloaded = $this->get_file($storedfile->get_reference(), $storedfile->get_filename()); $filename = $storedfile->get_filename(); if (isset($downloaded['newfilename'])) { $filename = $downloaded['newfilename']; } send_file($downloaded['path'], $filename, $lifetime, $filter, false, $forcedownload, '', false, $options); } else if ($source->link) { // Do not use redirect() here because is not compatible with webservice/pluginfile.php. header('Location: ' . $source->link); } else { $details = 'File is missing source link'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } } /** * See if a folder exists within a folder * * @param \repository_googledocs\rest $client Authenticated client. * @param string $foldername The folder we are looking for. * @param string $parentid The parent folder we are looking in. * @return string|boolean The file id if it exists or false. */ protected function folder_exists_in_folder(\repository_googledocs\rest $client, $foldername, $parentid) { $q = '\'' . addslashes($parentid) . '\' in parents and trashed = false and name = \'' . addslashes($foldername). '\''; $fields = 'files(id, name)'; $params = [ 'q' => $q, 'fields' => $fields]; $response = $client->call('list', $params); $missing = true; foreach ($response->files as $child) { if ($child->name == $foldername) { return $child->id; } } return false; } /** * Create a folder within a folder * * @param \repository_googledocs\rest $client Authenticated client. * @param string $foldername The folder we are creating. * @param string $parentid The parent folder we are creating in. * * @return string The file id of the new folder. */ protected function create_folder_in_folder(\repository_googledocs\rest $client, $foldername, $parentid) { $fields = 'id'; $params = ['fields' => $fields]; $folder = ['mimeType' => 'application/vnd.google-apps.folder', 'name' => $foldername, 'parents' => [$parentid]]; $created = $client->call('create', $params, json_encode($folder)); if (empty($created->id)) { $details = 'Cannot create folder:' . $foldername; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return $created->id; } /** * Get simple file info for humans. * * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are querying. * * @return stdClass */ protected function get_file_summary(\repository_googledocs\rest $client, $fileid) { $fields = "id,name,owners,parents,webContentLink,webViewLink"; $params = [ 'fileid' => $fileid, 'fields' => $fields ]; return $client->call('get', $params); } /** * Add a writer to the permissions on the file (temporary). * * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @param string $email The email of the writer account to add. * @return boolean */ protected function add_temp_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) { // Expires in 7 days. $expires = new DateTime(); $expires->add(new DateInterval("P7D")); $updateeditor = [ 'emailAddress' => $email, 'role' => 'writer', 'type' => 'user', 'expirationTime' => $expires->format(DateTime::RFC3339) ]; $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false', 'supportsAllDrives' => 'true']; $response = $client->call('create_permission', $params, json_encode($updateeditor)); if (empty($response->id)) { $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return true; } /** * Add a writer to the permissions on the file. * * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @param string $email The email of the writer account to add. * @return boolean */ protected function add_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) { $updateeditor = [ 'emailAddress' => $email, 'role' => 'writer', 'type' => 'user' ]; $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false', 'supportsAllDrives' => 'true']; $response = $client->call('create_permission', $params, json_encode($updateeditor)); if (empty($response->id)) { $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return true; } /** * Move from root to folder * * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @param string $folderid The id of the folder we are moving to * @return boolean */ protected function move_file_from_root_to_folder(\repository_googledocs\rest $client, $fileid, $folderid) { // Set the parent. $params = [ 'fileid' => $fileid, 'addParents' => $folderid, 'removeParents' => 'root' ]; $response = $client->call('update', $params, ' '); if (empty($response->id)) { $details = 'Cannot move the file to a folder: ' . $fileid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return true; } /** * Prevent writers from sharing. * * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @return boolean */ protected function prevent_writers_from_sharing_file(\repository_googledocs\rest $client, $fileid) { // We don't want anyone but Moodle to change the sharing settings. $params = [ 'fileid' => $fileid ]; $update = [ 'writersCanShare' => false ]; $response = $client->call('update', $params, json_encode($update)); if (empty($response->id)) { $details = 'Cannot prevent writers from sharing document: ' . $fileid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return true; } /** * Allow anyone with the link to read the file. * * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @return boolean */ protected function set_file_sharing_anyone_with_link_can_read(\repository_googledocs\rest $client, $fileid) { $updateread = [ 'type' => 'anyone', 'role' => 'reader', 'allowFileDiscovery' => 'false' ]; $params = ['fileid' => $fileid, 'supportsAllDrives' => 'true']; $response = $client->call('create_permission', $params, json_encode($updateread)); if (empty($response->id) || $response->id != 'anyoneWithLink') { $details = 'Cannot update link sharing for the document: ' . $fileid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return true; } /** * Called when a file is selected as a "link". * Invoked at MOODLE/repository/repository_ajax.php * * This is called at the point the reference files are being copied from the draft area to the real area * (when the file has really really been selected. * * @param string $reference this reference is generated by * repository::get_file_reference() * @param context $context the target context for this new file. * @param string $component the target component for this new file. * @param string $filearea the target filearea for this new file. * @param string $itemid the target itemid for this new file. * @return string updated reference (final one before it's saved to db). */ public function reference_file_selected($reference, $context, $component, $filearea, $itemid) { global $CFG, $SITE; // What we need to do here is transfer ownership to the system user (or copy) // then set the permissions so anyone with the share link can view, // finally update the reference to contain the share link if it was not // already there (and point to new file id if we copied). // Get the details from the reference. $source = json_decode($reference); if (!empty($source->usesystem)) { // If we already copied this file to the system account - we are done. return $reference; } // Check this issuer is enabled. if ($this->disabled) { throw new repository_exception('cannotdownload', 'repository'); } // Get a system oauth client and a user oauth client. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); if ($systemauth === false) { $details = 'Cannot connect as system user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } // Get the system user email so we can share the file with this user. $systemuserinfo = $systemauth->get_userinfo(); $systemuseremail = $systemuserinfo['email']; $userauth = $this->get_user_oauth_client(); if ($userauth === false) { $details = 'Cannot connect as current user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } $userservice = new repository_googledocs\rest($userauth); $systemservice = new repository_googledocs\rest($systemauth); $this->add_writer_to_file($userservice, $source->id, $systemuseremail); // Now move it to a sensible folder. $contextlist = array_reverse($context->get_parent_contexts(true)); $cache = cache::make('repository_googledocs', 'folder'); $parentid = 'root'; $fullpath = 'root'; $allfolders = []; foreach ($contextlist as $context) { // Prepare human readable context folders names, making sure they are still unique within the site. $prevlang = force_current_language($CFG->lang); $foldername = $context->get_context_name(); force_current_language($prevlang); if ($context->contextlevel == CONTEXT_SYSTEM) { // Append the site short name to the root folder. $foldername .= ' ('.$SITE->shortname.')'; // Append the relevant object id. } else if ($context->instanceid) { $foldername .= ' (id '.$context->instanceid.')'; } else { // This does not really happen but just in case. $foldername .= ' (ctx '.$context->id.')'; } $foldername = clean_param($foldername, PARAM_PATH); $allfolders[] = $foldername; } $allfolders[] = clean_param($component, PARAM_PATH); $allfolders[] = clean_param($filearea, PARAM_PATH); $allfolders[] = clean_param($itemid, PARAM_PATH); // Variable $allfolders is the full path we want to put the file in - so walk it and create each folder. foreach ($allfolders as $foldername) { // Make sure a folder exists here. $fullpath .= '/' . $foldername; $folderid = $cache->get($fullpath); if (empty($folderid)) { $folderid = $this->folder_exists_in_folder($systemservice, $foldername, $parentid); } if ($folderid !== false) { $cache->set($fullpath, $folderid); $parentid = $folderid; } else { // Create it. $parentid = $this->create_folder_in_folder($systemservice, $foldername, $parentid); $cache->set($fullpath, $parentid); } } $originalfile = $this->get_file_summary($userservice, $source->id); // Use the user service to download the file. $downloadedfile = $this->download_file( $userservice, $source->id, $originalfile->name, ); // Upload the user file to the system drive. $uploaded = $this->upload_file( $systemservice, $downloadedfile['path'], $downloadedfile['newfilename'], $source->exportformat, $parentid, ); // Add the original file owner as a writer to the file. $this->add_writer_to_file($systemservice, $uploaded->id, $originalfile->owners[0]->emailAddress); $newsource = $this->get_file_summary($systemservice, $uploaded->id); // Move the copied file to the correct folder. $this->move_file_from_root_to_folder($systemservice, $newsource->id, $parentid); // Set the sharing options. $this->set_file_sharing_anyone_with_link_can_read($systemservice, $newsource->id); $this->prevent_writers_from_sharing_file($systemservice, $newsource->id); // Update the returned reference so that the stored_file in moodle points to the newly copied file. $source->id = $newsource->id; $source->link = isset($newsource->webViewLink) ? $newsource->webViewLink : ''; $source->usesystem = true; if (empty($source->link)) { $source->link = isset($newsource->webContentLink) ? $newsource->webContentLink : ''; } $reference = json_encode($source); return $reference; } /** * Uploads a file to Google Docs using the provided REST client. * * @param \repository_googledocs\rest $client The REST client for Google Docs API communication. * @param string $filepath The local path to the file to be uploaded. * @param string $filename The name to assign to the uploaded file. * @param string $exportformat The export format for the file (e.g., 'pdf', 'docx'). * @param string $parentid The ID of the parent folder in Google Drive where the file will be uploaded. * @return stdClass|coding_exception Returns the response from the Google Docs API after uploading the file. */ public function upload_file( \repository_googledocs\rest $client, string $filepath, string $filename, string $exportformat, string $parentid ): stdClass { $fileinfo = [ 'name' => $filename, 'mimeType' => $exportformat, 'parents' => [$parentid], // We will move it later. ]; $params = [ 'supportsAllDrives' => 'true', // Support shared drives. 'uploadType' => 'resumable', // Use resumable upload. ]; $headers = $client->call('upload', $params, json_encode($fileinfo)); $uploadurl = ''; // Google returns a location header with the location for the upload. foreach ($headers as $header) { if (stripos($header, 'Location:') === 0) { $uploadurl = trim(substr($header, strpos($header, ':') + 1)); } } $params = ['uploadurl' => $uploadurl]; return $client->call('upload_content', $params, file_get_contents($filepath), mime_content_type($filepath)); } /** * Downloads a file from Google Docs using the provided user service. * * @param \repository_googledocs\rest $userservice The user service instance for Google Docs REST API. * @param string $fileid The ID of the file to download. * @param string $originalfilename The file original name * @return array|repository_exception The downloaded file content or relevant response. */ protected function download_file( \repository_googledocs\rest $userservice, string $fileid, string $originalfilename ): array|repository_exception { global $CFG; $client = $this->get_user_oauth_client(); $base = 'https://www.googleapis.com/drive/v3'; $params = ['alt' => 'media']; $sourceurl = new moodle_url($base . '/files/' . $fileid, $params); $path = $this->prepare_file($originalfilename); $options = ['filepath' => $path, 'timeout' => $CFG->repositorygetfiletimeout, 'followlocation' => true, 'maxredirs' => 5]; $success = $client->download_one($sourceurl->out(false), null, $options); if ($success) { @chmod($path, $CFG->filepermissions); return [ 'path' => $path, 'newfilename' => $originalfilename, ]; } throw new repository_exception('cannotdownload', 'repository'); } /** * Get human readable file info from a the reference. * * @param string $reference * @param int $filestatus */ public function get_reference_details($reference, $filestatus = 0) { if ($this->disabled) { throw new repository_exception('cannotdownload', 'repository'); } if (empty($reference)) { return get_string('unknownsource', 'repository'); } $source = json_decode($reference); if (empty($source->usesystem)) { return ''; } $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); if ($systemauth === false) { return ''; } $systemservice = new repository_googledocs\rest($systemauth); $info = $this->get_file_summary($systemservice, $source->id); $owner = ''; if (!empty($info->owners[0]->displayName)) { $owner = $info->owners[0]->displayName; } if ($owner) { return get_string('owner', 'repository_googledocs', $owner); } else { return $info->name; } } /** * Edit/Create Admin Settings Moodle form. * * @param moodleform $mform Moodle form (passed by reference). * @param string $classname repository class name. */ public static function type_config_form($mform, $classname = 'repository') { $url = new moodle_url('/admin/tool/oauth2/issuers.php'); $url = $url->out(); $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_googledocs', $url)); 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', 'issuerid', get_string('issuer', 'repository_googledocs'), $options); $mform->addHelpButton('issuerid', 'issuer', 'repository_googledocs'); $mform->addRule('issuerid', $strrequired, 'required', null, 'client'); $mform->addElement('static', null, '', get_string('fileoptions', 'repository_googledocs')); $choices = [ 'internal' => get_string('internal', 'repository_googledocs'), 'external' => get_string('external', 'repository_googledocs'), 'both' => get_string('both', 'repository_googledocs') ]; $mform->addElement('select', 'supportedreturntypes', get_string('supportedreturntypes', 'repository_googledocs'), $choices); $choices = [ FILE_INTERNAL => get_string('internal', 'repository_googledocs'), FILE_CONTROLLED_LINK => get_string('external', 'repository_googledocs'), ]; $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_googledocs'), $choices); $mform->addElement('static', null, '', get_string('importformat', 'repository_googledocs')); // Documents. $docsformat = array(); $docsformat['html'] = 'html'; $docsformat['docx'] = 'docx'; $docsformat['odt'] = 'odt'; $docsformat['pdf'] = 'pdf'; $docsformat['rtf'] = 'rtf'; $docsformat['txt'] = 'txt'; core_collator::ksort($docsformat, core_collator::SORT_NATURAL); $mform->addElement('select', 'documentformat', get_string('docsformat', 'repository_googledocs'), $docsformat); $mform->setDefault('documentformat', $docsformat['rtf']); $mform->setType('documentformat', PARAM_ALPHANUM); // Drawing. $drawingformat = array(); $drawingformat['jpeg'] = 'jpeg'; $drawingformat['png'] = 'png'; $drawingformat['svg'] = 'svg'; $drawingformat['pdf'] = 'pdf'; core_collator::ksort($drawingformat, core_collator::SORT_NATURAL); $mform->addElement('select', 'drawingformat', get_string('drawingformat', 'repository_googledocs'), $drawingformat); $mform->setDefault('drawingformat', $drawingformat['pdf']); $mform->setType('drawingformat', PARAM_ALPHANUM); // Presentation. $presentationformat = array(); $presentationformat['pdf'] = 'pdf'; $presentationformat['pptx'] = 'pptx'; $presentationformat['txt'] = 'txt'; core_collator::ksort($presentationformat, core_collator::SORT_NATURAL); $str = get_string('presentationformat', 'repository_googledocs'); $mform->addElement('select', 'presentationformat', $str, $presentationformat); $mform->setDefault('presentationformat', $presentationformat['pptx']); $mform->setType('presentationformat', PARAM_ALPHANUM); // Spreadsheet. $spreadsheetformat = array(); $spreadsheetformat['csv'] = 'csv'; $spreadsheetformat['ods'] = 'ods'; $spreadsheetformat['pdf'] = 'pdf'; $spreadsheetformat['xlsx'] = 'xlsx'; core_collator::ksort($spreadsheetformat, core_collator::SORT_NATURAL); $str = get_string('spreadsheetformat', 'repository_googledocs'); $mform->addElement('select', 'spreadsheetformat', $str, $spreadsheetformat); $mform->setDefault('spreadsheetformat', $spreadsheetformat['xlsx']); $mform->setType('spreadsheetformat', PARAM_ALPHANUM); } } /** * Callback to get the required scopes for system account. * * @param \core\oauth2\issuer $issuer * @return string */ function repository_googledocs_oauth2_system_scopes(\core\oauth2\issuer $issuer) { if ($issuer->get('id') == get_config('googledocs', 'issuerid')) { return Google_Service_Drive::DRIVE_FILE; } return ''; } PK*\ دgoogledocs/version.phpnu[. /** * Version details * * @package repository * @subpackage googledocs * @copyright 2009 Dan Poltawski * @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_googledocs'; // Full name of the plugin (used for diagnostics). PK*\o o 0googledocs/classes/googledocs_content_search.phpnu[. namespace repository_googledocs; /** * Utility class for displaying google drive content that matched a given search criteria. * * This class is responsible for generating the content that is returned based on a given search query. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class googledocs_content_search extends googledocs_content { /** * Returns all relevant contents based on the given path and/or search query. * * The method fetches all content (files) through an API call that matches a given search criteria. * * @param string $query The search query * @return array The array containing the contents */ protected function get_contents(string $query): array { $searchterm = str_replace("'", "\'", $query); // Define the parameters required by the API call. // Query all contents which name contains $searchterm and have not been trashed. $q = "fullText contains '{$searchterm}' AND trashed = false"; // The file fields that should be returned in the response. $fields = "files(id,name,mimeType,webContentLink,webViewLink,fileExtension,modifiedTime,size,iconLink)"; $params = [ 'q' => $q, 'fields' => $fields, 'spaces' => 'drive', 'supportsAllDrives' => 'true', 'includeItemsFromAllDrives' => 'true', 'corpora' => 'allDrives', ]; // Request the content through the API call. $response = helper::request($this->service, 'list', $params); return $response->files ?? []; } } PK*\Q6:sUU'googledocs/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for repository_googledocs. * * @package repository_googledocs * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_googledocs\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_googledocs 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( 'drive.google.com', [ 'email' => 'privacy:metadata:repository_googledocs:email', 'searchtext' => 'privacy:metadata:repository_googledocs:searchtext' ], 'privacy:metadata:repository_googledocs' ); 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) { } } PK*\,LUB&&&googledocs/classes/local/node/node.phpnu[. namespace repository_googledocs\local\node; /** * The googledocs repository content node interface. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface node { /** * Create a repository node array. * * This method returns an array which structure is compatible to represent a content node (file/folder) * in the repository. * * @return array|null The node array or null if the node could not be created */ public function create_node_array(): ?array; } PK*\ٲi!!+googledocs/classes/local/node/file_node.phpnu[. namespace repository_googledocs\local\node; /** * Class used to represent a file node in the googledocs repository. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class file_node implements node { /** @var string The ID of the file node. */ private $id; /** @var string|null The title of the file node. */ private $title; /** @var string The name of the file. */ private $name; /** @var string|null The file's export format. */ private $exportformat; /** @var string The external link to the file. */ private $link; /** @var string The timestamp representing the last modified date. */ private $modified; /** @var string|null The size of the file. */ private $size; /** @var string The thumbnail of the file. */ private $thumbnail; /** @var string|null The type of the Google Doc file (document, presentation, etc.), null if it is a regular file. */ private $googledoctype; /** * Constructor. * * @param \stdClass $gdfile The Google Drive file object */ public function __construct(\stdClass $gdfile) { $this->id = $gdfile->id; $this->title = $this->generate_file_title($gdfile); $this->name = $gdfile->name; $this->exportformat = $this->generate_file_export_format($gdfile); $this->link = $this->generate_file_link($gdfile); $this->modified = ($gdfile->modifiedTime) ? strtotime($gdfile->modifiedTime) : ''; $this->size = !empty($gdfile->size) ? $gdfile->size : null; // Use iconLink as a file thumbnail if set, otherwise use the default icon depending on the file type. // Note: The Google Drive API can return a link to a preview thumbnail of the file (via thumbnailLink). // However, in many cases the Google Drive files are not public and an authorized request is required // to get the thumbnail which we currently do not support. Therefore, to avoid displaying broken // thumbnail images in the repository, the icon of the Google Drive file is being used as a thumbnail // instead as it does not require an authorized request. // Currently, the returned file icon link points to the 16px version of the icon by default which would result // in displaying 16px file thumbnails in the repository. To avoid this, the link can be slightly modified in // order to get a larger version of the icon as there isn't an option to request this through the API call. $this->thumbnail = !empty($gdfile->iconLink) ? str_replace('/16/', '/64/', $gdfile->iconLink) : ''; $this->googledoctype = !isset($gdfile->fileExtension) ? str_replace('application/vnd.google-apps.', '', $gdfile->mimeType) : null; } /** * Create a repository file array. * * This method returns an array which structure is compatible to represent a file node in the repository. * * @return array|null The node array or null if the node could not be created */ public function create_node_array(): ?array { // Cannot create the file node if the file title was not generated or the export format. // This means that the current file type is invalid or unknown. if (!$this->title || !$this->exportformat) { return null; } return [ 'id' => $this->id, 'title' => $this->title, 'source' => json_encode( [ 'id' => $this->id, 'name' => $this->name, 'link' => $this->link, 'exportformat' => $this->exportformat, 'googledoctype' => $this->googledoctype, ] ), 'date' => $this->modified, 'size' => $this->size, 'thumbnail' => $this->thumbnail, 'thumbnail_height' => 64, 'thumbnail_width' => 64, ]; } /** * Generates and returns the title for the file node depending on the type of the Google drive file. * * @param \stdClass $gdfile The Google Drive file object * @return string The file title */ private function generate_file_title(\stdClass $gdfile): ?string { // Determine the file type through the file extension. if (isset($gdfile->fileExtension)) { // The file is a regular file. return $gdfile->name; } else { // The file is probably a Google Doc file. // We need to generate the name by appending the proper google doc extension. $type = str_replace('application/vnd.google-apps.', '', $gdfile->mimeType); if ($type === 'document') { return "{$gdfile->name}.gdoc"; } if ($type === 'presentation') { return "{$gdfile->name}.gslides"; } if ($type === 'spreadsheet') { return "{$gdfile->name}.gsheet"; } if ($type === 'drawing') { $config = get_config('googledocs'); $ext = $config->drawingformat; return "{$gdfile->name}.{$ext}"; } } return null; } /** * Generates and returns the file export format depending on the type of the Google drive file. * * @param \stdClass $gdfile The Google Drive file object * @return string The file export format */ private function generate_file_export_format(\stdClass $gdfile): ?string { // Determine the file type through the file extension. if (isset($gdfile->fileExtension)) { // The file is a regular file. // The file has an extension, therefore we can download it. return 'download'; } else { // The file is probably a Google Doc file, we get the corresponding export link. $type = str_replace('application/vnd.google-apps.', '', $gdfile->mimeType); $types = get_mimetypes_array(); $config = get_config('googledocs'); if ($type === 'document' && !empty($config->documentformat)) { $ext = $config->documentformat; if ($ext === 'rtf') { // Moodle user 'text/rtf' as the MIME type for RTF files. // Google uses 'application/rtf' for the same type of file. // See https://developers.google.com/drive/v3/web/manage-downloads. return 'application/rtf'; } else { return $types[$ext]['type']; } } if ($type === 'presentation' && !empty($config->presentationformat)) { $ext = $config->presentationformat; return $types[$ext]['type']; } if ($type === 'spreadsheet' && !empty($config->spreadsheetformat)) { $ext = $config->spreadsheetformat; return $types[$ext]['type']; } if ($type === 'drawing' && !empty($config->drawingformat)) { $ext = $config->drawingformat; return $types[$ext]['type']; } } return null; } /** * Generates and returns the external link to the file. * * @param \stdClass $gdfile The Google Drive file object * @return string The link to the file */ private function generate_file_link(\stdClass $gdfile): string { // If the google drive file has webViewLink set, use it as an external link. $link = !empty($gdfile->webViewLink) ? $gdfile->webViewLink : ''; // Otherwise, use webContentLink if set or leave the external link empty. if (empty($link) && !empty($gdfile->webContentLink)) { $link = $gdfile->webContentLink; } return $link; } } PK*\G -googledocs/classes/local/node/folder_node.phpnu[. namespace repository_googledocs\local\node; use repository_googledocs\helper; /** * Class used to represent a folder node in the googledocs repository. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class folder_node implements node { /** @var string The ID of the folder node. */ private $id; /** @var string The title of the folder node. */ private $title; /** @var bool The timestamp representing the last modified date. */ private $modified; /** @var string The path of the folder node. */ private $path; /** * Constructor. * * @param \stdClass $gdfolder The Google Drive folder object * @param string $path The path of the folder node */ public function __construct(\stdClass $gdfolder, string $path) { $this->id = $gdfolder->id; $this->title = $gdfolder->name; $this->modified = $gdfolder->modifiedTime ? strtotime($gdfolder->modifiedTime) : ''; $this->path = $path; } /** * Create a repository folder array. * * This method returns an array which structure is compatible to represent a folder node in the repository. * * @return array|null The node array or null if the node could not be created */ public function create_node_array(): ?array { global $OUTPUT; return [ 'id' => $this->id, 'title' => $this->title, 'path' => helper::build_node_path($this->id, $this->title, $this->path), 'date' => $this->modified, 'thumbnail' => $OUTPUT->image_url(file_folder_icon())->out(false), 'thumbnail_height' => 64, 'thumbnail_width' => 64, 'children' => [], ]; } } PK*\!st t =googledocs/classes/local/browser/googledocs_drive_content.phpnu[. namespace repository_googledocs\local\browser; use repository_googledocs\googledocs_content; use repository_googledocs\helper; /** * Utility class for browsing content from or within a specified google drive. * * This class is responsible for generating the content that would be displayed for a specified drive such as * 'my drive' or any existing shared drive. It also supports generating data for paths which are located * within a given drive. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class googledocs_drive_content extends googledocs_content { /** * Returns all relevant contents based on the given path or search query. * * The method fetches all existing content (files and folders) located in a specific folder under a given drive * through an API call. * * @param string $query The search query * @return array The array containing the contents */ protected function get_contents(string $query): array { $id = str_replace("'", "\'", $query); // Define the parameters required by the API call. // Query all files and folders which have not been trashed and located directly in the folder whose ID is $id. $q = "'{$id}' in parents AND trashed = false"; // The file fields that should be returned in the response. $fields = 'files(id,name,mimeType,webContentLink,webViewLink,fileExtension,modifiedTime,size,iconLink)'; $params = [ 'q' => $q, 'fields' => $fields, 'spaces' => 'drive', 'supportsAllDrives' => 'true', 'includeItemsFromAllDrives' => 'true', 'corpora' => 'allDrives', ]; // Request the content through the API call. $response = helper::request($this->service, 'list', $params); return $response->files ?? []; } } PK*\RMO!!<googledocs/classes/local/browser/googledocs_root_content.phpnu[. namespace repository_googledocs\local\browser; use repository_googledocs\googledocs_content; /** * Utility class for browsing the content within the googledocs repository root. * * This class is responsible for generating the content that would be displayed in the googledocs repository root. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class googledocs_root_content extends googledocs_content { /** * Returns all relevant contents based on the given path or search query. * * The method predefines the content which will be displayed in the repository root level. Currently, * only the folders representing 'My drives' and 'Shared drives' will be displayed in the root level. * * @param string $query The search query * @return array The array containing the contents */ protected function get_contents(string $query): array { // Add 'My drive' folder into the displayed contents. return [ (object)[ 'id' => \repository_googledocs::MY_DRIVE_ROOT_ID, 'name' => get_string('mydrive', 'repository_googledocs'), 'mimeType' => 'application/vnd.google-apps.folder', 'modifiedTime' => '', ], ]; } } PK*\_q q Egoogledocs/classes/local/browser/googledocs_shared_drives_content.phpnu[. namespace repository_googledocs\local\browser; use repository_googledocs\googledocs_content; use repository_googledocs\helper; /** * Utility class for browsing the content within the googledocs repository shared drives root. * * This class is responsible for generating the content that would be displayed in the googledocs repository * shared drives root. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class googledocs_shared_drives_content extends googledocs_content { /** * Returns all relevant contents based on the given path or search query. * * The method generates the content which will be displayed in the repository shared drives root level. * All existing shared drives will be fetched through an API call and presented as folders. * * @param string $query The search query * @return array The array containing the contents */ protected function get_contents(string $query): array { // Make an API request to get all existing shared drives. $response = helper::request($this->service, 'shared_drives_list', []); // If shared drives exist, create folder for each shared drive. if ($shareddrives = $response->drives) { return array_map(function($shareddrive) { return (object)[ 'id' => $shareddrive->id, 'name' => $shareddrive->name, 'mimeType' => 'application/vnd.google-apps.folder', 'modifiedTime' => '', ]; }, $shareddrives); } return []; } } PK*\=6..googledocs/classes/helper.phpnu[. namespace repository_googledocs; use repository_googledocs\local\browser\googledocs_root_content; use repository_googledocs\local\browser\googledocs_shared_drives_content; use repository_googledocs\local\browser\googledocs_drive_content; use repository_googledocs\local\node\node; use repository_googledocs\local\node\file_node; use repository_googledocs\local\node\folder_node; /** * Helper class for the googledocs repository. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { /** * Generates a safe path to a node. * * Typically, a node will be id|name of the node. * * @param string $id The ID of the node * @param string $name The name of the node, will be URL encoded * @param string $root The path to append the node on (must be a result of this function) * @return string The path to the node */ public static function build_node_path(string $id, string $name = '', string $root = ''): string { $path = $id; if (!empty($name)) { $path .= '|' . urlencode($name); } if (!empty($root)) { $path = trim($root, '/') . '/' . $path; } return $path; } /** * Returns information about a node in a path. * * @param string $node The node string to extract information from * @return array The array containing the information about the node * @see self::build_node_path() */ public static function explode_node_path(string $node): array { if (strpos($node, '|') !== false) { list($id, $name) = explode('|', $node, 2); $name = urldecode($name); } else { $id = $node; $name = ''; } $id = urldecode($id); return [ 0 => $id, 1 => $name, 'id' => $id, 'name' => $name, ]; } /** * Returns the relevant googledocs content browser class based on the given path. * * @param rest $service The rest API object * @param string $path The current path * @return googledocs_content The googledocs repository content browser */ public static function get_browser(rest $service, string $path): googledocs_content { $pathnodes = explode('/', $path); $currentnode = self::explode_node_path(array_pop($pathnodes)); // Return the relevant content browser class based on the ID of the current path node. switch ($currentnode['id']) { case \repository_googledocs::REPOSITORY_ROOT_ID: return new googledocs_root_content($service, $path, false); case \repository_googledocs::SHARED_DRIVES_ROOT_ID: return new googledocs_shared_drives_content($service, $path); default: return new googledocs_drive_content($service, $path); } } /** * Returns the relevant repository content node class based on the Google Drive file's mimetype. * * @param \stdClass $gdcontent The Google Drive content (file/folder) object * @param string $path The current path * @return node The content node object */ public static function get_node(\stdClass $gdcontent, string $path): node { // Return the relevant content browser class based on the ID of the current path node. switch ($gdcontent->mimeType) { case 'application/vnd.google-apps.folder': return new folder_node($gdcontent, $path); default: return new file_node($gdcontent); } } /** * Wrapper function to perform an API call and also catch and handle potential exceptions. * * @param rest $service The rest API object * @param string $api The name of the API call * @param array $params The parameters required by the API call * @return \stdClass The response object * @throws \repository_exception */ public static function request(rest $service, string $api, array $params): ?\stdClass { try { // Retrieving files and folders. $response = $service->call($api, $params); } catch (\Exception $e) { if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) { // This is raised when the service Drive API has not been enabled on Google APIs control panel. throw new \repository_exception('servicenotenabled', 'repository_googledocs'); } throw $e; } return $response; } } PK*\S?)googledocs/classes/googledocs_content.phpnu[. namespace repository_googledocs; /** * Base class for presenting the googledocs repository contents. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class googledocs_content { /** @var rest The rest API object. */ protected $service; /** @var string The current path. */ protected $path; /** @var bool Whether sorting should be applied to the fetched content. */ protected $sortcontent; /** * Constructor. * * @param rest $service The rest API object * @param string $path The current path * @param bool $sortcontent Whether sorting should be applied to the content */ public function __construct(rest $service, string $path, bool $sortcontent = true) { $this->service = $service; $this->path = $path; $this->sortcontent = $sortcontent; } /** * Generate and return an array containing all repository node (files and folders) arrays for the existing content * based on the path or search query. * * @param string $query The search query * @param callable $isaccepted The callback function which determines whether a given file should be displayed * or filtered based on the existing file restrictions * @return array The array containing the repository content node arrays */ public function get_content_nodes(string $query, callable $isaccepted): array { $files = []; $folders = []; foreach ($this->get_contents($query) as $gdcontent) { $node = helper::get_node($gdcontent, $this->path); // Create the repository node array. $nodearray = $node->create_node_array(); // If the repository node array was successfully generated and the type of the content is accepted, // add it to the repository content nodes array. if ($nodearray && $isaccepted($nodearray)) { // Group the content nodes by type (files and folders). Generate unique array keys for each content node // which will be later used by the sorting function. Note: Using the item id along with the name as key // of the array because Google Drive allows files and folders with identical names. if (isset($nodearray['source'])) { // If the content node has a source attribute, it is a file node. $files["{$nodearray['title']}{$nodearray['id']}"] = $nodearray; } else { $folders["{$nodearray['title']}{$nodearray['id']}"] = $nodearray; } } } // If sorting is required, order the results alphabetically by their array keys. if ($this->sortcontent) { \core_collator::ksort($files, \core_collator::SORT_STRING); \core_collator::ksort($folders, \core_collator::SORT_STRING); } return array_merge(array_values($folders), array_values($files)); } /** * Build the navigation (breadcrumb) from a given path. * * @return array Array containing name and path of each navigation node */ public function get_navigation(): array { $nav = []; $navtrail = ''; $pathnodes = explode('/', $this->path); foreach ($pathnodes as $node) { list($id, $name) = helper::explode_node_path($node); $name = empty($name) ? $id : $name; $nav[] = [ 'name' => $name, 'path' => helper::build_node_path($id, $name, $navtrail), ]; $tmp = end($nav); $navtrail = $tmp['path']; } return $nav; } /** * Returns all relevant contents (files and folders) based on the given path or search query. * * @param string $query The search query * @return array The array containing the contents */ abstract protected function get_contents(string $query): array; } PK*\~M33googledocs/classes/rest.phpnu[. /** * Google Drive Rest API. * * @package repository_googledocs * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_googledocs; defined('MOODLE_INTERNAL') || die(); /** * Google Drive Rest API. * * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class rest extends \core\oauth2\rest { /** * Define the functions of the rest API. * * @return array Example: * [ 'listFiles' => [ 'method' => 'get', 'endpoint' => 'http://...', 'args' => [ 'folder' => PARAM_STRING ] ] ] */ public function get_api_functions() { return [ 'list' => [ 'endpoint' => 'https://www.googleapis.com/drive/v3/files', 'method' => 'get', 'args' => [ 'corpus' => PARAM_RAW, 'orderBy' => PARAM_RAW, 'fields' => PARAM_RAW, 'pageSize' => PARAM_INT, 'pageToken' => PARAM_RAW, 'q' => PARAM_RAW, 'spaces' => PARAM_RAW, 'supportsAllDrives' => PARAM_RAW, 'includeItemsFromAllDrives' => PARAM_RAW, 'corpora' => PARAM_RAW, ], 'response' => 'json', ], 'get' => [ 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}', 'method' => 'get', 'args' => [ 'fields' => PARAM_RAW, 'fileid' => PARAM_RAW, 'supportsAllDrives' => PARAM_RAW, ], 'response' => 'json', ], 'delete' => [ 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}', 'method' => 'delete', 'args' => [ 'fileid' => PARAM_RAW, 'supportsAllDrives' => PARAM_RAW, ], 'response' => 'json', ], 'create' => [ 'endpoint' => 'https://www.googleapis.com/drive/v3/files', 'method' => 'post', 'args' => [ 'fields' => PARAM_RAW, 'supportsAllDrives' => PARAM_RAW, ], 'response' => 'json', ], 'update' => [ 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}', 'method' => 'patch', 'args' => [ 'fileid' => PARAM_RAW, 'fields' => PARAM_RAW, 'addParents' => PARAM_RAW, 'removeParents' => PARAM_RAW, 'supportsAllDrives' => PARAM_RAW, ], 'response' => 'json', ], 'create_permission' => [ 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}/permissions', 'method' => 'post', 'args' => [ 'fileid' => PARAM_RAW, 'emailMessage' => PARAM_RAW, 'sendNotificationEmail' => PARAM_RAW, 'transferOwnership' => PARAM_RAW, 'supportsAllDrives' => PARAM_RAW, ], 'response' => 'json', ], 'update_permission' => [ 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}/permissions/{permissionid}', 'method' => 'patch', 'args' => [ 'fileid' => PARAM_RAW, 'permissionid' => PARAM_RAW, 'emailMessage' => PARAM_RAW, 'sendNotificationEmail' => PARAM_RAW, 'transferOwnership' => PARAM_RAW, 'supportsAllDrives' => PARAM_RAW, ], 'response' => 'json', ], 'upload' => [ 'endpoint' => 'https://www.googleapis.com/upload/drive/v3/files', 'method' => 'post', 'args' => [ 'uploadType' => PARAM_RAW, 'fields' => PARAM_RAW, 'supportsAllDrives' => PARAM_RAW, ], 'response' => 'headers', ], 'upload_content' => [ 'endpoint' => '{uploadurl}', 'method' => 'put', 'args' => [ 'uploadurl' => PARAM_URL, ], 'response' => 'json', ], ]; } } PK*\W_-googledocs/templates/upload_dialogue.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template repository_googledocs/upload_dialogue Google repository upload dialogue. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }}
{{#str}} attachedfiles, repository {{/str}}
    PK*\%]googledocs/amd/src/upload.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Upload module for Google Docs repository. * * @module repository_googledocs/upload * @copyright 2025 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {subscribe} from 'core/pubsub'; import SaveCancelModal from 'core/modal_save_cancel'; import {getString} from 'core/str'; import Templates from 'core/templates'; import ModalEvents from 'core/modal_events'; import Dropzone from 'core/dropzone'; import * as config from 'core/config'; import Notification from 'core/notification'; let listenersRegistered = false; let droppedFiles = []; /** * Open the upload modal. * * @param {object} data Data passed from the event */ const openUploadModal = (data) => { Templates.render('repository_googledocs/upload_dialogue', {}).then(function(bodyHtml) { return SaveCancelModal.create({ title: getString('upload'), body: bodyHtml, large: true, }).then(function(modal) { modal.getRoot().on(ModalEvents.shown, () => { droppedFiles = []; initDropzone(modal); }); modal.getRoot().on(ModalEvents.hidden, () => { modal.destroy(); }); modal.getRoot().on(ModalEvents.save, (e) => { e.preventDefault(); commitFiles(data.repoId, data.contextId, data.callback, droppedFiles, modal); }); modal.getRoot().on(ModalEvents.cancel, () => { modal.hide(); }); modal.show(); return modal; }); }); }; /** * Initialize the dropzone inside the modal. * * @param {Modal} modal Modal instance */ const initDropzone = (modal) => { const $body = modal.getBody(); const dropzoneContainer = $body.find('.repository_googledocs_dropzone_container').get(0); const fileListContainer = $body.find('ul.repository_googledocs_files_list').get(0); const dz = new Dropzone(dropzoneContainer, '*', (files) => { droppedFiles.push(...files); let fileListHTML = ''; for (let i = 0; i < droppedFiles.length; i++) { fileListHTML += '
  • ' + droppedFiles[i].name + '
  • '; } fileListContainer.innerHTML = fileListHTML; }); getString('dropfiles', 'repository').then((label) => { dz.setLabel(label); }); dz.init(); }; /** * Upload files to server. * * @param {Integer} repoId Repository ID * @param {Integer} contextId Context ID * @param {function} callback Callback function * @param {array} files Files to be uploaded * @param {Modal} modal Modal instance */ const commitFiles = (repoId, contextId, callback, files, modal) => { const saveButton = modal.getFooter().find('[data-action="save"]'); const formData = new FormData(); formData.append('action', 'upload'); formData.append('repo_id', repoId); formData.append('contextid', contextId); formData.append('sesskey', config.sesskey); for (let i = 0; i < files.length; i++) { formData.append("files[]", files[i]); } const xhr = new XMLHttpRequest(); xhr.open('POST', config.wwwroot + '/repository/googledocs/repository_ajax.php', false); xhr.onload = function() { const response = JSON.parse(xhr.responseText); if (response.error) { saveButton.removeAttr('disabled'); Notification.alert( getString('uploaderror', 'repository'), response.error, getString('close', 'repository'), ); } else { saveButton.removeAttr('disabled'); modal.hide(); callback(); } }; xhr.send(formData); }; /** * Register events. */ const registerEventListeners = () => { if (!listenersRegistered) { subscribe('repository_googledocs_upload', (data) => { openUploadModal(data); }); listenersRegistered = true; } }; /** * Initializes the upload module. */ export const init = () => { registerEventListeners(); }; PK*\"P11"googledocs/amd/build/upload.min.jsnu[define("repository_googledocs/upload",["exports","core/pubsub","core/modal_save_cancel","core/str","core/templates","core/modal_events","core/dropzone","core/config","core/notification"],(function(_exports,_pubsub,_modal_save_cancel,_str,_templates,_modal_events,_dropzone,config,_notification){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Upload module for Google Docs repository. * * @module repository_googledocs/upload * @copyright 2025 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_templates=_interopRequireDefault(_templates),_modal_events=_interopRequireDefault(_modal_events),_dropzone=_interopRequireDefault(_dropzone),config=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(config),_notification=_interopRequireDefault(_notification);let listenersRegistered=!1,droppedFiles=[];const initDropzone=modal=>{const $body=modal.getBody(),dropzoneContainer=$body.find(".repository_googledocs_dropzone_container").get(0),fileListContainer=$body.find("ul.repository_googledocs_files_list").get(0),dz=new _dropzone.default(dropzoneContainer,"*",(files=>{droppedFiles.push(...files);let fileListHTML="";for(let i=0;i"+droppedFiles[i].name+"";fileListContainer.innerHTML=fileListHTML}));(0,_str.getString)("dropfiles","repository").then((label=>{dz.setLabel(label)})),dz.init()},commitFiles=(repoId,contextId,callback,files,modal)=>{const saveButton=modal.getFooter().find('[data-action="save"]'),formData=new FormData;formData.append("action","upload"),formData.append("repo_id",repoId),formData.append("contextid",contextId),formData.append("sesskey",config.sesskey);for(let i=0;i{listenersRegistered||((0,_pubsub.subscribe)("repository_googledocs_upload",(data=>{(data=>{_templates.default.render("repository_googledocs/upload_dialogue",{}).then((function(bodyHtml){return _modal_save_cancel.default.create({title:(0,_str.getString)("upload"),body:bodyHtml,large:!0}).then((function(modal){return modal.getRoot().on(_modal_events.default.shown,(()=>{droppedFiles=[],initDropzone(modal)})),modal.getRoot().on(_modal_events.default.hidden,(()=>{modal.destroy()})),modal.getRoot().on(_modal_events.default.save,(e=>{e.preventDefault(),commitFiles(data.repoId,data.contextId,data.callback,droppedFiles,modal)})),modal.getRoot().on(_modal_events.default.cancel,(()=>{modal.hide()})),modal.show(),modal}))}))})(data)})),listenersRegistered=!0)};_exports.init=()=>{registerEventListeners()}})); //# sourceMappingURL=upload.min.js.mapPK*\1&&&googledocs/amd/build/upload.min.js.mapnu[{"version":3,"file":"upload.min.js","sources":["../src/upload.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Upload module for Google Docs repository.\n *\n * @module repository_googledocs/upload\n * @copyright 2025 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {subscribe} from 'core/pubsub';\nimport SaveCancelModal from 'core/modal_save_cancel';\nimport {getString} from 'core/str';\nimport Templates from 'core/templates';\nimport ModalEvents from 'core/modal_events';\nimport Dropzone from 'core/dropzone';\nimport * as config from 'core/config';\nimport Notification from 'core/notification';\n\nlet listenersRegistered = false;\nlet droppedFiles = [];\n\n/**\n * Open the upload modal.\n *\n * @param {object} data Data passed from the event\n */\nconst openUploadModal = (data) => {\n Templates.render('repository_googledocs/upload_dialogue', {}).then(function(bodyHtml) {\n return SaveCancelModal.create({\n title: getString('upload'),\n body: bodyHtml,\n large: true,\n }).then(function(modal) {\n modal.getRoot().on(ModalEvents.shown, () => {\n droppedFiles = [];\n initDropzone(modal);\n });\n modal.getRoot().on(ModalEvents.hidden, () => {\n modal.destroy();\n });\n modal.getRoot().on(ModalEvents.save, (e) => {\n e.preventDefault();\n commitFiles(data.repoId, data.contextId, data.callback, droppedFiles, modal);\n });\n modal.getRoot().on(ModalEvents.cancel, () => {\n modal.hide();\n });\n modal.show();\n\n return modal;\n });\n });\n};\n\n/**\n * Initialize the dropzone inside the modal.\n *\n * @param {Modal} modal Modal instance\n */\nconst initDropzone = (modal) => {\n const $body = modal.getBody();\n const dropzoneContainer = $body.find('.repository_googledocs_dropzone_container').get(0);\n const fileListContainer = $body.find('ul.repository_googledocs_files_list').get(0);\n const dz = new Dropzone(dropzoneContainer, '*', (files) => {\n droppedFiles.push(...files);\n let fileListHTML = '';\n for (let i = 0; i < droppedFiles.length; i++) {\n fileListHTML += '
  • ' + droppedFiles[i].name + '
  • ';\n }\n fileListContainer.innerHTML = fileListHTML;\n });\n\n getString('dropfiles', 'repository').then((label) => {\n dz.setLabel(label);\n });\n\n dz.init();\n};\n\n/**\n * Upload files to server.\n *\n * @param {Integer} repoId Repository ID\n * @param {Integer} contextId Context ID\n * @param {function} callback Callback function\n * @param {array} files Files to be uploaded\n * @param {Modal} modal Modal instance\n */\nconst commitFiles = (repoId, contextId, callback, files, modal) => {\n const saveButton = modal.getFooter().find('[data-action=\"save\"]');\n const formData = new FormData();\n formData.append('action', 'upload');\n formData.append('repo_id', repoId);\n formData.append('contextid', contextId);\n formData.append('sesskey', config.sesskey);\n for (let i = 0; i < files.length; i++) {\n formData.append(\"files[]\", files[i]);\n }\n const xhr = new XMLHttpRequest();\n xhr.open('POST', config.wwwroot + '/repository/googledocs/repository_ajax.php', false);\n xhr.onload = function() {\n const response = JSON.parse(xhr.responseText);\n if (response.error) {\n saveButton.removeAttr('disabled');\n Notification.alert(\n getString('uploaderror', 'repository'),\n response.error,\n getString('close', 'repository'),\n );\n } else {\n saveButton.removeAttr('disabled');\n modal.hide();\n callback();\n }\n };\n xhr.send(formData);\n};\n\n/**\n * Register events.\n */\nconst registerEventListeners = () => {\n if (!listenersRegistered) {\n subscribe('repository_googledocs_upload', (data) => {\n openUploadModal(data);\n });\n listenersRegistered = true;\n }\n};\n\n/**\n * Initializes the upload module.\n */\nexport const init = () => {\n registerEventListeners();\n};\n"],"names":["listenersRegistered","droppedFiles","initDropzone","modal","$body","getBody","dropzoneContainer","find","get","fileListContainer","dz","Dropzone","files","push","fileListHTML","i","length","name","innerHTML","then","label","setLabel","init","commitFiles","repoId","contextId","callback","saveButton","getFooter","formData","FormData","append","config","sesskey","xhr","XMLHttpRequest","open","wwwroot","onload","response","JSON","parse","responseText","error","removeAttr","alert","hide","send","registerEventListeners","data","render","bodyHtml","SaveCancelModal","create","title","body","large","getRoot","on","ModalEvents","shown","hidden","destroy","save","e","preventDefault","cancel","show","openUploadModal"],"mappings":";;;;;;;k/BAgCIA,qBAAsB,EACtBC,aAAe,SAwCbC,aAAgBC,cACZC,MAAQD,MAAME,UACdC,kBAAoBF,MAAMG,KAAK,6CAA6CC,IAAI,GAChFC,kBAAoBL,MAAMG,KAAK,uCAAuCC,IAAI,GAC1EE,GAAK,IAAIC,kBAASL,kBAAmB,KAAMM,QAC7CX,aAAaY,QAAQD,WACjBE,aAAe,OACd,IAAIC,EAAI,EAAGA,EAAId,aAAae,OAAQD,IACrCD,cAAgB,OAASb,aAAac,GAAGE,KAAO,QAEpDR,kBAAkBS,UAAYJ,mCAGxB,YAAa,cAAcK,MAAMC,QACvCV,GAAGW,SAASD,UAGhBV,GAAGY,QAYDC,YAAc,CAACC,OAAQC,UAAWC,SAAUd,MAAOT,eAC/CwB,WAAaxB,MAAMyB,YAAYrB,KAAK,wBACpCsB,SAAW,IAAIC,SACrBD,SAASE,OAAO,SAAU,UAC1BF,SAASE,OAAO,UAAWP,QAC3BK,SAASE,OAAO,YAAaN,WAC7BI,SAASE,OAAO,UAAWC,OAAOC,aAC7B,IAAIlB,EAAI,EAAGA,EAAIH,MAAMI,OAAQD,IAC9Bc,SAASE,OAAO,UAAWnB,MAAMG,UAE/BmB,IAAM,IAAIC,eAChBD,IAAIE,KAAK,OAAQJ,OAAOK,QAAU,8CAA8C,GAChFH,IAAII,OAAS,iBACHC,SAAWC,KAAKC,MAAMP,IAAIQ,cAC5BH,SAASI,OACThB,WAAWiB,WAAW,kCACTC,OACT,kBAAU,cAAe,cACzBN,SAASI,OACT,kBAAU,QAAS,iBAGvBhB,WAAWiB,WAAW,YACtBzC,MAAM2C,OACNpB,aAGRQ,IAAIa,KAAKlB,WAMPmB,uBAAyB,KACtBhD,4CACS,gCAAiCiD,OAjG1BA,CAAAA,0BACXC,OAAO,wCAAyC,IAAI/B,MAAK,SAASgC,iBACjEC,2BAAgBC,OAAO,CAC1BC,OAAO,kBAAU,UACjBC,KAAMJ,SACNK,OAAO,IACRrC,MAAK,SAAShB,cACbA,MAAMsD,UAAUC,GAAGC,sBAAYC,OAAO,KAClC3D,aAAe,GACfC,aAAaC,UAEjBA,MAAMsD,UAAUC,GAAGC,sBAAYE,QAAQ,KACnC1D,MAAM2D,aAEV3D,MAAMsD,UAAUC,GAAGC,sBAAYI,MAAOC,IAClCA,EAAEC,iBACF1C,YAAY0B,KAAKzB,OAAQyB,KAAKxB,UAAWwB,KAAKvB,SAAUzB,aAAcE,UAE1EA,MAAMsD,UAAUC,GAAGC,sBAAYO,QAAQ,KACnC/D,MAAM2C,UAEV3C,MAAMgE,OAEChE,aA2EPiE,CAAgBnB,SAEpBjD,qBAAsB,kBAOV,KAChBgD"}PK*\~)""3googledocs/tests/googledocs_search_content_test.phpnu[. namespace repository_googledocs; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/repository/googledocs/tests/googledocs_content_testcase.php'); require_once($CFG->dirroot . '/repository/googledocs/lib.php'); /** * Class containing unit tests for the search content class. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class googledocs_search_content_test extends \googledocs_content_testcase { /** * Test get_content_nodes(). * * @covers \repository_googledocs\googledocs_content_search * @dataProvider get_content_nodes_provider * @param string $query The query string * @param bool $sortcontent Whether the contents should be sorted in alphabetical order * @param array $filterextensions The array containing file extensions that should be disallowed (filtered) * @param array $searccontents The array containing the fetched google drive contents that match the search criteria * @param array $expected The expected array which contains the generated repository content nodes */ public function test_get_content_nodes( string $query, bool $sortcontent, array $filterextensions, array $searccontents, array $expected ): void { // Mock the service object. $servicemock = $this->createMock(rest::class); $searchparams = [ 'q' => "fullText contains '" . str_replace("'", "\'", $query) . "' AND trashed = false", 'fields' => 'files(id,name,mimeType,webContentLink,webViewLink,fileExtension,modifiedTime,size,iconLink)', 'spaces' => 'drive', 'supportsAllDrives' => 'true', 'includeItemsFromAllDrives' => 'true', 'corpora' => 'allDrives', ]; // Assert that the call() method is being called to fetch the relevant drive contents (list), // that match the search criteria. Also, define the returned data objects by these calls. // This simulates the more restricted drive.file scope behavior. $callinvocations = $this->exactly(1); $servicemock->expects($callinvocations) ->method('call') ->willReturnCallback(function(string $method, array $params) use ( $searccontents, $searchparams, ) { $this->assertEquals('list', $method); $this->assertEquals($searchparams, $params); return (object) [ 'files' => $searccontents, ]; }); // Construct the node path. $path = \repository_googledocs::REPOSITORY_ROOT_ID . '|' . urlencode('Google Drive') . '/' . \repository_googledocs::SEARCH_ROOT_ID . '|' . urlencode( get_string('searchfor', 'repository_googledocs') . " '{$query}'"); $searchcontentobj = new googledocs_content_search($servicemock, $path, $sortcontent); $this->disallowedextensions = $filterextensions; $contentnodes = $searchcontentobj->get_content_nodes($query, [$this, 'filter']); // Assert that the returned array of repository content nodes is equal to the expected one. $this->assertEquals($expected, $contentnodes); } /** * Data provider for test_get_content_nodes(). * * @return array */ public static function get_content_nodes_provider(): array { $rootid = \repository_googledocs::REPOSITORY_ROOT_ID; $searchnodeid = \repository_googledocs::SEARCH_ROOT_ID; $searchforstring = get_string('searchfor', 'repository_googledocs'); return [ 'Folders and files match the search criteria; ordering applied.' => [ 'test', true, [], [ self::create_google_drive_file_object('d85b21c0f86cb0', 'Test file 3.pdf', 'application/pdf', 'pdf', '1000', '', 'https://drive.google.com/uc?id=d85b21c0f86cb0&export=download'), self::create_google_drive_folder_object('0c4ad262c65333', 'Test folder 1'), self::create_google_drive_file_object('bed5a0f08d412a', 'Test file 1.pdf', 'application/pdf', 'pdf'), self::create_google_drive_folder_object('9c4ad262c65333', 'Test folder 2'), ], [ self::create_folder_content_node_array('0c4ad262c65333', 'Test folder 1', "{$rootid}|Google+Drive/{$searchnodeid}|" . urlencode("{$searchforstring} 'test'")), self::create_folder_content_node_array('9c4ad262c65333', 'Test folder 2', "{$rootid}|Google+Drive/{$searchnodeid}|" . urlencode("{$searchforstring} 'test'")), self::create_file_content_node_array('bed5a0f08d412a', 'Test file 1.pdf', 'Test file 1.pdf', null, '', 'https://googleusercontent.com/type/application/pdf', '', 'download'), self::create_file_content_node_array('d85b21c0f86cb0', 'Test file 3.pdf', 'Test file 3.pdf', '1000', '', 'https://googleusercontent.com/type/application/pdf', 'https://drive.google.com/uc?id=d85b21c0f86cb0&export=download', 'download'), ], ], 'Only folders match the search criteria; ordering not applied.' => [ 'testing', false, [], [ self::create_google_drive_folder_object('0c4ad262c65333', 'Testing folder 3'), self::create_google_drive_folder_object('d85b21c0f86cb0', 'Testing folder 1'), self::create_google_drive_folder_object('bed5a0f08d412a', 'Testing folder 2'), ], [ self::create_folder_content_node_array('0c4ad262c65333', 'Testing folder 3', "{$rootid}|Google+Drive/{$searchnodeid}|" . urlencode("{$searchforstring} 'testing'")), self::create_folder_content_node_array('d85b21c0f86cb0', 'Testing folder 1', "{$rootid}|Google+Drive/{$searchnodeid}|" . urlencode("{$searchforstring} 'testing'")), self::create_folder_content_node_array('bed5a0f08d412a', 'Testing folder 2', "{$rootid}|Google+Drive/{$searchnodeid}|" . urlencode("{$searchforstring} 'testing'")), ], ], 'Only files match the search criteria; ordering not applied; filter .doc and .txt.' => [ 'root', false, ['doc', 'txt'], [ self::create_google_drive_file_object('d85b21c0f86cb0', 'Testing file 3.pdf', 'application/pdf', 'pdf', '1000'), self::create_google_drive_file_object('a85b21c0f86cb0', 'Testing file 1.txt', 'text/plain', 'txt', '3000'), self::create_google_drive_file_object('f85b21c0f86cb0', 'Testing file 2.doc', 'application/msword', 'doc', '2000'), ], [ self::create_file_content_node_array('d85b21c0f86cb0', 'Testing file 3.pdf', 'Testing file 3.pdf', '1000', '', 'https://googleusercontent.com/type/application/pdf', '', 'download'), ], ], 'No content that matches the search criteria.' => [ 'root', false, [], [], [], ], ]; } } PK*\`(x)) googledocs/tests/helper_test.phpnu[. namespace repository_googledocs; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/repository/googledocs/tests/repository_googledocs_testcase.php'); require_once($CFG->dirroot . '/repository/googledocs/lib.php'); /** * Class containing unit tests for the helper class. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class helper_test extends \repository_googledocs_testcase { /** * Test build_node_path(). * * @dataProvider build_node_path_provider * @param string $id The ID of the node * @param string $name The name of the node * @param string $rootpath The path to append the node on * @param string $expected The expected node path */ public function test_build_node_path(string $id, string $name, string $rootpath, string $expected): void { // Assert that the returned node path is equal to the expected one. $this->assertEquals($expected, helper::build_node_path($id, $name, $rootpath)); } /** * Data provider for test_build_node_path(). * * @return array */ public static function build_node_path_provider(): array { $rootid = \repository_googledocs::REPOSITORY_ROOT_ID; $mydriveid = \repository_googledocs::MY_DRIVE_ROOT_ID; $shareddrivesid = \repository_googledocs::SHARED_DRIVES_ROOT_ID; return [ 'Generate the path for a node without a root path.' => [ $rootid, 'Google Drive', '', "{$rootid}|Google+Drive", ], 'Generate the path for a node without a name and root path.' => [ $rootid, '', '', $rootid, ], 'Generate the path for a node without a name.' => [ $mydriveid, '', "{$rootid}|Google+Drive", "{$rootid}|Google+Drive/{$mydriveid}", ], 'Generate the path for a node which has a name and root path.' => [ '092cdf4732b9d5', 'Shared Drive 5', "{$rootid}|Google+Drive/{$shareddrivesid}|Shared+Drives", "{$rootid}|Google+Drive/{$shareddrivesid}|Shared+Drives/092cdf4732b9d5|Shared+Drive+5", ], ]; } /** * Test explode_node_path(). * * @dataProvider explode_node_path_provider * @param string $node The node string to extract information from * @param array $expected The expected array containing the information about the node */ public function test_explode_node_path(string $node, array $expected): void { // Assert that the returned array is equal to the expected one. $this->assertEquals($expected, helper::explode_node_path($node)); } /** * Data provider for test_explode_node_path(). * * @return array */ public static function explode_node_path_provider(): array { $rootid = \repository_googledocs::REPOSITORY_ROOT_ID; return [ 'Return the information for a path node that has a name.' => [ "{$rootid}|Google+Drive", [ 0 => $rootid, 1 => 'Google Drive', 'id' => $rootid, 'name' => 'Google Drive', ], ], 'Return the information for a path node that does not have a name.' => [ $rootid, [ 0 => $rootid, 1 => '', 'id' => $rootid, 'name' => '', ], ], ]; } /** * Test get_browser(). * * @dataProvider get_browser_provider * @param string $nodepath The node path string * @param string $expected The expected browser class */ public function test_get_browser(string $nodepath, string $expected): void { // The service (rest API) object is required by get_browser(), but not being used to determine which browser // object should be returned. Therefore, we can simply mock this object in this test. $servicemock = $this->createMock(rest::class); $browser = helper::get_browser($servicemock, $nodepath); // Assert that the returned browser class by get_browser() is equal to the expected one. $this->assertEquals($expected, get_class($browser)); } /** * Data provider for test_get_browser(). * * @return array */ public static function get_browser_provider(): array { $rootid = \repository_googledocs::REPOSITORY_ROOT_ID; $mydriveid = \repository_googledocs::MY_DRIVE_ROOT_ID; $shareddrivesid = \repository_googledocs::SHARED_DRIVES_ROOT_ID; return [ 'Repository root level path.' => [ "{$rootid}|Google+Drive", \repository_googledocs\local\browser\googledocs_root_content::class, ], 'My drive path.' => [ "{$rootid}|Google+Drive/{$mydriveid}|My+Drive", \repository_googledocs\local\browser\googledocs_drive_content::class, ], 'Shared drives root path.' => [ "{$rootid}|Google+Drive/{$shareddrivesid}|Shared+Drives", \repository_googledocs\local\browser\googledocs_shared_drives_content::class, ], 'Path within a shared drive.' => [ "{$rootid}|Google+Drive/{$shareddrivesid}|Shared+Drives/092cdf4732b9d5|Shared+Drive+5", \repository_googledocs\local\browser\googledocs_drive_content::class, ], ]; } /** * Test get_node(). * * @dataProvider get_node_provider * @param \stdClass $gdcontent The Google Drive content (file/folder) object * @param string $expected The expected content node class */ public function test_get_node(\stdClass $gdcontent, string $expected): void { // The path is required by get_content_node(), but not being used to determine which content node // object should be returned. Therefore, we can just generate a dummy path. $path = \repository_googledocs::REPOSITORY_ROOT_ID . '|Google+Drive|' . \repository_googledocs::MY_DRIVE_ROOT_ID . '|My+Drive'; $node = helper::get_node($gdcontent, $path); // Assert that the returned content node class by get_node() is equal to the expected one. $this->assertEquals($expected, get_class($node)); } /** * Data provider for test_get_node(). * * @return array */ public static function get_node_provider(): array { return [ 'The content object represents a Google Drive folder.' => [ self::create_google_drive_folder_object('e3b0c44298fc1c149', 'Folder', ''), \repository_googledocs\local\node\folder_node::class, ], 'The content object represents a Google Drive file.' => [ self::create_google_drive_file_object('de04d58dc5ccc', 'File.pdf', 'application/pdf'), \repository_googledocs\local\node\file_node::class, ], ]; } /** * Test request() when an exception is thrown by the API call. * * @dataProvider request_exception_provider * @param \Exception $exception The exception thrown by the API call * @param \Exception $expected The expected exception thrown by request() */ public function test_request_exception(\Exception $exception, \Exception $expected): void { // Mock the service object. $servicemock = $this->createMock(rest::class); // Assert that the call() method is being called only once with the given arguments. // Define the thrown exception by this call. $servicemock->expects($this->once()) ->method('call') ->with('list', []) ->willThrowException($exception); $this->expectExceptionObject($expected); helper::request($servicemock, 'list', []); } /** * Data provider for test_request_exception(). * * @return array */ public static function request_exception_provider(): array { return [ 'The API call throws exception (status: 403; message: Access Not Configured).' => [ new \Exception('Access Not Configured', 403), new \repository_exception('servicenotenabled', 'repository_googledocs'), ], 'The API call throws exception (status: 405; message: Access Not Configured).' => [ new \Exception('Access Not Configured', 405), new \Exception('Access Not Configured', 405), ], 'The API call throws exception (status: 403; message: Access Forbidden).' => [ new \Exception('Access Forbidden', 403), new \Exception('Access Forbidden', 403), ], 'The API call throws exception (status: 404; message: Not Found).' => [ new \Exception('Not Found', 404), new \Exception('Not Found', 404), ], ]; } } PK*\rI "googledocs/tests/generator/lib.phpnu[. /** * Google Docs repository data generator * * @package repository_googledocs * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use \core\oauth2\issuer; use \core\oauth2\endpoint; /** * Google Docs repository data generator class * * @package repository_googledocs * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_googledocs_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); $issuerrecord = (object) [ 'name' => 'Google', 'image' => 'https://accounts.google.com/favicon.ico', 'baseurl' => 'https://accounts.google.com/', 'loginparamsoffline' => 'access_type=offline&prompt=consent', 'showonloginpage' => issuer::EVERYWHERE ]; $issuer = new issuer(0, $issuerrecord); $issuer->create(); $endpointrecord = (object) [ 'issuerid' => $issuer->get('id'), 'name' => 'discovery_endpoint', 'url' => 'https://accounts.google.com/.well-known/openid-configuration' ]; $endpoint = new endpoint(0, $endpointrecord); $endpoint->create(); if (!isset($record['issuerid'])) { $record['issuerid'] = $issuer->get('id'); } if (!isset($record['defaultreturntype'])) { $record['defaultreturntype'] = FILE_INTERNAL; } if (!isset($record['supportedreturntypes'])) { $record['supportedreturntypes'] = 'both'; } if (!isset($record['documentformat'])) { $record['documentformat'] = 'pdf'; } if (!isset($record['presentationformat'])) { $record['presentationformat'] = 'pdf'; } if (!isset($record['drawingformat'])) { $record['drawingformat'] = 'pdf'; } if (!isset($record['spreadsheetformat'])) { $record['spreadsheetformat'] = 'pdf'; } return $record; } } PK*\>~.googledocs/tests/local/node/file_node_test.phpnu[. namespace repository_googledocs\local\node; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/repository/googledocs/tests/repository_googledocs_testcase.php'); /** * Class containing unit tests for the repository file node class. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class file_node_test extends \repository_googledocs_testcase { /** * Test create_node_array(). * * @dataProvider create_node_array_provider * @param \stdClass $gdfile The Google Drive file object * @param array $configsettings The googledoc repository config settings that should be set * @param array|null $expected The expected repository file node array */ public function test_create_node_array(\stdClass $gdfile, array $configsettings, ?array $expected): void { $this->resetAfterTest(); // Set the required config settings. array_walk($configsettings, function($value, $name) { set_config($name, $value, 'googledocs'); }); $filenode = new file_node($gdfile); $filenodearray = $filenode->create_node_array(); // Assert that the returned repository file node array by create_node_array() is equal to the expected one. $this->assertEquals($expected, $filenodearray); } /** * Data provider for test_create_node_array(). * * @return array */ public static function create_node_array_provider(): array { return [ 'Google Drive file with an extension.' => [ self::create_google_drive_file_object('d85b21c0f86cb0', 'File.pdf', 'application/pdf', 'pdf', '1000', '01/01/21 0:30'), [], self::create_file_content_node_array('d85b21c0f86cb0', 'File.pdf', 'File.pdf', '1000', '1609432200', 'https://googleusercontent.com/type/application/pdf', '', 'download'), ], 'Google Drive file that has webContentLink and webViewLink.' => [ self::create_google_drive_file_object('d85b21c0f86cb0', 'File.pdf', 'application/pdf', 'pdf', null, '', 'https://drive.google.com/uc?id=d85b21c0f86cb0&export=download', 'https://drive.google.com/file/d/d85b21c0f86cb0/view?usp=drivesdk'), [ 'documentformat' => 'rtf', ], self::create_file_content_node_array('d85b21c0f86cb0', 'File.pdf', 'File.pdf', null, '', 'https://googleusercontent.com/type/application/pdf', 'https://drive.google.com/file/d/d85b21c0f86cb0/view?usp=drivesdk', 'download'), ], 'Google Drive file that has webContentLink and no webViewLink.' => [ self::create_google_drive_file_object('d85b21c0f86cb0', 'File.pdf', 'application/pdf', 'pdf', null, '', 'https://drive.google.com/uc?id=d85b21c0f86cb0&export=download', ''), [], self::create_file_content_node_array('d85b21c0f86cb0', 'File.pdf', 'File.pdf', null, '', 'https://googleusercontent.com/type/application/pdf', 'https://drive.google.com/uc?id=d85b21c0f86cb0&export=download', 'download'), ], 'Google Drive file without an extension (Google document file; documentformat config set to rtf).' => [ self::create_google_drive_file_object('d85b21c0f86cb0', 'File', 'application/vnd.google-apps.document', null), [ 'documentformat' => 'rtf', ], self::create_file_content_node_array('d85b21c0f86cb0', 'File', 'File.gdoc', '', '', 'https://googleusercontent.com/type/application/vnd.google-apps.document', '', 'application/rtf', 'document'), ], 'Google Drive file without an extension (Google presentation file; presentationformat config not set).' => [ self::create_google_drive_file_object('d85b21c0f86cb0', 'File', 'application/vnd.google-apps.presentation', null), [ 'documentformat' => 'rtf', ], null, ], 'Google Drive file without an extension (File type not supported).' => [ self::create_google_drive_file_object('d85b21c0f86cb0', 'File', 'application/vnd.google-apps.unknownmimetype', null), [], null, ], ]; } } PK*\đ 0googledocs/tests/local/node/folder_node_test.phpnu[. namespace repository_googledocs\local\node; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/repository/googledocs/tests/repository_googledocs_testcase.php'); /** * Class containing unit tests for the repository folder node class. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class folder_node_test extends \repository_googledocs_testcase { /** * Test create_node_array(). * * @dataProvider create_node_array_provider * @param \stdClass $gdfolder The Google Drive folder object * @param string $path The current path * @param array $expected The expected repository folder node array */ public function test_create_node_array(\stdClass $gdfolder, string $path, array $expected): void { $foldernode = new folder_node($gdfolder, $path); $foldernodearray = $foldernode->create_node_array(); // Assert that the returned repository folder node array by create_node_array() is equal to the expected one. $this->assertEquals($expected, $foldernodearray); } /** * Data provider for test_create_node_array(). * * @return array */ public static function create_node_array_provider(): array { $rootid = \repository_googledocs::REPOSITORY_ROOT_ID; return [ 'Google Drive folder with modified date.' => [ self::create_google_drive_folder_object('d85b21c0f86cb0', 'Folder', '01/01/21 0:30'), "{$rootid}|Google+Drive", self::create_folder_content_node_array( 'd85b21c0f86cb0', 'Folder', "{$rootid}|Google+Drive", '1609432200', ), ], 'Google Drive folder without modified date.' => [ self::create_google_drive_folder_object('d85b21c0f86cb0', 'Folder', ''), '', self::create_folder_content_node_array('d85b21c0f86cb0', 'Folder', '', ''), ], ]; } } PK*\@*܆Hgoogledocs/tests/local/browser/googledocs_shared_drives_content_test.phpnu[. namespace repository_googledocs\local\browser; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/repository/googledocs/tests/googledocs_content_testcase.php'); require_once($CFG->dirroot . '/repository/googledocs/lib.php'); /** * Class containing unit tests for the shared drives browser class. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class googledocs_shared_drives_content_test extends \googledocs_content_testcase { /** * Test get_content_nodes(). * * @dataProvider get_content_nodes_provider * @param array $shareddrives The array containing the existing shared drives * @param bool $sortcontent Whether the contents should be sorted in alphabetical order * @param array $expected The expected array which contains the generated repository content nodes */ public function test_get_content_nodes(array $shareddrives, bool $sortcontent, array $expected): void { // Mock the service object. $servicemock = $this->createMock(\repository_googledocs\rest::class); // Assert that the call() method is being called only once with the given arguments to fetch the existing // shared drives. Define the returned data object by this call. $servicemock->expects($this->once()) ->method('call') ->with('shared_drives_list', []) ->willReturn((object)[ 'kind' => 'drive#driveList', 'nextPageToken' => 'd838181f30b0f5', 'drives' => $shareddrives, ]); $path = \repository_googledocs::REPOSITORY_ROOT_ID . '|Google+Drive/' . \repository_googledocs::SHARED_DRIVES_ROOT_ID . '|Shared+Drives'; $shareddrivesbrowser = new googledocs_shared_drives_content($servicemock, $path, $sortcontent); $contentnodes = $shareddrivesbrowser->get_content_nodes('', [$this, 'filter']); // Assert that the returned array of repository content nodes is equal to the expected one. $this->assertEquals($expected, $contentnodes); } /** * Data provider for test_get_content_nodes(). * * @return array */ public static function get_content_nodes_provider(): array { $rootid = \repository_googledocs::REPOSITORY_ROOT_ID; $shareddrivesid = \repository_googledocs::SHARED_DRIVES_ROOT_ID; $shareddrivesstring = get_string('shareddrives', 'repository_googledocs'); return [ 'Shared drives exist; ordering applied.' => [ [ self::create_google_drive_shared_drive_object('d85b21c0f86cb5', 'Shared Drive 1'), self::create_google_drive_shared_drive_object('d85b21c0f86cb0', 'Shared Drive 3'), self::create_google_drive_shared_drive_object('bed5a0f08d412a', 'Shared Drive 2'), ], true, [ self::create_folder_content_node_array('d85b21c0f86cb5', 'Shared Drive 1', "{$rootid}|Google+Drive/{$shareddrivesid}|" . urlencode($shareddrivesstring)), self::create_folder_content_node_array('bed5a0f08d412a', 'Shared Drive 2', "{$rootid}|Google+Drive/{$shareddrivesid}|" . urlencode($shareddrivesstring)), self::create_folder_content_node_array('d85b21c0f86cb0', 'Shared Drive 3', "{$rootid}|Google+Drive/{$shareddrivesid}|" . urlencode($shareddrivesstring)), ], ], 'Shared drives exist; ordering not applied.' => [ [ self::create_google_drive_shared_drive_object('0c4ad262c65333', 'Shared Drive 3'), self::create_google_drive_shared_drive_object('d85b21c0f86cb0', 'Shared Drive 1'), self::create_google_drive_shared_drive_object('bed5a0f08d412a', 'Shared Drive 2'), ], false, [ self::create_folder_content_node_array('0c4ad262c65333', 'Shared Drive 3', "{$rootid}|Google+Drive/{$shareddrivesid}|" . urlencode($shareddrivesstring)), self::create_folder_content_node_array('d85b21c0f86cb0', 'Shared Drive 1', "{$rootid}|Google+Drive/{$shareddrivesid}|" . urlencode($shareddrivesstring)), self::create_folder_content_node_array('bed5a0f08d412a', 'Shared Drive 2', "{$rootid}|Google+Drive/{$shareddrivesid}|" . urlencode($shareddrivesstring)), ], ], 'Shared drives do not exist.' => [ [], false, [], ], ]; } } PK*\).7.7@googledocs/tests/local/browser/googledocs_drive_content_test.phpnu[. namespace repository_googledocs\local\browser; use Google_Service_Drive; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/repository/googledocs/tests/googledocs_content_testcase.php'); require_once($CFG->dirroot . '/repository/googledocs/lib.php'); /** * Class containing unit tests for the drive browser class. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class googledocs_drive_content_test extends \googledocs_content_testcase { /** * Test get_content_nodes(). * * @dataProvider get_content_nodes_provider * @param string $query The query string * @param string $path The path * @param bool $sortcontent Whether the contents should be sorted in alphabetical order * @param array $filterextensions The array containing file extensions that should be disallowed (filtered) * @param array $shareddrives The array containing the existing shared drives * @param array $drivecontents The array containing the fetched google drive contents * @param array $expected The expected array which contains the generated repository content nodes */ public function test_get_content_nodes(string $query, string $path, bool $sortcontent, array $filterextensions, array $shareddrives, array $drivecontents, array $expected): void { // Mock the service object. $servicemock = $this->createMock(\repository_googledocs\rest::class); $listparams = [ 'q' => "'" . str_replace("'", "\'", $query) . "' in parents AND trashed = false", 'fields' => 'files(id,name,mimeType,webContentLink,webViewLink,fileExtension,modifiedTime,size,iconLink)', 'spaces' => 'drive', 'supportsAllDrives' => 'true', 'includeItemsFromAllDrives' => 'true', 'corpora' => 'allDrives', ]; if (!$this->shared_drives_supported()) { $callinvocations = $this->exactly(1); $servicemock->expects($callinvocations) ->method('call') ->willReturnCallback(function ( string $method, array $params ) use ( $listparams, $drivecontents, ) { $this->assertEquals('list', $method); $this->assertEquals($listparams, $params); return (object) [ 'files' => $drivecontents, ]; }); } else { // Assert that the call() method is being called twice with the given arguments consecutively. In the first // instance it is being called to fetch the shared drives (shared_drives_list), while in the second instance // to fetch the relevant drive contents (list). Also, define the returned data objects by these calls. $callinvocations = $this->exactly(2); $servicemock->expects($callinvocations) ->method('call') ->willReturnCallback(function ( string $method, array $params ) use ( $callinvocations, $shareddrives, $listparams, $drivecontents, ) { switch (self::getInvocationCount($callinvocations)) { case 1: $this->assertEquals('shared_drives_list', $method); $this->assertEquals([], $params); return (object) [ 'kind' => 'drive#driveList', 'nextPageToken' => 'd838181f30b0f5', 'drives' => $shareddrives, ]; case 2: $this->assertEquals('list', $method); $this->assertEquals($listparams, $params); return (object)[ 'files' => $drivecontents, ]; default: $this->fail('Unexpected call to the call() method.'); } }); } // Set the disallowed file types (extensions). $this->disallowedextensions = $filterextensions; $drivebrowser = new googledocs_drive_content($servicemock, $path, $sortcontent); $contentnodes = $drivebrowser->get_content_nodes($query, [$this, 'filter']); // Assert that the returned array of repository content nodes is equal to the expected one. $this->assertEquals($expected, $contentnodes); } /** * Data provider for test_get_content_nodes(). * * @return array */ public static function get_content_nodes_provider(): array { $rootid = \repository_googledocs::REPOSITORY_ROOT_ID; $mydriveid = \repository_googledocs::MY_DRIVE_ROOT_ID; return [ 'Folders and files exist in the drive; shared drives exist; ordering applied.' => [ $mydriveid, "{$rootid}|Google+Drive/{$mydriveid}|My+Drive", true, [], [ self::create_google_drive_shared_drive_object('d85b21c0f86cb5', 'Shared Drive 1'), ], [ self::create_google_drive_folder_object('1c4ad262c65333', 'Folder 2'), self::create_google_drive_file_object('d85b21c0f86cb0', 'File 3.pdf', 'application/pdf', 'pdf', '1000'), self::create_google_drive_folder_object('0c4ad262c65333', 'Folder 1'), self::create_google_drive_file_object('bed5a0f08d412a', 'File 1.pdf', 'application/pdf', 'pdf'), ], [ self::create_folder_content_node_array('0c4ad262c65333', 'Folder 1', "{$rootid}|Google+Drive/{$mydriveid}|My+Drive"), self::create_folder_content_node_array('1c4ad262c65333', 'Folder 2', "{$rootid}|Google+Drive/{$mydriveid}|My+Drive"), self::create_file_content_node_array('bed5a0f08d412a', 'File 1.pdf', 'File 1.pdf', null, '', 'https://googleusercontent.com/type/application/pdf', '', 'download'), self::create_file_content_node_array('d85b21c0f86cb0', 'File 3.pdf', 'File 3.pdf', '1000', '', 'https://googleusercontent.com/type/application/pdf', '', 'download'), ], ], 'Only folders exist in the drive; shared drives do not exist; ordering not applied.' => [ $mydriveid, "{$rootid}|Google+Drive/{$mydriveid}|My+Drive", false, [], [], [ self::create_google_drive_folder_object('0c4ad262c65333', 'Folder 3'), self::create_google_drive_folder_object('d85b21c0f86cb0', 'Folder 1'), self::create_google_drive_folder_object('bed5a0f08d412a', 'Folder 2'), ], [ self::create_folder_content_node_array('0c4ad262c65333', 'Folder 3', "{$rootid}|Google+Drive/{$mydriveid}|My+Drive"), self::create_folder_content_node_array('d85b21c0f86cb0', 'Folder 1', "{$rootid}|Google+Drive/{$mydriveid}|My+Drive"), self::create_folder_content_node_array('bed5a0f08d412a', 'Folder 2', "{$rootid}|Google+Drive/{$mydriveid}|My+Drive"), ], ], 'Only files exist in the drive; shared drives do not exist; ordering not applied; filter .txt.' => [ $mydriveid, "{$rootid}|Google+Drive/{$mydriveid}|My+Drive", false, ['txt'], [], [ self::create_google_drive_file_object('d85b21c0f86cb0', 'File 3.pdf', 'application/pdf', 'pdf', '1000'), self::create_google_drive_file_object('a85b21c0f86cb0', 'File 1.txt', 'text/plain', 'txt', '3000'), self::create_google_drive_file_object('f85b21c0f86cb0', 'File 2.doc', 'application/msword', 'doc', '2000'), ], [ self::create_file_content_node_array('d85b21c0f86cb0', 'File 3.pdf', 'File 3.pdf', '1000', '', 'https://googleusercontent.com/type/application/pdf', '', 'download'), self::create_file_content_node_array('f85b21c0f86cb0', 'File 2.doc', 'File 2.doc', '2000', '', 'https://googleusercontent.com/type/application/msword', '', 'download'), ], ], 'Contents do not exist in the drive; shared drives do not exist.' => [ $mydriveid, "{$rootid}|Google+Drive/{$mydriveid}|My+Drive", false, [], [], [], [], ], ]; } /** * Test get_navigation(). * * @dataProvider get_navigation_provider * @param string $nodepath The node path string * @param array $expected The expected array containing the repository navigation nodes */ public function test_get_navigation(string $nodepath, array $expected): void { // Mock the service object. $servicemock = $this->createMock(\repository_googledocs\rest::class); $drivebrowser = new googledocs_drive_content($servicemock, $nodepath); $navigation = $drivebrowser->get_navigation(); // Assert that the returned array containing the navigation nodes is equal to the expected one. $this->assertEquals($expected, $navigation); } /** * Data provider for test_get_navigation(). * * @return array */ public static function get_navigation_provider(): array { $rootid = \repository_googledocs::REPOSITORY_ROOT_ID; $mydriveid = \repository_googledocs::MY_DRIVE_ROOT_ID; return [ 'Return navigation nodes array from path where all nodes have a name.' => [ "{$rootid}|Google+Drive/{$mydriveid}|My+Drive/bed5a0f08d|Test+Folder", [ [ 'name' => 'Google Drive', 'path' => "{$rootid}|Google+Drive", ], [ 'name' => 'My Drive', 'path' => "{$rootid}|Google+Drive/{$mydriveid}|My+Drive", ], [ 'name' => 'Test Folder', 'path' => "{$rootid}|Google+Drive/{$mydriveid}|My+Drive/bed5a0f08d|Test+Folder", ], ], ], 'Return navigation nodes array from path where some nodes do not have a name.' => [ "{$rootid}|Google+Drive/{$mydriveid}|My+Drive/bed5a0f08d/d85b21c0f8|Test+Folder", [ [ 'name' => 'Google Drive', 'path' => "{$rootid}|Google+Drive", ], [ 'name' => 'My Drive', 'path' => "{$rootid}|Google+Drive/{$mydriveid}|My+Drive", ], [ 'name' => 'bed5a0f08d', 'path' => "{$rootid}|Google+Drive/{$mydriveid}|My+Drive/bed5a0f08d|bed5a0f08d", ], [ 'name' => 'Test Folder', 'path' => "{$rootid}|Google+Drive/{$mydriveid}|My+Drive/bed5a0f08d|bed5a0f08d/" . "d85b21c0f8|Test+Folder", ], ], ], ]; } /** * Determines whether shared drives are supported under the current Google Drive scope. * * @return bool */ private function shared_drives_supported(): bool { $scopes = Google_Service_Drive::DRIVE_FILE; // Full access is needed for shared drives (not just drive.file). return str_contains($scopes, 'auth/drive') && !str_contains($scopes, 'auth/drive.file'); } } PK*\NNN?googledocs/tests/local/browser/googledocs_root_content_test.phpnu[. namespace repository_googledocs\local\browser; use Google_Service_Drive; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/repository/googledocs/tests/googledocs_content_testcase.php'); require_once($CFG->dirroot . '/repository/googledocs/lib.php'); /** * Class containing unit tests for the repository root browser class. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class googledocs_root_content_test extends \googledocs_content_testcase { /** * Test get_content_nodes(). * * @dataProvider get_content_nodes_provider * @param array $shareddrives The array containing the existing shared drives * @param array $expected The expected array which contains the generated repository content nodes * @param bool $expectshared Whether shared drives should be tested * @covers \repository_googledocs */ public function test_get_content_nodes(array $shareddrives, array $expected, bool $expectshared): void { $scopessupportshared = $this->shared_drives_supported(); if ($expectshared && !$scopessupportshared) { $this->markTestSkipped('Shared drives not supported in current OAuth scope.'); } // Mock the service object. $servicemock = $this->createMock(\repository_googledocs\rest::class); if ($expectshared && $scopessupportshared) { // Expect shared drives API to be called. // Assert that the call() method is being called only once with the given arguments to fetch the existing // shared drives. Define the returned data object by this call. $servicemock->expects($this->once()) ->method('call') ->with('shared_drives_list', []) ->willReturn((object)[ 'kind' => 'drive#driveList', 'nextPageToken' => 'd838181f30b0f5', 'drives' => $shareddrives, ]); } else { // If shared drives are not expected or not supported, call() should not be invoked. $servicemock->expects($this->never()) ->method('call'); } $showshared = $expectshared && $scopessupportshared; $rootbrowser = new googledocs_root_content( $servicemock, \repository_googledocs::REPOSITORY_ROOT_ID . '|Google+Drive', $showshared ); $contentnodes = $rootbrowser->get_content_nodes('', [$this, 'filter']); // Assert that the returned array of repository content nodes is equal to the expected one. $this->assertEquals($expected, $contentnodes); } /** * Data provider for test_get_content_nodes(). * * @return array */ public static function get_content_nodes_provider(): array { $rootid = \repository_googledocs::REPOSITORY_ROOT_ID; $mydriveid = \repository_googledocs::MY_DRIVE_ROOT_ID; $shareddrivesid = \repository_googledocs::SHARED_DRIVES_ROOT_ID; return [ 'Shared drives exist.' => [ [ self::create_google_drive_shared_drive_object('d85b21c0f86cb5', 'Shared Drive 1'), self::create_google_drive_shared_drive_object('d85b21c0f86cb0', 'Shared Drive 3'), ], [ self::create_folder_content_node_array($mydriveid, get_string('mydrive', 'repository_googledocs'), "{$rootid}|Google+Drive"), self::create_folder_content_node_array($shareddrivesid, get_string('shareddrives', 'repository_googledocs'), "{$rootid}|Google+Drive"), ], true, // Expect shared drives. ], 'Shared drives do not exist.' => [ [], [ self::create_folder_content_node_array($mydriveid, get_string('mydrive', 'repository_googledocs'), "{$rootid}|Google+Drive"), ], false, // Do not expect shared drives. ], ]; } /** * Determines whether shared drives are supported under the current Google Drive scope. * * @return bool */ private function shared_drives_supported(): bool { $scopes = Google_Service_Drive::DRIVE_FILE; // Full access is needed for shared drives (not just drive.file). return str_contains($scopes, 'auth/drive') && !str_contains($scopes, 'auth/drive.file'); } } PK*\(p3googledocs/tests/repository_googledocs_testcase.phpnu[. /** * Base class for the googledoc repository unit tests. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class repository_googledocs_testcase extends \advanced_testcase { /** * Create an array that would replicate the structure of a repository folder node array. * * @param string $id The ID of the folder * @param string $name The name of the folder * @param string $path The root path * @param string $modified The date of the last modification * @return array The repository folder node array */ protected static function create_folder_content_node_array( string $id, string $name, string $path, string $modified = '', ): array { global $CFG, $OUTPUT; return [ 'id' => $id, 'title' => $name, 'path' => repository_googledocs\helper::build_node_path($id, $name, $path), 'date' => $modified, 'thumbnail' => "{$CFG->wwwroot}/theme/image.php/boost/core/1/" . file_folder_icon(), 'thumbnail_height' => 64, 'thumbnail_width' => 64, 'children' => [], ]; } /** * Create an array that would replicate the structure of a repository file node array. * * @param string $id The ID of the file * @param string $name The name of the file * @param string $title The title of the file node * @param string|null $size The size of the file * @param string $modified The date of the last modification * @param string $thumbnail The thumbnail of the file * @param string|null $link The external link to the file * @param string|null $exportformat The export format of the file * @param string|null $googledoctype The type of the Google Doc file (if applicable) * @return array The repository file node array */ protected static function create_file_content_node_array( string $id, string $name, string $title, ?string $size = null, string $modified = '', string $thumbnail = '', string $link = '', string $exportformat = '', ?string $googledoctype = null, ): array { return [ 'id' => $id, 'title' => $title, 'source' => json_encode([ 'id' => $id, 'name' => $name, 'link' => $link, 'exportformat' => $exportformat, 'googledoctype' => $googledoctype, ]), 'date' => $modified, 'size' => $size, 'thumbnail' => $thumbnail, 'thumbnail_height' => 64, 'thumbnail_width' => 64, ]; } /** * Create an object that would replicate the metadata for a shared drive returned by the Google Drive API call. * * @param string $id The ID of the shared drive * @param string $name The name of the shared drive * @return \stdClass The shared drive object */ protected static function create_google_drive_shared_drive_object(string $id, string $name): \stdClass { return (object) [ 'kind' => 'drive#drive', 'id' => $id, 'name' => $name, ]; } /** * Create an object that would replicate the metadata for a folder returned by the Google Drive API call. * * @param string $id The ID of the folder * @param string $name The name of the folder * @param string|null $modified The date of the last modification * @return \stdClass The folder object */ protected static function create_google_drive_folder_object(string $id, string $name, ?string $modified = null): \stdClass { return (object)[ 'id' => $id, 'name' => $name, 'mimeType' => 'application/vnd.google-apps.folder', 'webViewLink' => "https://drive.google.com/drive/folders/{$id}", 'iconLink' => 'https://googleusercontent.com/16/type/application/vnd.google-apps.folder+shared', 'modifiedTime' => $modified ?? '', ]; } /** * Create an object that would replicate the metadata for a file returned by the Google Drive API call. * * @param string $id The ID of the file * @param string $name The name of the file * @param string $mimetype The mimetype of the file * @param string|null $extension The extension of the file * @param string|null $size The size of the file * @param string|null $modified The date of the last modification * @param string|null $webcontentlink The link for downloading the content of the file * @param string|null $webviewlink The link for opening the file in a browser * @return \stdClass The file object */ protected static function create_google_drive_file_object( string $id, string $name, string $mimetype, ?string $extension = null, ?string $size = null, ?string $modified = null, ?string $webcontentlink = null, ?string $webviewlink = null, ): \stdClass { $googledrivefile = [ 'id' => $id, 'name' => $name, 'mimeType' => $mimetype, 'size' => $size ?? '', 'webContentLink' => $webcontentlink ?? '', 'webViewLink' => $webviewlink ?? '', 'iconLink' => "https://googleusercontent.com/type/{$mimetype}", 'modifiedTime' => $modified ?? '', ]; // The returned metadata might not always have the 'fileExtension' property. if ($extension) { $googledrivefile['fileExtension'] = $extension; } return (object)$googledrivefile; } } PK*\0googledocs/tests/googledocs_content_testcase.phpnu[. defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/repository/googledocs/tests/repository_googledocs_testcase.php'); /** * Base class for the googledoc repository unit tests related to content browsing and searching. * * @package repository_googledocs * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class googledocs_content_testcase extends repository_googledocs_testcase { /** @var array The array which contains the disallowed file extensions. */ protected $disallowedextensions = []; /** * Method used for filtering repository file nodes based on the current disallowed file extensions list. * * @param array $content The repository content node * @return bool If returns false, the repository content node should be filtered, otherwise do not filter. */ public function filter(array $content): bool { // If the disallowed file extensions list is empty, do not filter the content node. if (empty($this->disallowedextensions)) { return true; } foreach ($this->disallowedextensions as $extension) { // If the disallowed file extension matches the extension of the repository file node, // than filter this node. if (preg_match("#.{$extension}#i", $content['title'])) { return false; } } return true; } } PK*\4webdav/db/access.phpnu[. /** * Plugin capabilities. * * @package repository_webdav * @copyright 2009 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/webdav:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'coursecreator' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ) ); PK*\:$webdav/lang/en/repository_webdav.phpnu[. /** * Strings for component 'repository_webdav', language 'en', branch 'MOODLE_20_STABLE' * * @package repository_webdav * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['configplugin'] = 'WebDAV configuration'; $string['http'] = 'HTTP'; $string['https'] = 'HTTPS'; $string['pluginname'] = 'WebDAV repository'; $string['webdav_password'] = 'WebDAV server password'; $string['webdav_port'] = 'WebDAV server port'; $string['webdav_server'] = 'WebDAV server'; $string['webdav_path'] = 'WebDAV path'; $string['webdav_type'] = 'WebDAV type'; $string['webdav_user'] = 'WebDAV server user'; $string['webdavbasicauth'] = 'WebDAV basic authentication'; $string['webdavdigestauth'] = 'WebDAV digest authentication'; $string['webdav'] = 'WebDAV'; $string['webdav:view'] = 'View WebDav repository'; $string['privacy:metadata'] = 'The WebDAV repository plugin does not store any personal data, but it is transmitted from Moodle to the remote system.'; PK*\0J >>webdav/pix/icon.pngnu[PNG  IHDR IDATxV$W4=۶YUbI9f!mmLf1l|rQ?_0 eia}eOӪv޽%x<^SS{Nl_W ŗ뺂MsεLEe=s NQUE rP/d8ֲI UЋ鬑hh> `"5TC\vMZc;/1SW?l$D(ŵ"56M)!\4` 9 [6mq#5;XAڪ 'lUYi%Pdc.@ekql5 9Ð0f `U7Ӊt9]Ei]m0Ma4xviQ/ n3(-ҧyd 0 &dc< [~gU(*h+q$ H'aڐ!CZ`paݰaC= ;i$80ZQpaGۏh4`=}tC&Ϝ9VS`.j٥KC I0d!I$mcذa@رc޽UӴBV?cƌP(v:LΛ70 nВ f.p>| A"ŏX4< +{M8(sQ  ˌ[DJ+GƅWGOX==ݐNd:WwNRo6y^6>y$…nݺz!n+3%M~-Ww:_Bn̅.*i}^~],e6a9!n.6I$=.A8Vceb qŭ+#^sUjUБhˍ`/F;IENDB`PK*\H""webdav/lib.phpnu[. /** * This plugin is used to access webdav files * * @since Moodle 2.0 * @package repository_webdav * @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'); require_once($CFG->libdir.'/webdavlib.php'); /** * repository_webdav class * * @since Moodle 2.0 * @package repository_webdav * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_webdav extends repository { /** @var string webdav type. */ protected $webdav_type; /** @var mixed webdav port. */ protected $webdav_port; /** @var string webdav host. */ protected $webdav_host; /** @var webdav_client webdav client. */ protected $dav; public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) { parent::__construct($repositoryid, $context, $options); // set up webdav client if (empty($this->options['webdav_server'])) { return; } if ($this->options['webdav_auth'] == 'none') { $this->options['webdav_auth'] = false; } if (empty($this->options['webdav_type'])) { $this->webdav_type = ''; } else { $this->webdav_type = 'ssl://'; } if (empty($this->options['webdav_port'])) { $port = ''; if (empty($this->webdav_type)) { $this->webdav_port = 80; } else { $this->webdav_port = 443; $port = ':443'; } } else { $this->webdav_port = $this->options['webdav_port']; $port = ':' . $this->webdav_port; } $this->webdav_host = $this->webdav_type.$this->options['webdav_server'].$port; $this->dav = new webdav_client($this->options['webdav_server'], $this->options['webdav_user'], $this->options['webdav_password'], $this->options['webdav_auth'], $this->webdav_type); $this->dav->port = $this->webdav_port; $this->dav->debug = false; } public function check_login() { return true; } public function get_file($url, $title = '') { $url = urldecode($url); // Prepare a file with an arbitrary name - cannot be $title because of special chars (cf. MDL-57002). $path = $this->prepare_file(uniqid()); if (!$this->dav->open()) { return false; } $webdavpath = rtrim('/'.ltrim($this->options['webdav_path'], '/ '), '/ '); // without slash in the end $this->dav->get_file($webdavpath. $url, $path); return array('path'=>$path); } public function global_search() { return false; } public function get_listing($path='', $page = '') { global $CFG, $OUTPUT; $list = array(); $ret = array(); $ret['dynload'] = true; $ret['nosearch'] = true; $ret['nologin'] = true; $ret['path'] = array(array('name'=>get_string('webdav', 'repository_webdav'), 'path'=>'')); $ret['list'] = array(); if (!$this->dav->open()) { return $ret; } $webdavpath = rtrim('/'.ltrim($this->options['webdav_path'], '/ '), '/ '); // without slash in the end if (empty($path) || $path =='/') { $path = '/'; } else { $chunks = preg_split('|/|', trim($path, '/')); for ($i = 0; $i < count($chunks); $i++) { $ret['path'][] = array( 'name' => urldecode($chunks[$i]), 'path' => '/'. join('/', array_slice($chunks, 0, $i+1)). '/' ); } } $dir = $this->dav->ls($webdavpath. urldecode($path)); if (!is_array($dir)) { return $ret; } $folders = array(); $files = array(); foreach ($dir as $v) { if (!empty($v['lastmodified'])) { $v['lastmodified'] = strtotime($v['lastmodified']); } else { $v['lastmodified'] = null; } // Remove the server URL from the path (if present), otherwise links will not work - MDL-37014 $server = preg_quote($this->options['webdav_server']); $v['href'] = preg_replace("#https?://{$server}#", '', $v['href']); // Extracting object title from absolute path $v['href'] = substr(urldecode($v['href']), strlen($webdavpath)); $title = substr($v['href'], strlen($path)); if (!empty($v['resourcetype']) && $v['resourcetype'] == 'collection') { // A folder. if ($path != $v['href']) { $folders[strtoupper($title)] = array( 'title' => rtrim($title, '/'), 'thumbnail' => $OUTPUT->image_url(file_folder_icon())->out(false), 'children' => array(), 'datemodified' => $v['lastmodified'], 'path' => $v['href'], ); } }else{ // A file. $size = !empty($v['getcontentlength'])? $v['getcontentlength']:''; $files[strtoupper($title)] = array( 'title' => $title, 'thumbnail' => $OUTPUT->image_url(file_extension_icon($title))->out(false), 'size' => $size, 'datemodified' => $v['lastmodified'], 'source' => $v['href'], ); } } ksort($files); ksort($folders); $ret['list'] = array_merge($folders, $files); return $ret; } public static function get_instance_option_names() { return array('webdav_type', 'webdav_server', 'webdav_port', 'webdav_path', 'webdav_user', 'webdav_password', 'webdav_auth'); } public static function instance_config_form($mform) { $choices = array(0 => get_string('http', 'repository_webdav'), 1 => get_string('https', 'repository_webdav')); $mform->addElement('select', 'webdav_type', get_string('webdav_type', 'repository_webdav'), $choices); $mform->addRule('webdav_type', get_string('required'), 'required', null, 'client'); $mform->addElement('text', 'webdav_server', get_string('webdav_server', 'repository_webdav'), array('size' => '40')); $mform->addRule('webdav_server', get_string('required'), 'required', null, 'client'); $mform->setType('webdav_server', PARAM_HOST); $mform->addElement('text', 'webdav_path', get_string('webdav_path', 'repository_webdav'), array('size' => '40')); $mform->addRule('webdav_path', get_string('required'), 'required', null, 'client'); $mform->setType('webdav_path', PARAM_PATH); $choices = array(); $choices['none'] = get_string('none'); $choices['basic'] = get_string('webdavbasicauth', 'repository_webdav'); $choices['digest'] = get_string('webdavdigestauth', 'repository_webdav'); $mform->addElement('select', 'webdav_auth', get_string('authentication', 'admin'), $choices); $mform->addRule('webdav_auth', get_string('required'), 'required', null, 'client'); $mform->addElement('text', 'webdav_port', get_string('webdav_port', 'repository_webdav'), array('size' => '40')); $mform->setType('webdav_port', PARAM_INT); $mform->addElement('text', 'webdav_user', get_string('webdav_user', 'repository_webdav'), array('size' => '40')); $mform->setType('webdav_user', PARAM_RAW_TRIMMED); // Not for us to clean. $mform->addElement('passwordunmask', 'webdav_password', get_string('webdav_password', 'repository_webdav'), array('size' => '40')); } public function supported_returntypes() { return (FILE_INTERNAL | FILE_EXTERNAL); } /** * Is this repository accessing private data? * * @return bool */ public function contains_private_data() { return false; } } PK*\. /** * Version details * * @package repository * @subpackage webdav * @copyright 2009 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_webdav'; // Full name of the plugin (used for diagnostics) PK*\b#webdav/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for repository_webdav. * * @package repository_webdav * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_webdav\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for repository_webdav implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK*\ggwebdav/tests/generator/lib.phpnu[. /** * WebDAV repository data generator * * @package repository_webdav * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * WebDAV repository data generator class * * @package repository_webdav * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_webdav_generator extends testing_repository_generator { /** * Fill in record defaults. * * @param array $record * @return array */ protected function prepare_record(array $record) { $record = parent::prepare_record($record); if (!isset($record['webdav_type'])) { $record['webdav_type'] = 0; } if (!isset($record['webdav_server'])) { $record['webdav_server'] = 'webdav.server.local'; } if (!isset($record['webdav_port'])) { $record['webdav_port'] = ''; } if (!isset($record['webdav_path'])) { $record['webdav_path'] = '/'; } if (!isset($record['webdav_user'])) { $record['webdav_user'] = ''; } if (!isset($record['webdav_password'])) { $record['webdav_password'] = ''; } if (!isset($record['webdav_auth'])) { $record['webdav_auth'] = 'none'; } return $record; } } PK*\ 5""""classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for core_repository. * * @package core_repository * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_repository\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\context; use core_privacy\local\request\contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use core_privacy\local\request\userlist; use \core_privacy\local\request\approved_userlist; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for core_repository 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_database_table( 'repository_instances', [ 'name' => 'privacy:metadata:repository_instances:name', 'typeid' => 'privacy:metadata:repository_instances:typeid', 'userid' => 'privacy:metadata:repository_instances:userid', 'username' => 'privacy:metadata:repository_instances:username', 'password' => 'privacy:metadata:repository_instances:password', 'timecreated' => 'privacy:metadata:repository_instances:timecreated', 'timemodified' => 'privacy:metadata:repository_instances:timemodified', ], 'privacy:metadata:repository_instances' ); $collection->add_plugintype_link('repository', [], 'privacy:metadata:repository'); 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 { $contextlist = new contextlist(); // The repository_instances data is associated at the user context level, so retrieve the user's context id. $sql = "SELECT c.id FROM {repository_instances} ri JOIN {context} c ON c.instanceid = ri.userid AND c.contextlevel = :contextuser WHERE ri.userid = :userid GROUP BY c.id"; $params = [ 'contextuser' => CONTEXT_USER, 'userid' => $userid ]; $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific 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) { $context = $userlist->get_context(); if (!$context instanceof \context_user) { return; } $sql = "SELECT userid FROM {repository_instances} WHERE userid = ?"; $params = [$context->instanceid]; $userlist->add_from_sql('userid', $sql, $params); } /** * 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) { global $DB; // If the user has repository_instances data, then only the User context should be present so get the first context. $contexts = $contextlist->get_contexts(); if (count($contexts) == 0) { return; } $context = reset($contexts); // Sanity check that context is at the User context level, then get the userid. if ($context->contextlevel !== CONTEXT_USER) { return; } $userid = $context->instanceid; $sql = "SELECT DISTINCT ri.id as id, r.type as type, ri.name as name, ri.timecreated as timecreated, ri.timemodified as timemodified FROM {repository_instances} ri JOIN {repository} r ON r.id = ri.typeid WHERE ri.userid = :userid"; $params = [ 'userid' => $userid ]; $repositoryinstances = $DB->get_records_sql($sql, $params); foreach ($repositoryinstances as $repositoryinstance) { // The repository_instances data export is organised in: {User Context}/Repository plug-ins/{Plugin Name}/data.json. $subcontext = [ get_string('plugin', 'core_repository'), get_string('pluginname', 'repository_' . $repositoryinstance->type) ]; $data = (object) [ 'type' => $repositoryinstance->type, 'name' => $repositoryinstance->name, 'timecreated' => transform::datetime($repositoryinstance->timecreated), 'timemodified' => transform::datetime($repositoryinstance->timemodified) ]; writer::with_context($context)->export_data($subcontext, $data); } } /** * 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) { global $DB; // Sanity check that context is at the User context level, then get the userid. if ($context->contextlevel !== CONTEXT_USER) { return; } $userid = $context->instanceid; // Delete the repository_instances records created for the userid. $DB->delete_records('repository_instances', ['userid' => $userid]); } /** * 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) { global $DB; $context = $userlist->get_context(); if ($context instanceof \context_user) { $DB->delete_records('repository_instances', ['userid' => $context->instanceid]); } } /** * 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) { global $DB; // If the user has repository_instances data, then only the User context should be present so get the first context. $contexts = $contextlist->get_contexts(); if (count($contexts) == 0) { return; } $context = reset($contexts); // Sanity check that context is at the User context level, then get the userid. if ($context->contextlevel !== CONTEXT_USER) { return; } $userid = $context->instanceid; // Delete the repository_instances records created for the userid. $DB->delete_records('repository_instances', ['userid' => $userid]); } } PK*\#(~55draftfiles_ajax.phpnu[. /** * Draft file ajax file manager * * @package core * @subpackage repository * @copyright 2010 Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('AJAX_SCRIPT', true); require('../config.php'); require_once($CFG->libdir.'/filelib.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->dirroot.'/repository/lib.php'); $PAGE->set_context(context_system::instance()); require_login(); if (isguestuser()) { throw new \moodle_exception('noguest'); } require_sesskey(); $action = required_param('action', PARAM_ALPHA); $draftid = required_param('itemid', PARAM_INT); $filepath = optional_param('filepath', '/', PARAM_PATH); $usercontext = context_user::instance($USER->id); echo $OUTPUT->header(); // send headers // //NOTE TO ALL DEVELOPERS: this script must deal only with draft area of current user, it has to use only file_storage and no file_browser!! // switch ($action) { case 'dir': $data = new stdClass(); file_get_drafarea_folders($draftid, $filepath, $data); echo json_encode($data); die; case 'list': $filepath = optional_param('filepath', '/', PARAM_PATH); $data = repository::prepare_listing(file_get_drafarea_files($draftid, $filepath)); $info = file_get_draft_area_info($draftid); $data->filecount = $info['filecount']; $data->filesize = $info['filesize']; $data->tree = new stdClass(); file_get_drafarea_folders($draftid, '/', $data->tree); echo json_encode($data); die; case 'mkdir': $filepath = required_param('filepath', PARAM_PATH); $newdirname = required_param('newdirname', PARAM_FILE); $fs = get_file_storage(); $fs->create_directory($usercontext->id, 'user', 'draft', $draftid, file_correct_filepath(file_correct_filepath($filepath).$newdirname)); $return = new stdClass(); $return->filepath = $filepath; echo json_encode($return); die; case 'delete': $filename = required_param('filename', PARAM_FILE); $filepath = required_param('filepath', PARAM_PATH); $selectedfile = (object)[ 'filename' => $filename, 'filepath' => $filepath ]; $return = repository_delete_selected_files($usercontext, 'user', 'draft', $draftid, [$selectedfile]); if ($return) { $response = new stdClass(); $response->filepath = array_keys($return)[0]; echo json_encode($response); die; } echo json_encode(false); die; case 'deleteselected': $selected = required_param('selected', PARAM_RAW); $return = []; $selectedfiles = json_decode($selected); $return = repository_delete_selected_files($usercontext, 'user', 'draft', $draftid, $selectedfiles); echo (json_encode($return ? array_keys($return) : false)); die; case 'setmainfile': $filename = required_param('filename', PARAM_FILE); $filepath = required_param('filepath', PARAM_PATH); $filepath = file_correct_filepath($filepath); // reset sort order file_reset_sortorder($usercontext->id, 'user', 'draft', $draftid); // set main file $return = file_set_sortorder($usercontext->id, 'user', 'draft', $draftid, $filepath, $filename, 1); echo json_encode($return); die; case 'updatefile': // Allows to Rename file, move it to another directory, change it's license and author information in one request $filename = required_param('filename', PARAM_FILE); $filepath = required_param('filepath', PARAM_PATH); $updatedata = array(); $updatedata['filename'] = optional_param('newfilename', $filename, PARAM_FILE); $updatedata['filepath'] = $newfilepath = optional_param('newfilepath', $filepath, PARAM_PATH); if (($v = optional_param('newlicense', false, PARAM_TEXT)) !== false) { $updatedata['license'] = $v; } if (($v = optional_param('newauthor', false, PARAM_TEXT)) !== false) { $updatedata['author'] = $v; } try { repository::update_draftfile($draftid, $filepath, $filename, $updatedata); } catch (moodle_exception $e) { die(json_encode((object)array('error' => $e->getMessage()))); } die(json_encode((object)array('filepath' => $newfilepath))); case 'updatedir': $filepath = required_param('filepath', PARAM_PATH); $newdirname = required_param('newdirname', PARAM_FILE); $parent = required_param('newfilepath', PARAM_PATH); $newfilepath = clean_param($parent . '/' . $newdirname . '/', PARAM_PATH); try { repository::update_draftfile($draftid, $filepath, '.', array('filepath' => $newfilepath)); } catch (moodle_exception $e) { die(json_encode((object)array('error' => $e->getMessage()))); } die(json_encode((object)array('filepath' => $parent))); case 'zip': $filepath = required_param('filepath', PARAM_PATH); $zipper = get_file_packer('application/zip'); $fs = get_file_storage(); $file = $fs->get_file($usercontext->id, 'user', 'draft', $draftid, $filepath, '.'); $parent_path = $file->get_parent_directory()->get_filepath(); $filepath = explode('/', trim($file->get_filepath(), '/')); $filepath = array_pop($filepath); $zipfile = repository::get_unused_filename($draftid, $parent_path, $filepath . '.zip'); if ($newfile = $zipper->archive_to_storage([$filepath => $file], $usercontext->id, 'user', 'draft', $draftid, $parent_path, $zipfile, $USER->id)) { $return = new stdClass(); $return->filepath = $parent_path; echo json_encode($return); } else { echo json_encode(false); } die; case 'downloadselected': $selected = required_param('selected', PARAM_RAW); $selectedfiles = json_decode($selected); if (!count($selectedfiles)) { $filepath = required_param('filepath', PARAM_PATH); $selectedfiles = [(object)[ 'filename' => '', 'filepath' => $filepath ]]; } $return = repository_download_selected_files($usercontext, 'user', 'draft', $draftid, $selectedfiles); echo (json_encode($return)); die; case 'downloaddir': $filepath = required_param('filepath', PARAM_PATH); $selectedfile = (object)[ 'filename' => '', 'filepath' => $filepath ]; $return = repository_download_selected_files($usercontext, 'user', 'draft', $draftid, [$selectedfile]); echo json_encode($return); die; case 'unzip': $filename = required_param('filename', PARAM_FILE); $filepath = required_param('filepath', PARAM_PATH); $areamaxbytes = required_param('areamaxbytes', PARAM_INT); $return = new stdClass(); $zipper = get_file_packer('application/zip'); $fs = get_file_storage(); $file = $fs->get_file($usercontext->id, 'user', 'draft', $draftid, $filepath, $filename); // Get the total size of the content in the archive. $filecontentsize = $file->get_total_content_size($zipper); // Return an error if the returned size of the content is NULL. // This means the utility class was unable to read the content of the archive. if (is_null($filecontentsize)) { $return->error = get_string('cannotunzipcontentunreadable', 'repository'); die(json_encode($return)); } // Check whether the maximum size allowed in this draft area will be exceeded with unzipping the file. // If the maximum size allowed is exceeded, return an error before attempting to unzip. if (file_is_draft_area_limit_reached($draftid, $areamaxbytes, $filecontentsize)) { $return->error = get_string('cannotunzipquotaexceeded', 'repository'); die(json_encode($return)); } // Find unused name for directory to extract the archive. $temppath = $fs->get_unused_dirname($usercontext->id, 'user', 'draft', $draftid, $filepath. pathinfo($filename, PATHINFO_FILENAME). '/'); $donotremovedirs = array(); $doremovedirs = array($temppath); // Extract archive and move all files from $temppath to $filepath if (($processed = $file->extract_to_storage($zipper, $usercontext->id, 'user', 'draft', $draftid, $temppath, $USER->id)) !== false) { // Find all failures within the processed files, and return an error if any are found. $failed = array_filter($processed, static function($result): bool { return $result !== true; }); if (count($failed) > 0) { $return->error = get_string('cannotunzipextractfileerror', 'repository'); die(json_encode($return)); } $extractedfiles = $fs->get_directory_files($usercontext->id, 'user', 'draft', $draftid, $temppath, true); $xtemppath = preg_quote($temppath, '|'); foreach ($extractedfiles as $file) { $realpath = preg_replace('|^'.$xtemppath.'|', $filepath, $file->get_filepath()); if (!$file->is_directory()) { // Set the source to the extracted file to indicate that it came from archive. $file->set_source(serialize((object)array('source' => $filepath))); } if (!$fs->file_exists($usercontext->id, 'user', 'draft', $draftid, $realpath, $file->get_filename())) { // File or directory did not exist, just move it. $file->rename($realpath, $file->get_filename()); } else if (!$file->is_directory()) { // File already existed, overwrite it repository::overwrite_existing_draftfile($draftid, $realpath, $file->get_filename(), $file->get_filepath(), $file->get_filename()); } else { // Directory already existed, remove temporary dir but make sure we don't remove the existing dir $doremovedirs[] = $file->get_filepath(); $donotremovedirs[] = $realpath; } } $return->filepath = $filepath; } else { $return = false; } // Remove remaining temporary directories. foreach (array_diff($doremovedirs, $donotremovedirs) as $filepath) { if ($file = $fs->get_file($usercontext->id, 'user', 'draft', $draftid, $filepath, '.')) { $file->delete(); } } die(json_encode($return)); case 'getoriginal': $filename = required_param('filename', PARAM_FILE); $filepath = required_param('filepath', PARAM_PATH); $fs = get_file_storage(); $file = $fs->get_file($usercontext->id, 'user', 'draft', $draftid, $filepath, $filename); if (!$file) { echo json_encode(false); } else { $return = array('filename' => $filename, 'filepath' => $filepath, 'original' => $file->get_reference_details()); echo json_encode((object)$return); } die; case 'getreferences': $filename = required_param('filename', PARAM_FILE); $filepath = required_param('filepath', PARAM_PATH); $fs = get_file_storage(); $file = $fs->get_file($usercontext->id, 'user', 'draft', $draftid, $filepath, $filename); if (!$file) { echo json_encode(false); } else { $source = unserialize_object($file->get_source()); $return = array('filename' => $filename, 'filepath' => $filepath, 'references' => array()); $browser = get_file_browser(); if (isset($source->original)) { $reffiles = $fs->search_references($source->original); foreach ($reffiles as $reffile) { $refcontext = context::instance_by_id($reffile->get_contextid()); $fileinfo = $browser->get_file_info($refcontext, $reffile->get_component(), $reffile->get_filearea(), $reffile->get_itemid(), $reffile->get_filepath(), $reffile->get_filename()); if (empty($fileinfo)) { $return['references'][] = get_string('undisclosedreference', 'repository'); } else { $return['references'][] = $fileinfo->get_readable_fullname(); } } } echo json_encode((object)$return); } die; default: // no/unknown action? echo json_encode(false); die; } PK*\jc4flickr_public/db/access.phpnu[. /** * Plugin capabilities. * * @package repository_flickr_public * @copyright 2009 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/flickr_public:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'user' => CAP_ALLOW ) ) ); PK*\e  2flickr_public/lang/en/repository_flickr_public.phpnu[. /** * Strings for component 'repository_flickr_public', language 'en', branch 'MOODLE_20_STABLE' * * @package repository_flickr_public * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['all'] = 'All'; $string['apikey'] = 'API key'; $string['backtosearch'] = 'Back to search screen'; $string['by'] = 'Attribution license'; $string['by-nc'] = 'Attribution-NonCommercial license'; $string['by-nc-nd'] = 'Attribution-NonCommercial-NoDerivs license'; $string['by-nc-sa'] = 'Attribution-NonCommercial-ShareAlike license'; $string['by-nd'] = 'Attribution-NoDerivs license'; $string['by-sa'] = 'Attribution-ShareAlike license'; $string['callbackurl'] = 'Callback URL'; $string['commercialuse'] = 'I want to use the images commercially'; $string['configplugin'] = 'Flickr public configuration'; $string['creativecommonscommercial'] = 'Only creative commons commercial'; $string['emailaddress'] = 'Email address'; $string['flickr_public:view'] = 'Use Flickr public repository in file picker'; $string['fulltext'] = 'Full text'; $string['information'] = '
    Get a Flickr API Key for your Moodle site.
    '; $string['invalidemail'] = 'Invalid email account for flickr'; $string['license'] = 'Licence'; $string['modification'] = 'I want to be able to modify the images'; $string['notitle'] = 'notitle'; $string['nullphotolist'] = 'There are no photos in this account'; $string['remember'] = 'Remember me'; $string['pluginname_help'] = 'Repository on flickr.com'; $string['pluginname'] = 'Flickr public'; $string['secret'] = 'Secret'; $string['sharealike'] = 'Yes, as long as others share alike'; $string['tag'] = 'Tag'; $string['username'] = 'Flickr account email'; $string['watermark'] = 'Add watermark to photos'; $string['privacy:metadata:repository_flickr_public'] = 'The Flickr public repository plugin does not store any personal data, but does transmit user data from Moodle to the remote system.'; $string['privacy:metadata:repository_flickr_public:author'] = 'The Flickr public repository content author.'; $string['privacy:metadata:repository_flickr_public:email_address'] = 'The Flickr public repository user email address.'; $string['privacy:metadata:repository_flickr_public:text'] = 'The Flickr public repository user search text.'; $string['privacy:metadata:repository_flickr_public:user_id'] = 'The Flickr public repository user ID.'; PK*\F)\Rflickr_public/image.phpnu[. /** * moodle_image class * * @package repository * @subpackage flickr_public * @author Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ class moodle_image { private $imagepath; private $info; private $width; private $height; private $image; private $backup; function __construct($img) { ini_set('gd.jpeg_ignore_warning', 1); if(!function_exists('imagecreatefrompng') and !function_exists('imagecreatefromjpeg')) { throw new moodle_exception('gdnotexist'); } if(!file_exists($img) or !is_readable($img)) { throw new moodle_exception('invalidfile'); } $this->imagepath = $img; unset($img); $this->info = getimagesize($this->imagepath); switch($this->info['mime']) { case 'image/jpeg': $this->image = imagecreatefromjpeg($this->imagepath); break; case 'image/png': $this->image = imagecreatefrompng($this->imagepath); break; case 'image/gif': $this->image = imagecreatefromgif($this->imagepath); break; default: break; } $this->width = imagesx($this->image); $this->height = imagesy($this->image); } function destroy() { imagedestroy($this->image); imagedestroy($this->backup); return true; } function undo() { $this->image = $this->backup; return $this; } function watermark($text='', $pos=array(), $options=array()) { global $CFG; $text = iconv('ISO-8859-8', 'UTF-8', $text); if (empty($options['fontsize'])) { if (!empty($options['ttf'])) { $options['fontsize'] = 12; } else { $options['fontsize'] = 1; } } if (empty($options['font'])) { $options['font'] = $CFG->libdir . '/default.ttf'; } if (empty($options['angle'])) { $options['angle'] = 0; } $clr = imagecolorallocate($this->image, 255, 255, 255); if (!empty($options['ttf'])) { imagettftext($this->image, $options['fontsize'], // font size $options['angle'], $pos[0], $pos[1]+$options['fontsize'], $clr, $options['font'], $text); } else { imagestring($this->image, $options['fontsize'], $pos[0], $pos[1], $text, $clr); } return $this; } function rotate($angle=0, $bgcolor=0) { $this->image = imagerotate($this->image, $angle, $bgcolor); return $this; } function resize($w, $h, $use_resize = true) { if(empty($h) && !empty($w)) { $h = $this->height * ($w/$this->width); } if(!empty($h) && empty($w)) { $w = $this->width * ($h/$this->height); } $new_img = imagecreatetruecolor($w, $h); imagealphablending($new_img, false); imagecopyresampled($new_img /* dst */, $this->image /* src */, 0, 0, 0, 0, $w, $h, $this->width, $this->height); $this->image = $new_img; return $this; } function saveas($imagepath) { switch($this->info['mime']) { case 'image/jpeg': return imagejpeg($this->image, $imagepath); break; case 'image/png': return imagepng($this->image, $imagepath); break; case 'image/gif': return imagegif($this->image, $imagepath); break; default: break; } if(!$this->destroy()) { return false; } else { return $this; } } function display() { header('Content-type: '.$this->info['mime']); switch($this->info['mime']) { case 'image/png': imagepng($this->image); break; case 'image/jpeg': imagejpeg($this->image); break; case 'image/gif': imagegif($this->image); break; default: break; } $this->destroy(); return $this; } } PK*\ l||flickr_public/pix/icon.pngnu[PNG  IHDR(-SPLTE̵ʷPUacdkkl,|/6QsssttC3quxy*,O]UMtRNS#$,:;>GJJLSTVabdellrxy~.\IDATx5AQEuܝp*hpHw[DW%bAաa3/cc ~۸~dB4yPGO\jOݓl}T$ԔiŸxkU $QqQ.x tBy 7k9{]ڇ tܱ#!@O9׽s,"@x"Aב} cx k"_]z EJIENDB`PK*\f.TTflickr_public/lib.phpnu[. /** * This plugin is used to access flickr pictures * * @since Moodle 2.0 * @package repository_flickr_public * @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'); require_once($CFG->libdir.'/flickrlib.php'); require_once(__DIR__ . '/image.php'); /** * repository_flickr_public class * This one is used to create public repository * You can set up a public account in admin page, so everyone can access * flickr photos from this plugin * * @since Moodle 2.0 * @package repository_flickr_public * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_flickr_public extends repository { /** @var phpFlickr Flickr class. */ private $flickr; /** @var string Flick photos. */ public $photos; /** @var string API key. */ protected $api_key; /** @var string email address account. */ protected $flickr_account; /** @var string watermarks usage status. */ protected $usewatermarks; /** @var string session account. */ protected $sess_account; /** @var string session tag. */ protected $sess_tag; /** @var string session text. */ protected $sess_text; /** @var string Flickr user identifier. */ protected $nsid; /** * Stores sizes of images to prevent multiple API call */ static private $sizes = array(); /** * constructor method * * @global object $CFG * @global object $SESSION * @param int $repositoryid * @param int $context * @param array $options * @param boolean $readonly */ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly=0) { global $CFG, $SESSION; parent::__construct($repositoryid, $context, $options,$readonly); $this->api_key = $this->get_option('api_key'); $this->flickr = new phpFlickr($this->api_key); $this->flickr_account = $this->get_option('email_address'); $this->usewatermarks = $this->get_option('usewatermarks'); $account = optional_param('flickr_account', '', PARAM_RAW); $fulltext = optional_param('flickr_fulltext', '', PARAM_RAW); if (empty($fulltext)) { $fulltext = optional_param('s', '', PARAM_RAW); } $tag = optional_param('flickr_tag', '', PARAM_RAW); $license = optional_param('flickr_license', '', PARAM_RAW); $this->sess_account = 'flickr_public_'.$this->id.'_account'; $this->sess_tag = 'flickr_public_'.$this->id.'_tag'; $this->sess_text = 'flickr_public_'.$this->id.'_text'; if (!empty($account) or !empty($fulltext) or !empty($tag) or !empty($license)) { $SESSION->{$this->sess_tag} = $tag; $SESSION->{$this->sess_text} = $fulltext; $SESSION->{$this->sess_account} = $account; } } /** * save api_key in config table * @param array $options * @return boolean */ public function set_option($options = array()) { if (!empty($options['api_key'])) { set_config('api_key', trim($options['api_key']), 'flickr_public'); } unset($options['api_key']); return parent::set_option($options); } /** * get api_key from config table * * @param string $config * @return mixed */ public function get_option($config = '') { if ($config==='api_key') { return trim(get_config('flickr_public', 'api_key')); } else { $options['api_key'] = trim(get_config('flickr_public', 'api_key')); } return parent::get_option($config); } /** * is global_search available? * * @return boolean */ public function global_search() { if (empty($this->flickr_account)) { return false; } else { return true; } } /** * check if flickr account * @return boolean */ public function check_login() { return !empty($this->flickr_account); } /** * construct login form * * @param boolean $ajax * @return array */ public function print_login() { if ($this->options['ajax']) { $ret = array(); $fulltext = new stdClass(); $fulltext->label = get_string('fulltext', 'repository_flickr_public').': '; $fulltext->id = 'el_fulltext'; $fulltext->type = 'text'; $fulltext->name = 'flickr_fulltext'; $tag = new stdClass(); $tag->label = get_string('tag', 'repository_flickr_public').': '; $tag->id = 'el_tag'; $tag->type = 'text'; $tag->name = 'flickr_tag'; $email_field = new stdClass(); $email_field->label = get_string('username', 'repository_flickr_public').': '; $email_field->id = 'account'; $email_field->type = 'text'; $email_field->name = 'flickr_account'; $commercial = new stdClass(); $commercial->label = get_string('commercialuse', 'repository_flickr_public').': '; $commercial->id = 'flickr_commercial_id'; $commercial->type = 'checkbox'; $commercial->name = 'flickr_commercial'; $commercial->value = 'yes'; $modification = new stdClass(); $modification->label = get_string('modification', 'repository_flickr_public').': '; $modification->id = 'flickr_modification_id'; $modification->type = 'checkbox'; $modification->name = 'flickr_modification'; $modification->value = 'yes'; $ret['login'] = array($fulltext, $tag, $email_field, $commercial, $modification); $ret['login_btn_label'] = get_string('search'); $ret['login_btn_action'] = 'search'; return $ret; } else { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo '
    '; echo ''; echo '
    '; echo ''; echo '
    '; echo ''; echo ''; } } /** * destroy session * * @return object */ public function logout() { global $SESSION; unset($SESSION->{$this->sess_tag}); unset($SESSION->{$this->sess_text}); unset($SESSION->{$this->sess_account}); return $this->print_login(); } public function license4moodle($license_id) { $license = array( '0' => 'allrightsreserved', '1' => 'cc-nc-sa', '2' => 'cc-nc', '3' => 'cc-nc-nd', '4' => 'cc', '5' => 'cc-sa', '6' => 'cc-nd', '7' => 'other' ); return $license[$license_id]; } /** * search images on flickr * * @param string $search_text * @return array */ public function search($search_text, $page = 0) { global $SESSION; $ret = array(); if (empty($page)) { $page = 1; } if (!empty($this->flickr_account)) { $people = $this->flickr->people_findByEmail($this->flickr_account); $this->nsid = $people['nsid']; } if (!empty($SESSION->{$this->sess_account})) { $people = $this->flickr->people_findByEmail($SESSION->{$this->sess_account}); $this->nsid = $people['nsid']; } if (empty($this->nsid)) { $this->nsid = null; // user specify a flickr account, but it is not valid if (!empty($this->flickr_account) or !empty($SESSION->{$this->sess_account})) { $ret['e'] = get_string('invalidemail', 'repository_flickr_public'); return $ret; } } // including all licenses by default $licenses = array(1=>1, 2, 3, 4, 5, 6, 7); $commercial = optional_param('flickr_commercial', '', PARAM_RAW); $modification = optional_param('flickr_modification', '', PARAM_RAW); if ($commercial == 'yes') { // including // 4: Attribution License // 5: Attribution ShareAlike // 6: Attribution NoDerives // 7: unknown license unset($licenses[1], $licenses[2], $licenses[3]); } if ($modification == 'yes') { // including // 1: Attribution NonCommercial ShareAlike // 2: Attribution NonCommercial // 4: Attribution License // 5: Attribution ShareAlike // 7: unknown license unset($licenses[3], $licenses[6]); } //if ($modification == 'sharealike') { // including // 1: Attribution NonCommercial ShareAlike // 5: Attribution ShareAlike //unset($licenses[2], $licenses[3], $licenses[4], $licenses[6], $licenses[7]); //} $licenses = implode(',', $licenses); $tag = !empty($SESSION->{$this->sess_tag}) ? $SESSION->{$this->sess_tag} : null; $text = !empty($SESSION->{$this->sess_text}) ? $SESSION->{$this->sess_text} : null; $nsid = !empty($this->nsid) ? $this->nsid : null; $photos = $this->flickr->photos_search( array( 'tags' => $tag, 'page' => $page, 'per_page' => 24, 'user_id' => $nsid, 'license' => $licenses, 'text' => $text, 'extras' => 'original_format,license,date_upload,last_update', 'media' => 'photos' ) ); $ret['total'] = $photos['total']; $ret['perpage'] = $photos['perpage']; if (empty($photos)) { $ret['list'] = array(); return $ret; } $ret = $this->build_list($photos, $page, $ret); $ret['list'] = array_filter($ret['list'], array($this, 'filter')); return $ret; } /** * return an image list * * @param string $path * @param int $page * @return array */ public function get_listing($path = '', $page = 1) { $people = $this->flickr->people_findByEmail($this->flickr_account); $this->nsid = $people['nsid']; $photos = $this->flickr->people_getPublicPhotos($people['nsid'], 'original_format,license,date_upload,last_update', 24, $page); $ret = array(); return $this->build_list($photos, $page, $ret); } /** * build an image list * * @param array $photos * @param int $page * @return array */ private function build_list($photos, $page, &$ret) { global $OUTPUT; if (!empty($this->nsid)) { $photos_url = $this->flickr->urls_getUserPhotos($this->nsid); $ret['manage'] = $photos_url; } $ret['list'] = array(); $ret['nosearch'] = true; $ret['norefresh'] = true; $ret['logouttext'] = get_string('backtosearch', 'repository_flickr_public'); $ret['pages'] = $photos['pages']; if (is_int($page) && $page <= $ret['pages']) { $ret['page'] = $page; } else { $ret['page'] = 1; } if (!empty($photos['photo'])) { foreach ($photos['photo'] as $p) { if(empty($p['title'])) { $p['title'] = get_string('notitle', 'repository_flickr'); } if (isset($p['originalformat'])) { $format = $p['originalformat']; } else { $format = 'jpg'; } $format = '.'.$format; if (substr($p['title'], strlen($p['title'])-strlen($format)) != $format) { // append author id // $p['title'] .= '-'.$p['owner']; // append file extension $p['title'] .= $format; } // Get the thumbnail source URL. $thumbnailsource = $this->flickr->buildPhotoURL($p, 'Square'); if (!@getimagesize($thumbnailsource)) { // Use the file extension icon as a thumbnail if the original thumbnail does not exist to avoid // displaying broken thumbnails in the repository. $thumbnailsource = $OUTPUT->image_url(file_extension_icon($p['title']))->out(false); } // Perform a HEAD request to the image to obtain it's Content-Length. $curl = new curl(); $curl->head($this->get_link($p['id'])); // The photo sizes are statically cached, so we can retrieve image dimensions without another API call. $bestsize = $this->get_best_size($p['id']); $ret['list'][] = array( 'title' => $p['title'], 'source' => $p['id'], 'id' => $p['id'], 'thumbnail' => $thumbnailsource, 'datecreated' => $p['dateupload'], 'datemodified' => $p['lastupdate'], 'size' => (int)($curl->get_info()['download_content_length']), 'image_width' => $bestsize['width'], 'image_height' => $bestsize['height'], 'url' => 'http://www.flickr.com/photos/' . $p['owner'] . '/' . $p['id'], 'license' => $this->license4moodle($p['license']), 'author' => $p['owner'], ); } } return $ret; } /** * Print a search form * * @return string */ public function print_search() { $str = ''; $str .= ''; $str .= ''; $str .= ''; $str .= '

    '; $str .= '

    '; return $str; } /** * Return photo url by given photo id * @param string $photoid * @return string */ private function build_photo_url($photoid) { $bestsize = $this->get_best_size($photoid); if (!isset($bestsize['source'])) { throw new repository_exception('cannotdownload', 'repository'); } return $bestsize['source']; } /** * Returns the best size for a photo * * @param string $photoid the photo identifier * @return array of information provided by the API */ protected function get_best_size($photoid) { if (!isset(self::$sizes[$photoid])) { // Sizes are returned from smallest to greatest. self::$sizes[$photoid] = $this->flickr->photos_getSizes($photoid); } $sizes = self::$sizes[$photoid]; $bestsize = array(); if (is_array($sizes)) { while ($bestsize = array_pop($sizes)) { // Make sure the source is set. Exit the loop if found. if (isset($bestsize['source'])) { break; } } } return $bestsize; } public function get_link($photoid) { return $this->build_photo_url($photoid); } /** * * @global object $CFG * @param string $photoid * @param string $file * @return string */ public function get_file($photoid, $file = '') { global $CFG; $info = $this->flickr->photos_getInfo($photoid); // If we can read the original secret, it means that we have access to the original picture. if (isset($info['originalsecret'])) { $source = $this->flickr->buildPhotoURL($info, 'original'); } else { $source = $this->build_photo_url($photoid); } // Make sure the source image exists. if (!@getimagesize($source)) { throw new moodle_exception('cannotdownload', 'repository'); } if ($info['owner']['realname']) { $author = $info['owner']['realname']; } else { $author = $info['owner']['username']; } $copyright = get_string('author', 'repository') . ': ' . $author; $result = parent::get_file($source, $file); $path = $result['path']; if (!empty($this->usewatermarks)) { $img = new moodle_image($path); $img->watermark($copyright, array(10,10), array('ttf'=>true, 'fontsize'=>12))->saveas($path); } return array('path'=>$path, 'author'=>$info['owner']['realname'], 'license'=>$this->license4moodle($info['license'])); } /** * Add Instance settings input to Moodle form * @param object $mform */ public static function instance_config_form($mform) { $mform->addElement('text', 'email_address', get_string('emailaddress', 'repository_flickr_public')); $mform->setType('email_address', PARAM_RAW_TRIMMED); // This is for sending to flickr. Not our job to validate it. $mform->addElement('checkbox', 'usewatermarks', get_string('watermark', 'repository_flickr_public')); $mform->setDefault('usewatermarks', 0); } /** * Names of the instance settings * @return array */ public static function get_instance_option_names() { return array('email_address', 'usewatermarks'); } /** * Add Plugin settings input to Moodle form * @param object $mform */ public static function type_config_form($mform, $classname = 'repository') { $api_key = get_config('flickr_public', 'api_key'); if (empty($api_key)) { $api_key = ''; } $strrequired = get_string('required'); $mform->addElement('text', 'api_key', get_string('apikey', 'repository_flickr_public'), array('value'=>$api_key,'size' => '40')); $mform->setType('api_key', PARAM_RAW_TRIMMED); $mform->addRule('api_key', $strrequired, 'required', null, 'client'); $mform->addElement('static', null, '', get_string('information','repository_flickr_public')); } /** * Names of the plugin settings * @return array */ public static function get_type_option_names() { return array('api_key', 'pluginname'); } /** * is run when moodle administrator add the plugin */ public static function plugin_init() { //here we create a default instance for this type $id = repository::static_function('flickr_public','create', 'flickr_public', 0, context_system::instance(), array('name'=>'', 'email_address' => null, 'usewatermarks' => false), 0); if (empty($id)) { return false; } else { return true; } } public function supported_filetypes() { return array('web_image'); } public function supported_returntypes() { return (FILE_INTERNAL | FILE_EXTERNAL); } /** * Return the source information * * @param string $photoid photo id * @return string|null */ public function get_file_source_info($photoid) { return $this->build_photo_url($photoid); } /** * Is this repository accessing private data? * * @return bool */ public function contains_private_data() { return false; } } PK*\flickr_public/version.phpnu[. /** * Version details * * @package repository * @subpackage flickr_public * @copyright 2009 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_flickr_public'; // Full name of the plugin (used for diagnostics) PK*\  *flickr_public/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for repository_flickr_public. * * @package repository_flickr_public * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_flickr_public\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_flickr_public 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( 'flickr.com', [ 'author' => 'privacy:metadata:repository_flickr_public:author', 'email_address' => 'privacy:metadata:repository_flickr_public:email_address', 'text' => 'privacy:metadata:repository_flickr_public:text', 'user_id' => 'privacy:metadata:repository_flickr_public:user_id' ], 'privacy:metadata:repository_flickr_public' ); 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) { } } PK*\c%flickr_public/tests/generator/lib.phpnu[. /** * Flickr Public repository data generator * * @package repository_flickr_public * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Flickr Public repository data generator class * * @package repository_flickr_public * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_flickr_public_generator extends testing_repository_generator { /** * Fill in record defaults. * * @param array $record * @return array */ protected function prepare_record(array $record) { $record = parent::prepare_record($record); if (!isset($record['email_address'])) { $record['email_address'] = ''; } if (!isset($record['usewatermarks'])) { $record['usewatermarks'] = 0; } return $record; } /** * 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['api_key'])) { $record['api_key'] = 'api_key'; } return $record; } } PK*\]x upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /repository/ repository system, information provided here is intended especially for developers. Full details of the repository API are available on Moodle docs: https://moodledev.io/docs/apis/plugintypes/repository === 4.3 === * The base `repository` class method `get_file_size` has been deprecated and should not be used any more === 4.0 === * The repository_boxnet has been completely removed. * The repository_picasa has been completely removed (Picasa is discontinued since 2016). * The skydrive repository has been completely removed from core. It has been moved to the plugins database repository, so it can still be installed as a third-party plugin. * Methods can_import_skydrive_files() and import_skydrive_files have been deprecated from repository_onedrive. The feature For importing files from repository_skydrive to repository_onedrive will be completely removed in Moodle 4.4. === 3.11 === * The Google Drive repository now includes a new rest API function 'shared_drives_list', which can be used to fetch a list of existing shared drives. * The Google Drive repository now supports browsing and searching for content from shared drives. * The method build_breadcrumb() in repository/googledocs/lib.php has been deprecated, please use get_navigation() from the googledocs repository content classes instead. * The method build_node_path() in repository/googledocs/lib.php has been deprecated, please use \repository_googledocs\helper::build_node_path() instead. * The method explode_node_path() in repository/googledocs/lib.php has been deprecated, please use \repository_googledocs\helper::explode_node_path() instead. * The function query() in repository/googledocs/lib.php is deprecated, please use get_content_nodes() from the googledocs repository content classes instead. === 3.4 === Repositories should no longer directly call file_system#add_file_to_pool or file_system#add_string_to_pool instead they should call the stored_file method, set_synchronised_content_from_file or set_synchronised_content_from_string === 3.3 === The skydrive repository is deprecated - please migrate to the newer onedrive repository. === 3.2 === * The method repository::uses_post_requests() has been deprecated and must not be used anymore. * The alfresco repository has been moved to the plugins database. It was using an older version of the Alfresco SDK which is not compatible with recent versions of Alfresco. === 3.1 === * The following functions, previously used (exclusively) by upgrade steps are not available anymore because of the upgrade cleanup performed for this version. See MDL-51580 for more info: - repository_picasa_admin_upgrade_notification() - repository_googledocs_admin_upgrade_notification() - repository_boxnet_admin_upgrade_notification() - repository_alfresco_admin_security_key_notice() * The prepare_file() function will now return a file in a per-request directory which will be automatically cleaned at the end of the request. No modifications should be required as a result of this change. === 2.8 === * Repositories working with Moodle files must replace serialize() with json_encode() in the attribute 'source' returned by get_listing(). If repository overrides file_is_accessible(), get_file_reference() or get_file_source_info() they need to be changed too. See MDL-45616. === 2.6 === * get_option() now always return null when the first parameter ($config) is not empty, and no value was found for this $config. Previously this could sometimes return an empty array(). * The function repository_attach_id() was removed, it was never used and was not useful. * New functions send_relative_file() and supports_relative_file() to allow sending relative linked files - see filesystem repository for example. * DB fields files.referencelifetime, files.referencelastsync and files_reference.lifetime are deleted. The last synchronization time is stored only in files_reference.lastsync and lifetime is not stored in DB any more, each repository must decide for itself when to synchronize the file in function repository::sync_reference(). * The following methods in class repository are deprecated: sync_external_file(), get_file_by_reference(), get_reference_file_lifetime(), sync_individual_file() and reset_caches(). Instead there is one method repository::sync_reference() - this simplifies the repositories API and reduces the number of DB queries. * Deprecated const GETFILE_TIMEOUT, SYNCFILE_TIMEOUT and SYNCIMAGE_TIMEOUT and replaced them with config variables repositorygetfiletimeout, repositorysyncfiletimeout and repositorysyncimagetimeout. === 2.5 === * repository::append_suffix() has been deprecated, use repository::get_unused_filename() if you need to get a file name which has not yet been used in the draft area. * contains_private_data() is a new method to determine if a user 'logged in as' another user can access the content of the repository. The default is to return True (no access). * get_typename() returns the type of repository: dropbox, googledocs, etc... * can_be_edited_by_user() encapsulates all the tests to make sure that the current user has the rights to edit the instance of this repository. * repository::get_instances() (used by filepicker and texteditor to populate the list of available repositories) now calls repository constructor specifying the context where repository will be used. When checking permissions inside repository class use: $this->instance->contextid - to find the context where repository was created; $this->context - to find the current context where repository is used. This also means that the same repository instance may now have different names when called from different contexts. Avoid calling repository::get_instance(), use repository::get_repository_by_id() instead. === 2.4 === * copy_to_area() can receive a new parameter called $areamaxbytes which controls the maximum size of the area the files will be stored in. * the repositories using the upload() method should implement a check for $areamaxbytes, see repository/upload/lib.php upload() and process_upload() for an example on how handling it. === 2.3 === * instance_config_form() must now be declared static for php5.4 compatibility. * get_listing() and search() file metadata details are now now more prominently displayed in the interface. Dates and timestamos are now able to be sorted on in the filepicker interface so plugin authors are asked to ensure: - Dates are defined as UNIX timestamps - Filesize is an integer in bytes * It is recomended to define function get_file_source_info() to provide more rich infromation about the source of a file. * The API was extended to allow repositories can now support external references, please see https://moodledev.io/docs/apis/plugintypes/repository for further details. PK*\9s3/thirdpartylibs.xmlnu[ S3.php S3 A standalone Amazon S3 (REST) client for PHP 5.2.x using CURL that does not require PEAR. 0.5.1 BSD https://github.com/tpyo/amazon-s3-php-class 2013, Donovan Schönknecht PK*\i\s3/db/access.phpnu[. /** * Plugin capabilities. * * @package repository_s3 * @copyright 2009 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/s3:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'user' => CAP_ALLOW ) ) ); PK*\5s3/lang/en/repository_s3.phpnu[. /** * Strings for component 'repository_s3', language 'en', branch 'MOODLE_20_STABLE' * * @package repository_s3 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['access_key'] = 'Access key'; $string['configplugin'] = 'Amazon S3 settings'; $string['endpoint'] = 'Amazon S3 endpoint'; $string['needaccesskey'] = 'Access key must be provided'; $string['pluginname'] = 'Amazon S3'; $string['secret_key'] = 'Secret key'; $string['s3:view'] = 'View amazon s3 repository'; $string['privacy:metadata'] = 'The Amazon S3 repository plugin does not store any personal data, but does transmit user data from Moodle to the remote system.'; PK*\In ffs3/pix/icon.pngnu[PNG  IHDR_*?PLTEd؏3@@@>*|T~ ```PPPؓ 000 ppp]?Ϗ Ɉ  i^0t ʆ. lJM4>=;p8̯t/,%?6$NMJOI= g?:1r ." zPk|\nfUN>^\X n Q''tRNS@fTIDATx5a-E|,\9c@Pm 'O0pؿAR 융mr<@OSyq%;'IENDB`PK*\/'M^H-H- s3/lib.phpnu[. /** * This plugin is used to access s3 files * * @since Moodle 2.0 * @package repository_s3 * @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'); require_once($CFG->dirroot . '/repository/s3/S3.php'); /** * This is a repository class used to browse Amazon S3 content. * * @since Moodle 2.0 * @package repository_s3 * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_s3 extends repository { /** @var string access key. */ protected $access_key; /** @var string secret key. */ protected $secret_key; /** @var string endpoint URL. */ protected $endpoint; /** @var S3 S3 class. */ protected $s; /** * Constructor * @param int $repositoryid * @param object $context * @param array $options */ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) { global $CFG; parent::__construct($repositoryid, $context, $options); $this->access_key = get_config('s3', 'access_key'); $this->secret_key = get_config('s3', 'secret_key'); $this->endpoint = get_config('s3', 'endpoint'); if ($this->endpoint === false) { // If no endpoint has been set, use the default. $this->endpoint = 's3.amazonaws.com'; } $this->s = new S3($this->access_key, $this->secret_key, false, $this->endpoint); $this->s->setExceptions(true); // Port of curl::__construct(). if (!empty($CFG->proxyhost)) { if (empty($CFG->proxyport)) { $proxyhost = $CFG->proxyhost; } else { $proxyhost = $CFG->proxyhost . ':' . $CFG->proxyport; } $proxytype = CURLPROXY_HTTP; $proxyuser = null; $proxypass = null; if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) { $proxyuser = $CFG->proxyuser; $proxypass = $CFG->proxypassword; } if (!empty($CFG->proxytype) && $CFG->proxytype == 'SOCKS5') { $proxytype = CURLPROXY_SOCKS5; } $this->s->setProxy($proxyhost, $proxyuser, $proxypass, $proxytype); } } /** * Extracts the Bucket and URI from the path * * @param string $path path in this format 'bucket/path/to/folder/and/file' * @return array including bucket and uri */ protected function explode_path($path) { $parts = explode('/', $path, 2); if (isset($parts[1]) && $parts[1] !== '') { list($bucket, $uri) = $parts; } else { $bucket = $parts[0]; $uri = ''; } return array($bucket, $uri); } /** * Get S3 file list * * @param string $path * @return array The file list and options */ public function get_listing($path = '', $page = '') { global $CFG, $OUTPUT; if (empty($this->access_key)) { throw new moodle_exception('needaccesskey', 'repository_s3'); } $list = array(); $list['list'] = array(); $list['path'] = array( array('name' => get_string('pluginname', 'repository_s3'), 'path' => '') ); // the management interface url $list['manage'] = false; // dynamically loading $list['dynload'] = true; // the current path of this list. // set to true, the login link will be removed $list['nologin'] = true; // set to true, the search button will be removed $list['nosearch'] = true; $tree = array(); if (empty($path)) { try { $buckets = $this->s->listBuckets(); } catch (S3Exception $e) { throw new moodle_exception( 'errorwhilecommunicatingwith', 'repository', '', $this->get_name(), $e->getMessage() ); } foreach ($buckets as $bucket) { $folder = array( 'title' => $bucket, 'children' => array(), 'thumbnail' => $OUTPUT->image_url(file_folder_icon())->out(false), 'path' => $bucket ); $tree[] = $folder; } } else { $files = array(); $folders = array(); list($bucket, $uri) = $this->explode_path($path); try { $contents = $this->s->getBucket($bucket, $uri, null, null, '/', true); } catch (S3Exception $e) { throw new moodle_exception( 'errorwhilecommunicatingwith', 'repository', '', $this->get_name(), $e->getMessage() ); } foreach ($contents as $object) { // If object has a prefix, it is a 'CommonPrefix', which we consider a folder if (isset($object['prefix'])) { $title = rtrim($object['prefix'], '/'); } else { $title = $object['name']; } // Removes the prefix (folder path) from the title if (strlen($uri) > 0) { $title = substr($title, strlen($uri)); // Check if title is empty and not zero if (empty($title) && !is_numeric($title)) { // Amazon returns the prefix itself, we skip it continue; } } // This is a so-called CommonPrefix, we consider it as a folder if (isset($object['prefix'])) { $folders[] = array( 'title' => $title, 'children' => array(), 'thumbnail' => $OUTPUT->image_url(file_folder_icon())->out(false), 'path' => $bucket . '/' . $object['prefix'], ); } else { $files[] = array( 'title' => $title, 'size' => $object['size'], 'datemodified' => $object['time'], 'source' => $bucket . '/' . $object['name'], 'thumbnail' => $OUTPUT->image_url(file_extension_icon($title))->out(false) ); } } $tree = array_merge($folders, $files); } $trail = ''; if (!empty($path)) { $parts = explode('/', $path); if (count($parts) > 1) { foreach ($parts as $part) { if (!empty($part)) { $trail .= $part . '/'; $list['path'][] = array('name' => $part, 'path' => $trail); } } } else { $list['path'][] = array('name' => $path, 'path' => $path); } } $list['list'] = $tree; return $list; } /** * Download S3 files to moodle * * @param string $filepath * @param string $file The file path in moodle * @return array The local stored path */ public function get_file($filepath, $file = '') { list($bucket, $uri) = $this->explode_path($filepath); $path = $this->prepare_file($file); try { $this->s->getObject($bucket, $uri, $path); } catch (S3Exception $e) { throw new moodle_exception( 'errorwhilecommunicatingwith', 'repository', '', $this->get_name(), $e->getMessage() ); } return array('path' => $path); } /** * Return the source information * * @param stdClass $filepath * @return string */ public function get_file_source_info($filepath) { return 'Amazon S3: ' . $filepath; } /** * S3 doesn't require login * * @return bool */ public function check_login() { return true; } /** * S3 doesn't provide search * * @return bool */ public function global_search() { return false; } public static function get_type_option_names() { return array('access_key', 'secret_key', 'endpoint', 'pluginname'); } public static function type_config_form($mform, $classname = 'repository') { parent::type_config_form($mform); $strrequired = get_string('required'); $endpointselect = array( // List of possible Amazon S3 Endpoints. "s3.amazonaws.com" => "s3.amazonaws.com", "s3-external-1.amazonaws.com" => "s3-external-1.amazonaws.com", "s3-us-west-2.amazonaws.com" => "s3-us-west-2.amazonaws.com", "s3-us-west-1.amazonaws.com" => "s3-us-west-1.amazonaws.com", "s3-eu-west-1.amazonaws.com" => "s3-eu-west-1.amazonaws.com", "s3.eu-central-1.amazonaws.com" => "s3.eu-central-1.amazonaws.com", "s3-eu-central-1.amazonaws.com" => "s3-eu-central-1.amazonaws.com", "s3-ap-southeast-1.amazonaws.com" => "s3-ap-southeast-1.amazonaws.com", "s3-ap-southeast-2.amazonaws.com" => "s3-ap-southeast-2.amazonaws.com", "s3-ap-northeast-1.amazonaws.com" => "s3-ap-northeast-1.amazonaws.com", "s3-sa-east-1.amazonaws.com" => "s3-sa-east-1.amazonaws.com" ); $mform->addElement('text', 'access_key', get_string('access_key', 'repository_s3')); $mform->setType('access_key', PARAM_RAW_TRIMMED); $mform->addElement('text', 'secret_key', get_string('secret_key', 'repository_s3')); $mform->setType('secret_key', PARAM_RAW_TRIMMED); $mform->addElement('select', 'endpoint', get_string('endpoint', 'repository_s3'), $endpointselect); $mform->setDefault('endpoint', 's3.amazonaws.com'); // Default to US Endpoint. $mform->addRule('access_key', $strrequired, 'required', null, 'client'); $mform->addRule('secret_key', $strrequired, 'required', null, 'client'); } /** * S3 plugins doesn't support return links of files * * @return int */ public function supported_returntypes() { return FILE_INTERNAL; } /** * Is this repository accessing private data? * * @return bool */ public function contains_private_data() { return false; } } PK*\h=s3/version.phpnu[. /** * Version details * * @package repository * @subpackage s3 * @copyright 2009 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_s3'; // Full name of the plugin (used for diagnostics) PK*\M=s3/readme_moodle.txtnu[- S3.php - Amazon S3 PHP Class Cloned from https://github.com/tpyo/amazon-s3-php-class.git https://github.com/tpyo/amazon-s3-php-class http://undesigned.org.za/2007/10/22/amazon-s3-php-class Local changes applied: (verify on each upgrade of the library if they have been applied upstream. Remove the local changes if so) MDL-67031 php74 compliance. Change curly to square braces. MDL-77342 PHP 8.2 compliance. Add property $file in class S3Request. PK*\3Is3/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for repository_s3. * * @package repository_s3 * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_s3\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for repository_s3 implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK*\/ p!p! s3/S3.phpnu[ $host, 'type' => $type, 'user' => $user, 'pass' => $pass); } /** * Set the error mode to exceptions * * @param boolean $enabled Enable exceptions * @return void */ public static function setExceptions($enabled = true) { self::$useExceptions = $enabled; } /** * Set AWS time correction offset (use carefully) * * This can be used when an inaccurate system time is generating * invalid request signatures. It should only be used as a last * resort when the system time cannot be changed. * * @param string $offset Time offset (set to zero to use AWS server time) * @return void */ public static function setTimeCorrectionOffset($offset = 0) { if ($offset == 0) { $rest = new S3Request('HEAD'); $rest = $rest->getResponse(); $awstime = $rest->headers['date']; $systime = time(); $offset = $systime > $awstime ? -($systime - $awstime) : ($awstime - $systime); } self::$__timeOffset = $offset; } /** * Set signing key * * @param string $keyPairId AWS Key Pair ID * @param string $signingKey Private Key * @param boolean $isFile Load private key from file, set to false to load string * @return boolean */ public static function setSigningKey($keyPairId, $signingKey, $isFile = true) { self::$__signingKeyPairId = $keyPairId; if ((self::$__signingKeyResource = openssl_pkey_get_private($isFile ? file_get_contents($signingKey) : $signingKey)) !== false) return true; self::__triggerError('S3::setSigningKey(): Unable to open load private key: '.$signingKey, __FILE__, __LINE__); return false; } /** * Free signing key from memory, MUST be called if you are using setSigningKey() * * @return void */ public static function freeSigningKey() { if (self::$__signingKeyResource !== false) { self::$__signingKeyResource = null; } } /** * Internal error handler * * @internal Internal error handler * @param string $message Error message * @param string $file Filename * @param integer $line Line number * @param integer $code Error code * @return void */ private static function __triggerError($message, $file, $line, $code = 0) { if (self::$useExceptions) throw new S3Exception($message, $file, $line, $code); else trigger_error($message, E_USER_WARNING); } /** * Get a list of buckets * * @param boolean $detailed Returns detailed bucket list when true * @return array | false */ public static function listBuckets($detailed = false) { $rest = new S3Request('GET', '', '', self::$endpoint); $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 200) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::listBuckets(): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } $results = array(); if (!isset($rest->body->Buckets)) return $results; if ($detailed) { if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) $results['owner'] = array( 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName ); $results['buckets'] = array(); foreach ($rest->body->Buckets->Bucket as $b) $results['buckets'][] = array( 'name' => (string)$b->Name, 'time' => strtotime((string)$b->CreationDate) ); } else foreach ($rest->body->Buckets->Bucket as $b) $results[] = (string)$b->Name; return $results; } /** * Get contents for a bucket * * If maxKeys is null this method will loop through truncated result sets * * @param string $bucket Bucket name * @param string $prefix Prefix * @param string $marker Marker (last file listed) * @param string $maxKeys Max keys (maximum number of keys to return) * @param string $delimiter Delimiter * @param boolean $returnCommonPrefixes Set to true to return CommonPrefixes * @return array | false */ public static function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null, $returnCommonPrefixes = false) { $rest = new S3Request('GET', $bucket, '', self::$endpoint); if ($maxKeys == 0) $maxKeys = null; if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); if ($marker !== null && $marker !== '') $rest->setParameter('marker', $marker); if ($maxKeys !== null && $maxKeys !== '') $rest->setParameter('max-keys', $maxKeys); if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); else if (!empty(self::$defDelimiter)) $rest->setParameter('delimiter', self::$defDelimiter); $response = $rest->getResponse(); if ($response->error === false && $response->code !== 200) $response->error = array('code' => $response->code, 'message' => 'Unexpected HTTP status'); if ($response->error !== false) { self::__triggerError(sprintf("S3::getBucket(): [%s] %s", $response->error['code'], $response->error['message']), __FILE__, __LINE__); return false; } $results = array(); $nextMarker = null; if (isset($response->body, $response->body->Contents)) foreach ($response->body->Contents as $c) { $results[(string)$c->Key] = array( 'name' => (string)$c->Key, 'time' => strtotime((string)$c->LastModified), 'size' => (int)$c->Size, 'hash' => substr((string)$c->ETag, 1, -1) ); $nextMarker = (string)$c->Key; } if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes)) foreach ($response->body->CommonPrefixes as $c) $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix); if (isset($response->body, $response->body->IsTruncated) && (string)$response->body->IsTruncated == 'false') return $results; if (isset($response->body, $response->body->NextMarker)) $nextMarker = (string)$response->body->NextMarker; // Loop through truncated results if maxKeys isn't specified if ($maxKeys == null && $nextMarker !== null && (string)$response->body->IsTruncated == 'true') do { $rest = new S3Request('GET', $bucket, '', self::$endpoint); if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); $rest->setParameter('marker', $nextMarker); if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); if (($response = $rest->getResponse()) == false || $response->code !== 200) break; if (isset($response->body, $response->body->Contents)) foreach ($response->body->Contents as $c) { $results[(string)$c->Key] = array( 'name' => (string)$c->Key, 'time' => strtotime((string)$c->LastModified), 'size' => (int)$c->Size, 'hash' => substr((string)$c->ETag, 1, -1) ); $nextMarker = (string)$c->Key; } if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes)) foreach ($response->body->CommonPrefixes as $c) $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix); if (isset($response->body, $response->body->NextMarker)) $nextMarker = (string)$response->body->NextMarker; } while ($response !== false && (string)$response->body->IsTruncated == 'true'); return $results; } /** * Put a bucket * * @param string $bucket Bucket name * @param constant $acl ACL flag * @param string $location Set as "EU" to create buckets hosted in Europe * @return boolean */ public static function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false) { $rest = new S3Request('PUT', $bucket, '', self::$endpoint); $rest->setAmzHeader('x-amz-acl', $acl); if ($location !== false) { $dom = new DOMDocument; $createBucketConfiguration = $dom->createElement('CreateBucketConfiguration'); $locationConstraint = $dom->createElement('LocationConstraint', $location); $createBucketConfiguration->appendChild($locationConstraint); $dom->appendChild($createBucketConfiguration); $rest->data = $dom->saveXML(); $rest->size = strlen($rest->data); $rest->setHeader('Content-Type', 'application/xml'); } $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 200) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::putBucket({$bucket}, {$acl}, {$location}): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return true; } /** * Delete an empty bucket * * @param string $bucket Bucket name * @return boolean */ public static function deleteBucket($bucket) { $rest = new S3Request('DELETE', $bucket, '', self::$endpoint); $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 204) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::deleteBucket({$bucket}): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return true; } /** * Create input info array for putObject() * * @param string $file Input file * @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own) * @return array | false */ public static function inputFile($file, $md5sum = true) { if (!file_exists($file) || !is_file($file) || !is_readable($file)) { self::__triggerError('S3::inputFile(): Unable to open input file: '.$file, __FILE__, __LINE__); return false; } clearstatcache(false, $file); return array('file' => $file, 'size' => filesize($file), 'md5sum' => $md5sum !== false ? (is_string($md5sum) ? $md5sum : base64_encode(md5_file($file, true))) : ''); } /** * Create input array info for putObject() with a resource * * @param string $resource Input resource to read from * @param integer $bufferSize Input byte size * @param string $md5sum MD5 hash to send (optional) * @return array | false */ public static function inputResource(&$resource, $bufferSize = false, $md5sum = '') { if (!is_resource($resource) || (int)$bufferSize < 0) { self::__triggerError('S3::inputResource(): Invalid resource or buffer size', __FILE__, __LINE__); return false; } // Try to figure out the bytesize if ($bufferSize === false) { if (fseek($resource, 0, SEEK_END) < 0 || ($bufferSize = ftell($resource)) === false) { self::__triggerError('S3::inputResource(): Unable to obtain resource size', __FILE__, __LINE__); return false; } fseek($resource, 0); } $input = array('size' => $bufferSize, 'md5sum' => $md5sum); $input['fp'] =& $resource; return $input; } /** * Put an object * * @param mixed $input Input data * @param string $bucket Bucket name * @param string $uri Object URI * @param constant $acl ACL constant * @param array $metaHeaders Array of x-amz-meta-* headers * @param array $requestHeaders Array of request headers or content type as a string * @param constant $storageClass Storage class constant * @param constant $serverSideEncryption Server-side encryption * @return boolean */ public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD, $serverSideEncryption = self::SSE_NONE) { if ($input === false) return false; $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); if (!is_array($input)) $input = array( 'data' => $input, 'size' => strlen($input), 'md5sum' => base64_encode(md5($input, true)) ); // Data if (isset($input['fp'])) $rest->fp =& $input['fp']; elseif (isset($input['file'])) $rest->fp = @fopen($input['file'], 'rb'); elseif (isset($input['data'])) $rest->data = $input['data']; // Content-Length (required) if (isset($input['size']) && $input['size'] >= 0) $rest->size = $input['size']; else { if (isset($input['file'])) { clearstatcache(false, $input['file']); $rest->size = filesize($input['file']); } elseif (isset($input['data'])) $rest->size = strlen($input['data']); } // Custom request headers (Content-Type, Content-Disposition, Content-Encoding) if (is_array($requestHeaders)) foreach ($requestHeaders as $h => $v) strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v); elseif (is_string($requestHeaders)) // Support for legacy contentType parameter $input['type'] = $requestHeaders; // Content-Type if (!isset($input['type'])) { if (isset($requestHeaders['Content-Type'])) $input['type'] =& $requestHeaders['Content-Type']; elseif (isset($input['file'])) $input['type'] = self::__getMIMEType($input['file']); else $input['type'] = 'application/octet-stream'; } if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class $rest->setAmzHeader('x-amz-storage-class', $storageClass); if ($serverSideEncryption !== self::SSE_NONE) // Server-side encryption $rest->setAmzHeader('x-amz-server-side-encryption', $serverSideEncryption); // We need to post with Content-Length and Content-Type, MD5 is optional if ($rest->size >= 0 && ($rest->fp !== false || $rest->data !== false)) { $rest->setHeader('Content-Type', $input['type']); if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']); $rest->setAmzHeader('x-amz-acl', $acl); foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); $rest->getResponse(); } else $rest->response->error = array('code' => 0, 'message' => 'Missing input parameters'); if ($rest->response->error === false && $rest->response->code !== 200) $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); if ($rest->response->error !== false) { self::__triggerError(sprintf("S3::putObject(): [%s] %s", $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__); return false; } return true; } /** * Put an object from a file (legacy function) * * @param string $file Input file path * @param string $bucket Bucket name * @param string $uri Object URI * @param constant $acl ACL constant * @param array $metaHeaders Array of x-amz-meta-* headers * @param string $contentType Content type * @return boolean */ public static function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null) { return self::putObject(self::inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType); } /** * Put an object from a string (legacy function) * * @param string $string Input data * @param string $bucket Bucket name * @param string $uri Object URI * @param constant $acl ACL constant * @param array $metaHeaders Array of x-amz-meta-* headers * @param string $contentType Content type * @return boolean */ public static function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain') { return self::putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType); } /** * Get an object * * @param string $bucket Bucket name * @param string $uri Object URI * @param mixed $saveTo Filename or resource to write to * @return mixed */ public static function getObject($bucket, $uri, $saveTo = false) { $rest = new S3Request('GET', $bucket, $uri, self::$endpoint); if ($saveTo !== false) { if (is_resource($saveTo)) $rest->fp =& $saveTo; else if (($rest->fp = @fopen($saveTo, 'wb')) !== false) $rest->file = realpath($saveTo); else $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo); } if ($rest->response->error === false) $rest->getResponse(); if ($rest->response->error === false && $rest->response->code !== 200) $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); if ($rest->response->error !== false) { self::__triggerError(sprintf("S3::getObject({$bucket}, {$uri}): [%s] %s", $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__); return false; } return $rest->response; } /** * Get object information * * @param string $bucket Bucket name * @param string $uri Object URI * @param boolean $returnInfo Return response information * @return mixed | false */ public static function getObjectInfo($bucket, $uri, $returnInfo = true) { $rest = new S3Request('HEAD', $bucket, $uri, self::$endpoint); $rest = $rest->getResponse(); if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404)) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::getObjectInfo({$bucket}, {$uri}): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false; } /** * Copy an object * * @param string $srcBucket Source bucket name * @param string $srcUri Source object URI * @param string $bucket Destination bucket name * @param string $uri Destination object URI * @param constant $acl ACL constant * @param array $metaHeaders Optional array of x-amz-meta-* headers * @param array $requestHeaders Optional array of request headers (content type, disposition, etc.) * @param constant $storageClass Storage class constant * @return mixed | false */ public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD) { $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); $rest->setHeader('Content-Length', 0); foreach ($requestHeaders as $h => $v) strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v); foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class $rest->setAmzHeader('x-amz-storage-class', $storageClass); $rest->setAmzHeader('x-amz-acl', $acl); $rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, rawurlencode($srcUri))); if (sizeof($requestHeaders) > 0 || sizeof($metaHeaders) > 0) $rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE'); $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 200) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return isset($rest->body->LastModified, $rest->body->ETag) ? array( 'time' => strtotime((string)$rest->body->LastModified), 'hash' => substr((string)$rest->body->ETag, 1, -1) ) : false; } /** * Set up a bucket redirection * * @param string $bucket Bucket name * @param string $location Target host name * @return boolean */ public static function setBucketRedirect($bucket = NULL, $location = NULL) { $rest = new S3Request('PUT', $bucket, '', self::$endpoint); if( empty($bucket) || empty($location) ) { self::__triggerError("S3::setBucketRedirect({$bucket}, {$location}): Empty parameter.", __FILE__, __LINE__); return false; } $dom = new DOMDocument; $websiteConfiguration = $dom->createElement('WebsiteConfiguration'); $redirectAllRequestsTo = $dom->createElement('RedirectAllRequestsTo'); $hostName = $dom->createElement('HostName', $location); $redirectAllRequestsTo->appendChild($hostName); $websiteConfiguration->appendChild($redirectAllRequestsTo); $dom->appendChild($websiteConfiguration); $rest->setParameter('website', null); $rest->data = $dom->saveXML(); $rest->size = strlen($rest->data); $rest->setHeader('Content-Type', 'application/xml'); $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 200) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::setBucketRedirect({$bucket}, {$location}): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return true; } /** * Set logging for a bucket * * @param string $bucket Bucket name * @param string $targetBucket Target bucket (where logs are stored) * @param string $targetPrefix Log prefix (e,g; domain.com-) * @return boolean */ public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = null) { // The S3 log delivery group has to be added to the target bucket's ACP if ($targetBucket !== null && ($acp = self::getAccessControlPolicy($targetBucket, '')) !== false) { // Only add permissions to the target bucket when they do not exist $aclWriteSet = false; $aclReadSet = false; foreach ($acp['acl'] as $acl) if ($acl['type'] == 'Group' && $acl['uri'] == 'http://acs.amazonaws.com/groups/s3/LogDelivery') { if ($acl['permission'] == 'WRITE') $aclWriteSet = true; elseif ($acl['permission'] == 'READ_ACP') $aclReadSet = true; } if (!$aclWriteSet) $acp['acl'][] = array( 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'WRITE' ); if (!$aclReadSet) $acp['acl'][] = array( 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'READ_ACP' ); if (!$aclReadSet || !$aclWriteSet) self::setAccessControlPolicy($targetBucket, '', $acp); } $dom = new DOMDocument; $bucketLoggingStatus = $dom->createElement('BucketLoggingStatus'); $bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/'); if ($targetBucket !== null) { if ($targetPrefix == null) $targetPrefix = $bucket . '-'; $loggingEnabled = $dom->createElement('LoggingEnabled'); $loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket)); $loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix)); // TODO: Add TargetGrants? $bucketLoggingStatus->appendChild($loggingEnabled); } $dom->appendChild($bucketLoggingStatus); $rest = new S3Request('PUT', $bucket, '', self::$endpoint); $rest->setParameter('logging', null); $rest->data = $dom->saveXML(); $rest->size = strlen($rest->data); $rest->setHeader('Content-Type', 'application/xml'); $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 200) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::setBucketLogging({$bucket}, {$targetBucket}): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return true; } /** * Get logging status for a bucket * * This will return false if logging is not enabled. * Note: To enable logging, you also need to grant write access to the log group * * @param string $bucket Bucket name * @return array | false */ public static function getBucketLogging($bucket) { $rest = new S3Request('GET', $bucket, '', self::$endpoint); $rest->setParameter('logging', null); $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 200) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::getBucketLogging({$bucket}): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } if (!isset($rest->body->LoggingEnabled)) return false; // No logging return array( 'targetBucket' => (string)$rest->body->LoggingEnabled->TargetBucket, 'targetPrefix' => (string)$rest->body->LoggingEnabled->TargetPrefix, ); } /** * Disable bucket logging * * @param string $bucket Bucket name * @return boolean */ public static function disableBucketLogging($bucket) { return self::setBucketLogging($bucket, null); } /** * Get a bucket's location * * @param string $bucket Bucket name * @return string | false */ public static function getBucketLocation($bucket) { $rest = new S3Request('GET', $bucket, '', self::$endpoint); $rest->setParameter('location', null); $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 200) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::getBucketLocation({$bucket}): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return (isset($rest->body[0]) && (string)$rest->body[0] !== '') ? (string)$rest->body[0] : 'US'; } /** * Set object or bucket Access Control Policy * * @param string $bucket Bucket name * @param string $uri Object URI * @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy) * @return boolean */ public static function setAccessControlPolicy($bucket, $uri = '', $acp = array()) { $dom = new DOMDocument; $dom->formatOutput = true; $accessControlPolicy = $dom->createElement('AccessControlPolicy'); $accessControlList = $dom->createElement('AccessControlList'); // It seems the owner has to be passed along too $owner = $dom->createElement('Owner'); $owner->appendChild($dom->createElement('ID', $acp['owner']['id'])); $owner->appendChild($dom->createElement('DisplayName', $acp['owner']['name'])); $accessControlPolicy->appendChild($owner); foreach ($acp['acl'] as $g) { $grant = $dom->createElement('Grant'); $grantee = $dom->createElement('Grantee'); $grantee->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); if (isset($g['id'])) { // CanonicalUser (DisplayName is omitted) $grantee->setAttribute('xsi:type', 'CanonicalUser'); $grantee->appendChild($dom->createElement('ID', $g['id'])); } elseif (isset($g['email'])) { // AmazonCustomerByEmail $grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail'); $grantee->appendChild($dom->createElement('EmailAddress', $g['email'])); } elseif ($g['type'] == 'Group') { // Group $grantee->setAttribute('xsi:type', 'Group'); $grantee->appendChild($dom->createElement('URI', $g['uri'])); } $grant->appendChild($grantee); $grant->appendChild($dom->createElement('Permission', $g['permission'])); $accessControlList->appendChild($grant); } $accessControlPolicy->appendChild($accessControlList); $dom->appendChild($accessControlPolicy); $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); $rest->setParameter('acl', null); $rest->data = $dom->saveXML(); $rest->size = strlen($rest->data); $rest->setHeader('Content-Type', 'application/xml'); $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 200) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return true; } /** * Get object or bucket Access Control Policy * * @param string $bucket Bucket name * @param string $uri Object URI * @return mixed | false */ public static function getAccessControlPolicy($bucket, $uri = '') { $rest = new S3Request('GET', $bucket, $uri, self::$endpoint); $rest->setParameter('acl', null); $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 200) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } $acp = array(); if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) $acp['owner'] = array( 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName ); if (isset($rest->body->AccessControlList)) { $acp['acl'] = array(); foreach ($rest->body->AccessControlList->Grant as $grant) { foreach ($grant->Grantee as $grantee) { if (isset($grantee->ID, $grantee->DisplayName)) // CanonicalUser $acp['acl'][] = array( 'type' => 'CanonicalUser', 'id' => (string)$grantee->ID, 'name' => (string)$grantee->DisplayName, 'permission' => (string)$grant->Permission ); elseif (isset($grantee->EmailAddress)) // AmazonCustomerByEmail $acp['acl'][] = array( 'type' => 'AmazonCustomerByEmail', 'email' => (string)$grantee->EmailAddress, 'permission' => (string)$grant->Permission ); elseif (isset($grantee->URI)) // Group $acp['acl'][] = array( 'type' => 'Group', 'uri' => (string)$grantee->URI, 'permission' => (string)$grant->Permission ); else continue; } } } return $acp; } /** * Delete an object * * @param string $bucket Bucket name * @param string $uri Object URI * @return boolean */ public static function deleteObject($bucket, $uri) { $rest = new S3Request('DELETE', $bucket, $uri, self::$endpoint); $rest = $rest->getResponse(); if ($rest->error === false && $rest->code !== 204) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::deleteObject(): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return true; } /** * Get a query string authenticated URL * * @param string $bucket Bucket name * @param string $uri Object URI * @param integer $lifetime Lifetime in seconds * @param boolean $hostBucket Use the bucket name as the hostname * @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification) * @return string */ public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false) { $expires = self::__getTime() + $lifetime; $uri = str_replace(array('%2F', '%2B'), array('/', '+'), rawurlencode($uri)); return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s', // $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires, $hostBucket ? $bucket : self::$endpoint.'/'.$bucket, $uri, self::$__accessKey, $expires, urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}"))); } /** * Get a CloudFront signed policy URL * * @param array $policy Policy * @return string */ public static function getSignedPolicyURL($policy) { $data = json_encode($policy); $signature = ''; if (!openssl_sign($data, $signature, self::$__signingKeyResource)) return false; $encoded = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($data)); $signature = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($signature)); $url = $policy['Statement'][0]['Resource'] . '?'; foreach (array('Policy' => $encoded, 'Signature' => $signature, 'Key-Pair-Id' => self::$__signingKeyPairId) as $k => $v) $url .= $k.'='.str_replace('%2F', '/', rawurlencode($v)).'&'; return substr($url, 0, -1); } /** * Get a CloudFront canned policy URL * * @param string $url URL to sign * @param integer $lifetime URL lifetime * @return string */ public static function getSignedCannedURL($url, $lifetime) { return self::getSignedPolicyURL(array( 'Statement' => array( array('Resource' => $url, 'Condition' => array( 'DateLessThan' => array('AWS:EpochTime' => self::__getTime() + $lifetime) )) ) )); } /** * Get upload POST parameters for form uploads * * @param string $bucket Bucket name * @param string $uriPrefix Object URI prefix * @param constant $acl ACL constant * @param integer $lifetime Lifetime in seconds * @param integer $maxFileSize Maximum filesize in bytes (default 5MB) * @param string $successRedirect Redirect URL or 200 / 201 status code * @param array $amzHeaders Array of x-amz-meta-* headers * @param array $headers Array of request headers or content type as a string * @param boolean $flashVars Includes additional "Filename" variable posted by Flash * @return object */ public static function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600, $maxFileSize = 5242880, $successRedirect = "201", $amzHeaders = array(), $headers = array(), $flashVars = false) { // Create policy object $policy = new stdClass; $policy->expiration = gmdate('Y-m-d\TH:i:s\Z', (self::__getTime() + $lifetime)); $policy->conditions = array(); $obj = new stdClass; $obj->bucket = $bucket; array_push($policy->conditions, $obj); $obj = new stdClass; $obj->acl = $acl; array_push($policy->conditions, $obj); $obj = new stdClass; // 200 for non-redirect uploads if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) $obj->success_action_status = (string)$successRedirect; else // URL $obj->success_action_redirect = $successRedirect; array_push($policy->conditions, $obj); if ($acl !== self::ACL_PUBLIC_READ) array_push($policy->conditions, array('eq', '$acl', $acl)); array_push($policy->conditions, array('starts-with', '$key', $uriPrefix)); if ($flashVars) array_push($policy->conditions, array('starts-with', '$Filename', '')); foreach (array_keys($headers) as $headerKey) array_push($policy->conditions, array('starts-with', '$'.$headerKey, '')); foreach ($amzHeaders as $headerKey => $headerVal) { $obj = new stdClass; $obj->{$headerKey} = (string)$headerVal; array_push($policy->conditions, $obj); } array_push($policy->conditions, array('content-length-range', 0, $maxFileSize)); $policy = base64_encode(str_replace('\/', '/', json_encode($policy))); // Create parameters $params = new stdClass; $params->AWSAccessKeyId = self::$__accessKey; $params->key = $uriPrefix.'${filename}'; $params->acl = $acl; $params->policy = $policy; unset($policy); $params->signature = self::__getHash($params->policy); if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) $params->success_action_status = (string)$successRedirect; else $params->success_action_redirect = $successRedirect; foreach ($headers as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal; foreach ($amzHeaders as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal; return $params; } /** * Create a CloudFront distribution * * @param string $bucket Bucket name * @param boolean $enabled Enabled (true/false) * @param array $cnames Array containing CNAME aliases * @param string $comment Use the bucket name as the hostname * @param string $defaultRootObject Default root object * @param string $originAccessIdentity Origin access identity * @param array $trustedSigners Array of trusted signers * @return array | false */ public static function createDistribution($bucket, $enabled = true, $cnames = array(), $comment = null, $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array()) { if (!extension_loaded('openssl')) { self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): %s", "CloudFront functionality requires SSL"), __FILE__, __LINE__); return false; } $useSSL = self::$useSSL; self::$useSSL = true; // CloudFront requires SSL $rest = new S3Request('POST', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com'); $rest->data = self::__getCloudFrontDistributionConfigXML( $bucket.'.s3.amazonaws.com', $enabled, (string)$comment, (string)microtime(true), $cnames, $defaultRootObject, $originAccessIdentity, $trustedSigners ); $rest->size = strlen($rest->data); $rest->setHeader('Content-Type', 'application/xml'); $rest = self::__getCloudFrontResponse($rest); self::$useSSL = $useSSL; if ($rest->error === false && $rest->code !== 201) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } elseif ($rest->body instanceof SimpleXMLElement) return self::__parseCloudFrontDistributionConfig($rest->body); return false; } /** * Get CloudFront distribution info * * @param string $distributionId Distribution ID from listDistributions() * @return array | false */ public static function getDistribution($distributionId) { if (!extension_loaded('openssl')) { self::__triggerError(sprintf("S3::getDistribution($distributionId): %s", "CloudFront functionality requires SSL"), __FILE__, __LINE__); return false; } $useSSL = self::$useSSL; self::$useSSL = true; // CloudFront requires SSL $rest = new S3Request('GET', '', '2010-11-01/distribution/'.$distributionId, 'cloudfront.amazonaws.com'); $rest = self::__getCloudFrontResponse($rest); self::$useSSL = $useSSL; if ($rest->error === false && $rest->code !== 200) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::getDistribution($distributionId): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } elseif ($rest->body instanceof SimpleXMLElement) { $dist = self::__parseCloudFrontDistributionConfig($rest->body); $dist['hash'] = $rest->headers['hash']; $dist['id'] = $distributionId; return $dist; } return false; } /** * Update a CloudFront distribution * * @param array $dist Distribution array info identical to output of getDistribution() * @return array | false */ public static function updateDistribution($dist) { if (!extension_loaded('openssl')) { self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): %s", "CloudFront functionality requires SSL"), __FILE__, __LINE__); return false; } $useSSL = self::$useSSL; self::$useSSL = true; // CloudFront requires SSL $rest = new S3Request('PUT', '', '2010-11-01/distribution/'.$dist['id'].'/config', 'cloudfront.amazonaws.com'); $rest->data = self::__getCloudFrontDistributionConfigXML( $dist['origin'], $dist['enabled'], $dist['comment'], $dist['callerReference'], $dist['cnames'], $dist['defaultRootObject'], $dist['originAccessIdentity'], $dist['trustedSigners'] ); $rest->size = strlen($rest->data); $rest->setHeader('If-Match', $dist['hash']); $rest = self::__getCloudFrontResponse($rest); self::$useSSL = $useSSL; if ($rest->error === false && $rest->code !== 200) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } else { $dist = self::__parseCloudFrontDistributionConfig($rest->body); $dist['hash'] = $rest->headers['hash']; return $dist; } return false; } /** * Delete a CloudFront distribution * * @param array $dist Distribution array info identical to output of getDistribution() * @return boolean */ public static function deleteDistribution($dist) { if (!extension_loaded('openssl')) { self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): %s", "CloudFront functionality requires SSL"), __FILE__, __LINE__); return false; } $useSSL = self::$useSSL; self::$useSSL = true; // CloudFront requires SSL $rest = new S3Request('DELETE', '', '2008-06-30/distribution/'.$dist['id'], 'cloudfront.amazonaws.com'); $rest->setHeader('If-Match', $dist['hash']); $rest = self::__getCloudFrontResponse($rest); self::$useSSL = $useSSL; if ($rest->error === false && $rest->code !== 204) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } return true; } /** * Get a list of CloudFront distributions * * @return array */ public static function listDistributions() { if (!extension_loaded('openssl')) { self::__triggerError(sprintf("S3::listDistributions(): [%s] %s", "CloudFront functionality requires SSL"), __FILE__, __LINE__); return false; } $useSSL = self::$useSSL; self::$useSSL = true; // CloudFront requires SSL $rest = new S3Request('GET', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com'); $rest = self::__getCloudFrontResponse($rest); self::$useSSL = $useSSL; if ($rest->error === false && $rest->code !== 200) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { self::__triggerError(sprintf("S3::listDistributions(): [%s] %s", $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); return false; } elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->DistributionSummary)) { $list = array(); if (isset($rest->body->Marker, $rest->body->MaxItems, $rest->body->IsTruncated)) { //$info['marker'] = (string)$rest->body->Marker; //$info['maxItems'] = (int)$rest->body->MaxItems; //$info['isTruncated'] = (string)$rest->body->IsTruncated == 'true' ? true : false; } foreach ($rest->body->DistributionSummary as $summary) $list[(string)$summary->Id] = self::__parseCloudFrontDistributionConfig($summary); return $list; } return array(); } /** * List CloudFront Origin Access Identities * * @return array */ public static function listOriginAccessIdentities() { if (!extension_loaded('openssl')) { self::__triggerError(sprintf("S3::listOriginAccessIdentities(): [%s] %s", "CloudFront functionality requires SSL"), __FILE__, __LINE__); return false; } self::$useSSL = true; // CloudFront requires SSL $rest = new S3Request('GET', '', '2010-11-01/origin-access-identity/cloudfront', 'cloudfront.amazonaws.com'); $rest = self::__getCloudFrontResponse($rest); $useSSL = self::$useSSL; if ($rest->error === false && $rest->code !== 200) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { trigger_error(sprintf("S3::listOriginAccessIdentities(): [%s] %s", $rest->error['code'], $rest->error['message']), E_USER_WARNING); return false; } if (isset($rest->body->CloudFrontOriginAccessIdentitySummary)) { $identities = array(); foreach ($rest->body->CloudFrontOriginAccessIdentitySummary as $identity) if (isset($identity->S3CanonicalUserId)) $identities[(string)$identity->Id] = array('id' => (string)$identity->Id, 's3CanonicalUserId' => (string)$identity->S3CanonicalUserId); return $identities; } return false; } /** * Invalidate objects in a CloudFront distribution * * Thanks to Martin Lindkvist for S3::invalidateDistribution() * * @param string $distributionId Distribution ID from listDistributions() * @param array $paths Array of object paths to invalidate * @return boolean */ public static function invalidateDistribution($distributionId, $paths) { if (!extension_loaded('openssl')) { self::__triggerError(sprintf("S3::invalidateDistribution(): [%s] %s", "CloudFront functionality requires SSL"), __FILE__, __LINE__); return false; } $useSSL = self::$useSSL; self::$useSSL = true; // CloudFront requires SSL $rest = new S3Request('POST', '', '2010-08-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com'); $rest->data = self::__getCloudFrontInvalidationBatchXML($paths, (string)microtime(true)); $rest->size = strlen($rest->data); $rest = self::__getCloudFrontResponse($rest); self::$useSSL = $useSSL; if ($rest->error === false && $rest->code !== 201) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { trigger_error(sprintf("S3::invalidate('{$distributionId}',{$paths}): [%s] %s", $rest->error['code'], $rest->error['message']), E_USER_WARNING); return false; } return true; } /** * Get a InvalidationBatch DOMDocument * * @internal Used to create XML in invalidateDistribution() * @param array $paths Paths to objects to invalidateDistribution * @param int $callerReference * @return string */ private static function __getCloudFrontInvalidationBatchXML($paths, $callerReference = '0') { $dom = new DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; $invalidationBatch = $dom->createElement('InvalidationBatch'); foreach ($paths as $path) $invalidationBatch->appendChild($dom->createElement('Path', $path)); $invalidationBatch->appendChild($dom->createElement('CallerReference', $callerReference)); $dom->appendChild($invalidationBatch); return $dom->saveXML(); } /** * List your invalidation batches for invalidateDistribution() in a CloudFront distribution * * http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/ListInvalidation.html * returned array looks like this: * Array * ( * [I31TWB0CN9V6XD] => InProgress * [IT3TFE31M0IHZ] => Completed * [I12HK7MPO1UQDA] => Completed * [I1IA7R6JKTC3L2] => Completed * ) * * @param string $distributionId Distribution ID from listDistributions() * @return array */ public static function getDistributionInvalidationList($distributionId) { if (!extension_loaded('openssl')) { self::__triggerError(sprintf("S3::getDistributionInvalidationList(): [%s] %s", "CloudFront functionality requires SSL"), __FILE__, __LINE__); return false; } $useSSL = self::$useSSL; self::$useSSL = true; // CloudFront requires SSL $rest = new S3Request('GET', '', '2010-11-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com'); $rest = self::__getCloudFrontResponse($rest); self::$useSSL = $useSSL; if ($rest->error === false && $rest->code !== 200) $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); if ($rest->error !== false) { trigger_error(sprintf("S3::getDistributionInvalidationList('{$distributionId}'): [%s]", $rest->error['code'], $rest->error['message']), E_USER_WARNING); return false; } elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->InvalidationSummary)) { $list = array(); foreach ($rest->body->InvalidationSummary as $summary) $list[(string)$summary->Id] = (string)$summary->Status; return $list; } return array(); } /** * Get a DistributionConfig DOMDocument * * http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?PutConfig.html * * @internal Used to create XML in createDistribution() and updateDistribution() * @param string $bucket S3 Origin bucket * @param boolean $enabled Enabled (true/false) * @param string $comment Comment to append * @param string $callerReference Caller reference * @param array $cnames Array of CNAME aliases * @param string $defaultRootObject Default root object * @param string $originAccessIdentity Origin access identity * @param array $trustedSigners Array of trusted signers * @return string */ private static function __getCloudFrontDistributionConfigXML($bucket, $enabled, $comment, $callerReference = '0', $cnames = array(), $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array()) { $dom = new DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; $distributionConfig = $dom->createElement('DistributionConfig'); $distributionConfig->setAttribute('xmlns', 'http://cloudfront.amazonaws.com/doc/2010-11-01/'); $origin = $dom->createElement('S3Origin'); $origin->appendChild($dom->createElement('DNSName', $bucket)); if ($originAccessIdentity !== null) $origin->appendChild($dom->createElement('OriginAccessIdentity', $originAccessIdentity)); $distributionConfig->appendChild($origin); if ($defaultRootObject !== null) $distributionConfig->appendChild($dom->createElement('DefaultRootObject', $defaultRootObject)); $distributionConfig->appendChild($dom->createElement('CallerReference', $callerReference)); foreach ($cnames as $cname) $distributionConfig->appendChild($dom->createElement('CNAME', $cname)); if ($comment !== '') $distributionConfig->appendChild($dom->createElement('Comment', $comment)); $distributionConfig->appendChild($dom->createElement('Enabled', $enabled ? 'true' : 'false')); $trusted = $dom->createElement('TrustedSigners'); foreach ($trustedSigners as $id => $type) $trusted->appendChild($id !== '' ? $dom->createElement($type, $id) : $dom->createElement($type)); $distributionConfig->appendChild($trusted); $dom->appendChild($distributionConfig); //var_dump($dom->saveXML()); return $dom->saveXML(); } /** * Parse a CloudFront distribution config * * See http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?GetDistribution.html * * @internal Used to parse the CloudFront DistributionConfig node to an array * @param object &$node DOMNode * @return array */ private static function __parseCloudFrontDistributionConfig(&$node) { if (isset($node->DistributionConfig)) return self::__parseCloudFrontDistributionConfig($node->DistributionConfig); $dist = array(); if (isset($node->Id, $node->Status, $node->LastModifiedTime, $node->DomainName)) { $dist['id'] = (string)$node->Id; $dist['status'] = (string)$node->Status; $dist['time'] = strtotime((string)$node->LastModifiedTime); $dist['domain'] = (string)$node->DomainName; } if (isset($node->CallerReference)) $dist['callerReference'] = (string)$node->CallerReference; if (isset($node->Enabled)) $dist['enabled'] = (string)$node->Enabled == 'true' ? true : false; if (isset($node->S3Origin)) { if (isset($node->S3Origin->DNSName)) $dist['origin'] = (string)$node->S3Origin->DNSName; $dist['originAccessIdentity'] = isset($node->S3Origin->OriginAccessIdentity) ? (string)$node->S3Origin->OriginAccessIdentity : null; } $dist['defaultRootObject'] = isset($node->DefaultRootObject) ? (string)$node->DefaultRootObject : null; $dist['cnames'] = array(); if (isset($node->CNAME)) foreach ($node->CNAME as $cname) $dist['cnames'][(string)$cname] = (string)$cname; $dist['trustedSigners'] = array(); if (isset($node->TrustedSigners)) foreach ($node->TrustedSigners as $signer) { if (isset($signer->Self)) $dist['trustedSigners'][''] = 'Self'; elseif (isset($signer->KeyPairId)) $dist['trustedSigners'][(string)$signer->KeyPairId] = 'KeyPairId'; elseif (isset($signer->AwsAccountNumber)) $dist['trustedSigners'][(string)$signer->AwsAccountNumber] = 'AwsAccountNumber'; } $dist['comment'] = isset($node->Comment) ? (string)$node->Comment : null; return $dist; } /** * Grab CloudFront response * * @internal Used to parse the CloudFront S3Request::getResponse() output * @param object &$rest S3Request instance * @return object */ private static function __getCloudFrontResponse(&$rest) { $rest->getResponse(); if ($rest->response->error === false && isset($rest->response->body) && is_string($rest->response->body) && substr($rest->response->body, 0, 5) == 'response->body = simplexml_load_string($rest->response->body); // Grab CloudFront errors if (isset($rest->response->body->Error, $rest->response->body->Error->Code, $rest->response->body->Error->Message)) { $rest->response->error = array( 'code' => (string)$rest->response->body->Error->Code, 'message' => (string)$rest->response->body->Error->Message ); unset($rest->response->body); } } return $rest->response; } /** * Get MIME type for file * * To override the putObject() Content-Type, add it to $requestHeaders * * To use fileinfo, ensure the MAGIC environment variable is set * * @internal Used to get mime types * @param string &$file File path * @return string */ private static function __getMIMEType(&$file) { static $exts = array( 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png', 'ico' => 'image/x-icon', 'pdf' => 'application/pdf', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'svg' => 'image/svg+xml', 'svgz' => 'image/svg+xml', 'swf' => 'application/x-shockwave-flash', 'zip' => 'application/zip', 'gz' => 'application/x-gzip', 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip', 'bz2' => 'application/x-bzip2', 'rar' => 'application/x-rar-compressed', 'exe' => 'application/x-msdownload', 'msi' => 'application/x-msdownload', 'cab' => 'application/vnd.ms-cab-compressed', 'txt' => 'text/plain', 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html', 'css' => 'text/css', 'js' => 'text/javascript', 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml', 'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav', 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg', 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php' ); $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); if (isset($exts[$ext])) return $exts[$ext]; // Use fileinfo if available if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) { if (($type = finfo_file($finfo, $file)) !== false) { // Remove the charset and grab the last content-type $type = explode(' ', str_replace('; charset=', ';charset=', $type)); $type = array_pop($type); $type = explode(';', $type); $type = trim(array_shift($type)); } finfo_close($finfo); if ($type !== false && strlen($type) > 0) return $type; } return 'application/octet-stream'; } /** * Get the current time * * @internal Used to apply offsets to sytem time * @return integer */ public static function __getTime() { return time() + self::$__timeOffset; } /** * Generate the auth string: "AWS AccessKey:Signature" * * @internal Used by S3Request::getResponse() * @param string $string String to sign * @return string */ public static function __getSignature($string) { return 'AWS '.self::$__accessKey.':'.self::__getHash($string); } /** * Creates a HMAC-SHA1 hash * * This uses the hash extension if loaded * * @internal Used by __getSignature() * @param string $string String to sign * @return string */ private static function __getHash($string) { return base64_encode(extension_loaded('hash') ? hash_hmac('sha1', $string, self::$__secretKey, true) : pack('H*', sha1( (str_pad(self::$__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) . pack('H*', sha1((str_pad(self::$__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x36), 64))) . $string))))); } } /** * S3 Request class * * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class * @version 0.5.0-dev */ final class S3Request { /** * AWS URI * * @var string * @access pricate */ private $endpoint; /** * Verb * * @var string * @access private */ private $verb; /** * S3 bucket name * * @var string * @access private */ private $bucket; /** * Object URI * * @var string * @access private */ private $uri; /** * Final object URI * * @var string * @access private */ private $resource = ''; /** * Additional request parameters * * @var array * @access private */ private $parameters = array(); /** * Amazon specific request headers * * @var array * @access private */ private $amzHeaders = array(); /** * HTTP request headers * * @var array * @access private */ private $headers = array( 'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => '' ); /** * Use HTTP PUT? * * @var bool * @access public */ public $fp = false; /** * PUT file size * * @var int * @access public */ public $size = 0; /** * PUT post fields * * @var array * @access public */ public $data = false; /** * S3 request respone * * @var object * @access public */ public $response; /** * Filename or resource to write to. * * @var string * @access public */ public $file; /** * Constructor * * @param string $verb Verb * @param string $bucket Bucket name * @param string $uri Object URI * @param string $endpoint AWS endpoint URI * @return mixed */ function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.com') { $this->endpoint = $endpoint; $this->verb = $verb; $this->bucket = $bucket; $this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/'; //if ($this->bucket !== '') // $this->resource = '/'.$this->bucket.$this->uri; //else // $this->resource = $this->uri; if ($this->bucket !== '') { if ($this->__dnsBucketName($this->bucket)) { $this->headers['Host'] = $this->bucket.'.'.$this->endpoint; $this->resource = '/'.$this->bucket.$this->uri; } else { $this->headers['Host'] = $this->endpoint; $this->uri = $this->uri; if ($this->bucket !== '') $this->uri = '/'.$this->bucket.$this->uri; $this->bucket = ''; $this->resource = $this->uri; } } else { $this->headers['Host'] = $this->endpoint; $this->resource = $this->uri; } $this->headers['Date'] = gmdate('D, d M Y H:i:s T'); $this->response = new STDClass; $this->response->error = false; $this->response->body = null; $this->response->headers = array(); } /** * Set request parameter * * @param string $key Key * @param string $value Value * @return void */ public function setParameter($key, $value) { $this->parameters[$key] = $value; } /** * Set request header * * @param string $key Key * @param string $value Value * @return void */ public function setHeader($key, $value) { $this->headers[$key] = $value; } /** * Set x-amz-meta-* header * * @param string $key Key * @param string $value Value * @return void */ public function setAmzHeader($key, $value) { $this->amzHeaders[$key] = $value; } /** * Get the S3 response * * @return object | false */ public function getResponse() { $query = ''; if (sizeof($this->parameters) > 0) { $query = substr($this->uri, -1) !== '?' ? '?' : '&'; foreach ($this->parameters as $var => $value) if ($value == null || $value == '') $query .= $var.'&'; else $query .= $var.'='.rawurlencode($value).'&'; $query = substr($query, 0, -1); $this->uri .= $query; if (array_key_exists('acl', $this->parameters) || array_key_exists('location', $this->parameters) || array_key_exists('torrent', $this->parameters) || array_key_exists('website', $this->parameters) || array_key_exists('logging', $this->parameters)) $this->resource .= $query; } $url = (S3::$useSSL ? 'https://' : 'http://') . ($this->headers['Host'] !== '' ? $this->headers['Host'] : $this->endpoint) . $this->uri; //var_dump('bucket: ' . $this->bucket, 'uri: ' . $this->uri, 'resource: ' . $this->resource, 'url: ' . $url); // Basic setup $curl = curl_init(); curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php'); if (S3::$useSSL) { // Set protocol version curl_setopt($curl, CURLOPT_SSLVERSION, S3::$useSSLVersion); // SSL Validation can now be optional for those with broken OpenSSL installations curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, S3::$useSSLValidation ? 2 : 0); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, S3::$useSSLValidation ? 1 : 0); if (S3::$sslKey !== null) curl_setopt($curl, CURLOPT_SSLKEY, S3::$sslKey); if (S3::$sslCert !== null) curl_setopt($curl, CURLOPT_SSLCERT, S3::$sslCert); if (S3::$sslCACert !== null) curl_setopt($curl, CURLOPT_CAINFO, S3::$sslCACert); } curl_setopt($curl, CURLOPT_URL, $url); if (S3::$proxy != null && isset(S3::$proxy['host'])) { curl_setopt($curl, CURLOPT_PROXY, S3::$proxy['host']); curl_setopt($curl, CURLOPT_PROXYTYPE, S3::$proxy['type']); if (isset(S3::$proxy['user'], S3::$proxy['pass']) && S3::$proxy['user'] != null && S3::$proxy['pass'] != null) curl_setopt($curl, CURLOPT_PROXYUSERPWD, sprintf('%s:%s', S3::$proxy['user'], S3::$proxy['pass'])); } // Headers $headers = array(); $amz = array(); foreach ($this->amzHeaders as $header => $value) if (strlen($value) > 0) $headers[] = $header.': '.$value; foreach ($this->headers as $header => $value) if (strlen($value) > 0) $headers[] = $header.': '.$value; // Collect AMZ headers for signature foreach ($this->amzHeaders as $header => $value) if (strlen($value) > 0) $amz[] = strtolower($header).':'.$value; // AMZ headers must be sorted if (sizeof($amz) > 0) { //sort($amz); usort($amz, array(&$this, '__sortMetaHeadersCmp')); $amz = "\n".implode("\n", $amz); } else $amz = ''; if (S3::hasAuth()) { // Authorization string (CloudFront stringToSign should only contain a date) if ($this->headers['Host'] == 'cloudfront.amazonaws.com') $headers[] = 'Authorization: ' . S3::__getSignature($this->headers['Date']); else { $headers[] = 'Authorization: ' . S3::__getSignature( $this->verb."\n". $this->headers['Content-MD5']."\n". $this->headers['Content-Type']."\n". $this->headers['Date'].$amz."\n". $this->resource ); } } curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); curl_setopt($curl, CURLOPT_HEADER, false); curl_setopt($curl, CURLOPT_RETURNTRANSFER, false); curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback')); curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback')); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); // Request types switch ($this->verb) { case 'GET': break; case 'PUT': case 'POST': // POST only used for CloudFront if ($this->fp !== false) { curl_setopt($curl, CURLOPT_PUT, true); curl_setopt($curl, CURLOPT_INFILE, $this->fp); if ($this->size >= 0) curl_setopt($curl, CURLOPT_INFILESIZE, $this->size); } elseif ($this->data !== false) { curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data); } else curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); break; case 'HEAD': curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD'); curl_setopt($curl, CURLOPT_NOBODY, true); break; case 'DELETE': curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); break; default: break; } // Execute, grab errors if (curl_exec($curl)) $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE); else $this->response->error = array( 'code' => curl_errno($curl), 'message' => curl_error($curl), 'resource' => $this->resource ); @curl_close($curl); // Parse body into XML if ($this->response->error === false && isset($this->response->headers['type']) && $this->response->headers['type'] == 'application/xml' && isset($this->response->body)) { $this->response->body = simplexml_load_string($this->response->body); // Grab S3 errors if (!in_array($this->response->code, array(200, 204, 206)) && isset($this->response->body->Code, $this->response->body->Message)) { $this->response->error = array( 'code' => (string)$this->response->body->Code, 'message' => (string)$this->response->body->Message ); if (isset($this->response->body->Resource)) $this->response->error['resource'] = (string)$this->response->body->Resource; unset($this->response->body); } } // Clean up file resources if ($this->fp !== false && is_resource($this->fp)) fclose($this->fp); return $this->response; } /** * Sort compare for meta headers * * @internal Used to sort x-amz meta headers * @param string $a String A * @param string $b String B * @return integer */ private function __sortMetaHeadersCmp($a, $b) { $lenA = strpos($a, ':'); $lenB = strpos($b, ':'); $minLen = min($lenA, $lenB); $ncmp = strncmp($a, $b, $minLen); if ($lenA == $lenB) return $ncmp; if (0 == $ncmp) return $lenA < $lenB ? -1 : 1; return $ncmp; } /** * CURL write callback * * @param resource &$curl CURL resource * @param string &$data Data * @return integer */ private function __responseWriteCallback(&$curl, &$data) { if (in_array($this->response->code, array(200, 206)) && $this->fp !== false) return fwrite($this->fp, $data); else $this->response->body .= $data; return strlen($data); } /** * Check DNS conformity * * @param string $bucket Bucket name * @return boolean */ private function __dnsBucketName($bucket) { if (strlen($bucket) > 63 || preg_match("/[^a-z0-9\.-]/", $bucket) > 0) return false; if (S3::$useSSL && strstr($bucket, '.') !== false) return false; if (strstr($bucket, '-.') !== false) return false; if (strstr($bucket, '..') !== false) return false; if (!preg_match("/^[0-9a-z]/", $bucket)) return false; if (!preg_match("/[0-9a-z]$/", $bucket)) return false; return true; } /** * CURL header callback * * @param resource $curl CURL resource * @param string $data Data * @return integer */ private function __responseHeaderCallback($curl, $data) { if (($strlen = strlen($data)) <= 2) return $strlen; if (substr($data, 0, 4) == 'HTTP') $this->response->code = (int)substr($data, 9, 3); else { $data = trim($data); if (strpos($data, ': ') === false) return $strlen; list($header, $value) = explode(': ', $data, 2); if ($header == 'Last-Modified') $this->response->headers['time'] = strtotime($value); elseif ($header == 'Date') $this->response->headers['date'] = strtotime($value); elseif ($header == 'Content-Length') $this->response->headers['size'] = (int)$value; elseif ($header == 'Content-Type') $this->response->headers['type'] = $value; elseif ($header == 'ETag') $this->response->headers['hash'] = $value[0] == '"' ? substr($value, 1, -1) : $value; elseif (preg_match('/^x-amz-meta-.*$/', $header)) $this->response->headers[$header] = $value; } return $strlen; } } /** * S3 exception class * * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class * @version 0.5.0-dev */ class S3Exception extends Exception { /** * Class constructor * * @param string $message Exception message * @param string $file File in which exception was created * @param string $line Line number on which exception was created * @param int $code Exception code */ function __construct($message, $file, $line, $code = 0) { parent::__construct($message, $code); $this->file = $file; $this->line = $line; } } PK*\&@s3/tests/generator/lib.phpnu[. /** * Amazon S3 repository data generator * * @package repository_s3 * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Amazon S3 repository data generator class * * @package repository_s3 * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_s3_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['access_key'])) { $record['access_key'] = 'access_key'; } if (!isset($record['secret_key'])) { $record['secret_key'] = 'secret_key'; } if (!isset($record['endpoint'])) { $record['endpoint'] = 'endpoint'; } return $record; } } PK*\iicontentbank/db/install.phpnu[. /** * Installation file for the content bank repository * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Create a default instance of the content bank repository * * @return bool A status indicating success or failure */ function xmldb_repository_contentbank_install() { global $CFG; $result = true; require_once($CFG->dirroot.'/repository/lib.php'); $userplugin = new repository_type('contentbank', [], true); if (!$id = $userplugin->create(true)) { $result = false; } return $result; } PK*\>contentbank/db/access.phpnu[. /** * Plugin capabilities. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @author Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = [ 'repository/contentbank:view' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'coursecreator' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ] ], 'repository/contentbank:accesscoursecontent' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => [ 'coursecreator' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ] ], 'repository/contentbank:accesscoursecategorycontent' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_COURSECAT, 'archetypes' => [ 'coursecreator' => CAP_ALLOW, 'manager' => CAP_ALLOW ] ], 'repository/contentbank:accessgeneralcontent' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_COURSECAT, 'archetypes' => [ 'user' => CAP_ALLOW ] ] ]; PK*\Ǚ[TT.contentbank/lang/en/repository_contentbank.phpnu[. /** * Strings for component 'repository_contentbank', language 'en'. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @author Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['configplugin'] = 'Configuration for content bank repository'; $string['pluginname_help'] = 'Files in content bank'; $string['pluginname'] = 'Content bank'; $string['emptyfilelist'] = 'There are no files to show'; $string['contentbank:view'] = 'View content bank repository'; $string['contentbank:accesscoursecontent'] = 'Access course content bank files'; $string['contentbank:accesscoursecategorycontent'] = 'Access course category content bank files'; $string['contentbank:accessgeneralcontent'] = 'Access system content bank files'; $string['privacy:metadata'] = 'The Content bank repository plugin does not store or transmit any personal data.'; PK*\2%contentbank/pix/icon.svgnu[ PK*\$contentbank/pix/icon.pngnu[PNG  IHDR^`>\sRGB, pHYs9]9]C! {IDATx \U;2*BH K - tw [B(AE$,ʠ1#K$ԭNqT@epAQD1l# ,A!p;NѦutCuys.紴B!B!B!B!B!B!BMƕJ2rrC! m,D#ז_#/#@] !y$HsW3cq1_B܍Q7K=B(h[PǏgWC#pkלz~τbHo:Ü W ?DYu |;2=͜qkLr*ri5"0)EJHNdX7tjҶwKBP(9:Eސiᾂ$#! i"$+2>oCXKK3Z .V۰pe&s[rӅGa,{}<@!c"LCMrtB\(`,Ds%H!D9ts>~UKXWXH!DӦM 3@"K˃ta̕&GQ7˜8uHLMbfv$8JkkƘ.?pc9U] !޾1biY%![8~ytB1g |9mMܠ,.?3"ۜ3f)ASȏ}ֵ"gc۟#8 i}7>@y~>dIkLKB%0pI} `p̂2%e[46AO: !h+!$Z"_C^ ޘE+Qb߯cѱt!n "/H Vh !$$'O~OX<e4瑙} 0x*1m 2ABP(^S 6FI!BH /ryޘJ}U,lZ_{UZn[.\27sT%!dC[ޕ-2n<YiQ0Jls-yq|>_FZL}P(/ݿ! ̃K ;;!D ʇ},g!I=)+-N6H5BRY+ܒYqsu;$>ko y}'?I;BR;3QD2~HH*0ޚ]x1v`3򹮵uc~IH"ikkAvfw(0|t%$1ԅk}Ł8rt%D5c1 Ԍ\ w Qn҃j~L rܦ0Y-=pD2 dL6mrc2jЧIq))7Uzȅ5߻V.߯dƚ w;׿nu/+JOs{2ͦI_B!S,hXy߲Io7Nd/ݵJ9@@3 utR.Ê,҃IOJ`f!~l{IJ#r%<~;l3擺N麦<>.#5{獝15^-9a0;?cRI.[?:Ee|qmٸIJ YH8i ccڲͦawiw%L%P߹ers%,(=fV$H9 ½M\lb.T*$90ÝNUM0]vA斫jfiߕD; 掎Iz ya|2=>+ZY3AV 1r||S`4K3_ATSxlS`{<:3҃aȜ=7F9>/<]xmsUa1x(eю|=Y瀄Ҝ{zDvG0GM 9àɤ–MJK9ӕ)FCtttl ~Y'=0V39g`z/--Eì/ޛ]<fFt.d`)k{o6q꡷<*/f8?-1k H|Ǔq4kp\lL{gY͎B`l'>SnxI/j谷1xYt⃚QGbSDW5t2/4&:~]_;nq/ P*SZ7#UD##2 aN6vwLrОӷp`1iOq@'ǟ_,i0aIo7# K 3t?-X[h Ә> g#yQ\ -fubp3\`.z͢8 g{N=^?3kTlOl7rvaq90T5|1ɵ{2vąČw`FZ{Bp''+.Xt3ԝxEՎn6\~p9>{/b*^-tQ07F($[^X21eb݂}ۃ9{ yf%0tdӧo.]рZ#>bKzGYL]vz sf- vUvc~~{,l .>b`Ӄܽ:v+H^><;Z?=Xzn\5WnD/CtW[raZ;H{3C74[SH7Tc^[oӷ V-*)ߞug >cb jЕ-](1GbI%tNǬPEhY{1GK7x"u==[kmkk5UE4 7eVyxBtmkuA}WcZJO;)tQJҺv ) e9ߐ jrjk5ԁuAΩUφ. rÌw)I!]cԪE⵵-?o{'k-lWo~;EOe e3!5fjn"eSڋY"tQ U >mN+(]cٖxMi,S gYS$}d. ̢1ҵ:xOkl|Hs]@6K%]_'^ck񮑮Mjs[kc dAi85EJ&IsD 5- 3?*u[vZMxt}m*DbxqjI'kkl`. 7-J/v;AśȧU?$|ƛ셎B׬D6 X>ϛpV6{Nƶ9[6Uklm5/^ԲHHH9{ks5d;|m%9R[,${ƛtz+Ip 1xJ^3@ wa†v9x rPzxBvf7nS@ '+uXn (^Pzx[[,?f-DxBmy m!ūW/oKFMn%~Ɛ ūԋsj3Ŭ#v(^PzIx!}cchXxB%-7+|>K m+^(^^f{fś;(^Pzxe͊T*$^(^PzIx!ӋC7qkRzxBJW/chXxB-٬xmHK ^B&.AKŋ0;.A Z1KlPzxzB]a QCūW//DzzHCūW//dzrHCūW//DzDȻ,AK[,w)DKK[(2!O5CūW/B}9|&B w7!f/XnXxBm~;xYnXxBmvF lY ūOB>6$V(^PzxP] C zjC @.ۮ#v(^PzxjB,^(^P(J&;B[@ ;pD"PzxBzv6^(^Pr=kq;A Ż"8ÈA W/^(!0{>^xƗM(!(m`{@m ūW/ׇ@W/^( ~eQšxBlQšxBRt`]"PzxByXb="PzxBپ(Ω (^Pzx3 u_6x<5 ֍#yaxBmGE ūI ¾ kE ū7Cȓ0^(^P!tO4[EPzxB~k͑0^(^P!)"QA(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śr(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śr(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śr(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śr(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śr(^PzxSūW/oʡxBM9^(^P) ū7PzxBW/^(ޔC śrz| ڤV.vVgH&xJV sK!B!B!B!B!B!B!B!B!B!B哏&IENDB`PK*\i"contentbank/lib.phpnu[. /** * This plugin is used to access the content bank files. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/repository/lib.php'); /** * repository_contentbank class is used to browse the content bank files * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_contentbank extends repository { /** * Get file listing. * * @param string $encodedpath * @param string $page * @return array */ public function get_listing($encodedpath = '', $page = '') { global $SITE; $ret = []; $ret['dynload'] = true; $ret['nosearch'] = false; $ret['nologin'] = true; // Return the parameters from the encoded path if the encoded path is not empty. if (!empty($encodedpath)) { $params = json_decode(base64_decode($encodedpath), true); if (is_array($params) && isset($params['contextid'])) { $context = context::instance_by_id(clean_param($params['contextid'], PARAM_INT)); } } // Return the current context if the context was not specified in the encoded path. // The current context should be an instance of context_system, context_coursecat or course related contexts. if (empty($context) && !empty($this->context)) { if ($this->context instanceof \context_system || $this->context instanceof \context_coursecat) { $context = $this->context; } else if ($coursecontext = $this->context->get_course_context(false)) { // Skip if front page context. if ($coursecontext->instanceid !== $SITE->id) { $context = $coursecontext; } } } // If not, return the system context as a default context. if (empty($context)) { $context = context_system::instance(); } $ret['list'] = []; $ret['path'] = []; // Get the content bank browser for the specified context. if ($browser = \repository_contentbank\helper::get_contentbank_browser($context)) { $manageurl = new moodle_url('/contentbank/index.php', ['contextid' => $context->id]); $canaccesscontent = has_capability('moodle/contentbank:access', $context); $ret['manage'] = $canaccesscontent ? $manageurl->out() : ''; $ret['list'] = $browser->get_content(); $ret['path'] = $browser->get_navigation(); } return $ret; } /** * Is this repository used to browse moodle files? * * @return boolean */ public function has_moodle_files() { return true; } /** * Tells how the file can be picked from this repository. * * @return int */ public function supported_returntypes() { return FILE_INTERNAL | FILE_REFERENCE; } /** * Which return type should be selected by default. * * @return int */ public function default_returntype() { return FILE_REFERENCE; } /** * Is this repository accessing private data? * * @return bool */ public function contains_private_data() { return false; } /** * Repository method to make sure that user can access particular file. * * This is checked when user tries to pick the file from repository to deal with * potential parameter substitutions in request * * @param string $source * @return bool whether the file is accessible by current user */ public function file_is_accessible($source) { global $DB; $fileparams = json_decode(base64_decode($source)); $itemid = clean_param($fileparams->itemid, PARAM_INT); $contextid = clean_param($fileparams->contextid, PARAM_INT); $contentbankfile = $DB->get_record('contentbank_content', ['id' => $itemid]); $plugin = \core_plugin_manager::instance()->get_plugin_info($contentbankfile->contenttype); $managerclass = "\\$contentbankfile->contenttype\\content"; if ($plugin && $plugin->is_enabled() && class_exists($managerclass)) { $context = \context::instance_by_id($contextid); $browser = \repository_contentbank\helper::get_contentbank_browser($context); return $browser->can_access_content(); } return false; } /** * Return search results. * * @param string $search * @param int $page * @return array */ public function search($search, $page = 0) { $ret = []; $ret['nologin'] = true; $ret['list'] = \repository_contentbank\contentbank_search::get_search_contents($search); return $ret; } } PK*\% contentbank/version.phpnu[. /** * Version details * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @author Mihail Geshoski * @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_contentbank'; // Full name of the plugin (used for diagnostics). PK*\(contentbank/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for repository_contentbank. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\privacy; /** * Privacy Subsystem for repository_contentbank implementing null_provider. * * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK*\ Bcontentbank/classes/browser/contentbank_browser_context_course.phpnu[. /** * Utility class for browsing of content bank files in the course context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\browser; /** * Represents the content bank browser in the course context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contentbank_browser_context_course extends contentbank_browser { /** * Constructor. * * @param \context_course $context The current context */ public function __construct(\context_course $context) { $this->context = $context; } /** * Define the allowed child context levels. * * @return int[] The array containing the relevant child context levels */ protected function allowed_child_context_levels(): array { // The course context is the last relevant context level, therefore child context levels are not being returned. return []; } /** * The required condition to enable the user to view/access the content bank content in this context. * * @return bool Whether the user can view/access the content bank content in the context */ public function can_access_content(): bool { // When the following conditions are met, the user would be able to share the content created in the course // context level all over the site. // The content from the course context level should be available to: // * Every user which has capability to access the content of a given course. return has_capability('repository/contentbank:accesscoursecontent', $this->context); } } PK*\`8v* * Econtentbank/classes/browser/contentbank_browser_context_coursecat.phpnu[. /** * Utility class for browsing of content bank files in the course category context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\browser; /** * Represents the content bank browser in the course category context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contentbank_browser_context_coursecat extends contentbank_browser { /** * Constructor. * * @param \context_coursecat $context The current context */ public function __construct(\context_coursecat $context) { $this->context = $context; } /** * Define the allowed child context levels. * * @return int[] The array containing the relevant child context levels */ protected function allowed_child_context_levels(): array { // The expected child contexts in the course category context level are the course category context // (ex. subcategories) and the course context. return [\CONTEXT_COURSECAT, \CONTEXT_COURSE]; } /** * The required condition to enable the user to view/access the content bank content in this context. * * @return bool Whether the user can view/access the content bank content in the context */ public function can_access_content(): bool { // When the following conditions are met, the user would be able to share the content created in the course // category context level all over the site. // The content from the course category context level should be available to either: // * Every user which has a capability to access the 'general' content and has capability to access the // content of any child course of the given course category. // * Users that have capability to access content at a course category context level. if (has_capability('repository/contentbank:accesscoursecategorycontent', $this->context)) { return true; } $canaccesschildcontent = false; foreach ($this->get_child_contexts() as $childcontext) { $browser = \repository_contentbank\helper::get_contentbank_browser($childcontext); if ($canaccesschildcontent = $browser->can_access_content()) { break; } } return $canaccesschildcontent && has_capability('repository/contentbank:accessgeneralcontent', $this->context); } } PK*\P3contentbank/classes/browser/contentbank_browser.phpnu[. /** * Utility class for browsing of content bank files. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\browser; /** * Base class for the content bank browsers. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class contentbank_browser { /** @var \context The current context. */ protected $context; /** * Get all content nodes in the current context which can be viewed/accessed by the user. * * @return array[] The array containing all nodes which can be viewed/accessed by the user in the current context */ public function get_content(): array { return array_merge($this->get_context_folders(), $this->get_contentbank_content()); } /** * Generate the full navigation to the current node. * * @return array[] The array containing the path to each node in the navigation. * Each navigation node is an array with keys: name, path. */ public function get_navigation(): array { // Get the current navigation node. $currentnavigationnode = \repository_contentbank\helper::create_navigation_node($this->context); $navigationnodes = [$currentnavigationnode]; // Get the parent content bank browser. $parent = $this->get_parent(); // Prepend parent navigation node in the navigation nodes array until there is no existing parent. while ($parent !== null) { $parentnavigationnode = \repository_contentbank\helper::create_navigation_node($parent->context); array_unshift($navigationnodes, $parentnavigationnode); $parent = $parent->get_parent(); } return $navigationnodes; } /** * The required condition to enable the user to view/access the content bank content in this context. * * @return bool Whether the user can view/access the content bank content in the context */ abstract public function can_access_content(): bool; /** * Define the allowed child context levels. * * @return int[] The array containing the relevant child context levels */ abstract protected function allowed_child_context_levels(): array; /** * Get the relevant child contexts. * * @return \context[] The array containing the relevant, next-level children contexts */ protected function get_child_contexts(): array { global $DB; if (empty($allowedcontextlevels = $this->allowed_child_context_levels())) { // Early return if there aren't any defined child context levels. return []; } list($contextlevelsql, $params) = $DB->get_in_or_equal($allowedcontextlevels, SQL_PARAMS_NAMED); $pathsql = $DB->sql_like('path', ':path', false, false); $select = "contextlevel {$contextlevelsql} AND {$pathsql} AND depth = :depth"; $params['path'] = "{$this->context->path}/%"; $params['depth'] = $this->context->depth + 1; $childcontexts = $DB->get_records_select('context', $select, $params); return array_map(function($childcontext) { return \context::instance_by_id($childcontext->id); }, $childcontexts); } /** * Get the content bank browser class of the parent context. Currently used to generate the navigation path. * * @return contentbank_browser|null The content bank browser of the parent context */ private function get_parent(): ?self { if ($parentcontext = $this->context->get_parent_context()) { return \repository_contentbank\helper::get_contentbank_browser($parentcontext); } return null; } /** * Generate folder nodes for the relevant child contexts which can be accessed/viewed by the user. * * @return array[] The array containing the context folder nodes where each folder node is an array with keys: * title, datemodified, datecreated, path, thumbnail, children. */ private function get_context_folders(): array { // Get all relevant child contexts. $children = $this->get_child_contexts(); // Return all child context folder nodes which can be accessed by the user following the defined conditions // in can_access_content(). return array_reduce($children, function ($list, $child) { $browser = \repository_contentbank\helper::get_contentbank_browser($child); if ($browser->can_access_content()) { $name = $child->get_context_name(false); $path = base64_encode(json_encode(['contextid' => $child->id])); $list[] = \repository_contentbank\helper::create_context_folder_node($name, $path); } return $list; }, []); } /** * Generate nodes for the content bank content in the current context which can be accessed/viewed by the user. * * @return array[] The array containing the content nodes where each content node is an array with keys: * shorttitle, title, datemodified, datecreated, author, license, isref, source, icon, thumbnail. */ private function get_contentbank_content(): array { $cb = new \core_contentbank\contentbank(); // Get all content bank files in the current context. $contents = $cb->search_contents(null, $this->context->id); // Return all content bank content nodes from the current context which can be accessed by the user following // the defined conditions in can_access_content(). return array_reduce($contents, function($list, $content) { if ($this->can_access_content() && $contentnode = \repository_contentbank\helper::create_contentbank_content_node($content)) { $list[] = $contentnode; } return $list; }, []); } } PK*\^> Bcontentbank/classes/browser/contentbank_browser_context_system.phpnu[. /** * Utility class for browsing of content bank files in the system context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\browser; /** * Represents the content bank browser in the system context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contentbank_browser_context_system extends contentbank_browser { /** * Constructor. * * @param \context_system $context The current context */ public function __construct(\context_system $context) { $this->context = $context; } /** * Define the allowed child context levels. * * @return int[] The array containing the relevant child context levels */ protected function allowed_child_context_levels(): array { // The expected child context in the system context level is the course category context. return [\CONTEXT_COURSECAT]; } /** * The required condition to enable the user to view/access the content bank content in this context. * * @return bool Whether the user can view/access the content bank content in the context */ public function can_access_content(): bool { // When the following conditions are met, the user would be able to share the content created in the system // context level all over the site. // The content from the system context level should be available to: // * Every user that has a capability to access the 'general' content. return has_capability('repository/contentbank:accessgeneralcontent', $this->context); } } PK*\Dcontentbank/classes/helper.phpnu[. /** * Content bank files repository helpers. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank; use repository_contentbank\browser\contentbank_browser; /** * Helper class for content bank files repository. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { /** * Get the content bank repository browser for a certain context. * * @param \context $context The context * @return \repository_contentbank\browser\contentbank_browser|null The content bank repository browser */ public static function get_contentbank_browser(\context $context): ?contentbank_browser { switch ($context->contextlevel) { case CONTEXT_SYSTEM: return new \repository_contentbank\browser\contentbank_browser_context_system($context); case CONTEXT_COURSECAT: return new \repository_contentbank\browser\contentbank_browser_context_coursecat($context); case CONTEXT_COURSE: return new \repository_contentbank\browser\contentbank_browser_context_course($context); } return null; } /** * Create the context folder node. * * @param string $name The name of the context folder node * @param string $path The path to the context folder node * @return array The context folder node */ public static function create_context_folder_node(string $name, string $path): array { global $OUTPUT; return [ 'title' => $name, 'datemodified' => '', 'datecreated' => '', 'path' => $path, 'thumbnail' => $OUTPUT->image_url(file_folder_icon())->out(false), 'children' => [] ]; } /** * Create the content bank content node. * * @param \core_contentbank\content $content The content bank content * @return array|null The content bank content node */ public static function create_contentbank_content_node(\core_contentbank\content $content): ?array { global $OUTPUT; // Only content files are currently supported, but should be able to create content folder nodes in the future. // Early return if the content is not a stored file. if (!$file = $content->get_file()) { return null; } $params = [ 'contextid' => $file->get_contextid(), 'component' => $file->get_component(), 'filearea' => $file->get_filearea(), 'itemid' => $file->get_itemid(), 'filepath' => $file->get_filepath(), 'filename' => $file->get_filename() ]; $contenttype = $content->get_content_type_instance(); $encodedpath = base64_encode(json_encode($params)); $node = [ 'shorttitle' => $content->get_name(), 'title' => $file->get_filename(), 'datemodified' => $file->get_timemodified(), 'datecreated' => $file->get_timecreated(), 'author' => $file->get_author(), 'license' => $file->get_license(), 'isref' => $file->is_external_file(), 'size' => $file->get_filesize(), 'source' => $encodedpath, 'icon' => $contenttype->get_icon($content), 'thumbnail' => $contenttype->get_icon($content) ]; if ($file->get_status() == 666) { $node['originalmissing'] = true; } return $node; } /** * Generate a navigation node. * * @param \context $context The context * @return array The navigation node */ public static function create_navigation_node(\context $context): array { return [ 'path' => base64_encode(json_encode(['contextid' => $context->id])), 'name' => $context->get_context_name(false) ]; } } PK*\oQ؃ *contentbank/classes/contentbank_search.phpnu[. /** * Utility class for searching of content bank files. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank; /** * Represents the content bank search related functionality. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contentbank_search { /** * Generate and return content nodes for all content bank files that match the search criteria * and can be viewed/accessed by the user. * * @param string $search The search string * @return array[] The array containing all content file nodes that match the search criteria. Each content node is * an array with keys: shorttitle, title, datemodified, datecreated, author, license, isref, source, * icon, thumbnail. */ public static function get_search_contents(string $search): array { $contentbank = new \core_contentbank\contentbank(); // Return all content bank content that matches the search criteria and can be viewed/accessed by the user. $contents = $contentbank->search_contents($search); return array_reduce($contents, function($list, $content) { $contentcontext = \context::instance_by_id($content->get_content()->contextid); $browser = \repository_contentbank\helper::get_contentbank_browser($contentcontext); // If the user can access the content and content node can be created, add the node into the // search results list. if ($browser->can_access_content() && $contentnode = \repository_contentbank\helper::create_contentbank_content_node($content)) { $list[] = $contentnode; } return $list; }, []); } } PK*\z'#contentbank/tests/generator/lib.phpnu[. /** * Content bank files repository data generator * * @package repository_contentbank * @category test * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Content bank files repository data generator class * * @package repository_contentbank * @category test * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_contentbank_generator extends testing_repository_generator { } PK*\ ee"contentbank/tests/browser_test.phpnu[. namespace repository_contentbank; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("$CFG->dirroot/repository/lib.php"); /** * Tests for the content bank browser class. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class browser_test extends \advanced_testcase { /** * Test get_content() in the system context with users that have capability to access/view content bank content * within the system context. By default, every authenticated user should be able to access/view the content in * the system context. */ public function test_get_content_system_context_user_has_capabilities(): void { global $DB, $CFG; $this->resetAfterTest(true); $systemcontext = \context_system::instance(); // Create a course category $coursecategory. $coursecategory = $this->getDataGenerator()->create_category(['name' => 'Category']); $coursecatcontext = \context_coursecat::instance($coursecategory->id); // Get the default course category. $defaultcat = \core_course_category::get(1); $defaultcatcontext = \context_coursecat::instance($defaultcat->id); // Create course. $course = $this->getDataGenerator()->create_course(['category' => $coursecategory->id]); $admin = get_admin(); // Create a user (not enrolled in a course). $user = $this->getDataGenerator()->create_user(); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add some content bank files in the system context. $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p'; $contentbankcontents = $generator->generate_contentbank_data('contenttype_h5p', 3, $admin->id, $systemcontext, true, $filepath); // Log in as admin. $this->setUser($admin); // Get the content bank nodes displayed to the admin in the system context. $browser = new \repository_contentbank\browser\contentbank_browser_context_system($systemcontext); $repositorycontentnodes = $browser->get_content(); // All content nodes should be available to the admin user. // There should be a total of 5 nodes, 3 file nodes representing the existing content bank files in the // system context and 2 folder nodes representing the default course category and 'Category'. $this->assertCount(5, $repositorycontentnodes); $contextfolders = [ [ 'name' => get_string('defaultcategoryname'), 'contextid' => $defaultcatcontext->id ], [ 'name' => 'Category', 'contextid' => $coursecatcontext->id ] ]; $expected = $this->generate_expected_content($contextfolders, $contentbankcontents); $this->assertEqualsCanonicalizing($expected, $repositorycontentnodes); // Log in as a user. $this->setUser($user); // Get the content bank nodes displayed to an authenticated user in the system context. $browser = new \repository_contentbank\browser\contentbank_browser_context_system($systemcontext); $repositorycontentnodes = $browser->get_content(); // There should be 3 nodes representing the existing content bank files in the system context. // The course category context folder node should be ignored as the user does not have an access to // the content of the category's courses. $this->assertCount(3, $repositorycontentnodes); $expected = $this->generate_expected_content([], $contentbankcontents); $this->assertEqualsCanonicalizing($expected, $repositorycontentnodes); // Enrol the user as an editing teacher in the course. $editingteacherrole = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']); $this->getDataGenerator()->enrol_user($user->id, $course->id, $editingteacherrole); // Get the content bank nodes displayed to the editing teacher in the system context. $browser = new \repository_contentbank\browser\contentbank_browser_context_system($systemcontext); $repositorycontentnodes = $browser->get_content(); // All content nodes should now be available to the editing teacher. // There should be a total of 4 nodes, 3 file nodes representing the existing content bank files in the // system context and 1 folder node representing the course category 'Category' (The editing teacher is now // enrolled in a course from the category). $this->assertCount(4, $repositorycontentnodes); $contextfolders = [ [ 'name' => 'Category', 'contextid' => $coursecatcontext->id ] ]; $expected = $this->generate_expected_content($contextfolders, $contentbankcontents); $this->assertEqualsCanonicalizing($expected, $repositorycontentnodes); } /** * Test get_content() in the system context with users that do not have a capability to access/view content bank * content within the system context. By default, every non-authenticated user should not be able to access/view * the content in the system context. */ public function test_get_content_system_context_user_missing_capabilities(): void { $this->resetAfterTest(true); $systemcontext = \context_system::instance(); $admin = get_admin(); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add some content bank files in the system context. $generator->generate_contentbank_data('contenttype_h5p', 3, $admin->id, $systemcontext, true); // Log out. $this->setUser(); // Get the content bank nodes displayed to a non-authenticated user in the system context. $browser = new \repository_contentbank\browser\contentbank_browser_context_system($systemcontext); $repositorycontents = $browser->get_content(); // Content nodes should not be available to the non-authenticated user in the system context. $this->assertCount(0, $repositorycontents); } /** * Test get_content() in the course category context with users that have capability to access/view content * bank content within the course category context. By default, every authenticated user that has access to * any category course should be able to access/view the content in the course category context. */ public function test_get_content_course_category_context_user_has_capabilities(): void { global $CFG; $this->resetAfterTest(true); // Create a course category. $category = $this->getDataGenerator()->create_category(['name' => 'Category']); $coursecatcontext = \context_coursecat::instance($category->id); // Create course1. $course1 = $this->getDataGenerator()->create_course(['fullname' => 'Course1', 'category' => $category->id]); $course1context = \context_course::instance($course1->id); // Create course2. $course2 = $this->getDataGenerator()->create_course(['fullname' => 'Course2', 'category' => $category->id]); $course2context = \context_course::instance($course2->id); $admin = get_admin(); // Create editing teacher enrolled in course1. $editingteacher = $this->getDataGenerator()->create_and_enrol($course1, 'editingteacher'); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add some content bank files in the course category context. $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p'; $contentbankcontents = $generator->generate_contentbank_data('contenttype_h5p', 3, $admin->id, $coursecatcontext, true, $filepath); $this->setUser($admin); // Get the content bank nodes displayed to the admin in the course category context. $browser = new \repository_contentbank\browser\contentbank_browser_context_coursecat($coursecatcontext); $repositorycontents = $browser->get_content(); // All content nodes should be available to the admin user. // There should be a total of 5 nodes, 3 file nodes representing the existing content bank files in the // course category context and 2 folder nodes representing the courses 'Course1' and 'Course2'. $this->assertCount(5, $repositorycontents); $contextfolders = [ [ 'name' => 'Course1', 'contextid' => $course1context->id ], [ 'name' => 'Course2', 'contextid' => $course2context->id ] ]; $expected = $this->generate_expected_content($contextfolders, $contentbankcontents); $this->assertEqualsCanonicalizing($expected, $repositorycontents); // Log in as an editing teacher enrolled in a child course. $this->setUser($editingteacher); // Get the content bank nodes displayed to the editing teacher in the course category context. $browser = new \repository_contentbank\browser\contentbank_browser_context_coursecat($coursecatcontext); $repositorycontents = $browser->get_content(); // There should be a total of 4 nodes, 3 file nodes representing the existing content bank files in the // course category context and 1 folder node representing the course 'Course1' (The editing teacher is only // enrolled in course1). $this->assertCount(4, $repositorycontents); $contextfolders = [ [ 'name' => 'Course1', 'contextid' => $course1context->id ] ]; $expected = $this->generate_expected_content($contextfolders, $contentbankcontents); $this->assertEqualsCanonicalizing($expected, $repositorycontents); } /** * Test get_content() in the course category context with users that do not have capability to access/view content * bank content within the course category context. By default, every non-authenticated user or authenticated users * that cannot access/view course content from the course category should not be able to access/view the * content in the course category context. */ public function test_get_content_course_category_context_user_missing_capabilities(): void { $this->resetAfterTest(true); // Create a course category 'Category'. $category = $this->getDataGenerator()->create_category(['name' => 'Category']); // Create course1 in 'Category'. $course1 = $this->getDataGenerator()->create_course(['fullname' => 'Course1', 'category' => $category->id]); // Create course2 in default category by default. $course2 = $this->getDataGenerator()->create_course(['fullname' => 'Course2']); // Create a teacher enrolled in course1. $teacher = $this->getDataGenerator()->create_and_enrol($course1, 'teacher'); // Create an editing teacher enrolled in course2. $editingteacher = $this->getDataGenerator()->create_and_enrol($course2, 'editingteacher'); $admin = get_admin(); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add some content bank files in the 'Category' context. $coursecatcontext = \context_coursecat::instance($category->id); $generator->generate_contentbank_data('contenttype_h5p', 3, $admin->id, $coursecatcontext, true); // Log in as a non-editing teacher. $this->setUser($teacher); // Get the content bank nodes displayed to a non-editing teacher in the 'Category' context. $browser = new \repository_contentbank\browser\contentbank_browser_context_coursecat($coursecatcontext); $repositorycontents = $browser->get_content(); // Content nodes should not be available to a non-editing teacher in the 'Category' context. $this->assertCount(0, $repositorycontents); // Log in as an editing teacher. $this->setUser($editingteacher); // Get the content bank nodes displayed to a an editing teacher in the 'Category' context. $browser = new \repository_contentbank\browser\contentbank_browser_context_coursecat($coursecatcontext); $repositorycontents = $browser->get_content(); // Content nodes should not be available to an editing teacher in the 'Category' context. $this->assertCount(0, $repositorycontents); // Log out. $this->setUser(); // Get the content bank nodes displayed to a non-authenticated user in the course category context. $browser = new \repository_contentbank\browser\contentbank_browser_context_coursecat($coursecatcontext); $repositorycontents = $browser->get_content(); // Content nodes should not be available to the non-authenticated user in the course category context. $this->assertCount(0, $repositorycontents); } /** * Test get_content() in the course context with users that have capability to access/view content * bank content within the course context. By default, admin, managers, course creators, editing teachers enrolled * in the course should be able to access/view the content. */ public function test_get_content_course_context_user_has_capabilities(): void { global $CFG; $this->resetAfterTest(true); // Create course1. $course = $this->getDataGenerator()->create_course(['fullname' => 'Course']); $coursecontext = \context_course::instance($course->id); $admin = get_admin(); // Create editing teacher enrolled in course. $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add some content bank files in the course context. $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p'; $contentbankcontents = $generator->generate_contentbank_data('contenttype_h5p', 3, $admin->id, $coursecontext, true, $filepath); $this->setUser($admin); // Get the content bank nodes displayed to the admin in the course context. $browser = new \repository_contentbank\browser\contentbank_browser_context_course($coursecontext); $repositorycontents = $browser->get_content(); // All content nodes should be available to the admin user. // There should be 3 file nodes representing the existing content bank files in the // course context. $this->assertCount(3, $repositorycontents); $expected = $this->generate_expected_content([], $contentbankcontents); $this->assertEqualsCanonicalizing($expected, $repositorycontents); // Log in as an editing teacher. $this->setUser($editingteacher); // All content nodes should also be available to the editing teacher. // Get the content bank nodes displayed to the editing teacher in the course context. $browser = new \repository_contentbank\browser\contentbank_browser_context_course($coursecontext); $repositorycontents = $browser->get_content(); // There should be 3 file nodes representing the existing content bank files in the // course context. $this->assertCount(3, $repositorycontents); $expected = $this->generate_expected_content([], $contentbankcontents); $this->assertEqualsCanonicalizing($expected, $repositorycontents); } /** * Test get_content() in the course context with users that do not have capability to access/view content * bank content within the course context. By default, every user which is not an admin, manager, course creator, * editing teacher enrolled in the course should not be able to access/view the content. */ public function test_get_content_course_context_user_missing_capabilities(): void { $this->resetAfterTest(true); // Create course1. $course1 = $this->getDataGenerator()->create_course(['fullname' => 'Course1']); $course1context = \context_course::instance($course1->id); // Create course2. $course2 = $this->getDataGenerator()->create_course(['fullname' => 'Course2']); $course2context = \context_course::instance($course2->id); $admin = get_admin(); // Create non-editing teacher enrolled in course1. $teacher = $this->getDataGenerator()->create_and_enrol($course1, 'teacher'); // Create editing teacher enrolled in course1. $editingteacher = $this->getDataGenerator()->create_and_enrol($course1, 'editingteacher'); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add some content bank files in the course1 context. $generator->generate_contentbank_data('contenttype_h5p', 2, $admin->id, $course1context, true); // Add some content bank files in the course2 context. $generator->generate_contentbank_data('contenttype_h5p', 3, $admin->id, $course2context, true); // Log in as a non-editing teacher. $this->setUser($teacher); // Get the content bank nodes displayed to the non-editing teacher in the course1 context. $browser = new \repository_contentbank\browser\contentbank_browser_context_course($course1context); $repositorycontents = $browser->get_content(); // Content nodes should not be available to the teacher in the course1 context. $this->assertCount(0, $repositorycontents); // Log in as editing teacher. $this->setUser($editingteacher); // Get the content bank nodes displayed to the editing teacher in the course2 context. $browser = new \repository_contentbank\browser\contentbank_browser_context_course($course2context); $repositorycontents = $browser->get_content(); // Content nodes should not be available to the teacher in the course2 context. The editing teacher is not // enrolled in this course. $this->assertCount(0, $repositorycontents); } /** * Test get_navigation() in the system context. */ public function test_get_navigation_system_context(): void { $this->resetAfterTest(true); $systemcontext = \context_system::instance(); $browser = new \repository_contentbank\browser\contentbank_browser_context_system($systemcontext); $navigation = $browser->get_navigation(); // The navigation array should contain only 1 element, representing the system navigation node. $this->assertCount(1, $navigation); $expected = [ \repository_contentbank\helper::create_navigation_node($systemcontext) ]; $this->assertEquals($expected, $navigation); } /** * Test get_navigation() in the course category context. */ public function test_get_navigation_course_category_context(): void { $this->resetAfterTest(true); $systemcontext = \context_system::instance(); // Create a course category. $category = $this->getDataGenerator()->create_category(['name' => 'category']); $categorycontext = \context_coursecat::instance($category->id); // Create a course subcategory. $subcategory = $this->getDataGenerator()->create_category(['name' => 'subcategory', 'parent' => $category->id]); $subcategorytcontext = \context_coursecat::instance($subcategory->id); // Get navigation nodes in the category context. $browser = new \repository_contentbank\browser\contentbank_browser_context_coursecat($categorycontext); $navigation = $browser->get_navigation(); // The navigation array should contain 2 elements, representing the system and course category // navigation nodes. $this->assertCount(2, $navigation); $expected = [ \repository_contentbank\helper::create_navigation_node($systemcontext), \repository_contentbank\helper::create_navigation_node($categorycontext) ]; $this->assertEquals($expected, $navigation); // Get navigation nodes in the subcategory context. $browser = new \repository_contentbank\browser\contentbank_browser_context_coursecat($subcategorytcontext); $navigation = $browser->get_navigation(); // The navigation array should contain 3 elements, representing the system, category and subcategory // navigation nodes. $this->assertCount(3, $navigation); $expected = [ \repository_contentbank\helper::create_navigation_node($systemcontext), \repository_contentbank\helper::create_navigation_node($categorycontext), \repository_contentbank\helper::create_navigation_node($subcategorytcontext) ]; $this->assertEquals($expected, $navigation); } /** * Test get_navigation() in the course context. */ public function test_get_navigation_course_context(): void { $this->resetAfterTest(true); $systemcontext = \context_system::instance(); // Create a category. $category = $this->getDataGenerator()->create_category(['name' => 'category']); $categorycontext = \context_coursecat::instance($category->id); // Create a subcategory. $subcategory = $this->getDataGenerator()->create_category(['name' => 'category', 'parent' => $category->id]); $subcategorycontext = \context_coursecat::instance($subcategory->id); // Create a course in category. $categorycourse = $this->getDataGenerator()->create_course(['category' => $category->id]); $categorycoursecontext = \context_course::instance($categorycourse->id); // Create a course in subcategory. $subcategorycourse = $this->getDataGenerator()->create_course(['category' => $subcategory->id]); $subcategorycoursecontext = \context_course::instance($subcategorycourse->id); // Get navigation nodes in the category course context. $browser = new \repository_contentbank\browser\contentbank_browser_context_course($categorycoursecontext); $navigation = $browser->get_navigation(); // The navigation array should contain 3 elements, representing the system, category and course // navigation nodes. $this->assertCount(3, $navigation); $expected = [ \repository_contentbank\helper::create_navigation_node($systemcontext), \repository_contentbank\helper::create_navigation_node($categorycontext), \repository_contentbank\helper::create_navigation_node($categorycoursecontext) ]; $this->assertEquals($expected, $navigation); // Get navigation nodes in the subcategory course context. $browser = new \repository_contentbank\browser\contentbank_browser_context_course($subcategorycoursecontext); $navigation = $browser->get_navigation(); // The navigation array should contain 4 elements, representing the system, category, subcategory and // subcategory course navigation nodes. $this->assertCount(4, $navigation); $expected = [ \repository_contentbank\helper::create_navigation_node($systemcontext), \repository_contentbank\helper::create_navigation_node($categorycontext), \repository_contentbank\helper::create_navigation_node($subcategorycontext), \repository_contentbank\helper::create_navigation_node($subcategorycoursecontext) ]; $this->assertEquals($expected, $navigation); } /** * Generate the expected array of content bank nodes. * * @param array $contextfolders The array containing the expected folder nodes * @param array $contentbankcontents The array containing the expected contents * @return array[] The expected array of content bank nodes */ private function generate_expected_content(array $contextfolders = [], array $contentbankcontents = []): array { $expected = []; if (!empty($contextfolders)) { foreach ($contextfolders as $contextfolder) { $expected[] = \repository_contentbank\helper::create_context_folder_node($contextfolder['name'], base64_encode(json_encode(['contextid' => $contextfolder['contextid']]))); } } if (!empty($contentbankcontents)) { foreach ($contentbankcontents as $content) { $expected[] = \repository_contentbank\helper::create_contentbank_content_node($content); } } return $expected; } } PK*\"{n+n+(contentbank/tests/search/search_test.phpnu[. /** * Content bank repository search unit tests. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\search; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("$CFG->dirroot/repository/lib.php"); /** * Tests for the content bank search class. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class search_test extends \advanced_testcase { /** * Test get_search_contents() by searching through some existing content using different search terms. * * @dataProvider get_search_contents_provider * @param array $contentnames The array containing the names of the content that needs to be generated * @param string $search The search string * @param array $expected The array containing the expected content names that should be returned by the search */ public function test_get_search_contents(array $contentnames, string $search, array $expected): void { $this->resetAfterTest(); $admin = get_admin(); $systemcontext = \context_system::instance(); // Add some content to the content bank. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); foreach ($contentnames as $contentname) { $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, $systemcontext, true, 'file.h5p', $contentname); } // Log in as admin. $this->setUser($admin); // Search for content bank content items which have the search pattern within the name. $searchcontentnodes = \repository_contentbank\contentbank_search::get_search_contents($search); // Get the content name of the each content returned after performing the search. $actual = array_map(function($searchcontentnode) { return $searchcontentnode['shorttitle']; }, $searchcontentnodes); $this->assertEqualsCanonicalizing($expected, $actual); } /** * Data provider for test_get_search_contents(). * * @return array */ public static function get_search_contents_provider(): array { return [ 'Search for existing pattern found within the name of content items' => [ [ 'systemcontentfile1', 'systemcontentfile2', 'somesystemfile' ], 'content', [ 'systemcontentfile1', 'systemcontentfile2' ] ], 'Search for existing pattern found at the beginning of the name of content items' => [ [ 'systemcontentfile1', 'systemcontentfile2', 'somesystemfile' ], 'some', [ 'somesystemfile', ] ], 'Search for existing pattern found at the end of the name of content items' => [ [ 'systemcontentfile1', 'systemcontentfile2', 'somesystemfile' ], 'file2', [ 'systemcontentfile2', ] ], 'Search for a pattern which does not exist within the name of any content item' => [ [ 'systemcontentfile1', 'somesystemfile' ], 'somefile', [] ], 'Case-insensitive search for a pattern which exists within the name of content items' => [ [ 'systemcontentfile1', 'systemcontentfile2', 'somesystemfile' ], 'CONTENT', [ 'systemcontentfile1', 'systemcontentfile2' ] ] ]; } /** * Test get_search_contents() by searching for content with users that have capability to access/view * all existing content bank content. By default, admins, managers should be able to view every existing content * that matches the search criteria. */ public function test_get_search_contents_user_can_access_all_content(): void { $this->resetAfterTest(true); // Create a course in default category by default. $course = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course->id); // Create a course category without a course. $category = $this->getDataGenerator()->create_category(); $categorycontext = \context_coursecat::instance($category->id); $admin = get_admin(); // Add some content to the content bank in different contexts. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add a content bank file in the category context. $categorycontents = $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, $categorycontext, true, 'file.h5p', 'categorycontentfile'); $categorycontent = reset($categorycontents); // Add a content bank file in the course context. $coursecontents = $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, $coursecontext, true, 'file.h5p', 'coursecontentfile'); $coursecontent = reset($coursecontents); // Log in as admin. $this->setUser($admin); // Search for content bank content items which have the pattern 'contentfile' within the name. $search = 'contentfile'; $searchcontentnodes = \repository_contentbank\contentbank_search::get_search_contents($search); // All content files which name matches the search criteria should be available to the admin user. // The search should return 2 file nodes. $this->assertCount(2, $searchcontentnodes); $expected = [ \repository_contentbank\helper::create_contentbank_content_node($categorycontent), \repository_contentbank\helper::create_contentbank_content_node($coursecontent), ]; $this->assertEqualsCanonicalizing($expected, $searchcontentnodes); } /** * Test get_search_contents() by searching for content with users that have capability to access/view only * certain existing content bank content. By default, editing teachers should be able to view content that matches * the search criteria AND is in the courses they are enrolled, course categories of the enrolled courses * and system content. Other authenticated users should be able to access only the system content. */ public function test_get_search_contents_user_can_access_certain_content(): void { global $CFG; $this->resetAfterTest(true); $systemcontext = \context_system::instance(); // Create course1. $course1 = $this->getDataGenerator()->create_course(); $course1context = \context_course::instance($course1->id); // Create course2. $course2 = $this->getDataGenerator()->create_course(); $course2context = \context_course::instance($course2->id); $admin = get_admin(); // Create and enrol an editing teacher in course1. $editingteacher = $this->getDataGenerator()->create_and_enrol($course1, 'editingteacher'); // Create and enrol a teacher in course2. $teacher = $this->getDataGenerator()->create_and_enrol($course2, 'teacher'); // Add some content to the content bank in different contexts. $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Add a content bank file in the system context. $filepath = $CFG->dirroot . '/h5p/tests/fixtures/ipsums.h5p'; $systemcontents = $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, $systemcontext, true, $filepath, 'systemcontentfile'); $systemcontent = reset($systemcontents); // Add a content bank file in the course1 context. $course1contents = $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, $course1context, true, $filepath, 'coursecontentfile1'); $course1content = reset($course1contents); // Add a content bank file in the course2 context. $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, $course2context, true, $filepath, 'coursecontentfile2'); // Log in as an editing teacher. $this->setUser($editingteacher); // Search for content bank content items which have the pattern 'contentfile' within the name. $search = 'contentfile'; $searchcontentnodes = \repository_contentbank\contentbank_search::get_search_contents($search); // The search should return 2 file nodes. The editing teacher does not have access to the content of course2 // and therefore, the course2 content should be skipped. $this->assertCount(2, $searchcontentnodes); $expected = [ \repository_contentbank\helper::create_contentbank_content_node($systemcontent), \repository_contentbank\helper::create_contentbank_content_node($course1content), ]; $this->assertEqualsCanonicalizing($expected, $searchcontentnodes); // Log in as a teacher. $this->setUser($teacher); // Search for content bank content items which have the pattern 'contentfile' within the name. $search = 'contentfile'; $searchcontentnodes = \repository_contentbank\contentbank_search::get_search_contents($search); // The search should return 1 file node. The teacher should only be able to view system content. $this->assertCount(1, $searchcontentnodes); $expected = [ \repository_contentbank\helper::create_contentbank_content_node($systemcontent), ]; $this->assertEqualsCanonicalizing($expected, $searchcontentnodes); } } PK*\*O<<.contentbank/tests/behat/search_content.featurenu[@repository @repository_contentbank @javascript Feature: Search content bank files using the content bank files repository In order to find the content I need to select in the file picker As a user I need to be able to search in the content bank files repository by content name Background: Given the following "users" exist: | username | firstname | lastname | email | | student | Student | 1 | student@example.com | | teacher | Teacher | 1 | teacher1@example.com | And the following "categories" exist: | name | category | idnumber | | Category1 | 0 | CAT1 | | Category2 | 0 | CAT2 | And the following "courses" exist: | fullname | shortname | category | | Course1 | C1 | CAT1 | | Course2 | C2 | CAT2 | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | coursecontent1.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | Course | C2 | contenttype_h5p | admin | coursecontent2.h5p | /h5p/tests/fixtures/find-the-words.h5p | | Category | CAT1 | contenttype_h5p | admin | categorycontent1.h5p | /h5p/tests/fixtures/ipsums.h5p | | Category | CAT2 | contenttype_h5p | admin | categorycontent2.h5p | /h5p/tests/fixtures/multiple-choice-2-6.h5p | | System | | contenttype_h5p | admin | systemcontent.h5p | /h5p/tests/fixtures/greeting-card.h5p | And the following "activities" exist: | activity | name | intro | introformat | course | idnumber | | folder | Folder | FolderDesc | 1 | C1 | folder | And the following "course enrolments" exist: | user | course | role | | teacher | C1 | editingteacher | Scenario: User can see a search field and reset search button in the content bank files repository Given I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" When I select "Content bank" repository in file picker Then "Search repository" "field" should be visible And "Refresh" "link" should be visible Scenario: User can see search results when there is content that matches the search criteria Given I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" And I select "Content bank" repository in file picker And I set the field "Search repository" to "content" When I press enter Then I should see "5" elements in repository content area And I should see "systemcontent.h5p" "file" in repository content area And I should see "categorycontent1.h5p" "file" in repository content area And I should see "categorycontent2.h5p" "file" in repository content area And I should see "coursecontent1.h5p" "file" in repository content area And I should see "coursecontent2.h5p" "file" in repository content area Scenario: User can see search results when there is content that matches the search criteria ignoring case sensitivity Given I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" And I select "Content bank" repository in file picker And I set the field "Search repository" to "COURSE" When I press enter Then I should see "2" elements in repository content area And I should see "coursecontent1.h5p" "file" in repository content area And I should see "coursecontent2.h5p" "file" in repository content area Scenario: User can not see any search results when there is not a content that matches the search criteria Given I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" And I select "Content bank" repository in file picker And I set the field "Search repository" to "somecontent" When I press enter Then I should see "0" elements in repository content area And I should see "No files available" in the ".filepicker .fp-content" "css_element" Scenario: User can reset search criteria and see all content displayed prior the search action Given I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" And I select "Content bank" repository in file picker And I should see "1" elements in repository content area And I should see "coursecontent1.h5p" "file" in repository content area And I set the field "Search repository" to "category" And I press enter And I should see "2" elements in repository content area And I should see "categorycontent1.h5p" "file" in repository content area And I should see "categorycontent2.h5p" "file" in repository content area When I click on "Refresh" "link" Then I should see "1" elements in repository content area And I should see "coursecontent1.h5p" "file" in repository content area Scenario: Editing teacher can see search results when the content is available to him and matches the search criteria Given I am on the Folder "Folder activity" page logged in as teacher And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" And I select "Content bank" repository in file picker And I set the field "Search repository" to "content" When I press enter Then I should see "3" elements in repository content area And I should see "coursecontent1.h5p" "file" in repository content area And I should see "categorycontent1.h5p" "file" in repository content area And I should see "systemcontent.h5p" "file" in repository content area PK*\"n+contentbank/tests/behat/file_update.featurenu[@repository @repository_contentbank @javascript @core_h5p Feature: Updating a file in the content bank after using in a course In order to use file alias As a user Updated files must update references when is an alias Background: Given the following "categories" exist: | name | category | idnumber | | Category1 | 0 | CAT1 | And the following "courses" exist: | fullname | shortname | category | | Course1 | C1 | CAT1 | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | package.h5p | /h5p/tests/fixtures/guess-the-answer.h5p | Scenario: Referenced files is the default option and updates alias as well When I log in as "admin" And I add an h5pactivity activity to course "Course1" section "1" And I set the following fields to these values: | Name | guessFile | And I click on "Add..." "link" in the ".fp-toolbar" "css_element" And I select "Content bank" repository in file picker And I click on "package.h5p" "file" in repository content area And I click on "Select this file" "button" And I click on "Save and display" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "Press here to reveal answer" And I switch to the main frame # Now edit the content in the content bank. When I am on "Course1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I expand "Site pages" node And I click on "Content bank" "link" And I click on "package.h5p" "link" And I click on "Edit" "link" And I wait until the page is ready And I switch to "h5p-editor-iframe" class iframe And I set the field "Title" to "Required title" And I set the field "Descriptive solution label" to "This is a new text" And I switch to the main frame And I click on "Save" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "This is a new text" And I switch to the main frame # Check the course page is updated. When I am on "Course1" course homepage with editing mode on And I click on "guessFile" "link" in the "page-content" "region" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "This is a new text" And I switch to the main frame Scenario: Copied files should not be updated if the original is edited When I log in as "admin" And I add an h5pactivity activity to course "Course1" section "1" And I set the following fields to these values: | Name | guessFile | And I click on "Add..." "link" in the ".fp-toolbar" "css_element" And I select "Content bank" repository in file picker And I click on "package.h5p" "file" in repository content area And I click on "Make a copy of the file" "radio" And I click on "Select this file" "button" And I click on "Save and display" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "Press here to reveal answer" And I switch to the main frame # Now edit the content in the content bank. When I am on "Course1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I expand "Site pages" node And I click on "Content bank" "link" And I click on "package.h5p" "link" And I click on "Edit" "link" And I wait until the page is ready And I switch to "h5p-editor-iframe" class iframe And I set the field "Title" to "Required title" And I set the field "Descriptive solution label" to "This is a new text" And I switch to the main frame And I click on "Save" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "This is a new text" And I switch to the main frame # Check the course page is not updated. When I am on "Course1" course homepage with editing mode on And I click on "guessFile" "link" in the "page-content" "region" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "Press here to reveal answer" And I switch to the main frame PK*\׆N''.contentbank/tests/behat/select_content.featurenu[@repository @repository_contentbank @javascript Feature: Select content bank files using the content bank files repository In order to re-use content bank files As a user I need to be able to view and select content bank files using the content bank repository Background: Given the following "users" exist: | username | firstname | lastname | email | | student | Student | 1 | student@example.com | | teacher1 | Teacher 1 | 1 | teacher1@example.com | | teacher2 | Teacher 2 | 1 | teacher2@example.com | And the following "categories" exist: | name | category | idnumber | | Category1 | 0 | CAT1 | | SubCategory1 | CAT1 | SUBCAT1 | And the following "courses" exist: | fullname | shortname | category | | MiscellaneousCourse1 | mscC1 | 0 | | MiscellaneousCourse2 | mscC2 | 0 | | Category1Course1 | cat1C1 | CAT1 | | SubCategory1Course1 | subcat1C1 | SUBCAT1 | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | mscC1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | | Course | mscC2 | contenttype_h5p | admin | find-the-words.h5p | /h5p/tests/fixtures/find-the-words.h5p | | Course | subcat1C1 | contenttype_h5p | admin | greeting-card.h5p | /h5p/tests/fixtures/greeting-card.h5p | | Category | CAT1 | contenttype_h5p | admin | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p | | Category | SUBCAT1 | contenttype_h5p | admin | multiple-choice-2-6.h5p | /h5p/tests/fixtures/multiple-choice-2-6.h5p | | System | | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | And the following "activities" exist: | activity | name | intro | introformat | course | idnumber | | forum | Forum | | 1 | mscC1 | forum1 | | folder | Folder | FolderDesc | 1 | mscC1 | folder1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | mscC1 | editingteacher | | teacher2 | mscC1 | teacher | | student | mscC1 | student | Scenario: Admin can navigate and see all existing content bank files using the content bank repository Given I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" When I select "Content bank" repository in file picker Then I should see "System > Category 1 > MiscellaneousCourse1" breadcrumb navigation in repository And I should see "1" elements in repository content area And I should see "filltheblanks.h5p" "file" in repository content area And I click on "Category 1" "link" in the ".file-picker .fp-pathbar" "css_element" And I should see "System > Category 1" breadcrumb navigation in repository And I should see "2" elements in repository content area And I should see "MiscellaneousCourse1" "folder" in repository content area And I should see "MiscellaneousCourse2" "folder" in repository content area And I click on "MiscellaneousCourse2" "folder" in repository content area And I should see "System > Category 1 > MiscellaneousCourse2" breadcrumb navigation in repository And I should see "1" elements in repository content area And I should see "find-the-words.h5p" "file" in repository content area And I click on "System" "link" in the ".file-picker .fp-pathbar" "css_element" And I should see "System" breadcrumb navigation in repository And I should see "3" elements in repository content area And I should see "filltheblanks.h5p" "file" in repository content area And I should see "Category 1" "folder" in repository content area And I should see "Category1" "folder" in repository content area And I click on "Category1" "folder" in repository content area And I should see "System > Category1" breadcrumb navigation in repository And I should see "3" elements in repository content area And I should see "SubCategory1" "folder" in repository content area And I should see "Category1Course1" "folder" in repository content area And I should see "ipsums.h5p" "file" in repository content area And I click on "SubCategory1" "folder" in repository content area And I should see "System > Category1 > SubCategory1" breadcrumb navigation in repository And I should see "2" elements in repository content area And I should see "SubCategory1Course1" "folder" in repository content area And I should see "multiple-choice-2-6.h5p" "file" in repository content area And I click on "SubCategory1Course1" "folder" in repository content area And I should see "System > Category1 > SubCategory1 > SubCategory1Course1" breadcrumb navigation in repository And I should see "1" elements in repository content area And I should see "greeting-card.h5p" "file" in repository content area Scenario: Admin can select and re-use content bank files using the content bank repository Given I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" And I select "Content bank" repository in file picker And I should see "System > Category 1 > MiscellaneousCourse1" breadcrumb navigation in repository And I click on "System" "link" in the ".file-picker .fp-pathbar" "css_element" And I click on "Category1" "folder" in repository content area And I should see "ipsums.h5p" "file" in repository content area And I click on "ipsums.h5p" "file" in repository content area And I should see "Select ipsums.h5p" When I click on "Select this file" "button" Then I should see "1" elements in "Files" filemanager And I should see "ipsums.h5p" in the ".fp-content .fp-file" "css_element" Scenario: Editing teacher can navigate and see content bank files available to him using the content bank repository Given I am on the Folder "Folder activity" page logged in as teacher1 And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" When I select "Content bank" repository in file picker Then I should see "System > Category 1 > MiscellaneousCourse1" breadcrumb navigation in repository And I should see "1" elements in repository content area And I should see "filltheblanks.h5p" "file" in repository content area And I click on "Category 1" "link" in the ".file-picker .fp-pathbar" "css_element" And I should see "System > Category 1" breadcrumb navigation in repository And I should see "1" elements in repository content area And I should see "MiscellaneousCourse1" "folder" in repository content area And I click on "System" "link" in the ".file-picker .fp-pathbar" "css_element" And I should see "System" breadcrumb navigation in repository And I should see "2" elements in repository content area And I should see "filltheblanks.h5p" "file" in repository content area And I should see "Category 1" "folder" in repository content area Scenario: Editing teacher can select and re-use content bank files available to him using the content bank repository Given I am on the Folder "Folder activity" page logged in as teacher1 And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" And I select "Content bank" repository in file picker And I should see "System > Category 1 > MiscellaneousCourse1" breadcrumb navigation in repository And I click on "System" "link" in the ".file-picker .fp-pathbar" "css_element" And I should see "filltheblanks.h5p" "file" in repository content area And I click on "filltheblanks.h5p" "file" in repository content area And I should see "Select filltheblanks.h5p" When I click on "Select this file" "button" Then I should see "1" elements in "Files" filemanager And I should see "filltheblanks.h5p" in the ".fp-content .fp-file" "css_element" Scenario: Non-editing teacher can not see the content bank repository Given I am on the Forum "Forum activity" page logged in as teacher2 And I click on "Add discussion topic" "link" And I click on "Advanced" "button" When I click on "Add..." "button" Then I should not see "Content bank" in the ".fp-repo-area" "css_element" Scenario: Student can not see the content bank repository Given I am on the Forum "Forum activity" page logged in as student And I click on "Add discussion topic" "link" And I click on "Advanced" "button" When I click on "Add..." "button" Then I should not see "Content bank" in the ".fp-repo-area" "css_element" Scenario: Both content name and file name are shown when a content is selected Given the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | mscC1 | contenttype_h5p | admin | My content | /h5p/tests/fixtures/ipsums.h5p | And I am on the Folder "Folder activity" page logged in as admin And I click on "Edit" "button" And I click on "Add..." "button" And I should see "Content bank" in the ".fp-repo-area" "css_element" When I select "Content bank" repository in file picker Then I should see "My content" And I click on "My content" "link" And I should see "Select My content" And the field "Save as" matches value "ipsums.h5p" PK*\UnZXXonedrive/db/tasks.phpnu[. /** * Definition of repository_onedrive scheduled tasks. * * The handlers defined on this file are processed and registered into * the Moodle DB after any install or upgrade operation. All plugins * support this. * * @package repository_onedrive * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /* List of handlers */ $tasks = array( array( 'classname' => 'repository_onedrive\remove_temp_access_task', 'blocking' => 0, 'minute' => 'R', 'hour' => 'R', 'day' => '*', 'dayofweek' => 'R', 'month' => '*' ), ); PK*\ʓ$`onedrive/db/access.phpnu[. /** * Capability definitions for onedrive repository * * @package repository_onedrive * @copyright 2012 Lancaster University Network Services Ltd * @author Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $capabilities = array( 'repository/onedrive:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'user' => CAP_ALLOW ) ) ); PK*\]jonedrive/db/upgrade.phpnu[. /** * Upgrade this plugin. * * @param int $oldversion the version we are upgrading from * @package repository_onedrive * @return bool result */ function xmldb_repository_onedrive_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; } PK*\onedrive/db/install.xmlnu[
    PK*\onedrive/db/caches.phpnu[. /** * Cache definitions. * * @package repository_onedrive * @copyright 2013 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $definitions = array( // Used to store file ids for folders. // The keys used are full path to the folder, the values are the id in office 365. // The static acceleration size has been based upon the depths of a single path. 'folder' => array( 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => false, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 10, 'canuselocalstore' => true ), ); PK*\fM??(onedrive/lang/en/repository_onedrive.phpnu[. /** * Language file definitions for onedrive repository * * @package repository_onedrive * @copyright 2012 Lancaster University Network Services Ltd * @author Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['access'] = 'Access'; $string['both'] = 'Internal and external'; $string['cachedef_folder'] = 'OneDrive file IDs for folders in the system account'; $string['configplugin'] = 'Configure OneDrive plugin'; $string['defaultreturntype'] = 'Default return type'; $string['external'] = 'External (only links stored in Moodle)'; $string['fileoptions'] = 'The types and defaults for returned files is configurable here. Note that all files linked externally will be updated so that the owner is the Moodle system account.'; $string['internal'] = 'Internal (files stored in Moodle)'; $string['issuer_help'] = 'Select the OAuth 2 service that is configured to talk to the OneDrive API. If the service does not exist yet, you will need to create it.'; $string['issuer'] = 'OAuth 2 service'; $string['mysitenotfound'] = 'You have never logged into OneDrive before. You must log in to OneDrive at least once before it can be used with Moodle.'; $string['oauth2serviceslink'] = 'OAuth 2 services configuration'; $string['owner'] = 'Owned by: {$a}'; $string['pluginname'] = 'Microsoft OneDrive'; $string['removetempaccesstask'] = 'Remove temporary write access from controlled links'; $string['searchfor'] = 'Search for {$a}'; $string['servicenotenabled'] = 'Access not configured.'; $string['onedrive:view'] = 'View OneDrive repository'; $string['supportedreturntypes'] = 'Supported files'; $string['privacy:metadata:repository_onedrive'] = 'The Microsoft OneDrive repository stores temporary access grants, and transmits user data from Moodle to the remote system.'; $string['privacy:metadata:repository_onedrive:searchtext'] = 'The Microsoft OneDrive repository user search text query.'; $string['privacy:metadata:repository_onedrive:repository_onedrive_access:itemid'] = 'The Microsoft OneDrive with a temporary access grant item ID.'; $string['privacy:metadata:repository_onedrive:repository_onedrive_access:permissionid'] = 'The Microsoft OneDrive temporary access grant permission ID.'; $string['privacy:metadata:repository_onedrive:repository_onedrive_access:timecreated'] = 'The Microsoft OneDrive temporary access grant creation date/time.'; $string['privacy:metadata:repository_onedrive:repository_onedrive_access:timemodified'] = 'The Microsoft OneDrive temporary access grant modification date/time.'; $string['privacy:metadata:repository_onedrive:repository_onedrive_access:usermodified'] = 'The ID of the user modifying the Microsoft OneDrive temporary access grant.'; // Deprecated since Moodle 4.5. $string['confirmimportskydrive'] = 'Are you sure you want to import all files from the Microsoft SkyDrive repository to the Microsoft OneDrive repository? The Microsoft OneDrive repository must be configured and working for imported files to continue working as before. Warning: This action cannot be undone!'; $string['importskydrivefiles'] = 'Import files from Microsoft SkyDrive repository'; $string['skydrivefilesexist'] = 'The Microsoft SkyDrive repository is enabled but it has been deprecated. Please ensure you migrate files from SkyDrive to the OneDrive repository as soon as possible. In Moodle 4.4 it will no longer be possible.'; $string['skydrivefilesimported'] = 'All files were imported from the Microsoft SkyDrive repository.'; $string['skydrivefilesnotimported'] = 'Some files could not be imported from the Microsoft SkyDrive repository.'; PK*\ubonedrive/lang/en/deprecated.txtnu[confirmimportskydrive,repository_onedrive importskydrivefiles,repository_onedrive skydrivefilesexist,repository_onedrive skydrivefilesimported,repository_onedrive skydrivefilesnotimported,repository_onedrive PK*\|onedrive/pix/icon.svgnu[PK*\MeUUonedrive/pix/icon.pngnu[PNG  IHDR szzsBIT|d pHYs+tEXtSoftwarewww.inkscape.org<IDATXMh\eͤ3w&ih1DXDE)1Eԅ N\FB5bTHQ3Aۈ Vl)Y($q1MLNV{sqy/]`0b')5|x:;NC}s0p.K]/S/|2{mXc3Š!P\ #^D܉[5e[*'fVCEqxOb?T]d}^ÏaRYVkmpp3k $]xj xu&ݿxW:{ suC¥rG# ܞVS ] ^7vVpkc!+ ,bjx3ɰF1 bq@ #RX~|j MӂEy}K3G˥Ju%y1-O9DWe`YRn체tuM2bvB|%nIENDB`PK*\P~~onedrive/lib.phpnu[. /** * Microsoft onedrive repository plugin. * * @package repository_onedrive * @copyright 2012 Lancaster University Network Services Ltd * @author Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_onedrive extends repository { /** * OAuth 2 client * @var \core\oauth2\client */ private $client = null; /** * OAuth 2 Issuer * @var \core\oauth2\issuer */ private $issuer = null; /** * Additional scopes required for drive. */ const SCOPES = 'files.readwrite.all'; /** * Constructor. * * @param int $repositoryid repository instance id. * @param int|stdClass $context a context id or context object. * @param array $options repository options. * @param int $readonly indicate this repo is readonly or not. * @return void */ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) { parent::__construct($repositoryid, $context, $options, $readonly = 0); try { $this->issuer = \core\oauth2\api::get_issuer(get_config('onedrive', 'issuerid')); } catch (dml_missing_record_exception $e) { $this->disabled = true; } if ($this->issuer && !$this->issuer->get('enabled')) { $this->disabled = true; } } /** * Get a cached user authenticated oauth client. * * @param moodle_url $overrideurl - Use this url instead of the repo callback. * @return \core\oauth2\client */ protected function get_user_oauth_client($overrideurl = false) { if ($this->client) { return $this->client; } if ($overrideurl) { $returnurl = $overrideurl; } else { $returnurl = new moodle_url('/repository/repository_callback.php'); $returnurl->param('callback', 'yes'); $returnurl->param('repo_id', $this->id); $returnurl->param('sesskey', sesskey()); } $this->client = \core\oauth2\api::get_user_oauth_client($this->issuer, $returnurl, self::SCOPES, true); return $this->client; } /** * Checks whether the user is authenticate or not. * * @return bool true when logged in. */ public function check_login() { $client = $this->get_user_oauth_client(); return $client->is_logged_in(); } /** * Print or return the login form. * * @return void|array for ajax. */ public function print_login() { $client = $this->get_user_oauth_client(); $url = $client->get_login_url(); if ($this->options['ajax']) { $popup = new stdClass(); $popup->type = 'popup'; $popup->url = $url->out(false); return array('login' => array($popup)); } else { echo ''.get_string('login', 'repository').''; } } /** * Print the login in a popup. * * @param array|null $attr Custom attributes to be applied to popup div. */ public function print_login_popup($attr = null) { global $OUTPUT, $PAGE; $client = $this->get_user_oauth_client(false); $url = new moodle_url($client->get_login_url()); $state = $url->get_param('state') . '&reloadparent=true'; $url->param('state', $state); $PAGE->set_pagelayout('embedded'); echo $OUTPUT->header(); $repositoryname = get_string('pluginname', 'repository_onedrive'); $button = new single_button( $url, get_string('logintoaccount', 'repository', $repositoryname), 'post', single_button::BUTTON_PRIMARY ); $button->add_action(new popup_action('click', $url, 'Login')); $button->class = 'mdl-align'; $button = $OUTPUT->render($button); echo html_writer::div($button, '', $attr); echo $OUTPUT->footer(); } /** * Build the breadcrumb from a path. * * @param string $path to create a breadcrumb from. * @return array containing name and path of each crumb. */ protected function build_breadcrumb($path) { $bread = explode('/', $path); $crumbtrail = ''; foreach ($bread as $crumb) { list($id, $name) = $this->explode_node_path($crumb); $name = empty($name) ? $id : $name; $breadcrumb[] = array( 'name' => $name, 'path' => $this->build_node_path($id, $name, $crumbtrail) ); $tmp = end($breadcrumb); $crumbtrail = $tmp['path']; } return $breadcrumb; } /** * Generates a safe path to a node. * * Typically, a node will be id|Name of the node. * * @param string $id of the node. * @param string $name of the node, will be URL encoded. * @param string $root to append the node on, must be a result of this function. * @return string path to the node. */ protected function build_node_path($id, $name = '', $root = '') { $path = $id; if (!empty($name)) { $path .= '|' . urlencode($name); } if (!empty($root)) { $path = trim($root, '/') . '/' . $path; } return $path; } /** * Returns information about a node in a path. * * @see self::build_node_path() * @param string $node to extrat information from. * @return array about the node. */ protected function explode_node_path($node) { if (strpos($node, '|') !== false) { list($id, $name) = explode('|', $node, 2); $name = urldecode($name); } else { $id = $node; $name = ''; } $id = urldecode($id); return array( 0 => $id, 1 => $name, 'id' => $id, 'name' => $name ); } /** * List the files and folders. * * @param string $path path to browse. * @param string $page page to browse. * @return array of result. */ public function get_listing($path='', $page = '') { if (empty($path)) { $path = $this->build_node_path('root', get_string('pluginname', 'repository_onedrive')); } if ($this->disabled) { // Empty list of files for disabled repository. return ['dynload' => false, 'list' => [], 'nologin' => true]; } // We analyse the path to extract what to browse. $trail = explode('/', $path); $uri = array_pop($trail); list($id, $name) = $this->explode_node_path($uri); // Handle the special keyword 'search', which we defined in self::search() so that // we could set up a breadcrumb in the search results. In any other case ID would be // 'root' which is a special keyword, or a parent (folder) ID. if ($id === 'search') { $q = $name; $id = 'root'; // Append the active path for search. $str = get_string('searchfor', 'repository_onedrive', $searchtext); $path = $this->build_node_path('search', $str, $path); } // Query the Drive. $parent = $id; if ($parent != 'root') { $parent = 'items/' . $parent; } $q = ''; $results = $this->query($q, $path, $parent); $ret = []; $ret['dynload'] = true; $ret['path'] = $this->build_breadcrumb($path); $ret['list'] = $results; $ret['manage'] = 'https://www.office.com/'; return $ret; } /** * Search throughout the OneDrive * * @param string $searchtext text to search for. * @param int $page search page. * @return array of results. */ public function search($searchtext, $page = 0) { $path = $this->build_node_path('root', get_string('pluginname', 'repository_onedrive')); $str = get_string('searchfor', 'repository_onedrive', $searchtext); $path = $this->build_node_path('search', $str, $path); // Query the Drive. $parent = 'root'; $results = $this->query($searchtext, $path, 'root'); $ret = []; $ret['dynload'] = true; $ret['path'] = $this->build_breadcrumb($path); $ret['list'] = $results; $ret['manage'] = 'https://www.office.com/'; return $ret; } /** * Query OneDrive for files and folders using a search query. * * Documentation about the query format can be found here: * https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/driveitem * https://developer.microsoft.com/en-us/graph/docs/overview/query_parameters * * This returns a list of files and folders with their details as they should be * formatted and returned by functions such as get_listing() or search(). * * @param string $q search query as expected by the Graph API. * @param string $path parent path of the current files, will not be used for the query. * @param string $parent Parent id. * @param int $page page. * @return array of files and folders. * @throws Exception * @throws repository_exception */ protected function query($q, $path = null, $parent = null, $page = 0) { global $OUTPUT; $files = []; $folders = []; $fields = "folder,id,lastModifiedDateTime,name,size,webUrl,thumbnails"; $params = ['$select' => $fields, '$expand' => 'thumbnails', 'parent' => $parent]; try { // Retrieving files and folders. $client = $this->get_user_oauth_client(); $service = new repository_onedrive\rest($client); if (!empty($q)) { $params['search'] = urlencode($q); // MS does not return thumbnails on a search. unset($params['$expand']); $response = $service->call('search', $params); } else { $response = $service->call('list', $params); } } catch (Exception $e) { if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) { throw new repository_exception('servicenotenabled', 'repository_onedrive'); } else if (strpos($e->getMessage(), 'mysite not found') !== false) { throw new repository_exception('mysitenotfound', 'repository_onedrive'); } } $remotefiles = isset($response->value) ? $response->value : []; foreach ($remotefiles as $remotefile) { if (!empty($remotefile->folder)) { // This is a folder. $folders[$remotefile->id] = [ 'title' => $remotefile->name, 'path' => $this->build_node_path($remotefile->id, $remotefile->name, $path), 'date' => strtotime($remotefile->lastModifiedDateTime), 'thumbnail' => $OUTPUT->image_url(file_folder_icon())->out(false), 'thumbnail_height' => 64, 'thumbnail_width' => 64, 'children' => [] ]; } else { // We can download all other file types. $title = $remotefile->name; $source = json_encode([ 'id' => $remotefile->id, 'name' => $remotefile->name, 'link' => $remotefile->webUrl ]); $thumb = ''; $thumbwidth = 0; $thumbheight = 0; $extendedinfoerr = false; if (empty($remotefile->thumbnails)) { // Try and get it directly from the item. $params = ['fileid' => $remotefile->id, '$select' => $fields, '$expand' => 'thumbnails']; try { $response = $service->call('get', $params); $remotefile = $response; } catch (Exception $e) { // This is not a failure condition - we just could not get extended info about the file. $extendedinfoerr = true; } } if (!empty($remotefile->thumbnails)) { $thumbs = $remotefile->thumbnails; if (count($thumbs)) { $first = reset($thumbs); if (!empty($first->medium) && !empty($first->medium->url)) { $thumb = $first->medium->url; $thumbwidth = min($first->medium->width, 64); $thumbheight = min($first->medium->height, 64); } } } $files[$remotefile->id] = [ 'title' => $title, 'source' => $source, 'date' => strtotime($remotefile->lastModifiedDateTime), 'size' => isset($remotefile->size) ? $remotefile->size : null, 'thumbnail' => $thumb, 'thumbnail_height' => $thumbwidth, 'thumbnail_width' => $thumbheight, ]; } } // Filter and order the results. $files = array_filter($files, [$this, 'filter']); core_collator::ksort($files, core_collator::SORT_NATURAL); core_collator::ksort($folders, core_collator::SORT_NATURAL); return array_merge(array_values($folders), array_values($files)); } /** * Logout. * * @return string */ public function logout() { $client = $this->get_user_oauth_client(); $client->log_out(); return parent::logout(); } /** * Get a file. * * @param string $reference reference of the file. * @param string $filename filename to save the file to. * @return string JSON encoded array of information about the file. */ public function get_file($reference, $filename = '') { global $CFG; if ($this->disabled) { throw new repository_exception('cannotdownload', 'repository'); } $sourceinfo = json_decode($reference); $client = null; if (!empty($sourceinfo->usesystem)) { $client = \core\oauth2\api::get_system_oauth_client($this->issuer); } else { $client = $this->get_user_oauth_client(); } $base = 'https://graph.microsoft.com/v1.0/'; // Fetch the item info. $infourl = (new moodle_url($base . 'me/drive/items/' . $sourceinfo->id))->out(false); $response = $client->get($infourl); if (!$response) { throw new repository_exception('cannotdownload', 'repository'); } $response = json_decode($response, true); $downloadurl = $response['@microsoft.graph.downloadUrl']; // We use download_one and not the rest API because it has special timeouts etc. $path = $this->prepare_file($filename); $options = ['filepath' => $path, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5]; // We cannot send authorization headers in the direct download request, it will fail. $c = new curl(); $result = $c->download_one($downloadurl, null, $options); if ($result) { @chmod($path, $CFG->filepermissions); return array( 'path' => $path, 'url' => $reference ); } throw new repository_exception('cannotdownload', 'repository'); } /** * Prepare file reference information. * * We are using this method to clean up the source to make sure that it * is a valid source. * * @param string $source of the file. * @return string file reference. */ public function get_file_reference($source) { // We could do some magic upgrade code here. return $source; } /** * What kind of files will be in this repository? * * @return array return '*' means this repository support any files, otherwise * return mimetypes of files, it can be an array */ public function supported_filetypes() { return '*'; } /** * Tells how the file can be picked from this repository. * * @return int */ public function supported_returntypes() { // We can only support references if the system account is connected. if (!empty($this->issuer) && $this->issuer->is_system_account_connected()) { $setting = get_config('onedrive', 'supportedreturntypes'); if ($setting == 'internal') { return FILE_INTERNAL; } else if ($setting == 'external') { return FILE_CONTROLLED_LINK; } else { return FILE_CONTROLLED_LINK | FILE_INTERNAL; } } else { return FILE_INTERNAL; } } /** * Which return type should be selected by default. * * @return int */ public function default_returntype() { $setting = get_config('onedrive', 'defaultreturntype'); $supported = get_config('onedrive', 'supportedreturntypes'); if (($setting == FILE_INTERNAL && $supported != 'external') || $supported == 'internal') { return FILE_INTERNAL; } else { return FILE_CONTROLLED_LINK; } } /** * Return names of the general options. * By default: no general option name. * * @return array */ public static function get_type_option_names() { return array('issuerid', 'pluginname', 'defaultreturntype', 'supportedreturntypes'); } /** * Store the access token. */ public function callback() { $client = $this->get_user_oauth_client(); // This will upgrade to an access token if we have an authorization code and save the access token in the session. $client->is_logged_in(); } /** * Repository method to serve the referenced file * * @see send_stored_file * * @param stored_file $storedfile the file that contains the reference * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime) * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin * @param array $options additional options affecting the file serving */ public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, ?array $options = null) { if ($this->disabled) { throw new repository_exception('cannotdownload', 'repository'); } $source = json_decode($storedfile->get_reference()); $fb = get_file_browser(); $context = context::instance_by_id($storedfile->get_contextid(), MUST_EXIST); $info = $fb->get_file_info($context, $storedfile->get_component(), $storedfile->get_filearea(), $storedfile->get_itemid(), $storedfile->get_filepath(), $storedfile->get_filename()); if (empty($options['offline']) && !empty($info) && $info->is_writable() && !empty($source->usesystem)) { // Add the current user as an OAuth writer. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); if ($systemauth === false) { $details = 'Cannot connect as system user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } $systemservice = new repository_onedrive\rest($systemauth); // Get the user oauth so we can get the account to add. $url = moodle_url::make_pluginfile_url($storedfile->get_contextid(), $storedfile->get_component(), $storedfile->get_filearea(), $storedfile->get_itemid(), $storedfile->get_filepath(), $storedfile->get_filename(), $forcedownload); $url->param('sesskey', sesskey()); $param = ($options['embed'] == true) ? false : $url; $userauth = $this->get_user_oauth_client($param); if (!$userauth->is_logged_in()) { if ($options['embed'] == true) { // Due to Same-origin policy, we cannot redirect to onedrive login page. // If the requested file is embed and the user is not logged in, add option to log in using a popup. $this->print_login_popup(['style' => 'margin-top: 250px']); exit; } redirect($userauth->get_login_url()); } if ($userauth === false) { $details = 'Cannot connect as current user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } $userinfo = $userauth->get_userinfo(); $useremail = $userinfo['email']; $this->add_temp_writer_to_file($systemservice, $source->id, $useremail); } if (!empty($options['offline'])) { $downloaded = $this->get_file($storedfile->get_reference(), $storedfile->get_filename()); $filename = $storedfile->get_filename(); send_file($downloaded['path'], $filename, $lifetime, $filter, false, $forcedownload, '', false, $options); } else if ($source->link) { // Do not use redirect() here because is not compatible with webservice/pluginfile.php. header('Location: ' . $source->link); } else { $details = 'File is missing source link'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } } /** * See if a folder exists within a folder * * @param \repository_onedrive\rest $client Authenticated client. * @param string $fullpath * @return string|boolean The file id if it exists or false. */ protected function get_file_id_by_path(\repository_onedrive\rest $client, $fullpath) { $fields = "id"; try { $response = $client->call('get_file_by_path', ['fullpath' => $fullpath, '$select' => $fields]); } catch (\core\oauth2\rest_exception $re) { return false; } return $response->id; } /** * Delete a file by full path. * * @param \repository_onedrive\rest $client Authenticated client. * @param string $fullpath * @return boolean */ protected function delete_file_by_path(\repository_onedrive\rest $client, $fullpath) { try { $response = $client->call('delete_file_by_path', ['fullpath' => $fullpath]); } catch (\core\oauth2\rest_exception $re) { return false; } return true; } /** * Create a folder within a folder * * @param \repository_onedrive\rest $client Authenticated client. * @param string $foldername The folder we are creating. * @param string $parentid The parent folder we are creating in. * * @return string The file id of the new folder. */ protected function create_folder_in_folder(\repository_onedrive\rest $client, $foldername, $parentid) { $params = ['parentid' => $parentid]; $folder = [ 'name' => $foldername, 'folder' => [ 'childCount' => 0 ]]; $created = $client->call('create_folder', $params, json_encode($folder)); if (empty($created->id)) { $details = 'Cannot create folder:' . $foldername; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return $created->id; } /** * Get simple file info for humans. * * @param \repository_onedrive\rest $client Authenticated client. * @param string $fileid The file we are querying. * * @return stdClass */ protected function get_file_summary(\repository_onedrive\rest $client, $fileid) { $fields = "folder,id,lastModifiedDateTime,name,size,webUrl,createdByUser"; $response = $client->call('get', ['fileid' => $fileid, '$select' => $fields]); return $response; } /** * Add a writer to the permissions on the file (temporary). * * @param \repository_onedrive\rest $client Authenticated client. * @param string $fileid The file we are updating. * @param string $email The email of the writer account to add. * @return boolean */ protected function add_temp_writer_to_file(\repository_onedrive\rest $client, $fileid, $email) { // Expires in 7 days. $expires = new DateTime(); $expires->add(new DateInterval("P7D")); $updateeditor = [ 'recipients' => [[ 'email' => $email ]], 'roles' => ['write'], 'requireSignIn' => true, 'sendInvitation' => false ]; $params = ['fileid' => $fileid]; $response = $client->call('create_permission', $params, json_encode($updateeditor)); if (empty($response->value[0]->id)) { $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } // Store the permission id in the DB. Scheduled task will remove this permission after 7 days. if ($access = repository_onedrive\access::get_record(['permissionid' => $response->value[0]->id, 'itemid' => $fileid ])) { // Update the timemodified. $access->update(); } else { $record = (object) [ 'permissionid' => $response->value[0]->id, 'itemid' => $fileid ]; $access = new repository_onedrive\access(0, $record); $access->create(); } return true; } /** * Allow anyone with the link to read the file. * * @param \repository_onedrive\rest $client Authenticated client. * @param string $fileid The file we are updating. * @return boolean */ protected function set_file_sharing_anyone_with_link_can_read(\repository_onedrive\rest $client, $fileid) { $type = (isset($this->options['embed']) && $this->options['embed'] == true) ? 'embed' : 'view'; $updateread = [ 'type' => $type, 'scope' => 'anonymous' ]; $params = ['fileid' => $fileid]; $response = $client->call('create_link', $params, json_encode($updateread)); if (empty($response->link)) { $details = 'Cannot update link sharing for the document: ' . $fileid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return $response->link->webUrl; } /** * Given a filename, use the core_filetypes registered types to guess a mimetype. * * If no mimetype is known, return 'application/unknown'; * * @param string $filename * @return string $mimetype */ protected function get_mimetype_from_filename($filename) { $mimetype = 'application/unknown'; $types = core_filetypes::get_types(); $fileextension = '.bin'; if (strpos($filename, '.') !== false) { $fileextension = substr($filename, strrpos($filename, '.') + 1); } if (isset($types[$fileextension])) { $mimetype = $types[$fileextension]['type']; } return $mimetype; } /** * Upload a file to onedrive. * * @param \repository_onedrive\rest $service Authenticated client. * @param \curl $curl Curl client to perform the put operation (with no auth headers). * @param \curl $authcurl Curl client that will send authentication headers * @param string $filepath The local path to the file to upload * @param string $mimetype The new mimetype * @param string $parentid The folder to put it. * @param string $filename The name of the new file * @return string $fileid */ protected function upload_file(\repository_onedrive\rest $service, \curl $curl, \curl $authcurl, $filepath, $mimetype, $parentid, $filename) { // Start an upload session. // Docs https://learn.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0 link. $params = ['parentid' => $parentid, 'filename' => urlencode($filename)]; $behaviour = [ 'item' => [ "@microsoft.graph.conflictBehavior" => "rename" ] ]; $created = $service->call('create_upload', $params, json_encode($behaviour)); if (empty($created->uploadUrl)) { $details = 'Cannot begin upload session:' . $parentid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } $options = ['file' => $filepath]; // Try each curl class in turn until we succeed. // First attempt an upload with no auth headers (will work for personal onedrive accounts). // If that fails, try an upload with the auth headers (will work for work onedrive accounts). $curls = [$curl, $authcurl]; $response = null; foreach ($curls as $curlinstance) { $curlinstance->setHeader('Content-type: ' . $mimetype); $size = filesize($filepath); $curlinstance->setHeader('Content-Range: bytes 0-' . ($size - 1) . '/' . $size); // Microsoft does not allow authorization headers for PUT, so we do not include them here. $response = $curlinstance->put($created->uploadUrl, $options, [], false); // False = do not include auth header. if ($curlinstance->errno == 0) { $response = json_decode($response); } if (!empty($response->id)) { // We can stop now - there is a valid file returned. break; } } if (empty($response->id)) { $details = 'File not created'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } return $response->id; } /** * Called when a file is selected as a "link". * Invoked at MOODLE/repository/repository_ajax.php * * What should happen here is that the file should be copied to a new file owned by the moodle system user. * It should be organised in a folder based on the file context. * It's sharing permissions should allow read access with the link. * The returned reference should point to the newly copied file - not the original. * * @param string $reference this reference is generated by * repository::get_file_reference() * @param context $context the target context for this new file. * @param string $component the target component for this new file. * @param string $filearea the target filearea for this new file. * @param string $itemid the target itemid for this new file. * @return string $modifiedreference (final one before saving to DB) */ public function reference_file_selected($reference, $context, $component, $filearea, $itemid) { global $CFG, $SITE; // What we need to do here is transfer ownership to the system user (or copy) // then set the permissions so anyone with the share link can view, // finally update the reference to contain the share link if it was not // already there (and point to new file id if we copied). $source = json_decode($reference); if (!empty($source->usesystem)) { // If we already copied this file to the system account - we are done. return $reference; } // Get a system and a user oauth client. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); if ($systemauth === false) { $details = 'Cannot connect as system user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } $userauth = $this->get_user_oauth_client(); if ($userauth === false) { $details = 'Cannot connect as current user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } $systemservice = new repository_onedrive\rest($systemauth); $base = 'https://graph.microsoft.com/v1.0/'; // Fetch the item info. $infourl = (new moodle_url($base . 'me/drive/items/' . $source->id))->out(false); $response = $userauth->get($infourl); if (!$response) { throw new repository_exception('cannotdownload', 'repository'); } $response = json_decode($response, true); $downloadurl = $response['@microsoft.graph.downloadUrl']; // Download the file. $tmpfilename = clean_param($source->id, PARAM_PATH); $temppath = make_request_directory() . $tmpfilename; // We cannot send authorization headers in the direct download request, it will fail. $c = new curl(); $options = ['filepath' => $temppath, 'timeout' => 60, 'followlocation' => true, 'maxredirs' => 5]; $result = $c->download_one($downloadurl, null, $options); if (!$result) { throw new repository_exception('cannotdownload', 'repository'); } // Now copy it to a sensible folder. $contextlist = array_reverse($context->get_parent_contexts(true)); $cache = cache::make('repository_onedrive', 'folder'); $parentid = 'root'; $fullpath = ''; $allfolders = []; foreach ($contextlist as $context) { // Prepare human readable context folders names, making sure they are still unique within the site. $prevlang = force_current_language($CFG->lang); $foldername = $context->get_context_name(); force_current_language($prevlang); if ($context->contextlevel == CONTEXT_SYSTEM) { // Append the site short name to the root folder. $foldername .= '_'.$SITE->shortname; // Append the relevant object id. } else if ($context->instanceid) { $foldername .= '_id_'.$context->instanceid; } else { // This does not really happen but just in case. $foldername .= '_ctx_'.$context->id; } $foldername = urlencode(clean_param($foldername, PARAM_PATH)); $allfolders[] = $foldername; } $allfolders[] = urlencode(clean_param($component, PARAM_PATH)); $allfolders[] = urlencode(clean_param($filearea, PARAM_PATH)); $allfolders[] = urlencode(clean_param($itemid, PARAM_PATH)); // Variable $allfolders now has the complete path we want to store the file in. // Create each folder in $allfolders under the system account. foreach ($allfolders as $foldername) { if ($fullpath) { $fullpath .= '/'; } $fullpath .= $foldername; $folderid = $cache->get($fullpath); if (empty($folderid)) { $folderid = $this->get_file_id_by_path($systemservice, $fullpath); } if ($folderid !== false) { $cache->set($fullpath, $folderid); $parentid = $folderid; } else { // Create it. $parentid = $this->create_folder_in_folder($systemservice, $foldername, $parentid); $cache->set($fullpath, $parentid); } } // Delete any existing file at this path. $path = $fullpath . '/' . urlencode(clean_param($source->name, PARAM_PATH)); $this->delete_file_by_path($systemservice, $path); // Upload the file. $safefilename = clean_param($source->name, PARAM_PATH); $mimetype = $this->get_mimetype_from_filename($safefilename); // We cannot send authorization headers in the upload or personal microsoft accounts will fail (what a joke!). $curl = new \curl(); $fileid = $this->upload_file($systemservice, $curl, $systemauth, $temppath, $mimetype, $parentid, $safefilename); // Read with link. $link = $this->set_file_sharing_anyone_with_link_can_read($systemservice, $fileid); $summary = $this->get_file_summary($systemservice, $fileid); // Update the details in the file reference before it is saved. $source->id = $summary->id; $source->link = $link; $source->usesystem = true; $reference = json_encode($source); return $reference; } /** * Get human readable file info from the reference. * * @param string $reference * @param int $filestatus */ public function get_reference_details($reference, $filestatus = 0) { if (empty($reference)) { return get_string('unknownsource', 'repository'); } $source = json_decode($reference); if (empty($source->usesystem)) { return ''; } $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); if ($systemauth === false) { return ''; } $systemservice = new repository_onedrive\rest($systemauth); $info = $this->get_file_summary($systemservice, $source->id); $owner = ''; if (!empty($info->createdByUser->displayName)) { $owner = $info->createdByUser->displayName; } if ($owner) { return get_string('owner', 'repository_onedrive', $owner); } else { return $info->name; } } /** * @deprecated since Moodle 4.0 */ #[\core\attribute\deprecated(null, reason: 'It is no longer used', since: '4.0', final: true)] public static function can_import_skydrive_files() { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); } /** * @deprecated since Moodle 4.0 */ #[\core\attribute\deprecated(null, reason: 'It is no longer used', since: '4.0', final: true)] public static function import_skydrive_files() { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); } /** * Edit/Create Admin Settings Moodle form. * * @param moodleform $mform Moodle form (passed by reference). * @param string $classname repository class name. */ public static function type_config_form($mform, $classname = 'repository') { global $OUTPUT; $url = new moodle_url('/admin/tool/oauth2/issuers.php'); $url = $url->out(); $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_onedrive', $url)); 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', 'issuerid', get_string('issuer', 'repository_onedrive'), $options); $mform->addHelpButton('issuerid', 'issuer', 'repository_onedrive'); $mform->addRule('issuerid', $strrequired, 'required', null, 'client'); $mform->addElement('static', null, '', get_string('fileoptions', 'repository_onedrive')); $choices = [ 'internal' => get_string('internal', 'repository_onedrive'), 'external' => get_string('external', 'repository_onedrive'), 'both' => get_string('both', 'repository_onedrive') ]; $mform->addElement('select', 'supportedreturntypes', get_string('supportedreturntypes', 'repository_onedrive'), $choices); $choices = [ FILE_INTERNAL => get_string('internal', 'repository_onedrive'), FILE_CONTROLLED_LINK => get_string('external', 'repository_onedrive'), ]; $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_onedrive'), $choices); } } /** * Callback to get the required scopes for system account. * * @param \core\oauth2\issuer $issuer * @return string */ function repository_onedrive_oauth2_system_scopes(\core\oauth2\issuer $issuer) { if ($issuer->get('id') == get_config('onedrive', 'issuerid')) { return repository_onedrive::SCOPES; } return ''; } PK*\_onedrive/version.phpnu[. /** * Version details for onedrive repository * * @package repository_onedrive * @copyright 2012 Lancaster University Network Services Ltd * @author Dan Poltawski * @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_onedrive'; // Full name of the plugin (used for diagnostics). PK*\z[8'8'%onedrive/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for repository_onedrive. * * @package repository_onedrive * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_onedrive\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\transform; use core_privacy\local\request\userlist; use \core_privacy\local\request\writer; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for repository_onedrive 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( 'onedrive.live.com', [ 'searchtext' => 'privacy:metadata:repository_onedrive:searchtext' ], 'privacy:metadata:repository_onedrive' ); // The repository_onedrive has a 'repository_onedrive_access' table that contains user data. $collection->add_database_table( 'repository_onedrive_access', [ 'itemid' => 'privacy:metadata:repository_onedrive:repository_onedrive_access:itemid', 'permissionid' => 'privacy:metadata:repository_onedrive:repository_onedrive_access:permissionid', 'timecreated' => 'privacy:metadata:repository_onedrive:repository_onedrive_access:timecreated', 'timemodified' => 'privacy:metadata:repository_onedrive:repository_onedrive_access:timemodified', 'usermodified' => 'privacy:metadata:repository_onedrive:repository_onedrive_access:usermodified' ], 'privacy:metadata:repository_onedrive' ); 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 { $contextlist = new contextlist(); // The data is associated at the user context level, so retrieve the user's context id. $sql = "SELECT c.id FROM {repository_onedrive_access} roa JOIN {context} c ON c.instanceid = roa.usermodified AND c.contextlevel = :contextuser WHERE roa.usermodified = :userid GROUP BY c.id"; $params = [ 'contextuser' => CONTEXT_USER, 'userid' => $userid ]; $contextlist->add_from_sql($sql, $params); return $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) { $context = $userlist->get_context(); if (!$context instanceof \context_user) { return; } // The data is associated at the user context level, so retrieve the user's context id. $sql = "SELECT usermodified AS userid FROM {repository_onedrive_access} WHERE usermodified = ?"; $params = [$context->instanceid]; $userlist->add_from_sql('userid', $sql, $params); } /** * 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) { global $DB; // If the user has data, then only the User context should be present so get the first context. $contexts = $contextlist->get_contexts(); if (count($contexts) == 0) { return; } $context = reset($contexts); // Sanity check that context is at the User context level, then get the userid. if ($context->contextlevel !== CONTEXT_USER) { return; } $userid = $context->instanceid; $sql = "SELECT roa.id as id, roa.itemid as itemid, roa.permissionid as permissionid, roa.timecreated as timecreated, roa.timemodified as timemodified FROM {repository_onedrive_access} roa WHERE roa.usermodified = :userid"; $params = [ 'userid' => $userid ]; $onedriveaccesses = $DB->get_records_sql($sql, $params); $index = 0; foreach ($onedriveaccesses as $onedriveaccess) { // Data export is organised in: {User Context}/Repository plug-ins/{Plugin Name}/Access/{index}/data.json. $index++; $subcontext = [ get_string('plugin', 'core_repository'), get_string('pluginname', 'repository_onedrive'), get_string('access', 'repository_onedrive'), $index ]; $data = (object) [ 'itemid' => $onedriveaccess->itemid, 'permissionid' => $onedriveaccess->permissionid, 'timecreated' => transform::datetime($onedriveaccess->timecreated), 'timemodified' => transform::datetime($onedriveaccess->timemodified) ]; writer::with_context($context)->export_data($subcontext, $data); } } /** * 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) { global $DB; // Sanity check that context is at the User context level, then get the userid. if ($context->contextlevel !== CONTEXT_USER) { return; } $userid = $context->instanceid; $DB->delete_records('repository_onedrive_access', ['usermodified' => $userid]); } /** * 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) { global $DB; // If the user has data, then only the User context should be present so get the first context. $contexts = $contextlist->get_contexts(); if (count($contexts) == 0) { return; } $context = reset($contexts); // Sanity check that context is at the User context level, then get the userid. if ($context->contextlevel !== CONTEXT_USER) { return; } $userid = $context->instanceid; $DB->delete_records('repository_onedrive_access', ['usermodified' => $userid]); } /** * 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) { global $DB; $context = $userlist->get_context(); // Sanity check that context is at the User context level, then get the userid. if ($context->contextlevel !== CONTEXT_USER) { return; } $userids = $userlist->get_userids(); list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); $params = [ 'contextid' => $context->id, 'contextuser' => CONTEXT_USER, ]; $params = array_merge($params, $inparams); // Fetch the repository_onedrive_access IDs in the context for approved users. $sql = "SELECT roa.id FROM {repository_onedrive_access} roa JOIN {context} c ON c.instanceid = roa.usermodified AND c.contextlevel = :contextuser AND c.id = :contextid WHERE roa.usermodified {$insql}"; $accessids = $DB->get_fieldset_sql($sql, $params); // Delete the relevant repository_onedrive_access data. list($insql, $params) = $DB->get_in_or_equal($accessids, SQL_PARAMS_NAMED); $DB->delete_records_select('repository_onedrive_access', "id {$insql}", $params); } } PK*\ ,onedrive/classes/remove_temp_access_task.phpnu[. /** * A scheduled task. * * @package repository_onedrive * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_onedrive; use \core\task\scheduled_task; use DateTime; use DateInterval; use repository_exception; use \core\oauth2\rest_exception; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/repository/lib.php'); /** * Simple task to delete temporary permission records. * @package repository_onedrive * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class remove_temp_access_task extends scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('removetempaccesstask', 'repository_onedrive'); } /** * Do the job. * Throw exceptions on errors (the job will be retried). */ public function execute() { $accessrecords = access::get_records(); $expires = new DateTime(); $expires->sub(new DateInterval("P7D")); $timestamp = $expires->getTimestamp(); $issuerid = get_config('onedrive', 'issuerid'); $issuer = \core\oauth2\api::get_issuer($issuerid); // Add the current user as an OAuth writer. $systemauth = \core\oauth2\api::get_system_oauth_client($issuer); if ($systemauth === false) { $details = 'Cannot connect as system user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } $systemservice = new \repository_onedrive\rest($systemauth); foreach ($accessrecords as $access) { if ($access->get('timemodified') < $timestamp) { $params = ['permissionid' => $access->get('permissionid'), 'itemid' => $access->get('itemid')]; try { $systemservice->call('delete_permission', $params); } catch (rest_exception $re) { // We log and give up here or we will always fail for eternity. mtrace('Failed to remove access from file: ' . $access->get('itemid')); } $access->delete(); } } } } PK*\ =jjonedrive/classes/access.phpnu[. /** * Class for loading/storing access records from the DB. * * @package repository_onedrive * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_onedrive; defined('MOODLE_INTERNAL') || die(); use core\persistent; /** * Class for loading/storing issuer from the DB * * @package repository_onedrive * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class access extends persistent { const TABLE = 'repository_onedrive_access'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'permissionid' => array( 'type' => PARAM_RAW ), 'itemid' => array( 'type' => PARAM_RAW ) ); } } PK*\u>icconedrive/classes/rest.phpnu[. /** * Microsoft Graph API Rest Interface. * * @package repository_onedrive * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_onedrive; defined('MOODLE_INTERNAL') || die(); /** * Microsoft Graph API Rest Interface. * * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class rest extends \core\oauth2\rest { /** * Define the functions of the rest API. * * @return array Example: * [ 'listFiles' => [ 'method' => 'get', 'endpoint' => 'http://...', 'args' => [ 'folder' => PARAM_STRING ] ] ] */ public function get_api_functions() { return [ 'list' => [ 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/{parent}/children', 'method' => 'get', 'args' => [ '$select' => PARAM_RAW, '$expand' => PARAM_RAW, 'parent' => PARAM_RAW, '$skip' => PARAM_INT, '$skipToken' => PARAM_RAW, '$count' => PARAM_INT ], 'response' => 'json' ], 'search' => [ 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/{parent}/search(q=\'{search}\')', 'method' => 'get', 'args' => [ 'search' => PARAM_NOTAGS, '$select' => PARAM_RAW, 'parent' => PARAM_RAW, '$skip' => PARAM_INT, '$skipToken' => PARAM_RAW, '$count' => PARAM_INT ], 'response' => 'json' ], 'get' => [ 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}', 'method' => 'get', 'args' => [ 'fileid' => PARAM_RAW, '$select' => PARAM_RAW, '$expand' => PARAM_RAW ], 'response' => 'json' ], 'create_permission' => [ 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}/invite', 'method' => 'post', 'args' => [ 'fileid' => PARAM_RAW ], 'response' => 'json' ], 'create_upload' => [ 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{parentid}:/{filename}:/createUploadSession', 'method' => 'post', 'args' => [ 'parentid' => PARAM_RAW, 'filename' => PARAM_RAW ], 'response' => 'json' ], 'get_file_by_path' => [ 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/root:/{fullpath}', 'method' => 'get', 'args' => [ 'fullpath' => PARAM_RAW, '$select' => PARAM_RAW ], 'response' => 'json' ], 'create_folder' => [ 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{parentid}/children', 'method' => 'post', 'args' => [ 'parentid' => PARAM_RAW ], 'response' => 'json' ], 'create_link' => [ 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}/createLink', 'method' => 'post', 'args' => [ 'fileid' => PARAM_RAW ], 'response' => 'json' ], 'delete_file_by_path' => [ 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/root:/{fullpath}', 'method' => 'delete', 'args' => [ 'fullpath' => PARAM_RAW, ], 'response' => 'json' ], 'delete_permission' => [ 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}/permissions/{permissionid}', 'method' => 'delete', 'args' => [ 'fileid' => PARAM_RAW, 'permissionid' => PARAM_RAW ], 'response' => 'json' ], ]; } } PK*\ VVonedrive/UPGRADING.mdnu[# repository_onedrive Upgrade notes ## 4.5 ### Removed - The following previously deprecated methods have been removed and can no longer be used: - `\repository_onedrive::can_import_skydrive_files()` - `\repository_onedrive::import_skydrive_files()` For more information see [MDL-72620](https://tracker.moodle.org/browse/MDL-72620) PK*\r8\?\?(onedrive/tests/privacy/provider_test.phpnu[. /** * Unit tests for the repository_onedrive implementation of the privacy API. * * @package repository_onedrive * @category test * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_onedrive\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\local\metadata\collection; use core_privacy\local\request\writer; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use repository_onedrive\privacy\provider; /** * Unit tests for the repository_onedrive implementation of the privacy API. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends \core_privacy\tests\provider_testcase { /** * Overriding setUp() function to always reset after tests. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); } /** * Test for provider::get_contexts_for_userid(). */ public function test_get_contexts_for_userid(): void { global $DB; // Test setup. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Add two repository_onedrive_access records for the User. $access = (object)[ 'usermodified' => $user->id, 'itemid' => 'Some onedrive access item data', 'permissionid' => 'Some onedrive access permission data', 'timecreated' => date('u'), 'timemodified' => date('u'), ]; $DB->insert_record('repository_onedrive_access', $access); $access = (object)[ 'usermodified' => $user->id, 'itemid' => 'Another onedrive access item data', 'permissionid' => 'Another onedrive access permission data', 'timecreated' => date('u'), 'timemodified' => date('u'), ]; $DB->insert_record('repository_onedrive_access', $access); // Test there are two repository_onedrive_access records for the User. $access = $DB->get_records('repository_onedrive_access', ['usermodified' => $user->id]); $this->assertCount(2, $access); // Test the User's retrieved contextlist contains only one context. $contextlist = provider::get_contexts_for_userid($user->id); $contexts = $contextlist->get_contexts(); $this->assertCount(1, $contexts); // Test the User's contexts equal the User's own context. $context = reset($contexts); $this->assertEquals(CONTEXT_USER, $context->contextlevel); $this->assertEquals($user->id, $context->instanceid); } /** * Test for provider::test_get_users_in_context(). */ public function test_get_users_in_context(): void { global $DB; $component = 'repository_onedrive'; // Test setup. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $u1id = $user1->id; $u2id = $user2->id; // Add a repository_onedrive_access records for each user. $this->setUser($user1); $access = (object)[ 'usermodified' => $u1id, 'itemid' => 'Some onedrive access item data', 'permissionid' => 'Some onedrive access permission data', 'timecreated' => date('u'), 'timemodified' => date('u'), ]; $DB->insert_record('repository_onedrive_access', $access); $this->setUser($user2); $access = (object)[ 'usermodified' => $u2id, 'itemid' => 'Another onedrive access item data', 'permissionid' => 'Another onedrive access permission data', 'timecreated' => date('u'), 'timemodified' => date('u'), ]; $DB->insert_record('repository_onedrive_access', $access); // Fetch the context of each user's access record. $contextlist = provider::get_contexts_for_userid($u1id); $u1contexts = $contextlist->get_contexts(); $this->assertCount(1, $u1contexts); $contextlist = provider::get_contexts_for_userid($u2id); $u2contexts = $contextlist->get_contexts(); $this->assertCount(1, $u2contexts); $contexts = [ $u1id => $u1contexts[0], $u2id => $u2contexts[0], ]; // Test context 1 only contains user 1. $userlist = new \core_privacy\local\request\userlist($contexts[$u1id], $component); provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $actual = $userlist->get_userids(); $this->assertEquals([$u1id], $actual); // Test context 2 only contains user 2. $userlist = new \core_privacy\local\request\userlist($contexts[$u2id], $component); provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $actual = $userlist->get_userids(); $this->assertEquals([$u2id], $actual); // Test the contexts match the users' contexts. $this->assertEquals($u1id, $contexts[$u1id]->instanceid); $this->assertEquals($u2id, $contexts[$u2id]->instanceid); } /** * Test for provider::export_user_data(). */ public function test_export_user_data(): void { global $DB; // Test setup. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Add two repository_onedrive_access records for the User. $access = (object)[ 'usermodified' => $user->id, 'itemid' => 'Some onedrive access item data', 'permissionid' => 'Some onedrive access permission data', 'timecreated' => date('u'), 'timemodified' => date('u'), ]; $DB->insert_record('repository_onedrive_access', $access); $access = (object)[ 'usermodified' => $user->id, 'itemid' => 'Another onedrive access item data', 'permissionid' => 'Another onedrive access permission data', 'timecreated' => date('u'), 'timemodified' => date('u'), ]; $DB->insert_record('repository_onedrive_access', $access); // Test there are two repository_onedrive_access records for the User. $access = $DB->get_records('repository_onedrive_access', ['usermodified' => $user->id]); $this->assertCount(2, $access); // Test the User's retrieved contextlist contains only one context. $contextlist = provider::get_contexts_for_userid($user->id); $contexts = $contextlist->get_contexts(); $this->assertCount(1, $contexts); // Test the User's contexts equal the User's own context. $context = reset($contexts); $this->assertEquals(CONTEXT_USER, $context->contextlevel); $this->assertEquals($user->id, $context->instanceid); $approvedcontextlist = new approved_contextlist($user, 'repository_onedrive', $contextlist->get_contextids()); // Retrieve repository_onedrive_access data only for this user. provider::export_user_data($approvedcontextlist); // Test the repository_onedrive_access data is exported at the User context level. $user = $approvedcontextlist->get_user(); $contextuser = \context_user::instance($user->id); $writer = writer::with_context($contextuser); $this->assertTrue($writer->has_any_data()); } /** * Test for provider::delete_data_for_all_users_in_context(). */ public function test_delete_data_for_all_users_in_context(): void { global $DB; // Test setup. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Add two repository_onedrive_access records for the User. $access = (object)[ 'usermodified' => $user->id, 'itemid' => 'Some onedrive access item data', 'permissionid' => 'Some onedrive access permission data', 'timecreated' => date('u'), 'timemodified' => date('u'), ]; $DB->insert_record('repository_onedrive_access', $access); $access = (object)[ 'usermodified' => $user->id, 'itemid' => 'Another onedrive access item data', 'permissionid' => 'Another onedrive access permission data', 'timecreated' => date('u'), 'timemodified' => date('u'), ]; $DB->insert_record('repository_onedrive_access', $access); // Test there are two repository_onedrive_access records for the User. $access = $DB->get_records('repository_onedrive_access', ['usermodified' => $user->id]); $this->assertCount(2, $access); // Test the User's retrieved contextlist contains only one context. $contextlist = provider::get_contexts_for_userid($user->id); $contexts = $contextlist->get_contexts(); $this->assertCount(1, $contexts); // Test the User's contexts equal the User's own context. $context = reset($contexts); $this->assertEquals(CONTEXT_USER, $context->contextlevel); $this->assertEquals($user->id, $context->instanceid); // Test delete all users content by context. provider::delete_data_for_all_users_in_context($context); $access = $DB->get_records('repository_onedrive_access', ['usermodified' => $user->id]); $this->assertCount(0, $access); } /** * Test for provider::delete_data_for_user(). */ public function test_delete_data_for_user(): void { global $DB; // Test setup. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->setUser($user1); // Add 3 repository_onedrive_accesss records for User 1. $noaccess = 3; for ($a = 0; $a < $noaccess; $a++) { $access = (object)[ 'usermodified' => $user1->id, 'itemid' => 'Some onedrive access item data - ' . $a, 'permissionid' => 'Some onedrive access permission data - ' . $a, 'timecreated' => date('u'), 'timemodified' => date('u'), ]; $DB->insert_record('repository_onedrive_access', $access); } // Add 1 repository_onedrive_accesss record for User 2. $access = (object)[ 'usermodified' => $user2->id, 'itemid' => 'Some onedrive access item data', 'permissionid' => 'Some onedrive access permission data', 'timecreated' => date('u'), 'timemodified' => date('u'), ]; $DB->insert_record('repository_onedrive_access', $access); // Test the created repository_onedrive records for User 1 equals test number of access specified. $communities = $DB->get_records('repository_onedrive_access', ['usermodified' => $user1->id]); $this->assertCount($noaccess, $communities); // Test the created repository_onedrive records for User 2 equals 1. $communities = $DB->get_records('repository_onedrive_access', ['usermodified' => $user2->id]); $this->assertCount(1, $communities); // Test the deletion of repository_onedrive_access records for User 1 results in zero records. $contextlist = provider::get_contexts_for_userid($user1->id); $contexts = $contextlist->get_contexts(); $this->assertCount(1, $contexts); // Test the User's contexts equal the User's own context. $context = reset($contexts); $this->assertEquals(CONTEXT_USER, $context->contextlevel); $this->assertEquals($user1->id, $context->instanceid); $approvedcontextlist = new approved_contextlist($user1, 'repository_onedrive', $contextlist->get_contextids()); provider::delete_data_for_user($approvedcontextlist); $access = $DB->get_records('repository_onedrive_access', ['usermodified' => $user1->id]); $this->assertCount(0, $access); // Test that User 2's single repository_onedrive_access record still exists. $contextlist = provider::get_contexts_for_userid($user2->id); $contexts = $contextlist->get_contexts(); $this->assertCount(1, $contexts); // Test the User's contexts equal the User's own context. $context = reset($contexts); $this->assertEquals(CONTEXT_USER, $context->contextlevel); $this->assertEquals($user2->id, $context->instanceid); $access = $DB->get_records('repository_onedrive_access', ['usermodified' => $user2->id]); $this->assertCount(1, $access); } /** * Test for provider::delete_data_for_users(). */ public function test_delete_data_for_users(): void { global $DB; $component = 'repository_onedrive'; // Test setup. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->setUser($user1); // Add 3 repository_onedrive_accesss records for User 1. $noaccess = 3; for ($a = 0; $a < $noaccess; $a++) { $access = (object)[ 'usermodified' => $user1->id, 'itemid' => 'Some onedrive access item data for user 1 - ' . $a, 'permissionid' => 'Some onedrive access permission data - ' . $a, 'timecreated' => date('u'), 'timemodified' => date('u'), ]; $DB->insert_record('repository_onedrive_access', $access); } // Add 1 repository_onedrive_accesss record for User 2. $access = (object)[ 'usermodified' => $user2->id, 'itemid' => 'Some onedrive access item data for user 2', 'permissionid' => 'Some onedrive access permission data', 'timecreated' => date('u'), 'timemodified' => date('u'), ]; $DB->insert_record('repository_onedrive_access', $access); // Test the created repository_onedrive records for User 1 equals test number of access specified. $communities = $DB->get_records('repository_onedrive_access', ['usermodified' => $user1->id]); $this->assertCount($noaccess, $communities); // Test the created repository_onedrive records for User 2 equals 1. $communities = $DB->get_records('repository_onedrive_access', ['usermodified' => $user2->id]); $this->assertCount(1, $communities); // Fetch the context of each user's access record. $contextlist = provider::get_contexts_for_userid($user1->id); $u1contexts = $contextlist->get_contexts(); // Test the deletion of context 1 results in deletion of user 1's records only. $approveduserids = [$user1->id, $user2->id]; $approvedlist = new approved_userlist($u1contexts[0], $component, $approveduserids); provider::delete_data_for_users($approvedlist); $access = $DB->get_records('repository_onedrive_access', ['usermodified' => $user1->id]); $this->assertCount(0, $access); // Ensure user 2's record still exists. $access = $DB->get_records('repository_onedrive_access', ['usermodified' => $user2->id]); $this->assertCount(1, $access); } } PK*\(ncoursefiles/db/access.phpnu[. /** * Plugin capabilities. * * @package repository_coursefiles * @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/coursefiles:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'coursecreator' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ) ); PK*\٫.coursefiles/lang/en/repository_coursefiles.phpnu[. /** * Strings for component 'repository_coursefiles', language 'en', branch 'MOODLE_20_STABLE' * * @package repository_coursefiles * @copyright 2010 Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['configplugin'] = 'Configuration for legacy course files repository'; $string['emptyfilelist'] = 'There are no files to show'; $string['notitle'] = 'notitle'; $string['remember'] = 'Remember me'; $string['pluginname_help'] = 'Legacy course files'; $string['pluginname'] = 'Legacy course files'; $string['coursefiles:view'] = 'Use course files repository plugin'; $string['privacy:metadata'] = 'The Legacy course files repository plugin does not store or transmit any personal data.'; PK*\coursefiles/pix/icon.svgnu[ ]> PK*\3ԭcoursefiles/pix/icon.pngnu[PNG  IHDRatIDATx3Qmo۶U'UltqbL۶m083֯yyC7E4 Yj[VļՇ \ѣ /=16՟4%|̘575tkP-?cY&LnҢN'P7 9`5p(ąs['$&ׯԉG"~~*DP4yZq DUs-R"M1 A +EDޯ'~B5W͖&)imXYy6>1gƍ>8k%@`*@TI[k7J @>HY +. /** * This plugin is used to access coursefiles repository * * @since Moodle 2.0 * @package repository_coursefiles * @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_coursefiles class is used to browse course files * * @since Moodle 2.0 * @package repository_coursefiles * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_coursefiles extends repository { /** * coursefiles plugin doesn't require login, so list all files * * @return mixed */ public function print_login() { return $this->get_listing(); } /** * Get file listing * * @param string $encodedpath * @return mixed */ public function get_listing($encodedpath = '', $page = '') { global $CFG, $USER, $OUTPUT; $ret = array(); $ret['dynload'] = true; $ret['nosearch'] = true; $ret['nologin'] = true; $list = array(); $component = 'course'; $filearea = 'legacy'; $itemid = 0; $browser = get_file_browser(); if (!empty($encodedpath)) { $params = json_decode(base64_decode($encodedpath), true); if (is_array($params)) { $filepath = is_null($params['filepath']) ? NULL : clean_param($params['filepath'], PARAM_PATH); $filename = is_null($params['filename']) ? NULL : clean_param($params['filename'], PARAM_FILE); $context = context::instance_by_id(clean_param($params['contextid'], PARAM_INT)); } } else { $filename = null; $filepath = null; list($context, $course, $cm) = get_context_info_array($this->context->id); $courseid = is_object($course) ? $course->id : SITEID; $context = context_course::instance($courseid); } if ($fileinfo = $browser->get_file_info($context, $component, $filearea, $itemid, $filepath, $filename)) { // build path navigation $pathnodes = array(); $encodedpath = base64_encode(json_encode($fileinfo->get_params())); $pathnodes[] = array('name'=>$fileinfo->get_visible_name(), 'path'=>$encodedpath); $level = $fileinfo->get_parent(); while ($level) { $params = $level->get_params(); $encodedpath = base64_encode(json_encode($params)); if ($params['contextid'] != $context->id) { break; } $pathnodes[] = array('name'=>$level->get_visible_name(), 'path'=>$encodedpath); $level = $level->get_parent(); } if (!empty($pathnodes) && is_array($pathnodes)) { $pathnodes = array_reverse($pathnodes); $ret['path'] = $pathnodes; } // build file tree $children = $fileinfo->get_children(); foreach ($children as $child) { if ($child->is_directory()) { $params = $child->get_params(); $subdir_children = $child->get_children(); $encodedpath = base64_encode(json_encode($params)); $node = array( 'title' => $child->get_visible_name(), 'datemodified' => $child->get_timemodified(), 'datecreated' => $child->get_timecreated(), 'path' => $encodedpath, 'children'=>array(), 'thumbnail' => $OUTPUT->image_url(file_folder_icon())->out(false) ); $list[] = $node; } else { $encodedpath = base64_encode(json_encode($child->get_params())); $node = array( 'title' => $child->get_visible_name(), 'size' => $child->get_filesize(), 'author' => $child->get_author(), 'license' => $child->get_license(), 'datemodified' => $child->get_timemodified(), 'datecreated' => $child->get_timecreated(), 'source'=> $encodedpath, 'isref' => $child->is_external_file(), 'thumbnail' => $OUTPUT->image_url(file_file_icon($child))->out(false) ); if ($child->get_status() == 666) { $node['originalmissing'] = true; } if ($imageinfo = $child->get_imageinfo()) { $fileurl = new moodle_url($child->get_url()); $node['realthumbnail'] = $fileurl->out(false, array('preview' => 'thumb', 'oid' => $child->get_timemodified())); $node['realicon'] = $fileurl->out(false, array('preview' => 'tinyicon', 'oid' => $child->get_timemodified())); $node['image_width'] = $imageinfo['width']; $node['image_height'] = $imageinfo['height']; } $list[] = $node; } } } else { $list = array(); } $ret['list'] = array_filter($list, array($this, 'filter')); return $ret; } public function get_link($encoded) { $browser = get_file_browser(); // the final file $params = (array) unserialize_array(base64_decode($encoded)); $contextid = clean_param($params['contextid'], PARAM_INT); $fileitemid = clean_param($params['itemid'], PARAM_INT); $filename = clean_param($params['filename'], PARAM_FILE); $filepath = clean_param($params['filepath'], PARAM_PATH); $filearea = clean_param($params['filearea'], PARAM_AREA); $component = clean_param($params['component'], PARAM_COMPONENT); $context = context::instance_by_id($contextid); $file_info = $browser->get_file_info($context, $component, $filearea, $fileitemid, $filepath, $filename); return $file_info->get_url(); } /** * Return is the instance is visible * (is the type visible ? is the context enable ?) * * @return boolean */ public function is_visible() { global $COURSE; //TODO: this is deprecated (skodak) if ($COURSE->legacyfiles != 2) { // do not show repo if legacy files disabled in this course... return false; } return parent::is_visible(); } /** * Return the repository name. * * @return string */ public function get_name() { $context = $this->context->get_course_context(false); if ($context) { return get_string('courselegacyfilesofcourse', 'moodle', $context->get_context_name(false, true)); } else { return get_string('courselegacyfiles'); } } public function supported_returntypes() { return (FILE_INTERNAL | FILE_REFERENCE); } public static function get_type_option_names() { return array(); } /** * Does this repository used to browse moodle files? * * @return boolean */ public function has_moodle_files() { return true; } /** * Is this repository accessing private data? * * @return bool */ public function contains_private_data() { return false; } } PK*\2Wcoursefiles/version.phpnu[. /** * Version details * * @package repository * @subpackage coursefiles * @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_coursefiles'; // Full name of the plugin (used for diagnostics) PK*\iD(coursefiles/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for repository_coursefiles. * * @package repository_coursefiles * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_coursefiles\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for repository_coursefiles implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK*\eE#coursefiles/tests/generator/lib.phpnu[. /** * Course files repository data generator * * @package repository_coursefiles * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Course files repository data generator class * * @package repository_coursefiles * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_coursefiles_generator extends testing_repository_generator { } PK*\\4ځ filepicker.jsnu[// YUI3 File Picker module for moodle // Author: Dongsheng Cai /** * * File Picker UI * ===== * this.fpnode, contains reference to filepicker Node, non-empty if and only if rendered * this.api, stores the URL to make ajax request * this.mainui, YUI Panel * this.selectnode, contains reference to select-file Node * this.selectui, YUI Panel for selecting particular file * this.msg_dlg, YUI Panel for error or info message * this.process_dlg, YUI Panel for processing existing filename * this.treeview, YUI Treeview * this.viewmode, store current view mode * this.pathbar, reference to the Node with path bar * this.pathnode, a Node element representing one folder in a path bar (not attached anywhere, just used for template) * this.currentpath, the current path in the repository (or last requested path) * * Filepicker options: * ===== * this.options.client_id, the instance id * this.options.contextid * this.options.itemid * this.options.repositories, stores all repositories displayed in file picker * this.options.formcallback * * Active repository options * ===== * this.active_repo.id * this.active_repo.defaultreturntype * this.active_repo.nosearch * this.active_repo.norefresh * this.active_repo.nologin * this.active_repo.help * this.active_repo.manage * * Server responses * ===== * this.filelist, cached filelist * this.pages * this.page * this.filepath, current path (each element of the array is a part of the breadcrumb) * this.logindata, cached login form */ YUI.add('moodle-core_filepicker', function(Y) { /** help function to extract width/height style as a number, not as a string */ Y.Node.prototype.getStylePx = function(attr) { var style = this.getStyle(attr); if (''+style == '0' || ''+style == '0px') { return 0; } var matches = style.match(/^([\d\.]+)px$/) if (matches && parseFloat(matches[1])) { return parseFloat(matches[1]); } return null; } /** if condition is met, the class is added to the node, otherwise - removed */ Y.Node.prototype.addClassIf = function(className, condition) { if (condition) { this.addClass(className); } else { this.removeClass(className); } return this; } /** sets the width(height) of the node considering existing minWidth(minHeight) */ Y.Node.prototype.setStyleAdv = function(stylename, value) { var stylenameCap = stylename.substr(0,1).toUpperCase() + stylename.substr(1, stylename.length-1).toLowerCase(); this.setStyle(stylename, '' + Math.max(value, this.getStylePx('min'+stylenameCap)) + 'px') return this; } /** set image source to src, if there is preview, remember it in lazyloading. * If there is a preview and it was already loaded, use it. */ Y.Node.prototype.setImgSrc = function(src, realsrc, lazyloading) { if (realsrc) { if (M.core_filepicker.loadedpreviews[realsrc]) { this.set('src', realsrc).addClass('realpreview'); return this; } else { if (!this.get('id')) { this.generateID(); } lazyloading[this.get('id')] = realsrc; } } this.set('src', src); return this; } /** * Replaces the image source with preview. If the image is inside the treeview, we need * also to update the html property of corresponding YAHOO.widget.HTMLNode * @param array lazyloading array containing associations of imgnodeid->realsrc */ Y.Node.prototype.setImgRealSrc = function(lazyloading) { if (this.get('id') && lazyloading[this.get('id')]) { var newsrc = lazyloading[this.get('id')]; M.core_filepicker.loadedpreviews[newsrc] = true; this.set('src', newsrc).addClass('realpreview'); delete lazyloading[this.get('id')]; var treenode = this.ancestor('.fp-treeview') if (treenode && treenode.get('parentNode').treeview) { treenode.get('parentNode').treeview.getRoot().refreshPreviews(this.get('id'), newsrc); } } return this; } /** scan TreeView to find which node contains image with id=imgid and replace it's html * with the new image source. */ Y.YUI2.widget.Node.prototype.refreshPreviews = function(imgid, newsrc, regex) { if (!regex) { regex = new RegExp("]*id=\""+imgid+"\"[^>]*?(/?)>", "im"); } if (this.expanded || this.isLeaf) { var html = this.getContentHtml(); if (html && this.setHtml && regex.test(html)) { var newhtml = this.html.replace(regex, "", html); this.setHtml(newhtml); return true; } if (!this.isLeaf && this.children) { for(var c in this.children) { if (this.children[c].refreshPreviews(imgid, newsrc, regex)) { return true; } } } } return false; } /** * Displays a list of files (used by filepicker, filemanager) inside the Node * * @param array options * viewmode : 1 - icons, 2 - tree, 3 - table * appendonly : whether fileslist need to be appended instead of replacing the existing content * filenode : Node element that contains template for displaying one file * callback : On click callback. The element of the fileslist array will be passed as argument * rightclickcallback : On right click callback (optional). * callbackcontext : context where callbacks are executed * sortable : whether content may be sortable (in table mode) * dynload : allow dynamic load for tree view * filepath : for pre-building of tree view - the path to the current directory in filepicker format * treeview_dynload : callback to function to dynamically load the folder in tree view * classnamecallback : callback to function that returns the class name for an element * @param array fileslist array of files to show, each array element may have attributes: * title or fullname : file name * shorttitle (optional) : display file name * thumbnail : url of image * icon : url of icon image * thumbnail_width : width of thumbnail, default 90 * thumbnail_height : height of thumbnail, default 90 * thumbnail_alt : TODO not needed! * description or thumbnail_title : alt text * @param array lazyloading : reference to the array with lazy loading images */ Y.Node.prototype.fp_display_filelist = function(options, fileslist, lazyloading) { var viewmodeclassnames = {1:'fp-iconview', 2:'fp-treeview', 3:'fp-tableview'}; var classname = viewmodeclassnames[options.viewmode]; var scope = this; /** return whether file is a folder (different attributes in FileManager and FilePicker) */ var file_is_folder = function(node) { if (node.children) {return true;} if (node.type && node.type == 'folder') {return true;} return false; }; /** return the name of the file (different attributes in FileManager and FilePicker) */ var file_get_filename = function(node) { return node.title ? node.title : node.fullname; }; /** return display name of the file (different attributes in FileManager and FilePicker) */ var file_get_displayname = function(node) { var displayname = node.shorttitle ? node.shorttitle : file_get_filename(node); return Y.Escape.html(displayname); }; /** return file description (different attributes in FileManager and FilePicker) */ var file_get_description = function(node) { var description = ''; if (node.description) { description = node.description; } else if (node.thumbnail_title) { description = node.thumbnail_title; } else { description = file_get_filename(node); } return Y.Escape.html(description); }; /** help funciton for tree view */ var build_tree = function(node, level) { // prepare file name with icon var el = Y.Node.create('
    '); el.appendChild(options.filenode.cloneNode(true)); el.one('.fp-filename').setContent(file_get_displayname(node)); // TODO add tooltip with node.title or node.thumbnail_title var tmpnodedata = {className:options.classnamecallback(node)}; el.get('children').addClass(tmpnodedata.className); if (node.icon) { el.one('.fp-icon').appendChild(Y.Node.create('')); el.one('.fp-icon img').setImgSrc(node.icon, node.realicon, lazyloading); } // create node tmpnodedata.html = el.getContent(); var tmpNode = new Y.YUI2.widget.HTMLNode(tmpnodedata, level, false); if (node.dynamicLoadComplete) { tmpNode.dynamicLoadComplete = true; } tmpNode.fileinfo = node; tmpNode.isLeaf = !file_is_folder(node); if (!tmpNode.isLeaf) { if(node.expanded) { tmpNode.expand(); } tmpNode.path = node.path ? node.path : (node.filepath ? node.filepath : ''); for(var c in node.children) { build_tree(node.children[c], tmpNode); } } }; /** initialize tree view */ var initialize_tree_view = function() { var parentid = scope.one('.'+classname).get('id'); // TODO MDL-32736 use YUI3 gallery TreeView scope.treeview = new Y.YUI2.widget.TreeView(parentid); if (options.dynload) { scope.treeview.setDynamicLoad(Y.bind(options.treeview_dynload, options.callbackcontext), 1); } scope.treeview.singleNodeHighlight = true; if (options.filepath && options.filepath.length) { // we just jumped from icon/details view, we need to show all parents // we extract as much information as possible from filepath and filelist // and send additional requests to retrieve siblings for parent folders var mytree = {}; var mytreeel = null; for (var i in options.filepath) { if (mytreeel == null) { mytreeel = mytree; } else { mytreeel.children = [{}]; mytreeel = mytreeel.children[0]; } var pathelement = options.filepath[i]; mytreeel.path = pathelement.path; mytreeel.title = pathelement.name; mytreeel.icon = pathelement.icon; mytreeel.dynamicLoadComplete = true; // we will call it manually mytreeel.expanded = true; } mytreeel.children = fileslist; build_tree(mytree, scope.treeview.getRoot()); // manually call dynload for parent elements in the tree so we can load other siblings if (options.dynload) { var root = scope.treeview.getRoot(); // Whether search results are currently displayed in the active repository in the filepicker. // We do not want to load siblings of parent elements when displaying search tree results. var isSearchResult = typeof options.callbackcontext.active_repo !== 'undefined' && options.callbackcontext.active_repo.issearchresult; while (root && root.children && root.children.length) { root = root.children[0]; if (root.path == mytreeel.path) { root.origpath = options.filepath; root.origlist = fileslist; } else if (!root.isLeaf && root.expanded && !isSearchResult) { Y.bind(options.treeview_dynload, options.callbackcontext)(root, null); } } } } else { // there is no path information, just display all elements as a list, without hierarchy for(k in fileslist) { build_tree(fileslist[k], scope.treeview.getRoot()); } } scope.treeview.subscribe('clickEvent', function(e){ e.node.highlight(false); var callback = options.callback; if (options.rightclickcallback && e.event.target && Y.Node(e.event.target).ancestor('.fp-treeview .fp-contextmenu', true)) { callback = options.rightclickcallback; } Y.bind(callback, options.callbackcontext)(e, e.node.fileinfo); Y.YUI2.util.Event.stopEvent(e.event) }); // Simulate click on file not folder. scope.treeview.subscribe('enterKeyPressed', function(node) { if (node.children.length === 0) { Y.one(node.getContentEl()).one('a').simulate('click'); } }); // TODO MDL-32736 support right click /*if (options.rightclickcallback) { scope.treeview.subscribe('dblClickEvent', function(e){ e.node.highlight(false); Y.bind(options.rightclickcallback, options.callbackcontext)(e, e.node.fileinfo); }); }*/ scope.treeview.draw(); }; /** formatting function for table view */ var formatValue = function (o){ if (o.data[''+o.column.key+'_f_s']) {return o.data[''+o.column.key+'_f_s'];} else if (o.data[''+o.column.key+'_f']) {return o.data[''+o.column.key+'_f'];} else if (o.value) {return o.value;} else {return '';} }; /** formatting function for table view */ var formatTitle = function(o) { var el = Y.Node.create('
    '); el.appendChild(options.filenode.cloneNode(true)); // TODO not node but string! el.get('children').addClass(o.data['classname']); el.one('.fp-filename').setContent(o.value); if (o.data['icon']) { el.one('.fp-icon').appendChild(Y.Node.create('')); el.one('.fp-icon img').setImgSrc(o.data['icon'], o.data['realicon'], lazyloading); } if (options.rightclickcallback) { el.get('children').addClass('fp-hascontextmenu'); } // TODO add tooltip with o.data['title'] (o.value) or o.data['thumbnail_title'] return el.getContent(); } /** * Generate slave checkboxes based on toggleall's specification * @param {object} o An object reprsenting the record for the current row. * @return {html} The checkbox html */ var formatCheckbox = function(o) { var el = Y.Node.create('
    '); var parentid = scope.one('.' + classname).get('id'); var checkbox = Y.Node.create('') .setAttribute('type', 'checkbox') .setAttribute('data-fieldtype', 'checkbox') .setAttribute('data-fullname', o.data.fullname) .setAttribute('data-action', 'toggle') .setAttribute('data-toggle', 'slave') .setAttribute('data-togglegroup', 'file-selections-' + parentid); var checkboxLabel = Y.Node.create('