commit 3e895d5a38eae3e95eaf5ab636a0d3a79843808b Author: TalesRG Date: Thu Mar 6 17:23:55 2025 -0300 chore(saberes-3.7): adicionando plugin necessario para funcionamento do saberes 3.7 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..ae4b835 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +amd/src/list.js diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..412eeda --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3e6cd3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# ========================= +# Operating System Files +# ========================= + +# OSX +# ========================= + +.DS_Store +.AppleDouble +.LSOverride + +# Icon must ends with two \r. +Icon + + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes +.idea + +# Grunt stuff +.sass-cache +node_modules diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 0000000..ae4b835 --- /dev/null +++ b/.jshintignore @@ -0,0 +1 @@ +amd/src/list.js diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..87f92ee --- /dev/null +++ b/.jshintrc @@ -0,0 +1,7 @@ +{ + "extends": "../../.jshintrc", + "globals": { + "$": false, + "console": false + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..64c719e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,62 @@ +language: php + +# We use trusty as currently there is an issue with java and moodle-plugin-ci mustache linting which +# causes causes tests to fail on xenial and bionic. (See https://github.com/blackboard-open-source/moodle-plugin-ci/issues/91). +dist: trusty + +sudo: true + +addons: + firefox: "47.0.1" + postgresql: "9.4" + mysql: "8.0.2" + apt: + packages: + - openjdk-8-jre-headless + +services: mysql + +cache: + directories: + - $HOME/.composer/cache + - $HOME/.npm + +php: + - 7.1 + - 7.2 + +env: + - MOODLE_BRANCH=MOODLE_38_STABLE DB=pgsql + - MOODLE_BRANCH=MOODLE_38_STABLE DB=mysqli + +matrix: + exclude: + - php: 7.1 + env: MOODLE_BRANCH=MOODLE_38_STABLE DB=mysqli + - php: 7.2 + env: MOODLE_BRANCH=MOODLE_38_STABLE DB=pgsql + +before_install: + - phpenv config-rm xdebug.ini + - nvm install 8.9 + - nvm use 8.9 + - cd ../.. + - composer create-project -n --no-dev --prefer-dist blackboard-open-source/moodle-plugin-ci ci ^2 + - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" + +install: + - moodle-plugin-ci install + - moodle-plugin-ci add-config '$CFG->curlsecurityallowedport = "";' + - moodle-plugin-ci add-config '$CFG->curlsecurityblockedhosts = "";' + +script: + - moodle-plugin-ci phplint + - moodle-plugin-ci phpcpd + - moodle-plugin-ci phpmd + - moodle-plugin-ci codechecker + - moodle-plugin-ci validate + - moodle-plugin-ci savepoints + - moodle-plugin-ci mustache + - moodle-plugin-ci grunt + - moodle-plugin-ci phpunit + - moodle-plugin-ci behat diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..a91e7b4 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,6 @@ +Release Notes + +Release 3.8.0 (Build 2019112600) +Updated subplugin declaration file to new subplugins.json structure. + +(see CHANGES.TXT in release 3.3 for earlier changes.) \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..f40780d --- /dev/null +++ b/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/LICENSE b/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program 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. + + This program 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 this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..11f8fcc --- /dev/null +++ b/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/amd/build/list.min.js b/amd/build/list.min.js new file mode 100644 index 0000000..81ebe62 --- /dev/null +++ b/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/amd/build/oembed.min.js b/amd/build/oembed.min.js new file mode 100644 index 0000000..08e0b01 --- /dev/null +++ b/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/amd/build/preloader.min.js b/amd/build/preloader.min.js new file mode 100644 index 0000000..6a29e6c --- /dev/null +++ b/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/amd/build/responsivecontent.min.js b/amd/build/responsivecontent.min.js new file mode 100644 index 0000000..b8c4c1d --- /dev/null +++ b/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/amd/src/list.js b/amd/src/list.js new file mode 100644 index 0000000..09d566a --- /dev/null +++ b/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/amd/src/manageproviders.js b/amd/src/manageproviders.js new file mode 100644 index 0000000..700cd00 --- /dev/null +++ b/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/amd/src/oembed.js b/amd/src/oembed.js new file mode 100644 index 0000000..1a0fcc3 --- /dev/null +++ b/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/amd/src/preloader.js b/amd/src/preloader.js new file mode 100644 index 0000000..a5d0e78 --- /dev/null +++ b/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/amd/src/responsivecontent.js b/amd/src/responsivecontent.js new file mode 100644 index 0000000..b0998f1 --- /dev/null +++ b/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/classes/db/abstract_dbrow.php b/classes/db/abstract_dbrow.php new file mode 100644 index 0000000..ab91cd9 --- /dev/null +++ b/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/classes/db/providerrow.php b/classes/db/providerrow.php new file mode 100644 index 0000000..06bb265 --- /dev/null +++ b/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/classes/forms/provider.php b/classes/forms/provider.php new file mode 100644 index 0000000..9887874 --- /dev/null +++ b/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/classes/output/managementpage.php b/classes/output/managementpage.php new file mode 100644 index 0000000..e41a9ea --- /dev/null +++ b/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/classes/output/providermodel.php b/classes/output/providermodel.php new file mode 100644 index 0000000..95dbdbb --- /dev/null +++ b/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/classes/output/renderer.php b/classes/output/renderer.php new file mode 100644 index 0000000..1604256 --- /dev/null +++ b/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/classes/plugininfo/oembedprovider.php b/classes/plugininfo/oembedprovider.php new file mode 100644 index 0000000..e1cc541 --- /dev/null +++ b/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/classes/provider/endpoint.php b/classes/provider/endpoint.php new file mode 100644 index 0000000..e0d4574 --- /dev/null +++ b/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/classes/provider/provider.php b/classes/provider/provider.php new file mode 100644 index 0000000..f9d089b --- /dev/null +++ b/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/classes/service/oembed.php b/classes/service/oembed.php new file mode 100644 index 0000000..8771db4 --- /dev/null +++ b/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/classes/service/util.php b/classes/service/util.php new file mode 100644 index 0000000..3e07cd5 --- /dev/null +++ b/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/classes/task/update_providers.php b/classes/task/update_providers.php new file mode 100644 index 0000000..a7f53ca --- /dev/null +++ b/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/classes/webservice/ws_provider_manage.php b/classes/webservice/ws_provider_manage.php new file mode 100644 index 0000000..c0ac060 --- /dev/null +++ b/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/classes/webservice/ws_providers.php b/classes/webservice/ws_providers.php new file mode 100644 index 0000000..b32ada6 --- /dev/null +++ b/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/db/caches.php b/db/caches.php new file mode 100644 index 0000000..c6d4913 --- /dev/null +++ b/db/caches.php @@ -0,0 +1,33 @@ +. + +/** + * oEmbed cache definitions + * + * @package filter_oembed + * @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, + 'ttl' => HOURSECS, + ), +); + diff --git a/db/install.php b/db/install.php new file mode 100644 index 0000000..26e3cbd --- /dev/null +++ b/db/install.php @@ -0,0 +1,39 @@ +. + +/** + * Filter for component 'filter_oembed' + * + * @package filter_oembed + * @copyright 2012 Matthew Cannings; modified 2015 by Microsoft, Inc. + * @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) + */ + +defined('MOODLE_INTERNAL') || die(); + +use filter_oembed\service\oembed; +/** + * Installs the OEmbed filter. + */ +function xmldb_filter_oembed_install() { + 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/db/install.xml b/db/install.xml new file mode 100644 index 0000000..6b07857 --- /dev/null +++ b/db/install.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/db/services.php b/db/services.php new file mode 100644 index 0000000..d06d4fc --- /dev/null +++ b/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/db/subplugins.json b/db/subplugins.json new file mode 100644 index 0000000..e0daeb4 --- /dev/null +++ b/db/subplugins.json @@ -0,0 +1,5 @@ +{ + "plugintypes": { + "oembedprovider": "filter\/oembed\/provider" + } +} \ No newline at end of file diff --git a/db/tasks.php b/db/tasks.php new file mode 100644 index 0000000..a0c4efc --- /dev/null +++ b/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/db/upgrade.php b/db/upgrade.php new file mode 100644 index 0000000..7409bdf --- /dev/null +++ b/db/upgrade.php @@ -0,0 +1,142 @@ +. + +/** + * Filter for component 'filter_oembed' + * + * @package filter_oembed + * @copyright 2012 Matthew Cannings; modified 2015 by Microsoft, Inc. + * @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) + */ + +defined('MOODLE_INTERNAL') || die(); + +use filter_oembed\service\oembed; +use filter_oembed\provider\provider; + +/** + * Upgrades the OEmbed filter. + * + * @param $oldversion Version to be upgraded from. + * @return bool Success. + */ +function xmldb_filter_oembed_upgrade($oldversion) { + 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.php b/filter.php new file mode 100644 index 0000000..c6e67e8 --- /dev/null +++ b/filter.php @@ -0,0 +1,101 @@ +. + +/** + * Filter for component 'filter_oembed' + * + * @package filter_oembed + * @copyright Erich M. Wappis / Guy Thomas 2016 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @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'); +/** + * 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 { + + /** + * 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 $PAGE; + + static $initialised = false; + + if (!$initialised) { + $PAGE->requires->js_call_amd('filter_oembed/oembed', 'init'); + $initialised = true; + } + + $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; + } + + $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'; + } + + $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; + } + } + + /** + * Callback function to be used by the main filter + * + * @param $match array An array of matched groups, where [1] is the URL matched. + * + */ + private static function find_oembeds_callback($match) { + $instance = oembed::get_instance(); + $result = $instance->html_output($match[1]); + if (empty($result)) { + $result = $match[0]; + } + return $result; + } +} diff --git a/lang/cs/filter_oembed.php b/lang/cs/filter_oembed.php new file mode 100644 index 0000000..830bb1d --- /dev/null +++ b/lang/cs/filter_oembed.php @@ -0,0 +1,39 @@ +. + +/** + * Language strings for component 'filter_oembed' + * + * @package filter_oembed + * @copyright 2012 Matthew Cannings; modified 2015 by Microsoft Inc. + * @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) + */ + +$string['filtername'] = 'Filtr oEmbed'; +$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'] = 'Zpozdit načítání vložených prvků (opožděné načítání)'; +$string['connection_error'] = 'Při přístupu k vloženému médiu došlo k chybě. Zkuste aktualizovat stránku.'; diff --git a/lang/de/filter_oembed.php b/lang/de/filter_oembed.php new file mode 100644 index 0000000..1dd8348 --- /dev/null +++ b/lang/de/filter_oembed.php @@ -0,0 +1,41 @@ +. + +/** + * Filter for component 'filter_oembed' + * + * @package filter_oembed + * @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 filter + * oEmbed filter ( Mike Churchward, James McQuillan, Vinayak (Vin) Bhalerao, Josh Gavant and Rob Dolin) + */ + +$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/lang/en/filter_oembed.php b/lang/en/filter_oembed.php new file mode 100644 index 0000000..c7479bd --- /dev/null +++ b/lang/en/filter_oembed.php @@ -0,0 +1,64 @@ +. + +/** + * Filter for component 'filter_oembed' + * + * @package filter_oembed + * @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 filter + * oEmbed filter ( Mike Churchward, James McQuillan, Vinayak (Vin) Bhalerao, Josh Gavant and Rob Dolin) + */ + +$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['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/lang/es/filter_oembed.php b/lang/es/filter_oembed.php new file mode 100644 index 0000000..0f0aa56 --- /dev/null +++ b/lang/es/filter_oembed.php @@ -0,0 +1,39 @@ +. + +/** + * Language strings for component 'filter_oembed' + * + * @package filter_oembed + * @copyright 2012 Matthew Cannings; modified 2015 by Microsoft Inc. + * @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) + */ + +$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'] = 'Demorar carga integrada (Lazyload)'; +$string['connection_error'] = 'Error al acceder al medio integrado. Intente actualizar la página.'; diff --git a/lang/fi/filter_oembed.php b/lang/fi/filter_oembed.php new file mode 100644 index 0000000..065ac51 --- /dev/null +++ b/lang/fi/filter_oembed.php @@ -0,0 +1,39 @@ +. + +/** + * Language strings for component 'filter_oembed' + * + * @package filter_oembed + * @copyright 2012 Matthew Cannings; modified 2015 by Microsoft Inc. + * @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) + */ + +$string['filtername'] = 'oEmbed-suodatin'; +$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'] = 'Lykkää upotetun median lataamista'; +$string['connection_error'] = 'Virhe upotetun median käytön aikana. Kokeile päivittää sivu.'; diff --git a/lang/fr/filter_oembed.php b/lang/fr/filter_oembed.php new file mode 100644 index 0000000..5a186ce --- /dev/null +++ b/lang/fr/filter_oembed.php @@ -0,0 +1,39 @@ +. + +/** + * Language strings for component 'filter_oembed' + * + * @package filter_oembed + * @copyright 2012 Matthew Cannings; modified 2015 by Microsoft Inc. + * @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) + */ + +$string['filtername'] = 'Filtre oEmbed'; +$string['youtube'] = 'Youtube'; +$string['vimeo'] = 'Vimeo'; +$string['ted'] = 'Chats Ted'; +$string['slideshare'] = 'SlideShare'; +$string['officemix'] = 'Combinaison Office'; +$string['issuu'] = 'Problème'; +$string['screenr'] = 'Écran'; +$string['soundcloud'] = 'SoundCloud'; +$string['pollev'] = 'Sonder partout'; +$string['lazyload'] = 'Retarder l\'intégration du chargement (Lazyload)'; +$string['connection_error'] = 'Erreur lors de l\'accès au média intégré. Veuillez actualiser la page.'; diff --git a/lang/it/filter_oembed.php b/lang/it/filter_oembed.php new file mode 100644 index 0000000..746f426 --- /dev/null +++ b/lang/it/filter_oembed.php @@ -0,0 +1,39 @@ +. + +/** + * Language strings for component 'filter_oembed' + * + * @package filter_oembed + * @copyright 2012 Matthew Cannings; modified 2015 by Microsoft Inc. + * @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) + */ + +$string['filtername'] = 'Filtro oEmbed'; +$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['connection_error'] = 'Errore durante l\'accesso al media incorporato. Prova ad aggiornare la pagina.'; diff --git a/lang/ja/filter_oembed.php b/lang/ja/filter_oembed.php new file mode 100644 index 0000000..5c44ce7 --- /dev/null +++ b/lang/ja/filter_oembed.php @@ -0,0 +1,39 @@ +. + +/** + * Language strings for component 'filter_oembed' + * + * @package filter_oembed + * @copyright 2012 Matthew Cannings; modified 2015 by Microsoft Inc. + * @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) + */ + +$string['filtername'] = 'oEmbedフィルタ'; +$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'] = '遅延埋め込みロード (Lazyload)'; +$string['connection_error'] = '埋め込みメディアへのアクセス中にエラーが発生しました。ページを再読み込みしてください。'; diff --git a/lang/nl/filter_oembed.php b/lang/nl/filter_oembed.php new file mode 100644 index 0000000..59f8b73 --- /dev/null +++ b/lang/nl/filter_oembed.php @@ -0,0 +1,39 @@ +. + +/** + * Language strings for component 'filter_oembed' + * + * @package filter_oembed + * @copyright 2012 Matthew Cannings; modified 2015 by Microsoft Inc. + * @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) + */ + +$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['connection_error'] = 'Fout tijdens het openen van ingesloten media. Probeer de pagina te vernieuwen.'; diff --git a/lang/pl/filter_oembed.php b/lang/pl/filter_oembed.php new file mode 100644 index 0000000..f6a6ab4 --- /dev/null +++ b/lang/pl/filter_oembed.php @@ -0,0 +1,39 @@ +. + +/** + * Language strings for component 'filter_oembed' + * + * @package filter_oembed + * @copyright 2012 Matthew Cannings; modified 2015 by Microsoft Inc. + * @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) + */ + +$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'] = 'Opóźnij ładowanie osadzania (Lazyload)'; +$string['connection_error'] = 'Błąd podczas uzyskiwania dostępu do osadzonego elementu multimedialnego. Spróbuj odświeżyć stronę.'; diff --git a/lang/pt_br/filter_oembed.php b/lang/pt_br/filter_oembed.php new file mode 100644 index 0000000..00c5f41 --- /dev/null +++ b/lang/pt_br/filter_oembed.php @@ -0,0 +1,39 @@ +. + +/** + * Language strings for component 'filter_oembed' + * + * @package filter_oembed + * @copyright 2012 Matthew Cannings; modified 2015 by Microsoft Inc. + * @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) + */ + +$string['filtername'] = 'Filtro oEmbed'; +$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'] = 'Carregamento de incorporação com atraso (Lazy load)'; +$string['connection_error'] = 'Erro ao acessar a mídia incorporada. Tente atualizar a página.'; diff --git a/lib.php b/lib.php new file mode 100644 index 0000000..ad95266 --- /dev/null +++ b/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/manageproviders.php b/manageproviders.php new file mode 100644 index 0000000..2e60159 --- /dev/null +++ b/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/package.json b/package.json new file mode 100644 index 0000000..491b689 --- /dev/null +++ b/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/pix/play.svg b/pix/play.svg new file mode 100644 index 0000000..027ed60 --- /dev/null +++ b/pix/play.svg @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/provider/docsdotcom/docsdotcom.php b/provider/docsdotcom/docsdotcom.php new file mode 100644 index 0000000..9305f24 --- /dev/null +++ b/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/provider/docsdotcom/lang/en/oembedprovider_docsdotcom.php b/provider/docsdotcom/lang/en/oembedprovider_docsdotcom.php new file mode 100644 index 0000000..89a74b6 --- /dev/null +++ b/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/provider/docsdotcom/version.php b/provider/docsdotcom/version.php new file mode 100644 index 0000000..bdafbbe --- /dev/null +++ b/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/provider/issuu/issuu.php b/provider/issuu/issuu.php new file mode 100644 index 0000000..d919651 --- /dev/null +++ b/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/provider/issuu/lang/en/oembedprovider_issuu.php b/provider/issuu/lang/en/oembedprovider_issuu.php new file mode 100644 index 0000000..4998dbd --- /dev/null +++ b/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/provider/issuu/version.php b/provider/issuu/version.php new file mode 100644 index 0000000..b037764 --- /dev/null +++ b/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/provider/o365video/lang/en/oembedprovider_o365video.php b/provider/o365video/lang/en/oembedprovider_o365video.php new file mode 100644 index 0000000..9927b0e --- /dev/null +++ b/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/provider/o365video/o365video.php b/provider/o365video/o365video.php new file mode 100644 index 0000000..2cd24e7 --- /dev/null +++ b/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/provider/o365video/version.php b/provider/o365video/version.php new file mode 100644 index 0000000..bf1fed1 --- /dev/null +++ b/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/provider/officeforms/lang/en/oembedprovider_officeforms.php b/provider/officeforms/lang/en/oembedprovider_officeforms.php new file mode 100644 index 0000000..a0608aa --- /dev/null +++ b/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/provider/officeforms/officeforms.php b/provider/officeforms/officeforms.php new file mode 100644 index 0000000..0fb5446 --- /dev/null +++ b/provider/officeforms/officeforms.php @@ -0,0 +1,108 @@ +. + +/** + * @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 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. + * + * @param array $matched Matched URL. + * @return string The replacement text/HTML. + */ + public function get_replacement($matched) { + 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; + } + return $matched[0]; + } + + /** + * 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) { + $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; + } + + /** + * Return the HTML content to be embedded. + * + * @param string $embedurl Additional parameters to include in the embed URL. + * @return string The HTML content to be embedded in the page. + */ + private function getembedhtml($embedurl) { + $iframeattrs = [ + 'src' => $embedurl, + 'height' => '768px', + 'width' => '99%', + 'frameborder' => '0', + 'marginwidth' => '0', + 'marginheight' => '0', + 'style' => 'border: none; max-width: 100%; max-height: 100vh', + 'allowfullscreen' => 'true', + 'webkitallowfullscreen' => 'true', + 'mozallowfullscreen' => 'true', + 'msallowfullscreen' => 'true', + ]; + return \html_writer::tag('iframe', ' ', $iframeattrs); + } +} diff --git a/provider/officeforms/version.php b/provider/officeforms/version.php new file mode 100644 index 0000000..f8c5b58 --- /dev/null +++ b/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/provider/pollev/lang/en/oembedprovider_pollev.php b/provider/pollev/lang/en/oembedprovider_pollev.php new file mode 100644 index 0000000..4bf0641 --- /dev/null +++ b/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/provider/pollev/pollev.php b/provider/pollev/pollev.php new file mode 100644 index 0000000..2004727 --- /dev/null +++ b/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/provider/pollev/version.php b/provider/pollev/version.php new file mode 100644 index 0000000..336fbe6 --- /dev/null +++ b/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/provider/powerbi/lang/en/oembedprovider_powerbi.php b/provider/powerbi/lang/en/oembedprovider_powerbi.php new file mode 100644 index 0000000..f566660 --- /dev/null +++ b/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/provider/powerbi/powerbi.php b/provider/powerbi/powerbi.php new file mode 100644 index 0000000..ac0cc0d --- /dev/null +++ b/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/provider/powerbi/rest/powerbi.php b/provider/powerbi/rest/powerbi.php new file mode 100644 index 0000000..beff7b7 --- /dev/null +++ b/provider/powerbi/rest/powerbi.php @@ -0,0 +1,56 @@ +. + +/** + * @package filter_oembed + * @author Sushant Gawali + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright (C) 2014 onwards Microsoft Open Technologies, Inc. (http://msopentech.com/) + */ + +namespace filter_oembed\provider\powerbi\rest; + +defined('MOODLE_INTERNAL') || die(); + +/** + * API client for Power BI. + */ +class powerbi extends \local_o365\rest\o365api { + /** + * Get the base URI that API calls should be sent to. + * + * @return string|bool The URI to send API calls to, or false if a precondition failed. + */ + public function get_apiuri() { + return "https://api.powerbi.com/beta/myorg/"; + } + /** + * Get the API client's oauth2 resource. + * + * @return string The resource for oauth2 tokens. + */ + public static function get_resource() { + return 'https://analysis.windows.net/powerbi/api'; + } + public function getreportoembedurl($reportid, $reportsdata) { + $reportsdata = $this->process_apicall_response($reportsdata); + foreach ($reportsdata['value'] as $report) { + if ($report['id'] == $reportid) { + return $report['embedUrl']; + } + } + } +} \ No newline at end of file diff --git a/provider/powerbi/version.php b/provider/powerbi/version.php new file mode 100644 index 0000000..bc2374b --- /dev/null +++ b/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/provider/powerbi/yui/powerbiloader/powerbiloader.js b/provider/powerbi/yui/powerbiloader/powerbiloader.js new file mode 100644 index 0000000..62ce0ef --- /dev/null +++ b/provider/powerbi/yui/powerbiloader/powerbiloader.js @@ -0,0 +1,39 @@ +YUI.add('moodle-filter_oembed-powerbiloader', function (Y) { + var ModulenameNAME = 'powerbiloader'; + var token = ''; + var MODULENAME = function () { + MODULENAME.superclass.constructor.apply(this, arguments); + }; + Y.extend(MODULENAME, Y.Base, { + initializer: function (config) { + if (document.getElementsByClassName('token').length > 0) + { + token = document.getElementsByClassName('token')[0].value; + var iframes = document.getElementsByClassName('powerbi_iframe'); + for (var i = 0; i < iframes.length; i++) { + iframes[i].onload = postActionLoadReport; + } + } + } + }, { + NAME: ModulenameNAME, + ATTRS: { + aparam: {} + } + }); + M.filter_oembed = M.filter_oembed || {}; + + M.filter_oembed.init_powerbiloader = function (config) { + return new MODULENAME(config); + }; + + // Post the auth token to the iFrame. + postActionLoadReport = function () { + var m = {action: "loadReport", accessToken: token}; + message = JSON.stringify(m); + // Push the message. + this.contentWindow.postMessage(message, "*"); + }; +}, '@VERSION@', { + requires: ['base'] +}); \ No newline at end of file diff --git a/provider/providers.json b/provider/providers.json new file mode 100644 index 0000000..0670fd8 --- /dev/null +++ b/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/sass/styles.scss b/sass/styles.scss new file mode 100644 index 0000000..ba727ca --- /dev/null +++ b/sass/styles.scss @@ -0,0 +1,100 @@ +.filter_oembed_docsdotcom { + iframe { + width: 100%; + height: 400px; + } +} + +.oembed-card { + position: relative; + min-height: 10em; + background-size: cover; + transition: all 0.4s ease-in-out; +} + +.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%; + } + video { + // Responsive video for HTML5 only + height: auto; + } +} + +.btn.btn-link.oembed-card-play { + background-image: url([[pix:filter_oembed|play]]); + background-repeat: no-repeat; + position: absolute; + 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); +} + +.btn.btn-link.oembed-card-play:hover { + background-position: 0; + filter: drop-shadow(0 0 0 #666); + opacity: 1; +} + +.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; + } +} + +.oembed-responsive-pad { + display: block; +} + +.oembed-provider-details { + font-size: 0.9em; + margin-top: 1em; + display: none; +} + +tr.oembed-provider-editing { + .oembed-provider-details { + display: block; + } + .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/settings.php b/settings.php new file mode 100644 index 0000000..c58a481 --- /dev/null +++ b/settings.php @@ -0,0 +1,63 @@ +. + +/** + * Filter for component 'filter_oembed' + * + * @package filter_oembed + * @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 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) { + $targettags = [ + 'a' => get_string('atag', 'filter_oembed'), + 'div' => get_string('divtag', 'filter_oembed') + ]; + + $config = get_config('filter_oembed'); + + $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); + + $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/styles.css b/styles.css new file mode 100644 index 0000000..4572ad2 --- /dev/null +++ b/styles.css @@ -0,0 +1,100 @@ +.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; + 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; + 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); +} + +.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; +} + +.oembed-responsive { + width: 100%; + display: block; + position: relative; +} + +.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; +} + +.oembed-responsive-pad { + display: block; +} + +.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/templates/managementpage.mustache b/templates/managementpage.mustache new file mode 100644 index 0000000..64c2f53 --- /dev/null +++ b/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/templates/managementpagerow.mustache b/templates/managementpagerow.mustache new file mode 100644 index 0000000..dda248d --- /dev/null +++ b/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/templates/preload.mustache b/templates/preload.mustache new file mode 100644 index 0000000..127db29 --- /dev/null +++ b/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/tests/behat/behat_filter_oembed.php b/tests/behat/behat_filter_oembed.php new file mode 100644 index 0000000..611d8bf --- /dev/null +++ b/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/tests/behat/management.feature b/tests/behat/management.feature new file mode 100644 index 0000000..0207dd9 --- /dev/null +++ b/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/tests/filter_test.php b/tests/filter_test.php new file mode 100644 index 0000000..5b83ffa --- /dev/null +++ b/tests/filter_test.php @@ -0,0 +1,152 @@ +. + +/** + * Unit tests for the filter_oembed. + * + * @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. + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/filter/oembed/filter.php'); + +/** + * @group filter_oembed + */ +class filter_oembed_testcase extends advanced_testcase { + + protected $filter; + + /** + * Sets up the test cases. + */ + 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; + } + } + } + + /** + * Performs unit tests for all services supported by the filter. + * + * Need to update this test to not contact external services. + */ + public function test_filter() { + $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

'; + $vimeolink = '

vimeo

'; + $tedlink = '

Ted

'; + $slidesharelink = '

slideshare

'; + $issuulink = '

issuu

'; + $polleverywherelink = '

'; + $polleverywherelink .= '$popolleverywhere

'; + + $filterinput = $soundcloudlink.$youtubelink.$vimeolink.$tedlink.$slidesharelink.$issuulink.$polleverywherelink; + + $filteroutput = $this->filter->filter($filterinput); + + $youtubeoutput = '/.*