- cleared when the given entry is edited/deleted * p4cms_content_type_ - cleared when the given type is edited/deleted * p4cms_content_list - cleared when any content is added/edited/deleted intended for * updating aggregate lists of content * The <> brackets are just to show position, only the binhex'd id will be present. * * @copyright 2011 Perforce Software. All rights reserved. * @license Please see LICENSE.txt in top-level folder of this distribution. * @version / */ class Content_IndexController extends Zend_Controller_Action { // arbitrary limit for image scaling operations // 4k allows scaling upto 10 megapixel output and uses about 1/4 GB of ram. const MAX_SCALE = 4000; public $contexts = array( 'add' => array('dojoio' => 'post', 'json' => 'post'), 'browse' => array('json', 'partial'), 'edit' => array('dojoio' => 'post', 'json' => 'post'), 'choose-type' => array('partial'), 'delete' => array('json', 'partial'), 'form' => array('partial', 'dojoio' => 'post'), 'index' => array('json'), 'sub-form' => array('partial'), 'toolbar' => array('partial'), 'validate-field' => array('json'), 'view' => array('json', 'preview', 'partial'), 'opened' => array('json') ); /** * Initialize object * * Extends parent to add content and content type tags to page cache * if it is present. */ public function init() { parent::init(); if (P4Cms_Cache::canCache('page')) { P4Cms_Cache::getCache('page') ->addTag('p4cms_content') ->addTag('p4cms_content_type'); } } /** * Display a list of recent content. */ public function indexAction() { $this->view->canAdd = $this->acl->isAllowed('content', 'add'); if (!$this->acl->isAllowed('content', 'access')) { return; } $this->view->recent = P4Cms_Content::fetchAll( P4Cms_Record_Query::create() ->setMaxRows(5) ->setSortBy(P4Cms_Record_Query::SORT_DATE) ->setReverseOrder(true) ); // tag the page cache so it can be appropriately cleared later if (P4Cms_Cache::canCache('page')) { P4Cms_Cache::getCache('page')->addTag('p4cms_content_list'); } } /** * List content for management. * * To provide an action the grid participants subscribe to the topic: * p4cms.content.grid.actions * * One argument will be passed to subscribers: * P4Cms_Navigation $actions a navigation container to hold all actions * * They are expected to add/modify/etc. pages to the navigation container. * The pages will be rendered to a Menu Dijit so utilizing the onClick * and, optionally, onShow events is advised to control menu item behaviour. * * The default actions are added during module init. To modify/remove a default * action, subscribe during module load, or later, to ensure the default nav * entries are already present. */ public function manageAction() { // enforce permissions. $this->acl->check('content', 'manage'); // generate page with data grid and form $this->browseAction(); // the request can specify which columns appear - only permit column names. $request = $this->getRequest(); $columns = $request->getParam('columns', array('type', 'title', 'modified', 'actions')); $columns = array_filter($columns, 'is_string'); // enable manage-specific settings $view = $this->view; $view->showAddLink = $this->acl->isAllowed('content', 'add'); $view->showDeleteButton = $this->acl->isAllowed('content', 'delete'); $view->selectionMode = 'extended'; $view->columns = $columns; $view->headTitle()->set('Manage Content'); $this->getHelper('helpUrl')->setUrl('content.html'); } /** * Handle requests to display a list of content * Prepares list options form for tradtional requests. * Prepares content query for context specific requests. * * @publishes p4cms.content.grid.actions * Modify the passed menu (add/modify/delete items) to influence the actions shown * on entries in the Manage Content grid. * P4Cms_Navigation $actions A menu to hold grid actions. * * @publishes p4cms.content.grid.data.item * Return the passed item after applying any modifications (add properties, change * values, etc.) to influence the row values sent to the Manage Content grid. * array $item The item to potentially modify. * mixed $model The original object/array that was used * to make the item. * Ui_View_Helper_DataGrid $helper The view helper that broadcast this * topic. * * @publishes p4cms.content.grid.data * Adjust the passed data (add properties, modify values, etc.) to influence the * row values sent to the Manage Content grid. * Zend_Dojo_Data $data The data to be filtered. * Ui_View_Helper_DataGrid $helper The view helper that broadcast this * topic. * * @publishes p4cms.content.grid.populate * Adjust the passed query (possibly based on values in the passed form) to filter * which content entries will be shown on the Manage Content grid. * P4Cms_Record_Query $query The query to filter content entries. * P4Cms_Form_PubSubForm $form A form containing filter options. * * @publishes p4cms.content.grid.render * Make adjustments to the datagrid helper's options pre-render (e.g. change * options to add columns) for the Manage Content grid. * Ui_View_Helper_DataGrid $helper The view helper that broadcast this * topic. * * @publishes p4cms.content.grid.form * Make arbitrary modifications to the Manage Content filters form. * P4Cms_Form_PubSubForm $form The form that published this event. * * @publishes p4cms.content.grid.form.subForms * Return a Form (or array of Forms) to have them added to the Manage Content * filters form. The returned form(s) should have a 'name' set on them to allow * them to be uniquely identified. * P4Cms_Form_PubSubForm $form The form that published this event. * * @publishes p4cms.content.grid.form.preValidate * Allows subscribers to adjust the Manage Content filters form prior to * validation of the passed data. For example, modify element values based on * related selections to permit proper validation. * P4Cms_Form_PubSubForm $form The form that published this event. * array $values An associative array of form values. * * @publishes p4cms.content.grid.form.validate * Return false to indicate the Manage Content filters form is invalid. Return true * to indicate your custom checks were satisfied, so form validity should be * unchanged. * P4Cms_Form_PubSubForm $form The form that published this event. * array $values An associative array of form values. * * @publishes p4cms.content.grid.form.populate * Allows subscribers to adjust the Manage Content filters form after it has been * populated with the passed data. * P4Cms_Form_PubSubForm $form The form that published this event. * array $values The values passed to the populate * method. */ public function browseAction() { // ensure users are allowed to access content before the list is displayed $this->acl->check('content', 'access'); // get list option sub-forms. $request = $this->getRequest(); $gridNamespace = 'p4cms.content.grid'; $form = new Ui_Form_GridOptions( array( 'namespace' => $gridNamespace ) ); $form->populate($request->getParams()); // the request can specify which columns appear - only permit column names. $columns = $request->getParam('columns', array('type', 'title', 'modified')); $columns = array_filter($columns, 'is_string'); // if the title column is requested, ensure that it isn't "linkified" $columns = array_flip($columns); if (isset($columns['title'])) { $columns['title'] = array( 'formatter' => 'p4cms.content.grid.Formatters.titleNoLink' ); } // setup access-restricted view, manageAction will expand these settings as needed $view = $this->view; $view->form = $form; $view->pageSize = $request->getParam('count', 100); $view->rowOffset = $request->getParam('start', 0); $view->pageOffset = round($view->rowOffset / $view->pageSize, 0) + 1; $view->columns = $columns; $view->showAddLink = false; $view->showDeleteButton = false; $view->selectionMode = $request->getParam('selectionMode', 'single'); // set DataGrid view helper namespace $helper = $view->dataGrid(); $helper->setNamespace($gridNamespace); // early exit for standard requests (ie. not json or partial) if (!$this->contextSwitch->getCurrentContext()) { $this->_helper->layout->setLayout('manage-layout'); return; } // construct list query - allow third-parties to influence query. $query = new P4Cms_Record_Query; $query->setRecordClass('P4Cms_Content'); try { $result = P4Cms_PubSub::publish($gridNamespace . '.populate', $query, $form); } catch (Exception $e) { P4Cms_Log::logException("Error building content list query.", $e); } // prepare sorting options $sortKey = $sortKeyOriginal = $request->getParam('sort', '-#REdate'); $sortFlags = array(); // handle sort order; descending sort identified with '-' prefix. if (substr($sortKey, 0, 1) == '-') { $sortKey = substr($sortKey, 1); $sortFlags[] = P4Cms_Record_Query::SORT_DESCENDING; } // if we're sorting via an internal attribute, use the traditional // syntax by knocking out the query options and reversing the // results if necessary. if (strpos($sortKey, '#') === 0) { $sortFlags = null; $query->setReverseOrder($sortKey === $sortKeyOriginal ? false : true); } // some column names differ from the model, so we need a map. $sortKeyMap = array( 'type' => 'contentType' ); // look up requested sort column in our map. $sortKey = isset($sortKeyMap[$sortKey]) ? $sortKeyMap[$sortKey] : $sortKey; $query->setSortBy($sortKey, $sortFlags); // add query to the view. $view->query = $query; } /** * Choose a content type to add. * Prompts the user to select from the available content types. */ public function chooseTypeAction() { // load types into view. $this->view->typeGroups = P4Cms_Content_Type::fetchGroups(); $this->view->headTitle()->set('Choose Content Type'); } /** * Renders the content form for the requested entry or content type. * * @param boolean $getForm if true, return the content form. * * The p4cms.content.form events documented on the manage action will * also be broadcast when this action is accessed. */ public function formAction($getForm = false) { $request = $this->getRequest(); $type = $request->getParam('contentType'); $id = $request->getParam('id'); // if a version is present, generate rev-spec. $revspec = $request->getParam('version'); if ($revspec) { // enforce permissions - viewing historic versions requires 'access-history' privilege. $this->acl->check('content', 'access-history'); $revspec = '#' . $revspec; } $entry = $id ? P4Cms_Content::fetch($id . $revspec, $revspec ? array('includeDeleted' => true) : null) : new P4Cms_Content(array(P4Cms_Content::TYPE_FIELD => $type)); if (!$getForm && $request->isPost()) { return $this->editAction($entry, 'edit'); } // construct and populate the content form. $form = $this->_getContentForm($entry); $form->populate($request->getPost()); // populate the view. $this->view->entry = $entry; $this->view->type = $type; $this->view->form = $form; // explicitly set partial context for other requests. $this->contextSwitch->initContext('partial'); return $form; } /** * Renders a specific sub-form of the content form. * Forces the 'partial' request context. */ public function subFormAction() { $form = $this->formAction(true); // ensure requested sub-form exists. $subForm = $form->getSubForm($this->getRequest()->getParam('form')); if (!$subForm instanceof Zend_Form) { throw new Content_Exception( "Cannot get sub-form. Requested sub-form does not exist." ); } // populate the view. $this->view->subForm = $subForm; } /** * Add content. * * If not type has been selected, forwards to choose-type action. * If a type is indicated, create new entry and forward to edit action. */ public function addAction() { // enforce permissions. $this->acl->check('content', 'add'); $request = $this->getRequest(); // if get request and no valid type specified, prompt user for type. // check 'contentType' param first, and 'type' param second. $type = $request->getParam('contentType', $request->getParam('type')); if ($request->isGet() && (!$type || !P4Cms_Content_Type::exists($type))) { return $this->_forward('choose-type'); } // get the content type definition. $type = P4Cms_Content_Type::fetch($type); // create new content model and set type. $entry = new P4Cms_Content; $entry->setContentType($type); // set the page title. $this->view->headTitle()->set("Add '" . $type->getLabel() . "'"); return $this->editAction($entry, 'edit'); } /** * Display content. * * @param P4Cms_Content $entry optional - content entry to display. * @param null|string|array $skipAclCheck optional - pass string or array of strings * enumerating the privilegs that can be skipped */ public function viewAction(P4Cms_Content $entry = null, $skipAclCheck = null) { // enforce permissions. if (!in_array('access', (array)$skipAclCheck)) { $this->acl->check('content', 'access'); } // if not called with an entry, we must fetch one. $request = $this->getRequest(); // if a version is present, generate rev-spec. $revspec = $request->getParam('version'); if ($revspec) { // enforce permissions - viewing historic versions requires 'access-history' privilege. if (!in_array('access-history', (array)$skipAclCheck)) { $this->acl->check('content', 'access-history'); } // inject javascript to enable the history toolbar button // if the user came directly to the view action. // the edit action forwards here and doesn't want this. if ($request->getActionName() == 'view') { $this->view->dojo()->addOnLoad( "function(){ p4cms.content.startHistory(); }" ); } $revspec = '#' . $revspec; } // attempt to fetch content entry. try { $entry = $entry ?: P4Cms_Content::fetch( $request->getParam('id') . $revspec, $revspec ? array('includeDeleted' => true) : null ); } catch (Exception $e) { // we only have special handling for specific types; rethrow anything else if (!$e instanceof P4Cms_Record_NotFoundException && !$e instanceof InvalidArgumentException ) { throw $e; } return $this->_forward('page-not-found', 'index', 'error'); } // validate the content type - if the type has no id, we assume it // is missing and was dynamically generated by the content class. $type = $entry->getContentType(); $typeId = $entry->getContentTypeId(); if (!$type->getId()) { $message = $typeId ? "This content entry requires a missing content type ('$typeId')." : "This content entry has no content type."; throw new Content_Exception($message); } // populate the view. $view = $this->view; $view->entry = $entry; $view->type = $type; // request can specify an array of fields, true or false for json context. $view->fields = $request->getParam('fields', true); // request can request the change be included for json context. $view->includeChange = (bool) $request->getParam('includeChange', false); // request can request the status be included for json context. $view->includeStatus = (bool) $request->getParam('includeStatus', false); // request can request the opened status be included for json context. $view->includeOpened = (bool) $request->getParam('includeOpened', false); // tag the page cache so it can be appropriately cleared later if (P4Cms_Cache::canCache('page')) { P4Cms_Cache::getCache('page') ->addTag('p4cms_content_' . bin2hex($entry->getId())) ->addTag('p4cms_content_type_' . bin2hex($entry->getContentTypeId())); } // set the page title if entry has one. if (strlen($entry->getTitle())) { $this->view->headTitle()->set($entry->getTitle()); } // set the meta description from the entry's excerpt $excerpt = $entry->getExcerpt(150, array('fullExcerpt' => true)); if (strlen($excerpt)) { $this->view->headMeta()->setName('description', $excerpt); } // set the contentEntry view helper defautls $view->contentEntry()->setDefaults( $entry, array(Content_View_Helper_ContentEntry::OPT_PRELOAD_FORM => true) ); // record the content id in the widget context to assist any plugins // that need to know what content is currently being displayed. $this->widgetContext->setValue('contentId', $entry->getId()); // select the layout and view scripts to use. if (!$this->contextSwitch->getCurrentContext()) { $this->getHelper('layout')->setLayout($this->_getEntryLayoutScript($entry)); } $this->getHelper('viewRenderer')->setRender($this->_getEntryViewScript($entry)); } /** * This action exposes the list of users currently editing a given * content entry (along with the entry's status and change details). * If posted to with an event parameter set to start/ping/stop the * active user will be updated in opened list. * * If the event param is start, the active user will be added to the * opened list with a pingTime of now and editTime of null. * * If the event param is ping, the active user will have their ping * time updated to now. The edit time will be unchanged (the edit * time is updated whenever 'validateFieldAction' is called). * * For the stop event the active user will be removed from * the opened list. * * Lastly, if the event param is missing or unrecognized the status * will be returned but no changes will be made to the users opened * details. * * Even if the caller lacks 'access-history' permissions conflicts * involving a deleted revision are reported. */ public function openedAction() { // require edit access as that is the only time this action makes sense $this->acl->check('content', 'edit'); // force json context; without fields the html version won't work well. $this->contextSwitch->initContext('json'); // force the settings we care about to known values, we don't // want to risk leaking field values via this action. $request = $this->getRequest(); $request->setParam('fields', false) ->setParam('includeChange', true) ->setParam('includeStatus', true) ->setParam('includeOpened', true) ->setParam('version', 'head'); // let view take care of fetching the entry and providing output $this->viewAction(null, array('access', 'access-history')); // exit if no content entry could be retrieved $entry = $this->view->entry; if (!$entry instanceof P4Cms_Content) { return; } // if its a post deal with the start/ping/stop events if ($request->isPost()) { $user = P4Cms_User::fetchActive(); $record = new P4Cms_Content_Opened; $record->setAdapter(P4Cms_Site::fetchActive()->getStorageAdapter()) ->setId($entry->getId()); switch ($request->getParam('event')) { case 'start': $record->setUserStartTime($user) ->setUserPingTime($user) ->setUserEditTime($user, null) ->save(); break; case 'ping': $record->setUserPingTime($user) ->save(); break; case 'stop': $record->setUserPingTime($user, null) ->setUserEditTime($user, null) ->save(); break; } } } /** * Rollback content. * * If the record id and change are valid; rolls back the specified * entry and redirects to view so the result will be shown. */ public function rollbackAction() { $request = $this->getRequest(); $id = $request->getParam('id'); // enforce permissions - requires both access-history and edit privileges. $message = 'You do not have permission to rollback this content entry.'; $this->acl->check('content', 'access-history', null, $message); $this->acl->check('content/' . $id, 'edit', null, $message); if ($request->getParam('change')) { $revSpec = '@' . $request->getParam('change'); } else if ($request->getParam('version')) { $revSpec = '#' . $request->getParam('version'); } else { throw new InvalidArgumentException( 'A version or change number must be specified' ); } // fetch the entry at the requested revision $entry = P4Cms_Content::fetch( $id . $revSpec, array('includeDeleted' => true) ); $version = $entry->toP4File()->getStatus('headRev'); $entry->save('Rollback to version ' . $version); // clear any cached entries related to this page P4Cms_Cache::clean( Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, array('p4cms_content_' . bin2hex($entry->getId()), 'p4cms_content_list') ); // redirect to view the specified entry $this->getHelper('redirector')->gotoUrl($entry->getUri()); } /** * Edit content. * * HTTP get requests are forwarded to the view action for in-place * editing. Post requests are validated and saved. * * The p4cms.content.form events documented on the manage action will * also be broadcast when this action is accessed. * * @param P4Cms_Content $entry optional - content entry to display. * @param null|string|array $skipAclCheck optional - pass string or array of strings * enumerating the privilegs that can be skipped */ public function editAction(P4Cms_Content $entry = null, $skipAclCheck = null) { $request = $this->getRequest(); $headVersion = $request->getParam('headVersion'); $entryId = $request->getParam('id'); // If we have a head version and ID ensure we fetch that // revision of the entry. This will cause a conflict // exception on save should we be out of date. if ($entryId && $headVersion) { $entryId = $entryId . "#" . $headVersion; } // enforce permissions. if (!in_array('edit', (array)$skipAclCheck)) { $this->acl->check('content/' . $entryId, 'edit'); } // forward get requests to view action if ($request->isGet()) { // inject javascript to enable the appropariate add/edit toolbar button $action = $entryId ? 'startEdit' : 'startAdd'; $this->view->dojo()->addOnLoad( "function(){ p4cms.content.$action(); }" ); return $this->viewAction($entry, $skipAclCheck); } // if not called with an entry, we must fetch one. try { $entry = $entry ?: P4Cms_Content::fetch($entryId, array('includeDeleted' => true)); } catch (P4Cms_Record_NotFoundException $e) { return $this->_forward('page-not-found', 'index', 'error'); } // construct and populate the content form. $form = $this->_getContentForm($entry); $form->populate($request->getPost()); // populate the view. $this->view->entry = $entry; $this->view->type = $entry->getContentType(); $this->view->form = $form; $this->view->isValid = true; // if form was posted and is valid, save it. if ($request->isPost() && $form->isValid($request->getPost())) { try { // if this content entry doesn't have an owner, // set the content owner to the current user. $entry->setOwner($entry->getOwner() ?: P4Cms_User::fetchActive()); // if a comment was provided by the user, set it as change // description, otherwise provide default value $saveForm = $form->getSubForm('save'); $description = $saveForm && $saveForm->getValue('comment') ? $saveForm->getValue('comment') : 'Saved content change.'; // copy form values to the content entry and save it // entry is a pub/sub record so third-parties can participate // ensure we throw on conflict so we can alert user $entry->setValues($form) ->save($description, P4Cms_Content::SAVE_THROW_CONFLICT); // clear any cached entries related to this page // @todo connect to p4cms.content.record.postSave P4Cms_Cache::clean( Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, array('p4cms_content_' . bin2hex($entry->getId()), 'p4cms_content_list') ); // remove ourselves from the 'opened' list if we had an identified record if ($entryId) { $user = P4Cms_User::fetchActive(); $record = new P4Cms_Content_Opened; $record->setAdapter(P4Cms_Site::fetchActive()->getStorageAdapter()) ->setId($entry->getId()) ->setUserStartTime($user, null) ->setUserPingTime($user, null) ->setUserEditTime($user, null) ->save(); } // notify success and redirect for traditional requests. if (!$this->contextSwitch->getCurrentContext()) { P4Cms_Notifications::add( 'Content saved.', P4Cms_Notifications::SEVERITY_SUCCESS ); return $this->_redirect($request->getBaseUrl()); } } catch (P4_Connection_ConflictException $e) { // if we received a conflict exception we need to mark // the form as being in error and inform the view. $form->markAsError(); $this->view->isConflict = true; } } // save failed validation - include errors in response if ($form->isErrors()) { $this->view->isValid = false; $this->view->form = $form; $this->view->errors = array( 'form' => $form->getErrorMessages(), 'elements' => $form->getMessages() ); } } /** * Delete content. Supports deleting of multiple content entries that will be deleted * in a batch - i.e. either all of them or none. * * List of entry ids to delete are passed in the 'ids' parameter, however the method * also accepts passing entry id(s) via 'id' parameter (this will have precedence if * both 'id and 'ids' parameters are present). * * Requires HTTP post request to perform delete. Traditional requests are redirected * to the index and a notification is set. Context specific requests are rendered using * the appropriate context view script. */ public function deleteAction() { $request = $this->getRequest(); // set up the view $form = new Content_Form_Delete; $view = $this->view; $view->form = $form; // populate the form from the request // support passing entry ids in both 'id' and/or 'ids' params, // ensure 'ids' param will get the value from 'id' if it was set $params = $request->getParams(); $params['ids'] = $request->getParam('id', $request->getParam('ids')); $form->populate($params); // if there are posted data, validate the form and delete selected entries if ($request->isPost()) { // if form is invalid, set response code and exit if (!$form->isValid($params)) { $this->getResponse()->setHttpResponseCode(400); $view->errors = $form->getMessages(); return; } // get adapter for batch and fetch all entries to delete $adapter = P4Cms_Content::getDefaultAdapter(); $entries = P4Cms_Content::fetchAll(array('ids' => (array) $form->getValue('ids'))); // attempt to delete all specified entries in a batch $adapter->beginBatch($form->getValue('comment') ?: 'No description provided.'); foreach ($entries as $entry) { try { // enforce permissions $this->acl->check('content/' . $entry->getId(), 'delete'); // delete content entry $entry->delete(); } catch (Exception $e) { // cannot delete the entry; revert the batch, set the response code and exit $adapter->revertBatch(); $this->getResponse()->setHttpResponseCode(400); $view->message = $e->getMessage(); return; } } // commit batch $adapter->commitBatch(); // clear any affected cached entries $tags = array('p4cms_content_list'); $deletedIds = $entries->invoke('getId'); foreach ($deletedIds as $entryId) { $tags[] = 'p4cms_content_' . bin2hex($entryId); } P4Cms_Cache::clean(Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, $tags); // add notification and redirect to index for traditional requests if (!$this->contextSwitch->getCurrentContext()) { $message = 'Deleted ' . count($deletedIds) . (count($deletedIds) === 1 ? ' content entry.' : ' content entries.'); P4Cms_Notifications::add($message, P4Cms_Notifications::SEVERITY_SUCCESS); return $this->redirector->gotoSimple('index'); } // pass list of deleted entries to the view $view->ids = $deletedIds; } } /** * Download the first image from a piece of content * * Looks at the type definition for the first 'imagefile' field of the * requested content entry. If it has a valid content serves it. * * If content type has no imagefile field, serves the content of the * first available 'file' field if it contains a valid image. * * Otherwise, forward to page-not-found. */ public function imageAction() { // enforce permissions - deny if user doesn't have access permission. $this->acl->check('content', 'access'); // get the content entry to download. $request = $this->getRequest(); // if a version is present, generate rev-spec. $revspec = $request->getParam('version'); $options = null; if ($revspec) { // enforce permissions - viewing historic versions requires 'access-history' privilege. $this->acl->check('content', 'access-history'); $revspec = '#' . $revspec; $options = array('includeDeleted' => true); } // try to retreive the requested record and type; error out if not found try { $entry = P4Cms_Content::fetch($request->getParam('id') . $revspec, $options); } catch (Exception $e) { // we only have special handling for specific types; rethrow anything else if (!$e instanceof P4Cms_Record_NotFoundException && !$e instanceof InvalidArgumentException ) { throw $e; } return $this->_forward('page-not-found', 'index', 'error'); } // check to see if there is an image element on the content type $image = null; $elements = $entry->getContentType()->getFormElements(); foreach ($elements as $element) { if ($element instanceof P4Cms_Form_Element_ImageFile) { $image = $element->getName(); break; } } // if there was no image, check the file, which may be an image if (!$image && $entry->hasFileContentField()) { $image = $entry->getFileContentField(); } // if request specifies a field, use that, otherwise // use the field that we detected above $image = $request->getParam('field', $image); // if we've found something, ensure it's an image and has content, // assuming that an empty element won't have mime data. // we also allow pdf files to be 'viewed as images' - support for // rendering pdf documents directly in the browser is pretty common if ($image) { $metadata = $entry->getFieldMetadata($image); $mimeType = isset($metadata['mimeType']) ? $metadata['mimeType'] : null; if (strpos($mimeType, 'image/') !== 0 && $mimeType !== 'application/pdf') { $image = null; } } // tag the page cache so it can be appropriately cleared later if (P4Cms_Cache::canCache('page')) { P4Cms_Cache::getCache('page') ->addTag('p4cms_content_' . bin2hex($entry->getId())) ->addTag('p4cms_content_type_' . bin2hex($entry->getContentTypeId())); } // if we didn't find a valid image, send a 404 if (!$image) { $this->_forward('page-not-found', 'index', 'error'); return; } // if we did find a valid image, let the download action handle it $request->setParam('field', $image); $this->downloadAction($entry, false); } /** * Download content. * * Serves the contents of the requested field or if no field is * specified, uses the file field. If the requested content entry * does not exist or has no suitable field, forwards to page-not-found. * * @param P4Cms_Content $entry optional - entry instance to download * @param boolean $download optional - indicate whether content should be downloaded * defaults to true. influences content-disposition header. */ public function downloadAction(P4Cms_Content $entry = null, $download = true) { // enforce permissions - deny if user doesn't have access permission. $this->acl->check('content', 'access'); // get the content entry to download. $request = $this->getRequest(); $id = $request->getParam('id'); // if a version is present, generate rev-spec. $revspec = $request->getParam('version'); $options = null; if ($revspec) { // enforce permissions - viewing historic versions requires 'access-history' privilege. $this->acl->check('content', 'access-history'); $revspec = '#' . $revspec; $options = array('includeDeleted' => true); } try { $entry = $entry ?: P4Cms_Content::fetch($id . $revspec, $options); } catch (Exception $e) { // we only have special handling for specific types; rethrow anything else if (!$e instanceof P4Cms_Record_NotFoundException && !$e instanceof InvalidArgumentException ) { throw $e; } return $this->_forward('page-not-found', 'index', 'error'); } // tag the page cache so it can be appropriately cleared later if (P4Cms_Cache::canCache('page')) { P4Cms_Cache::getCache('page') ->addTag('p4cms_content_' . bin2hex($entry->getId())) ->addTag('p4cms_content_type_' . bin2hex($entry->getContentTypeId())); } // determine what content field to deliver. // if the request specifies a field, serve it. // fallback to the entry's file content field. $field = $request->getParam('field', $entry->getFileContentField()); // if there is still no field to serve, indicate 404. if (!$field) { $this->_forward('page-not-found', 'index', 'error'); return; } // get entry data to download $data = $entry->getValue($field); // obtain file metadata. $metadata = $entry->getFieldMetadata($field); $filename = isset($metadata['filename']) ? $metadata['filename'] : null; $mimeType = isset($metadata['mimeType']) ? $metadata['mimeType'] : 'application/octet-stream'; // if data represents an image, adjust it before sending to output if (strpos($mimeType, 'image/') === 0) { try { $data = $this->_adjustImage($data); } catch (Exception $e) { P4Cms_Log::log("Image adjust failed: " . $e->getMessage(), P4Cms_Log::WARN); } } $this->getResponse()->setHeader('Content-Type', $mimeType); if ($download) { $this->getResponse()->setHeader( 'Content-Disposition', 'attachment;' . ($filename ? ' filename="' . $filename . '"' : '') ); } // if entry's field value is empty, render the page if (!$data) { return $this->viewAction($entry); } // disable autorendering for the download $this->_helper->viewRenderer->setNoRender(); $this->_helper->layout->disableLayout(); print $data; } /** * Validate the passed field */ public function validateFieldAction() { // force json context. $this->contextSwitch->initContext('json'); // extract field/value from request. $request = $this->getRequest(); $field = $request->getParam('field'); $value = $request->getParam('value'); $contentId = $request->getParam('contentId', null); // enforce permissions. if ($contentId) { $this->acl->check('content' . '/' . $contentId, 'edit'); } else { $this->acl->check('content', 'add'); } // verify that field is specified if ($field == '') { throw new P4Cms_Content_Exception( "Cannot validate field - no field given." ); } // fetch the type to ensure it is valid $type = P4Cms_Content_Type::fetch($request->getParam('contentType')); // setup an entry to get the display value from $entry = new P4Cms_Content; $entry->setContentType($type) ->setValue($field, $value); // get the content type and element definition. $type = $entry->getContentType(); $element = $type->getFormElement($field); // validate the field. $isValid = $element->isValid($value); // get the entry at the head revision $options = array('includeDeleted' => true); $headEntry = null; if ($contentId && P4Cms_Content::exists($contentId, $options)) { $headEntry = P4Cms_Content::fetch($contentId, $options); // update the opened info to reflect our edit/ping $user = P4Cms_User::fetchActive(); $record = new P4Cms_Content_Opened; $record->setAdapter(P4Cms_Site::fetchActive()->getStorageAdapter()) ->setId($contentId) ->setUserPingTime($user) ->setUserEditTime($user) ->save(); } // populate the view. $this->view->type = $type; $this->view->entry = $headEntry; $this->view->element = $element; $this->view->fieldName = $field; $this->view->fieldValue = $value; $this->view->isValid = $isValid; $this->view->errors = $element->getMessages(); $this->view->displayValue = $entry->getDisplayValue($field); } /** * Get the view script to use for the given content entry. * * Searches view script paths for most specific template. * 1. index/view-entry-.phtml * 2. index/view-type-.phtml * 3. index/view.phtml * * @param P4Cms_Content $entry the entry to be rendered. * @return string the name of the view script to use. * * @publishes p4cms.content.view.scripts * Return the passed scripts array, making any modifications (add, remove or change * values), to influence the view script that ends up being selected for rendering. * The first view script filename in the list that exists in the view's search * paths gets used. Note that the filename should not include the suffix * (typically ".phtml"). * array $scripts The list of view script filenames. * P4Cms_Content $entry The content entry to render. */ protected function _getEntryViewScript(P4Cms_Content $entry) { $scripts = array(); // convention for entry ids. if ($entry->getId()) { $scripts[] = 'index/view-entry-'. $entry->getId(); } // convention for content types. $scripts[] = 'index/view-type-'. $entry->getContentType()->getId(); // let third-parties add to or alter the view script conventions $scripts = P4Cms_PubSub::filter( 'p4cms.content.view.scripts', $scripts, $entry ); // find the first template that exists among the possible scripts. $suffix = '.' . $this->_helper->viewRenderer->getViewSuffix(); foreach ($scripts as $script) { if ($this->view->getScriptPath($script . $suffix)) { return basename($script); } } // no match, fallback to default view. return 'view'; } /** * Get the layout script to use for the given content entry. * If the content type specifies a valid layout, returns it. * Otherwise, returns the current default layout. * * @param P4Cms_Content $entry the entry to be rendered. * @return string the name of the layout script to use. */ protected function _getEntryLayoutScript(P4Cms_Content $entry) { $layout = $entry->getContentType()->getLayout(); $suffix = '.' . $this->_helper->viewRenderer->getViewSuffix(); if ($layout && $this->view->getScriptPath($layout . $suffix)) { return $layout; } return $this->_helper->layout->getLayout(); } /** * Creates the content form, passing in the formIdPrefix if present * * @param P4Cms_Content $entry The content entry to make a form for. * @return Content_Form_Content The completed form. */ protected function _getContentForm(P4Cms_Content $entry) { $options = array('entry' => $entry); // if the entry doesn't have a content type id, we can't build a proper form. // this is likely the case of a missing content type or missing attributes. if (!$entry->getContentTypeId()) { throw new Content_Exception("Cannot get content form. Content type is invalid or missing."); } // if request specifies an id prefix, add it to options $request = $this->getRequest(); $options['idPrefix'] = $request->getParam('formIdPrefix'); return new Content_Form_Content($options); } /** * Adjusts given image (represented by $imageData) according to the request params. * * Following request parameters are recognized: * 'width' - target width in pixels; if not set, but 'height' is set, * then target width will be computed from 'height' such * that image keeps same aspect ratio as original * 'height' - target height in pixels; if not set, but 'width' is set, * then target height will be computed from 'width' such * that image keeps same aspect ratio as original * 'maxWidth' - maximum target width, image will be proportionally shrunk * if computed width is greater than this value * 'maxHeight' - maximum target height, image will be proportionally shrunk * if computed height is greater than this value * 'sharpen' - if set, image will be sharpened (applied after resizing) * * @param string $imageData input image data * @return string adjusted image data */ protected function _adjustImage($imageData) { $request = $this->getRequest(); $width = (int) $request->getParam('width'); $height = (int) $request->getParam('height'); $maxWidth = (int) $request->getParam('maxWidth'); $maxHeight = (int) $request->getParam('maxHeight'); $sharpen = $request->getParam('sharpen'); // early exit if nothing to apply if (!$width && !$height && !$maxWidth && !$maxHeight && !$sharpen) { return $imageData; } $image = new P4Cms_Image; $image->setData($imageData); $dimensions = $image->getDriver()->getImageSize(); $ratio = $dimensions['width'] / $dimensions['height']; // set target width and height: // - set to original size if neither dimension was specified // - if only one dimension was specified, compute the other one // to keep the aspect ration of the original image if (!$width && !$height) { $width = $dimensions['width']; $height = $dimensions['height']; } else if (!$width) { $width = round($height * $ratio); } else if (!$height) { $height = round($width / $ratio); } // lower image dimensions if they exceed maximum dimensions (if provided) if ($maxHeight && $maxHeight < $height) { $width = round($width * $maxHeight / $height); $height = $maxHeight; } if ($maxWidth && $maxWidth < $width) { $height = round($height * $maxWidth / $width); $width = $maxWidth; } // resize image according to computed width and height (if they differ from original size) if ($width !== $dimensions['width'] || $height !== $dimensions['height']) { if ($width > static::MAX_SCALE || $height > static::MAX_SCALE) { throw new Content_Exception( "Width or height exceeds maximum scale of " . static::MAX_SCALE . "px." ); } $image->transform('scale', array($width, $height)); } // sharpen output image if requested if ($sharpen) { $image->transform('sharpen'); } return $image->getData(); } }