<?php
/**
* Perforce Swarm
*
* @copyright 2014 Perforce Software. All rights reserved.
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
*/
namespace Avatar\Controller;
use Projects\Model\Project;
use Zend\Http\Exception\InvalidArgumentException;
use Zend\Mvc\Controller\AbstractActionController;
use P4\File\File;
use Zend\View\Model\JsonModel;
/**
* Class IndexController
* @package Avatar\Controller
*
*
* Remaining issues:
* - display avatar and splash information on project page and in project edit form
*/
class IndexController extends AbstractActionController
{
/**
* When a project is added or edited, run the Projects add action, then
* handle the image.
*
* @return mixed
*/
public function projectAddAction()
{
$response = $this->forward()->dispatch(
'Projects\Controller\Index',
array(
'action' => 'add'
)
);
$this->doAddEdit('add');
return $response;
}
/**
* When a project is added or edited, run the Projects edit action, then
* handle the image.
*
* @return mixed
*/
public function projectEditAction()
{
$response = $this->forward()->dispatch(
'Projects\Controller\Index',
array(
'action' => 'edit',
'project' => $this->getEvent()->getRouteMatch()->getParam('project')
)
);
$this->doAddEdit('edit');
return $response;
}
/**
* Handles image validation and thumbnail generation
* if POST, validates file, generates thumbnail
* if GET, displays the cached image for the specified project and size. If it does not exist,
* create it, then display it.
*
* This method returns json if validating an image, exits once the image is displayed properly, and returns null if
* errors are encountered.
*
* Project id must be set as a route parameter when using GET; size defaults to 64px square if unset
*
* Thumbnail generation is done by cropping the image to a square apsect ratio, then saving it in data/thumbs/
* Actual image is saved when the form is submitted.
*
* @thorws InvalidArgumentException
* @return array|\Zend\Http\Response|null
*/
public function projectAction()
{
$request = $this->getRequest();
$services = $this->getServiceLocator();
$projectId = $this->getEvent()->getRouteMatch()->getParam('project');
$type = $this->getEvent()->getRouteMatch()->getParam('type');
$format = $this->getEvent()->getRouteMatch()->getParam('format');
// if post, handle preview
if ($request->isPost()) {
if ($type == 'avatar') {
$filters = $services->get('InputFilterManager');
$projectFilter = $filters->get('ProjectFilter');
$imageFilter = $projectFilter->get('project-avatar');
} elseif ($type == 'splash') {
$filters = $services->get('InputFilterManager');
$projectFilter = $filters->get('ProjectFilter');
$imageFilter = $projectFilter->get('project-splash');
} else {
throw new InvalidArgumentException;
}
// Make certain to merge the files info!
$post = array_merge_recursive(
$request->getPost()->toArray(),
$request->getFiles()->toArray()
);
$imageFilter->setValue($post['file']);
if (!$imageFilter->isValid()) {
return new JsonModel(
array(
'isValid' => $imageFilter->isValid(),
'messages' => array('project-' . $type => $imageFilter->getMessages())
)
);
}
$data = $imageFilter->getValue();
$info = $this->getImageInfo($data['tmp_name']);
$image = $this->fetchImage($data['tmp_name'], $info);
$explodedMime = explode('/', $info['mime']);
$tempFileName = $this->getFilename($projectId, $explodedMime[1], $type);
unlink($data['tmp_name']);
$tempFile = $this->getCacheDir() . '/' . $tempFileName;
$image->writeimage($tempFile);
return new JsonModel(
array(
'isValid' => true,
'filename' => $tempFileName,
'content' => 'data:' . $info['mime'] . ';base64,'.base64_encode($image)
)
);
}
// if not post, display image
$p4Admin = $services->get('p4_admin');
$storage = $services->get('depot_storage');
if ($type == 'avatar') {
$size = $this->getEvent()->getRouteMatch()->getParam('size');
$size = (!empty($size)) ? $size : 64;
} else {
$size = 'splash';
}
if (!Project::exists($projectId, $p4Admin)) {
$this->getResponse()->setStatusCode(404);
return;
}
try {
$project = Project::fetch($projectId, $p4Admin);
$cacheDir = $this->getCacheDir();
$filename = $project->get($type);
if (empty($filename)) {
$this->getResponse()->setStatusCode(404);
return;
}
$cacheFile = $cacheDir . '/' . $size . '_' . $filename;
if (!is_readable($cacheFile)) {
$depotPath = $type . 's/';
$depotFile = $storage->absolutize($depotPath . $filename);
try {
$imageFile = File::fetch($depotFile, $p4Admin, true);
} catch (\P4\File\Exception\NotFoundException $e) {
$this->getResponse()->setStatusCode(404);
return;
} catch (\P4\Spec\Exception\NotFoundException $e) {
$this->getResponse()->setStatusCode(404);
return;
} catch (\Exception $e) {
$this->getResponse()->setStatusCode(500);
return;
}
$pool = $p4Admin->getService('clients');
$pool->grab();
$pool->reset(true);
$local = $imageFile->open()->getLocalFilename();
$image = $this->fetchImage($local);
if ($type == 'avatar') {
$image->scaleImage($size, $size, true);
}
$image->writeimage($cacheFile);
}
} catch (Exception $e) {
$this->getResponse()->setStatusCode(404);
return;
}
$info = $this->getImageInfo($cacheFile);
// if this is a test, don't actually output the image
if ($this->getRequest()->isTest) {
return new JsonModel($info);
} elseif ($format == 'base64') {
echo 'data:' . $info['mime'] . ';base64,'.base64_encode(file_get_contents($cacheFile));
exit;
}
header('Content-Type: ' . $info['mime']);
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . filesize($cacheFile));
header('Content-Disposition: filename="' . $cacheFile . '"');
readfile($cacheFile);
exit;
}
/**
* @param string $mode The mode to set on the filter - add or edit.
*/
protected function doAddEdit($mode)
{
if ($mode != 'add' && $mode != 'edit') {
return;
}
$request = $this->getRequest();
$post = $request->getPost()->toArray();
if (!$request->isPost()) {
return;
}
// configure our filter with the p4 connection and add/edit mode
$filter = $this->getServiceLocator()->get('InputFilterManager')->get('ProjectFilter');
$filter->setMode($mode)
->setData($post);
$isValid = $filter->isValid();
if (!$isValid) {
return;
}
$values = $filter->getValues();
// note that this skips the empty value
foreach (array('avatar', 'splash') as $type) {
if (!array_key_exists($type, $post) || empty($post[$type])) {
continue;
}
$this->submitImage($values, $type);
}
}
/**
* Handles submitting image to the depot. Triggered on add/edit.
*
* @param array $data The project data that was saved from the form.
* @param string $type The type of image we're submitting; avatar or splash;
* dictates depot and file path.
*/
protected function submitImage($data = array(), $type = 'avatar')
{
$filename = $this->getCacheDir() . '/' . $data[$type];
$id = $data['id'];
$services = $this->getServiceLocator();
$depotPath = $type . 's/';
$depotFile = $depotPath . $this->getFilename($id, substr(strrchr($filename, "."), 1), $type);
$storage = $services->get('depot_storage');
$depotFile = $storage->absolutize($depotFile);
$storage->writeFromFile($depotFile, $filename);
// clear file cache of any size so that the new file is cached instead
$clearPattern = $this->getCacheDir() . '/' . '*'
. $this->getFilename($id, substr(strrchr($filename, "."), 1), $type);
array_map('unlink', glob($clearPattern));
}
private function getImageInfo($file)
{
return getimagesize($file);
}
/**
* @param $file File path to the image.
* @param null $info Information array to be filled.
* @return \Imagick Returns created image.
* @throws \Exception Throws Exception on invalid filetype - only jpeg, png, and gif are allowed.
*/
private function fetchImage($file, &$info = null)
{
if (is_null($info)) {
$info = $this->getImageInfo($file);
}
switch($info[2]) {
case IMAGETYPE_JPEG:
$image = new \Imagick('jpeg:' . $file);
break;
case IMAGETYPE_PNG:
$image = new \Imagick('png:' . $file);
break;
case IMAGETYPE_GIF:
$image = new \Imagick('gif:' . $file);
break;
default:
throw new \Exception('Invalid filetype!');
}
return $image;
}
protected function getFilename($id, $fileType, $imageType = '')
{
if ($imageType != '') {
$imageType = $imageType . '.';
}
return md5($id) . '.' . $imageType . strtolower($fileType);
}
/**
* Get the path to write converted images to. Ensure directory is writable.
*
* @return string the cache directory to write to
* @throws \RuntimeException if the directory cannot be created or made writable
*/
protected function getCacheDir()
{
$dir = DATA_PATH . '/cache/images';
if (!is_dir($dir)) {
@mkdir($dir, 0700, true);
}
if (!is_writable($dir)) {
@chmod($dir, 0700);
}
if (!is_dir($dir) || !is_writable($dir)) {
throw new \RuntimeException(
"Cannot write to cache directory ('" . $dir . "'). Check permissions."
);
}
return $dir;
}
}