Changeset 3321992
- Timestamp:
- 07/03/2025 07:40:50 PM (9 months ago)
- Location:
- groundhogg
- Files:
-
- 28 edited
- 1 copied
-
tags/4.2.2 (copied) (copied from groundhogg/trunk)
-
tags/4.2.2/README.txt (modified) (2 diffs)
-
tags/4.2.2/admin/contacts/cards/user.php (modified) (1 diff)
-
tags/4.2.2/admin/contacts/parts/contact-editor.php (modified) (1 diff)
-
tags/4.2.2/admin/tools/tools-page.php (modified) (5 diffs)
-
tags/4.2.2/api/v4/files-api.php (modified) (2 diffs)
-
tags/4.2.2/assets/js/admin/filters/contacts.js (modified) (2 diffs)
-
tags/4.2.2/assets/js/admin/filters/contacts.min.js (modified) (2 diffs)
-
tags/4.2.2/db/contacts.php (modified) (5 diffs)
-
tags/4.2.2/groundhogg.php (modified) (2 diffs)
-
tags/4.2.2/includes/classes/traits/file-box.php (modified) (5 diffs)
-
tags/4.2.2/includes/contact-query.php (modified) (1 diff)
-
tags/4.2.2/includes/functions.php (modified) (4 diffs)
-
tags/4.2.2/includes/rewrites.php (modified) (2 diffs)
-
tags/4.2.2/includes/utils/files.php (modified) (3 diffs)
-
trunk/README.txt (modified) (2 diffs)
-
trunk/admin/contacts/cards/user.php (modified) (1 diff)
-
trunk/admin/contacts/parts/contact-editor.php (modified) (1 diff)
-
trunk/admin/tools/tools-page.php (modified) (5 diffs)
-
trunk/api/v4/files-api.php (modified) (2 diffs)
-
trunk/assets/js/admin/filters/contacts.js (modified) (2 diffs)
-
trunk/assets/js/admin/filters/contacts.min.js (modified) (2 diffs)
-
trunk/db/contacts.php (modified) (5 diffs)
-
trunk/groundhogg.php (modified) (2 diffs)
-
trunk/includes/classes/traits/file-box.php (modified) (5 diffs)
-
trunk/includes/contact-query.php (modified) (1 diff)
-
trunk/includes/functions.php (modified) (4 diffs)
-
trunk/includes/rewrites.php (modified) (2 diffs)
-
trunk/includes/utils/files.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
groundhogg/tags/4.2.2/README.txt
r3320187 r3321992 7 7 Tested up to: 6.8 8 8 Requires PHP: 7.1 9 Stable tag: 4.2. 19 Stable tag: 4.2.2 10 10 License: GPLv3 11 11 License URI: https://www.gnu.org/licenses/gpl.md … … 354 354 355 355 == Changelog == 356 357 = 4.2.2 (2025-07-03) = 358 * FIXED User ID not syncing. 359 * FIXED Arbitrary file upload vulnerability. Credit to Patchstack for practicing responsible disclosure. 356 360 357 361 = 4.2.1 (2025-06-30) = -
groundhogg/tags/4.2.2/admin/contacts/cards/user.php
r3029787 r3321992 10 10 * @var $contact \Groundhogg\Contact 11 11 */ 12 13 /* Auto link the account before we see the create account form. */ 14 $contact->auto_link_account(); 12 15 13 16 if ( $contact->get_userdata() ): -
groundhogg/tags/4.2.2/admin/contacts/parts/contact-editor.php
r3209982 r3321992 19 19 * @var $contact Contact 20 20 */ 21 22 //var_dump( $contact );23 24 /* Auto link the account before we see the create account form. */25 $contact->auto_link_account();26 21 27 22 $tabs = [ -
groundhogg/tags/4.2.2/admin/tools/tools-page.php
r3320187 r3321992 11 11 use Groundhogg\DB\Query\Table_Query; 12 12 use Groundhogg\Extension_Upgrader; 13 use Groundhogg\Files; 13 14 use Groundhogg\License_Manager; 14 15 use Groundhogg\Plugin; … … 54 55 class Tools_Page extends Tabbed_Admin_Page { 55 56 56 protected $uploads_path = [];57 58 57 /** 59 58 * @var \Groundhogg\Bulk_Jobs\Import_Contacts … … 562 561 } 563 562 564 $file = get_array_var( $_FILES, 'import_file' ); 565 566 $validate = wp_check_filetype( $file['name'], [ 'csv' => 'text/csv' ] ); 567 568 if ( $validate['ext'] !== 'csv' || $validate['type'] !== 'text/csv' ) { 569 return new WP_Error( 'invalid_csv', sprintf( 'Please upload a valid CSV. Expected mime type of <i>text/csv</i> but got <i>%s</i>', esc_html( $file['type'] ) ) ); 570 } 571 572 $file['name'] = wp_unique_filename( files()->get_csv_imports_dir(), $file['name'] ); 573 574 $result = $this->handle_file_upload( $file ); 563 $file = $_FILES['import_file']; 564 565 $result = files()->safe_file_upload( $file, [ 566 'csv' => 'text/csv', 567 ], 'imports' ); 575 568 576 569 if ( is_wp_error( $result ) ) { 577 578 if ( is_multisite() ) {579 return new WP_Error( 'multisite_add_csv', 'Could not import because CSV is not an allowed file type on this subsite. Please add CSV to the list of allowed file types in the network settings.' );580 }581 582 570 return $result; 583 571 } … … 586 574 'action' => 'map', 587 575 'tab' => 'import', 588 'import' => urlencode( basename( $result[' file'] ) ),576 'import' => urlencode( basename( $result['path'] ) ), 589 577 ] ) ); 590 578 591 }592 593 /**594 * Upload a file to the Groundhogg file directory595 *596 * @param $file array597 * @param $config598 *599 * @return array|bool|WP_Error600 */601 private function handle_file_upload( $file ) {602 $upload_overrides = array( 'test_form' => false );603 604 if ( ! function_exists( 'wp_handle_upload' ) ) {605 require_once( ABSPATH . '/wp-admin/includes/file.php' );606 }607 608 $this->set_uploads_path();609 610 add_filter( 'upload_dir', array( $this, 'files_upload_dir' ) );611 $mfile = wp_handle_upload( $file, $upload_overrides );612 remove_filter( 'upload_dir', array( $this, 'files_upload_dir' ) );613 614 if ( isset( $mfile['error'] ) ) {615 616 if ( empty( $mfile['error'] ) ) {617 $mfile['error'] = _x( 'Could not upload file.', 'error', 'groundhogg' );618 }619 620 return new WP_Error( 'BAD_UPLOAD', $mfile['error'] );621 }622 623 return $mfile;624 }625 626 /**627 * Change the default upload directory628 *629 * @param $param630 *631 * @return mixed632 */633 public function files_upload_dir( $param ) {634 $param['path'] = $this->uploads_path['path'];635 $param['url'] = $this->uploads_path['url'];636 $param['subdir'] = $this->uploads_path['subdir'];637 638 return $param;639 }640 641 /**642 * Initialize the base upload path643 */644 private function set_uploads_path() {645 $this->uploads_path['subdir'] = Plugin::$instance->utils->files->get_base_uploads_dir();646 $this->uploads_path['path'] = Plugin::$instance->utils->files->get_csv_imports_dir();647 $this->uploads_path['url'] = Plugin::$instance->utils->files->get_csv_imports_url();648 579 } 649 580 … … 1232 1163 $file_path = wp_normalize_path( $groundhogg_path . DIRECTORY_SEPARATOR . $short_path ); 1233 1164 1234 if ( ! $file_path || ! file_exists( $file_path ) || ! is_file( $file_path ) ) { 1165 // guard against ../../ traversal attack 1166 if ( ! $file_path || ! file_exists( $file_path ) || ! is_file( $file_path ) || ! Files::is_file_within_directory( $file_path, $groundhogg_path ) ) { 1235 1167 wp_die( 'The requested file was not found.', 'File not found.', [ 'status' => 404 ] ); 1236 1168 } -
groundhogg/tags/4.2.2/api/v4/files-api.php
r2507120 r3321992 38 38 39 39 register_rest_route( self::NAME_SPACE, "/files/exports", [ 40 [41 'methods' => WP_REST_Server::CREATABLE,42 'callback' => [ $this, 'create_exports' ],43 'permission_callback' => [ $this, 'create_exports_permissions_callback' ]44 ],40 // [ 41 // 'methods' => WP_REST_Server::CREATABLE, 42 // 'callback' => [ $this, 'create_exports' ], 43 // 'permission_callback' => [ $this, 'create_exports_permissions_callback' ] 44 // ], 45 45 [ 46 46 'methods' => WP_REST_Server::READABLE, … … 71 71 foreach ( $_FILES as $FILE ) { 72 72 73 $validate = wp_check_filetype( $FILE['name'], [ 'csv' => 'text/csv' ] ); 74 75 if ( $validate['ext'] !== 'csv' || $validate['text/csv'] ) { 76 return self::ERROR_500( 'invalid_csv', sprintf( 'Please upload a valid CSV. Expected mime type of <i>text/csv</i> but got <i>%s</i>', esc_html( $FILE['type'] ) ) ); 77 } 78 79 $file_name = str_replace( '.csv', '', $FILE['name'] ); 80 $file_name .= '-' . current_time( 'mysql', true ) . '.csv'; 81 82 $FILE['name'] = sanitize_file_name( $file_name ); 83 84 $result = files()->upload( $FILE, 'imports' ); 73 $result = files()->safe_file_upload( $FILE, [ 74 'csv' => 'text/csv' 75 ], 'imports' ); 85 76 86 77 if ( is_wp_error( $result ) ) { -
groundhogg/tags/4.2.2/assets/js/admin/filters/contacts.js
r3264477 r3321992 52 52 Input, 53 53 Div, 54 makeEl 54 makeEl, 55 InputRepeater 55 56 } = MakeEl 56 57 … … 2435 2436 ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory('current_time', 'Current Time', 'time', ( time ) => formatTime(`2000-01-01T${time}`) )) 2436 2437 2438 registerFilterGroup( 'submissions', 'Submissions' ) 2439 2440 ContactFilterRegistry.registerFilter( createPastDateFilter( 'form_submissions', 'Form Submissions', 'submissions', { 2441 edit: ({ 2442 form_id = '', 2443 form_type = '', 2444 meta_filters = [], 2445 updateFilter = () => {} 2446 }) => { 2447 2448 return Fragment([ 2449 2450 Input({ 2451 placeholder: 'ID', 2452 id: 'form_id', 2453 name: 'form_id', 2454 value: form_id, 2455 onChange: e => updateFilter({ 2456 form_id: e.target.value 2457 }) 2458 }), 2459 2460 Input({ 2461 placeholder: 'Type', 2462 id: 'type', 2463 name: 'type', 2464 value: form_type, 2465 onChange: e => updateFilter({ 2466 form_type: e.target.value 2467 }) 2468 }), 2469 2470 `<label>${ __('Filter by submission meta', 'groundhogg') }</label>`, 2471 InputRepeater({ 2472 id: 'submission-meta-filters', 2473 rows: meta_filters, 2474 cells: [ 2475 props => Input({...props, placeholder: 'Key'}), 2476 ({ 2477 value, 2478 ...props 2479 }) => Select({ 2480 selected: value, 2481 options : AllComparisons, 2482 ...props, 2483 }), 2484 props => Input({...props, placeholder: 'Value'}), 2485 ], 2486 fillRow: () => [ 2487 '', 2488 'equals', 2489 '' 2490 ], 2491 onChange: rows => { 2492 updateFilter({ 2493 meta_filters: rows 2494 }) 2495 } 2496 }) 2497 ]) 2498 } 2499 } ) ) 2500 2437 2501 if (!Groundhogg.filters) { 2438 2502 Groundhogg.filters = {} -
groundhogg/tags/4.2.2/assets/js/admin/filters/contacts.min.js
r3264477 r3321992 1 (function($){const{input,select,orList,andList,bold,inputRepeater}=Groundhogg.element;const{broadcastPicker,funnelPicker,tagPicker,emailPicker,linkPicker,metaValuePicker,metaPicker,userMetaPicker}=Groundhogg.pickers;const{assoc2array}=Groundhogg.functions;const{broadcasts:BroadcastsStore,emails:EmailsStore,tags:TagsStore,funnels:FunnelsStore,searches:SearchesStore}=Groundhogg.stores;const{sprintf,__,_x,_n}=wp.i18n;const{formatDate,formatDateTime,formatTime}=Groundhogg.formatting;const{Fragment,ItemPicker,Select,Input,Div,makeEl }=MakeEl;const{Filters,FilterRegistry,createFilter,createGroup,FilterDisplay,createDateFilter,createPastDateFilter,createStringFilter,createNumberFilter,createTimeFilter,unsubReasons}=Groundhogg.filters;const{ComparisonsTitleGenerators,AllComparisons,StringComparisons,NumericComparisons,pastDateRanges,futureDateRanges,allDateRanges}=Groundhogg.filters.comparisons;const ContactFilterRegistry=FilterRegistry({});const uid=function(){return Date.now().toString(36)+Math.random().toString(36).substring(2)};const createFilters=(el="",filters=[],onChange=f=>{console.log(f)})=>({el:el,onChange:onChange,filters:Array.isArray(filters)?filters:[],id:uid(),init(){this.mount()},mount(){let container=document.querySelector(el);container.innerHTML="";document.querySelector(el).appendChild(ContactFilters(this.id,this.filters,this.onChange))}});const ContactFilters=(id,filters,onChange)=>Filters({id:id,filterRegistry:ContactFilterRegistry,filters:filters,onChange:onChange});const ContactFilterDisplay=filters=>FilterDisplay({filters:filters,filterRegistry:ContactFilterRegistry});const registerFilterGroup=(group,name)=>{ContactFilterRegistry.registerGroup(createGroup(group,name))};const registerFilter=(type,group="general",name="",opts={})=>{if(typeof name==="object"){let tempOpts=name;name=tempOpts.name;opts=tempOpts}const{defaults:defaults={},preload:preload=()=>{},view:view=()=>"",edit:edit=()=>"",onMount:onMount=()=>""}=opts;ContactFilterRegistry.registerFilter(createFilter(type,name,group,{display:view,preload:preload,edit:({updateFilter,...filter})=>Fragment([edit(filter)],{onCreate:el=>{setTimeout(()=>{onMount(filter,updateFilter)},50)}})},defaults))};const standardActivityDateFilterOnMount=(filter,updateFilter)=>{$("#filter-date-range, #filter-before, #filter-after, #filter-days").on("change",function(e){const $el=$(this);updateFilter({[$el.prop("name")]:$el.val()});if($el.prop("name")==="date_range"){const $before=$("#filter-before");const $after=$("#filter-after");const $days=$("#filter-days");$before.addClass("hidden");$after.addClass("hidden");$days.addClass("hidden");switch($el.val()){case"between":$before.removeClass("hidden");$after.removeClass("hidden");break;case"day_of":case"after":$after.removeClass("hidden");break;case"before":$before.removeClass("hidden");break;case"x_days":$days.removeClass("hidden");break}}})};const standardActivityDateTitle=(prepend,{date_range,before,after,days:days=0,future:future=false})=>{let ranges=future?futureDateRanges:pastDateRanges;switch(date_range){default:return`${prepend} ${ranges[date_range]?ranges[date_range].replace("X",days).toLowerCase():""}`;case"between":return`${prepend} ${sprintf(_x("between %1$s and %2$s","where %1 and %2 are dates","groundhogg"),`<b>${formatDate(after)}</b>`,`<b>${formatDate(before)}</b>`)}`;case"before":return`${prepend} ${sprintf(_x("before %s","%s is a date","groundhogg"),`<b>${formatDate(before)}</b>`)}`;case"after":return`${prepend} ${sprintf(_x("after %s","%s is a date","groundhogg"),`<b>${formatDate(after)}</b>`)}`;case"day_of":return`${prepend} ${sprintf(_x("on %s","%s is a date","groundhogg"),`<b>${formatDate(after)}</b>`)}`}};const standardActivityDateOptions=({date_range:date_range="24_hours",after:after="",before:before="",days:days=0,future:future=false})=>{return[select({id:"filter-date-range",name:"date_range"},future?futureDateRanges:pastDateRanges,date_range),input({type:"date",value:after.split(" ")[0],id:"filter-after",className:`date ${["between","after","day_of"].includes(date_range)?"":"hidden"}`,name:"after"}),input({type:"date",value:before.split(" ")[0],id:"filter-before",className:`value ${["between","before"].includes(date_range)?"":"hidden"}`,name:"before"}),input({type:"number",value:days,id:"filter-days",min:0,className:`value ${["x_days","next_x_days"].includes(date_range)?"":"hidden"}`,name:"days"})].join("")};const standardActivityDateDefaults={date_range:"any",before:"",after:"",count:1,days:0};const filterCountDefaults={count:1,count_compare:"greater_than_or_equal_to"};const activityFilterComparisons={equals:_x("Exactly","comparison","groundhogg"),less_than:_x("Less than","comparison","groundhogg"),greater_than:_x("More than","comparison","groundhogg"),less_than_or_equal_to:_x("At most","comparison","groundhogg"),greater_than_or_equal_to:_x("At least","comparison","groundhogg")};const filterCount=({count,count_compare})=>{return`1 (function($){const{input,select,orList,andList,bold,inputRepeater}=Groundhogg.element;const{broadcastPicker,funnelPicker,tagPicker,emailPicker,linkPicker,metaValuePicker,metaPicker,userMetaPicker}=Groundhogg.pickers;const{assoc2array}=Groundhogg.functions;const{broadcasts:BroadcastsStore,emails:EmailsStore,tags:TagsStore,funnels:FunnelsStore,searches:SearchesStore}=Groundhogg.stores;const{sprintf,__,_x,_n}=wp.i18n;const{formatDate,formatDateTime,formatTime}=Groundhogg.formatting;const{Fragment,ItemPicker,Select,Input,Div,makeEl,InputRepeater}=MakeEl;const{Filters,FilterRegistry,createFilter,createGroup,FilterDisplay,createDateFilter,createPastDateFilter,createStringFilter,createNumberFilter,createTimeFilter,unsubReasons}=Groundhogg.filters;const{ComparisonsTitleGenerators,AllComparisons,StringComparisons,NumericComparisons,pastDateRanges,futureDateRanges,allDateRanges}=Groundhogg.filters.comparisons;const ContactFilterRegistry=FilterRegistry({});const uid=function(){return Date.now().toString(36)+Math.random().toString(36).substring(2)};const createFilters=(el="",filters=[],onChange=f=>{console.log(f)})=>({el:el,onChange:onChange,filters:Array.isArray(filters)?filters:[],id:uid(),init(){this.mount()},mount(){let container=document.querySelector(el);container.innerHTML="";document.querySelector(el).appendChild(ContactFilters(this.id,this.filters,this.onChange))}});const ContactFilters=(id,filters,onChange)=>Filters({id:id,filterRegistry:ContactFilterRegistry,filters:filters,onChange:onChange});const ContactFilterDisplay=filters=>FilterDisplay({filters:filters,filterRegistry:ContactFilterRegistry});const registerFilterGroup=(group,name)=>{ContactFilterRegistry.registerGroup(createGroup(group,name))};const registerFilter=(type,group="general",name="",opts={})=>{if(typeof name==="object"){let tempOpts=name;name=tempOpts.name;opts=tempOpts}const{defaults:defaults={},preload:preload=()=>{},view:view=()=>"",edit:edit=()=>"",onMount:onMount=()=>""}=opts;ContactFilterRegistry.registerFilter(createFilter(type,name,group,{display:view,preload:preload,edit:({updateFilter,...filter})=>Fragment([edit(filter)],{onCreate:el=>{setTimeout(()=>{onMount(filter,updateFilter)},50)}})},defaults))};const standardActivityDateFilterOnMount=(filter,updateFilter)=>{$("#filter-date-range, #filter-before, #filter-after, #filter-days").on("change",function(e){const $el=$(this);updateFilter({[$el.prop("name")]:$el.val()});if($el.prop("name")==="date_range"){const $before=$("#filter-before");const $after=$("#filter-after");const $days=$("#filter-days");$before.addClass("hidden");$after.addClass("hidden");$days.addClass("hidden");switch($el.val()){case"between":$before.removeClass("hidden");$after.removeClass("hidden");break;case"day_of":case"after":$after.removeClass("hidden");break;case"before":$before.removeClass("hidden");break;case"x_days":$days.removeClass("hidden");break}}})};const standardActivityDateTitle=(prepend,{date_range,before,after,days:days=0,future:future=false})=>{let ranges=future?futureDateRanges:pastDateRanges;switch(date_range){default:return`${prepend} ${ranges[date_range]?ranges[date_range].replace("X",days).toLowerCase():""}`;case"between":return`${prepend} ${sprintf(_x("between %1$s and %2$s","where %1 and %2 are dates","groundhogg"),`<b>${formatDate(after)}</b>`,`<b>${formatDate(before)}</b>`)}`;case"before":return`${prepend} ${sprintf(_x("before %s","%s is a date","groundhogg"),`<b>${formatDate(before)}</b>`)}`;case"after":return`${prepend} ${sprintf(_x("after %s","%s is a date","groundhogg"),`<b>${formatDate(after)}</b>`)}`;case"day_of":return`${prepend} ${sprintf(_x("on %s","%s is a date","groundhogg"),`<b>${formatDate(after)}</b>`)}`}};const standardActivityDateOptions=({date_range:date_range="24_hours",after:after="",before:before="",days:days=0,future:future=false})=>{return[select({id:"filter-date-range",name:"date_range"},future?futureDateRanges:pastDateRanges,date_range),input({type:"date",value:after.split(" ")[0],id:"filter-after",className:`date ${["between","after","day_of"].includes(date_range)?"":"hidden"}`,name:"after"}),input({type:"date",value:before.split(" ")[0],id:"filter-before",className:`value ${["between","before"].includes(date_range)?"":"hidden"}`,name:"before"}),input({type:"number",value:days,id:"filter-days",min:0,className:`value ${["x_days","next_x_days"].includes(date_range)?"":"hidden"}`,name:"days"})].join("")};const standardActivityDateDefaults={date_range:"any",before:"",after:"",count:1,days:0};const filterCountDefaults={count:1,count_compare:"greater_than_or_equal_to"};const activityFilterComparisons={equals:_x("Exactly","comparison","groundhogg"),less_than:_x("Less than","comparison","groundhogg"),greater_than:_x("More than","comparison","groundhogg"),less_than_or_equal_to:_x("At most","comparison","groundhogg"),greater_than_or_equal_to:_x("At least","comparison","groundhogg")};const filterCount=({count,count_compare})=>{return` 2 2 <div class="space-between" style="gap: 10px"> 3 3 <div class="gh-input-group"> … … 63 63 </div> 64 64 </div> 65 `,filterCount(filter),standardActivityDateOptions(filter)].join("")},onMount(filter,updateFilter){onMount(filter,updateFilter);$("#filter-value,#filter-value-compare").on("change",e=>{updateFilter({[e.target.name]:e.target.value})});filterCountOnMount(updateFilter);standardActivityDateFilterOnMount(filter,updateFilter)},defaults:{...defaults,...standardActivityDateDefaults,...filterCountDefaults,value:0,value_compare:"greater_than_or_equal_to"},...rest})};registerActivityFilterWithValue("custom_activity","activity",__("Custom Activity","groundhogg"),{view:({activity})=>`<b>${activity}</b>`,edit:({activity,...filter})=>{return[input({id:"filter-activity-type",name:"activity",value:activity,placeholder:"custom_activity"}),`<label>${__("Filter by activity meta","groundhogg")}</label>`,`<div id="custom-activity-meta-filters"></div>`].join("")},onMount(filter,updateFilter){$("#filter-activity-type").on("input",e=>{updateFilter({activity:e.target.value})});let{meta_filters:meta_filters=[]}=filter;inputRepeater("#custom-activity-meta-filters",{rows:meta_filters,cells:[props=>input({placeholder:"Key",className:"input",...props}),({value,...props})=>select({selected:value,options:AllComparisons,...props}),props=>input({placeholder:"Value",className:"input",...props})],addRow:()=>["","equals",""],onChange:rows=>{updateFilter({meta_filters:rows})}}).mount()},defaults:{activity:"",meta_filters:[]}});registerFilterGroup("query","Query");registerFilter("saved_search","query",__("Saved Search"),{view:({compare:compare="in",search})=>{return sprintf(__("Is %s search %s"),compare==="in"?"in":"not in",bold(SearchesStore.get(search)?.name))},edit:({compare})=>{return[select({name:"filter_compare",id:"filter-compare",options:{in:__("In"),not_in:__("Not in")},selected:compare}),select({name:"filter_search",id:"filter-search"})].join("")},onMount:({search},updateFilter)=>{SearchesStore.maybeFetchItems().then(items=>{$("#filter-search").select2({data:[{id:"",text:""},...items.map(({id,name})=>({id:id,text:name,selected:id===search}))],placeholder:__("Type to search...")}).on("change",e=>{updateFilter({search:e.target.value})})});$("#filter-compare").on("change",e=>{updateFilter({compare:e.target.value})})},defaults:{compare:"in",search:null},preload:({search})=>{if(!SearchesStore.hasItems()){return SearchesStore.fetchItems([])}}});ContactFilterRegistry.registerFilter(createFilter("sub_query","Sub Query","query",{display:({include_filters:include_filters=[],exclude_filters:exclude_filters=[]})=>{let texts=[ContactFilterRegistry.displayFilters(include_filters),ContactFilterRegistry.displayFilters(exclude_filters)];if(include_filters.length&&exclude_filters.length){return texts.join(' <abbr title="exclude">and exclude</abbr> ')}if(exclude_filters.length){return sprintf('<abbr title="exclude">Exclude</abbr> %s',texts[1])}if(include_filters.length){return texts[0]}throw new Error("No filters defined.")},edit:({include_filters:include_filters=[],exclude_filters:exclude_filters=[],updateFilter})=>{return Fragment([Div({className:"include-search-filters"},[Filters({id:"sub-query-filters",filters:include_filters,filterRegistry:ContactFilterRegistry,onChange:include_filters=>updateFilter({include_filters:include_filters})})]),Div({className:"exclude-search-filters"},[Filters({id:"sub-query-exclude-filters",filters:exclude_filters,filterRegistry:ContactFilterRegistry,onChange:exclude_filters=>updateFilter({exclude_filters:exclude_filters})})])])},preload:({include_filters:include_filters=[],exclude_filters:exclude_filters=[]})=>{return Promise.all([ContactFilterRegistry.preloadFilters(include_filters),ContactFilterRegistry.preloadFilters(exclude_filters)])}},{}));ContactFilterRegistry.registerFilter(createFilter("secondary_related","Is Child Of","query",{edit:({object_type:object_type="",object_id:object_id="",updateFilter})=>Fragment([Input({id:"object-type",name:"object_type",value:object_type,placeholder:"Parent Type",onInput:e=>{updateFilter({object_type:e.target.value})}}),Input({type:"number",id:"object-id",name:"object_id",value:object_id,placeholder:"Parent ID",min:0,onInput:e=>{updateFilter({object_id:e.target.value})}})]),display:({object_type,object_id})=>{if(!object_type){throw new Error("Type be defined")}if(!object_id){return`Is a child of ${object_type}`}return`Is a child of ${object_type} with ID ${object_id}`}},{object_type:"contact"}));ContactFilterRegistry.registerFilter(createFilter("primary_related","Is Parent Of","query",{edit:({object_type:object_type="",object_id:object_id="",updateFilter})=>Fragment([Input({id:"object-type",name:"object_type",value:object_type,placeholder:"Child Type",onInput:e=>{updateFilter({object_type:e.target.value})}}),Input({type:"number",id:"object-id",name:"object_id",value:object_id,placeholder:"Child ID",min:0,onInput:e=>{updateFilter({object_id:e.target.value})}})]),display:({object_type,object_id})=>{if(!object_type){throw new Error("Type must be defined")}if(!object_id){return`Is a parent of ${object_type}`}return`Is a parent of ${object_type} with ID ${object_id}`}},{object_type:"contact"}));registerFilterGroup("date","Date");const CurrentDateCompareFilterFactory=(id,name,type,formatter)=>createFilter(id,name,"date",{edit:({compare:compare="",after:after="",before:before="",updateFilter})=>Fragment([Select({id:"select-compare",selected:compare,options:{after:"After",before:"Before",between:"Between"},onChange:e=>{updateFilter({compare:e.target.value})}}),compare==="before"?null:Input({type:type,id:"after-date",name:"after_date",value:after,placeholder:"After...",onChange:e=>{updateFilter({after:e.target.value})}}),compare==="after"?null:Input({type:type,id:"before-date",name:"before_date",value:before,placeholder:"Before...",min:0,onInput:e=>{updateFilter({before:e.target.value})}})]),display:({compare:compare="",after,before})=>{let prefix=`<b>${name}</b>`;switch(compare){case"between":return ComparisonsTitleGenerators.between(prefix,formatter(after),formatter(before));case"after":return ComparisonsTitleGenerators.after(prefix,formatter(after));case"before":return ComparisonsTitleGenerators.before(prefix,formatter(before));default:throw new Error("Invalid date comparison.")}}},{compare:"between",before:"",after:""});ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory("current_datetime","Current Date & Time","datetime-local",formatDateTime));ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory("current_date","Current Date","date",formatDate));ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory("current_time","Current Time","time",time=>formatTime(`2000-01-01T${time}`))); if(!Groundhogg.filters){Groundhogg.filters={}}Groundhogg.filters.ContactFilters=ContactFilters;Groundhogg.filters.ContactFilterDisplay=ContactFilterDisplay;Groundhogg.filters.ContactFilterRegistry=ContactFilterRegistry;Groundhogg.filters.functions={createFilters:createFilters,registerFilter:registerFilter,registerFilterGroup:registerFilterGroup,ComparisonsTitleGenerators:ComparisonsTitleGenerators,AllComparisons:AllComparisons,NumericComparisons:NumericComparisons,StringComparisons:StringComparisons,standardActivityDateOptions:standardActivityDateOptions,standardActivityDateTitle:standardActivityDateTitle,standardActivityDateDefaults:standardActivityDateDefaults,standardActivityDateFilterOnMount:standardActivityDateFilterOnMount,BasicTextFilter:BasicTextFilter,registerActivityFilter:registerActivityFilter,registerActivityFilterWithValue:registerActivityFilterWithValue}})(jQuery);65 `,filterCount(filter),standardActivityDateOptions(filter)].join("")},onMount(filter,updateFilter){onMount(filter,updateFilter);$("#filter-value,#filter-value-compare").on("change",e=>{updateFilter({[e.target.name]:e.target.value})});filterCountOnMount(updateFilter);standardActivityDateFilterOnMount(filter,updateFilter)},defaults:{...defaults,...standardActivityDateDefaults,...filterCountDefaults,value:0,value_compare:"greater_than_or_equal_to"},...rest})};registerActivityFilterWithValue("custom_activity","activity",__("Custom Activity","groundhogg"),{view:({activity})=>`<b>${activity}</b>`,edit:({activity,...filter})=>{return[input({id:"filter-activity-type",name:"activity",value:activity,placeholder:"custom_activity"}),`<label>${__("Filter by activity meta","groundhogg")}</label>`,`<div id="custom-activity-meta-filters"></div>`].join("")},onMount(filter,updateFilter){$("#filter-activity-type").on("input",e=>{updateFilter({activity:e.target.value})});let{meta_filters:meta_filters=[]}=filter;inputRepeater("#custom-activity-meta-filters",{rows:meta_filters,cells:[props=>input({placeholder:"Key",className:"input",...props}),({value,...props})=>select({selected:value,options:AllComparisons,...props}),props=>input({placeholder:"Value",className:"input",...props})],addRow:()=>["","equals",""],onChange:rows=>{updateFilter({meta_filters:rows})}}).mount()},defaults:{activity:"",meta_filters:[]}});registerFilterGroup("query","Query");registerFilter("saved_search","query",__("Saved Search"),{view:({compare:compare="in",search})=>{return sprintf(__("Is %s search %s"),compare==="in"?"in":"not in",bold(SearchesStore.get(search)?.name))},edit:({compare})=>{return[select({name:"filter_compare",id:"filter-compare",options:{in:__("In"),not_in:__("Not in")},selected:compare}),select({name:"filter_search",id:"filter-search"})].join("")},onMount:({search},updateFilter)=>{SearchesStore.maybeFetchItems().then(items=>{$("#filter-search").select2({data:[{id:"",text:""},...items.map(({id,name})=>({id:id,text:name,selected:id===search}))],placeholder:__("Type to search...")}).on("change",e=>{updateFilter({search:e.target.value})})});$("#filter-compare").on("change",e=>{updateFilter({compare:e.target.value})})},defaults:{compare:"in",search:null},preload:({search})=>{if(!SearchesStore.hasItems()){return SearchesStore.fetchItems([])}}});ContactFilterRegistry.registerFilter(createFilter("sub_query","Sub Query","query",{display:({include_filters:include_filters=[],exclude_filters:exclude_filters=[]})=>{let texts=[ContactFilterRegistry.displayFilters(include_filters),ContactFilterRegistry.displayFilters(exclude_filters)];if(include_filters.length&&exclude_filters.length){return texts.join(' <abbr title="exclude">and exclude</abbr> ')}if(exclude_filters.length){return sprintf('<abbr title="exclude">Exclude</abbr> %s',texts[1])}if(include_filters.length){return texts[0]}throw new Error("No filters defined.")},edit:({include_filters:include_filters=[],exclude_filters:exclude_filters=[],updateFilter})=>{return Fragment([Div({className:"include-search-filters"},[Filters({id:"sub-query-filters",filters:include_filters,filterRegistry:ContactFilterRegistry,onChange:include_filters=>updateFilter({include_filters:include_filters})})]),Div({className:"exclude-search-filters"},[Filters({id:"sub-query-exclude-filters",filters:exclude_filters,filterRegistry:ContactFilterRegistry,onChange:exclude_filters=>updateFilter({exclude_filters:exclude_filters})})])])},preload:({include_filters:include_filters=[],exclude_filters:exclude_filters=[]})=>{return Promise.all([ContactFilterRegistry.preloadFilters(include_filters),ContactFilterRegistry.preloadFilters(exclude_filters)])}},{}));ContactFilterRegistry.registerFilter(createFilter("secondary_related","Is Child Of","query",{edit:({object_type:object_type="",object_id:object_id="",updateFilter})=>Fragment([Input({id:"object-type",name:"object_type",value:object_type,placeholder:"Parent Type",onInput:e=>{updateFilter({object_type:e.target.value})}}),Input({type:"number",id:"object-id",name:"object_id",value:object_id,placeholder:"Parent ID",min:0,onInput:e=>{updateFilter({object_id:e.target.value})}})]),display:({object_type,object_id})=>{if(!object_type){throw new Error("Type be defined")}if(!object_id){return`Is a child of ${object_type}`}return`Is a child of ${object_type} with ID ${object_id}`}},{object_type:"contact"}));ContactFilterRegistry.registerFilter(createFilter("primary_related","Is Parent Of","query",{edit:({object_type:object_type="",object_id:object_id="",updateFilter})=>Fragment([Input({id:"object-type",name:"object_type",value:object_type,placeholder:"Child Type",onInput:e=>{updateFilter({object_type:e.target.value})}}),Input({type:"number",id:"object-id",name:"object_id",value:object_id,placeholder:"Child ID",min:0,onInput:e=>{updateFilter({object_id:e.target.value})}})]),display:({object_type,object_id})=>{if(!object_type){throw new Error("Type must be defined")}if(!object_id){return`Is a parent of ${object_type}`}return`Is a parent of ${object_type} with ID ${object_id}`}},{object_type:"contact"}));registerFilterGroup("date","Date");const CurrentDateCompareFilterFactory=(id,name,type,formatter)=>createFilter(id,name,"date",{edit:({compare:compare="",after:after="",before:before="",updateFilter})=>Fragment([Select({id:"select-compare",selected:compare,options:{after:"After",before:"Before",between:"Between"},onChange:e=>{updateFilter({compare:e.target.value})}}),compare==="before"?null:Input({type:type,id:"after-date",name:"after_date",value:after,placeholder:"After...",onChange:e=>{updateFilter({after:e.target.value})}}),compare==="after"?null:Input({type:type,id:"before-date",name:"before_date",value:before,placeholder:"Before...",min:0,onInput:e=>{updateFilter({before:e.target.value})}})]),display:({compare:compare="",after,before})=>{let prefix=`<b>${name}</b>`;switch(compare){case"between":return ComparisonsTitleGenerators.between(prefix,formatter(after),formatter(before));case"after":return ComparisonsTitleGenerators.after(prefix,formatter(after));case"before":return ComparisonsTitleGenerators.before(prefix,formatter(before));default:throw new Error("Invalid date comparison.")}}},{compare:"between",before:"",after:""});ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory("current_datetime","Current Date & Time","datetime-local",formatDateTime));ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory("current_date","Current Date","date",formatDate));ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory("current_time","Current Time","time",time=>formatTime(`2000-01-01T${time}`)));registerFilterGroup("submissions","Submissions");ContactFilterRegistry.registerFilter(createPastDateFilter("form_submissions","Form Submissions","submissions",{edit:({form_id:form_id="",form_type:form_type="",meta_filters:meta_filters=[],updateFilter:updateFilter=()=>{}})=>{return Fragment([Input({placeholder:"ID",id:"form_id",name:"form_id",value:form_id,onChange:e=>updateFilter({form_id:e.target.value})}),Input({placeholder:"Type",id:"type",name:"type",value:form_type,onChange:e=>updateFilter({form_type:e.target.value})}),`<label>${__("Filter by submission meta","groundhogg")}</label>`,InputRepeater({id:"submission-meta-filters",rows:meta_filters,cells:[props=>Input({...props,placeholder:"Key"}),({value,...props})=>Select({selected:value,options:AllComparisons,...props}),props=>Input({...props,placeholder:"Value"})],fillRow:()=>["","equals",""],onChange:rows=>{updateFilter({meta_filters:rows})}})])}}));if(!Groundhogg.filters){Groundhogg.filters={}}Groundhogg.filters.ContactFilters=ContactFilters;Groundhogg.filters.ContactFilterDisplay=ContactFilterDisplay;Groundhogg.filters.ContactFilterRegistry=ContactFilterRegistry;Groundhogg.filters.functions={createFilters:createFilters,registerFilter:registerFilter,registerFilterGroup:registerFilterGroup,ComparisonsTitleGenerators:ComparisonsTitleGenerators,AllComparisons:AllComparisons,NumericComparisons:NumericComparisons,StringComparisons:StringComparisons,standardActivityDateOptions:standardActivityDateOptions,standardActivityDateTitle:standardActivityDateTitle,standardActivityDateDefaults:standardActivityDateDefaults,standardActivityDateFilterOnMount:standardActivityDateFilterOnMount,BasicTextFilter:BasicTextFilter,registerActivityFilter:registerActivityFilter,registerActivityFilterWithValue:registerActivityFilterWithValue}})(jQuery); -
groundhogg/tags/4.2.2/db/contacts.php
r3320187 r3321992 270 270 271 271 // where based, not ID based 272 if ( is_array( $row_id_or_where ) || ! empty( $where) ) {272 if ( is_array( $row_id_or_where ) || ( ! empty( $where ) && is_array( $where ) ) ) { 273 273 274 274 // don't allow bulk updating some columns … … 284 284 } 285 285 286 $column = ! empty( $where ) && is_string( $where ) ? $where : $this->primary_key; 287 286 288 if ( isset( $data['email'] ) ) { 287 289 … … 293 295 // check to see if this email address is already in use 294 296 $query = new Table_Query( $this ); 295 $query->where()->notEquals( 'ID', $row_id_or_where )->equals( 'email', $data['email'] );297 $query->where()->notEquals( $column, $row_id_or_where )->equals( 'email', $data['email'] ); 296 298 if ( $query->count() > 0 ) { 297 299 unset( $data['email'] ); … … 303 305 // check to see if this user_id is being used by another contact 304 306 $query = new Table_Query( $this ); 305 $query->where()->notEquals( 'ID', $row_id_or_where )->equals( 'user_id', $data['user_id'] ); 307 308 $query->where()->notEquals( $column, $row_id_or_where )->equals( 'user_id', $data['user_id'] ); 309 306 310 // it is being used by another contact :/ 307 311 if ( $query->count() > 0 ) { … … 500 504 break; 501 505 case 'optin_status': 506 $cols[ $key ] = Preferences::sanitize( $val ); 507 break; 502 508 case 'owner_id': 503 509 case 'user_id': -
groundhogg/tags/4.2.2/groundhogg.php
r3320187 r3321992 4 4 * Plugin URI: https://www.groundhogg.io/?utm_source=wp-plugins&utm_campaign=plugin-uri&utm_medium=wp-dash 5 5 * Description: CRM and marketing automation for WordPress 6 * Version: 4.2. 16 * Version: 4.2.2 7 7 * Author: Groundhogg Inc. 8 8 * Author URI: https://www.groundhogg.io/?utm_source=wp-plugins&utm_campaign=author-uri&utm_medium=wp-dash … … 25 25 } 26 26 27 define( 'GROUNDHOGG_VERSION', '4.2. 1' );27 define( 'GROUNDHOGG_VERSION', '4.2.2' ); 28 28 define( 'GROUNDHOGG_PREVIOUS_STABLE_VERSION', '4.2' ); 29 29 -
groundhogg/tags/4.2.2/includes/classes/traits/file-box.php
r3145628 r3321992 3 3 namespace Groundhogg\Classes\Traits; 4 4 5 use WP_Error; 5 6 use function Groundhogg\convert_to_local_time; 6 7 use function Groundhogg\encrypt; … … 32 33 33 34 /** 35 * Return the list of allowed mimes for this object 36 * 37 * @return array 38 */ 39 public function get_allowed_mime_types() { 40 41 $allowed_mimes = get_allowed_mime_types(); 42 43 return apply_filters( "groundhogg/{$this->get_object_type()}/allowed_mimes", $allowed_mimes ); 44 } 45 46 /** 34 47 * Upload a file 35 48 * … … 42 55 public function upload_file( &$file ) { 43 56 44 if ( ! isset_not_empty( $file, 'name' ) ) { 45 return new \WP_Error( 'invalid_file_name', __( 'Invalid file name.', 'groundhogg' ) ); 46 } 47 48 $file['name'] = sanitize_file_name( $file['name'] ); 49 50 $upload_overrides = array( 'test_form' => false ); 51 52 if ( ! function_exists( 'wp_handle_upload' ) ) { 57 $folder = $this->get_uploads_folder_subdir() . DIRECTORY_SEPARATOR . $this->get_upload_folder_basename(); 58 $result = files()->safe_file_upload( $file, $this->get_allowed_mime_types(), $folder ); 59 60 return $result; 61 } 62 63 /** 64 * Copy a file given an uploaded URL 65 * 66 * @param string $url_or_path path or url of asset to upload 67 * @param bool $delete_tmp whether to delete the temp file after 68 * 69 * @return array|string[]|\WP_Error 70 */ 71 public function copy_file( $url_or_path, $delete_tmp = true ) { 72 73 if ( ! function_exists( 'download_url' ) ) { 53 74 require_once( ABSPATH . '/wp-admin/includes/file.php' ); 54 75 } 55 76 56 $this->get_uploads_folder();57 add_filter( 'upload_dir', [ $this, 'map_upload' ] );58 $mfile = wp_handle_upload( $file, $upload_overrides );59 remove_filter( 'upload_dir', [ $this, 'map_upload' ] );60 61 if ( isset_not_empty( $mfile, 'error' ) ) {62 return new \WP_Error( 'bad_upload', __( 'Unable to upload file.', 'groundhogg' ) );63 }64 65 return $mfile;66 }67 68 /**69 * Copy a file given an uploaded URL70 *71 * @param string $url url of the file to copy72 * @param bool $delete_tmp whether to delete the tgemp file after73 *74 * @return bool75 */76 public function copy_file( $url_or_path, $delete_tmp = true ) {77 78 if ( ! function_exists( 'download_url' ) ) {79 require_once ABSPATH . '/wp-admin/includes/file.php';80 }81 82 // File cannot be copied83 77 if ( ! is_copyable_file( $url_or_path ) ) { 84 return false; 85 } 86 87 // path given 88 if ( file_exists( $url_or_path ) ) { 89 $tmp_file = $url_or_path; 90 // if using path, force false 91 $delete_tmp = false; 92 } else { 93 94 // We should only be doing this for files which are already uploaded to the server, 95 // We should reject urls for sites that aren't this one 96 if ( get_hostname( $url_or_path ) !== get_hostname() ) { 97 return false; 98 } 99 100 add_filter( 'https_local_ssl_verify', '__return_false' ); 101 add_filter( 'https_ssl_verify', '__return_false' ); 102 103 $tmp_file = download_url( $url_or_path, 300, false ); 104 105 if ( is_wp_error( $tmp_file ) ) { 106 return false; 107 } 108 } 109 110 if ( ! is_dir( $this->get_uploads_folder() ['path'] ) ) { 111 mkdir( $this->get_uploads_folder() ['path'] ); 112 } 113 try { 114 copy( $tmp_file, $this->get_uploads_folder()['path'] . '/' . basename( $url_or_path ) ); 115 } catch ( \Exception $e ) { 116 } 117 118 if ( $delete_tmp ) { 119 @unlink( $tmp_file ); 120 } 121 122 return true; 78 return new WP_Error( 'not_copyable', 'This file can\'t be uploaded.' ); 79 } 80 81 $file = [ 82 'name' => wp_basename( $url_or_path ), 83 // Original filename on the user's system 84 'tmp_name' => file_exists( $url_or_path ) ? $url_or_path : download_url( $url_or_path ), 85 // Temporary filename on the server 86 ]; 87 88 $folder = $this->get_uploads_folder_subdir() . DIRECTORY_SEPARATOR . $this->get_upload_folder_basename(); 89 90 $result = files()->safe_file_sideload( $file, $this->get_allowed_mime_types(), $folder ); 91 92 if ( is_wp_error( $result ) ) { 93 @unlink( $file['tmp_name'] ); 94 } 95 96 return $result; 123 97 } 124 98 … … 133 107 } 134 108 135 public function get_uploads_folder_subdir() {109 public function get_uploads_folder_subdir() { 136 110 return $this->get_object_type() . 's'; 137 111 } … … 218 192 219 193 // Might have to create the directory 220 if ( ! is_dir( $uploads_dir['path'] ) ) {194 if ( ! is_dir( $uploads_dir['path'] ) ) { 221 195 wp_mkdir_p( $uploads_dir['path'] ); 222 196 } -
groundhogg/tags/4.2.2/includes/contact-query.php
r3320187 r3321992 1154 1154 $activityQuery->where()->compare( "$alias.meta_value", $value, $compare ); 1155 1155 } 1156 } 1157 1158 /** 1159 * Filter by the custom activity 1160 * 1161 * @param $filter 1162 * @param Where $where 1163 * 1164 * @return void 1165 */ 1166 public static function filter_form_submissions( $filter, Where $where ) { 1167 1168 $filter = wp_parse_args( $filter, [ 1169 'step_id' => '', 1170 'form_type' => '', 1171 'name' => '', 1172 'meta_filters' => [] 1173 ] ); 1174 1175 $submissionQuery = new Table_Query( 'submissions' ); 1176 $submissionQuery->setSelect( 'contact_id' ); 1177 1178 if ( ! empty( $filter['step_id'] ) ) { 1179 $submissionQuery->where->equals( 'step_id', $filter['step_id'] ); 1180 } 1181 1182 if ( ! empty( $filter['form_type'] ) ) { 1183 $submissionQuery->where->equals( 'type', $filter['form_type'] ); 1184 } 1185 1186 Filters::mysqlDateTime( 'date_created', $filter, $submissionQuery->where() ); 1187 1188 foreach ( $filter['meta_filters'] as $metaFilter ) { 1189 1190 [ 0 => $key, 1 => $compare, 2 => $value ] = $metaFilter; 1191 1192 if ( ! $key || ! $compare ) { 1193 continue; 1194 } 1195 1196 $alias = $submissionQuery->joinMeta( $key ); 1197 $submissionQuery->where()->compare( "$alias.meta_value", $value, $compare ); 1198 } 1199 1200 $where->in( 'ID', $submissionQuery ); 1156 1201 } 1157 1202 -
groundhogg/tags/4.2.2/includes/functions.php
r3320187 r3321992 2678 2678 // passed path as the value 2679 2679 if ( file_exists( $value ) ) { 2680 $ files[ $column] = $value;2680 $copy[] = $value; 2681 2681 } // Get from $_FILES 2682 2682 else if ( isset_not_empty( $_FILES, $column ) ) { … … 7485 7485 */ 7486 7486 function is_copyable_file( $file ) { 7487 return file_exists( $file ) || get_hostname( $file ) === get_hostname(); 7487 7488 // the file exists on the server 7489 if ( file_exists( $file ) ) { 7490 // no streams allowed here, only absolute paths 7491 return ! str_contains( $file, '://' ); 7492 } 7493 7494 // if a url was provided using https:// check that the file is hosted locally first 7495 return get_hostname( $file ) === get_hostname(); 7488 7496 } 7489 7497 … … 8589 8597 8590 8598 /** 8591 * A dd a filter that removes itself after being calledonce8599 * Alias for add_filter_use_once 8592 8600 * 8593 8601 * @param string $filter … … 8599 8607 */ 8600 8608 function add_self_removing_filter( string $filter, callable $callback, int $priority = 10, int $args = 1 ) { 8609 _deprecated_function( __FUNCTION__, '4.2', __NAMESPACE__ . '\add_filter_use_once' ); 8610 return add_filter_use_once( $filter, $callback, $priority, $args ); 8611 } 8612 8613 /** 8614 * Add a filter that removes itself after being called once 8615 * 8616 * @param string $filter 8617 * @param callable $callback 8618 * @param int $priority 8619 * @param int $args 8620 * 8621 * @return bool 8622 */ 8623 function add_filter_use_once( string $filter, callable $callback, int $priority = 10, int $args = 1 ) { 8601 8624 8602 8625 $callbackWrapper = function ( ...$args ) use ( &$callbackWrapper, $filter, $callback, $priority ) { -
groundhogg/tags/4.2.2/includes/rewrites.php
r3145628 r3321992 371 371 exit(); 372 372 break; 373 374 373 case 'files': 375 374 … … 378 377 $file_path = wp_normalize_path( $groundhogg_path . DIRECTORY_SEPARATOR . $short_path ); 379 378 380 if ( ! $file_path || ! file_exists( $file_path ) || ! is_file( $file_path ) ) { 379 // guard against ../../ traversal attack 380 if ( ! $file_path || ! file_exists( $file_path ) || ! is_file( $file_path ) || ! Files::is_file_within_directory( $file_path, $groundhogg_path ) ) { 381 381 wp_die( 'The requested file was not found.', 'File not found.', [ 'status' => 404 ] ); 382 382 } -
groundhogg/tags/4.2.2/includes/utils/files.php
r3264477 r3321992 99 99 * @param string $subdir 100 100 * @param string $file_path 101 * @param bool $create_folders101 * @param bool $create_folders 102 102 * 103 103 * @return string … … 199 199 } 200 200 201 /** 202 * @var array 203 */ 204 protected $uploads_path = []; 205 206 /** 207 * Change the default upload directory 208 * 209 * @param $param 210 * 211 * @return mixed 212 */ 213 public function files_upload_dir( $param ) { 214 $param['path'] = $this->uploads_path['path']; 215 $param['url'] = $this->uploads_path['url']; 216 $param['subdir'] = $this->uploads_path['subdir']; 217 218 return $param; 219 } 220 221 /** 222 * Initialize the base upload path 223 * 224 * @param string $where 225 */ 226 private function set_uploads_path( $where='imports' ) { 227 $this->uploads_path['subdir'] = Plugin::$instance->utils->files->get_base_uploads_dir(); 228 $this->uploads_path['path'] = Plugin::$instance->utils->files->get_uploads_dir( $where, '', true ); 229 $this->uploads_path['url'] = Plugin::$instance->utils->files->get_uploads_dir( $where, '', true ); 201 function is_allowed_stream( $file ) { 202 203 $allowed_streams = [ 204 'https:', 205 'http:', 206 ]; 207 208 } 209 210 /** 211 * Sideload a file 212 * 213 * @param $file array from constructed to resemble something from $_FILES 214 * @param $allowed_mime_types array list of allowed mime types for this upload 215 * @param $folder string the folder to upload to within the /wp-content/uploads/groundhogg directory 216 * 217 * @return array|string[]|WP_Error 218 * 219 * @see wp_check_filetype_and_ext() 220 * @see get_allowed_mime_types() 221 */ 222 function safe_file_sideload( &$file, $allowed_mime_types, $folder ) { 223 224 $uploads_dir = wp_get_upload_dir(); 225 226 // given local path to a file, only allow sideloading within the uploads directory 227 // protects against ../../ traversal attack 228 if ( file_exists( $file['tmp_name'] ) && ! self::is_file_within_directory( $file['tmp_name'], $uploads_dir['basedir'] ) ) { 229 return new WP_Error( 'nope', 'File upload not allowed.' ); 230 } 231 232 return $this->safe_file_upload( $file, $allowed_mime_types, $folder, true ); 233 } 234 235 /** 236 * Upload a file safely 237 * 238 * @param $file array from $_FILES 239 * @param $allowed_mime_types array list of allowed mime types for this upload 240 * @param $folder string the folder to upload to within the /wp-content/uploads/groundhogg directory 241 * 242 * @return array|string[]|WP_Error 243 * 244 * @see wp_check_filetype_and_ext() 245 * @see get_allowed_mime_types() 246 */ 247 function safe_file_upload( &$file, $allowed_mime_types, $folder, $sideload = false ) { 248 249 if ( ! function_exists( '_wp_handle_upload' ) ) { 250 require_once( ABSPATH . '/wp-admin/includes/file.php' ); 251 } 252 253 // do some basic checks on the file content to test for malicious content 254 if ( self::is_malicious( $file['tmp_name'], $file['name'] ) ) { 255 return new WP_Error( 'file_upload_malicious', 'Potentially malicious upload detected.' ); 256 } 257 258 $upload_overrides = [ 259 'test_form' => false, 260 'test_size' => true, 261 'test_type' => true, 262 'mimes' => $allowed_mime_types, 263 ]; 264 265 $path = $this->get_uploads_dir( $folder ); 266 $url = $this->get_uploads_url( $folder ); 267 268 $change_upload_dir = fn( $uploads ) => array_merge( $uploads, [ 269 'path' => $path, 270 'url' => $url, 271 'subdir' => basename( $path ), 272 'basedir' => dirname( $path ), 273 'baseurl' => dirname( $url ), 274 ] ); 275 276 add_filter( 'upload_dir', $change_upload_dir ); 277 278 $result = _wp_handle_upload( 279 $file, 280 $upload_overrides, 281 null, 282 $sideload ? 'wp_handle_sideload' : 'wp_handle_upload' 283 ); 284 285 remove_filter( 'upload_dir', $change_upload_dir ); 286 287 if ( isset( $result['error'] ) ) { 288 return new WP_Error( 'file_upload_failed', $result['error'] ); 289 } 290 291 return $result; 230 292 } 231 293 … … 238 300 * @return array|bool|WP_Error 239 301 */ 240 function upload( &$file, $where='imports' ) { 241 $upload_overrides = array( 'test_form' => false ); 242 243 if ( ! function_exists( 'wp_handle_upload' ) ) { 244 require_once( ABSPATH . '/wp-admin/includes/file.php' ); 245 } 246 247 $this->set_uploads_path( $where ); 248 249 add_filter( 'upload_dir', array( $this, 'files_upload_dir' ) ); 250 $mfile = wp_handle_upload( $file, $upload_overrides ); 251 remove_filter( 'upload_dir', array( $this, 'files_upload_dir' ) ); 252 253 if ( isset( $mfile['error'] ) ) { 254 255 if ( empty( $mfile['error'] ) ) { 256 $mfile['error'] = _x( 'Could not upload file.', 'error', 'groundhogg' ); 302 function upload( &$file, $where = 'imports' ) { 303 return $this->safe_file_upload( $file, get_allowed_mime_types(), $where ); 304 } 305 306 /** 307 * Check if a file path is strictly inside a base directory. 308 * 309 * @param string $file_path Absolute path to the file. 310 * @param string $base_dir Absolute base directory to restrict access to. 311 * 312 * @return bool True if the file is inside the base directory, false otherwise. 313 */ 314 static public function is_file_within_directory( string $file_path, string $base_dir ): bool { 315 $file_path_real = realpath( $file_path ); 316 $base_dir_real = realpath( $base_dir ); 317 318 if ( ! $file_path_real || ! $base_dir_real ) { 319 return false; 320 } 321 322 $file_path_real = wp_normalize_path( $file_path_real ); 323 $base_dir_real = wp_normalize_path( rtrim( $base_dir_real, '/' ) ); 324 325 return str_starts_with( $file_path_real, $base_dir_real . '/' ); 326 } 327 328 /** 329 * Basic check for potentially malicious file content. 330 * 331 * @param string $filepath Path to the file on disk. 332 * 333 * @return bool True if the file appears malicious, false otherwise. 334 */ 335 static public function is_malicious( string $filepath, string $filename ): bool { 336 337 // Fail safe: unreadable or missing file 338 if ( ! file_exists( $filepath ) || ! is_readable( $filepath ) ) { 339 return true; 340 } 341 342 // Block known dangerous filenames 343 $basename = strtolower( basename( $filename ) ); 344 $blocked_filenames = [ 345 '.htaccess', 346 '.user.ini', 347 'php.ini', 348 'web.config', 349 '.env', 350 ]; 351 352 if ( in_array( $basename, $blocked_filenames, true ) ) { 353 return true; 354 } 355 356 // Read the first part of the file 357 $bytes = file_get_contents( $filepath, false, null, 0, 2048 ); 358 if ( $bytes === false ) { 359 return true; 360 } 361 362 $bytes = strtolower( $bytes ); 363 364 // Look for dangerous patterns 365 $danger_signatures = [ 366 '<?php', // Executable PHP 367 '__halt_compiler()', // PHAR stub 368 'phar://', // PHAR stream reference 369 'base64_decode', // Obfuscation 370 'eval(', // Dynamic execution 371 'shell_exec', // Command execution 372 'passthru', // Another dangerous system function 373 ]; 374 375 foreach ( $danger_signatures as $needle ) { 376 if ( strpos( $bytes, $needle ) !== false ) { 377 return true; 257 378 } 258 259 return new WP_Error( 'BAD_UPLOAD', $mfile['error'] ); 260 } 261 262 return $mfile; 379 } 380 381 return false; 263 382 } 264 383 -
groundhogg/trunk/README.txt
r3320187 r3321992 7 7 Tested up to: 6.8 8 8 Requires PHP: 7.1 9 Stable tag: 4.2. 19 Stable tag: 4.2.2 10 10 License: GPLv3 11 11 License URI: https://www.gnu.org/licenses/gpl.md … … 354 354 355 355 == Changelog == 356 357 = 4.2.2 (2025-07-03) = 358 * FIXED User ID not syncing. 359 * FIXED Arbitrary file upload vulnerability. Credit to Patchstack for practicing responsible disclosure. 356 360 357 361 = 4.2.1 (2025-06-30) = -
groundhogg/trunk/admin/contacts/cards/user.php
r3029787 r3321992 10 10 * @var $contact \Groundhogg\Contact 11 11 */ 12 13 /* Auto link the account before we see the create account form. */ 14 $contact->auto_link_account(); 12 15 13 16 if ( $contact->get_userdata() ): -
groundhogg/trunk/admin/contacts/parts/contact-editor.php
r3209982 r3321992 19 19 * @var $contact Contact 20 20 */ 21 22 //var_dump( $contact );23 24 /* Auto link the account before we see the create account form. */25 $contact->auto_link_account();26 21 27 22 $tabs = [ -
groundhogg/trunk/admin/tools/tools-page.php
r3320187 r3321992 11 11 use Groundhogg\DB\Query\Table_Query; 12 12 use Groundhogg\Extension_Upgrader; 13 use Groundhogg\Files; 13 14 use Groundhogg\License_Manager; 14 15 use Groundhogg\Plugin; … … 54 55 class Tools_Page extends Tabbed_Admin_Page { 55 56 56 protected $uploads_path = [];57 58 57 /** 59 58 * @var \Groundhogg\Bulk_Jobs\Import_Contacts … … 562 561 } 563 562 564 $file = get_array_var( $_FILES, 'import_file' ); 565 566 $validate = wp_check_filetype( $file['name'], [ 'csv' => 'text/csv' ] ); 567 568 if ( $validate['ext'] !== 'csv' || $validate['type'] !== 'text/csv' ) { 569 return new WP_Error( 'invalid_csv', sprintf( 'Please upload a valid CSV. Expected mime type of <i>text/csv</i> but got <i>%s</i>', esc_html( $file['type'] ) ) ); 570 } 571 572 $file['name'] = wp_unique_filename( files()->get_csv_imports_dir(), $file['name'] ); 573 574 $result = $this->handle_file_upload( $file ); 563 $file = $_FILES['import_file']; 564 565 $result = files()->safe_file_upload( $file, [ 566 'csv' => 'text/csv', 567 ], 'imports' ); 575 568 576 569 if ( is_wp_error( $result ) ) { 577 578 if ( is_multisite() ) {579 return new WP_Error( 'multisite_add_csv', 'Could not import because CSV is not an allowed file type on this subsite. Please add CSV to the list of allowed file types in the network settings.' );580 }581 582 570 return $result; 583 571 } … … 586 574 'action' => 'map', 587 575 'tab' => 'import', 588 'import' => urlencode( basename( $result[' file'] ) ),576 'import' => urlencode( basename( $result['path'] ) ), 589 577 ] ) ); 590 578 591 }592 593 /**594 * Upload a file to the Groundhogg file directory595 *596 * @param $file array597 * @param $config598 *599 * @return array|bool|WP_Error600 */601 private function handle_file_upload( $file ) {602 $upload_overrides = array( 'test_form' => false );603 604 if ( ! function_exists( 'wp_handle_upload' ) ) {605 require_once( ABSPATH . '/wp-admin/includes/file.php' );606 }607 608 $this->set_uploads_path();609 610 add_filter( 'upload_dir', array( $this, 'files_upload_dir' ) );611 $mfile = wp_handle_upload( $file, $upload_overrides );612 remove_filter( 'upload_dir', array( $this, 'files_upload_dir' ) );613 614 if ( isset( $mfile['error'] ) ) {615 616 if ( empty( $mfile['error'] ) ) {617 $mfile['error'] = _x( 'Could not upload file.', 'error', 'groundhogg' );618 }619 620 return new WP_Error( 'BAD_UPLOAD', $mfile['error'] );621 }622 623 return $mfile;624 }625 626 /**627 * Change the default upload directory628 *629 * @param $param630 *631 * @return mixed632 */633 public function files_upload_dir( $param ) {634 $param['path'] = $this->uploads_path['path'];635 $param['url'] = $this->uploads_path['url'];636 $param['subdir'] = $this->uploads_path['subdir'];637 638 return $param;639 }640 641 /**642 * Initialize the base upload path643 */644 private function set_uploads_path() {645 $this->uploads_path['subdir'] = Plugin::$instance->utils->files->get_base_uploads_dir();646 $this->uploads_path['path'] = Plugin::$instance->utils->files->get_csv_imports_dir();647 $this->uploads_path['url'] = Plugin::$instance->utils->files->get_csv_imports_url();648 579 } 649 580 … … 1232 1163 $file_path = wp_normalize_path( $groundhogg_path . DIRECTORY_SEPARATOR . $short_path ); 1233 1164 1234 if ( ! $file_path || ! file_exists( $file_path ) || ! is_file( $file_path ) ) { 1165 // guard against ../../ traversal attack 1166 if ( ! $file_path || ! file_exists( $file_path ) || ! is_file( $file_path ) || ! Files::is_file_within_directory( $file_path, $groundhogg_path ) ) { 1235 1167 wp_die( 'The requested file was not found.', 'File not found.', [ 'status' => 404 ] ); 1236 1168 } -
groundhogg/trunk/api/v4/files-api.php
r2507120 r3321992 38 38 39 39 register_rest_route( self::NAME_SPACE, "/files/exports", [ 40 [41 'methods' => WP_REST_Server::CREATABLE,42 'callback' => [ $this, 'create_exports' ],43 'permission_callback' => [ $this, 'create_exports_permissions_callback' ]44 ],40 // [ 41 // 'methods' => WP_REST_Server::CREATABLE, 42 // 'callback' => [ $this, 'create_exports' ], 43 // 'permission_callback' => [ $this, 'create_exports_permissions_callback' ] 44 // ], 45 45 [ 46 46 'methods' => WP_REST_Server::READABLE, … … 71 71 foreach ( $_FILES as $FILE ) { 72 72 73 $validate = wp_check_filetype( $FILE['name'], [ 'csv' => 'text/csv' ] ); 74 75 if ( $validate['ext'] !== 'csv' || $validate['text/csv'] ) { 76 return self::ERROR_500( 'invalid_csv', sprintf( 'Please upload a valid CSV. Expected mime type of <i>text/csv</i> but got <i>%s</i>', esc_html( $FILE['type'] ) ) ); 77 } 78 79 $file_name = str_replace( '.csv', '', $FILE['name'] ); 80 $file_name .= '-' . current_time( 'mysql', true ) . '.csv'; 81 82 $FILE['name'] = sanitize_file_name( $file_name ); 83 84 $result = files()->upload( $FILE, 'imports' ); 73 $result = files()->safe_file_upload( $FILE, [ 74 'csv' => 'text/csv' 75 ], 'imports' ); 85 76 86 77 if ( is_wp_error( $result ) ) { -
groundhogg/trunk/assets/js/admin/filters/contacts.js
r3264477 r3321992 52 52 Input, 53 53 Div, 54 makeEl 54 makeEl, 55 InputRepeater 55 56 } = MakeEl 56 57 … … 2435 2436 ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory('current_time', 'Current Time', 'time', ( time ) => formatTime(`2000-01-01T${time}`) )) 2436 2437 2438 registerFilterGroup( 'submissions', 'Submissions' ) 2439 2440 ContactFilterRegistry.registerFilter( createPastDateFilter( 'form_submissions', 'Form Submissions', 'submissions', { 2441 edit: ({ 2442 form_id = '', 2443 form_type = '', 2444 meta_filters = [], 2445 updateFilter = () => {} 2446 }) => { 2447 2448 return Fragment([ 2449 2450 Input({ 2451 placeholder: 'ID', 2452 id: 'form_id', 2453 name: 'form_id', 2454 value: form_id, 2455 onChange: e => updateFilter({ 2456 form_id: e.target.value 2457 }) 2458 }), 2459 2460 Input({ 2461 placeholder: 'Type', 2462 id: 'type', 2463 name: 'type', 2464 value: form_type, 2465 onChange: e => updateFilter({ 2466 form_type: e.target.value 2467 }) 2468 }), 2469 2470 `<label>${ __('Filter by submission meta', 'groundhogg') }</label>`, 2471 InputRepeater({ 2472 id: 'submission-meta-filters', 2473 rows: meta_filters, 2474 cells: [ 2475 props => Input({...props, placeholder: 'Key'}), 2476 ({ 2477 value, 2478 ...props 2479 }) => Select({ 2480 selected: value, 2481 options : AllComparisons, 2482 ...props, 2483 }), 2484 props => Input({...props, placeholder: 'Value'}), 2485 ], 2486 fillRow: () => [ 2487 '', 2488 'equals', 2489 '' 2490 ], 2491 onChange: rows => { 2492 updateFilter({ 2493 meta_filters: rows 2494 }) 2495 } 2496 }) 2497 ]) 2498 } 2499 } ) ) 2500 2437 2501 if (!Groundhogg.filters) { 2438 2502 Groundhogg.filters = {} -
groundhogg/trunk/assets/js/admin/filters/contacts.min.js
r3264477 r3321992 1 (function($){const{input,select,orList,andList,bold,inputRepeater}=Groundhogg.element;const{broadcastPicker,funnelPicker,tagPicker,emailPicker,linkPicker,metaValuePicker,metaPicker,userMetaPicker}=Groundhogg.pickers;const{assoc2array}=Groundhogg.functions;const{broadcasts:BroadcastsStore,emails:EmailsStore,tags:TagsStore,funnels:FunnelsStore,searches:SearchesStore}=Groundhogg.stores;const{sprintf,__,_x,_n}=wp.i18n;const{formatDate,formatDateTime,formatTime}=Groundhogg.formatting;const{Fragment,ItemPicker,Select,Input,Div,makeEl }=MakeEl;const{Filters,FilterRegistry,createFilter,createGroup,FilterDisplay,createDateFilter,createPastDateFilter,createStringFilter,createNumberFilter,createTimeFilter,unsubReasons}=Groundhogg.filters;const{ComparisonsTitleGenerators,AllComparisons,StringComparisons,NumericComparisons,pastDateRanges,futureDateRanges,allDateRanges}=Groundhogg.filters.comparisons;const ContactFilterRegistry=FilterRegistry({});const uid=function(){return Date.now().toString(36)+Math.random().toString(36).substring(2)};const createFilters=(el="",filters=[],onChange=f=>{console.log(f)})=>({el:el,onChange:onChange,filters:Array.isArray(filters)?filters:[],id:uid(),init(){this.mount()},mount(){let container=document.querySelector(el);container.innerHTML="";document.querySelector(el).appendChild(ContactFilters(this.id,this.filters,this.onChange))}});const ContactFilters=(id,filters,onChange)=>Filters({id:id,filterRegistry:ContactFilterRegistry,filters:filters,onChange:onChange});const ContactFilterDisplay=filters=>FilterDisplay({filters:filters,filterRegistry:ContactFilterRegistry});const registerFilterGroup=(group,name)=>{ContactFilterRegistry.registerGroup(createGroup(group,name))};const registerFilter=(type,group="general",name="",opts={})=>{if(typeof name==="object"){let tempOpts=name;name=tempOpts.name;opts=tempOpts}const{defaults:defaults={},preload:preload=()=>{},view:view=()=>"",edit:edit=()=>"",onMount:onMount=()=>""}=opts;ContactFilterRegistry.registerFilter(createFilter(type,name,group,{display:view,preload:preload,edit:({updateFilter,...filter})=>Fragment([edit(filter)],{onCreate:el=>{setTimeout(()=>{onMount(filter,updateFilter)},50)}})},defaults))};const standardActivityDateFilterOnMount=(filter,updateFilter)=>{$("#filter-date-range, #filter-before, #filter-after, #filter-days").on("change",function(e){const $el=$(this);updateFilter({[$el.prop("name")]:$el.val()});if($el.prop("name")==="date_range"){const $before=$("#filter-before");const $after=$("#filter-after");const $days=$("#filter-days");$before.addClass("hidden");$after.addClass("hidden");$days.addClass("hidden");switch($el.val()){case"between":$before.removeClass("hidden");$after.removeClass("hidden");break;case"day_of":case"after":$after.removeClass("hidden");break;case"before":$before.removeClass("hidden");break;case"x_days":$days.removeClass("hidden");break}}})};const standardActivityDateTitle=(prepend,{date_range,before,after,days:days=0,future:future=false})=>{let ranges=future?futureDateRanges:pastDateRanges;switch(date_range){default:return`${prepend} ${ranges[date_range]?ranges[date_range].replace("X",days).toLowerCase():""}`;case"between":return`${prepend} ${sprintf(_x("between %1$s and %2$s","where %1 and %2 are dates","groundhogg"),`<b>${formatDate(after)}</b>`,`<b>${formatDate(before)}</b>`)}`;case"before":return`${prepend} ${sprintf(_x("before %s","%s is a date","groundhogg"),`<b>${formatDate(before)}</b>`)}`;case"after":return`${prepend} ${sprintf(_x("after %s","%s is a date","groundhogg"),`<b>${formatDate(after)}</b>`)}`;case"day_of":return`${prepend} ${sprintf(_x("on %s","%s is a date","groundhogg"),`<b>${formatDate(after)}</b>`)}`}};const standardActivityDateOptions=({date_range:date_range="24_hours",after:after="",before:before="",days:days=0,future:future=false})=>{return[select({id:"filter-date-range",name:"date_range"},future?futureDateRanges:pastDateRanges,date_range),input({type:"date",value:after.split(" ")[0],id:"filter-after",className:`date ${["between","after","day_of"].includes(date_range)?"":"hidden"}`,name:"after"}),input({type:"date",value:before.split(" ")[0],id:"filter-before",className:`value ${["between","before"].includes(date_range)?"":"hidden"}`,name:"before"}),input({type:"number",value:days,id:"filter-days",min:0,className:`value ${["x_days","next_x_days"].includes(date_range)?"":"hidden"}`,name:"days"})].join("")};const standardActivityDateDefaults={date_range:"any",before:"",after:"",count:1,days:0};const filterCountDefaults={count:1,count_compare:"greater_than_or_equal_to"};const activityFilterComparisons={equals:_x("Exactly","comparison","groundhogg"),less_than:_x("Less than","comparison","groundhogg"),greater_than:_x("More than","comparison","groundhogg"),less_than_or_equal_to:_x("At most","comparison","groundhogg"),greater_than_or_equal_to:_x("At least","comparison","groundhogg")};const filterCount=({count,count_compare})=>{return`1 (function($){const{input,select,orList,andList,bold,inputRepeater}=Groundhogg.element;const{broadcastPicker,funnelPicker,tagPicker,emailPicker,linkPicker,metaValuePicker,metaPicker,userMetaPicker}=Groundhogg.pickers;const{assoc2array}=Groundhogg.functions;const{broadcasts:BroadcastsStore,emails:EmailsStore,tags:TagsStore,funnels:FunnelsStore,searches:SearchesStore}=Groundhogg.stores;const{sprintf,__,_x,_n}=wp.i18n;const{formatDate,formatDateTime,formatTime}=Groundhogg.formatting;const{Fragment,ItemPicker,Select,Input,Div,makeEl,InputRepeater}=MakeEl;const{Filters,FilterRegistry,createFilter,createGroup,FilterDisplay,createDateFilter,createPastDateFilter,createStringFilter,createNumberFilter,createTimeFilter,unsubReasons}=Groundhogg.filters;const{ComparisonsTitleGenerators,AllComparisons,StringComparisons,NumericComparisons,pastDateRanges,futureDateRanges,allDateRanges}=Groundhogg.filters.comparisons;const ContactFilterRegistry=FilterRegistry({});const uid=function(){return Date.now().toString(36)+Math.random().toString(36).substring(2)};const createFilters=(el="",filters=[],onChange=f=>{console.log(f)})=>({el:el,onChange:onChange,filters:Array.isArray(filters)?filters:[],id:uid(),init(){this.mount()},mount(){let container=document.querySelector(el);container.innerHTML="";document.querySelector(el).appendChild(ContactFilters(this.id,this.filters,this.onChange))}});const ContactFilters=(id,filters,onChange)=>Filters({id:id,filterRegistry:ContactFilterRegistry,filters:filters,onChange:onChange});const ContactFilterDisplay=filters=>FilterDisplay({filters:filters,filterRegistry:ContactFilterRegistry});const registerFilterGroup=(group,name)=>{ContactFilterRegistry.registerGroup(createGroup(group,name))};const registerFilter=(type,group="general",name="",opts={})=>{if(typeof name==="object"){let tempOpts=name;name=tempOpts.name;opts=tempOpts}const{defaults:defaults={},preload:preload=()=>{},view:view=()=>"",edit:edit=()=>"",onMount:onMount=()=>""}=opts;ContactFilterRegistry.registerFilter(createFilter(type,name,group,{display:view,preload:preload,edit:({updateFilter,...filter})=>Fragment([edit(filter)],{onCreate:el=>{setTimeout(()=>{onMount(filter,updateFilter)},50)}})},defaults))};const standardActivityDateFilterOnMount=(filter,updateFilter)=>{$("#filter-date-range, #filter-before, #filter-after, #filter-days").on("change",function(e){const $el=$(this);updateFilter({[$el.prop("name")]:$el.val()});if($el.prop("name")==="date_range"){const $before=$("#filter-before");const $after=$("#filter-after");const $days=$("#filter-days");$before.addClass("hidden");$after.addClass("hidden");$days.addClass("hidden");switch($el.val()){case"between":$before.removeClass("hidden");$after.removeClass("hidden");break;case"day_of":case"after":$after.removeClass("hidden");break;case"before":$before.removeClass("hidden");break;case"x_days":$days.removeClass("hidden");break}}})};const standardActivityDateTitle=(prepend,{date_range,before,after,days:days=0,future:future=false})=>{let ranges=future?futureDateRanges:pastDateRanges;switch(date_range){default:return`${prepend} ${ranges[date_range]?ranges[date_range].replace("X",days).toLowerCase():""}`;case"between":return`${prepend} ${sprintf(_x("between %1$s and %2$s","where %1 and %2 are dates","groundhogg"),`<b>${formatDate(after)}</b>`,`<b>${formatDate(before)}</b>`)}`;case"before":return`${prepend} ${sprintf(_x("before %s","%s is a date","groundhogg"),`<b>${formatDate(before)}</b>`)}`;case"after":return`${prepend} ${sprintf(_x("after %s","%s is a date","groundhogg"),`<b>${formatDate(after)}</b>`)}`;case"day_of":return`${prepend} ${sprintf(_x("on %s","%s is a date","groundhogg"),`<b>${formatDate(after)}</b>`)}`}};const standardActivityDateOptions=({date_range:date_range="24_hours",after:after="",before:before="",days:days=0,future:future=false})=>{return[select({id:"filter-date-range",name:"date_range"},future?futureDateRanges:pastDateRanges,date_range),input({type:"date",value:after.split(" ")[0],id:"filter-after",className:`date ${["between","after","day_of"].includes(date_range)?"":"hidden"}`,name:"after"}),input({type:"date",value:before.split(" ")[0],id:"filter-before",className:`value ${["between","before"].includes(date_range)?"":"hidden"}`,name:"before"}),input({type:"number",value:days,id:"filter-days",min:0,className:`value ${["x_days","next_x_days"].includes(date_range)?"":"hidden"}`,name:"days"})].join("")};const standardActivityDateDefaults={date_range:"any",before:"",after:"",count:1,days:0};const filterCountDefaults={count:1,count_compare:"greater_than_or_equal_to"};const activityFilterComparisons={equals:_x("Exactly","comparison","groundhogg"),less_than:_x("Less than","comparison","groundhogg"),greater_than:_x("More than","comparison","groundhogg"),less_than_or_equal_to:_x("At most","comparison","groundhogg"),greater_than_or_equal_to:_x("At least","comparison","groundhogg")};const filterCount=({count,count_compare})=>{return` 2 2 <div class="space-between" style="gap: 10px"> 3 3 <div class="gh-input-group"> … … 63 63 </div> 64 64 </div> 65 `,filterCount(filter),standardActivityDateOptions(filter)].join("")},onMount(filter,updateFilter){onMount(filter,updateFilter);$("#filter-value,#filter-value-compare").on("change",e=>{updateFilter({[e.target.name]:e.target.value})});filterCountOnMount(updateFilter);standardActivityDateFilterOnMount(filter,updateFilter)},defaults:{...defaults,...standardActivityDateDefaults,...filterCountDefaults,value:0,value_compare:"greater_than_or_equal_to"},...rest})};registerActivityFilterWithValue("custom_activity","activity",__("Custom Activity","groundhogg"),{view:({activity})=>`<b>${activity}</b>`,edit:({activity,...filter})=>{return[input({id:"filter-activity-type",name:"activity",value:activity,placeholder:"custom_activity"}),`<label>${__("Filter by activity meta","groundhogg")}</label>`,`<div id="custom-activity-meta-filters"></div>`].join("")},onMount(filter,updateFilter){$("#filter-activity-type").on("input",e=>{updateFilter({activity:e.target.value})});let{meta_filters:meta_filters=[]}=filter;inputRepeater("#custom-activity-meta-filters",{rows:meta_filters,cells:[props=>input({placeholder:"Key",className:"input",...props}),({value,...props})=>select({selected:value,options:AllComparisons,...props}),props=>input({placeholder:"Value",className:"input",...props})],addRow:()=>["","equals",""],onChange:rows=>{updateFilter({meta_filters:rows})}}).mount()},defaults:{activity:"",meta_filters:[]}});registerFilterGroup("query","Query");registerFilter("saved_search","query",__("Saved Search"),{view:({compare:compare="in",search})=>{return sprintf(__("Is %s search %s"),compare==="in"?"in":"not in",bold(SearchesStore.get(search)?.name))},edit:({compare})=>{return[select({name:"filter_compare",id:"filter-compare",options:{in:__("In"),not_in:__("Not in")},selected:compare}),select({name:"filter_search",id:"filter-search"})].join("")},onMount:({search},updateFilter)=>{SearchesStore.maybeFetchItems().then(items=>{$("#filter-search").select2({data:[{id:"",text:""},...items.map(({id,name})=>({id:id,text:name,selected:id===search}))],placeholder:__("Type to search...")}).on("change",e=>{updateFilter({search:e.target.value})})});$("#filter-compare").on("change",e=>{updateFilter({compare:e.target.value})})},defaults:{compare:"in",search:null},preload:({search})=>{if(!SearchesStore.hasItems()){return SearchesStore.fetchItems([])}}});ContactFilterRegistry.registerFilter(createFilter("sub_query","Sub Query","query",{display:({include_filters:include_filters=[],exclude_filters:exclude_filters=[]})=>{let texts=[ContactFilterRegistry.displayFilters(include_filters),ContactFilterRegistry.displayFilters(exclude_filters)];if(include_filters.length&&exclude_filters.length){return texts.join(' <abbr title="exclude">and exclude</abbr> ')}if(exclude_filters.length){return sprintf('<abbr title="exclude">Exclude</abbr> %s',texts[1])}if(include_filters.length){return texts[0]}throw new Error("No filters defined.")},edit:({include_filters:include_filters=[],exclude_filters:exclude_filters=[],updateFilter})=>{return Fragment([Div({className:"include-search-filters"},[Filters({id:"sub-query-filters",filters:include_filters,filterRegistry:ContactFilterRegistry,onChange:include_filters=>updateFilter({include_filters:include_filters})})]),Div({className:"exclude-search-filters"},[Filters({id:"sub-query-exclude-filters",filters:exclude_filters,filterRegistry:ContactFilterRegistry,onChange:exclude_filters=>updateFilter({exclude_filters:exclude_filters})})])])},preload:({include_filters:include_filters=[],exclude_filters:exclude_filters=[]})=>{return Promise.all([ContactFilterRegistry.preloadFilters(include_filters),ContactFilterRegistry.preloadFilters(exclude_filters)])}},{}));ContactFilterRegistry.registerFilter(createFilter("secondary_related","Is Child Of","query",{edit:({object_type:object_type="",object_id:object_id="",updateFilter})=>Fragment([Input({id:"object-type",name:"object_type",value:object_type,placeholder:"Parent Type",onInput:e=>{updateFilter({object_type:e.target.value})}}),Input({type:"number",id:"object-id",name:"object_id",value:object_id,placeholder:"Parent ID",min:0,onInput:e=>{updateFilter({object_id:e.target.value})}})]),display:({object_type,object_id})=>{if(!object_type){throw new Error("Type be defined")}if(!object_id){return`Is a child of ${object_type}`}return`Is a child of ${object_type} with ID ${object_id}`}},{object_type:"contact"}));ContactFilterRegistry.registerFilter(createFilter("primary_related","Is Parent Of","query",{edit:({object_type:object_type="",object_id:object_id="",updateFilter})=>Fragment([Input({id:"object-type",name:"object_type",value:object_type,placeholder:"Child Type",onInput:e=>{updateFilter({object_type:e.target.value})}}),Input({type:"number",id:"object-id",name:"object_id",value:object_id,placeholder:"Child ID",min:0,onInput:e=>{updateFilter({object_id:e.target.value})}})]),display:({object_type,object_id})=>{if(!object_type){throw new Error("Type must be defined")}if(!object_id){return`Is a parent of ${object_type}`}return`Is a parent of ${object_type} with ID ${object_id}`}},{object_type:"contact"}));registerFilterGroup("date","Date");const CurrentDateCompareFilterFactory=(id,name,type,formatter)=>createFilter(id,name,"date",{edit:({compare:compare="",after:after="",before:before="",updateFilter})=>Fragment([Select({id:"select-compare",selected:compare,options:{after:"After",before:"Before",between:"Between"},onChange:e=>{updateFilter({compare:e.target.value})}}),compare==="before"?null:Input({type:type,id:"after-date",name:"after_date",value:after,placeholder:"After...",onChange:e=>{updateFilter({after:e.target.value})}}),compare==="after"?null:Input({type:type,id:"before-date",name:"before_date",value:before,placeholder:"Before...",min:0,onInput:e=>{updateFilter({before:e.target.value})}})]),display:({compare:compare="",after,before})=>{let prefix=`<b>${name}</b>`;switch(compare){case"between":return ComparisonsTitleGenerators.between(prefix,formatter(after),formatter(before));case"after":return ComparisonsTitleGenerators.after(prefix,formatter(after));case"before":return ComparisonsTitleGenerators.before(prefix,formatter(before));default:throw new Error("Invalid date comparison.")}}},{compare:"between",before:"",after:""});ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory("current_datetime","Current Date & Time","datetime-local",formatDateTime));ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory("current_date","Current Date","date",formatDate));ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory("current_time","Current Time","time",time=>formatTime(`2000-01-01T${time}`))); if(!Groundhogg.filters){Groundhogg.filters={}}Groundhogg.filters.ContactFilters=ContactFilters;Groundhogg.filters.ContactFilterDisplay=ContactFilterDisplay;Groundhogg.filters.ContactFilterRegistry=ContactFilterRegistry;Groundhogg.filters.functions={createFilters:createFilters,registerFilter:registerFilter,registerFilterGroup:registerFilterGroup,ComparisonsTitleGenerators:ComparisonsTitleGenerators,AllComparisons:AllComparisons,NumericComparisons:NumericComparisons,StringComparisons:StringComparisons,standardActivityDateOptions:standardActivityDateOptions,standardActivityDateTitle:standardActivityDateTitle,standardActivityDateDefaults:standardActivityDateDefaults,standardActivityDateFilterOnMount:standardActivityDateFilterOnMount,BasicTextFilter:BasicTextFilter,registerActivityFilter:registerActivityFilter,registerActivityFilterWithValue:registerActivityFilterWithValue}})(jQuery);65 `,filterCount(filter),standardActivityDateOptions(filter)].join("")},onMount(filter,updateFilter){onMount(filter,updateFilter);$("#filter-value,#filter-value-compare").on("change",e=>{updateFilter({[e.target.name]:e.target.value})});filterCountOnMount(updateFilter);standardActivityDateFilterOnMount(filter,updateFilter)},defaults:{...defaults,...standardActivityDateDefaults,...filterCountDefaults,value:0,value_compare:"greater_than_or_equal_to"},...rest})};registerActivityFilterWithValue("custom_activity","activity",__("Custom Activity","groundhogg"),{view:({activity})=>`<b>${activity}</b>`,edit:({activity,...filter})=>{return[input({id:"filter-activity-type",name:"activity",value:activity,placeholder:"custom_activity"}),`<label>${__("Filter by activity meta","groundhogg")}</label>`,`<div id="custom-activity-meta-filters"></div>`].join("")},onMount(filter,updateFilter){$("#filter-activity-type").on("input",e=>{updateFilter({activity:e.target.value})});let{meta_filters:meta_filters=[]}=filter;inputRepeater("#custom-activity-meta-filters",{rows:meta_filters,cells:[props=>input({placeholder:"Key",className:"input",...props}),({value,...props})=>select({selected:value,options:AllComparisons,...props}),props=>input({placeholder:"Value",className:"input",...props})],addRow:()=>["","equals",""],onChange:rows=>{updateFilter({meta_filters:rows})}}).mount()},defaults:{activity:"",meta_filters:[]}});registerFilterGroup("query","Query");registerFilter("saved_search","query",__("Saved Search"),{view:({compare:compare="in",search})=>{return sprintf(__("Is %s search %s"),compare==="in"?"in":"not in",bold(SearchesStore.get(search)?.name))},edit:({compare})=>{return[select({name:"filter_compare",id:"filter-compare",options:{in:__("In"),not_in:__("Not in")},selected:compare}),select({name:"filter_search",id:"filter-search"})].join("")},onMount:({search},updateFilter)=>{SearchesStore.maybeFetchItems().then(items=>{$("#filter-search").select2({data:[{id:"",text:""},...items.map(({id,name})=>({id:id,text:name,selected:id===search}))],placeholder:__("Type to search...")}).on("change",e=>{updateFilter({search:e.target.value})})});$("#filter-compare").on("change",e=>{updateFilter({compare:e.target.value})})},defaults:{compare:"in",search:null},preload:({search})=>{if(!SearchesStore.hasItems()){return SearchesStore.fetchItems([])}}});ContactFilterRegistry.registerFilter(createFilter("sub_query","Sub Query","query",{display:({include_filters:include_filters=[],exclude_filters:exclude_filters=[]})=>{let texts=[ContactFilterRegistry.displayFilters(include_filters),ContactFilterRegistry.displayFilters(exclude_filters)];if(include_filters.length&&exclude_filters.length){return texts.join(' <abbr title="exclude">and exclude</abbr> ')}if(exclude_filters.length){return sprintf('<abbr title="exclude">Exclude</abbr> %s',texts[1])}if(include_filters.length){return texts[0]}throw new Error("No filters defined.")},edit:({include_filters:include_filters=[],exclude_filters:exclude_filters=[],updateFilter})=>{return Fragment([Div({className:"include-search-filters"},[Filters({id:"sub-query-filters",filters:include_filters,filterRegistry:ContactFilterRegistry,onChange:include_filters=>updateFilter({include_filters:include_filters})})]),Div({className:"exclude-search-filters"},[Filters({id:"sub-query-exclude-filters",filters:exclude_filters,filterRegistry:ContactFilterRegistry,onChange:exclude_filters=>updateFilter({exclude_filters:exclude_filters})})])])},preload:({include_filters:include_filters=[],exclude_filters:exclude_filters=[]})=>{return Promise.all([ContactFilterRegistry.preloadFilters(include_filters),ContactFilterRegistry.preloadFilters(exclude_filters)])}},{}));ContactFilterRegistry.registerFilter(createFilter("secondary_related","Is Child Of","query",{edit:({object_type:object_type="",object_id:object_id="",updateFilter})=>Fragment([Input({id:"object-type",name:"object_type",value:object_type,placeholder:"Parent Type",onInput:e=>{updateFilter({object_type:e.target.value})}}),Input({type:"number",id:"object-id",name:"object_id",value:object_id,placeholder:"Parent ID",min:0,onInput:e=>{updateFilter({object_id:e.target.value})}})]),display:({object_type,object_id})=>{if(!object_type){throw new Error("Type be defined")}if(!object_id){return`Is a child of ${object_type}`}return`Is a child of ${object_type} with ID ${object_id}`}},{object_type:"contact"}));ContactFilterRegistry.registerFilter(createFilter("primary_related","Is Parent Of","query",{edit:({object_type:object_type="",object_id:object_id="",updateFilter})=>Fragment([Input({id:"object-type",name:"object_type",value:object_type,placeholder:"Child Type",onInput:e=>{updateFilter({object_type:e.target.value})}}),Input({type:"number",id:"object-id",name:"object_id",value:object_id,placeholder:"Child ID",min:0,onInput:e=>{updateFilter({object_id:e.target.value})}})]),display:({object_type,object_id})=>{if(!object_type){throw new Error("Type must be defined")}if(!object_id){return`Is a parent of ${object_type}`}return`Is a parent of ${object_type} with ID ${object_id}`}},{object_type:"contact"}));registerFilterGroup("date","Date");const CurrentDateCompareFilterFactory=(id,name,type,formatter)=>createFilter(id,name,"date",{edit:({compare:compare="",after:after="",before:before="",updateFilter})=>Fragment([Select({id:"select-compare",selected:compare,options:{after:"After",before:"Before",between:"Between"},onChange:e=>{updateFilter({compare:e.target.value})}}),compare==="before"?null:Input({type:type,id:"after-date",name:"after_date",value:after,placeholder:"After...",onChange:e=>{updateFilter({after:e.target.value})}}),compare==="after"?null:Input({type:type,id:"before-date",name:"before_date",value:before,placeholder:"Before...",min:0,onInput:e=>{updateFilter({before:e.target.value})}})]),display:({compare:compare="",after,before})=>{let prefix=`<b>${name}</b>`;switch(compare){case"between":return ComparisonsTitleGenerators.between(prefix,formatter(after),formatter(before));case"after":return ComparisonsTitleGenerators.after(prefix,formatter(after));case"before":return ComparisonsTitleGenerators.before(prefix,formatter(before));default:throw new Error("Invalid date comparison.")}}},{compare:"between",before:"",after:""});ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory("current_datetime","Current Date & Time","datetime-local",formatDateTime));ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory("current_date","Current Date","date",formatDate));ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory("current_time","Current Time","time",time=>formatTime(`2000-01-01T${time}`)));registerFilterGroup("submissions","Submissions");ContactFilterRegistry.registerFilter(createPastDateFilter("form_submissions","Form Submissions","submissions",{edit:({form_id:form_id="",form_type:form_type="",meta_filters:meta_filters=[],updateFilter:updateFilter=()=>{}})=>{return Fragment([Input({placeholder:"ID",id:"form_id",name:"form_id",value:form_id,onChange:e=>updateFilter({form_id:e.target.value})}),Input({placeholder:"Type",id:"type",name:"type",value:form_type,onChange:e=>updateFilter({form_type:e.target.value})}),`<label>${__("Filter by submission meta","groundhogg")}</label>`,InputRepeater({id:"submission-meta-filters",rows:meta_filters,cells:[props=>Input({...props,placeholder:"Key"}),({value,...props})=>Select({selected:value,options:AllComparisons,...props}),props=>Input({...props,placeholder:"Value"})],fillRow:()=>["","equals",""],onChange:rows=>{updateFilter({meta_filters:rows})}})])}}));if(!Groundhogg.filters){Groundhogg.filters={}}Groundhogg.filters.ContactFilters=ContactFilters;Groundhogg.filters.ContactFilterDisplay=ContactFilterDisplay;Groundhogg.filters.ContactFilterRegistry=ContactFilterRegistry;Groundhogg.filters.functions={createFilters:createFilters,registerFilter:registerFilter,registerFilterGroup:registerFilterGroup,ComparisonsTitleGenerators:ComparisonsTitleGenerators,AllComparisons:AllComparisons,NumericComparisons:NumericComparisons,StringComparisons:StringComparisons,standardActivityDateOptions:standardActivityDateOptions,standardActivityDateTitle:standardActivityDateTitle,standardActivityDateDefaults:standardActivityDateDefaults,standardActivityDateFilterOnMount:standardActivityDateFilterOnMount,BasicTextFilter:BasicTextFilter,registerActivityFilter:registerActivityFilter,registerActivityFilterWithValue:registerActivityFilterWithValue}})(jQuery); -
groundhogg/trunk/db/contacts.php
r3320187 r3321992 270 270 271 271 // where based, not ID based 272 if ( is_array( $row_id_or_where ) || ! empty( $where) ) {272 if ( is_array( $row_id_or_where ) || ( ! empty( $where ) && is_array( $where ) ) ) { 273 273 274 274 // don't allow bulk updating some columns … … 284 284 } 285 285 286 $column = ! empty( $where ) && is_string( $where ) ? $where : $this->primary_key; 287 286 288 if ( isset( $data['email'] ) ) { 287 289 … … 293 295 // check to see if this email address is already in use 294 296 $query = new Table_Query( $this ); 295 $query->where()->notEquals( 'ID', $row_id_or_where )->equals( 'email', $data['email'] );297 $query->where()->notEquals( $column, $row_id_or_where )->equals( 'email', $data['email'] ); 296 298 if ( $query->count() > 0 ) { 297 299 unset( $data['email'] ); … … 303 305 // check to see if this user_id is being used by another contact 304 306 $query = new Table_Query( $this ); 305 $query->where()->notEquals( 'ID', $row_id_or_where )->equals( 'user_id', $data['user_id'] ); 307 308 $query->where()->notEquals( $column, $row_id_or_where )->equals( 'user_id', $data['user_id'] ); 309 306 310 // it is being used by another contact :/ 307 311 if ( $query->count() > 0 ) { … … 500 504 break; 501 505 case 'optin_status': 506 $cols[ $key ] = Preferences::sanitize( $val ); 507 break; 502 508 case 'owner_id': 503 509 case 'user_id': -
groundhogg/trunk/groundhogg.php
r3320187 r3321992 4 4 * Plugin URI: https://www.groundhogg.io/?utm_source=wp-plugins&utm_campaign=plugin-uri&utm_medium=wp-dash 5 5 * Description: CRM and marketing automation for WordPress 6 * Version: 4.2. 16 * Version: 4.2.2 7 7 * Author: Groundhogg Inc. 8 8 * Author URI: https://www.groundhogg.io/?utm_source=wp-plugins&utm_campaign=author-uri&utm_medium=wp-dash … … 25 25 } 26 26 27 define( 'GROUNDHOGG_VERSION', '4.2. 1' );27 define( 'GROUNDHOGG_VERSION', '4.2.2' ); 28 28 define( 'GROUNDHOGG_PREVIOUS_STABLE_VERSION', '4.2' ); 29 29 -
groundhogg/trunk/includes/classes/traits/file-box.php
r3145628 r3321992 3 3 namespace Groundhogg\Classes\Traits; 4 4 5 use WP_Error; 5 6 use function Groundhogg\convert_to_local_time; 6 7 use function Groundhogg\encrypt; … … 32 33 33 34 /** 35 * Return the list of allowed mimes for this object 36 * 37 * @return array 38 */ 39 public function get_allowed_mime_types() { 40 41 $allowed_mimes = get_allowed_mime_types(); 42 43 return apply_filters( "groundhogg/{$this->get_object_type()}/allowed_mimes", $allowed_mimes ); 44 } 45 46 /** 34 47 * Upload a file 35 48 * … … 42 55 public function upload_file( &$file ) { 43 56 44 if ( ! isset_not_empty( $file, 'name' ) ) { 45 return new \WP_Error( 'invalid_file_name', __( 'Invalid file name.', 'groundhogg' ) ); 46 } 47 48 $file['name'] = sanitize_file_name( $file['name'] ); 49 50 $upload_overrides = array( 'test_form' => false ); 51 52 if ( ! function_exists( 'wp_handle_upload' ) ) { 57 $folder = $this->get_uploads_folder_subdir() . DIRECTORY_SEPARATOR . $this->get_upload_folder_basename(); 58 $result = files()->safe_file_upload( $file, $this->get_allowed_mime_types(), $folder ); 59 60 return $result; 61 } 62 63 /** 64 * Copy a file given an uploaded URL 65 * 66 * @param string $url_or_path path or url of asset to upload 67 * @param bool $delete_tmp whether to delete the temp file after 68 * 69 * @return array|string[]|\WP_Error 70 */ 71 public function copy_file( $url_or_path, $delete_tmp = true ) { 72 73 if ( ! function_exists( 'download_url' ) ) { 53 74 require_once( ABSPATH . '/wp-admin/includes/file.php' ); 54 75 } 55 76 56 $this->get_uploads_folder();57 add_filter( 'upload_dir', [ $this, 'map_upload' ] );58 $mfile = wp_handle_upload( $file, $upload_overrides );59 remove_filter( 'upload_dir', [ $this, 'map_upload' ] );60 61 if ( isset_not_empty( $mfile, 'error' ) ) {62 return new \WP_Error( 'bad_upload', __( 'Unable to upload file.', 'groundhogg' ) );63 }64 65 return $mfile;66 }67 68 /**69 * Copy a file given an uploaded URL70 *71 * @param string $url url of the file to copy72 * @param bool $delete_tmp whether to delete the tgemp file after73 *74 * @return bool75 */76 public function copy_file( $url_or_path, $delete_tmp = true ) {77 78 if ( ! function_exists( 'download_url' ) ) {79 require_once ABSPATH . '/wp-admin/includes/file.php';80 }81 82 // File cannot be copied83 77 if ( ! is_copyable_file( $url_or_path ) ) { 84 return false; 85 } 86 87 // path given 88 if ( file_exists( $url_or_path ) ) { 89 $tmp_file = $url_or_path; 90 // if using path, force false 91 $delete_tmp = false; 92 } else { 93 94 // We should only be doing this for files which are already uploaded to the server, 95 // We should reject urls for sites that aren't this one 96 if ( get_hostname( $url_or_path ) !== get_hostname() ) { 97 return false; 98 } 99 100 add_filter( 'https_local_ssl_verify', '__return_false' ); 101 add_filter( 'https_ssl_verify', '__return_false' ); 102 103 $tmp_file = download_url( $url_or_path, 300, false ); 104 105 if ( is_wp_error( $tmp_file ) ) { 106 return false; 107 } 108 } 109 110 if ( ! is_dir( $this->get_uploads_folder() ['path'] ) ) { 111 mkdir( $this->get_uploads_folder() ['path'] ); 112 } 113 try { 114 copy( $tmp_file, $this->get_uploads_folder()['path'] . '/' . basename( $url_or_path ) ); 115 } catch ( \Exception $e ) { 116 } 117 118 if ( $delete_tmp ) { 119 @unlink( $tmp_file ); 120 } 121 122 return true; 78 return new WP_Error( 'not_copyable', 'This file can\'t be uploaded.' ); 79 } 80 81 $file = [ 82 'name' => wp_basename( $url_or_path ), 83 // Original filename on the user's system 84 'tmp_name' => file_exists( $url_or_path ) ? $url_or_path : download_url( $url_or_path ), 85 // Temporary filename on the server 86 ]; 87 88 $folder = $this->get_uploads_folder_subdir() . DIRECTORY_SEPARATOR . $this->get_upload_folder_basename(); 89 90 $result = files()->safe_file_sideload( $file, $this->get_allowed_mime_types(), $folder ); 91 92 if ( is_wp_error( $result ) ) { 93 @unlink( $file['tmp_name'] ); 94 } 95 96 return $result; 123 97 } 124 98 … … 133 107 } 134 108 135 public function get_uploads_folder_subdir() {109 public function get_uploads_folder_subdir() { 136 110 return $this->get_object_type() . 's'; 137 111 } … … 218 192 219 193 // Might have to create the directory 220 if ( ! is_dir( $uploads_dir['path'] ) ) {194 if ( ! is_dir( $uploads_dir['path'] ) ) { 221 195 wp_mkdir_p( $uploads_dir['path'] ); 222 196 } -
groundhogg/trunk/includes/contact-query.php
r3320187 r3321992 1154 1154 $activityQuery->where()->compare( "$alias.meta_value", $value, $compare ); 1155 1155 } 1156 } 1157 1158 /** 1159 * Filter by the custom activity 1160 * 1161 * @param $filter 1162 * @param Where $where 1163 * 1164 * @return void 1165 */ 1166 public static function filter_form_submissions( $filter, Where $where ) { 1167 1168 $filter = wp_parse_args( $filter, [ 1169 'step_id' => '', 1170 'form_type' => '', 1171 'name' => '', 1172 'meta_filters' => [] 1173 ] ); 1174 1175 $submissionQuery = new Table_Query( 'submissions' ); 1176 $submissionQuery->setSelect( 'contact_id' ); 1177 1178 if ( ! empty( $filter['step_id'] ) ) { 1179 $submissionQuery->where->equals( 'step_id', $filter['step_id'] ); 1180 } 1181 1182 if ( ! empty( $filter['form_type'] ) ) { 1183 $submissionQuery->where->equals( 'type', $filter['form_type'] ); 1184 } 1185 1186 Filters::mysqlDateTime( 'date_created', $filter, $submissionQuery->where() ); 1187 1188 foreach ( $filter['meta_filters'] as $metaFilter ) { 1189 1190 [ 0 => $key, 1 => $compare, 2 => $value ] = $metaFilter; 1191 1192 if ( ! $key || ! $compare ) { 1193 continue; 1194 } 1195 1196 $alias = $submissionQuery->joinMeta( $key ); 1197 $submissionQuery->where()->compare( "$alias.meta_value", $value, $compare ); 1198 } 1199 1200 $where->in( 'ID', $submissionQuery ); 1156 1201 } 1157 1202 -
groundhogg/trunk/includes/functions.php
r3320187 r3321992 2678 2678 // passed path as the value 2679 2679 if ( file_exists( $value ) ) { 2680 $ files[ $column] = $value;2680 $copy[] = $value; 2681 2681 } // Get from $_FILES 2682 2682 else if ( isset_not_empty( $_FILES, $column ) ) { … … 7485 7485 */ 7486 7486 function is_copyable_file( $file ) { 7487 return file_exists( $file ) || get_hostname( $file ) === get_hostname(); 7487 7488 // the file exists on the server 7489 if ( file_exists( $file ) ) { 7490 // no streams allowed here, only absolute paths 7491 return ! str_contains( $file, '://' ); 7492 } 7493 7494 // if a url was provided using https:// check that the file is hosted locally first 7495 return get_hostname( $file ) === get_hostname(); 7488 7496 } 7489 7497 … … 8589 8597 8590 8598 /** 8591 * A dd a filter that removes itself after being calledonce8599 * Alias for add_filter_use_once 8592 8600 * 8593 8601 * @param string $filter … … 8599 8607 */ 8600 8608 function add_self_removing_filter( string $filter, callable $callback, int $priority = 10, int $args = 1 ) { 8609 _deprecated_function( __FUNCTION__, '4.2', __NAMESPACE__ . '\add_filter_use_once' ); 8610 return add_filter_use_once( $filter, $callback, $priority, $args ); 8611 } 8612 8613 /** 8614 * Add a filter that removes itself after being called once 8615 * 8616 * @param string $filter 8617 * @param callable $callback 8618 * @param int $priority 8619 * @param int $args 8620 * 8621 * @return bool 8622 */ 8623 function add_filter_use_once( string $filter, callable $callback, int $priority = 10, int $args = 1 ) { 8601 8624 8602 8625 $callbackWrapper = function ( ...$args ) use ( &$callbackWrapper, $filter, $callback, $priority ) { -
groundhogg/trunk/includes/rewrites.php
r3145628 r3321992 371 371 exit(); 372 372 break; 373 374 373 case 'files': 375 374 … … 378 377 $file_path = wp_normalize_path( $groundhogg_path . DIRECTORY_SEPARATOR . $short_path ); 379 378 380 if ( ! $file_path || ! file_exists( $file_path ) || ! is_file( $file_path ) ) { 379 // guard against ../../ traversal attack 380 if ( ! $file_path || ! file_exists( $file_path ) || ! is_file( $file_path ) || ! Files::is_file_within_directory( $file_path, $groundhogg_path ) ) { 381 381 wp_die( 'The requested file was not found.', 'File not found.', [ 'status' => 404 ] ); 382 382 } -
groundhogg/trunk/includes/utils/files.php
r3264477 r3321992 99 99 * @param string $subdir 100 100 * @param string $file_path 101 * @param bool $create_folders101 * @param bool $create_folders 102 102 * 103 103 * @return string … … 199 199 } 200 200 201 /** 202 * @var array 203 */ 204 protected $uploads_path = []; 205 206 /** 207 * Change the default upload directory 208 * 209 * @param $param 210 * 211 * @return mixed 212 */ 213 public function files_upload_dir( $param ) { 214 $param['path'] = $this->uploads_path['path']; 215 $param['url'] = $this->uploads_path['url']; 216 $param['subdir'] = $this->uploads_path['subdir']; 217 218 return $param; 219 } 220 221 /** 222 * Initialize the base upload path 223 * 224 * @param string $where 225 */ 226 private function set_uploads_path( $where='imports' ) { 227 $this->uploads_path['subdir'] = Plugin::$instance->utils->files->get_base_uploads_dir(); 228 $this->uploads_path['path'] = Plugin::$instance->utils->files->get_uploads_dir( $where, '', true ); 229 $this->uploads_path['url'] = Plugin::$instance->utils->files->get_uploads_dir( $where, '', true ); 201 function is_allowed_stream( $file ) { 202 203 $allowed_streams = [ 204 'https:', 205 'http:', 206 ]; 207 208 } 209 210 /** 211 * Sideload a file 212 * 213 * @param $file array from constructed to resemble something from $_FILES 214 * @param $allowed_mime_types array list of allowed mime types for this upload 215 * @param $folder string the folder to upload to within the /wp-content/uploads/groundhogg directory 216 * 217 * @return array|string[]|WP_Error 218 * 219 * @see wp_check_filetype_and_ext() 220 * @see get_allowed_mime_types() 221 */ 222 function safe_file_sideload( &$file, $allowed_mime_types, $folder ) { 223 224 $uploads_dir = wp_get_upload_dir(); 225 226 // given local path to a file, only allow sideloading within the uploads directory 227 // protects against ../../ traversal attack 228 if ( file_exists( $file['tmp_name'] ) && ! self::is_file_within_directory( $file['tmp_name'], $uploads_dir['basedir'] ) ) { 229 return new WP_Error( 'nope', 'File upload not allowed.' ); 230 } 231 232 return $this->safe_file_upload( $file, $allowed_mime_types, $folder, true ); 233 } 234 235 /** 236 * Upload a file safely 237 * 238 * @param $file array from $_FILES 239 * @param $allowed_mime_types array list of allowed mime types for this upload 240 * @param $folder string the folder to upload to within the /wp-content/uploads/groundhogg directory 241 * 242 * @return array|string[]|WP_Error 243 * 244 * @see wp_check_filetype_and_ext() 245 * @see get_allowed_mime_types() 246 */ 247 function safe_file_upload( &$file, $allowed_mime_types, $folder, $sideload = false ) { 248 249 if ( ! function_exists( '_wp_handle_upload' ) ) { 250 require_once( ABSPATH . '/wp-admin/includes/file.php' ); 251 } 252 253 // do some basic checks on the file content to test for malicious content 254 if ( self::is_malicious( $file['tmp_name'], $file['name'] ) ) { 255 return new WP_Error( 'file_upload_malicious', 'Potentially malicious upload detected.' ); 256 } 257 258 $upload_overrides = [ 259 'test_form' => false, 260 'test_size' => true, 261 'test_type' => true, 262 'mimes' => $allowed_mime_types, 263 ]; 264 265 $path = $this->get_uploads_dir( $folder ); 266 $url = $this->get_uploads_url( $folder ); 267 268 $change_upload_dir = fn( $uploads ) => array_merge( $uploads, [ 269 'path' => $path, 270 'url' => $url, 271 'subdir' => basename( $path ), 272 'basedir' => dirname( $path ), 273 'baseurl' => dirname( $url ), 274 ] ); 275 276 add_filter( 'upload_dir', $change_upload_dir ); 277 278 $result = _wp_handle_upload( 279 $file, 280 $upload_overrides, 281 null, 282 $sideload ? 'wp_handle_sideload' : 'wp_handle_upload' 283 ); 284 285 remove_filter( 'upload_dir', $change_upload_dir ); 286 287 if ( isset( $result['error'] ) ) { 288 return new WP_Error( 'file_upload_failed', $result['error'] ); 289 } 290 291 return $result; 230 292 } 231 293 … … 238 300 * @return array|bool|WP_Error 239 301 */ 240 function upload( &$file, $where='imports' ) { 241 $upload_overrides = array( 'test_form' => false ); 242 243 if ( ! function_exists( 'wp_handle_upload' ) ) { 244 require_once( ABSPATH . '/wp-admin/includes/file.php' ); 245 } 246 247 $this->set_uploads_path( $where ); 248 249 add_filter( 'upload_dir', array( $this, 'files_upload_dir' ) ); 250 $mfile = wp_handle_upload( $file, $upload_overrides ); 251 remove_filter( 'upload_dir', array( $this, 'files_upload_dir' ) ); 252 253 if ( isset( $mfile['error'] ) ) { 254 255 if ( empty( $mfile['error'] ) ) { 256 $mfile['error'] = _x( 'Could not upload file.', 'error', 'groundhogg' ); 302 function upload( &$file, $where = 'imports' ) { 303 return $this->safe_file_upload( $file, get_allowed_mime_types(), $where ); 304 } 305 306 /** 307 * Check if a file path is strictly inside a base directory. 308 * 309 * @param string $file_path Absolute path to the file. 310 * @param string $base_dir Absolute base directory to restrict access to. 311 * 312 * @return bool True if the file is inside the base directory, false otherwise. 313 */ 314 static public function is_file_within_directory( string $file_path, string $base_dir ): bool { 315 $file_path_real = realpath( $file_path ); 316 $base_dir_real = realpath( $base_dir ); 317 318 if ( ! $file_path_real || ! $base_dir_real ) { 319 return false; 320 } 321 322 $file_path_real = wp_normalize_path( $file_path_real ); 323 $base_dir_real = wp_normalize_path( rtrim( $base_dir_real, '/' ) ); 324 325 return str_starts_with( $file_path_real, $base_dir_real . '/' ); 326 } 327 328 /** 329 * Basic check for potentially malicious file content. 330 * 331 * @param string $filepath Path to the file on disk. 332 * 333 * @return bool True if the file appears malicious, false otherwise. 334 */ 335 static public function is_malicious( string $filepath, string $filename ): bool { 336 337 // Fail safe: unreadable or missing file 338 if ( ! file_exists( $filepath ) || ! is_readable( $filepath ) ) { 339 return true; 340 } 341 342 // Block known dangerous filenames 343 $basename = strtolower( basename( $filename ) ); 344 $blocked_filenames = [ 345 '.htaccess', 346 '.user.ini', 347 'php.ini', 348 'web.config', 349 '.env', 350 ]; 351 352 if ( in_array( $basename, $blocked_filenames, true ) ) { 353 return true; 354 } 355 356 // Read the first part of the file 357 $bytes = file_get_contents( $filepath, false, null, 0, 2048 ); 358 if ( $bytes === false ) { 359 return true; 360 } 361 362 $bytes = strtolower( $bytes ); 363 364 // Look for dangerous patterns 365 $danger_signatures = [ 366 '<?php', // Executable PHP 367 '__halt_compiler()', // PHAR stub 368 'phar://', // PHAR stream reference 369 'base64_decode', // Obfuscation 370 'eval(', // Dynamic execution 371 'shell_exec', // Command execution 372 'passthru', // Another dangerous system function 373 ]; 374 375 foreach ( $danger_signatures as $needle ) { 376 if ( strpos( $bytes, $needle ) !== false ) { 377 return true; 257 378 } 258 259 return new WP_Error( 'BAD_UPLOAD', $mfile['error'] ); 260 } 261 262 return $mfile; 379 } 380 381 return false; 263 382 } 264 383
Note: See TracChangeset
for help on using the changeset viewer.