Merge pull request #5979 from gijsmartens/ticket/15769

[ticket/15769] Crop avatar on upload
This commit is contained in:
Marc Alexander 2022-12-27 22:39:38 +01:00 committed by GitHub
commit 8faabb559d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 4647 additions and 21 deletions

View file

@ -14,13 +14,18 @@
"phpBB/adm/style/permissions.js", "phpBB/adm/style/permissions.js",
"phpBB/adm/style/tooltip.js", "phpBB/adm/style/tooltip.js",
"phpBB/assets/javascript/core.js", "phpBB/assets/javascript/core.js",
"phpBB/assets/javascript/cropper.js",
"phpBB/assets/javascript/editor.js", "phpBB/assets/javascript/editor.js",
"phpBB/assets/javascript/hermite.js",
"phpBB/assets/javascript/installer.js", "phpBB/assets/javascript/installer.js",
"phpBB/assets/javascript/jquery-cropper.js",
"phpBB/assets/javascript/plupload.js", "phpBB/assets/javascript/plupload.js",
"phpBB/ext/**/*.js",
"phpBB/styles/prosilver/template/ajax.js", "phpBB/styles/prosilver/template/ajax.js",
"phpBB/styles/prosilver/template/forum_fn.js", "phpBB/styles/prosilver/template/forum_fn.js",
"phpBB/**/*.min.js", "phpBB/**/*.min.js",
"phpBB/vendor/**/*.js", "phpBB/vendor/**/*.js",
"phpBB/vendor-ext/**/*.js",
"phpBB/phpbb/**/*.js", "phpBB/phpbb/**/*.js",
"phpBB/tests/**/*.js" "phpBB/tests/**/*.js"
], ],
@ -40,6 +45,10 @@
], ],
"multiline-comment-style": "off", "multiline-comment-style": "off",
"computed-property-spacing": "off", "computed-property-spacing": "off",
"space-before-function-paren": [
"error",
"never"
],
"space-in-parens": "off", "space-in-parens": "off",
"capitalized-comments": "off", "capitalized-comments": "off",
"object-curly-spacing": [ "object-curly-spacing": [

View file

@ -2,3 +2,68 @@
<dt><label for="avatar_upload_file">{L_UPLOAD_AVATAR_FILE}{L_COLON}</label></dt> <dt><label for="avatar_upload_file">{L_UPLOAD_AVATAR_FILE}{L_COLON}</label></dt>
<dd><input type="hidden" name="MAX_FILE_SIZE" value="{AVATAR_UPLOAD_SIZE}" /><input type="file" name="avatar_upload_file" id="avatar_upload_file" class="inputbox autowidth" accept="{{ AVATAR_ALLOWED_EXTENSIONS }}" /></dd> <dd><input type="hidden" name="MAX_FILE_SIZE" value="{AVATAR_UPLOAD_SIZE}" /><input type="file" name="avatar_upload_file" id="avatar_upload_file" class="inputbox autowidth" accept="{{ AVATAR_ALLOWED_EXTENSIONS }}" /></dd>
</dl> </dl>
{% INCLUDECSS T_ASSETS_PATH ~ '/css/cropper.min.css' %}
{% INCLUDEJS T_ASSETS_PATH ~ '/javascript/cropper.min.js' %}
{% INCLUDEJS T_ASSETS_PATH ~ '/javascript/jquery-cropper.js' %}
{% INCLUDEJS T_ASSETS_PATH ~ '/javascript/phpbb-avatars.js' %}
<input type="hidden" id="avatar-cropper-data" name="avatar_cropper_data" value=""
data-min-width="{{ AVATAR_MIN_WIDTH }}" data-max-width="{{ AVATAR_MAX_WIDTH }}"
data-min-height="{{ AVATAR_MIN_HEIGHT }}" data-max-height="{{ AVATAR_MAX_HEIGHT }}"
/>
{% apply spaceless %}
<div class="avatar-cropper-buttons" id="avatar-cropper-buttons">
<div class="button-group">
<button class="button" type="button" title="{{ lang('ZOOM_IN') }}" data-cropper-action="zoom,0.1">
<i class="icon fa-search-plus fa-fw"></i>
</button>
<button class="button" type="button" title="{{ lang('ZOOM_OUT') }}" data-cropper-action="zoom,-0.1">
<i class="icon fa-search-minus fa-fw"></i>
</button>
</div>
<div class="button-group">
<button class="button" type="button" title="{{ lang('MOVE_LEFT') }}" data-cropper-action="move,-10,0">
<i class="icon fa-arrow-left fa-fw"></i>
</button>
<button class="button" type="button" title="{{ lang('MOVE_RIGHT') }}" data-cropper-action="move,10,0">
<i class="icon fa-arrow-right fa-fw"></i>
</button>
<button class="button" type="button" title="{{ lang('MOVE_UP') }}" data-cropper-action="move,0,-10">
<i class="icon fa-arrow-up fa-fw"></i>
</button>
<button class="button" type="button" title="{{ lang('MOVE_DOWN') }}" data-cropper-action="move,0,10">
<i class="icon fa-arrow-down fa-fw"></i>
</button>
</div>
<div class="button-group">
<button class="button" type="button" title="{{ lang('ROTATE_LEFT') }}" data-cropper-action="rotate,-90">
<i class="icon fa-rotate-left fa-fw"></i>
</button>
<button class="button" type="button" title="{{ lang('ROTATE_RIGHT') }}" data-cropper-action="rotate,90">
<i class="icon fa-rotate-right fa-fw"></i>
</button>
</div>
<div class="button-group">
<button class="button" type="button" title="{{ lang('FLIP_HORIZONTALLY') }}" data-cropper-action="scaleX">
<i class="icon fa-arrows-h fa-fw"></i>
</button>
<button class="button" type="button" title="{{ lang('FLIP_VERTICALLY') }}" data-cropper-action="scaleY">
<i class="icon fa-arrows-v fa-fw"></i>
</button>
</div>
<div class="button-group">
<button class="button" type="button" title="{{ lang('RESET') }}" data-cropper-action="reset">
<i class="icon fa-refresh fa-fw"></i>
</button>
<button class="button" type="button" title="{{ lang('CLEAR') }}" data-cropper-action="clear">
<i class="icon fa-times fa-fw"></i>
</button>
</div>
</div>
{% endapply %}

View file

@ -113,7 +113,7 @@
<legend>{L_GROUP_AVATAR}</legend> <legend>{L_GROUP_AVATAR}</legend>
<dl> <dl>
<dt><label>{L_CURRENT_IMAGE}{L_COLON}</label><br /><span>{L_AVATAR_EXPLAIN}</span></dt> <dt><label>{L_CURRENT_IMAGE}{L_COLON}</label><br /><span>{L_AVATAR_EXPLAIN}</span></dt>
<dd>{% if AVATAR_HTML %}{{ AVATAR_HTML }}{% else %}<img src="{{ ADMIN_ROOT_PATH ~ 'images/no_avatar.gif' }}" alt="">{% endif %}</dd> <dd class="c-avatar-box">{% if AVATAR_HTML %}{{ AVATAR_HTML }}{% else %}<img src="{{ ADMIN_ROOT_PATH ~ 'images/no_avatar.gif' }}" alt="">{% endif %}</dd>
<dd><label for="avatar_delete"><input type="checkbox" name="avatar_delete" id="avatar_delete" /> {L_DELETE_AVATAR}</label></dd> <dd><label for="avatar_delete"><input type="checkbox" name="avatar_delete" id="avatar_delete" /> {L_DELETE_AVATAR}</label></dd>
</dl> </dl>
<dl> <dl>

View file

@ -5,7 +5,7 @@
<!-- IF ERROR --><p class="error">{ERROR}</p><!-- ENDIF --> <!-- IF ERROR --><p class="error">{ERROR}</p><!-- ENDIF -->
<dl> <dl>
<dt><label>{L_CURRENT_IMAGE}{L_COLON}</label><br /><span>{L_AVATAR_EXPLAIN}</span></dt> <dt><label>{L_CURRENT_IMAGE}{L_COLON}</label><br /><span>{L_AVATAR_EXPLAIN}</span></dt>
<dd>{% if AVATAR_HTML %}{{ AVATAR_HTML }}{% else %}<img src="{{ ADMIN_ROOT_PATH ~ 'images/no_avatar.gif' }}" alt="">{% endif %}</dd> <dd class="c-avatar-box">{% if AVATAR_HTML %}{{ AVATAR_HTML }}{% else %}<img src="{{ ADMIN_ROOT_PATH ~ 'images/no_avatar.gif' }}" alt="">{% endif %}</dd>
<dd><label for="avatar_delete"><input type="checkbox" name="avatar_delete" id="avatar_delete" /> {L_DELETE_AVATAR}</label></dd> <dd><label for="avatar_delete"><input type="checkbox" name="avatar_delete" id="avatar_delete" /> {L_DELETE_AVATAR}</label></dd>
</dl> </dl>
</fieldset> </fieldset>

View file

@ -1708,6 +1708,7 @@ input.autowidth {
/* Form button styles /* Form button styles
---------------------------------------- */ ---------------------------------------- */
.button,
a.button1, a.button1,
input.button1, input.button1,
a.button2, a.button2,
@ -1721,6 +1722,7 @@ input.button2 {
cursor: pointer; cursor: pointer;
} }
.button,
a.button1, a.button1,
input.button1 { input.button1 {
font-weight: bold; font-weight: bold;
@ -1734,6 +1736,10 @@ input.button2 {
} }
/* <a> button in the style of the form buttons */ /* <a> button in the style of the form buttons */
.button,
.button:link,
.button:visited,
.button:active,
a.button1, a.button1,
a.button1:link, a.button1:link,
a.button1:visited, a.button1:visited,
@ -1748,6 +1754,7 @@ a.button2:active {
} }
/* Hover states */ /* Hover states */
.button:hover,
a.button1:hover, a.button1:hover,
input.button1:hover, input.button1:hover,
a.button2:hover, a.button2:hover,
@ -1768,6 +1775,37 @@ input.button2:focus {
outline-style: none; outline-style: none;
} }
/* Avatar cropper */
.avatar-cropper-buttons {
text-align: center;
display: none;
}
/** Button groups */
.button-group {
display: inline-block;
}
.button-group + .button-group {
margin-left: 8px;
}
.button-group > .button:first-child {
border-radius: 4px 0 0 4px;
}
.button-group > .button:last-child {
border-radius: 0 4px 4px 0;
}
.button-group > .button:not(:first-child):not(:last-child) {
border-radius: 0;
}
.avatar-cropper-buttons > .button-group {
margin: 4px;
}
/* jQuery popups /* jQuery popups
---------------------------------------- */ ---------------------------------------- */
.phpbb_alert { .phpbb_alert {

View file

@ -0,0 +1,304 @@
/*!
* Cropper.js v1.5.11
* https://fengyuanchen.github.io/cropperjs
*
* Copyright 2015-present Chen Fengyuan
* Released under the MIT license
*
* Date: 2021-02-17T11:53:21.992Z
*/
.cropper-container {
direction: ltr;
font-size: 0;
line-height: 0;
position: relative;
-ms-touch-action: none;
touch-action: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.cropper-container img {
display: block;
height: 100%;
image-orientation: 0deg;
max-height: none !important;
max-width: none !important;
min-height: 0 !important;
min-width: 0 !important;
width: 100%;
}
.cropper-wrap-box,
.cropper-canvas,
.cropper-drag-box,
.cropper-crop-box,
.cropper-modal {
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
}
.cropper-wrap-box,
.cropper-canvas {
overflow: hidden;
}
.cropper-drag-box {
background-color: #fff;
opacity: 0;
}
.cropper-modal {
background-color: #000;
opacity: 0.5;
}
.cropper-view-box {
display: block;
height: 100%;
outline: 1px solid #39f;
outline-color: rgba(51, 153, 255, 0.75);
overflow: hidden;
width: 100%;
}
.cropper-dashed {
border: 0 dashed #eee;
display: block;
opacity: 0.5;
position: absolute;
}
.cropper-dashed.dashed-h {
border-bottom-width: 1px;
border-top-width: 1px;
height: calc(100% / 3);
left: 0;
top: calc(100% / 3);
width: 100%;
}
.cropper-dashed.dashed-v {
border-left-width: 1px;
border-right-width: 1px;
height: 100%;
left: calc(100% / 3);
top: 0;
width: calc(100% / 3);
}
.cropper-center {
display: block;
height: 0;
left: 50%;
opacity: 0.75;
position: absolute;
top: 50%;
width: 0;
}
.cropper-center::before,
.cropper-center::after {
background-color: #eee;
content: ' ';
display: block;
position: absolute;
}
.cropper-center::before {
height: 1px;
left: -3px;
top: 0;
width: 7px;
}
.cropper-center::after {
height: 7px;
left: 0;
top: -3px;
width: 1px;
}
.cropper-face,
.cropper-line,
.cropper-point {
display: block;
height: 100%;
opacity: 0.1;
position: absolute;
width: 100%;
}
.cropper-face {
background-color: #fff;
left: 0;
top: 0;
}
.cropper-line {
background-color: #39f;
}
.cropper-line.line-e {
cursor: ew-resize;
right: -3px;
top: 0;
width: 5px;
}
.cropper-line.line-n {
cursor: ns-resize;
height: 5px;
left: 0;
top: -3px;
}
.cropper-line.line-w {
cursor: ew-resize;
left: -3px;
top: 0;
width: 5px;
}
.cropper-line.line-s {
bottom: -3px;
cursor: ns-resize;
height: 5px;
left: 0;
}
.cropper-point {
background-color: #39f;
height: 5px;
opacity: 0.75;
width: 5px;
}
.cropper-point.point-e {
cursor: ew-resize;
margin-top: -3px;
right: -3px;
top: 50%;
}
.cropper-point.point-n {
cursor: ns-resize;
left: 50%;
margin-left: -3px;
top: -3px;
}
.cropper-point.point-w {
cursor: ew-resize;
left: -3px;
margin-top: -3px;
top: 50%;
}
.cropper-point.point-s {
bottom: -3px;
cursor: s-resize;
left: 50%;
margin-left: -3px;
}
.cropper-point.point-ne {
cursor: nesw-resize;
right: -3px;
top: -3px;
}
.cropper-point.point-nw {
cursor: nwse-resize;
left: -3px;
top: -3px;
}
.cropper-point.point-sw {
bottom: -3px;
cursor: nesw-resize;
left: -3px;
}
.cropper-point.point-se {
bottom: -3px;
cursor: nwse-resize;
height: 20px;
opacity: 1;
right: -3px;
width: 20px;
}
@media (min-width: 768px) {
.cropper-point.point-se {
height: 15px;
width: 15px;
}
}
@media (min-width: 992px) {
.cropper-point.point-se {
height: 10px;
width: 10px;
}
}
@media (min-width: 1200px) {
.cropper-point.point-se {
height: 5px;
opacity: 0.75;
width: 5px;
}
}
.cropper-point.point-se::before {
background-color: #39f;
bottom: -50%;
content: ' ';
display: block;
height: 200%;
opacity: 0;
position: absolute;
right: -50%;
width: 200%;
}
.cropper-invisible {
opacity: 0;
}
.cropper-bg {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');
}
.cropper-hide {
display: block;
height: 0;
position: absolute;
width: 0;
}
.cropper-hidden {
display: none !important;
}
.cropper-move {
cursor: move;
}
.cropper-crop {
cursor: crosshair;
}
.cropper-disabled .cropper-drag-box,
.cropper-disabled .cropper-face,
.cropper-disabled .cropper-line,
.cropper-disabled .cropper-point {
cursor: not-allowed;
}

9
phpBB/assets/css/cropper.min.css vendored Normal file
View file

@ -0,0 +1,9 @@
/*!
* Cropper.js v1.5.11
* https://fengyuanchen.github.io/cropperjs
*
* Copyright 2015-present Chen Fengyuan
* Released under the MIT license
*
* Date: 2021-02-17T11:53:21.992Z
*/.cropper-container{direction:ltr;font-size:0;line-height:0;position:relative;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.cropper-container img{display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-canvas,.cropper-crop-box,.cropper-drag-box,.cropper-modal,.cropper-wrap-box{bottom:0;left:0;position:absolute;right:0;top:0}.cropper-canvas,.cropper-wrap-box{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:rgba(51,153,255,.75);overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:33.33333%;left:0;top:33.33333%;width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:33.33333%;top:0;width:33.33333%}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:after,.cropper-center:before{background-color:#eee;content:" ";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}@media (min-width:768px){.cropper-point.point-se{height:15px;width:15px}}@media (min-width:992px){.cropper-point.point-se{height:10px;width:10px}}@media (min-width:1200px){.cropper-point.point-se{height:5px;opacity:.75;width:5px}}.cropper-point.point-se:before{background-color:#39f;bottom:-50%;content:" ";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%}.cropper-invisible{opacity:0}.cropper-bg{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC")}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed}

File diff suppressed because it is too large Load diff

10
phpBB/assets/javascript/cropper.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,7 @@
/**
* hermite-resize - Canvas image resize/resample using Hermite filter with JavaScript.
* @version v2.2.10
* @link https://github.com/viliusle/miniPaint
* @license MIT
*/
function Hermite_class(){var w,p,_=[];this.init=void(w=navigator.hardwareConcurrency||4),this.getCores=function(){return w},this.resample_auto=function(t,a,e,r,h){var i=this.getCores();window.Worker&&1<i?this.resample(t,a,e,r,h):(this.resample_single(t,a,e,!0),null!=h&&h())},this.resize_image=function(t,a,e,r,h){var i=document.getElementById(t),n=document.createElement("canvas");if(n.width=i.width,n.height=i.height,n.getContext("2d").drawImage(i,0,0),null==a&&null==e&&null!=r&&(a=i.width/100*r,e=i.height/100*r),null==e){var o=i.width/a;e=i.height/o}a=Math.round(a),e=Math.round(e);function l(){var t=n.toDataURL();i.width=a,i.height=e,i.src=t,n=t=null}null==h||1==h?this.resample(n,a,e,!0,l):(this.resample_single(n,a,e,!0),l())},this.resample=function(t,r,a,e,h){var i=t.width,n=t.height;r=Math.round(r);var o=n/(a=Math.round(a));if(0<_.length)for(var l=0;l<w;l++)null!=_[l]&&(_[l].terminate(),delete _[l]);_=new Array(w);var s=t.getContext("2d"),c=[],g=2*Math.ceil(n/w/2),d=-1;for(l=0;l<w;l++){var u=d+1;if(!(n<=u)){d=u+g-1,d=Math.min(d,n-1);var f;f=Math.min(g,n-u),c[l]={},c[l].source=s.getImageData(0,u,i,g),c[l].target=!0,c[l].start_y=Math.ceil(u/o),c[l].height=f}}!0===e?(t.width=r,t.height=a):s.clearRect(0,0,i,n);var M=0;for(l=0;l<w;l++)if(null!=c[l]){M++;var v=new Worker(p);(_[l]=v).onmessage=function(t){M--;var a=t.data.core;_[a].terminate(),delete _[a];var e=Math.ceil(c[a].height/o);c[a].target=s.createImageData(r,e),c[a].target.data.set(t.data.target),s.putImageData(c[a].target,0,c[a].start_y),M<=0&&null!=h&&h()};var m={width_source:i,height_source:c[l].height,width:r,height:Math.ceil(c[l].height/o),core:l,source:c[l].source.data.buffer};v.postMessage(m,[m.source])}},p=window.URL.createObjectURL(new Blob(["(",function(){onmessage=function(t){for(var a=t.data.core,e=t.data.width_source,r=t.data.height_source,h=t.data.width,i=t.data.height,n=e/h,o=r/i,l=Math.ceil(n/2),s=Math.ceil(o/2),c=new Uint8ClampedArray(t.data.source),g=(c.length,h*i*4),d=new ArrayBuffer(g),u=new Uint8ClampedArray(d,0,g),f=0;f<i;f++)for(var M=0;M<h;M++){var v=4*(M+f*h),m=0,w=0,p=0,_=0,y=0,b=0,C=0,I=f*o,D=Math.floor(M*n),R=Math.ceil((M+1)*n),U=Math.floor(f*o),A=Math.ceil((f+1)*o);R=Math.min(R,e),A=Math.min(A,r);for(var x=U;x<A;x++)for(var B=Math.abs(I-x)/s,L=M*n,j=B*B,k=D;k<R;k++){var q=Math.abs(L-k)/l,E=Math.sqrt(j+q*q);if(!(1<=E)){var W=4*(k+x*e);C+=(m=2*E*E*E-3*E*E+1)*c[3+W],p+=m,c[3+W]<255&&(m=m*c[3+W]/250),_+=m*c[W],y+=m*c[1+W],b+=m*c[2+W],w+=m}}u[v]=_/w,u[1+v]=y/w,u[2+v]=b/w,u[3+v]=C/p}postMessage({core:a,target:u},[u.buffer])}}.toString(),")()"],{type:"application/javascript"})),this.resample_single=function(t,a,e,r){for(var h=t.width,i=t.height,n=h/(a=Math.round(a)),o=i/(e=Math.round(e)),l=Math.ceil(n/2),s=Math.ceil(o/2),c=t.getContext("2d"),g=c.getImageData(0,0,h,i),d=c.createImageData(a,e),u=g.data,f=d.data,M=0;M<e;M++)for(var v=0;v<a;v++){var m=4*(v+M*a),w=0,p=0,_=0,y=0,b=0,C=0,I=0,D=M*o,R=Math.floor(v*n),U=Math.ceil((v+1)*n),A=Math.floor(M*o),x=Math.ceil((M+1)*o);U=Math.min(U,h),x=Math.min(x,i);for(var B=A;B<x;B++)for(var L=Math.abs(D-B)/s,j=v*n,k=L*L,q=R;q<U;q++){var E=Math.abs(j-q)/l,W=Math.sqrt(k+E*E);if(!(1<=W)){var z=4*(q+B*h);I+=(w=2*W*W*W-3*W*W+1)*u[3+z],_+=w,u[3+z]<255&&(w=w*u[3+z]/250),y+=w*u[z],b+=w*u[1+z],C+=w*u[2+z],p+=w}}f[m]=y/p,f[1+m]=b/p,f[2+m]=C/p,f[3+m]=I/_}!0===r?(t.width=a,t.height=e):c.clearRect(0,0,h,i),c.putImageData(d,0,0)}}

View file

@ -0,0 +1,73 @@
/*!
* jQuery Cropper v1.0.1
* https://fengyuanchen.github.io/jquery-cropper
*
* Copyright 2018-present Chen Fengyuan
* Released under the MIT license
*
* Date: 2019-10-19T08:48:33.062Z
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('jquery'), require('cropperjs')) :
typeof define === 'function' && define.amd ? define(['jquery', 'cropperjs'], factory) :
(global = global || self, factory(global.jQuery, global.Cropper));
}(this, function ($, Cropper) { 'use strict';
$ = $ && $.hasOwnProperty('default') ? $['default'] : $;
Cropper = Cropper && Cropper.hasOwnProperty('default') ? Cropper['default'] : Cropper;
if ($ && $.fn && Cropper) {
var AnotherCropper = $.fn.cropper;
var NAMESPACE = 'cropper';
$.fn.cropper = function jQueryCropper(option) {
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
var result;
this.each(function (i, element) {
var $element = $(element);
var isDestroy = option === 'destroy';
var cropper = $element.data(NAMESPACE);
if (!cropper) {
if (isDestroy) {
return;
}
var options = $.extend({}, $element.data(), $.isPlainObject(option) && option);
cropper = new Cropper(element, options);
$element.data(NAMESPACE, cropper);
}
if (typeof option === 'string') {
var fn = cropper[option];
if ($.isFunction(fn)) {
result = fn.apply(cropper, args);
if (result === cropper) {
result = undefined;
}
if (isDestroy) {
$element.removeData(NAMESPACE);
}
}
}
});
return result !== undefined ? result : this;
};
$.fn.cropper.Constructor = Cropper;
$.fn.cropper.setDefaults = Cropper.setDefaults;
$.fn.cropper.noConflict = function noConflict() {
$.fn.cropper = AnotherCropper;
return this;
};
}
}));

View file

@ -0,0 +1,10 @@
/*!
* jQuery Cropper v1.0.1
* https://fengyuanchen.github.io/jquery-cropper
*
* Copyright 2018-present Chen Fengyuan
* Released under the MIT license
*
* Date: 2019-10-19T08:48:33.062Z
*/
!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(require("jquery"),require("cropperjs")):"function"==typeof define&&define.amd?define(["jquery","cropperjs"],r):r((e=e||self).jQuery,e.Cropper)}(this,function(c,s){"use strict";if(c=c&&c.hasOwnProperty("default")?c.default:c,s=s&&s.hasOwnProperty("default")?s.default:s,c&&c.fn&&s){var e=c.fn.cropper,d="cropper";c.fn.cropper=function(p){for(var e=arguments.length,a=new Array(1<e?e-1:0),r=1;r<e;r++)a[r-1]=arguments[r];var u;return this.each(function(e,r){var t=c(r),n="destroy"===p,o=t.data(d);if(!o){if(n)return;var f=c.extend({},t.data(),c.isPlainObject(p)&&p);o=new s(r,f),t.data(d,o)}if("string"==typeof p){var i=o[p];c.isFunction(i)&&((u=i.apply(o,a))===o&&(u=void 0),n&&t.removeData(d))}}),void 0!==u?u:this},c.fn.cropper.Constructor=s,c.fn.cropper.setDefaults=s.setDefaults,c.fn.cropper.noConflict=function(){return c.fn.cropper=e,this}}});

View file

@ -0,0 +1,281 @@
/* global phpbb */
(function($) { // Avoid conflicts with other libraries
'use strict';
/**
* phpBB Avatars namespace.
*
* Handles cropping for local file uploads.
*/
phpbb.avatars = {
cropper: null,
image: null,
/** @type {jQuery} */
$form: null,
/** @type {jQuery} */
$buttons: $('#avatar-cropper-buttons'),
/** @type {jQuery} */
$box: $('.c-avatar-box'),
/** @type {jQuery} */
$data: $('#avatar-cropper-data'),
/** @type {jQuery} */
$input: $('#avatar_upload_file'),
/** @type {jQuery} */
$driver: $('#avatar_driver'),
/** @type {string} */
driverUpload: 'avatar_driver_upload',
/**
* Initialise avatar cropping.
*/
init() {
// If the cropper library is not available
if (!$.isFunction($.fn.cropper)) {
return;
}
// Correctly position the cropper buttons
this.$buttons.appendTo(this.$box);
// Ensure we have an img for the cropping
if (this.$box.children('img').length === 0) {
const $avatarImg = $('<img src="" alt="">');
$avatarImg.setAttribute('width', phpbb.avatars.$data.data().maxWidth);
$avatarImg.setAttribute('height', phpbb.avatars.$data.data().maxHeight);
$avatarImg.addClass('avatar');
this.image = $avatarImg;
this.$box.prepend($avatarImg);
} else {
this.image = this.$box.children('img');
}
this.bindInput();
this.bindSelect();
this.bindSubmit();
},
/**
* Destroy (undo) any initialisation.
*/
destroy() {
this.$buttons.find('[data-cropper-action]').off('click.phpbb.avatars');
this.image.off('crop.phpbb.avatars');
this.$form.off('submit');
this.$data.val('');
this.$buttons.hide();
this.$box.removeClass('c-cropper-avatar-box');
if (this.cropper !== null) {
this.cropper.destroy();
}
},
/**
* Bind a function to the avatar driver <select> element.
*
* If a different driver than the "upload" driver is selected, the cropper is destroyed.
* Otherwise if the "upload" driver is (re-)selected, and it has a value, initialise it.
*/
bindSelect() {
this.$driver.on('change', function() {
if ($(this).val() === phpbb.avatars.driverUpload) {
if (phpbb.avatars.$input.val() !== '') {
phpbb.avatars.$input.trigger('change');
}
} else {
phpbb.avatars.destroy();
}
});
},
/**
* Bind a function to the avatar file upload <input> element.
*
* If a file was chosen and it is a valid image file, the cropper is initialised.
* Otherwise the cropper is destroyed.
*/
bindInput() {
this.$input.on('change', function() {
const fileReader = new FileReader();
if (this.files[0].type.match('image.*')) {
fileReader.readAsDataURL(this.files[0]);
fileReader.addEventListener('load', function() {
phpbb.avatars.image.cropper('destroy').attr('src', this.result).addClass('avatar');
phpbb.avatars.$box.addClass('c-cropper-avatar-box');
phpbb.avatars.initCropper();
phpbb.avatars.initButtons();
});
} else {
phpbb.avatars.destroy();
}
});
},
/**
* Bind submit button to be handled by ajax submit
*/
bindSubmit() {
const $this = this;
$this.$form = this.$input.closest('form');
$this.$form.on('submit', () => {
const data = phpbb.avatars.$data.data();
const avatarCanvas = phpbb.avatars.cropper.getCroppedCanvas({
maxWidth: 4096, // High values for max quality cropping
maxHeight: 4096, // High values for max quality cropping
});
// eslint-disable-next-line no-undef
const hermiteResize = new Hermite_class();
hermiteResize.resample_single(avatarCanvas, data.maxWidth, data.maxHeight, true);
avatarCanvas.toBlob(blob => {
const formData = new FormData($this.$form[0]);
formData.set('avatar_upload_file', blob, $this.getUploadFileName());
formData.set('submit', '1');
const canvasDataUrl = avatarCanvas.toDataURL('image/png');
$.ajax({
url: $this.$form.attr('action'),
type: 'POST',
data: formData,
processData: false,
contentType: false,
success(response) {
$this.uploadDone(response, canvasDataUrl);
},
error() {
console.log('Upload error');
},
});
}, 'image/png');
return false;
});
},
/**
* Get upload filename for the blob data
*
* As the blob data is always in png format, we'll replace the file
* extension in the upload name with one that ends with .png
*
* @return {string} Upload file name
*/
getUploadFileName() {
const originalName = this.$input[0].files[0].name;
return originalName.replace(/\.[^/\\.]+$/, '.png');
},
/**
* Handle response from avatar submission
* @param {Object} response AJAX response object
* @param {string} canvasDataUrl Uploaded canvas element as data URL
*/
uploadDone(response, canvasDataUrl) {
if (typeof response !== 'object') {
return;
}
// Handle errors while deleting file
if (typeof response.error === 'undefined') {
const alert = phpbb.alert(response.MESSAGE_TITLE, response.MESSAGE_TEXT);
setTimeout(() => {
window.location = response.REFRESH_DATA.url.replace('&amp;', '&');
alert.hide();
}, response.REFRESH_DATA.time * 1000);
phpbb.avatars.image.attr('src', canvasDataUrl);
phpbb.avatars.destroy();
} else {
phpbb.alert(response.error.title, response.error.messages.join('<br>'));
}
},
/**
* Bind a function to all the cropper <button> elements.
*
* Only buttons with a data-cropper-action attribute are recognized.
* The value for this data attribute should be a function available in the cropper.
* It also takes two optional parameters, imploded by a comma.
* For example: data-cropper-action="move,0,10" which results in $().cropper('move', 0, 10)
*/
initButtons() {
this.$buttons.show().find('[data-cropper-action]').off('click.phpbb.avatars').on('click.phpbb.avatars', function() {
const option = $(this).data('cropper-action').split(',');
const action = option[0];
if (typeof phpbb.avatars.cropper[action] === 'function') {
// Special case: flip, set it to the opposite value (-1 and 1).
if (action === 'scaleX' || action === 'scaleY') {
phpbb.avatars.image.cropper(action, -phpbb.avatars.cropper.getData(true)[action]);
} else {
phpbb.avatars.image.cropper(action, option[1], option[2]);
}
}
});
},
/**
* Initialise the cropper (CropperJS).
*
* @see https://github.com/fengyuanchen/cropperjs
*
* This creates a cropper instance with a 1 to 1 (square) aspect ratio,
* automatically creates the maximum available and allowed cropping area,
* and registers a callback function for the 'crop' event.
*/
initCropper() {
this.cropper = this.image.cropper({
aspectRatio: 1,
autoCropArea: 1,
}).data('cropper');
this.image.off('crop.phpbb.avatars').on('crop.phpbb.avatars', phpbb.avatars.onCrop);
},
/**
* The callback function for the 'crop' event.
*
* This function ensures that the crop area is within the configured dimensions.
* Meaning the width and height can not exceed the limits set by an Administrator.
*
* It also JSON encodes the data array and places it into an <input> element,
* which will be requested server side, and crop the image accordingly.
* Image cropping is done server side, to ensure the best image quality
* and image blobs (from .toBlob()) can only be send through AJAX requests.
*
* @param {object} event
*/
onCrop(event) {
const data = phpbb.avatars.$data.data();
let { width, height } = event.detail;
if (width < data.minWidth || height < data.minHeight) {
width = Math.max(data.minWidth, Math.min(data.maxWidth, width));
height = Math.max(data.minHeight, Math.min(data.maxHeight, height));
phpbb.avatars.cropper.setData({
width,
height,
});
}
},
};
$(() => {
phpbb.avatars.init();
});
})(jQuery); // Avoid conflicts with other libraries

View file

@ -32,7 +32,7 @@ class ucp_profile
function main($id, $mode) function main($id, $mode)
{ {
global $config, $db, $user, $auth, $template, $phpbb_root_path, $phpEx; global $config, $db, $user, $auth, $template, $phpbb_root_path, $phpEx;
global $request, $phpbb_container, $phpbb_log, $phpbb_dispatcher; global $request, $phpbb_container, $phpbb_log, $phpbb_dispatcher, $language;
$user->add_lang('posting'); $user->add_lang('posting');
@ -669,7 +669,7 @@ class ucp_profile
); );
/** /**
* Trigger events on successfull avatar change * Trigger events on successful avatar change
* *
* @event core.ucp_profile_avatar_sql * @event core.ucp_profile_avatar_sql
* @var array result Array with data to be stored in DB * @var array result Array with data to be stored in DB
@ -683,11 +683,48 @@ class ucp_profile
WHERE user_id = ' . (int) $user->data['user_id']; WHERE user_id = ' . (int) $user->data['user_id'];
$db->sql_query($sql); $db->sql_query($sql);
if ($request->is_ajax())
{
/** @var \phpbb\avatar\helper $avatar_helper */
$avatar_helper = $phpbb_container->get('avatar.helper');
$avatar = $avatar_helper->get_user_avatar($user->data, 'USER_AVATAR', true);
$json_response = new \phpbb\json_response;
$json_response->send(array(
'success' => true,
'MESSAGE_TITLE' => $language->lang('INFORMATION'),
'MESSAGE_TEXT' => $language->lang('PROFILE_UPDATED'),
'AVATAR' => $avatar_helper->get_template_vars($avatar),
'REFRESH_DATA' => [
'time' => 3,
'url' => $this->u_action,
'text' => $language->lang('RETURN_TO_UCP'),
]
));
}
else
{
meta_refresh(3, $this->u_action); meta_refresh(3, $this->u_action);
$message = $user->lang['PROFILE_UPDATED'] . '<br /><br />' . sprintf($user->lang['RETURN_UCP'], '<a href="' . $this->u_action . '">', '</a>'); $message = $language->lang('PROFILE_UPDATED') . '<br><br>' . $language->lang('RETURN_UCP', '<a href="' . $this->u_action . '">', '</a>');
trigger_error($message); trigger_error($message);
} }
} }
else if ($request->is_ajax())
{
$error = $phpbb_avatar_manager->localize_errors($user, $error);
$json_response = new \phpbb\json_response;
$json_response->send([
'success' => false,
'error' => [
'title' => $language->lang('INFORMATION'),
'messages' => $error,
],
]);
}
}
} }
else else
{ {

View file

@ -46,12 +46,12 @@ INSERT INTO phpbb_config (config_name, config_value) VALUES ('auth_bbcode_pm', '
INSERT INTO phpbb_config (config_name, config_value) VALUES ('auth_img_pm', '1'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('auth_img_pm', '1');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('auth_method', 'db'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('auth_method', 'db');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('auth_smilies_pm', '1'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('auth_smilies_pm', '1');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('avatar_filesize', '6144'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('avatar_filesize', '262144');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('avatar_gallery_path', 'images/avatars/gallery'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('avatar_gallery_path', 'images/avatars/gallery');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('avatar_max_height', '90'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('avatar_max_height', '120');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('avatar_max_width', '90'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('avatar_max_width', '120');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('avatar_min_height', '20'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('avatar_min_height', '40');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('avatar_min_width', '20'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('avatar_min_width', '40');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('avatar_salt', 'phpbb_avatar'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('avatar_salt', 'phpbb_avatar');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('board_contact', 'contact@yourdomain.tld'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('board_contact', 'contact@yourdomain.tld');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('board_contact_name', ''); INSERT INTO phpbb_config (config_name, config_value) VALUES ('board_contact_name', '');

View file

@ -130,6 +130,7 @@ $lang = array_merge($lang, array(
'CANNOT_REMOVE_FOLDER' => 'This folder cannot be removed.', 'CANNOT_REMOVE_FOLDER' => 'This folder cannot be removed.',
'CHANGE_DEFAULT_GROUP' => 'Change default group', 'CHANGE_DEFAULT_GROUP' => 'Change default group',
'CHANGE_PASSWORD' => 'Change password', 'CHANGE_PASSWORD' => 'Change password',
'CLEAR' => 'Clear',
'CLICK_GOTO_FOLDER' => '%1$sGo to your “%3$s” folder%2$s', 'CLICK_GOTO_FOLDER' => '%1$sGo to your “%3$s” folder%2$s',
'CLICK_RETURN_FOLDER' => '%1$sReturn to your “%3$s” folder%2$s', 'CLICK_RETURN_FOLDER' => '%1$sReturn to your “%3$s” folder%2$s',
'CONFIRMATION' => 'Confirmation of registration', 'CONFIRMATION' => 'Confirmation of registration',
@ -222,6 +223,8 @@ $lang = array_merge($lang, array(
'FIELD_INVALID_URL' => 'The field “%s” has an invalid url.', 'FIELD_INVALID_URL' => 'The field “%s” has an invalid url.',
'FIELD_INVALID_VALUE' => 'The field “%s” has an invalid value.', 'FIELD_INVALID_VALUE' => 'The field “%s” has an invalid value.',
'FLIP_HORIZONTALLY' => 'Flip horizontally',
'FLIP_VERTICALLY' => 'Flip vertically',
'FOE_MESSAGE' => 'Message from foe', 'FOE_MESSAGE' => 'Message from foe',
'FOES_EXPLAIN' => 'Foes are users which will be ignored by default. Posts by these users will not be fully visible. Private messages from foes are still permitted. Please note that you cannot ignore moderators or administrators.', 'FOES_EXPLAIN' => 'Foes are users which will be ignored by default. Posts by these users will not be fully visible. Private messages from foes are still permitted. Please note that you cannot ignore moderators or administrators.',
'FOES_UPDATED' => 'Your foes list has been updated successfully.', 'FOES_UPDATED' => 'Your foes list has been updated successfully.',
@ -304,11 +307,13 @@ $lang = array_merge($lang, array(
'MESSAGES_DELETED' => 'Messages successfully deleted', 'MESSAGES_DELETED' => 'Messages successfully deleted',
'MOVE_DELETED_MESSAGES_TO' => 'Move messages from removed folder to', 'MOVE_DELETED_MESSAGES_TO' => 'Move messages from removed folder to',
'MOVE_DOWN' => 'Move down', 'MOVE_DOWN' => 'Move down',
'MOVE_LEFT' => 'Move left',
'MOVE_MARKED_TO_FOLDER' => 'Move marked to %s', 'MOVE_MARKED_TO_FOLDER' => 'Move marked to %s',
'MOVE_PM_ERROR' => array( 'MOVE_PM_ERROR' => array(
1 => 'An error occurred while moving the messages to the new folder, only %2$d out of %1$s was moved.', 1 => 'An error occurred while moving the messages to the new folder, only %2$d out of %1$s was moved.',
2 => 'An error occurred while moving the messages to the new folder, only %2$d out of %1$s were moved.', 2 => 'An error occurred while moving the messages to the new folder, only %2$d out of %1$s were moved.',
), ),
'MOVE_RIGHT' => 'Move right',
'MOVE_TO_FOLDER' => 'Move to folder', 'MOVE_TO_FOLDER' => 'Move to folder',
'MOVE_UP' => 'Move up', 'MOVE_UP' => 'Move up',
@ -468,6 +473,9 @@ $lang = array_merge($lang, array(
'RESIGN_SELECTED' => 'Resign selected', 'RESIGN_SELECTED' => 'Resign selected',
'RETURN_FOLDER' => '%1$sReturn to previous folder%2$s', 'RETURN_FOLDER' => '%1$sReturn to previous folder%2$s',
'RETURN_UCP' => '%sReturn to the User Control Panel%s', 'RETURN_UCP' => '%sReturn to the User Control Panel%s',
'RETURN_TO_UCP' => 'Return to the User Control Panel',
'ROTATE_LEFT' => 'Rotate left',
'ROTATE_RIGHT' => 'Rotate right',
'RULE_ADDED' => 'Rule successfully added.', 'RULE_ADDED' => 'Rule successfully added.',
'RULE_ALREADY_DEFINED' => 'This rule was defined previously.', 'RULE_ALREADY_DEFINED' => 'This rule was defined previously.',
'RULE_DELETED' => 'Rule successfully removed.', 'RULE_DELETED' => 'Rule successfully removed.',
@ -649,6 +657,9 @@ $lang = array_merge($lang, array(
'TO_ME' => 'to me', 'TO_ME' => 'to me',
), ),
'ZOOM_IN' => 'Zoom in',
'ZOOM_OUT' => 'Zoom out',
'GROUPS_EXPLAIN' => 'Usergroups enable board admins to better administer users. By default you will be placed in a specific group, this is your default group. This group defines how you may appear to other users, for example your username colouration, avatar, rank, etc. Depending on whether the administrator allows it you may be allowed to change your default group. You may also be placed in or allowed to join other groups. Some groups may give you additional permissions to view content or increase your capabilities in other areas.', 'GROUPS_EXPLAIN' => 'Usergroups enable board admins to better administer users. By default you will be placed in a specific group, this is your default group. This group defines how you may appear to other users, for example your username colouration, avatar, rank, etc. Depending on whether the administrator allows it you may be allowed to change your default group. You may also be placed in or allowed to join other groups. Some groups may give you additional permissions to view content or increase your capabilities in other areas.',
'GROUP_LEADER' => 'Leaderships', 'GROUP_LEADER' => 'Leaderships',
'GROUP_MEMBER' => 'Memberships', 'GROUP_MEMBER' => 'Memberships',

View file

@ -23,7 +23,7 @@ use phpbb\storage\exception\exception as storage_exception;
use phpbb\storage\storage; use phpbb\storage\storage;
/** /**
* Handles avatars uploaded to the board * Handles avatars uploaded to the board.
*/ */
class upload extends \phpbb\avatar\driver\driver class upload extends \phpbb\avatar\driver\driver
{ {
@ -100,10 +100,14 @@ class upload extends \phpbb\avatar\driver\driver
return false; return false;
} }
$template->assign_vars(array( $use_board = defined('PHPBB_USE_BOARD_URL_PATH') && PHPBB_USE_BOARD_URL_PATH;
'AVATAR_UPLOAD_SIZE' => $this->config['avatar_filesize'], $web_path = $use_board ? generate_board_url() . '/' : $this->path_helper->get_web_root_path();
$template->assign_vars([
'AVATAR_ALLOWED_EXTENSIONS' => implode(',', preg_replace('/^/', '.', $this->allowed_extensions)), 'AVATAR_ALLOWED_EXTENSIONS' => implode(',', preg_replace('/^/', '.', $this->allowed_extensions)),
)); 'AVATAR_UPLOAD_SIZE' => $this->config['avatar_filesize'],
'T_ASSETS_PATH' => $web_path . '/assets',
]);
return true; return true;
} }
@ -137,6 +141,7 @@ class upload extends \phpbb\avatar\driver\driver
return false; return false;
} }
/** @var \phpbb\files\filespec_storage $file */
$file = $upload->handle_upload('files.types.form_storage', 'avatar_upload_file'); $file = $upload->handle_upload('files.types.form_storage', 'avatar_upload_file');
$prefix = $this->config['avatar_salt'] . '_'; $prefix = $this->config['avatar_salt'] . '_';
@ -226,7 +231,6 @@ class upload extends \phpbb\avatar\driver\driver
*/ */
public function delete($row) public function delete($row)
{ {
$error = array(); $error = array();
$prefix = $this->config['avatar_salt'] . '_'; $prefix = $this->config['avatar_salt'] . '_';
$ext = substr(strrchr($row['avatar'], '.'), 1); $ext = substr(strrchr($row['avatar'], '.'), 1);

View file

@ -0,0 +1,40 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
namespace phpbb\db\migration\data\v400;
use phpbb\db\migration\container_aware_migration;
class increase_avatar_size extends container_aware_migration
{
public static function depends_on()
{
return ['\phpbb\db\migration\data\v400\dev'];
}
public function update_data()
{
return [
['custom', [[$this, 'increase_size']]],
];
}
public function increase_size(): void
{
$this->config->set('avatar_filesize', max(262144, $this->config['avatar_filesize'])); // Increase to 256 KiB
$this->config->set('avatar_max_height', max('120', $this->config['avatar_max_height'])); // Increase to max 120px height
$this->config->set('avatar_max_width', max('120', $this->config['avatar_max_width'])); // Increase to max 120px width
$this->config->set('avatar_min_height', max('40', $this->config['avatar_min_height'])); // Increase to min 40px height
$this->config->set('avatar_min_width', max('40', $this->config['avatar_min_width'])); // Increase to max 40px width
}
}

View file

@ -8,7 +8,7 @@
<!-- IF ERROR --><p class="error">{ERROR}</p><!-- ENDIF --> <!-- IF ERROR --><p class="error">{ERROR}</p><!-- ENDIF -->
<dl> <dl>
<dt><label>{L_CURRENT_IMAGE}{L_COLON}</label><br /><span>{L_AVATAR_EXPLAIN}</span></dt> <dt><label>{L_CURRENT_IMAGE}{L_COLON}</label><br /><span>{L_AVATAR_EXPLAIN}</span></dt>
<dd><!-- IF AVATAR -->{AVATAR_HTML}<!-- ELSE --><img src="{{ NO_AVATAR_SOURCE }}" alt="" /><!-- ENDIF --></dd> <dd class="c-avatar-box"><!-- IF AVATAR -->{AVATAR_HTML}<!-- ELSE --><img src="{{ NO_AVATAR_SOURCE }}" alt="" /><!-- ENDIF --></dd>
<dd><label for="avatar_delete"><input type="checkbox" name="avatar_delete" id="avatar_delete" /> {L_DELETE_AVATAR}</label></dd> <dd><label for="avatar_delete"><input type="checkbox" name="avatar_delete" id="avatar_delete" /> {L_DELETE_AVATAR}</label></dd>
</dl> </dl>
</fieldset> </fieldset>

View file

@ -2,3 +2,68 @@
<dt><label for="avatar_upload_file">{L_UPLOAD_AVATAR_FILE}{L_COLON}</label></dt> <dt><label for="avatar_upload_file">{L_UPLOAD_AVATAR_FILE}{L_COLON}</label></dt>
<dd><input type="hidden" name="MAX_FILE_SIZE" value="{AVATAR_UPLOAD_SIZE}" /><input type="file" name="avatar_upload_file" id="avatar_upload_file" class="inputbox autowidth" accept="{{ AVATAR_ALLOWED_EXTENSIONS }}" /></dd> <dd><input type="hidden" name="MAX_FILE_SIZE" value="{AVATAR_UPLOAD_SIZE}" /><input type="file" name="avatar_upload_file" id="avatar_upload_file" class="inputbox autowidth" accept="{{ AVATAR_ALLOWED_EXTENSIONS }}" /></dd>
</dl> </dl>
{% INCLUDECSS T_ASSETS_PATH ~ '/css/cropper.min.css' %}
{% INCLUDEJS T_ASSETS_PATH ~ '/javascript/cropper.min.js' %}
{% INCLUDEJS T_ASSETS_PATH ~ '/javascript/jquery-cropper.js' %}
{% INCLUDEJS T_ASSETS_PATH ~ '/javascript/hermite.js' %}
{% INCLUDEJS T_ASSETS_PATH ~ '/javascript/phpbb-avatars.js' %}
<input type="hidden" id="avatar-cropper-data" name="avatar_cropper_data" value=""
data-min-width="{{ AVATAR_MIN_WIDTH }}" data-max-width="{{ AVATAR_MAX_WIDTH }}"
data-min-height="{{ AVATAR_MIN_HEIGHT }}" data-max-height="{{ AVATAR_MAX_HEIGHT }}" />
{% apply spaceless %}
<div class="avatar-cropper-buttons" id="avatar-cropper-buttons">
<div class="button-group">
<button class="button" type="button" title="{{ lang('ZOOM_IN') }}" data-cropper-action="zoom,0.1">
{{ Icon('iconify', 'fa:search-plus', '', true) }}
</button>
<button class="button" type="button" title="{{ lang('ZOOM_OUT') }}" data-cropper-action="zoom,-0.1">
{{ Icon('iconify', 'fa:search-minus', '', true) }}
</button>
</div>
<div class="button-group">
<button class="button" type="button" title="{{ lang('MOVE_LEFT') }}" data-cropper-action="move,-10,0">
{{ Icon('iconify', 'fa:arrow-left', '', true) }}
</button>
<button class="button" type="button" title="{{ lang('MOVE_RIGHT') }}" data-cropper-action="move,10,0">
{{ Icon('iconify', 'fa:arrow-right', '', true) }}
</button>
<button class="button" type="button" title="{{ lang('MOVE_UP') }}" data-cropper-action="move,0,-10">
{{ Icon('iconify', 'fa:arrow-up', '', true) }}
</button>
<button class="button" type="button" title="{{ lang('MOVE_DOWN') }}" data-cropper-action="move,0,10">
{{ Icon('iconify', 'fa:arrow-down', '', true) }}
</button>
</div>
<div class="button-group">
<button class="button" type="button" title="{{ lang('ROTATE_LEFT') }}" data-cropper-action="rotate,-90">
{{ Icon('iconify', 'fa:rotate-left', '', true) }}
</button>
<button class="button" type="button" title="{{ lang('ROTATE_RIGHT') }}" data-cropper-action="rotate,90">
{{ Icon('iconify', 'fa:rotate-right', '', true) }}
</button>
</div>
<div class="button-group">
<button class="button" type="button" title="{{ lang('FLIP_HORIZONTALLY') }}" data-cropper-action="scaleX">
{{ Icon('iconify', 'fa:arrows-h', '', true) }}
</button>
<button class="button" type="button" title="{{ lang('FLIP_VERTICALLY') }}" data-cropper-action="scaleY">
{{ Icon('iconify', 'fa:arrows-v', '', true) }}
</button>
</div>
<div class="button-group">
<button class="button" type="button" title="{{ lang('RESET') }}" data-cropper-action="reset">
{{ Icon('iconify', 'fa:refresh', '', true) }}
</button>
<button class="button" type="button" title="{{ lang('CLEAR') }}" data-cropper-action="clear">
{{ Icon('iconify', 'fa:times', '', true) }}
</button>
</div>
</div>
{% endapply %}

View file

@ -42,6 +42,29 @@
vertical-align: top; vertical-align: top;
} }
/** Button groups */
.button-group {
display: inline-block;
}
.button-group + .button-group {
margin-left: 8px;
}
.button-group > .button:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.button-group > .button:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.button-group > .button:not(:first-child):not(:last-child) {
border-radius: 0;
}
/* Posting page styles /* Posting page styles
---------------------------------------- */ ---------------------------------------- */
.button-form, .button-form,
@ -173,3 +196,13 @@ button::-moz-focus-inner {
border: 0; border: 0;
padding: 0; padding: 0;
} }
/* UCP: Avatar cropper */
.avatar-cropper-buttons {
text-align: center;
display: none;
}
.avatar-cropper-buttons > .button-group {
margin: 4px;
}

View file

@ -357,6 +357,11 @@ ol.def-rules li {
padding: 5px; padding: 5px;
} }
.c-cropper-avatar-box {
min-height: 240px;
max-height: 400px;
}
p.notification-title, p.notification-title,
p.notification-forum, p.notification-forum,
p.notification-reason, p.notification-reason,

View file

@ -38,12 +38,12 @@ class phpbb_functional_avatar_acp_groups_test extends phpbb_functional_common_av
), ),
// Gravatar with incorrect size // Gravatar with incorrect size
array( array(
'The submitted avatar is 120 wide and 120 high. Avatars must be at least 20 wide and 20 high, but no larger than 90 wide and 90 high.', 'The submitted avatar is 140 wide and 140 high. Avatars must be at least 40 wide and 40 high, but no larger than 120 wide and 120 high.',
'avatar_driver_gravatar', 'avatar_driver_gravatar',
array( array(
'avatar_gravatar_email' => 'test@example.com', 'avatar_gravatar_email' => 'test@example.com',
'avatar_gravatar_width' => 120, 'avatar_gravatar_width' => 140,
'avatar_gravatar_height' => 120, 'avatar_gravatar_height' => 140,
), ),
), ),
// Delete avatar image to reset group settings // Delete avatar image to reset group settings