";
}
/**
* Given a Pageimage, return the closest/last cropped version of it
*
* This is so that resizes can use the highest quality version to resize from.
*
* @param Pageimage $image
* @return Pageimage
*
*/
protected function getLastCrop(Pageimage $image) {
$info = $image->isVariation($image->name, true);
if(!$info || !empty($info['crop'])) {
return $image;
}
$cropName = null;
$parentInfo = $info;
while(!empty($parentInfo['parent'])) {
$parentInfo = $parentInfo['parent'];
if(!empty($parentInfo['crop'])) {
$cropName = $parentInfo['name'];
break;
}
}
$original = $image->getOriginal();
if(!$original) return $image;
if(filemtime($original->filename) > filemtime($image->filename)) return $image;
// if no last crop found, return original
if(!$cropName) return $original;
$variations = $original->getVariations();
// return last found crop
$cropImage = $variations->get($cropName);
if($cropImage && filemtime($cropImage->filename) > filemtime($image->filename)) return $image;
return $cropImage ? $cropImage : $original;
}
/**
* Make a image edit URL
*
* @param string $file
* @param array $parts Additional variables you want to add
* @return string
*
*/
protected function makeEditURL($file, $parts = array()) {
$input = $this->wire()->input;
$file = basename($file);
$id = isset($parts['id']) ? (int) $parts['id'] : $this->page->id;
if(!isset($parts['modal'])) $parts['modal'] = 1;
if(!isset($parts['edit_page_id']) && $this->editorPage) $parts['edit_page_id'] = $this->editorPage->id;
if(!isset($parts['hidpi'])) $parts['hidpi'] = (int) $this->hidpi;
if(!isset($parts['original'])) {
if($this->original) {
$parts['original'] = $this->original;
} else {
$parts['original'] = $file;
}
}
if(!isset($parts['class']) && $input->get('class')) {
$class = $input->get('class');
if($class) {
$validClasses = array_merge(
explode(' ', $this->alignLeftClass),
explode(' ', $this->alignCenterClass),
explode(' ', $this->alignRightClass)
);
$classes = array();
foreach(explode(' ', $class) as $c) {
if(empty($c)) continue;
if(in_array($c, $validClasses)) $classes[] = $c;
}
if(count($classes)) $parts['class'] = urlencode(implode(' ' , $classes));
}
}
if(!$this->rte) $parts['rte'] = '0';
if($this->field) $parts['field'] = $this->fieldName;
if(!isset($parts['winwidth'])) {
$winwidth = (int) $input->get('winwidth');
if($winwidth) $parts['winwidth'] = $winwidth;
}
if(!isset($parts['caption']) && $this->rte && $this->caption) {
$parts['caption'] = 1;
}
// @todo should we check for 'class' ?
unset($parts['id'], $parts['file']); // in case they are set here
$url = $this->wire()->config->urls->admin . "page/image/edit/?id=$id&file=$id,$file";
foreach($parts as $key => $value) $url .= "&$key=$value";
return $url;
}
/**
* Check if user has image edit permission and throw exception if they don't
*
* @param bool $throw Specify false if you only want this method to return a true|false rather than throw exception
* @throws WirePermissionException
* @return bool if the $throw argument was false
*
*/
public function checkImageEditPermission($throw = true) {
if(!$this->rte && !$this->wire()->user->hasPermission('page-edit-images', $this->masterPage)) {
if($throw) {
throw new WirePermissionException($this->labels['noAccess']);
} else {
return false;
}
}
return true;
}
/**
* Edit a selected image
*
* Required GET variables:
* - file (string): URL of image, or preferably "123,basename.jpg" where 123 is ID.
* - id (int): ID of page images is from, optional if specified in 'file'.
*
* Optional GET variables:
* - edit_page_id (int): ID of the page being edited, where image will be placed.
* - width (int): Current width of image in the editor
* - height (int): Current height of image in the editor
* - winwidth (int): Current width of client-side window in pixels
* - id (int): ID of the page the image lives on (if omitted, we attempt to determine from 'file' path).
* - class (string): Class name to pass along to the editor (i.e. align_left, align_right, etc.)
* - hidpi (int): Whether hidpi mode is used or not (0=no, 1=yes)
* - link: (string): URL that image is linking to in the editor.
* - description: (string): Text for description "alt" tag.
* - rte (int): Rich text editor mode (0=off, 1 or omit=on)
* - field (string): Name of field (for non-rte mode)
*
* Optional GET variables that must come together as a group. When present, the image editor
* will start in crop mode and show the original image with this portion selected for crop:
* - crop_x (int): Predefined crop left position
* - crop_y (int): Predefined crop top position
* - crop_w (int): Predefined crop width
* - crop_h (int): Predefined crop height
*
* @return string
* @throws \Exception|WireException
*
*/
public function ___executeEdit() {
$input = $this->wire()->input;
$config = $this->wire()->config;
$session = $this->wire()->session;
$sanitizer = $this->wire()->sanitizer;
$adminTheme = $this->wire()->adminTheme;
$checkboxClass = $adminTheme instanceof AdminThemeFramework ? $adminTheme->getClass('input-checkbox') : '';
$radioClass = $adminTheme instanceof AdminThemeFramework ? $adminTheme->getClass('input-radio') : '';
$this->checkImageEditPermission();
if($input->post('submit_crop')) {
$crop = $this->processCrop();
$parts = $this->hidpi ? array('width' => $crop->hidpiWidth()) : array();
$session->location($this->makeEditURL($crop->basename, $parts));
return '';
} else if($input->post('submit_save_replace')) {
return $this->processSave(true);
} else if($input->post('submit_save_copy')) {
return $this->processSave(false);
}
$path = $config->urls('ProcessPageEditImageSelect');
$config->styles->add($path . 'cropper/cropper.min.css');
$config->scripts->add($path . 'cropper/cropper.min.js');
$labels =& $this->labels;
$formClasses = array();
$icons = array(
'top' => 'long-arrow-up',
'left' => 'long-arrow-left',
'width' => 'arrows-h',
'height' => 'arrows-v',
);
foreach($icons as $key => $value) {
$icons[$key] = "";
}
$optionalClasses = array(
$this->alignLeftClass => $labels['alignLeft'],
$this->alignRightClass => $labels['alignRight'],
$this->alignCenterClass => $labels['alignCenter'],
);
$cropX = null;
$cropY = null;
$cropWidth = null;
$cropHeight = null;
$isCropped = preg_match_all('/\.(\d+)x(\d+).*?-crop[xy](\d+)[xy](\d+)[-.]/', $input->get('file'), $matches);
if($isCropped) { // get last coordinates present
$cropWidth = (int) array_pop($matches[1]);
$cropHeight = (int) array_pop($matches[2]);
$cropX = (int) array_pop($matches[3]);
$cropY = (int) array_pop($matches[4]);
}
$isCroppable = !$isCropped;
try {
$original = $this->getPageimage(false);
} catch(\Exception $e) {
if($this->rte) {
// when RTE mode, go to image selection again when invalid image
$this->error($e->getMessage());
$session->location("./?id={$this->page->id}");
return '';
} else {
throw $e;
}
}
if($isCropped) {
$image = $this->getPageimage(true);
$recropURL = $this->makeEditURL($original->basename, array(
'crop_x' => $cropX,
'crop_y' => $cropY,
'crop_w' => $cropWidth,
'crop_h' => $cropHeight
));
} else {
$recropURL = "";
$image = $original;
}
if(!is_file($image->filename)) throw new WireException("Image file does not exist");
if($this->field) {
$found = false;
if($this->field->type instanceof FieldtypeImage) {
$pageimages = $this->page->get($this->field->name);
if($pageimages && $pageimages->get($original->name)) $found = true;
} else if(method_exists($this->field->type, 'getPageimages')) {
/** @var FieldtypeHasPageimages $fieldtype */
$fieldtype = $this->field->type;
$pageimages = $fieldtype->getPageimages($this->page, $this->field);
foreach($pageimages as $pageimage) {
if($pageimage->name === $original->name) $found = true;
if($found) break;
}
}
if(!$found) {
throw new WireException("Please save the page before editing newly uploaded images.");
}
}
$basename = $image->basename;
$fullname = $image->page->id . ',' . $basename; // "123,basename.jpg"
$originalWidth = $image->width();
$originalHeight = $image->height();
// attributes for #selected_image
$attrs = array(
'class' => '',
'width' => 0,
'height' => 0,
'data-origwidth' => $originalWidth,
'data-origheight' => $originalHeight,
'data-nosize' => ($this->noSizeAttrs ? '1' : ''),
);
if(!$this->rte && $this->field) {
$minWidth = $this->field->get('minWidth') ? (int) $this->field->get('minWidth') : 1;
$minHeight = $this->field->get('minHeight') ? (int) $this->field->get('minHeight') : 1;
} else {
$minWidth = 1;
$minHeight = 1;
}
// bundle in crop information to image attributes, if specified
if($input->get('crop_x')) {
$attrs['data-crop'] =
((int) $input->get('crop_x')) . ',' .
((int) $input->get('crop_y')) . ',' .
((int) $input->get('crop_w')) . ',' .
((int) $input->get('crop_h'));
}
// determine width/height we will display image at in editor, so that it fits
$width = (int) $input->get('width');
$height = (int) $input->get('height');
if(!$width) $width = $this->editWidth;
if(!$width) $width = '';
if(!$height) $height = $this->editHeight;
if(!$height) $height = '';
// if width not specified in edit URL and image exceeds the window width, then scale to fit window
if(!$width) {
$width = $image->width;
}
$attrs['width'] = $width;
$attrs['data-fit'] = '1';
$this->wire('processBrowserTitle', $basename);
// if they aren't already working with a resized image, and it's being scaled down,
// then add the 'resized' class to ensure that our RTE 'pwimage' plugin knows to perform the resize
// by checking for the 'resized' classname on the image
if(basename($input->get('file')) == $fullname && $originalWidth > $width) $attrs['class'] .= " resized";
// if hidpi specified keep it checed
$hidpiChecked = $this->hidpi ? " checked='checked'" : "";
$captionChecked = $this->caption ? " checked='checked'" : "";
// alignment class options
$classOptions = '';
foreach($optionalClasses as $class => $label) {
$labelKey = array_search($label, $labels);
$selected = strpos((string) $input->get('class'), $class) !== false ? " selected='selected'" : '';
if($selected) $attrs['class'] .= " $class";
$classOptions .= "";
}
// convert $attrs to an attribute string for placement in #selected_image markup
$attrStr = '';
foreach($attrs as $key => $value) {
if(!$value && $key != 'data-nosize') continue; // skip most empty attributes
$attrStr .= "$key='" . trim($value) . "' ";
}
// prepare description (alt)
$description = isset($_GET['description']) ? $input->get('description') : ''; // $image->description;
if(strlen($description) > 8192) $description = substr($description, 0, 8192);
$description = $sanitizer->entities($description);
// if dealing with a variation size or crop provide the option to link to the original (larger)
$linkOriginalChecked = '';
if($image->name != $original->name || $input->get('link')) {
if($image->name != $original->name) $formClasses[] = 'not-original';
$imgURL = str_replace($config->urls->root, '/', $original->url);
$imgLinkURL = trim((string) $input->get('link'));
if($imgLinkURL) {
if($imgLinkURL === "1") $imgLinkURL = $imgURL; // if in toggle mode, substitute URL
// determine if 'link to original' checkbox should be checked
$test = substr($imgLinkURL, -1 * strlen($imgURL));
$linkOriginalChecked = $test == $imgURL ? " checked='checked' data-was-checked='1'" : "";
unset($imgLinkURL);
}
} else {
$formClasses[] = 'original';
}
$resizeYesChecked = $this->rte ? " checked='checked'" : "";
$resizeNoChecked = !$this->rte ? " checked='checked'" : "";
if($this->field && $this->field->get('maxFiles') == 1) $formClasses[] = 'maxfiles1';
// form attributes
$formClasses[] = $isCroppable ? 'croppable' : 'not-croppable';
$formClasses[] = $this->rte ? 'rte' : 'not-rte';
$formClass = implode(' ', $formClasses);
$formActionURL = $this->makeEditURL($basename);
$originalDimension = $originalWidth . 'x' . $originalHeight;
$imageURL = $this->rte ? $image->url : $image->URL;
// form header and inputs
// @todo move all this markup to a separate template file
$out =
"";
// if($this->wire('config')->debug) $out .= "
$basename
";
return $out;
}
/**
* Get a bunch of latin text
*
* @param int $n How many blocks of latin to get (default=1)
* @return string
*
*/
protected function getLatin($n = 1) {
$latin = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In malesuada magna ex. Donec sed consectetur felis,
ac molestie massa. Duis fermentum rutrum vehicula. Praesent consequat efficitur dolor quis vestibulum.
Sed vulputate, nisi et efficitur semper, erat odio scelerisque massa, nec convallis leo ipsum ac eros. Donec a
augue eleifend, pellentesque sapien a, tristique orci. Sed ac leo vel libero iaculis facilisis in at mauris.
Etiam vitae mi cursus nisl gravida vulputate. Nam in magna risus. Nullam vel suscipit lacus. Ut sit amet malesuada
massa. Etiam rhoncus, tellus vel porta dignissim, elit massa cursus tortor, vitae fermentum dolor augue ac tortor.
Aliquam eleifend mi at dictum pulvinar. Sed bibendum dolor non mi placerat, euismod euismod est commodo.
Vivamus ultrices orci arcu, sed pulvinar tellus molestie quis. Quisque pharetra velit a metus mattis, vitae
fermentum orci hendrerit. Fusce id libero ac urna ultrices consequat. Donec lorem lacus, dignissim vitae faucibus
eget, tincidunt sit amet felis. Aenean tempor quis sapien vitae euismod. Donec mattis mi at sem egestas interdum.
Integer sed diam finibus, volutpat lectus ac, ornare dui. Suspendisse potenti. Suspendisse sagittis interdum
elit sodales commodo. Maecenas porttitor mi sit amet velit porta, sit amet eleifend massa malesuada. Phasellus
luctus nibh id sem venenatis, dictum fermentum turpis hendrerit. Nam tempor, ex eu sagittis gravida, mi lorem
maximus ex, aliquam commodo nunc leo et lacus. Nunc vel faucibus lacus, non pellentesque elit. Nulla non rutrum
magna, nec porta augue. ";
$out = '';
for($c = 1; $c <= $n; $c++) $out .= $latin;
return $out;
}
/**
* Process an image crop from submitted POST vars
*
* @return Pageimage
* @throws WireException
*
*/
protected function processCrop() {
$input = $this->wire()->input;
$cropX = (int) $input->post('crop_x');
$cropY = (int) $input->post('crop_y');
$cropW = (int) $input->post('crop_w');
$cropH = (int) $input->post('crop_h');
$image = $this->getPageimage();
if(!$image) throw new WireException("Unable to load image");
$suffix = $this->rte ? array('is') : array();
$options = array('cleanFilename' => true, 'suffix' => $suffix);
$crop = $image->crop($cropX, $cropY, $cropW, $cropH, $options);
return $crop;
}
/**
* Save resized image and existing eisting or create a copy
*
* @param bool $replace Replace existing image? (default=false)
* @return string Returns status output which may appear momentarily while dialog updating
* @throws WireException
*
*/
protected function processSave($replace = false) {
if(!$this->field) throw new WireException("Field is not defined");
if(!$this->original) throw new WireException("Original is not set");
if($this->page->hasStatus(Page::statusLocked)) throw new WireException("Page is locked for edits");
$pages = $this->wire()->pages;
$input = $this->wire()->input;
$fieldtype = $this->field->type;
$image = $this->getPageimage(false);
if($input->get('file') != "$this->page,$image->name") $image = $this->getPageimage(true);
$width = (int) $input->post('width');
if(!$width) throw new WireException("Width not specified");
$rebuildVariations = preg_match('/-cropx\d+y\d+/', $image->name);
$useResize = ((int) $input->post('use_resize')) == 1;
// image2 = resized version
if($useResize) {
$image2 = $image->width($width);
if(!$image2 || !$image2->width) throw new WireException('Unable to complete resize');
} else {
$image2 = $image;
}
/** @var Pageimages $pageimages */
if($fieldtype instanceof FieldtypeImage) {
$pageimages = $this->page->getUnformatted($this->field->name);
} else {
$pageimages = $image->pagefiles;
}
$path = $pageimages->path();
$fileID = '';
$isNew = 0;
$body = '';
if($replace) {
// replace original image
if($this->original && $this->original != $image2->basename() && $original = $image->pagefiles->get($this->original)) {
/** @var Pageimage $original */
$fileID = 'file_' . $original->hash;
if($rebuildVariations && $this->field->get('adminThumbs')) {
// remove original thumbnail
/** @var InputfieldImage $inputfield */
$inputfield = $this->field->getInputfield($this->page);
if($inputfield) {
$thumb = $inputfield->getAdminThumb($original);
$thumb = $thumb['thumb'];
if($thumb->url != $original->url) {
// there is a thumbnail, distinct from the original image
$this->wire()->files->unlink($thumb->filename);
}
}
}
// replace original image
if($original->replaceFile($image2->filename())) {
$original->modified = time();
$original->modifiedUser = $this->wire()->user;
/** @var FieldtypeFile $fieldtype */
if($fieldtype instanceof FieldtypeFile) {
$fieldtype->saveFileCols($this->page, $this->field, $original, array(
'filesize' => $original->filesize(),
'modified' => $original->modified,
'modified_users_id' => $original->modified_users_id,
'width' => $original->width(),
'height' => $original->height(),
'ratio' => $original->ratio(),
));
} else {
$this->page->trackChange($this->field->name);
}
}
$pages->uncacheAll();
$page = $pages->get($this->page->id);
/** @var Pageimages $value */
$value = $page->getUnformatted($this->field->name);
if(!$value instanceof Pageimages) $value = $pageimages;
if($rebuildVariations) {
/** @var Pageimage $finalImage */
$finalImage = $value->get($this->original);
$variationInfo = $finalImage->rebuildVariations(0, array('is', 'hidpi'));
foreach($variationInfo as $type => $files) {
if(!wireCount($files)) continue;
$body .= "