Merge branch 'MDL-38808' of git://github.com/jmvedrine/moodle
authorDan Poltawski <dan@moodle.com>
Wed, 3 Apr 2013 06:46:58 +0000 (14:46 +0800)
committerDan Poltawski <dan@moodle.com>
Wed, 3 Apr 2013 06:46:58 +0000 (14:46 +0800)
384 files changed:
admin/index.php
admin/registration/forms.php
admin/registration/lib.php
admin/registration/register.php
admin/settings/badges.php [new file with mode: 0644]
admin/settings/courses.php
admin/settings/subsystems.php
admin/settings/top.php
admin/tests/behat/behat_admin.php [new file with mode: 0644]
admin/tests/behat/display_short_names.feature [new file with mode: 0644]
admin/tests/behat/upload_users.feature [new file with mode: 0644]
admin/tool/behat/cli/init.php
admin/tool/behat/renderer.php
admin/tool/installaddon/classes/installer.php [new file with mode: 0644]
admin/tool/installaddon/classes/installfromzip_form.php [new file with mode: 0644]
admin/tool/installaddon/classes/pluginfo_client.php [new file with mode: 0644]
admin/tool/installaddon/classes/validator.php [new file with mode: 0644]
admin/tool/installaddon/deploy.php [new file with mode: 0644]
admin/tool/installaddon/index.php [new file with mode: 0644]
admin/tool/installaddon/lang/en/tool_installaddon.php [new file with mode: 0644]
admin/tool/installaddon/permcheck.php [new file with mode: 0644]
admin/tool/installaddon/pix/icon.png [new file with mode: 0644]
admin/tool/installaddon/pix/icon.svg [new file with mode: 0644]
admin/tool/installaddon/renderer.php [new file with mode: 0644]
admin/tool/installaddon/settings.php [new file with mode: 0644]
admin/tool/installaddon/styles.css [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/emptydir/emptydir/README.txt [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/github/moodle-repository_mahara-master/lang/en/repository_mahara.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/github/moodle-repository_mahara-master/version.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/installed/greenbar/index.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/installed/greenbar/lang/en/local_greenbar.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/installed/greenbar/version.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/multidir/one/version.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/multidir/two/README.txt [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/nolang/bah/index.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/nolang/bah/lang/en/bah.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/nolang/bah/lang/en/bleh.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/nolang/bah/lib.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/nolang/bah/version.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/nolang/bah/view.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/noversionmod/noversion/lang/en/noversion.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/noversiontheme/noversion/lang/en/theme_noversion.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/nowrapdir/index.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/nowrapdir/lang/en/foo.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/nowrapdir/version.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/plugindir/foobar/index.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/plugindir/foobar/lang/en/local_foobar.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/plugindir/foobar/version.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/versionphp/version1.php [new file with mode: 0644]
admin/tool/installaddon/tests/fixtures/zips/invalidroot.zip [new file with mode: 0644]
admin/tool/installaddon/tests/installer_test.php [new file with mode: 0644]
admin/tool/installaddon/tests/validator_test.php [new file with mode: 0644]
admin/tool/installaddon/validate.php [new file with mode: 0644]
admin/tool/installaddon/version.php [new file with mode: 0644]
admin/tool/installaddon/yui/permcheck/permcheck.js [new file with mode: 0644]
backup/restorefile_form.php
backup/util/settings/base_setting.class.php
backup/util/ui/backup_ui_setting.class.php
backup/util/ui/base_moodleform.class.php
backup/util/ui/renderer.php
backup/util/ui/restore_ui_components.php
backup/util/ui/tests/behat/backup_courses.feature [new file with mode: 0644]
backup/util/ui/tests/behat/behat_backup.php [new file with mode: 0644]
backup/util/ui/tests/behat/duplicate_activities.feature [new file with mode: 0644]
backup/util/ui/tests/behat/import_course.feature [new file with mode: 0644]
backup/util/ui/tests/behat/restore_moodle2_courses.feature [new file with mode: 0644]
badges/action.php [new file with mode: 0644]
badges/ajax.php [new file with mode: 0644]
badges/assertion.php [new file with mode: 0644]
badges/award.php [new file with mode: 0644]
badges/backpack.js [new file with mode: 0644]
badges/backpack_form.php [new file with mode: 0644]
badges/badge.php [new file with mode: 0644]
badges/criteria.php [new file with mode: 0644]
badges/criteria/award_criteria.php [new file with mode: 0644]
badges/criteria/award_criteria_activity.php [new file with mode: 0644]
badges/criteria/award_criteria_course.php [new file with mode: 0644]
badges/criteria/award_criteria_courseset.php [new file with mode: 0644]
badges/criteria/award_criteria_manual.php [new file with mode: 0644]
badges/criteria/award_criteria_overall.php [new file with mode: 0644]
badges/criteria/award_criteria_profile.php [new file with mode: 0644]
badges/criteria_action.php [new file with mode: 0644]
badges/criteria_form.php [new file with mode: 0644]
badges/criteria_settings.php [new file with mode: 0644]
badges/cron.php [new file with mode: 0644]
badges/edit.php [new file with mode: 0644]
badges/edit_form.php [new file with mode: 0644]
badges/external.php [new file with mode: 0644]
badges/index.php [new file with mode: 0644]
badges/lib/awardlib.php [new file with mode: 0644]
badges/lib/backpacklib.php [new file with mode: 0644]
badges/lib/bakerlib.php [new file with mode: 0644]
badges/mybackpack.php [new file with mode: 0644]
badges/mybadges.php [new file with mode: 0644]
badges/newbadge.php [new file with mode: 0644]
badges/overview.php [new file with mode: 0644]
badges/recipients.php [new file with mode: 0644]
badges/renderer.php [new file with mode: 0644]
badges/tests/badgeslib_test.php [new file with mode: 0644]
badges/tests/behat/add_badge.feature [new file with mode: 0644]
badges/tests/behat/award_badge.feature [new file with mode: 0644]
badges/tests/behat/badge.png [new file with mode: 0644]
badges/view.php [new file with mode: 0644]
blocks/badges/block_badges.php [new file with mode: 0644]
blocks/badges/db/access.php [new file with mode: 0644]
blocks/badges/edit_form.php [new file with mode: 0644]
blocks/badges/lang/en/block_badges.php [new file with mode: 0644]
blocks/badges/version.php [new file with mode: 0644]
blocks/comments/tests/behat/add_comment.feature [new file with mode: 0644]
blocks/comments/tests/behat/behat_block_comments.php [new file with mode: 0644]
blocks/comments/tests/behat/delete_comment.feature [new file with mode: 0644]
blocks/edit_form.php
blocks/tests/behat/add_blocks.feature
blocks/tests/behat/behat_blocks.php
completion/tests/behat/behat_completion.php [new file with mode: 0644]
completion/tests/behat/enable_manual_complete_mark.feature [new file with mode: 0644]
completion/tests/behat/restrict_section_availability.feature [new file with mode: 0644]
config-dist.php
course/edit_form.php
course/externallib.php
course/lib.php
course/modlib.php
course/tests/behat/activities_group_icons.feature [new file with mode: 0644]
course/tests/behat/activities_indentation.feature [new file with mode: 0644]
course/tests/behat/activities_visibility_icons.feature [new file with mode: 0644]
course/tests/behat/add_activities.feature
course/tests/behat/behat_course.php
course/tests/behat/force_group_mode.feature [new file with mode: 0644]
course/tests/behat/paged_course_navigation.feature [new file with mode: 0644]
course/tests/behat/restrict_available_activities.feature [new file with mode: 0644]
course/tests/behat/section_highlighting.feature [new file with mode: 0644]
course/tests/behat/section_visibility.feature [new file with mode: 0644]
course/tests/courselib_test.php
course/tests/externallib_test.php
enrol/cohort/edit_form.php
enrol/imsenterprise/lib.php
enrol/locallib.php
enrol/manual/edit_form.php
enrol/paypal/edit.php
enrol/paypal/edit_form.php
enrol/renderer.php
enrol/self/edit_form.php
enrol/users.php
enrol/users_forms.php
lang/en/backup.php
lang/en/badges.php [new file with mode: 0644]
lang/en/completion.php
lang/en/hub.php
lang/en/moodle.php
lang/en/role.php
lib/badgeslib.php [new file with mode: 0644]
lib/behat/behat_base.php
lib/behat/behat_field_manager.php
lib/behat/form_field/behat_form_field.php
lib/behat/form_field/behat_form_modvisible.php [new file with mode: 0644]
lib/completionlib.php
lib/cronlib.php
lib/datalib.php
lib/db/access.php
lib/db/events.php
lib/db/install.xml [changed mode: 0644->0755]
lib/db/upgrade.php
lib/filelib.php
lib/form/yui/shortforms/shortforms.js
lib/formslib.php
lib/moodlelib.php
lib/navigationlib.php
lib/pluginlib.php
lib/portfolio/forms.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/datalib_test.php
lib/tests/fixtures/upload_users.csv
lib/tests/statslib_test.php
lib/thirdpartylibs.xml
mdeploy.php
mdeploytest.php
mod/forum/tests/behat/add_forum.feature [new file with mode: 0644]
mod/forum/tests/behat/behat_mod_forum.php [new file with mode: 0644]
mod/forum/tests/behat/completion_condition_number_discussions.feature [new file with mode: 0644]
mod/forum/tests/behat/discussion_display.feature [new file with mode: 0644]
mod/forum/tests/behat/edit_post_student.feature [new file with mode: 0644]
mod/forum/tests/behat/edit_post_teacher.feature [new file with mode: 0644]
mod/forum/tests/behat/single_forum_discussion.feature [new file with mode: 0644]
mod/forum/tests/behat/track_read_posts.feature [new file with mode: 0644]
mod/glossary/mod_form.php
mod/quiz/attempt.php
mod/quiz/attemptlib.php
mod/quiz/autosave.ajax.php [new file with mode: 0644]
mod/quiz/lang/en/quiz.php
mod/quiz/settings.php
mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-coverage.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave.js [new file with mode: 0644]
mod/quiz/yui/src/autosave/build.json [new file with mode: 0644]
mod/quiz/yui/src/autosave/js/autosave.js [new file with mode: 0644]
mod/quiz/yui/src/autosave/meta/autosave.json [new file with mode: 0644]
mod/scorm/mod_form.php
mod/survey/tests/behat/survey_types.feature [new file with mode: 0644]
mod/workshop/assessment.php
mod/workshop/backup/moodle2/backup_workshop_stepslib.php
mod/workshop/backup/moodle2/restore_workshop_stepslib.php
mod/workshop/db/install.xml
mod/workshop/db/upgrade.php
mod/workshop/exassessment.php
mod/workshop/fileinfolib.php
mod/workshop/form/assessment_form.php
mod/workshop/lang/en/workshop.php
mod/workshop/lib.php
mod/workshop/locallib.php
mod/workshop/mod_form.php
mod/workshop/renderer.php
mod/workshop/styles.css
mod/workshop/version.php
phpunit.xml.dist
pix/i/badge.png [new file with mode: 0644]
pix/i/badge.svg [new file with mode: 0644]
pix/i/expired.png [new file with mode: 0644]
pix/i/expired.svg [new file with mode: 0644]
pix/t/award.png [new file with mode: 0644]
pix/t/award.svg [new file with mode: 0644]
pix/t/backpack.png [new file with mode: 0644]
pix/t/backpack.svg [new file with mode: 0644]
pix/t/download.svg [new file with mode: 0644]
question/behaviour/behaviourbase.php
question/engine/datalib.php
question/engine/lib.php
question/engine/questionattempt.php
question/engine/questionattemptstep.php
question/engine/questionusage.php
question/engine/tests/helpers.php
question/engine/tests/questionattempt_db_test.php [new file with mode: 0644]
question/engine/tests/questionattempt_test.php
question/engine/tests/questionattempt_with_steps_test.php [new file with mode: 0644]
question/engine/tests/questionattemptstep_db_test.php [new file with mode: 0644]
question/engine/tests/questionattemptstep_test.php
question/engine/tests/questionusage_autosave_test.php [new file with mode: 0644]
question/engine/tests/unitofwork_test.php
question/format/blackboard_six/formatqti.php
question/format/blackboard_six/tests/blackboardsixformatqti_test.php
question/format/xml/format.php
question/format/xml/tests/xmlformat_test.php
question/tests/generator/lib.php
question/type/essay/backup/moodle1/lib.php
question/type/essay/backup/moodle2/backup_qtype_essay_plugin.class.php
question/type/essay/backup/moodle2/restore_qtype_essay_plugin.class.php
question/type/essay/db/install.xml
question/type/essay/db/upgrade.php
question/type/essay/edit_essay_form.php
question/type/essay/lang/en/qtype_essay.php
question/type/essay/question.php
question/type/essay/questiontype.php
question/type/essay/renderer.php
question/type/essay/tests/helper.php
question/type/essay/tests/question_test.php
question/type/essay/tests/walkthrough_test.php
question/type/essay/version.php
question/type/match/tests/walkthrough_test.php
question/type/multianswer/edit_multianswer_form.php
question/type/multianswer/tests/walkthrough_test.php
question/type/numerical/tests/walkthrough_test.php
question/type/shortanswer/tests/helper.php
question/type/upgrade.txt
repository/tests/behat/behat_filepicker.php [moved from lib/tests/behat/behat_filepicker.php with 99% similarity]
repository/tests/behat/create_folders.feature [new file with mode: 0644]
repository/tests/behat/zip_and_unzip.feature [moved from admin/tool/behat/tests/behat/manipulate_filepicker.feature with 66% similarity]
repository/upload/tests/behat/upload_file.feature
theme/base/style/core.css
theme/bootstrap/README.txt [new file with mode: 0644]
theme/bootstrap/config.php [new file with mode: 0644]
theme/bootstrap/javascript/bootstrapcollapse.js [new file with mode: 0644]
theme/bootstrap/javascript/bootstrapdropdown.js [new file with mode: 0644]
theme/bootstrap/javascript/bootstrapengine.js [new file with mode: 0644]
theme/bootstrap/javascript/headercollapse.js [new file with mode: 0644]
theme/bootstrap/javascript/html5shiv.js [new file with mode: 0644]
theme/bootstrap/javascript/moodlebootstrap.js [new file with mode: 0644]
theme/bootstrap/lang/en/theme_bootstrap.php [new file with mode: 0644]
theme/bootstrap/layout/general.php [new file with mode: 0644]
theme/bootstrap/less/README [new file with mode: 0644]
theme/bootstrap/less/bootstrap/accordion.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/alerts.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/bootstrap.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/breadcrumbs.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/button-groups.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/buttons.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/carousel.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/close.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/code.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/component-animations.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/dropdowns.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/forms.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/grid.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/hero-unit.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/labels-badges.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/layouts.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/media.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/mixins.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/modals.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/navbar.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/navs.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/pager.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/pagination.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/popovers.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/progress-bars.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/reset.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/responsive-1200px-min.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/responsive-767px-max.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/responsive-768px-979px.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/responsive-navbar.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/responsive-utilities.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/responsive.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/scaffolding.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/sprites.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/tables.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/thumbnails.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/tooltip.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/type.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/utilities.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/variables.less [new file with mode: 0644]
theme/bootstrap/less/bootstrap/wells.less [new file with mode: 0644]
theme/bootstrap/less/editor.less [new file with mode: 0644]
theme/bootstrap/less/moodle.less [new file with mode: 0644]
theme/bootstrap/less/moodle/admin.less [new file with mode: 0644]
theme/bootstrap/less/moodle/backup-restore.less [new file with mode: 0644]
theme/bootstrap/less/moodle/blocks.less [new file with mode: 0644]
theme/bootstrap/less/moodle/buttons.less [new file with mode: 0644]
theme/bootstrap/less/moodle/calendar.less [new file with mode: 0644]
theme/bootstrap/less/moodle/core.less [new file with mode: 0644]
theme/bootstrap/less/moodle/course.less [new file with mode: 0644]
theme/bootstrap/less/moodle/debug.less [new file with mode: 0644]
theme/bootstrap/less/moodle/expendable.less [new file with mode: 0644]
theme/bootstrap/less/moodle/filemanager.less [new file with mode: 0644]
theme/bootstrap/less/moodle/forms.less [new file with mode: 0644]
theme/bootstrap/less/moodle/grade.less [new file with mode: 0644]
theme/bootstrap/less/moodle/message.less [new file with mode: 0644]
theme/bootstrap/less/moodle/modules.less [new file with mode: 0644]
theme/bootstrap/less/moodle/question.less [new file with mode: 0644]
theme/bootstrap/less/moodle/recess.txt [new file with mode: 0644]
theme/bootstrap/less/moodle/responsive.less [new file with mode: 0644]
theme/bootstrap/less/moodle/tables.less [new file with mode: 0644]
theme/bootstrap/less/moodle/tabs.less [new file with mode: 0644]
theme/bootstrap/less/moodle/undo.less [new file with mode: 0644]
theme/bootstrap/less/moodle/user.less [new file with mode: 0644]
theme/bootstrap/less/moodle/yui_fixes.less [new file with mode: 0644]
theme/bootstrap/pix/favicon.ico [new file with mode: 0644]
theme/bootstrap/pix/fp/alias.png [new file with mode: 0644]
theme/bootstrap/pix/fp/alias_sm.png [new file with mode: 0644]
theme/bootstrap/pix/fp/check.png [new file with mode: 0644]
theme/bootstrap/pix/fp/cross.png [new file with mode: 0644]
theme/bootstrap/pix/fp/dnd_arrow.gif [new file with mode: 0644]
theme/bootstrap/pix/fp/link.png [new file with mode: 0644]
theme/bootstrap/pix/fp/link_sm.png [new file with mode: 0644]
theme/bootstrap/pix/fp/path_folder.png [new file with mode: 0644]
theme/bootstrap/pix/fp/path_folder_rtl.png [new file with mode: 0644]
theme/bootstrap/pix/fp/view_icon_active.png [new file with mode: 0644]
theme/bootstrap/pix/fp/view_icon_inactive.png [new file with mode: 0644]
theme/bootstrap/pix/fp/view_icon_selected.png [new file with mode: 0644]
theme/bootstrap/pix/fp/view_list_active.png [new file with mode: 0644]
theme/bootstrap/pix/fp/view_list_inactive.png [new file with mode: 0644]
theme/bootstrap/pix/fp/view_list_selected.png [new file with mode: 0644]
theme/bootstrap/pix/fp/view_tree_active.png [new file with mode: 0644]
theme/bootstrap/pix/fp/view_tree_inactive.png [new file with mode: 0644]
theme/bootstrap/pix/fp/view_tree_selected.png [new file with mode: 0644]
theme/bootstrap/pix/glyphicons-halflings-white.png [new file with mode: 0644]
theme/bootstrap/pix/glyphicons-halflings.png [new file with mode: 0644]
theme/bootstrap/pix/header.jpg [new file with mode: 0644]
theme/bootstrap/pix/horizontal-menu-submenu-indicator.png [new file with mode: 0644]
theme/bootstrap/pix/screenshot.jpg [new file with mode: 0644]
theme/bootstrap/pix/sprite.png [new file with mode: 0644]
theme/bootstrap/pix/vertical-menu-submenu-indicator.png [new file with mode: 0644]
theme/bootstrap/pix/yui2-treeview-sprite-rtl.gif [new file with mode: 0644]
theme/bootstrap/readme_moodle.txt [new file with mode: 0644]
theme/bootstrap/renderers.php [new file with mode: 0644]
theme/bootstrap/renderers/core.php [new file with mode: 0644]
theme/bootstrap/style/README [new file with mode: 0644]
theme/bootstrap/style/editor.css [new file with mode: 0644]
theme/bootstrap/style/generated.css [new file with mode: 0644]
theme/bootstrap/version.php [new file with mode: 0644]
theme/mymobile/config.php
user/files_form.php
user/profile.php
user/selector/lib.php
version.php

index 35cbdaf..def20d2 100644 (file)
@@ -59,10 +59,18 @@ $confirmplugins = optional_param('confirmplugincheck', 0, PARAM_BOOL);
 $showallplugins = optional_param('showallplugins', 0, PARAM_BOOL);
 $agreelicense   = optional_param('agreelicense', 0, PARAM_BOOL);
 $fetchupdates   = optional_param('fetchupdates', 0, PARAM_BOOL);
+$newaddonreq    = optional_param('installaddonrequest', null, PARAM_RAW);
 
 // Check some PHP server settings
 
-$PAGE->set_url('/admin/index.php');
+if (is_null($newaddonreq)) {
+    $PAGE->set_url('/admin/index.php');
+} else {
+    // We need to set the eventual add-on installation request in the $PAGE's URL
+    // so that it is stored in $SESSION->wantsurl and the admin is redirected
+    // correctly once they are logged-in.
+    $PAGE->set_url('/admin/index.php', array('installaddonrequest' => $newaddonreq));
+}
 $PAGE->set_pagelayout('admin'); // Set a default pagelayout
 
 $documentationlink = '<a href="http://docs.moodle.org/en/Installation">Installation docs</a>';
@@ -423,6 +431,17 @@ if (!empty($id) and $id == $CFG->siteidentifier) {
     set_config('registered', time());
 }
 
+// Check if we are returning from an add-on installation request at moodle.org/plugins
+if (!is_null($newaddonreq)) {
+    if (!empty($CFG->disableonclickaddoninstall)) {
+        // The feature is disabled in config.php, ignore the request.
+    } else {
+        redirect(new moodle_url('/admin/tool/installaddon/index.php', array(
+            'installaddonrequest' => $newaddonreq,
+            'confirm' => 0)));
+    }
+}
+
 // setup critical warnings before printing admin tree block
 $insecuredataroot = is_dataroot_insecure(true);
 $SESSION->admin_critical_warning = ($insecuredataroot==INSECURE_DATAROOT_ERROR);
index 47a450f..863af66 100644 (file)
@@ -243,6 +243,8 @@ class site_registration_form extends moodleform {
         $postsnumber = get_config('hub', 'site_postsnumber_' . $cleanhuburl);
         $questionsnumber = get_config('hub', 'site_questionsnumber_' . $cleanhuburl);
         $resourcesnumber = get_config('hub', 'site_resourcesnumber_' . $cleanhuburl);
+        $badges = get_config('hub', 'site_badges_' . $cleanhuburl);
+        $issuedbadges = get_config('hub', 'site_issuedbadges_' . $cleanhuburl);
         $mediancoursesize = get_config('hub', 'site_mediancoursesize_' . $cleanhuburl);
 
         //hidden parameters
@@ -372,6 +374,9 @@ class site_registration_form extends moodleform {
         require_once($CFG->dirroot . "/course/lib.php");
         $participantnumberaverage = number_format(average_number_of_participants(), 2);
         $modulenumberaverage = number_format(average_number_of_courses_modules(), 2);
+        require_once($CFG->libdir . '/badgeslib.php');
+        $badges = $DB->count_records_select('badge', 'status <> ' . BADGE_STATUS_ARCHIVED);
+        $issuedbadges = $DB->count_records('badge_issued');
 
         if (HUB_MOODLEORGHUBURL != $huburl) {
             $mform->addElement('checkbox', 'courses', get_string('sendfollowinginfo', 'hub'),
@@ -399,6 +404,14 @@ class site_registration_form extends moodleform {
                     " " . get_string('resourcesnumber', 'hub', $resourcecount));
             $mform->setDefault('resources', true);
 
+            $mform->addElement('checkbox', 'badges', '',
+                    " " . get_string('badgesnumber', 'hub', $badges));
+            $mform->setDefault('badges', true);
+
+            $mform->addElement('checkbox', 'issuedbadges', '',
+                    " " . get_string('issuedbadgesnumber', 'hub', $issuedbadges));
+            $mform->setDefault('issuedbadges', true);
+
             $mform->addElement('checkbox', 'participantnumberaverage', '',
                     " " . get_string('participantnumberaverage', 'hub', $participantnumberaverage));
             $mform->setDefault('participantnumberaverage', true);
@@ -438,6 +451,16 @@ class site_registration_form extends moodleform {
             $mform->addElement('hidden', 'resources', true);
             $mform->setType('resources', PARAM_FLOAT);
 
+            $mform->addElement('static', 'badgeslabel', '',
+                    " " . get_string('badgesnumber', 'hub', $badges));
+            $mform->addElement('hidden', 'badges', true);
+            $mform->setType('badges', PARAM_INT);
+
+            $mform->addElement('static', 'issuedbadgeslabel', '',
+                    " " . get_string('issuedbadgesnumber', 'hub', $issuedbadges));
+            $mform->addElement('hidden', 'issuedbadges', true);
+            $mform->setType('issuedbadges', PARAM_INT);
+
             $mform->addElement('static', 'participantnumberaveragelabel', '',
                     " " . get_string('participantnumberaverage', 'hub', $participantnumberaverage));
             $mform->addElement('hidden', 'participantnumberaverage', true);
index 4179d79..a8086e1 100644 (file)
@@ -247,6 +247,20 @@ class registration_manager {
             $resourcecount = $DB->count_records('resource');
         }
         $siteinfo['resources'] = $resourcecount;
+        // Badge statistics.
+        require_once($CFG->libdir . '/badgeslib.php');
+        if (get_config('hub', 'site_badges_' . $cleanhuburl) == -1) {
+            $badges = -1;
+        } else {
+            $badges = $DB->count_records_select('badge', 'status <> ' . BADGE_STATUS_ARCHIVED);
+        }
+        $siteinfo['badges'] = $badges;
+        if (get_config('hub', 'site_issuedbadges_' . $cleanhuburl) == -1) {
+            $issuedbadges = -1;
+        } else {
+            $issuedbadges = $DB->count_records('badge_issued');
+        }
+        $siteinfo['issuedbadges'] = $issuedbadges;
         //TODO
         require_once($CFG->dirroot . "/course/lib.php");
         if (get_config('hub', 'site_participantnumberaverage_' . $cleanhuburl) == -1) {
index 7a6bc92..760dd1f 100644 (file)
@@ -84,6 +84,8 @@ if (!empty($fromform) and confirm_sesskey()) {
     set_config('site_postsnumber_' . $cleanhuburl, $fromform->posts, 'hub');
     set_config('site_questionsnumber_' . $cleanhuburl, $fromform->questions, 'hub');
     set_config('site_resourcesnumber_' . $cleanhuburl, $fromform->resources, 'hub');
+    set_config('site_badges_' . $cleanhuburl, $fromform->badges, 'hub');
+    set_config('site_issuedbadges_' . $cleanhuburl, $fromform->issuedbadges, 'hub');
     set_config('site_modulenumberaverage_' . $cleanhuburl, $fromform->modulenumberaverage, 'hub');
     set_config('site_participantnumberaverage_' . $cleanhuburl, $fromform->participantnumberaverage, 'hub');
 }
diff --git a/admin/settings/badges.php b/admin/settings/badges.php
new file mode 100644 (file)
index 0000000..7e6f41e
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+* This file defines settingpages and externalpages under the "badges" section
+*
+* @package    core
+* @subpackage badges
+* @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+* @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+* @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
+*/
+
+global $SITE;
+
+if (!empty($CFG->enablebadges) && ($hassiteconfig || has_any_capability(array(
+            'moodle/badges:viewawarded',
+            'moodle/badges:createbadge',
+            'moodle/badges:manageglobalsettings',
+            'moodle/badges:awardbadge',
+            'moodle/badges:configuremessages',
+            'moodle/badges:configuredetails',
+            'moodle/badges:deletebadge'), $systemcontext))) {
+
+    require_once($CFG->libdir . '/badgeslib.php');
+
+    $globalsettings = new admin_settingpage('badgesettings', new lang_string('badgesettings', 'badges'),
+            array('moodle/badges:manageglobalsettings'));
+
+    $globalsettings->add(new admin_setting_configtext('badges_defaultissuername',
+            new lang_string('defaultissuername', 'badges'),
+            new lang_string('defaultissuername_desc', 'badges'),
+            $SITE->fullname ? $SITE->fullname : $SITE->shortname, PARAM_TEXT));
+
+    $globalsettings->add(new admin_setting_configtext('badges_defaultissuercontact',
+            new lang_string('defaultissuercontact', 'badges'),
+            new lang_string('defaultissuercontact_desc', 'badges'),
+            get_config('moodle','supportemail'), PARAM_EMAIL));
+
+    $globalsettings->add(new admin_setting_configtext('badges_badgesalt',
+            new lang_string('badgesalt', 'badges'),
+            new lang_string('badgesalt_desc', 'badges'),
+            'badges' . $SITE->timecreated, PARAM_ALPHANUM));
+
+    $globalsettings->add(new admin_setting_configcheckbox('badges_allowexternalbackpack',
+            new lang_string('allowexternalbackpack', 'badges'),
+            new lang_string('allowexternalbackpack_desc', 'badges'), 1));
+
+    $globalsettings->add(new admin_setting_configcheckbox('badges_allowcoursebadges',
+            new lang_string('allowcoursebadges', 'badges'),
+            new lang_string('allowcoursebadges_desc', 'badges'), 1));
+
+    $ADMIN->add('badges', $globalsettings);
+
+    $ADMIN->add('badges',
+        new admin_externalpage('managebadges',
+            new lang_string('managebadges', 'badges'),
+            new moodle_url($CFG->wwwroot . '/badges/index.php', array('type' => BADGE_TYPE_SITE)),
+            array('moodle/badges:viewawarded')
+        )
+    );
+
+    $ADMIN->add('badges',
+        new admin_externalpage('newbadge',
+            new lang_string('newbadge', 'badges'),
+            new moodle_url($CFG->wwwroot . '/badges/newbadge.php', array('type' => BADGE_TYPE_SITE)),
+            array('moodle/badges:createbadge')
+        )
+    );
+}
index 75b3246..9100567 100644 (file)
@@ -80,7 +80,6 @@ if ($hassiteconfig
     $temp->add(new admin_setting_configselect('moodlecourse/enablecompletion', new lang_string('completion','completion'), '',
         0, array(0 => new lang_string('completiondisabled','completion'), 1 => new lang_string('completionenabled','completion'))));
 
-    $temp->add(new admin_setting_configcheckbox('moodlecourse/completionstartonenrol', new lang_string('completionstartonenrol','completion'), new lang_string('completionstartonenrolhelp', 'completion'), 0));
     $ADMIN->add('courses', $temp);
 
 /// "courserequests" settingpage
@@ -135,6 +134,11 @@ if ($hassiteconfig
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_histories', new lang_string('generalhistories','backup'), new lang_string('configgeneralhistories','backup'), array('value'=>0, 'locked'=>0)));
     $ADMIN->add('backups', $temp);
 
+    // Create a page for general import configuration and defaults.
+    $temp = new admin_settingpage('importgeneralsettings', new lang_string('importgeneralsettings', 'backup'), 'moodle/backup:backupcourse');
+    $temp->add(new admin_setting_configtext('backup/import_general_maxresults', new lang_string('importgeneralmaxresults', 'backup'), new lang_string('importgeneralmaxresults_desc', 'backup'), 10));
+    $ADMIN->add('backups', $temp);
+
     // Create a page for automated backups configuration and defaults.
     $temp = new admin_settingpage('automated', new lang_string('automatedsetup','backup'), 'moodle/backup:backupcourse');
 
index 0d3db2e..961ab76 100644 (file)
@@ -41,4 +41,6 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $checkbox->set_affects_modinfo(true);
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('enableplagiarism', new lang_string('enableplagiarism','plagiarism'), new lang_string('configenableplagiarism','plagiarism'), 0));
+
+    $optionalsubsystems->add(new admin_setting_configcheckbox('enablebadges', new lang_string('enablebadges', 'badges'), new lang_string('configenablebadges', 'badges'), 1));
 }
index cd52953..e855519 100644 (file)
@@ -30,6 +30,7 @@ if ($hassiteconfig) {
 $ADMIN->add('root', new admin_category('users', new lang_string('users','admin')));
 $ADMIN->add('root', new admin_category('courses', new lang_string('courses','admin')));
 $ADMIN->add('root', new admin_category('grades', new lang_string('grades')));
+$ADMIN->add('root', new admin_category('badges', new lang_string('badges'), (isset($CFG->enablebadges) && $CFG->enablebadges == 0)));
 $ADMIN->add('root', new admin_category('location', new lang_string('location','admin')));
 $ADMIN->add('root', new admin_category('language', new lang_string('language')));
 $ADMIN->add('root', new admin_category('modules', new lang_string('plugins', 'admin')));
diff --git a/admin/tests/behat/behat_admin.php b/admin/tests/behat/behat_admin.php
new file mode 100644 (file)
index 0000000..cdca881
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Steps definitions related with administration.
+ *
+ * @package   core
+ * @category  test
+ * @copyright 2013 David Monllaó
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
+require_once(__DIR__ . '/../../../lib/behat/behat_field_manager.php');
+
+use Behat\Gherkin\Node\TableNode as TableNode,
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+
+/**
+ * Site administration level steps definitions.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2013 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_admin extends behat_base {
+
+    /**
+     * Sets the specified site settings. A table with | Setting label | value | is expected.
+     *
+     * @Given /^I set the following administration settings values:$/
+     * @param TableNode $table
+     */
+    public function i_set_the_following_administration_settings_values(TableNode $table) {
+
+        if (!$data = $table->getRowsHash()) {
+            return;
+        }
+
+        foreach ($data as $label => $value) {
+
+            // We expect admin block to be visible, otherwise go to homepage.
+            if (!$this->getSession()->getPage()->find('css', '.block_settings')) {
+                $this->getSession()->visit($this->locate_path('/'));
+                $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")');
+            }
+
+            // Search by label.
+            $searchbox = $this->find_field('Search in settings');
+            $searchbox->setValue($label);
+            $submitsearch = $this->find('css', 'form.adminsearchform input[type=submit]');
+            $submitsearch->press();
+
+            $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")');
+
+            // Admin settings does not use the same DOM structure than other moodle forms
+            // but we also need to use lib/behat/form_field/* to deal with the different moodle form elements.
+            $exception = new ElementNotFoundException($this->getSession(), '"' . $label . '" administration setting ');
+            $fieldxpath = "//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]
+[@id=//label[contains(normalize-space(string(.)), '" . $label . "')]/@for]";
+            $fieldnode = $this->find('xpath', $fieldxpath, $exception);
+            $formfieldtypenode = $this->find('xpath', $fieldxpath . "/ancestor::div[@class='form-setting']
+/child::div[contains(concat(' ', @class, ' '),  ' form-')]/child::*/parent::div");
+
+            // Getting the class which contains the field type.
+            $classes = explode(' ', $formfieldtypenode->getAttribute('class'));
+            foreach ($classes as $class) {
+                if (substr($class, 0, 5) == 'form-') {
+                    $type = substr($class, 5);
+                }
+            }
+
+            // Instantiating the appropiate field type.
+            $field = behat_field_manager::get_field_instance($type, $fieldnode, $this->getSession());
+            $field->set_value($value);
+
+            $this->find_button('Save changes')->press();
+        }
+    }
+
+}
diff --git a/admin/tests/behat/display_short_names.feature b/admin/tests/behat/display_short_names.feature
new file mode 100644 (file)
index 0000000..836d753
--- /dev/null
@@ -0,0 +1,22 @@
+@admin
+Feature: Display extended course names
+  In order to display more info about the courses
+  As a moodle admin
+  I need to display courses short names along with courses full names
+
+  Background:
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Course fullname | C_shortname | 0 |
+    And I log in as "admin"
+
+  Scenario: Courses list without extended course names (default value)
+    Then I should see "Course fullname"
+    And I should not see "C_shortname Course fullname"
+
+  Scenario: Courses list with extended course names
+    Given I click on "Courses" "link" in the "//div[@id='settingsnav']//descendant::li[contains(concat(' ', @class, ' '), ' type_setting ')][not(contains(., 'Site administration'))][contains(., 'Appearance')]" "xpath_element"
+    And I check "Display extended course names"
+    When I press "Save changes"
+    And I am on homepage
+    Then I should see "C_shortname Course fullname"
diff --git a/admin/tests/behat/upload_users.feature b/admin/tests/behat/upload_users.feature
new file mode 100644 (file)
index 0000000..e61208e
--- /dev/null
@@ -0,0 +1,46 @@
+@admin @_only_local
+Feature: Upload users
+  In order to add users to the system
+  As a moodle admin
+  I need to upload files containing the users data
+
+  @javascript
+  Scenario: Upload users enrolling them on courses and groups
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Maths | math102 | 0 |
+    And the following "groups" exists:
+      | name | course | idnumber |
+      | Section 1 | math102 | S1 |
+      | Section 3 | math102 | S3 |
+    And I log in as "admin"
+    And I expand "Front page settings" node
+    And I expand "Site administration" node
+    And I expand "Users" node
+    And I expand "Accounts" node
+    And I follow "Upload users"
+    When I upload "lib/tests/fixtures/upload_users.csv" file to "File" filepicker
+    And I press "Upload users"
+    Then I should see "Upload users preview"
+    And I should see "Tom"
+    And I should see "Jones"
+    And I should see "verysecret"
+    And I should see "jonest@someplace.edu"
+    And I should see "Reznor"
+    And I should see "course1"
+    And I should see "math102"
+    And I should see "group1"
+    And I should see "Section 1"
+    And I press "Upload users"
+    And I press "Continue"
+    And I follow "Browse list of users"
+    And I should see "Tom Jones"
+    And I should see "Trent Reznor"
+    And I should see "reznor@someplace.edu"
+    And I am on homepage
+    And I follow "Maths"
+    And I expand "Users" node
+    And I follow "Groups"
+    And I select "Section 1 (1)" from "groups"
+    And I wait "4" seconds
+    And the "members" select box should contain "Tom Jones"
index 1916367..ca68c16 100644 (file)
@@ -65,7 +65,7 @@ if ($code == 0) {
     // Changing to moodle dirroot to run composer related commands at project level.
     chdir(__DIR__ . '/../../../..');
     if (!file_exists(__DIR__ . '/../../../../composer.phar')) {
-        passthru("curl http://getcomposer.org/install | php", $code);
+        passthru("curl http://getcomposer.org/installer | php", $code);
         if ($code != 0) {
             exit($code);
         }
index 0df7e3a..503bc09 100644 (file)
@@ -89,7 +89,7 @@ class tool_behat_renderer extends plugin_renderer_base {
             $stepsdefinitions = implode('', $stepsdefinitions);
 
             // Replace text selector type arguments with a user-friendly select.
-            $stepsdefinitions = preg_replace_callback('/(TEXT_SELECTOR_STRING)/',
+            $stepsdefinitions = preg_replace_callback('/(TEXT_SELECTOR\d?_STRING)/',
                 function ($matches) {
                     return html_writer::select(behat_command::$allowedtextselectors, uniqid());
                 },
@@ -97,7 +97,7 @@ class tool_behat_renderer extends plugin_renderer_base {
             );
 
             // Replace selector type arguments with a user-friendly select.
-            $stepsdefinitions = preg_replace_callback('/(SELECTOR_STRING)/',
+            $stepsdefinitions = preg_replace_callback('/(SELECTOR\d?_STRING)/',
                 function ($matches) {
                     return html_writer::select(behat_command::$allowedselectors, uniqid());
                 },
diff --git a/admin/tool/installaddon/classes/installer.php b/admin/tool/installaddon/classes/installer.php
new file mode 100644 (file)
index 0000000..fa4babb
--- /dev/null
@@ -0,0 +1,574 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Provides tool_installaddon_installer related classes
+ *
+ * @package     tool_installaddon
+ * @subpackage  classes
+ * @copyright   2013 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Implements main plugin features.
+ *
+ * @copyright 2013 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_installaddon_installer {
+
+    /** @var tool_installaddon_installfromzip */
+    protected $installfromzipform = null;
+
+    /**
+     * Factory method returning an instance of this class.
+     *
+     * @return tool_installaddon_installer
+     */
+    public static function instance() {
+        return new static();
+    }
+
+    /**
+     * Returns the URL to the main page of this admin tool
+     *
+     * @param array optional parameters
+     * @return moodle_url
+     */
+    public function index_url(array $params = null) {
+        return new moodle_url('/admin/tool/installaddon/index.php', $params);
+    }
+
+    /**
+     * Returns URL to the repository that addons can be searched in and installed from
+     *
+     * @return moodle_url
+     */
+    public function get_addons_repository_url() {
+        global $CFG;
+
+        if (!empty($CFG->config_php_settings['alternativeaddonsrepositoryurl'])) {
+            $url = $CFG->config_php_settings['alternativeaddonsrepositoryurl'];
+        } else {
+            $url = 'https://moodle.org/plugins/get.php';
+        }
+
+        if (!$this->should_send_site_info()) {
+            return new moodle_url($url);
+        }
+
+        // Append the basic information about our site.
+        $site = array(
+            'fullname' => $this->get_site_fullname(),
+            'url' => $this->get_site_url(),
+            'majorversion' => $this->get_site_major_version(),
+        );
+
+        $site = $this->encode_site_information($site);
+
+        return new moodle_url($url, array('site' => $site));
+    }
+
+    /**
+     * @return tool_installaddon_installfromzip
+     */
+    public function get_installfromzip_form() {
+        global $CFG;
+        require_once(dirname(__FILE__).'/installfromzip_form.php');
+
+        if (!is_null($this->installfromzipform)) {
+            return $this->installfromzipform;
+        }
+
+        $action = $this->index_url();
+        $customdata = array('installer' => $this);
+
+        $this->installfromzipform = new tool_installaddon_installfromzip($action, $customdata);
+
+        return $this->installfromzipform;
+    }
+
+    /**
+     * Saves the ZIP file from the {@link tool_installaddon_installfromzip} form
+     *
+     * The file is saved into the given temporary location for inspection and eventual
+     * deployment. The form is expected to be submitted and validated.
+     *
+     * @param tool_installaddon_installfromzip $form
+     * @param string $targetdir full path to the directory where the ZIP should be stored to
+     * @return string filename of the saved file relative to the given target
+     */
+    public function save_installfromzip_file(tool_installaddon_installfromzip $form, $targetdir) {
+
+        $filename = clean_param($form->get_new_filename('zipfile'), PARAM_FILE);
+        $form->save_file('zipfile', $targetdir.'/'.$filename);
+
+        return $filename;
+    }
+
+    /**
+     * Extracts the saved file previously saved by {self::save_installfromzip_file()}
+     *
+     * The list of files found in the ZIP is returned via $zipcontentfiles parameter
+     * by reference. The format of that list is array of (string)filerelpath => (bool|string)
+     * where the array value is either true or a string describing the problematic file.
+     *
+     * @see zip_packer::extract_to_pathname()
+     * @param string $zipfilepath full path to the saved ZIP file
+     * @param string $targetdir full path to the directory to extract the ZIP file to
+     * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value
+     * @param array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}
+     */
+    public function extract_installfromzip_file($zipfilepath, $targetdir, $rootdir = '') {
+        global $CFG;
+        require_once($CFG->libdir.'/filelib.php');
+
+        $fp = get_file_packer('application/zip');
+        $files = $fp->extract_to_pathname($zipfilepath, $targetdir);
+
+        if ($files) {
+            if (!empty($rootdir)) {
+                $files = $this->rename_extracted_rootdir($targetdir, $rootdir, $files);
+            }
+            return $files;
+
+        } else {
+            return array();
+        }
+    }
+
+    /**
+     * Returns localised list of available plugin types
+     *
+     * @return array (string)plugintype => (string)plugin name
+     */
+    public function get_plugin_types_menu() {
+        global $CFG;
+        require_once($CFG->libdir.'/pluginlib.php');
+
+        $pluginman = plugin_manager::instance();
+
+        $menu = array('' => get_string('choosedots'));
+        foreach (array_keys($pluginman->get_plugin_types()) as $plugintype) {
+            $menu[$plugintype] = $pluginman->plugintype_name($plugintype).' ('.$plugintype.')';
+        }
+
+        return $menu;
+    }
+
+    /**
+     * Returns the full path of the root of the given plugin type
+     *
+     * Null is returned if the plugin type is not known. False is returned if the plugin type
+     * root is expected but not found. Otherwise, string is returned.
+     *
+     * @param string $plugintype
+     * @return string|bool|null
+     */
+    public function get_plugintype_root($plugintype) {
+
+        $plugintypepath = null;
+        foreach (get_plugin_types() as $type => $fullpath) {
+            if ($type === $plugintype) {
+                $plugintypepath = $fullpath;
+                break;
+            }
+        }
+        if (is_null($plugintypepath)) {
+            return null;
+        }
+
+        if (!is_dir($plugintypepath)) {
+            return false;
+        }
+
+        return $plugintypepath;
+    }
+
+    /**
+     * Is it possible to create a new plugin directory for the given plugin type?
+     *
+     * @throws coding_exception for invalid plugin types or non-existing plugin type locations
+     * @param string $plugintype
+     * @return boolean
+     */
+    public function is_plugintype_writable($plugintype) {
+
+        $plugintypepath = $this->get_plugintype_root($plugintype);
+
+        if (is_null($plugintypepath)) {
+            throw new coding_exception('Unknown plugin type!');
+        }
+
+        if ($plugintypepath === false) {
+            throw new coding_exception('Plugin type location does not exist!');
+        }
+
+        return is_writable($plugintypepath);
+    }
+
+    /**
+     * Hook method to handle the remote request to install an add-on
+     *
+     * This is used as a callback when the admin picks a plugin version in the
+     * Moodle Plugins directory and is redirected back to their site to install
+     * it.
+     *
+     * This hook is called early from admin/tool/installaddon/index.php page so that
+     * it has opportunity to take over the UI.
+     *
+     * @param tool_installaddon_renderer $output
+     * @param string|null $request
+     * @param bool $confirmed
+     */
+    public function handle_remote_request(tool_installaddon_renderer $output, $request, $confirmed = false) {
+        global $CFG;
+        require_once(dirname(__FILE__).'/pluginfo_client.php');
+
+        if (is_null($request)) {
+            return;
+        }
+
+        $data = $this->decode_remote_request($request);
+
+        if ($data === false) {
+            echo $output->remote_request_invalid_page($this->index_url());
+            exit();
+        }
+
+        list($plugintype, $pluginname) = normalize_component($data->component);
+
+        $plugintypepath = $this->get_plugintype_root($plugintype);
+
+        if (file_exists($plugintypepath.'/'.$pluginname)) {
+            echo $output->remote_request_alreadyinstalled_page($data, $this->index_url());
+            exit();
+        }
+
+        if (!$this->is_plugintype_writable($plugintype)) {
+            $continueurl = $this->index_url(array('installaddonrequest' => $request));
+            echo $output->remote_request_permcheck_page($data, $plugintypepath, $continueurl, $this->index_url());
+            exit();
+        }
+
+        $continueurl = $this->index_url(array(
+            'installaddonrequest' => $request,
+            'confirm' => 1,
+            'sesskey' => sesskey()));
+
+        if (!$confirmed) {
+            echo $output->remote_request_confirm_page($data, $continueurl, $this->index_url());
+            exit();
+        }
+
+        // The admin has confirmed their intention to install the add-on.
+        require_sesskey();
+
+        // Fetch the plugin info. The essential information is the URL to download the ZIP
+        // and the MD5 hash of the ZIP, obtained via HTTPS.
+        $client = tool_installaddon_pluginfo_client::instance();
+
+        try {
+            $pluginfo = $client->get_pluginfo($data->component, $data->version);
+
+        } catch (tool_installaddon_pluginfo_exception $e) {
+            if (debugging()) {
+                throw $e;
+            } else {
+                echo $output->remote_request_pluginfo_exception($data, $e, $this->index_url());
+                exit();
+            }
+        }
+
+        // Fetch the ZIP with the plugin version
+        $jobid = md5(rand().uniqid('', true));
+        $sourcedir = make_temp_directory('tool_installaddon/'.$jobid.'/source');
+        $zipfilename = 'downloaded.zip';
+
+        try {
+            $this->download_file($pluginfo->downloadurl, $sourcedir.'/'.$zipfilename);
+
+        } catch (tool_installaddon_installer_exception $e) {
+            if (debugging()) {
+                throw $e;
+            } else {
+                echo $output->installer_exception($e, $this->index_url());
+                exit();
+            }
+        }
+
+        // Check the MD5 checksum
+        $md5expected = $pluginfo->downloadmd5;
+        $md5actual = md5_file($sourcedir.'/'.$zipfilename);
+        if ($md5expected !== $md5actual) {
+            $e = new tool_installaddon_installer_exception('err_zip_md5', array('expected' => $md5expected, 'actual' => $md5actual));
+            if (debugging()) {
+                throw $e;
+            } else {
+                echo $output->installer_exception($e, $this->index_url());
+                exit();
+            }
+        }
+
+        // Redirect to the validation page.
+        $nexturl = new moodle_url('/admin/tool/installaddon/validate.php', array(
+            'sesskey' => sesskey(),
+            'jobid' => $jobid,
+            'zip' => $zipfilename,
+            'type' => $plugintype));
+        redirect($nexturl);
+    }
+
+    /**
+     * Download the given file into the given destination.
+     *
+     * This is basically a simplified version of {@link download_file_content()} from
+     * Moodle itself, tuned for fetching files from moodle.org servers. Same code is used
+     * in mdeploy.php for fetching available updates.
+     *
+     * @param string $source file url starting with http(s)://
+     * @param string $target store the downloaded content to this file (full path)
+     * @throws tool_installaddon_installer_exception
+     */
+    public function download_file($source, $target) {
+        global $CFG;
+        require_once($CFG->libdir.'/filelib.php');
+
+        $targetfile = fopen($target, 'w');
+
+        if (!$targetfile) {
+            throw new tool_installaddon_installer_exception('err_download_write_file', $target);
+        }
+
+        $options = array(
+            'file' => $targetfile,
+            'timeout' => 300,
+            'followlocation' => true,
+            'maxredirs' => 3,
+            'ssl_verifypeer' => true,
+            'ssl_verifyhost' => 2,
+        );
+
+        $cacertfile = $CFG->dataroot.'/moodleorgca.crt';
+        if (is_readable($cacertfile)) {
+            // Do not use CA certs provided by the operating system. Instead,
+            // use this CA cert to verify the ZIP provider.
+            $options['cainfo'] = $cacertfile;
+        }
+
+        $curl = new curl(array('proxy' => true));
+
+        $result = $curl->download_one($source, null, $options);
+
+        $curlinfo = $curl->get_info();
+
+        fclose($targetfile);
+
+        if ($result !== true) {
+            throw new tool_installaddon_installer_exception('err_curl_exec', array(
+                'url' => $source, 'errorno' => $curl->get_errno(), 'error' => $result));
+
+        } else if (empty($curlinfo['http_code']) or $curlinfo['http_code'] != 200) {
+            throw new tool_installaddon_installer_exception('err_curl_http_code', array(
+                'url' => $source, 'http_code' => $curlinfo['http_code']));
+
+        } else if (isset($curlinfo['ssl_verify_result']) and $curlinfo['ssl_verify_result'] != 0) {
+            throw new tool_installaddon_installer_exception('err_curl_ssl_verify', array(
+                'url' => $source, 'ssl_verify_result' => $curlinfo['ssl_verify_result']));
+        }
+    }
+
+    //// End of external API ///////////////////////////////////////////////////
+
+    /**
+     * @see self::instance()
+     */
+    protected function __construct() {
+    }
+
+    /**
+     * @return string this site full name
+     */
+    protected function get_site_fullname() {
+        global $SITE;
+
+        return strip_tags($SITE->fullname);
+    }
+
+    /**
+     * @return string this site URL
+     */
+    protected function get_site_url() {
+        global $CFG;
+
+        return $CFG->wwwroot;
+    }
+
+    /**
+     * @return string major version like 2.5, 2.6 etc.
+     */
+    protected function get_site_major_version() {
+        return moodle_major_version();
+    }
+
+    /**
+     * Encodes the given array in a way that can be safely appended as HTTP GET param
+     *
+     * Be ware! The recipient may rely on the exact way how the site information is encoded.
+     * Do not change anything here unless you know what you are doing and understand all
+     * consequences! (Don't you love warnings like that, too? :-p)
+     *
+     * @param array $info
+     * @return string
+     */
+    protected function encode_site_information(array $info) {
+        return base64_encode(json_encode($info));
+    }
+
+    /**
+     * Decide if the encoded site information should be sent to the add-ons repository site
+     *
+     * For now, we just return true. In the future, we may want to implement some
+     * privacy aware logic (based on site/user preferences for example).
+     *
+     * @return bool
+     */
+    protected function should_send_site_info() {
+        return true;
+    }
+
+    /**
+     * Renames the root directory of the extracted ZIP package.
+     *
+     * This method does not validate the presence of the single root directory
+     * (the validator does it later). It just searches for the first directory
+     * under the given location and renames it.
+     *
+     * The method will not rename the root if the requested location already
+     * exists.
+     *
+     * @param string $dirname the location of the extracted ZIP package
+     * @param string $rootdir the requested name of the root directory
+     * @param array $files list of extracted files
+     * @return array eventually amended list of extracted files
+     */
+    protected function rename_extracted_rootdir($dirname, $rootdir, array $files) {
+
+        if (!is_dir($dirname)) {
+            debugging('Unable to rename rootdir of non-existing content', DEBUG_DEVELOPER);
+            return $files;
+        }
+
+        if (file_exists($dirname.'/'.$rootdir)) {
+            debugging('Unable to rename rootdir to already existing folder', DEBUG_DEVELOPER);
+            return $files;
+        }
+
+        $found = null; // The name of the first subdirectory under the $dirname.
+        foreach (scandir($dirname) as $item) {
+            if (substr($item, 0, 1) === '.') {
+                continue;
+            }
+            if (is_dir($dirname.'/'.$item)) {
+                $found = $item;
+                break;
+            }
+        }
+
+        if (!is_null($found)) {
+            if (rename($dirname.'/'.$found, $dirname.'/'.$rootdir)) {
+                $newfiles = array();
+                foreach ($files as $filepath => $status) {
+                    $newpath = preg_replace('~^'.preg_quote($found.'/').'~', preg_quote($rootdir.'/'), $filepath);
+                    $newfiles[$newpath] = $status;
+                }
+                return $newfiles;
+            }
+        }
+
+        return $files;
+    }
+
+    /**
+     * Decode the request from the Moodle Plugins directory
+     *
+     * @param string $request submitted via 'installaddonrequest' HTTP parameter
+     * @return stdClass|bool false on error, object otherwise
+     */
+    protected function decode_remote_request($request) {
+
+        $data = base64_decode($request, true);
+
+        if ($data === false) {
+            return false;
+        }
+
+        $data = json_decode($data);
+
+        if (is_null($data)) {
+            return false;
+        }
+
+        if (!isset($data->name) or !isset($data->component) or !isset($data->version)) {
+            return false;
+        }
+
+        $data->name = s(strip_tags($data->name));
+
+        if ($data->component !== clean_param($data->component, PARAM_COMPONENT)) {
+            return false;
+        }
+
+        list($plugintype, $pluginname) = normalize_component($data->component);
+
+        if ($plugintype === 'core') {
+            return false;
+        }
+
+        if ($data->component !== $plugintype.'_'.$pluginname) {
+            return false;
+        }
+
+        // Keep this regex in sync with the one used by the download.moodle.org/api/x.y/pluginfo.php
+        if (!preg_match('/^[0-9]+$/', $data->version)) {
+            return false;
+        }
+
+        return $data;
+    }
+}
+
+
+/**
+ * General exception thrown by {@link tool_installaddon_installer} class
+ *
+ * @copyright 2013 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_installaddon_installer_exception extends moodle_exception {
+
+    /**
+     * @param string $errorcode exception description identifier
+     * @param mixed $debuginfo debugging data to display
+     */
+    public function __construct($errorcode, $a=null, $debuginfo=null) {
+        parent::__construct($errorcode, 'tool_installaddon', '', $a, print_r($debuginfo, true));
+    }
+}
diff --git a/admin/tool/installaddon/classes/installfromzip_form.php b/admin/tool/installaddon/classes/installfromzip_form.php
new file mode 100644 (file)
index 0000000..18552fa
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * @package     tool_installaddon
+ * @subpackage  classes
+ * @category    form
+ * @copyright   2013 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Defines a simple form for uploading the add-on ZIP package
+ *
+ * @copyright 2013 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_installaddon_installfromzip extends moodleform {
+
+    /**
+     * Defines the form elements
+     */
+    public function definition() {
+
+        $mform = $this->_form;
+        $installer = $this->_customdata['installer'];
+
+        $mform->addElement('header', 'general', get_string('installfromzip', 'tool_installaddon'));
+        $mform->addHelpButton('general', 'installfromzip', 'tool_installaddon');
+
+        $options = $installer->get_plugin_types_menu();
+        $mform->addElement('select', 'plugintype', get_string('installfromziptype', 'tool_installaddon'), $options,
+            array('id' => 'tool_installaddon_installfromzip_plugintype'));
+        $mform->addHelpButton('plugintype', 'installfromziptype', 'tool_installaddon');
+        $mform->addRule('plugintype', null, 'required', null, 'client');
+
+        $mform->addElement('static', 'permcheck', '',
+            html_writer::span(get_string('permcheck', 'tool_installaddon'), '',
+                array('id' => 'tool_installaddon_installfromzip_permcheck')));
+
+        $mform->addElement('filepicker', 'zipfile', get_string('installfromzipfile', 'tool_installaddon'),
+            null, array('accepted_types' => '.zip'));
+        $mform->addHelpButton('zipfile', 'installfromzipfile', 'tool_installaddon');
+        $mform->addRule('zipfile', null, 'required', null, 'client');
+
+        $mform->addElement('text', 'rootdir', get_string('installfromziprootdir', 'tool_installaddon'));
+        $mform->addHelpButton('rootdir', 'installfromziprootdir', 'tool_installaddon');
+        $mform->setType('rootdir', PARAM_PLUGIN);
+        $mform->setAdvanced('rootdir');
+
+        $mform->addElement('checkbox', 'acknowledgement', get_string('acknowledgement', 'tool_installaddon'),
+            ' '.get_string('acknowledgementtext', 'tool_installaddon'));
+        $mform->addRule('acknowledgement', get_string('acknowledgementmust', 'tool_installaddon'), 'required', null, 'client');
+
+        $this->add_action_buttons(false, get_string('installfromzipsubmit', 'tool_installaddon'));
+    }
+
+    /**
+     * Validate the form fields
+     *
+     * @param array $data
+     * @param array $files
+     * @return array (string)field name => (string)validation error text
+     */
+    public function validation($data, $files) {
+
+        $installer = $this->_customdata['installer'];
+        $errors = parent::validation($data, $files);
+
+        if (!$installer->is_plugintype_writable($data['plugintype'])) {
+            $path = $installer->get_plugintype_root($data['plugintype']);
+            $errors['plugintype'] = get_string('permcheckresultno', 'tool_installaddon', array('path' => $path));
+        }
+
+        return $errors;
+    }
+}
diff --git a/admin/tool/installaddon/classes/pluginfo_client.php b/admin/tool/installaddon/classes/pluginfo_client.php
new file mode 100644 (file)
index 0000000..49cc334
--- /dev/null
@@ -0,0 +1,216 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Provides tool_installaddon_pluginfo_client and related classes
+ *
+ * @package     tool_installaddon
+ * @subpackage  classes
+ * @copyright   2013 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Implements a client for https://download.moodle.org/api/x.y/pluginfo.php service
+ *
+ * @copyright 2013 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_installaddon_pluginfo_client {
+
+    /**
+     * Factory method returning an instance of this class.
+     *
+     * @return tool_installaddon_pluginfo_client
+     */
+    public static function instance() {
+        return new static();
+    }
+
+    /**
+     * Return the information about the plugin
+     *
+     * @throws tool_installaddon_pluginfo_exception
+     * @param string $component
+     * @param string $version
+     * @return stdClass the pluginfo structure
+     */
+    public function get_pluginfo($component, $version) {
+
+        $response = $this->call_service($component, $version);
+        $response = $this->decode_response($response);
+        $this->validate_response($response);
+
+        return $response->pluginfo;
+    }
+
+    // End of external API /////////////////////////////////////////////////
+
+    /**
+     * @see self::instance()
+     */
+    protected function __construct() {
+    }
+
+    /**
+     * Calls the pluginfo.php service and returns the raw response
+     *
+     * @param string $component
+     * @param string $version
+     * @return string
+     */
+    protected function call_service($component, $version) {
+        global $CFG;
+        require_once($CFG->libdir.'/filelib.php');
+
+        $curl = new curl(array('proxy' => true));
+
+        $response = $curl->get(
+            $this->service_request_url(),
+            $this->service_request_params($component, $version),
+            $this->service_request_options());
+
+        $curlerrno = $curl->get_errno();
+        $curlinfo = $curl->get_info();
+
+        if (!empty($curlerrno)) {
+            throw new tool_installaddon_pluginfo_exception('err_curl_exec', array(
+                'url' => $curlinfo['url'], 'errno' => $curlerrno, 'error' => $curl->error));
+
+        } else if ($curlinfo['http_code'] != 200) {
+            throw new tool_installaddon_pluginfo_exception('err_curl_http_code', array(
+                'url' => $curlinfo['url'], 'http_code' => $curlinfo['http_code']));
+
+        } else if (isset($curlinfo['ssl_verify_result']) and $curlinfo['ssl_verify_result'] != 0) {
+            throw new tool_installaddon_pluginfo_exception('err_curl_ssl_verify', array(
+                'url' => $curlinfo['url'], 'ssl_verify_result' => $curlinfo['ssl_verify_result']));
+        }
+
+        return $response;
+    }
+
+    /**
+     * Return URL to the pluginfo.php service
+     *
+     * @return moodle_url
+     */
+    protected function service_request_url() {
+        global $CFG;
+
+        if (!empty($CFG->config_php_settings['alternativepluginfoserviceurl'])) {
+            $url = $CFG->config_php_settings['alternativepluginfoserviceurl'];
+        } else {
+            $url = 'https://download.moodle.org/api/1.2/pluginfo.php';
+        }
+
+        return new moodle_url($url);
+    }
+
+    /**
+     * Return list of pluginfo service parameters
+     *
+     * @param string $component
+     * @param string $version
+     * @return array
+     */
+    protected function service_request_params($component, $version) {
+
+        $params = array();
+        $params['format'] = 'json';
+        $params['plugin'] = $component.'@'.$version;
+
+        return $params;
+    }
+
+    /**
+     * Return cURL options for the service request
+     *
+     * @return array of (string)param => (string)value
+     */
+    protected function service_request_options() {
+        global $CFG;
+
+        $options = array(
+            'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
+            'CURLOPT_SSL_VERIFYPEER' => true,
+        );
+
+        $cacertfile = $CFG->dataroot.'/moodleorgca.crt';
+        if (is_readable($cacertfile)) {
+            // Do not use CA certs provided by the operating system. Instead,
+            // use this CA cert to verify the updates provider.
+            $options['CURLOPT_CAINFO'] = $cacertfile;
+        }
+
+        return $options;
+    }
+
+    /**
+     * Decode the raw service response
+     *
+     * @param string $raw
+     * @return stdClass
+     */
+    protected function decode_response($raw) {
+        return json_decode($raw);
+    }
+
+    /**
+     * Validate decoded service response
+     *
+     * @param stdClass $response
+     */
+    protected function validate_response($response) {
+
+        if (empty($response)) {
+            throw new tool_installaddon_pluginfo_exception('err_response_empty');
+        }
+
+        if (empty($response->status) or $response->status !== 'OK') {
+            throw new tool_installaddon_pluginfo_exception('err_response_status', $response->status);
+        }
+
+        if (empty($response->apiver) or $response->apiver !== '1.2') {
+            throw new tool_installaddon_pluginfo_exception('err_response_api_version', $response->apiver);
+        }
+
+        if (empty($response->pluginfo->component) or empty($response->pluginfo->downloadurl)
+                or empty($response->pluginfo->downloadmd5)) {
+            throw new tool_installaddon_pluginfo_exception('err_response_pluginfo');
+        }
+    }
+}
+
+
+/**
+ * General exception thrown by {@link tool_installaddon_pluginfo_client} class
+ *
+ * @copyright 2013 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_installaddon_pluginfo_exception extends moodle_exception {
+
+    /**
+     * @param string $errorcode exception description identifier
+     * @param mixed $debuginfo debugging data to display
+     */
+    public function __construct($errorcode, $a=null, $debuginfo=null) {
+        parent::__construct($errorcode, 'tool_installaddon', '', $a, print_r($debuginfo, true));
+    }
+}
diff --git a/admin/tool/installaddon/classes/validator.php b/admin/tool/installaddon/classes/validator.php
new file mode 100644 (file)
index 0000000..557fbc1
--- /dev/null
@@ -0,0 +1,575 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Provides validation class to check the plugin ZIP contents
+ *
+ * Uses fragments of the local_plugins_archive_validator class copyrighted by
+ * Marina Glancy that is part of the local_plugins plugin.
+ *
+ * @package     tool_installaddon
+ * @subpackage  classes
+ * @copyright   2013 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if (!defined('T_ML_COMMENT')) {
+   define('T_ML_COMMENT', T_COMMENT);
+} else {
+   define('T_DOC_COMMENT', T_ML_COMMENT);
+}
+
+/**
+ * Validates the contents of extracted plugin ZIP file
+ *
+ * @copyright 2013 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_installaddon_validator {
+
+    /** Critical error message level, causes the validation fail. */
+    const ERROR     = 'error';
+
+    /** Warning message level, validation does not fail but the admin should be always informed. */
+    const WARNING   = 'warning';
+
+    /** Information message level that the admin should be aware of. */
+    const INFO      = 'info';
+
+    /** Debugging message level, should be displayed in debugging mode only. */
+    const DEBUG     = 'debug';
+
+    /** @var string full path to the extracted ZIP contents */
+    protected $extractdir = null;
+
+    /** @var array as returned by {@link zip_packer::extract_to_pathname()} */
+    protected $extractfiles = null;
+
+    /** @var bool overall result of validation */
+    protected $result = null;
+
+    /** @var string the name of the plugin root directory */
+    protected $rootdir = null;
+
+    /** @var array explicit list of expected/required characteristics of the ZIP */
+    protected $assertions = null;
+
+    /** @var array of validation log messages */
+    protected $messages = array();
+
+    /** @var array|null array of relevant data obtained from version.php */
+    protected $versionphp = null;
+
+    /** @var string|null the name of found English language file without the .php extension */
+    protected $langfilename = null;
+
+    /** @var moodle_url|null URL to continue with the installation of validated add-on */
+    protected $continueurl = null;
+
+    /**
+     * Factory method returning instance of the validator
+     *
+     * @param string $zipcontentpath full path to the extracted ZIP contents
+     * @param array $zipcontentfiles (string)filerelpath => (bool|string)true or error
+     * @return tool_installaddon_validator
+     */
+    public static function instance($zipcontentpath, array $zipcontentfiles) {
+        return new static($zipcontentpath, $zipcontentfiles);
+    }
+
+    /**
+     * Set the expected plugin type, fail the validation otherwise
+     *
+     * @param string $required plugin type
+     */
+    public function assert_plugin_type($required) {
+        $this->assertions['plugintype'] = $required;
+    }
+
+    /**
+     * Set the expectation that the plugin can be installed into the given Moodle version
+     *
+     * @param string $required Moodle version we are about to install to
+     */
+    public function assert_moodle_version($required) {
+        $this->assertions['moodleversion'] = $required;
+    }
+
+    /**
+     * Execute the validation process against all explicit and implicit requirements
+     *
+     * Returns true if the validation passes (all explicit and implicit requirements
+     * pass) and the plugin can be installed. Returns false if the validation fails
+     * (some explicit or implicit requirement fails) and the plugin must not be
+     * installed.
+     *
+     * @return bool
+     */
+    public function execute() {
+
+        $this->result = (
+                $this->validate_files_layout()
+            and $this->validate_version_php()
+            and $this->validate_language_pack()
+            and $this->validate_target_location()
+        );
+
+        return $this->result;
+    }
+
+    /**
+     * Returns overall result of the validation.
+     *
+     * Null is returned if the validation has not been executed yet. Otherwise
+     * this method returns true (the installation can continue) or false (it is not
+     * safe to continue with the installation).
+     *
+     * @return bool|null
+     */
+    public function get_result() {
+        return $this->result;
+    }
+
+    /**
+     * Return the list of validation log messages
+     *
+     * Each validation message is a plain object with properties level, msgcode
+     * and addinfo.
+     *
+     * @return array of (int)index => (stdClass) validation message
+     */
+    public function get_messages() {
+        return $this->messages;
+    }
+
+    /**
+     * Return the information provided by the the plugin's version.php
+     *
+     * If version.php was not found in the plugin (which is tolerated for
+     * themes only at the moment), null is returned. Otherwise the array
+     * is returned. It may be empty if no information was parsed (which
+     * should not happen).
+     *
+     * @return null|array
+     */
+    public function get_versionphp_info() {
+        return $this->versionphp;
+    }
+
+    /**
+     * Returns the name of the English language file without the .php extension
+     *
+     * This can be used as a suggestion for fixing the plugin root directory in the
+     * ZIP file during the upload. If no file was found, or multiple PHP files are
+     * located in lang/en/ folder, then null is returned.
+     *
+     * @return null|string
+     */
+    public function get_language_file_name() {
+        return $this->langfilename;
+    }
+
+    /**
+     * Returns the rootdir of the extracted package (after eventual renaming)
+     *
+     * @return string|null
+     */
+    public function get_rootdir() {
+        return $this->rootdir;
+    }
+
+    /**
+     * Sets the URL to continue to after successful validation
+     *
+     * @param moodle_url $url
+     */
+    public function set_continue_url(moodle_url $url) {
+        $this->continueurl = $url;
+    }
+
+    /**
+     * Get the URL to continue to after successful validation
+     *
+     * Null is returned if the URL has not been explicitly set by the caller.
+     *
+     * @return moodle_url|null
+     */
+    public function get_continue_url() {
+        return $this->continueurl;
+    }
+
+    // End of external API /////////////////////////////////////////////////////
+
+    /**
+     * @param string $zipcontentpath full path to the extracted ZIP contents
+     * @param array $zipcontentfiles (string)filerelpath => (bool|string)true or error
+     */
+    protected function __construct($zipcontentpath, array $zipcontentfiles) {
+        $this->extractdir = $zipcontentpath;
+        $this->extractfiles = $zipcontentfiles;
+    }
+
+    // Validation methods //////////////////////////////////////////////////////
+
+    /**
+     * @return bool false if files in the ZIP do not have required layout
+     */
+    protected function validate_files_layout() {
+
+        if (!is_array($this->extractfiles) or count($this->extractfiles) < 4) {
+            // We need the English language pack with the name of the plugin at least
+            $this->add_message(self::ERROR, 'filesnumber');
+            return false;
+        }
+
+        foreach ($this->extractfiles as $filerelname => $filestatus) {
+            if ($filestatus !== true) {
+                $this->add_message(self::ERROR, 'filestatus', array('file' => $filerelname, 'status' => $filestatus));
+                return false;
+            }
+        }
+
+        foreach (array_keys($this->extractfiles) as $filerelname) {
+            if (!file_exists($this->extractdir.'/'.$filerelname)) {
+                $this->add_message(self::ERROR, 'filenotexists', array('file' => $filerelname));
+                return false;
+            }
+        }
+
+        foreach (array_keys($this->extractfiles) as $filerelname) {
+            $matches = array();
+            if (!preg_match("#^([^/]+)/#", $filerelname, $matches) or (!is_null($this->rootdir) and $this->rootdir !== $matches[1])) {
+                $this->add_message(self::ERROR, 'onedir');
+                return false;
+            }
+            $this->rootdir = $matches[1];
+        }
+
+        if ($this->rootdir !== clean_param($this->rootdir, PARAM_PLUGIN)) {
+            $this->add_message(self::ERROR, 'rootdirinvalid', $this->rootdir);
+            return false;
+        } else {
+            $this->add_message(self::INFO, 'rootdir', $this->rootdir);
+        }
+
+        return is_dir($this->extractdir.'/'.$this->rootdir);
+    }
+
+    /**
+     * @return bool false if the version.php file does not declare required information
+     */
+    protected function validate_version_php() {
+
+        if (!isset($this->assertions['plugintype'])) {
+            throw new coding_exception('Required plugin type must be set before calling this');
+        }
+
+        if (!isset($this->assertions['moodleversion'])) {
+            throw new coding_exception('Required Moodle version must be set before calling this');
+        }
+
+        $fullpath = $this->extractdir.'/'.$this->rootdir.'/version.php';
+
+        if (!file_exists($fullpath)) {
+            // This is tolerated for themes only.
+            if ($this->assertions['plugintype'] === 'theme') {
+                $this->add_message(self::DEBUG, 'missingversionphp');
+                return true;
+            } else {
+                $this->add_message(self::ERROR, 'missingversionphp');
+                return false;
+            }
+        }
+
+        $this->versionphp = array();
+        $info = $this->parse_version_php($fullpath);
+
+        if ($this->assertions['plugintype'] === 'mod') {
+            $type = 'module';
+        } else {
+            $type = 'plugin';
+        }
+
+        if (!isset($info[$type.'->version'])) {
+            if ($type === 'module' and isset($info['plugin->version'])) {
+                // Expect the activity module using $plugin in version.php instead of $module.
+                $type = 'plugin';
+                $this->versionphp['version'] = $info[$type.'->version'];
+                $this->add_message(self::INFO, 'pluginversion', $this->versionphp['version']);
+            } else {
+                $this->add_message(self::ERROR, 'missingversion');
+                return false;
+            }
+        } else {
+            $this->versionphp['version'] = $info[$type.'->version'];
+            $this->add_message(self::INFO, 'pluginversion', $this->versionphp['version']);
+        }
+
+        if (isset($info[$type.'->requires'])) {
+            $this->versionphp['requires'] = $info[$type.'->requires'];
+            if ($this->versionphp['requires'] > $this->assertions['moodleversion']) {
+                $this->add_message(self::ERROR, 'requiresmoodle', $this->versionphp['requires']);
+                return false;
+            }
+            $this->add_message(self::INFO, 'requiresmoodle', $this->versionphp['requires']);
+        }
+
+        if (isset($info[$type.'->component'])) {
+            $this->versionphp['component'] = $info[$type.'->component'];
+            list($reqtype, $reqname) = normalize_component($this->versionphp['component']);
+            if ($reqtype !== $this->assertions['plugintype']) {
+                $this->add_message(self::ERROR, 'componentmismatchtype', array(
+                    'expected' => $this->assertions['plugintype'],
+                    'found' => $reqtype));
+                return false;
+            }
+            if ($reqname !== $this->rootdir) {
+                $this->add_message(self::ERROR, 'componentmismatchname', $reqname);
+                return false;
+            }
+            $this->add_message(self::INFO, 'componentmatch', $this->versionphp['component']);
+        }
+
+        if (isset($info[$type.'->maturity'])) {
+            $this->versionphp['maturity'] = $info[$type.'->maturity'];
+            if ($this->versionphp['maturity'] === 'MATURITY_STABLE') {
+                $this->add_message(self::INFO, 'maturity', $this->versionphp['maturity']);
+            } else {
+                $this->add_message(self::WARNING, 'maturity', $this->versionphp['maturity']);
+            }
+        }
+
+        if (isset($info[$type.'->release'])) {
+            $this->versionphp['release'] = $info[$type.'->release'];
+            $this->add_message(self::INFO, 'release', $this->versionphp['release']);
+        }
+
+        return true;
+    }
+
+    /**
+     * @return bool false if the English language pack is not provided correctly
+     */
+    protected function validate_language_pack() {
+
+        if (!isset($this->assertions['plugintype'])) {
+            throw new coding_exception('Required plugin type must be set before calling this');
+        }
+
+        if (!isset($this->extractfiles[$this->rootdir.'/lang/en/'])
+                or $this->extractfiles[$this->rootdir.'/lang/en/'] !== true
+                or !is_dir($this->extractdir.'/'.$this->rootdir.'/lang/en')) {
+            $this->add_message(self::ERROR, 'missinglangenfolder');
+            return false;
+        }
+
+        $langfiles = array();
+        foreach (array_keys($this->extractfiles) as $extractfile) {
+            $matches = array();
+            if (preg_match('#^'.preg_quote($this->rootdir).'/lang/en/([^/]+).php?$#i', $extractfile, $matches)) {
+                $langfiles[] = $matches[1];
+            }
+        }
+
+        if (empty($langfiles)) {
+            $this->add_message(self::ERROR, 'missinglangenfile');
+            return false;
+        } else if (count($langfiles) > 1) {
+            $this->add_message(self::WARNING, 'multiplelangenfiles');
+        } else {
+            $this->langfilename = $langfiles[0];
+            $this->add_message(self::DEBUG, 'foundlangfile', $this->langfilename);
+        }
+
+        if ($this->assertions['plugintype'] === 'mod') {
+            $expected = $this->rootdir.'.php';
+        } else {
+            $expected = $this->assertions['plugintype'].'_'.$this->rootdir.'.php';
+        }
+
+        if (!isset($this->extractfiles[$this->rootdir.'/lang/en/'.$expected])
+                or $this->extractfiles[$this->rootdir.'/lang/en/'.$expected] !== true
+                or !is_file($this->extractdir.'/'.$this->rootdir.'/lang/en/'.$expected)) {
+            $this->add_message(self::ERROR, 'missingexpectedlangenfile', $expected);
+            return false;
+        }
+
+        return true;
+    }
+
+
+    /**
+     * @return bool false of the given add-on can't be installed into its location
+     */
+    public function validate_target_location() {
+
+        if (!isset($this->assertions['plugintype'])) {
+            throw new coding_exception('Required plugin type must be set before calling this');
+        }
+
+        $plugintypepath = $this->get_plugintype_location($this->assertions['plugintype']);
+
+        if (is_null($plugintypepath)) {
+            $this->add_message(self::ERROR, 'unknowntype', $this->assertions['plugintype']);
+            return false;
+        }
+
+        if (!is_dir($plugintypepath)) {
+            throw new coding_exception('Plugin type location does not exist!');
+        }
+
+        $target = $plugintypepath.'/'.$this->rootdir;
+
+        if (file_exists($target)) {
+            $this->add_message(self::ERROR, 'targetexists', $target);
+            return false;
+        }
+
+        if (is_writable($plugintypepath)) {
+            $this->add_message(self::INFO, 'pathwritable', $plugintypepath);
+        } else {
+            $this->add_message(self::ERROR, 'pathwritable', $plugintypepath);
+            return false;
+        }
+
+        return true;
+    }
+
+    // Helper methods //////////////////////////////////////////////////////////
+
+    /**
+     * Get as much information from existing version.php as possible
+     *
+     * @param string full path to the version.php file
+     * @return array of found meta-info declarations
+     */
+    protected function parse_version_php($fullpath) {
+
+        $content = $this->get_stripped_file_contents($fullpath);
+
+        preg_match_all('#\$((plugin|module)\->(version|maturity|release|requires))=()(\d+(\.\d+)?);#m', $content, $matches1);
+        preg_match_all('#\$((plugin|module)\->(maturity))=()(MATURITY_\w+);#m', $content, $matches2);
+        preg_match_all('#\$((plugin|module)\->(release))=([\'"])(.*?)\4;#m', $content, $matches3);
+        preg_match_all('#\$((plugin|module)\->(component))=([\'"])(.+?_.+?)\4;#m', $content, $matches4);
+
+        if (count($matches1[1]) + count($matches2[1]) + count($matches3[1]) + count($matches4[1])) {
+            $info = array_combine(
+                array_merge($matches1[1], $matches2[1], $matches3[1], $matches4[1]),
+                array_merge($matches1[5], $matches2[5], $matches3[5], $matches4[5])
+            );
+
+        } else {
+            $info = array();
+        }
+
+        return $info;
+    }
+
+    /**
+     * Append the given message to the messages log
+     *
+     * @param string $level e.g. self::ERROR
+     * @param string $msgcode may form a string
+     * @param string|array|object $a optional additional info suitable for {@link get_string()}
+     */
+    protected function add_message($level, $msgcode, $a = null) {
+        $msg = (object)array(
+            'level'     => $level,
+            'msgcode'   => $msgcode,
+            'addinfo'   => $a,
+        );
+        $this->messages[] = $msg;
+    }
+
+    /**
+     * Returns bare PHP code from the given file
+     *
+     * Returns contents without PHP opening and closing tags, text outside php code,
+     * comments and extra whitespaces.
+     *
+     * @param string $fullpath full path to the file
+     * @return string
+     */
+    protected function get_stripped_file_contents($fullpath) {
+
+        $source = file_get_contents($fullpath);
+        $tokens = token_get_all($source);
+        $output = '';
+        $doprocess = false;
+        foreach ($tokens as $token) {
+            if (is_string($token)) {
+                // Simple one character token.
+                $id = -1;
+                $text = $token;
+            } else {
+                // Token array.
+                list($id, $text) = $token;
+            }
+            switch ($id) {
+            case T_WHITESPACE:
+            case T_COMMENT:
+            case T_ML_COMMENT:
+            case T_DOC_COMMENT:
+                // Ignore whitespaces, inline comments, multiline comments and docblocks.
+                break;
+            case T_OPEN_TAG:
+                // Start processing.
+                $doprocess = true;
+                break;
+            case T_CLOSE_TAG:
+                // Stop processing.
+                $doprocess = false;
+                break;
+            default:
+                // Anything else is within PHP tags, return it as is.
+                if ($doprocess) {
+                    $output .= $text;
+                    if ($text === 'function') {
+                        // Explicitly keep the whitespace that would be ignored.
+                        $output .= ' ';
+                    }
+                }
+                break;
+            }
+        }
+
+        return $output;
+    }
+
+
+    /**
+     * Returns the full path to the root directory of the given plugin type
+     *
+     * @param string $plugintype
+     * @return string|null
+     */
+    public function get_plugintype_location($plugintype) {
+
+        $plugintypepath = null;
+
+        foreach (get_plugin_types() as $type => $fullpath) {
+            if ($type === $plugintype) {
+                $plugintypepath = $fullpath;
+                break;
+            }
+        }
+
+        return $plugintypepath;
+    }
+}
diff --git a/admin/tool/installaddon/deploy.php b/admin/tool/installaddon/deploy.php
new file mode 100644 (file)
index 0000000..932e9f2
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Deploy the validated contents of the ZIP package to the $CFG->dirroot
+ *
+ * @package     tool_installaddon
+ * @copyright   2013 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir.'/filelib.php');
+require_once(dirname(__FILE__).'/classes/installer.php');
+require_once(dirname(__FILE__).'/classes/validator.php');
+
+require_login();
+require_capability('moodle/site:config', context_system::instance());
+
+if (!empty($CFG->disableonclickaddoninstall)) {
+    notice(get_string('featuredisabled', 'tool_installaddon'));
+}
+
+require_sesskey();
+
+$jobid = required_param('jobid', PARAM_ALPHANUM);
+$plugintype = required_param('type', PARAM_ALPHANUMEXT);
+$pluginname = required_param('name', PARAM_PLUGIN);
+
+$zipcontentpath = $CFG->tempdir.'/tool_installaddon/'.$jobid.'/contents';
+
+if (!is_dir($zipcontentpath)) {
+    debugging('Invalid location of the extracted ZIP package: '.s($zipcontentpath), DEBUG_DEVELOPER);
+    redirect(new moodle_url('/admin/tool/installaddon/index.php'),
+        get_string('invaliddata', 'core_error'));
+}
+
+if (!is_dir($zipcontentpath.'/'.$pluginname)) {
+    debugging('Invalid location of the plugin root directory: '.$zipcontentpath.'/'.$pluginname, DEBUG_DEVELOPER);
+    redirect(new moodle_url('/admin/tool/installaddon/index.php'),
+        get_string('invaliddata', 'core_error'));
+}
+
+$installer = tool_installaddon_installer::instance();
+
+if (!$installer->is_plugintype_writable($plugintype)) {
+    debugging('Plugin type location not writable', DEBUG_DEVELOPER);
+    redirect(new moodle_url('/admin/tool/installaddon/index.php'),
+        get_string('invaliddata', 'core_error'));
+}
+
+$plugintypepath = $installer->get_plugintype_root($plugintype);
+
+if (file_exists($plugintypepath.'/'.$pluginname)) {
+    debugging('Target location already exists', DEBUG_DEVELOPER);
+    redirect(new moodle_url('/admin/tool/installaddon/index.php'),
+        get_string('invaliddata', 'core_error'));
+}
+
+rename($zipcontentpath.'/'.$pluginname, $plugintypepath.'/'.$pluginname);
+fulldelete($CFG->tempdir.'/tool_installaddon/'.$jobid);
+redirect(new moodle_url('/admin'));
diff --git a/admin/tool/installaddon/index.php b/admin/tool/installaddon/index.php
new file mode 100644 (file)
index 0000000..cb0d2a6
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * The main screen of the tool.
+ *
+ * @package     tool_installaddon
+ * @copyright   2013 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+require_once(dirname(__FILE__).'/classes/installer.php');
+
+admin_externalpage_setup('tool_installaddon_index');
+
+if (!empty($CFG->disableonclickaddoninstall)) {
+    notice(get_string('featuredisabled', 'tool_installaddon'));
+}
+
+$installer = tool_installaddon_installer::instance();
+
+$output = $PAGE->get_renderer('tool_installaddon');
+$output->set_installer_instance($installer);
+
+// Handle the eventual request for installing from remote repository.
+$remoterequest = optional_param('installaddonrequest', null, PARAM_RAW);
+$confirmed = optional_param('confirm', false, PARAM_BOOL);
+$installer->handle_remote_request($output, $remoterequest, $confirmed);
+
+$form = $installer->get_installfromzip_form();
+
+if ($form->is_cancelled()) {
+    redirect($PAGE->url);
+
+} else if ($data = $form->get_data()) {
+    // Save the ZIP file into a temporary location.
+    $jobid = md5(rand().uniqid('', true));
+    $sourcedir = make_temp_directory('tool_installaddon/'.$jobid.'/source');
+    $zipfilename = $installer->save_installfromzip_file($form, $sourcedir);
+    // Redirect to the validation page.
+    $nexturl = new moodle_url('/admin/tool/installaddon/validate.php', array(
+        'sesskey' => sesskey(),
+        'jobid' => $jobid,
+        'zip' => $zipfilename,
+        'type' => $data->plugintype,
+        'rootdir' => $data->rootdir));
+    redirect($nexturl);
+}
+
+// Output starts here.
+echo $output->index_page();
diff --git a/admin/tool/installaddon/lang/en/tool_installaddon.php b/admin/tool/installaddon/lang/en/tool_installaddon.php
new file mode 100644 (file)
index 0000000..bca494f
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for the tool_installaddon component.
+ *
+ * @package     tool_installaddon
+ * @category    string
+ * @copyright   2013 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['acknowledgement'] = 'Acknowledgement';
+$string['acknowledgementmust'] = 'You must acknowledge this';
+$string['acknowledgementtext'] = 'I understand that it is my responsibility to have full backups of this site prior to installing add-ons. I accept and understand that add-ons (especially but not only those originating in unofficial sources) may contain security holes, can make the site unavailable, or cause private data leaks or loss.';
+$string['featuredisabled'] = 'Add-on installer is disabled at this site.';
+$string['installaddon'] = 'Install add-on!';
+$string['installaddons'] = 'Install add-ons';
+$string['installexception'] = 'Oops... An error occured while trying to install the add-on. Turn debugging mode on to see more details about the error.';
+$string['installfromrepo'] = 'Install add-ons from Moodle plugins directory';
+$string['installfromrepo_help'] = 'You will be redirected to the Moodle plugins directory to search for and install an add-on. Note that your site fullname, URL and major version will be sent as well, to make the installation process easier for you.';
+$string['installfromzip'] = 'Install add-on from the ZIP file';
+$string['installfromzip_help'] = 'Alternatively to installing add-ons directly from the Moodle plugins directory, you can install add-ons from manually uploaded ZIP packages. Such ZIP packages are expected to have same structure as the ones available in the Moodle plugins directory.';
+$string['installfromzipfile'] = 'ZIP package';
+$string['installfromzipfile_help'] = 'The plugin ZIP package must contain just one directory with the name of the plugin. The ZIP will be extracted into the appropriate location for the given plugin type. Packages downloaded from the Moodle plugins directory have this format.';
+$string['installfromziprootdir'] = 'Rename the root directory';
+$string['installfromziprootdir_help'] = 'Some ZIP packages, such as those generated by Github, may contain incorrect name of the root directory. You can rename the root directory of the extracted package to the correct value defined in this field.';
+$string['installfromzipsubmit'] = 'Install add-on from the ZIP file';
+$string['installfromziptype'] = 'Plugin type';
+$string['installfromziptype_help'] = 'Choose the correct type of plugin you are about to install. The installation procedure may fail badly when incorrect plugin type is provided.';
+$string['permcheck'] = 'Make sure the plugin type root location is writable by the web server process';
+$string['permcheckerror'] = 'Error while checking for write permission';
+$string['permcheckprogress'] = 'Checking for write permission ...';
+$string['permcheckresultno'] = 'Plugin type location <em>{$a->path}</em> not writable';
+$string['permcheckresultyes'] = 'Plugin type location <em>{$a->path}</em> is writable';
+$string['pluginname'] = 'Add-on installer';
+$string['remoterequestalreadyinstalled'] = 'There is a request to install add-on {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory to this site. This plugin is <strong>already installed</strong> at this site.';
+$string['remoterequestconfirm'] = 'There is a request to install add-on <strong>{$a->name}</strong> ({$a->component}) version {$a->version} from the Moodle plugins directory to this site. If you continue, the add-on ZIP package will be downloaded for validation. Nothing will be installed yet.';
+$string['remoterequestinvalid'] = 'There is a request to install add-on from the Moodle plugins directory to this site. Unfortunately, the request is not valid. The add-on cannot be installed.';
+$string['remoterequestpermcheck'] = 'There is a request to install add-on {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory to this site. The plugin type location <strong>{$a->typepath}</strong> is <strong>not writable</strong> though. You need to give the write access for the web server user to the plugin type location now. Once the write access is granted, press the continue button to repeat the check.';
+$string['remoterequestpluginfoexception'] = 'Oops... An error occured while trying to obtain information about the add-on {$a->name} ({$a->component}) version {$a->version}. The add-on cannot be installed. Turn debugging mode on to see more details about the error.';
+$string['validation'] = 'Add-on package validation';
+$string['validationmsg_componentmatch'] = 'Full component name';
+$string['validationmsg_componentmismatchname'] = 'Add-on name mismatch';
+$string['validationmsg_componentmismatchname_help'] = 'Some ZIP packages, such as those generated by Github, may contain incorrect name of the root directory. You have to fix the name of the root directory to match the declared add-on name.';
+$string['validationmsg_componentmismatchname_info'] = 'The add-on declares its name is \'{$a}\' but that does not match the name of the root directory.';
+$string['validationmsg_componentmismatchtype'] = 'Add-on type mismatch';
+$string['validationmsg_componentmismatchtype_info'] = 'You have selected the type \'{$a->expected}\' but the add-on declares its type is \'{$a->found}\'.';
+$string['validationmsg_filenotexists'] = 'Extracted file not found';
+$string['validationmsg_filesnumber'] = 'Not enough files found in the package';
+$string['validationmsg_filestatus'] = 'Unable to extract all files';
+$string['validationmsg_filestatus_info'] = 'Attempting to extract file {$a->file} resulted in error \'{$a->status}\'.';
+$string['validationmsg_maturity'] = 'Declared maturity level';
+$string['validationmsg_maturity_help'] = 'The add-on can declare its maturity level. If the maintainer considers the add-on stable, the declared maturity level will read MATURITY_STABLE. All other maturity levels (such as alpha or beta) should be considered unstable and a warning is raised.';
+$string['validationmsg_missingexpectedlangenfile'] = 'English language file name mismatch';
+$string['validationmsg_missingexpectedlangenfile_info'] = 'The given add-on type would need to provide the English language file {$a}.';
+$string['validationmsg_missinglangenfile'] = 'No English language file found';
+$string['validationmsg_missinglangenfolder'] = 'Missing English language folder';
+$string['validationmsg_missingversion'] = 'Add-on does not declare its version';
+$string['validationmsg_missingversionphp'] = 'File version.php not found';
+$string['validationmsg_multiplelangenfiles'] = 'Multiple English language files found';
+$string['validationmsg_onedir'] = 'Invalid structure of the ZIP package.';
+$string['validationmsg_onedir_help'] = 'The ZIP package must contain just one root directory that holds the add-on code. The name of that root directory must match the name of the plugin.';
+$string['validationmsg_pathwritable'] = 'Write access check';
+$string['validationmsg_pluginversion'] = 'Add-on version';
+$string['validationmsg_release'] = 'Add-on release';
+$string['validationmsg_requiresmoodle'] = 'Required Moodle version';
+$string['validationmsg_rootdir'] = 'Name of the add-on to be installed';
+$string['validationmsg_rootdir_help'] = 'The name of the root directory in the ZIP package forms the name of the add-on to be installed. If the name is not correct, you may wish to rename the root directory in the ZIP prior to installing the add-on.';
+$string['validationmsg_rootdirinvalid'] = 'Invalid name of the add-on';
+$string['validationmsg_rootdirinvalid_help'] = 'The name of the root directory in the ZIP package violates formal syntax requirements. Some ZIP packages, such as those generated by Github, may contain incorrect name of the root directory. You have to fix the name of the root directory to match the add-on name.';
+$string['validationmsg_targetexists'] = 'Target location already exists';
+$string['validationmsg_targetexists_help'] = 'The directory that the add-on is to be installed to, must not exist yet.';
+$string['validationmsg_unknowntype'] = 'Unknown plugin type';
+$string['validationmsglevel_debug'] = 'Debug';
+$string['validationmsglevel_error'] = 'Error';
+$string['validationmsglevel_info'] = 'OK';
+$string['validationmsglevel_warning'] = 'Warning';
+$string['validationresult0'] = 'Validation failed!';
+$string['validationresult0_help'] = 'Some serious problem was detected. It is not safe to install the add-on. See the validation log messages for more details.';
+$string['validationresult1'] = 'Validation passed!';
+$string['validationresult1_help'] = 'No serious problems were detected. You can continue with the add-on installation. See the validation log messages for more details and eventual warnings.';
+$string['validationresult1_help'] = 'The add-on package has been validated and no serious problems were detected.';
+$string['validationresultinfo'] = 'Info';
+$string['validationresultmsg'] = 'Message';
+$string['validationresultstatus'] = 'Status';
diff --git a/admin/tool/installaddon/permcheck.php b/admin/tool/installaddon/permcheck.php
new file mode 100644 (file)
index 0000000..f38d109
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Checks the write permission for the given plugin type
+ *
+ * @package     tool_installaddon
+ * @subpackage  ajax
+ * @copyright   2013 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('AJAX_SCRIPT', true);
+
+require(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+require_once(dirname(__FILE__).'/classes/installer.php');
+
+require_login();
+
+if (!has_capability('moodle/site:config', context_system::instance())) {
+    header('HTTP/1.1 403 Forbidden');
+    die();
+}
+
+if (!empty($CFG->disableonclickaddoninstall)) {
+    header('HTTP/1.1 403 Forbidden');
+    die();
+}
+
+if (!confirm_sesskey()) {
+    header('HTTP/1.1 403 Forbidden');
+    die();
+}
+
+$plugintype = optional_param('plugintype', null, PARAM_ALPHANUMEXT);
+if (is_null($plugintype)) {
+    header('HTTP/1.1 400 Bad Request');
+    die();
+}
+
+$installer = tool_installaddon_installer::instance();
+
+$plugintypepath = $installer->get_plugintype_root($plugintype);
+
+if (empty($plugintypepath)) {
+    header('HTTP/1.1 400 Bad Request');
+    die();
+}
+
+$response = array('path' => $plugintypepath);
+
+if ($installer->is_plugintype_writable($plugintype)) {
+    $response['writable'] = 1;
+} else {
+    $response['writable'] = 0;
+}
+
+header('Content-Type: application/json; charset: utf-8');
+echo json_encode($response);
diff --git a/admin/tool/installaddon/pix/icon.png b/admin/tool/installaddon/pix/icon.png
new file mode 100644 (file)
index 0000000..f2d450b
Binary files /dev/null and b/admin/tool/installaddon/pix/icon.png differ
diff --git a/admin/tool/installaddon/pix/icon.svg b/admin/tool/installaddon/pix/icon.svg
new file mode 100644 (file)
index 0000000..19dead1
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->\r
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]>\r
+<svg version="1.1"\r
+        xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"\r
+        x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"\r
+        xml:space="preserve" preserveAspectRatio="xMinYMid meet">\r
+<defs>\r
+</defs>\r
+<path style="fill:#999999;" d="M16,9v6c0,0.5-0.5,1-1,1h-1H2H1c-0.5,0-1-0.5-1-1V9c0-0.5,0.5-1,1-1h1c0.5,0,1,0.5,1,1v4h10V9\r
+       c0-0.5,0.5-1,1-1h1C15.5,8,16,8.5,16,9z M12.4,5.1l-0.7-0.7c-0.4-0.4-1-0.4-1.4,0L9.5,5.2V1c0-0.5-0.4-1-1-1h-1c-0.5,0-1,0.5-1,1\r
+       v4.2L5.7,4.4C5.3,4,4.7,4,4.3,4.4L3.6,5.1c-0.4,0.4-0.4,1,0,1.4l3.7,3.7c0.2,0.2,0.5,0.3,0.7,0.3c0.3,0,0.5-0.1,0.7-0.3l3.7-3.7\r
+       C12.8,6.2,12.8,5.5,12.4,5.1z"/>\r
+</svg>\r
diff --git a/admin/tool/installaddon/renderer.php b/admin/tool/installaddon/renderer.php
new file mode 100644 (file)
index 0000000..c4c9706
--- /dev/null
@@ -0,0 +1,398 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Output rendering for the plugin.
+ *
+ * @package     tool_installaddon
+ * @category    output
+ * @copyright   2013 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Implements the plugin renderer
+ *
+ * @copyright 2013 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_installaddon_renderer extends plugin_renderer_base {
+
+    /** @var tool_installaddon_installer */
+    protected $installer = null;
+
+    /** @var tool_installaddon_validator */
+    protected $validator = null;
+
+    /**
+     * Sets the tool_installaddon_installer instance being used.
+     *
+     * @throws coding_exception if the installer has been already set
+     * @param tool_installaddon_installer $installer
+     */
+    public function set_installer_instance(tool_installaddon_installer $installer) {
+        if (is_null($this->installer)) {
+            $this->installer = $installer;
+        } else {
+            throw new coding_exception('Attempting to reset the installer instance.');
+        }
+    }
+
+    /**
+     * Sets the tool_installaddon_validator instance being used.
+     *
+     * @throws coding_exception if the validator has been already set
+     * @param tool_installaddon_validator $validator
+     */
+    public function set_validator_instance(tool_installaddon_validator $validator) {
+        if (is_null($this->validator)) {
+            $this->validator = $validator;
+        } else {
+            throw new coding_exception('Attempting to reset the validator instance.');
+        }
+    }
+
+    /**
+     * Defines the index page layout
+     *
+     * @return string
+     */
+    public function index_page() {
+
+        if (is_null($this->installer)) {
+            throw new coding_exception('Installer instance has not been set.');
+        }
+
+        $permcheckurl = new moodle_url('/admin/tool/installaddon/permcheck.php');
+        $this->page->requires->yui_module('moodle-tool_installaddon-permcheck', 'M.tool_installaddon.permcheck.init',
+            array(array('permcheckurl' => $permcheckurl->out())));
+        $this->page->requires->strings_for_js(
+            array('permcheckprogress', 'permcheckresultno', 'permcheckresultyes', 'permcheckerror'), 'tool_installaddon');
+
+        $out = $this->output->header();
+        $out .= $this->index_page_heading();
+        $out .= $this->index_page_repository();
+        $out .= $this->index_page_upload();
+        $out .= $this->output->footer();
+
+        return $out;
+    }
+
+    /**
+     * Defines the validation results page layout
+     *
+     * @return string
+     */
+    public function validation_page() {
+
+        if (is_null($this->installer)) {
+            throw new coding_exception('Installer instance has not been set.');
+        }
+
+        if (is_null($this->validator)) {
+            throw new coding_exception('Validator instance has not been set.');
+        }
+
+        $out = $this->output->header();
+        $out .= $this->validation_page_heading();
+        $out .= $this->validation_page_messages();
+        $out .= $this->validation_page_continue();
+        $out .= $this->output->footer();
+
+        return $out;
+    }
+
+    /**
+     * Inform the user about invalid remote installation request.
+     *
+     * @param moodle_url $continueurl
+     * @return string
+     */
+    public function remote_request_invalid_page(moodle_url $continueurl) {
+
+        $out = $this->output->header();
+        $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon'));
+        $out .= $this->output->box(get_string('remoterequestinvalid', 'tool_installaddon'), 'generalbox', 'notice');
+        $out .= $this->output->continue_button($continueurl, 'get');
+        $out .= $this->output->footer();
+
+        return $out;
+    }
+
+    /**
+     * Inform the user that such plugin is already installed
+     *
+     * @param stdClass $data decoded request data
+     * @param moodle_url $continueurl
+     * @return string
+     */
+    public function remote_request_alreadyinstalled_page(stdClass $data, moodle_url $continueurl) {
+
+        $out = $this->output->header();
+        $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon'));
+        $out .= $this->output->box(get_string('remoterequestalreadyinstalled', 'tool_installaddon', $data), 'generalbox', 'notice');
+        $out .= $this->output->continue_button($continueurl, 'get');
+        $out .= $this->output->footer();
+
+        return $out;
+    }
+
+    /**
+     * Let the user confirm the remote installation request.
+     *
+     * @param stdClass $data decoded request data
+     * @param moodle_url $continueurl
+     * @param moodle_url $cancelurl
+     * @return string
+     */
+    public function remote_request_confirm_page(stdClass $data, moodle_url $continueurl, moodle_url $cancelurl) {
+
+        $out = $this->output->header();
+        $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon'));
+        $out .= $this->output->confirm(get_string('remoterequestconfirm', 'tool_installaddon', $data), $continueurl, $cancelurl);
+        $out .= $this->output->footer();
+
+        return $out;
+    }
+
+    /**
+     * Inform the user that the target plugin type location is not writable.
+     *
+     * @param stdClass $data decoded request data
+     * @param string $plugintypepath full path to the plugin type location
+     * @param moodle_url $continueurl to repeat the write permission check
+     * @param moodle_url $cancelurl to cancel the installation
+     * @return string
+     */
+    public function remote_request_permcheck_page(stdClass $data, $plugintypepath, moodle_url $continueurl, moodle_url $cancelurl) {
+
+        $data->typepath = $plugintypepath;
+
+        $out = $this->output->header();
+        $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon'));
+        $out .= $this->output->confirm(get_string('remoterequestpermcheck', 'tool_installaddon', $data), $continueurl, $cancelurl);
+        $out .= $this->output->footer();
+
+        return $out;
+    }
+
+    /**
+     * Inform the user about pluginfo service call exception
+     *
+     * This implementation does not actually use the passed exception. Custom renderers might want to
+     * display additional data obtained via {@link get_exception_info()}. Also note, this method is called
+     * in non-debugging mode only. If debugging is allowed at the site, default exception handler is triggered.
+     *
+     * @param stdClass $data decoded request data
+     * @param tool_installaddon_pluginfo_exception $e thrown exception
+     * @param moodle_url $continueurl
+     * @return string
+     */
+    public function remote_request_pluginfo_exception(stdClass $data, tool_installaddon_pluginfo_exception $e, moodle_url $continueurl) {
+
+        $out = $this->output->header();
+        $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon'));
+        $out .= $this->output->box(get_string('remoterequestpluginfoexception', 'tool_installaddon', $data), 'generalbox', 'notice');
+        $out .= $this->output->continue_button($continueurl, 'get');
+        $out .= $this->output->footer();
+
+        return $out;
+    }
+
+    /**
+     * Inform the user about the installer exception
+     *
+     * This implementation does not actually use the passed exception. Custom renderers might want to
+     * display additional data obtained via {@link get_exception_info()}. Also note, this method is called
+     * in non-debugging mode only. If debugging is allowed at the site, default exception handler is triggered.
+     *
+     * @param tool_installaddon_installer_exception $e thrown exception
+     * @param moodle_url $continueurl
+     * @return string
+     */
+    public function installer_exception(tool_installaddon_installer_exception $e, moodle_url $continueurl) {
+
+        $out = $this->output->header();
+        $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon'));
+        $out .= $this->output->box(get_string('installexception', 'tool_installaddon'), 'generalbox', 'notice');
+        $out .= $this->output->continue_button($continueurl, 'get');
+        $out .= $this->output->footer();
+
+        return $out;
+    }
+
+    // End of the external API /////////////////////////////////////////////////
+
+    /**
+     * Renders the index page heading
+     *
+     * @return string
+     */
+    protected function index_page_heading() {
+        return $this->output->heading(get_string('pluginname', 'tool_installaddon'));
+    }
+
+    /**
+     * Renders the widget for browsing the add-on repository
+     *
+     * @return string
+     */
+    protected function index_page_repository() {
+
+        $url = $this->installer->get_addons_repository_url();
+
+        $out = $this->box(
+            $this->output->single_button($url, get_string('installfromrepo', 'tool_installaddon'), 'get').
+            $this->output->help_icon('installfromrepo', 'tool_installaddon'),
+            'generalbox', 'installfromrepobox'
+        );
+
+        return $out;
+    }
+
+    /**
+     * Renders the widget for uploading the add-on ZIP package
+     *
+     * @return string
+     */
+    protected function index_page_upload() {
+
+        $form = $this->installer->get_installfromzip_form();
+
+        ob_start();
+        $form->display();
+        $out = ob_get_clean();
+
+        $out = $this->box($out, 'generalbox', 'installfromzipbox');
+
+        return $out;
+    }
+
+    /**
+     * Renders the page title and the overall validation verdict
+     *
+     * @return string
+     */
+    protected function validation_page_heading() {
+
+        $heading = $this->output->heading(get_string('validation', 'tool_installaddon'));
+
+        if ($this->validator->get_result()) {
+            $status = $this->output->container(
+                html_writer::span(get_string('validationresult1', 'tool_installaddon'), 'verdict').
+                    $this->output->help_icon('validationresult1', 'tool_installaddon'),
+                array('validationresult', 'success')
+            );
+        } else {
+            $status = $this->output->container(
+                html_writer::span(get_string('validationresult0', 'tool_installaddon'), 'verdict').
+                    $this->output->help_icon('validationresult0', 'tool_installaddon'),
+                array('validationresult', 'failure')
+            );
+        }
+
+        return $heading . $status;
+    }
+
+    /**
+     * Renders validation log messages.
+     *
+     * @return string
+     */
+    protected function validation_page_messages() {
+
+        $validator = $this->validator; // We need this to be able to use their constants.
+        $messages = $validator->get_messages();
+
+        if (empty($messages)) {
+            return '';
+        }
+
+        $table = new html_table();
+        $table->attributes['class'] = 'validationmessages generaltable';
+        $table->head = array(
+            get_string('validationresultstatus', 'tool_installaddon'),
+            get_string('validationresultmsg', 'tool_installaddon'),
+            get_string('validationresultinfo', 'tool_installaddon')
+        );
+        $table->colclasses = array('msgstatus', 'msgtext', 'msginfo');
+
+        $stringman = get_string_manager();
+
+        foreach ($messages as $message) {
+
+            if ($message->level === $validator::DEBUG and !debugging()) {
+                continue;
+            }
+
+            $msgstatus = get_string('validationmsglevel_'.$message->level, 'tool_installaddon');
+            $msgtext = $msgtext = s($message->msgcode);
+            if (is_null($message->addinfo)) {
+                $msginfo = '';
+            } else {
+                $msginfo = html_writer::tag('pre', s(print_r($message->addinfo, true)));
+            }
+            $msghelp = '';
+
+            // Replace the message code with the string if it is defined.
+            if ($stringman->string_exists('validationmsg_'.$message->msgcode, 'tool_installaddon')) {
+                $msgtext = get_string('validationmsg_'.$message->msgcode, 'tool_installaddon');
+                // And check for the eventual help, too.
+                if ($stringman->string_exists('validationmsg_'.$message->msgcode.'_help', 'tool_installaddon')) {
+                    $msghelp = $this->output->help_icon('validationmsg_'.$message->msgcode, 'tool_installaddon');
+                }
+            }
+
+            // Re-format the message info using a string if it is define.
+            if (!is_null($message->addinfo) and $stringman->string_exists('validationmsg_'.$message->msgcode.'_info', 'tool_installaddon')) {
+                $msginfo = get_string('validationmsg_'.$message->msgcode.'_info', 'tool_installaddon', $message->addinfo);
+            }
+
+            $row = new html_table_row(array($msgstatus, $msgtext.$msghelp, $msginfo));
+            $row->attributes['class'] = 'level-'.$message->level.' '.$message->msgcode;
+
+            $table->data[] = $row;
+        }
+
+        return html_writer::table($table);
+    }
+
+    /**
+     * Renders widgets to continue from the validation results page
+     *
+     * @return string
+     */
+    protected function validation_page_continue() {
+
+        $conturl = $this->validator->get_continue_url();
+        if (is_null($conturl)) {
+            $contbutton = '';
+        } else {
+            $contbutton = $this->output->single_button(
+                $conturl, get_string('installaddon', 'tool_installaddon'), 'post',
+                array('class' => 'singlebutton continuebutton'));
+        }
+
+        $cancelbutton = $this->output->single_button(
+            new moodle_url('/admin/tool/installaddon/index.php'), get_string('cancel', 'core'), 'get',
+            array('class' => 'singlebutton cancelbutton'));
+
+        return $this->output->container($cancelbutton.$contbutton, 'postvalidationbuttons');
+    }
+}
diff --git a/admin/tool/installaddon/settings.php b/admin/tool/installaddon/settings.php
new file mode 100644 (file)
index 0000000..b533360
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Puts the plugin actions into the admin settings tree.
+ *
+ * @package     tool_installaddon
+ * @copyright   2013 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if ($hassiteconfig and empty($CFG->disableonclickaddoninstall)) {
+
+    $ADMIN->add('modules', new admin_externalpage('tool_installaddon_index',
+        get_string('installaddons', 'tool_installaddon'),
+        "$CFG->wwwroot/$CFG->admin/tool/installaddon/index.php"), 'modsettings');
+
+    $ADMIN->add('modules', new admin_externalpage('tool_installaddon_validate',
+        get_string('validation', 'tool_installaddon'),
+        "$CFG->wwwroot/$CFG->admin/tool/installaddon/validate.php",
+        'moodle/site:config',
+        true), 'modsettings');
+}
diff --git a/admin/tool/installaddon/styles.css b/admin/tool/installaddon/styles.css
new file mode 100644 (file)
index 0000000..05155ca
--- /dev/null
@@ -0,0 +1,68 @@
+#page-admin-tool-installaddon-index #installfromrepobox {
+    text-align: center;
+    padding-top: 2em;
+    padding-bottom: 2em;
+}
+
+#page-admin-tool-installaddon-index #installfromrepobox .singlebutton {
+    display: inline-block;
+}
+
+#page-admin-tool-installaddon-index #installfromrepobox .singlebutton input[type=submit] {
+    padding: 1em;
+}
+
+#page-admin-tool-installaddon-validate .validationresult {
+    margin: 2em auto;
+    text-align: center;
+}
+
+#page-admin-tool-installaddon-validate .validationresult .verdict {
+    margin: 0em 0.5em;
+    padding: 0.5em;
+    border: 2px solid;
+    -webkit-border-radius: 5px;
+    -moz-border-radius: 5px;
+    border-radius: 5px;
+    font-weight: bold;
+}
+
+#page-admin-tool-installaddon-validate .validationresult.success .verdict {
+    background-color: #e7f1c3;
+    border-color: #aaeeaa;
+}
+
+#page-admin-tool-installaddon-validate .validationresult.failure .verdict {
+    background-color: #ffd3d9;
+    border-color: #eeaaaa;
+}
+
+#page-admin-tool-installaddon-validate .validationmessages {
+    margin: 0px auto;
+}
+
+#page-admin-tool-installaddon-validate .validationmessages .level-error .msgstatus {
+    background-color: #ffd3d9;
+}
+
+#page-admin-tool-installaddon-validate .validationmessages .level-warning .msgstatus {
+    background-color: #f3f2aa;
+}
+
+#page-admin-tool-installaddon-validate .validationmessages .level-info .msgstatus {
+    background-color: #e7f1c3;
+}
+
+#page-admin-tool-installaddon-validate .validationmessages .level-debug .msgstatus {
+    background-color: #d2ebff;
+}
+
+#page-admin-tool-installaddon-validate .postvalidationbuttons {
+    text-align: center;
+    margin: 1em auto;
+}
+
+#page-admin-tool-installaddon-validate .postvalidationbuttons .singlebutton {
+    display: inline-block;
+    margin: 1em 1em;
+}
diff --git a/admin/tool/installaddon/tests/fixtures/emptydir/emptydir/README.txt b/admin/tool/installaddon/tests/fixtures/emptydir/emptydir/README.txt
new file mode 100644 (file)
index 0000000..158d760
--- /dev/null
@@ -0,0 +1 @@
+Plugin must have more than one file.
diff --git a/admin/tool/installaddon/tests/fixtures/github/moodle-repository_mahara-master/lang/en/repository_mahara.php b/admin/tool/installaddon/tests/fixtures/github/moodle-repository_mahara-master/lang/en/repository_mahara.php
new file mode 100644 (file)
index 0000000..b3d9bbc
--- /dev/null
@@ -0,0 +1 @@
+<?php
diff --git a/admin/tool/installaddon/tests/fixtures/github/moodle-repository_mahara-master/version.php b/admin/tool/installaddon/tests/fixtures/github/moodle-repository_mahara-master/version.php
new file mode 100644 (file)
index 0000000..c2ce9d0
--- /dev/null
@@ -0,0 +1,4 @@
+<?php
+
+$plugin->component = 'repository_mahara';
+$plugin->version = 2014010100;
diff --git a/admin/tool/installaddon/tests/fixtures/installed/greenbar/index.php b/admin/tool/installaddon/tests/fixtures/installed/greenbar/index.php
new file mode 100644 (file)
index 0000000..c65b6f7
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+echo 'One, my little hobbit, never installs malicisous add-ons';
diff --git a/admin/tool/installaddon/tests/fixtures/installed/greenbar/lang/en/local_greenbar.php b/admin/tool/installaddon/tests/fixtures/installed/greenbar/lang/en/local_greenbar.php
new file mode 100644 (file)
index 0000000..e11876b
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+$string['pluginname'] = 'Foo bar!';
diff --git a/admin/tool/installaddon/tests/fixtures/installed/greenbar/version.php b/admin/tool/installaddon/tests/fixtures/installed/greenbar/version.php
new file mode 100644 (file)
index 0000000..8b91a85
--- /dev/null
@@ -0,0 +1,4 @@
+<?php
+
+$plugin->version = 2013031900;
+$plugin->component = 'local_greenbar';
diff --git a/admin/tool/installaddon/tests/fixtures/multidir/one/version.php b/admin/tool/installaddon/tests/fixtures/multidir/one/version.php
new file mode 100644 (file)
index 0000000..84af5f2
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+$plugin->component = 'local_one';
diff --git a/admin/tool/installaddon/tests/fixtures/multidir/two/README.txt b/admin/tool/installaddon/tests/fixtures/multidir/two/README.txt
new file mode 100644 (file)
index 0000000..84e1459
--- /dev/null
@@ -0,0 +1 @@
+Only one dir is allowed
diff --git a/admin/tool/installaddon/tests/fixtures/nolang/bah/index.php b/admin/tool/installaddon/tests/fixtures/nolang/bah/index.php
new file mode 100644 (file)
index 0000000..b3d9bbc
--- /dev/null
@@ -0,0 +1 @@
+<?php
diff --git a/admin/tool/installaddon/tests/fixtures/nolang/bah/lang/en/bah.php b/admin/tool/installaddon/tests/fixtures/nolang/bah/lang/en/bah.php
new file mode 100644 (file)
index 0000000..b3d9bbc
--- /dev/null
@@ -0,0 +1 @@
+<?php
diff --git a/admin/tool/installaddon/tests/fixtures/nolang/bah/lang/en/bleh.php b/admin/tool/installaddon/tests/fixtures/nolang/bah/lang/en/bleh.php
new file mode 100644 (file)
index 0000000..4f3b6fc
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+$string['pluginnname'] = 'Root directory mismatch';
diff --git a/admin/tool/installaddon/tests/fixtures/nolang/bah/lib.php b/admin/tool/installaddon/tests/fixtures/nolang/bah/lib.php
new file mode 100644 (file)
index 0000000..50cce95
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+
diff --git a/admin/tool/installaddon/tests/fixtures/nolang/bah/version.php b/admin/tool/installaddon/tests/fixtures/nolang/bah/version.php
new file mode 100644 (file)
index 0000000..6c648f9
--- /dev/null
@@ -0,0 +1,4 @@
+<?php
+
+$module->version = 2014122455;
+$plugin->version = 2014122455;
diff --git a/admin/tool/installaddon/tests/fixtures/nolang/bah/view.php b/admin/tool/installaddon/tests/fixtures/nolang/bah/view.php
new file mode 100644 (file)
index 0000000..654f070
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+echo 'Do not use hardcoded strings, provide the language pack';
diff --git a/admin/tool/installaddon/tests/fixtures/noversionmod/noversion/lang/en/noversion.php b/admin/tool/installaddon/tests/fixtures/noversionmod/noversion/lang/en/noversion.php
new file mode 100644 (file)
index 0000000..82700cb
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+$string['pluginversion'] = 'Activity module with no version.php';
diff --git a/admin/tool/installaddon/tests/fixtures/noversiontheme/noversion/lang/en/theme_noversion.php b/admin/tool/installaddon/tests/fixtures/noversiontheme/noversion/lang/en/theme_noversion.php
new file mode 100644 (file)
index 0000000..78436a1
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+$string['pluginversion'] = 'A theme with no version.php';
diff --git a/admin/tool/installaddon/tests/fixtures/nowrapdir/index.php b/admin/tool/installaddon/tests/fixtures/nowrapdir/index.php
new file mode 100644 (file)
index 0000000..a37d47e
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+// index.php
diff --git a/admin/tool/installaddon/tests/fixtures/nowrapdir/lang/en/foo.php b/admin/tool/installaddon/tests/fixtures/nowrapdir/lang/en/foo.php
new file mode 100644 (file)
index 0000000..0b2f728
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+$string['pluginname'] = 'Foo!';
diff --git a/admin/tool/installaddon/tests/fixtures/nowrapdir/version.php b/admin/tool/installaddon/tests/fixtures/nowrapdir/version.php
new file mode 100644 (file)
index 0000000..d31292c
--- /dev/null
@@ -0,0 +1,3 @@
+<?php // $Id$
+
+// I don't miss CVS, do you?
diff --git a/admin/tool/installaddon/tests/fixtures/plugindir/foobar/index.php b/admin/tool/installaddon/tests/fixtures/plugindir/foobar/index.php
new file mode 100644 (file)
index 0000000..c65b6f7
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+echo 'One, my little hobbit, never installs malicisous add-ons';
diff --git a/admin/tool/installaddon/tests/fixtures/plugindir/foobar/lang/en/local_foobar.php b/admin/tool/installaddon/tests/fixtures/plugindir/foobar/lang/en/local_foobar.php
new file mode 100644 (file)
index 0000000..e11876b
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+$string['pluginname'] = 'Foo bar!';
diff --git a/admin/tool/installaddon/tests/fixtures/plugindir/foobar/version.php b/admin/tool/installaddon/tests/fixtures/plugindir/foobar/version.php
new file mode 100644 (file)
index 0000000..ebad339
--- /dev/null
@@ -0,0 +1,9 @@
+<?php
+
+$module->version = 10; // Ignored, this should use $plugin
+$plugin->version = 2013031900;
+$plugin->component = 'local_foobar';
+$plugin->requires = 2013031200;
+$module->release = 'We are not an activity module!';
+$plugin->maturity = MATURITY_ALPHA;
+//$plugin->release = 'And this is commented';
diff --git a/admin/tool/installaddon/tests/fixtures/versionphp/version1.php b/admin/tool/installaddon/tests/fixtures/versionphp/version1.php
new file mode 100644 (file)
index 0000000..61646f0
--- /dev/null
@@ -0,0 +1,52 @@
+<h1>Example version.php file</h1>
+
+<p>version.php is required for all plugins but themes.</p>
+
+<h2>Example of values</h2>
+
+<pre>
+    $plugin->version = 2011051000;
+    $plugin->requires = 2010112400;
+    $plugin->cron = 0;
+    $plugin->component = 'plugintype_pluginname';
+    $plugin->maturity = MATURITY_STABLE;
+    $plugin->release = '2.x (Build: 2011051000)';
+    $plugin->dependencies = array('mod_forum' => ANY_VERSION, 'mod_data' => 2010020300);
+</pre>
+
+Replace $plugin with $module for activity modules, as in
+
+<pre>
+    $module->version = 2012122400;
+</pre><?php // $Id$ $module->version = 1;
+
+    $plugin->component
+        = 'old_foobar';//$plugin->component='commented';
+
+    $plugin->component      =   
+        'block_foobar';
+    
+$plugin->version = 2013010100;
+ ////////$plugin->version = 0;
+    /* for activity
+       modules use:
+    $module->version = 2014131300;
+
+    ***/
+$plugin->version = "2010091855";        // Do not use quotes here.
+$plugin->version = '2010091856.9'; // Do not use quotes here.
+
+
+$plugin->requires = /* 2012010100  */ 2012122401  ;
+
+$module->maturity = MATURITY_STABLE;
+$module->maturity = 50; // If both present, the constant wins (on contrary to what PHP would do)
+$module->maturity = 'MATURITY_BETA'; // Do not use quotes here.
+
+$plugin->maturity = 10;
+$plugin->maturity = MATURITY_ALPHA;
+
+
+
+$module->release = 2.3;         $plugin->release  = 'v2.4';
+$module->release = "v2.3";      $plugin->release    = 2.4;
diff --git a/admin/tool/installaddon/tests/fixtures/zips/invalidroot.zip b/admin/tool/installaddon/tests/fixtures/zips/invalidroot.zip
new file mode 100644 (file)
index 0000000..a209025
Binary files /dev/null and b/admin/tool/installaddon/tests/fixtures/zips/invalidroot.zip differ
diff --git a/admin/tool/installaddon/tests/installer_test.php b/admin/tool/installaddon/tests/installer_test.php
new file mode 100644 (file)
index 0000000..fd8d1d1
--- /dev/null
@@ -0,0 +1,158 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Provides the unit tests class and some helper classes
+ *
+ * @package     tool_installaddon
+ * @category    test
+ * @copyright   2013 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/installaddon/classes/installer.php');
+
+
+/**
+ * Unit tests for the {@link tool_installaddon_installer} class
+ *
+ * @copyright 2013 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_installaddon_installer_test extends advanced_testcase {
+
+    public function test_get_addons_repository_url() {
+        $installer = testable_tool_installaddon_installer::instance();
+        $url = $installer->get_addons_repository_url();
+        $query = parse_url($url, PHP_URL_QUERY);
+        $this->assertEquals(1, preg_match('~^site=(.+)$~', $query, $matches));
+        $site = rawurldecode($matches[1]);
+        $site = json_decode(base64_decode($site), true);
+        $this->assertEquals('array', gettype($site));
+        $this->assertEquals(3, count($site));
+        $this->assertSame('Nasty site', $site['fullname']);
+        $this->assertSame('file:///etc/passwd', $site['url']);
+        $this->assertSame("2.5'; DROP TABLE mdl_user; --", $site['majorversion']);
+    }
+
+    public function test_extract_installfromzip_file() {
+        $jobid = md5(rand().uniqid('test_', true));
+        $sourcedir = make_temp_directory('tool_installaddon/'.$jobid.'/source');
+        $contentsdir = make_temp_directory('tool_installaddon/'.$jobid.'/contents');
+        copy(dirname(__FILE__).'/fixtures/zips/invalidroot.zip', $sourcedir.'/testinvalidroot.zip');
+
+        $installer = tool_installaddon_installer::instance();
+        $files = $installer->extract_installfromzip_file($sourcedir.'/testinvalidroot.zip', $contentsdir, 'fixed_root');
+        $this->assertEquals('array', gettype($files));
+        $this->assertEquals(4, count($files));
+        $this->assertSame(true, $files['fixed_root/']);
+        $this->assertSame(true, $files['fixed_root/lang/']);
+        $this->assertSame(true, $files['fixed_root/lang/en/']);
+        $this->assertSame(true, $files['fixed_root/lang/en/fixed_root.php']);
+        foreach ($files as $file => $status) {
+            if (substr($file, -1) === '/') {
+                $this->assertTrue(is_dir($contentsdir.'/'.$file));
+            } else {
+                $this->assertTrue(is_file($contentsdir.'/'.$file));
+            }
+        }
+    }
+
+    public function test_decode_remote_request() {
+        $installer = testable_tool_installaddon_installer::instance();
+
+        $request = base64_encode(json_encode(array(
+            'name' => '<h1>Stamp collection</h1>"; DELETE FROM mdl_users; --',
+            'component' => 'mod_stampcoll',
+            'version' => 2013032800,
+        )));
+        $request = $installer->testable_decode_remote_request($request);
+        $this->assertTrue(is_object($request));
+        // One, my little hobbit, never trusts the input parameters!
+        $this->assertEquals('Stamp collection&quot;; DELETE FROM mdl_users; --', $request->name);
+        $this->assertEquals('mod_stampcoll', $request->component);
+        $this->assertEquals(2013032800, $request->version);
+
+        $request = base64_encode(json_encode(array(
+            'name' => 'Theme with invalid version number',
+            'component' => 'theme_invalid',
+            'version' => '1.0',
+        )));
+        $this->assertSame(false, $installer->testable_decode_remote_request($request));
+
+        $request = base64_encode(json_encode(array(
+            'name' => 'Invalid activity name',
+            'component' => 'mod_invalid_activity',
+            'version' => 2013032800,
+        )));
+        $this->assertSame(false, $installer->testable_decode_remote_request($request));
+
+        $request = base64_encode(json_encode(array(
+            'name' => 'Moodle 3.0',
+            'component' => 'core',
+            'version' => 2022010100,
+        )));
+        $this->assertSame(false, $installer->testable_decode_remote_request($request));
+
+        $request = base64_encode(json_encode(array(
+            'name' => 'Invalid core subsystem',
+            'component' => 'core_cache',
+            'version' => 2014123400,
+        )));
+        $this->assertSame(false, $installer->testable_decode_remote_request($request));
+
+        $request = base64_encode(json_encode(array(
+            'name' => 'Non-existing plugintype',
+            'component' => 'david_mudrak',
+            'version' => 2012123199,
+        )));
+        $this->assertSame(false, $installer->testable_decode_remote_request($request));
+    }
+}
+
+
+/**
+ * Testable subclass of the tested class
+ *
+ * @copyright 2013 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_tool_installaddon_installer extends tool_installaddon_installer {
+
+    public function get_site_fullname() {
+        return strip_tags('<h1 onmouseover="alert(\'Hello Moodle.org!\');">Nasty site</h1>');
+    }
+
+    public function get_site_url() {
+        return 'file:///etc/passwd';
+    }
+
+    public function get_site_major_version() {
+        return "2.5'; DROP TABLE mdl_user; --";
+    }
+
+    public function testable_decode_remote_request($request) {
+        return parent::decode_remote_request($request);
+    }
+
+    protected function should_send_site_info() {
+        return true;
+    }
+}
diff --git a/admin/tool/installaddon/tests/validator_test.php b/admin/tool/installaddon/tests/validator_test.php
new file mode 100644 (file)
index 0000000..c008d9c
--- /dev/null
@@ -0,0 +1,335 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Provides the unit tests class and some helper classes
+ *
+ * @package     tool_installaddon
+ * @category    test
+ * @copyright   2013 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/installaddon/classes/validator.php');
+
+
+/**
+ * Unit tests for the {@link tool_installaddon_installer} class
+ *
+ * @copyright 2013 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_installaddon_validator_test extends basic_testcase {
+
+    public function test_validate_files_layout() {
+        $fixtures = dirname(__FILE__).'/fixtures';
+
+        // Non-existing directory.
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/nulldir', array(
+            'null/' => true,
+            'null/lang/' => true,
+            'null/lang/en/' => true,
+            'null/lang/en/null.php' => true));
+        $this->assertEquals('testable_tool_installaddon_validator', get_class($validator));
+        $this->assertFalse($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR,
+            'filenotexists', array('file' => 'null/')));
+
+        // Missing expected file
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array(
+            'foobar/' => true,
+            'foobar/version.php' => true,
+            'foobar/index.php' => true,
+            'foobar/lang/' => true,
+            'foobar/lang/en/' => true,
+            'foobar/lang/en/local_foobar.php' => true,
+            'foobar/NOTEXISTS.txt' => true));
+        $this->assertFalse($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR,
+            'filenotexists', array('file' => 'foobar/NOTEXISTS.txt')));
+
+        // Errors during ZIP extraction
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/multidir', array(
+            'one/' => true,
+            'one/version.php' => 'Can not write target file',
+            'two/' => true,
+            'two/README.txt' => true));
+        $this->assertFalse($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'filestatus',
+            array('file' => 'one/version.php', 'status' => 'Can not write target file')));
+
+        // Insufficient number of extracted files
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/emptydir', array(
+            'emptydir/' => true,
+            'emptydir/README.txt' => true));
+        $this->assertFalse($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'filesnumber'));
+
+        // No wrapping directory
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/nowrapdir', array(
+            'version.php' => true,
+            'index.php' => true,
+            'lang/' => true,
+            'lang/en/' => true,
+            'lang/en/foo.php' => true));
+        $this->assertFalse($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'onedir'));
+
+        // Multiple directories
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/multidir', array(
+            'one/' => true,
+            'one/version.php' => true,
+            'two/' => true,
+            'two/README.txt' => true));
+        $this->assertFalse($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'onedir'));
+
+        // Invalid root directory name
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/github', array(
+            'moodle-repository_mahara-master/' => true,
+            'moodle-repository_mahara-master/lang/' => true,
+            'moodle-repository_mahara-master/lang/en/' => true,
+            'moodle-repository_mahara-master/lang/en/repository_mahara.php' => true,
+            'moodle-repository_mahara-master/version.php' => true));
+        $this->assertFalse($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'rootdirinvalid',
+            'moodle-repository_mahara-master'));
+    }
+
+    public function test_validate_version_php() {
+        $fixtures = dirname(__FILE__).'/fixtures';
+
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/noversiontheme', array(
+            'noversion/' => true,
+            'noversion/lang/' => true,
+            'noversion/lang/en/' => true,
+            'noversion/lang/en/theme_noversion.php' => true));
+        $validator->assert_plugin_type('theme');
+        $validator->assert_moodle_version(0);
+        $this->assertTrue($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::DEBUG, 'missingversionphp'));
+        $this->assertTrue(is_null($validator->get_versionphp_info()));
+
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/noversionmod', array(
+            'noversion/' => true,
+            'noversion/lang/' => true,
+            'noversion/lang/en/' => true,
+            'noversion/lang/en/noversion.php' => true));
+        $validator->assert_plugin_type('mod');
+        $validator->assert_moodle_version(0);
+        $this->assertFalse($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missingversionphp'));
+
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array(
+            'foobar/' => true,
+            'foobar/version.php' => true,
+            'foobar/index.php' => true,
+            'foobar/lang/' => true));
+        $validator->assert_plugin_type('block');
+        $validator->assert_moodle_version('2013031400.00');
+        $this->assertFalse($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'componentmismatchtype',
+            array('expected' => 'block', 'found' => 'local')));
+
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array(
+            'foobar/' => true,
+            'foobar/version.php' => true,
+            'foobar/index.php' => true,
+            'foobar/lang/' => true,
+            'foobar/lang/en/' => true,
+            'foobar/lang/en/local_foobar.php' => true));
+        $validator->assert_plugin_type('local');
+        $validator->assert_moodle_version('2013031400.00');
+        $this->assertTrue($validator->execute());
+        $this->assertTrue($validator->get_result());
+        $this->assertEquals('foobar', $validator->get_rootdir());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::INFO, 'rootdir', 'foobar'));
+        $versionphpinfo = $validator->get_versionphp_info();
+        $this->assertEquals('array', gettype($versionphpinfo));
+        $this->assertEquals(4, count($versionphpinfo));
+        $this->assertEquals(2013031900, $versionphpinfo['version']);
+        $this->assertEquals(2013031200, $versionphpinfo['requires']);
+        $this->assertEquals('local_foobar', $versionphpinfo['component']);
+        $this->assertEquals('MATURITY_ALPHA', $versionphpinfo['maturity']); // Note we get the constant name here.
+        $this->assertEquals(MATURITY_ALPHA, constant($versionphpinfo['maturity'])); // This is how to get the real value.
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::WARNING, 'maturity', 'MATURITY_ALPHA'));
+    }
+
+    public function test_validate_language_pack() {
+        $fixtures = dirname(__FILE__).'/fixtures';
+
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/nolang', array(
+            'bah/' => true,
+            'bah/index.php' => true,
+            'bah/view.php' => true,
+            'bah/version.php' => true));
+        $validator->assert_plugin_type('mod');
+        $validator->assert_moodle_version(0);
+        $this->assertFalse($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missinglangenfolder'));
+
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/nolang', array(
+            'bah/' => true,
+            'bah/version.php' => true,
+            'bah/lang/' => true,
+            'bah/lang/en/' => true));
+        $validator->assert_plugin_type('mod');
+        $validator->assert_moodle_version(0);
+        $this->assertFalse($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missinglangenfile'));
+
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/nolang', array(
+            'bah/' => true,
+            'bah/version.php' => true,
+            'bah/lang/' => true,
+            'bah/lang/en/' => true,
+            'bah/lang/en/bleh.php' => true,
+            'bah/lang/en/bah.php' => true));
+        $validator->assert_plugin_type('mod');
+        $validator->assert_moodle_version(0);
+        $this->assertTrue($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::WARNING, 'multiplelangenfiles'));
+        $this->assertTrue(is_null($validator->get_language_file_name()));
+
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/nolang', array(
+            'bah/' => true,
+            'bah/version.php' => true,
+            'bah/lang/' => true,
+            'bah/lang/en/' => true,
+            'bah/lang/en/bah.php' => true));
+        $validator->assert_plugin_type('block');
+        $validator->assert_moodle_version(0);
+        $this->assertFalse($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missingexpectedlangenfile', 'block_bah.php'));
+        $this->assertEquals('bah', $validator->get_language_file_name());
+
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/noversiontheme', array(
+            'noversion/' => true,
+            'noversion/lang/' => true,
+            'noversion/lang/en/' => true,
+            'noversion/lang/en/theme_noversion.php' => true));
+        $validator->assert_plugin_type('theme');
+        $validator->assert_moodle_version(0);
+        $this->assertTrue($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::DEBUG, 'foundlangfile', 'theme_noversion'));
+        $this->assertEquals('theme_noversion', $validator->get_language_file_name());
+
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array(
+            'foobar/' => true,
+            'foobar/version.php' => true,
+            'foobar/index.php' => true,
+            'foobar/lang/' => true,
+            'foobar/lang/en/' => true,
+            'foobar/lang/en/local_foobar.php' => true));
+        $validator->assert_plugin_type('local');
+        $validator->assert_moodle_version('2013031400.00');
+        $this->assertTrue($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::DEBUG, 'foundlangfile', 'local_foobar'));
+        $this->assertEquals('local_foobar', $validator->get_language_file_name());
+    }
+
+    public function test_validate_target_location() {
+        $fixtures = dirname(__FILE__).'/fixtures';
+
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/installed', array(
+            'greenbar/' => true,
+            'greenbar/version.php' => true,
+            'greenbar/index.php' => true,
+            'greenbar/lang/' => true,
+            'greenbar/lang/en/' => true,
+            'greenbar/lang/en/local_greenbar.php' => true));
+        $validator->assert_plugin_type('local');
+        $validator->assert_moodle_version('2013031400.00');
+        $this->assertFalse($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'targetexists',
+            $validator->get_plugintype_location('local').'/greenbar'));
+
+        $validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array(
+            'foobar/' => true,
+            'foobar/version.php' => true,
+            'foobar/index.php' => true,
+            'foobar/lang/' => true,
+            'foobar/lang/en/' => true,
+            'foobar/lang/en/local_foobar.php' => true));
+        $validator->assert_plugin_type('local');
+        $validator->assert_moodle_version('2013031400.00');
+        $this->assertTrue($validator->execute());
+        $this->assertTrue($this->has_message($validator->get_messages(), $validator::INFO, 'pathwritable',
+            $validator->get_plugintype_location('local')));
+    }
+
+    public function test_parse_version_php() {
+        $fixtures = dirname(__FILE__).'/fixtures/versionphp';
+
+        $validator = testable_tool_installaddon_validator::instance($fixtures, array());
+        $this->assertEquals('testable_tool_installaddon_validator', get_class($validator));
+
+        $info = $validator->testable_parse_version_php($fixtures.'/version1.php');
+        $this->assertEquals('array', gettype($info));
+        $this->assertEquals(7, count($info));
+        $this->assertEquals('block_foobar', $info['plugin->component']);    // Later in the file.
+        $this->assertEquals('2013010100', $info['plugin->version']);        // Numeric wins over strings.
+        $this->assertEquals('2012122401', $info['plugin->requires']);       // Commented.
+        $this->assertEquals('MATURITY_STABLE', $info['module->maturity']);  // Constant wins regardless the order (non-PHP behaviour).
+        $this->assertEquals('MATURITY_ALPHA', $info['plugin->maturity']);   // Constant wins regardless the order (non-PHP behaviour).
+        $this->assertEquals('v2.3', $info['module->release']);              // String wins over numeric (non-PHP behaviour).
+        $this->assertEquals('v2.4', $info['plugin->release']);              // String wins over numeric (non-PHP behaviour).
+    }
+
+    // Helper methods //////////////////////////////////////////////////////////
+
+    protected function has_message(array $messages, $level, $msgcode, $addinfo = null) {
+        foreach ($messages as $message) {
+            if ($message->level === $level and $message->msgcode === $msgcode and $message->addinfo === $addinfo) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
+
+
+/**
+ * Provides access to protected methods we want to explicitly test
+ *
+ * @copyright 2013 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_tool_installaddon_validator extends tool_installaddon_validator {
+
+    public function testable_parse_version_php($fullpath) {
+        return parent::parse_version_php($fullpath);
+    }
+
+    public function get_plugintype_location($plugintype) {
+
+        $testableroot = make_temp_directory('testable_tool_installaddon_validator/plugintypes');
+        if (!is_dir($testableroot.'/'.$plugintype)) {
+            make_temp_directory('testable_tool_installaddon_validator/plugintypes/'.$plugintype);
+        }
+
+        if ($plugintype === 'local') {
+            // We need the following for the test_validate_target_location() method
+            make_temp_directory('testable_tool_installaddon_validator/plugintypes/local/greenbar');
+        }
+
+        return $testableroot.'/'.$plugintype;
+    }
+}
diff --git a/admin/tool/installaddon/validate.php b/admin/tool/installaddon/validate.php
new file mode 100644 (file)
index 0000000..2fc66cf
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * The ZIP package validation.
+ *
+ * @package     tool_installaddon
+ * @copyright   2013 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->libdir.'/filelib.php');
+require_once(dirname(__FILE__).'/classes/installer.php');
+require_once(dirname(__FILE__).'/classes/validator.php');
+
+navigation_node::override_active_url(new moodle_url('/admin/tool/installaddon/index.php'));
+admin_externalpage_setup('tool_installaddon_validate');
+
+if (!empty($CFG->disableonclickaddoninstall)) {
+    notice(get_string('featuredisabled', 'tool_installaddon'));
+}
+
+require_sesskey();
+
+$jobid = required_param('jobid', PARAM_ALPHANUM);
+$zipfilename = required_param('zip', PARAM_FILE);
+$plugintype = required_param('type', PARAM_ALPHANUMEXT);
+$rootdir = optional_param('rootdir', '', PARAM_PLUGIN);
+
+$zipfilepath = $CFG->tempdir.'/tool_installaddon/'.$jobid.'/source/'.$zipfilename;
+if (!file_exists($zipfilepath)) {
+    redirect(new moodle_url('/admin/tool/installaddon/index.php'),
+        get_string('invaliddata', 'core_error'));
+}
+
+$installer = tool_installaddon_installer::instance();
+
+// Extract the ZIP contents.
+fulldelete($CFG->tempdir.'/tool_installaddon/'.$jobid.'/contents');
+$zipcontentpath = make_temp_directory('tool_installaddon/'.$jobid.'/contents');
+$zipcontentfiles = $installer->extract_installfromzip_file($zipfilepath, $zipcontentpath, $rootdir);
+
+// Validate the contents of the plugin ZIP file.
+$validator = tool_installaddon_validator::instance($zipcontentpath, $zipcontentfiles);
+$validator->assert_plugin_type($plugintype);
+$validator->assert_moodle_version($CFG->version);
+$result = $validator->execute();
+
+if ($result) {
+    $validator->set_continue_url(new moodle_url('/admin/tool/installaddon/deploy.php', array(
+        'sesskey' => sesskey(),
+        'jobid' => $jobid,
+        'type' => $plugintype,
+        'name' => $validator->get_rootdir())));
+
+} else {
+    fulldelete($CFG->tempdir.'/tool_installaddon/'.$jobid);
+}
+
+// Display the validation results.
+$output = $PAGE->get_renderer('tool_installaddon');
+$output->set_installer_instance($installer);
+$output->set_validator_instance($validator);
+echo $output->validation_page();
diff --git a/admin/tool/installaddon/version.php b/admin/tool/installaddon/version.php
new file mode 100644 (file)
index 0000000..94ab2e5
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * @package     tool_installaddon
+ * @copyright   2013 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->component  = 'tool_installaddon';
+$plugin->version    = 2013031400;
+$plugin->requires   = 2013031400;
+$plugin->maturity   = MATURITY_BETA;
diff --git a/admin/tool/installaddon/yui/permcheck/permcheck.js b/admin/tool/installaddon/yui/permcheck/permcheck.js
new file mode 100644 (file)
index 0000000..ad0e4bd
--- /dev/null
@@ -0,0 +1,105 @@
+/**
+ * Check for write permission for the selected plugin type
+ *
+ * @module      moodle-tool_installaddon-permcheck
+ * @author      David Mudrak <david@moodle.com>
+ */
+YUI.add('moodle-tool_installaddon-permcheck', function(Y) {
+
+    M.tool_installaddon = M.tool_installaddon || {};
+
+    /**
+     * @class permcheck
+     * @static
+     */
+    M.tool_installaddon.permcheck = {
+
+        /**
+         * @method init
+         * @param {Object} config Configuration passed from the PHP
+         */
+        init : function(config) {
+            this.config = config;
+            var plugintypesel = Y.one('#tool_installaddon_installfromzip_plugintype');
+            if (plugintypesel) {
+                plugintypesel.on('change', this.check_for_permission, this);
+            }
+        },
+
+        /**
+         * @method check_for_permission
+         * @param {Event} e
+         */
+        check_for_permission : function(e) {
+            var plugintype = e.currentTarget.get('value');
+            if (plugintype == '') {
+                return;
+            }
+            Y.log('Selected plugin type: ' + plugintype, 'debug', 'moodle-tool_installaddon-permcheck');
+            Y.io(this.config.permcheckurl, {
+                'method' : 'GET',
+                'data' : {
+                    'sesskey' : M.cfg.sesskey,
+                    'plugintype' : plugintype
+                },
+                'arguments' : {
+                    'plugintypeselector' : e.currentTarget,
+                    'showresult' : function(msg, status) {
+                        var resultline = Y.one('#tool_installaddon_installfromzip_permcheck');
+                        if (resultline) {
+                            if (status === 'success') {
+                                resultline.setContent('<span class="success"><img src="' + M.util.image_url('i/tick_green_big') + '" /> ' +
+                                    msg + '</span>');
+                            } else if (status === 'progress') {
+                                resultline.setContent('<span class="progress"><img src="' + M.cfg.loadingicon + '" /> ' +
+                                    msg + '</span>');
+                            } else {
+                                resultline.setContent('<span class="error"><img src="' + M.util.image_url('i/cross_red_big') + '" /> ' +
+                                    msg + '</span>');
+                            }
+                        }
+                    }
+                },
+                'on' : {
+                    'start' : function(transid, args) {
+                        args.showresult(M.util.get_string('permcheckprogress', 'tool_installaddon'), 'progress');
+                    },
+                    'success': function(transid, outcome, args) {
+                        var response;
+                        try {
+                            response = Y.JSON.parse(outcome.responseText);
+                            if (response.error) {
+                                Y.log(response.error, 'error', 'moodle-tool_installaddon-permcheck');
+                                args.showresult(M.util.get_string('permcheckerror', 'tool_installaddon', response), 'error');
+                            } else if (response.path && response.writable == 1) {
+                                args.showresult(M.util.get_string('permcheckresultyes', 'tool_installaddon', response), 'success');
+                            } else if (response.path && response.writable == 0) {
+                                args.showresult(M.util.get_string('permcheckresultno', 'tool_installaddon', response), 'error');
+                            } else {
+                                Y.log(response, 'debug', 'moodle-tool_installaddon-permcheck');
+                                args.showresult(M.util.get_string('permcheckerror', 'tool_installaddon', response), 'error');
+                            }
+
+                        } catch (e) {
+                            Y.log(e, 'error', 'moodle-tool_installaddon-permcheck');
+                            args.showresult(M.util.get_string('permcheckerror', 'tool_installaddon'), 'error');
+                        }
+                    },
+                    'failure': function(transid, outcome, args) {
+                        Y.log(outcome.statusText, 'error', 'moodle-tool_installaddon-permcheck');
+                        args.showresult(M.util.get_string('permcheckerror', 'tool_installaddon'));
+                    }
+                }
+            });
+        },
+
+        /**
+         * @property
+         * @type {Object}
+         */
+        config : null
+    };
+
+}, '@VERSION@', {
+    requires:['node', 'event', 'io-base']
+});
index f83ee22..f903415 100644 (file)
@@ -28,6 +28,7 @@ class course_restore_form extends moodleform {
         $mform =& $this->_form;
         $contextid = $this->_customdata['contextid'];
         $mform->addElement('hidden', 'contextid', $contextid);
+        $mform->setType('contextid', PARAM_INT);
         $mform->addElement('filepicker', 'backupfile', get_string('files'));
         $submit_string = get_string('restore');
         $this->add_action_buttons(false, $submit_string);
index 765c6ac..c5cb567 100644 (file)
@@ -404,6 +404,27 @@ abstract class base_setting {
         $dependency->get_dependent_setting()->register_dependent_dependency($dependency);
     }
 
+    /**
+     * Get the PARAM_XXXX validation to be applied to the setting
+     *
+     * @return string The PARAM_XXXX constant of null if the setting type is not defined
+     */
+    public function get_param_validation() {
+        switch ($this->vtype) {
+            case self::IS_BOOLEAN:
+                return PARAM_BOOL;
+            case self::IS_INTEGER:
+                return PARAM_INT;
+            case self::IS_FILENAME:
+                return PARAM_FILE;
+            case self::IS_PATH:
+                return PARAM_PATH;
+            case self::IS_TEXT:
+                return PARAM_TEXT;
+        }
+        return null;
+    }
+
 // Protected API starts here
 
     protected function validate_value($vtype, $value) {
index e660c4a..9ef5865 100644 (file)
@@ -124,6 +124,16 @@ class base_setting_ui {
     public function get_static_value() {
         return $this->setting->get_value();
     }
+
+    /**
+     * Gets the the PARAM_XXXX validation to be applied to the setting
+     *
+     * return string The PARAM_XXXX constant of null if the setting type is not defined
+     */
+    public function get_param_validation() {
+        return $this->setting->get_param_validation();
+    }
+
     /**
      * Sets the label
      * @param string $label
index 7a733ee..f0604a5 100644 (file)
@@ -83,11 +83,21 @@ abstract class base_moodleform extends moodleform {
         $mform = $this->_form;
         $mform->setDisableShortforms();
         $stage = $mform->addElement('hidden', 'stage', $this->uistage->get_stage());
+        $mform->setType('stage', PARAM_INT);
         $stage = $mform->addElement('hidden', $ui->get_name(), $ui->get_uniqueid());
+        $mform->setType($ui->get_name(), PARAM_ALPHANUM);
         $params = $this->uistage->get_params();
         if (is_array($params) && count($params) > 0) {
             foreach ($params as $name=>$value) {
+                // TODO: Horrible hack, but current backup ui structure does not allow
+                // to make this easy (only changing params to objects that would be
+                // possible. MDL-38735.
+                $intparams = array(
+                        'contextid', 'importid', 'target');
                 $stage = $mform->addElement('hidden', $name, $value);
+                if (in_array($name, $intparams)) {
+                    $mform->setType($name, PARAM_INT);
+                }
             }
         }
     }
@@ -156,6 +166,7 @@ abstract class base_moodleform extends moodleform {
 
             // Then call the add method with the get_element_properties array
             call_user_func_array(array($this->_form, 'addElement'), $setting->get_ui()->get_element_properties($task, $OUTPUT));
+            $this->_form->setType($setting->get_ui_name(), $setting->get_param_validation());
             $defaults[$setting->get_ui_name()] = $setting->get_value();
             if ($setting->has_help()) {
                 list($identifier, $component) = $setting->get_help();
@@ -262,6 +273,7 @@ abstract class base_moodleform extends moodleform {
             $this->_form->addElement('html', html_writer::end_tag('div'));
         }
         $this->_form->addElement('hidden', $settingui->get_name(), $settingui->get_value());
+        $this->_form->setType($settingui->get_name(), $settingui->get_param_validation());
     }
     /**
      * Adds dependencies to the form recursively
index 7b8dfe2..b9a2277 100644 (file)
@@ -590,8 +590,14 @@ class core_backup_renderer extends plugin_renderer_base {
             return $output;
         }
 
-        $output .= html_writer::tag('div', get_string('totalcoursesearchresults', 'backup', $component->get_count()), array('class'=>'ics-totalresults'));
+        $countstr = '';
+        if ($component->has_more_results()) {
+            $countstr = get_string('morecoursesearchresults', 'backup', $component->get_count());
+        } else {
+            $countstr = get_string('totalcoursesearchresults', 'backup', $component->get_count());
+        }
 
+        $output .= html_writer::tag('div', $countstr, array('class'=>'ics-totalresults'));
         $output .= html_writer::start_tag('div', array('class' => 'ics-results'));
 
         $table = new html_table();
@@ -610,6 +616,14 @@ class core_backup_renderer extends plugin_renderer_base {
             );
             $table->data[] = $row;
         }
+        if ($component->has_more_results()) {
+            $cell = new html_table_cell(get_string('moreresults', 'backup'));
+            $cell->colspan = 3;
+            $cell->attributes['class'] = 'notifyproblem';
+            $row = new html_table_row(array($cell));
+            $row->attributes['class'] = 'rcs-course';
+            $table->data[] = $row;
+        }
         $output .= html_writer::table($table);
         $output .= html_writer::end_tag('div');
 
index 7ebaea8..e1aca51 100644 (file)
@@ -39,7 +39,6 @@ abstract class restore_search_base implements renderable {
      */
     static $VAR_SEARCH = 'search';
 
-    static $MAXRESULTS = 10;
     /**
      * The current search string
      * @var string|null
@@ -65,6 +64,16 @@ abstract class restore_search_base implements renderable {
      * @var array
      */
     private $requiredcapabilities = array();
+    /**
+     * Max number of courses to return in a search.
+     * @var int
+     */
+    private $maxresults = null;
+    /**
+     * Indicates if we have more than maxresults found.
+     * @var boolean
+     */
+    private $hasmoreresults = false;
 
     /**
      * Constructor
@@ -73,6 +82,7 @@ abstract class restore_search_base implements renderable {
     public function __construct(array $config=array()) {
 
         $this->search = optional_param($this->get_varsearch(), self::DEFAULT_SEARCH, PARAM_NOTAGS);
+        $this->maxresults = get_config('backup', 'import_general_maxresults');
 
         foreach ($config as $name=>$value) {
             $method = 'set_'.$name;
@@ -177,8 +187,8 @@ abstract class restore_search_base implements renderable {
         foreach ($this->requiredcapabilities as $cap) {
             $requiredcaps[] = $cap['capability'];
         }
-        // Iterate while we have records and haven't reached MAXRESULTS
-        while ($totalcourses > $offs and $this->totalcount < self::$MAXRESULTS) {
+        // Iterate while we have records and haven't reached $this->maxresults.
+        while ($totalcourses > $offs and $this->totalcount < $this->maxresults) {
             $resultset = $DB->get_records_sql($sql, $params, $offs, $blocksz);
             foreach ($resultset as $result) {
                 context_instance_preload($result);
@@ -189,11 +199,14 @@ abstract class restore_search_base implements renderable {
                         continue;
                     }
                 }
-                $this->results[$result->id] = $result;
-                $this->totalcount++;
-                if ($this->totalcount >= self::$MAXRESULTS) {
+                // Check if we are over the limit.
+                if ($this->totalcount+1 > $this->maxresults) {
+                    $this->hasmoreresults = true;
                     break;
                 }
+                // If not, then continue.
+                $this->totalcount++;
+                $this->results[$result->id] = $result;
             }
             $offs += $blocksz;
         }
@@ -202,7 +215,10 @@ abstract class restore_search_base implements renderable {
     }
 
     final public function has_more_results() {
-        return $this->get_count() >= self::$MAXRESULTS;
+        if ($this->results === null) {
+            $this->search();
+        }
+        return $this->hasmoreresults;
     }
 
     /**
diff --git a/backup/util/ui/tests/behat/backup_courses.feature b/backup/util/ui/tests/behat/backup_courses.feature
new file mode 100644 (file)
index 0000000..584354e
--- /dev/null
@@ -0,0 +1,37 @@
+@backup
+Feature: Backup Moodle courses
+  In order to save and store course contents
+  As a moodle admin
+  I need to create backups of courses
+
+  Background:
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And I log in as "admin"
+
+  @javascript
+  Scenario: Backup a course providing options
+    When I backup "Course 1" course using this options:
+    Then I should see "Restore"
+    And I click on "Restore" "link" in the ".backup-files-table" "css_element"
+    And I should see "URL of backup"
+    And I should see "Anonymize user information"
+
+  @javascript
+  Scenario: Backup a course with default options
+    When I backup "Course 1" course using this options:
+      | Filename | test_backup.mbz |
+      | Include calendar events | 0 |
+      | Include course logs | 1 |
+      | setting_section_section_5_userinfo | 0 |
+      | setting_section_section_5_included | 0 |
+    Then I should see "Restore"
+    And I click on "Restore" "link" in the ".backup-files-table" "css_element"
+    And I should not see "Section 3"
+    And I press "Continue"
+    And I click on "Continue" "button" in the ".bcs-current-course" "css_element"
+    And I click on "//div[contains(concat(' ', @class, ' '), ' fitem ')][contains(., 'Include calendar events')]/descendant::img" "xpath_element"
+    And I click on "setting_root_logs" "checkbox" in the "//div[contains(@class, 'fitem')][contains(., 'Include course logs')]" "xpath_element"
+    And I press "Cancel"
+    And I click on "Cancel" "button" in the ".confirmation-dialogue" "css_element"
diff --git a/backup/util/ui/tests/behat/behat_backup.php b/backup/util/ui/tests/behat/behat_backup.php
new file mode 100644 (file)
index 0000000..79c05e1
--- /dev/null
@@ -0,0 +1,346 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Backup and restore actions to help behat feature files writting.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2013 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+require_once(__DIR__ . '/../../../../../lib/behat/behat_field_manager.php');
+
+use Behat\Gherkin\Node\TableNode as TableNode,
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
+    Behat\Mink\Exception\ExpectationException as ExpectationException;
+
+/**
+ * Backup-related steps definitions.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2013 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_backup extends behat_base {
+
+    /**
+     * Backups the specified course using the provided options. If you are interested in restoring this backup would be useful to provide a 'Filename' option.
+     *
+     * @Given /^I backup "(?P<course_fullname_string>(?:[^"]|\\")*)" course using this options:$/
+     * @param string $backupcourse
+     * @param TableNode $options Backup options or false if no options provided
+     */
+    public function i_backup_course_using_this_options($backupcourse, $options = false) {
+
+        // We can not use other steps here as we don't know where the provided data
+        // table elements are used, and we need to catch exceptions contantly.
+
+        // Go to homepage.
+        $this->getSession()->visit($this->locate_path('/'));
+
+        // Click the course link.
+        $this->find_link($backupcourse)->click();
+
+        // Click the backup link.
+        $this->find_link('Backup')->click();
+
+        // Initial settings.
+        $this->fill_backup_restore_form($options);
+        $this->find_button('Next')->press();
+
+        // Schema settings.
+        $this->fill_backup_restore_form($options);
+        $this->find_button('Next')->press();
+
+        // Confirmation and review, backup filename can also be specified.
+        $this->fill_backup_restore_form($options);
+        $this->find_button('Perform backup')->press();
+
+        // Waiting for it to finish.
+        $this->wait(10);
+
+        // Last backup continue button.
+        $this->find_button('Continue')->press();
+    }
+
+    /**
+     * Imports the specified origin course into the other course using the provided options.
+     *
+     * Keeping it separatelly from backup & restore, it the number of
+     * steps and duplicate code becomes bigger a common method should
+     * be generalized.
+     *
+     * @Given /^I import "(?P<from_course_fullname_string>(?:[^"]|\\")*)" course into "(?P<to_course_fullname_string>(?:[^"]|\\")*)" course using this options:$/
+     * @param string $fromcourse
+     * @param string $tocourse
+     * @param TableNode $options
+     */
+    public function i_import_course_into_course($fromcourse, $tocourse, $options = false) {
+
+        // We can not use other steps here as we don't know where the provided data
+        // table elements are used, and we need to catch exceptions contantly.
+
+        // Go to homepage.
+        $this->getSession()->visit($this->locate_path('/'));
+
+        // Click the course link.
+        $this->find_link($tocourse)->click();
+
+        // Click the backup link.
+        $this->find_link('Import')->click();
+
+        // Select the course.
+        $exception = new ExpectationException('"' . $fromcourse . '" course not found in the list of courses to import from', $this->getSession());
+
+        $fromcourse = str_replace("'", "\'", $fromcourse);
+        $xpath = "//div[contains(concat(' ', @class, ' '), ' ics-results ')]
+/descendant::tr[contains(., '" . $fromcourse . "')]
+/descendant::input[@type='radio']";
+        $radionode = $this->find('xpath', $xpath, $exception);
+        $radionode->check();
+        $radionode->click();
+
+        $this->find_button('Continue')->press();
+
+        // Initial settings.
+        $this->fill_backup_restore_form($options);
+        $this->find_button('Next')->press();
+
+        // Schema settings.
+        $this->fill_backup_restore_form($options);
+        $this->find_button('Next')->press();
+
+        // Run it.
+        $this->find_button('Perform import')->press();
+        $this->wait();
+
+        // Continue and redirect to 'to' course.
+        $this->find_button('Continue')->press();
+    }
+
+    /**
+     * Restores the backup into the specified course and the provided options. You should be in the 'Restore' page where the backup is.
+     *
+     * @Given /^I restore "(?P<backup_filename_string>(?:[^"]|\\")*)" backup into "(?P<existing_course_fullname_string>(?:[^"]|\\")*)" course using this options:$/
+     * @param string $backupfilename
+     * @param string $existingcourse
+     * @param TableNode $options Restore forms options or false if no options provided
+     */
+    public function i_restore_backup_into_course_using_this_options($backupfilename, $existingcourse, $options = false) {
+
+        // Confirm restore.
+        $this->select_backup($backupfilename);
+
+        // Selecting the specified course (we can not call behat_forms::select_radio here as is in another behat subcontext).
+        $existingcourse = str_replace("'", "\'", $existingcourse);
+        $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]
+/descendant::div[@class='restore-course-search']
+/descendant::tr[contains(., '" . $existingcourse . "')]
+/descendant::input[@type='radio']");
+        $radionode->check();
+        $radionode->click();
+
+        // Pressing the continue button of the restore into an existing course section.
+        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]/descendant::input[@type='submit'][@value='Continue']");
+        $continuenode->click();
+        $this->wait();
+
+        // Common restore process using provided key/value options.
+        $this->process_restore($options);
+    }
+
+    /**
+     * Restores the specified backup into a new course using the provided options. You should be in the 'Restore' page where the backup is.
+     *
+     * @Given /^I restore "(?P<backup_filename_string>(?:[^"]|\\")*)" backup into a new course using this options:$/
+     * @param string $backupfilename
+     * @param TableNode $options Restore forms options or false if no options provided
+     */
+    public function i_restore_backup_into_a_new_course_using_this_options($backupfilename, $options = false) {
+
+        // Confirm restore.
+        $this->select_backup($backupfilename);
+
+        // The first category in the list.
+        $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-new-course')]
+/descendant::div[@class='restore-course-search']
+/descendant::input[@type='radio']");
+        $radionode->check();
+        $radionode->click();
+
+        // Pressing the continue button of the restore into an existing course section.
+        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-new-course')]/descendant::input[@type='submit'][@value='Continue']");
+        $continuenode->click();
+        $this->wait();
+
+        // Common restore process using provided key/value options.
+        $this->process_restore($options);
+    }
+
+    /**
+     * Merges the backup into the current course using the provided restore options. You should be in the 'Restore' page where the backup is.
+     *
+     * @Given /^I merge "(?P<backup_filename_string>(?:[^"]|\\")*)" backup into the current course using this options:$/
+     * @param string $backupfilename
+     * @param TableNode $options Restore forms options or false if no options provided
+     */
+    public function i_merge_backup_into_the_current_course($backupfilename, $options = false) {
+
+        // Confirm restore.
+        $this->select_backup($backupfilename);
+
+        // Merge without deleting radio option.
+        $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]
+/descendant::input[@type='radio'][@name='target'][@value='1']");
+        $radionode->check();
+        $radionode->click();
+
+        // Pressing the continue button of the restore merging section.
+        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]/descendant::input[@type='submit'][@value='Continue']");
+        $continuenode->click();
+        $this->wait();
+
+        // Common restore process using provided key/value options.
+        $this->process_restore($options);
+    }
+
+    /**
+     * Merges the backup into the current course after deleting this contents, using the provided restore options. You should be in the 'Restore' page where the backup is.
+     *
+     * @Given /^I merge "(?P<backup_filename_string>(?:[^"]|\\")*)" backup into the current course after deleting it's contents using this options:$/
+     * @param string $backupfilename
+     * @param TableNode $options Restore forms options or false if no options provided
+     */
+    public function i_merge_backup_into_current_course_deleting_its_contents($backupfilename, $options = false) {
+
+        // Confirm restore.
+        $this->select_backup($backupfilename);
+
+        // Delete contents radio option.
+        $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]
+/descendant::input[@type='radio'][@name='target'][@value='0']");
+        $radionode->check();
+        $radionode->click();
+
+        // Pressing the continue button of the restore merging section.
+        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]/descendant::input[@type='submit'][@value='Continue']");
+        $continuenode->click();
+        $this->wait();
+
+        // Common restore process using provided key/value options.
+        $this->process_restore($options);
+    }
+
+    /**
+     * Selects the backup to restore.
+     *
+     * @throws ExpectationException
+     * @param string $backupfilename
+     * @return void
+     */
+    protected function select_backup($backupfilename) {
+
+        // Using xpath as there are other restore links before this one.
+        $exception = new ExpectationException('The "' . $backupfilename . '" backup file can not be found in this page', $this->getSession());
+        $xpath = "//tr[contains(., '" . $backupfilename . "')]/descendant::a[contains(., 'Restore')]";
+        $restorelink = $this->find('xpath', $xpath, $exception);
+        $restorelink->click();
+
+        // Confirm the backup contents.
+        $restore = $this->find_button('Continue')->press();
+    }
+
+    /**
+     * Executes the common steps of all restore processes.
+     *
+     * @param TableNode $options The backup and restore options or false if no options provided
+     * @return void
+     */
+    protected function process_restore($options) {
+
+        // We can not use other steps here as we don't know where the provided data
+        // table elements are used, and we need to catch exceptions contantly.
+
+        // Settings.
+        $this->fill_backup_restore_form($options);
+        $this->find_button('Next')->press();
+
+        // Schema.
+        $this->fill_backup_restore_form($options);
+        $this->find_button('Next')->press();
+
+        // Review, no options here.
+        $this->find_button('Perform restore')->press();
+        $this->wait(10);
+
+        // Last restore continue button, redirected to restore course after this.
+        $this->find_button('Continue')->press();
+    }
+
+    /**
+     * Tries to fill the current page form elements with the provided options.
+     *
+     * This step is slow as it spins over each provided option, we are
+     * not expected to have lots of provided options, anyways, is better
+     * to be conservative and wait for the elements to appear rather than
+     * to have false failures.
+     *
+     * @param TableNode $options The backup and restore options or false if no options provided
+     * @return void
+     */
+    protected function fill_backup_restore_form($options) {
+
+        // Nothing to fill if no options are provided.
+        if (!$options) {
+            return;
+        }
+
+        // If we find any of the provided options in the current form we should set the value.
+        $datahash = $options->getRowsHash();
+        foreach ($datahash as $locator => $value) {
+
+            try {
+                $fieldnode = $this->find_field($locator);
+                $field = behat_field_manager::get_field($fieldnode, $locator, $this->getSession());
+                $field->set_value($value);
+
+            } catch (ElementNotFoundException $e) {
+                // Next provided option then, this one should be part of another page's fields.
+            }
+        }
+    }
+
+    /**
+     * Waits until the DOM is ready.
+     *
+     * @param int To override the default timeout
+     * @return void
+     */
+    protected function wait($timeout = false) {
+
+        if (!$timeout) {
+            $timeout = self::TIMEOUT;
+        }
+        $this->getSession()->wait($timeout, '(document.readyState === "complete")');
+    }
+
+}
diff --git a/backup/util/ui/tests/behat/duplicate_activities.feature b/backup/util/ui/tests/behat/duplicate_activities.feature
new file mode 100644 (file)
index 0000000..f23efc4
--- /dev/null
@@ -0,0 +1,33 @@
+@backup
+Feature: Duplicate activities
+  In order to set up my course contents quickly
+  As a moodle teacher
+  I need to duplicate activities inside the same course
+
+  @javascript
+  Scenario: Duplicate an activity
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Database" to section "1" and I fill the form with:
+      | Name | Test database name |
+      | Description | Test database description |
+    When I click on "Duplicate" "link" in the "#section-1" "css_element"
+    And I press "Continue"
+    And I press "Edit the new copy"
+    And I fill the moodle form with:
+      | Name | Duplicated database name |
+      | Description | Duplicated database description |
+    And I press "Save and return to course"
+    Then I should see "Test database name" in the "#section-1" "css_element"
+    And I should see "Duplicated database name" in the "#section-1" "css_element"
+    And "Test database name" "link" should appear before "Duplicated database name" "link"
diff --git a/backup/util/ui/tests/behat/import_course.feature b/backup/util/ui/tests/behat/import_course.feature
new file mode 100644 (file)
index 0000000..1acf15c
--- /dev/null
@@ -0,0 +1,35 @@
+@backup
+Feature: Import course's contents into another course
+  In order to move and copy contents between courses
+  As a moodle teacher
+  I need to import a course contents into another course selecting what I want to import
+
+  @javascript
+  Scenario: Import course's contents to another course
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+      | Course 2 | C2 | 0 |
+    And the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | teacher1 | C2 | editingteacher |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Database" to section "1" and I fill the form with:
+      | Name | Test database name |
+      | Description | Test database description |
+    And I add a "Forum" to section "2" and I fill the form with:
+      | Forum name | Test forum name |
+      | Description | Test forum description |
+    And I add the "Comments" block
+    And I add the "Recent blog entries" block
+    When I import "Course 1" course into "Course 2" course using this options:
+    Then I should see "Test database name"
+    And I should see "Test forum name"
+    And I should see "Comments"
+    And I should see "Recent blog entries"
diff --git a/backup/util/ui/tests/behat/restore_moodle2_courses.feature b/backup/util/ui/tests/behat/restore_moodle2_courses.feature
new file mode 100644 (file)
index 0000000..177a92b
--- /dev/null
@@ -0,0 +1,130 @@
+@backup
+Feature: Restore Moodle 2 course backups
+  In order to continue using my stored course contents
+  As a moodle teacher and as a moodle admin
+  I need to restore them inside other Moodle courses or in new courses
+
+  Background:
+    Given the following "courses" exists:
+      | fullname | shortname | category | format | numsections | coursedisplay |
+      | Course 1 | C1 | 0 | topics | 15 | 1 |
+      | Course 2 | C2 | 0 | topics | 5 | 0 |
+    And I log in as "admin"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Description | Test forum description |
+    And I add the "Community finder" block
+
+  @javascript
+  Scenario: Restore a course in another existing course
+    When I backup "Course 1" course using this options:
+      | Filename | test_backup.mbz |
+    And I restore "test_backup.mbz" backup into "Course 2" course using this options:
+    Then I should see "Course 2"
+    And I should see "Community finder"
+    And I should see "Test forum name"
+
+  @javascript
+  Scenario: Restore a course in a new course
+    When I backup "Course 1" course using this options:
+      | Filename | test_backup.mbz |
+    And I restore "test_backup.mbz" backup into a new course using this options:
+      | Course name | Course 1 restored in a new course |
+    Then I should see "Course 1 restored in a new course"
+    And I should see "Community finder"
+    And I should see "Test forum name"
+    And I follow "Edit settings"
+    And I expand all fieldsets
+    And the "id_format" field should match "Topics format" value
+    And the "Number of sections" field should match "15" value
+    And the "Course layout" field should match "Show one section per page" value
+    And I press "Cancel"
+
+  @javascript
+  Scenario: Restore a backup into the same course
+    When I backup "Course 1" course using this options:
+      | Filename | test_backup.mbz |
+    And I merge "test_backup.mbz" backup into the current course using this options:
+      | setting_section_section_5_included | 0 |
+      | setting_section_section_5_userinfo | 0 |
+    Then I should see "Course 1"
+    And I should not see "Section 3"
+    And I should see "Community finder"
+    And I should see "Test forum name"
+
+  @javascript
+  Scenario: Restore a backup into the same course removing it's contents before that
+    When I backup "Course 1" course using this options:
+      | Filename | test_backup.mbz |
+    And I follow "Course 1"
+    And I add a "Forum" to section "1" and I fill the form with:
+      | Forum name | Test forum post backup name |
+      | Description | Test forum post backup description |
+    And I follow "Restore"
+    And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
+      | setting_section_section_5_userinfo | 0 |
+      | setting_section_section_5_included | 0 |
+    Then I should see "Course 1"
+    And I should not see "Section 3"
+    And I should not see "Test forum post backup name"
+    And I should see "Community finder"
+    And I should see "Test forum name"
+
+  @javascript
+  Scenario: Restore a backup into a new course changing the course format afterwards
+    Given I backup "Course 1" course using this options:
+      | Filename | test_backup.mbz |
+    When I restore "test_backup.mbz" backup into a new course using this options:
+    Then I should see "Topic 1"
+    And I should see "Test forum name"
+    And I follow "Edit settings"
+    And the "id_format" field should match "Topics format" value
+    And I fill the moodle form with:
+      | id_startdate_day | 1 |
+      | id_startdate_month | January |
+      | id_startdate_year | 2020 |
+      | id_format | Weekly format |
+    And I press "Save changes"
+    And I should see "1 January - 7 January"
+    And I should see "Test forum name"
+    And I follow "Edit settings"
+    And the "id_format" field should match "Weekly format" value
+    And I fill the moodle form with:
+      | id_format | Social format |
+    And I press "Save changes"
+    And I should see "An open forum for chatting about anything you want to"
+    And I follow "Edit settings"
+    And the "id_format" field should match "Social format" value
+    And I fill the moodle form with:
+      | id_format | SCORM format |
+    And I press "Save changes"
+    And I should see "Adding a new SCORM package"
+    And I follow "Edit settings"
+    And the "id_format" field should match "SCORM format" value
+    And I press "Cancel"
+
+  @javascript
+  Scenario: Restore a backup in an existing course retaining the backup course settings
+    Given I add a "URL" to section "3" and I fill the form with:
+      | Name | Test URL name |
+      | Description | Test URL description |
+      | id_externalurl | http://www.moodle.org |
+    And I hide section "3"
+    And I hide section "7"
+    When I backup "Course 1" course using this options:
+      | Filename | test_backup.mbz |
+    And I restore "test_backup.mbz" backup into "Course 2" course using this options:
+      | Overwrite course configuration | Yes |
+    And I follow "Edit settings"
+    And I expand all fieldsets
+    Then the "id_format" field should match "Topics format" value
+    And the "Number of sections" field should match "15" value
+    And the "Course layout" field should match "Show one section per page" value
+    And I press "Cancel"
+    And section "3" should be hidden
+    And section "7" should be hidden
+    And section "15" should be visible
+    And I should see "Test URL name" in the "#section-3" "css_element"
+    And I should see "Test forum name" in the "#section-1" "css_element"
diff --git a/badges/action.php b/badges/action.php
new file mode 100644 (file)
index 0000000..33181a5
--- /dev/null
@@ -0,0 +1,160 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Page to handle actions associated with badges management.
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
+ */
+
+require_once(dirname(dirname(__FILE__)) . '/config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+$badgeid = required_param('id', PARAM_INT);
+$copy = optional_param('copy', 0, PARAM_BOOL);
+$delete    = optional_param('delete', 0, PARAM_BOOL);
+$activate = optional_param('activate', 0, PARAM_BOOL);
+$deactivate = optional_param('lock', 0, PARAM_BOOL);
+$confirm   = optional_param('confirm', 0, PARAM_BOOL);
+$return = optional_param('return', 0, PARAM_LOCALURL);
+
+require_login();
+
+$badge = new badge($badgeid);
+$context = $badge->get_context();
+$navurl = new moodle_url('/badges/index.php', array('type' => $badge->type));
+
+if ($badge->type == BADGE_TYPE_COURSE) {
+    require_login($badge->courseid);
+    $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type, 'id' => $badge->courseid));
+}
+
+$PAGE->set_context($context);
+$PAGE->set_url('/badges/action.php', array('id' => $badge->id));
+$PAGE->set_pagelayout('standard');
+navigation_node::override_active_url($navurl);
+
+if ($return !== 0) {
+    $returnurl = new moodle_url($return);
+} else {
+    $returnurl = new moodle_url('/badges/overview.php', array('id' => $badge->id));
+}
+$returnurl->remove_params('awards');
+
+if ($delete) {
+    require_capability('moodle/badges:deletebadge', $context);
+
+    $PAGE->url->param('delete', 1);
+    if ($confirm) {
+        require_sesskey();
+        $badge->delete();
+        redirect(new moodle_url('/badges/index.php', array('type' => $badge->type, 'id' => $badge->courseid)));
+    }
+
+    $strheading = get_string('delbadge', 'badges');
+    $PAGE->navbar->add($strheading);
+    $PAGE->set_title($strheading);
+    $PAGE->set_heading($badge->name);
+    echo $OUTPUT->header();
+    echo $OUTPUT->heading($strheading);
+
+    $urlparams = array(
+        'id' => $badge->id,
+        'delete' => 1,
+        'confirm' => 1,
+        'sesskey' => sesskey()
+    );
+    $continue = new moodle_url('/badges/action.php', $urlparams);
+    $cancel = new moodle_url('/badges/index.php', array('type' => $badge->type, 'id' => $badge->courseid));
+
+    $message = get_string('delconfirm', 'badges', $badge->name);
+    echo $OUTPUT->confirm($message, $continue, $cancel);
+    echo $OUTPUT->footer();
+    die;
+}
+
+if ($copy) {
+    require_sesskey();
+    require_capability('moodle/badges:createbadge', $context);
+
+    $cloneid = $badge->make_clone();
+    redirect(new moodle_url('/badges/edit.php', array('id' => $cloneid, 'action' => 'details')));
+}
+
+if ($activate) {
+    require_capability('moodle/badges:configurecriteria', $context);
+
+    $PAGE->url->param('activate', 1);
+    $status = ($badge->status == BADGE_STATUS_INACTIVE) ? BADGE_STATUS_ACTIVE : BADGE_STATUS_ACTIVE_LOCKED;
+    if ($confirm == 1) {
+        require_sesskey();
+        $badge->set_status($status);
+
+        if ($badge->type == BADGE_TYPE_SITE) {
+            // Review on cron if there are more than 1000 users who can earn a site-level badge.
+            $sql = 'SELECT COUNT(u.id) as num
+                        FROM {user} u
+                        LEFT JOIN {badge_issued} bi
+                            ON u.id = bi.userid AND bi.badgeid = :badgeid
+                        WHERE bi.badgeid IS NULL AND u.id != :guestid AND u.deleted = 0';
+            $toearn = $DB->get_record_sql($sql, array('badgeid' => $badge->id, 'guestid' => $CFG->siteguest));
+
+            if ($toearn->num < 1000) {
+                $awards = $badge->review_all_criteria();
+                $returnurl->param('awards', $awards);
+            } else {
+                $returnurl->param('awards', 'cron');
+            }
+        } else {
+            $awards = $badge->review_all_criteria();
+            $returnurl->param('awards', $awards);
+         }
+        redirect($returnurl);
+    }
+
+    $strheading = get_string('reviewbadge', 'badges');
+    $PAGE->navbar->add($strheading);
+    $PAGE->set_title($strheading);
+    $PAGE->set_heading($badge->name);
+    echo $OUTPUT->header();
+    echo $OUTPUT->heading($strheading);
+
+    $params = array('id' => $badge->id, 'activate' => 1, 'sesskey' => sesskey(), 'confirm' => 1, 'return' => $return);
+    $url = new moodle_url('/badges/action.php', $params);
+
+    if (!$badge->has_criteria()) {
+        echo $OUTPUT->notification(get_string('error:cannotact', 'badges') . get_string('nocriteria', 'badges'));
+        echo $OUTPUT->continue_button($returnurl);
+    } else {
+        $message = get_string('reviewconfirm', 'badges', $badge->name);
+        echo $OUTPUT->confirm($message, $url, $returnurl);
+    }
+    echo $OUTPUT->footer();
+    die;
+}
+
+if ($deactivate) {
+    require_sesskey();
+    require_capability('moodle/badges:configurecriteria', $context);
+
+    $status = ($badge->status == BADGE_STATUS_ACTIVE) ? BADGE_STATUS_INACTIVE : BADGE_STATUS_INACTIVE_LOCKED;
+    $badge->set_status($status);
+    redirect($returnurl);
+}
diff --git a/badges/ajax.php b/badges/ajax.php
new file mode 100644 (file)
index 0000000..5f99cb9
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Sends request to check web site availability.
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2013 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
+ */
+
+define('AJAX_SCRIPT', true);
+
+require_once(dirname(dirname(__FILE__)) . '/config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+require_login();
+$PAGE->set_url('/badges/ajax.php');
+$PAGE->set_context(context_system::instance());
+
+$result = badges_check_backpack_accessibility();
+
+$outcome = new stdClass();
+$outcome->code = $result;
+$outcome->response = get_string('error:backpacknotavailable', 'badges') . $OUTPUT->help_icon('backpackavailability', 'badges');
+echo json_encode($outcome);
+
+die();
\ No newline at end of file
diff --git a/badges/assertion.php b/badges/assertion.php
new file mode 100644 (file)
index 0000000..24b8e60
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Serve assertion JSON by unique hash of issued badge
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
+ */
+
+require_once(dirname(dirname(__FILE__)) . '/config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+if (empty($CFG->enablebadges)) {
+    print_error('badgesdisabled', 'badges');
+}
+
+$hash = required_param('b', PARAM_ALPHANUM);
+
+$badge = badges_get_issued_badge_info($hash);
+
+header('Content-type: application/json; charset=utf-8');
+
+echo json_encode($badge);
\ No newline at end of file
diff --git a/badges/award.php b/badges/award.php
new file mode 100644 (file)
index 0000000..b388e36
--- /dev/null
@@ -0,0 +1,143 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Handle manual badge award.
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
+ */
+
+require_once(dirname(dirname(__FILE__)) . '/config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+require_once($CFG->dirroot . '/badges/lib/awardlib.php');
+
+$badgeid = required_param('id', PARAM_INT);
+$role = optional_param('role', 0, PARAM_INT);
+
+require_login();
+
+if (empty($CFG->enablebadges)) {
+    print_error('badgesdisabled', 'badges');
+}
+
+$badge = new badge($badgeid);
+$context = $badge->get_context();
+$isadmin = is_siteadmin($USER);
+
+$navurl = new moodle_url('/badges/index.php', array('type' => $badge->type));
+
+if ($badge->type == BADGE_TYPE_COURSE) {
+    require_login($badge->courseid);
+    $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type, 'id' => $badge->courseid));
+}
+
+require_capability('moodle/badges:awardbadge', $context);
+
+$url = new moodle_url('/badges/award.php', array('id' => $badgeid));
+$PAGE->set_url($url);
+$PAGE->set_context($context);
+
+// Set up navigation and breadcrumbs.
+$strrecipients = get_string('recipients', 'badges');
+navigation_node::override_active_url($navurl);
+$PAGE->navbar->add($badge->name, new moodle_url('overview.php', array('id' => $badge->id)))->add($strrecipients);
+$PAGE->set_title($strrecipients);
+$PAGE->set_heading($badge->name);
+$PAGE->set_pagelayout('standard');
+
+if (!$badge->is_active()) {
+    echo $OUTPUT->header();
+    echo $OUTPUT->notification(get_string('donotaward', 'badges'));
+    echo $OUTPUT->footer();
+    die();
+}
+
+$output = $PAGE->get_renderer('core', 'badges');
+
+// Roles that can award this badge.
+$accepted_roles = array_keys($badge->criteria[BADGE_CRITERIA_TYPE_MANUAL]->params);
+
+// If site admin, select a role to award a badge.
+if ($isadmin) {
+    list($usertest, $userparams) = $DB->get_in_or_equal($accepted_roles, SQL_PARAMS_NAMED, 'existing', true);
+    $options = $DB->get_records_sql('SELECT * FROM {role} WHERE id ' . $usertest, $userparams);
+    foreach ($options as $p) {
+        $select[$p->id] = role_get_name($p);
+    }
+    if (!$role) {
+        echo $OUTPUT->header();
+        echo $OUTPUT->box(get_string('adminaward', 'badges') . $OUTPUT->single_select(new moodle_url($PAGE->url), 'role', $select));
+        echo $OUTPUT->footer();
+        die();
+    } else {
+        $issuerrole = new stdClass();
+        $issuerrole->roleid = $role;
+        $roleselect = get_string('adminaward', 'badges') . $OUTPUT->single_select(new moodle_url($PAGE->url), 'role', $select, $role);
+    }
+} else {
+    // Current user's role.
+    $issuerrole = array_shift(get_user_roles($context, $USER->id));
+    if (!isset($issuerrole->roleid) || !in_array($issuerrole->roleid, $accepted_roles)) {
+        echo $OUTPUT->header();
+        $rlink = html_writer::link(new moodle_url('recipients.php', array('id' => $badge->id)), get_string('recipients', 'badges'));
+        echo $OUTPUT->notification(get_string('notacceptedrole', 'badges', $rlink));
+        echo $OUTPUT->footer();
+        die();
+    }
+}
+$options = array(
+        'badgeid' => $badge->id,
+        'context' => $context,
+        'issuerid' => $USER->id,
+        'issuerrole' => $issuerrole->roleid
+        );
+$existingselector = new badge_existing_users_selector('existingrecipients', $options);
+$recipientselector = new badge_potential_users_selector('potentialrecipients', $options);
+$recipientselector->set_existing_recipients($existingselector->find_users(''));
+
+if (optional_param('award', false, PARAM_BOOL) && data_submitted() && has_capability('moodle/badges:awardbadge', $context)) {
+    require_sesskey();
+    $users = $recipientselector->get_selected_users();
+    foreach ($users as $user) {
+        if (process_manual_award($user->id, $USER->id, $issuerrole->roleid, $badgeid)) {
+            // If badge was successfully awarded, review manual badge criteria.
+            $data = new stdClass();
+            $data->crit = $badge->criteria[BADGE_CRITERIA_TYPE_MANUAL];
+            $data->userid = $user->id;
+            badges_award_handle_manual_criteria_review($data);
+        } else {
+            echo $OUTPUT->error_text(get_string('error:cannotawardbadge', 'badges'));
+        }
+    }
+
+    $recipientselector->invalidate_selected_users();
+    $existingselector->invalidate_selected_users();
+    $recipientselector->set_existing_recipients($existingselector->find_users(''));
+}
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading($strrecipients);
+
+if ($isadmin) {
+    echo $OUTPUT->box($roleselect);
+}
+
+echo $output->recipients_selection_form($existingselector, $recipientselector);
+echo $OUTPUT->footer();
diff --git a/badges/backpack.js b/badges/backpack.js
new file mode 100644 (file)
index 0000000..b0caec4
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Push badges to backpack.
+ */
+function addtobackpack(event, args) {
+    OpenBadges.issue([args.assertion], function(errors, successes) { });
+}
+
+/**
+ * Check if website is externally accessible from the backpack.
+ */
+function check_site_access() {
+    var add = Y.one('#check_connection');
+    var callback = {
+        success: function(o) {
+            var data = Y.JSON.parse(o.responseText);
+            if (data.code == 'http-unreachable') {
+                add.setHTML(data.response);
+                add.removeClass('hide');
+            }
+        },
+        failure: function(o) { }
+    };
+
+    YUI().use('yui2-connection', function (Y) {
+        Y.YUI2.util.Connect.asyncRequest('GET', 'ajax.php', callback, null);
+    });
+
+    return false;
+}
\ No newline at end of file
diff --git a/badges/backpack_form.php b/badges/backpack_form.php
new file mode 100644 (file)
index 0000000..e0e25ad
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Form class for mybackpack.php
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/formslib.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+/**
+ * Form to edit backpack initial details.
+ *
+ */
+class edit_backpack_form extends moodleform {
+
+    /**
+     * Defines the form
+     */
+    public function definition() {
+        global $USER;
+        $mform = $this->_form;
+
+        $mform->addElement('header', 'backpackheader', get_string('backpackdetails', 'badges'));
+        $mform->addElement('static', 'url', get_string('url'), 'http://backpack.openbadges.org');
+
+        $mform->addElement('text', 'email', get_string('email'), array('size' => '50'));
+        $mform->setDefault('email', $USER->email);
+        $mform->setType('email', PARAM_EMAIL);
+        $mform->addRule('email', get_string('required'), 'required', null , 'client');
+        $mform->addRule('email', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
+
+        $mform->addElement('hidden', 'userid', $USER->id);
+        $mform->setType('userid', PARAM_INT);
+
+        $mform->addElement('hidden', 'backpackurl', 'http://backpack.openbadges.org');
+        $mform->setType('backpackurl', PARAM_URL);
+
+        $this->add_action_buttons();
+    }
+}
+
+/**
+ * Form to select backpack group options.
+ *
+ */
+class edit_backpack_group_form extends moodleform {
+
+    /**
+     * Defines the form
+     */
+    public function definition() {
+        global $USER;
+        $mform = $this->_form;
+        $data = $this->_customdata['data'];
+        $groups = $this->_customdata['groups'];
+        $uid = $this->_customdata['backpackuid'];
+
+        $selet = array();
+        foreach ($groups as $group) {
+            $select[$group->groupId] = $group->name;
+        }
+
+        $mform->addElement('header', 'groupheader', get_string('backpackdetails', 'badges'));
+        $mform->addElement('static', 'url', get_string('url'), 'http://backpack.openbadges.org');
+
+        $mform->addElement('text', 'email', get_string('email'), array('size' => '50'));
+        $mform->setDefault('email', $data->email);
+        $mform->freeze(array('email'));
+
+        $mform->addElement('select', 'backpackgid', get_string('selectgroup', 'badges'), $select);
+        $mform->addRule('backpackgid', get_string('required'), 'required', null , 'client');
+        if (isset($data->backpackgid)) {
+            $mform->setDefault('backpackgid', $data->backpackgid);
+        }
+
+        $mform->addElement('hidden', 'userid', $data->userid);
+        $mform->setType('userid', PARAM_INT);
+
+        $mform->addElement('hidden', 'backpackurl', 'http://backpack.openbadges.org');
+        $mform->setType('backpackurl', PARAM_URL);
+
+        $mform->addElement('hidden', 'backpackuid', $uid);
+        $mform->setType('backpackuid', PARAM_INT);
+
+        $this->add_action_buttons();
+    }
+}
\ No newline at end of file
diff --git a/badges/badge.php b/badges/badge.php
new file mode 100644 (file)
index 0000000..1061eb4
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Display details of an issued badge with criteria and evidence
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
+ */
+
+require_once(dirname(dirname(__FILE__)) . '/config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+$id = required_param('hash', PARAM_ALPHANUM);
+$bake = optional_param('bake', 0, PARAM_BOOL);
+
+$PAGE->set_context(context_system::instance());
+$output = $PAGE->get_renderer('core', 'badges');
+
+$badge = new issued_badge($id);
+
+if ($bake && ($badge->recipient == $USER->id)) {
+    $name = str_replace(' ', '_', $badge->issued['badge']['name']) . '.png';
+    ob_start();
+    $file = badges_bake($id, $badge->badgeid);
+    header('Content-Type: image/png');
+    header('Content-Disposition: attachment; filename="'. $name .'"');
+    readfile($file);
+    ob_flush();
+}
+
+$PAGE->set_url('/badges/badge.php', array('hash' => $id));
+$PAGE->set_pagelayout('base');
+$PAGE->set_title(get_string('issuedbadge', 'badges'));
+
+if (isloggedin()) {
+    $PAGE->set_heading($badge->issued['badge']['name']);
+    $PAGE->navbar->add($badge->issued['badge']['name']);
+    $url = new moodle_url('/badges/mybadges.php');
+    navigation_node::override_active_url($url);
+}
+
+// TODO: Better way of pushing badges to Mozilla backpack?
+if ($CFG->badges_allowexternalbackpack) {
+    $PAGE->requires->js(new moodle_url('http://backpack.openbadges.org/issuer.js'), true);
+}
+
+echo $OUTPUT->header();
+
+echo $output->render($badge);
+
+echo $OUTPUT->footer();
diff --git a/badges/criteria.php b/badges/criteria.php
new file mode 100644 (file)
index 0000000..7844499
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Editing badge details, criteria, messages
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
+ */
+
+require_once(dirname(dirname(__FILE__)) . '/config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+$badgeid = required_param('id', PARAM_INT);
+$update = optional_param('update', 0, PARAM_INT);
+
+require_login();
+
+if (empty($CFG->enablebadges)) {
+    print_error('badgesdisabled', 'badges');
+}
+
+$badge = new badge($badgeid);
+$context = $badge->get_context();
+$navurl = new moodle_url('/badges/index.php', array('type' => $badge->type));
+
+if ($badge->type == BADGE_TYPE_COURSE) {
+    require_login($badge->courseid);
+    $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type, 'id' => $badge->courseid));
+}
+
+$currenturl = new moodle_url('/badges/criteria.php', array('id' => $badge->id));
+
+$PAGE->set_context($context);
+$PAGE->set_url($currenturl);
+$PAGE->set_pagelayout('standard');
+$PAGE->set_heading($badge->name);
+$PAGE->set_title($badge->name);
+
+// Set up navigation and breadcrumbs.
+navigation_node::override_active_url($navurl);
+$PAGE->navbar->add($badge->name);
+
+$output = $PAGE->get_renderer('core', 'badges');
+$msg = optional_param('msg', '', PARAM_TEXT);
+$emsg = optional_param('emsg', '', PARAM_TEXT);
+
+if ((($update == BADGE_CRITERIA_AGGREGATION_ALL) || ($update == BADGE_CRITERIA_AGGREGATION_ANY))) {
+    require_sesskey();
+    require_capability('moodle/badges:configurecriteria', $context);
+    $obj = new stdClass();
+    $obj->id = $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->id;
+    $obj->method = $update;
+    if ($DB->update_record('badge_criteria', $obj)) {
+        $msg = get_string('changessaved');
+    } else {
+        $emsg = get_string('error:save', 'badges');
+    }
+}
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading(print_badge_image($badge, $context, 'small') . ' ' . $badge->name);
+
+if ($emsg !== '') {
+    echo $OUTPUT->notification($emsg);
+} else if ($msg !== '') {
+    echo $OUTPUT->notification($msg, 'notifysuccess');
+}
+
+echo $output->print_badge_status_box($badge);
+$output->print_badge_tabs($badgeid, $context, 'criteria');
+
+if (!$badge->is_locked() && !$badge->is_active()) {
+    echo $output->print_criteria_actions($badge);
+}
+
+if ($badge->has_criteria()) {
+    ksort($badge->criteria);
+
+    foreach ($badge->criteria as $crit) {
+        $crit->config_form_criteria($badge);
+    }
+} else {
+    echo $OUTPUT->box(get_string('addcriteriatext', 'badges'));
+}
+
+echo $OUTPUT->footer();
\ No newline at end of file
diff --git a/badges/criteria/award_criteria.php b/badges/criteria/award_criteria.php
new file mode 100644 (file)
index 0000000..9a469e0
--- /dev/null
@@ -0,0 +1,390 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Badge award criteria
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/*
+ * Role completion criteria type
+ * Criteria type constant, primarily for storing criteria type in the database.
+ */
+define('BADGE_CRITERIA_TYPE_OVERALL', 0);
+
+/*
+ * Activity completion criteria type
+ * Criteria type constant, primarily for storing criteria type in the database.
+ */
+define('BADGE_CRITERIA_TYPE_ACTIVITY', 1);
+
+/*
+ * Duration completion criteria type
+ * Criteria type constant, primarily for storing criteria type in the database.
+ */
+define('BADGE_CRITERIA_TYPE_MANUAL', 2);
+
+/*
+ * Grade completion criteria type
+ * Criteria type constant, primarily for storing criteria type in the database.
+ */
+define('BADGE_CRITERIA_TYPE_SOCIAL', 3);
+
+/*
+ * Course completion criteria type
+ * Criteria type constant, primarily for storing criteria type in the database.
+*/
+define('BADGE_CRITERIA_TYPE_COURSE', 4);
+
+/*
+ * Courseset completion criteria type
+ * Criteria type constant, primarily for storing criteria type in the database.
+ */
+define('BADGE_CRITERIA_TYPE_COURSESET', 5);
+
+/*
+ * Course completion criteria type
+ * Criteria type constant, primarily for storing criteria type in the database.
+ */
+define('BADGE_CRITERIA_TYPE_PROFILE', 6);
+
+/*
+ * Criteria type constant to class name mapping
+ */
+global $BADGE_CRITERIA_TYPES;
+$BADGE_CRITERIA_TYPES = array(
+    BADGE_CRITERIA_TYPE_OVERALL   => 'overall',
+    BADGE_CRITERIA_TYPE_ACTIVITY  => 'activity',
+    BADGE_CRITERIA_TYPE_MANUAL    => 'manual',
+    BADGE_CRITERIA_TYPE_SOCIAL    => 'social',
+    BADGE_CRITERIA_TYPE_COURSE    => 'course',
+    BADGE_CRITERIA_TYPE_COURSESET => 'courseset',
+    BADGE_CRITERIA_TYPE_PROFILE   => 'profile'
+);
+
+/**
+ * Award criteria abstract definition
+ *
+ */
+abstract class award_criteria {
+
+    public $id;
+    public $method;
+    public $badgeid;
+    public $params = array();
+
+    /**
+     * The base constructor
+     *
+     * @param array $params
+     */
+    public function __construct($params) {
+        $this->id = isset($params['id']) ? $params['id'] : 0;
+        $this->method = isset($params['method']) ? $params['method'] : BADGE_CRITERIA_AGGREGATION_ANY;
+        $this->badgeid = $params['badgeid'];
+        if (isset($params['id'])) {
+            $this->params = $this->get_params($params['id']);
+        }
+    }
+
+    /**
+     * Factory method for creating criteria class object
+     *
+     * @param array $params associative arrays varname => value
+     * @return award_criteria
+     */
+    public static function build($params) {
+        global $CFG, $BADGE_CRITERIA_TYPES;
+
+        if (!isset($params['criteriatype']) || !isset($BADGE_CRITERIA_TYPES[$params['criteriatype']])) {
+            print_error('error:invalidcriteriatype', 'badges');
+        }
+
+        $class = 'award_criteria_' . $BADGE_CRITERIA_TYPES[$params['criteriatype']];
+        require_once($CFG->dirroot . '/badges/criteria/' . $class . '.php');
+
+        return new $class($params);
+    }
+
+    /**
+     * Return criteria title
+     *
+     * @return string
+     */
+    public function get_title() {
+        return get_string('criteria_' . $this->criteriatype, 'badges');
+    }
+
+    /**
+     * Get criteria details for displaying to users
+     *
+     * @param string $short Print short version of criteria
+     * @return string
+     */
+    abstract public function get_details($short = '');
+
+    /**
+     * Add appropriate criteria options to the form
+     *
+     */
+    abstract public function get_options(&$mform);
+
+    /**
+     * Add appropriate parameter elements to the criteria form
+     *
+     */
+    public function config_options(&$mform, $param) {
+        global $OUTPUT;
+        $prefix = $this->required_param . '_';
+
+        if ($param['error']) {
+            $parameter[] =& $mform->createElement('advcheckbox', $prefix . $param['id'], '',
+                    $OUTPUT->error_text($param['name']), null, array(0, $param['id']));
+            $mform->addGroup($parameter, 'param_' . $prefix . $param['id'], '', array(' '), false);
+        } else {
+            $parameter[] =& $mform->createElement('advcheckbox', $prefix . $param['id'], '', $param['name'], null, array(0, $param['id']));
+            $parameter[] =& $mform->createElement('static', 'break_start_' . $param['id'], null, '<div style="margin-left: 3em;">');
+
+            if (in_array('grade', $this->optional_params)) {
+                $parameter[] =& $mform->createElement('static', 'mgrade_' . $param['id'], null, get_string('mingrade', 'badges'));
+                $parameter[] =& $mform->createElement('text', 'grade_' . $param['id'], '', array('size' => '5'));
+            }
+
+            if (in_array('bydate', $this->optional_params)) {
+                $parameter[] =& $mform->createElement('static', 'complby_' . $param['id'], null, get_string('bydate', 'badges'));
+                $parameter[] =& $mform->createElement('date_selector', 'bydate_' . $param['id'], "", array('optional' => true));
+            }
+
+            $parameter[] =& $mform->createElement('static', 'break_end_' . $param['id'], null, '</div>');
+            $mform->addGroup($parameter, 'param_' . $prefix . $param['id'], '', array(' '), false);
+            if (in_array('grade', $this->optional_params)) {
+                $mform->addGroupRule('param_' . $prefix . $param['id'], array(
+                    'grade_' . $param['id'] => array(array(get_string('err_numeric', 'form'), 'numeric', '', 'client'))));
+            }
+            $mform->disabledIf('bydate_' . $param['id'] . '[day]', 'bydate_' . $param['id'] . '[enabled]', 'notchecked');
+            $mform->disabledIf('bydate_' . $param['id'] . '[month]', 'bydate_' . $param['id'] . '[enabled]', 'notchecked');
+            $mform->disabledIf('bydate_' . $param['id'] . '[year]', 'bydate_' . $param['id'] . '[enabled]', 'notchecked');
+            $mform->disabledIf('param_' . $prefix . $param['id'], $prefix . $param['id'], 'notchecked');
+        }
+
+        // Set default values.
+        $mform->setDefault($prefix . $param['id'], $param['checked']);
+        if (isset($param['bydate'])) {
+            $mform->setDefault('bydate_' . $param['id'], $param['bydate']);
+        }
+        if (isset($param['grade'])) {
+            $mform->setDefault('grade_' . $param['id'], $param['grade']);
+        }
+    }
+
+    /**
+     * Add appropriate criteria elements
+     *
+     * @param stdClass $data details of various criteria
+     */
+    public function config_form_criteria($data) {
+        global $OUTPUT;
+        $agg = $data->get_aggregation_methods();
+
+        $editurl = new moodle_url('/badges/criteria_settings.php',
+                array('badgeid' => $this->badgeid, 'edit' => true, 'type' => $this->criteriatype, 'crit' => $this->id));
+        $deleteurl = new moodle_url('/badges/criteria_action.php',
+                array('badgeid' => $this->badgeid, 'delete' => true, 'type' => $this->criteriatype));
+        $editaction = $OUTPUT->action_icon($editurl, new pix_icon('t/edit', get_string('edit')), null, array('class' => 'criteria-action'));
+        $deleteaction = $OUTPUT->action_icon($deleteurl, new pix_icon('t/delete', get_string('delete')), null, array('class' => 'criteria-action'));
+
+        echo $OUTPUT->box_start();
+        if (!$data->is_locked() && !$data->is_active()) {
+            echo $OUTPUT->box($deleteaction . $editaction, array('criteria-header'));
+        }
+        echo $OUTPUT->heading($this->get_title() . $OUTPUT->help_icon('criteria_' . $this->criteriatype, 'badges'), 3, 'main help');
+
+        if (!empty($this->params)) {
+            if (count($this->params) > 1) {
+                echo $OUTPUT->box(get_string('criteria_descr_' . $this->criteriatype, 'badges',
+                        strtoupper($agg[$data->get_aggregation_method($this->criteriatype)])), array('clearfix'));
+            } else {
+                echo $OUTPUT->box(get_string('criteria_descr_single_' . $this->criteriatype , 'badges'), array('clearfix'));
+            }
+            echo $OUTPUT->box($this->get_details(), array('clearfix'));
+        }
+        echo $OUTPUT->box_end();
+    }
+
+    /**
+     * Review this criteria and decide if the user has completed
+     *
+     * @param int $userid User whose criteria completion needs to be reviewed.
+     * @return bool Whether criteria is complete
+     */
+    abstract public function review($userid);
+
+    /**
+     * Mark this criteria as complete for a user
+     *
+     * @param int $userid User whose criteria is completed.
+     */
+    public function mark_complete($userid) {
+        global $DB;
+        $obj = array();
+        $obj['critid'] = $this->id;
+        $obj['userid'] = $userid;
+        $obj['datemet'] = time();
+        if (!$DB->record_exists('badge_criteria_met', array('critid' => $this->id, 'userid' => $userid))) {
+            $DB->insert_record('badge_criteria_met', $obj);
+        }
+    }
+
+    /**
+     * Return criteria parameters
+     *
+     * @param int $critid Criterion ID
+     * @return array
+     */
+    public function get_params($cid) {
+        global $DB;
+        $params = array();
+
+        $records = $DB->get_records('badge_criteria_param', array('critid' => $cid));
+        foreach ($records as $rec) {
+            $arr = explode('_', $rec->name);
+            $params[$arr[1]][$arr[0]] = $rec->value;
+        }
+
+        return $params;
+    }
+
+    /**
+     * Delete this criterion
+     *
+     */
+    public function delete() {
+        global $DB;
+
+        // Remove any records if it has already been met.
+        $DB->delete_records('badge_criteria_met', array('critid' => $this->id));
+
+        // Remove all parameters records.
+        $DB->delete_records('badge_criteria_param', array('critid' => $this->id));
+
+        // Finally remove criterion itself.
+        $DB->delete_records('badge_criteria', array('id' => $this->id));
+    }
+
+    /**
+     * Saves intial criteria records with required parameters set up.
+     */
+    public function save($params = array()) {
+        global $DB;
+        $fordb = new stdClass();
+        $fordb->criteriatype = $this->criteriatype;
+        $fordb->method = isset($params->agg) ? $params->agg : $params['agg'];
+        $fordb->badgeid = $this->badgeid;
+        $t = $DB->start_delegated_transaction();
+
+        // Unset unnecessary parameters supplied with form.
+        if (isset($params->agg)) {
+            unset($params->agg);
+        } else {
+            unset($params['agg']);
+        }
+        unset($params->submitbutton);
+        $params = array_filter((array)$params);
+
+        if ($this->id !== 0) {
+            $cid = $this->id;
+
+            // Update criteria before doing anything with parameters.
+            $fordb->id = $cid;
+            $DB->update_record('badge_criteria', $fordb, true);
+
+            $existing = $DB->get_fieldset_select('badge_criteria_param', 'name', 'critid = ?', array($cid));
+            $todelete = array_diff($existing, array_keys($params));
+
+            if (!empty($todelete)) {
+                // A workaround to add some disabled elements that are still being submitted from the form.
+                foreach ($todelete as $del) {
+                    $name = explode('_', $del);
+                    if ($name[0] == $this->required_param) {
+                        foreach ($this->optional_params as $opt) {
+                            $todelete[] = $opt . '_' . $name[1];
+                        }
+                    }
+                }
+                $todelete = array_unique($todelete);
+                list($sql, $sqlparams) = $DB->get_in_or_equal($todelete, SQL_PARAMS_NAMED, 'd', true);
+                $sqlparams = array_merge(array('critid' => $cid), $sqlparams);
+                $DB->delete_records_select('badge_criteria_param', 'critid = :critid AND name ' . $sql, $sqlparams);
+            }
+
+            foreach ($params as $key => $value) {
+                if (in_array($key, $existing)) {
+                    $updp = $DB->get_record('badge_criteria_param', array('name' => $key, 'critid' => $cid));
+                    $updp->value = $value;
+                    $DB->update_record('badge_criteria_param', $updp, true);
+                } else {
+                    $newp = new stdClass();
+                    $newp->critid = $cid;
+                    $newp->name = $key;
+                    $newp->value = $value;
+                    $DB->insert_record('badge_criteria_param', $newp);
+                }
+            }
+        } else {
+            $cid = $DB->insert_record('badge_criteria', $fordb, true);
+            if ($cid) {
+                foreach ($params as $key => $value) {
+                    $newp = new stdClass();
+                    $newp->critid = $cid;
+                    $newp->name = $key;
+                    $newp->value = $value;
+                    $DB->insert_record('badge_criteria_param', $newp, false, true);
+                }
+            }
+         }
+         $t->allow_commit();
+    }
+
+    /**
+     * Saves intial criteria records with required parameters set up.
+     */
+    public function make_clone($newbadgeid) {
+        global $DB;
+
+        $fordb = new stdClass();
+        $fordb->criteriatype = $this->criteriatype;
+        $fordb->method = $this->method;
+        $fordb->badgeid = $newbadgeid;
+        if (($newcrit = $DB->insert_record('badge_criteria', $fordb, true)) && isset($this->params)) {
+            foreach ($this->params as $k => $param) {
+                foreach ($param as $key => $value) {
+                    $paramdb = new stdClass();
+                    $paramdb->critid = $newcrit;
+                    $paramdb->name = $key . '_' . $k;
+                    $paramdb->value = $value;
+                    $DB->insert_record('badge_criteria_param', $paramdb);
+                }
+            }
+        }
+    }
+}
diff --git a/badges/criteria/award_criteria_activity.php b/badges/criteria/award_criteria_activity.php
new file mode 100644 (file)
index 0000000..3a14e6b
--- /dev/null
@@ -0,0 +1,232 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the activity badge award criteria type class
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/completionlib.php');
+
+/**
+ * Badge award criteria -- award on activity completion
+ *
+ */
+class award_criteria_activity extends award_criteria {
+
+    /* @var int Criteria [BADGE_CRITERIA_TYPE_ACTIVITY] */
+    public $criteriatype = BADGE_CRITERIA_TYPE_ACTIVITY;
+
+    private $courseid;
+
+    public $required_param = 'module';
+    public $optional_params = array('bydate');
+
+    public function __construct($record) {
+        parent::__construct($record);
+        $this->courseid = self::get_course();
+    }
+
+    /**
+     * Gets the module instance from the database and returns it.
+     * If no module instance exists this function returns false.
+     *
+     * @return stdClass|bool
+     */
+    private function get_mod_instance($cmid) {
+        global $DB;
+        $rec = $DB->get_record_sql("SELECT md.name
+                               FROM {course_modules} cm,
+                                    {modules} md
+                               WHERE cm.id = ? AND
+                                     md.id = cm.module", array($cmid));
+
+        if ($rec) {
+            return get_coursemodule_from_id($rec->name, $cmid);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Get criteria description for displaying to users
+     *
+     * @return string
+     */
+    public function get_details($short = '') {
+        global $DB, $OUTPUT;
+        $output = array();
+        foreach ($this->params as $p) {
+            $mod = self::get_mod_instance($p['module']);
+            if (!$mod) {
+                $str = $OUTPUT->error_text(get_string('error:nosuchmod', 'badges'));
+            } else {
+                $str = html_writer::tag('b', '"' . ucfirst($mod->modname) . ' - ' . $mod->name . '"');
+                if (isset($p['bydate'])) {
+                    $str .= get_string('criteria_descr_bydate', 'badges', userdate($p['bydate'], get_string('strftimedate', 'core_langconfig')));
+                }
+            }
+            $output[] = $str;
+        }
+
+        if ($short) {
+            return implode(', ', $output);
+        } else {
+            return html_writer::alist($output, array(), 'ul');
+        }
+    }
+
+    /**
+     * Return course ID for activities
+     *
+     * @return int
+     */
+    private function get_course() {
+        global $DB;
+        $courseid = $DB->get_field('badge', 'courseid', array('id' => $this->badgeid));
+        return $courseid;
+    }
+
+    /**
+     * Add appropriate new criteria options to the form
+     *
+     */
+    public function get_options(&$mform) {
+        global $DB;
+
+        $none = true;
+        $existing = array();
+        $missing = array();
+
+        $course = $DB->get_record('course', array('id' => $this->courseid));
+        $info = new completion_info($course);
+        $mods = $info->get_activities();
+        $mids = array_map(create_function('$o', 'return $o->id;'), $mods);
+
+        if ($this->id !== 0) {
+            $existing = array_keys($this->params);
+            $missing = array_diff($existing, $mids);
+        }
+
+        if (!empty($missing)) {
+            $mform->addElement('header', 'category_errors', get_string('criterror', 'badges'));
+            $mform->addHelpButton('category_errors', 'criterror', 'badges');
+            foreach ($missing as $m) {
+                $this->config_options($mform, array('id' => $m, 'checked' => true,
+                        'name' => get_string('error:nosuchmod', 'badges'), 'error' => true));
+                $none = false;
+            }
+        }
+
+        if (!empty($mods)) {
+            $mform->addElement('header', 'first_header', $this->get_title());
+            foreach ($mods as $mod) {
+                $checked = false;
+                if (in_array($mod->id, $existing)) {
+                    $checked = true;
+                }
+                $param = array('id' => $mod->id,
+                        'checked' => $checked,
+                        'name' => ucfirst($mod->modname) . ' - ' . $mod->name,
+                        'error' => false
+                        );
+
+                if ($this->id !== 0 && isset($this->params[$mod->id]['bydate'])) {
+                    $param['bydate'] = $this->params[$mod->id]['bydate'];
+                }
+
+                if ($this->id !== 0 && isset($this->params[$mod->id]['grade'])) {
+                    $param['grade'] = $this->params[$mod->id]['grade'];
+                }
+
+                $this->config_options($mform, $param);
+                $none = false;
+            }
+        }
+
+        // Add aggregation.
+        if (!$none) {
+            $mform->addElement('header', 'aggregation', get_string('method', 'badges'));
+            $agg = array();
+            $agg[] =& $mform->createElement('radio', 'agg', '', get_string('allmethodactivity', 'badges'), 1);
+            $agg[] =& $mform->createElement('radio', 'agg', '', get_string('anymethodactivity', 'badges'), 2);
+            $mform->addGroup($agg, 'methodgr', '', array('<br/>'), false);
+            if ($this->id !== 0) {
+                $mform->setDefault('agg', $this->method);
+            } else {
+                $mform->setDefault('agg', BADGE_CRITERIA_AGGREGATION_ANY);
+            }
+        }
+
+        return array($none, get_string('error:noactivities', 'badges'));
+    }
+
+    /**
+     * Review this criteria and decide if it has been completed
+     *
+     * @param int $userid User whose criteria completion needs to be reviewed.
+     * @return bool Whether criteria is complete
+     */
+    public function review($userid) {
+        global $DB;
+        $completionstates = array(COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS);
+        $course = $DB->get_record('course', array('id' => $this->courseid));
+
+        if ($course->startdate > time()) {
+            return false;
+        }
+
+        $info = new completion_info($course);
+
+        $overall = false;
+        foreach ($this->params as $param) {
+            $cm = new stdClass();
+            $cm->id = $param['module'];
+
+            $data = $info->get_data($cm, false, $userid);
+            $check_date = true;
+
+            if (isset($param['bydate'])) {
+                $date = $data->timemodified;
+                $check_date = ($date <= $param['bydate']);
+            }
+
+            if ($this->method == BADGE_CRITERIA_AGGREGATION_ALL) {
+                if (in_array($data->completionstate, $completionstates) && $check_date) {
+                    $overall = true;
+                    continue;
+                } else {
+                    return false;
+                }
+            } else if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) {
+                if (in_array($data->completionstate, $completionstates) && $check_date) {
+                    return true;
+                } else {
+                    $overall = false;
+                    continue;
+                }
+            }
+        }
+
+        return $overall;
+    }
+}
diff --git a/badges/criteria/award_criteria_course.php b/badges/criteria/award_criteria_course.php
new file mode 100644 (file)
index 0000000..1ad58a5
--- /dev/null
@@ -0,0 +1,178 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the course completion badge award criteria type class
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/completionlib.php');
+require_once($CFG->dirroot . '/grade/querylib.php');
+require_once($CFG->libdir . '/gradelib.php');
+
+/**
+ * Badge award criteria -- award on course completion
+ *
+ */
+class award_criteria_course extends award_criteria {
+
+    /* @var int Criteria [BADGE_CRITERIA_TYPE_COURSE] */
+    public $criteriatype = BADGE_CRITERIA_TYPE_COURSE;
+
+    public $required_param = 'course';
+    public $optional_params = array('grade', 'bydate');
+
+    /**
+     * Add appropriate form elements to the criteria form
+     *
+     * @param moodleform $mform  Moodle forms object
+     * @param stdClass $data details of various modules
+     */
+    public function config_form_criteria($data) {
+        global $OUTPUT;
+
+        $editurl = new moodle_url('/badges/criteria_settings.php', array('badgeid' => $this->badgeid, 'edit' => true, 'type' => $this->criteriatype, 'crit' => $this->id));
+        $deleteurl = new moodle_url('/badges/criteria_action.php', array('badgeid' => $this->badgeid, 'delete' => true, 'type' => $this->criteriatype));
+        $editaction = $OUTPUT->action_icon($editurl, new pix_icon('t/edit', get_string('edit')), null, array('class' => 'criteria-action'));
+        $deleteaction = $OUTPUT->action_icon($deleteurl, new pix_icon('t/delete', get_string('delete')), null, array('class' => 'criteria-action'));
+
+        echo $OUTPUT->box_start();
+        if (!$data->is_locked() && !$data->is_active()) {
+            echo $OUTPUT->box($deleteaction . $editaction, array('criteria-header'));
+        }
+        echo $OUTPUT->heading($this->get_title() . $OUTPUT->help_icon('criteria_' . $this->criteriatype, 'badges'), 3, 'main help');
+
+        if (!empty($this->params)) {
+            echo $OUTPUT->box(get_string('criteria_descr_' . $this->criteriatype, 'badges') . $this->get_details(), array('clearfix'));
+        }
+        echo $OUTPUT->box_end();
+    }
+
+    /**
+     * Get criteria details for displaying to users
+     *
+     * @return string
+     */
+    public function get_details($short = '') {
+        global $DB;
+        $param = reset($this->params);
+
+        $course = $DB->get_record('course', array('id' => $param['course']));
+        $str = '"' . $course->fullname . '"';
+        if (isset($param[