/[drupal]/contributions/modules/project/project.module
ViewVC logotype

Contents of /contributions/modules/project/project.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.355 - (show annotations) (download) (as text)
Wed Aug 19 00:50:35 2009 UTC (3 months, 1 week ago) by dww
Branch: MAIN
CVS Tags: DRUPAL-6--1-0-ALPHA3, HEAD
Changes since 1.354: +2 -2 lines
File MIME type: text/x-php
#98278 by dww: Follow-up fix in project_project_access() when checking
view access on a project_release node -- we need to inspect
$node->project_release['pid'] not $node->project['pid'].
1 <?php
2 // $Id: project.module,v 1.354 2009/08/10 22:50:39 dww Exp $
3
4 /**
5 * Implementation of hook_init().
6 */
7 function project_init() {
8 $path = drupal_get_path('module', 'project');
9 drupal_add_css($path .'/project.css');
10 if (file_exists("$path/project.inc")) {
11 require_once "$path/project.inc";
12 }
13 }
14
15 function project_help($path, $arg) {
16 switch ($path) {
17 case 'admin/project/project-settings':
18 if (project_use_taxonomy()) {
19 return _project_taxonomy_help();
20 }
21 break;
22 case 'admin/content/taxonomy/%':
23 $vid = _project_get_vid();
24 if ($arg[3] == $vid) {
25 return _project_taxonomy_help($vid, FALSE);
26 }
27 break;
28 }
29 }
30
31 /**
32 * Prints out a help message about how to configure the project vocabulary.
33 *
34 * @param $vid
35 * Vocabulary ID of the project taxonomy.
36 * @param $vocab_link
37 * Boolean that controls if a link to the vocabulary admin page is added.
38 */
39 function _project_taxonomy_help($vid = 0, $vocab_link = TRUE) {
40 if (!$vid) {
41 $vid = _project_get_vid();
42 }
43 $vocabulary = taxonomy_vocabulary_load($vid);
44 $text = '<p>'. t('The project module makes special use of the taxonomy (category) system. A special vocabulary, %vocabulary_name, has been created automatically.', array('%vocabulary_name' => $vocabulary->name)) .'</p>';
45 $text .= '<p>'. t('To take full advantage of project categorization, add at least two levels of terms to this vocabulary. The first level will be the basic project types, e.g., "Modules", "Themes", "Translations".') .'</p>';
46 $text .= '<p>'. t('Subterms of each of these types will be the categories that users can select to classify the projects. For example, "Modules" might have sub-terms including "Mail" and "XML".') .'</p>';
47 if ($vocab_link) {
48 $text .= '<p>'. t('Use the <a href="@taxonomy-admin">vocabulary administration page</a> to view and add terms.', array('@taxonomy-admin' => url('admin/content/taxonomy/'. $vid))) .'</p>';
49 }
50 return $text;
51 }
52
53 /**
54 * Implementation of hook_block().
55 */
56 function project_block($op = 'list', $delta = 0, $edit = array()) {
57 if ($op == 'list') {
58 // Note: We can get by with using BLOCK_CACHE_PER_ROLE below because
59 // block caching is disabled when node access control modules are in use.
60 $blocks['project_navigation_select'] = array(
61 'info' => t('Project navigation (drop-down select)'),
62 'cache' => BLOCK_CACHE_PER_ROLE,
63 );
64 $blocks['project_navigation_text'] = array(
65 'info' => t('Project navigation (text field)'),
66 'cache' => BLOCK_CACHE_PER_ROLE,
67 );
68 if (module_exists('search')) {
69 $blocks['project_search'] = array(
70 'info' => t('Project search'),
71 'cache' => BLOCK_CACHE_PER_ROLE,
72 );
73 }
74
75 module_load_include('inc', 'project', 'project');
76 foreach (project_get_project_link_info() as $key => $links) {
77 if (isset($links['type']) && $links['type'] == 'block') {
78 $blocks[$key] = array(
79 'info' => t('Project: @section', array('@section' => $links['name'])),
80 'cache' => BLOCK_CACHE_PER_ROLE | BLOCK_CACHE_PER_PAGE,
81 );
82 }
83 }
84
85 return $blocks;
86 }
87 else if ($op == 'view') {
88 switch ($delta) {
89 case 'project_navigation_select':
90 $block = array(
91 'subject' => t('Project navigation'),
92 'content' => drupal_get_form('project_quick_navigate_form'),
93 );
94 break;
95
96 case 'project_navigation_text':
97 $block = array(
98 'subject' => t('Project navigation'),
99 'content' => drupal_get_form('project_quick_navigate_title_form'),
100 );
101 break;
102
103 case 'project_search':
104 if (user_access('search content')) {
105 $block = array(
106 'subject' => t('Search projects'),
107 'content' => drupal_get_form('project_search_block_form', 'project_project'),
108 );
109 }
110 break;
111
112 }
113 if (empty($block) && ($node = project_get_project_from_menu()) && node_access('view', $node)) {
114 module_load_include('inc', 'project', 'project');
115 foreach (project_get_project_link_info($node) as $key => $links) {
116 if ($delta == $key && isset($links['type']) && $links['type'] == 'block' && !empty($links['links'])) {
117 $block = array(
118 'subject' => $links['name'],
119 'content' => theme('item_list', $links['links']),
120 );
121 }
122 }
123 }
124 return $block;
125 }
126 elseif ($op == 'configure' && $delta == 1) {
127 $form = array();
128 $form['help_text'] = array(
129 '#type' => 'textfield',
130 '#title' => t('Help text'),
131 '#description' => t('Enter optional help text to display in the block.'),
132 '#default_value' => variable_get('project_search_block_help_text', ''),
133 );
134 return $form;
135 }
136 elseif ($op == 'save' && $delta == 1) {
137 variable_set('project_search_block_help_text', $edit['help_text']);
138 }
139 }
140
141 function project_search_block_form($form_state, $node_type) {
142 $form = search_box($form_state, 'project_search_block_form');
143 $form['node_type'] = array(
144 '#type' => 'value',
145 '#value' => $node_type,
146 );
147 $form['#base'] = 'project_search_block_form';
148 $help_text = variable_get('project_search_block_help_text', '');
149 if (!empty($help_text)) {
150 $element = 'project_search_block_form';
151 $form[$element]['#description'] = check_plain($help_text);
152 }
153 $form['#submit'][] = 'project_search_block_form_submit';
154 return $form;
155 }
156
157 function project_search_block_form_submit($form, &$form_state) {
158 $form_state['redirect'] = 'search/node/type:'. $form_state['values']['node_type'] .' '. trim($form_state['values'][$form_state['values']['form_id']]);
159 }
160
161 function project_node_info() {
162 return array(
163 'project_project' => array(
164 'name' => t('Project'),
165 'module' => 'project_project',
166 'description' => t('A project is something a group is working on. It can optionally have issue tracking, integration with revision control systems, releases, and so on.'),
167 ),
168 );
169 }
170
171 function project_perm() {
172 $perms = array(
173 'administer projects',
174 'maintain projects',
175 'access projects',
176 'access own projects',
177 'delete any projects',
178 'delete own projects',
179 'browse project listings',
180 );
181 return $perms;
182 }
183
184 /**
185 * Implementation of hook_db_rewrite_sql().
186 *
187 * Both project (and, if enabled, project_issue) provide some permissions that
188 * restrict access to viewing projects (and issues). For these permissions to
189 * be globally honored by the system, db_rewrite_sql() has to check what
190 * permissions the current user has and restrict query results accordingly.
191 *
192 * If a user has 'access projects' and 'access project issues', they have full
193 * access, so there's nothing to re-write. If they have 'access own projects'
194 * and/or 'access own project issues', the resulting query should JOIN on
195 * {node} and ensure that the node type is not project_project or
196 * project_issue, or that the owner of the node is the current user. If they
197 * do not have any access to either, then we can just restrict the query based
198 * on the {node}.type column.
199 *
200 * @see project_perm()
201 * @see project_issue_perm()
202 * @see project_find_alias()
203 */
204 function project_db_rewrite_sql($query, $primary_table, $primary_field) {
205 if ($primary_field == 'nid') {
206 $return = array();
207 $access_projects = user_access('access projects');
208 $admin_projects = user_access('administer projects');
209 if (module_exists('project_issue')) {
210 $access_issues = user_access('access project issues');
211 $access_own_issues = user_access('access own project issues');
212 }
213 else {
214 $access_issues = TRUE;
215 $access_own_issues = TRUE;
216 }
217 if ($admin_projects || ($access_projects && $access_issues)) {
218 // User has full access, nothing to re-write.
219 return;
220 }
221 else {
222 // We have to make sure {node} is in the query and know the alias.
223 if ($primary_table == 'n' || $primary_table == 'node') {
224 // Great, it's the primary table and we already know the alias.
225 $alias = $primary_table;
226 }
227 else {
228 // Look for {node} in the query.
229 if (!($alias = project_find_alias($query, 'node'))) {
230 // Not already in the query, JOIN it.
231 $return['join'] = "INNER JOIN {node} pn ON pn.nid = ". $primary_table .'.nid';
232 $alias = 'pn';
233 }
234 }
235 }
236 // At this point, we know we have to restrict something, and we know what
237 // the {node} table's alias is in the query.
238 $where = array();
239
240 global $user;
241 $uid = $user->uid;
242
243 // Some node types will be restriced by our query, but we want to allow
244 // every other node type. We keep track of the types we're handling, and
245 // at the end we'll add a clause to ignore/allow all other types.
246 $restricted_types = array();
247
248 if (!$access_projects) {
249 $restricted_types[] = "'project_project'";
250 if (user_access('access own projects')) {
251 $where[] = "($alias.type = 'project_project' AND $alias.uid = $uid)";
252 }
253 }
254
255 if (module_exists('project_issue') && !$access_issues) {
256 $restricted_types[] = "'project_issue'";
257 if ($access_own_issues) {
258 $where[] = "($alias.type = 'project_issue' AND $alias.uid = $uid)";
259 }
260 }
261
262 // If the type is not one of the restricted project* ones, allow access.
263 $where[] = "($alias.type NOT IN (". implode(', ', $restricted_types) ."))";
264
265 // Now that we have all our WHERE clauses, we just OR them all together.
266 $return['where'] = implode(' OR ', $where);
267 return $return;
268 }
269 }
270
271 /**
272 * Find the table alias for a table in a query.
273 *
274 * @param $query
275 * The query to examine for the alias.
276 * @param
277 * $table The table to examine for the alias.
278 * @return
279 * The alias if it exists, or the name of the table if it's present but not
280 * aliased, or FALSE if the table is not present.
281 *
282 * @see project_db_rewrite_sql()
283 */
284 function project_find_alias($query, $table) {
285 // See if {$table} is already in the query.
286 $match = array();
287 // This regexp handles many cases:
288 // - {$table} can be either in FROM or in a JOIN clause
289 // - Query might end immediately after {$table}
290 // - Might not have a table alias for {$table}
291 $pattern = "@.*(FROM|JOIN)\s+\{$table\}\s*(\S+)?@";
292 if (preg_match($pattern, $query, $match)) {
293 $keywords = '@(ON|INNER|LEFT|RIGHT|WHERE|ORDER|GROUP|HAVING|LIMIT)@';
294 if (!isset($match[2]) || preg_match($keywords, $match[2])) {
295 // No alias found, just use $table.
296 $alias = $table;
297 }
298 else {
299 // Alias found.
300 $alias = $match[2];
301 }
302 }
303 else {
304 // Table not in query.
305 $alias = FALSE;
306 }
307
308 return $alias;
309 }
310
311 /**
312 * Callback for the main settings page.
313 *
314 * @TODO: This function is probably one we can delete, since there are no settings
315 * to be set anymore.
316 */
317 function project_settings_form() {
318 $form = array();
319
320 if (module_exists('path')) {
321 $form['project_enable_alias'] = array(
322 '#type' => 'checkbox',
323 '#title' => t('Enable auto-generated project aliases'),
324 '#default_value' => variable_get('project_enable_alias', TRUE),
325 '#description' => t('If checked, project module will automatically generate path aliases (eg. /project/&lt;shortname&gt;/) for each project. Uncheck if you want to use another module, such as pathauto, to generate aliases.')
326 );
327 }
328 else {
329 $form['project_enable_alias'] = array(
330 '#type' => 'markup',
331 '#value' => t('If you enable the Path module from Drupal core, you can control if the Project module will automatically generate path aliases (eg. /project/&lt;shortname&gt;/) for each project.')
332 );
333 variable_set('project_enable_alias', FALSE);
334 }
335
336 return system_settings_form($form);
337 }
338
339 /**
340 * Returns the vocabulary id for projects.
341 */
342 function _project_get_vid() {
343 $vid = variable_get('project_vocabulary', '');
344 if (empty($vid)) {
345 // Check to see if a project module vocabulary exists.
346 $vid = db_result(db_query("SELECT vid FROM {vocabulary} WHERE module='%s'", 'project'));
347 if (!$vid) {
348 $edit = array(
349 'name' => t('Project types'),
350 'multiple' => 1,
351 'hierarchy' => 1,
352 'relations' => 0,
353 'module' => 'project',
354 'nodes' => array('project_project' => 1),
355 );
356 // If there is already a vocabulary assigned to 'project_project' nodes, use it.
357 $vocabularies = taxonomy_get_vocabularies('project_project');
358 if (count($vocabularies)) {
359 $vocabulary = reset($vocabularies);
360 $edit['vid'] = $vocabulary->vid;
361 }
362 taxonomy_save_vocabulary($edit);
363 $vid = $edit['vid'];
364 }
365 variable_set('project_vocabulary', $vid);
366 }
367
368 return $vid;
369 }
370
371 /**
372 * Implementation of hook_term_path().
373 *
374 * @TODO: Modify this function to use the new project settings where the
375 * user selects the views to use.
376 */
377 function project_term_path($term) {
378 // Make sure we have the entire term object.
379 $term = taxonomy_get_term($term->tid);
380
381 // The path must include the first-level term name for this term.
382 $tree = taxonomy_get_tree(_project_get_vid());
383 $parents = taxonomy_get_parents_all($term->tid);
384 foreach($parents as $parent) {
385 $ancestors[] = $parent->tid;
386 }
387 foreach ($tree as $t) {
388 if (in_array($t->tid, $ancestors) && $t->depth == 0) {
389 $termname = $t->name;
390 if ($term->name == $termname) {
391 return 'project/' . drupal_strtolower($termname);
392 }
393 break;
394 }
395 }
396 // TODO: when the project browsing views work, if there's a direct URL to
397 // link to a project category view, we should add that here.
398 // Also, note that in the project_solr case, we'd like to link to,
399 // e.g. "project/modules?filter=tid:12" but it's apparently impossible to
400 // return url() for this since things get double encoded or something.
401 // So, for now, we punt and always link the subterms to the taxonomy page.
402 return 'taxonomy/term/'. $term->tid;
403 }
404
405 /**
406 * Implementation of hook_taxonomy().
407 */
408 function project_taxonomy($op, $type, $array = NULL) {
409 if ($op == 'delete' && $type == 'vocabulary' && $array['vid'] == _project_get_vid()) {
410 variable_del('project_vocabulary');
411 }
412 }
413
414 /**
415 * Determine if the currently logged in user could have access to any project_project nodes.
416 */
417 function project_project_access_any() {
418 return user_access('access projects') || user_access('access own projects') || user_access('administer projects');
419 }
420
421 /**
422 * Implement hook_menu().
423 */
424 function project_menu() {
425 $items = array();
426
427 // @TODO: The project type and browsing views are currently set to require
428 // 'access projects' permission. However, we really want them to require at
429 // least one of 'access projects', 'access own projects', or 'administer
430 // projects'. At this point I'm not sure what the best way to do this is,
431 // but we need to figure this out to keep the same functionality.
432
433 $items['project/autocomplete/project'] = array(
434 'title' => 'Autocomplete project',
435 'page callback' => 'project_autocomplete_project',
436 'access callback' => 'project_project_access_any',
437 'type' => MENU_CALLBACK,
438 );
439
440 $items['project/autocomplete/project-user/%'] = array(
441 'title' => 'Autocomplete project',
442 'page callback' => 'project_autocomplete_project_user',
443 'page arguments' => array(3, 4),
444 'access callback' => 'project_project_access_any',
445 'type' => MENU_CALLBACK,
446 );
447
448 // CVS messages:
449 $items['project/cvs'] = array(
450 'title' => 'CVS',
451 'page callback' => 'project_cvs',
452 'access callback' => 'project_project_access_any',
453 'type' => MENU_CALLBACK,
454 );
455
456 // Administration pages
457 $items['admin/project'] = array(
458 'title' => 'Project administration',
459 'description' => 'Administrative interface for project management and related modules.',
460 'page callback' => 'system_admin_menu_block_page',
461 'access arguments' => array('administer projects'),
462 'file' => 'system.admin.inc',
463 'file path' => drupal_get_path('module', 'system'),
464 'type' => MENU_NORMAL_ITEM,
465 );
466 $items['admin/project/project-settings'] = array(
467 'title' => 'Project settings',
468 'description' => 'Configure settings for the Project module.',
469 'page callback' => 'drupal_get_form',
470 'page arguments' => array('project_settings_form'),
471 'access arguments' => array('administer projects'),
472 'type' => MENU_NORMAL_ITEM,
473 );
474
475 $items['node/%project_edit_project/edit/project'] = array(
476 'title' => 'Project',
477 'page callback' => 'node_page',
478 'page arguments' => array(1),
479 'access callback' => 'node_access',
480 'access arguments' => array('update', 1),
481 'weight' => -5, 'type' => MENU_DEFAULT_LOCAL_TASK,
482 );
483 return $items;
484 }
485
486 /**
487 * Menu loader callback to load a project node.
488 *
489 * @param $arg
490 * The menu argument to attempt to load a project from. Can be either a
491 * numeric node ID (nid), or a string project short name (uri).
492 *
493 * @return
494 * The loaded node object if the argument was a valid project, FALSE if not.
495 */
496 function project_node_load($arg) {
497 if (is_numeric($arg)) {
498 $nid = $arg;
499 }
500 else {
501 $nid = db_result(db_query(db_rewrite_sql("SELECT p.nid FROM {project_projects} p WHERE p.uri = '%s'", 'p'), $arg));
502 }
503 $node = node_load($nid);
504 if (!isset($node->type) || $node->type != 'project_project') {
505 return FALSE;
506 }
507 return $node;
508 }
509
510 /**
511 * Menu loader callback to load a project node if the edit tab needs subtabs.
512 *
513 * Load a project_project node if the given nid is a project_project node and
514 * if the user has permission to edit the project and if either the
515 * project_issue or project_release module exists.
516 */
517 function project_edit_project_load($nid) {
518 if (!module_exists('project_issue') && !module_exists('project_release')) {
519 return FALSE;
520 }
521 return project_node_load($nid);
522 }
523
524 function project_check_admin_access($project, $cvs_access = NULL) {
525 global $user;
526 if (empty($user->uid)) {
527 return FALSE;
528 }
529
530 $project_obj = is_numeric($project) ? node_load($project) : $project;
531 if (!isset($project_obj) || (isset($project_obj->type) && $project_obj->type != 'project_project')) {
532 return FALSE;
533 }
534
535 if (user_access('administer projects')) {
536 return TRUE;
537 }
538
539 // If $cvs_access is not defined, check to make sure the user has cvs access
540 // and that the user's cvs account is approved.
541 if (project_use_cvs($project_obj) && !isset($cvs_access)) {
542 if (db_result(db_query("SELECT COUNT(*) FROM {cvs_accounts} WHERE uid = %d AND status = %d", $user->uid, CVS_APPROVED))) {
543 $cvs_access = TRUE;
544 }
545 else {
546 $cvs_access = FALSE;
547 }
548 }
549
550 if (user_access('maintain projects')) {
551 if ($user->uid == $project_obj->uid) {
552 return TRUE;
553 }
554 if (project_use_cvs($project_obj) && $cvs_access) {
555 if (db_result(db_query("SELECT COUNT(*) FROM {cvs_project_maintainers} WHERE uid = %d AND nid = %d", $user->uid, $project_obj->nid))) {
556 return TRUE;
557 }
558 }
559 }
560 return FALSE;
561 }
562
563 /**
564 * Implementation of form_alter. This removes the work of
565 * taxonomy.module's form_alter() so we can do our own taxonomy
566 * selection.
567 */
568 function project_form_alter(&$form, &$form_state, $form_id) {
569 switch ($form_id) {
570 case 'project_project_node_form':
571 $vid = _project_get_vid();
572 if (isset($form['taxonomy'][$vid])) {
573 unset($form['taxonomy'][$vid]);
574 }
575 // If there are no children elements, we should unset the entire
576 // thing so we don't end up with an empty fieldset.
577 if (!empty($form['taxonomy']) && (!element_children($form['taxonomy']))) {
578 unset($form['taxonomy']);
579 }
580
581 // In D6, FAPI changed in such a way that completely unsetting
582 // $form['taxonomy'] causes warnings and errors when the project node is
583 // previewed. In order to prevent these problems, we need to add a
584 // submit handler to the preview button, and this handler needs to put
585 // the taxonomy terms back into $node->taxonomy like they would be if
586 // project wasn't replacing the usual taxonomy selection form elements
587 // with its own elements.
588 if (isset($form['buttons']['preview'])) {
589 $form['buttons']['preview']['#submit'][] = 'project_project_form_preview_submit';
590 }
591
592 // If the form has an element for specifying a URL alias, we want
593 // to alter it, since we're just going to automatically override
594 // the specified value.
595 if (variable_get('project_enable_alias', TRUE) && isset($form['path'])) {
596 $url_alias = $form['path']['path']['#default_value'];
597 if (empty($url_alias)) {
598 unset($form['path']);
599 }
600 else {
601 unset($form['path']['path']);
602 $form['path']['value'] = array(
603 '#prefix' => '<div>',
604 '#value' => t('Automatically generated path alias: %url', array('%url' => $url_alias)),
605 '#suffix' => '</div>',
606 );
607 }
608 }
609 break;
610
611 case 'taxonomy_form_vocabulary':
612 // Add a validate handler to the vocabulary form if the
613 // Project type vocabulary term is being edited so that its
614 // possible to detect if this vocabulary was set to be
615 // a free tagging vocabulary, and if so then throw an error.
616 $project_type_vid = _project_get_vid();
617 if (isset($project_type_vid) && $form['vid']['#value'] == $project_type_vid) {
618 $form['#validate'][] = 'project_project_type_vocabulary_validate';
619
620 }
621 break;
622 }
623 }
624
625 /**
626 * Implement hook_access().
627 */
628 function project_project_access($op, $node, $account) {
629 switch ($op) {
630 case 'view':
631 // Since this function is shared for project_release nodes, we have to
632 // be careful what node we pass to project_check_admin_access().
633 if ($node->type == 'project_release') {
634 $node = node_load($node->project_release['pid']);
635 }
636 if (project_check_admin_access($node)) {
637 return TRUE;
638 }
639 if (!user_access('access projects')) {
640 return FALSE;
641 }
642 break;
643 case 'create':
644 if ($account->uid && user_access('maintain projects')) {
645 // Since this CVS access checking is non-standard, we need to
646 // special-case uid 1 to always allow everything.
647 if ($account->uid != 1 && module_exists('cvs') && variable_get('cvs_restrict_project_creation', 1)) {
648 return db_result(db_query("SELECT uid FROM {cvs_accounts} WHERE uid = %d AND status = %d", $account->uid, CVS_APPROVED)) ? TRUE : FALSE;
649 }
650 else {
651 return TRUE;
652 }
653 }
654 break;
655 case 'update':
656 if (project_check_admin_access($node)) {
657 return TRUE;
658 }
659 break;
660 case 'delete':
661 if (user_access('delete any projects', $account) || (user_access('delete own projects', $account) && ($account->uid == $node->uid))) {
662 return TRUE;
663 }
664 break;
665 }
666 }
667
668 /**
669 * Implement hook_load().
670 */
671 function project_project_load($node) {
672 $additions = db_fetch_array(db_query('SELECT * FROM {project_projects} WHERE nid = %d', $node->nid));
673 $project = new stdClass;
674 $project->project = $additions;
675 return $project;
676 }
677
678 /**
679 * hook_nodeapi() implementation. This just decides what type of node
680 * is being passed, and calls the appropriate type-specific hook.
681 *
682 * @see project_project_nodeapi().
683 */
684 function project_nodeapi(&$node, $op, $arg) {
685 switch ($node->type) {
686 case 'project_project':
687 module_load_include('inc', 'project', 'project');
688 project_project_nodeapi($node, $op, $arg);
689 break;
690 }
691 }
692
693 /**
694 * Build a SQL statment from a structured array.
695 *
696 * @param $sql_elements
697 * An array with the following keys:
698 * 'fields', 'from', 'joins', 'wheres', 'group_bys', 'order_bys',
699 * 'parameters'
700 * Each value is an array with the following keys:
701 * 'prefix', 'glue', 'pieces'
702 * @return
703 * SQL string.
704 *
705 * @TODO This should move to project_usage.module
706 */
707 function project_build_query($sql_elements) {
708 $sql = '';
709 foreach ($sql_elements as $key => $sql_element) {
710 if ($key != 'parameters' && count($sql_element['pieces'])) {
711 $sql .= $sql_element['prefix'] . implode($sql_element['glue'], $sql_element['pieces']);
712 }
713 }
714 return db_rewrite_sql($sql);
715 }
716
717 /**
718 * Construct an empty query for project_build_query().
719 *
720 * Set the default elements that will be used to construct the SQL statement.
721 *
722 * @TODO This should move to project_usage.module
723 */
724 function project_empty_query() {
725 return array(
726 'fields' => array(
727 'prefix' => 'SELECT ',
728 'glue' => ', ',
729 'pieces' => array(),
730 ),
731 'from' => array(
732 'prefix' => ' FROM ',
733 'glue' => NULL,
734 'pieces' => array(''),
735 ),
736 'joins' => array(
737 'prefix' => '',
738 'glue' => ' ',
739 'pieces' => array(),
740 ),
741 'wheres' => array(
742 'prefix' => ' WHERE ',
743 'glue' => ' AND ',
744 'pieces' => array(),
745 ),
746 'group_bys' => array(
747 'prefix' => ' GROUP BY ',
748 'glue' => ', ',
749 'pieces' => array(),
750 ),
751 'order_bys' => array(
752 'prefix' => ' ORDER BY ',
753 'glue' => ', ',
754 'pieces' => array(),
755 ),
756 'parameters' => array(
757 'prefix' => NULL,
758 'glue' => NULL,
759 'pieces' => array(),
760 )
761 );
762 }
763
764 /**
765 * Helper function for grouping nodes by date.
766 */
767 function _project_date($timestamp) {
768 static $last;
769 $date = format_date($timestamp, 'custom', 'F j, Y');
770 if ($date != $last) {
771 $last = $date;
772 return $date;
773 }
774 }
775
776 /**
777 * Returns an array of all projects on the system for use with select
778 * form elements. The keys are the project nid, and the values are
779 * the project names. If the project taxonomy is in use, the array
780 * will be sorted into the project categories with appropriate headers
781 * for each term.
782 *
783 * @param $project_urls
784 * Reference to be filled with an array of project uri => id mappings. This
785 * array is used by the project_issue search form code.
786 * @param $issues
787 * If TRUE, only projects with issue tracking enabled are returned.
788 * For this option to do much good, the project_issue.module should be
789 * enabled.
790 * @param $key_prefix
791 * Prefix to prepend to all keys in the returned array.
792 */
793 function project_projects_select_options(&$project_urls, $issues = TRUE, $key_prefix = NULL) {
794 $projects = array();
795 $ISSUE_JOIN = '';
796 $ISSUE_WHERE = '';
797 $args_use_taxonomy = array(1, 0); // n.status, h.parent
798 $args_no_taxonomy = array(1); // n.status
799 if ($issues && module_exists('project_issue')) {
800 $ISSUE_JOIN ='INNER JOIN {project_issue_projects} pip ON n.nid = pip.nid';
801 $ISSUE_WHERE = 'AND pip.issues = %d';
802 $args_use_taxonomy[] = 1;
803 $args_no_taxonomy[] = 1;
804 }
805 if (project_use_taxonomy()) {
806 $vid = _project_get_vid();
807 $args_use_taxonomy[] = $vid;
808 $result = db_query(db_rewrite_sql("SELECT n.nid, n.title, d.name, p.uri FROM {project_projects} p INNER JOIN {node} n ON n.nid = p.nid $ISSUE_JOIN LEFT JOIN {term_node} t ON t.vid = n.vid INNER JOIN {term_data} d ON t.tid = d.tid INNER JOIN {term_hierarchy} h ON t.tid = h.tid WHERE n.status = %d AND h.parent = %d $ISSUE_WHERE AND d.vid = %d GROUP BY n.nid, n.title, d.name, p.uri, d.weight ORDER BY d.weight, n.title"), $args_use_taxonomy);
809 while ($project = db_fetch_object($result)) {
810 if (isset($project->name)) {
811 if (!isset($projects[$project->name])) {
812 $projects[$project->name] = array();
813 }
814 $projects[$project->name][$key_prefix . $project->nid] = $project->title;
815 }
816 else {
817 $projects[$key_prefix . $project->nid] = $project->title;
818 }
819 if (is_array($project_urls)) {
820 $project_urls[$project->uri] = $project->nid;
821 }
822 }
823 }
824 else {
825 $result = db_query(db_rewrite_sql("SELECT n.nid, p.uri, n.title FROM {project_projects} p INNER JOIN {node} n ON n.nid = p.nid $ISSUE_JOIN WHERE n.status = %d $ISSUE_WHERE ORDER BY n.title"), $args_no_taxonomy);
826 while ($project = db_fetch_object($result)) {
827 $projects[$key_prefix . $project->nid] = $project->title;
828 if (is_array($project_urls)) {
829 $project_urls[$project->uri] = $project->nid;
830 }
831 }
832 }
833 return $projects;
834 }
835
836 function project_quick_navigate_form() {
837 $uris = NULL;
838 $projects = array(t('- Select a project -')) + project_projects_select_options($uris, FALSE);
839 $form = array();
840 $form['project_goto'] = array(
841 '#type' => 'select',
842 '#default_value' => '0',
843 '#options' => $projects,
844 '#attributes' => array('title' => t('Select a project to view')),
845 );
846 $form['project'] = array(
847 '#type' => 'submit',
848 '#value' => t('View project'),
849 '#submit' => array('project_quick_navigate_project_submit'),
850 );
851 return $form;
852 }
853
854 function project_quick_navigate_form_validate($form, $form_state) {
855 if (empty($form_state['values']['project_goto'])) {
856 form_set_error('project_goto', t('You must select a project to navigate to.'));
857 }
858 }
859
860 function project_quick_navigate_project_submit($form, &$form_state) {
861 $form_state['redirect'] = 'node/'. $form_state['values']['project_goto'];
862 }
863
864 function project_quick_navigate_title_form() {
865 $form = array();
866 $form['project_title'] = array(
867 '#title' => t('Project name'),
868 '#type' => 'textfield',
869 '#size' => 20,
870 '#autocomplete_path' => 'project/autocomplete/project',
871 );
872 $form['project'] = array(
873 '#type' => 'submit',
874 '#value' => t('View project'),
875 '#validate' => array('project_quick_navigate_title_project_validate'),
876 '#submit' => array('project_quick_navigate_title_project_submit'),
877 );
878 return $form;
879 }
880
881 function project_quick_navigate_title_project_validate($form, &$form_state) {
882 if (empty($form_state['values']['project_title'])) {
883 form_set_error('project_title', t('You must enter a project to navigate to.'));
884 }
885 else {
886 $nid = db_result(db_query("SELECT nid FROM {node} WHERE title = '%s' AND type = '%s'", $form_state['values']['project_title'], 'project_project'));
887 if (empty($nid)) {
888 form_set_error('project_title', t('The name you entered (%title) is not a valid project.', array('%title' => $form_state['values']['project_title'])));
889 }
890 else {
891 $form_state['nid'] = $nid;
892 }
893 }
894 }
895
896 function project_quick_navigate_title_project_submit($form, &$form_state) {
897 $form_state['redirect'] = 'node/'. $form_state['nid'];
898 }
899
900
901 /**
902 * Implementation of hook_views_api().
903 */
904 function project_views_api() {
905 return array(
906 'api' => 2,
907 'path' => drupal_get_path('module', 'project') .'/views',
908 );
909 }
910
911 // Themables
912
913 /**
914 * Implementation of hook_theme().
915 */
916 function project_theme() {
917 return array(
918 'project_term_list' => array(
919 'arguments' => array(
920 'terms' => NULL,
921 'path' => NULL,
922 ),
923 ),
924 'project_summary' => array(
925 'arguments' => array(
926 'project' => NULL,
927 ),
928 ),
929 'project_feed_icon' => array(
930 'arguments' => array(
931 'url' => NULL,
932 'title' => NULL,
933 ),
934 ),
935 'project_project_node_form_taxonomy' => array(
936 'file' => 'project.inc',
937 'arguments' => array(
938 'form' => NULL,
939 ),
940 ),
941 'project_views_project_sort_method_options_form' => array(
942 'file' => 'theme.inc',
943 'path' => drupal_get_path('module', 'project') .'/views/theme',
944 'arguments' => array(
945 'form' => NULL,
946 ),
947 ),
948 'project_type' => array(
949 'arguments' => array(
950 'term' => NULL,
951 ),
952 ),
953 'project_type_description' => array(
954 'arguments' => array(
955 'term' => NULL,
956 ),
957 ),
958 );
959 }
960
961 function theme_project_term_list($terms, $path) {
962 $depth = 0;
963 $output = "\n<ul class=\"project-terms\">\n";
964 foreach ($terms as $term) {
965 if (!empty($term->description)){
966 $description = strip_tags($term->description);
967 }
968 else {
969 $description = '';
970 }
971 $link = l($term->name .' ('. $term->count .')', "$path/$term->tid", array('attributes' => array('title' => $description)));
972 $output .= '<li class="leaf">' . $link . "</li>\n";
973 }
974 $output .= "\n</ul>\n";
975 return $output;
976 }
977
978 /**
979 * Theme a compact project view/summary.
980 *
981 * $project has the following fields:
982 * - title: Title
983 * - nid: Node id
984 * - body: Filtered description
985 * - term: String with the selected term name
986 * - version: String with the version
987 * - links: Array of links
988 */
989 function theme_project_summary($project) {
990 $output = '<div class="' . $project->class . '">';
991 $output .= '<h2>'. l($project->title, "node/$project->nid") .'</h2>';
992 if (!empty($project->changed)) {
993 $output .= '<p><small>' . t('Last changed: !interval ago', array('!interval' => format_interval(time() - $project->changed, 2))) . '</small></p>';
994 }
995 $output .= $project->body;
996 if (!empty($project->download_table)) {
997 $output .= $project->download_table;
998 }
999 if (!empty($project->links)) {
1000 $output .= theme('links', $project->links);
1001 }
1002 if (!empty($project->terms)) {
1003 $output .= theme('links', $project->terms);
1004 }
1005 $output .= '</div>';
1006 return $output;
1007 }
1008
1009 /**
1010 * Display an RSS feed icon for use on project nodes.
1011 */
1012 function theme_project_feed_icon($url, $title) {
1013 if ($icon = theme('image', 'misc/feed.png', $title, $title)) {
1014 return '<a href="'. check_url($url) .'" class="project-feed-icon">'. $icon .'</a>';
1015 }
1016 }
1017
1018 /**
1019 * Return a string of valid project names for use with JS autocomplete fields.
1020 */
1021 function project_autocomplete_project($string = '') {
1022 $matches = array();
1023
1024 // The user enters a comma-separated list of projects. We only autocomplete
1025 // the last one.
1026 $array = drupal_explode_tags($string);
1027 $last_string = trim(array_pop($array));
1028
1029 if ($last_string != '') {
1030 $result = db_query_range(db_rewrite_sql("SELECT n.title FROM {node} n WHERE n.status = %d AND n.type = '%s' AND LOWER(n.title) LIKE LOWER('%%%s%%')"), 1, 'project_project', $last_string, 0, 10);
1031
1032 $prefix = count($array) ? implode(', ', $array) .', ' : '';
1033 while ($project = db_fetch_object($result)) {
1034 $title = $project->title;
1035 // Commas and quotes in terms are special cases, so encode 'em.
1036 if (strpos($title, ',') !== FALSE || strpos($title, '"') !== FALSE) {
1037 $title = '"'. str_replace('"', '""', $project->title) .'"';
1038 }
1039 $matches[$prefix . $title] = check_plain($project->title);
1040 }
1041 }
1042
1043 drupal_json($matches);
1044 }
1045
1046
1047 /**
1048 * Return a string of valid project names owned by a given user.
1049 */
1050 function project_autocomplete_project_user($uid, $string = '') {
1051 $matches = array();
1052
1053 // The user enters a comma-separated list of projects. We only autocomplete
1054 // the last one.
1055 $array = drupal_explode_tags($string);
1056 $last_string = trim(array_pop($array));
1057
1058 if ($last_string != '') {
1059 $result = db_query_range(db_rewrite_sql("SELECT n.title FROM {node} n WHERE n.status = %d AND n.type = '%s' AND n.uid = %d AND LOWER(n.title) LIKE LOWER('%%%s%%')"), 1, 'project_project', $uid, $last_string, 0, 10);
1060
1061 $prefix = count($array) ? implode(', ', $array) .', ' : '';
1062 while ($project = db_fetch_object($result)) {
1063 $title = $project->title;
1064 // Commas and quotes in terms are special cases, so encode 'em.
1065 if (strpos($title, ',') !== FALSE || strpos($title, '"') !== FALSE) {
1066 $title = '"'. str_replace('"', '""', $project->title) .'"';
1067 }
1068 $matches[$prefix . $title] = check_plain($project->title);
1069 }
1070 }
1071
1072 drupal_json($matches);
1073 }
1074
1075 /**
1076 * Returns whether or not the project module should use
1077 * taxonomy-specific functionality.
1078 */
1079 function project_use_taxonomy() {
1080 return module_exists('taxonomy') && taxonomy_get_tree(_project_get_vid());
1081 }
1082
1083 /**
1084 * Returns whether or not the project uses version control or not.
1085 */
1086 function project_use_cvs($project) {
1087 if (module_exists('cvs')) {
1088 $project = is_numeric($project) ? node_load($project) : $project;
1089 return !empty($project->cvs['repository']);
1090 }
1091 }
1092
1093 /**
1094 * Determines if the current site supports caching of project-related pages.
1095 *
1096 * The pages can be cached if:
1097 * 1. Anonymous users have the 'access projects' permission.
1098 * 2. No node access modules are installed.
1099 *
1100 * @return TRUE if the output can be cached, FALSE otherwise.
1101 */
1102 function project_can_cache() {
1103 $grants = module_implements('node_grants');
1104 if (!empty($grants)) {
1105 return FALSE;
1106 }
1107 $allowed_roles = user_roles(FALSE, 'access projects');
1108 if (!isset($allowed_roles[DRUPAL_ANONYMOUS_RID])) {
1109 return FALSE;
1110 }
1111
1112 return TRUE;
1113 }
1114
1115 /**
1116 * Intelligently merge project type terms selected by the
1117 * user back into $node->taxonomy during a node preview.
1118 */
1119 function project_project_form_preview_submit($form, &$form_state) {
1120 $project_type_vid = _project_get_vid();
1121 if (isset($project_type_vid) && isset($form_state['values']['project_type'])) {
1122 $project_type_tid = $form_state['values']['project_type'];
1123 $project_type_tid_field = 'tid_' . $project_type_tid;
1124 $project_type_terms = array();
1125 $project_type_terms[$project_type_tid] = $project_type_tid;
1126 if (isset($form_state['values']->$project_type_tid_field)) {
1127 foreach ($form_state['values'][$project_type_tid_field] as $tid) {
1128 $project_type_terms[$tid] = $tid;
1129 }
1130 }
1131 $form_state['node']['taxonomy'][$project_type_vid] = $project_type_terms;
1132 }
1133 }
1134
1135 /**
1136 * Present a validation error if the user tries to make the
1137 * Project types vocabulary a free tagging vocabulary.
1138 */
1139 function project_project_type_vocabulary_validate($form, &$form_state) {
1140 if (!empty($form_state['values']['tags'])) {
1141 form_set_error('tags', t('The %name vocabulary does not support tags.', array('%name' => $form_state['values']['name'])));
1142 }
1143 }
1144
1145 /**
1146 * Theme a project type item.
1147 */
1148 function theme_project_type($term) {
1149 $link = l($term->name, check_url('project/'. drupal_strtolower($term->name)));
1150 $link = str_replace('%20', '+', $link);
1151 $output = "<div>$link</div>\n";
1152 if ($term->description) {
1153 $output .= '<p>' . filter_xss($term->description) . '</p>';
1154 }
1155 return $output;
1156 }
1157
1158 /**
1159 * Theme a project type description.
1160 */
1161 function theme_project_type_description($term) {
1162 $output = '';
1163 if ($term->description) {
1164 $output .= '<p>' . filter_xss($term->description) . '</p>';
1165 }
1166 return $output;
1167 }
1168
1169
1170 /**
1171 * Find a project node ID (nid) based on project short name (uri).
1172 *
1173 * @param $uri
1174 * The project shortname (uri) to search for.
1175 *
1176 * @return
1177 * The project node ID (nid) of the given project, or NULL if not found.
1178 */
1179 function project_get_nid_from_uri($uri) {
1180 static $nids = array();
1181 if (!isset($nids[$uri])) {
1182 $nids[$uri] = db_result(db_query_range("SELECT nid FROM {project_projects} WHERE uri = '%s'", $uri, 0, 1));
1183 }
1184 return $nids[$uri];
1185 }
1186
1187 /**
1188 * Find a project shortname (uri) based on project node ID (nid).
1189 *
1190 * @param $uri
1191 * The project node ID (nid) to search for.
1192 *
1193 * @return
1194 * The project shortname (uri) of the given project, or NULL if not found.
1195 */
1196 function project_get_uri_from_nid($nid) {
1197