'Package browser', 'description' => 'Configure package browser', 'page callback' => 'system_admin_menu_block_page', 'access arguments' => array('access apk browser'), //system_admin_menu_block_page needs admin inc 'file' => 'system.admin.inc', 'file path' => drupal_get_path('module', 'system'), ); $items['admin/config/apk_browser/settings'] = array( 'title' => 'Settings', 'description' => 'Configure global APK browser settings', 'page callback' => 'drupal_get_form', 'page arguments' => array('apk_browser_settings'), 'access arguments' => array('access apk browser'), ); $items['admin/config/apk_browser/repositories'] = array( 'title' => 'Repositories', 'description' => 'Configure APK repositories', 'page callback' => 'apk_browser_repositories_list', 'access arguments' => array('access apk browser'), ); $items['admin/config/apk_browser/repositories/add'] = array( 'title' => 'Add repository', 'description' => 'Add repositoriy', 'page callback' => 'drupal_get_form', 'page arguments' => array('apk_browser_repository_edit_form'), 'access arguments' => array('access apk browser'), ); $items['admin/config/apk_browser/repositories/%/edit'] = array( 'title' => 'Edit repository', 'description' => 'Edit repositoriy', 'page callback' => 'drupal_get_form', 'page arguments' => array('apk_browser_repository_edit_form', 4), 'access arguments' => array('access apk browser'), 'access callback' => TRUE, ); $items['admin/config/apk_browser/repositories/%/delete'] = array( 'title' => 'Delete repository', 'description' => 'Delete repositoriy', 'page callback' => 'drupal_get_form', 'page arguments' => array('apk_browser_repository_delete_form', 4), 'access arguments' => array('access apk browser'), 'access callback' => TRUE, ); $items['admin/config/apk_browser/import'] = array( 'title' => 'Import packages', 'description' => 'Import APK packages', 'page callback' => 'apk_browser_importer', 'access arguments' => array('access apk browser'), ); $items['admin/config/apk_browser/delete/all'] = array( 'title' => 'Delete all packages', 'description' => 'Delete all APK packages', 'page callback' => 'drupal_get_form', 'page arguments' => array('apk_browser_delete_all_apk_form'), 'access arguments' => array('access apk browser'), ); return $items; } function apk_browser_permission() { return array( 'access apk browser' => array( 'title' => t('Access APK browser'), 'description' => t('Access permissions for APK browser'), ), ); } function apk_browser_settings($form) { $form['settings'] = array( '#type' => 'fieldset', '#title' => t('APK browser settings'), ); $form['settings']['apk_import_delete_all_apk_limit'] = array( '#type' => 'textfield', '#size' => '4', '#title' => t('Node delete batch limit'), '#description' => t('While deleting all packages, we need to limit the batch process. If it times out please lower this value.'), '#default_value' => variable_get('apk_import_delete_all_apk_limit', '20') ); $form['settings']['apk_import_commit_url'] = array( '#type' => 'textfield', '#title' => t('Commit URL'), '#description' => t('URL to construct a link to repository browser. Use %commit% to be replaced with the actual commit'), '#default_value' => variable_get('apk_import_commit_url', '') ); $form['settings']['apk_import_status'] = array( '#type' => 'checkbox', '#title' => t('Enable apk import'), '#description' => t('Enable or disable package importing and cleanup'), '#default_value' => variable_get('apk_import_status', '0') ); $feeds = array('0' => t('Disabled')); if (module_exists('aggregator')) { $feeds += db_query("SELECT fid,title FROM {aggregator_feed}")->fetchAllKeyed(); $disabled = FALSE; $description = t('When enabled, commit title will be searched in this aggregator feed.'); } else { $disabled = TRUE; $description = t('You need to have aggregator enabled and configured to enable this.'); } $form['settings']['apk_import_aggregator'] = array( '#type' => 'select', '#title' => t('RSS commit lookup'), '#description' => $description, '#default_value' => variable_get('apk_import_aggregator','0'), '#options' => $feeds, '#disabled' => $disabled, ); return system_settings_form($form); } function apk_browser_cron() { if (variable_get('apk_import_status', '0')) { apk_browser_importer(); //cleanup old packages ones in 24h if ((time() - variable_get('apk_browser_cleanup', '0')) > '86400') { apk_browser_cleanup(); } } } function apk_browser_repository_delete_form($form, &$form_state, $rid) { $repos = variable_get('apk_repositories', array()); if (array_key_exists($rid, $repos)) { $form['repo_id'] = array('#type' => 'hidden', '#value' => $rid); return confirm_form($form, t('Are you sure you want to delete repository with ID @ID?', array('@ID' => $rid)), 'admin/config/apk_browser/repositories', t('This action cannot be undone.'), t('Delete repository'), t('Cancel')); } else { drupal_set_message(t('Please select an existing repository ID'), 'error'); drupal_goto('admin/config/apk_browser/repositories'); } } function apk_browser_repository_delete_form_submit($form, &$form_state) { $repos = variable_get('apk_repositories', array()); unset($repos[$form_state['values']['repo_id']]); variable_set('apk_repositories', array_values($repos)); drupal_set_message(t('Repository deleted')); $form_state['redirect'] = 'admin/config/apk_browser/repositories'; } function apk_browser_repository_edit_form($form, &$form_state, $rid = array()) { $repos = variable_get('apk_repositories', array()); if (is_string($rid)) { $form['repo_id'] = array('#type' => 'hidden', '#value' => $rid); if (!array_key_exists($rid, $repos)) { drupal_set_message(t('Please select an existing repository ID'), 'error'); drupal_goto('admin/config/apk_browser/repositories'); } } $repo = taxonomy_vocabulary_machine_name_load('apk_repo'); $repo_tree = taxonomy_get_tree($repo->vid); $arch = taxonomy_vocabulary_machine_name_load('apk_arch'); $arch_tree = taxonomy_get_tree($arch->vid); if (empty($arch_tree) || empty($repo_tree)) { $form['warning'] = array( '#type' => 'item', '#markup' => '

