diff --git a/core/modules/project_browser/config/project_browser.settings.yml b/core/modules/project_browser/config/project_browser.settings.yml new file mode 100644 index 0000000..d70f477 --- /dev/null +++ b/core/modules/project_browser/config/project_browser.settings.yml @@ -0,0 +1,8 @@ +cache_lifetime: 86400 +# @todo Change the link once drupal.org is ready. +server: + default: + url: http://drupal:drupal@pbs-drupal_7.redesign.devdrupal.org/project_browser/server + name: Drupal.org + method: json + list: [] diff --git a/core/modules/project_browser/css/project_browser.admin.css b/core/modules/project_browser/css/project_browser.admin.css new file mode 100644 index 0000000..0b4e08f --- /dev/null +++ b/core/modules/project_browser/css/project_browser.admin.css @@ -0,0 +1,174 @@ +/* -------------- Project Browser ------------- */ + +#project-browser-install-button-form { + clear: both; + margin: 10px 0 0; +} +#project-browser-install-queue { + margin: 5px 0 0; +} +.project-browser-install-queue-item { + margin: 5px 0; +} +.project-browser-install-queue-items { + clear: both; + margin: 0 0 15px; +} +.project-browser-install-link { + clear: both; +} +.project-browser-install-queue-item a { + padding-left: 20px; /* LTR */ + background: url("../images/red-x.png") no-repeat scroll left 2px transparent; +} +.project-browser-selected-release { + display: none; +} +.project-browser-show-releases-link { + cursor: pointer; +} + +/* ------------------ List Styles ------------------ */ + +div.item-list ul.project-browser-sort-list, +div.item-list ul.project-browser-servers-list { + margin: 0; +} +div.item-list ul.project-browser-sort-list li, +div.item-list ul.project-browser-servers-list li { + display: inline; + list-style-image: none; + margin-right: 10px; /* LTR */ +} +div.item-list ul.project-browser-sort-list li a, +div.item-list ul.project-browser-servers-list li a { + color: #0074bd; +} +div.item-list ul.project-browser-sort-list li.sort-active a, +div.item-list ul.project-browser-servers-list li.server-active a { + color: #000000; +} +div.item-list ul.project-browser-sort-list li.sort-header, +div.item-list ul.project-browser-servers-list li.server-header { + font-weight: bold; +} +div.item-list ul.project-browser-sort-list li.sort-asc { + background: url("../images/arrow-asc.png") no-repeat scroll right 2px transparent; + padding-right: 15px; /* LTR */ +} +div.item-list ul.project-browser-sort-list li.sort-desc { + background: url("../images/arrow-desc.png") no-repeat scroll right 2px transparent; + padding-right: 15px; /* LTR */ +} +a.show-more { + float: right; /* LTR */ +} + +/* ------------------ Project ----------------- */ + +.project-extra { + clear: both; + color: gray; + text-align: right; /* LTR */ +} +.project-author { + color: gray; + font-size: 0.9em; +} +.project-updated { + color: gray; + font-size: 0.9em; +} +.project-image { + float: left; /* LTR */ + margin-right: 10px; /* LTR */ +} +.project-image img { + max-height: 150px; + max-width: 200px; +} +#project-browser-main ul, #project-browser-main ol { + list-style-position: inside; +} +.project-item { + border-top: 1px solid #e0e0d8; + clear: both; + padding: 10px; + position: relative; +} +.project-item-first { + clear: both; + padding: 10px; + position: relative; +} +div.project-status { + position: absolute; + right: 10px; /* LTR */ + text-align: right; /* LTR */ + top: 5px; +} +div.project-information { + +} +.project-browser-install-main { + padding: 10px; + float: right; /* LTR */ + width: 76%; +} +.install-disabled { + color: gray; +} +.install-enabled { + color: green; +} +.project-browser-install-sidebar-left { + float: left; /* LTR */ + width: 19%; + padding: 10px; +} +div.project-title { + font-size: 18px; +} +fieldset#edit-category, +fieldset#edit-version { + border: none; +} +div.install-item-prefix { + float:left; /* LTR */ + margin-left: 50px; /* LTR */ +} + +/* -------------- Project Browser ------------- */ + +#project-browser-main div.form-item-install { + float: right; /* LTR */ +} +#project-browser div.project-browser-region { + min-height: 1px; +} +#project-browser div#project-browser-main { + width: 75%; + float: left; /* LTR */ + margin-right: 1%; /* LTR */ +} +#project-browser div#project-browser-sidebar-right { + width: 23%; + float: right; /* LTR */ +} +#project-browser div.project_browser_block { + margin-bottom: 20px; + border: 1px solid #cccccc; +} +#project-browser .project-browser-region .project_browser_block { + clear: both; +} +#project-browser div.project_browser_block h2 { + float: none; + font-size: 1em; + margin: 0; + padding: 3px 10px; + background: none repeat scroll 0 0 #e0e0d8; +} +#project-browser div#project-browser-sidebar-right div.project_browser_block div.content { + padding: 5px 10px; +} diff --git a/core/modules/project_browser/images/arrow-asc.png b/core/modules/project_browser/images/arrow-asc.png new file mode 100644 index 0000000..a3ccabc --- /dev/null +++ b/core/modules/project_browser/images/arrow-asc.png @@ -0,0 +1,5 @@ +PNG + + IHDR H%v?PLTEkR%tRNS@fIDATc`@L L LL + +Li|q)IENDB` \ No newline at end of file diff --git a/core/modules/project_browser/images/arrow-desc.png b/core/modules/project_browser/images/arrow-desc.png new file mode 100644 index 0000000..2edbb17 --- /dev/null +++ b/core/modules/project_browser/images/arrow-desc.png @@ -0,0 +1,4 @@ +PNG + + IHDR H%v?PLTEkR%tRNS@fIDATxc`@0`o``g`` +KzHIENDB` \ No newline at end of file diff --git a/core/modules/project_browser/images/circle.png b/core/modules/project_browser/images/circle.png new file mode 100644 index 0000000..6b2d63f --- /dev/null +++ b/core/modules/project_browser/images/circle.png @@ -0,0 +1,3 @@ +PNG + + IHDR6|JPLTEE<tRNS@fIDAT[c`cfe ( $b IENDB` \ No newline at end of file diff --git a/core/modules/project_browser/images/red-x.png b/core/modules/project_browser/images/red-x.png new file mode 100644 index 0000000..486390c --- /dev/null +++ b/core/modules/project_browser/images/red-x.png @@ -0,0 +1,7 @@ +PNG + + IHDRaIDATxڥ?HBQƣr #p(2hAk +!GP2ISiъ +:W_O"sw^gH^3כT(Ppc"31!h>_af Epd!H ߯! LNj Mp7<ǃpl2LIlo#6; a!vp˅r4 +u +RpNE>yHnlG.RWR B?%a<a2#=4}DrOu_ TWXTPjmA(`%xZZjKjFUtNjX'';GWXCW83p~zo$B{;rtAjX) |!uwsֆhx-=2ةQ-riIMQ(r?6{L~_ͧGIENDB` \ No newline at end of file diff --git a/core/modules/project_browser/js/project_browser_more_link.js b/core/modules/project_browser/js/project_browser_more_link.js new file mode 100644 index 0000000..c389cf1 --- /dev/null +++ b/core/modules/project_browser/js/project_browser_more_link.js @@ -0,0 +1,41 @@ +/** + * @file + * Makes the 'More' button toggle the display of the full project texts. + * + * The project descriptions are by default trimmed to a certain height. When + * the user clicks the more link, then the full text is shown. + */ +(function ($) { + + "use strict"; + + Drupal.behaviors.projectBrowserMoreLink = { + attach: function (context, settings) { + // The height of the content block when it's not expanded + var adjustheight = 80; + // The "more" link text + var moreText = Drupal.t('More'); + // The "less" link text + var lessText = Drupal.t('Less'); + + $(".project-information .project-description").each(function(index) { + if ($(this).height() > adjustheight) + { + $(this).css('height', adjustheight).css('overflow', 'hidden'); + $(this).parents(".project-information").append(''); + } + }); + + $("a.show-more").text(moreText); + + $(".show-more").toggle(function() { + $(this).parents("div:first").find(".project-description").css('height', 'auto').css('overflow', 'visible'); + $(this).text(lessText); + }, function() { + $(this).parents("div:first").find(".project-description").css('height', adjustheight).css('overflow', 'hidden'); + $(this).text(moreText); + }); + } + }; + +})(jQuery); diff --git a/core/modules/project_browser/js/project_browser_select_releases.js b/core/modules/project_browser/js/project_browser_select_releases.js new file mode 100644 index 0000000..05a4614 --- /dev/null +++ b/core/modules/project_browser/js/project_browser_select_releases.js @@ -0,0 +1,22 @@ +/** + * @file + * Shows the default Select Releases page until 'Show All Releases' is clicked. + */ +(function ($) { + + "use strict"; + + Drupal.behaviors.projectBrowserSelectReleases = { + attach: function (context, settings) { + $('.project-browser-releases-wrapper').hide(); + $('.project-browser-selected-release').show(); + + $('.project-browser-show-releases-link').click(function() { + var target = $(this).attr('rel'); + $('.project-browser-release-' + target).show(); + $('.project-browser-selected-release-' + target).hide(); + }) + } + }; + +})(jQuery); diff --git a/core/modules/project_browser/lib/Drupal/project_browser/Tests/ProjectBrowserTest.php b/core/modules/project_browser/lib/Drupal/project_browser/Tests/ProjectBrowserTest.php new file mode 100644 index 0000000..b39701f --- /dev/null +++ b/core/modules/project_browser/lib/Drupal/project_browser/Tests/ProjectBrowserTest.php @@ -0,0 +1,111 @@ + 'Project Browser Install Project Test', + 'description' => 'Attempts to install a project.', + 'group' => 'Project Browser', + ); + } + + function setUp() { + parent::setUp(); + + // Set the default server variable. + $server_url = url('project_browser_test/query', array('absolute' => TRUE)); + $config = config('project_browser.settings'); + $config->set('server.default', array( + 'name' => 'Test Server', + 'method' => 'json', + 'url' => $server_url, + )); + $config->save(); + + // Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'use project browser', + )); + $this->drupalLogin($this->privilegedUser); + } + + /** + * Tests a search for the Token string, ensuring that token shows. + */ + public function testProjectBrowserSearchToken() { + // Search for 'token'. + $edit = array(); + $edit['search_text'] = 'token'; + $this->drupalPost('admin/modules/project-browser/modules', $edit, t('Filter')); + $this->assertResponse(200); + $this->assertText('Showing 1 to'); + $this->assertText('Tokens are small bits of text'); + } + + /** + * Tests that the Projects Listing displays properly with the default filters. + */ + public function testProjectBrowserGetProjects() { + // Attempt to fetch the default projects. + $edit = array(); + $edit['search_text'] = ''; + $this->drupalPost('admin/modules/project-browser/modules', $edit, t('Filter')); + $this->assertResponse(200); + $this->assertText('Showing 1 to'); + } + + /** + * Tests to make sure that the enabled project detection works. + */ + public function testProjectBrowserProjectEnabled() { + // Make sure project enabled detection works. + module_load_include('inc', 'project_browser', 'project_browser'); + $this->assertTrue(_project_browser_is_project_enabled('module', 'project_browser'), 'Make sure project enabled detection works.'); + } + + /** + * Tests that adding and removing items from the queue works properly. + */ + public function testProjectBrowserAddRemoveQueue() { + // Refresh the page. + $this->drupalGet('admin/modules/project-browser/modules'); + + // Simulate adding a project to the install queue. + $this->drupalGet('project-browser/nojs/install-queue/add/views', array('query' => array('destination' => 'admin/modules/project-browser'))); + $this->assertNoText('Install queue is empty.'); + $this->assertNoText('Error: The project was not found.'); + + // Simulate removing a project from the install queue. + $this->drupalGet('project-browser/nojs/install-queue/remove/views', array('query' => array('destination' => 'admin/modules/project-browser'))); + $this->assertText('Install queue is empty.'); + $this->assertNoText('Error: The project was not found.'); + } + +} diff --git a/core/modules/project_browser/project_browser.admin.inc b/core/modules/project_browser/project_browser.admin.inc new file mode 100644 index 0000000..880f240 --- /dev/null +++ b/core/modules/project_browser/project_browser.admin.inc @@ -0,0 +1,41 @@ + 'details', + '#title' => t('Main settings'), + '#collapsed' => FALSE, + ); + // Because this is a pluggable system, there can be other repositories besides + // Drupal.org. + $form['main']['server_list'] = array( + '#type' => 'textarea', + '#title' => t('Repositories'), + '#default_value' => config('project_browser.settings')->get('server.list'), + '#description' => t("Add new repositories to use for the Project Browser, one per line, in the 'url|method|Site Name' format. Drupal.org is added by default, and doesn't need to be set here."), + '#required' => FALSE, + ); + + return system_config_form($form, $form_state); +} + +/** + * Form submission handler for project_browser_admin_form(). + */ +function project_browser_admin_form_submit($form, &$form_state) { + $config = config('project_browser.settings'); + $config->set('server.list', $form_state['values']['server_list']); + $config->save(); +} diff --git a/core/modules/project_browser/project_browser.inc b/core/modules/project_browser/project_browser.inc new file mode 100644 index 0000000..57118b9 --- /dev/null +++ b/core/modules/project_browser/project_browser.inc @@ -0,0 +1,1073 @@ + $queued_projects)); +} + +/** + * Form constructor for the install button for the Install Queue block. + * + * Since the selected projects are stored in the $_SESSION variable, no real + * processing is done, we just redirect to the install/select_versions page. + * + * @see project_browser_preprocess_project_browser_install_queue() + * @ingroup forms + */ +function project_browser_install_button_form($form, &$form_state) { + $form['#attributes']['id'] = 'project-browser-install-button-form'; + $form['submit'] = array( + '#type' => 'submit', + '#value' => 't(Install)', + ); + + $form['#action'] = url('admin/modules/project-browser/install/select_versions'); + + return $form; +} + +/** + * Form constructor for the filters form. + * + * This includes categories and the string search box, and the $type is stored. + * + * @param string $type + * The type of project (module or theme). + * + * @see project_browser_filters_form_submit() + * @ingroup forms + */ +function project_browser_filters_form($form, &$form_state, $type) { + $form['search_text'] = array( + '#type' => 'textfield', + '#size' => '25', + '#title' => t('Search String'), + '#default_value' => isset($_SESSION['project_browser_text_filter_' . $type]) ? $_SESSION['project_browser_text_filter_' . $type] : '', + ); + + // Add the category filter if there are categories. + if ($categories = project_browser_get_categories($type)) { + array_unshift($categories, t('-- Any --')); + $form['category'] = array( + '#type' => 'select', + '#title' => t('Category'), + '#options' => $categories, + '#default_value' => isset($_SESSION['project_browser_category_filter_' . $type]) ? $_SESSION['project_browser_category_filter_' . $type] : NULL, + ); + } + + $form['project_type'] = array( + '#type' => 'value', + '#value' => $type, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Filter'), + ); + + return $form; +} + +/** + * Form submission handler for project_browser_filters_form(). + * + * All that we do here is store the selected category and search string in the + * $_SESSION variable. + */ +function project_browser_filters_form_submit($form, &$form_state) { + $type = $form_state['values']['project_type']; + if (!empty($form_state['values']['category'])) { + $_SESSION['project_browser_category_filter_' . $type] = array($form_state['values']['category'] => $form_state['values']['category']); + } + else { + $_SESSION['project_browser_category_filter_' . $type] = array(); + } + $_SESSION['project_browser_text_filter_' . $type] = $form_state['values']['search_text']; +} + +/** + * Builds a themed sort widget for the results. + * + * These are links which can be clicked/toggled to select and change direction. + * + * @param array $sort_options + * An array of sort options. + * @param string $current_sort_option + * The currently selected sort option, which is a string. + * @param string $current_sort_direction + * The currently selected sort direction, which is a string. + * + * @return string + * A themed list of sort options, as a string. + */ +function project_browser_get_sort_widget(array $sort_options, $current_sort_option, $current_sort_direction) { + $sort_list = array(); + $sort_list[] = array( + '#markup' => t('Sort by:'), + '#wrapper_attributes' => array( + 'class' => array('sort-header'), + ), + ); + + foreach ($sort_options as $sort_option) { + $classes = array(); + $query = array( + 'order_by' => $sort_option['method'], + 'sort' => $sort_option['default_sort'], + ); + + // If the sort option is currently active, handle it differently. + if ($current_sort_option == $sort_option['method']) { + $classes[] = 'sort-active'; + $classes[] = 'sort-' . $current_sort_direction; + + // Set the direction of the sort link to the opposite of what it currently + // is. + if ($current_sort_direction == $query['sort']) { + if ($query['sort'] == 'desc') { + $query['sort'] = 'asc'; + } + else { + $query['sort'] = 'desc'; + } + } + } + else { + $classes[] = 'sort-inactive'; + } + $sort_list[] = array( + '#type' => 'link', + '#title' => $sort_option['name'], + '#href' => current_path(), + '#options' => array('query' => $query, 'class' => array()), + '#wrapper_attributes' => array( + 'class' => $classes, + ), + ); + } + + // @todo Should return a render array. + return array( + '#theme' => 'item_list', + '#items' => $sort_list, + '#type' => 'ul', + '#attributes' => array('class' => array('project-browser-sort-list')), + ); +} + +/** + * Builds a themed widget to select the server. + * + * This is only called if there are more than one server enabled in the + * settings. + * + * @param array $servers + * An array of servers that should be available as options. + * @param string $current_server + * The currently selected server, which is a string. + * + * @return string + * A themed server select widget, as a string. + */ +function project_browser_get_server_widget(array $servers, $current_server) { + $list = array(); + $list[] = array( + 'data' => t('Repository:'), + 'class' => array('server-header') + ); + $current_path = current_path(); + + // @todo Improve comment or variable name of $i. + // Interate through the repository number and current server number. + $i = 0; + + foreach ($servers as $url => $server) { + $classes = array(); + $query = array( + 'repository' => $i, + ); + + // If the sort option is currently active, handle it differently. + if ($current_server == $i) { + $classes[] = 'server-active'; + } + else { + $classes[] = 'server-inactive'; + } + + $list[] = array( + '#type' => 'link', + '#title' => $server['name'], + '#href' => $current_path, + '#options' => array('query' => $query, 'class' => array()), + '#wrapper_attributes' => array( + 'class' => $classes, + ), + ); + + $i += 1; + } + + // @todo Should return a render array. + return array( + '#theme' => 'item_list', + '#items' => $list, + '#type' => 'ul', + '#attributes' => array('class' => array('project-browser-servers-list')), + ); +} + +/** + * Builds and returns an array of sort options, keyed by method. + * + * @param bool $full + * (optional) Set this to TRUE if you want to get all of the supported sort + * methods. Defaults to FALSE. + * + * @return array + * An array of sort options, keyed by method. + */ +function project_browser_get_sort_options($full = FALSE) { + $sort_options = array( + 'score' => array('method' => 'score', 'name' => t('Relevancy'), 'default_sort' => 'desc'), + 'usage' => array('method' => 'usage', 'name' => t('Most installed'), 'default_sort' => 'desc'), + 'title' => array('method' => 'title', 'name' => t('Title'), 'default_sort' => 'asc'), + 'name' => array('method' => 'name', 'name' => t('Author'), 'default_sort' => 'asc'), + 'latest_release' => array('method' => 'latest_release', 'name' => t('Latest release'), 'default_sort' => 'desc'), + ); + + if ($full) { + $sort_options['type'] = array( + 'method' => 'type', + 'name' => t('Type'), + 'default_sort' => 'asc' + ); + $sort_options['created'] = array( + 'method' => 'created', + 'name' => t('Date created'), + 'default_sort' => 'asc' + ); + $sort_options['latest_activity'] = array( + 'method' => 'latest_activity', + 'name' => t('Latest build'), + 'default_sort' => 'desc' + ); + } + + return $sort_options; +} + +/** + * Builds an array of all available categories for a project type. + * + * @param string $type + * The type of project to get the categories for. Example: 'module' or + * 'theme'. + * + * @return array|null + * Array containing all available categories or NULL if no categories. + */ +function project_browser_get_categories($type) { + $categories = array(); + + // Get which server to use from $_SESSION. + $use_server = isset($_SESSION['project_browser_server_filter']) ? $_SESSION['project_browser_server_filter'] : 0; + + $categories_raw = project_browser_fetch_categories($type, $use_server); + + if (is_array($categories_raw) && !empty($categories_raw)) { + foreach ($categories_raw as $url => $cats) { + foreach ($cats as $key => $value) { + // Create a new key so that there are no duplicate categories from + // different sites. + $new_key = preg_replace('/[^a-z0-9_]+/', '_', strtolower($value)); + $categories[$new_key] = $value; + } + } + } + + if (is_array($categories) && !empty($categories)) { + ksort($categories); + + return $categories; + } + return NULL; +} + +/** + * Prepares the categories for sending to the servers as filters. + * + * @param array $raw_cats + * An array of categories from $form_state['values']. + * @param string $type + * The type of project for which to prepare the categories, as a string, for + * example, string 'module' or 'theme'. + * + * @return array + * An array of server categories, keyed by server url. + */ +function project_browser_prepare_categories(array $raw_cats, $type) { + $categories = project_browser_fetch_categories($type); + + // Set the value of the categories to true if it is selected. + foreach ($categories as $url => $cats) { + foreach ($cats as $key => $value) { + $new_key = preg_replace('/[^a-z0-9_]+/', '_', strtolower($value)); + if (isset($raw_cats[$new_key]) && $raw_cats[$new_key]) { + $categories[$url][$key] = TRUE; + } + else { + unset($categories[$url][$key]); + } + } + + // Unset the parent if there are no children. + if (empty($categories[$url])) { + unset($categories[$url]); + } + } + + return $categories; +} + +/** + * Checks if a project is enabled. + * + * @param string $type + * The type of project, as a string. For example, could be 'theme' or + * 'module'. + * @param string $name + * The short name of the project, as a string. + * + * @return bool + * TRUE if the project is enabled, FALSE otherwise. + */ +function _project_browser_is_project_enabled($type, $name) { + switch ($type) { + case 'module': + return module_exists($name); + break; + + case 'theme': + $themes = list_themes(); + return isset($themes[$name]); + break; + } + return FALSE; +} + +/** + * Gets the currently listed projects from the session. + * + * @return array + * An array of listed projects from the $_SESSION variable. + */ +function project_browser_get_listed_projects() { + if (isset($_SESSION['project_browser_listed_projects'])) { + return $_SESSION['project_browser_listed_projects']; + } + + return array(); +} + +/** + * Gets the currently queued projects from the $_SESSION variable. + * + * @param string|null $type + * (optional) The type of project (module or theme). Defaults to NULL, which + * will return projects of all types. + * + * @return array + * An array of projects that are queued for install. + */ +function project_browser_get_queued_projects($type = NULL) { + $projects = array(); + + if (isset($_SESSION['project_browser_install_list'])) { + foreach ($_SESSION['project_browser_install_list'] as $project) { + if (is_array($project) && !empty($project)) { + if (isset($type) && $type != $project['type']) { + continue; + } + else { + $projects[$project['name']] = $project; + } + } + } + } + + return $projects; +} + +/** + * Gets a release from a project and a release_name. + * + * @param string $release_name + * The name of the release, such as '7.x-1.2'. + * @param array $project + * The $project data array. + * + * @return array|null + * The release data array or NULL if the release doesn't exist. + */ +function project_browser_get_release($release_name, array $project) { + $release_data = project_browser_get_project_release_data($project); + + return isset($release_data['releases'][$release_name]) ? $release_data['releases'][$release_name] : NULL; +} + +/** + * Gets the newly installed projects from the session. + * + * @return array + * An array of all of the newly installed projects. + */ +function project_browser_get_installed_projects() { + $projects = array(); + + if (isset($_SESSION['project_browser_installed_projects'])) { + foreach ($_SESSION['project_browser_installed_projects'] as $project) { + if (is_array($project) && !empty($project)) { + $projects[$project['name']] = $project; + } + } + } + + return $projects; +} + +/** + * Adds a project to the install queue $_SESSION variable. + * + * @param array $project + * An array of $project data for a single project. + */ +function project_browser_install_queue_add(array $project) { + $_SESSION['project_browser_install_list'][$project['name']] = $project; +} + +/** + * Removes a project from the install queue $_SESSION variable. + * + * @param string $project_name + * The name of the project to remove, as a string, such as 'views'. + */ +function project_browser_install_queue_remove($project_name) { + if (isset($_SESSION['project_browser_install_list'][$project_name])) { + unset($_SESSION['project_browser_install_list'][$project_name]); + } +} + +/** + * Gets the currently queued releases from the $_SESSION variable. + * + * @return array + * An array of the currently selected releases. + */ +function project_browser_get_queued_releases() { + $releases = array(); + + if (isset($_SESSION['project_browser_install_releases_list'])) { + foreach ($_SESSION['project_browser_install_releases_list'] as $short_name => $info) { + if (is_array($info['project']) && !empty($info['project'])) { + $releases[$short_name] = $info; + } + } + } + + return $releases; +} + +/** + * Fetches results from the servers based on the parameters passed in. + * + * @param array $filters + * An associative array of queries to use to filter results, containing: + * - version: The Major Version of Drupal that is running on the Client. + * Example: '7' or '8'. + * - text: The text that was entered as the search query, or '' if none. + * Example: 'Link field'. + * - categories: An array of categories that were selected, if any. + * - type: The type of project being searched for. Example: 'module' or + * 'theme'. + * - page: The zero-based page number. + * - requested: How many results are requested per page. + * + * @return array + * An array of results formatted like: + * - total: The total number of results found for the filters. + * - projects: An array of projects returned for this page request, + * containing: + * - KEY: A project array keyed by the machine name: + * - type: The type of project this is. Can be 'module' or 'theme'. + * - title: The title of the project. + * - name: The machine name of the project. + * - author: The author's name. + * - description: Long project description text. + * - image: Absolute url to the image, if any. + * - usage: How many sites are using module. + * - project url: Absolute url to the project page, if any. + * - project status url: The absolute url of the update checker, + * formatted like how Drupal.org Update Status does it. + * - last updated: UNIX Timestamp of when the project was last updated. + * - maintenance status: Maintenance status. + * - development status: Development status. + * - rating: A rating on a scale of 1 to 10 of the project, if available + * - dependencies: An array of the dependencies of this module, by + * project shortname, if available. + */ +function project_browser_fetch_results(array $filters) { + $servers = project_browser_get_servers($filters['server']); + // Attempt to retrieve the cached version of this page. + $cid = md5(serialize(array_merge($filters, $servers))); + + if ($cache = cache()->get($cid)) { + return $cache->data; + } + + $results = array( + 'projects' => array(), + 'total' => 0, + ); + + unset($filters['server']); + + foreach ($servers as $url => $server) { + $local_filters = $filters; + + // We are not using this right now because we only expect to handle 1 + // server at a time currently. + // $local_filters['requested'] = floor($filters['requested'] / count($servers)); + + // Send only the relevant categories to the server. + if (isset($filters['categories'])) { + if (!isset($filters['categories'][$url])) { + // Don't call a server for results if categories are being used, and + // none of them belong to the server. + continue; + } + $local_filters['categories'] = $filters['categories'][$url]; + } + + // Use XMLRPC if it is set. + if ($server['method'] == 'xmlrpc') { + $results_raw = xmlrpc($url, array( + 'project_browser_server.fetch_results' => array($local_filters), + )); + + // Check for errors. + if ($error = xmlrpc_error() && $error->is_error) { + drupal_set_message(t("Encountered an error when trying to fetch results from @name. Error @code : @message", + array('@name' => $server['name'], '@code' => $error->code, '@message' => $error->message))); + continue; + } + } + + // Use json if it is set. + if ($server['method'] == 'json') { + $local_filters['method'] = 'query'; + if (isset($local_filters['categories'])) { + $local_filters['categories'] = serialize($local_filters['categories']); + } + + $query_url = $url . '/query/' . $local_filters['type'] . '/8?' . http_build_query($local_filters, FALSE, '&'); + $response = drupal_http_request($query_url); + if ($response->code == '200') { + $results_raw = drupal_json_decode($response->data); + } + else { + drupal_set_message(t("Encountered an error when trying to fetch results from @name. Error @code : @message", + array('@name' => $server['name'], '@code' => $response->code, '@message' => $response->error))); + continue; + } + } + + if (isset($results_raw['total'])) { + $results['total'] += $results_raw['total']; + } + + if (!empty($results_raw['projects']) && is_array($results_raw['projects'])) { + // Merge the results. + $results['projects'] = array_merge($results['projects'], $results_raw['projects']); + } + } + + cache()->set($cid, $results, REQUEST_TIME + config('project_browser.settings')->get('cache_lifetime')); + + return $results; +} + +/** + * Fetches categories from the servers based on the type of project. + * + * @param string $type + * The type of project we are getting categories for, a string, which can be + * 'module' or 'theme'. + * @param string $use_server + * (optional) The server to use, which is a string. Defaults to 'all'. + * + * @return array + * Returns an array of the categories. + */ +function project_browser_fetch_categories($type, $use_server = 'all') { + $servers = project_browser_get_servers($use_server); + + // Attempt to retrieve the cached version of this page. + $cid = md5('categories_' . $type . serialize($servers)); + + if ($cache = cache()->get($cid)) { + return $cache->data; + } + else { + $categories = array(); + + foreach ($servers as $url => $server) { + $categories_raw = array(); + // Use xmlrpc if it is set. + if ($server['method'] == 'xmlrpc') { + $categories_raw = xmlrpc($url, array( + 'project_browser_server.fetch_categories' => array($type), + )); + + // Check for errors. + if ($error = xmlrpc_error() && $error->is_error) { + drupal_set_message(t("Encountered an error when trying to fetch categories from @name. Error @code : @message", + array('@name' => $server['name'], '@code' => $error->code, '@message' => $error->message))); + continue; + } + } + + // Use json if it is set. + if ($server['method'] == 'json') { + $params = array( + 'method' => 'categories', + 'type' => $type, + ); + $response = drupal_http_request($url . '/categories/' . $type . '?' . http_build_query($params, FALSE, '&')); + if ($response->code == '200') { + $categories_raw = drupal_json_decode($response->data); + } + else { + drupal_set_message(t("Encountered an error when trying to fetch categories from @name. Error @code : @message", + array('@name' => $server['name'], '@code' => $response->code, '@message' => $response->error))); + continue; + } + } + + if (is_array($categories_raw) && !empty($categories_raw)) { + $categories[$url] = $categories_raw; + } + } + + // Cache this for 24 hours. + cache()->set($cid, $categories, REQUEST_TIME + config('project_browser.settings')->get('cache_lifetime')); + } + + return $categories; +} + +/** + * Gets an array of servers to use for fetching results. + * + * @param string $use_server + * (optional) The server to use, which is a string. Defaults to 'all'. + * + * @return array + * Returns an associative array of servers, populated from the + * project_browser_servers variable, in 'url => name' format. + */ +function project_browser_get_servers($use_server = 'all') { + $default_server = config('project_browser.settings')->get('server.default'); + $url = $default_server['url']; + unset($default_server['url']); + $servers[$url] = $default_server; + if ($servers_raw = config('project_browser.settings')->get('server.list')) { + // Process the variable and add the servers to the list. + $custom_servers = array(); + + $list = explode("\n", $servers_raw); + $list = array_map('trim', $list); + $list = array_filter($list, 'strlen'); + + foreach ($list as $position => $text) { + $method = $name = $url = FALSE; + + $matches = array(); + if (preg_match('/(.*)\|(.*)\|(.*)/', $text, $matches)) { + $url = $matches[1]; + $method = $matches[2]; + $name = $matches[3]; + $custom_servers[$url] = array('name' => $name, 'method' => $method); + } + } + + $servers = array_merge($servers, $custom_servers); + } + + // Filter out servers if necessary. + // @todo Improve comment or variable name of $i. + // Iterate through the server number. + if ($use_server !== 'all') { + $i = 0; + foreach ($servers as $url => $server) { + if ($use_server != $i) { + unset($servers[$url]); + } + $i += 1; + } + } + + return $servers; +} + +/** + * Gets the available releases based on project status url. + * + * @param array $project + * An array with inforamation for the project to get the releases for. + * + * @return array|false + * An array of releases for this project, or FALSE if it can't be found. + */ +function project_browser_get_project_release_data(array $project) { + $releases = array(); + $project['project_type'] = $project['type']; + $project['includes'] = array(); + + // Build the releases cache for this project. + module_load_include('inc', 'update', 'update.fetch'); + if (_update_process_fetch_task($project)) { + $data = _update_cache_get('available_releases::' . $project['name']); + if (isset($data->data) && isset($data->data['releases']) && is_array($data->data['releases'])) { + return $data->data; + } + } + + return FALSE; +} + +/** + * Downloads and installs a project using the update module. + * + * @todo Use this new method once the patch in + * http://drupal.org/node/1846078 is committed. + * + * @param string $url + * A string for the url of the release download. + * + * @return array + * An array indicating whether or not this was successful, and an error + * message if applicable. + */ + +// @todo When will this fuction be uncommented? +/** +function project_browser_download_project($url) { + module_load_include('inc', 'update', 'update.manager'); + module_load_include('inc', 'update', 'update.authorize'); + // Download the file. + $local_cache = update_manager_file_get($url); + if (!$local_cache) { + return array( + 'success' => FALSE, + 'message' => t('Unable to retrieve Drupal project from %url.', array('%url' => $url)), + ); + } + + try { + if ($arguments = update_manager_install_project($local_cache)) { + extract($arguments); + try { + $context = array(); + watchdog('debug', '
' . print_r($arguments, TRUE) . '
'); + update_authorize_batch_copy_project($project, $updater_name, $local_url, $filetransfer, $context); + return array( + 'success' => TRUE, + ); + } + catch (UpdaterException $e) { + return array( + 'success' => FALSE, + 'message' => $e->getMessage(), + ); + } + } + else { + return array( + 'success' => FALSE, + 'message' => t('The project could not be installed.'), + ); + } + } + catch (Exception $e) { + return array( + 'success' => FALSE, + 'message' => $e->getMessage(), + ); + } +} +*/ + +/** + * Downloads and installs a project using the update module. + * + * @param string $url + * A string for the url of the release download. + * + * @return array + * An array indicating whether or not this was successful, and an error + * message if applicable. + */ +function project_browser_download_project($url) { + module_load_include('inc', 'update', 'update.manager'); + // Download the file. + $local_cache = update_manager_file_get($url); + if (!$local_cache) { + return array( + 'success' => FALSE, + 'message' => t('Unable to retrieve Drupal project from %url.', array('%url' => $url)), + ); + } + + // Try to extract it. + $directory = _update_manager_extract_directory(); + try { + $archive = update_manager_archive_extract($local_cache, $directory); + } + catch (Exception $e) { + return array( + 'success' => FALSE, + 'message' => $e->getMessage(), + ); + } + $files = $archive->listContents(); + if (!$files) { + return array( + 'success' => FALSE, + 'message' => t('Provided archive contains no files.'), + ); + } + + $project = strtok($files[0], '/\\'); + + $archive_errors = update_manager_archive_verify($project, $local_cache, $directory); + if (!empty($archive_errors)) { + if (!empty($archive_errors)) { + foreach ($archive_errors as $error) { + drupal_set_message(check_plain($error), 'error'); + } + } + return array( + 'success' => FALSE, + 'message' => array_shift($archive_errors), + ); + } + + // Make sure the Updater registry is loaded. + drupal_get_updaters(); + + $project_location = $directory . '/' . $project; + try { + $updater = Updater::factory($project_location); + } + catch (Exception $e) { + return array( + 'success' => FALSE, + 'message' => $e->getMessage(), + ); + } + + try { + $project_title = Updater::getProjectTitle($project_location); + } + catch (Exception $e) { + return array( + 'success' => FALSE, + 'message' => $e->getMessage(), + ); + } + + if ($updater->isInstalled()) { + return array( + 'success' => FALSE, + 'message' => t('%project is already installed.', array('%project' => $project_title)), + ); + } + + $project_real_location = drupal_realpath($project_location); + $updater_name = get_class($updater); + + if (fileowner($project_real_location) == fileowner(conf_path())) { + module_load_include('inc', 'update', 'update.authorize'); + $filetransfer = new Local(DRUPAL_ROOT); + + // Initialize some variables in the Batch API $context array. + $updater = new $updater_name($project_real_location); + + try { + if ($updater->isInstalled()) { + // This is an update. + $tasks = $updater->update($filetransfer); + } + else { + $tasks = $updater->install($filetransfer); + } + } + catch (UpdaterException $e) { + return array( + 'success' => FALSE, + 'message' => t('Error installing / updating. Error: @error', array('@error' => $e->getMessage())), + ); + } + } + else { + return array( + 'success' => FALSE, + 'message' => t('Permissions are not set up properly.'), + ); + } + + return array( + 'success' => TRUE, + ); +} + +/** + * Installs a single release of a project during batch, for example. + * + * @param string $release_name + * The name of the release, as a string, for example, '7.x-1.2'. + * @param array $project + * The project data array. + * @param array &$context + * The context of the batch so that the results can be reported. + */ +function _project_browser_batch_install_release($release_name, array $project, array &$context) { + module_load_include('inc', 'project_browser', 'project_browser.pages'); + $release = project_browser_get_release($release_name, $project); + + $result = project_browser_download_project($release['download_link']); + + if ($result['success']) { + $context['results']['successes'][] = t('Successfully installed %project.', array('%project' => $project['title'])); + $context['message'] = t('Installed %project...', array('%project' => $project['title'])); + + // Add this to the session variable and remove it from the install_queue + // variable. + $_SESSION['project_browser_installed_projects'][$project['name']] = $project; + unset($_SESSION['project_browser_install_list'][$project['name']]); + } + else { + watchdog('project_browser', 'There was an error while installing %project. + !message', + array('%project' => $project['title'], '!message' => $result['message']), WATCHDOG_ERROR); + $context['results']['failures'][] = t('Error installing %project. Errors have been logged.', + array('%project' => $project['title'])); + $context['message'] = t('Error installing %project. !message', + array('%project' => $project['title'], '!message' => $result['message'])); + } +} + +/** + * Shows a message and finishes up the batch. + * + * If there were any errors, they are reported here with drupal_set_message(). + * The user is then redirected to the select versions page if there were errors, + * the install dependencies page if there were any detected missing + * dependencies, or the enable modules page if there were no errors. + * + * @param bool $success + * Whether or not the whole operation was successful, a Boolean. + * @param array $results + * An array of messages about any failures. + * @param array $operations + * An array of operations that need to be performed. + */ +function _project_browser_batch_install_releases_finished(bool $success, array $results, array $operations) { + drupal_get_messages(); + + // Restore the maintenance mode to what it was at the start. + if (isset($_SESSION['maintenance_mode'])) { + config('system.maintenance')->set('enabled', $_SESSION['maintenance_mode'])->save(); + unset($_SESSION['maintenance_mode']); + } + + unset($_SESSION['project_browser_install_releases_list']); + if ($success) { + if (!empty($results)) { + if (!empty($results['failures'])) { + drupal_set_message(format_plural(count($results['failures']), 'Failed to install one project.', 'Failed to install @count projects.'), 'error'); + } + } + } + else { + drupal_set_message(t('Error installing projects.'), 'error'); + drupal_goto('admin/modules/project-browser/install/select_versions'); + } + + $projects = project_browser_get_installed_projects(); + $missing = project_browser_get_missing_dependencies($projects); + // If there are missing dependencies, go to install dependencies. + if (count($missing) > 0) { + drupal_goto('admin/modules/project-browser/install/install_dependencies'); + } + else { + drupal_goto('admin/modules/project-browser/install/enable'); + } +} + +/** + * Gets the dependencies for installed projects. + * + * @param array $projects + * An array of projects to get the missing dependencies for. + * + * @return array + * An array of missing dependencies, if any were detected. + */ +function project_browser_get_missing_dependencies(array $projects) { + $modules = system_rebuild_module_data(); + + $missing = array(); + + foreach ($projects as $project) { + if ($project['type'] == 'module') { + $dependency_check = TRUE; + $dependencies = array(); + if (isset($modules[$project['name']])) { + foreach ($modules[$project['name']]->info['dependencies'] as $dependency) { + if (!isset($modules[$dependency])) { + $dependencies[] = $dependency; + } + } + if (count($dependencies) > 0) { + $missing[$project['name']] = $dependencies; + } + } + else { + drupal_set_message(t('There was an error getting information for @module', + array('@module' => $project['name'])), 'error'); + } + } + } + + return $missing; +} diff --git a/core/modules/project_browser/project_browser.info b/core/modules/project_browser/project_browser.info new file mode 100644 index 0000000..343c840 --- /dev/null +++ b/core/modules/project_browser/project_browser.info @@ -0,0 +1,7 @@ +name = Project Browser +description = Allows users to browse for and install modules and themes. +dependencies[] = update +package = Core +version = VERSION +core = 8.x +configure = admin/config/development/project_browser diff --git a/core/modules/project_browser/project_browser.module b/core/modules/project_browser/project_browser.module new file mode 100644 index 0000000..520f052 --- /dev/null +++ b/core/modules/project_browser/project_browser.module @@ -0,0 +1,547 @@ +' . t('About') . ''; + $output .= '

