Changeset 3763663
- Timestamp:
- 10/24/2026 06:45:09 PM (2 hours ago)
- Location:
- leanmerce-influencer-collaboration
- Files:
-
- 10 added
- 2 deleted
- 26 edited
- 1 copied
-
tags/1.0.8 (copied) (copied from leanmerce-influencer-collaboration/trunk )
-
tags/1.0.8/leanmerce-influencer-collaboration.php (modified) (2 diffs)
-
tags/1.0.8/readme.txt (modified) (2 diffs)
-
tags/1.0.8/src/Admin/AgreementsPage.php (modified) (2 diffs)
-
tags/1.0.8/src/Agreement/ExternalAgreementService.php (modified) (1 diff)
-
tags/1.0.8/src/Bootstrap.php (modified) (2 diffs)
-
tags/1.0.8/src/Content/ContentFileService.php (modified) (2 diffs)
-
tags/1.0.8/src/Core (added)
-
tags/1.0.8/src/Core/Rest (added)
-
tags/1.0.8/src/Core/Rest/ReportingController.php (added)
-
tags/1.0.8/src/Database/MigrationManager.php (modified) (1 diff)
-
tags/1.0.8/src/Database/SchemaHealth.php (added)
-
tags/1.0.8/src/Infrastructure/Activator.php (modified) (1 diff)
-
tags/1.0.8/src/Rest/AgreementController.php (modified) (2 diffs)
-
tags/1.0.8/src/Rest/ContentController.php (modified) (2 diffs)
-
tags/1.0.8/src/Rest/ReportingController.php (deleted)
-
tags/1.0.8/src/Selection/SelectionService.php (modified) (2 diffs)
-
tags/1.0.8/src/Storage/EncryptedLocalStorage.php (modified) (2 diffs)
-
tags/1.0.8/src/Support/EscapedDomainException.php (added)
-
tags/1.0.8/uninstall.php (modified) (1 diff)
-
trunk/leanmerce-influencer-collaboration.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/src/Admin/AgreementsPage.php (modified) (2 diffs)
-
trunk/src/Agreement/ExternalAgreementService.php (modified) (1 diff)
-
trunk/src/Bootstrap.php (modified) (2 diffs)
-
trunk/src/Content/ContentFileService.php (modified) (2 diffs)
-
trunk/src/Core (added)
-
trunk/src/Core/Rest (added)
-
trunk/src/Core/Rest/ReportingController.php (added)
-
trunk/src/Database/MigrationManager.php (modified) (1 diff)
-
trunk/src/Database/SchemaHealth.php (added)
-
trunk/src/Infrastructure/Activator.php (modified) (1 diff)
-
trunk/src/Rest/AgreementController.php (modified) (2 diffs)
-
trunk/src/Rest/ContentController.php (modified) (2 diffs)
-
trunk/src/Rest/ReportingController.php (deleted)
-
trunk/src/Selection/SelectionService.php (modified) (2 diffs)
-
trunk/src/Storage/EncryptedLocalStorage.php (modified) (2 diffs)
-
trunk/src/Support/EscapedDomainException.php (added)
-
trunk/uninstall.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
leanmerce-influencer-collaboration/tags/1.0.8/leanmerce-influencer-collaboration.php
r3722211 r3763663 3 3 * Plugin Name: Leanmerce Influencer Collaboration 4 4 * Description: Manage influencer collaborations directly in WooCommerce, from onboarding and opportunities through fulfillment, content review, and history. 5 * Version: 1.0. 75 * Version: 1.0.8 6 6 * Requires at least: 6.9 7 7 * Requires PHP: 8.1 … … 22 22 } 23 23 24 define( 'LEANMERCE_INFLUENCER_VERSION', '1.0. 7' );24 define( 'LEANMERCE_INFLUENCER_VERSION', '1.0.8' ); 25 25 define( 'LEANMERCE_INFLUENCER_DB_VERSION', '10.0.0' ); 26 26 define( 'LEANMERCE_INFLUENCER_FILE', __FILE__ ); -
leanmerce-influencer-collaboration/tags/1.0.8/readme.txt
r3722211 r3763663 5 5 Tested up to: 7.1 6 6 Requires PHP: 8.1 7 Stable tag: 1.0. 77 Stable tag: 1.0.8 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 43 43 Free/Core stores influencer profiles, manually entered social references, analytics evidence and historical metric records, private evidence and content files, applications, immutable commercial terms, factual external-agreement confirmations, fulfillment data, content review history, CRM tags, agreed-rights facts, and audit events on the WordPress site. It does not store social-provider credentials or access tokens. Private evidence and content use local encrypted storage. Linked WooCommerce orders follow WooCommerce and site retention settings. 44 44 45 == Upgrade Notice == 46 47 = 1.0.8 = 48 Back up the database, private uploads and WordPress salts. Records and completed agreements stay intact. Failed schema checks pause affected features; retry after five minutes once permissions recover. Large encrypted uploads may need smaller files when memory is limited. 49 45 50 == Changelog == 51 52 = 1.0.8 = 53 * Keep Free/Core reporting handlers and suspended-profile restrictions independent of Pro version and plugin load order. 54 * Keep localized validation messages readable in REST JSON and escape them once for HTML display. 55 * Verify required database tables, columns, and indexes before accepting setup as complete; retry failed setup with a bounded delay and pause affected plugin features safely. 56 * Count active and consumed allocations consistently when selecting creators, accepting offers, and confirming new external agreements. 57 * Reject encrypted content uploads that exceed the current server memory budget before loading file buffers, with a clear client error; preserve the existing encryption format and keys. 58 * Preserve existing records, completed agreements, upload encryption, and all current minimum requirements. 46 59 47 60 = 1.0.7 = -
leanmerce-influencer-collaboration/tags/1.0.8/src/Admin/AgreementsPage.php
r3722211 r3763663 5 5 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-owned custom tables require direct transactional queries; prepared-SQL security checks remain enabled. 6 6 use DomainException; 7 use Leanmerce\Influencer\Support\EscapedDomainException; 7 8 use Leanmerce\Influencer\Agreement\ExternalAgreementService; 8 9 use Leanmerce\Influencer\Database\Schema; … … 22 23 /** @param array<string,mixed> $row */ 23 24 private function state_action(array$row):void{$status=(string)$row['status'];$id=(int)$row['id'];if('contracted'===$status){echo'<a class="button button-primary" href="'.esc_url(admin_url('admin.php?page=leanmerce-fulfillment&collaboration_id='.$id)).'">'.esc_html__('Continue to fulfillment','leanmerce-influencer-collaboration').'</a>';}elseif('awaiting_influencer_acceptance'===$status){echo'<p class="lmi-callout">'.esc_html__('Waiting for creator acceptance. No shop action is available yet.','leanmerce-influencer-collaboration').'</p>';}elseif('content_pending'===$status){echo'<a class="button button-primary" href="'.esc_url(admin_url('admin.php?page=leanmerce-content')).'">'.esc_html__('Review content','leanmerce-influencer-collaboration').'</a>';}elseif('content_complete'===$status){echo'<a class="button" href="'.esc_url(admin_url('admin.php?page=leanmerce-collaborations&collaboration_id='.$id)).'">'.esc_html__('View completed collaboration','leanmerce-influencer-collaboration').'</a>';}} 24 public function handle():void{check_admin_referer(self::ACTION);if(!isset($_POST['confirmed'])||'1'!==sanitize_text_field((string)wp_unslash($_POST['confirmed']))){wp_die(esc_html__('Explicit confirmation is required.','leanmerce-influencer-collaboration'));}try{(new ExternalAgreementService())->confirm(absint(wp_unslash($_POST['collaboration_id']??0)),get_current_user_id(),sanitize_text_field(wp_unslash($_POST['reference']??'')));}catch(DomainException$e){wp_die(esc_html($e ->getMessage()));}wp_safe_redirect(admin_url('admin.php?page=leanmerce-agreements'));exit;}25 public function handle():void{check_admin_referer(self::ACTION);if(!isset($_POST['confirmed'])||'1'!==sanitize_text_field((string)wp_unslash($_POST['confirmed']))){wp_die(esc_html__('Explicit confirmation is required.','leanmerce-influencer-collaboration'));}try{(new ExternalAgreementService())->confirm(absint(wp_unslash($_POST['collaboration_id']??0)),get_current_user_id(),sanitize_text_field(wp_unslash($_POST['reference']??'')));}catch(DomainException$e){wp_die(esc_html($e instanceof EscapedDomainException?$e->plain_text():$e->getMessage()));}wp_safe_redirect(admin_url('admin.php?page=leanmerce-agreements'));exit;} 25 26 } -
leanmerce-influencer-collaboration/tags/1.0.8/src/Agreement/ExternalAgreementService.php
r3722211 r3763663 27 27 $json=(string)wp_json_encode($this->sort($snapshot),JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);$hash=hash('sha256',$json);$now=current_time('mysql',true); 28 28 $wpdb->query('START TRANSACTION'); 29 $wpdb->get_var($wpdb->prepare('SELECT id FROM %i WHERE id=%d FOR UPDATE', $t['opportunities'], (int)$c['opportunity_id'])); 30 (new \Leanmerce\Influencer\Selection\SelectionService())->assert_agreement_capacity((int)$c['opportunity_id'], (int)$c['selection_offer_id']); 29 31 if(false===$wpdb->insert($t['final_terms'],array('collaboration_id'=>$collaboration_id,'schema_version'=>'1.0.0','snapshot_json'=>$json,'snapshot_hash'=>$hash,'source_selection_offer_id'=>(int)$offer['id'],'source_commercial_snapshot_hash'=>(string)$offer['commercial_snapshot_hash'],'merchant_legal_snapshot_id'=>0,'influencer_legal_snapshot_id'=>0,'contract_pack_key'=>'external_manual','contract_pack_version'=>'1.0.0','template_id'=>0,'created_gmt'=>$now))){$wpdb->query('ROLLBACK');throw new DomainException('Immutable Final Terms could not be created.');} 30 32 $id=(int)$wpdb->insert_id;$changed=$wpdb->update($t['collaborations'],array('status'=>'contracted','updated_gmt'=>$now),array('id'=>$collaboration_id,'status'=>$c['status']));if(1!==$changed){$wpdb->query('ROLLBACK');throw new DomainException('Collaboration changed before agreement confirmation.');}$wpdb->query('COMMIT'); -
leanmerce-influencer-collaboration/tags/1.0.8/src/Bootstrap.php
r3722211 r3763663 37 37 use Leanmerce\Influencer\Rest\FulfillmentController; 38 38 use Leanmerce\Influencer\Rest\ContentController; 39 use Leanmerce\Influencer\ Rest\ReportingController;39 use Leanmerce\Influencer\Core\Rest\ReportingController; 40 40 use Leanmerce\Influencer\Roles\Capabilities; 41 41 … … 67 67 } 68 68 69 ( new MigrationManager() )->maybe_migrate(); 69 if ( ! ( new MigrationManager() )->maybe_migrate() ) { 70 add_action( 'admin_notices', static function (): void { 71 if ( current_user_can( 'manage_options' ) ) { 72 echo '<div class="notice notice-error"><p>' . esc_html__( 'Influencer Collaboration is temporarily paused because its database setup could not be verified. Check database permissions and reload after five minutes. Existing records and other WooCommerce features are preserved.', 'leanmerce-influencer-collaboration' ) . '</p></div>'; 73 } 74 } ); 75 return; 76 } 70 77 Capabilities::maybe_sync(); 71 78 -
leanmerce-influencer-collaboration/tags/1.0.8/src/Content/ContentFileService.php
r3722211 r3763663 8 8 9 9 use DomainException; 10 use Leanmerce\Influencer\Support\EscapedDomainException; 10 11 use Leanmerce\Influencer\Database\Schema; 11 12 use Leanmerce\Influencer\Storage\EncryptedLocalStorage; … … 17 18 public function max_bytes():int{return(int)apply_filters('leanmerce_influencer_content_max_upload_bytes',min(104857600,wp_max_upload_size()));} 18 19 /** @param array{name:string,tmp_name:string,size:int,error:int} $file */ 19 public function upload(int$profile_id,int$user_id,array$file):int{if(UPLOAD_ERR_OK!==(int)$file['error']||(int)$file['size']<1||(int)$file['size']>$this->max_bytes()){throw new DomainException('The content file is empty, too large, or could not be uploaded.');} $allowed=array('jpg|jpeg|jpe'=>'image/jpeg','png'=>'image/png','webp'=>'image/webp','mp4'=>'video/mp4','mov|qt'=>'video/quicktime');$checked=wp_check_filetype_and_ext($file['tmp_name'],$file['name'],$allowed);$mime=(string)($checked['type']??'');$ext=strtolower((string)($checked['ext']??''));$map=array('image/jpeg'=>array('jpg','jpeg','jpe'),'image/png'=>array('png'),'image/webp'=>array('webp'),'video/mp4'=>array('mp4'),'video/quicktime'=>array('mov','qt'));if(!isset($map[$mime])||!in_array($ext,$map[$mime],true)){throw new DomainException('Only inspected JPEG, PNG, WEBP, MP4, or MOV content is accepted.');}$stored=$this->storage->store($file['tmp_name']);global$wpdb;$table=Schema::table_names()['files'];if(false===$wpdb->insert($table,array('influencer_profile_id'=>$profile_id,'storage_key'=>$stored['storage_key'],'original_name'=>sanitize_file_name(wp_basename($file['name'])),'mime_type'=>$mime,'size_bytes'=>$stored['size'],'content_sha256'=>$stored['sha256'],'created_by'=>$user_id,'created_gmt'=>current_time('mysql',true)))){$this->storage->delete($stored['storage_key']);throw new DomainException('Private content metadata could not be saved.');}return(int)$wpdb->insert_id;}20 public function upload(int$profile_id,int$user_id,array$file):int{if(UPLOAD_ERR_OK!==(int)$file['error']||(int)$file['size']<1||(int)$file['size']>$this->max_bytes()){throw new DomainException('The content file is empty, too large, or could not be uploaded.');}if($this->storage instanceof EncryptedLocalStorage&&(int)$file['size']>$this->storage->max_storable_bytes()){throw new EscapedDomainException(esc_textarea(__('The content file exceeds the safe memory budget for this server. Choose a smaller file.','leanmerce-influencer-collaboration')));}$allowed=array('jpg|jpeg|jpe'=>'image/jpeg','png'=>'image/png','webp'=>'image/webp','mp4'=>'video/mp4','mov|qt'=>'video/quicktime');$checked=wp_check_filetype_and_ext($file['tmp_name'],$file['name'],$allowed);$mime=(string)($checked['type']??'');$ext=strtolower((string)($checked['ext']??''));$map=array('image/jpeg'=>array('jpg','jpeg','jpe'),'image/png'=>array('png'),'image/webp'=>array('webp'),'video/mp4'=>array('mp4'),'video/quicktime'=>array('mov','qt'));if(!isset($map[$mime])||!in_array($ext,$map[$mime],true)){throw new DomainException('Only inspected JPEG, PNG, WEBP, MP4, or MOV content is accepted.');}$stored=$this->storage->store($file['tmp_name']);global$wpdb;$table=Schema::table_names()['files'];if(false===$wpdb->insert($table,array('influencer_profile_id'=>$profile_id,'storage_key'=>$stored['storage_key'],'original_name'=>sanitize_file_name(wp_basename($file['name'])),'mime_type'=>$mime,'size_bytes'=>$stored['size'],'content_sha256'=>$stored['sha256'],'created_by'=>$user_id,'created_gmt'=>current_time('mysql',true)))){$this->storage->delete($stored['storage_key']);throw new DomainException('Private content metadata could not be saved.');}return(int)$wpdb->insert_id;} 20 21 /** @return array{metadata:array<string,mixed>,content:string} */ 21 22 public function read_authorized(int$file_id,int$user_id):array{global$wpdb;$t=Schema::table_names();$row=$wpdb->get_row($wpdb->prepare("SELECT f.*,p.user_id owner_user_id FROM %i f INNER JOIN %i p ON p.id=f.influencer_profile_id INNER JOIN %i sf ON sf.private_file_id=f.id WHERE f.id=%d AND f.deleted_gmt IS NULL LIMIT 1", $t['files'], $t['profiles'], $t['submission_files'],$file_id),ARRAY_A);if(!is_array($row)||((int)$row['owner_user_id']!==$user_id&&!user_can($user_id,'view_leanmerce_influencer_content'))){throw new DomainException('Private content not found.');}return array('metadata'=>$row,'content'=>$this->storage->read((string)$row['storage_key']));} -
leanmerce-influencer-collaboration/tags/1.0.8/src/Database/MigrationManager.php
r3722211 r3763663 5 5 namespace Leanmerce\Influencer\Database; 6 6 7 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Current plugin schema, migration locks, and allocation checks require live prepared queries. 8 7 9 final class MigrationManager { 8 10 private const OPTION_KEY = 'leanmerce_influencer_collaboration_db_version'; 11 private const HEALTH_KEY = 'leanmerce_influencer_collaboration_schema_health'; 12 private const RETRY_KEY = 'leanmerce_influencer_collaboration_schema_retry_after'; 13 private const RETRY_SECONDS = 300; 14 private const HEALTH_SECONDS = 86400; 9 15 10 public function maybe_migrate(): void{16 public function maybe_migrate(): bool { 11 17 $current = (string) get_option( self::OPTION_KEY, '' ); 12 if ( version_compare( $current, LEANMERCE_INFLUENCER_DB_VERSION, '<' ) ) { 13 $this->migrate(); 18 if ( version_compare( $current, LEANMERCE_INFLUENCER_DB_VERSION, '>' ) ) { 19 return false; 20 } 21 $health = get_option( self::HEALTH_KEY, array() ); 22 if ( LEANMERCE_INFLUENCER_DB_VERSION === $current && is_array( $health ) && ( $health['version'] ?? '' ) === LEANMERCE_INFLUENCER_DB_VERSION && (int) ( $health['until'] ?? 0 ) > time() ) { 23 return true; 24 } 25 if ( (int) get_option( self::RETRY_KEY, 0 ) > time() ) { 26 return false; 27 } 28 return $this->migrate(); 29 } 30 31 public function migrate(): bool { 32 global $wpdb; 33 if ( version_compare( self::current_version(), LEANMERCE_INFLUENCER_DB_VERSION, '>' ) ) { 34 return false; 35 } 36 $lock = 'lmi-schema-' . substr( hash( 'sha256', DB_NAME . '|' . $wpdb->prefix ), 0, 40 ); 37 if ( '1' !== (string) $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s,0)', $lock ) ) ) { 38 return false; 39 } 40 try { 41 delete_option( self::HEALTH_KEY ); 42 $healthy = SchemaHealth::is_current(); 43 if ( ! $healthy ) { 44 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 45 foreach ( Schema::create_statements() as $statement ) { 46 dbDelta( $statement ); 47 } 48 } 49 if ( ! $healthy && ! SchemaHealth::is_current() ) { 50 return $this->failed(); 51 } 52 update_option( self::OPTION_KEY, LEANMERCE_INFLUENCER_DB_VERSION, false ); 53 if ( self::current_version() !== LEANMERCE_INFLUENCER_DB_VERSION ) { 54 return $this->failed(); 55 } 56 $health = array( 'version' => LEANMERCE_INFLUENCER_DB_VERSION, 'until' => time() + self::HEALTH_SECONDS ); 57 update_option( self::HEALTH_KEY, $health, false ); 58 if ( get_option( self::HEALTH_KEY, array() ) !== $health ) { 59 return $this->failed(); 60 } 61 delete_option( self::RETRY_KEY ); 62 do_action( 'leanmerce_influencer_core_migrated', LEANMERCE_INFLUENCER_DB_VERSION ); 63 return true; 64 } catch ( \Throwable $exception ) { 65 return $this->failed(); 66 } finally { 67 $wpdb->get_var( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock ) ); 14 68 } 15 69 } 16 70 17 public function migrate(): void { 18 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 19 20 foreach ( Schema::create_statements() as $statement ) { 21 dbDelta( $statement ); 22 } 23 24 update_option( self::OPTION_KEY, LEANMERCE_INFLUENCER_DB_VERSION, false ); 25 do_action( 'leanmerce_influencer_core_migrated', LEANMERCE_INFLUENCER_DB_VERSION ); 71 private function failed(): bool { 72 delete_option( self::HEALTH_KEY ); 73 update_option( self::RETRY_KEY, time() + self::RETRY_SECONDS, false ); 74 return false; 26 75 } 27 76 -
leanmerce-influencer-collaboration/tags/1.0.8/src/Infrastructure/Activator.php
r3722211 r3763663 17 17 } 18 18 19 ( new MigrationManager() )->migrate(); 19 if ( ! ( new MigrationManager() )->maybe_migrate() ) { 20 return; 21 } 20 22 Capabilities::install(); 21 23 -
leanmerce-influencer-collaboration/tags/1.0.8/src/Rest/AgreementController.php
r3722211 r3763663 6 6 7 7 use DomainException; 8 use Leanmerce\Influencer\Support\EscapedDomainException; 8 9 use Leanmerce\Influencer\Agreement\ExternalAgreementService; 9 10 use WP_Error; … … 14 15 public function register():void{add_action('rest_api_init',array($this,'routes'));} 15 16 public function routes():void{register_rest_route('leanmerce-influencer-collaboration/v1','/agreements/(?P<id>\d+)/external',array('methods'=>'POST','callback'=>array($this,'confirm'),'permission_callback'=>static fn():bool=>current_user_can('manage_leanmerce_influencer_contracts')));} 16 public function confirm(WP_REST_Request$r):WP_REST_Response|WP_Error{try{return new WP_REST_Response((new ExternalAgreementService())->confirm((int)$r['id'],get_current_user_id(),sanitize_text_field((string)$r->get_param('reference'))),201);}catch(DomainException$e){return new WP_Error('invalid_external_agreement',$e ->getMessage(),array('status'=>400));}}17 public function confirm(WP_REST_Request$r):WP_REST_Response|WP_Error{try{return new WP_REST_Response((new ExternalAgreementService())->confirm((int)$r['id'],get_current_user_id(),sanitize_text_field((string)$r->get_param('reference'))),201);}catch(DomainException$e){return new WP_Error('invalid_external_agreement',$e instanceof EscapedDomainException?$e->plain_text():$e->getMessage(),array('status'=>400));}} 17 18 } -
leanmerce-influencer-collaboration/tags/1.0.8/src/Rest/ContentController.php
r3722211 r3763663 6 6 7 7 use DomainException; 8 use Leanmerce\Influencer\Support\EscapedDomainException; 8 9 use Leanmerce\Influencer\Content\ContentFileService; 9 10 use Leanmerce\Influencer\Content\ContentRepository; … … 44 45 public function start(WP_REST_Request$r):WP_REST_Response|WP_Error{return$this->run(fn()=>(new ContentWorkflowService())->start((int)$r['id'],get_current_user_id(),true),201);} 45 46 public function review(WP_REST_Request$r):WP_REST_Response|WP_Error{$input=InputSanitizer::json($r,array('action','feedback','version'));return$this->run(fn()=>(new ContentWorkflowService())->review((int)$r['id'],get_current_user_id(),sanitize_key((string)($input['action']??'')),(string)($input['feedback']??''),absint($input['version']??0)));} 46 private function run(callable$fn,int$status=200):WP_REST_Response|WP_Error{try{return new WP_REST_Response($fn(),$status);}catch(DomainException$e){$ conflict=(bool)preg_match('/changed|already|newer|state|currently|limit/i',$e->getMessage());return new WP_Error('invalid_content_operation',$e->getMessage(),array('status'=>$conflict?409:400));}}47 private function run(callable$fn,int$status=200):WP_REST_Response|WP_Error{try{return new WP_REST_Response($fn(),$status);}catch(DomainException$e){$message=$e instanceof EscapedDomainException?$e->plain_text():$e->getMessage();$conflict=(bool)preg_match('/changed|already|newer|state|currently|limit/i',$message);return new WP_Error('invalid_content_operation',$message,array('status'=>$conflict?409:400));}} 47 48 } -
leanmerce-influencer-collaboration/tags/1.0.8/src/Selection/SelectionService.php
r3722211 r3763663 8 8 9 9 use DomainException; 10 use Leanmerce\Influencer\Support\EscapedDomainException; 10 11 use Leanmerce\Influencer\Application\ApplicationRepository; 11 12 use Leanmerce\Influencer\Application\ApplicationService; … … 54 55 private function respond( array $offer, string $status, int $user_id ): void { global $wpdb;$t=Schema::table_names();$wpdb->query('START TRANSACTION');$changed=$wpdb->query($wpdb->prepare("UPDATE %i SET status=%s,pending_guard=NULL,responded_gmt=%s WHERE id=%d AND status='pending' AND pending_guard=1", $t['selection_offers'],$status,current_time('mysql',true),(int)$offer['id']));if(1!==$changed){$wpdb->query('ROLLBACK');throw new DomainException('This selection offer changed before your response.');}$wpdb->update($t['soft_allocations'],array('status'=>'released','released_gmt'=>current_time('mysql',true)),array('selection_offer_id'=>(int)$offer['id'],'status'=>'active'));$wpdb->update($t['applications'],array('status'=>'submitted','updated_gmt'=>current_time('mysql',true)),array('id'=>(int)$offer['application_id']));$wpdb->query('COMMIT');(new AuditService())->record('application',(int)$offer['application_id'],'selection_offer.'.$status,array('context'=>(string)$offer['commercial_snapshot_hash']),$user_id);(new AuditService())->record('application',(int)$offer['application_id'],'soft_allocation.released',array('source'=>$status),$user_id); } 55 56 private function owned_offer( int $id, int $user_id ): array { $offer=$this->offer($id);if(!$offer){throw new DomainException('Selection offer not found.');}(new ApplicationService())->owned_application((int)$offer['application_id'],$user_id);return$offer; } 56 private function assert_capacity( array $opportunity, ?int $exclude_offer ): void { if(empty($opportunity['max_collaborations'])){return;}global $wpdb;$t=Schema::table_names();if($exclude_offer){$count=(int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM %i WHERE opportunity_id=%d AND status='active' AND selection_offer_id<>%d",$t['soft_allocations'],(int)$opportunity['id'],$exclude_offer));}else{$count=(int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM %i WHERE opportunity_id=%d AND status='active'",$t['soft_allocations'],(int)$opportunity['id']));}if($count>=(int)$opportunity['max_collaborations']){throw new DomainException('This opportunity has reached its collaboration capacity.');} } 57 private function assert_product_capacity( int $opportunity_product_id, int $quantity, ?int $exclude_offer ): void { global $wpdb;$t=Schema::table_names();$cap=$wpdb->get_var($wpdb->prepare("SELECT capacity FROM %i WHERE id=%d",$t['opportunity_products'],$opportunity_product_id));if(null===$cap){return;}if($exclude_offer){$used=(int)$wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(quantity),0) FROM %i WHERE opportunity_product_id=%d AND status='active' AND selection_offer_id<>%d",$t['soft_allocations'],$opportunity_product_id,$exclude_offer));}else{$used=(int)$wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(quantity),0) FROM %i WHERE opportunity_product_id=%d AND status='active'",$t['soft_allocations'],$opportunity_product_id));}if($used+$quantity>(int)$cap){throw new DomainException('This product allocation has reached capacity.');} } 57 private function assert_capacity( array $opportunity, ?int $exclude_offer ): void { if(empty($opportunity['max_collaborations'])){return;}global $wpdb;$t=Schema::table_names();if($exclude_offer){$count=(int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM %i WHERE opportunity_id=%d AND status IN ('active','consumed') AND selection_offer_id<>%d",$t['soft_allocations'],(int)$opportunity['id'],$exclude_offer));}else{$count=(int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM %i WHERE opportunity_id=%d AND status IN ('active','consumed')",$t['soft_allocations'],(int)$opportunity['id']));}if($count>=(int)$opportunity['max_collaborations']){$wpdb->query('ROLLBACK');throw new DomainException('This opportunity has reached its collaboration capacity.');} } 58 private function assert_product_capacity( int $opportunity_product_id, int $quantity, ?int $exclude_offer ): void { global $wpdb;$t=Schema::table_names();$cap=$wpdb->get_var($wpdb->prepare("SELECT capacity FROM %i WHERE id=%d",$t['opportunity_products'],$opportunity_product_id));if(null===$cap){return;}if($exclude_offer){$used=(int)$wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(quantity),0) FROM %i WHERE opportunity_product_id=%d AND status IN ('active','consumed') AND selection_offer_id<>%d",$t['soft_allocations'],$opportunity_product_id,$exclude_offer));}else{$used=(int)$wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(quantity),0) FROM %i WHERE opportunity_product_id=%d AND status IN ('active','consumed')",$t['soft_allocations'],$opportunity_product_id));}if($used+$quantity>(int)$cap){$wpdb->query('ROLLBACK');throw new DomainException('This product allocation has reached capacity.');} } 59 60 /** Validate a still-pending agreement against the same capacity policy, without rewriting prior terms. */ 61 public function assert_agreement_capacity( int $opportunity_id, int $offer_id ): void { 62 global $wpdb; 63 $t = Schema::table_names(); 64 $opportunity = ( new OpportunityRepository() )->find( $opportunity_id ); 65 $allocation = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE selection_offer_id=%d', $t['soft_allocations'], $offer_id ), ARRAY_A ); 66 if ( ! $opportunity || ! $allocation || 'active' !== $allocation['status'] ) { 67 $wpdb->query( 'ROLLBACK' ); 68 throw new EscapedDomainException( esc_textarea( __( 'The creator-program allocation is not valid.', 'leanmerce-influencer-collaboration' ) ) ); 69 } 70 $this->assert_capacity( $opportunity, $offer_id ); 71 if ( ! empty( $allocation['opportunity_product_id'] ) ) { 72 $this->assert_product_capacity( (int) $allocation['opportunity_product_id'], (int) $allocation['quantity'], $offer_id ); 73 } 74 } 58 75 /** @return array{product_id:int,variation_id:int,opportunity_product_id:int,quantity:int,choice_timing:string} */ 59 76 private function resolve_offer_product( array $app, int $product_id, int $variation_id ): array { $rows=(new ProductService())->for_opportunity((int)$app['opportunity_id']);foreach($rows as $row){if('when_applying'===$row['choice_timing']&&(int)$row['product_id']===(int)$app['selected_product_id']&&$this->valid_row_choice($row,(int)$app['selected_product_id'],(int)$app['selected_variation_id'])){return$this->choice($row,(int)$app['selected_product_id'],(int)$app['selected_variation_id']);}}foreach($rows as $row){if('merchant_decides'===$row['choice_timing']&&$this->valid_row_choice($row,$product_id,$variation_id)){return$this->choice($row,$product_id,$variation_id);}}foreach($rows as $row){if('after_selection'===$row['choice_timing']){return array('product_id'=>0,'variation_id'=>0,'opportunity_product_id'=>0,'quantity'=>(int)$row['allocation_quantity'],'choice_timing'=>'after_selection');}}if(!$rows){return array('product_id'=>0,'variation_id'=>0,'opportunity_product_id'=>0,'quantity'=>1,'choice_timing'=>'none');}throw new DomainException('A valid product decision is required for the selection offer.'); } -
leanmerce-influencer-collaboration/tags/1.0.8/src/Storage/EncryptedLocalStorage.php
r3722211 r3763663 8 8 9 9 final class EncryptedLocalStorage implements PrivateFileStorageInterface { 10 /** Budget for the plaintext, ciphertext and unchanged LME1 payload plus request headroom. */ 11 public function max_storable_bytes(): int { 12 $limit = trim( (string) ini_get( 'memory_limit' ) ); 13 if ( '' === $limit || '-1' === $limit ) { 14 return PHP_INT_MAX; 15 } 16 $bytes = (int) $limit; 17 switch ( strtolower( substr( $limit, -1 ) ) ) { 18 case 'g': $bytes *= 1073741824; break; 19 case 'm': $bytes *= 1048576; break; 20 case 'k': $bytes *= 1024; break; 21 } 22 return max( 0, (int) floor( ( $bytes - memory_get_usage( true ) - 16777216 ) / 3 ) ); 23 } 24 10 25 private function directory(): string { 11 26 $uploads = wp_upload_dir(); … … 35 50 /** @return array{storage_key:string,size:int,sha256:string} */ 36 51 public function store( string $temporary_path ): array { 52 $size = filesize( $temporary_path ); 53 if ( false !== $size && $size > $this->max_storable_bytes() ) { 54 throw new RuntimeException( 'The uploaded file exceeds the safe memory budget for this server.' ); 55 } 37 56 $plain = file_get_contents( $temporary_path ); 38 57 if ( false === $plain ) { -
leanmerce-influencer-collaboration/tags/1.0.8/uninstall.php
r3722211 r3763663 98 98 99 99 delete_option( 'leanmerce_influencer_collaboration_db_version' ); 100 delete_option( 'leanmerce_influencer_collaboration_schema_health' ); 101 delete_option( 'leanmerce_influencer_collaboration_schema_retry_after' ); 100 102 delete_option( 'leanmerce_influencer_collaboration_capabilities_version' ); 101 103 delete_option( 'leanmerce_influencer_collaboration_delete_data_on_uninstall' ); -
leanmerce-influencer-collaboration/trunk/leanmerce-influencer-collaboration.php
r3722211 r3763663 3 3 * Plugin Name: Leanmerce Influencer Collaboration 4 4 * Description: Manage influencer collaborations directly in WooCommerce, from onboarding and opportunities through fulfillment, content review, and history. 5 * Version: 1.0. 75 * Version: 1.0.8 6 6 * Requires at least: 6.9 7 7 * Requires PHP: 8.1 … … 22 22 } 23 23 24 define( 'LEANMERCE_INFLUENCER_VERSION', '1.0. 7' );24 define( 'LEANMERCE_INFLUENCER_VERSION', '1.0.8' ); 25 25 define( 'LEANMERCE_INFLUENCER_DB_VERSION', '10.0.0' ); 26 26 define( 'LEANMERCE_INFLUENCER_FILE', __FILE__ ); -
leanmerce-influencer-collaboration/trunk/readme.txt
r3722211 r3763663 5 5 Tested up to: 7.1 6 6 Requires PHP: 8.1 7 Stable tag: 1.0. 77 Stable tag: 1.0.8 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 43 43 Free/Core stores influencer profiles, manually entered social references, analytics evidence and historical metric records, private evidence and content files, applications, immutable commercial terms, factual external-agreement confirmations, fulfillment data, content review history, CRM tags, agreed-rights facts, and audit events on the WordPress site. It does not store social-provider credentials or access tokens. Private evidence and content use local encrypted storage. Linked WooCommerce orders follow WooCommerce and site retention settings. 44 44 45 == Upgrade Notice == 46 47 = 1.0.8 = 48 Back up the database, private uploads and WordPress salts. Records and completed agreements stay intact. Failed schema checks pause affected features; retry after five minutes once permissions recover. Large encrypted uploads may need smaller files when memory is limited. 49 45 50 == Changelog == 51 52 = 1.0.8 = 53 * Keep Free/Core reporting handlers and suspended-profile restrictions independent of Pro version and plugin load order. 54 * Keep localized validation messages readable in REST JSON and escape them once for HTML display. 55 * Verify required database tables, columns, and indexes before accepting setup as complete; retry failed setup with a bounded delay and pause affected plugin features safely. 56 * Count active and consumed allocations consistently when selecting creators, accepting offers, and confirming new external agreements. 57 * Reject encrypted content uploads that exceed the current server memory budget before loading file buffers, with a clear client error; preserve the existing encryption format and keys. 58 * Preserve existing records, completed agreements, upload encryption, and all current minimum requirements. 46 59 47 60 = 1.0.7 = -
leanmerce-influencer-collaboration/trunk/src/Admin/AgreementsPage.php
r3722211 r3763663 5 5 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-owned custom tables require direct transactional queries; prepared-SQL security checks remain enabled. 6 6 use DomainException; 7 use Leanmerce\Influencer\Support\EscapedDomainException; 7 8 use Leanmerce\Influencer\Agreement\ExternalAgreementService; 8 9 use Leanmerce\Influencer\Database\Schema; … … 22 23 /** @param array<string,mixed> $row */ 23 24 private function state_action(array$row):void{$status=(string)$row['status'];$id=(int)$row['id'];if('contracted'===$status){echo'<a class="button button-primary" href="'.esc_url(admin_url('admin.php?page=leanmerce-fulfillment&collaboration_id='.$id)).'">'.esc_html__('Continue to fulfillment','leanmerce-influencer-collaboration').'</a>';}elseif('awaiting_influencer_acceptance'===$status){echo'<p class="lmi-callout">'.esc_html__('Waiting for creator acceptance. No shop action is available yet.','leanmerce-influencer-collaboration').'</p>';}elseif('content_pending'===$status){echo'<a class="button button-primary" href="'.esc_url(admin_url('admin.php?page=leanmerce-content')).'">'.esc_html__('Review content','leanmerce-influencer-collaboration').'</a>';}elseif('content_complete'===$status){echo'<a class="button" href="'.esc_url(admin_url('admin.php?page=leanmerce-collaborations&collaboration_id='.$id)).'">'.esc_html__('View completed collaboration','leanmerce-influencer-collaboration').'</a>';}} 24 public function handle():void{check_admin_referer(self::ACTION);if(!isset($_POST['confirmed'])||'1'!==sanitize_text_field((string)wp_unslash($_POST['confirmed']))){wp_die(esc_html__('Explicit confirmation is required.','leanmerce-influencer-collaboration'));}try{(new ExternalAgreementService())->confirm(absint(wp_unslash($_POST['collaboration_id']??0)),get_current_user_id(),sanitize_text_field(wp_unslash($_POST['reference']??'')));}catch(DomainException$e){wp_die(esc_html($e ->getMessage()));}wp_safe_redirect(admin_url('admin.php?page=leanmerce-agreements'));exit;}25 public function handle():void{check_admin_referer(self::ACTION);if(!isset($_POST['confirmed'])||'1'!==sanitize_text_field((string)wp_unslash($_POST['confirmed']))){wp_die(esc_html__('Explicit confirmation is required.','leanmerce-influencer-collaboration'));}try{(new ExternalAgreementService())->confirm(absint(wp_unslash($_POST['collaboration_id']??0)),get_current_user_id(),sanitize_text_field(wp_unslash($_POST['reference']??'')));}catch(DomainException$e){wp_die(esc_html($e instanceof EscapedDomainException?$e->plain_text():$e->getMessage()));}wp_safe_redirect(admin_url('admin.php?page=leanmerce-agreements'));exit;} 25 26 } -
leanmerce-influencer-collaboration/trunk/src/Agreement/ExternalAgreementService.php
r3722211 r3763663 27 27 $json=(string)wp_json_encode($this->sort($snapshot),JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);$hash=hash('sha256',$json);$now=current_time('mysql',true); 28 28 $wpdb->query('START TRANSACTION'); 29 $wpdb->get_var($wpdb->prepare('SELECT id FROM %i WHERE id=%d FOR UPDATE', $t['opportunities'], (int)$c['opportunity_id'])); 30 (new \Leanmerce\Influencer\Selection\SelectionService())->assert_agreement_capacity((int)$c['opportunity_id'], (int)$c['selection_offer_id']); 29 31 if(false===$wpdb->insert($t['final_terms'],array('collaboration_id'=>$collaboration_id,'schema_version'=>'1.0.0','snapshot_json'=>$json,'snapshot_hash'=>$hash,'source_selection_offer_id'=>(int)$offer['id'],'source_commercial_snapshot_hash'=>(string)$offer['commercial_snapshot_hash'],'merchant_legal_snapshot_id'=>0,'influencer_legal_snapshot_id'=>0,'contract_pack_key'=>'external_manual','contract_pack_version'=>'1.0.0','template_id'=>0,'created_gmt'=>$now))){$wpdb->query('ROLLBACK');throw new DomainException('Immutable Final Terms could not be created.');} 30 32 $id=(int)$wpdb->insert_id;$changed=$wpdb->update($t['collaborations'],array('status'=>'contracted','updated_gmt'=>$now),array('id'=>$collaboration_id,'status'=>$c['status']));if(1!==$changed){$wpdb->query('ROLLBACK');throw new DomainException('Collaboration changed before agreement confirmation.');}$wpdb->query('COMMIT'); -
leanmerce-influencer-collaboration/trunk/src/Bootstrap.php
r3722211 r3763663 37 37 use Leanmerce\Influencer\Rest\FulfillmentController; 38 38 use Leanmerce\Influencer\Rest\ContentController; 39 use Leanmerce\Influencer\ Rest\ReportingController;39 use Leanmerce\Influencer\Core\Rest\ReportingController; 40 40 use Leanmerce\Influencer\Roles\Capabilities; 41 41 … … 67 67 } 68 68 69 ( new MigrationManager() )->maybe_migrate(); 69 if ( ! ( new MigrationManager() )->maybe_migrate() ) { 70 add_action( 'admin_notices', static function (): void { 71 if ( current_user_can( 'manage_options' ) ) { 72 echo '<div class="notice notice-error"><p>' . esc_html__( 'Influencer Collaboration is temporarily paused because its database setup could not be verified. Check database permissions and reload after five minutes. Existing records and other WooCommerce features are preserved.', 'leanmerce-influencer-collaboration' ) . '</p></div>'; 73 } 74 } ); 75 return; 76 } 70 77 Capabilities::maybe_sync(); 71 78 -
leanmerce-influencer-collaboration/trunk/src/Content/ContentFileService.php
r3722211 r3763663 8 8 9 9 use DomainException; 10 use Leanmerce\Influencer\Support\EscapedDomainException; 10 11 use Leanmerce\Influencer\Database\Schema; 11 12 use Leanmerce\Influencer\Storage\EncryptedLocalStorage; … … 17 18 public function max_bytes():int{return(int)apply_filters('leanmerce_influencer_content_max_upload_bytes',min(104857600,wp_max_upload_size()));} 18 19 /** @param array{name:string,tmp_name:string,size:int,error:int} $file */ 19 public function upload(int$profile_id,int$user_id,array$file):int{if(UPLOAD_ERR_OK!==(int)$file['error']||(int)$file['size']<1||(int)$file['size']>$this->max_bytes()){throw new DomainException('The content file is empty, too large, or could not be uploaded.');} $allowed=array('jpg|jpeg|jpe'=>'image/jpeg','png'=>'image/png','webp'=>'image/webp','mp4'=>'video/mp4','mov|qt'=>'video/quicktime');$checked=wp_check_filetype_and_ext($file['tmp_name'],$file['name'],$allowed);$mime=(string)($checked['type']??'');$ext=strtolower((string)($checked['ext']??''));$map=array('image/jpeg'=>array('jpg','jpeg','jpe'),'image/png'=>array('png'),'image/webp'=>array('webp'),'video/mp4'=>array('mp4'),'video/quicktime'=>array('mov','qt'));if(!isset($map[$mime])||!in_array($ext,$map[$mime],true)){throw new DomainException('Only inspected JPEG, PNG, WEBP, MP4, or MOV content is accepted.');}$stored=$this->storage->store($file['tmp_name']);global$wpdb;$table=Schema::table_names()['files'];if(false===$wpdb->insert($table,array('influencer_profile_id'=>$profile_id,'storage_key'=>$stored['storage_key'],'original_name'=>sanitize_file_name(wp_basename($file['name'])),'mime_type'=>$mime,'size_bytes'=>$stored['size'],'content_sha256'=>$stored['sha256'],'created_by'=>$user_id,'created_gmt'=>current_time('mysql',true)))){$this->storage->delete($stored['storage_key']);throw new DomainException('Private content metadata could not be saved.');}return(int)$wpdb->insert_id;}20 public function upload(int$profile_id,int$user_id,array$file):int{if(UPLOAD_ERR_OK!==(int)$file['error']||(int)$file['size']<1||(int)$file['size']>$this->max_bytes()){throw new DomainException('The content file is empty, too large, or could not be uploaded.');}if($this->storage instanceof EncryptedLocalStorage&&(int)$file['size']>$this->storage->max_storable_bytes()){throw new EscapedDomainException(esc_textarea(__('The content file exceeds the safe memory budget for this server. Choose a smaller file.','leanmerce-influencer-collaboration')));}$allowed=array('jpg|jpeg|jpe'=>'image/jpeg','png'=>'image/png','webp'=>'image/webp','mp4'=>'video/mp4','mov|qt'=>'video/quicktime');$checked=wp_check_filetype_and_ext($file['tmp_name'],$file['name'],$allowed);$mime=(string)($checked['type']??'');$ext=strtolower((string)($checked['ext']??''));$map=array('image/jpeg'=>array('jpg','jpeg','jpe'),'image/png'=>array('png'),'image/webp'=>array('webp'),'video/mp4'=>array('mp4'),'video/quicktime'=>array('mov','qt'));if(!isset($map[$mime])||!in_array($ext,$map[$mime],true)){throw new DomainException('Only inspected JPEG, PNG, WEBP, MP4, or MOV content is accepted.');}$stored=$this->storage->store($file['tmp_name']);global$wpdb;$table=Schema::table_names()['files'];if(false===$wpdb->insert($table,array('influencer_profile_id'=>$profile_id,'storage_key'=>$stored['storage_key'],'original_name'=>sanitize_file_name(wp_basename($file['name'])),'mime_type'=>$mime,'size_bytes'=>$stored['size'],'content_sha256'=>$stored['sha256'],'created_by'=>$user_id,'created_gmt'=>current_time('mysql',true)))){$this->storage->delete($stored['storage_key']);throw new DomainException('Private content metadata could not be saved.');}return(int)$wpdb->insert_id;} 20 21 /** @return array{metadata:array<string,mixed>,content:string} */ 21 22 public function read_authorized(int$file_id,int$user_id):array{global$wpdb;$t=Schema::table_names();$row=$wpdb->get_row($wpdb->prepare("SELECT f.*,p.user_id owner_user_id FROM %i f INNER JOIN %i p ON p.id=f.influencer_profile_id INNER JOIN %i sf ON sf.private_file_id=f.id WHERE f.id=%d AND f.deleted_gmt IS NULL LIMIT 1", $t['files'], $t['profiles'], $t['submission_files'],$file_id),ARRAY_A);if(!is_array($row)||((int)$row['owner_user_id']!==$user_id&&!user_can($user_id,'view_leanmerce_influencer_content'))){throw new DomainException('Private content not found.');}return array('metadata'=>$row,'content'=>$this->storage->read((string)$row['storage_key']));} -
leanmerce-influencer-collaboration/trunk/src/Database/MigrationManager.php
r3722211 r3763663 5 5 namespace Leanmerce\Influencer\Database; 6 6 7 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Current plugin schema, migration locks, and allocation checks require live prepared queries. 8 7 9 final class MigrationManager { 8 10 private const OPTION_KEY = 'leanmerce_influencer_collaboration_db_version'; 11 private const HEALTH_KEY = 'leanmerce_influencer_collaboration_schema_health'; 12 private const RETRY_KEY = 'leanmerce_influencer_collaboration_schema_retry_after'; 13 private const RETRY_SECONDS = 300; 14 private const HEALTH_SECONDS = 86400; 9 15 10 public function maybe_migrate(): void{16 public function maybe_migrate(): bool { 11 17 $current = (string) get_option( self::OPTION_KEY, '' ); 12 if ( version_compare( $current, LEANMERCE_INFLUENCER_DB_VERSION, '<' ) ) { 13 $this->migrate(); 18 if ( version_compare( $current, LEANMERCE_INFLUENCER_DB_VERSION, '>' ) ) { 19 return false; 20 } 21 $health = get_option( self::HEALTH_KEY, array() ); 22 if ( LEANMERCE_INFLUENCER_DB_VERSION === $current && is_array( $health ) && ( $health['version'] ?? '' ) === LEANMERCE_INFLUENCER_DB_VERSION && (int) ( $health['until'] ?? 0 ) > time() ) { 23 return true; 24 } 25 if ( (int) get_option( self::RETRY_KEY, 0 ) > time() ) { 26 return false; 27 } 28 return $this->migrate(); 29 } 30 31 public function migrate(): bool { 32 global $wpdb; 33 if ( version_compare( self::current_version(), LEANMERCE_INFLUENCER_DB_VERSION, '>' ) ) { 34 return false; 35 } 36 $lock = 'lmi-schema-' . substr( hash( 'sha256', DB_NAME . '|' . $wpdb->prefix ), 0, 40 ); 37 if ( '1' !== (string) $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s,0)', $lock ) ) ) { 38 return false; 39 } 40 try { 41 delete_option( self::HEALTH_KEY ); 42 $healthy = SchemaHealth::is_current(); 43 if ( ! $healthy ) { 44 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 45 foreach ( Schema::create_statements() as $statement ) { 46 dbDelta( $statement ); 47 } 48 } 49 if ( ! $healthy && ! SchemaHealth::is_current() ) { 50 return $this->failed(); 51 } 52 update_option( self::OPTION_KEY, LEANMERCE_INFLUENCER_DB_VERSION, false ); 53 if ( self::current_version() !== LEANMERCE_INFLUENCER_DB_VERSION ) { 54 return $this->failed(); 55 } 56 $health = array( 'version' => LEANMERCE_INFLUENCER_DB_VERSION, 'until' => time() + self::HEALTH_SECONDS ); 57 update_option( self::HEALTH_KEY, $health, false ); 58 if ( get_option( self::HEALTH_KEY, array() ) !== $health ) { 59 return $this->failed(); 60 } 61 delete_option( self::RETRY_KEY ); 62 do_action( 'leanmerce_influencer_core_migrated', LEANMERCE_INFLUENCER_DB_VERSION ); 63 return true; 64 } catch ( \Throwable $exception ) { 65 return $this->failed(); 66 } finally { 67 $wpdb->get_var( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock ) ); 14 68 } 15 69 } 16 70 17 public function migrate(): void { 18 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 19 20 foreach ( Schema::create_statements() as $statement ) { 21 dbDelta( $statement ); 22 } 23 24 update_option( self::OPTION_KEY, LEANMERCE_INFLUENCER_DB_VERSION, false ); 25 do_action( 'leanmerce_influencer_core_migrated', LEANMERCE_INFLUENCER_DB_VERSION ); 71 private function failed(): bool { 72 delete_option( self::HEALTH_KEY ); 73 update_option( self::RETRY_KEY, time() + self::RETRY_SECONDS, false ); 74 return false; 26 75 } 27 76 -
leanmerce-influencer-collaboration/trunk/src/Infrastructure/Activator.php
r3722211 r3763663 17 17 } 18 18 19 ( new MigrationManager() )->migrate(); 19 if ( ! ( new MigrationManager() )->maybe_migrate() ) { 20 return; 21 } 20 22 Capabilities::install(); 21 23 -
leanmerce-influencer-collaboration/trunk/src/Rest/AgreementController.php
r3722211 r3763663 6 6 7 7 use DomainException; 8 use Leanmerce\Influencer\Support\EscapedDomainException; 8 9 use Leanmerce\Influencer\Agreement\ExternalAgreementService; 9 10 use WP_Error; … … 14 15 public function register():void{add_action('rest_api_init',array($this,'routes'));} 15 16 public function routes():void{register_rest_route('leanmerce-influencer-collaboration/v1','/agreements/(?P<id>\d+)/external',array('methods'=>'POST','callback'=>array($this,'confirm'),'permission_callback'=>static fn():bool=>current_user_can('manage_leanmerce_influencer_contracts')));} 16 public function confirm(WP_REST_Request$r):WP_REST_Response|WP_Error{try{return new WP_REST_Response((new ExternalAgreementService())->confirm((int)$r['id'],get_current_user_id(),sanitize_text_field((string)$r->get_param('reference'))),201);}catch(DomainException$e){return new WP_Error('invalid_external_agreement',$e ->getMessage(),array('status'=>400));}}17 public function confirm(WP_REST_Request$r):WP_REST_Response|WP_Error{try{return new WP_REST_Response((new ExternalAgreementService())->confirm((int)$r['id'],get_current_user_id(),sanitize_text_field((string)$r->get_param('reference'))),201);}catch(DomainException$e){return new WP_Error('invalid_external_agreement',$e instanceof EscapedDomainException?$e->plain_text():$e->getMessage(),array('status'=>400));}} 17 18 } -
leanmerce-influencer-collaboration/trunk/src/Rest/ContentController.php
r3722211 r3763663 6 6 7 7 use DomainException; 8 use Leanmerce\Influencer\Support\EscapedDomainException; 8 9 use Leanmerce\Influencer\Content\ContentFileService; 9 10 use Leanmerce\Influencer\Content\ContentRepository; … … 44 45 public function start(WP_REST_Request$r):WP_REST_Response|WP_Error{return$this->run(fn()=>(new ContentWorkflowService())->start((int)$r['id'],get_current_user_id(),true),201);} 45 46 public function review(WP_REST_Request$r):WP_REST_Response|WP_Error{$input=InputSanitizer::json($r,array('action','feedback','version'));return$this->run(fn()=>(new ContentWorkflowService())->review((int)$r['id'],get_current_user_id(),sanitize_key((string)($input['action']??'')),(string)($input['feedback']??''),absint($input['version']??0)));} 46 private function run(callable$fn,int$status=200):WP_REST_Response|WP_Error{try{return new WP_REST_Response($fn(),$status);}catch(DomainException$e){$ conflict=(bool)preg_match('/changed|already|newer|state|currently|limit/i',$e->getMessage());return new WP_Error('invalid_content_operation',$e->getMessage(),array('status'=>$conflict?409:400));}}47 private function run(callable$fn,int$status=200):WP_REST_Response|WP_Error{try{return new WP_REST_Response($fn(),$status);}catch(DomainException$e){$message=$e instanceof EscapedDomainException?$e->plain_text():$e->getMessage();$conflict=(bool)preg_match('/changed|already|newer|state|currently|limit/i',$message);return new WP_Error('invalid_content_operation',$message,array('status'=>$conflict?409:400));}} 47 48 } -
leanmerce-influencer-collaboration/trunk/src/Selection/SelectionService.php
r3722211 r3763663 8 8 9 9 use DomainException; 10 use Leanmerce\Influencer\Support\EscapedDomainException; 10 11 use Leanmerce\Influencer\Application\ApplicationRepository; 11 12 use Leanmerce\Influencer\Application\ApplicationService; … … 54 55 private function respond( array $offer, string $status, int $user_id ): void { global $wpdb;$t=Schema::table_names();$wpdb->query('START TRANSACTION');$changed=$wpdb->query($wpdb->prepare("UPDATE %i SET status=%s,pending_guard=NULL,responded_gmt=%s WHERE id=%d AND status='pending' AND pending_guard=1", $t['selection_offers'],$status,current_time('mysql',true),(int)$offer['id']));if(1!==$changed){$wpdb->query('ROLLBACK');throw new DomainException('This selection offer changed before your response.');}$wpdb->update($t['soft_allocations'],array('status'=>'released','released_gmt'=>current_time('mysql',true)),array('selection_offer_id'=>(int)$offer['id'],'status'=>'active'));$wpdb->update($t['applications'],array('status'=>'submitted','updated_gmt'=>current_time('mysql',true)),array('id'=>(int)$offer['application_id']));$wpdb->query('COMMIT');(new AuditService())->record('application',(int)$offer['application_id'],'selection_offer.'.$status,array('context'=>(string)$offer['commercial_snapshot_hash']),$user_id);(new AuditService())->record('application',(int)$offer['application_id'],'soft_allocation.released',array('source'=>$status),$user_id); } 55 56 private function owned_offer( int $id, int $user_id ): array { $offer=$this->offer($id);if(!$offer){throw new DomainException('Selection offer not found.');}(new ApplicationService())->owned_application((int)$offer['application_id'],$user_id);return$offer; } 56 private function assert_capacity( array $opportunity, ?int $exclude_offer ): void { if(empty($opportunity['max_collaborations'])){return;}global $wpdb;$t=Schema::table_names();if($exclude_offer){$count=(int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM %i WHERE opportunity_id=%d AND status='active' AND selection_offer_id<>%d",$t['soft_allocations'],(int)$opportunity['id'],$exclude_offer));}else{$count=(int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM %i WHERE opportunity_id=%d AND status='active'",$t['soft_allocations'],(int)$opportunity['id']));}if($count>=(int)$opportunity['max_collaborations']){throw new DomainException('This opportunity has reached its collaboration capacity.');} } 57 private function assert_product_capacity( int $opportunity_product_id, int $quantity, ?int $exclude_offer ): void { global $wpdb;$t=Schema::table_names();$cap=$wpdb->get_var($wpdb->prepare("SELECT capacity FROM %i WHERE id=%d",$t['opportunity_products'],$opportunity_product_id));if(null===$cap){return;}if($exclude_offer){$used=(int)$wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(quantity),0) FROM %i WHERE opportunity_product_id=%d AND status='active' AND selection_offer_id<>%d",$t['soft_allocations'],$opportunity_product_id,$exclude_offer));}else{$used=(int)$wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(quantity),0) FROM %i WHERE opportunity_product_id=%d AND status='active'",$t['soft_allocations'],$opportunity_product_id));}if($used+$quantity>(int)$cap){throw new DomainException('This product allocation has reached capacity.');} } 57 private function assert_capacity( array $opportunity, ?int $exclude_offer ): void { if(empty($opportunity['max_collaborations'])){return;}global $wpdb;$t=Schema::table_names();if($exclude_offer){$count=(int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM %i WHERE opportunity_id=%d AND status IN ('active','consumed') AND selection_offer_id<>%d",$t['soft_allocations'],(int)$opportunity['id'],$exclude_offer));}else{$count=(int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM %i WHERE opportunity_id=%d AND status IN ('active','consumed')",$t['soft_allocations'],(int)$opportunity['id']));}if($count>=(int)$opportunity['max_collaborations']){$wpdb->query('ROLLBACK');throw new DomainException('This opportunity has reached its collaboration capacity.');} } 58 private function assert_product_capacity( int $opportunity_product_id, int $quantity, ?int $exclude_offer ): void { global $wpdb;$t=Schema::table_names();$cap=$wpdb->get_var($wpdb->prepare("SELECT capacity FROM %i WHERE id=%d",$t['opportunity_products'],$opportunity_product_id));if(null===$cap){return;}if($exclude_offer){$used=(int)$wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(quantity),0) FROM %i WHERE opportunity_product_id=%d AND status IN ('active','consumed') AND selection_offer_id<>%d",$t['soft_allocations'],$opportunity_product_id,$exclude_offer));}else{$used=(int)$wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(quantity),0) FROM %i WHERE opportunity_product_id=%d AND status IN ('active','consumed')",$t['soft_allocations'],$opportunity_product_id));}if($used+$quantity>(int)$cap){$wpdb->query('ROLLBACK');throw new DomainException('This product allocation has reached capacity.');} } 59 60 /** Validate a still-pending agreement against the same capacity policy, without rewriting prior terms. */ 61 public function assert_agreement_capacity( int $opportunity_id, int $offer_id ): void { 62 global $wpdb; 63 $t = Schema::table_names(); 64 $opportunity = ( new OpportunityRepository() )->find( $opportunity_id ); 65 $allocation = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE selection_offer_id=%d', $t['soft_allocations'], $offer_id ), ARRAY_A ); 66 if ( ! $opportunity || ! $allocation || 'active' !== $allocation['status'] ) { 67 $wpdb->query( 'ROLLBACK' ); 68 throw new EscapedDomainException( esc_textarea( __( 'The creator-program allocation is not valid.', 'leanmerce-influencer-collaboration' ) ) ); 69 } 70 $this->assert_capacity( $opportunity, $offer_id ); 71 if ( ! empty( $allocation['opportunity_product_id'] ) ) { 72 $this->assert_product_capacity( (int) $allocation['opportunity_product_id'], (int) $allocation['quantity'], $offer_id ); 73 } 74 } 58 75 /** @return array{product_id:int,variation_id:int,opportunity_product_id:int,quantity:int,choice_timing:string} */ 59 76 private function resolve_offer_product( array $app, int $product_id, int $variation_id ): array { $rows=(new ProductService())->for_opportunity((int)$app['opportunity_id']);foreach($rows as $row){if('when_applying'===$row['choice_timing']&&(int)$row['product_id']===(int)$app['selected_product_id']&&$this->valid_row_choice($row,(int)$app['selected_product_id'],(int)$app['selected_variation_id'])){return$this->choice($row,(int)$app['selected_product_id'],(int)$app['selected_variation_id']);}}foreach($rows as $row){if('merchant_decides'===$row['choice_timing']&&$this->valid_row_choice($row,$product_id,$variation_id)){return$this->choice($row,$product_id,$variation_id);}}foreach($rows as $row){if('after_selection'===$row['choice_timing']){return array('product_id'=>0,'variation_id'=>0,'opportunity_product_id'=>0,'quantity'=>(int)$row['allocation_quantity'],'choice_timing'=>'after_selection');}}if(!$rows){return array('product_id'=>0,'variation_id'=>0,'opportunity_product_id'=>0,'quantity'=>1,'choice_timing'=>'none');}throw new DomainException('A valid product decision is required for the selection offer.'); } -
leanmerce-influencer-collaboration/trunk/src/Storage/EncryptedLocalStorage.php
r3722211 r3763663 8 8 9 9 final class EncryptedLocalStorage implements PrivateFileStorageInterface { 10 /** Budget for the plaintext, ciphertext and unchanged LME1 payload plus request headroom. */ 11 public function max_storable_bytes(): int { 12 $limit = trim( (string) ini_get( 'memory_limit' ) ); 13 if ( '' === $limit || '-1' === $limit ) { 14 return PHP_INT_MAX; 15 } 16 $bytes = (int) $limit; 17 switch ( strtolower( substr( $limit, -1 ) ) ) { 18 case 'g': $bytes *= 1073741824; break; 19 case 'm': $bytes *= 1048576; break; 20 case 'k': $bytes *= 1024; break; 21 } 22 return max( 0, (int) floor( ( $bytes - memory_get_usage( true ) - 16777216 ) / 3 ) ); 23 } 24 10 25 private function directory(): string { 11 26 $uploads = wp_upload_dir(); … … 35 50 /** @return array{storage_key:string,size:int,sha256:string} */ 36 51 public function store( string $temporary_path ): array { 52 $size = filesize( $temporary_path ); 53 if ( false !== $size && $size > $this->max_storable_bytes() ) { 54 throw new RuntimeException( 'The uploaded file exceeds the safe memory budget for this server.' ); 55 } 37 56 $plain = file_get_contents( $temporary_path ); 38 57 if ( false === $plain ) { -
leanmerce-influencer-collaboration/trunk/uninstall.php
r3722211 r3763663 98 98 99 99 delete_option( 'leanmerce_influencer_collaboration_db_version' ); 100 delete_option( 'leanmerce_influencer_collaboration_schema_health' ); 101 delete_option( 'leanmerce_influencer_collaboration_schema_retry_after' ); 100 102 delete_option( 'leanmerce_influencer_collaboration_capabilities_version' ); 101 103 delete_option( 'leanmerce_influencer_collaboration_delete_data_on_uninstall' );
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)