diff --git a/filter/oembed/CHANGES.txt b/filter/oembed/CHANGES.txt new file mode 100644 index 000000000..0b01d2d22 --- /dev/null +++ b/filter/oembed/CHANGES.txt @@ -0,0 +1,30 @@ +Release Notes + +Release 3.3.2 (Build - 2018120300) +Verified 3.6 compatibility. +Added an exception handler to ignore malformed regex expressions that the plugin has no control over. +Removed Office Mix tests as Office Mix has been discontinued. +Updated Behat tests to access site administration in the new way. +Updated Travis file to test multiple installs. + +Release 3.3.1 (Build - 2018051500) +Fixed a unit test issue for Vimeo. +An extra check to deal with potential malformed provider endpoint URL. + +For release 3.3.0, an xpath change was required in the Behat tests in order to get Behat to pass due to changes in Moodle's output. + +For release 3.2, the plugin was rewritten to utilize the oembed provider definitions from http://oembed.com/providers.json. +The previous provider definitions that were hardcoded, but not present in the oembed provider list, were added as "local" +definitions to avoid regression errors. + +Release 3.2.0.0 (Alpha) +Change highlights: +- Oembed providers defintions are downloaded from http://oembed.com/providers.json and stored in the Moodle database, rather than +code. +- Oembed defintions are refreshed nightly with new additions and deletions. +- Management screen allows for administrators to save provider defintions as local overrides not refreshed by oembed.com. +- Two types of tags can be used for filtering. The one desired can be configured. +- Lazy loading can be turned on or off; default is on. This can improve site performance. +- Provider management screen allows enable/disable, and edit as local as well as providing all of the provider information. +- A subplugin system is in place to allow providers not stored at oembed.com to define oembed information. Some of the existing +Microsoft providers have been rewritten as these. diff --git a/filter/oembed/Gruntfile.js b/filter/oembed/Gruntfile.js new file mode 100644 index 000000000..f40780d1f --- /dev/null +++ b/filter/oembed/Gruntfile.js @@ -0,0 +1,144 @@ +/** + * Gruntfile for compiling theme_bootstrap .less files. + * + * This file configures tasks to be run by Grunt + * http://gruntjs.com/ for the current theme. + * + * Requirements: + * nodejs, npm, grunt-cli. + * + * Installation: + * node and npm: instructions at http://nodejs.org/ + * grunt-cli: `[sudo] npm install -g grunt-cli` + * node dependencies: run `npm install` in the root directory. + * + * Usage: + * Default behaviour is to watch all .less files and compile + * into compressed CSS when a change is detected to any and then + * clear the theme's caches. Invoke either `grunt` or `grunt watch` + * in the theme's root directory. + * + * To separately compile only moodle or editor .less files + * run `grunt less:moodle` or `grunt less:editor` respectively. + * + * To only clear the theme caches invoke `grunt exec:decache` in + * the theme's root directory. + * + * @package filter + * @subpackage oembed + * @author Joby Harding / David Scotson / Stuart Lamour / Guy Thomas + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +module.exports = function(grunt) { + + // We need to include the core Moodle grunt file too, otherwise we can't run tasks like "amd". + require("grunt-load-gruntfile")(grunt); + grunt.loadGruntfile("../../Gruntfile.js"); + + // PHP strings for exec task. + var moodleroot = 'dirname(dirname(__DIR__))', + configfile = moodleroot + ' . "/config.php"', + decachephp = ''; + + decachephp += "define(\"CLI_SCRIPT\", true);"; + decachephp += "require(" + configfile + ");"; + + // The previously used theme_reset_all_caches() stopped working for us, we investigated but couldn't figure out why. + // Using purge_all_caches() is a bit of a nuclear option, as it clears more than we should need to + // but it gets the job done. + decachephp += "purge_all_caches();"; + + grunt.mergeConfig = grunt.config.merge; + + grunt.mergeConfig({ + sass: { + oembed: { + options: { + compress: false + }, + files: { + "styles.css": "sass/styles.scss", + } + } + }, + csslint: { + src: "styles.css", + options: { + "adjoining-classes": false, + "box-sizing": false, + "box-model": false, + "overqualified-elements": false, + "bulletproof-font-face": false, + "compatible-vendor-prefixes": false, + "selector-max-approaching": false, + "fallback-colors": false, + "floats": false, + "ids": false, + "qualified-headings": false, + "selector-max": false, + "unique-headings": false, + "gradients": false, + "important": false, + "font-sizes": false, + } + }, + cssbeautifier : { + files : ["styles.css"] + }, + autoprefixer: { + options: { + browsers: [ + 'Android 2.3', + 'Android >= 4', + 'Chrome >= 20', + 'Firefox >= 24', // Firefox 24 is the latest ESR. + 'Explorer >= 9', + 'iOS >= 6', + 'Opera >= 12.1', + 'Safari >= 6' + ] + }, + core: { + options: { + map: false + }, + src: ['styles.css'], + }, + }, + exec: { + decache: { + cmd: "php -r '" + decachephp + "'", + callback: function(error, stdout, stderror) { + // Exec will output error messages. + // Just add one to confirm success. + if (!error) { + grunt.log.writeln("Moodle theme cache reset."); + } + } + } + }, + watch: { + // Watch for any changes to sass files and compile. + files: ["sass/*.scss"], + tasks: ["compile"], + options: { + spawn: false + } + } + }); + + // Load contrib tasks. + grunt.loadNpmTasks("grunt-autoprefixer"); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-cssbeautifier'); + grunt.loadNpmTasks('grunt-contrib-csslint'); + grunt.loadNpmTasks("grunt-sass"); + grunt.loadNpmTasks("grunt-contrib-watch"); + grunt.loadNpmTasks("grunt-exec"); + + // Register tasks. + grunt.registerTask("default", ["watch"]); + grunt.registerTask("compile", ["sass:oembed", "autoprefixer", "cssbeautifier", "decache"]); + grunt.registerTask("decache", ["exec:decache"]); +}; diff --git a/filter/oembed/README.md b/filter/oembed/README.md deleted file mode 100644 index 3d19d210f..000000000 --- a/filter/oembed/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# oEmbed Filter - -This is a text filter for Moodle that converts urls from many different media sites into embeded content. -Embed code is retrieved from the original site so should work even if the site changes embed format. - -## Installation -1. Download the source files. (zip file is available under download section) -2. Unzip the package -3. Copy the "oembed" folder to moodle/filter on the Moodle server. -4. Login as an admin on the Moodle site and install the filter. - -## Usage -By default the oembed filter is enabled for all content. You can change this under Plugins > Filters. - -When inserting a media link url into a discussion, create a hyperlink and insert the url as the target. -When the discussion is posted the url will be changed into the embed content. -N.B. if you enable the "Convert URLs into links and images" filter ahead of this then it is easier for users to embed media. - -## Support - -If you are experiencing problems, have a feature request, or have a question, please open an issue on Github at https://github.com/Microsoft/o365-moodle. - -To help developers debug problems, please include the following in all issues: -- Plugin versions. -- Moodle version. -- Detailed instructions of what went wrong and how to reproduce the problem. -- Any error messages encountered. -- PHP version. -- Database software and versions. -- Any other environmental information available. - -Note that developers will triage issues and deal with more serious problems first. All issues will be addressed but some may not be addressed immediately. - -## Contributing - -We're looking for community contributions! Feel free to submit pull requests, but please do so against the development repository at https://github.com/Microsoft/o365-moodle. Pull requests submitted to individual plugin repositories cannot be accepted. - -### Needed Contributions -Smaller issues that developers cannot address right away will be labeled with "Help Wanted" in the issue tracker in the development repository at https://github.com/Microsoft/o365-moodle/issues. These are only suggestions - we can also accept pull requests fixing other bugs, or even adding new features. - -Pull requests adding new features are much appreciated but note that they may be rejected (even if technically sound) if they do not match the direction of the project. If you want to add a new feature, it's best to open an issue outlining your idea first, and get feedback from the maintainers. - -Contributions to our documentation are especially appreciated! All documentation lives in the /local/o365docs folder of the development repository (https://github.com/Microsoft/o365-moodle). Updates to this documentation can be sent via pull request like any other contributions. - -### Code Review -All pull requests go through a thorough examination from developers before they are merged. Please read our [code review process](https://github.com/Microsoft/o365-moodle/tree/master/local/o365docs/codereview.md) and ensure your code is consistent before submitting. A developer may respond with changes that are needed before a pull request can be accepted and it is up to the submitter to make those changes. If accepted, your commit will remain as-is to ensure you get credit, but developers may modify solutions slightly in subsequent commits. - -### CLA -Finally, before we can accept your pull request, you'll need to electronically complete Microsoft's [Contributor License Agreement](https://cla.microsoft.com/). If you've done this for other Microsoft projects, then you're already covered. - -[Why a CLA?](https://www.gnu.org/licenses/why-assign.html) (from the FSF) - diff --git a/filter/oembed/README.txt b/filter/oembed/README.txt new file mode 100644 index 000000000..11f8fccc9 --- /dev/null +++ b/filter/oembed/README.txt @@ -0,0 +1,33 @@ +Description: +This is a text filter for Moodle that converts urls from many different media sites into embeded content. +Embed code is retrieved from the original site so should work even if the site changes embed format. + +Installation: +Download the source files. (zip file is available under download section) +Unzip the package +Copy the "oembed" folder to moodle/filter on the Moodle server. +Login as an admin on the Moodle site and install the filter. + +Upgrading from earlier versions: +Upgrade per normal procedures. Your settings from earlier plugins will be preserved. +NOTE - Embed providers may change the text that identifies them. It is possible that media embedded previously on your site no +longer meets the provider text definitions, and as such, may not show up as embedded media. Check the provider definition to see +if the media link needs to change. + +To use: +Under Plugins > Filters > Oembed Filter / Settings, you can choose: + - The type of tag to identify the embedded media. + - To delay the media loading or load it immediately. +By default the oembed filter disables all providers. +You can change this under Plugins > Filters > Oembed Filter / Manage providers. + +When inserting a media link url into a discussion, create a hyperlink and insert the url as the target. +When the discussion is posted the url will be changed into the embed content. +N.B. if you enable the "Convert URLs into links and images" filter ahead of this then it is easier for users to embed media. + +The embedded media providers are in three groups: + - Downloaded from http://oembed.com/providers.json. This is the main repository that manages Oembed provider definitions. + These are updated regularly in the cron job, and can change. + - Plugins provided to extend media providers provided in earlier versions of the plugin, but not contained in the provider repo. + - Local providers which allow a site administrator to save a downloaded one locally, so that it does not change with download + updates. This also allows new providers to be created that are not part of the omebed repo. diff --git a/filter/oembed/amd/build/list.min.js b/filter/oembed/amd/build/list.min.js new file mode 100644 index 000000000..81ebe6260 --- /dev/null +++ b/filter/oembed/amd/build/list.min.js @@ -0,0 +1 @@ +!function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;gr.page,h=new s(a[f],d,e),r.items.push(h),c.push(h)}return r.update(),c}},this.show=function(a,b){return this.i=a,this.page=b,r.update(),r},this.remove=function(a,b,c){for(var d=0,e=0,f=r.items.length;e-1&&c.splice(d,1),r},this.trigger=function(a){for(var b=r.handlers[a].length;b--;)r.handlers[a][b](r);return r},this.reset={filter:function(){for(var a=r.items,b=a.length;b--;)a[b].filtered=!1;return r},search:function(){for(var a=r.items,b=a.length;b--;)a[b].found=!1;return r}},this.update=function(){var a=r.items,b=a.length;r.visibleItems=[],r.matchingItems=[],r.templater.clear();for(var c=0;c=r.i&&r.visibleItems.length0?setTimeout(function(){b(c,d,e)},1):(a.update(),d(e))};return b}},{}],3:[function(a,b,c){b.exports=function(a){return a.handlers.filterStart=a.handlers.filterStart||[],a.handlers.filterComplete=a.handlers.filterComplete||[],function(b){if(a.trigger("filterStart"),a.i=1,a.reset.filter(),void 0===b)a.filtered=!1;else{a.filtered=!0;for(var c=a.items,d=0,e=c.length;d0?setTimeout(function(){f(a,c)},1):(b.update(),b.trigger("parseComplete"))};return b.handlers.parseComplete=b.handlers.parseComplete||[],function(){var a=d(b.list),c=b.valueNames;b.indexAsync?f(a,c):e(a,c)}}},{"./item":4}],6:[function(a,b,c){b.exports=function(a){var b,c,d,e,f={resetList:function(){a.i=1,a.templater.clear(),e=void 0},setOptions:function(a){2==a.length&&a[1]instanceof Array?c=a[1]:2==a.length&&"function"==typeof a[1]?e=a[1]:3==a.length&&(c=a[1],e=a[2])},setColumns:function(){0!==a.items.length&&void 0===c&&(c=void 0===a.searchColumns?f.toArray(a.items[0].values()):a.searchColumns)},setSearchString:function(b){b=a.utils.toString(b).toLowerCase(),b=b.replace(/[-[\]{}()*+?.,\\^$|#]/g,"\\$&"),d=b},toArray:function(a){var b=[];for(var c in a)b.push(c);return b}},g={list:function(){for(var b=0,c=a.items.length;b-1))},reset:function(){a.reset.search(),a.searched=!1}},h=function(b){return a.trigger("searchStart"),f.resetList(),f.setSearchString(b),f.setOptions(arguments),f.setColumns(),""===d?g.reset():(a.searched=!0,e?e(d,c):g.list()),a.update(),a.trigger("searchComplete"),a.visibleItems};return a.handlers.searchStart=a.handlers.searchStart||[],a.handlers.searchComplete=a.handlers.searchComplete||[],a.utils.events.bind(a.utils.getByClass(a.listContainer,a.searchClass),"keyup",function(b){var c=b.target||b.srcElement,d=""===c.value&&!a.searched;d||h(c.value)}),a.utils.events.bind(a.utils.getByClass(a.listContainer,a.searchClass),"input",function(a){var b=a.target||a.srcElement;""===b.value&&h("")}),h}},{}],7:[function(a,b,c){b.exports=function(a){a.sortFunction=a.sortFunction||function(b,c,d){return d.desc="desc"==d.order,a.utils.naturalSort(b.values()[d.valueName],c.values()[d.valueName],d)};var b={els:void 0,clear:function(){for(var c=0,d=b.els.length;c]/.exec(b)){var f=document.createElement("table");return f.innerHTML=b,f.firstChild}if(b.indexOf("<")!==-1){var g=document.createElement("div");return g.innerHTML=b,g.firstChild}var h=document.getElementById(a.item);if(h)return h}throw new Error("The list need to have at list one item on init otherwise you'll have to add a template.")},this.get=function(b,d){c.create(b);for(var e={},f=0,g=d.length;f=1;)a.list.removeChild(a.list.firstChild)},d()};b.exports=function(a){return new d(a)}},{}],9:[function(a,b,c){function d(a){if(!a||!a.nodeType)throw new Error("A DOM element reference is required");this.el=a,this.list=a.classList}var e=a("./index-of"),f=/\s+/,g=Object.prototype.toString;b.exports=function(a){return new d(a)},d.prototype.add=function(a){if(this.list)return this.list.add(a),this;var b=this.array(),c=e(b,a);return~c||b.push(a),this.el.className=b.join(" "),this},d.prototype.remove=function(a){if("[object RegExp]"==g.call(a))return this.removeMatching(a);if(this.list)return this.list.remove(a),this;var b=this.array(),c=e(b,a);return~c&&b.splice(c,1),this.el.className=b.join(" "),this},d.prototype.removeMatching=function(a){for(var b=this.array(),c=0;cs)return 1}for(var u=0,v=p.length,w=q.length,x=Math.max(v,w);ue)return 1}return 0}},{}],16:[function(a,b,c){function d(a){return"[object Array]"===Object.prototype.toString.call(a)}b.exports=function(a){if("undefined"==typeof a)return[];if(null===a)return[null];if(a===window)return[window];if("string"==typeof a)return[a];if(d(a))return a;if("number"!=typeof a.length)return[a];if("function"==typeof a&&a instanceof Function)return[a];for(var b=[],c=0;c-1){var k=d+" .js-oembed-newprovider",l=a(k);l.length&&(g=l.data("newproviderid"))}var m=function(){var b=a("#oembed-display-providers_"+g+" td");a(b).append(j),a(b).find(" div.alert-success").attr("tabindex",-1),a(b).find(" div.alert-success").focus()};i.indexOf("download::")>-1?b.reloadProviders(m):b.reloadRow(g,e,"reload",m)}})}),a("#providermanagement").on("click",".oembed-provider-details form #id_cancel",function(b){b.preventDefault();var d=a(this).parents("tr")[0];c(a(d).data("pid"))})},init:function(){var a={valueNames:["list-providername"]};new g("providermanagement",a),this.listenEnableDisable(),this.listenDelete(),this.listenEdit()}}}); \ No newline at end of file diff --git a/filter/oembed/amd/build/oembed.min.js b/filter/oembed/amd/build/oembed.min.js new file mode 100644 index 000000000..08e0b0103 --- /dev/null +++ b/filter/oembed/amd/build/oembed.min.js @@ -0,0 +1 @@ +define(["jquery","filter_oembed/preloader","filter_oembed/responsivecontent"],function(a,b,c){return{init:function(){var d=function(){var b=function(b){return!!b.className&&a(b).is(".oembed-content, .oembed-card-container")},d=new MutationObserver(function(d){d.forEach(function(d){for(var e in d.addedNodes){var f=d.addedNodes[e];b(f)&&c.apply(a(f).find("> *:not(video):first-child, .oembed-card"))}})}),e={attributes:!0,childList:!0,characterData:!0,subtree:!0},f=document.body;d.observe(f,e)};d(),a(document).ready(function(){b.apply(),c.apply()})}}}); \ No newline at end of file diff --git a/filter/oembed/amd/build/preloader.min.js b/filter/oembed/amd/build/preloader.min.js new file mode 100644 index 000000000..6a29e6c27 --- /dev/null +++ b/filter/oembed/amd/build/preloader.min.js @@ -0,0 +1 @@ +define(["jquery"],function(a){return{apply:function(){a(".oembed-card-play").on("click",function(){var b=a(this).parent(".oembed-card"),c=a(b.data("embed")),d=a(b).width(),e=a(b).height();if(a(c).find("iframe").length){var f=a(a(c).find("iframe")[0]),g=f.attr("src"),h=g.indexOf("?")>-1?"&":"?";g+=h+"autoplay=1",g+="&auto_play=1",f.attr("src",g)}c.attr("data-card-width",d),c.attr("data-card-height",e),b.parent(".oembed-card-container").replaceWith(c)})}}}); \ No newline at end of file diff --git a/filter/oembed/amd/build/responsivecontent.min.js b/filter/oembed/amd/build/responsivecontent.min.js new file mode 100644 index 000000000..b8c4c1d92 --- /dev/null +++ b/filter/oembed/amd/build/responsivecontent.min.js @@ -0,0 +1 @@ +define(["jquery"],function(a){var b=function(){this.apply=function(b){if(!b){var c=".oembed-content:not(.oembed-responsive) > *:not(video):first-child,";c+=" .oembed-card:not(.oembed-processed)",b=a(c)}a(b).each(function(){var b=a(this).parent();if(!b.hasClass("oembed-responsive")){var c,d,e;e=this.getAttribute("data-aspect-ratio"),null!==e&&"0"!==e||(c=this.width||this.offsetWidth,d=this.height||this.offsetHeight,(c.indexOf("%")>-1&&d.indexOf("%")==-1||c.indexOf("%")==-1&&d.indexOf("%")>-1)&&(a(this).parent().attr("data-card-width")&&a(this).parent().attr("data-card-height")?(c=a(this).parent().attr("data-card-width"),d=a(this).parent().attr("data-card-height")):(c=this.offsetWidth,d=this.offsetHeight)),c=parseInt(c),d=parseInt(d),e=d/c,this.setAttribute("data-aspect-ratio",e));var f=this.tagName.toLowerCase();"iframe"===f&&(a(this).removeAttr("width"),a(this).removeAttr("height")),c=parseInt(this.offsetWidth);var g={width:"100%"};if(a(this).css(g),!b.find(".oembed-responsive-pad").length){var h=100*e,i='
';b.append(i)}b.addClass("oembed-responsive")}})}};return new b}); \ No newline at end of file diff --git a/filter/oembed/amd/src/list.js b/filter/oembed/amd/src/list.js new file mode 100644 index 000000000..09d566a87 --- /dev/null +++ b/filter/oembed/amd/src/list.js @@ -0,0 +1,1254 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o self.page) ? true : false; + item = new Item(values[i], undefined, notCreate); + self.items.push(item); + added.push(item); + } + self.update(); + return added; + }; + + this.show = function(i, page) { + this.i = i; + this.page = page; + self.update(); + return self; + }; + + /* Removes object from list. + * Loops through the list and removes objects where + * property "valuename" === value + */ + this.remove = function(valueName, value, options) { + var found = 0; + for (var i = 0, il = self.items.length; i < il; i++) { + if (self.items[i].values()[valueName] == value) { + self.templater.remove(self.items[i], options); + self.items.splice(i,1); + il--; + i--; + found++; + } + } + self.update(); + return found; + }; + + /* Gets the objects in the list which + * property "valueName" === value + */ + this.get = function(valueName, value) { + var matchedItems = []; + for (var i = 0, il = self.items.length; i < il; i++) { + var item = self.items[i]; + if (item.values()[valueName] == value) { + matchedItems.push(item); + } + } + return matchedItems; + }; + + /* + * Get size of the list + */ + this.size = function() { + return self.items.length; + }; + + /* + * Removes all items from the list + */ + this.clear = function() { + self.templater.clear(); + self.items = []; + return self; + }; + + this.on = function(event, callback) { + self.handlers[event].push(callback); + return self; + }; + + this.off = function(event, callback) { + var e = self.handlers[event]; + var index = indexOf(e, callback); + if (index > -1) { + e.splice(index, 1); + } + return self; + }; + + this.trigger = function(event) { + var i = self.handlers[event].length; + while(i--) { + self.handlers[event][i](self); + } + return self; + }; + + this.reset = { + filter: function() { + var is = self.items, + il = is.length; + while (il--) { + is[il].filtered = false; + } + return self; + }, + search: function() { + var is = self.items, + il = is.length; + while (il--) { + is[il].found = false; + } + return self; + } + }; + + this.update = function() { + var is = self.items, + il = is.length; + + self.visibleItems = []; + self.matchingItems = []; + self.templater.clear(); + for (var i = 0; i < il; i++) { + if (is[i].matching() && ((self.matchingItems.length+1) >= self.i && self.visibleItems.length < self.page)) { + is[i].show(); + self.visibleItems.push(is[i]); + self.matchingItems.push(is[i]); + } else if (is[i].matching()) { + self.matchingItems.push(is[i]); + is[i].hide(); + } else { + is[i].hide(); + } + } + self.trigger('updated'); + return self; + }; + + init.start(); + }; + + + // AMD support + if (typeof define === 'function' && define.amd) { + define(function () { return List; }); + } + module.exports = List; + window.List = List; + + })(window); + +},{"./src/add-async":2,"./src/filter":3,"./src/item":4,"./src/parse":5,"./src/search":6,"./src/sort":7,"./src/templater":8,"./src/utils/classes":9,"./src/utils/events":10,"./src/utils/extend":11,"./src/utils/get-attribute":12,"./src/utils/get-by-class":13,"./src/utils/index-of":14,"./src/utils/natural-sort":15,"./src/utils/to-array":16,"./src/utils/to-string":17}],2:[function(require,module,exports){ + module.exports = function(list) { + var addAsync = function(values, callback, items) { + var valuesToAdd = values.splice(0, 50); + items = items || []; + items = items.concat(list.add(valuesToAdd)); + if (values.length > 0) { + setTimeout(function() { + addAsync(values, callback, items); + }, 1); + } else { + list.update(); + callback(items); + } + }; + return addAsync; + }; + +},{}],3:[function(require,module,exports){ + module.exports = function(list) { + + // Add handlers + list.handlers.filterStart = list.handlers.filterStart || []; + list.handlers.filterComplete = list.handlers.filterComplete || []; + + return function(filterFunction) { + list.trigger('filterStart'); + list.i = 1; // Reset paging + list.reset.filter(); + if (filterFunction === undefined) { + list.filtered = false; + } else { + list.filtered = true; + var is = list.items; + for (var i = 0, il = is.length; i < il; i++) { + var item = is[i]; + if (filterFunction(item)) { + item.filtered = true; + } else { + item.filtered = false; + } + } + } + list.update(); + list.trigger('filterComplete'); + return list.visibleItems; + }; + }; + +},{}],4:[function(require,module,exports){ + module.exports = function(list) { + return function(initValues, element, notCreate) { + var item = this; + + this._values = {}; + + this.found = false; // Show if list.searched == true and this.found == true + this.filtered = false;// Show if list.filtered == true and this.filtered == true + + var init = function(initValues, element, notCreate) { + if (element === undefined) { + if (notCreate) { + item.values(initValues, notCreate); + } else { + item.values(initValues); + } + } else { + item.elm = element; + var values = list.templater.get(item, initValues); + item.values(values); + } + }; + + this.values = function(newValues, notCreate) { + if (newValues !== undefined) { + for(var name in newValues) { + item._values[name] = newValues[name]; + } + if (notCreate !== true) { + list.templater.set(item, item.values()); + } + } else { + return item._values; + } + }; + + this.show = function() { + list.templater.show(item); + }; + + this.hide = function() { + list.templater.hide(item); + }; + + this.matching = function() { + return ( + (list.filtered && list.searched && item.found && item.filtered) || + (list.filtered && !list.searched && item.filtered) || + (!list.filtered && list.searched && item.found) || + (!list.filtered && !list.searched) + ); + }; + + this.visible = function() { + return (item.elm && (item.elm.parentNode == list.list)) ? true : false; + }; + + init(initValues, element, notCreate); + }; + }; + +},{}],5:[function(require,module,exports){ + module.exports = function(list) { + + var Item = require('./item')(list); + + var getChildren = function(parent) { + var nodes = parent.childNodes, + items = []; + for (var i = 0, il = nodes.length; i < il; i++) { + // Only textnodes have a data attribute + if (nodes[i].data === undefined) { + items.push(nodes[i]); + } + } + return items; + }; + + var parse = function(itemElements, valueNames) { + for (var i = 0, il = itemElements.length; i < il; i++) { + list.items.push(new Item(valueNames, itemElements[i])); + } + }; + var parseAsync = function(itemElements, valueNames) { + var itemsToIndex = itemElements.splice(0, 50); // TODO: If < 100 items, what happens in IE etc? + parse(itemsToIndex, valueNames); + if (itemElements.length > 0) { + setTimeout(function() { + parseAsync(itemElements, valueNames); + }, 1); + } else { + list.update(); + list.trigger('parseComplete'); + } + }; + + list.handlers.parseComplete = list.handlers.parseComplete || []; + + return function() { + var itemsToIndex = getChildren(list.list), + valueNames = list.valueNames; + + if (list.indexAsync) { + parseAsync(itemsToIndex, valueNames); + } else { + parse(itemsToIndex, valueNames); + } + }; + }; + +},{"./item":4}],6:[function(require,module,exports){ + module.exports = function(list) { + var item, + text, + columns, + searchString, + customSearch; + + var prepare = { + resetList: function() { + list.i = 1; + list.templater.clear(); + customSearch = undefined; + }, + setOptions: function(args) { + if (args.length == 2 && args[1] instanceof Array) { + columns = args[1]; + } else if (args.length == 2 && typeof(args[1]) == "function") { + customSearch = args[1]; + } else if (args.length == 3) { + columns = args[1]; + customSearch = args[2]; + } + }, + setColumns: function() { + if (list.items.length === 0) return; + if (columns === undefined) { + columns = (list.searchColumns === undefined) ? prepare.toArray(list.items[0].values()) : list.searchColumns; + } + }, + setSearchString: function(s) { + s = list.utils.toString(s).toLowerCase(); + s = s.replace(/[-[\]{}()*+?.,\\^$|#]/g, "\\$&"); // Escape regular expression characters + searchString = s; + }, + toArray: function(values) { + var tmpColumn = []; + for (var name in values) { + tmpColumn.push(name); + } + return tmpColumn; + } + }; + var search = { + list: function() { + for (var k = 0, kl = list.items.length; k < kl; k++) { + search.item(list.items[k]); + } + }, + item: function(item) { + item.found = false; + for (var j = 0, jl = columns.length; j < jl; j++) { + if (search.values(item.values(), columns[j])) { + item.found = true; + return; + } + } + }, + values: function(values, column) { + if (values.hasOwnProperty(column)) { + text = list.utils.toString(values[column]).toLowerCase(); + if ((searchString !== "") && (text.search(searchString) > -1)) { + return true; + } + } + return false; + }, + reset: function() { + list.reset.search(); + list.searched = false; + } + }; + + var searchMethod = function(str) { + list.trigger('searchStart'); + + prepare.resetList(); + prepare.setSearchString(str); + prepare.setOptions(arguments); // str, cols|searchFunction, searchFunction + prepare.setColumns(); + + if (searchString === "" ) { + search.reset(); + } else { + list.searched = true; + if (customSearch) { + customSearch(searchString, columns); + } else { + search.list(); + } + } + + list.update(); + list.trigger('searchComplete'); + return list.visibleItems; + }; + + list.handlers.searchStart = list.handlers.searchStart || []; + list.handlers.searchComplete = list.handlers.searchComplete || []; + + list.utils.events.bind(list.utils.getByClass(list.listContainer, list.searchClass), 'keyup', function(e) { + var target = e.target || e.srcElement, // IE have srcElement + alreadyCleared = (target.value === "" && !list.searched); + if (!alreadyCleared) { // If oninput already have resetted the list, do nothing + searchMethod(target.value); + } + }); + + // Used to detect click on HTML5 clear button + list.utils.events.bind(list.utils.getByClass(list.listContainer, list.searchClass), 'input', function(e) { + var target = e.target || e.srcElement; + if (target.value === "") { + searchMethod(''); + } + }); + + return searchMethod; + }; + +},{}],7:[function(require,module,exports){ + module.exports = function(list) { + list.sortFunction = list.sortFunction || function(itemA, itemB, options) { + options.desc = options.order == "desc" ? true : false; // Natural sort uses this format + return list.utils.naturalSort(itemA.values()[options.valueName], itemB.values()[options.valueName], options); + }; + + var buttons = { + els: undefined, + clear: function() { + for (var i = 0, il = buttons.els.length; i < il; i++) { + list.utils.classes(buttons.els[i]).remove('asc'); + list.utils.classes(buttons.els[i]).remove('desc'); + } + }, + getOrder: function(btn) { + var predefinedOrder = list.utils.getAttribute(btn, 'data-order'); + if (predefinedOrder == "asc" || predefinedOrder == "desc") { + return predefinedOrder; + } else if (list.utils.classes(btn).has('desc')) { + return "asc"; + } else if (list.utils.classes(btn).has('asc')) { + return "desc"; + } else { + return "asc"; + } + }, + getInSensitive: function(btn, options) { + var insensitive = list.utils.getAttribute(btn, 'data-insensitive'); + if (insensitive === "false") { + options.insensitive = false; + } else { + options.insensitive = true; + } + }, + setOrder: function(options) { + for (var i = 0, il = buttons.els.length; i < il; i++) { + var btn = buttons.els[i]; + if (list.utils.getAttribute(btn, 'data-sort') !== options.valueName) { + continue; + } + var predefinedOrder = list.utils.getAttribute(btn, 'data-order'); + if (predefinedOrder == "asc" || predefinedOrder == "desc") { + if (predefinedOrder == options.order) { + list.utils.classes(btn).add(options.order); + } + } else { + list.utils.classes(btn).add(options.order); + } + } + } + }; + var sort = function() { + list.trigger('sortStart'); + var options = {}; + + var target = arguments[0].currentTarget || arguments[0].srcElement || undefined; + + if (target) { + options.valueName = list.utils.getAttribute(target, 'data-sort'); + buttons.getInSensitive(target, options); + options.order = buttons.getOrder(target); + } else { + options = arguments[1] || options; + options.valueName = arguments[0]; + options.order = options.order || "asc"; + options.insensitive = (typeof options.insensitive == "undefined") ? true : options.insensitive; + } + buttons.clear(); + buttons.setOrder(options); + + options.sortFunction = options.sortFunction || list.sortFunction; + list.items.sort(function(a, b) { + var mult = (options.order === 'desc') ? -1 : 1; + return (options.sortFunction(a, b, options) * mult); + }); + list.update(); + list.trigger('sortComplete'); + }; + + // Add handlers + list.handlers.sortStart = list.handlers.sortStart || []; + list.handlers.sortComplete = list.handlers.sortComplete || []; + + buttons.els = list.utils.getByClass(list.listContainer, list.sortClass); + list.utils.events.bind(buttons.els, 'click', sort); + list.on('searchStart', buttons.clear); + list.on('filterStart', buttons.clear); + + return sort; + }; + +},{}],8:[function(require,module,exports){ + var Templater = function(list) { + var itemSource, + templater = this; + + var init = function() { + itemSource = templater.getItemSource(list.item); + itemSource = templater.clearSourceItem(itemSource, list.valueNames); + }; + + this.clearSourceItem = function(el, valueNames) { + for(var i = 0, il = valueNames.length; i < il; i++) { + var elm; + if (valueNames[i].data) { + for (var j = 0, jl = valueNames[i].data.length; j < jl; j++) { + el.setAttribute('data-'+valueNames[i].data[j], ''); + } + } else if (valueNames[i].attr && valueNames[i].name) { + elm = list.utils.getByClass(el, valueNames[i].name, true); + if (elm) { + elm.setAttribute(valueNames[i].attr, ""); + } + } else { + elm = list.utils.getByClass(el, valueNames[i], true); + if (elm) { + elm.innerHTML = ""; + } + } + elm = undefined; + } + return el; + }; + + this.getItemSource = function(item) { + if (item === undefined) { + var nodes = list.list.childNodes, + items = []; + + for (var i = 0, il = nodes.length; i < il; i++) { + // Only textnodes have a data attribute + if (nodes[i].data === undefined) { + return nodes[i].cloneNode(true); + } + } + } else if (/^tr[\s>]/.exec(item)) { + var table = document.createElement('table'); + table.innerHTML = item; + return table.firstChild; + } else if (item.indexOf("<") !== -1) { + var div = document.createElement('div'); + div.innerHTML = item; + return div.firstChild; + } else { + var source = document.getElementById(list.item); + if (source) { + return source; + } + } + throw new Error("The list need to have at list one item on init otherwise you'll have to add a template."); + }; + + this.get = function(item, valueNames) { + templater.create(item); + var values = {}; + for(var i = 0, il = valueNames.length; i < il; i++) { + var elm; + if (valueNames[i].data) { + for (var j = 0, jl = valueNames[i].data.length; j < jl; j++) { + values[valueNames[i].data[j]] = list.utils.getAttribute(item.elm, 'data-'+valueNames[i].data[j]); + } + } else if (valueNames[i].attr && valueNames[i].name) { + elm = list.utils.getByClass(item.elm, valueNames[i].name, true); + values[valueNames[i].name] = elm ? list.utils.getAttribute(elm, valueNames[i].attr) : ""; + } else { + elm = list.utils.getByClass(item.elm, valueNames[i], true); + values[valueNames[i]] = elm ? elm.innerHTML : ""; + } + elm = undefined; + } + return values; + }; + + this.set = function(item, values) { + var getValueName = function(name) { + for (var i = 0, il = list.valueNames.length; i < il; i++) { + if (list.valueNames[i].data) { + var data = list.valueNames[i].data; + for (var j = 0, jl = data.length; j < jl; j++) { + if (data[j] === name) { + return { data: name }; + } + } + } else if (list.valueNames[i].attr && list.valueNames[i].name && list.valueNames[i].name == name) { + return list.valueNames[i]; + } else if (list.valueNames[i] === name) { + return name; + } + } + }; + var setValue = function(name, value) { + var elm; + var valueName = getValueName(name); + if (!valueName) + return; + if (valueName.data) { + item.elm.setAttribute('data-'+valueName.data, value); + } else if (valueName.attr && valueName.name) { + elm = list.utils.getByClass(item.elm, valueName.name, true); + if (elm) { + elm.setAttribute(valueName.attr, value); + } + } else { + elm = list.utils.getByClass(item.elm, valueName, true); + if (elm) { + elm.innerHTML = value; + } + } + elm = undefined; + }; + if (!templater.create(item)) { + for(var v in values) { + if (values.hasOwnProperty(v)) { + setValue(v, values[v]); + } + } + } + }; + + this.create = function(item) { + if (item.elm !== undefined) { + return false; + } + /* If item source does not exists, use the first item in list as + source for new items */ + var newItem = itemSource.cloneNode(true); + newItem.removeAttribute('id'); + item.elm = newItem; + templater.set(item, item.values()); + return true; + }; + this.remove = function(item) { + if (item.elm.parentNode === list.list) { + list.list.removeChild(item.elm); + } + }; + this.show = function(item) { + templater.create(item); + list.list.appendChild(item.elm); + }; + this.hide = function(item) { + if (item.elm !== undefined && item.elm.parentNode === list.list) { + list.list.removeChild(item.elm); + } + }; + this.clear = function() { + /* .innerHTML = ''; fucks up IE */ + if (list.list.hasChildNodes()) { + while (list.list.childNodes.length >= 1) + { + list.list.removeChild(list.list.firstChild); + } + } + }; + + init(); + }; + + module.exports = function(list) { + return new Templater(list); + }; + +},{}],9:[function(require,module,exports){ + /** + * Module dependencies. + */ + + var index = require('./index-of'); + + /** + * Whitespace regexp. + */ + + var re = /\s+/; + + /** + * toString reference. + */ + + var toString = Object.prototype.toString; + + /** + * Wrap `el` in a `ClassList`. + * + * @param {Element} el + * @return {ClassList} + * @api public + */ + + module.exports = function(el){ + return new ClassList(el); + }; + + /** + * Initialize a new ClassList for `el`. + * + * @param {Element} el + * @api private + */ + + function ClassList(el) { + if (!el || !el.nodeType) { + throw new Error('A DOM element reference is required'); + } + this.el = el; + this.list = el.classList; + } + + /** + * Add class `name` if not already present. + * + * @param {String} name + * @return {ClassList} + * @api public + */ + + ClassList.prototype.add = function(name){ + // classList + if (this.list) { + this.list.add(name); + return this; + } + + // fallback + var arr = this.array(); + var i = index(arr, name); + if (!~i) arr.push(name); + this.el.className = arr.join(' '); + return this; + }; + + /** + * Remove class `name` when present, or + * pass a regular expression to remove + * any which match. + * + * @param {String|RegExp} name + * @return {ClassList} + * @api public + */ + + ClassList.prototype.remove = function(name){ + if ('[object RegExp]' == toString.call(name)) { + return this.removeMatching(name); + } + + // classList + if (this.list) { + this.list.remove(name); + return this; + } + + // fallback + var arr = this.array(); + var i = index(arr, name); + if (~i) arr.splice(i, 1); + this.el.className = arr.join(' '); + return this; + }; + + /** + * Remove all classes matching `re`. + * + * @param {RegExp} re + * @return {ClassList} + * @api private + */ + + ClassList.prototype.removeMatching = function(re){ + var arr = this.array(); + for (var i = 0; i < arr.length; i++) { + if (re.test(arr[i])) { + this.remove(arr[i]); + } + } + return this; + }; + + /** + * Toggle class `name`, can force state via `force`. + * + * For browsers that support classList, but do not support `force` yet, + * the mistake will be detected and corrected. + * + * @param {String} name + * @param {Boolean} force + * @return {ClassList} + * @api public + */ + + ClassList.prototype.toggle = function(name, force){ + // classList + if (this.list) { + if ("undefined" !== typeof force) { + if (force !== this.list.toggle(name, force)) { + this.list.toggle(name); // toggle again to correct + } + } else { + this.list.toggle(name); + } + return this; + } + + // fallback + if ("undefined" !== typeof force) { + if (!force) { + this.remove(name); + } else { + this.add(name); + } + } else { + if (this.has(name)) { + this.remove(name); + } else { + this.add(name); + } + } + + return this; + }; + + /** + * Return an array of classes. + * + * @return {Array} + * @api public + */ + + ClassList.prototype.array = function(){ + var className = this.el.getAttribute('class') || ''; + var str = className.replace(/^\s+|\s+$/g, ''); + var arr = str.split(re); + if ('' === arr[0]) arr.shift(); + return arr; + }; + + /** + * Check if class `name` is present. + * + * @param {String} name + * @return {ClassList} + * @api public + */ + + ClassList.prototype.has = + ClassList.prototype.contains = function(name){ + return this.list ? this.list.contains(name) : !! ~index(this.array(), name); + }; + +},{"./index-of":14}],10:[function(require,module,exports){ + var bind = window.addEventListener ? 'addEventListener' : 'attachEvent', + unbind = window.removeEventListener ? 'removeEventListener' : 'detachEvent', + prefix = bind !== 'addEventListener' ? 'on' : '', + toArray = require('./to-array'); + + /** + * Bind `el` event `type` to `fn`. + * + * @param {Element} el, NodeList, HTMLCollection or Array + * @param {String} type + * @param {Function} fn + * @param {Boolean} capture + * @api public + */ + + exports.bind = function(el, type, fn, capture){ + el = toArray(el); + for ( var i = 0; i < el.length; i++ ) { + el[i][bind](prefix + type, fn, capture || false); + } + }; + + /** + * Unbind `el` event `type`'s callback `fn`. + * + * @param {Element} el, NodeList, HTMLCollection or Array + * @param {String} type + * @param {Function} fn + * @param {Boolean} capture + * @api public + */ + + exports.unbind = function(el, type, fn, capture){ + el = toArray(el); + for ( var i = 0; i < el.length; i++ ) { + el[i][unbind](prefix + type, fn, capture || false); + } + }; + +},{"./to-array":16}],11:[function(require,module,exports){ + /* + * Source: https://github.com/segmentio/extend + */ + + module.exports = function extend (object) { + // Takes an unlimited number of extenders. + var args = Array.prototype.slice.call(arguments, 1); + + // For each extender, copy their properties on our object. + for (var i = 0, source; source = args[i]; i++) { + if (!source) continue; + for (var property in source) { + object[property] = source[property]; + } + } + + return object; + }; + +},{}],12:[function(require,module,exports){ + /** + * A cross-browser implementation of getAttribute. + * Source found here: http://stackoverflow.com/a/3755343/361337 written by Vivin Paliath + * + * Return the value for `attr` at `element`. + * + * @param {Element} el + * @param {String} attr + * @api public + */ + + module.exports = function(el, attr) { + var result = (el.getAttribute && el.getAttribute(attr)) || null; + if( !result ) { + var attrs = el.attributes; + var length = attrs.length; + for(var i = 0; i < length; i++) { + if (attr[i] !== undefined) { + if(attr[i].nodeName === attr) { + result = attr[i].nodeValue; + } + } + } + } + return result; + }; + +},{}],13:[function(require,module,exports){ + /** + * A cross-browser implementation of getElementsByClass. + * Heavily based on Dustin Diaz's function: http://dustindiaz.com/getelementsbyclass. + * + * Find all elements with class `className` inside `container`. + * Use `single = true` to increase performance in older browsers + * when only one element is needed. + * + * @param {String} className + * @param {Element} container + * @param {Boolean} single + * @api public + */ + + module.exports = (function() { + if (document.getElementsByClassName) { + return function(container, className, single) { + if (single) { + return container.getElementsByClassName(className)[0]; + } else { + return container.getElementsByClassName(className); + } + }; + } else if (document.querySelector) { + return function(container, className, single) { + className = '.' + className; + if (single) { + return container.querySelector(className); + } else { + return container.querySelectorAll(className); + } + }; + } else { + return function(container, className, single) { + var classElements = [], + tag = '*'; + if (container === null) { + container = document; + } + var els = container.getElementsByTagName(tag); + var elsLen = els.length; + var pattern = new RegExp("(^|\\s)"+className+"(\\s|$)"); + for (var i = 0, j = 0; i < elsLen; i++) { + if ( pattern.test(els[i].className) ) { + if (single) { + return els[i]; + } else { + classElements[j] = els[i]; + j++; + } + } + } + return classElements; + }; + } + })(); + +},{}],14:[function(require,module,exports){ + var indexOf = [].indexOf; + + module.exports = function(arr, obj){ + if (indexOf) return arr.indexOf(obj); + for (var i = 0; i < arr.length; ++i) { + if (arr[i] === obj) return i; + } + return -1; + }; + +},{}],15:[function(require,module,exports){ + /* + * Natural Sort algorithm for Javascript - Version 0.8 - Released under MIT license + * Author: Jim Palmer (based on chunking idea from Dave Koelle) + */ + module.exports = function(a, b, opts) { + var re = /(^([+\-]?(?:\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[\da-fA-F]+$|\d+)/g, + sre = /^\s+|\s+$/g, // trim pre-post whitespace + snre = /\s+/g, // normalize all whitespace to single ' ' character + dre = /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/, + hre = /^0x[0-9a-f]+$/i, + ore = /^0/, + options = opts || {}, + i = function(s) { return options.insensitive && (''+s).toLowerCase() || ''+s; }, + // convert all to strings strip whitespace + x = i(a) || '', + y = i(b) || '', + // chunk/tokenize + xN = x.replace(re, '\0$1\0').replace(/\0$/,'').replace(/^\0/,'').split('\0'), + yN = y.replace(re, '\0$1\0').replace(/\0$/,'').replace(/^\0/,'').split('\0'), + // numeric, hex or date detection + xD = parseInt(x.match(hre), 16) || (xN.length !== 1 && Date.parse(x)), + yD = parseInt(y.match(hre), 16) || xD && y.match(dre) && Date.parse(y) || null, + normChunk = function(s, l) { + // normalize spaces; find floats not starting with '0', string or 0 if not defined (Clint Priest) + return (!s.match(ore) || l == 1) && parseFloat(s) || s.replace(snre, ' ').replace(sre, '') || 0; + }, + oFxNcL, oFyNcL; + // first try and sort Hex codes or Dates + if (yD) { + if ( xD < yD ) { return -1; } + else if ( xD > yD ) { return 1; } + } + // natural sorting through split numeric strings and default strings + for(var cLoc=0, xNl = xN.length, yNl = yN.length, numS=Math.max(xNl, yNl); cLoc < numS; cLoc++) { + oFxNcL = normChunk(xN[cLoc], xNl); + oFyNcL = normChunk(yN[cLoc], yNl); + // handle numeric vs string comparison - number < string - (Kyle Adams) + if (isNaN(oFxNcL) !== isNaN(oFyNcL)) { return (isNaN(oFxNcL)) ? 1 : -1; } + // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2' + else if (typeof oFxNcL !== typeof oFyNcL) { + oFxNcL += ''; + oFyNcL += ''; + } + if (oFxNcL < oFyNcL) { return -1; } + if (oFxNcL > oFyNcL) { return 1; } + } + return 0; + }; + +},{}],16:[function(require,module,exports){ + /** + * Source: https://github.com/timoxley/to-array + * + * Convert an array-like object into an `Array`. + * If `collection` is already an `Array`, then will return a clone of `collection`. + * + * @param {Array | Mixed} collection An `Array` or array-like object to convert e.g. `arguments` or `NodeList` + * @return {Array} Naive conversion of `collection` to a new `Array`. + * @api public + */ + + module.exports = function toArray(collection) { + if (typeof collection === 'undefined') return []; + if (collection === null) return [null]; + if (collection === window) return [window]; + if (typeof collection === 'string') return [collection]; + if (isArray(collection)) return collection; + if (typeof collection.length != 'number') return [collection]; + if (typeof collection === 'function' && collection instanceof Function) return [collection]; + + var arr = []; + for (var i = 0; i < collection.length; i++) { + if (Object.prototype.hasOwnProperty.call(collection, i) || i in collection) { + arr.push(collection[i]); + } + } + if (!arr.length) return []; + return arr; + }; + + function isArray(arr) { + return Object.prototype.toString.call(arr) === "[object Array]"; + } + +},{}],17:[function(require,module,exports){ + module.exports = function(s) { + s = (s === undefined) ? "" : s; + s = (s === null) ? "" : s; + s = s.toString(); + return s; + }; + +},{}]},{},[1]); diff --git a/filter/oembed/amd/src/manageproviders.js b/filter/oembed/amd/src/manageproviders.js new file mode 100644 index 000000000..700cd0043 --- /dev/null +++ b/filter/oembed/amd/src/manageproviders.js @@ -0,0 +1,300 @@ +/** + * 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 . + * + * @package filter_oembed + * @copyright Guy Thomas / moodlerooms.com 2016 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Oembed provider management module. + */ +define(['jquery', 'core/notification', 'core/ajax', 'core/templates', 'core/fragment', 'core/str', + 'filter_oembed/list'], + function($, notification, ajax, templates, fragment, str, List) { + return { + + prevEditId: null, + + /** + * Reload provider row. + * @param {int} pid + * @param {jQuery} row + * @param {string|null} action + * @param {function|null} callback + */ + reloadRow: function(pid, row, action, callback) { + action = !action ? 'reload' : action; + ajax.call([ + { + methodname: 'filter_oembed_provider_manage', + args: { + pid: pid, + action: action + }, + done: function(response) { + // Update row. + templates.render('filter_oembed/managementpagerow', response.providermodel) + .done(function(result) { + $(row).replaceWith(result); + row = $('#oembed-display-providers_' + pid); + if (typeof(callback) === 'function') { + callback(row); + } + }); + }, + fail: function(response) { + notification.exception(response); + } + } + ], true, true); + }, + + /** + * Reload all providers. + * @param {function|null} callback + */ + reloadProviders: function(callback) { + ajax.call([ + { + methodname: 'filter_oembed_providers', + args: { + scope: 'all' + }, + done: function(response) { + // Update table. + templates.render('filter_oembed/managementpage', response) + .done(function(result) { + var resultHtml = $($.parseHTML(result)).html(); + $('#providermanagement').html(resultHtml); + if (typeof(callback) === 'function') { + callback(); + } + }); + }, + fail: function(response) { + notification.exception(response); + } + } + ], true, true); + }, + + /** + * Listen for enable / disable action. + */ + listenEnableDisable: function() { + var self = this; + $('#providermanagement').on('click', '.oembed-provider-actions .filter-oembed-visibility', function(e) { + e.preventDefault(); + + var row = $(this).parents('tr')[0]; + var pid = $(row).data('pid'); + var enabled = !$(row).hasClass('dimmed_text'); + var action = enabled ? 'disable' : 'enable'; + + self.reloadRow(pid, row, action); + }); + }, + + /** + * Listen for delete action. + */ + listenDelete: function() { + var onConfirm = function(row) { + + var pid = $(row).data('pid'); + + ajax.call([ + { + methodname: 'filter_oembed_provider_manage', + args: { + pid: pid, + action: 'delete' + }, + done: function() { + // Remove row. + $(row).remove(); + }, + fail: function(response) { + notification.exception(response); + } + } + ], true, true); + }; + + $('#providermanagement').on('click', '.oembed-provider-actions .filter-oembed-delete', function(e) { + e.preventDefault(); + + var row = $(this).parents('tr')[0]; + var providerName = $($(this).parents('td').find('.list-providername')[0]).text(); + + str.get_strings([ + {key: 'deleteprovidertitle', component: 'filter_oembed'}, + {key: 'deleteproviderconfirm', component: 'filter_oembed', param: providerName}, + {key: 'ok', component: 'core'}, + {key: 'cancel', component: 'core'} + ]).done(function(strings) { + var delTitle = strings[0]; + var delConf = strings[1]; + var ok = strings[2]; + var cancel = strings[3]; + notification.confirm(delTitle, delConf, ok, cancel, function() { + onConfirm(row); + }); + }); + }); + }, + + /** + * Listen for edit action. + */ + listenEdit: function() { + var self = this; + + /** + * Turn editing off for a row by id + * @param {string} providerId + */ + var turnEditingOff = function(provderId) { + var sel = '#oembed-display-providers_' + provderId; + $(sel).removeClass('oembed-provider-editing'); + $(sel + ' form').remove(); + $(sel + ' td div.alert').remove(); + }; + + /** + * Update the provider form with data. + * @param string data - serialized form data. + */ + var updateProviderForm = function(pid, data, callback) { + + var rx = new RegExp('(?:course-)(\\S)'); + var result = rx.exec($('body').attr('class')); + var contextid = parseInt(result[1]); + var params; + if (data) { + params = {formdata: data, pid: pid}; + } else { + params = {pid: pid}; + } + + fragment.loadFragment('filter_oembed', 'provider', contextid, params).done( + function(html, js) { + $('#oembed-display-providers_' + pid).addClass('oembed-provider-editing'); + templates.replaceNodeContents( + $('#oembed-display-providers_' + pid + ' .oembed-provider-details'), + html, + js + ); + if (typeof(callback) === 'function') { + callback(); + } + } + ); + }; + + // Listen for click cancel. + $('#providermanagement').on('click', '.oembed-provider-actions .filter-oembed-edit', function(e) { + e.preventDefault(); + + var row = $(this).parents('tr')[0]; + var pid = $(row).data('pid'); + + // Remove editing class from current row / previous row and delete form. + if (self.prevEditId !== null) { + turnEditingOff(self.prevEditId); + turnEditingOff(pid); + } + + self.prevEditId = pid; + + updateProviderForm(pid); + }); + + // Listen for form click submit. + $('#providermanagement').on('click', '.oembed-provider-details form #id_submitbutton', function(e) { + e.preventDefault(); + var row = $(this).parents('tr')[0]; + var pid = $(row).data('pid'); + var form = $(this).parents('form')[0]; + var source = $(form).find('input[name="source"]').val(); + + $(form).trigger('save-form-state'); + var data = $(form).serialize(); + updateProviderForm(pid, data, function() { + var detailsSel = '#oembed-display-providers_' + pid + ' .oembed-provider-details'; + var successSel = detailsSel + ' div.alert-success'; + var successEl = $(successSel); + + if (successEl.length) { + var successHTML = successEl[0].outerHTML; + turnEditingOff(pid); + + // Get new provider id and set pid to it so correct row is targeted on reload. + if (source.indexOf('download::') > -1) { + var newProviderSel = detailsSel + ' .js-oembed-newprovider'; + var newProviderEl = $(newProviderSel); + if (newProviderEl.length) { + pid = newProviderEl.data('newproviderid'); + } + } + + /** + * On reloading providers or single row append success HTML. + */ + var onReload = function() { + var rowcell = $('#oembed-display-providers_' + pid + ' td'); + $(rowcell).append(successHTML); + $(rowcell).find(' div.alert-success').attr('tabindex', -1); + $(rowcell).find(' div.alert-success').focus(); + }; + + if (source.indexOf('download::') > -1) { + // When a downloaded provider is saved, a new one is created as a local provider, so we + // need to reload the full list. + self.reloadProviders(onReload); + } else { + self.reloadRow(pid, row, 'reload', onReload); + } + } + }); + }); + + // Listen for form click cancel. + $('#providermanagement').on('click', '.oembed-provider-details form #id_cancel', function(e) { + e.preventDefault(); + var row = $(this).parents('tr')[0]; + turnEditingOff($(row).data('pid')); + }); + }, + + /** + * Initialise. + */ + init: function() { + var options = { + valueNames: [ 'list-providername'] + }; + + new List('providermanagement', options); + + this.listenEnableDisable(); + this.listenDelete(); + this.listenEdit(); + } + }; + } +); diff --git a/filter/oembed/amd/src/oembed.js b/filter/oembed/amd/src/oembed.js new file mode 100644 index 000000000..1a0fcc3d2 --- /dev/null +++ b/filter/oembed/amd/src/oembed.js @@ -0,0 +1,82 @@ +/** + * 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 . + * + * @package filter_oembed + * @copyright Guy Thomas / moodlerooms.com 2016 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Oembed main module. + */ +define(['jquery', 'filter_oembed/preloader', 'filter_oembed/responsivecontent'], + function($, preloader, responsiveContent) { + return { + init: function() { + /** + * Apply a mutation observer to track oembed-content being dynamically added to the page. + */ + var responsiveContentOnInsert = function() { + /** + * Does a node have the oembed-content class + * @param {opbject} node (dom element) + * @returns {boolean} + */ + var hasOembedClass = function(node) { + if (!node.className) { + return false; + } + return $(node).is(".oembed-content, .oembed-card-container"); + }; + + var observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + for (var n in mutation.addedNodes) { + var node = mutation.addedNodes[n]; + if (hasOembedClass(node)) { + // Only apply responsive content to the newly added node for efficiency. + responsiveContent.apply($(node).find('> *:not(video):first-child, .oembed-card')); + } + } + }); + }); + + var observerConfig = { + attributes: true, + childList: true, + characterData: true, + subtree: true + }; + + // Note: Currently observing mutations throughout the document body - We might want to limit scope for + // observation at some point in the future. + var targetNode = document.body; + observer.observe(targetNode, observerConfig); + }; + + responsiveContentOnInsert(); + + $(document).ready(function() { + // Apply preloader listeners. + preloader.apply(); + + // Call responsive content on dom ready, to catch things that existed prior to mutation observation. + responsiveContent.apply(); + }); + } + }; + } +); diff --git a/filter/oembed/amd/src/preloader.js b/filter/oembed/amd/src/preloader.js new file mode 100644 index 000000000..a5d0e78c6 --- /dev/null +++ b/filter/oembed/amd/src/preloader.js @@ -0,0 +1,55 @@ +/** + * 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 . + * + * @package filter_oembed + * @copyright Guy Thomas / moodlerooms.com 2016 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Oembed preloader. + */ +define(['jquery'], + function($) { + return { + apply: function() { + $(".oembed-card-play").on("click", function() { + var card = $(this).parent('.oembed-card'); + var data = $(card.data('embed')); + var cardwidth = $(card).width(); + var cardheight = $(card).height(); + + // Add auto play params. + // Because we are using a preloader we ideally want the content to play after clicking the preloader + // play button. + if ($(data).find('iframe').length) { + var iframe = $($(data).find('iframe')[0]); + var src = iframe.attr('src'); + var paramglue = src.indexOf('?') > -1 ? '&' : '?'; + src += paramglue + 'autoplay=1'; + src += '&' + 'auto_play=1'; + iframe.attr('src', src); + } + + // Replace card with oembed html. + data.attr('data-card-width', cardwidth); + data.attr('data-card-height', cardheight); + card.parent('.oembed-card-container').replaceWith(data); + }); + } + }; + } +); diff --git a/filter/oembed/amd/src/responsivecontent.js b/filter/oembed/amd/src/responsivecontent.js new file mode 100644 index 000000000..b0998f1c3 --- /dev/null +++ b/filter/oembed/amd/src/responsivecontent.js @@ -0,0 +1,109 @@ +/** + * 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 . + * + * @package filter_oembed + * @copyright Guy Thomas / moodlerooms.com 2016 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Main responsive content function. + */ +define(['jquery'], function($) { + + /** + * Apply responsive video to non HTML5 video elements. + */ + var ResponsiveContent = function() { + + /** + * Apply to specific node / nodes or use selector. + * @param {jQuery|null} nodes- jquery node / collection of nodes or null + */ + this.apply = function(nodes) { + if (!nodes){ + var selectortoprocess = '.oembed-content:not(.oembed-responsive) > *:not(video):first-child,'; + selectortoprocess += ' .oembed-card:not(.oembed-processed)'; + nodes = $(selectortoprocess); + } + // Apply aspect ratio to height for all nodes or single node. + $(nodes).each(function() { + + var parent = $(this).parent(); + if (parent.hasClass('oembed-responsive')) { + // Already processed. + return; + } + + var width, + height, + aspectratio; + + aspectratio = this.getAttribute('data-aspect-ratio'); + if (aspectratio === null || aspectratio === '0') { // Note, an empty attribute should evaluate to null. + // Calculate aspect ratio. + width = this.width || this.offsetWidth; + height = this.height || this.offsetHeight; + + // If only the width or height contains percentages then we can't use it and will have to fall back + // on the card size OR offsets. + if (width.indexOf('%') > -1 && height.indexOf('%') == -1 + || width.indexOf('%') == -1 && height.indexOf('%') > -1 + ) { + if ($(this).parent().attr('data-card-width') && $(this).parent().attr('data-card-height')) { + width = $(this).parent().attr('data-card-width'); + height = $(this).parent().attr('data-card-height'); + } else { + width = this.offsetWidth; + height = this.offsetHeight; + } + } + + width = parseInt(width); + height = parseInt(height); + aspectratio = height / width; + this.setAttribute('data-aspect-ratio', aspectratio); + } + + var tagname = this.tagName.toLowerCase(); + if (tagname === 'iframe') { + // Remove attributes. + $(this).removeAttr('width'); + $(this).removeAttr('height'); + } + + // Get width again. + width = parseInt(this.offsetWidth); + // Set width. + var style = {width: '100%'}; + $(this).css(style); + + // Make sure parent has a padding element. + if (!parent.find('.oembed-responsive-pad').length) { + var aspectPerc = aspectratio * 100; + var responsivePad = '
'; + parent.append(responsivePad); + } + + // Add responsive class to parent element. + parent.addClass('oembed-responsive'); + }); + }; + + }; + + return new ResponsiveContent(); +}); diff --git a/filter/oembed/classes/db/abstract_dbrow.php b/filter/oembed/classes/db/abstract_dbrow.php new file mode 100644 index 000000000..ab91cd9cd --- /dev/null +++ b/filter/oembed/classes/db/abstract_dbrow.php @@ -0,0 +1,54 @@ +. + +/** + * Base class for classes which map to db tables. + * @author Guy Thomas + * @copyright Copyright (c) 2016 Blackboard Inc. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace filter_oembed\db; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +class abstract_dbrow { + + /** + * anstract_dbrow constructor. + * @param stdClass $row + */ + public function __construct($row) { + + if (!$row) { + throw new \coding_exception('$row does not exist'); + } + + if (!$row instanceof stdClass) { + throw new \coding_exception('$row must be an instance of std class', var_export($row, true)); + } + + $vars = array_keys(get_object_vars($this)); + + foreach ($row as $key => $val) { + if (!in_array($key, $vars)) { + throw new \coding_exception('Row model '.get_class($this).' is missing key '.$key); + } + $this->$key = $val; + } + } +} diff --git a/filter/oembed/classes/db/providerrow.php b/filter/oembed/classes/db/providerrow.php new file mode 100644 index 000000000..06bb26539 --- /dev/null +++ b/filter/oembed/classes/db/providerrow.php @@ -0,0 +1,69 @@ +. + +/** + * Provider Row. + * @author Guy Thomas + * @copyright Copyright (c) 2016 Blackboard Inc. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace filter_oembed\db; + +defined('MOODLE_INTERNAL') || die(); + +class providerrow extends abstract_dbrow{ + /** + * @var int id + */ + public $id; + + /** + * @var str provider name + */ + public $providername; + + /** + * @var str provider url + */ + public $providerurl; + + /** + * @var str end points + */ + public $endpoints; + + /** + * @var str source + */ + public $source; + + /** + * @var bool enabled status + */ + public $enabled; + + /** + * @var int time created + */ + public $timecreated; + + /** + * @var int time modified + */ + public $timemodified; + +} diff --git a/filter/oembed/classes/forms/provider.php b/filter/oembed/classes/forms/provider.php new file mode 100644 index 000000000..988787443 --- /dev/null +++ b/filter/oembed/classes/forms/provider.php @@ -0,0 +1,90 @@ +. + +/** + * Provider mform. + * @author Guy Thomas + * @copyright Copyright (c) 2016 Blackboard Inc. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace filter_oembed\forms; + +use moodleform; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/lib/formslib.php'); + +class provider extends moodleform { + /** + * Define this form - is called from parent constructor. + */ + public function definition() { + $mform = $this->_form; + + // Form configuration. + $config = (object)[ + 'id' => ['required' => true, 'type' => 'hidden', 'paramtype' => PARAM_INT], + 'providername' => ['required' => true, 'type' => 'text', 'paramtype' => PARAM_TEXT], + 'providerurl' => ['required' => true, 'type' => 'text', 'paramtype' => PARAM_URL], + 'endpoints' => ['required' => true, 'type' => 'textarea', 'paramtype' => PARAM_TEXT], + 'enabled' => ['required' => false, 'type' => 'checkbox', 'paramtype' => PARAM_INT], + 'source' => ['required' => true, 'type' => 'hidden', 'paramtype' => PARAM_TEXT], + ]; + + // The source type is stored in "_customdata". + $sourcetype = $this->_customdata; + // Common attributes to be appleid to all fields. + $commonattributes = null; + if ($sourcetype === \filter_oembed\provider\provider::PROVIDER_SOURCE_PLUGIN) { + $commonattributes = 'disabled="disabled"'; + } + + // Define form according to configuration. + foreach ($config as $fieldname => $row) { + $row = (object)$row; + if ($row->type == 'hidden') { + $fieldlabel = ''; + } else { + $fieldlabel = get_string($fieldname, 'filter_oembed'); + } + $el = $mform->addElement($row->type, $fieldname, $fieldlabel); + if (!empty($commonattributes)) { + $el->updateAttributes($commonattributes); + } + $mform->setType($fieldname, $row->paramtype); + if ($row->required) { + $mform->addRule($fieldname, get_string('requiredfield', 'filter_oembed', $fieldlabel), 'required'); + } + } + + $mform->addElement('static', 'sourcetext', get_string('source', 'filter_oembed')); + + if ($sourcetype === \filter_oembed\provider\provider::PROVIDER_SOURCE_PLUGIN) { + // Plugins can't be edited. + $mform->addElement('cancel'); + } else { + if ($sourcetype == \filter_oembed\provider\provider::PROVIDER_SOURCE_DOWNLOAD) { + // Downloads can be saved as new locals. + $label = get_string('saveasnew', 'filter_oembed'); + } else { + // Locals can be edited. + $label = null; + } + $this->add_action_buttons(true, $label); + } + } +} diff --git a/filter/oembed/classes/output/managementpage.php b/filter/oembed/classes/output/managementpage.php new file mode 100644 index 000000000..e41a9ea2f --- /dev/null +++ b/filter/oembed/classes/output/managementpage.php @@ -0,0 +1,88 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +namespace filter_oembed\output; + +use filter_oembed\db\providerrow; +use filter_oembed\provider\provider; + +defined('MOODLE_INTERNAL') || die(); + +class managementpage implements \renderable, \templatable { + + /** + * An array of rows + * + * @var array + */ + protected $rows; + + /** + * Construct the renderable. + * @param array $content The array of rows. + */ + public function __construct(array $content = array()) { + if (!empty($content)) { + foreach ($content as $row) { + $this->rows[] = $row; + } + } + } + + /** + * Export the data for template. + * @param \renderer_base $output + */ + public function export_for_template(\renderer_base $output) { + $data = [ + 'localrows' => [], + 'downloadrows' => [], + 'pluginrows' => [], + ]; + + if (count($this->rows) < 1) { + return $data; + } + + // Separate out the rows by source for display. + foreach ($this->rows as $row) { + $sourcetype = provider::source_type($row->source); + switch ($sourcetype) { + case provider::PROVIDER_SOURCE_DOWNLOAD: + $data['downloadrows'][] = new providermodel($row); + break; + + case provider::PROVIDER_SOURCE_PLUGIN: + $data['pluginrows'][] = new providermodel($row); + break; + + case provider::PROVIDER_SOURCE_LOCAL: + default: + $data['localrows'][] = new providermodel($row); + break; + } + } + return $data; + } + +} diff --git a/filter/oembed/classes/output/providermodel.php b/filter/oembed/classes/output/providermodel.php new file mode 100644 index 000000000..95dbdbb7e --- /dev/null +++ b/filter/oembed/classes/output/providermodel.php @@ -0,0 +1,165 @@ +. + +/** + * @package filter_oembed + * @author Guy Thomas + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +namespace filter_oembed\output; + +use filter_oembed\service\oembed; +use filter_oembed\provider\provider; +use filter_oembed\db\providerrow; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class providermodel + * @package filter_oembed\output + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ +class providermodel implements \renderable { + + /** + * @var int provider row id + */ + public $pid; + + /** + * @var string provider name + */ + public $providername; + + /** + * @var string provider url + */ + public $providerurl; + + /** + * @var bool is this provider enabled or not + */ + public $enabled; + + /** + * @var string current action - enable or disable + */ + public $enableaction; + + /** + * @var string additional row class + */ + public $extraclass; + + /** + * @var string html for edit action + */ + public $editaction; + + /** + * @var string html for delete action + */ + public $deleteaction; + + /** + * @var int 1 if editing, else 0 + */ + public $editing; + + /** + * @var string provider source + */ + public $source; + + /** + * @var string source type - local, download, plugin + */ + public $sourcetype; + + /** + * @var string provider scehmes + */ + public $schemes; + + /** + * @var bool allow for discovery + */ + public $discovery; + + /** + * @var string TODO add description + */ + public $formats; + + /** + * providermodel constructor. + * @param mixed $provider + */ + public function __construct($provider) { + global $PAGE, $CFG; + $PAGE->set_context(\context_system::instance()); + $output = $PAGE->get_renderer('filter_oembed', null, RENDERER_TARGET_GENERAL); + + $provider = (object)$provider; + + $this->pid = $provider->id; + $this->providername = $provider->providername; + $this->providerurl = $provider->providerurl; + $this->source = $provider->source; + $this->sourcetype = provider::source_type($provider->source); + if ($provider->enabled) { + + // Disable action. + $this->enabled = true; + $this->extraclass = ''; + $action = $CFG->wwwroot . '/filter/oembed/manageproviders.php?action=disable&pid=' . + $provider->id . '&sesskey=' . sesskey(); + $this->enableaction = $output->action_icon($action, + new \pix_icon('t/hide', get_string('hide')), null, ['class' => 'action-icon filter-oembed-visibility']); + } else { + + // Enable action. + $action = $CFG->wwwroot . '/filter/oembed/manageproviders.php?action=enable&pid=' . + $provider->id . '&sesskey=' . sesskey(); + $this->extraclass = 'dimmed_text'; + $this->enableaction = $output->action_icon($action, + new \pix_icon('t/show', get_string('show')), null, ['class' => 'action-icon filter-oembed-visibility']); + } + + // Edit action. + $action = $CFG->wwwroot . '/filter/oembed/manageproviders.php?action=edit&pid=' . + $provider->id . '&sesskey=' . sesskey(); + $this->editaction = $output->action_icon($action, + new \pix_icon('t/edit', get_string('edit')), null, ['class' => 'action-icon filter-oembed-edit']); + + // Delete action. + if ($this->sourcetype == provider::PROVIDER_SOURCE_LOCAL) { + $action = $CFG->wwwroot . '/filter/oembed/manageproviders.php?action=delete&pid=' . + $provider->id . '&sesskey=' . sesskey(); + $this->deleteaction = $output->action_icon($action, + new \pix_icon('t/delete', get_string('delete')), + null, + ['class' => 'action-icon filter-oembed-delete'] + ); + } else { + $this->deleteaction = ''; + } + + } +} diff --git a/filter/oembed/classes/output/renderer.php b/filter/oembed/classes/output/renderer.php new file mode 100644 index 000000000..1604256f4 --- /dev/null +++ b/filter/oembed/classes/output/renderer.php @@ -0,0 +1,52 @@ +. + +/** + * Renderer for oembed filter. + * @author gthomas2 + * @copyright Copyright (c) 2016 Moodlerooms Inc. (http://www.moodlerooms.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace filter_oembed\output; + +defined('MOODLE_INTERNAL') || die(); + +class renderer extends \plugin_renderer_base { + + /** + * Pre loader HTML. + * + * @param string $embedhtml + * @param array $json + * @return string + */ + public function preload($embedhtml, array $json) { + $data = (object)$json; + $data->embedhtml = $embedhtml; // Has some extra processing to what is available in $json['html']. + return $this->render_from_template('filter_oembed/preload', $data); + } + + /** + * Provider management page. + * @param \templateable $page + * @return string | boolean + */ + public function render_managementpage($page) { + $data = $page->export_for_template($this); + return $this->render_from_template('filter_oembed/managementpage', $data); + } +} diff --git a/filter/oembed/classes/plugininfo/oembedprovider.php b/filter/oembed/classes/plugininfo/oembedprovider.php new file mode 100644 index 000000000..e1cc5417f --- /dev/null +++ b/filter/oembed/classes/plugininfo/oembedprovider.php @@ -0,0 +1,32 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +namespace filter_oembed\plugininfo; + +defined('MOODLE_INTERNAL') || die(); + +class oembedprovider extends \core\plugininfo\base { + public function is_uninstall_allowed() { + return true; + } +} \ No newline at end of file diff --git a/filter/oembed/classes/provider/base.php b/filter/oembed/classes/provider/base.php deleted file mode 100644 index 5c2f7ec16..000000000 --- a/filter/oembed/classes/provider/base.php +++ /dev/null @@ -1,122 +0,0 @@ -. - -/** - * @package filter_oembed - * @author Matthew Cannings - * @author James McQuillan - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @copyright 2012 Matthew Cannings; modified 2015 by Microsoft, Inc. - */ - -namespace filter_oembed\provider; - -global $CFG; -require_once($CFG->dirroot.'/lib/filelib.php'); - -/** - * Base class for oembed providers. - */ -class base { - /** - * Main filter function. - * - * @param string $text Incoming text. - * @return string Filtered text. - */ - public function filter($text) { - return $text; - } - - /** - * Return the HTML content to be embedded given the response from the OEmbed request. - * This method returns the thumbnail image if we lazy loading is enabled. Ogtherwise it returns the - * embeddable HTML returned from the OEmbed request. An error message is returned if there was an error during - * the request. - * - * @param array $json Response object returned from the OEmbed request. - * @param string $params Additional parameters to include in the embed URL. - * @return string The HTML content to be embedded in the page. - */ - protected function getoembedhtml($json, $params = '') { - if ($json === null) { - return '

'.get_string('connection_error', 'filter_oembed').'

'; - } - - $embed = $json['html']; - - if (!empty($params)) { - $embed = str_replace('?feature=oembed', '?feature=oembed'.htmlspecialchars($params), $embed); - } - - if (get_config('filter_oembed', 'lazyload')) { - $embed = htmlspecialchars($embed); - $dom = new \DOMDocument(); - - // To surpress the loadHTML Warnings. - libxml_use_internal_errors(true); - $dom->loadHTML($json['html']); - libxml_use_internal_errors(false); - - // Get height and width of iframe. - $height = $dom->getElementsByTagName('iframe')->item(0)->getAttribute('height'); - $width = $dom->getElementsByTagName('iframe')->item(0)->getAttribute('width'); - - $embedcode = '
'; - $embedcode .= ''; - $embedcode .= '
'.$json['title'].'
'; - $embedcode .= ''; - $embedcode .= '
'; - } else { - $embedcode = $embed; - } - - return $embedcode; - } - - /** - * Makes the OEmbed request to the service that supports the protocol. - * - * @param string $url URL for the OEmbed request - * @return mixed|null|string The HTTP response object from the OEmbed request. - */ - protected function getoembeddata($url, $retryno = 0) { - $curl = new \curl(); - $ret = $curl->get($url); - - // Check if curl call fails. - if ($curl->errno != CURLE_OK) { - $retrylimit = get_config('filter_oembed', 'retrylimit'); - // Check if error is due to network connection. - if (in_array($curl->errno, [6, 7, 28])) { - // Try curl call up to 3 times. - usleep(50000); - $retryno = (!is_int($retryno)) ? 0 : $retryno+1; - if ($retryno < $retrylimit) { - return $this->getoembeddata($url, $retryno); - } else { - return null; - } - } else { - return null; - } - } - - $result = json_decode($ret, true); - return $result; - } -} diff --git a/filter/oembed/classes/provider/docsdotcom.php b/filter/oembed/classes/provider/docsdotcom.php deleted file mode 100644 index 545013d7c..000000000 --- a/filter/oembed/classes/provider/docsdotcom.php +++ /dev/null @@ -1,62 +0,0 @@ -. - -/** - * @package filter_oembed - * @author James McQuillan - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @copyright (C) 2016 onwards Microsoft, Inc. (http://microsoft.com/) - */ - -namespace filter_oembed\provider; - -/** - * oEmbed provider implementation for Docs.com - */ -class docsdotcom extends base { - /** - * Get the replacement oembed HTML. - * - * @param array $matched Matched URL. - * @return string The replacement text/HTML. - */ - public function get_replacement($matched) { - if (!empty($matched[0])) { - $params = [ - 'url' => $matched[1]. $matched[3] . '/' . $matched[4] . '/' . $matched[5] . '/' . $matched[6], - 'format' => 'json', - 'maxwidth' => '600', - 'maxheight' => '400', - ]; - $oembedurl = new \moodle_url('https://docs.com/api/oembed', $params); - $oembeddata = $this->getoembeddata($oembedurl->out(false)); - return '
'.$this->getoembedhtml($oembeddata).'
'; - } else { - return $matched[0]; - } - } - - /** - * Filter the text. - * - * @param string $text Incoming text. - * @return string Filtered text. - */ - public function filter($text) { - $search = '/]*href="(https?:\/\/(www\.)?)(docs\.com)\/(.+?)\/(.+?)\/(.+?)"(.*?)>(.*?)<\/a>/is'; - return preg_replace_callback($search, [$this, 'get_replacement'], $text); - } -} diff --git a/filter/oembed/classes/provider/endpoint.php b/filter/oembed/classes/provider/endpoint.php new file mode 100644 index 000000000..e0d4574ea --- /dev/null +++ b/filter/oembed/classes/provider/endpoint.php @@ -0,0 +1,90 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +namespace filter_oembed\provider; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Base class for oembed endpoints. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ +class endpoint { + /** + * @var array + */ + protected $schemes = []; + + /** + * @var string + */ + protected $url = ''; + + /** + * @var boolean + */ + protected $discovery = false; + + /** + * @var array + */ + protected $formats = ['json']; + + /** + * Constructor. + * @param $data JSON decoded array or data object containing all endpoint data. + */ + public function __construct($data = null) { + if (is_object($data)) { + $data = (array)$data; + } + if (isset($data['schemes'])) { + $this->schemes = $data['schemes']; + } + if (isset($data['url'])) { + $this->url = $data['url']; + } + if (isset($data['discovery'])) { + $this->discovery = !empty($data['discovery']); + } + if (isset($data['formats'])) { + $this->formats = $data['formats']; + } + } + + /** + * Magic method for getting properties. + * @param string $name + * @return mixed + * @throws \coding_exception + */ + public function __get($name) { + $allowed = ['schemes', 'url', 'discovery', 'formats']; + if (in_array($name, $allowed)) { + return $this->$name; + } else { + throw new \coding_exception($name.' is not a publicly accessible property of '.get_class($this)); + } + } +} \ No newline at end of file diff --git a/filter/oembed/classes/provider/powerbi.php b/filter/oembed/classes/provider/powerbi.php deleted file mode 100644 index 8ebf11c88..000000000 --- a/filter/oembed/classes/provider/powerbi.php +++ /dev/null @@ -1,68 +0,0 @@ -. - -/** - * @package filter_oembed - * @author Sushant Gawali - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @copyright (C) 2016 onwards Microsoft Open Technologies, Inc. (http://msopentech.com/) - */ - -namespace filter_oembed\provider; -/** - * oEmbed provider implementation for Docs.com - */ -class powerbi extends base { - /** - * Get the replacement oembed HTML. - * - * @param array $matched Matched URL. - * @return string The replacement text/HTML. - */ - public function get_replacement($matched) { - $httpclient = new \local_o365\httpclient(); - $clientdata = \local_o365\oauth2\clientdata::instance_from_oidc(); - $resource = \filter_oembed\rest\powerbi::get_resource(); - $token = \local_o365\oauth2\systemtoken::instance(null, $resource, $clientdata, $httpclient); - if (!empty($token)) { - $powerbi = new \filter_oembed\rest\powerbi($token, $httpclient); - if ($matched[6] == 'reports') { - $reportsdata = $powerbi->apicall('get', 'reports'); - $embedurl = $powerbi->getreportoembedurl($matched[7], $reportsdata); - $embedhtml = $this->getembedhtml($embedurl); - $embedhtml .= ''; - return $embedhtml; - } - } - return $matched[0]; - } - - /** - * Filter the text. - * - * @param string $text Incoming text. - * @return string Filtered text. - */ - public function filter($text) { - $search = '/]*href="(https?:\/\/(app\.)?)(powerbi\.com)\/(.+?)\/(.+?)\/(.+?)\/(.+?)\/(.+?)"(.*?)>(.*?)<\/a>/is'; - return preg_replace_callback($search, [$this, 'get_replacement'], $text); - } - - private function getembedhtml($embedurl) { - return ''; - } -} diff --git a/filter/oembed/classes/provider/provider.php b/filter/oembed/classes/provider/provider.php new file mode 100644 index 000000000..f9d089b68 --- /dev/null +++ b/filter/oembed/classes/provider/provider.php @@ -0,0 +1,258 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @author Erich M. Wappis + * @author Guy Thomas + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +namespace filter_oembed\provider; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Base class for oembed providers and plugins. Plugins should extend this class. + * If "filter" is provided, there is nothing else a plugin needs to implement. + * Plugins can instead / additionally override "get_oembed_request", "oembed_response" and "endpoints_regex". + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ +class provider { + + /** + * @var int + */ + protected $id; + + /** + * @var boolean + */ + protected $enabled; + + /** + * @var string + */ + protected $providername = ''; + + /** + * @var string + */ + protected $providerurl = ''; + + /** + * @var endpoints + */ + protected $endpoints = []; + + /** + * @var source + */ + protected $source = ''; + + /** + * @var Class constant descriptions. + */ + const PROVIDER_SOURCE_LOCAL = 'local::'; + const PROVIDER_SOURCE_DOWNLOAD = 'download::'; + const PROVIDER_SOURCE_PLUGIN = 'plugin::'; + + /** + * Constructor. + * Note - provider data is expcted to come from the moodle data (db) which excludes + * "_" in variable names. Providers coming directly from oembed (http://oembed.com/providers.json), + * include "_" in variable names, which violates the Moodle coding standard. Currently, + * this is managed by the update processes to ensure compatibility. + * + * @param $data JSON decoded array or a data object containing all provider data. + */ + public function __construct($data = null) { + if (is_object($data)) { + $data = (array)$data; + } + if (!empty($data)) { + $this->id = isset($data['id']) ? $data['id'] : 0; + $this->enabled = isset($data['enabled']) ? $data['enabled'] : false; + $this->providername = $data['providername']; + $this->providerurl = $data['providerurl']; + + // If the endpoint data is a string, assume its a json encoded string. + if (is_string($data['endpoints'])) { + $data['endpoints'] = json_decode($data['endpoints'], true); + } + if (is_array($data['endpoints'])) { + foreach ($data['endpoints'] as $endpoint) { + $this->endpoints[] = new endpoint($endpoint); + } + } else { + throw new \coding_exception('"endpoint" data must be an array for '.get_class($this)); + } + + $this->source = isset($data['source']) ? $data['source'] : ''; + } + } + + /** + * Main filter function. This should only be used by subplugins, and it is preferable + * to not use it even then. Ideally, a provider plugin should provide a JSON oembed provider + * response (http://oembed.com/#section2.3) and let the main filter handle the HTML. Use this + * only if the HTML must be determined by the plugin. If implemented, ensure FALSE is returned + * if no filtering occurred. + * + * @param string $text Incoming text. + * @return string Filtered text, or false for no changes. + */ + public function filter($text) { + return false; + } + + /** + * Return the JSON decoded provider implementation info as in http://oembed.com/providers.json. + * + * @return array JSON decoded implemenation info. + */ + public function implementation() { + $implarr = [ + 'provider_name' => $this->providername, + 'provider_url' => $this->providerurl, + 'endpoints' => [], + ]; + foreach ($this->endpoints as $endpoint) { + $implarr['endpoints'][] = [ + 'schemes' => $endpoint->schemes, + 'url' => $endpoint->url, + 'discovery' => $endpoint->discovery, + 'formats' => $endpoint->formats, + ]; + } + return $implarr; + } + /** + * If a matching endpoint scheme is found in the passed text, return a consumer request URL. + * + * @param string $text The text to look for an URL resource using provider's schemes. + * @return string Consumer request URL. + */ + public function get_oembed_request($text) { + $requesturl = ''; + // For each endpoint, look for a matching scheme. + foreach ($this->endpoints as $endpoint) { + // Get the regex arrauy to look for matching schemes. + $regexarr = $this->endpoints_regex($endpoint); + foreach ($regexarr as $regex) { + // Endpoints may have invalid regex strings that cause preg_match to throw an exception. We need to skip them + // in that case. Eventually, need to figure out how to inform the site about this. + try { + if (preg_match($regex, $text)) { + // If {format} is in the URL, replace it with the actual format. + // At the moment, we're only supporting JSON, so this must be JSON. + $requesturl = str_replace('{format}', 'json', $endpoint->url) . + '?url=' . urlencode($text) . '&format=json'; + break 2; // Done, break out of all loops. + } + } catch (\Exception $e) { + continue; + } + } + } + + return $requesturl; + } + + /** + * Make a consumer oembed request and return the JSON provider response. + * + * @param string $url The consumer request URL. + * @return array JSON decoded array. + */ + public function oembed_response($url) { + $ret = download_file_content($url, null, null, true, 300, 20, false, null, false); + return json_decode($ret->results, true); + } + + /** + * Return the type of the provider source parameter (download, local, plugin). + * + * @param string $source The source value to get the type for. + * @return string One of 'download::', 'local::', 'plugin::'. + */ + public static function source_type($source) { + $sourcetype = substr($source, 0, strpos($source, '::')); + if (empty($sourcetype)) { + $sourcetype = self::PROVIDER_SOURCE_LOCAL; + } else { + $sourcetype .= '::'; + } + return $sourcetype; + } + + /** + * Return a regular expression that can be used to search text for an endpoint's schemes. + * + * @param endpoint $endpoint + * @return array Array of regular expressions matching all endpoints and schemes. + */ + protected function endpoints_regex(endpoint $endpoint) { + $schemes = $endpoint->schemes; + if (empty($schemes)) { + $schemes = [$this->providerurl]; + } + + foreach ($schemes as $scheme) { + // An "http[s]:" may not be present, so flag it as a non-capturing subpattern with "(?:". + $url1 = preg_split('/((?:https?:)?\/\/)/', $scheme); + if (!empty($url1[1])) { + $url2 = preg_split('/\//', $url1[1]); + $regexarr = []; + foreach ($url2 as $url) { + $find = ['.', '*']; + $replace = ['\.', '.*?']; + $url = str_replace($find, $replace, $url); + $regexarr[] = '(' . $url . ')'; + } + + $regex[] = '/(https?:\/\/)' . implode('\/', $regexarr) . '/'; + } + } + return $regex; + } + + /** + * Magic method for getting properties. + * @param string $name + * @return mixed + * @throws \coding_exception + */ + public function __get($name) { + $allowed = ['id', 'enabled', 'providername', 'providerurl', 'endpoints', 'source']; + if (in_array($name, $allowed)) { + return $this->$name; + } else { + throw new \coding_exception($name.' is not a publicly accessible property of '.get_class($this)); + } + } + + /** + * Set enabled? + * @param boolean $enabled + */ + public function set_enabled($enabled) { + $this->enabled = $enabled; + } +} diff --git a/filter/oembed/classes/service/oembed.php b/filter/oembed/classes/service/oembed.php new file mode 100644 index 000000000..8771db4fc --- /dev/null +++ b/filter/oembed/classes/service/oembed.php @@ -0,0 +1,743 @@ +. + +/** + * Filter for component 'filter_oembed' + * + * @package filter_oembed + * @copyright Erich M. Wappis / Guy Thomas 2016 + * @author Erich M. Wappis + * @author Guy Thomas + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace filter_oembed\service; +use filter_oembed\db\providerrow; +use filter_oembed\provider\provider; +use Exception; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/filelib.php'); + +/** + * Class oembed + * @package filter_oembed\service + * @copyright Erich M. Wappis / Guy Thomas 2016 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * Singleton class providing function for filtering embedded content links in text. + */ +class oembed { + + /** + * @var array + */ + protected $warnings = []; + + /** + * @var provider[] + */ + protected $providers = []; + + /** + * Constructor - protected singeton. + * + * @param string $providerstate Either 'enabled', 'disabled', or 'all'. + */ + protected function __construct($providerstate = 'enabled') { + $this->set_providers($providerstate); + } + + /** + * Singleton + * + * @param string $providerstate Either 'enabled', 'disabled', or 'all'. + * @return oembed + */ + public static function get_instance($providerstate = 'enabled') { + /** @var $instance oembed */ + static $instance = []; + if (!isset($instance[$providerstate])) { + $instance[$providerstate] = new oembed($providerstate); + } + return $instance[$providerstate]; + } + + /** + * Set providers property. + * + * @param string $state Either 'enabled', 'disabled', or 'all'. + */ + protected function set_providers($state = 'enabled') { + switch ($state) { + case 'enabled': + $providers = self::get_enabled_provider_data(); + break; + + case 'disabled': + $providers = self::get_disabled_provider_data(); + break; + + case 'all': + default: + $providers = self::get_all_provider_data(); + break; + } + foreach ($providers as $provider) { + $this->providers[$provider->id] = $this->get_provider_instance($provider); + } + } + + /** + * Get a provider instance using appropriate provider class. + * + * @param object $provider Data record from oembed_filter table. + * @return object provider Object of provider class or extended class. + */ + protected function get_provider_instance($provider) { + global $CFG; + + $pluginprefix = provider::PROVIDER_SOURCE_PLUGIN; + if (provider::source_type($provider->source) == $pluginprefix) { + $name = substr($provider->source, strlen($pluginprefix)); + require_once($CFG->dirroot.'/filter/oembed/provider/'.$name.'/'.$name.'.php'); + $classname = "\\filter_oembed\\provider\\{$name}"; + return new $classname($provider); + } else { + return new provider($provider); + } + } + + /** + * Filter text - convert oembed divs and links into oembed code. + * + * @param string $text + * @return string + */ + public function html_output($text) { + $lazyload = get_config('filter_oembed', 'lazyload'); + $lazyload = $lazyload == 1 || $lazyload === false; + $output = ''; + + // Loop through each provider asking for a match. + foreach ($this->providers as $provider) { + if (($completeoutput = $provider->filter($text)) !== false) { + // Plugins may provide everything required. If so, just return it. + $output = $completeoutput; + break; + } else if ($requesturl = $provider->get_oembed_request($text)) { + // Get additional url params out of original url. + $parsed = parse_url($text); + $query = isset($parsed['query']) ? $parsed['query'] : ''; + $params = []; + parse_str($query, $params); + + // If we have a consumer request, we're done searching. Try for a response. + $jsonret = $provider->oembed_response($requesturl); + if (!$jsonret) { + $output = ''; + } else if ($lazyload) { + $output = $this->oembed_getpreloadhtml($jsonret, $params); + } else { + $output = $this->oembed_gethtml($jsonret, $params); + } + break; // Done, break out of all loops. + } + } + return $output; + } + + /** + * Get oembed html. + * + * @param array $jsonarr + * @param array $params + * @return string + * @throws \coding_exception + */ + protected function oembed_gethtml($jsonarr, $params = []) { + if ($jsonarr === null) { + $this->warnings[] = get_string('connection_error', 'filter_oembed'); + return ''; + } + + $embed = $jsonarr['html']; + + // Add original params into content url. + // This is necessary for youtube videos - e.g. preserving auto play, etc.. + if (!empty($params)) { + $paramstr = ''; + foreach ($params as $key => $val) { + $paramstr .= '&'; + $paramstr .= $key . '=' . urlencode($val); + } + $embed = str_replace('?feature=oembed', '?feature=oembed'.$paramstr, $embed); + } + + $aspectratio = 0; + + if (!empty($jsonarr['width']) && !empty($jsonarr['height'])) { + $width = $jsonarr['width']; + $height = $jsonarr['height']; + $aspectratio = $this->get_aspect_ratio($width, $height); + } + if ($aspectratio > 0) { + $padding = $aspectratio * 100; + $paddiv = '
'; + // Wrapper for responsive processing. + $output = '
' . $embed . $paddiv . '
'; + } else { + // Wrapper for responsive processing. + $output = '
' . $embed . '
'; + } + + return $output; + } + + /** + * Generate preloader html. + * @param array $jsonarr + * @param array $params + * @return string + */ + protected function oembed_getpreloadhtml(array $jsonarr, $params = []) { + global $PAGE; + /** @var \filter_oembed\output\renderer $renderer */ + $renderer = $PAGE->get_renderer('filter_oembed'); + + // To surpress the loadHTML Warnings. + $dom = new \DOMDocument(); + libxml_use_internal_errors(true); + $dom->loadHTML($jsonarr['html']); + libxml_use_internal_errors(false); + + // Get aspect ratio of iframe or use width in json. + if ($dom->getElementsByTagName('iframe')->length > 0) { + $width = $dom->getElementsByTagName('iframe')->item(0)->getAttribute('width'); + $height = $dom->getElementsByTagName('iframe')->item(0)->getAttribute('height'); + $aspectratio = self::get_aspect_ratio($width, $height); + if ($aspectratio === 0) { + if (isset($jsonarr['width']) && isset($jsonarr['height'])) { + $width = $jsonarr['width']; + $height = $jsonarr['height']; + $aspectratio = self::get_aspect_ratio($width, $height); + if ($aspectratio === 0) { + // Couldn't get a decent aspect ratio, let's go with 0.5625 (16:9). + $aspectratio = 0.5625; + } + } + } + if ($aspectratio !== 0) { + $jsonarr['aspectratio'] = $aspectratio * 100; + } + + // This html is intentionally hardcoded and excluded from the mustache template as javascript relies on it. + $jsonarr['jshtml'] = ' data-aspect-ratio = "'.$aspectratio.'" '; + } + + return $renderer->preload($this->oembed_gethtml($jsonarr, $params), $jsonarr); + } + + // ---- PROVIDER DATA MANAGEMENT SECTION ---- + + /** + * Function to update provider data in database with current provider sources. + * + * @return string Any notification messages. + */ + public static function update_provider_data() { + global $DB; + + $warnings = []; + // Is there any data currently at all? + if ($DB->count_records('filter_oembed') <= 0) { + // Initial load. + try { + self::create_initial_provider_data(); + } catch (Exception $e) { + // Handle no initial data situation. + $warnings[] = $e->getMessage(); + } + } else { + // Update all existing provider data. + try { + $providers = self::download_providers(); + } catch (Exception $e) { + $warnings[] = $e->getMessage(); + $providers = []; + } + mtrace(' Checking for updated downloads...'); + self::update_downloaded_providers($providers); + mtrace(' Checking for updated subplugins...'); + self::update_plugin_providers(self::get_plugin_providers()); + } + + // If no providers were retrieved, log the issue. + return $warnings; + } + + /** + * Get the latest provider list from http://oembed.com/providers.json + * + * @return space array + */ + protected static function download_providers() { + global $CFG; + + // Wondering if there is any reason to make this configurable? + $www = 'http://oembed.com/providers.json'; + + $timeout = 15; + + // Ensure that the configuration doesn't prevent us from downloading. + // This is a hack caused by new settings not always existing on a new install in 3.2. + $hackedport = false; + if (!isset($CFG->curlsecurityallowedport)) { + $CFG->curlsecurityallowedport = ''; + $hackedport = true; + } + $hackedhosts = false; + if (!isset($CFG->curlsecurityblockedhosts)) { + $CFG->curlsecurityblockedhosts = ''; + $hackedhosts = true; + } + + $ret = download_file_content($www, null, null, true, $timeout, 20, false, null, false); + + if ($hackedport) { + unset($CFG->curlsecurityallowedport); + } + if ($hackedhosts) { + unset($CFG->curlsecurityblockedhosts); + } + + if ($ret->status == '200') { + $ret = $ret->results; + } else { + $ret = ''; + } + + $providers = json_decode($ret, true); + + if (!is_array($providers)) { + $providers = false; + } + + if (empty($providers)) { + throw new \moodle_exception('error:noproviders', 'filter_oembed', ''); + } + + return $providers; + } + + /** + * Function to get providers from a local, static JSON file, for last resort action. + * + * @return space array + */ + protected static function get_local_providers() { + global $CFG; + + $ret = file_get_contents($CFG->dirroot.'/filter/oembed/provider/providers.json'); + return json_decode($ret, true); + } + + /** + * Function to return a list of providers provided by the current sub plugins. + * Since Moodle doesn't currently support subplugins for filters, do this in this plugin. + * + * @return space array + */ + protected static function get_plugin_providers() { + global $CFG; + + $pluginproviders = []; + $path = $CFG->dirroot.'/filter/oembed/provider/'; + $thisdir = new \DirectoryIterator($path); + foreach ($thisdir as $dir) { + if ($dir->isDir()) { + $name = $dir->getFilename(); + if (($name != '.') && ($name != '..')) { + require_once($CFG->dirroot.'/filter/oembed/provider/'.$name.'/'.$name.'.php'); + $classname = "\\filter_oembed\\provider\\{$name}"; + $newplugin = new $classname(); + $pluginproviders[] = array_merge($newplugin->implementation(), ['plugin' => $name]); + } + } + } + return $pluginproviders; + } + + /** + * Create initial provider data from known provider sources. + * + */ + protected static function create_initial_provider_data() { + global $CFG; + + $warnings = []; + try { + $providers = self::download_providers(); + $source = provider::PROVIDER_SOURCE_DOWNLOAD . 'http://oembed.com/providers.json'; + } catch (Exception $e) { + $warnings[] = $e->getMessage(); + // If no providers were retrieved, get the local, static ones. + $providers = self::get_local_providers(); + if (empty($providers)) { + throw new \moodle_exception('No initial provider data available. Oembed filter will not function properly.'); + } + $source = provider::PROVIDER_SOURCE_LOCAL . $CFG->dirroot.'/filter/oembed/provider/providers.json'; + } + + // Load each downloaded provider into the database. + self::update_downloaded_providers($providers, $source); + + // Next, add the plugin providers that exist. + $providers = self::get_plugin_providers(); + self::update_plugin_providers($providers); + + return $warnings; + } + + /** + * Update the database with the downloaded provider data. + * At this point, we can't depend on the Provider Name being unique, or consistent. + * + * @param array $providers The JSON decoded provider data. + * @param string $source The source name for the provided providers. + */ + private static function update_downloaded_providers(array $providers, $source = null) { + global $DB; + + if ($source === null) { + $source = provider::PROVIDER_SOURCE_DOWNLOAD . 'http://oembed.com/providers.json'; + } + + // Get current providers as array indexed by id. + $currentproviders = self::get_all_provider_data(); + + foreach ($providers as $provider) { + // Get a matching provider if it exists. + $currprovider = self::match_provider_names($currentproviders, $provider); + + if ($currprovider !== false) { + // Existing provider exists; check for update. + $change = false; + + if ($currprovider->providerurl != $provider['provider_url']) { + // Perform change URL actions. + $currprovider->providerurl = $provider['provider_url']; + $change = true; + } + + $endpoints = json_encode($provider['endpoints']); + if ($currprovider->endpoints != $endpoints) { + // Perform change endpoints actions. + $currprovider->endpoints = $endpoints; + $change = true; + } + + if ($change) { + mtrace(' updating '.$currprovider->providername); + $currprovider->timemodified = time(); + $DB->update_record('filter_oembed', $currprovider); + } + unset($currentproviders[$currprovider->id]); + + } else { + // New provider. + $record = new \stdClass(); + $record->providername = $provider['provider_name']; + $record->providerurl = $provider['provider_url']; + $record->endpoints = json_encode($provider['endpoints']); + $record->source = $source; + $record->enabled = 0; // Disable everything by default. + $record->timecreated = time(); + $record->timemodified = time(); + mtrace(' creating '.$record->providername); + $DB->insert_record('filter_oembed', $record); + } + } + + // Any current providers left must have been deleted if they have the same source. + foreach ($currentproviders as $providerdata) { + if ($providerdata->source == $source) { + // Perform delete provider actions. + mtrace(' deleting '.$providerdata->providername); + $DB->delete_records('filter_oembed', ['id' => $providerdata->id]); + } + } + } + + /** + * Update the database with the plugin provider data. + * + * @param array $providers The JSON decoded provider data. + */ + private static function update_plugin_providers(array $providers) { + global $DB; + + $source = provider::PROVIDER_SOURCE_PLUGIN; + + // Get current providers as array. + $currentproviders = self::get_all_provider_data(); + + foreach ($providers as $provider) { + // Get a matching provider if it exists. + $currprovider = self::match_provider_names($currentproviders, $provider); + + if ($currprovider !== false) { + // Existing provider exists, remove for delete check. + unset($currentproviders[$currprovider->id]); + } else { + // New provider. + $record = new \stdClass(); + $record->providername = $provider['provider_name']; + $record->providerurl = $provider['provider_url']; + $record->endpoints = json_encode($provider['endpoints']); + $record->source = $source.$provider['plugin']; + $record->enabled = 1; // Enable plugins by default. + $record->timecreated = time(); + $record->timemodified = time(); + mtrace(' creating '.$record->providername); + $DB->insert_record('filter_oembed', $record); + } + } + + // Any current plugin providers left must have been deleted if they have the same source. + foreach ($currentproviders as $providerdata) { + if (provider::source_type($providerdata->source) == $source) { + // Perform delete provider actions. + mtrace(' deleting '.$providerdata->providername); + $DB->delete_records('filter_oembed', ['id' => $providerdata->id]); + } + } + } + + /** + * Static function to search an array of database records for a specific name. + * Note this is "final protected" rather than "private" so it can be unit tested. + * + * @param array $providerarray An array of provider data records. + * @param array $provider The provider information to match. + * @return object A data record object. + */ + final protected static function match_provider_names($providerarray, $provider) { + $foundrecord = false; + $foundarray = []; + foreach ($providerarray as $providerrecord) { + if ($providerrecord->providername == $provider['provider_name']) { + $foundarray[] = $providerrecord; + } + } + if (count($foundarray) > 1) { + // If more than one with the same name, match the url. Otherwise return false. + foreach ($foundarray as $match) { + if ($match->providerurl == $provider['provider_url']) { + $foundrecord = $match; + } + } + } else if (!empty($foundarray)) { + // If only one with matching name, use it. + $foundrecord = reset($foundarray); + } + return $foundrecord; + } + + // ---- OTHER HELPER FUNCTIONS ---- + + /** + * Magic method for getting properties. + * @param string $name + * @return mixed + * @throws \coding_exception + */ + public function __get($name) { + $allowed = ['providers', 'warnings']; + if (in_array($name, $allowed)) { + return $this->$name; + } else { + throw new \coding_exception($name.' is not a publicly accessible property of '.get_class($this)); + } + } + + /** + * Set the provider to "enabled". + * + * @param int | provider The provider to enable. + */ + public function enable_provider($provider) { + $this->set_provider_enable_value($provider, 1); + } + + /** + * Set the provider to "disabled". + * + * @param int | provider The provider to disable. + */ + public function disable_provider($provider) { + $this->set_provider_enable_value($provider, 0); + } + + /** + * Delete the local provider. + * + * @param int | provider The provider to delete. + */ + public function delete_provider($provider) { + global $DB; + + if (is_object($provider)) { + $lookup = ['providername' => $provider->providername]; + $pid = $DB->get_field('filter_oembed', 'id', $lookup); + } else if (is_int($provider) || is_numeric($provider)) { + $pid = $provider; + } else { + throw new \coding_exception('oembed::enable_provider requires either a provider object or a data id integer.'); + } + + if (!isset($this->providers[$pid])) { + // Already deleted? + return; + } + + // Only delete local providers this way. + if (provider::source_type($this->providers[$pid]->source) == provider::PROVIDER_SOURCE_LOCAL) { + $DB->delete_records('filter_oembed', ['id' => $pid]); + unset($this->providers[$pid]); + } + } + + /** + * Get provider row from db. + * @param int $providerid The provider id for which we want to retrieve. + * @return providerrow + */ + public function get_provider_row($providerid) { + global $DB; + return new providerrow($DB->get_record('filter_oembed', ['id' => $providerid])); + } + + /** + * Update provider row. + * @param array|object $providerdata + * @return bool + */ + public function update_provider_row($providerdata) { + global $DB; + return $DB->update_record('filter_oembed', $providerdata); + } + + /** + * Copy downloaded provider row to new local row (or update). + * @param array|object $providerdata + * @return bool|int + */ + public function copy_provider_to_local($providerdata) { + global $DB; + if (provider::source_type($providerdata['source']) != provider::PROVIDER_SOURCE_DOWNLOAD) { + return false; + } + $newsource = provider::PROVIDER_SOURCE_LOCAL . strtolower(str_replace(' ', '', $providerdata['providername'])); + if ($DB->record_exists('filter_oembed', ['source' => $newsource])) { + return false; + } + $providerdata['source'] = $newsource; + return $DB->insert_record('filter_oembed', $providerdata, true); + } + + /** + * Set the provider enabled field to the specified value. + * + * @param int | object $provider The provider to modify. + * @param int $value Value to set. + */ + private function set_provider_enable_value($provider, $value) { + global $DB; + + if (is_object($provider)) { + $lookup = ['providername' => $provider->providername]; + $pid = $DB->get_field('filter_oembed', 'id', $lookup); + } else if (is_int($provider) || is_numeric($provider)) { + $pid = $provider; + } else { + throw new \coding_exception('oembed::enable_provider requires either a provider object or a data id integer.'); + } + + $DB->set_field('filter_oembed', 'enabled', $value, ['id' => $pid]); + $this->providers[$pid]->set_enabled($value == 1); + } + + /** + * Attempt to get aspect ratio from strings. + * @param string $width + * @param string $height + * @return float|int + */ + protected static function get_aspect_ratio($width, $height) { + $bothperc = strpos($height, '%') !== false && strpos($width, '%') !== false; + $neitherperc = strpos($height, '%') === false && strpos($width, '%') === false; + // If both height and width use percentages or both don't then we can calculate an aspect ratio. + if ($bothperc || $neitherperc) { + // Calculate aspect ratio. + $aspectratio = intval($height) / intval($width); + } else { + $aspectratio = 0; + } + return $aspectratio; + } + + /** + * Get enabled provder data from the filter table and return as array of data records. + * Provider data is set when the plugin is installed, by scheduled tasks, by admin tools and + * by subplugins. + * @return array data records. + */ + protected static function get_enabled_provider_data() { + global $DB; + + // Get providers from database. This includes sub-plugins. + return $DB->get_records('filter_oembed', array('enabled' => 1)); + } + + /** + * Get disabled provder data from the filter table and return as array of data records. + * Provider data is set when the plugin is installed, by scheduled tasks, by admin tools and + * by subplugins. + * @return array data records. + */ + protected static function get_disabled_provider_data() { + global $DB; + + // Get providers from database. This includes sub-plugins. + return $DB->get_records('filter_oembed', array('enabled' => 0)); + } + + /** + * Get all provder data from the filter table and return as array of data records. + * Provider data is set when the plugin is installed, by scheduled tasks, by admin tools and + * by subplugins. + * + * @param string $fields Comma separated list of fields, the first of which is the index of the returned array. + * @return array data records. + */ + protected static function get_all_provider_data($fields = '*') { + global $DB; + + // Get providers from database. This includes sub-plugins. + return $DB->get_records('filter_oembed', null, '', $fields); + } +} diff --git a/filter/oembed/classes/service/util.php b/filter/oembed/classes/service/util.php new file mode 100644 index 000000000..3e07cd564 --- /dev/null +++ b/filter/oembed/classes/service/util.php @@ -0,0 +1,103 @@ +. + +/** + * General utility class + * @author gthomas2 + * @copyright Copyright (c) 2016 Moodlerooms Inc. (http://www.moodlerooms.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace filter_oembed\service; + +use external_value; +use coding_exception; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/lib/externallib.php'); + +/** + * General utility class. + * @author gthomas2 + * @copyright Copyright (c) 2016 Moodlerooms Inc. (http://www.moodlerooms.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class util { + /** + * Returns an array of \external_values based on a class or object for use with defining a webservice. + * + * NOTE: Current limitations - does not recurse to properties that are object instances or arrays. + * + * @param $classorobject + * @throws coding_exception + * @return external_value[] + */ + public static function define_class_for_webservice($classorobject) { + $reflect = new \ReflectionClass($classorobject); + + $public = $reflect->getProperties(\ReflectionProperty::IS_PUBLIC); + $singlemembers = []; + foreach ($public as $property) { + $name = $property->getName(); + $comment = $property->getDocComment(); + $regex = '/(?<=\*\s@wsparam\s)(\S*)\s(.*)/'; + $matches = []; + $haswsparamdoc = preg_match($regex, $comment, $matches); + if ($haswsparamdoc === 1) { + if (!defined($matches[1])) { + throw new coding_exception('Unknown / incompatible var type '.$matches[1].' for '.$name); + } + if (count($matches) < 3) { + throw new coding_exception('Missing description for '.$name); + } + $description = $matches[2]; + $type = constant($matches[1]); + } else { + $regex = '/(?<=\*\s@var\s)(\S*)\s(.*)/'; + $matches = []; + $aliases = [ + 'bool' => PARAM_BOOL, + 'str' => PARAM_RAW, + 'string' => PARAM_RAW, + 'int' => PARAM_INT, + 'integer' => PARAM_INT + ]; + $hasvardoc = preg_match($regex, $comment, $matches); + if ($hasvardoc !== 1) { + throw new coding_exception('Property without @var or @wsparam doc'); + } + if (count($matches) < 3) { + throw new coding_exception('Missing description for '.$name); + } + $description = $matches[2]; + $type = $matches[1]; + if (isset($aliases[$type])) { + $type = $aliases[$type]; + } else { + throw new coding_exception('Unknown / incompatible var type '.$type.' for '.$name); + } + } + + $regex = '/(?<=\*\s@wsrequired\s)/'; + $required = preg_match($regex, $comment, $matches); + + $singlemembers[$name] = new external_value($type, $description, $required); + + } + + return $singlemembers; + } +} diff --git a/filter/oembed/classes/task/update_providers.php b/filter/oembed/classes/task/update_providers.php new file mode 100644 index 000000000..a7f53ca28 --- /dev/null +++ b/filter/oembed/classes/task/update_providers.php @@ -0,0 +1,48 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +namespace filter_oembed\task; + +defined('MOODLE_INTERNAL') || die(); + +use filter_oembed\service\oembed; + +class update_providers extends \core\task\scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('updateproviders', 'filter_oembed'); + } + + /** + * Run forum cron. + */ + public function execute() { + oembed::update_provider_data(); + } + +} diff --git a/filter/oembed/classes/webservice/ws_provider_manage.php b/filter/oembed/classes/webservice/ws_provider_manage.php new file mode 100644 index 000000000..c0ac06019 --- /dev/null +++ b/filter/oembed/classes/webservice/ws_provider_manage.php @@ -0,0 +1,96 @@ +. + +namespace filter_oembed\webservice; + +use filter_oembed\output\providermodel; +use filter_oembed\service\util; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../../../../lib/externallib.php'); + +/** + * Web service for managing provider visibility. + * @author Guy Thomas + * @copyright Copyright (c) 2016 Moodlerooms Inc. (http://www.moodlerooms.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class ws_provider_manage extends \external_api { + /** + * @return \external_function_parameters + */ + public static function service_parameters() { + $parameters = [ + 'pid' => new \external_value(PARAM_INT, 'Provider id', VALUE_REQUIRED), + 'action' => new \external_value(PARAM_ALPHA, 'Action: enable / disable / reload / delete', VALUE_REQUIRED) + ]; + return new \external_function_parameters($parameters); + } + + /** + * @return \external_single_structure + */ + public static function service_returns() { + $keys = [ + 'visible' => new \external_value(PARAM_INT, 'Provider visibility', VALUE_REQUIRED), + 'providermodel' => new \external_single_structure( + util::define_class_for_webservice('filter_oembed\output\providermodel'), + 'Provider renderable', + VALUE_OPTIONAL + ) + ]; + + return new \external_single_structure($keys, 'provider'); + } + + /** + * @param int $pid + * @param string $action + * @return array + */ + public static function service($pid, $action) { + $oembed = \filter_oembed\service\oembed::get_instance('all'); + + if ($action === 'enable' || $action === 'disable') { + if ($action === 'enable') { + $oembed->enable_provider($pid); + } else { + $oembed->disable_provider($pid); + } + } + + if ($action === 'delete') { + $oembed->delete_provider($pid); + return [ + 'visible' => 0 + ]; + } else { + $providerrow = $oembed->get_provider_row($pid); + $providermodel = new providermodel($providerrow); + $visible = intval($providerrow->enabled); + if ($action === 'enable' || $action === 'disable') { + $visible = $action === 'enable' ? 1 : 0; + } + return [ + 'visible' => $visible, + 'providermodel' => $providermodel + ]; + } + + throw new coding_exception('Invalid action - '.$action); + } +} diff --git a/filter/oembed/classes/webservice/ws_providers.php b/filter/oembed/classes/webservice/ws_providers.php new file mode 100644 index 000000000..b32ada6e9 --- /dev/null +++ b/filter/oembed/classes/webservice/ws_providers.php @@ -0,0 +1,93 @@ +. + +namespace filter_oembed\webservice; + +use filter_oembed\output\managementpage; +use filter_oembed\service\oembed; +use filter_oembed\service\util; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../../../../lib/externallib.php'); + +/** + * Web service for getting array of provider models. + * @author Guy Thomas + * @copyright Copyright (c) 2016 Moodlerooms Inc. (http://www.moodlerooms.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class ws_providers extends \external_api { + /** + * @return \external_function_parameters + */ + public static function service_parameters() { + $parameters = [ + 'scope' => new \external_value(PARAM_ALPHA, 'Providers scope - all, enabled, disabled', VALUE_OPTIONAL, 'all') + ]; + return new \external_function_parameters($parameters); + } + + /** + * @return \external_single_structure + */ + public static function service_returns() { + $keys = [ + 'downloadrows' => new \external_multiple_structure( + new \external_single_structure( + util::define_class_for_webservice('filter_oembed\output\providermodel'), + 'Provider renderable', + VALUE_REQUIRED + ), 'Array of downloaded providers', VALUE_REQUIRED + ), + 'pluginrows' => new \external_multiple_structure( + new \external_single_structure( + util::define_class_for_webservice('filter_oembed\output\providermodel'), + 'Provider renderable', + VALUE_REQUIRED + ), 'Array of plugin providers', VALUE_REQUIRED + ), + 'localrows' => new \external_multiple_structure( + new \external_single_structure( + util::define_class_for_webservice('filter_oembed\output\providermodel'), + 'Provider renderable', + VALUE_REQUIRED + ), 'Array of local providers', VALUE_REQUIRED + ) + + ]; + + return new \external_single_structure($keys, 'Providers array.'); + } + + /** + * @param int $pid + * @param string $action + * @return array + */ + public static function service($scope) { + global $PAGE; + $PAGE->set_context(\context_system::instance()); + $output = $PAGE->get_renderer('core', '', RENDERER_TARGET_GENERAL); + + $oembed = oembed::get_instance($scope); + + $providerrows = $oembed->providers; + + $page = new managementpage($providerrows); + return $page->export_for_template($output); + } +} diff --git a/filter/oembed/db/caches.php b/filter/oembed/db/caches.php index d453abace..c6d491367 100644 --- a/filter/oembed/db/caches.php +++ b/filter/oembed/db/caches.php @@ -21,6 +21,9 @@ * @copyright 2016 Blackboard Inc. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + +defined('MOODLE_INTERNAL') || die(); + $definitions = array( 'embeddata' => array( 'mode' => cache_store::MODE_APPLICATION, diff --git a/filter/oembed/db/install.php b/filter/oembed/db/install.php index 6b272ae93..26e3cbde4 100644 --- a/filter/oembed/db/install.php +++ b/filter/oembed/db/install.php @@ -25,12 +25,15 @@ * Soundcloud (Troy Williams) */ +defined('MOODLE_INTERNAL') || die(); + +use filter_oembed\service\oembed; /** * Installs the OEmbed filter. */ function xmldb_filter_oembed_install() { - global $CFG; - filter_set_global_state('filter/oembed', TEXTFILTER_ON); -} + // Insert the initial data elements from the instance's providers. + oembed::update_provider_data(); +} \ No newline at end of file diff --git a/filter/oembed/db/install.xml b/filter/oembed/db/install.xml new file mode 100644 index 000000000..6b0785706 --- /dev/null +++ b/filter/oembed/db/install.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/filter/oembed/db/services.php b/filter/oembed/db/services.php new file mode 100644 index 000000000..d06d4fc84 --- /dev/null +++ b/filter/oembed/db/services.php @@ -0,0 +1,44 @@ +. + +/** + * Services + * @author Guy Thomas + * @copyright Copyright (c) 2016 Moodlerooms Inc. (http://www.moodlerooms.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$functions = [ + 'filter_oembed_provider_manage' => [ + 'classname' => 'filter_oembed\\webservice\\ws_provider_manage', + 'methodname' => 'service', + 'description' => 'Manage provider visibility / reload', + 'type' => 'write', + 'ajax' => true, + 'loginrequired' => true + ], + 'filter_oembed_providers' => [ + 'classname' => 'filter_oembed\\webservice\\ws_providers', + 'methodname' => 'service', + 'description' => 'Array of providers', + 'type' => 'read', + 'ajax' => true, + 'loginrequired' => true + ] +]; + diff --git a/filter/oembed/db/subplugins.php b/filter/oembed/db/subplugins.php new file mode 100644 index 000000000..fb7f2646d --- /dev/null +++ b/filter/oembed/db/subplugins.php @@ -0,0 +1,30 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +defined('MOODLE_INTERNAL') || die(); + +// Note: Filters do not currently support subplugins, so this file is here +// in case they ever do. +$subplugins = [ + 'oembedprovider' => 'filter/oembed/provider' +]; \ No newline at end of file diff --git a/filter/oembed/db/tasks.php b/filter/oembed/db/tasks.php new file mode 100644 index 000000000..a0c4efc2c --- /dev/null +++ b/filter/oembed/db/tasks.php @@ -0,0 +1,36 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +defined('MOODLE_INTERNAL') || die(); + +$tasks = array( + array( + 'classname' => 'filter_oembed\task\update_providers', + 'blocking' => 0, + 'minute' => 'R', + 'hour' => '3', + 'day' => '*', + 'month' => '*', + 'dayofweek' => '*' + ) +); diff --git a/filter/oembed/db/upgrade.php b/filter/oembed/db/upgrade.php index f6ee5c61f..7409bdfd6 100644 --- a/filter/oembed/db/upgrade.php +++ b/filter/oembed/db/upgrade.php @@ -25,6 +25,11 @@ * Soundcloud (Troy Williams) */ +defined('MOODLE_INTERNAL') || die(); + +use filter_oembed\service\oembed; +use filter_oembed\provider\provider; + /** * Upgrades the OEmbed filter. * @@ -32,7 +37,106 @@ * @return bool Success. */ function xmldb_filter_oembed_upgrade($oldversion) { - global $CFG, $DB, $OUTPUT; + global $DB; + + $dbman = $DB->get_manager(); + + if ($oldversion < 2016070501) { + + // Define table filter_oembed to be created. + $table = new xmldb_table('filter_oembed'); + + // Adding fields to table filter_oembed. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('providername', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('providerurl', XMLDB_TYPE_CHAR, '1333', null, XMLDB_NOTNULL, null, null); + $table->add_field('endpoints', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('source', XMLDB_TYPE_CHAR, '255', null, null, null, null); + $table->add_field('enabled', XMLDB_TYPE_INTEGER, '1', null, null, null, '0'); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, null, null, '0'); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, null, null, '0'); + + // Adding keys to table filter_oembed. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table filter_oembed. + $table->add_index('providernameix', XMLDB_INDEX_NOTUNIQUE, array('providername')); + + // Conditionally launch create table for filter_oembed. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Insert the initial data elements from the instance's providers. + oembed::update_provider_data(); + + // Migrate old settings to new settings. Ensure all old filters are still present. + $config = get_config('filter_oembed'); + $providermap = [ + 'youtube' => ['YouTube', 'http://www.youtube.com', ['http://www.youtube.com/*'], + 'http://www.youtube.com/oembed'], + 'vimeo' => ['Vimeo', 'http://vimeo.com', ['http://vimeo.com/*'], 'https://vimeo.com/api/omebed.json'], + 'ted' => ['Ted', 'http://ted.com', ['http://ted.com/talks/*'], 'http://www.ted.com/talks/oembed.json'], + 'slideshare' => ['SlideShare', 'http://www.slideshare.net', + ['http://www.slideshare.net/*'], 'http://www.slideshare.net/api/oembed/2'], + 'officemix' => ['Office Mix', 'http://mix.office.com', ['http://mix.office.com/*'], + 'https://mix.office.com/oembed'], + 'issuu' => ['ISSUU', 'http://issuu.com', ['http://issuu.com/*'], 'http://issuu.com/oembed'], + 'soundcloud' => ['SoundCloud', 'http://soundcloud.com', ['http://soundcloud.com/*'], + 'https://soundcloud.com/oembed'], + 'pollev' => ['Poll Everywhere', 'http://polleverywhere.com', + ['http://polleverywhere.com/polls/*', 'http://polleverywhere.com/multiple_choice_polls/*', + 'http://polleverywhere.com/free_text_polls/*'], 'http://www.polleverywhere.com/services/oembed'], + 'o365video' => ['Office365 Video', '', [''], ''], + 'sway' => ['Sway', 'https://www,sway.com', ['http://www.sway.com/*'], 'https://sway.com/api/v1.0/oembed'], + 'provider_docsdotcom_enabled' => ['Docs', '', [''], ''], + 'provider_powerbi_enabled' => ['Power BI', '', [''], ''], + 'provider_officeforms_enabled' => ['Office Forms', '', [''], ''] + ]; + + foreach ($providermap as $oldprovider => $newprovider) { + // There may be more than one provider with the same name. If that happens, use the first. + $provider = $DB->get_record('filter_oembed', ['providername' => $newprovider[0]], '*', IGNORE_MULTIPLE); + + // Look for originally hard-coded plugins. If still not present, create it from old code. + // If it is present, assume that it has since been added to the oembed repo and use that. + $insert = false; + + if (empty($provider)) { + // Handle non-downloaded Oembed types. + $insert = true; + $provider = new stdClass(); + $provider->providername = $newprovider[0]; + $provider->providerurl = $newprovider[1]; + $endpoints = [ + 'schemes' => $newprovider[2], + 'url' => $newprovider[3], + ]; + $provider->endpoints = json_encode($endpoints); + if (($oldprovider == 'provider_powerbi_enabled') || ($oldprovider == 'provider_officeforms_enabled') || + ($oldprovider == 'o365video')) { + $provider->source = provider::PROVIDER_SOURCE_PLUGIN . $oldprovider; + } else { + $provider->source = provider::PROVIDER_SOURCE_LOCAL . 'oldoembed'; + } + $provider->timecreated = time(); + } + $provider->enabled = (!isset($config->$oldprovider) || empty($config->$oldprovider)) ? 0 : 1; + $provider->timemodified = time(); + if ($insert) { + $DB->insert_record('filter_oembed', $provider); + } else { + $DB->update_record('filter_oembed', $provider); + } + unset_config($oldprovider, 'filter_oembed'); + } + + // Remove other configuration settings no longer used. + unset_config('providersrestrict', 'filter_oembed'); + + // Oembed savepoint reached. + upgrade_plugin_savepoint(true, 2016070501, 'filter', 'oembed'); + } return true; -} +} \ No newline at end of file diff --git a/filter/oembed/filter.php b/filter/oembed/filter.php index 366f0a2b2..c6e67e86e 100644 --- a/filter/oembed/filter.php +++ b/filter/oembed/filter.php @@ -18,447 +18,84 @@ * Filter for component 'filter_oembed' * * @package filter_oembed - * @copyright 2012 Matthew Cannings; modified 2015 by Microsoft, Inc. + * @copyright Erich M. Wappis / Guy Thomas 2016 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * code based on the following filters... - * Screencast (Mark Schall) - * Soundcloud (Troy Williams) + * @author Mat Cannings + * @author James McQuillan + * @author Vin Bhalerao + * @author Erich M. Wappis + * @author Guy Thomas + * @author Mike Churchward */ defined('MOODLE_INTERNAL') || die(); +use filter_oembed\service\oembed; + require_once($CFG->libdir.'/filelib.php'); /** - * Filter for processing HTML content containing links to media from services that support the OEmbed protocol. - * The filter replaces the links with the embeddable content returned from the service via the Oembed protocol. + * Main filter class for embedded remote content. * * @package filter_oembed + * @copyright Erich M. Wappis / Guy Thomas 2016 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class filter_oembed extends moodle_text_filter { /** - * Set up the filter using settings provided in the admin settings page. - * - * @param $page - * @param $context - */ - public function setup($page, $context) { - // This only requires execution once per request. - static $jsinitialised = false; - if (get_config('filter_oembed', 'lazyload')) { - if (empty($jsinitialised)) { - $page->requires->yui_module( - 'moodle-filter_oembed-lazyload', - 'M.filter_oembed.init_filter_lazyload', - array(array('courseid' => 0))); - $jsinitialised = true; - } - } - if (get_config('filter_oembed', 'provider_powerbi_enabled')) { - global $PAGE; - $PAGE->requires->yui_module('moodle-filter_oembed-powerbiloader', 'M.filter_oembed.init_powerbiloader'); - } - } - - /** - * Filters the given HTML text, looking for links pointing to media from services that support the Oembed - * protocol and replacing them with the embeddable content returned from the protocol. + * content gets filtered, links either wrapped in an tag or in a
tag with class="oembed" + * will be replaced by embeded content * * @param $text HTML to be processed. * @param $options * @return string String containing processed HTML. */ public function filter($text, array $options = array()) { - global $CFG; + global $PAGE; - if (!is_string($text) or empty($text)) { - // Non string data can not be filtered anyway. - return $text; + static $initialised = false; + + if (!$initialised) { + $PAGE->requires->js_call_amd('filter_oembed/oembed', 'init'); + $initialised = true; } - // if (get_user_device_type() !== 'default'){ - // no lazy video on mobile - // return $text; - // } - if (stripos($text, '') === false) { + $targettag = get_config('filter_oembed', 'targettag'); + + if ($targettag == 'atag' && stripos($text, '') === false) { // Performance shortcut - all regexes below end with the tag. // If not present nothing can match. return $text; } - $newtext = $text; // We need to return the original value if regex fails! - - if (get_config('filter_oembed', 'youtube')) { - $search = '/]*href="((https?:\/\/(www\.)?)(youtube\.com|youtu\.be|youtube\.googleapis.com)\/(?:embed\/|v\/|watch\?v=|watch\?.+?&v=|watch\?.+?&v=)?((\w|-){11})(.*?))"(.*?)>(.*?)<\/a>/is'; - $newtext = preg_replace_callback($search, 'filter_oembed_youtubecallback', $newtext); - } - if (get_config('filter_oembed', 'vimeo')) { - $search = '/]*href="(https?:\/\/(www\.)?)(vimeo\.com)\/(\d+)(.*?)"(.*?)>(.*?)<\/a>/is'; - $newtext = preg_replace_callback($search, 'filter_oembed_vimeocallback', $newtext); - } - if (get_config('filter_oembed', 'slideshare')) { - $search = '/]*href="(https?:\/\/(www\.)?)(slideshare\.net)\/(.*?)"(.*?)>(.*?)<\/a>/is'; - $newtext = preg_replace_callback($search, 'filter_oembed_slidesharecallback', $newtext); - } - if (get_config('filter_oembed', 'officemix')) { - $search = '/]*href="(https?:\/\/(www\.)?)(mix\.office\.com)\/(.*?)"(.*?)>(.*?)<\/a>/is'; - $newtext = preg_replace_callback($search, 'filter_oembed_officemixcallback', $newtext); - } - if (get_config('filter_oembed', 'issuu')) { - $search = '/]*href="(https?:\/\/(www\.)?)(issuu\.com)\/(.*?)"(.*?)>(.*?)<\/a>/is'; - $newtext = preg_replace_callback($search, 'filter_oembed_issuucallback', $newtext); - } - if (get_config('filter_oembed', 'soundcloud')) { - $search = '/]*href="(https?:\/\/(www\.)?)(soundcloud\.com)\/(.*?)"(.*?)>(.*?)<\/a>/is'; - $newtext = preg_replace_callback($search, 'filter_oembed_soundcloudcallback', $newtext); - } - if (get_config('filter_oembed', 'ted')) { - $search = '/]*href="(https?:\/\/(www\.)?)(ted\.com)\/talks\/(.*?)"(.*?)>(.*?)<\/a>/is'; - $newtext = preg_replace_callback($search, 'filter_oembed_tedcallback', $newtext); - } - if (get_config('filter_oembed', 'pollev')) { - $search = '/]*href="(https?:\/\/(www\.)?)(polleverywhere\.com)\/(polls|multiple_choice_polls|free_text_polls)\/(.*?)"(.*?)>(.*?)<\/a>/is'; - $newtext = preg_replace_callback($search, 'filter_oembed_pollevcallback', $newtext); - } - $odburl = get_config('local_o365', 'odburl'); - if (get_config('filter_oembed', 'o365video') && !empty($odburl)) { - $odburl = preg_replace('/^https?:\/\//', '', $odburl); - $odburl = preg_replace('/\/.*/', '', $odburl); - $trimedurl = preg_replace("/-my/", "", $odburl); - $search = '/]*href="(https?:\/\/)('.$odburl.'|'.$trimedurl.')\/(.*?)"(.*?)>(.*?)<\/a>/is'; - $newtext = preg_replace_callback($search, 'filter_oembed_o365videocallback', $newtext); - } - if (get_config('filter_oembed', 'sway')) { - $search = '/]*href="(https?:\/\/(www\.)?)(sway\.com)\/(.*?)"(.*?)>(.*?)<\/a>/is'; - $newtext = preg_replace_callback($search, 'filter_oembed_swaycallback', $newtext); - } - - // New method for embed providers. - $providers = static::get_supported_providers(); - $filterconfig = get_config('filter_oembed'); - foreach ($providers as $provider) { - $enabledkey = 'provider_'.$provider.'_enabled'; - if (!empty($filterconfig->$enabledkey)) { - $providerclass = '\filter_oembed\provider\\'.$provider; - if (class_exists($providerclass)) { - $provider = new $providerclass(); - $newtext = $provider->filter($newtext); - } - } + $filtered = $text; // We need to return the original value if regex fails! + if ($targettag == 'divtag') { + $search = '/\]*data-oembed-href="(.*?)"(.*?)>(.*?)\<\/div\>/'; + } else { // Using 'atag'. + $search = '/\]*href="(.*?)"(?:.*?)>(?:.*?)\<\/a\>/is'; } - if (empty($newtext) or $newtext === $text) { - // Error or not filtered. - unset($newtext); + $filtered = preg_replace_callback($search, 'self::find_oembeds_callback', $filtered); + if (empty($filtered)) { + // If $filtered is emtpy return original $text. return $text; + } else { + return $filtered; } - - return $newtext; } /** - * Return list of supported providers. + * Callback function to be used by the main filter + * + * @param $match array An array of matched groups, where [1] is the URL matched. * - * @return array Array of supported providers. */ - public static function get_supported_providers() { - return [ - 'docsdotcom', 'powerbi', 'officeforms' - ]; - } -} - -/** - * Looks for links pointing to Youtube content and processes them. - * - * @param $link HTML tag containing a link - * @return string HTML content after processing. - */ -function filter_oembed_youtubecallback($link) { - global $CFG; - $url = "http://www.youtube.com/oembed?url=".urlencode(trim($link[1]))."&format=json"; - $jsonret = filter_oembed_curlcall($url); - return filter_oembed_vidembed($jsonret, trim($link[7])); -} - -/** - * Looks for links pointing to Vimeo content and processes them. - * - * @param $link HTML tag containing a link - * @return string HTML content after processing. - */ -function filter_oembed_vimeocallback($link) { - global $CFG; - $url = "http://vimeo.com/api/oembed.json?url=".trim($link[1]).trim($link[2]).trim($link[3]).'/'.trim($link[4]).'&maxwidth=480&maxheight=270'; - $jsonret = filter_oembed_curlcall($url); - return filter_oembed_vidembed($jsonret); -} - -/** - * Looks for links pointing to TED content and processes them. - * - * @param $link HTML tag containing a link - * @return string HTML content after processing. - */ -function filter_oembed_tedcallback($link) { - global $CFG; - $url = "http://www.ted.com/services/v1/oembed.json?url=".trim($link[1]).trim($link[3]).'/talks/'.trim($link[4]).'&maxwidth=480&maxheight=270'; - $jsonret = filter_oembed_curlcall($url); - return filter_oembed_vidembed($jsonret); -} - -/** - * Looks for links pointing to SlideShare content and processes them. - * - * @param $link HTML tag containing a link - * @return string HTML content after processing. - */ -function filter_oembed_slidesharecallback($link) { - global $CFG; - $url = "http://www.slideshare.net/api/oembed/2?url=".trim($link[1]).trim($link[3]).'/'.trim($link[4])."&format=json&maxwidth=480&maxheight=270"; - $json = filter_oembed_curlcall($url); - return $json === null ? '

'. get_string('connection_error', 'filter_oembed') .'

' : $json['html']; -} - -/** - * Looks for links pointing to Microsoft Office Mix content and processes them. - * - * @param $link HTML tag containing a link - * @return string HTML content after processing. - */ -function filter_oembed_officemixcallback($link) { - global $CFG; - $url = "https://mix.office.com/oembed/?url=".trim($link[1]).trim($link[2]).trim($link[3]).'/'.trim($link[4]); - $json = filter_oembed_curlcall($url); - - if($json === null){ - return '

'. get_string('connection_error', 'filter_oembed') .'

'; - } - - // Increase the height and width of iframe. - $json['html'] = str_replace('width="348"', 'width="480"', $json['html']); - $json['html'] = str_replace('height="245"', 'height="320"', $json['html']); - $json['html'] = str_replace('height="310"', 'height="410"', $json['html']); - $json['html'] = str_replace('height="267"', 'height="350"', $json['html']); - return filter_oembed_vidembed($json); -} - -/** - * Looks for links pointing to PollEverywhere content and processes them. - * - * @param $link HTML tag containing a link - * @return string HTML content after processing. - */ -function filter_oembed_pollevcallback($link) { - global $CFG; - $url = "http://www.polleverywhere.com/services/oembed?url=".trim($link[1]).trim($link[3]).'/'.trim($link[4]).'/'.trim($link[5])."&format=json&maxwidth=480&maxheight=270"; - $json = filter_oembed_curlcall($url); - return $json === null ? '

'. get_string('connection_error', 'filter_oembed') .'

' : $json['html']; -} - -/** - * Looks for links pointing to Issuu content and processes them. - * - * @param $link HTML tag containing a link - * @return string HTML content after processing. - */ -function filter_oembed_issuucallback($link) { - global $CFG; - $url = "http://issuu.com/oembed?url=".trim($link[1]).trim($link[3]).'/'.trim($link[4])."&format=json"; - $json = filter_oembed_curlcall($url); - return $json === null ? '

'. get_string('connection_error', 'filter_oembed') .'

' : $json['html']; -} - -/** - * Looks for links pointing to SoundCloud content and processes them. - * - * @param $link HTML tag containing a link - * @return string HTML content after processing. - */ -function filter_oembed_soundcloudcallback($link) { - global $CFG; - $url = "http://soundcloud.com/oembed?url=".trim($link[1]).trim($link[3]).'/'.trim($link[4])."&format=json&maxwidth=480&maxheight=270'"; - $json = filter_oembed_curlcall($url); - return filter_oembed_vidembed($json); -} - -/** - * Looks for links pointing to Office 365 Video content and processes them. - * - * @param $link HTML tag containing a link - * @return string HTML content after processing. - */ -function filter_oembed_o365videocallback($link) { - if (empty($link[3])) { - return $link[0]; - } - $link[3] = preg_replace("/&/", "&", $link[3]); - $values = array(); - parse_str($link[3], $values); - if (empty($values['chid']) || empty($values['vid'])) { - return $link[0]; - } - if (!\local_o365\rest\sharepoint::is_configured()) { - \local_o365\utils::debug('filter_oembed share point is not configured', 'filter_oembed_o365videocallback'); - return $link[0]; - } - try { - $spresource = \local_o365\rest\sharepoint::get_resource(); - if (!empty($spresource)) { - $httpclient = new \local_o365\httpclient(); - $clientdata = \local_o365\oauth2\clientdata::instance_from_oidc(); - $sptoken = \local_o365\oauth2\systemtoken::instance(null, $spresource, $clientdata, $httpclient); - if (!empty($sptoken)) { - $sharepoint = new \local_o365\rest\sharepoint($sptoken, $httpclient); - // Retrieve api url for video service. - $url = $sharepoint->videoservice_discover(); - if (!empty($url)) { - $sharepoint->override_resource($url); - $width = 640; - if (!empty($values['width'])) { - $width = $values['width']; - } - $height = 360; - if (!empty($values['height'])) { - $height = $values['height']; - } - // Retrieve embed code. - return $sharepoint->get_video_embed_code($values['chid'], $values['vid'], $width, $height); - } - } + private static function find_oembeds_callback($match) { + $instance = oembed::get_instance(); + $result = $instance->html_output($match[1]); + if (empty($result)) { + $result = $match[0]; } - } catch (\Exception $e) { - \local_o365\utils::debug('filter_oembed share point execption: '.$e->getMessage(), 'filter_oembed_o365videocallback', $e); + return $result; } - return $link[0]; -} - -/** - * Looks for links pointing to sway.com content and processes them. - * - * @param $link HTML tag containing a link - * @return string HTML content after processing. - */ -function filter_oembed_swaycallback($link) { - global $CFG; - $width = 500; - $height = 760; - $link[4] = preg_replace("/&/", "&", $link[4]); - $id = preg_replace("/^(.*)(\?(.*)?)/", "$1", $link[4]); - $url = "https://www.sway.com/s/".trim($id)."/embed"; - // Check for optional width and height passed as query string. - if (preg_match("/width/", $link[4])) { - $query = array(); - parse_str(preg_replace("/^(.*)\?/", "", $link[4]), $query); - if (!empty($query['width'])) { - $width = $query['width']; - } - if (!empty($query['height'])) { - $height = $query['height']; - } - } - $options = array( - 'class' => 'oembed_sway', - 'width' => $width.'px', - 'height' => $height.'px', - 'src' => $url, - 'frameborder' => '0', - 'marginwidth' => '0', - 'scrolling' => 'no', - 'style' => 'border: none; max-width:100%; max-height:100vh', - 'allowfullscreen' => '', - 'webkitallowfullscreen' => '', - 'msallowfullscreen' => '', - ); - return html_writer::tag('iframe', '', $options); -} - -/** - * Makes the OEmbed request to the service that supports the protocol. - * - * @param $url URL for the Oembed request - * @return mixed|null|string The HTTP response object from the OEmbed request. - */ -function filter_oembed_curlcall($url) { - static $cache; - - if (!isset($cache)) { - $cache = cache::make('filter_oembed', 'embeddata'); - } - - if ($ret = $cache->get(md5($url))) { - return json_decode($ret, true); - } - - $curl = new \curl(); - $ret = $curl->get($url); - - // Check if curl call fails. - if ($curl->errno != CURLE_OK) { - // Check if error is due to network connection. - if (in_array($curl->errno, [6, 7, 28])) { - // Try curl call up to 3 times. - usleep(50000); - $retryno = (!is_int($retryno)) ? 0 : $retryno+1; - if ($retryno < 3) { - return $this->getoembeddata($url, $retryno); - } else { - return null; - } - } else { - return null; - } - } - - $cache->set(md5($url), $ret); - $result = json_decode($ret, true); - return $result; -} - -/** - * Return the HTML content to be embedded given the response from the OEmbed request. - * This method returns the thumbnail image if we lazy loading is enabled. Ogtherwise it returns the - * embeddable HTML returned from the OEmbed request. An error message is returned if there was an error during - * the request. - * - * @param array $json Response object returned from the OEmbed request. - * @param string $params Additional parameters to include in the embed URL. - * @return string The HTML content to be embedded in the page. - */ -function filter_oembed_vidembed($json, $params = '') { - - if ($json === null) { - return '

'. get_string('connection_error', 'filter_oembed') .'

'; - } - - $embed = $json['html']; - - if ($params != ''){ - $embed = str_replace('?feature=oembed', '?feature=oembed'.htmlspecialchars($params), $embed ); - } - - if (get_config('filter_oembed', 'lazyload')) { - $embed = htmlspecialchars($embed); - $dom = new DOMDocument(); - - // To surpress the loadHTML Warnings. - libxml_use_internal_errors(true); - $dom->loadHTML($json['html']); - libxml_use_internal_errors(false); - - // Get height and width of iframe. - $height = $dom->getElementsByTagName('iframe')->item(0)->getAttribute('height'); - $width = $dom->getElementsByTagName('iframe')->item(0)->getAttribute('width'); - - $embedcode = '
'; - $embedcode .= ''; - $embedcode .= '
'.$json['title'].'
'; - $embedcode .= ''; - $embedcode .= '
'; - } else { - $embedcode = $embed; - } - - return $embedcode; } diff --git a/filter/oembed/lang/de/filter_oembed.php b/filter/oembed/lang/de/filter_oembed.php index 5f6801343..1dd834805 100644 --- a/filter/oembed/lang/de/filter_oembed.php +++ b/filter/oembed/lang/de/filter_oembed.php @@ -15,25 +15,27 @@ // along with Moodle. If not, see . /** - * Language strings for component 'filter_oembed' + * Filter for component 'filter_oembed' * * @package filter_oembed - * @copyright 2012 Matthew Cannings; modified 2015 by Microsoft Inc. + * @copyright Erich M. Wappis / Guy Thomas 2016 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * code based on the following filters... - * Screencast (Mark Schall) - * Soundcloud (Troy Williams) + * code based on the following filter + * oEmbed filter ( Mike Churchward, James McQuillan, Vinayak (Vin) Bhalerao, Josh Gavant and Rob Dolin) */ -$string['filtername'] = 'oEmbed-Filter'; -$string['youtube'] = 'YouTube'; -$string['vimeo'] = 'Vimeo'; -$string['ted'] = 'Ted Talks'; -$string['slideshare'] = 'SlideShare'; -$string['officemix'] = 'Office Mix'; -$string['issuu'] = 'Issuu'; -$string['screenr'] = 'Screenr'; -$string['soundcloud'] = 'SoundCloud'; -$string['pollev'] = 'Poll Everywhere'; -$string['lazyload'] = 'Delay Embed Loading (Lazyload)'; +$string['filtername'] = 'Embed Remote Content Filter'; +$string['cachelifespan_disabled'] = 'Cache Lebensdauer deaktiviert'; +$string['cachelifespan'] = 'Cache Lebensdauer'; +$string['cachelifespan_desc'] = 'Zeitabstand nach dem die Providerliste aktualisiert wird.'; +$string['cachelifespan_daily'] = '1 Tag'; +$string['cachelifespan_weekly'] = '1 Woche'; +$string['atag'] = 'Filtere < a > tags'; +$string['divtag'] = 'Filtere < div > tags'; +$string['targettag'] = 'Ziel tag'; +$string['targettag_desc'] = 'Welche Art von tag soll gefiltert werden? Links oder divs mit der oembed Klasse.'; +$string['providersrestrict'] = 'Providerbeschränkung'; +$string['providersrestrict_desc'] = 'Beschränke Provider mit einer List zugelassener Provider'; +$string['providersallowed'] = 'Zugelassene Provider.'; +$string['providersallowed_desc'] = 'Die Provider die vor diese Moodleinstallation verfügbar sind.'; $string['connection_error'] = 'Fehler beim Zugriff auf die integrierten Medien. Versuchen Sie, die Seite zu aktualisieren.'; diff --git a/filter/oembed/lang/en/filter_oembed.php b/filter/oembed/lang/en/filter_oembed.php index 872bfa115..c7479bd97 100644 --- a/filter/oembed/lang/en/filter_oembed.php +++ b/filter/oembed/lang/en/filter_oembed.php @@ -15,33 +15,50 @@ // along with Moodle. If not, see . /** - * Language strings for component 'filter_oembed' + * Filter for component 'filter_oembed' * * @package filter_oembed - * @copyright 2012 Matthew Cannings; modified 2015 by Microsoft Inc. + * @copyright Erich M. Wappis / Guy Thomas 2016 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * code based on the following filters... - * Screencast (Mark Schall) - * Soundcloud (Troy Williams) + * code based on the following filter + * oEmbed filter ( Mike Churchward, James McQuillan, Vinayak (Vin) Bhalerao, Josh Gavant and Rob Dolin) */ -$string['filtername'] = 'oEmbed Filter'; -$string['youtube'] = 'Youtube'; -$string['vimeo'] = 'Vimeo'; -$string['ted'] = 'Ted Talks'; -$string['slideshare'] = 'SlideShare'; -$string['officemix'] = 'Office Mix'; -$string['issuu'] = 'Issuu'; -$string['soundcloud'] = 'SoundCloud'; -$string['pollev'] = 'Poll Everywhere'; -$string['sway'] = 'Sway'; -$string['once'] = 'Once'; -$string['times'] = '{$a} times'; -$string['retrylimit'] = 'Limit to retry getting filtered content'; -$string['cachedef_embeddata'] = 'Cache for filtered content'; +$string['filtername'] = 'Oembed Filter'; +$string['atag'] = 'Filter on < a > tags'; +$string['cachelifespan_disabled'] = 'Cache lifespan disabled'; +$string['cachelifespan'] = 'Cache lifespan'; +$string['cachelifespan_desc'] = 'The duration of time before the providers list should be refreshed.'; +$string['cachelifespan_daily'] = '1 day'; +$string['cachelifespan_weekly'] = '1 week'; +$string['connection_error'] = 'Error connecting to external provider, please try reloading the page.'; +$string['copytolocal'] = 'Created new local provider definition for "{$a}".'; +$string['deleteprovidertitle'] = 'Delete provider'; +$string['deleteproviderconfirm'] = 'Are you sure you want to delete provider "{$a}"?'; +$string['divtag'] = 'Filter on < div > tags'; +$string['downloadproviders'] = 'Downloaded providers'; +$string['enabled'] = 'Enabled'; +$string['endpoints'] = 'End points'; $string['lazyload'] = 'Delay Embed Loading (Lazyload)'; -$string['connection_error'] = 'Error while accessing the embedded media. Please try refreshing the page.'; -$string['o365video'] = 'Office 365 Video'; -$string['provider_docsdotcom'] = 'Docs.com'; -$string['provider_powerbi'] = 'Power BI'; -$string['provider_officeforms'] = 'Forms'; +$string['localproviders'] = 'Local providers'; +$string['manageproviders'] = 'Manage providers'; +$string['nocopytolocal'] = 'Could not create new local provider definition for "{$a}". It may already exist.'; +$string['playoembed'] = 'Play'; +$string['pluginproviders'] = 'Plugin providers'; +$string['provider'] = 'Provider'; +$string['providername'] = 'Provider Name'; +$string['providersrestrict'] = 'Restrict providers'; +$string['providersrestrict_desc'] = 'Restrict providers to a list of allowed providers'; +$string['providersallowed'] = 'Providers allowed.'; +$string['providersallowed_desc'] = 'Providers whitelisted to be used with this plugin'; +$string['providerurl'] = 'Provider URL'; +$string['requiredfield'] = 'The field "{$a}" must be completed'; +$string['subplugintype_oembedprovider'] = 'Oembed provider'; +$string['subplugintype_oembedprovider_plural'] = 'Oembed providers'; +$string['saveasnew'] = 'Save as new local'; +$string['saveok'] = 'Successfully saved provider.'; +$string['savefailed'] = 'Failed to save provider.'; +$string['source'] = 'Provider source'; +$string['targettag'] = 'Target tag'; +$string['targettag_desc'] = 'What tag type should be filtered - anchors or divs with the oembed class.'; +$string['updateproviders'] = 'Update Oembed provider information.'; diff --git a/filter/oembed/lib.php b/filter/oembed/lib.php new file mode 100644 index 000000000..ad95266fa --- /dev/null +++ b/filter/oembed/lib.php @@ -0,0 +1,99 @@ +. + +/** + * General lib file + * @author Guy Thomas + * @copyright Copyright (c) 2016 Blackboard Inc. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use filter_oembed\forms\provider; + +/** + * Serve the edit form as a fragment. + * + * @param array $args List of named arguments for the fragment loader. + * @return string + */ +function filter_oembed_output_fragment_provider($args) { + global $PAGE, $CFG; + + $output = $PAGE->get_renderer('core', '', RENDERER_TARGET_GENERAL); + + $oembed = \filter_oembed\service\oembed::get_instance('all'); + + $data = null; + $ajaxdata = null; + if (!empty($args['formdata'])) { + $data = []; + parse_str($args['formdata'], $data); + if ($data) { + $ajaxdata = $data; + } + } else { + if (!isset($args['pid'])) { + throw new coding_exception('missing "pid" param'); + } else { + $data = $oembed->get_provider_row($args['pid']); + if (!$data) { + throw new coding_exception('Invalid "pid" param', $args['pid']); + } + $data = (array)$data; + } + } + + if (!isset($ajaxdata['enabled'])) { + $ajaxdata['enabled'] = 0; + } + $actionurl = $CFG->wwwroot.'/filter/oembed/manageproviders.php'; + // Pass the source type as custom data so it can by used to detetmine the type of edit. + $form = new provider($actionurl, \filter_oembed\provider\provider::source_type($data['source']), + 'post', '', null, true, $ajaxdata); + $form->validate_defined_fields(true); + $data['sourcetext'] = $data['source']; + $form->set_data($data); + + $msg = ''; + if (!empty($ajaxdata)) { + if ($form->is_validated()) { + // If editing a downloaded provider, create a new local one and disable the download one. + $sourcetype = \filter_oembed\provider\provider::source_type($ajaxdata['source']); + if ($sourcetype == \filter_oembed\provider\provider::PROVIDER_SOURCE_DOWNLOAD) { + $newpid = $oembed->copy_provider_to_local($ajaxdata); + if ($newpid) { + $msg = $output->notification(get_string('copytolocal', 'filter_oembed', $ajaxdata['providername']), + 'notifysuccess'); + // Return an empty div with the new provider id in it so we can target it later with the message in $msg. + return '
'.$msg; + } else { + $msg = $output->notification(get_string('nocopytolocal', 'filter_oembed', $ajaxdata['providername']), + 'notifyproblem'); + } + } else { + $success = $oembed->update_provider_row($ajaxdata); + if ($success) { + $msg = $output->notification(get_string('saveok', 'filter_oembed'), 'notifysuccess'); + } else { + $msg = $output->notification(get_string('savefailed', 'filter_oembed'), 'notifyproblem'); + } + } + } + } + return $form->render().$msg; +} diff --git a/filter/oembed/manageproviders.php b/filter/oembed/manageproviders.php new file mode 100644 index 000000000..2e6015921 --- /dev/null +++ b/filter/oembed/manageproviders.php @@ -0,0 +1,83 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ +require_once(dirname(__FILE__) . '/../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +require_login(); + +$systemcontext = context_system::instance(); +require_capability('moodle/site:config', $systemcontext); +admin_externalpage_setup('filter_oembed_providers'); + +$action = optional_param('action', '', PARAM_ALPHA); +$pid = optional_param('pid', 0, PARAM_INT); + +if (!empty($action)) { + require_sesskey(); +} + +$PAGE->requires->js_call_amd('filter_oembed/manageproviders', 'init'); + +$oembed = \filter_oembed\service\oembed::get_instance('all'); + +// Process actions. +switch ($action) { + case 'edit': + break; + + case 'disable': + $oembed->disable_provider($pid); + break; + + case 'enable': + $oembed->enable_provider($pid); + break; + + case 'delete': + $oembed->delete_provider($pid); + break; +} + +$PAGE->set_context($systemcontext); +$baseurl = new moodle_url('/filter/oembed/manageproviders.php'); +$PAGE->set_url($baseurl); +$PAGE->set_pagelayout('standard'); +$strmanage = get_string('manageproviders', 'filter_oembed'); +$PAGE->set_title($strmanage); +$PAGE->set_heading($strmanage); +$PAGE->requires->strings_for_js( + [ + 'deleteprovidertitle', + 'deleteproviderconfirm' + ], + 'filter_oembed' +); + +$output = $PAGE->get_renderer('filter_oembed'); +echo $output->header(); + +$managepage = new \filter_oembed\output\managementpage($oembed->providers); +echo $output->render($managepage); + +// Finish the page. +echo $output->footer(); diff --git a/filter/oembed/package.json b/filter/oembed/package.json new file mode 100644 index 000000000..491b689b7 --- /dev/null +++ b/filter/oembed/package.json @@ -0,0 +1,15 @@ +{ + "description": "Grunt tasks for sass.", + "private": true, + "devDependencies": { + "grunt": "~0.4.1", + "grunt-autoprefixer": "^3.0.3", + "grunt-contrib-csslint": "^0.5.0", + "grunt-contrib-jshint": "^0.12.0", + "grunt-sass": "^1.2.1", + "grunt-cssbeautifier": "^0.1.2", + "grunt-contrib-watch": "^0.6.1", + "grunt-exec": "~0.4.2", + "grunt-load-gruntfile" : "^0.0.2" + } +} diff --git a/filter/oembed/pix/darkbutton.png b/filter/oembed/pix/darkbutton.png deleted file mode 100644 index ec62fa03a..000000000 Binary files a/filter/oembed/pix/darkbutton.png and /dev/null differ diff --git a/filter/oembed/pix/play.png b/filter/oembed/pix/play.png deleted file mode 100644 index 7fa693b58..000000000 Binary files a/filter/oembed/pix/play.png and /dev/null differ diff --git a/filter/oembed/pix/play.svg b/filter/oembed/pix/play.svg new file mode 100644 index 000000000..027ed6077 --- /dev/null +++ b/filter/oembed/pix/play.svg @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/filter/oembed/pix/purplebutton.png b/filter/oembed/pix/purplebutton.png deleted file mode 100644 index 0442ec001..000000000 Binary files a/filter/oembed/pix/purplebutton.png and /dev/null differ diff --git a/filter/oembed/provider/docsdotcom/docsdotcom.php b/filter/oembed/provider/docsdotcom/docsdotcom.php new file mode 100644 index 000000000..9305f246f --- /dev/null +++ b/filter/oembed/provider/docsdotcom/docsdotcom.php @@ -0,0 +1,86 @@ +. + +/** + * @package filter_oembed + * @author James McQuillan + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright (C) 2016 onwards Microsoft, Inc. (http://microsoft.com/) + */ + +namespace filter_oembed\provider; + +defined('MOODLE_INTERNAL') || die(); + +/** + * oEmbed provider implementation for Docs.com + */ +class docsdotcom extends provider { + + /** + * Constructor. + * @param $data JSON decoded array or a data object containing all provider data. + */ + public function __construct($data = null) { + if ($data === null) { + $data = [ + 'providername' => 'Docs', + 'providerurl' => 'https://docs.com', + 'endpoints' => [ + ['schemes' => ['https://docs.com/*', 'https://www.docs.com/*'], + 'url' => 'https:\/\/docs.com\/api\/oembed', + 'formats' => ['json'] + ] + ] + ]; + } + parent::__construct($data); + } + + /** + * If a matching endpoint scheme is found in the passed text, return a consumer request URL. + * + * @param string $text The text to look for an URL resource using provider's schemes. + * @return string Consumer request URL. + */ + public function get_oembed_request($text) { + $requesturl = ''; + // Get the regex arrauy to look for matching schemes. + $regex = $this->endpoints_regex(new endpoint()); + if (preg_match($regex, $text, $matched)) { + $params = [ + 'url' => $matched[1]. $matched[3] . '/' . $matched[4] . '/' . $matched[5] . '/' . $matched[6], + 'format' => 'json', + 'maxwidth' => '600', + 'maxheight' => '400', + ]; + $oembedurl = new \moodle_url('https://docs.com/api/oembed', $params); + $requesturl = $oembedurl->out(false); + } + return $requesturl; + } + + /** + * Return a regular expression that can be used to search text for an endpoint's schemes. + * + * @param endpoint $endpoint + * @return array Array of regular expressions matching all endpoints and schemes. + */ + protected function endpoints_regex(endpoint $endpoint) { + return '/(https?:\/\/(www\.)?)(docs\.com)\/(.+?)\/(.+?)\/(.+?)/is'; + } +} diff --git a/filter/oembed/provider/docsdotcom/lang/en/oembedprovider_docsdotcom.php b/filter/oembed/provider/docsdotcom/lang/en/oembedprovider_docsdotcom.php new file mode 100644 index 000000000..89a74b643 --- /dev/null +++ b/filter/oembed/provider/docsdotcom/lang/en/oembedprovider_docsdotcom.php @@ -0,0 +1,24 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +$string['pluginname'] = 'Docsdotcom'; \ No newline at end of file diff --git a/filter/oembed/provider/docsdotcom/version.php b/filter/oembed/provider/docsdotcom/version.php new file mode 100644 index 000000000..bdafbbe1b --- /dev/null +++ b/filter/oembed/provider/docsdotcom/version.php @@ -0,0 +1,28 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'oembedprovider_docsdotcom'; +$plugin->version = 2016070500; +$plugin->requires = 2016081700; // Requires this Moodle version. diff --git a/filter/oembed/provider/issuu/issuu.php b/filter/oembed/provider/issuu/issuu.php new file mode 100644 index 000000000..d919651d4 --- /dev/null +++ b/filter/oembed/provider/issuu/issuu.php @@ -0,0 +1,51 @@ +. + +/** + * @package filter_oembed + * @author James McQuillan + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright (C) 2016 onwards Microsoft, Inc. (http://microsoft.com/) + */ + +namespace filter_oembed\provider; + +defined('MOODLE_INTERNAL') || die(); + +/** + * oEmbed provider implementation for ISSUU + */ +class issuu extends provider { + + /** + * Constructor. + * @param $data JSON decoded array or a data object containing all provider data. + */ + public function __construct($data = null) { + if ($data === null) { + $data = [ + 'providername' => 'ISSUU', + 'providerurl' => 'https://issuu.com', + 'endpoints' => [ + ['schemes' => ['https://issuu.com/*'], + 'url' => 'https://issuu.com/oembed'], + ], + ]; + } + parent::__construct($data); + } +} diff --git a/filter/oembed/provider/issuu/lang/en/oembedprovider_issuu.php b/filter/oembed/provider/issuu/lang/en/oembedprovider_issuu.php new file mode 100644 index 000000000..4998dbd4e --- /dev/null +++ b/filter/oembed/provider/issuu/lang/en/oembedprovider_issuu.php @@ -0,0 +1,24 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +$string['pluginname'] = 'ISSUU'; \ No newline at end of file diff --git a/filter/oembed/provider/issuu/version.php b/filter/oembed/provider/issuu/version.php new file mode 100644 index 000000000..b03776407 --- /dev/null +++ b/filter/oembed/provider/issuu/version.php @@ -0,0 +1,28 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'oembedprovider_issuu'; +$plugin->version = 2016070500; +$plugin->requires = 2016081700; // Requires this Moodle version. diff --git a/filter/oembed/provider/o365video/lang/en/oembedprovider_o365video.php b/filter/oembed/provider/o365video/lang/en/oembedprovider_o365video.php new file mode 100644 index 000000000..9927b0e7c --- /dev/null +++ b/filter/oembed/provider/o365video/lang/en/oembedprovider_o365video.php @@ -0,0 +1,24 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +$string['pluginname'] = 'Office365 video'; \ No newline at end of file diff --git a/filter/oembed/provider/o365video/o365video.php b/filter/oembed/provider/o365video/o365video.php new file mode 100644 index 000000000..2cd24e740 --- /dev/null +++ b/filter/oembed/provider/o365video/o365video.php @@ -0,0 +1,128 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright (C) 2016 onwards Microsoft Open Technologies, Inc. (http://msopentech.com/) + */ + +namespace filter_oembed\provider; + +defined('MOODLE_INTERNAL') || die(); + +/** + * oEmbed provider implementation for Docs.com + */ +class o365video extends provider { + + /** + * Constructor. + * @param $data JSON decoded array or a data object containing all provider data. + */ + public function __construct($data = null) { + if ($data === null) { + $data = [ + 'providername' => 'Office365 Video', + 'providerurl' => '', + 'endpoints' => [], + ]; + } + parent::__construct($data); + } + + /** + * Main filter function. This should only be used by subplugins, and it is preferable + * to not use it even then. Ideally, a provider plugin should provide a JSON oembed provider + * response (http://oembed.com/#section2.3) and let the main filter handle the HTML. Use this + * only if the HTML must be determined by the plugin. If implemented, ensure FALSE is returned + * if no filtering occurred. + * + * @param string $text Incoming text. + * @return string Filtered text, or false for no changes. + */ + public function filter($text) { + // PowerBI depends on 'local_o365' installed. If it isn't, return false. + if (\core_plugin_manager::instance()->get_plugin_info('local_o365') == null) { + return false; + } + + $newtext = ''; + $odburl = get_config('local_o365', 'odburl'); + if (!empty($odburl)) { + $odburl = preg_replace('/^https?:\/\//', '', $odburl); + $odburl = preg_replace('/\/.*/', '', $odburl); + $trimedurl = preg_replace("/-my/", "", $odburl); + $search = '/(https?:\/\/)('.$odburl.'|'.$trimedurl.')\/(.*)/is'; + $newtext = preg_replace_callback($search, [$this, 'get_replacement'], $text); + } + return (empty($newtext) || ($newtext == $text)) ? false : $newtext; + } + + /** + * Get the replacement oembed HTML. + * + * @param array $matched Matched URL. + * @return string The replacement text/HTML. + */ + public function get_replacement($matched) { + + if (empty($matched[3])) { + return $matched[0]; + } + $matched[3] = preg_replace("/&/", "&", $matched[3]); + $values = array(); + parse_str($matched[3], $values); + if (empty($values['chid']) || empty($values['vid'])) { + return $matched[0]; + } + if (!\local_o365\rest\sharepoint::is_configured()) { + \local_o365\utils::debug('filter_oembed share point is not configured', 'filter_oembed_o365videocallback'); + return $matched[0]; + } + try { + $spresource = \local_o365\rest\sharepoint::get_resource(); + if (!empty($spresource)) { + $httpclient = new \local_o365\httpclient(); + $clientdata = \local_o365\oauth2\clientdata::instance_from_oidc(); + $sptoken = \local_o365\oauth2\systemtoken::instance(null, $spresource, $clientdata, $httpclient); + if (!empty($sptoken)) { + $sharepoint = new \local_o365\rest\sharepoint($sptoken, $httpclient); + // Retrieve api url for video service. + $url = $sharepoint->videoservice_discover(); + if (!empty($url)) { + $sharepoint->override_resource($url); + $width = 640; + if (!empty($values['width'])) { + $width = $values['width']; + } + $height = 360; + if (!empty($values['height'])) { + $height = $values['height']; + } + // Retrieve embed code. + return $sharepoint->get_video_embed_code($values['chid'], $values['vid'], $width, $height); + } + } + } + } catch (\Exception $e) { + \local_o365\utils::debug('filter_oembed share point execption: '.$e->getMessage(), + 'filter_oembed_o365videocallback', $e); + } + return $matched[0]; + } +} \ No newline at end of file diff --git a/filter/oembed/provider/o365video/version.php b/filter/oembed/provider/o365video/version.php new file mode 100644 index 000000000..bf1fed18f --- /dev/null +++ b/filter/oembed/provider/o365video/version.php @@ -0,0 +1,31 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'oembedprovider_o365video'; +$plugin->version = 2016070500; +$plugin->requires = 2016081700; // Requires this Moodle version. +$plugin->dependencies = [ + 'local_o365' => 2016062003, +]; diff --git a/filter/oembed/provider/officeforms/lang/en/oembedprovider_officeforms.php b/filter/oembed/provider/officeforms/lang/en/oembedprovider_officeforms.php new file mode 100644 index 000000000..a0608aa5f --- /dev/null +++ b/filter/oembed/provider/officeforms/lang/en/oembedprovider_officeforms.php @@ -0,0 +1,24 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +$string['pluginname'] = 'Officeforms'; \ No newline at end of file diff --git a/filter/oembed/classes/provider/officeforms.php b/filter/oembed/provider/officeforms/officeforms.php similarity index 55% rename from filter/oembed/classes/provider/officeforms.php rename to filter/oembed/provider/officeforms/officeforms.php index 505d08f65..0fb544613 100644 --- a/filter/oembed/classes/provider/officeforms.php +++ b/filter/oembed/provider/officeforms/officeforms.php @@ -17,15 +17,41 @@ /** * @package filter_oembed * @author Aashay Zajriya + * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @copyright (C) 2016 onwards Microsoft Open Technologies, Inc. (http://msopentech.com/) */ namespace filter_oembed\provider; + +defined('MOODLE_INTERNAL') || die(); + /** * oEmbed provider implementation for Microsoft Forms */ -class officeforms extends base { +class officeforms extends provider { + + /** + * Constructor. + * @param $data JSON decoded array or a data object containing all provider data. + */ + public function __construct($data = null) { + if ($data === null) { + $data = [ + 'providername' => 'Office Forms', + 'providerurl' => 'https://forms.office.com/', + 'endpoints' => [ + ['schemes' => ['https://forms.office.com/Pages/ResponsePage.aspx?id=*', + 'https://www.forms.office.com/Pages/ResponsePage.aspx?id=*'], + 'url' => 'https://forms.office.com/Pages/ResponsePage.aspx?id=*&embed=true', + 'formats' => ['json'] + ] + ] + ]; + } + parent::__construct($data); + } + /** * Get the replacement oembed HTML. * @@ -33,8 +59,8 @@ class officeforms extends base { * @return string The replacement text/HTML. */ public function get_replacement($matched) { - if (!empty($matched)) { - $url = $matched[1].$matched[3].'/'.$matched[4].'/ResponsePage.aspx?id='.$matched[6].'&embed=true'; + if (!empty($matched) && !empty($matched[1])) { + $url = 'https://forms.office.com/Pages/ResponsePage.aspx?id='.$matched[1].'&embed=true'; $embedhtml = $this->getembedhtml($url); return $embedhtml; } @@ -42,14 +68,19 @@ public function get_replacement($matched) { } /** - * Filter the text. + * Main filter function. This should only be used by subplugins, and it is preferable + * to not use it even then. Ideally, a provider plugin should provide a JSON oembed provider + * response (http://oembed.com/#section2.3) and let the main filter handle the HTML. Use this + * only if the HTML must be determined by the plugin. If implemented, ensure FALSE is returned + * if no filtering occurred. * * @param string $text Incoming text. - * @return string Filtered text. + * @return string Filtered text, or false for no changes. */ public function filter($text) { - $search = '/]*href="(https?:\/\/(www\.)?)(forms\.office\.com)\/(.+?)\/(DesignPage\.aspx)#FormId=(.+?)"(.*?)>(.*?)<\/a>/is'; - return preg_replace_callback($search, [$this, 'get_replacement'], $text); + $search = '/(?:https?:\/\/(?:www\.)?)(?:forms\.office\.com)\/(?:.+?)\/(?:DesignPage\.aspx)#FormId=(.+)/is'; + $newtext = preg_replace_callback($search, [$this, 'get_replacement'], $text); + return (empty($newtext) || ($newtext == $text)) ? false : $newtext; } /** diff --git a/filter/oembed/provider/officeforms/version.php b/filter/oembed/provider/officeforms/version.php new file mode 100644 index 000000000..f8c5b5884 --- /dev/null +++ b/filter/oembed/provider/officeforms/version.php @@ -0,0 +1,28 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'oembedprovider_officeforms'; +$plugin->version = 2016070500; +$plugin->requires = 2016081700; // Requires this Moodle version. diff --git a/filter/oembed/provider/pollev/lang/en/oembedprovider_pollev.php b/filter/oembed/provider/pollev/lang/en/oembedprovider_pollev.php new file mode 100644 index 000000000..4bf06410d --- /dev/null +++ b/filter/oembed/provider/pollev/lang/en/oembedprovider_pollev.php @@ -0,0 +1,24 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +$string['pluginname'] = 'Poll Everywhere'; \ No newline at end of file diff --git a/filter/oembed/provider/pollev/pollev.php b/filter/oembed/provider/pollev/pollev.php new file mode 100644 index 000000000..200472782 --- /dev/null +++ b/filter/oembed/provider/pollev/pollev.php @@ -0,0 +1,55 @@ +. + +/** + * @package filter_oembed + * @author James McQuillan + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright (C) 2016 onwards Microsoft, Inc. (http://microsoft.com/) + */ + +namespace filter_oembed\provider; + +defined('MOODLE_INTERNAL') || die(); + +/** + * oEmbed provider implementation for Poll Everywhere + */ +class pollev extends provider { + + /** + * Constructor. + * @param $data JSON decoded array or a data object containing all provider data. + */ + public function __construct($data = null) { + if ($data === null) { + $data = [ + 'providername' => 'Poll Everywhere', + 'providerurl' => 'https://www.polleverywhere.com', + 'endpoints' => [ + ['schemes' => [ + 'https://www.polleverywhere.com/polls/*', + 'https://www.polleverywhere.com/multiple_choice_polls/*', + 'https://www.polleverywhere.com/free_text_polls/*', + ], + 'url' => 'https://www.polleverywhere.com/services/oembed'], + ], + ]; + } + parent::__construct($data); + } +} diff --git a/filter/oembed/provider/pollev/version.php b/filter/oembed/provider/pollev/version.php new file mode 100644 index 000000000..336fbe621 --- /dev/null +++ b/filter/oembed/provider/pollev/version.php @@ -0,0 +1,28 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'oembedprovider_pollev'; +$plugin->version = 2016070500; +$plugin->requires = 2016081700; // Requires this Moodle version. diff --git a/filter/oembed/provider/powerbi/lang/en/oembedprovider_powerbi.php b/filter/oembed/provider/powerbi/lang/en/oembedprovider_powerbi.php new file mode 100644 index 000000000..f56666037 --- /dev/null +++ b/filter/oembed/provider/powerbi/lang/en/oembedprovider_powerbi.php @@ -0,0 +1,24 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +$string['pluginname'] = 'Power BI'; \ No newline at end of file diff --git a/filter/oembed/provider/powerbi/powerbi.php b/filter/oembed/provider/powerbi/powerbi.php new file mode 100644 index 000000000..ac0cc0d64 --- /dev/null +++ b/filter/oembed/provider/powerbi/powerbi.php @@ -0,0 +1,111 @@ +. + +/** + * @package filter_oembed + * @author Sushant Gawali + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright (C) 2016 onwards Microsoft Open Technologies, Inc. (http://msopentech.com/) + */ + +namespace filter_oembed\provider; + +defined('MOODLE_INTERNAL') || die(); + +/** + * oEmbed provider implementation for Docs.com + */ +class powerbi extends provider { + + /** + * Constructor. + * @param $data JSON decoded array or a data object containing all provider data. + */ + public function __construct($data = null) { + if ($data === null) { + $data = [ + 'providername' => 'Power BI', + 'providerurl' => '', + 'endpoints' => [ + ['schemes' => ['https://powerbi.com/*/*/*/*/*', + 'https://app.powerbi.com/*/*/*/*/*'], + 'url' => '', + 'formats' => ['json'] + ] + ] + ]; + } + parent::__construct($data); + } + + /** + * Main filter function. This should only be used by subplugins, and it is preferable + * to not use it even then. Ideally, a provider plugin should provide a JSON oembed provider + * response (http://oembed.com/#section2.3) and let the main filter handle the HTML. Use this + * only if the HTML must be determined by the plugin. If implemented, ensure FALSE is returned + * if no filtering occurred. + * + * @param string $text Incoming text. + * @return string Filtered text, or false for no changes. + */ + public function filter($text) { + // PowerBI depends on 'local_o365' installed. If it isn't, return false. + if (\core_plugin_manager::instance()->get_plugin_info('local_o365') == null) { + return false; + } + + $search = '/(https?:\/\/(app\.)?)(powerbi\.com)\/(.+?)\/(.+?)\/(.+?)\/(.+?)\/(.+?)/is'; + $newtext = preg_replace_callback($search, [$this, 'get_replacement'], $text); + return (empty($newtext) || ($newtext == $text)) ? false : $newtext; + } + + /** + * Get the replacement oembed HTML. + * + * @param array $matched Matched URL. + * @return string The replacement text/HTML. + */ + public function get_replacement($matched) { + global $CFG; + require_once($CFG->dirroot . '/filter/oembed/provider/powerbi/rest/powerbi.php'); + + $httpclient = new \local_o365\httpclient(); + try { + $clientdata = \local_o365\oauth2\clientdata::instance_from_oidc(); + $resource = \filter_oembed\provider\powerbi\rest\powerbi::get_resource(); + $token = \local_o365\oauth2\systemtoken::instance(null, $resource, $clientdata, $httpclient); + if (!empty($token)) { + $powerbi = new \filter_oembed\provider\powerbi\rest\powerbi($token, $httpclient); + if ($matched[6] == 'reports') { + $reportsdata = $powerbi->apicall('get', 'reports'); + $embedurl = $powerbi->getreportoembedurl($matched[7], $reportsdata); + $embedhtml = $this->getembedhtml($embedurl); + $embedhtml .= ''; + return $embedhtml; + } + } + } catch (\Exception $e) { + \local_o365\utils::debug('filter_oembed oauth2 exeception: '.$e->getMessage(), 'filter_oembed_powerbicallback', $e); + } + return $matched[0]; + } + + private function getembedhtml($embedurl) { + return ''; + } +} diff --git a/filter/oembed/classes/rest/powerbi.php b/filter/oembed/provider/powerbi/rest/powerbi.php similarity index 95% rename from filter/oembed/classes/rest/powerbi.php rename to filter/oembed/provider/powerbi/rest/powerbi.php index 9401e806a..beff7b7e4 100644 --- a/filter/oembed/classes/rest/powerbi.php +++ b/filter/oembed/provider/powerbi/rest/powerbi.php @@ -21,7 +21,9 @@ * @copyright (C) 2014 onwards Microsoft Open Technologies, Inc. (http://msopentech.com/) */ -namespace filter_oembed\rest; +namespace filter_oembed\provider\powerbi\rest; + +defined('MOODLE_INTERNAL') || die(); /** * API client for Power BI. diff --git a/filter/oembed/provider/powerbi/version.php b/filter/oembed/provider/powerbi/version.php new file mode 100644 index 000000000..bc2374bd1 --- /dev/null +++ b/filter/oembed/provider/powerbi/version.php @@ -0,0 +1,31 @@ +. + +/** + * @package filter_oembed + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 The POET Group + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'oembedprovider_powerbi'; +$plugin->version = 2016070500; +$plugin->requires = 2016081700; // Requires this Moodle version. +$plugin->dependencies = [ + 'local_o365' => 2016062003, +]; diff --git a/filter/oembed/yui/powerbiloader/powerbiloader.js b/filter/oembed/provider/powerbi/yui/powerbiloader/powerbiloader.js similarity index 98% rename from filter/oembed/yui/powerbiloader/powerbiloader.js rename to filter/oembed/provider/powerbi/yui/powerbiloader/powerbiloader.js index c859b27ee..62ce0ef3f 100644 --- a/filter/oembed/yui/powerbiloader/powerbiloader.js +++ b/filter/oembed/provider/powerbi/yui/powerbiloader/powerbiloader.js @@ -33,8 +33,7 @@ YUI.add('moodle-filter_oembed-powerbiloader', function (Y) { message = JSON.stringify(m); // Push the message. this.contentWindow.postMessage(message, "*"); - ; - } + }; }, '@VERSION@', { requires: ['base'] }); \ No newline at end of file diff --git a/filter/oembed/provider/providers.json b/filter/oembed/provider/providers.json new file mode 100644 index 000000000..0670fd8b3 --- /dev/null +++ b/filter/oembed/provider/providers.json @@ -0,0 +1,1347 @@ +[ + { + "provider_name": "23HQ", + "provider_url": "http:\/\/www.23hq.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.23hq.com\/*\/photo\/*" + ], + "url": "http:\/\/www.23hq.com\/23\/oembed" + } + ] + }, + { + "provider_name": "Alpha App Net", + "provider_url": "https:\/\/alpha.app.net\/browse\/posts\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/alpha.app.net\/*\/post\/*", + "https:\/\/photos.app.net\/*\/*" + ], + "url": "https:\/\/alpha-api.app.net\/oembed", + "formats": [ + "json" + ] + } + ] + }, + { + "provider_name": "amCharts Live Editor", + "provider_url": "http:\/\/live.amcharts.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/live.amcharts.com\/*" + ], + "url": "http:\/\/live.amcharts.com\/oembed" + } + ] + }, + { + "provider_name": "Animatron", + "provider_url": "https:\/\/www.animatron.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/www.animatron.com\/project\/*", + "https:\/\/animatron.com\/project\/*" + ], + "url": "https:\/\/animatron.com\/oembed\/json", + "discovery": true + } + ] + }, + { + "provider_name": "Animoto", + "provider_url": "http:\/\/animoto.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/animoto.com\/play\/*" + ], + "url": "http:\/\/animoto.com\/oembeds\/create" + } + ] + }, + { + "provider_name": "AudioSnaps", + "provider_url": "http:\/\/audiosnaps.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/audiosnaps.com\/k\/*" + ], + "url": "http:\/\/audiosnaps.com\/service\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Blackfire.io", + "provider_url": "https:\/\/blackfire.io", + "endpoints": [ + { + "schemes": [ + "https:\/\/blackfire.io\/profiles\/*\/graph", + "https:\/\/blackfire.io\/profiles\/compare\/*\/graph" + ], + "url": "https:\/\/blackfire.io\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Cacoo", + "provider_url": "https:\/\/cacoo.com", + "endpoints": [ + { + "schemes": [ + "https:\/\/cacoo.com\/diagrams\/*" + ], + "url": "http:\/\/cacoo.com\/oembed.{format}" + } + ] + }, + { + "provider_name": "CatBoat", + "provider_url": "http:\/\/img.catbo.at\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/img.catbo.at\/*" + ], + "url": "http:\/\/img.catbo.at\/oembed.json", + "formats": [ + "json" + ] + } + ] + }, + { + "provider_name": "ChartBlocks", + "provider_url": "http:\/\/www.chartblocks.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/public.chartblocks.com\/c\/*" + ], + "url": "http:\/\/embed.chartblocks.com\/1.0\/oembed" + } + ] + }, + { + "provider_name": "chirbit.com", + "provider_url": "http:\/\/www.chirbit.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/chirb.it\/*" + ], + "url": "http:\/\/chirb.it\/oembed.{format}", + "discovery": true + } + ] + }, + { + "provider_name": "CircuitLab", + "provider_url": "https:\/\/www.circuitlab.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/www.circuitlab.com\/circuit\/*" + ], + "url": "https:\/\/www.circuitlab.com\/circuit\/oembed\/", + "discovery": true + } + ] + }, + { + "provider_name": "Clipland", + "provider_url": "http:\/\/www.clipland.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.clipland.com\/v\/*", + "https:\/\/www.clipland.com\/v\/*" + ], + "url": "https:\/\/www.clipland.com\/api\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Clyp", + "provider_url": "http:\/\/clyp.it\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/clyp.it\/*", + "http:\/\/clyp.it\/playlist\/*" + ], + "url": "http:\/\/api.clyp.it\/oembed\/", + "discovery": true + } + ] + }, + { + "provider_name": "Codepen", + "provider_url": "https:\/\/codepen.io", + "endpoints": [ + { + "schemes": [ + "http:\/\/codepen.io\/*", + "https:\/\/codepen.io\/*" + ], + "url": "http:\/\/codepen.io\/api\/oembed" + } + ] + }, + { + "provider_name": "Codepoints", + "provider_url": "https:\/\/codepoints.net", + "endpoints": [ + { + "schemes": [ + "http:\/\/codepoints.net\/*", + "https:\/\/codepoints.net\/*", + "http:\/\/www.codepoints.net\/*", + "https:\/\/www.codepoints.net\/*" + ], + "url": "https:\/\/codepoints.net\/api\/v1\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "CollegeHumor", + "provider_url": "http:\/\/www.collegehumor.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.collegehumor.com\/video\/*" + ], + "url": "http:\/\/www.collegehumor.com\/oembed.{format}", + "discovery": true + } + ] + }, + { + "provider_name": "Coub", + "provider_url": "http:\/\/coub.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/coub.com\/view\/*", + "http:\/\/coub.com\/embed\/*" + ], + "url": "http:\/\/coub.com\/api\/oembed.{format}" + } + ] + }, + { + "provider_name": "Crowd Ranking", + "provider_url": "http:\/\/crowdranking.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/crowdranking.com\/*\/*" + ], + "url": "http:\/\/crowdranking.com\/api\/oembed.{format}" + } + ] + }, + { + "provider_name": "Daily Mile", + "provider_url": "http:\/\/www.dailymile.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.dailymile.com\/people\/*\/entries\/*" + ], + "url": "http:\/\/api.dailymile.com\/oembed?format=json", + "formats": [ + "json" + ] + } + ] + }, + { + "provider_name": "Dailymotion", + "provider_url": "http:\/\/www.dailymotion.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.dailymotion.com\/video\/*" + ], + "url": "http:\/\/www.dailymotion.com\/services\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Deviantart.com", + "provider_url": "http:\/\/www.deviantart.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/*.deviantart.com\/art\/*", + "http:\/\/*.deviantart.com\/*#\/d*", + "http:\/\/fav.me\/*", + "http:\/\/sta.sh\/*" + ], + "url": "http:\/\/backend.deviantart.com\/oembed" + } + ] + }, + { + "provider_name": "Didacte", + "provider_url": "https:\/\/www.didacte.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/*.didacte.com\/a\/course\/*" + ], + "url": "https:\/\/*.didacte.com\/cards\/oembed'", + "discovery": true, + "formats": [ + "json" + ] + } + ] + }, + { + "provider_name": "Dipity", + "provider_url": "http:\/\/www.dipity.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.dipity.com\/*\/*\/" + ], + "url": "http:\/\/www.dipity.com\/oembed\/timeline\/" + } + ] + }, + { + "provider_name": "Docs", + "provider_url": "https:\/\/www.docs.com", + "endpoints": [ + { + "schemes": [ + "https:\/\/docs.com\/*", + "https:\/\/www.docs.com\/*" + ], + "url": "https:\/\/docs.com\/api\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Dotsub", + "provider_url": "http:\/\/dotsub.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/dotsub.com\/view\/*" + ], + "url": "http:\/\/dotsub.com\/services\/oembed" + } + ] + }, + { + "provider_name": "edocr", + "provider_url": "http:\/\/www.edocr.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/edocr.com\/docs\/*" + ], + "url": "http:\/\/edocr.com\/api\/oembed" + } + ] + }, + { + "provider_name": "EgliseInfo", + "provider_url": "http:\/\/egliseinfo.catholique.fr\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/egliseinfo.catholique.fr\/*" + ], + "url": "http:\/\/egliseinfo.catholique.fr\/api\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Embed Articles", + "provider_url": "http:\/\/embedarticles.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/embedarticles.com\/*" + ], + "url": "http:\/\/embedarticles.com\/oembed\/" + } + ] + }, + { + "provider_name": "Embedly", + "provider_url": "http:\/\/api.embed.ly\/", + "endpoints": [ + { + "url": "http:\/\/api.embed.ly\/1\/oembed" + } + ] + }, + { + "provider_name": "Flickr", + "provider_url": "http:\/\/www.flickr.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/*.flickr.com\/photos\/*", + "http:\/\/flic.kr\/p\/*" + ], + "url": "http:\/\/www.flickr.com\/services\/oembed\/", + "discovery": true + } + ] + }, + { + "provider_name": "FOX SPORTS Australia", + "provider_url": "http:\/\/www.foxsports.com.au", + "endpoints": [ + { + "schemes": [ + "http:\/\/fiso.foxsports.com.au\/isomorphic-widget\/*", + "https:\/\/fiso.foxsports.com.au\/isomorphic-widget\/*" + ], + "url": "https:\/\/fiso.foxsports.com.au\/oembed" + } + ] + }, + { + "provider_name": "FunnyOrDie", + "provider_url": "http:\/\/www.funnyordie.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.funnyordie.com\/videos\/*" + ], + "url": "http:\/\/www.funnyordie.com\/oembed.{format}" + } + ] + }, + { + "provider_name": "Geograph Britain and Ireland", + "provider_url": "https:\/\/www.geograph.org.uk\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/*.geograph.org.uk\/*", + "http:\/\/*.geograph.co.uk\/*", + "http:\/\/*.geograph.ie\/*", + "http:\/\/*.wikimedia.org\/*_geograph.org.uk_*" + ], + "url": "http:\/\/api.geograph.org.uk\/api\/oembed" + } + ] + }, + { + "provider_name": "Geograph Channel Islands", + "provider_url": "http:\/\/channel-islands.geograph.org\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/*.geograph.org.gg\/*", + "http:\/\/*.geograph.org.je\/*", + "http:\/\/channel-islands.geograph.org\/*", + "http:\/\/channel-islands.geographs.org\/*", + "http:\/\/*.channel.geographs.org\/*" + ], + "url": "http:\/\/www.geograph.org.gg\/api\/oembed" + } + ] + }, + { + "provider_name": "Geograph Germany", + "provider_url": "http:\/\/geo-en.hlipp.de\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/geo-en.hlipp.de\/*", + "http:\/\/geo.hlipp.de\/*", + "http:\/\/germany.geograph.org\/*" + ], + "url": "http:\/\/geo.hlipp.de\/restapi.php\/api\/oembed" + } + ] + }, + { + "provider_name": "Getty Images", + "provider_url": "http:\/\/www.gettyimages.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/gty.im\/*" + ], + "url": "http:\/\/embed.gettyimages.com\/oembed", + "formats": [ + "json" + ] + } + ] + }, + { + "provider_name": "Gfycat", + "provider_url": "https:\/\/gfycat.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/gfycat.com\/*", + "http:\/\/www.gfycat.com\/*", + "https:\/\/gfycat.com\/*", + "https:\/\/www.gfycat.com\/*" + ], + "url": "https:\/\/api.gfycat.com\/v1\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "HuffDuffer", + "provider_url": "http:\/\/huffduffer.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/huffduffer.com\/*\/*" + ], + "url": "http:\/\/huffduffer.com\/oembed" + } + ] + }, + { + "provider_name": "Hulu", + "provider_url": "http:\/\/www.hulu.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.hulu.com\/watch\/*" + ], + "url": "http:\/\/www.hulu.com\/api\/oembed.{format}" + } + ] + }, + { + "provider_name": "iFixit", + "provider_url": "http:\/\/www.iFixit.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.ifixit.com\/Guide\/View\/*" + ], + "url": "http:\/\/www.ifixit.com\/Embed" + } + ] + }, + { + "provider_name": "IFTTT", + "provider_url": "http:\/\/www.ifttt.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/ifttt.com\/recipes\/*" + ], + "url": "http:\/\/www.ifttt.com\/oembed\/", + "discovery": true + } + ] + }, + { + "provider_name": "Infogram", + "provider_url": "https:\/\/infogr.am\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/infogr.am\/*" + ], + "url": "https:\/\/infogr.am\/oembed" + } + ] + }, + { + "provider_name": "Instagram", + "provider_url": "https:\/\/instagram.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/instagram.com\/p\/*", + "http:\/\/instagr.am\/p\/*", + "https:\/\/instagram.com\/p\/*", + "https:\/\/instagr.am\/p\/*" + ], + "url": "http:\/\/api.instagram.com\/oembed", + "formats": [ + "json" + ] + } + ] + }, + { + "provider_name": "iSnare Articles", + "provider_url": "https:\/\/www.isnare.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/www.isnare.com\/*" + ], + "url": "https:\/\/www.isnare.com\/oembed\/" + } + ] + }, + { + "provider_name": "Kickstarter", + "provider_url": "http:\/\/www.kickstarter.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.kickstarter.com\/projects\/*" + ], + "url": "http:\/\/www.kickstarter.com\/services\/oembed" + } + ] + }, + { + "provider_name": "Kitchenbowl", + "provider_url": "http:\/\/www.kitchenbowl.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.kitchenbowl.com\/recipe\/*" + ], + "url": "http:\/\/www.kitchenbowl.com\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "LearningApps.org", + "provider_url": "http:\/\/learningapps.org\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/learningapps.org\/*" + ], + "url": "http:\/\/learningapps.org\/oembed.php", + "discovery": true + } + ] + }, + { + "provider_name": "MathEmbed", + "provider_url": "http:\/\/mathembed.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/mathembed.com\/latex?inputText=*", + "http:\/\/mathembed.com\/latex?inputText=*" + ], + "url": "http:\/\/mathembed.com\/oembed" + } + ] + }, + { + "provider_name": "Meetup", + "provider_url": "http:\/\/www.meetup.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/meetup.com\/*", + "http:\/\/meetu.ps\/*" + ], + "url": "https:\/\/api.meetup.com\/oembed", + "formats": [ + "json" + ] + } + ] + }, + { + "provider_name": "MixCloud", + "provider_url": "http:\/\/mixcloud.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.mixcloud.com\/*\/*\/" + ], + "url": "http:\/\/www.mixcloud.com\/oembed\/" + } + ] + }, + { + "provider_name": "Moby Picture", + "provider_url": "http:\/\/www.mobypicture.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.mobypicture.com\/user\/*\/view\/*", + "http:\/\/moby.to\/*" + ], + "url": "http:\/\/api.mobypicture.com\/oEmbed" + } + ] + }, + { + "provider_name": "nfb.ca", + "provider_url": "http:\/\/www.nfb.ca\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/*.nfb.ca\/film\/*" + ], + "url": "http:\/\/www.nfb.ca\/remote\/services\/oembed\/", + "discovery": true + } + ] + }, + { + "provider_name": "Office Mix", + "provider_url": "http:\/\/mix.office.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/mix.office.com\/watch\/*", + "https:\/\/mix.office.com\/embed\/*" + ], + "url": "https:\/\/mix.office.com\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Official FM", + "provider_url": "http:\/\/official.fm", + "endpoints": [ + { + "schemes": [ + "http:\/\/official.fm\/tracks\/*", + "http:\/\/official.fm\/playlists\/*" + ], + "url": "http:\/\/official.fm\/services\/oembed.{format}" + } + ] + }, + { + "provider_name": "On Aol", + "provider_url": "http:\/\/on.aol.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/on.aol.com\/video\/*" + ], + "url": "http:\/\/on.aol.com\/api" + } + ] + }, + { + "provider_name": "Ora TV", + "provider_url": "http:\/\/www.ora.tv\/", + "endpoints": [ + { + "discovery": true, + "url": "https:\/\/www.ora.tv\/oembed\/*?format={format}" + } + ] + }, + { + "provider_name": "Oumy", + "provider_url": "https:\/\/www.oumy.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/www.oumy.com\/v\/*" + ], + "url": "https:\/\/www.oumy.com\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Pastery", + "provider_url": "https:\/\/www.pastery.net", + "endpoints": [ + { + "schemes": [ + "http:\/\/pastery.net\/*", + "https:\/\/pastery.net\/*", + "http:\/\/www.pastery.net\/*", + "https:\/\/www.pastery.net\/*" + ], + "url": "https:\/\/www.pastery.net\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Poll Daddy", + "provider_url": "http:\/\/polldaddy.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/*.polldaddy.com\/s\/*", + "http:\/\/*.polldaddy.com\/poll\/*", + "http:\/\/*.polldaddy.com\/ratings\/*" + ], + "url": "http:\/\/polldaddy.com\/oembed\/" + } + ] + }, + { + "provider_name": "Portfolium", + "provider_url": "https:\/\/portfolium.com", + "endpoints": [ + { + "schemes": [ + "https:\/\/portfolium.com\/entry\/*" + ], + "url": "https:\/\/api.portfolium.com\/oembed" + } + ] + }, + { + "provider_name": "Quiz.biz", + "provider_url": "http:\/\/www.quiz.biz\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.quiz.biz\/quizz-*.html" + ], + "url": "http:\/\/www.quiz.biz\/api\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Quizz.biz", + "provider_url": "http:\/\/www.quizz.biz\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.quizz.biz\/quizz-*.html" + ], + "url": "http:\/\/www.quizz.biz\/api\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "RapidEngage", + "provider_url": "https:\/\/rapidengage.com", + "endpoints": [ + { + "schemes": [ + "https:\/\/rapidengage.com\/s\/*" + ], + "url": "https:\/\/rapidengage.com\/api\/oembed" + } + ] + }, + { + "provider_name": "Reddit", + "provider_url": "https:\/\/reddit.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/reddit.com\/r\/*\/comments\/*\/*" + ], + "url": "https:\/\/www.reddit.com\/oembed" + } + ] + }, + { + "provider_name": "ReleaseWire", + "provider_url": "http:\/\/www.releasewire.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/rwire.com\/*" + ], + "url": "http:\/\/publisher.releasewire.com\/oembed\/", + "discovery": true + } + ] + }, + { + "provider_name": "RepubHub", + "provider_url": "http:\/\/repubhub.icopyright.net\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/repubhub.icopyright.net\/freePost.act?*" + ], + "url": "http:\/\/repubhub.icopyright.net\/oembed.act", + "discovery": true + } + ] + }, + { + "provider_name": "ReverbNation", + "provider_url": "https:\/\/www.reverbnation.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/www.reverbnation.com\/*", + "https:\/\/www.reverbnation.com\/*\/songs\/*" + ], + "url": "https:\/\/www.reverbnation.com\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Roomshare", + "provider_url": "http:\/\/roomshare.jp", + "endpoints": [ + { + "schemes": [ + "http:\/\/roomshare.jp\/post\/*", + "http:\/\/roomshare.jp\/en\/post\/*" + ], + "url": "http:\/\/roomshare.jp\/en\/oembed.{format}" + } + ] + }, + { + "provider_name": "Rumble", + "provider_url": "https:\/\/rumble.com\/", + "endpoints": [ + { + "url": "https:\/\/rumble.com\/api\/Media\/oembed.{format}", + "discovery": true + } + ] + }, + { + "provider_name": "Sapo Videos", + "provider_url": "http:\/\/videos.sapo.pt", + "endpoints": [ + { + "schemes": [ + "http:\/\/videos.sapo.pt\/*" + ], + "url": "http:\/\/videos.sapo.pt\/oembed" + } + ] + }, + { + "provider_name": "Screenr", + "provider_url": "http:\/\/www.screenr.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.screenr.com\/*\/" + ], + "url": "http:\/\/www.screenr.com\/api\/oembed.{format}" + } + ] + }, + { + "provider_name": "Scribd", + "provider_url": "http:\/\/www.scribd.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.scribd.com\/doc\/*" + ], + "url": "http:\/\/www.scribd.com\/services\/oembed\/" + } + ] + }, + { + "provider_name": "ShortNote", + "provider_url": "https:\/\/www.shortnote.jp\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/www.shortnote.jp\/view\/notes\/*" + ], + "url": "https:\/\/www.shortnote.jp\/oembed\/", + "discovery": true + } + ] + }, + { + "provider_name": "Shoudio", + "provider_url": "http:\/\/shoudio.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/shoudio.com\/*", + "http:\/\/shoud.io\/*" + ], + "url": "http:\/\/shoudio.com\/api\/oembed" + } + ] + }, + { + "provider_name": "Show the Way, actionable location info", + "provider_url": "https:\/\/showtheway.io", + "endpoints": [ + { + "schemes": [ + "https:\/\/showtheway.io\/to\/*" + ], + "url": "https:\/\/showtheway.io\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Silk", + "provider_url": "http:\/\/www.silk.co\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/*.silk.co\/explore\/*", + "https:\/\/*.silk.co\/explore\/*", + "http:\/\/*.silk.co\/s\/embed\/*", + "https:\/\/*.silk.co\/s\/embed\/*" + ], + "url": "http:\/\/www.silk.co\/oembed\/", + "discovery": true + } + ] + }, + { + "provider_name": "Sketchfab", + "provider_url": "http:\/\/sketchfab.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/sketchfab.com\/models\/*", + "https:\/\/sketchfab.com\/models\/*", + "https:\/\/sketchfab.com\/*\/folders\/*" + ], + "url": "http:\/\/sketchfab.com\/oembed", + "formats": [ + "json" + ] + } + ] + }, + { + "provider_name": "SlideShare", + "provider_url": "http:\/\/www.slideshare.net\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.slideshare.net\/*\/*", + "http:\/\/fr.slideshare.net\/*\/*", + "http:\/\/de.slideshare.net\/*\/*", + "http:\/\/es.slideshare.net\/*\/*", + "http:\/\/pt.slideshare.net\/*\/*" + ], + "url": "http:\/\/www.slideshare.net\/api\/oembed\/2", + "discovery": true + } + ] + }, + { + "provider_name": "SmugMug", + "provider_url": "http:\/\/www.smugmug.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/*.smugmug.com\/*" + ], + "url": "http:\/\/api.smugmug.com\/services\/oembed\/", + "discovery": true + } + ] + }, + { + "provider_name": "SoundCloud", + "provider_url": "http:\/\/soundcloud.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/soundcloud.com\/*" + ], + "url": "https:\/\/soundcloud.com\/oembed" + } + ] + }, + { + "provider_name": "SpeakerDeck", + "provider_url": "https:\/\/speakerdeck.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/speakerdeck.com\/*\/*", + "https:\/\/speakerdeck.com\/*\/*" + ], + "url": "https:\/\/speakerdeck.com\/oembed.json", + "discovery": true, + "formats": [ + "json" + ] + } + ] + }, + { + "provider_name": "Streamable", + "provider_url": "https:\/\/streamable.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/streamable.com\/*", + "https:\/\/streamable.com\/*" + ], + "url": "https:\/\/api.streamable.com\/oembed.json", + "discovery": true + } + ] + }, + { + "provider_name": "StreamOneCloud", + "provider_url": "https:\/\/www.streamone.nl", + "endpoints": [ + { + "schemes": [ + "https:\/\/content.streamonecloud.net\/embed\/*" + ], + "url": "https:\/\/content.streamonecloud.net\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Sway", + "provider_url": "https:\/\/www.sway.com", + "endpoints": [ + { + "schemes": [ + "https:\/\/sway.com\/*", + "https:\/\/www.sway.com\/*" + ], + "url": "https:\/\/sway.com\/api\/v1.0\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Ted", + "provider_url": "http:\/\/ted.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/ted.com\/talks\/*" + ], + "url": "http:\/\/www.ted.com\/talks\/oembed.{format}" + } + ] + }, + { + "provider_name": "The New York Times", + "provider_url": "https:\/\/www.nytimes.com", + "endpoints": [ + { + "schemes": [ + "https:\/\/www.nytimes.com\/svc\/oembed" + ], + "url": "https:\/\/www.nytimes.com\/svc\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "They Said So", + "provider_url": "https:\/\/theysaidso.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/theysaidso.com\/image\/*" + ], + "url": "https:\/\/theysaidso.com\/extensions\/oembed\/", + "discovery": true + } + ] + }, + { + "provider_name": "Topy", + "provider_url": "http:\/\/www.topy.se\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.topy.se\/image\/*" + ], + "url": "http:\/\/www.topy.se\/oembed\/", + "discovery": true + } + ] + }, + { + "provider_name": "Ustream", + "provider_url": "http:\/\/www.ustream.tv", + "endpoints": [ + { + "schemes": [ + "http:\/\/*.ustream.tv\/*", + "http:\/\/*.ustream.com\/*" + ], + "url": "http:\/\/www.ustream.tv\/oembed", + "formats": [ + "json" + ] + } + ] + }, + { + "provider_name": "Uttles", + "provider_url": "http:\/\/uttles.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/uttles.com\/uttle\/*" + ], + "url": "http:\/\/uttles.com\/api\/reply\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Verse", + "provider_url": "http:\/\/verse.media\/", + "endpoints": [ + { + "url": "http:\/\/verse.media\/services\/oembed\/" + } + ] + }, + { + "provider_name": "VEVO", + "provider_url": "http:\/\/www.vevo.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.vevo.com\/*", + "https:\/\/www.vevo.com\/*" + ], + "url": "https:\/\/www.vevo.com\/oembed", + "discovery": false + } + ] + }, + { + "provider_name": "VideoJug", + "provider_url": "http:\/\/www.videojug.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.videojug.com\/film\/*", + "http:\/\/www.videojug.com\/interview\/*" + ], + "url": "http:\/\/www.videojug.com\/oembed.{format}" + } + ] + }, + { + "provider_name": "Vidlit", + "provider_url": "https:\/\/vidl.it\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/vidl.it\/*" + ], + "url": "https:\/\/api.vidl.it\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Vimeo", + "provider_url": "https:\/\/vimeo.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/vimeo.com\/*", + "https:\/\/vimeo.com\/album\/*\/video\/*", + "https:\/\/vimeo.com\/channels\/*\/*", + "https:\/\/vimeo.com\/groups\/*\/videos\/*", + "https:\/\/vimeo.com\/ondemand\/*\/*", + "https:\/\/player.vimeo.com\/video\/*" + ], + "url": "https:\/\/vimeo.com\/api\/oembed.{format}", + "discovery": true + } + ] + }, + { + "provider_name": "Vine", + "provider_url": "https:\/\/vine.co\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/vine.co\/v\/*", + "https:\/\/vine.co\/v\/*" + ], + "url": "https:\/\/vine.co\/oembed.json", + "discovery": true + } + ] + }, + { + "provider_name": "Wiredrive", + "provider_url": "https:\/\/www.wiredrive.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/*.wiredrive.com\/*" + ], + "url": "http:\/\/*.wiredrive.com\/present-oembed\/", + "formats": [ + "json" + ], + "discovery": true + } + ] + }, + { + "provider_name": "WordPress.com", + "provider_url": "http:\/\/wordpress.com\/", + "endpoints": [ + { + "url": "http:\/\/public-api.wordpress.com\/oembed\/", + "discovery": true + } + ] + }, + { + "provider_name": "YFrog", + "provider_url": "http:\/\/yfrog.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/*.yfrog.com\/*", + "http:\/\/yfrog.us\/*" + ], + "url": "http:\/\/www.yfrog.com\/api\/oembed", + "formats": [ + "json" + ] + } + ] + }, + { + "provider_name": "YouTube", + "provider_url": "http:\/\/www.youtube.com\/", + "endpoints": [ + { + "url": "http:\/\/www.youtube.com\/oembed", + "discovery": true + } + ] + } +] \ No newline at end of file diff --git a/filter/oembed/sass/styles.scss b/filter/oembed/sass/styles.scss index ed762c0f9..ba727ca2a 100644 --- a/filter/oembed/sass/styles.scss +++ b/filter/oembed/sass/styles.scss @@ -1,57 +1,100 @@ -.filter_oembed_lazyvideo_placeholder { - width: 100%; +.filter_oembed_docsdotcom { + iframe { + width: 100%; + height: 400px; + } } -a.filter_oembed_lazyvideo { - display: block; +.oembed-card { + position: relative; + min-height: 10em; + background-size: cover; + transition: all 0.4s ease-in-out; } -.filter_oembed_lazyvideo_container { - max-width: 560px; - position: relative; - &:hover { - .filter_oembed_lazyvideo_playbutton { - background-image: url([[pix:filter_oembed|purplebutton]]); - } +.oembed-card-title { + position: absolute; + top: 0; + color: #fff; + background-color: rgba(0, 0, 0, 0.8); + padding: 0.5em 0.5em; +} + +.oembed-content { + > *:first-child { + width: 100%; } - .filter_oembed_lazyvideo_playbutton { - &:hover { - background-image: url([[pix:filter_oembed|purplebutton]]); - } - left: 40%; - position: absolute; - z-index: 1000 !important; - top: 40%; - width: 20%; - height: 25%; - background-image: url([[pix:filter_oembed|darkbutton]]); - background-repeat: no-repeat; - background-size: 100% auto; + video { + // Responsive video for HTML5 only + height: auto; } } -.filter_oembed_lazyvideo_title, .filter_oembed_lazyvideo_footer { +.btn.btn-link.oembed-card-play { + background-image: url([[pix:filter_oembed|play]]); + background-repeat: no-repeat; position: absolute; - background-color: #000; - width: 100%; - color: #ddd; + width: 15%; + // Center play button 50% - width / 2; + top: 42.5%; + left: 42.5%; + padding-top: 15%; + margin: 0; + background-position: 0; + opacity: 0.9; + transition: all 0.4s ease-in-out; + filter: drop-shadow(1px 1px 1px #666); } -.filter_oembed_lazyvideo_title { - top: 0; +.btn.btn-link.oembed-card-play:hover { + background-position: 0; + filter: drop-shadow(0 0 0 #666); + opacity: 1; } -.filter_oembed_lazyvideo_footer { - bottom: 0; +.oembed-responsive { + width: 100%; + display: block; + position: relative; + > *:not(video):first-child { + position: absolute !important; + top: 0 !important; + bottom: 0 !important; + right: 0 !important; + left: 0 !important; + height: 100% !important; + width: 100% !important; + } } -.filter_oembed_lazyvideo_text { - padding: 5px; +.oembed-responsive-pad { + display: block; } -.filter_oembed_docsdotcom { - iframe { - width: 100%; - height: 400px; +.oembed-provider-details { + font-size: 0.9em; + margin-top: 1em; + display: none; +} + +tr.oembed-provider-editing { + .oembed-provider-details { + display: block; } -} \ No newline at end of file + .oembed-provider-actions { + display: none; + } +} + +.oembed-provider-actions { + float: right; + margin-right: 1em; +} + +#oembedproviders td.provider div.alert { + margin-top: 1em; +} + +.oembed-providersource { + font-size: 90%; +} diff --git a/filter/oembed/settings.php b/filter/oembed/settings.php index 30e087976..c58a4816d 100644 --- a/filter/oembed/settings.php +++ b/filter/oembed/settings.php @@ -18,54 +18,46 @@ * Filter for component 'filter_oembed' * * @package filter_oembed - * @copyright 2012 Matthew Cannings, Sandwell College; modified 2015 by Microsoft, Inc. + * @copyright Erich M. Wappis / Guy Thomas 2016 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * code based on the following filters... - * Screencast (Mark Schall) - * Soundcloud (Troy Williams) + * code based on the following filter + * oEmbed filter ( Mike Churchward, James McQuillan, Vinayak (Vin) Bhalerao, Josh Gavant and Rob Dolin) */ defined('MOODLE_INTERNAL') || die; require_once(__DIR__.'/filter.php'); +require_once($CFG->libdir.'/formslib.php'); + +use filter_oembed\service\oembed; + +$ADMIN->add('filtersettings', new admin_category('filteroembedfolder', get_string('filtername', 'filter_oembed'))); +$settings = new admin_settingpage($section, get_string('settings')); if ($ADMIN->fulltree) { - $torf = array('1' => new lang_string('yes'), '0' => new lang_string('no')); - $item = new admin_setting_configselect('filter_oembed/youtube', new lang_string('youtube', 'filter_oembed'), '', 1, $torf); - $settings->add($item); - $item = new admin_setting_configselect('filter_oembed/vimeo', new lang_string('vimeo', 'filter_oembed'), '', 1, $torf); - $settings->add($item); - $item = new admin_setting_configselect('filter_oembed/ted', new lang_string('ted', 'filter_oembed'), '', 1, $torf); - $settings->add($item); - $item = new admin_setting_configselect('filter_oembed/slideshare', new lang_string('slideshare', 'filter_oembed'), '', 1, $torf); - $settings->add($item); - $item = new admin_setting_configselect('filter_oembed/officemix', new lang_string('officemix', 'filter_oembed'), '', 1, $torf); - $settings->add($item); - $item = new admin_setting_configselect('filter_oembed/issuu', new lang_string('issuu', 'filter_oembed'), '', 1, $torf); - $settings->add($item); - $item = new admin_setting_configselect('filter_oembed/soundcloud', new lang_string('soundcloud', 'filter_oembed'), '', 1, $torf); - $settings->add($item); - $item = new admin_setting_configselect('filter_oembed/pollev', new lang_string('pollev', 'filter_oembed'), '', 1, $torf); - $settings->add($item); - $item = new admin_setting_configselect('filter_oembed/o365video', new lang_string('o365video', 'filter_oembed'), '', 1, $torf); - $settings->add($item); - $item = new admin_setting_configselect('filter_oembed/sway', new lang_string('sway', 'filter_oembed'), '', 1, $torf); - $settings->add($item); + $targettags = [ + 'a' => get_string('atag', 'filter_oembed'), + 'div' => get_string('divtag', 'filter_oembed') + ]; - // New provider method. - $providers = \filter_oembed::get_supported_providers(); - foreach ($providers as $provider) { - $enabledkey = 'provider_'.$provider.'_enabled'; - $name = new lang_string('provider_'.$provider, 'filter_oembed'); - $item = new \admin_setting_configselect('filter_oembed/'.$enabledkey, $name, '', 1, $torf); - $settings->add($item); - } + $config = get_config('filter_oembed'); - $item = new admin_setting_configcheckbox('filter_oembed/lazyload', new lang_string('lazyload', 'filter_oembed'), '', 0); + $item = new admin_setting_configselect( + 'filter_oembed/targettag', + get_string('targettag', 'filter_oembed'), + get_string('targettag_desc', 'filter_oembed'), + 'atag', + ['atag' => 'atag', 'divtag' => 'divtag'] + ); $settings->add($item); - $retrylist = array('0' => new lang_string('none'), '1' => new lang_string('once', 'filter_oembed'), - '2' => new lang_string('times', 'filter_oembed', '2'), - '3' => new lang_string('times', 'filter_oembed', '3')); - $item = new admin_setting_configselect('filter_oembed/retrylimit', new lang_string('retrylimit', 'filter_oembed'), '', '1', $retrylist); + + $item = new admin_setting_configcheckbox('filter_oembed/lazyload', new lang_string('lazyload', 'filter_oembed'), '', 1); $settings->add($item); } + +$ADMIN->add('filteroembedfolder', $settings); + +$ADMIN->add('filteroembedfolder', new admin_externalpage('filter_oembed_providers', + get_string('manageproviders', 'filter_oembed'), new moodle_url('/filter/oembed/manageproviders.php'))); + +$settings = null; \ No newline at end of file diff --git a/filter/oembed/styles.css b/filter/oembed/styles.css index d0931cdcc..4572ad22f 100644 --- a/filter/oembed/styles.css +++ b/filter/oembed/styles.css @@ -1,42 +1,100 @@ -.filter_oembed_lazyvideo_placeholder { - width: 100%; } - -a.filter_oembed_lazyvideo { - display: block; } - -.filter_oembed_lazyvideo_container { - max-width: 560px; - position: relative; } - .filter_oembed_lazyvideo_container:hover .filter_oembed_lazyvideo_playbutton { - background-image: url([[pix:filter_oembed|purplebutton]]); } - .filter_oembed_lazyvideo_container .filter_oembed_lazyvideo_playbutton { - left: 40%; +.filter_oembed_docsdotcom iframe { + width: 100%; + height: 400px; +} + +.oembed-card { + position: relative; + min-height: 10em; + -webkit-background-size: cover; + background-size: cover; + -webkit-transition: all 0.4s ease-in-out; + transition: all 0.4s ease-in-out; +} + +.oembed-card-title { position: absolute; - z-index: 1000 !important; - top: 40%; - width: 20%; - height: 25%; - background-image: url([[pix:filter_oembed|darkbutton]]); + top: 0; + color: #fff; + background-color: rgba(0, 0, 0, 0.8); + padding: 0.5em 0.5em; +} + +.oembed-content > *:first-child { + width: 100%; +} + +.oembed-content video { + height: auto; +} + +.btn.btn-link.oembed-card-play { + background-image: url([[pix:filter_oembed|play]]); background-repeat: no-repeat; - background-size: 100% auto; } - .filter_oembed_lazyvideo_container .filter_oembed_lazyvideo_playbutton:hover { - background-image: url([[pix:filter_oembed|purplebutton]]); } + position: absolute; + width: 15%; + top: 42.5%; + left: 42.5%; + padding-top: 15%; + margin: 0; + background-position: 0; + opacity: 0.9; + -webkit-transition: all 0.4s ease-in-out; + transition: all 0.4s ease-in-out; + -webkit-filter: drop-shadow(1px 1px 1px #666); + filter: drop-shadow(1px 1px 1px #666); +} -.filter_oembed_lazyvideo_title, .filter_oembed_lazyvideo_footer { - position: absolute; - background-color: #000; - width: 100%; - color: #ddd; } +.btn.btn-link.oembed-card-play:hover { + background-position: 0; + -webkit-filter: drop-shadow(0 0 0 #666); + filter: drop-shadow(0 0 0 #666); + opacity: 1; +} -.filter_oembed_lazyvideo_title { - top: 0; } +.oembed-responsive { + width: 100%; + display: block; + position: relative; +} -.filter_oembed_lazyvideo_footer { - bottom: 0; } +.oembed-responsive > *:not(video):first-child { + position: absolute !important; + top: 0 !important; + bottom: 0 !important; + right: 0 !important; + left: 0 !important; + height: 100% !important; + width: 100% !important; +} -.filter_oembed_lazyvideo_text { - padding: 5px; } +.oembed-responsive-pad { + display: block; +} -.filter_oembed_docsdotcom iframe { - width: 100%; - height: 400px; } +.oembed-provider-details { + font-size: 0.9em; + margin-top: 1em; + display: none; +} + +tr.oembed-provider-editing .oembed-provider-details { + display: block; +} + +tr.oembed-provider-editing .oembed-provider-actions { + display: none; +} + +.oembed-provider-actions { + float: right; + margin-right: 1em; +} + +#oembedproviders td.provider div.alert { + margin-top: 1em; +} + +.oembed-providersource { + font-size: 90%; +} diff --git a/filter/oembed/templates/managementpage.mustache b/filter/oembed/templates/managementpage.mustache new file mode 100644 index 000000000..64c2f536a --- /dev/null +++ b/filter/oembed/templates/managementpage.mustache @@ -0,0 +1,84 @@ +{{! + 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 filter_oembed/managementpage + + Template which defines an oembed filter management page. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * headings - array: An array of table headings. + * rows - array: An array of content rows for the table. + + Example context (json): + { + "rows": [ + { + "pid": "1", + "providername": "23HQ", + "providerurl": "http://www.23hq.com/", + "enableaction": "http://localhost/moodlehq.git/filter/oembed/manageproviders.php?action=disable&pid=1&sesskey=dvBXP9rX1G", + "editaction": "http://localhost/moodlehq.git/filter/oembed/manageproviders.php?action=edit&pid=1&sesskey=dvBXP9rX1G", + "deleteaction": "http://localhost/moodlehq.git/filter/oembed/manageproviders.php?action=delete&pid=1&sesskey=dvBXP9rX1G", + "extraclass": "dimmed_text" + } + ] + } + }} +{{#x}} +Displayed if x was true. +{{/x}} +
+ + + + + + + + + + + {{#localrows}} + {{> filter_oembed/managementpagerow}} + {{/localrows}} + + + + {{#pluginrows}} + {{> filter_oembed/managementpagerow}} + {{/pluginrows}} + + + + {{#downloadrows}} + {{> filter_oembed/managementpagerow}} + {{/downloadrows}} + +
+ {{#str}}localproviders, filter_oembed{{/str}} +
+ {{#str}}pluginproviders, filter_oembed{{/str}} +
+ {{#str}}downloadproviders, filter_oembed{{/str}} +
+
diff --git a/filter/oembed/templates/managementpagerow.mustache b/filter/oembed/templates/managementpagerow.mustache new file mode 100644 index 000000000..dda248dda --- /dev/null +++ b/filter/oembed/templates/managementpagerow.mustache @@ -0,0 +1,45 @@ +{{! + 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 filter_oembed/managementpagerow + + oembed managementpagerow template. + + Renders a single oembed managementpagerow. See template filter_oembed/managementpage for details. + + Example context (json): + { + "pid": "72", + "extraclass": "testclass", + "editing": "1", + "providerurl": "https://theprovider.com/", + "providername": "The Provider", + "enableaction": "http://enableaction.com/", + "editaction": "http://editaction.com/", + "deleteaction": "http://deleteaction.com/" + } +}} + + + {{providername}} +
+
+
+ {{{enableaction}}} {{{editaction}}} {{{deleteaction}}} +
+ + diff --git a/filter/oembed/templates/preload.mustache b/filter/oembed/templates/preload.mustache new file mode 100644 index 000000000..127db291a --- /dev/null +++ b/filter/oembed/templates/preload.mustache @@ -0,0 +1,38 @@ +{{! + 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 . +}} +{{! +@package filter_oembed +@copyright Stuart Lamour / Guy Thomas 2016 +@license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +}} +{{! + @template filter_oembed/preload + Example context (json): + { + "aspectratio": "1", + "embedhtml": "
embed html
", + "jshtml": "data-aspect-ratio=10", + "title": "Card title" + } +}} +
+
+
{{title}}
+ +
+ {{#aspectratio}}
{{/aspectratio}} +
diff --git a/filter/oembed/tests/behat/behat_filter_oembed.php b/filter/oembed/tests/behat/behat_filter_oembed.php new file mode 100644 index 000000000..611d8bf4d --- /dev/null +++ b/filter/oembed/tests/behat/behat_filter_oembed.php @@ -0,0 +1,163 @@ +. + +/** + * Oembed filter custom behat steps. + * @author Guy Thomas + * @copyright Copyright (c) 2016 Blackboard Inc. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use Behat\Gherkin\Node\TableNode; + +require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); + +class behat_filter_oembed extends behat_base { + + /** + * Get provider action xpath for specific provider and action. + * @param string $provider + * @param string $actionclass + * @return string + */ + protected function provider_action_xpath($provider, $actionclass) { + $xpath = '//td/a[text()=\'' . $provider.'\']'; + $xpath .= '/parent::td/div/a[contains(@class,\''.$actionclass.'\')]'; + return $xpath; + } + + /** + * Provider action. + * @param string $provider + * @param string $actionclass + * @throws \Behat\Mink\Exception\ExpectationException + */ + protected function provider_action($provider, $actionclass) { + $xpath = $this->provider_action_xpath($provider, $actionclass); + $node = $this->find('xpath', $xpath); + $this->ensure_node_is_visible($node); + $node->click(); + } + + /** + * Toggle provider enable status. + * @Given /^I toggle the provider "(?P[^"]*)"$/ + * @param string $provider + */ + public function i_toggle_provider($provider) { + $this->provider_action($provider, 'filter-oembed-visibility'); + } + + /** + * Click edit action for provider. + * @Given /^I edit the provider "(?P[^"]*)"$/ + * @param string $provider + */ + public function i_edit_provider($provider) { + $this->provider_action($provider, 'filter-oembed-edit'); + } + + /** + * Ensure provider status is enabled or disabled. + * @param string $provider + * @param bool $enabled + */ + protected function ensure_provider_status($provider, $enabled = true) { + $action = $enabled ? 'disable' : 'enable'; + $xpath = $this->provider_action_xpath($provider, 'filter-oembed-visibility'); + $xpath = substr($xpath, 0, -1) . 'and contains(@href,"action='.$action.'")]'; + $this->ensure_element_exists($xpath, 'xpath_element'); + } + + /** + * @Given /^the provider "(?P[^"]*)" is disabled$/ + * @param string $provider + * @throws \Behat\Mink\Exception\ExpectationException + */ + public function the_provider_is_disabled($provider) { + $this->ensure_provider_status($provider, false); + } + + /** + * @Given /^the provider "(?P[^"]*)" is enabled$/ + * @param string $provider + * @throws \Behat\Mink\Exception\ExpectationException + */ + public function the_provider_is_enabled($provider) { + $this->ensure_provider_status($provider, true); + } + + /** + * @Given /^I filter the provider list to "(?P[^"]*)"$/ + * @param $provider + * @throws \Behat\Mink\Exception\ExpectationException + */ + public function i_filter_provider_list($provider) { + $fieldxpath = '//input[@placeholder="Provider"]'; + $fieldnode = $this->find('xpath', $fieldxpath); + $field = behat_field_manager::get_form_field($fieldnode, $this->getSession()); + $field->set_value($provider); + } + + /** + * Xpath for provider edit form. + * @param string $provider + * @return string + */ + protected function edit_form_xpath($provider) { + $xpath = '//td/a[text()=\'' . $provider.'\']'; + $xpath .= '/parent::td/div[contains(@class,\'oembed-provider-details\')]/form'; + return $xpath; + } + + /** + * Wait for edit form to be visible. + * @param string $provider + */ + protected function wait_for_edit_form($provider) { + $xpath = $this->edit_form_xpath($provider); + $this->ensure_element_is_visible($xpath, 'xpath_element'); + } + + /** + * @Given /^I edit the provider "(?P[^"]*)" with the values:$/ + * @param string $provider + * @param TableNode $table + */ + public function i_edit_provider_with_values($provider, TableNode $table) { + $this->i_edit_provider($provider); + + if (!$data = $table->getRowsHash()) { + return; + } + + /** @var behat_forms $formhelper */ + $formhelper = behat_context_helper::get('behat_forms'); + + // Field setting code taken from behat_admin.php. + foreach ($data as $label => $value) { + + $fieldxpath = $this->edit_form_xpath($provider); + $fieldxpath .= '//label[contains(text(),\''.$label.'\')]'; + $fieldxpath .= '/parent::div/parent::div/div[contains(@class, \'felement\')]/*'; + + $formhelper->i_set_the_field_with_xpath_to($fieldxpath, $value); + + $this->find_button(get_string('saveasnew', 'filter_oembed'))->press(); + } + + } +} diff --git a/filter/oembed/tests/behat/management.feature b/filter/oembed/tests/behat/management.feature new file mode 100644 index 000000000..0207dd91f --- /dev/null +++ b/filter/oembed/tests/behat/management.feature @@ -0,0 +1,45 @@ +# 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 . +# +# Tests for visibility of admin block by user type and page. +# +# @package filter_oembed +# @copyright 2016 Guy Thomas +# @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + +@filter @filter_oembed +Feature: Admin can use the provider management page to view / edit / enable / disable providers. + + @javascript + Scenario: Admin user carries out various provider management tasks. + #This is Done as one scenario for performance. + Given I log in as "admin" + When I navigate to "Plugins > Filters > Oembed Filter > Manage providers" in site administration + Then "#providermanagement" "css_element" should exist + # Test filtering list. + And I should see "YouTube" in the "oembedproviders" "table" + When I filter the provider list to "Vimeo" + Then I should not see "YouTube" in the "oembedproviders" "table" + And I should see "Vimeo" in the "oembedproviders" "table" + And the provider "Vimeo" is disabled + # Test enable / disable + When I toggle the provider "Vimeo" + Then the provider "Vimeo" is enabled + When I toggle the provider "Vimeo" + Then the provider "Vimeo" is disabled + # Test edit + When I edit the provider "Vimeo" with the values: + | Provider Name | Zimeo | + Then I should see "Created new local provider definition for \"Zimeo\"." in the "oembedproviders" "table" diff --git a/filter/oembed/tests/filter_test.php b/filter/oembed/tests/filter_test.php index 3702bddcb..53f034f45 100644 --- a/filter/oembed/tests/filter_test.php +++ b/filter/oembed/tests/filter_test.php @@ -19,6 +19,9 @@ * * @package filter_oembed * @author Sushant Gawali (sushant@introp.net) + * @author Erich M. Wappis + * @author Guy Thomas + * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @copyright Microsoft, Inc. */ @@ -30,9 +33,8 @@ /** * @group filter_oembed - * @group office365 */ -class filter_oembed_testcase extends basic_testcase { +class filter_oembed_testcase extends advanced_testcase { protected $filter; @@ -42,6 +44,44 @@ class filter_oembed_testcase extends basic_testcase { protected function setUp() { parent::setUp(); $this->filter = new filter_oembed(context_system::instance(), array()); + // Ensure all tested providers are enabled. + $oembed = \filter_oembed\service\oembed::get_instance('all'); + foreach ($oembed->providers as $pid => $provider) { + switch ($provider->providername) { + + case 'YouTube': + $oembed->enable_provider($pid); + break; + + case 'SoundCloud': + $oembed->enable_provider($pid); + break; + + case 'Office Mix': + $oembed->enable_provider($pid); + break; + + case 'Vimeo': + $oembed->enable_provider($pid); + break; + + case 'Ted': + $oembed->enable_provider($pid); + break; + + case 'Poll Everywhere': + $oembed->enable_provider($pid); + break; + + case 'SlideShare': + $oembed->enable_provider($pid); + break; + + case 'ISSUU': + $oembed->enable_provider($pid); + break; + } + } } /** @@ -50,56 +90,63 @@ protected function setUp() { * Need to update this test to not contact external services. */ public function test_filter() { - return true; - $souncloudlink = '

soundcloud

'; + $this->resetAfterTest(true); + + $curl = new curl(); + try { + $out = $curl->get('https://www.youtube.com'); + } catch (Exception $e) { + $out = ''; + } + + $cancontactyoutube = stripos(trim($out), 'markTestSkipped( + 'Unable to reach youtube' + ); + } + + set_config('lazyload', 0, 'filter_oembed'); + + $soundcloudlink = '

soundcloud

'; $youtubelink = '

Youtube

'; - $officemixlink = '

mix

'; $vimeolink = '

vimeo

'; - $tedlink = '

Ted

'; - $slidesharelink = '

slideshare

'; - $issuulink = '

issuu

'; + $tedlink = '

Ted

'; + $slidesharelink = '

slideshare

'; + $issuulink = '

issuu

'; $polleverywherelink = '

'; $polleverywherelink .= '$popolleverywhere

'; - $filterinput = $souncloudlink.$youtubelink.$officemixlink.$vimeolink.$tedlink.$slidesharelink.$issuulink; - $filterinput .= $polleverywherelink; + $filterinput = $soundcloudlink.$youtubelink.$vimeolink.$tedlink.$slidesharelink.$issuulink.$polleverywherelink; $filteroutput = $this->filter->filter($filterinput); - $youtubeoutput = ''; - $this->assertContains($soundcloudoutput, $filteroutput, 'Soundcloud filter fails'); - - $officemixoutput = ''; - $this->assertContains($vimeooutput, $filteroutput, 'Vimeo filter fails'); - - $tedoutput = ''; - $this->assertContains($slideshareoutput, $filteroutput, 'Slidershare filter fails'); + $youtubeoutput = '/.*