' . t("The Project Browser provides a UI for users to browse for and install new modules and themes from within their Drupal admin interface.") . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '
'; + $output .= '
' . t('Browsing') . '
'; + // @todo Fill in accurate information about Browsing. + $output .= '
' . t('Set browsing settings on the project browser settings page.', array('!url' => url('admin/config/development/project_browser'))) . '
'; + $output .= '
' . t('Downloading') . '
'; + // @todo Fill in accurate information about Downloading. + $output .= '
' . t('Project Browser downloads new modules and themes.') . '
'; + $output .= '
' . t('Installing') . '
'; + // @todo Fill in accurate information about Installing. + $output .= '
' . t('New projects can be installed from the Project Browser interface.') . '
'; + $output .= '
'; + return $output; + } +} + +/** + * Implements hook_permission(). + */ +function project_browser_permission() { + return array( + 'use project browser' => array( + 'title' => t('Use Project Browser'), + 'description' => t('Browse for and install new modules and themes from the UI.'), + 'restrict access' => TRUE, + ), + ); +} + +/** + * Implements hook_menu(). + */ +function project_browser_menu() { + $items = array(); + $items['admin/config/development/project-browser'] = array( + 'title' => 'Project Browser settings', + 'description' => 'Add new repositories and set other settings for Project Browser.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('project_browser_admin_form'), + 'access arguments' => array('access administration pages'), + 'file' => 'project_browser.admin.inc', + ); + + $items['admin/modules/project-browser'] = array( + 'title' => 'Project Browser', + 'description' => 'Browse and search for new modules.', + 'page callback' => 'project_browser_page', + 'page arguments' => array('module'), + 'access arguments' => array('use project browser'), + 'file' => 'project_browser.pages.inc', + ); + + $items['admin/modules/project-browser/modules'] = array( + 'title' => 'Modules', + 'description' => 'Browse and search for new modules.', + 'page callback' => 'project_browser_page', + 'page arguments' => array('module'), + 'access arguments' => array('use project browser'), + 'file' => 'project_browser.pages.inc', + 'type' => MENU_DEFAULT_LOCAL_TASK, + ); + + $items['admin/modules/project-browser/themes'] = array( + 'title' => 'Themes', + 'description' => 'Browse and search for new themes.', + 'page callback' => 'project_browser_page', + 'page arguments' => array('theme'), + 'access arguments' => array('use project browser'), + 'file' => 'project_browser.pages.inc', + 'type' => MENU_LOCAL_TASK, + ); + + $items['admin/modules/project-browser/install/%'] = array( + 'title' => 'Install', + 'page callback' => 'project_browser_installation_page', + 'page arguments' => array(4), + 'access arguments' => array('use project browser'), + 'type' => MENU_NORMAL_ITEM, + 'file' => 'project_browser.pages.inc', + ); + + $items['project-browser/%/install-queue/%/%'] = array( + 'page callback' => 'project_browser_install_queue_callback', + 'page arguments' => array(1, 3, 4), + 'access arguments' => array('use project browser'), + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** + * Page callback: Allows for adding to and removing from the install queue. + * + * This is invoked via AJAX most of the time. + * + * @param string $method + * The method used for this callback, which is a string. + * @param string $op + * The operation to perform, which is a string. + * @param string $project_name + * The short name of the project, which is a string. + * + * @return array + * An array of elements that should be changed. + * + * @see project_browser_menu() + */ +function project_browser_install_queue_callback($method, $op, $project_name) { + module_load_include('inc', 'project_browser', 'project_browser'); + + switch ($op) { + case 'add': + $projects = project_browser_get_listed_projects(); + + if (isset($projects[$project_name])) { + $project = $projects[$project_name]; + project_browser_install_queue_add($project); + } + else { + drupal_set_message(t('Error: The project was not found.'), 'error'); + } + break; + + case 'remove': + project_browser_install_queue_remove($project_name); + break; + } + + switch ($method) { + case 'nojs': + // Redirect to the page it came from. + $redirect = (isset($_GET['destination'])) ? $_GET['destination'] : 'admin/modules/project-browser'; + + drupal_goto($redirect); + break; + + case 'ajax': + $commands = array(); + + // Refresh the install queue. + $commands[] = ajax_command_replace('#project-browser-install-queue', project_browser_get_install_list()); + // Refresh the add to queue link. + $commands[] = ajax_command_replace('#add-to-queue-link-' . $project_name, project_browser_add_remove_queue_link($project_name)); + + return array('#type' => 'ajax', '#commands' => $commands); + break; + } +} + +/** + * Implements hook_menu_local_tasks_alter(). + */ +function project_browser_menu_local_tasks_alter(&$data, $router_item, $root_path) { + switch ($root_path) { + // Put the 'Project Browser' action on the 'Modules' page. + case 'admin/modules': + // Unset the install theme page. + foreach ($data['actions'] as $num => $item) { + if ($item['#link']['path'] == 'admin/modules/install') { + unset($data['actions'][$num]); + } + } + $item = menu_get_item('admin/modules/project-browser/modules'); + if ($item['access']) { + $item['title'] = t('Install new modules'); + $data['actions']['admin/modules/project-browser/modules'] = array( + '#theme' => 'menu_local_action', + '#link' => $item, + ); + } + break; + + // Include the old 'Install a module' link. + case 'admin/modules/project-browser': + case 'admin/modules/project-browser/modules': + case 'admin/modules/project-browser/themes': + $item = menu_get_item('admin/modules/install'); + if ($item['access']) { + $item['title'] = t('Install manually'); + $data['actions']['admin/modules/install'] = array( + '#theme' => 'menu_local_action', + '#link' => $item, + ); + } + break; + + // Put the 'Project Browser' action on the 'Appearance' page. + case 'admin/appearance': + // Unset the install theme page. + foreach ($data['actions'] as $num => $item) { + if ($item['#link']['path'] == 'admin/appearance/install') { + unset($data['actions'][$num]); + } + } + $item = menu_get_item('admin/modules/project-browser/themes'); + if ($item['access']) { + $item['title'] = t('Install new themes'); + $data['actions']['admin/modules/project-browser/themes'] = array( + '#theme' => 'menu_local_action', + '#link' => $item, + ); + } + break; + } +} + +/** + * Implements hook_theme(). + */ +function project_browser_theme($existing, $type, $theme, $path) { + return array( + // Template for installation page. + 'project_browser_install' => array( + 'variables' => array('current_task' => NULL, 'main_content' => NULL), + 'path' => $path . '/theme', + 'template' => 'project-browser-install', + ), + // Template for list of projects. + 'project_browser_list' => array( + 'variables' => array('projects_list' => NULL, 'type' => NULL), + 'path' => $path . '/theme', + 'template' => 'project-browser-list', + ), + // Template for list of projects. + 'project_browser_block' => array( + 'render element' => 'element', + 'path' => $path . '/theme', + 'template' => 'project-browser-block', + ), + // Template for single project. + 'project_browser_project' => array( + 'variables' => array('project' => NULL, 'first' => NULL), + 'path' => $path . '/theme', + 'template' => 'project-browser-project', + ), + // Template for install queue item. + 'project_browser_install_queue' => array( + 'variables' => array('projects' => NULL), + 'path' => $path . '/theme', + 'template' => 'project-browser-install-queue', + ), + ); +} + +/** + * Implements hook_preprocess_HOOK() for project-browser-install.tpl.php. + * + * Adds some variables for the projects install theme. + * + * @param array $variables + * An associative array containing: + * - current_task: The current task. + * + * @ingroup themeable + */ +function project_browser_preprocess_project_browser_install(array &$variables) { + module_load_include('inc', 'project_browser', 'project_browser.pages'); + // Add the themed list + $variables['task_list'] = project_browser_installation_task_list($variables['current_task']); +} + +/** + * Implements hook_preprocess_HOOK() for project-browser-install-queue.tpl.php. + * + * Adds some variables for the projects install queue theme. + * + * @param array $variables + * An associative array containing: + * - projects: An array of projects in the install queue. + * + * @ingroup themeable + */ +function project_browser_preprocess_project_browser_install_queue(array &$variables) { + $build = array(); + if (empty($variables['projects'])) { + $build['empty_text'] = array( + '#markup' => t('Install queue is empty.'), + ); + } + else { + foreach ($variables['projects'] as $project) { + $build['queued-item-' . $project['name']] = array( + '#prefix' => "
", + '#markup' => project_browser_add_remove_queue_link($project['name'], $project['title'], 'remove-queue-link'), + '#suffix' => "
", + ); + } + $build['install-link'] = drupal_get_form('project_browser_install_button_form'); + } + + // Add the install button. + $variables['queue_html'] = drupal_render($build); +} + +/** + * Implements hook_preprocess_HOOK() for project-browser-block.tpl.php. + * + * Adds some variables for the project browser block theme. + * + * @param array $variables + * An associative array containing: + * - element['#title']: The title of the block. + * - element['#content']: The content of the block. + * + * @ingroup themeable + */ +function project_browser_preprocess_project_browser_block(array &$variables) { + // Add the title and content variables. + $variables['title'] = $variables['element']['#title']; + $variables['content'] = $variables['element']['#content']; +} + +/** + * Implements hook_preprocess_HOOK() for project-browser-list.tpl.php. + * + * Adds some variables for the projects list theme. + * + * @param array $variables + * An associative array containing: + * - projects_list: An array of all projects. + * + * @ingroup themeable + */ +function project_browser_preprocess_project_browser_list(array &$variables) { + module_load_include('inc', 'project_browser', 'project_browser'); + drupal_add_library('project_browser', 'drupal.project_browser.css'); + + if (is_array($variables['projects_list']) AND !empty($variables['projects_list'])) { + $content = ''; + $first = TRUE; + // Theme each individual project and add to the list. + foreach ($variables['projects_list'] as $project) { + $content .= theme('project_browser_project', array('project' => $project, 'first' => $first)); + $first = FALSE; + } + } + else { + $content = t('No results found.'); + } + + switch ($variables['type']) { + case 'module': + $title = t('Most used Drupal modules'); + break; + + case 'theme': + $title = t('Most used Drupal themes'); + break; + + default: + $title = t('Projects'); + break; + } + + // If any filters were selected, change the title to 'Search results'. + if (!empty($_SESSION['project_browser_text_filter_' . $variables['type']]) || !empty($_SESSION['project_browser_category_filter_' . $variables['type']])) { + $title = t('Search results'); + } + + $main_content['project_browser_main_block'] = array( + '#theme' => 'project_browser_block', + '#title' => $title, + '#content' => $content, + '#contextual_links' => array( + 'project_browser' => array('admin/config/development/project_browser', array()), + ), + ); + $variables['main_content'] = render($main_content); + + // Add the pager. + $variables['pager'] = theme('pager', array('tags' => NULL)); + + // Add the filters. + $filters_form = drupal_get_form('project_browser_filters_form', $variables['type']); + $filters['project_browser_filters_block'] = array( + '#theme' => 'project_browser_block', + '#title' => t('Filters'), + '#content' => drupal_render($filters_form), + ); + $variables['filters'] = render($filters); + + // Add the install list. + $install_list['project_browser_filters_block'] = array( + '#theme' => 'project_browser_block', + '#title' => t('Install queue'), + '#content' => project_browser_get_install_list(), + ); + $variables['install_list'] = render($install_list); +} + +/** + * Implements hook_preprocess_HOOK() for project-browser-project.tpl.php. + * + * Adds some variables for the project theme. + * + * @param array $variables + * An associative array containing: + * - project: An associative array of project variables. + * + * @ingroup themeable + */ +function project_browser_preprocess_project_browser_project(array &$variables) { + module_load_include('inc', 'project_browser', 'project_browser'); + $project = $variables['project']; + + $variables['title'] = l($project['title'], check_url($project['project url']), + array('attributes' => array('target' => '_blank'), 'html' => TRUE)); + $variables['author'] = t('Author: @author', array('@author' => $project['author'])); + $variables['description'] = _filter_htmlcorrector(filter_xss($project['description'])); + $variables['image'] = $project['image']; + $variables['last_updated'] = ($project['last updated']) ? t('Last Updated: @date', array('@date' => format_date($project['last updated'], 'long'))) : ''; + + $extras = array(); + + if ($project['maintenance status']) { + $extras[] = check_plain($project['maintenance status']); + } + if ($project['development status']) { + // We are not showing this because it isn't a good indicator right now. + // $extras[] = check_plain($project['development status']); + } + if ($project['usage'] AND is_numeric($project['usage'])) { + $extras[] = format_plural($project['usage'], '1 Install', '@count Installs'); + } + if ($project['rating']) { + $extras[] = check_plain($project['rating']); + } + + $variables['extras'] = implode(' | ', $extras); + + // Check if the project is installed. + if (_project_browser_is_project_enabled($project['type'], $project['name'])) { + $variables['status'] = '
Already installed
'; + $variables['install'] = ''; + } + elseif (drupal_get_filename($project['type'], $project['name'])) { + $variables['status'] = '
Already downloaded
'; + $variables['install'] = ''; + } + else { + $variables['status'] = ''; + $variables['install'] = project_browser_add_remove_queue_link($project['name']); + } +} + +/** + * Builds the add/remove project to install queue link. + * + * @param string $project_name + * The short name of the project, such as 'views'. + * @param string|null $title + * (optional) The title of the project. Defaults to NULL. + * @param string $id_prefix + * (optional) The prefix that should be prepended to the id, to ensure unique + * id names. Defaults to 'add-to-queue-link'. + * + * @return string + * A themed link to remove or add an item from the install queue. + */ +function project_browser_add_remove_queue_link($project_name, $title = NULL, $id_prefix = 'add-to-queue-link') { + $queued_projects = project_browser_get_queued_projects(); + if (!$title) { + $title = isset($queued_projects[$project_name]) ? t('Remove from Install queue') : t('Add to Install queue'); + } + $op = isset($queued_projects[$project_name]) ? 'remove' : 'add'; + + $build['ajax_link'] = array( + '#type' => 'link', + '#title' => $title, + '#href' => 'project-browser/nojs/install-queue/' . $op . '/'. $project_name, + '#options' => array( + 'query' => drupal_get_destination(), + ), + '#id' => $id_prefix . '-' . $project_name, + '#ajax' => array( + 'effect' => 'fade', + 'speed' => 1000, + 'progress' => array( + 'type' => 'throbber', + 'message' => '', + ), + ), + ); + + return drupal_render($build); +} + +/** + * Implements hook_library_info(). + */ +function project_browser_library_info() { + $libraries['drupal.project_browser'] = array( + 'title' => 'Project Browser', + 'version' => VERSION, + 'js' => array( + drupal_get_path('module', 'project_browser') . '/js/project_browser_more_link.js' => array(), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('project_browser', 'drupal.project_browser.css'), + ), + ); + $libraries['drupal.project_browser.select_releases'] = array( + 'title' => 'Project Browser Select Releases', + 'version' => VERSION, + 'js' => array( + drupal_get_path('module', 'project_browser') . '/js/project_browser_select_releases.js' => array(), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('project_browser', 'drupal.project_browser.css'), + ), + ); + $libraries['drupal.project_browser.css'] = array( + 'title' => 'Project Browser CSS', + 'version' => VERSION, + 'css' => array( + drupal_get_path('module', 'project_browser') . '/css/project_browser.admin.css' => array( + 'type' => 'file', + 'media' => 'screen', + ), + ), + ); + return $libraries; +} diff --git a/core/modules/project_browser/project_browser.pages.inc b/core/modules/project_browser/project_browser.pages.inc new file mode 100644 index 0000000..24f286a --- /dev/null +++ b/core/modules/project_browser/project_browser.pages.inc @@ -0,0 +1,557 @@ + $drupal_version[0], + 'type' => $type, + ); + + // Add filters. + if (isset($_SESSION['project_browser_category_filter_' . $type])) { + $categories = array_filter($_SESSION['project_browser_category_filter_' . $type]); + if (!empty($categories)) { + $filters['categories'] = project_browser_prepare_categories($categories, $type); + } + } + if (isset($_SESSION['project_browser_text_filter_' . $type])) { + $filters['text'] = $_SESSION['project_browser_text_filter_' . $type]; + } + if (isset($_SESSION['project_browser_order_by_filter_' . $type])) { + $filters['order_by'] = $_SESSION['project_browser_order_by_filter_' . $type]; + } + if (isset($_SESSION['project_browser_sort_filter_' . $type])) { + $filters['sort'] = $_SESSION['project_browser_sort_filter_' . $type]; + } + if (isset($_SESSION['project_browser_server_filter'])) { + $filters['server'] = $_SESSION['project_browser_server_filter']; + } + else { + $filters['server'] = 0; + } + $filters['requested'] = 10; + $filters['page'] = isset($_GET['page']) ? $_GET['page'] : 0; + + // Get the projects to display here based on the filters. + $results = project_browser_fetch_results($filters); + + // Save the listed projects in the session so it can be used. + $_SESSION['project_browser_listed_projects'] = $results['projects']; + + $test = project_browser_get_listed_projects(); + + $list = array(); + foreach ($results['projects'] as $project) { + $list[] = $project; + } + + // Add the pager. + $total = $results['total']; + $num_per_page = 10; + $page = pager_default_initialize($total, $num_per_page); + $offset = $num_per_page * $page; + $start = ($total) ? $offset + 1 : 0; + $finish = $offset + $num_per_page; + if ($finish > $total) { + $finish = $total; + } + + $sort_options = project_browser_get_sort_options(); + $current_order_by = isset($_SESSION['project_browser_order_by_filter_' . $type]) ? $_SESSION['project_browser_order_by_filter_' . $type] : 'score'; + $current_sort = isset($_SESSION['project_browser_sort_filter_' . $type]) ? $_SESSION['project_browser_sort_filter_' . $type] : 'desc'; + + $build = array(); + $build['content'] = array( + 'project_browser_header' => array( + '#markup' => t('Showing @start to @finish of @total.', array( + '@start' => $start, '@finish' => $finish, '@total' => $total)), + '#weight' => 0, + ), + 'project_browser_sort_header' => array( + '#type' => 'item', + '#weight' => 2, + '#markup' => project_browser_get_sort_widget($sort_options, $current_order_by, $current_sort), + ), + 'project_browser_list' => array( + '#markup' => theme('project_browser_list', array('projects_list' => $list, 'type' => $type)), + '#weight' => 3, + ), + 'pager' => array( + '#theme' => 'pager', + '#weight' => 99, + ), + ); + + $servers = project_browser_get_servers(); + + if (count($servers) > 1) { + $build['content']['project_browser_server_header'] = array( + '#type' => 'item', + '#weight' => 1, + '#markup' => project_browser_get_server_widget($servers, $filters['server']), + ); + } + + $build['#attached']['library'][] = array( + 'project_browser', + 'drupal.project_browser' + ); + + return $build; +} + +/** + * Builds a page from the install process. + * + * @param string $op + * Operation to perform, which is a string, for example, 'enable'. + * + * @return string + * A themed page from the install process, depending on the $op. + * + * @see project_browser_menu() + */ +function project_browser_installation_page($op) { + drupal_add_library('project_browser', 'drupal.project_browser.css'); + + switch ($op) { + case 'select_versions': + drupal_set_title(t("Select versions")); + $content = project_browser_installation_select_versions_page(); + break; + + case 'install_dependencies': + drupal_set_title(t("Install Dependencies")); + $content = project_browser_installation_install_dependencies_page(); + break; + + case 'enable': + drupal_set_title(t("Enable modules")); + $content = project_browser_installation_enable_page(); + break; + } + return theme('project_browser_install', array('current_task' => $op, 'main_content' => drupal_render($content))); +} + +/** + * Page callback: Shows the Select versions installation task. + * + * Shows a form where the user can select which versions to install for each + * project. + * + * @return array + * The form to select the versions of the projects the user wants to install. + * + * @see project_browser_menu() + */ +function project_browser_installation_select_versions_page() { + module_load_include('inc', 'project_browser', 'project_browser'); + // Show a form that lets the user select which version of the projects to + // install. + $queued_projects = project_browser_get_queued_projects(); + unset($_SESSION['project_browser_installed_projects']); + + return drupal_get_form('project_browser_installation_select_versions_form', $queued_projects); +} + +/** + * Form constructor for the select versions form. + * + * @param array $projects + * An array of projects to get the releases for. + * + * @see project_browser_installation_select_versions_form_submit() + * @ingroup forms + */ +function project_browser_installation_select_versions_form($form, &$form_state, array $projects) { + module_load_include('inc', 'project_browser', 'project_browser'); + drupal_add_library('project_browser', 'drupal.project_browser.select_releases'); + + $form = array(); + + // First unset any old data. + unset($_SESSION['project_browser_install_releases_list']); + + $form['#tree'] = TRUE; + + $form['releases-header'] = array( + '#type' => 'item', + '#markup' => t("You're about to install:"), + ); + + $form['releases'] = array(); + + foreach ($projects as $project) { + // Get the available releases for this project. + if (!$release_data = project_browser_get_project_release_data($project)) { + drupal_set_message(t('Could not fetch releases for project %project.', array('%project' => $project['title'])), 'warning'); + watchdog('project_browser', 'Could not fetch releases for project %project.', array('%project' => $project['title']), WATCHDOG_ERROR); + project_browser_install_queue_remove($project['name']); + continue; + } + + // We use the update module to calculate the recommended version. + $project_data = array( + 'existing_major' => 0, + 'existing_version' => 0, + 'install_type' => '', + ); + module_load_include('inc', 'update', 'update.compare'); + update_calculate_project_update_status($project_data, $release_data); + + $releases_list = array(); + + foreach ($release_data['releases'] as $version => $release) { + $release_title = t("@project @version - @date", array( + '@project' => $project['title'], + '@version' => $release['version'], + '@date' => format_date($release['date'], 'custom', 'M j, Y'), + )); + if (isset($release['terms']['Release type']) AND !empty($release['terms']['Release type'])) { + $release_title .= " (" . implode(', ', $release['terms']['Release type']) . ")"; + } + if (isset($release['release_link'])) { + $releases_list[$version] = l($release_title, $release['release_link']); + } + else { + $releases_list[$version] = $release_title; + } + } + + $form['releases'][$project['name']]['project'] = array( + '#type' => 'value', + '#value' => $project, + ); + + $form['releases'][$project['name']]['release_name'] = array( + '#type' => 'radios', + '#title' => t('Select release for @project', array('@project' => $project['title'])), + '#options' => $releases_list, + '#default_value' => key($releases_list), + '#prefix' => '
', + '#suffix' => '
', + '#attributes' => array( + 'class' => array('project-browser-releases-radios'), + ), + '#required' => TRUE, + ); + $form['releases'][$project['name']]['selected_text'] = array( + '#type' => 'item', + '#prefix' => '
', + '#suffix' => '
', + '#markup' => reset($releases_list), + ); + if (isset($project_data['recommended'])) { + // If there is a recommended release set, then only show it and show the + // jQuery link. + $recommended_releases = array(); + $recommended_releases[$project_data['recommended']] = $releases_list[$project_data['recommended']]; + $form['releases'][$project['name']]['release_name']['#default_value'] = $project_data['recommended']; + $form['releases'][$project['name']]['selected_text']['#markup'] = $releases_list[$project_data['recommended']]; + } + if (count($releases_list) > 1) { + $form['releases'][$project['name']]['selected_text']['#markup'] .= " (" . t('change release') . ")"; + } + } + + // If there is nothing to install, go to the enable page. + if (empty($form['releases'])) { + drupal_set_message(t('No releases data found for any of the selected projects.'), 'warning'); + drupal_goto('admin/modules/project-browser/install/enable'); + } + + $form['backup_warning'] = array( + '#type' => 'markup', + '#markup' => t('Back up your database and site before you continue. !link.', array('!link' => l(t('Learn how'), 'http://drupal.org/node/22281'))), + ); + $form['maintenance_mode'] = array( + '#type' => 'checkbox', + '#title' => t('Perform updates with site in maintenance mode (strongly recommended)'), + '#default_value' => TRUE, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Install'), + ); + + return $form; +} + +/** + * Form submission handler for project_browser_installation_select_versions_form(). + * + * This sets the batch to install the different selected releases one by one. + */ +function project_browser_installation_select_versions_form_submit($form, &$form_state) { + module_load_include('inc', 'project_browser', 'project_browser'); + // Store maintenance_mode setting so we can restore it when done. + $_SESSION['maintenance_mode'] = config('system.maintenance')->get('enabled'); + if ($form_state['values']['maintenance_mode'] == TRUE) { + config('system.maintenance')->set('enabled', TRUE)->save(); + } + + foreach ($form_state['values']['releases'] as $item) { + // Load the selected release. + if ($release = project_browser_get_release($item['release_name'], $item['project'])) { + // Add the release to a session variable. + $_SESSION['project_browser_install_releases_list'][$item['project']['name']] = array( + 'release_name' => $item['release_name'], + 'project' => $item['project'], + ); + } + } + + // Install the projects with batch. + module_load_include('inc', 'update', 'update.manager'); + + $queued_releases = project_browser_get_queued_releases(); + + $operations = array(); + foreach ($queued_releases as $short_name => $info) { + $operations[] = array('_project_browser_batch_install_release', array($info['release_name'], $info['project'])); + } + $batch = array( + 'operations' => $operations, + 'finished' => '_project_browser_batch_install_releases_finished', + 'title' => t('Installing projects'), + 'init_message' => t('Installing modules...'), + 'progress_message' => t('Installed @current out of @total.'), + 'error_message' => t('Installation has encountered an error.'), + 'file' => drupal_get_path('module', 'project_browser') . '/project_browser.inc', + ); + batch_set($batch); +} + +/** + * Page callback: Handles the Install Dependencies installation task. + * + * This shows a form which lets the user select which version of dependencies + * to install. This is only shown if there are missing dependencies. If there + * are no missing dependencies, then we redirect to the enable page. + * + * @return array + * The form array to let the user select the dependencies to install. + * + * @see project_browser_menu() + */ +function project_browser_installation_install_dependencies_page() { + module_load_include('inc', 'project_browser', 'project_browser'); + $projects = project_browser_get_installed_projects(); + $missing = project_browser_get_missing_dependencies($projects); + + if (count($missing) > 0) { + $missing_projects = array(); + // Add the project data in the array as best we can. + foreach ($missing as $project_shortname => $dependencies) { + foreach ($dependencies as $shortname) { + $missing_projects[$shortname] = array( + 'name' => $shortname, + // Missing dependencies only works for projects of type 'module'. + 'type' => 'module', + 'title' => $shortname, + ); + } + } + + return drupal_get_form('project_browser_installation_select_versions_form', $missing_projects); + } + else { + drupal_goto('admin/modules/project-browser/install/enable'); + } +} + +/** + * Page callback: Shows the options for the Enable projects installation task. + * + * This shows a form which lets the user enable the newly installed projects. If + * there are unresolved dependencies, then the project is shown with a message + * about why it can't be enabled. This redirects to the project-browser page if + * there were no installed projects. + * + * @return array + * The form array to enable projects, or redirect to + * 'admin/modules/project-browser'. + * + * @see project_browser_menu() + */ +function project_browser_installation_enable_page() { + module_load_include('inc', 'project_browser', 'project_browser'); + $installed_projects = project_browser_get_installed_projects(); + + if (count($installed_projects) > 0) { + return drupal_get_form('project_browser_installation_enable_form', $installed_projects); + } + else { + drupal_goto('admin/modules/project-browser'); + } +} + +/** + * Form constructor for the enable projects form. + * + * @param array $projects + * An array of newly installed projects to enable. + * + * @see project_browser_installation_enable_form_submit() + * @ingroup forms + */ +function project_browser_installation_enable_form($form, &$form_state, array $projects) { + $modules = system_rebuild_module_data(); + $form['instructions'] = array( + '#type' => 'item', + '#markup' => t('The projects you selected have been successfully installed. If you installed any new modules, you may enable them using the form below or on the main !link page.', array('!link' => l(t('Modules'), 'admin/modules'))), + ); + + $options = array(); + $missing = array(); + + foreach ($projects as $project) { + if ($project['type'] == 'module') { + $dependency_check = TRUE; + $dependencies = array(); + if (isset($modules[$project['name']])) { + foreach ($modules[$project['name']]->info['dependencies'] as $dependency) { + if (isset($modules[$dependency])) { + $dependencies[] = $modules[$dependency]->info['name'] . ' (' . t('Installed') . ')'; + } + else { + $dependency_check = FALSE; + $dependencies[] = $dependency . ' (' . t('Missing') . ')'; + } + } + if ($dependency_check) { + $options[$project['name']] = array( + array('data' => $modules[$project['name']]->info['name']), + array('data' => $modules[$project['name']]->info['version']), + array('data' => implode(', ', $dependencies)), + ); + } + else { + $missing[$project['name']] = array( + array('data' => $modules[$project['name']]->info['name']), + array('data' => $modules[$project['name']]->info['version']), + array('data' => implode(', ', $dependencies)), + ); + } + } + else { + drupal_set_message(t('There was an error getting information for @module', array('@module' => $project['name'])), 'error'); + } + } + } + + $headers = array( + array('data' => t('Title')), + array('data' => t('Version')), + array('data' => t('Dependencies')), + ); + + if (!empty($options)) { + $form['modules'] = array( + '#type' => 'tableselect', + '#title' => t('Enable modules'), + '#description' => t('Select which modules you would like to enable.'), + '#header' => $headers, + '#options' => $options, + '#empty' => t('No new modules installed.'), + '#multiple' => TRUE, + '#js_select' => TRUE, + '#weight' => 1, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#submit' => array('project_browser_installation_enable_form_submit'), + '#value' => t('Enable modules'), + '#weight' => 99, + ); + } + + if (!empty($missing)) { + $form['missing'] = array( + '#type' => 'item', + '#title' => t('Missing Dependencies'), + '#description' => t('These modules are missing one or more dependencies, and so cannot be enabled.'), + '#markup' => theme('table', array('header' => $headers, 'rows' => $missing)), + '#weight' => 2, + ); + } + + return $form; +} + +/** + * Form submission handler for project_browser_installation_enable_form(). + * + * Enables the selected projects from the enable projects form. After the + * selected projects are enabled, we flush all caches and then redirect to the + * modules page. + */ +function project_browser_installation_enable_form_submit($form, &$form_state) { + $enable_queue = array_filter($form_state['values']['modules']); + // Enable these all at once so that dependencies are handled properly. + module_enable($enable_queue); + + drupal_flush_all_caches(); + + drupal_goto('admin/modules'); +} + +/** + * Builds a task list to the sidebar area when installing projects. + * + * This will need to be called from every page of the install process. + * + * @param string $active + * (optional) Set the active task by key, which is a string. Defaults to NULL. + * + * @return array + * The themed task list for the install projects process. + */ +function project_browser_installation_task_list($active = NULL) { + // Default list of tasks. + $tasks = array( + 'select_versions' => t('Select versions'), + 'install_dependencies' => t('Install Dependencies'), + 'enable' => t('Enable projects'), + ); + + require_once DRUPAL_ROOT . '/core/includes/theme.maintenance.inc'; + + return theme_task_list(array('items' => $tasks, 'active' => $active)); +} diff --git a/core/modules/project_browser/tests/project_browser_test.info b/core/modules/project_browser/tests/project_browser_test.info new file mode 100644 index 0000000..543e1d0 --- /dev/null +++ b/core/modules/project_browser/tests/project_browser_test.info @@ -0,0 +1,6 @@ +name = Project Browser module tests +description = Support module for Project Browser related testing. +package = Testing +version = VERSION +core = 8.x +hidden = TRUE diff --git a/core/modules/project_browser/tests/project_browser_test.module b/core/modules/project_browser/tests/project_browser_test.module new file mode 100644 index 0000000..766d415 --- /dev/null +++ b/core/modules/project_browser/tests/project_browser_test.module @@ -0,0 +1,323 @@ + 'Test Query page', + 'description' => "Tests the ability to fetch and display projects, and filter them appropriately.", + 'page callback' => 'project_browser_test_query', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + return $items; +} + +/** + * Page callback: Generates json based on the input filters. + * + * @see project_browser_test_menu() + */ +function project_browser_test_query() { + if (!isset($_GET['method'])) { + print drupal_json_encode(t('You must specify a method.')); + exit(); + } + switch ($_GET['method']) { + case 'categories': + if (!isset($_GET['type'])) { + print drupal_json_encode(t('You must specify a project type.')); + exit(); + } + + $categories = project_browser_test_get_categories($_GET['type']); + + print drupal_json_encode($categories); + exit(); + + case 'query': + // Check that we have valid data. + if (!isset($_GET['version'])) { + print drupal_json_encode(t('You must specify a drupal version.')); + exit(); + } + if (!isset($_GET['type'])) { + print drupal_json_encode(t('You must specify a project type.')); + exit(); + } + + // Get the filters. + $filters = array( + 'drupal_version' => $_GET['version'], + 'type' => $_GET['type'], + 'text' => (isset($_GET['text']) AND $_GET['text']) ? $_GET['text'] : '', + 'sort_method' => isset($_GET['sort_method']) ? $_GET['sort_method'] : 'usage', + 'sort_direction' => isset($_GET['sort_direction']) ? $_GET['sort_direction'] : 'desc', + 'requested' => isset($_GET['requested']) ? (int) $_GET['requested'] : 12, + 'page' => isset($_GET['page']) ? (int) $_GET['page'] : 0, + ); + + if (isset($_GET['categories'])) { + $categories = unserialize($_GET['categories']); + if (is_array($categories) AND !empty($categories)) { + $filters['categories'] = $categories; + } + } + + // Pass them off to the project_browser_server_get_results() function. + $results = project_browser_test_get_results($filters); + + print drupal_json_encode($results); + + exit(); + } +} + +/** + * Returns an array of static categories to use for testing. + * + * @param string $type + * The type of project, 'module' or 'theme'. + * + * @return array + * An array of categories for the project type. + */ +function project_browser_test_get_categories($type) { + $categories = array(); + + switch ($type) { + case 'module': + $categories = array( + 'admin' => "Administrative", + 'search' => "Search", + 'user_management' => "User Management", + ); + break; + + case 'theme': + $categories = array( + 'dark' => "Dark", + 'light' => "Light", + ); + } + + return $categories; +} + +/** + * Returns an array of filtered projects. + * + * @param array $filters + * An array of filters to use. + * + * @return array + * An array of projects. + */ +function project_browser_test_get_results(array $filters) { + $projects = project_browser_test_projects(); + + $results = array( + 'total' => count($projects), + 'projects' => array(), + ); + + // Filter out projects based on type. + if (isset($filters['type']) AND $type = $filters['type']) { + foreach ($projects as $name => $project) { + if ($type != $project['type']) { + unset($projects[$name]); + } + } + } + + // Filter out projects based on drupal version number. + if (isset($filters['drupal_version']) AND $version = $filters['drupal_version']) { + foreach ($projects as $name => $project) { + if ($version != $project['drupal version']) { + unset($projects[$name]); + } + } + } + + // Filter out projects based on categories number. + if (isset($filters['categories']) AND is_array($filters['categories']) AND !empty($filters['categories'])) { + $filtered = array(); + foreach ($projects as $name => $project) { + foreach ($project['categories'] as $category) { + if (in_array($category, $filters['categories'])) { + $filtered[$name] = $project; + } + } + } + $projects = $filtered; + } + + // Filter out projects based on the text query. + if (isset($filters['text']) AND $text = $filters['text']) { + foreach ($projects as $name => $project) { + if (!stristr($project['title'], $text) AND !stristr($project['description'], $text)) { + unset($projects[$name]); + } + } + } + + $results['total'] = count($projects); + + // Only send back the requested amount. + $start = $filters['page'] * $filters['requested']; + $end = $start + $filters['requested']; + + $results['projects'] = $projects; + + return $results; +} + +/** + * Returns an array of projects to use for testing. + * + * @return array + * An array of projects. + */ +function project_browser_test_projects() { + $projects = array(); + + $projects['views'] = array( + 'type' => 'module', + 'title' => 'Views', + 'name' => 'views', + 'drupal version' => 8, + 'author' => 'merlinofchaos', + 'description' => "The Views module provides a flexible method for Drupal site designers to control how lists and tables of content (nodes in Views 1, almost anything in Views 2) are presented. Traditionally, Drupal has hard-coded most of this, particularly in how taxonomy and tracker lists are formatted. ", + 'categories' => array('admin', 'search'), + 'image' => 'http://learnbythedrop.com/system/files/images/View-Edit_0.png', + 'usage' => '542312', + 'project url' => 'http://www.drupal.org/projects/views', + 'project status url' => 'http://updates.drupal.org/release-history/views/7.x', + 'last updated' => '12342523', + 'maintenance status' => 'Actively maintained', + 'development status' => 'Under active development', + 'rating' => '9.6', + 'dependencies' => array( + 'ctools', + ), + ); + + $projects['ctools_test'] = array( + 'type' => 'module', + 'title' => 'CTools Test', + 'name' => 'ctools_test', + 'drupal version' => 8, + 'author' => 'merlinofchaos', + 'description' => "This suite is primarily a set of APIs and tools to improve the developer experience. It also contains a module called the Page Manager whose job is to manage pages. In particular it manages panel pages, but as it grows it will be able to manage far more than just Panels.", + 'categories' => array(), + 'image' => '', + 'usage' => '4312', + 'project url' => 'http://www.drupal.org/projects/ctools', + 'project status url' => 'http://updates.drupal.org/release-history/ctools/7.x', + 'last updated' => '12354634', + 'maintenance status' => 'Actively maintained', + 'development status' => 'Under active development', + 'rating' => '7.6', + 'dependencies' => array(), + ); + + $projects['ctools'] = array( + 'type' => 'module', + 'title' => 'Chaos Tool Suite', + 'name' => 'ctools', + 'drupal version' => 8, + 'author' => 'merlinofchaos', + 'description' => "This suite is primarily a set of APIs and tools to improve the developer experience. It also contains a module called the Page Manager whose job is to manage pages. In particular it manages panel pages, but as it grows it will be able to manage far more than just Panels.", + 'categories' => array(), + 'image' => '', + 'usage' => '4312', + 'project url' => 'http://www.drupal.org/projects/ctools', + 'project status url' => 'http://updates.drupal.org/release-history/ctools/7.x', + 'last updated' => '12354634', + 'maintenance status' => 'Actively maintained', + 'development status' => 'Under active development', + 'rating' => '7.6', + 'dependencies' => array(), + ); + + $projects['token'] = array( + 'type' => 'module', + 'title' => 'Token', + 'name' => 'token', + 'drupal version' => 8, + 'author' => 'eaton', + 'description' => "Tokens are small bits of text that can be placed into larger documents via simple placeholders, like %site-name or [user]. The Token module provides a central API for modules to use these tokens, and expose their own token values.", + 'categories' => array('admin'), + 'image' => 'http://drupal.org/files/images/token_08.thumbnail.png', + 'usage' => '4563', + 'project url' => 'http://www.drupal.org/projects/token', + 'project status url' => 'http://updates.drupal.org/release-history/token/7.x', + 'last updated' => '12357351', + 'maintenance status' => 'Actively maintained', + 'development status' => 'Under active development', + 'rating' => '8.1', + 'dependencies' => array(), + ); + + $projects['zen'] = array( + 'type' => 'theme', + 'title' => 'Zen', + 'name' => 'zen', + 'drupal version' => 8, + 'author' => 'johnAlbin', + 'description' => "Zen is the ultimate starting theme for Drupal. If you are building your own standards-compliant theme, you will find it much easier to start with Zen than to start with Garland or Bluemarine. This theme has fantastic online documentation and tons of code comments for both the PHP (template.php) and HTML (page.tpl.php, node.tpl.php).", + 'categories' => array('light', 'dark'), + 'image' => 'http://drupal.org/files/images/zen-logo.thumbnail.png', + 'usage' => '4563', + 'project url' => 'http://www.drupal.org/project/zen', + 'project status url' => 'http://updates.drupal.org/release-history/zen/7.x', + 'last updated' => '12343634', + 'maintenance status' => 'Actively maintained', + 'development status' => 'Under active development', + 'rating' => '7.1', + 'dependencies' => array(), + ); + + $projects['acquia_marina'] = array( + 'type' => 'theme', + 'title' => 'Acquia Marina', + 'name' => 'acquia_marina', + 'drupal version' => 8, + 'author' => 'stephthegeek', + 'description' => "The Fusion base theme and Skinr are required. Skinr for Drupal 7 (dev release) is usable now but it is recommended that you proceed with caution and do some of your own testing.", + 'categories' => array('light'), + 'image' => 'http://drupal.org/files/images/acquia_marina.thumbnail.png', + 'usage' => '14563', + 'project url' => 'http://www.drupal.org/project/acquia_marina', + 'project status url' => 'http://updates.drupal.org/release-history/acquia_marina/7.x', + 'last updated' => '12346574', + 'maintenance status' => 'Actively maintained', + 'development status' => 'Under active development', + 'rating' => '7.8', + 'dependencies' => array( + 'fusion' + ), + ); + + $projects['fusion'] = array( + 'type' => 'theme', + 'title' => 'Fusion', + 'name' => 'fusion', + 'drupal version' => 8, + 'author' => 'stephthegeek', + 'description' => "Fusion is a powerful base theme, with layout and style configuration options built in that you can control through Drupal's UI. It's based on a simplified 960px or fluid 12/16-column grid. It's designed to be used with the Skinr module, with numerous useful block styles included.", + 'categories' => array('light'), + 'image' => 'http://drupal.org/files/images/fusion-powering-small-banner.thumbnail.png', + 'usage' => '14563', + 'project url' => 'http://www.drupal.org/project/fusion', + 'project status url' => 'http://updates.drupal.org/release-history/fusion/7.x', + 'last updated' => '12342643', + 'maintenance status' => 'Actively maintained', + 'development status' => 'Under active development', + 'rating' => '', + 'dependencies' => array(), + ); + + return $projects; +} diff --git a/core/modules/project_browser/theme/project-browser-block.tpl.php b/core/modules/project_browser/theme/project-browser-block.tpl.php new file mode 100644 index 0000000..e5286dd --- /dev/null +++ b/core/modules/project_browser/theme/project-browser-block.tpl.php @@ -0,0 +1,21 @@ + +
+ +

+ +
+ +
+
diff --git a/core/modules/project_browser/theme/project-browser-install-queue.tpl.php b/core/modules/project_browser/theme/project-browser-install-queue.tpl.php new file mode 100644 index 0000000..4d611ea --- /dev/null +++ b/core/modules/project_browser/theme/project-browser-install-queue.tpl.php @@ -0,0 +1,15 @@ + +
+ +
diff --git a/core/modules/project_browser/theme/project-browser-install.tpl.php b/core/modules/project_browser/theme/project-browser-install.tpl.php new file mode 100644 index 0000000..98abc9c --- /dev/null +++ b/core/modules/project_browser/theme/project-browser-install.tpl.php @@ -0,0 +1,21 @@ + + +
+ +
+
+ +
diff --git a/core/modules/project_browser/theme/project-browser-list.tpl.php b/core/modules/project_browser/theme/project-browser-list.tpl.php new file mode 100644 index 0000000..6e0132f --- /dev/null +++ b/core/modules/project_browser/theme/project-browser-list.tpl.php @@ -0,0 +1,24 @@ + +
+
+ +
+
+ + +
+
diff --git a/core/modules/project_browser/theme/project-browser-project.tpl.php b/core/modules/project_browser/theme/project-browser-project.tpl.php new file mode 100644 index 0000000..3d5f484 --- /dev/null +++ b/core/modules/project_browser/theme/project-browser-project.tpl.php @@ -0,0 +1,58 @@ + +
+ +
+ +
+ + +
+

+ +

+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+ + +
+
+