Please add terms to you your repositories and architectures

' . l('Manage Taxonomy', 'admin/structure/taxonomy') ); } else { $form['settings'] = array( '#type' => 'fieldset', '#title' => t('Repository settings'), ); $form['settings']['apk_repo_url'] = array( '#type' => 'textfield', '#title' => t('Repository URL'), '#size' => '100', '#default_value' => is_string($rid) ? $repos[$rid]['url'] : '', '#description' => t('Link to the APKINDEX.tar.gz file inside the repository'), '#required' => TRUE, ); foreach ($repo_tree as $key => $term) { $repo_options[$term->tid] = $term->name; } //generate a form item to select terms $form['settings']['repo'] = array( '#type' => 'select', '#options' => $repo_options, '#title' => 'Select repository', '#default_value' => is_string($rid) ? $repos[$rid]['repo'] : '', '#description' => t('Please make sure you select the correct repository. Preventing to do so will break package listing') ); foreach ($arch_tree as $key => $term) { $arch_options[$term->tid] = $term->name; } $form['settings']['arch'] = array( '#type' => 'select', '#options' => $arch_options, '#default_value' => is_string($rid) ? $repos[$rid]['arch'] : '', '#title' => 'Select architecture', '#description' => t('Please make sure you select the correct architecture. Preventing to dox so will break package listing') ); $form['submit'] = array( '#type' => 'submit', '#value' => t('Save repository') ); } return $form; } function apk_browser_repository_edit_form_validate($form, &$form_state) { if (!valid_url($form_state['values']['apk_repo_url'], TRUE)) { form_set_error('apk_repo_url', t('Please specify a valid URL!')); } //foreach (variable_get('apk_repositories', array()) as $repo) { // if ($form_state['values']['apk_repo_url'] == $repo['url']) { // form_set_error('apk_repo_url', t('You are trying to add a double url')); // } // } } function apk_browser_repository_edit_form_submit($form, &$form_state) { $repos = array( 'url' => $form_state['values']['apk_repo_url'], 'arch' => $form_state['values']['arch'], 'repo' => $form_state['values']['repo'] ); $repo_db = variable_get('apk_repositories', array()); isset($form_state['values']['repo_id']) ? ( $repo_db[$form_state['values']['repo_id']] = $repos) : ($repo_db[] = $repos); variable_set('apk_repositories', $repo_db); drupal_set_message(t('Repositories are saved')); $form_state['redirect'] = 'admin/config/apk_browser/repositories'; //drupal_set_message(t('Submitting values: @values', array('@values' => var_export($form_state, TRUE)))); } function apk_browser_repositories_list() { $terms = ''; $header = array('ID', 'URL', 'Repo', 'Arch', array('data' => 'Manage', 'colspan' => '2')); $add = l('', 'admin/config/apk_browser/repositories/add', array('html' => TRUE)); foreach (variable_get('apk_repositories', array()) as $key => $repo) { $edit = l('', 'admin/config/apk_browser/repositories/' . $key . '/edit', array('html' => TRUE)); $delete = l('', 'admin/config/apk_browser/repositories/' . $key . '/delete', array('html' => TRUE)); $terms = ''; $terms .= '
'; $tarch = taxonomy_term_load($repo['arch']); $trepo = taxonomy_term_load($repo['repo']); $rows[] = array($key, $repo['url'], $trepo->name, $tarch->name, $edit, $delete); } if (!empty($rows)) { $output = theme('table', array('header' => $header, 'rows' => $rows)); $output .= $add; return $output; } else { return t('No repositories added. Please add one.') . ' ' . $add; } } function apk_browser_importer() { $repos = variable_get('apk_repositories', NULL); if ($repos) { foreach ($repos as $id => $repo) { $apk_string = file_get_contents($repo['url']); $checksum = md5($apk_string); $checksum_db = variable_get('apk_import_checksum_' . $id, ''); if ($checksum != $checksum_db) { $repo_apks = apk_browser_apkindex_reader($apk_string); $skipped = '0'; $updated = array(); $added = array(); $db_apks = db_query(" SELECT node.nid, node.title, csum.apk_checksum_value as csum FROM {node}, {field_data_taxonomy_apk_arch} AS arch, {field_data_taxonomy_apk_repo} AS repo, {field_data_apk_checksum} AS csum WHERE node.nid = arch.entity_id AND node.nid = repo.entity_id AND node.status = '1' AND node.nid = csum.entity_id AND arch.taxonomy_apk_arch_tid = :arch AND repo.taxonomy_apk_repo_tid = :repo", array( ':arch' => $repo['arch'], ':repo' => $repo['repo'] ) )->fetchAllAssoc('title'); foreach ($repo_apks as $title => $repo_apk) { if (isset($db_apks[$title])) { if ($repo_apk['C'] != $db_apks[$title]->csum) { $repo_apk['update'] = $db_apks[$title]->nid; $updated[] = apk_browser_add_apk($repo_apk, $repo['arch'], $repo['repo']); } else { $skipped++; } } else { $added[] = apk_browser_add_apk($repo_apk, $repo['arch'], $repo['repo']); } } watchdog('apk', 'Added @added and updated @updated and skipped @skipped packages for repo @repo', array('@added' => count($added), '@updated' => count($updated), '@skipped' => $skipped, '@repo' => $id), WATCHDOG_INFO, NULL); variable_set('apk_import_checksum_' . $id, $checksum); } } } else { watchdog('apk', 'No repositories found. Please add them first', array(), WATCHDOG_WARNING, NULL); } } function apk_browser_add_apk($package, $arch_tid, $repo_tid) { /* * 1st array key is language, currently set to und * second key is for multi value fields like depend * php timeout set to 10min for initial repo import */ $repo = taxonomy_term_load($repo_tid); $arch = taxonomy_term_load($arch_tid); ini_set('max_execution_time', 600); $node = (isset($package['update'])) ? node_load($package['update']) : new stdClass(); $node->type = 'apk'; //set default value for build time. some pkg do not have ts and will not sort right. $node->apk_build_time['und'][0]['value'] = '0'; foreach ($package as $field => $value) { switch ($field) { case 'P': $node->title = $value; break; case 'C': $node->apk_checksum['und'][0]['value'] = $value; break; case 'V': $node->apk_version['und'][0]['value'] = $value; break; case 'S': $node->apk_size['und'][0]['value'] = $value; break; case 'I': $node->apk_installed_size['und'][0]['value'] = $value; break; case 'T': $node->apk_description['und'][0]['value'] = $value; break; case 'U': $node->apk_url['und'][0]['value'] = $value; break; case 'L': $node->apk_license['und'][0]['value'] = $value; break; case 'o': $node->apk_origin['und'][0]['value'] = $value; break; case 'm': $node->apk_maintainer['und'][0]['value'] = $value; break; case 't': $node->apk_build_time['und'][0]['value'] = $value; break; case 'c': $node->apk_commit['und'][0]['value'] = $value; break; //setting this will update package with this nid number case 'update': $node->revision = '1'; $node->nid = $value; break; case 'D': //do not keep old depends. unset($node->apk_depends['und']); foreach ($value as $depend) { $node->apk_depends['und'][]['value'] = $depend; } break; case 'i': //do not keep old install if. unset($node->apk_install_if['und']); foreach ($value as $installif) { $node->apk_install_if['und'][]['value'] = $installif; } break; } } $node->taxonomy_apk_repo['und'][0]['tid'] = $repo_tid; $node->taxonomy_apk_arch['und'][0]['tid'] = $arch_tid; $node->language = 'und'; $node = node_submit($node); node_save($node); //create a human path based on repo and arch $alias = array( 'source' => 'node' . '/' . $node->nid, 'alias' => 'apk/' . $repo->name . '/' . $arch->name . '/' . $node->title ); path_save($alias); return $node->nid; } function apk_browser_apkindex_reader($apk_string) { //file_get_contents outputs string and php tar needs file $temp = tempnam("/tmp", "apk_"); $fp = fopen($temp, 'w'); fwrite($fp, $apk_string); fclose($fp); $tar_object = new Archive_Tar($temp); $apkindex = $tar_object->extractInString("APKINDEX"); //convert packages data into array $packages = preg_split("`\n\W+`", $apkindex); //remove last empty line array_pop($packages); foreach ($packages as $key => $package) { //convert packages lines into array $apackage = preg_split("`\n`", $package); foreach ($apackage as $apk_line) { switch ($apk_line[0]) { //depend and install_if lines case "D": case "i": $result[$key][$apk_line[0]] = explode(" ", substr($apk_line, 2)); break; //other apk variables default: $result[$key][$apk_line[0]] = substr($apk_line, 2); } } } /* * this should take care of having multiple packages with different * version in repositories */ foreach ($result as $pkg) { //check if package name already exit in result array if (isset($output[$pkg['P']])) { //check if version number is higher then previous if ($output[$pkg['P']]['V'] < $pkg['V']) { //if version is higher we overwrite package $output[$pkg['P']] = $pkg; } //all not matching packages we just write them } else { $output[$pkg['P']] = $pkg; } } unlink($temp); return $output; } //this is needed for auto views integration function apk_browser_views_api() { return array('api' => 3.0); } //disable packages which seem to have gone function apk_browser_cleanup() { //load repo's and clean them $repos = variable_get('apk_repositories', NULL); if ($repos) { watchdog('apk', 'Starting apk cleanup', array(), WATCHDOG_INFO, NULL); foreach ($repos as $id => $repo) { //create array from apkindex and filter out name $apk_string = file_get_contents($repo['url']); $packages = apk_browser_apkindex_reader($apk_string); foreach ($packages as $package) { $apk_names[] = $package['P']; } //fetch all packages from db matching arch repo $db_apk = db_query(" SELECT node.nid, node.title FROM {node}, {field_data_taxonomy_apk_arch} AS arch, {field_data_taxonomy_apk_repo} AS repo WHERE node.nid = arch.entity_id AND node.nid = repo.entity_id AND node.status = '1' AND arch.taxonomy_apk_arch_tid = :arch AND repo.taxonomy_apk_repo_tid = :repo", array( ':arch' => $repo['arch'], ':repo' => $repo['repo'] ) )->fetchAllKeyed(0, 1); //check which packages are in db but not in repo $diff = array_diff($db_apk, $apk_names); foreach ($diff as $nid => $disable) { //load the node and unpublish it $node = node_load($nid); $node->status = '0'; $node = node_submit($node); node_save($node); watchdog('apk', 'Package @apk has been disabled', array('@apk' => $disable), WATCHDOG_INFO, NULL); } } //record timestamp so we can run this function based on age variable_set('apk_browser_cleanup', time()); } else { watchdog('apk', 'No repositories found. Please add them first', array(), WATCHDOG_WARNING, NULL); } } //create an url from the commit, and if we find it in aggregator we add a title function apk_browser_commit_url($commit) { $fid = variable_get('apk_import_aggregator','0'); $attributes = array(); $href = str_replace('%commit%', $commit, variable_get('apk_import_commit_url', '')); if (module_exists('aggregator') && $fid) { if (!empty($commit)) { $search = '%' . $commit . '%'; } $title = db_query("SELECT title FROM {aggregator_item} WHERE link LIKE :commit AND fid = :fid", array(':commit' => $search, ':fid' => $fid))->fetchField(); if ($title) { $attributes = array('title' => check_plain($title), 'class' => 'with-tooltip'); $path = drupal_get_path('module', 'apk_browser'); drupal_add_js($path . '/tooltip/tooltip.js'); drupal_add_js('jQuery().ready(function(){jQuery(".with-tooltip").simpletooltip();});', 'inline'); } } return l($commit, $href, array('attributes' => $attributes)); } //format the fields into a table function apk_browser_package_table($vars) { $empty = '

' . t('None') . '

'; //prepare depends fields $depends = ''; if (!empty($vars['apk_depends'])) { foreach ($vars['apk_depends'] as $dep) { $value = trim($dep['value'], '!'); $value = preg_split("/[<>=]/", $value); $depends .= '

' . l($dep['value'], 'apk/' . $vars['taxonomy_apk_repo']['0']['taxonomy_term']->name . '/' . $vars['taxonomy_apk_arch']['0']['taxonomy_term']->name . '/' . $value[0], array()) . '

'; } } else { $depends = $empty; } //prepare install if fields $installif = ''; if (!empty($vars['apk_install_if'])) { foreach ($vars['apk_install_if'] as $iif) { $value = trim($iif['value'], '!'); $value = preg_split("/[<>=]/", $value); $installif .= '

' . l($iif['value'], 'apk/' . $vars['taxonomy_apk_repo']['0']['taxonomy_term']->name . '/' . $vars['taxonomy_apk_arch']['0']['taxonomy_term']->name . '/' . $value[0], array()) . '

'; } } //prepare origin link if (!empty($vars['apk_origin'])) { $value = trim($vars['apk_origin'][0]['value'], '!'); $value = preg_split("/[<>=]/", $value); $origin = l($vars['apk_origin'][0]['safe_value'], 'apk/' . $vars['taxonomy_apk_repo']['0']['taxonomy_term']->name . '/' . $vars['taxonomy_apk_arch']['0']['taxonomy_term']->name . '/' . $vars['apk_origin'][0]['value'] ); } else { $origin = $empty; } //prepare all fields $fields[t('Package')] = $vars['title']; $fields[t('Version')] = $vars['apk_version'][0]['safe_value']; $fields[t('Architecture')] = $vars['taxonomy_apk_arch'][0]['taxonomy_term']->name; $fields[t('Repository')] = $vars['taxonomy_apk_repo'][0]['taxonomy_term']->name; $fields[t('Project')] = l('URL', $vars['apk_url'][0]['safe_value']); $fields[t('Description')] = $vars['apk_description'][0]['safe_value']; $fields[t('License')] = $vars['apk_license'][0]['safe_value']; $fields[t('Maintainer')] = empty($vars['apk_maintainer']) ? $empty : $vars['apk_maintainer'][0]['safe_value']; $fields[t('Build date')] = empty($vars['apk_build_time']) ? $empty : date('r', $vars['apk_build_time'][0]['value']); $fields[t('Commit')] = empty($vars['apk_commit']) ? $empty : apk_browser_commit_url($vars['apk_commit']['0']['value']); //only show origin if its different then itself if (!empty($vars['apk_origin']) && ($vars['apk_origin'][0]['value'] != $vars['title'])) { $fields[t('Origin')] = $origin; } //to prevent misunderstanding only display when needed. $fields[t('Depends')] = $depends; if (!empty($installif)) { $fields[t('Install if')] = $installif; } //setup array for theme function foreach ($fields as $header => $cell) { $table['rows'][]['data'] = array(array('data' => $header, 'header' => 'header'), $cell); } //render the table return theme('table', $table); } //create template variable with package table function apk_browser_preprocess_node(&$vars) { if ($vars['node']->type == 'apk') { $vars['apk_table'] = apk_browser_package_table($vars); } } function apk_browser_delete_all_apk_form($form, $form_state) { $form = array(); $question = t('Are you sure you want to remove all packages?'); $path = 'admin/config/apk_browser'; $description = t('This action cannot be undone.'); $yes = t('Remove all'); $no = t('Cancel'); return confirm_form($form, $question, $path, $description, $yes, $no); } function apk_browser_delete_all_apk_form_submit($form, &$form_state) { apk_browser_batch_delete_all_apk(); } function apk_browser_batch_delete_all_apk() { //define batch options $batch = array( 'operations' => array( array('apk_browser_delete_all_apk', array()), ), 'finished' => 'apk_browser_batch_delete_all_apk_finished', 'title' => t('Deleting all apks'), 'init_message' => t('Starting deletion of all apks.'), 'progress_message' => t('Processed @current out of @total.'), 'error_message' => t('Batch apk delete has encountered an error.'), ); //set the batch batch_set($batch); //Only used when not run from forms api. forwards to admin page after complete. batch_process('admin'); } function apk_browser_delete_all_apk(&$context) { //node delete limit depending on php timeout $limit = variable_get('apk_import_delete_all_apk_limit', '20'); if (!isset($context['sandbox']['progress'])) { $context['sandbox']['progress'] = 0; $qty = db_query("SELECT nid FROM {node} WHERE type = 'apk'")->rowCount(); $context['sandbox']['max'] = ceil($qty / $limit); $context['results']['qty'] = $qty; } //delete X nodes set by limit. $nids = db_query("SELECT nid FROM {node} WHERE type = 'apk' LIMIT :limit", array(':limit' => $limit))->fetchCol(); node_delete_multiple($nids); //setting progress to calculate progress and display progress bar. $context['sandbox']['progress']++; $context['message'] = t('Deleting @qty packages', array('@qty' => $context['results']['qty'])); //on last run we do not set finished and will end batch if ($context['sandbox']['progress'] != $context['sandbox']['max']) { $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; } } function apk_browser_batch_delete_all_apk_finished($success, $results, $operations) { if ($success) { // Here we do something meaningful with the results. $message = $results['qty'] . ' packages deleted.'; $message .= theme('item_list', $results); } else { // An error occurred. // $operations contains the operations that remained unprocessed. $error_operation = reset($operations); $message = t('An error occurred while processing %error_operation with arguments: @arguments', array('%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE))); } drupal_set_message($message); }