Plugin Directory

Changeset 3321992


Ignore:
Timestamp:
07/03/2025 07:40:50 PM (9 months ago)
Author:
trainingbusinesspros
Message:

Update to version 4.2.2 from GitHub

Location:
groundhogg
Files:
28 edited
1 copied

Legend:

Unmodified
Added
Removed
  • groundhogg/tags/4.2.2/README.txt

    r3320187 r3321992  
    77Tested up to: 6.8
    88Requires PHP: 7.1
    9 Stable tag: 4.2.1
     9Stable tag: 4.2.2
    1010License: GPLv3
    1111License URI: https://www.gnu.org/licenses/gpl.md
     
    354354
    355355== 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.
    356360
    357361= 4.2.1 (2025-06-30) =
  • groundhogg/tags/4.2.2/admin/contacts/cards/user.php

    r3029787 r3321992  
    1010 * @var $contact \Groundhogg\Contact
    1111 */
     12
     13/* Auto link the account before we see the create account form. */
     14$contact->auto_link_account();
    1215
    1316if ( $contact->get_userdata() ):
  • groundhogg/tags/4.2.2/admin/contacts/parts/contact-editor.php

    r3209982 r3321992  
    1919 * @var $contact Contact
    2020 */
    21 
    22 //var_dump( $contact );
    23 
    24 /* Auto link the account before we see the create account form. */
    25 $contact->auto_link_account();
    2621
    2722$tabs = [
  • groundhogg/tags/4.2.2/admin/tools/tools-page.php

    r3320187 r3321992  
    1111use Groundhogg\DB\Query\Table_Query;
    1212use Groundhogg\Extension_Upgrader;
     13use Groundhogg\Files;
    1314use Groundhogg\License_Manager;
    1415use Groundhogg\Plugin;
     
    5455class Tools_Page extends Tabbed_Admin_Page {
    5556
    56     protected $uploads_path = [];
    57 
    5857    /**
    5958     * @var \Groundhogg\Bulk_Jobs\Import_Contacts
     
    562561        }
    563562
    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' );
    575568
    576569        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 
    582570            return $result;
    583571        }
     
    586574            'action' => 'map',
    587575            'tab'    => 'import',
    588             'import' => urlencode( basename( $result['file'] ) ),
     576            'import' => urlencode( basename( $result['path'] ) ),
    589577        ] ) );
    590578
    591     }
    592 
    593     /**
    594      * Upload a file to the Groundhogg file directory
    595      *
    596      * @param $file array
    597      * @param $config
    598      *
    599      * @return array|bool|WP_Error
    600      */
    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 directory
    628      *
    629      * @param $param
    630      *
    631      * @return mixed
    632      */
    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 path
    643      */
    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();
    648579    }
    649580
     
    12321163        $file_path       = wp_normalize_path( $groundhogg_path . DIRECTORY_SEPARATOR . $short_path );
    12331164
    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 ) ) {
    12351167            wp_die( 'The requested file was not found.', 'File not found.', [ 'status' => 404 ] );
    12361168        }
  • groundhogg/tags/4.2.2/api/v4/files-api.php

    r2507120 r3321992  
    3838
    3939        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//          ],
    4545            [
    4646                'methods'             => WP_REST_Server::READABLE,
     
    7171        foreach ( $_FILES as $FILE ) {
    7272
    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' );
    8576
    8677            if ( is_wp_error( $result ) ) {
  • groundhogg/tags/4.2.2/assets/js/admin/filters/contacts.js

    r3264477 r3321992  
    5252    Input,
    5353    Div,
    54     makeEl
     54    makeEl,
     55    InputRepeater
    5556  } = MakeEl
    5657
     
    24352436  ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory('current_time', 'Current Time', 'time', ( time ) => formatTime(`2000-01-01T${time}`) ))
    24362437
     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
    24372501  if (!Groundhogg.filters) {
    24382502    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`
    22        <div class="space-between" style="gap: 10px">
    33            <div class="gh-input-group">
     
    6363                  </div>
    6464              </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  
    270270
    271271        // 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 ) ) ) {
    273273
    274274            // don't allow bulk updating some columns
     
    284284        }
    285285
     286        $column = ! empty( $where ) && is_string( $where ) ? $where : $this->primary_key;
     287
    286288        if ( isset( $data['email'] ) ) {
    287289
     
    293295            // check to see if this email address is already in use
    294296            $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'] );
    296298            if ( $query->count() > 0 ) {
    297299                unset( $data['email'] );
     
    303305            // check to see if this user_id is being used by another contact
    304306            $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
    306310            // it is being used by another contact :/
    307311            if ( $query->count() > 0 ) {
     
    500504                    break;
    501505                case 'optin_status':
     506                    $cols[ $key ] = Preferences::sanitize( $val );
     507                    break;
    502508                case 'owner_id':
    503509                case 'user_id':
  • groundhogg/tags/4.2.2/groundhogg.php

    r3320187 r3321992  
    44 * Plugin URI:  https://www.groundhogg.io/?utm_source=wp-plugins&utm_campaign=plugin-uri&utm_medium=wp-dash
    55 * Description: CRM and marketing automation for WordPress
    6  * Version: 4.2.1
     6 * Version: 4.2.2
    77 * Author: Groundhogg Inc.
    88 * Author URI: https://www.groundhogg.io/?utm_source=wp-plugins&utm_campaign=author-uri&utm_medium=wp-dash
     
    2525}
    2626
    27 define( 'GROUNDHOGG_VERSION', '4.2.1' );
     27define( 'GROUNDHOGG_VERSION', '4.2.2' );
    2828define( 'GROUNDHOGG_PREVIOUS_STABLE_VERSION', '4.2' );
    2929
  • groundhogg/tags/4.2.2/includes/classes/traits/file-box.php

    r3145628 r3321992  
    33namespace Groundhogg\Classes\Traits;
    44
     5use WP_Error;
    56use function Groundhogg\convert_to_local_time;
    67use function Groundhogg\encrypt;
     
    3233
    3334    /**
     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    /**
    3447     * Upload a file
    3548     *
     
    4255    public function upload_file( &$file ) {
    4356
    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' ) ) {
    5374            require_once( ABSPATH . '/wp-admin/includes/file.php' );
    5475        }
    5576
    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 URL
    70      *
    71      * @param string $url        url of the file to copy
    72      * @param bool   $delete_tmp whether to delete the tgemp file after
    73      *
    74      * @return bool
    75      */
    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 copied
    8377        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;
    12397    }
    12498
     
    133107    }
    134108
    135     public function get_uploads_folder_subdir(){
     109    public function get_uploads_folder_subdir() {
    136110        return $this->get_object_type() . 's';
    137111    }
     
    218192
    219193        // Might have to create the directory
    220         if ( ! is_dir( $uploads_dir['path'] ) ){
     194        if ( ! is_dir( $uploads_dir['path'] ) ) {
    221195            wp_mkdir_p( $uploads_dir['path'] );
    222196        }
  • groundhogg/tags/4.2.2/includes/contact-query.php

    r3320187 r3321992  
    11541154            $activityQuery->where()->compare( "$alias.meta_value", $value, $compare );
    11551155        }
     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 );
    11561201    }
    11571202
  • groundhogg/tags/4.2.2/includes/functions.php

    r3320187 r3321992  
    26782678                // passed path as the value
    26792679                if ( file_exists( $value ) ) {
    2680                     $files[ $column ] = $value;
     2680                    $copy[] = $value;
    26812681                } // Get from $_FILES
    26822682                else if ( isset_not_empty( $_FILES, $column ) ) {
     
    74857485 */
    74867486function 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();
    74887496}
    74897497
     
    85898597
    85908598/**
    8591  * Add a filter that removes itself after being called once
     8599 * Alias for add_filter_use_once
    85928600 *
    85938601 * @param string   $filter
     
    85998607 */
    86008608function 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 */
     8623function add_filter_use_once( string $filter, callable $callback, int $priority = 10, int $args = 1 ) {
    86018624
    86028625    $callbackWrapper = function ( ...$args ) use ( &$callbackWrapper, $filter, $callback, $priority ) {
  • groundhogg/tags/4.2.2/includes/rewrites.php

    r3145628 r3321992  
    371371                exit();
    372372                break;
    373 
    374373            case 'files':
    375374
     
    378377                $file_path       = wp_normalize_path( $groundhogg_path . DIRECTORY_SEPARATOR . $short_path );
    379378
    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 ) ) {
    381381                    wp_die( 'The requested file was not found.', 'File not found.', [ 'status' => 404 ] );
    382382                }
  • groundhogg/tags/4.2.2/includes/utils/files.php

    r3264477 r3321992  
    9999     * @param string $subdir
    100100     * @param string $file_path
    101      * @param bool $create_folders
     101     * @param bool   $create_folders
    102102     *
    103103     * @return string
     
    199199    }
    200200
    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;
    230292    }
    231293
     
    238300     * @return array|bool|WP_Error
    239301     */
    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;
    257378            }
    258 
    259             return new WP_Error( 'BAD_UPLOAD', $mfile['error'] );
    260         }
    261 
    262         return $mfile;
     379        }
     380
     381        return false;
    263382    }
    264383
  • groundhogg/trunk/README.txt

    r3320187 r3321992  
    77Tested up to: 6.8
    88Requires PHP: 7.1
    9 Stable tag: 4.2.1
     9Stable tag: 4.2.2
    1010License: GPLv3
    1111License URI: https://www.gnu.org/licenses/gpl.md
     
    354354
    355355== 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.
    356360
    357361= 4.2.1 (2025-06-30) =
  • groundhogg/trunk/admin/contacts/cards/user.php

    r3029787 r3321992  
    1010 * @var $contact \Groundhogg\Contact
    1111 */
     12
     13/* Auto link the account before we see the create account form. */
     14$contact->auto_link_account();
    1215
    1316if ( $contact->get_userdata() ):
  • groundhogg/trunk/admin/contacts/parts/contact-editor.php

    r3209982 r3321992  
    1919 * @var $contact Contact
    2020 */
    21 
    22 //var_dump( $contact );
    23 
    24 /* Auto link the account before we see the create account form. */
    25 $contact->auto_link_account();
    2621
    2722$tabs = [
  • groundhogg/trunk/admin/tools/tools-page.php

    r3320187 r3321992  
    1111use Groundhogg\DB\Query\Table_Query;
    1212use Groundhogg\Extension_Upgrader;
     13use Groundhogg\Files;
    1314use Groundhogg\License_Manager;
    1415use Groundhogg\Plugin;
     
    5455class Tools_Page extends Tabbed_Admin_Page {
    5556
    56     protected $uploads_path = [];
    57 
    5857    /**
    5958     * @var \Groundhogg\Bulk_Jobs\Import_Contacts
     
    562561        }
    563562
    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' );
    575568
    576569        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 
    582570            return $result;
    583571        }
     
    586574            'action' => 'map',
    587575            'tab'    => 'import',
    588             'import' => urlencode( basename( $result['file'] ) ),
     576            'import' => urlencode( basename( $result['path'] ) ),
    589577        ] ) );
    590578
    591     }
    592 
    593     /**
    594      * Upload a file to the Groundhogg file directory
    595      *
    596      * @param $file array
    597      * @param $config
    598      *
    599      * @return array|bool|WP_Error
    600      */
    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 directory
    628      *
    629      * @param $param
    630      *
    631      * @return mixed
    632      */
    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 path
    643      */
    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();
    648579    }
    649580
     
    12321163        $file_path       = wp_normalize_path( $groundhogg_path . DIRECTORY_SEPARATOR . $short_path );
    12331164
    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 ) ) {
    12351167            wp_die( 'The requested file was not found.', 'File not found.', [ 'status' => 404 ] );
    12361168        }
  • groundhogg/trunk/api/v4/files-api.php

    r2507120 r3321992  
    3838
    3939        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//          ],
    4545            [
    4646                'methods'             => WP_REST_Server::READABLE,
     
    7171        foreach ( $_FILES as $FILE ) {
    7272
    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' );
    8576
    8677            if ( is_wp_error( $result ) ) {
  • groundhogg/trunk/assets/js/admin/filters/contacts.js

    r3264477 r3321992  
    5252    Input,
    5353    Div,
    54     makeEl
     54    makeEl,
     55    InputRepeater
    5556  } = MakeEl
    5657
     
    24352436  ContactFilterRegistry.registerFilter(CurrentDateCompareFilterFactory('current_time', 'Current Time', 'time', ( time ) => formatTime(`2000-01-01T${time}`) ))
    24362437
     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
    24372501  if (!Groundhogg.filters) {
    24382502    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`
    22        <div class="space-between" style="gap: 10px">
    33            <div class="gh-input-group">
     
    6363                  </div>
    6464              </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  
    270270
    271271        // 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 ) ) ) {
    273273
    274274            // don't allow bulk updating some columns
     
    284284        }
    285285
     286        $column = ! empty( $where ) && is_string( $where ) ? $where : $this->primary_key;
     287
    286288        if ( isset( $data['email'] ) ) {
    287289
     
    293295            // check to see if this email address is already in use
    294296            $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'] );
    296298            if ( $query->count() > 0 ) {
    297299                unset( $data['email'] );
     
    303305            // check to see if this user_id is being used by another contact
    304306            $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
    306310            // it is being used by another contact :/
    307311            if ( $query->count() > 0 ) {
     
    500504                    break;
    501505                case 'optin_status':
     506                    $cols[ $key ] = Preferences::sanitize( $val );
     507                    break;
    502508                case 'owner_id':
    503509                case 'user_id':
  • groundhogg/trunk/groundhogg.php

    r3320187 r3321992  
    44 * Plugin URI:  https://www.groundhogg.io/?utm_source=wp-plugins&utm_campaign=plugin-uri&utm_medium=wp-dash
    55 * Description: CRM and marketing automation for WordPress
    6  * Version: 4.2.1
     6 * Version: 4.2.2
    77 * Author: Groundhogg Inc.
    88 * Author URI: https://www.groundhogg.io/?utm_source=wp-plugins&utm_campaign=author-uri&utm_medium=wp-dash
     
    2525}
    2626
    27 define( 'GROUNDHOGG_VERSION', '4.2.1' );
     27define( 'GROUNDHOGG_VERSION', '4.2.2' );
    2828define( 'GROUNDHOGG_PREVIOUS_STABLE_VERSION', '4.2' );
    2929
  • groundhogg/trunk/includes/classes/traits/file-box.php

    r3145628 r3321992  
    33namespace Groundhogg\Classes\Traits;
    44
     5use WP_Error;
    56use function Groundhogg\convert_to_local_time;
    67use function Groundhogg\encrypt;
     
    3233
    3334    /**
     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    /**
    3447     * Upload a file
    3548     *
     
    4255    public function upload_file( &$file ) {
    4356
    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' ) ) {
    5374            require_once( ABSPATH . '/wp-admin/includes/file.php' );
    5475        }
    5576
    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 URL
    70      *
    71      * @param string $url        url of the file to copy
    72      * @param bool   $delete_tmp whether to delete the tgemp file after
    73      *
    74      * @return bool
    75      */
    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 copied
    8377        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;
    12397    }
    12498
     
    133107    }
    134108
    135     public function get_uploads_folder_subdir(){
     109    public function get_uploads_folder_subdir() {
    136110        return $this->get_object_type() . 's';
    137111    }
     
    218192
    219193        // Might have to create the directory
    220         if ( ! is_dir( $uploads_dir['path'] ) ){
     194        if ( ! is_dir( $uploads_dir['path'] ) ) {
    221195            wp_mkdir_p( $uploads_dir['path'] );
    222196        }
  • groundhogg/trunk/includes/contact-query.php

    r3320187 r3321992  
    11541154            $activityQuery->where()->compare( "$alias.meta_value", $value, $compare );
    11551155        }
     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 );
    11561201    }
    11571202
  • groundhogg/trunk/includes/functions.php

    r3320187 r3321992  
    26782678                // passed path as the value
    26792679                if ( file_exists( $value ) ) {
    2680                     $files[ $column ] = $value;
     2680                    $copy[] = $value;
    26812681                } // Get from $_FILES
    26822682                else if ( isset_not_empty( $_FILES, $column ) ) {
     
    74857485 */
    74867486function 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();
    74887496}
    74897497
     
    85898597
    85908598/**
    8591  * Add a filter that removes itself after being called once
     8599 * Alias for add_filter_use_once
    85928600 *
    85938601 * @param string   $filter
     
    85998607 */
    86008608function 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 */
     8623function add_filter_use_once( string $filter, callable $callback, int $priority = 10, int $args = 1 ) {
    86018624
    86028625    $callbackWrapper = function ( ...$args ) use ( &$callbackWrapper, $filter, $callback, $priority ) {
  • groundhogg/trunk/includes/rewrites.php

    r3145628 r3321992  
    371371                exit();
    372372                break;
    373 
    374373            case 'files':
    375374
     
    378377                $file_path       = wp_normalize_path( $groundhogg_path . DIRECTORY_SEPARATOR . $short_path );
    379378
    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 ) ) {
    381381                    wp_die( 'The requested file was not found.', 'File not found.', [ 'status' => 404 ] );
    382382                }
  • groundhogg/trunk/includes/utils/files.php

    r3264477 r3321992  
    9999     * @param string $subdir
    100100     * @param string $file_path
    101      * @param bool $create_folders
     101     * @param bool   $create_folders
    102102     *
    103103     * @return string
     
    199199    }
    200200
    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;
    230292    }
    231293
     
    238300     * @return array|bool|WP_Error
    239301     */
    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;
    257378            }
    258 
    259             return new WP_Error( 'BAD_UPLOAD', $mfile['error'] );
    260         }
    261 
    262         return $mfile;
     379        }
     380
     381        return false;
    263382    }
    264383
Note: See TracChangeset for help on using the changeset viewer.