diff --git a/ContributionPlugin.php b/ContributionPlugin.php index 4afff01e..a5e33f3a 100644 --- a/ContributionPlugin.php +++ b/ContributionPlugin.php @@ -27,14 +27,17 @@ class ContributionPlugin extends Omeka_Plugin_AbstractPlugin protected $_hooks = array( 'initialize', 'install', - 'uninstall', 'upgrade', + 'uninstall', + 'uninstall_message', + 'config_form', + 'config', 'define_acl', 'define_routes', - 'uninstall_message', 'admin_items_search', 'admin_items_show_sidebar', 'admin_items_browse_detailed_each', + 'public_items_show', 'items_browse_sql', 'before_save_item', 'after_delete_item', @@ -59,16 +62,17 @@ class ContributionPlugin extends Omeka_Plugin_AbstractPlugin * @var array Options and their default values. */ protected $_options = array( - 'contribution_page_path', - 'contribution_email_sender', - 'contribution_email_recipients', - 'contribution_consent_text', - 'contribution_collection_id', - 'contribution_default_type', - 'contribution_user_profile_type', - 'contribution_open', - 'contribution_email', - 'contribution_strict_anonymous' + 'contribution_page_path' => 'contribution', + 'contribution_email_sender' => '', + 'contribution_email_recipients' => '', + 'contribution_consent_text' => '', + 'contribution_collection_id' => null, + 'contribution_default_type' => null, + 'contribution_user_profile_type' => null, + 'contribution_open' => false, + 'contribution_email' => '', + 'contribution_strict_anonymous' => false, + 'contribution_allow_edit' => false, ); public function setUp() @@ -78,7 +82,7 @@ public function setUp() $this->_hooks[] = 'user_profiles_user_page'; } - if (! is_admin_theme()) { + if (!is_admin_theme()) { //dig up all the elements being used, and add their ElementForm hook $elementsTable = $this->_db->getTable('Element'); $select = $elementsTable->getSelect(); @@ -111,10 +115,12 @@ public function hookInstall() `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `item_type_id` INT UNSIGNED NOT NULL, `display_name` VARCHAR(255) NOT NULL, - `file_permissions` ENUM('Disallowed', 'Allowed', 'Required') NOT NULL DEFAULT 'Disallowed', + `file_permissions` ENUM('Disallowed', 'Allowed', 'Required') NOT NULL, + `multiple_files` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', + `add_tags` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `item_type_id` (`item_type_id`) - ) ENGINE=MyISAM;"; + ) ENGINE=InnoDB;"; $this->_db->query($sql); $sql = "CREATE TABLE IF NOT EXISTS `$db->ContributionTypeElement` ( @@ -127,7 +133,7 @@ public function hookInstall() PRIMARY KEY (`id`), UNIQUE KEY `type_id_element_id` (`type_id`, `element_id`), KEY `order` (`order`) - ) ENGINE=MyISAM;"; + ) ENGINE=InnoDB;"; $this->_db->query($sql); $sql = "CREATE TABLE IF NOT EXISTS `$db->ContributionContributedItem` ( @@ -135,38 +141,22 @@ public function hookInstall() `item_id` INT UNSIGNED NOT NULL, `public` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', `anonymous` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', + `deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `item_id` (`item_id`) - ) ENGINE=MyISAM;"; + ) ENGINE=InnoDB;"; $this->_db->query($sql); + $this->_options['contribution_email_recipients'] = get_option('administrator_email'); + $this->_installOptions(); + $this->_createDefaultContributionTypes(); - set_option('contribution_email_recipients', get_option('administrator_email')); } - /** - * Contribution uninstall hook - */ - public function hookUninstall() + public function hookUpgrade($args) { - // Delete all the Contribution options - foreach ($this->_options as $option) { - delete_option($option); - } $db = $this->_db; - // Drop all the Contribution tables - $sql = "DROP TABLE IF EXISTS - `$db->ContributionType`, - `$db->ContributionTypeElement`, - `$db->ContributionContributor`, - `$db->ContributionContributedItem`, - `$db->ContributionContributorField`, - `$db->ContributionContributorValue`;"; - $this->_db->query($sql); - } - public function hookUpgrade($args) - { $oldVersion = $args['old_version']; $newVersion = $args['new_version']; // Catch-all for pre-2.0 versions @@ -181,7 +171,8 @@ public function hookUpgrade($args) } $pagePath = get_option('contribution_page_path'); - if ($pagePath = 'contribution/') { + $pagePath = 'contribution/'; + if ($pagePath) { delete_option('contribution_page_path'); } else { set_option('contribution_page_path', trim($pagePath, '/')); @@ -192,7 +183,7 @@ public function hookUpgrade($args) $this->hookInstall(); } - + if (version_compare($oldVersion, '3.0', '<')) { if(!is_writable(CONTRIBUTION_PLUGIN_DIR . "/upgrade_files")) { throw new Omeka_Plugin_Installer_Exception("'upgrade_files' directory must be writable by the web server"); @@ -227,7 +218,7 @@ public function hookUpgrade($args) $this->_db->query($sql); } - + if (version_compare($oldVersion, '3.0.2', '<')) { //fix some previous bad upgrades //need to check if contributor_posting was properly changed to anonymous @@ -254,11 +245,70 @@ public function hookUpgrade($args) $this->_db->query($sql); } } - + if (version_compare($oldVersion, 3.1, '<')) { set_option('contribution_open', get_option('contribution_simple')); delete_option('contribution_simple'); } + + if (version_compare($oldVersion, '3.1.1', '<')) { + // Need to check columns with old versions. + $sql = "SHOW COLUMNS IN `{$db->ContributionType}`"; + $result = $db->query($sql); + $cols = $result->fetchAll(Zend_Db::FETCH_COLUMN); + if (!in_array('multiple_files', $cols)) { + $sql = "ALTER TABLE `$db->ContributionType` ADD COLUMN `multiple_files` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0'"; + $db->query($sql); + } + } + + if (version_compare($oldVersion, '3.1.2', '<')) { + // Need to check columns with old versions. + $sql = "SHOW COLUMNS IN `{$db->ContributionType}`"; + $result = $db->query($sql); + $cols = $result->fetchAll(Zend_Db::FETCH_COLUMN); + if (!in_array('add_tags', $cols)) { + $sql = "ALTER TABLE `$db->ContributionType` ADD COLUMN `add_tags` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0'"; + $db->query($sql); + } + } + + if (version_compare($oldVersion, '3.1.3', '<')) { + // Need to check columns with old versions. + $sql = "SHOW COLUMNS IN `{$db->ContributionContributedItem}`"; + $result = $db->query($sql); + $cols = $result->fetchAll(Zend_Db::FETCH_COLUMN); + if (!in_array('deleted', $cols)) { + $sql = "ALTER TABLE `$db->ContributionContributedItem` ADD COLUMN `deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `anonymous`"; + $db->query($sql); + } + } + + if (version_compare($oldVersion, '3.1.4', '<')) { + $pagePath = get_option('contribution_page_path'); + if (empty($pagePath)) { + set_option('contribution_page_path', $this->_options['contribution_page_path']); + } + } + } + + /** + * Contribution uninstall hook + */ + public function hookUninstall() + { + $db = $this->_db; + // Drop all the Contribution tables + $sql = "DROP TABLE IF EXISTS + `$db->ContributionType`, + `$db->ContributionTypeElement`, + `$db->ContributionContributor`, + `$db->ContributionContributedItem`, + `$db->ContributionContributorField`, + `$db->ContributionContributorValue`;"; + $this->_db->query($sql); + + $this->_uninstallOptions(); } public function hookUninstallMessage() @@ -269,6 +319,45 @@ public function hookUninstallMessage()
The contributed items themselves will remain.
'; } + /** + * Shows plugin configuration page. + */ + public function hookConfigForm($args) + { + $view = $args['view']; + + $form = new Contribution_Form_Settings; + $defaults = $form->getCurrentOptions(); + $form->setDefaults($defaults); + + echo $view->partial( + 'plugins/contribution-config-form.php', + array( + 'form' => $form, + )); + } + + /** + * Saves plugin configuration page. + * + * @param array Options set in the config form. + */ + public function hookConfig($args) + { + $post = $args['post']; + + // Manage an exception. + if (empty($post['contribution_page_path'])) { + $post['contribution_page_path'] = $this->_options['contribution_page_path']; + } + + foreach ($this->_options as $optionKey => $optionValue) { + if (isset($post[$optionKey])) { + set_option($optionKey, $post[$optionKey]); + } + } + } + /** * Contribution define_acl hook * Restricts access to admin-only controllers and actions. @@ -276,15 +365,19 @@ public function hookUninstallMessage() public function hookDefineAcl($args) { $acl = $args['acl']; - + $acl->addRole(new Zend_Acl_Role('contribution-anonymous'), null); - + $acl->addResource('Contribution_Contribution'); $acl->allow(array('super', 'admin', 'researcher', 'contributor'), 'Contribution_Contribution'); + $privileges = array('show', 'contribute', 'thankyou', 'my-contributions', 'type-form'); + if (get_option('contribution_allow_edit')) { + $privileges[] = 'edit'; + } if (get_option('contribution_open')) { - $acl->allow(null, 'Contribution_Contribution', array('show', 'contribute', 'thankyou', 'my-contributions', 'type-form')); + $acl->allow(null, 'Contribution_Contribution', $privileges); } else { - $acl->allow('guest', 'Contribution_Contribution', array('show', 'contribute', 'thankyou', 'my-contributions', 'type-form')); + $acl->allow('guest', 'Contribution_Contribution', $privileges); } $acl->allow(null, 'Contribution_Contribution', array('contribute', 'terms', 'thankyou')); @@ -305,40 +398,51 @@ public function hookDefineAcl($args) /** * Contribution define_routes hook + * * Defines public-only routes that set the contribution controller as the * only accessible one. */ public function hookDefineRoutes($args) { $router = $args['router']; + // Only apply custom routes on public theme. // The wildcards on both routes make these routes always apply for the // contribution controller. - // get the base path - $bp = get_option('contribution_page_path'); - if ($bp) { - $router->addRoute('contributionCustom', - new Zend_Controller_Router_Route("$bp/:action/*", - array('module' => 'contribution', - 'controller' => 'contribution', - 'action' => 'contribute'))); - } else { - - $router->addRoute('contributionDefault', - new Zend_Controller_Router_Route('contribution/:action/*', - array('module' => 'contribution', - 'controller' => 'contribution', - 'action' => 'contribute'))); - - } + // Get the base path. The check is kept in case of error. + $basePath = get_option('contribution_page_path') ?: $this->_options['contribution_page_path']; + $router->addRoute('contribution', + new Zend_Controller_Router_Route( + "$basePath/:action/*", + array( + 'module' => 'contribution', + 'controller' => 'contribution', + 'action' => 'contribute', + ))); + + $router->addRoute('contributionId', + new Zend_Controller_Router_Route( + "$basePath/:action/:id/*", + array( + 'module' => 'contribution', + 'controller' => 'contribution', + 'action' => 'contribute', + ), + array( + 'action' => 'edit', + 'id' => '\d+', + ))); if (is_admin_theme()) { $router->addRoute('contributionAdmin', - new Zend_Controller_Router_Route('contribution/:controller/:action/*', - array('module' => 'contribution', - 'controller' => 'index', - 'action' => 'index'))); + new Zend_Controller_Router_Route( + 'contribution/:controller/:action/*', + array( + 'module' => 'contribution', + 'controller' => 'index', + 'action' => 'index', + ))); } } @@ -361,20 +465,20 @@ public function filterApiResources($apiResources) ); return $apiResources; } - + public function filterApiImportOmekaAdapters($adapters, $args) { if (strpos($args['endpointUri'], 'omeka.net') !== false) { - $contributedItemAdapter = + $contributedItemAdapter = new ApiImport_ResponseAdapter_Omeka_GenericAdapter(null, $args['endpointUri'], 'ContributionContributedItem'); $contributedItemAdapter->setResourceProperties(array('item' => 'Item')); $adapters['contributions'] = $contributedItemAdapter; - - $contributionTypeAdapter = + + $contributionTypeAdapter = new ApiImport_ResponseAdapter_Omeka_GenericAdapter(null, $args['endpointUri'], 'ContributionType'); $contributionTypeAdapter->setResourceProperties(array('item_type' => 'ItemType')); $adapters['contribution_types'] = $contributionTypeAdapter; - + $contributionTypeElementsAdapter = new ApiImport_ResponseAdapter_Omeka_GenericAdapter(null, $args['endpointUri'], 'ContributionTypeElement'); $contributionTypeElementsAdapter->setResourceProperties( @@ -385,22 +489,22 @@ public function filterApiImportOmekaAdapters($adapters, $args) ); $adapters['contribution_type_elements'] = $contributionTypeElementsAdapter; } else { - $contributionContributorsAdapter = + $contributionContributorsAdapter = new ApiImport_ResponseAdapter_OmekaNet_ContributorsAdapter( null, $args['endpointUri'], 'User' ); $adapters['contribution_contributors'] = $contributionContributorsAdapter; - $contributedItemAdapter = + $contributedItemAdapter = new ApiImport_ResponseAdapter_OmekaNet_ContributedItemsAdapter( null, $args['endpointUri'], 'ContributionContributedItem' ); $adapters['contribution_contributed_items'] = $contributedItemAdapter; - $typesAdapter = + $typesAdapter = new ApiImport_ResponseAdapter_Omeka_GenericAdapter(null, $args['endpointUri'], 'ContributionType'); $typesAdapter->setResourceProperties(array('item_type' => 'ItemType')); $adapters['contribution_types'] = $typesAdapter; - $typeElementsAdapter = + $typeElementsAdapter = new ApiImport_ResponseAdapter_Omeka_GenericAdapter(null, $args['endpointUri'], 'ContributionTypeElement'); $typeElementsAdapter->setResourceProperties( array('type' => 'ContributionType', @@ -516,6 +620,38 @@ public function hookAdminItemsBrowseDetailedEach($args) echo $this->_adminBaseInfo($args); } + public function hookPublicItemsShow($args) + { + if (!is_allowed('Contribution_Contribution', 'edit')) { + return; + } + + if (!plugin_is_active('UserProfiles')) { + return; + } + + $user = current_user(); + $item = $args['item']; + if (!$user || $user->id != $item->owner_id) { + return; + } + + $contributedItem = get_db()->getTable('ContributionContributedItem')->findByItem($item); + if (!$contributedItem) { + return; + } + + $html = ''; + $html .= '' . __('No contribution yet.') . '
'; + $html = '' . __('No contribution yet, or removed contributions.') . '
'; } $widget['content'] = $html; $widgets[] = $widget; @@ -714,7 +850,9 @@ private function _adminBaseInfo($args) $publicMessage = ''; if (is_allowed($item, 'edit')) { - if ($contributedItem->public) { + if ($contributedItem->deleted) { + $publicMessage = __('This item has been deleted by user. It cannot be made public.'); + } elseif ($contributedItem->public) { $publicMessage = __("This item can be made public."); } else { $publicMessage = __("This item cannot be made public."); @@ -762,11 +900,6 @@ public function _mapOwners($contribItemData, $map) } } - public function getOptions() - { - return $this->_options; - } - /** * Remove the form controls * @@ -800,7 +933,7 @@ public function elementFormFilter($components, $args) $type = $view->type; $contributionElement = $this->_db->getTable('ContributionTypeElement')->findByElementAndType($element, $type); $prompt = $contributionElement->prompt; - $components['label'] = ''; + $components['label'] = $view->formLabel(null, $prompt, array('disableFor' => true)); $components['add_input'] = null; return $components; } diff --git a/README.md b/README.md new file mode 100644 index 00000000..9df0f5d6 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +Contribution (plugin for Omeka) +=============================== + + +This plugin for [Omeka] provides a way to collect stories, images, or other +files from the public and manage those contributions in your Omeka archive as +items. The form can also automatically add a reCAPTCHA box at the bottom of each +form to prevent spam-bots from spamming your website. + +For more information, see the [Contribution presentation] on [Omeka] and the +[update] done for Omeka 2. + +This fork contains some improvements: + +* Possibility to upload multiple files, if wanted +* Possibility to add tags, if wanted +* Possibility for a contributor to edit and delete his/her contributions +* Simplification of some parts of code +* various strict standards fixes + + +Installation +------------ + +Contribution depends on the [Guest User] plugin. You will need to install it +before you can install [Contribution]. + +Uncompress files and rename plugin folder "Contribution". + +Then install it like any other Omeka plugin. + +Settings are not on the plugin page, but at https://www.example.com/admin/contribution/index. + + +Warning +------- + +Use it at your own risk. + +It's always recommended to backup your files and database regularly so you can +roll back if needed. + + +Troubleshooting +--------------- + +See online issues on the [plugin issues] page on GitHub. + + +License +------- + +This plugin is published under [GNU/GPL]. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +Contact +------- + +Current maintainers: + +* Roy Rosenzweig Center for History and New Media + + +Copyright +--------- + +* Copyright Roy Rosenzweig Center for History and New Media, 2010-2016 +* Copyright Daniel Berthereau, 2014-2016 (improvements, see [Daniel-KM]) + + +[Omeka]: https://omeka.org +[Contribution presentation]: http://omeka.org/codex/Plugins/Contribution +[update]: http://omeka.org/codex/Plugins/Contribution_2.0 +[Contribution]: https://github.com/Omeka/plugin-Contribution +[plugin issues]: https://github.com/Omeka/plugin-Contribution/issues +[Guest User]: https://github.com/Omeka/plugin-GuestUser +[GNU/GPL]: https://www.gnu.org/licenses/gpl-3.0.html "GNU/GPL v3" +[Daniel-KM]: https://github.com/Daniel-KM "Daniel Berthereau" diff --git a/controllers/AjaxController.php b/controllers/AjaxController.php index 28e91924..63ca6efe 100644 --- a/controllers/AjaxController.php +++ b/controllers/AjaxController.php @@ -40,7 +40,7 @@ public function updateAction() } $id = (integer) $this->_getParam('id'); - $contributedItem = get_record_by_id('ContributionContributedItem', $id); + $contributedItem = $this->_helper->db->find($id); if (!$contributedItem) { $this->getResponse()->setHttpResponseCode(400); return; @@ -74,7 +74,7 @@ public function deleteAction() // Handle action. try { $id = (integer) $this->_getParam('id'); - $contributedItem = get_record_by_id('ContributionContributedItem', $id); + $contributedItem = $this->_helper->db->find($id); if (!$contributedItem) { $this->getResponse()->setHttpResponseCode(400); return; diff --git a/controllers/ContributionController.php b/controllers/ContributionController.php index 96e931c1..5da9ee22 100644 --- a/controllers/ContributionController.php +++ b/controllers/ContributionController.php @@ -11,8 +11,17 @@ */ class Contribution_ContributionController extends Omeka_Controller_AbstractActionController { + protected $_autoCsrfProtection = true; + protected $_captcha; + protected $_profile; + + public function init() + { + $this->_helper->db->setDefaultModelName('ContributionContributedItem'); + } + /** * Index action; simply forwards to contributeAction. */ @@ -24,19 +33,19 @@ public function indexAction() public function myContributionsAction() { $user = current_user(); + if (empty($user)) { + $this->_helper->redirector('login', 'users', 'default'); + } + $contribItemTable = $this->_helper->db->getTable('ContributionContributedItem'); $contribItems = array(); if(!empty($_POST)) { foreach($_POST['contribution_public'] as $id=>$value) { $contribItem = $contribItemTable->find($id); - if($value) { - $contribItem->public = true; - } else { - $contribItem->makeNotPublic(); - } - $contribItem->public = $value; - $contribItem->anonymous = $_POST['contribution_anonymous'][$id]; + $contribItem->public = (integer) $value; + $contribItem->anonymous = (integer) $_POST['contribution_anonymous'][$id]; + $contribItem->deleted = (integer) $_POST['contribution_deleted'][$id]; if($contribItem->save()) { $this->_helper->flashMessenger( __('Your contributions have been updated.'), 'success'); @@ -44,12 +53,19 @@ public function myContributionsAction() $this->_helper->flashMessenger($contribItem->getErrors()); } - $contribItems[] = $contribItem; + // Clean list for next view. + if (!$contribItem->deleted) { + $contribItems[] = $contribItem; + } } } else { - $contribItems = $contribItemTable->findBy(array('contributor'=>$user->id)); + $contribItems = $contribItemTable->findBy(array( + 'contributor' => $user->id, + 'deleted' => false, + )); } $this->view->contrib_items = $contribItems; + $this->view->total_results = count($contribItems); } /** @@ -61,39 +77,44 @@ public function contributeAction() $csrf = new Omeka_Form_SessionCsrf; $this->view->csrf = $csrf; if(!empty($_POST)) { + $defaultType = get_option('contribution_default_type'); if (!$csrf->isValid($_POST)) { $this->_helper->_flashMessenger(__('There was an error on the form. Please try again.'), 'error'); $typeId = null; if (isset($_POST['contribution_type']) && ($postedType = $_POST['contribution_type'])) { $typeId = $postedType; - } else if ($defaultType = get_option('contribution_default_type')) { + } elseif ($defaultType) { $typeId = $defaultType; } $this->_setupContributeSubmit($typeId); return; } + if ($this->_processForm($_POST)) { - $route = $this->getFrontController()->getRouter()->getCurrentRouteName(); - $this->_helper->_redirector->gotoRoute(array('action' => 'thankyou'), $route); - } else { - $typeId = null; - if (isset($_POST['contribution_type']) && ($postedType = $_POST['contribution_type'])) { - $typeId = $postedType; - } else if ($defaultType = get_option('contribution_default_type')) { - $typeId = $defaultType; - } - if ($this->_captcha) { - $this->view->captchaScript = $this->_captcha->render(new Zend_View); - } - $this->_setupContributeSubmit($typeId); + return $this->_helper->_redirector->gotoRoute(array('action' => 'thankyou'), 'contribution'); + } - if(isset($this->_profile) && !$this->_profile->exists()) { - $this->_helper->flashMessenger($this->_profile->getErrors(), 'error'); - return; - } + // Error in contribution, so refresh view. + $typeId = null; + if (isset($_POST['contribution_type']) && ($postedType = $_POST['contribution_type'])) { + $typeId = $postedType; + } elseif ($defaultType) { + $typeId = $defaultType; } - } else { - if($this->_captcha) { + if ($this->_captcha) { + $this->view->captchaScript = $this->_captcha->render(new Zend_View); + } + $this->_setupContributeSubmit($typeId); + + if (!empty($this->_profile) && !$this->_profile->exists()) { + $this->_helper->flashMessenger($this->_profile->getErrors(), 'error'); + return; + } + } + + // No post, so prepare base view. + else { + if ($this->_captcha) { $this->view->captchaScript = $this->_captcha->render(new Zend_View); } $defaultType = get_option('contribution_default_type'); @@ -101,6 +122,105 @@ public function contributeAction() } } + /** + * Action for main contribution edit form. + * + * Current differences with "add contribution": The type cannot be changed; + * no captcha; old images are displayed and not removable. + */ + public function editAction() + { + // Check contribution (else keep error 404?). + $contributedItem = $this->_helper->db->findById(); + if (empty($contributedItem) || $contributedItem->deleted) { + $this->_helper->_flashMessenger(__("This contribution doesn't exist."), 'error'); + return current_user() + ? $this->_helper->redirector('my-contributions') + : $this->_helper->redirector('index', 'index', 'default'); + } + + // Check rights of the user on this contribution (owner of the item). + $contributor = $contributedItem->getContributor(); + $user = $this->getCurrentUser(); + $item = $contributedItem->Item; + if (!$user || $user->id !== $contributor->id || $item->owner_id !== $user->id) { + $this->_helper->_flashMessenger(__('You are not the contributor of this item.'), 'error'); + return $item->public + ? $this->_helper->redirector('show', 'items', 'default', array('id' => $item->id)) + : $this->_helper->redirector('index', 'index', 'default'); + } + + // Prepare next view (TODO Set it in another method to avoid preparation if not needed?) + $csrf = new Omeka_Form_SessionCsrf; + $this->view->csrf = $csrf; + $this->view->contribution_contributed_item = $contributedItem; + $this->view->item = $item; + $contributionType = $this->_helper->db->getTable('ContributionType')->getByItemType($item->item_type_id); + $this->_setupContributionSubmit($contributionType); + + if (!$this->getRequest()->isPost()) { + return; + } + + if (!$csrf->isValid($_POST)) { + $this->_helper->_flashMessenger(__('There was an error on the form. Please try again.'), 'error'); + return; + } + + if (!$this->_validateContribution($_POST)) { + return; + } + + // Because there is already one attached file when item is created, + // no new check is done on required file. + + // If not simple and the profile doesn't process, send back false for the + // error. This should be done before processing item in order to set + // profile type and to avoid settings profile elements to item. + $this->_processUserProfile($user, $_POST); + + // Use a specific setPostData() for item, because some post fields are + // not set in the post. Metadata are not changed via contribution + // form. + $this->_setPostData($item, $_POST); + + // Tags are added separately. + if ($contributionType->add_tags && isset($_POST['tags'])) { + $item->addTags($_POST['tags']); + } + + // Allow plugins to deal with the inputs they may have added to the form + // and that are not managed via hooks before or after save item. + fire_plugin_hook( + 'contribution_save_form', + array( + 'contributionType' => $contributionType, + 'record' => $item, + 'post' => $_POST, + )); + + // Everything has been checked, so save item. + if ($item->save(false)) { + $successMessage = $this->_getEditSuccessMessage($contributedItem); + $this->_helper->flashMessenger($successMessage, 'success'); + + // Update contribution before redirect. + $contributedItem->public = (integer) $_POST['contribution-public']; + if (!$contributedItem->public) { + $contributedItem->makeNotPublic(); + } + $contributedItem->anonymous = (integer) $_POST['contribution-anonymous']; + $contributedItem->deleted = 0; + $contributedItem->save(); + + $this->_redirectAfterEdit($contributedItem); + } + // Error during saving. + else { + $this->_helper->flashMessenger($item->getErrors()); + } + } + /** * Action for AJAX request from contribute form. */ @@ -127,26 +247,38 @@ public function thankyouAction() /** * Common tasks whenever displaying submit form for contribution. * - * @param int $typeId ContributionType id + * @param ContributionType|int $contributionType ContributionType id + */ + protected function _setupContributeSubmit($contributionType) + { + $this->view->item = new Item; + $this->_setupContributionSubmit($contributionType); + } + + /** + * Common tasks whenever displaying submit form for contribution or edition. + * + * @param ContributionType|int $contributionType ContributionType or id. */ - public function _setupContributeSubmit($typeId) + protected function _setupContributionSubmit($contributionType) { // Override default element form display $this->view->addHelperPath(CONTRIBUTION_HELPERS_DIR, 'Contribution_View_Helper'); - $item = new Item; - $this->view->item = $item; - $type = get_db()->getTable('ContributionType')->find($typeId); - $this->view->type = $type; + if (!is_object($contributionType)) { + $contributionType = get_db()->getTable('ContributionType')->find($contributionType); + } + $this->view->type = $contributionType; - //setup profile stuff, if needed + // Setup profile stuff, if needed. $profileTypeId = get_option('contribution_user_profile_type'); if(plugin_is_active('UserProfiles') && $profileTypeId) { $this->view->addHelperPath(USER_PROFILES_DIR . '/helpers', 'UserProfiles_View_Helper_'); $profileType = $this->_helper->db->getTable('UserProfilesType')->find($profileTypeId); $this->view->profileType = $profileType; - if($user = current_user()) { + $user = current_user(); + if($user) { $profile = $this->_helper->db->getTable('UserProfilesProfile')->findByUserIdAndTypeId($user->id, $profileTypeId); } if(empty($profile)) { @@ -159,7 +291,7 @@ public function _setupContributeSubmit($typeId) /** * Creates the reCAPTCHA object and returns it. - * + * * @return Zend_Captcha_Recaptcha|null */ protected function _setupCaptcha() @@ -184,147 +316,177 @@ protected function _setupCaptcha() */ protected function _processForm($post) { - if (!empty($post)) { + if (!$this->_validateContribution($post)) { + return false; + } - //for the "Simple" configuration, look for the user if exists by email. Log them in. - //If not, create the user and log them in. - $user = current_user(); - $open = get_option('contribution_open'); - if ( get_option('contribution_strict_anonymous') ) { - $strictAnonymous = empty($post['contribution_email']); - } else { - $strictAnonymous = false; - } - + // For the "Simple" configuration, look for the user if exists by email. + // Log them in. If not, create the user and log them in. + $user = current_user(); + $open = get_option('contribution_open'); + if ( get_option('contribution_strict_anonymous') ) { + $strictAnonymous = empty($post['contribution_email']); + } else { + $strictAnonymous = false; + } - if(!$user && $open && !$strictAnonymous) { - $user = $this->_helper->db->getTable('User')->findByEmail($post['contribution_email']); - } + if(!$user && $open && !$strictAnonymous) { + $user = $this->_helper->db->getTable('User')->findByEmail($post['contribution_email']); + } - if (!$user && $strictAnonymous) { - $user = $this->_createNewAnonymousUser(); - } - // if still not a user, need to create one based on the email address - if(!$user) { - $user = $this->_createNewGuestUser($post); - if($user->hasErrors()) { - $errors = $user->getErrors()->get(); - //since we're creating the user behind the scenes, skip username and name errors - unset($errors['name']); - unset($errors['username']); - foreach($errors as $error) { - $this->_helper->flashMessenger($error, 'error'); - } - return false; + if (!$user && $strictAnonymous) { + $user = $this->_createNewAnonymousUser(); + } + // If still not a user, need to create one based on the email address. + if (!$user) { + $user = $this->_createNewGuestUser($post); + if ($user->hasErrors()) { + $errors = $user->getErrors()->get(); + // Since we're creating the user behind the scenes, skip + // username and name errors. + unset($errors['name']); + unset($errors['username']); + foreach ($errors as $error) { + $this->_helper->flashMessenger($error, 'error'); } - } - - // The final form submit was not pressed. - if (!isset($post['form-submit'])) { - return false; - } - if (!$this->_validateContribution($post)) { return false; } + } + $contributor = $user; - $contributionTypeId = trim($post['contribution_type']); - if ($contributionTypeId !== "" && is_numeric($contributionTypeId)) { - $contributionType = get_db()->getTable('ContributionType')->find($contributionTypeId); - $itemTypeId = $contributionType->getItemType()->id; - } else { - $this->_helper->flashMessenger(__('You must select a type for your contribution.'), 'error'); - return false; - } - $itemMetadata = array('public' => false, - 'featured' => false, - 'item_type_id' => $itemTypeId); + $contributionTypeId = (integer) $post['contribution_type']; + if (!empty($contributionTypeId)) { + $contributionType = get_db()->getTable('ContributionType')->find($contributionTypeId); + $itemTypeId = $contributionType->getItemType()->id; + } else { + $this->_helper->flashMessenger(__('You must select a type for your contribution.'), 'error'); + return false; + } + // Public is updated with the contributed item. + $itemMetadata = array('public' => false, + 'featured' => false, + 'item_type_id' => $itemTypeId); + + $collectionId = (integer) get_option('contribution_collection_id'); + if (!empty($collectionId)) { + $itemMetadata['collection_id'] = $collectionId; + } - $collectionId = get_option('contribution_collection_id'); - if (!empty($collectionId) && is_numeric($collectionId)) { - $itemMetadata['collection_id'] = (int) $collectionId; - } + // In case we're doing Simple, create and save the Item so the owner + // is set, then update with the data. + $item = new Item(); + $item->setOwner($user); - $fileMetadata = $this->_processFileUpload($contributionType); + $fileMetadata = $this->_prepareFilesUpload($contributionType); - // This is a hack to allow the file upload job to succeed - // even with the synchronous job dispatcher. - if ($acl = get_acl()) { - $acl->allow(null, 'Items', 'showNotPublic'); - $acl->allow(null, 'Collections', 'showNotPublic'); - } - try { - //in case we're doing Simple, create and save the Item so the owner is set, then update with the data - $item = new Item(); - $item->setOwner($user); - $item->save(); - $item = update_item($item, $itemMetadata, array(), $fileMetadata); - } catch(Omeka_Validator_Exception $e) { - $this->flashValidatonErrors($e); - return false; - } catch (Omeka_File_Ingest_InvalidException $e) { - // Copying this cruddy hack - if (strstr($e->getMessage(), "'contributed_file'")) { - $this->_helper->flashMessenger("You must upload a file when making a {$contributionType->display_name} contribution.", 'error'); - } else { - $this->_helper->flashMessenger($e->getMessage()); - } - return false; - } catch (Exception $e) { - $this->_helper->flashMessenger($e->getMessage()); - return false; + // If not simple and the profile doesn't process, send back false for the + // error. This should be done before processing item in order to set + // profile type and to avoid settings profile elements to item. + $this->_processUserProfile($user, $post); + + // Use a specific setPostData() for item, because some post fields are + // not set in the post. + $this->_setPostData($item, $post, $itemMetadata); + + // Tags are added separately. + if ($contributionType->add_tags && isset($post['tags'])) { + $item->addTags($post['tags']); + } + + // Allow plugins to deal with the inputs they may have added to the form + // and that are not managed via hooks before or after save item. + fire_plugin_hook( + 'contribution_save_form', + array( + 'contributionType' => $contributionType, + 'record' => $item, + 'post' => $post, + )); + + // Everything has been checked, so save item. + if ($item->save(false)) { + // Check if a required file has been uploaded. + $fileCount = $item->fileCount(); + if ($contributionType->isFileRequired() && empty($fileCount)) { + $this->_helper->flashMessenger($item->getErrors()); + $this->_helper->flashMessenger(__('You must upload a file when making a %s contribution.', $contributionType->display_name), 'error'); + $item->delete(); + return; } - $this->_addElementTextsToItem($item, $post['Elements']); - // Allow plugins to deal with the inputs they may have added to the form. - fire_plugin_hook('contribution_save_form', array('contributionType'=>$contributionType,'record'=>$item, 'post'=>$post)); - $item->save(); - //if not simple and the profile doesn't process, send back false for the error - $this->_processUserProfile($post, $user); - $this->_linkItemToContributedItem($item, $contributor, $post); + $this->_linkItemToContributedItem($item, $post); $this->_sendEmailNotifications($user, $item); return true; } + // Error during saving. + else { + $this->_helper->flashMessenger($item->getErrors()); + $item->delete(); + } return false; } - protected function _processUserProfile($post, $user) + protected function _processUserProfile($user, $post) { - $profileTypeId = get_option('contribution_user_profile_type'); - if($profileTypeId && plugin_is_active('UserProfiles')) { - $profile = $this->_helper->db->getTable('UserProfilesProfile')->findByUserIdAndTypeId($user->id, $profileTypeId); - if(!$profile) { - $profile = new UserProfilesProfile(); - $profile->setOwner($user); - $profile->type_id = $profileTypeId; - $profile->public = 0; - $profile->setRelationData(array('subject_id'=>$user->id, 'user_id'=>$user->id)); + $profile = $this->_getProfileForUser($user); + // Check if there is a profile. + if (!$profile) { + return true; + } + $profile->setPostData($post); + return $profile->save(false); + } + + /** + * Get the user profile if the plugin User Profile is used and active. The + * profile is a new one, for the current user, if it is not set. + * + * @param User $user Current user if null. + * @return UserProfilesProfile|false. False if the plugin is not used. + */ + protected function _getProfileForUser($user = null) + { + if (is_null($this->_profile)) { + $profileTypeId = get_option('contribution_user_profile_type'); + if ($profileTypeId && plugin_is_active('UserProfiles')) { + if (is_null($user)) { + $user = current_user(); + } + $profile = $this->_helper->db->getTable('UserProfilesProfile')->findByUserIdAndTypeId($user->id, $profileTypeId); + if (!$profile) { + $profile = new UserProfilesProfile; + $profile->setOwner($user); + $profile->type_id = $profileTypeId; + $profile->public = 0; + $profile->setRelationData(array('subject_id' => $user->id, 'user_id' => $user->id)); + } + $this->_profile = $profile; } - $profile->setPostData($post); - $this->_profile = $profile; - if(!$profile->save(false)) { - return false; + // The plugin is not used. + else { + $this->_profile = false; } } - return true; + return $this->_profile; } /** * Deals with files specified on the contribution form. * + * @todo Check if multiple files are allowed. + * @todo Error when option is required and when multiple files are allowed: an empty contributed_file input generate an error. + * * @param ContributionType $contributionType Type of contribution. - * @return array File upload array. + * @return array Files upload array. */ - protected function _processFileUpload($contributionType) { + protected function _prepareFilesUpload($contributionType) + { if ($contributionType->isFileAllowed()) { $options = array(); - if ($contributionType->isFileRequired()) { - $options['ignoreNoFile'] = false; - } else { - $options['ignoreNoFile'] = true; - } + $options['ignoreNoFile'] = !$contributionType->isFileRequired(); $fileMetadata = array( 'file_transfer_type' => 'Upload', - 'files' => 'contributed_file', + 'files' => 'file', 'file_ingest_options' => $options ); @@ -337,40 +499,82 @@ protected function _processFileUpload($contributionType) { return array(); } - protected function _linkItemToContributedItem($item, $contributor, $post) + protected function _linkItemToContributedItem($item, $post) { $linkage = new ContributionContributedItem; $linkage->item_id = $item->id; - $linkage->public = $post['contribution-public']; - $linkage->anonymous = $post['contribution-anonymous']; + $linkage->public = (integer) $post['contribution-public']; + $linkage->anonymous = (integer) $post['contribution-anonymous']; + $linkage->deleted = 0; $linkage->save(); } /** - * Adds ElementTexts to item. + * Set post data to an item. + * + * All data in the post are set: elements, basic metadata (item_type_id, + * collection_id, featured, public, owner_id), and other data attached to an + * item, like geolocation. + * + * @todo Use builder? * * @param Item $item Item to add texts to. - * @param array $elements Array of element inputs from form + * @param array $post Array of element inputs from form. + * @param array $itemMetadata Array of basic data of item (public...). + * @return boolean True if success, false else. */ - protected function _addElementTextsToItem($item, $elements) + protected function _setPostData($item, $post, $itemMetadata = array()) { - $db = get_db(); - $elementTable = $db->getTable('Element'); - $sql = "SELECT DISTINCT `id` from `$db->ElementSet` WHERE `record_type` = 'UserProfilesType' "; - $userProfilesElementSets = $db->fetchCol($sql); - foreach($elements as $elementId => $elementTexts) { - $element = $elementTable->find($elementId); - $elSet = $element->getElementSet(); - //need to skip over elements that are intended for a User Profile, not the item - if (in_array($elSet->id, $userProfilesElementSets)) { - continue; - } - foreach($elementTexts as $elementText) { - if (!empty($elementText['text'])) { - $item->addTextForElement($element, $elementText['text']); + // Check if there is a profile in order to remove elements data used for + // profile because they are related to profile, not to item. + $profile = $this->_getProfileForUser(); + if ($profile) { + $profileElements = $profile->getAllElements(); + foreach ($profileElements as $key => $value) { + foreach ($value as $element) { + unset($post['Elements'][$element->id]); } } } + + if (!isset($post['Elements'])) { + $post['Elements'] = array(); + } + + // Overwrite post data with internal itemMetadata. + if ($itemMetadata) { + $post += $itemMetadata; + } + // Else reset metadata to keep clean current ones (avoid checking post). + else { + unset($post['item_type_id']); + unset($post['collection_id']); + unset($post['featured']); + unset($post['public']); + unset($post['owner_id']); + } + + // If ReplaceElementTexts() is true, all element texts are removed. + // If an element is set, it replaces all fields of the element. + // If an element is not set, it is removed. + // If a non element is not set, it is not changed. + // So, to get all element texts is needed to update a record. + + // Get and format current metadata. + $currentElements = array(); + foreach ($item->getAllElementTexts() as $elementText) { + $currentElements[$elementText->element_id] = array( + array( + 'text' => trim($elementText->text), + 'html' => $elementText->html, + ), + ); + } + + // This is not a merge, but an update. + $post['Elements'] += $currentElements; + + $item->setPostData($post); } /** @@ -385,17 +589,27 @@ protected function _addElementTextsToItem($item, $elements) */ protected function _validateContribution($post) { - - // ReCaptcha ignores the first argument. - if ($this->_captcha and !$this->_captcha->isValid(null, $_POST)) { - $this->_helper->flashMessenger(__('Your CAPTCHA submission was invalid, please try again.'), 'error'); + if (empty($post)) { + return false; + } + + // The final form submit was not pressed. + if (!isset($post['form-submit'])) { + $this->_helper->flashMessenger(__('You should press submit button.'), 'error'); return false; } - + if ($post['terms-agree'] == 0) { $this->_helper->flashMessenger(__('You must agree to the Terms and Conditions.'), 'error'); return false; } + + // ReCaptcha ignores the first argument. + if ($this->_captcha and !$this->_captcha->isValid(null, $post)) { + $this->_helper->flashMessenger(__('Your CAPTCHA submission was invalid, please try again.'), 'error'); + return false; + } + return true; } @@ -527,4 +741,39 @@ protected function _createNewAnonymousUser() $user->save(); return $user; } + + /** + * Return the success message for editing a record. + * + * @param Omeka_Record_AbstractRecord $record + * @return string + */ + protected function _getEditSuccessMessage($record) + { + $item = $record->Item; + $itemTitle = $this->_getElementMetadata($item, 'Dublin Core', 'Title'); + if ($itemTitle != '') { + return __('The contributed item "%s" was successfully updated!', $itemTitle); + } else { + return __('The contributed item #%d was successfully updated!', $item->id); + } + } + + protected function _getElementMetadata($record, $elementSetName, $elementName) + { + $m = new Omeka_View_Helper_Metadata; + return strip_formatting($m->metadata($record, array($elementSetName, $elementName))); + } + + /** + * Redirect to items/show after a contribution is successfully edited. + * + * The default is to redirect to this record's show page. + * + * @param Omeka_Record_AbstractRecord $record + */ + protected function _redirectAfterEdit($record) + { + $this->_helper->redirector('show', 'items', 'default', array('id' => $record->item_id)); + } } diff --git a/controllers/SettingsController.php b/controllers/SettingsController.php index 958aeccc..e37a26e4 100644 --- a/controllers/SettingsController.php +++ b/controllers/SettingsController.php @@ -24,14 +24,19 @@ public function indexAction() */ public function editAction() { - $form = $this->_getForm(); - $defaults = $this->_getOptions(); + $form = new Contribution_Form_Settings; + $defaults = $form->getCurrentOptions(); $form->setDefaults($defaults); if (isset($_POST['submit'])) { if ($form->isValid($_POST)) { - $this->_setOptions($form->getValues()); - $this->_helper->flashMessenger(__('Settings have been saved.')); + $options = array_keys($defaults); + foreach ($form->getValues() as $optionName => $optionValue) { + if (in_array($optionName, $options)) { + set_option($optionName, $optionValue); + } + } + $this->_helper->flashMessenger(__('Settings have been saved.'), 'success'); } else { $this->_helper->flashMessenger(__('There were errors found in your form. Please edit and resubmit.', 'error')); } @@ -39,116 +44,4 @@ public function editAction() $this->view->form = $form; } - - /** - * Returns the options that are specified in the $_options property. - * - * @return array Array of option names. - */ - private function _getOptions() - { - $options = array(); - $cnt = new ContributionPlugin(); - $pluginOptions = $cnt->getOptions(); - foreach ($pluginOptions as $option) { - $options[$option] = get_option($option); - } - return $options; - } - - /** - * Sets options that appear in both the form and $_options. - * - * @param array $newOptions array of $optionName => $optionValue. - */ - private function _setOptions($newOptions) - { - $cnt = new ContributionPlugin(); - $options = $cnt->getOptions(); - foreach ($newOptions as $optionName => $optionValue) { - if (in_array($optionName, $options)) { - set_option($optionName, $optionValue); - } - } - } - - private function _getForm() - { - $form = new Omeka_Form_Admin(array('type'=>'contribution_settings')); - - $form->addElementToEditGroup('text', 'contribution_page_path', array( - 'label' => __('Contribution Slug'), - 'description' => __('Relative path from the Omeka root to the desired location for the contribution form. If left blank, the default path will be named “contribution.”'), - 'filters' => array(array('StringTrim', '/\\\s')) - )); - - $form->addElementToEditGroup('text', 'contribution_email_sender', array( - 'label' => __('Contribution Confirmation Email'), - 'description' => __('An email message will be sent to each contributor from this address confirming that they submitted a contribution to this website. Leave blank if you do not want an email sent.'), - 'validators' => array('EmailAddress') - )); - - $form->addElementToEditGroup('textarea', 'contribution_email_recipients', array( - 'label' => __('New Contribution Notification Emails'), - 'description' => __('An email message will be sent to each address here whenever a new item is contributed. Leave blank if you do not want anyone to be alerted of contributions by email.'), - 'attribs' => array('rows' => '5') - )); - - $form->addElementToEditGroup('textarea', 'contribution_consent_text', array( - 'label' => __('Text of Terms of Service'), - 'description' => __('The text of the legal disclaimer to which contributors will agree.'), - 'attribs' => array('class' => 'html-editor', 'rows' => '15') - )); - - - $form->addElementToEditGroup('checkbox', 'contribution_open', array( - 'label' => __("Allow Non-registered Contributions"), - 'description' => __("This will require an email address from contributors, and create a guest user from that information. If those users want to use the account, they will have to request a new password for the account. If you want to collect additional information about contributors, they must create an account. See documentation for details. "), - ), - array('checked'=> (bool) get_option('contribution_open') ? 'checked' : '') - ); - - - $form->addElementToEditGroup('checkbox', 'contribution_strict_anonymous', array( - 'label' => __("Allow Anonymous Contributions"), - 'description' => __("If non-registered contributions are allowed above, this option allows contributors to remain completely anonymous, even to administrators. A dummy user account will be created that stores no identifing information. See documentation for details. "), - ), - array('checked'=> (bool) get_option('contribution_strict_anonymous') ? 'checked' : '') - ); - - $form->addElementToEditGroup('textarea', 'contribution_email', array( - 'label' => __("Email text to send to contributors"), - 'description' => __("Email text to send to contributors when they submit an item. A link to their contribution will be appended. If using the 'Non-registered', but not 'Anonymous', options, we recommend that you notify contributors that a guest user account has been created for them, and what they gain by confirming their account."), - 'attribs' => array('class' => 'html-editor', 'rows' => '15') - )); - - $collections = get_db()->getTable('Collection')->findPairsForSelectForm(); - $collections = array('' => __('Do not put contributions in any collection')) + $collections; - - $form->addElementToEditGroup('select', 'contribution_collection_id', array( - 'label' => __('Contribution Collection'), - 'description' => __('The collection to which contributions will be added. Changes here will only affect new contributions.'), - 'multiOptions' => $collections - )); - - $types = get_db()->getTable('ContributionType')->findPairsForSelectForm(); - $types = array('' => __('No default type')) + $types; - - $form->addElementToEditGroup('select', 'contribution_default_type', array( - 'label' => __('Default Contribution Type'), - 'description' => __('The type that will be chosen for contributors by default.'), - 'multiOptions' => $types - )); - - if(plugin_is_active('UserProfiles')) { - $profileTypes = $this->_helper->db->getTable('UserProfilesType')->findPairsForSelectForm(); - $form->addElementToEditGroup('select', 'contribution_user_profile_type', array( - 'label' => __('Choose a profile type for contributors'), - 'description' => __('Configure the profile type under User Profiles'), - 'multiOptions' => array('' => __("None")) + $profileTypes - )); - } - - return $form; - } } diff --git a/helpers/ThemeHelpers.php b/helpers/ThemeHelpers.php index 962d4b96..474e8679 100644 --- a/helpers/ThemeHelpers.php +++ b/helpers/ThemeHelpers.php @@ -6,58 +6,84 @@ * @package Contribution */ -/** - * Print the header for the contribution admin pages. - * - * Creates a consistent navigation across the pages. - * - * @param array $subsections Array of names that specify the "path" to this page. - * @return string - */ - - -function contribution_admin_header($subsections = array()) -{ - $mainTitle = __('Contribution'); - $subsections = array_merge(array($mainTitle), $subsections); - $displayTitle = implode(' | ', $subsections); - $head = array('title' => $displayTitle, - 'bodyclass' => 'contribution', - 'content_class' => 'horizontal-nav'); - echo head($head); -} +/** + * Print the header for the contribution admin pages. + * + * Creates a consistent navigation across the pages. + * + * @param array $subsections Array of names that specify the "path" to this page. + * @return string + */ + +function contribution_admin_header($subsections = array()) +{ + $mainTitle = __('Contribution'); + $subsections = array_merge(array($mainTitle), $subsections); + $displayTitle = implode(' | ', $subsections); + $head = array('title' => $displayTitle, + 'bodyclass' => 'contribution', + 'content_class' => 'horizontal-nav'); + echo head($head); +} + +/** + * Get a link to the public contribution page. + * + * @param string $linkText + * @param string $action Action to link to, main index if none. + * @return string HTML + */ +function contribution_link_to_contribute($linkText = 'Contribute', $actionName = null) +{ + $url = contribution_contribute_url($actionName); + return "$linkText"; +} -/** - * Get a link to the public contribution page. - * - * @param string $linkText - * @param string $action Action to link to, main index if none. - * @return string HTML - */ -function contribution_link_to_contribute($linkText = 'Contribute', $actionName = null) -{ - $url = contribution_contribute_url($actionName); - return "$linkText"; +/** + * Get a URL to the public contribution page. + * + * @param string $action Action to link to, main index if none. + * @return string URL + */ +function contribution_contribute_url($actionName = null) +{ + $options = array(); + if (!empty($actionName)) { + $options['action'] = $actionName; + } + return get_view()->url($options, 'contribution', array(), true); } -/** - * Get a URL to the public contribution page. - * - * @param string $action Action to link to, main index if none. - * @return string URL - */ -function contribution_contribute_url($actionName = null) -{ - $path = get_option('contribution_page_path'); - if (empty($path)) { - $route = 'contributionDefault'; - } else { - $route = 'contributionCustom'; - } - $options = array(); - if (!empty($actionName)) { - $options['action'] = $actionName; - } - return get_view()->url($options, $route, array(), true); -} +/** + * Get a link to the public contribution action page. + * + * @param Record|integer $contributedItem. + * @param string $action Action to link to, main index if none. + * @param string $linkText + * @return string HTML + */ +function contribution_link_to($contributedItem = null, $action = null, $linkText = 'Contribute') +{ + $url = contribution_url($action, $contributedItem); + return sprintf('%s', $url, $linkText); +} +/** + * Get a URL to the public contribution action page. + * + * @param string $action Action to link to, main index if none. + * @param Record|integer $contributedItem. + * @return string URL + */ +function contribution_url($action = null, $contributedItem = null) +{ + $string = get_option('contribution_page_path'); + if (!empty($action)) { + $string .= '/' . $action; + if (!empty($contributedItem)) { + $string .= '/'; + $string .= is_object($contributedItem) ? $contributedItem->id : (integer) $contributedItem; + } + } + return url($string); +} diff --git a/languages/template.pot b/languages/template.pot index 099a704c..2e633f12 100644 --- a/languages/template.pot +++ b/languages/template.pot @@ -607,7 +607,7 @@ msgstr "" #, php-format msgid "" "Your contribution will show up in the archive once an administrator approves " -"it. Meanwhile, feel free to %s or %s ." +"it. Meanwhile, feel free to %s or %s." msgstr "" #: views/public/contribution/thankyou.php:7 @@ -619,7 +619,7 @@ msgstr "" msgid "" "If you would like to interact with the site further, you can use an account " "that is ready for you. Visit %s, and request a new password for the email " -"you used" +"you used." msgstr "" #: views/public/contribution/type-form.php:3 diff --git a/libraries/ApiImport/ResponseAdapter/OmekaNet/ContributorFieldsAdapter.php b/libraries/ApiImport/ResponseAdapter/OmekaNet/ContributorFieldsAdapter.php index 2303f4b4..6873bf6f 100644 --- a/libraries/ApiImport/ResponseAdapter/OmekaNet/ContributorFieldsAdapter.php +++ b/libraries/ApiImport/ResponseAdapter/OmekaNet/ContributorFieldsAdapter.php @@ -39,7 +39,7 @@ protected function getElementSet() $userProfilesType->public = 0; $userProfilesType->required = 0; $userProfilesType->element_set_id = $this->elementSet->id; - $userProfileType->save(); + $userProfilesType->save(); return $this->elementSet; } } \ No newline at end of file diff --git a/libraries/Contribution/Form/Settings.php b/libraries/Contribution/Form/Settings.php new file mode 100644 index 00000000..535df808 --- /dev/null +++ b/libraries/Contribution/Form/Settings.php @@ -0,0 +1,113 @@ +setOptions(array('type' => 'contribution_settings')); + $this->setAttrib('id', 'settings-form'); + parent::init(); + + $db = get_db(); + + $this->addElement('text', 'contribution_page_path', array( + 'label' => __('Contribution Slug'), + 'description' => __('Relative path from the Omeka root to the desired location for the contribution form. Default path is “contribution“.'), + 'required' => true, + 'filters' => array(array('StringTrim', '/\\\s')), + )); + + $this->addElement('text', 'contribution_email_sender', array( + 'label' => __('Contribution Confirmation Email'), + 'description' => __('An email message will be sent to each contributor from this address confirming that they submitted a contribution to this website. Leave blank if you do not want an email sent.'), + 'validators' => array('EmailAddress'), + )); + + $this->addElement('textarea', 'contribution_email_recipients', array( + 'label' => __('New Contribution Notification Emails'), + 'description' => __('An email message will be sent to each address here whenever a new item is contributed. Leave blank if you do not want anyone to be alerted of contributions by email.'), + 'attribs' => array('rows' => '5'), + )); + + $this->addElement('textarea', 'contribution_consent_text', array( + 'label' => __('Text of Terms of Service'), + 'description' => __('The text of the legal disclaimer to which contributors will agree.'), + 'attribs' => array('class' => 'html-editor', 'rows' => '15'), + )); + + $this->addElement('checkbox', 'contribution_open', array( + 'label' => __("Allow Non-registered Contributions"), + 'description' => __("This will require an email address from contributors, and create a guest user from that information. If those users want to use the account, they will have to request a new password for the account. If you want to collect additional information about contributors, they must create an account. See documentation for details. "), + ), + array('checked' => (bool) get_option('contribution_open') ? 'checked' : '') + ); + + $this->addElement('checkbox', 'contribution_strict_anonymous', array( + 'label' => __("Allow Anonymous Contributions"), + 'description' => __("If non-registered contributions are allowed above, this option allows contributors to remain completely anonymous, even to administrators. A dummy user account will be created that stores no identifing information. See documentation for details. "), + ), + array('checked'=> (bool) get_option('contribution_strict_anonymous') ? 'checked' : '') + ); + + $this->addElement('textarea', 'contribution_email', array( + 'label' => __("Email text to send to contributors"), + 'description' => __("Email text to send to contributors when they submit an item. A link to their contribution will be appended. If using the 'Non-registered', but not 'Anonymous', options, we recommend that you notify contributors that a guest user account has been created for them, and what they gain by confirming their account."), + 'attribs' => array('class' => 'html-editor', 'rows' => '15'), + )); + + $collections = $db->getTable('Collection')->findPairsForSelectForm(); + $collections = array('' => __('Do not put contributions in any collection')) + $collections; + + $this->addElement('select', 'contribution_collection_id', array( + 'label' => __('Contribution Collection'), + 'description' => __('The collection to which contributions will be added. Changes here will only affect new contributions.'), + 'multiOptions' => $collections, + )); + + $types = $db->getTable('ContributionType')->findPairsForSelectForm(); + $types = array('' => __('No default type')) + $types; + + $this->addElement('select', 'contribution_default_type', array( + 'label' => __('Default Contribution Type'), + 'description' => __('The type that will be chosen for contributors by default.'), + 'multiOptions' => $types, + )); + + if (plugin_is_active('UserProfiles')) { + $profileTypes = $db->getTable('UserProfilesType')->findPairsForSelectForm(); + $this->addElement('select', 'contribution_user_profile_type', array( + 'label' => __('Choose a profile type for contributors'), + 'description' => __('Configure the profile type under User Profiles'), + 'multiOptions' => array('' => __("None")) + $profileTypes, + )); + } + + $this->addElement('checkbox', 'contribution_allow_edit', array( + 'label' => __("Allow to Edit Contribution"), + 'description' => __('If checked, contributors will be able to edit and to delete their contributions.') + . ' ' . __('Deleted contributions are only hidden for public and guest users.'), + ), + array('checked' => (bool) get_option('contribution_allow_edit') ? 'checked' : '') + ); + } + + public function getCurrentOptions() + { + $currents = array(); + $elements = $this->getElements(); + foreach ($elements as $element) { + $option = $element->getName(); + $currents[$option] = get_option($option); + } + return $currents; + } +} diff --git a/libraries/ContributionImportUsers.php b/libraries/ContributionImportUsers.php index 1f147e69..40ec9241 100644 --- a/libraries/ContributionImportUsers.php +++ b/libraries/ContributionImportUsers.php @@ -19,7 +19,8 @@ public function perform() //create username from email and set up for some validation checks $username = $contributor['email']; $email = $contributor['email']; - if($user = $db->getTable('User')->findByEmail($contributor['email'])) { + $user = $db->getTable('User')->findByEmail($contributor['email']); + if ($user) { $userContributorMap[$user->id][] = $contributor['id']; } else { if(!$emailValidator->isValid($email)) { diff --git a/models/Api/ContributionType.php b/models/Api/ContributionType.php index 37ec9cea..7690f8fd 100644 --- a/models/Api/ContributionType.php +++ b/models/Api/ContributionType.php @@ -6,14 +6,14 @@ public function getRepresentation(Omeka_Record_AbstractRecord $type) { $representation = array( 'id' => $type->id, - 'url' => self::getResourceUrl("/contribution_types/{$ype->id}"), + 'url' => self::getResourceUrl('/contribution_types/' . $type->id), 'display_name' => $type->display_name, 'file_permissions' => $type->file_permissions ); $representation['item_type'] = array( 'id' => $type->item_type_id, - 'url' => self::getResourceUrl("/item_types/{$type->item_type_id}") - ); + 'url' => self::getResourceUrl('/item_types/' . $type->item_type_id) + ); return $representation; } diff --git a/models/ContributionContributedItem.php b/models/ContributionContributedItem.php index c98276a3..04d8a8be 100644 --- a/models/ContributionContributedItem.php +++ b/models/ContributionContributedItem.php @@ -17,29 +17,29 @@ class ContributionContributedItem extends Omeka_Record_AbstractRecord public $item_id; public $public; public $anonymous; - + public $deleted = 0; + protected $_related = array( 'Item' => 'getItem', 'Contributor' => 'getContributor' ); - + public function getItem() { return $this->getDb()->getTable('Item')->find($this->item_id); } - public function makeNotPublic() - { - $this->public = false; - $item = $this->Item; - $item->public = false; - $item->save(); - release_object($item); - } - public function getContributor() { - $owner = $this->Item->getOwner(); + $item = $this->Item; + // If there is no item, make a fake user called "No User". + if (empty($item)) { + $owner = new User(); + $owner->name = __('No User'); + return $owner; + } + + $owner = $item->getOwner(); //if the user has been deleted, make a fake user called "Deleted User" if(!$owner) { $owner = new User(); @@ -57,4 +57,35 @@ public function getContributor() } return $owner; } + + /** + * Before-save hook. + * + * @param array $args + */ + protected function beforeSave($args) + { + // Delete a contributed item. In fact, for security reason, make it + // private and invisible to contributor. + if ($this->deleted) { + $this->public = false; + } + } + + /** + * After-save hook. + * + * @param array $args + */ + protected function afterSave($args) + { + if (!$this->public) { + $item = $this->Item; + if ($item->public) { + $item->public = false; + $item->save(false); + } + release_object($item); + } + } } diff --git a/models/ContributionType.php b/models/ContributionType.php index adfbaafb..4491db3a 100644 --- a/models/ContributionType.php +++ b/models/ContributionType.php @@ -22,9 +22,13 @@ class ContributionType extends Omeka_Record_AbstractRecord public $item_type_id; public $display_name; public $file_permissions = 'Disallowed'; - - protected $_related = array('ContributionTypeElements' => 'getTypeElements', - 'ItemType' => 'getItemType'); + public $multiple_files = 0; + public $add_tags = 0; + + protected $_related = array( + 'ContributionTypeElements' => 'getTypeElements', + 'ItemType' => 'getItemType', + ); protected function filterPostData($post) { @@ -34,7 +38,7 @@ protected function filterPostData($post) } return $post; } - + protected function _validate() { if(empty($this->item_type_id)) { @@ -47,7 +51,7 @@ protected function _initializeMixins() $this->_mixins[] = new Mixin_ContributionOrder($this, 'ContributionTypeElement', 'type_id', 'Elements'); } - + /** * Get the type elements associated with this type. * @@ -57,7 +61,7 @@ public function getTypeElements() { return $this->_db->getTable('ContributionTypeElement')->findByType($this); } - + /** * Get the item type associated with this type. * @@ -173,7 +177,7 @@ public function getPossibleTypeElements() } return $options; } - + public function getRecordUrl($action = 'show') { return url("contribution/types/$action/id/{$this->id}"); diff --git a/models/Mixin/ContributionOrder.php b/models/Mixin/ContributionOrder.php index 807b29e0..ed8a1b0a 100644 --- a/models/Mixin/ContributionOrder.php +++ b/models/Mixin/ContributionOrder.php @@ -95,7 +95,7 @@ public function reorderChildren() array($parentId)); } - public function addChild(Omeka_Record $child) + public function addChild(Omeka_Record_AbstractRecord $child) { if (!$this->_record->exists()) { throw new Omeka_Record_Exception(__('Cannot add a child to a record that does not exist yet!')); diff --git a/models/Table/ContributionType.php b/models/Table/ContributionType.php index 643ca940..0d5b7f4a 100644 --- a/models/Table/ContributionType.php +++ b/models/Table/ContributionType.php @@ -15,6 +15,19 @@ */ class Table_ContributionType extends Omeka_Db_Table { + /** + * Get a contribution type by item type. + * + * @param ItemType|integer $itemType Item type record or id. + * @return ContributionType|null + */ + public function getByItemType($itemType) + { + $params = array(); + $params['item_type_id'] = is_object($itemType) ? $itemType->id : (integer) $itemType; + $result = $this->findBy($params, 1); + return $result ? reset($result) : null; + } /** * Used to create options for HTML select form elements. diff --git a/plugin.ini b/plugin.ini index b9dfbc78..30d4ec3a 100644 --- a/plugin.ini +++ b/plugin.ini @@ -6,7 +6,7 @@ link="http://omeka.org/codex/Plugins/Contribution_2.0" support_link="http://omeka.org/forums/forum/plugins" omeka_minimum_version="2.3" omeka_target_version="2.3" -version="3.1.0" +version="3.1.4" tags="social, items" license="GPLv3" required_plugins="GuestUser" diff --git a/tests/cases/ContributionTypeTest.php b/tests/cases/ContributionTypeTest.php index d245f847..c6140026 100644 --- a/tests/cases/ContributionTypeTest.php +++ b/tests/cases/ContributionTypeTest.php @@ -25,13 +25,13 @@ public function testIsFileAllowed() $type = new ContributionType; $this->assertFalse($type->isFileAllowed()); - $type->file_permissions = ContributionType::FILE_PERMISSION_ALLOWED; + $type->file_permissions = 'Allowed'; $this->assertTrue($type->isFileAllowed()); - $type->file_permissions = ContributionType::FILE_PERMISSION_REQUIRED; + $type->file_permissions = 'Required'; $this->assertTrue($type->isFileAllowed()); - $type->file_permissions = ContributionType::FILE_PERMISSION_DISALLOWED; + $type->file_permissions = 'Disallowed'; $this->assertFalse($type->isFileAllowed()); } @@ -41,22 +41,22 @@ public function testIsFileRequired() $type = new ContributionType; $this->assertFalse($type->isFileRequired()); - $type->file_permissions = ContributionType::FILE_PERMISSION_ALLOWED; + $type->file_permissions = 'Allowed'; $this->assertFalse($type->isFileRequired()); - $type->file_permissions = ContributionType::FILE_PERMISSION_REQUIRED; + $type->file_permissions = 'Required'; $this->assertTrue($type->isFileRequired()); - $type->file_permissions = ContributionType::FILE_PERMISSION_DISALLOWED; + $type->file_permissions = 'Disallowed'; $this->assertFalse($type->isFileRequired()); } public function testFilePermissionsCoverage() { $permissions = ContributionType::getPossibleFilePermissions(); - $this->assertAndRemoveArrayKey(ContributionType::FILE_PERMISSION_ALLOWED, $permissions); - $this->assertAndRemoveArrayKey(ContributionType::FILE_PERMISSION_REQUIRED, $permissions); - $this->assertAndRemoveArrayKey(ContributionType::FILE_PERMISSION_DISALLOWED, $permissions); + $this->assertAndRemoveArrayKey('Allowed', $permissions); + $this->assertAndRemoveArrayKey('Required', $permissions); + $this->assertAndRemoveArrayKey('Disallowed', $permissions); $this->assertEquals(0, count($permissions), 'Not all file permission levels are covered by testing.'); } diff --git a/views/admin/common/contribution-quick-filters.php b/views/admin/common/contribution-quick-filters.php index f8d5c7c4..0abe00e3 100644 --- a/views/admin/common/contribution-quick-filters.php +++ b/views/admin/common/contribution-quick-filters.php @@ -6,6 +6,8 @@anonymous ? " | " . __('Anonymous') : ""; ?>
+anonymous ? " | " . __('Anonymous') : ""; ?>
array('class' => 'admin-thumb panel')), diff --git a/views/admin/items/browse.php b/views/admin/items/browse.php index 02eddb2f..fb3dd070 100644 --- a/views/admin/items/browse.php +++ b/views/admin/items/browse.php @@ -45,7 +45,7 @@ -